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
|
### 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
|
- 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
|
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
|
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.api_key` | `""` | The API key (`pk1_...`). Set both or neither. |
|
||||||
| `porkbun.secret_api_key` | `""` | Its secret (`sk1_...`). |
|
| `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
|
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
|
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
|
when everyone can read it. Only administrators can use the registrar
|
||||||
features.
|
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`:
|
**Using it**: the zone's **Registrar** tab, or `jarl zone registrar`:
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
|
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
"golang.org/x/net/idna"
|
"golang.org/x/net/idna"
|
||||||
|
"golang.org/x/net/publicsuffix"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Root is the root domain name.
|
// Root is the root domain name.
|
||||||
@@ -305,6 +306,28 @@ func EmailFromMailbox(rname string) string {
|
|||||||
return local + "@" + strings.Join(labels[1:], ".")
|
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.
|
// IsReverse reports whether name is in in-addr.arpa or ip6.arpa.
|
||||||
func IsReverse(name string) bool {
|
func IsReverse(name string) bool {
|
||||||
return IsSubdomain(name, "in-addr.arpa.") || IsSubdomain(name, "ip6.arpa.") || IsSubdomain(name, "ip6.int.")
|
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:
|
// 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 {
|
func (s *Server) registrarFor(z *model.Zone) string {
|
||||||
if s.cfg.Porkbun == nil || z.Type != model.ZonePrimary {
|
if s.cfg.Porkbun == nil || z.Type != model.ZonePrimary {
|
||||||
return ""
|
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)) {
|
if s.cfg.PorkbunAllows != nil && !s.cfg.PorkbunAllows(dnsname.Display(z.Name)) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -59,6 +64,16 @@ func (s *Server) registrarZone(r *http.Request, p *auth.Principal) (*model.State
|
|||||||
return nil, nil, "", err
|
return nil, nil, "", err
|
||||||
}
|
}
|
||||||
if s.registrarFor(z) == "" {
|
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 {
|
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)")
|
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)
|
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()
|
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