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"
)
// 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
// with a ".", the result won't either.
// 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.")
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:

View File

@ -8,6 +8,7 @@ import (
"math/rand"
"net"
"reflect"
"regexp"
"strconv"
"strings"
"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))
}
func ExampleParseZone() {
zone := `$ORIGIN .
$TTL 3600 ; 1 hour
name IN SOA a6.nstld.com. hostmaster.nic.name. (
203362132 ; serial
300 ; refresh (5 minutes)
300 ; retry (5 minutes)
1209600 ; expire (2 weeks)
300 ; minimum (5 minutes)
)
$TTL 10800 ; 3 hours
name. 10800 IN NS name.
IN NS g6.nstld.com.
7200 NS h6.nstld.com.
3600 IN NS j6.nstld.com.
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.
func TestOmittedTTL(t *testing.T) {
zone := `
$ORIGIN example.com.
example.com. 42 IN SOA ns1.example.com. hostmaster.example.com. 1 86400 60 86400 3600 ; TTL=42 SOA
example.com. NS 2 ; TTL=42 absolute owner name
@ MD 3 ; TTL=42 current-origin owner name
MF 4 ; TTL=42 leading-space implied owner name
43 TYPE65280 \# 1 05 ; TTL=43 implied owner name explicit TTL
MB 6 ; TTL=43 leading-tab implied owner name
$TTL 1337
example.com. 88 MG 7 ; TTL=88 explicit TTL
example.com. MR 8 ; TTL=1337 after first $TTL
$TTL 314
1 TXT 9 ; TTL=1 implied owner name explicit TTL
example.com. DNAME 10 ; TTL=314 after second $TTL
`
to := ParseZone(strings.NewReader(zone), "", "testzone")
for x := range to {
fmt.Println(x.RR)
reCaseFromComment := regexp.MustCompile(`TTL=(\d+)\s+(.*)`)
records := ParseZone(strings.NewReader(zone), "", "")
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() {
h := `www.example.com IN HIP ( 2 200100107B1A74DF365639CC39F1D578
AwEAAbdxyhNuSutc5EMzxTs9LBPCIkOFH8cIvM4p
9+LrV4e19WzK00+CI6zBCQTdtWsuxKbWIy87UOoJTwkUs7lBu+Upr1gsNrut79ryra+bSRGQ
b1slImA8YVJyuIDsj7kwzG7jnERNqnWxZ48AWkskmdHaVDP4BcelrTI3rMXdXF5D
rvs.example.com. )`
if hip, err := NewRR(h); err == nil {
fmt.Println(hip.String())
func TestRelativeNameErrors(t *testing.T) {
var badZones = []struct {
label string
zoneContents string
expectedErr 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) {
@ -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
func TestLineNumberError2(t *testing.T) {
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) {
msg := Msg{}

View File

@ -143,7 +143,7 @@ func (rd *VERSION) Len() int {
}
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
)
A 1.2.3.4

129
scan.go
View File

@ -105,6 +105,12 @@ type Token struct {
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
// 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
@ -120,7 +126,8 @@ func NewRR(s string) (RR, error) {
// ReadRR reads the RR contained in q.
// See NewRR for more documentation.
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 {
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
// returned channel, which consist out the parsed RR, a potential comment or an error.
// If there is an error the RR is nil. The string file is only used
// returned channel, each consisting of either a parsed RR and optional comment
// 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
// 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 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
// RR are discarded. Comments on a line by themselves are discarded too.
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)
go parseZone(r, origin, file, t, 0)
go parseZone(r, origin, defttl, file, t, 0)
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() {
if include == 0 {
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
// handling the rdata for each of these types.
if origin == "" {
origin = "."
}
origin = Fqdn(origin)
if _, ok := IsDomainName(origin); !ok {
t <- &Token{Error: &ParseError{f, "bad initial origin name", lex{}}}
return
if origin != "" {
origin = Fqdn(origin)
if _, ok := IsDomainName(origin); !ok {
t <- &Token{Error: &ParseError{f, "bad initial origin name", lex{}}}
return
}
}
st := zExpectOwnerDir // initial state
var h RR_Header
var defttl uint32 = defaultTtl
var prevName string
for l := range c {
// Lexer spotted an error already
@ -209,27 +214,21 @@ func parseZone(r io.Reader, origin, f string, t chan *Token, include int) {
switch st {
case zExpectOwnerDir:
// We can also expect a directive, like $TTL or $ORIGIN
h.Ttl = defttl
if defttl != nil {
h.Ttl = defttl.ttl
}
h.Class = ClassINET
switch l.value {
case zNewline:
st = zExpectOwnerDir
case zOwner:
h.Name = l.token
if l.token[0] == '@' {
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)
name, ok := toAbsoluteName(l.token, origin)
if !ok {
t <- &Token{Error: &ParseError{f, "bad owner name", l}}
return
}
h.Name = name
prevName = h.Name
st = zExpectOwnerBl
case zDirTtl:
@ -258,8 +257,9 @@ func parseZone(r io.Reader, origin, f string, t chan *Token, include int) {
return
}
h.Ttl = ttl
// Don't about the defttl, we should take the $TTL value
// defttl = ttl
if defttl == nil || !defttl.isByDirective {
defttl = &ttlState{ttl, false}
}
st = zExpectAnyNoTtlBl
default:
@ -282,20 +282,12 @@ func parseZone(r io.Reader, origin, f string, t chan *Token, include int) {
case zBlank:
l := <-c
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}}
return
}
// a new origin is specified.
if l.token[l.length-1] != '.' {
if origin != "." { // Prevent .. endings
neworigin = l.token + "." + origin
} else {
neworigin = l.token + origin
}
} else {
neworigin = l.token
}
neworigin = name
}
case zNewline, zEOF:
// 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}}
return
}
parseZone(r1, neworigin, l.token, t, include+1)
parseZone(r1, neworigin, defttl, l.token, t, include+1)
st = zExpectOwnerDir
case zExpectDirTtlBl:
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}}
return
}
defttl = ttl
defttl = &ttlState{ttl, true}
st = zExpectOwnerDir
case zExpectDirOriginBl:
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 {
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}}
return
}
if l.token[l.length-1] != '.' {
if origin != "." { // Prevent .. endings
origin = l.token + "." + origin
} else {
origin = l.token + origin
}
} else {
origin = l.token
}
origin = name
st = zExpectOwnerDir
case zExpectDirGenerateBl:
if l.value != zBlank {
@ -390,6 +375,10 @@ func parseZone(r io.Reader, origin, f string, t chan *Token, include int) {
case zExpectAny:
switch l.value {
case zRrtpe:
if defttl == nil {
t <- &Token{Error: &ParseError{f, "missing TTL with no previous value", l}}
return
}
h.Rrtype = l.torc
st = zExpectRdata
case zClass:
@ -402,7 +391,9 @@ func parseZone(r io.Reader, origin, f string, t chan *Token, include int) {
return
}
h.Ttl = ttl
// defttl = ttl // don't set the defttl here
if defttl == nil || !defttl.isByDirective {
defttl = &ttlState{ttl, false}
}
st = zExpectAnyNoTtlBl
default:
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
}
h.Ttl = ttl
// defttl = ttl // don't set the def ttl anymore
if defttl == nil || !defttl.isByDirective {
defttl = &ttlState{ttl, false}
}
st = zExpectRrtypeBl
case zRrtpe:
h.Rrtype = l.torc
@ -918,6 +911,34 @@ func stringToCm(token string) (e, m uint8, ok bool) {
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 {
if origin == "." {
return name + origin

File diff suppressed because it is too large Load Diff