diff --git a/README.markdown b/README.markdown index 7da16752..ebbce248 100644 --- a/README.markdown +++ b/README.markdown @@ -17,7 +17,8 @@ can build servers and resolvers with it. # Features * UDP/TCP queries, IPv4 and IPv6; -* RFC 1035 zone file parsing; +* RFC 1035 zone file parsing ($INCLUDE, $ORIGIN, $TTL and $GENERATE are + supported); * Fast: * Reply speed around 35/40K qps (faster hardware results in more qps); * Parsing RRs (zone files) with 95/100K RR/s, that's 5M records in about 50 seconds; diff --git a/defaults.go b/defaults.go index 79f1f4e4..a12cb198 100644 --- a/defaults.go +++ b/defaults.go @@ -228,7 +228,7 @@ func IsDomainName(s string) (uint8, uint8, bool) { // copied from net package. switch { default: return 0, uint8(l - longer), false - case 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '_' || c == '*': + case 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '_' || c == '*' || c == '/': ok = true partlen++ case c == '\\': diff --git a/msg.go b/msg.go index 71a38a6b..63006cfa 100644 --- a/msg.go +++ b/msg.go @@ -46,7 +46,6 @@ var ( ErrSigGen error = &Error{Err: "dns: bad signature generation"} ErrAuth error = &Error{Err: "dns: bad authentication"} ErrXfrSoa error = &Error{Err: "dns: no SOA seen"} - ErrXfrLast error = &Error{Err: "dns: last SOA"} ErrXfrType error = &Error{Err: "dns: no ixfr, nor axfr"} ErrHandle error = &Error{Err: "dns: handle is nil"} ErrChan error = &Error{Err: "dns: channel is nil"} diff --git a/parse_test.go b/parse_test.go index a9400efe..d2ec6762 100644 --- a/parse_test.go +++ b/parse_test.go @@ -553,3 +553,16 @@ func TestRfc1982(t *testing.T) { } } } + +func TestGenerate(t *testing.T) { + zone := "$GENERATE 65-126 $ 1D AAAA $.64/26" + to := ParseZone(strings.NewReader(zone), "miek.nl.", "") + for x := range to { + if x.Error == nil { + t.Logf("Read %s\n", x.RR.String()) + } else { + t.Logf("Failed to parse %v\n", x.Error) + t.Fail() + } + } +} diff --git a/xfr.go b/xfr.go index 49ba797f..74101a07 100644 --- a/xfr.go +++ b/xfr.go @@ -63,7 +63,7 @@ func (w *reply) axfrReceive(c chan *Exchange) { if !first { w.tsigTimersOnly = true // Subsequent envelopes use this. if checkXfrSOA(in, false) { - c <- &Exchange{w.req, in, w.rtt, w.conn.RemoteAddr(), ErrXfrLast} + c <- &Exchange{w.req, in, w.rtt, w.conn.RemoteAddr(), nil} return } c <- &Exchange{Request: w.req, Reply: in, Rtt: w.rtt, RemoteAddr: w.conn.RemoteAddr(), Error: nil} diff --git a/zgenerate.go b/zgenerate.go new file mode 100644 index 00000000..cf6cdcdd --- /dev/null +++ b/zgenerate.go @@ -0,0 +1,156 @@ +package dns + +import ( + "fmt" + "strconv" + "strings" +) + +// Parse the $GENERATE statement as used in BIND9 zones. +// See http://www.zytrax.com/books/dns/ch8/generate.html for instance. +// We are called after '$GENERATE '. After which we expect: +// * the range (12-24/2) +// * lhs (ownername) +// * [[ttl][class]] +// * type +// * rhs (rdata) +func generate(l lex, c chan lex, t chan Token, o string) string { + step := 1 + if i := strings.IndexAny(l.token, "/"); i != -1 { + if i+1 == len(l.token) { + return "bad step in $GENERATE range" + } + if s, e := strconv.Atoi(l.token[i+1:]); e != nil { + return "bad step in $GENERATE range" + } else { + if s < 0 { + return "bad step in $GENERATE range" + } + step = s + } + l.token = l.token[:i] + } + sx := strings.SplitN(l.token, "-", 2) + if len(sx) != 2 { + return "bad start/stop in $GENERATE range" + } + start, err := strconv.Atoi(sx[0]) + if err != nil { + return "bad start in $GENERATE range" + } + end, err := strconv.Atoi(sx[1]) + if err != nil { + return "bad end in $GENERATE range" + } + if end < 0 || start < 0 || end <= start { + return "bad range in $GENERATE range" + } + + <-c // _BLANK + // Create a complete new string, which we then parse again. + s := "" +BuildRR: + l = <-c + if l.value != _NEWLINE && l.value != _EOF { + s += l.token + goto BuildRR + } + for i := start; i <= end; i += step { + escape := false + dom := "" + // Defaults + mod := "%d" + offset := 0 + var err error + // Build the domain name. + for j := 0; j < len(s); j++ { // No 'range' because we need to jump around + switch s[j] { + case '\\': + if escape { + dom += "\\" + escape = false + continue + } + escape = true + case '$': + mod = "%d" + offset = 0 + if escape { + dom += "$" + escape = false + continue + } + escape = false + if j+1 >= len(s) { // End of the string + dom += fmt.Sprintf(mod, i+offset) + continue + } else { + if s[j+1] == '$' { + dom += "$" + j++ + continue + } + } + // Search for { and } + if s[j+1] == '{' { // Modifier block + sep := strings.Index(s[j+2:], "}") + if sep == -1 { + return "bad modifier in $GENERATE" + } + //println("checking", s[j+2:j+2+sep]) + mod, offset, err = modToPrintf(s[j+2 : j+2+sep]) + if err != nil { + return "bad modifier in $GENERATE" + } + j += 2 + sep // Jump to it + } + //println("mod", mod) + dom += fmt.Sprintf(mod, i+offset) + default: + if escape { // Pretty useless here + escape = false + continue + } + dom += string(s[j]) + } + } + // Re-parse the RR and send it on the current channel t + rx, err := NewRR("$ORIGIN " + o + "\n" + dom) + if err != nil { + return err.(*ParseError).err + } + t <- Token{RR: rx} + } + return "" +} + +// Convert a $GENERATE modifier 0,0,d to something Printf can deal with. +func modToPrintf(s string) (string, int, error) { + xs := strings.SplitN(s, ",", 3) + if len(xs) != 3 { + return "", 0, nil // make error + } + // xs[0] is offset, xs[1] is width, xs[2] is base + if xs[2] != "o" && xs[2] != "d" && xs[2] != "x" && xs[2] != "X" { + return "", 0, nil // make error + } + offset, err := strconv.Atoi(xs[0]) + if err != nil { + return "", 0, err + } + width, err := strconv.Atoi(xs[1]) + if err != nil { + return "", offset, err + } + printf := "%" + switch { + case width < 0: + return "", offset, nil // make error + case width == 0: + printf += xs[1] + default: + printf += "0" + xs[1] + } + printf += xs[2] + return printf, offset, nil +} diff --git a/zscan.go b/zscan.go index 7ea4976d..338dbd2a 100644 --- a/zscan.go +++ b/zscan.go @@ -31,9 +31,10 @@ const ( _RRTYPE _OWNER _CLASS - _DIRORIGIN // $ORIGIN - _DIRTTL // $TTL - _DIRINCLUDE // $INCLUDE + _DIRORIGIN // $ORIGIN + _DIRTTL // $TTL + _DIRINCLUDE // $INCLUDE + _DIRGENERATE // $GENERATE // Privatekey file _VALUE @@ -55,6 +56,8 @@ const ( _EXPECT_DIRORIGIN // Directive $ORIGIN _EXPECT_DIRINCLUDE_BL // Space after directive $INCLUDE _EXPECT_DIRINCLUDE // Directive $INCLUDE + _EXPECT_DIRGENERATE // Directive $GENERATE + _EXPECT_DIRGENERATE_BL // Space after directive $GENERATE ) // ParseError is a parsing error. It contains the parse error and the location in the io.Reader @@ -209,6 +212,8 @@ func parseZone(r io.Reader, origin, f string, t chan Token, include int) { st = _EXPECT_DIRORIGIN_BL case _DIRINCLUDE: st = _EXPECT_DIRINCLUDE_BL + case _DIRGENERATE: + st = _EXPECT_DIRGENERATE_BL case _RRTYPE: // Everthing has been omitted, this is the first thing on the line h.Name = prevName h.Rrtype = l.torc @@ -306,6 +311,22 @@ func parseZone(r io.Reader, origin, f string, t chan Token, include int) { origin = l.token } st = _EXPECT_OWNER_DIR + case _EXPECT_DIRGENERATE_BL: + if l.value != _BLANK { + t <- Token{Error: &ParseError{f, "no blank after $GENERATE-directive", l}} + return + } + st = _EXPECT_DIRGENERATE + case _EXPECT_DIRGENERATE: + if l.value != _STRING { + t <- Token{Error: &ParseError{f, "expecting $GENERATE value, not this...", l}} + return + } + if e := generate(l, c, t, origin); e != "" { + t <- Token{Error: &ParseError{f, e, l}} + return + } + st = _EXPECT_OWNER_DIR case _EXPECT_OWNER_BL: if l.value != _BLANK { t <- Token{Error: &ParseError{f, "no blank after owner", l}} @@ -389,7 +410,6 @@ func parseZone(r io.Reader, origin, f string, t chan Token, include int) { h.Rrtype = l.torc st = _EXPECT_RDATA case _EXPECT_RDATA: - // I could save my token here...? l r, e := setRR(h, c, origin, f) if e != nil { // If e.lex is nil than we have encounter a unknown RR type @@ -405,39 +425,12 @@ func parseZone(r io.Reader, origin, f string, t chan Token, include int) { } } // If we get here, we and the h.Rrtype is still zero, we haven't parsed anything - if h.Rrtype == 0 { - t <- Token{Error: &ParseError{f, "nothing made sense", lex{}}} - } + // Empty zonefile is also still zone file + // if h.Rrtype == 0 { + // t <- Token{Error: &ParseError{f, "nothing made sense", lex{}}} + // } } -/* -func (l lex) _string() string { - switch l.value { - case _STRING: - return "S:" + l.token + "$" - case _BLANK: - return "_" - case _QUOTE: - return "\"" - case _NEWLINE: - return "|" - case _RRTYPE: - return "R:" + l.token + "$" - case _OWNER: - return "O:" + l.token + "$" - case _CLASS: - return "C:" + l.token + "$" - case _DIRTTL: - return "$T:" + l.token + "$" - case _DIRORIGIN: - return "$O:" + l.token + "$" - case _DIRINCLUDE: - return "$I:" + l.token + "$" - } - return "**" -} -*/ - // zlexer scans the sourcefile and returns tokens on the channel c. func zlexer(s *scan, c chan lex) { var l lex @@ -488,6 +481,8 @@ func zlexer(s *scan, c chan lex) { l.value = _DIRORIGIN case "$INCLUDE": l.value = _DIRINCLUDE + case "$GENERATE": + l.value = _DIRGENERATE } c <- l } else {