Registrar: only for zones that are a registered domain
With porkbun.domains empty the Registrar tab was offered for every zone and always failed for subdomain zones, reverse zones and private names. dnsname.RegisteredDomain (public suffix list) now decides; the others get a message naming what delegates them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
65086e314a
commit
82c282cd32
7 files changed
+106
-2
No files matched your search
@@ -33,6 +33,14 @@ All notable changes to Jarl are documented here. The format follows
|
||||
|
||||
### Fixed
|
||||
|
||||
- The Registrar tab (and `jarl zone registrar`, and the DNSSEC page's
|
||||
registrar button) was offered for every zone when `porkbun.domains` is
|
||||
empty, and always failed for subdomain zones (`lab.adbraun.lol`),
|
||||
reverse zones and private names (`home.lan`): Porkbun only knows
|
||||
registered domains. Only zones that are a registered domain (by the
|
||||
public suffix list) get it now; the others say what delegates them
|
||||
instead.
|
||||
|
||||
- Auto-PTR in linked zones changed PTR records in every linked view, not
|
||||
just where the address record is. A record local to one view got PTR
|
||||
records in the other views too; deleting or changing the address in
|
||||
|
||||
@@ -163,7 +163,7 @@ check` says so).
|
||||
|-----|---------|---------|
|
||||
| `porkbun.api_key` | `""` | The API key (`pk1_...`). Set both or neither. |
|
||||
| `porkbun.secret_api_key` | `""` | Its secret (`sk1_...`). |
|
||||
| `porkbun.domains` | `[]` | Only these domains, for both uses (default: any domain the key can reach). |
|
||||
| `porkbun.domains` | `[]` | Only these domains, for both uses (default: any domain the key can reach). Subdomain and reverse zones never get the registrar features ([dnssec.md](dnssec.md#porkbun)). |
|
||||
|
||||
The key can move your domain anywhere, so it is only ever read from this
|
||||
file: it isn't stored in the database or backups, and there is no API to
|
||||
|
||||
@@ -204,6 +204,18 @@ root:jarl`, `chmod 640`, as the package installs it); jarld warns at start
|
||||
when everyone can read it. Only administrators can use the registrar
|
||||
features.
|
||||
|
||||
Only a zone that is a registered domain gets the registrar features
|
||||
(`adbraun.lol`, `example.co.uk`, judged by the public suffix list). Their
|
||||
parent zone delegates the others, not a registrar:
|
||||
|
||||
* a subdomain zone such as `lab.adbraun.lol`: the NS and DS records in
|
||||
`adbraun.lol`;
|
||||
* a reverse zone: whoever holds the addresses (your ISP, or the RIR);
|
||||
* a zone under a suffix that isn't public (`home.lan`, `home.arpa`):
|
||||
nobody.
|
||||
|
||||
The zone's **Name servers** tab (Check delegation) checks all of them.
|
||||
|
||||
**Using it**: the zone's **Registrar** tab, or `jarl zone registrar`:
|
||||
|
||||
| | |
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"golang.org/x/net/idna"
|
||||
"golang.org/x/net/publicsuffix"
|
||||
)
|
||||
|
||||
// Root is the root domain name.
|
||||
@@ -305,6 +306,28 @@ func EmailFromMailbox(rname string) string {
|
||||
return local + "@" + strings.Join(labels[1:], ".")
|
||||
}
|
||||
|
||||
// RegisteredDomain returns the domain registered at a registrar that name
|
||||
// is in, without a trailing dot: adbraun.lol for lab.adbraun.lol, from
|
||||
// the public suffix list. ok is false when no registrar is involved:
|
||||
// reverse zones and the rest of .arpa (delegated by whoever holds the
|
||||
// addresses), names under a suffix the list doesn't know (home.lan,
|
||||
// internal) and public suffixes themselves (co.uk).
|
||||
func RegisteredDomain(name string) (string, bool) {
|
||||
n := strings.ToLower(strings.TrimSuffix(name, "."))
|
||||
if n == "" || n == "arpa" || strings.HasSuffix(n, ".arpa") {
|
||||
return "", false
|
||||
}
|
||||
suffix, icann := publicsuffix.PublicSuffix(n)
|
||||
if !icann || suffix == n {
|
||||
return "", false
|
||||
}
|
||||
d, err := publicsuffix.EffectiveTLDPlusOne(n)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
return d, true
|
||||
}
|
||||
|
||||
// IsReverse reports whether name is in in-addr.arpa or ip6.arpa.
|
||||
func IsReverse(name string) bool {
|
||||
return IsSubdomain(name, "in-addr.arpa.") || IsSubdomain(name, "ip6.arpa.") || IsSubdomain(name, "ip6.int.")
|
||||
|
||||
@@ -249,3 +249,23 @@ func TestPrefixFromReverse(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisteredDomain(t *testing.T) {
|
||||
for name, want := range map[string]string{
|
||||
"adbraun.lol.": "adbraun.lol",
|
||||
"lab.adbraun.lol.": "adbraun.lol",
|
||||
"Example.CO.UK": "example.co.uk",
|
||||
"co.uk.": "",
|
||||
"lol.": "",
|
||||
"10.0.10.in-addr.arpa.": "",
|
||||
"d.f.ip6.arpa.": "",
|
||||
"home.arpa.": "",
|
||||
"nas.home.lan.": "",
|
||||
"": "",
|
||||
} {
|
||||
got, ok := RegisteredDomain(name)
|
||||
if got != want || ok != (want != "") {
|
||||
t.Errorf("RegisteredDomain(%q) = %q, %v; want %q", name, got, ok, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,11 +35,16 @@ import (
|
||||
)
|
||||
|
||||
// registrarFor names the registrar Jarl can manage z's delegation at:
|
||||
// "porkbun" when a key is configured and allows the zone's name.
|
||||
// "porkbun" when a key is configured and allows the zone's name, and the
|
||||
// zone is a registered domain (not a subdomain or a reverse zone, which
|
||||
// their parent zone delegates).
|
||||
func (s *Server) registrarFor(z *model.Zone) string {
|
||||
if s.cfg.Porkbun == nil || z.Type != model.ZonePrimary {
|
||||
return ""
|
||||
}
|
||||
if d, ok := dnsname.RegisteredDomain(z.Name); !ok || d != dnsname.Display(z.Name) {
|
||||
return ""
|
||||
}
|
||||
if s.cfg.PorkbunAllows != nil && !s.cfg.PorkbunAllows(dnsname.Display(z.Name)) {
|
||||
return ""
|
||||
}
|
||||
@@ -59,6 +64,16 @@ func (s *Server) registrarZone(r *http.Request, p *auth.Principal) (*model.State
|
||||
return nil, nil, "", err
|
||||
}
|
||||
if s.registrarFor(z) == "" {
|
||||
d, ok := dnsname.RegisteredDomain(z.Name)
|
||||
switch {
|
||||
case s.cfg.Porkbun == nil:
|
||||
case dnsname.IsReverse(z.Name):
|
||||
return nil, nil, "", badRequest("%s is a reverse zone: whoever holds the addresses (your ISP, or the RIR) delegates it, not a registrar. Check its delegation on the zone's Name servers tab (jarl zone delegation ZONE)", dnsname.Display(z.Name))
|
||||
case !ok:
|
||||
return nil, nil, "", badRequest("%s isn't a domain registered at a registrar (its suffix isn't a public one), so there is nothing to manage at Porkbun", dnsname.Display(z.Name))
|
||||
case d != dnsname.Display(z.Name):
|
||||
return nil, nil, "", badRequest("%s is a subdomain of %s: the NS (and DS) records in the zone %s delegate it, not the registrar. Manage %s at Porkbun instead", dnsname.Display(z.Name), d, d, d)
|
||||
}
|
||||
if s.cfg.Porkbun == nil {
|
||||
return nil, nil, "", badRequest("no registrar is configured: add a Porkbun API key to jarl.yaml (porkbun:, see docs/dnssec.md#porkbun)")
|
||||
}
|
||||
|
||||
@@ -170,3 +170,29 @@ func writeKey(t *testing.T, dir, zone string) uint16 {
|
||||
os.WriteFile(base+".state", []byte("Algorithm: 13\nKSK: yes\nZSK: yes\nGoalState: omnipresent\nDNSKEYState: omnipresent\nKRRSIGState: omnipresent\nZRRSIGState: omnipresent\nDSState: rumoured\n"), 0o644)
|
||||
return k.KeyTag()
|
||||
}
|
||||
|
||||
// With no porkbun.domains, only zones that are registered domains get the
|
||||
// registrar: not subdomains or reverse zones, which their parent delegates.
|
||||
func TestRegistrarOnlyRegisteredDomains(t *testing.T) {
|
||||
fake := porkbun.NewFake("pk1_a", "sk1_b", "adbraun.lol")
|
||||
pts := httptest.NewServer(fake)
|
||||
t.Cleanup(pts.Close)
|
||||
e := newAPIEnvConfig(t, Config{Version: "test", Porkbun: &porkbun.Client{BaseURL: pts.URL + "/api/json/v3", APIKey: "pk1_a", Secret: "sk1_b"}})
|
||||
for _, z := range []string{"adbraun.lol", "lab.adbraun.lol", "10.0.10.0/24", "home.lan"} {
|
||||
e.do("POST", "/zones", e.token, map[string]any{"name": z, "nameservers": []string{"ns1.example.net."}}, 201)
|
||||
}
|
||||
for zone, want := range map[string]string{"adbraun.lol": "porkbun", "lab.adbraun.lol": "", "10.0.10.in-addr.arpa": "", "home.lan": ""} {
|
||||
got, _ := e.do("GET", "/zones/"+zone, e.token, nil, 200)["registrar"].(string)
|
||||
if got != want {
|
||||
t.Errorf("%s: registrar %q, want %q", zone, got, want)
|
||||
}
|
||||
}
|
||||
for zone, msg := range map[string]string{"lab.adbraun.lol": "subdomain of adbraun.lol", "10.0.10.in-addr.arpa": "reverse zone", "home.lan": "isn't a domain registered"} {
|
||||
out := e.do("GET", "/zones/"+zone+"/registrar", e.token, nil, 400)
|
||||
errObj, _ := out["error"].(map[string]any)
|
||||
if m, _ := errObj["message"].(string); !strings.Contains(m, msg) {
|
||||
t.Errorf("%s: %v", zone, out)
|
||||
}
|
||||
}
|
||||
e.do("GET", "/zones/adbraun.lol/registrar", e.token, nil, 200)
|
||||
}
|
||||
Reference in new issue
Block a user