Correctly parse omitted TTLs and relative domains (#513)

* Fix $TTL handling
* Error when there is no TTL for an RR
* Fix relative name handling
* Error when a relative name is used without an origin (cf. https://tools.ietf.org/html/rfc1035#section-5.1 )

Fixes #484
This commit is contained in:
Richard Gibson 2017-09-26 11:15:37 -04:00 committed by GitHub
parent 689d334b01
commit eccf8bbe83
6 changed files with 412 additions and 505 deletions

View File

@ -11,7 +11,7 @@ import (
"github.com/miekg/dns" "github.com/miekg/dns"
) )
// AddDomain adds origin to s if s is not already a FQDN. // AddOrigin adds origin to s if s is not already a FQDN.
// Note that the result may not be a FQDN. If origin does not end // Note that the result may not be a FQDN. If origin does not end
// with a ".", the result won't either. // with a ".", the result won't either.
// This implements the zonefile convention (specified in RFC 1035, // This implements the zonefile convention (specified in RFC 1035,

4
doc.go
View File

@ -22,9 +22,9 @@ Or directly from a string:
mx, err := dns.NewRR("miek.nl. 3600 IN MX 10 mx.miek.nl.") mx, err := dns.NewRR("miek.nl. 3600 IN MX 10 mx.miek.nl.")
Or when the default TTL (3600) and class (IN) suit you: Or when the default origin (.) and TTL (3600) and class (IN) suit you:
mx, err := dns.NewRR("miek.nl. MX 10 mx.miek.nl.") mx, err := dns.NewRR("miek.nl MX 10 mx.miek.nl")
Or even: Or even:

View File

@ -8,6 +8,7 @@ import (
"math/rand" "math/rand"
"net" "net"
"reflect" "reflect"
"regexp"
"strconv" "strconv"
"strings" "strings"
"testing" "testing"
@ -571,81 +572,88 @@ test IN CNAME test.a.example.com.
t.Logf("%d RRs parsed in %.2f s (%.2f RR/s)", i, float32(delta)/1e9, float32(i)/(float32(delta)/1e9)) t.Logf("%d RRs parsed in %.2f s (%.2f RR/s)", i, float32(delta)/1e9, float32(i)/(float32(delta)/1e9))
} }
func ExampleParseZone() { func TestOmittedTTL(t *testing.T) {
zone := `$ORIGIN . zone := `
$TTL 3600 ; 1 hour $ORIGIN example.com.
name IN SOA a6.nstld.com. hostmaster.nic.name. ( example.com. 42 IN SOA ns1.example.com. hostmaster.example.com. 1 86400 60 86400 3600 ; TTL=42 SOA
203362132 ; serial example.com. NS 2 ; TTL=42 absolute owner name
300 ; refresh (5 minutes) @ MD 3 ; TTL=42 current-origin owner name
300 ; retry (5 minutes) MF 4 ; TTL=42 leading-space implied owner name
1209600 ; expire (2 weeks) 43 TYPE65280 \# 1 05 ; TTL=43 implied owner name explicit TTL
300 ; minimum (5 minutes) MB 6 ; TTL=43 leading-tab implied owner name
) $TTL 1337
$TTL 10800 ; 3 hours example.com. 88 MG 7 ; TTL=88 explicit TTL
name. 10800 IN NS name. example.com. MR 8 ; TTL=1337 after first $TTL
IN NS g6.nstld.com. $TTL 314
7200 NS h6.nstld.com. 1 TXT 9 ; TTL=1 implied owner name explicit TTL
3600 IN NS j6.nstld.com. example.com. DNAME 10 ; TTL=314 after second $TTL
IN 3600 NS k6.nstld.com.
NS l6.nstld.com.
NS a6.nstld.com.
NS c6.nstld.com.
NS d6.nstld.com.
NS f6.nstld.com.
NS m6.nstld.com.
(
NS m7.nstld.com.
)
$ORIGIN name.
0-0onlus NS ns7.ehiweb.it.
NS ns8.ehiweb.it.
0-g MX 10 mx01.nic
MX 10 mx02.nic
MX 10 mx03.nic
MX 10 mx04.nic
$ORIGIN 0-g.name
moutamassey NS ns01.yahoodomains.jp.
NS ns02.yahoodomains.jp.
` `
to := ParseZone(strings.NewReader(zone), "", "testzone") reCaseFromComment := regexp.MustCompile(`TTL=(\d+)\s+(.*)`)
for x := range to { records := ParseZone(strings.NewReader(zone), "", "")
fmt.Println(x.RR) var i int
for record := range records {
i++
if record.Error != nil {
t.Error(record.Error)
continue
}
expected := reCaseFromComment.FindStringSubmatch(record.Comment)
expectedTTL, _ := strconv.ParseUint(expected[1], 10, 32)
ttl := record.RR.Header().Ttl
if ttl != uint32(expectedTTL) {
t.Errorf("%s: expected TTL %d, got %d", expected[2], expectedTTL, ttl)
}
}
if i != 10 {
t.Errorf("expected %d records, got %d", 5, i)
} }
// Output:
// name. 3600 IN SOA a6.nstld.com. hostmaster.nic.name. 203362132 300 300 1209600 300
// name. 10800 IN NS name.
// name. 10800 IN NS g6.nstld.com.
// name. 7200 IN NS h6.nstld.com.
// name. 3600 IN NS j6.nstld.com.
// name. 3600 IN NS k6.nstld.com.
// name. 10800 IN NS l6.nstld.com.
// name. 10800 IN NS a6.nstld.com.
// name. 10800 IN NS c6.nstld.com.
// name. 10800 IN NS d6.nstld.com.
// name. 10800 IN NS f6.nstld.com.
// name. 10800 IN NS m6.nstld.com.
// name. 10800 IN NS m7.nstld.com.
// 0-0onlus.name. 10800 IN NS ns7.ehiweb.it.
// 0-0onlus.name. 10800 IN NS ns8.ehiweb.it.
// 0-g.name. 10800 IN MX 10 mx01.nic.name.
// 0-g.name. 10800 IN MX 10 mx02.nic.name.
// 0-g.name. 10800 IN MX 10 mx03.nic.name.
// 0-g.name. 10800 IN MX 10 mx04.nic.name.
// moutamassey.0-g.name.name. 10800 IN NS ns01.yahoodomains.jp.
// moutamassey.0-g.name.name. 10800 IN NS ns02.yahoodomains.jp.
} }
func ExampleHIP() { func TestRelativeNameErrors(t *testing.T) {
h := `www.example.com IN HIP ( 2 200100107B1A74DF365639CC39F1D578 var badZones = []struct {
AwEAAbdxyhNuSutc5EMzxTs9LBPCIkOFH8cIvM4p label string
9+LrV4e19WzK00+CI6zBCQTdtWsuxKbWIy87UOoJTwkUs7lBu+Upr1gsNrut79ryra+bSRGQ zoneContents string
b1slImA8YVJyuIDsj7kwzG7jnERNqnWxZ48AWkskmdHaVDP4BcelrTI3rMXdXF5D expectedErr string
rvs.example.com. )` }{
if hip, err := NewRR(h); err == nil { {
fmt.Println(hip.String()) "relative owner name without origin",
"example.com 3600 IN SOA ns.example.com. hostmaster.example.com. 1 86400 60 86400 3600",
"bad owner name",
},
{
"relative owner name in RDATA",
"example.com. 3600 IN SOA ns hostmaster 1 86400 60 86400 3600",
"bad SOA Ns",
},
{
"origin reference without origin",
"@ 3600 IN SOA ns.example.com. hostmaster.example.com. 1 86400 60 86400 3600",
"bad owner name",
},
{
"relative owner name in $INCLUDE",
"$INCLUDE file.db example.com",
"bad origin name",
},
{
"relative owner name in $ORIGIN",
"$ORIGIN example.com",
"bad origin name",
},
}
for _, errorCase := range badZones {
entries := ParseZone(strings.NewReader(errorCase.zoneContents), "", "")
for entry := range entries {
if entry.Error == nil {
t.Errorf("%s: expected error, got nil", errorCase.label)
continue
}
err := entry.Error.err
if err != errorCase.expectedErr {
t.Errorf("%s: expected error `%s`, got `%s`", errorCase.label, errorCase.expectedErr, err)
}
}
} }
// Output:
// www.example.com. 3600 IN HIP 2 200100107B1A74DF365639CC39F1D578 AwEAAbdxyhNuSutc5EMzxTs9LBPCIkOFH8cIvM4p9+LrV4e19WzK00+CI6zBCQTdtWsuxKbWIy87UOoJTwkUs7lBu+Upr1gsNrut79ryra+bSRGQb1slImA8YVJyuIDsj7kwzG7jnERNqnWxZ48AWkskmdHaVDP4BcelrTI3rMXdXF5D rvs.example.com.
} }
func TestHIP(t *testing.T) { func TestHIP(t *testing.T) {
@ -686,24 +694,6 @@ b1slImA8YVJyuIDsj7kwzG7jnERNqnWxZ48AWkskmdHaVDP4BcelrTI3rMXdXF5D
} }
} }
func ExampleSOA() {
s := "example.com. 1000 SOA master.example.com. admin.example.com. 1 4294967294 4294967293 4294967295 100"
if soa, err := NewRR(s); err == nil {
fmt.Println(soa.String())
}
// Output:
// example.com. 1000 IN SOA master.example.com. admin.example.com. 1 4294967294 4294967293 4294967295 100
}
func TestLineNumberError(t *testing.T) {
s := "example.com. 1000 SOA master.example.com. admin.example.com. monkey 4294967294 4294967293 4294967295 100"
if _, err := NewRR(s); err != nil {
if err.Error() != "dns: bad SOA zone parameter: \"monkey\" at line: 1:68" {
t.Error("not expecting this error: ", err)
}
}
}
// Test with no known RR on the line // Test with no known RR on the line
func TestLineNumberError2(t *testing.T) { func TestLineNumberError2(t *testing.T) {
tests := map[string]string{ tests := map[string]string{
@ -801,28 +791,6 @@ func TestLowercaseTokens(t *testing.T) {
} }
} }
func ExampleParseZone_generate() {
// From the manual: http://www.bind9.net/manual/bind/9.3.2/Bv9ARM.ch06.html#id2566761
zone := "$GENERATE 1-2 0 NS SERVER$.EXAMPLE.\n$GENERATE 1-8 $ CNAME $.0"
to := ParseZone(strings.NewReader(zone), "0.0.192.IN-ADDR.ARPA.", "")
for x := range to {
if x.Error == nil {
fmt.Println(x.RR.String())
}
}
// Output:
// 0.0.0.192.IN-ADDR.ARPA. 3600 IN NS SERVER1.EXAMPLE.
// 0.0.0.192.IN-ADDR.ARPA. 3600 IN NS SERVER2.EXAMPLE.
// 1.0.0.192.IN-ADDR.ARPA. 3600 IN CNAME 1.0.0.0.192.IN-ADDR.ARPA.
// 2.0.0.192.IN-ADDR.ARPA. 3600 IN CNAME 2.0.0.0.192.IN-ADDR.ARPA.
// 3.0.0.192.IN-ADDR.ARPA. 3600 IN CNAME 3.0.0.0.192.IN-ADDR.ARPA.
// 4.0.0.192.IN-ADDR.ARPA. 3600 IN CNAME 4.0.0.0.192.IN-ADDR.ARPA.
// 5.0.0.192.IN-ADDR.ARPA. 3600 IN CNAME 5.0.0.0.192.IN-ADDR.ARPA.
// 6.0.0.192.IN-ADDR.ARPA. 3600 IN CNAME 6.0.0.0.192.IN-ADDR.ARPA.
// 7.0.0.192.IN-ADDR.ARPA. 3600 IN CNAME 7.0.0.0.192.IN-ADDR.ARPA.
// 8.0.0.192.IN-ADDR.ARPA. 3600 IN CNAME 8.0.0.0.192.IN-ADDR.ARPA.
}
func TestSRVPacking(t *testing.T) { func TestSRVPacking(t *testing.T) {
msg := Msg{} msg := Msg{}

View File

@ -143,7 +143,7 @@ func (rd *VERSION) Len() int {
} }
var smallzone = `$ORIGIN example.org. var smallzone = `$ORIGIN example.org.
@ SOA sns.dns.icann.org. noc.dns.icann.org. ( @ 3600 IN SOA sns.dns.icann.org. noc.dns.icann.org. (
2014091518 7200 3600 1209600 3600 2014091518 7200 3600 1209600 3600
) )
A 1.2.3.4 A 1.2.3.4

129
scan.go
View File

@ -105,6 +105,12 @@ type Token struct {
Comment string Comment string
} }
// ttlState describes the state necessary to fill in an omitted RR TTL
type ttlState struct {
ttl uint32 // ttl is the current default TTL
isByDirective bool // isByDirective indicates whether ttl was set by a $TTL directive
}
// NewRR reads the RR contained in the string s. Only the first RR is // NewRR reads the RR contained in the string s. Only the first RR is
// returned. If s contains no RR, return nil with no error. The class // returned. If s contains no RR, return nil with no error. The class
// defaults to IN and TTL defaults to 3600. The full zone file syntax // defaults to IN and TTL defaults to 3600. The full zone file syntax
@ -120,7 +126,8 @@ func NewRR(s string) (RR, error) {
// ReadRR reads the RR contained in q. // ReadRR reads the RR contained in q.
// See NewRR for more documentation. // See NewRR for more documentation.
func ReadRR(q io.Reader, filename string) (RR, error) { func ReadRR(q io.Reader, filename string) (RR, error) {
r := <-parseZoneHelper(q, ".", filename, 1) defttl := &ttlState{defaultTtl, false}
r := <-parseZoneHelper(q, ".", defttl, filename, 1)
if r == nil { if r == nil {
return nil, nil return nil, nil
} }
@ -132,10 +139,10 @@ func ReadRR(q io.Reader, filename string) (RR, error) {
} }
// ParseZone reads a RFC 1035 style zonefile from r. It returns *Tokens on the // ParseZone reads a RFC 1035 style zonefile from r. It returns *Tokens on the
// returned channel, which consist out the parsed RR, a potential comment or an error. // returned channel, each consisting of either a parsed RR and optional comment
// If there is an error the RR is nil. The string file is only used // or a nil RR and an error. The string file is only used
// in error reporting. The string origin is used as the initial origin, as // in error reporting. The string origin is used as the initial origin, as
// if the file would start with: $ORIGIN origin . // if the file would start with an $ORIGIN directive.
// The directives $INCLUDE, $ORIGIN, $TTL and $GENERATE are supported. // The directives $INCLUDE, $ORIGIN, $TTL and $GENERATE are supported.
// The channel t is closed by ParseZone when the end of r is reached. // The channel t is closed by ParseZone when the end of r is reached.
// //
@ -157,16 +164,16 @@ func ReadRR(q io.Reader, filename string) (RR, error) {
// The text "; this is comment" is returned in Token.Comment. Comments inside the // The text "; this is comment" is returned in Token.Comment. Comments inside the
// RR are discarded. Comments on a line by themselves are discarded too. // RR are discarded. Comments on a line by themselves are discarded too.
func ParseZone(r io.Reader, origin, file string) chan *Token { func ParseZone(r io.Reader, origin, file string) chan *Token {
return parseZoneHelper(r, origin, file, 10000) return parseZoneHelper(r, origin, nil, file, 10000)
} }
func parseZoneHelper(r io.Reader, origin, file string, chansize int) chan *Token { func parseZoneHelper(r io.Reader, origin string, defttl *ttlState, file string, chansize int) chan *Token {
t := make(chan *Token, chansize) t := make(chan *Token, chansize)
go parseZone(r, origin, file, t, 0) go parseZone(r, origin, defttl, file, t, 0)
return t return t
} }
func parseZone(r io.Reader, origin, f string, t chan *Token, include int) { func parseZone(r io.Reader, origin string, defttl *ttlState, f string, t chan *Token, include int) {
defer func() { defer func() {
if include == 0 { if include == 0 {
close(t) close(t)
@ -186,18 +193,16 @@ func parseZone(r io.Reader, origin, f string, t chan *Token, include int) {
// After detecting these, we know the zRrtype so we can jump to functions // After detecting these, we know the zRrtype so we can jump to functions
// handling the rdata for each of these types. // handling the rdata for each of these types.
if origin == "" { if origin != "" {
origin = "." origin = Fqdn(origin)
} if _, ok := IsDomainName(origin); !ok {
origin = Fqdn(origin) t <- &Token{Error: &ParseError{f, "bad initial origin name", lex{}}}
if _, ok := IsDomainName(origin); !ok { return
t <- &Token{Error: &ParseError{f, "bad initial origin name", lex{}}} }
return
} }
st := zExpectOwnerDir // initial state st := zExpectOwnerDir // initial state
var h RR_Header var h RR_Header
var defttl uint32 = defaultTtl
var prevName string var prevName string
for l := range c { for l := range c {
// Lexer spotted an error already // Lexer spotted an error already
@ -209,27 +214,21 @@ func parseZone(r io.Reader, origin, f string, t chan *Token, include int) {
switch st { switch st {
case zExpectOwnerDir: case zExpectOwnerDir:
// We can also expect a directive, like $TTL or $ORIGIN // We can also expect a directive, like $TTL or $ORIGIN
h.Ttl = defttl if defttl != nil {
h.Ttl = defttl.ttl
}
h.Class = ClassINET h.Class = ClassINET
switch l.value { switch l.value {
case zNewline: case zNewline:
st = zExpectOwnerDir st = zExpectOwnerDir
case zOwner: case zOwner:
h.Name = l.token h.Name = l.token
if l.token[0] == '@' { name, ok := toAbsoluteName(l.token, origin)
h.Name = origin
prevName = h.Name
st = zExpectOwnerBl
break
}
if h.Name[l.length-1] != '.' {
h.Name = appendOrigin(h.Name, origin)
}
_, ok := IsDomainName(l.token)
if !ok { if !ok {
t <- &Token{Error: &ParseError{f, "bad owner name", l}} t <- &Token{Error: &ParseError{f, "bad owner name", l}}
return return
} }
h.Name = name
prevName = h.Name prevName = h.Name
st = zExpectOwnerBl st = zExpectOwnerBl
case zDirTtl: case zDirTtl:
@ -258,8 +257,9 @@ func parseZone(r io.Reader, origin, f string, t chan *Token, include int) {
return return
} }
h.Ttl = ttl h.Ttl = ttl
// Don't about the defttl, we should take the $TTL value if defttl == nil || !defttl.isByDirective {
// defttl = ttl defttl = &ttlState{ttl, false}
}
st = zExpectAnyNoTtlBl st = zExpectAnyNoTtlBl
default: default:
@ -282,20 +282,12 @@ func parseZone(r io.Reader, origin, f string, t chan *Token, include int) {
case zBlank: case zBlank:
l := <-c l := <-c
if l.value == zString { if l.value == zString {
if _, ok := IsDomainName(l.token); !ok || l.length == 0 || l.err { name, ok := toAbsoluteName(l.token, origin)
if !ok {
t <- &Token{Error: &ParseError{f, "bad origin name", l}} t <- &Token{Error: &ParseError{f, "bad origin name", l}}
return return
} }
// a new origin is specified. neworigin = name
if l.token[l.length-1] != '.' {
if origin != "." { // Prevent .. endings
neworigin = l.token + "." + origin
} else {
neworigin = l.token + origin
}
} else {
neworigin = l.token
}
} }
case zNewline, zEOF: case zNewline, zEOF:
// Ok // Ok
@ -313,7 +305,7 @@ func parseZone(r io.Reader, origin, f string, t chan *Token, include int) {
t <- &Token{Error: &ParseError{f, "too deeply nested $INCLUDE", l}} t <- &Token{Error: &ParseError{f, "too deeply nested $INCLUDE", l}}
return return
} }
parseZone(r1, neworigin, l.token, t, include+1) parseZone(r1, neworigin, defttl, l.token, t, include+1)
st = zExpectOwnerDir st = zExpectOwnerDir
case zExpectDirTtlBl: case zExpectDirTtlBl:
if l.value != zBlank { if l.value != zBlank {
@ -335,7 +327,7 @@ func parseZone(r io.Reader, origin, f string, t chan *Token, include int) {
t <- &Token{Error: &ParseError{f, "expecting $TTL value, not this...", l}} t <- &Token{Error: &ParseError{f, "expecting $TTL value, not this...", l}}
return return
} }
defttl = ttl defttl = &ttlState{ttl, true}
st = zExpectOwnerDir st = zExpectOwnerDir
case zExpectDirOriginBl: case zExpectDirOriginBl:
if l.value != zBlank { if l.value != zBlank {
@ -351,19 +343,12 @@ func parseZone(r io.Reader, origin, f string, t chan *Token, include int) {
if e, _ := slurpRemainder(c, f); e != nil { if e, _ := slurpRemainder(c, f); e != nil {
t <- &Token{Error: e} t <- &Token{Error: e}
} }
if _, ok := IsDomainName(l.token); !ok { name, ok := toAbsoluteName(l.token, origin)
if !ok {
t <- &Token{Error: &ParseError{f, "bad origin name", l}} t <- &Token{Error: &ParseError{f, "bad origin name", l}}
return return
} }
if l.token[l.length-1] != '.' { origin = name
if origin != "." { // Prevent .. endings
origin = l.token + "." + origin
} else {
origin = l.token + origin
}
} else {
origin = l.token
}
st = zExpectOwnerDir st = zExpectOwnerDir
case zExpectDirGenerateBl: case zExpectDirGenerateBl:
if l.value != zBlank { if l.value != zBlank {
@ -390,6 +375,10 @@ func parseZone(r io.Reader, origin, f string, t chan *Token, include int) {
case zExpectAny: case zExpectAny:
switch l.value { switch l.value {
case zRrtpe: case zRrtpe:
if defttl == nil {
t <- &Token{Error: &ParseError{f, "missing TTL with no previous value", l}}
return
}
h.Rrtype = l.torc h.Rrtype = l.torc
st = zExpectRdata st = zExpectRdata
case zClass: case zClass:
@ -402,7 +391,9 @@ func parseZone(r io.Reader, origin, f string, t chan *Token, include int) {
return return
} }
h.Ttl = ttl h.Ttl = ttl
// defttl = ttl // don't set the defttl here if defttl == nil || !defttl.isByDirective {
defttl = &ttlState{ttl, false}
}
st = zExpectAnyNoTtlBl st = zExpectAnyNoTtlBl
default: default:
t <- &Token{Error: &ParseError{f, "expecting RR type, TTL or class, not this...", l}} t <- &Token{Error: &ParseError{f, "expecting RR type, TTL or class, not this...", l}}
@ -441,7 +432,9 @@ func parseZone(r io.Reader, origin, f string, t chan *Token, include int) {
return return
} }
h.Ttl = ttl h.Ttl = ttl
// defttl = ttl // don't set the def ttl anymore if defttl == nil || !defttl.isByDirective {
defttl = &ttlState{ttl, false}
}
st = zExpectRrtypeBl st = zExpectRrtypeBl
case zRrtpe: case zRrtpe:
h.Rrtype = l.torc h.Rrtype = l.torc
@ -918,6 +911,34 @@ func stringToCm(token string) (e, m uint8, ok bool) {
return return
} }
func toAbsoluteName(name, origin string) (absolute string, ok bool) {
// check for an explicit origin reference
if name == "@" {
// require a nonempty origin
if origin == "" {
return "", false
}
return origin, true
}
// require a valid domain name
_, ok = IsDomainName(name)
if !ok || name == "" {
return "", false
}
// check if name is already absolute
if name[len(name)-1] == '.' {
return name, true
}
// require a nonempty origin
if origin == "" {
return "", false
}
return appendOrigin(name, origin), true
}
func appendOrigin(name, origin string) string { func appendOrigin(name, origin string) string {
if origin == "." { if origin == "." {
return name + origin return name + origin

File diff suppressed because it is too large Load Diff