Fix $TTL in the zone parser; add make check-go
The zone parser ignored an explicit $TTL and stamped every record with the SOA minimum instead, so a 60s zone served 3600s and DefaultTTL was wrong too. RFC 1035 §5.2: an explicit $TTL wins. qualifyRdata was also added to apply RFC 1035 §5.1 relative-name expansion to RDATA, which is correct but has one outstanding defect noted below. make check-go is the tight inner loop: gofmt check, go vet, compile. It uses go-build rather than build, because build depends on web and shells out to npm to rebuild a console bundle that rarely changes. check now depends on it, so the fast path and the CI path cannot drift apart. Outstanding, not fixed here: a relative CNAME target is still emitted as "host." instead of "host.example.com.". qualifyRdata is called with the right origin, and relative() is correct in isolation, so the target is almost certainly being dot-terminated in Zone.assemble before dns.NewRR ever sees it, which makes relative() treat it as already absolute. Narrowed to that, not confirmed — see the localzone test failures.
This commit is contained in:
1 parent
f02bb0aecf
commit
4c00bb4144
4 files changed
+1006
-36
No files matched your search
@@ -48,6 +48,9 @@ type Zone struct {
|
||||
SOA *dns.SOA
|
||||
// DefaultTTL is the $TTL directive value, else the SOA minimum.
|
||||
DefaultTTL uint32
|
||||
// ttlDirective records that the file carried a $TTL, which outranks the
|
||||
// SOA when the two disagree (RFC 1035 §5.2).
|
||||
ttlDirective bool
|
||||
}
|
||||
|
||||
// Options tunes parsing.
|
||||
@@ -101,7 +104,7 @@ func Parse(r io.Reader, opt Options) (*Zone, error) {
|
||||
lastOwner = z.Origin
|
||||
continue
|
||||
}
|
||||
assembled, err := z.assemble(logical, lastOwner)
|
||||
assembled, hasTTL, err := z.assemble(logical, lastOwner)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s:%d: %w", where(opt.File), startLine, err)
|
||||
}
|
||||
@@ -109,6 +112,17 @@ func Parse(r io.Reader, opt Options) (*Zone, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s:%d: %q: %w", where(opt.File), startLine, assembled, err)
|
||||
}
|
||||
// RFC 1035 §5.2: a record with no TTL column takes the $TTL in force.
|
||||
// The RR library stamps its own default on such a record, so the
|
||||
// zone default has to be applied here or $TTL is silently ignored.
|
||||
if !hasTTL && z.DefaultTTL > 0 {
|
||||
rr.Header().Ttl = z.DefaultTTL
|
||||
}
|
||||
// RFC 1035 §5.1: a domain name in the RDATA section that is written
|
||||
// without a trailing dot is relative to the zone origin, not to the
|
||||
// root. Left to the library it becomes "host.", which points a CNAME
|
||||
// at the root instead of at the zone.
|
||||
qualifyRdata(rr, z.Origin)
|
||||
rec := Record{
|
||||
Name: strings.ToLower(ensureDot(rr.Header().Name)),
|
||||
TTL: rr.Header().Ttl,
|
||||
@@ -124,10 +138,14 @@ func Parse(r io.Reader, opt Options) (*Zone, error) {
|
||||
}
|
||||
if soa, ok := rr.(*dns.SOA); ok {
|
||||
z.SOA = cloneSOA(soa)
|
||||
if rec.TTL > 0 {
|
||||
z.DefaultTTL = rec.TTL
|
||||
} else if z.DefaultTTL == 0 {
|
||||
z.DefaultTTL = soa.Minttl
|
||||
// An explicit $TTL wins; the SOA only supplies the default for a
|
||||
// file that never set one.
|
||||
if !z.ttlDirective {
|
||||
if rec.TTL > 0 {
|
||||
z.DefaultTTL = rec.TTL
|
||||
} else if z.DefaultTTL == 0 {
|
||||
z.DefaultTTL = soa.Minttl
|
||||
}
|
||||
}
|
||||
}
|
||||
z.Records = append(z.Records, rec)
|
||||
@@ -169,12 +187,15 @@ func deparen(s string) string {
|
||||
return strings.TrimSpace(s)
|
||||
}
|
||||
|
||||
// assemble fills in the omitted owner name, TTL and class so that the result
|
||||
// is a complete RR line that dns.NewRR will accept.
|
||||
func (z *Zone) assemble(line, lastOwner string) (string, error) {
|
||||
// assemble fills in the omitted owner name and class so that the result is a
|
||||
// complete RR line that dns.NewRR will accept. hasTTL reports whether the line
|
||||
// carried a TTL of its own, so the caller can tell an omitted TTL from an
|
||||
// explicit one and apply the $TTL default only to the former.
|
||||
func (z *Zone) assemble(line, lastOwner string) (string, bool, error) {
|
||||
var hasTTL bool
|
||||
fields := splitFields(line)
|
||||
if len(fields) == 0 {
|
||||
return "", fmt.Errorf("empty record")
|
||||
return "", false, fmt.Errorf("empty record")
|
||||
}
|
||||
var head string
|
||||
var rest []string
|
||||
@@ -187,7 +208,7 @@ func (z *Zone) assemble(line, lastOwner string) (string, error) {
|
||||
rest = fields[1:]
|
||||
}
|
||||
if head == "" {
|
||||
return "", fmt.Errorf("record has no owner name and no previous owner to inherit")
|
||||
return "", false, fmt.Errorf("record has no owner name and no previous owner to inherit")
|
||||
}
|
||||
head = relative(head, z.Origin)
|
||||
|
||||
@@ -197,18 +218,19 @@ func (z *Zone) assemble(line, lastOwner string) (string, error) {
|
||||
switch {
|
||||
case isTTLToken(rest[0]):
|
||||
out = append(out, rest[0])
|
||||
hasTTL = true
|
||||
case isClassToken(rest[0]):
|
||||
out = append(out, strings.ToUpper(rest[0]))
|
||||
default:
|
||||
return "", fmt.Errorf("unexpected token %q before the record type", rest[0])
|
||||
return "", false, fmt.Errorf("unexpected token %q before the record type", rest[0])
|
||||
}
|
||||
rest = rest[1:]
|
||||
}
|
||||
if len(rest) == 0 {
|
||||
return "", fmt.Errorf("record %q has no type", head)
|
||||
return "", false, fmt.Errorf("record %q has no type", head)
|
||||
}
|
||||
out = append(out, rest...)
|
||||
return strings.Join(out, " "), nil
|
||||
return strings.Join(out, " "), hasTTL, nil
|
||||
}
|
||||
|
||||
// directive handles $TTL, $ORIGIN and $INCLUDE.
|
||||
@@ -224,6 +246,7 @@ func (z *Zone) directive(line string, opt Options) error {
|
||||
return err
|
||||
}
|
||||
z.DefaultTTL = ttl
|
||||
z.ttlDirective = true
|
||||
case "$ORIGIN":
|
||||
if len(fields) < 2 {
|
||||
return fmt.Errorf("$ORIGIN needs a name")
|
||||
@@ -267,6 +290,48 @@ func cloneSOA(in *dns.SOA) *dns.SOA {
|
||||
return &out
|
||||
}
|
||||
|
||||
// qualifyRdata appends the origin to the domain names in a record's RDATA
|
||||
// that were written without a trailing dot. RFC 1035 §5.1 makes those names
|
||||
// relative to the zone origin, so "www CNAME host" in example.com. means
|
||||
// host.example.com. and not host. A name that already ends in a dot is
|
||||
// absolute and left alone, which keeps the RPZ targets "." and "rpz-drop."
|
||||
// and the null MX/SRV targets intact.
|
||||
func qualifyRdata(rr dns.RR, origin string) {
|
||||
switch v := rr.(type) {
|
||||
case *dns.CNAME:
|
||||
v.Target = relative(v.Target, origin)
|
||||
case *dns.DNAME:
|
||||
v.Target = relative(v.Target, origin)
|
||||
case *dns.NS:
|
||||
v.Ns = relative(v.Ns, origin)
|
||||
case *dns.PTR:
|
||||
v.Ptr = relative(v.Ptr, origin)
|
||||
case *dns.MX:
|
||||
v.Mx = relative(v.Mx, origin)
|
||||
case *dns.SRV:
|
||||
v.Target = relative(v.Target, origin)
|
||||
case *dns.SOA:
|
||||
v.Ns = relative(v.Ns, origin)
|
||||
v.Mbox = relative(v.Mbox, origin)
|
||||
case *dns.NAPTR:
|
||||
v.Replacement = relative(v.Replacement, origin)
|
||||
case *dns.RP:
|
||||
v.Mbox = relative(v.Mbox, origin)
|
||||
v.Txt = relative(v.Txt, origin)
|
||||
case *dns.MINFO:
|
||||
v.Rmail = relative(v.Rmail, origin)
|
||||
v.Email = relative(v.Email, origin)
|
||||
case *dns.MB:
|
||||
v.Mb = relative(v.Mb, origin)
|
||||
case *dns.MG:
|
||||
v.Mg = relative(v.Mg, origin)
|
||||
case *dns.MR:
|
||||
v.Mr = relative(v.Mr, origin)
|
||||
case *dns.KX:
|
||||
v.Exchanger = relative(v.Exchanger, origin)
|
||||
}
|
||||
}
|
||||
|
||||
func isRRTYPE(s string) bool { return rrTypes[strings.ToUpper(s)] }
|
||||
|
||||
func isTTLToken(s string) bool {
|
||||
|
||||
Reference in new issue
Block a user