From f6d8a66c018ffe7ad3d41e92deaae4049703be19 Mon Sep 17 00:00:00 2001 From: Andrew Tunnell-Jones Date: Mon, 17 Feb 2014 10:50:29 +0000 Subject: [PATCH 01/10] Add quick tests for domain name and TXT rr --- parse_test.go | 164 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) diff --git a/parse_test.go b/parse_test.go index f157653a..017d287d 100644 --- a/parse_test.go +++ b/parse_test.go @@ -5,12 +5,15 @@ package dns import ( + "bytes" "fmt" + "math/rand" "net" "os" "strconv" "strings" "testing" + "testing/quick" "time" ) @@ -92,6 +95,167 @@ func TestDomainName(t *testing.T) { } } +func GenerateDomain(r *rand.Rand, size int) []byte { + dnLen := size % 70 // artificially limit size so there's less to intrepret if a failure occurs + var dn []byte + done := false + for i := 0; i < dnLen && !done; { + max := dnLen - i + if max > 63 { + max = 63 + } + lLen := max + if lLen != 0 { + lLen = int(r.Uint32()) % max + } + done = lLen == 0 + if done { + continue + } + l := make([]byte, lLen+1) + l[0] = byte(lLen) + for j := 0; j < lLen; j++ { + l[j+1] = byte(rand.Uint32()) + } + dn = append(dn, l...) + i += 1 + lLen + } + return append(dn, 0) +} + +func TestDomainQuick(t *testing.T) { + r := rand.New(rand.NewSource(0)) + f := func(l int) bool { + db := GenerateDomain(r, l) + ds, _, err := UnpackDomainName(db, 0) + if err != nil { + panic(err) + } + buf := make([]byte, 255) + off, err := PackDomainName(ds, buf, 0, nil, false) + if err != nil { + t.Logf("Error packing domain: %s", err.Error()) + t.Logf(" Bytes: %v\n", db) + t.Logf("String: %v\n", ds) + return false + } + if !bytes.Equal(db, buf[:off]) { + t.Logf("Repacked domain doesn't match original:") + t.Logf("Src Bytes: %v", db) + t.Logf(" String: %v", ds) + t.Logf("Out Bytes: %v", buf[:off]) + return false + } + return true + } + if err := quick.Check(f, nil); err != nil { + t.Error(err) + } +} + +func GenerateTXT(r *rand.Rand, size int) []byte { + rdLen := size % 300 // artificially limit size so there's less to intrepret if a failure occurs + var rd []byte + for i := 0; i < rdLen; { + max := rdLen - 1 + if max > 255 { + max = 255 + } + sLen := max + if max != 0 { + sLen = int(r.Uint32()) % max + } + s := make([]byte, sLen+1) + s[0] = byte(sLen) + for j := 0; j < sLen; j++ { + s[j+1] = byte(rand.Uint32()) + } + rd = append(rd, s...) + i += 1 + sLen + } + return rd +} + +func TestTXTRRQuick(t *testing.T) { + s := rand.NewSource(0) + r := rand.New(s) + typeAndClass := []byte{ + byte(TypeTXT >> 8), byte(TypeTXT), + byte(ClassINET >> 8), byte(ClassINET), + 0, 0, 0, 1, // TTL + } + f := func(l int) bool { + owner := GenerateDomain(r, l) + rdata := GenerateTXT(r, l) + rrbytes := make([]byte, 0, len(owner)+2+2+4+2+len(rdata)) + rrbytes = append(rrbytes, owner...) + rrbytes = append(rrbytes, typeAndClass...) + rrbytes = append(rrbytes, byte(len(rdata)>>8)) + rrbytes = append(rrbytes, byte(len(rdata))) + rrbytes = append(rrbytes, rdata...) + rr, _, err := UnpackRR(rrbytes, 0) + if err != nil { + panic(err) + } + buf := make([]byte, len(rrbytes)*3) + off, err := PackRR(rr, buf, 0, nil, false) + if err != nil { + t.Logf("Pack Error: %s\nRR: %V", err.Error(), rr) + return false + } + buf = buf[:off] + if !bytes.Equal(buf, rrbytes) { + t.Logf("Packed bytes don't match original bytes") + t.Logf("Src Bytes: %v", rrbytes) + t.Logf(" Struct: %V", rr) + t.Logf("Out Bytes: %v", buf) + return false + } + if len(rdata) == 0 { + // string'ing won't produce any data to parse + return true + } + rrString := rr.String() + rr2, err := NewRR(rrString) + if err != nil { + t.Logf("Error parsing own output: %s", err.Error()) + t.Logf("Struct: %V", rr) + t.Logf("String: %v", rrString) + return false + } + if rr2.String() != rrString { + t.Logf("Parsed rr.String() doesn't match original string") + t.Logf("Original: %v", rrString) + t.Logf(" Parsed: %v", rr2.String()) + return false + } + + buf = make([]byte, len(rrbytes)*3) + off, err = PackRR(rr2, buf, 0, nil, false) + if err != nil { + t.Logf("Error packing parsed rr: %s", err.Error()) + t.Logf("Unpacked Struct: %V", rr) + t.Logf(" String: %v", rrString) + t.Logf(" Parsed Struct: %V", rr2) + return false + } + buf = buf[:off] + if !bytes.Equal(buf, rrbytes) { + t.Logf("Parsed packed bytes don't match original bytes") + t.Logf(" Source Bytes: %v", rrbytes) + t.Logf("Unpacked Struct: %V", rr) + t.Logf(" String: %v", rrString) + t.Logf(" Parsed Struct: %V", rr2) + t.Logf(" Repacked Bytes: %v", buf) + return false + } + return true + } + if err := quick.Check(f, nil); err != nil { + t.Error(err) + } +} + func TestParseDirectiveMisc(t *testing.T) { tests := map[string]string{ "$ORIGIN miek.nl.\na IN NS b": "a.miek.nl.\t3600\tIN\tNS\tb.miek.nl.", From 3f834a04fbfc2da02cd86751d1a62f6c1bfc11ee Mon Sep 17 00:00:00 2001 From: Andrew Tunnell-Jones Date: Wed, 26 Feb 2014 10:33:33 +0000 Subject: [PATCH 02/10] Update domain name and TXT string escape behaviour Changes to domain name packing and unpacking: * Escape dot, backslash, brackets, double-quote, semi-colon and space * Tab, line feed and carriage return become \t, \n and \r Changes to TXT string packing and unpacking: * Escape backslash and double-quote * Tab, line feed and carriage return become \t, \n and \r * Other unprintables to \DDD Stringers do the equivalent of putting domain names and TXT strings to the wire and back. There is some duplication of logic. I found performance suffered when I broke the logic out into smaller functions. I think this may have been due to functions not being inlined for various reasons. --- dns.go | 6 +- msg.go | 221 ++++++++++++++++++++++++++++++++++++++----------------- types.go | 199 ++++++++++++++++++++++++++++++++----------------- 3 files changed, 283 insertions(+), 143 deletions(-) diff --git a/dns.go b/dns.go index 4a5a6967..88d6f9cb 100644 --- a/dns.go +++ b/dns.go @@ -154,11 +154,7 @@ func (h *RR_Header) String() string { // and maybe other things } - if len(h.Name) == 0 { - s += ".\t" - } else { - s += h.Name + "\t" - } + s += sprintDomain(h.Name) + "\t" s += strconv.FormatInt(int64(h.Ttl), 10) + "\t" s += Class(h.Class).String() + "\t" s += Type(h.Rrtype).String() + "\t" diff --git a/msg.go b/msg.go index 03a79a82..cf105f8d 100644 --- a/msg.go +++ b/msg.go @@ -266,14 +266,18 @@ func packDomainName(s string, msg []byte, off int, compression map[string]int, c return lenmsg, labels, ErrBuf } // check for \DDD - if i+2 < ls && bs[i] >= '0' && bs[i] <= '9' && - bs[i+1] >= '0' && bs[i+1] <= '9' && - bs[i+2] >= '0' && bs[i+2] <= '9' { - bs[i] = byte((bs[i]-'0')*100 + (bs[i+1]-'0')*10 + (bs[i+2] - '0')) + if i+2 < ls && isDigit(bs[i]) && isDigit(bs[i+1]) && isDigit(bs[i+2]) { + bs[i] = dddToByte(bs[i:]) for j := i + 1; j < ls-2; j++ { bs[j] = bs[j+2] } ls -= 2 + } else if bs[i] == 't' { + bs[i] = '\t' + } else if bs[i] == 'r' { + bs[i] = '\r' + } else if bs[i] == 'n' { + bs[i] = '\n' } escaped_dot = bs[i] == '.' bs_fresh = false @@ -398,17 +402,21 @@ Loop: return "", lenmsg, ErrBuf } for j := off; j < off+c; j++ { - switch { - case msg[j] == '.': // literal dots - s = append(s, '\\', '.') - case msg[j] < 32: // unprintable use \DDD + switch b := msg[j]; b { + case '.', '(', ')', ';', ' ': fallthrough - case msg[j] >= 127: - for _, b := range fmt.Sprintf("\\%03d", msg[j]) { - s = append(s, byte(b)) - } + case '"', '\\': + s = append(s, '\\', b) + case '\t': + s = append(s, '\\', 't') + case '\r': + s = append(s, '\\', 'r') default: - s = append(s, msg[j]) + if b < 32 || b >= 127 { // unprintable use \DDD + s = append(s, fmt.Sprintf("\\%03d", b)...) + } else { + s = append(s, b) + } } } s = append(s, '.') @@ -442,9 +450,115 @@ Loop: return string(s), off1, nil } +func packTXT(txt []string, msg []byte, offset int, tmp []byte) (int, error) { + var err error + if len(txt) == 0 { + if offset >= len(msg) { + return offset, ErrBuf + } + msg[offset] = 0 + return offset, nil + } + for i := range txt { + if len(txt[i]) > len(tmp) { + return offset, ErrBuf + } + offset, err = packTXTString(txt[i], msg, offset, tmp) + if err != nil { + return offset, err + } + } + return offset, err +} + +func packTXTString(s string, msg []byte, offset int, tmp []byte) (int, error) { + lenByteOffset := offset + if offset >= len(msg) { + return offset, ErrBuf + } + offset++ + bs := tmp[:len(s)] + copy(bs, s) + for i := 0; i < len(bs); i++ { + if len(msg) <= offset { + return offset, ErrBuf + } + if bs[i] == '\\' { + i++ + if i == len(bs) { + break + } + // check for \DDD + if i+2 < len(bs) && isDigit(bs[i]) && isDigit(bs[i+1]) && isDigit(bs[i+2]) { + msg[offset] = dddToByte(bs[i:]) + i += 2 + } else if bs[i] == 't' { + msg[offset] = '\t' + } else if bs[i] == 'r' { + msg[offset] = '\r' + } else if bs[i] == 'n' { + msg[offset] = '\n' + } else { + msg[offset] = bs[i] + } + } else { + msg[offset] = bs[i] + } + offset++ + } + l := offset - lenByteOffset - 1 + if l > 255 { + return offset, &Error{err: "TXT string exceeded 255 bytes"} + } + msg[lenByteOffset] = byte(l) + return offset, nil +} + +func unpackTXT(msg []byte, offset, rdend int) ([]string, int, error) { + var err error + var ss []string + var s string + for offset < rdend && err == nil { + s, offset, err = unpackTXTString(msg, offset) + if err == nil { + ss = append(ss, s) + } + } + return ss, offset, err +} + +func unpackTXTString(msg []byte, offset int) (string, int, error) { + l := int(msg[offset]) + if offset+l+1 > len(msg) { + return "", offset, &Error{err: "TXT string truncated"} + } + s := make([]byte, 0, l) + for _, b := range msg[offset+1 : offset+1+l] { + switch b { + case '"', '\\': + s = append(s, '\\', b) + case '\t': + s = append(s, `\t`...) + case '\r': + s = append(s, `\r`...) + case '\n': + s = append(s, `\n`...) + default: + if b < 32 || b > 127 { // unprintable + s = append(s, fmt.Sprintf("\\%03d", b)...) + } else { + s = append(s, b) + } + } + } + offset += 1 + l + return string(s), offset, nil +} + // Pack a reflect.StructValue into msg. Struct members can only be uint8, uint16, uint32, string, // slices and other (often anonymous) structs. func packStructValue(val reflect.Value, msg []byte, off int, compression map[string]int, compress bool) (off1 int, err error) { + var txtTmp []byte lenmsg := len(msg) numfield := val.NumField() for i := 0; i < numfield; i++ { @@ -468,18 +582,12 @@ func packStructValue(val reflect.Value, msg []byte, off int, compression map[str } } case `dns:"txt"`: - for j := 0; j < val.Field(i).Len(); j++ { - element := val.Field(i).Index(j).String() - // Counted string: 1 byte length. - if len(element) > 255 || off+1+len(element) > lenmsg { - return lenmsg, &Error{err: "overflow packing txt"} - } - msg[off] = byte(len(element)) - off++ - for i := 0; i < len(element); i++ { - msg[off+i] = element[i] - } - off += len(element) + if txtTmp == nil { + txtTmp = make([]byte, 256*4+1) + } + off, err = packTXT(fv.Interface().([]string), msg, off, txtTmp) + if err != nil { + return lenmsg, err } case `dns:"opt"`: // edns for j := 0; j < val.Field(i).Len(); j++ { @@ -712,16 +820,13 @@ func packStructValue(val reflect.Value, msg []byte, off int, compression map[str case `dns:"txt"`: fallthrough case "": - // Counted string: 1 byte length. - if len(s) > 255 || off+1+len(s) > lenmsg { - return lenmsg, &Error{err: "overflow packing string"} + if txtTmp == nil { + txtTmp = make([]byte, 256*4+1) } - msg[off] = byte(len(s)) - off++ - for i := 0; i < len(s); i++ { - msg[off+i] = s[i] + off, err = packTXTString(fv.String(), msg, off, txtTmp) + if err != nil { + return lenmsg, err } - off += len(s) } } } @@ -771,20 +876,13 @@ func unpackStructValue(val reflect.Value, msg []byte, off int) (off1 int, err er } fv.Set(reflect.ValueOf(servers)) case `dns:"txt"`: - txt := make([]string, 0) - Txts: - if off == lenmsg || rdend == off { // dyn. updates, no rdata is OK + if off == lenmsg || rdend == off { break } - l := int(msg[off]) - if off+l+1 > lenmsg { // TODO(miek): +1 or ... not ... - return lenmsg, &Error{err: "overflow unpacking txt"} - } - txt = append(txt, string(msg[off+1:off+l+1])) - off += l + 1 - if off < rdend { - // More - goto Txts + var txt []string + txt, off, err = unpackTXT(msg, off, rdend) + if err != nil { + return lenmsg, err } fv.Set(reflect.ValueOf(txt)) case `dns:"opt"`: // edns0 @@ -1118,30 +1216,9 @@ func unpackStructValue(val reflect.Value, msg []byte, off int) (off1 int, err er s = hex.EncodeToString(msg[off : off+size]) off += size case `dns:"txt"`: - Txt: - if off >= lenmsg || off+1+int(msg[off]) > rdend { - return lenmsg, &Error{err: "overflow unpacking txt"} - } - n := int(msg[off]) - off++ - for i := 0; i < n; i++ { - s += string(msg[off+i]) - } - off += n - if off < rdend { - // More to - goto Txt - } + fallthrough case "": - if off >= lenmsg || off+1+int(msg[off]) > lenmsg { - return lenmsg, &Error{err: "overflow unpacking string"} - } - n := int(msg[off]) - off++ - for i := 0; i < n; i++ { - s += string(msg[off+i]) - } - off += n + s, off, err = unpackTXTString(msg, off) } fv.SetString(s) } @@ -1149,6 +1226,13 @@ func unpackStructValue(val reflect.Value, msg []byte, off int) (off1 int, err er return off, nil } +// Helpers for dealing with escaped bytes +func isDigit(b byte) bool { return b >= '0' && b <= '9' } + +func dddToByte(s []byte) byte { + return byte((s[0]-'0')*100 + (s[1]-'0')*10 + (s[2] - '0')) +} + // Helper function for unpacking func unpackUint16(msg []byte, off int) (v uint16, off1 int) { v = uint16(msg[off])<<8 | uint16(msg[off+1]) @@ -1527,7 +1611,6 @@ func (dns *Msg) Len() int { if dns.Compress { compression = make(map[string]int) } - for i := 0; i < len(dns.Question); i++ { l += dns.Question[i].len() if dns.Compress { diff --git a/types.go b/types.go index 6d3aa26a..6c8110f9 100644 --- a/types.go +++ b/types.go @@ -174,11 +174,7 @@ type Question struct { func (q *Question) String() (s string) { // prefix with ; (as in dig) - if len(q.Name) == 0 { - s = ";.\t" // root label - } else { - s = ";" + q.Name + "\t" - } + s = ";" + sprintDomain(q.Name) + "\t" s += Class(q.Qclass).String() + "\t" s += " " + Type(q.Qtype).String() return s @@ -205,7 +201,7 @@ type CNAME struct { } func (rr *CNAME) Header() *RR_Header { return &rr.Hdr } -func (rr *CNAME) copy() RR { return &CNAME{*rr.Hdr.copyHeader(), rr.Target} } +func (rr *CNAME) copy() RR { return &CNAME{*rr.Hdr.copyHeader(), sprintDomain(rr.Target)} } func (rr *CNAME) String() string { return rr.Hdr.String() + rr.Target } func (rr *CNAME) len() int { return rr.Hdr.len() + len(rr.Target) + 1 } @@ -226,7 +222,7 @@ type MB struct { } func (rr *MB) Header() *RR_Header { return &rr.Hdr } -func (rr *MB) copy() RR { return &MB{*rr.Hdr.copyHeader(), rr.Mb} } +func (rr *MB) copy() RR { return &MB{*rr.Hdr.copyHeader(), sprintDomain(rr.Mb)} } func (rr *MB) String() string { return rr.Hdr.String() + rr.Mb } func (rr *MB) len() int { return rr.Hdr.len() + len(rr.Mb) + 1 } @@ -239,7 +235,7 @@ type MG struct { func (rr *MG) Header() *RR_Header { return &rr.Hdr } func (rr *MG) copy() RR { return &MG{*rr.Hdr.copyHeader(), rr.Mg} } func (rr *MG) len() int { l := len(rr.Mg) + 1; return rr.Hdr.len() + l } -func (rr *MG) String() string { return rr.Hdr.String() + rr.Mg } +func (rr *MG) String() string { return rr.Hdr.String() + sprintDomain(rr.Mg) } type MINFO struct { Hdr RR_Header @@ -251,7 +247,7 @@ func (rr *MINFO) Header() *RR_Header { return &rr.Hdr } func (rr *MINFO) copy() RR { return &MINFO{*rr.Hdr.copyHeader(), rr.Rmail, rr.Email} } func (rr *MINFO) String() string { - return rr.Hdr.String() + rr.Rmail + " " + rr.Email + return rr.Hdr.String() + sprintDomain(rr.Rmail) + " " + sprintDomain(rr.Email) } func (rr *MINFO) len() int { @@ -270,7 +266,7 @@ func (rr *MR) copy() RR { return &MR{*rr.Hdr.copyHeader(), rr.Mr} } func (rr *MR) len() int { l := len(rr.Mr) + 1; return rr.Hdr.len() + l } func (rr *MR) String() string { - return rr.Hdr.String() + rr.Mr + return rr.Hdr.String() + sprintDomain(rr.Mr) } type MF struct { @@ -283,7 +279,7 @@ func (rr *MF) copy() RR { return &MF{*rr.Hdr.copyHeader(), rr.Mf} } func (rr *MF) len() int { return rr.Hdr.len() + len(rr.Mf) + 1 } func (rr *MF) String() string { - return rr.Hdr.String() + " " + rr.Mf + return rr.Hdr.String() + " " + sprintDomain(rr.Mf) } type MD struct { @@ -296,7 +292,7 @@ func (rr *MD) copy() RR { return &MD{*rr.Hdr.copyHeader(), rr.Md} } func (rr *MD) len() int { return rr.Hdr.len() + len(rr.Md) + 1 } func (rr *MD) String() string { - return rr.Hdr.String() + " " + rr.Md + return rr.Hdr.String() + " " + sprintDomain(rr.Md) } type MX struct { @@ -310,7 +306,7 @@ func (rr *MX) copy() RR { return &MX{*rr.Hdr.copyHeader(), rr.Preferen func (rr *MX) len() int { l := len(rr.Mx) + 1; return rr.Hdr.len() + l + 2 } func (rr *MX) String() string { - return rr.Hdr.String() + strconv.Itoa(int(rr.Preference)) + " " + rr.Mx + return rr.Hdr.String() + strconv.Itoa(int(rr.Preference)) + " " + sprintDomain(rr.Mx) } type AFSDB struct { @@ -324,7 +320,7 @@ func (rr *AFSDB) copy() RR { return &AFSDB{*rr.Hdr.copyHeader(), rr.Su func (rr *AFSDB) len() int { l := len(rr.Hostname) + 1; return rr.Hdr.len() + l + 2 } func (rr *AFSDB) String() string { - return rr.Hdr.String() + strconv.Itoa(int(rr.Subtype)) + " " + rr.Hostname + return rr.Hdr.String() + strconv.Itoa(int(rr.Subtype)) + " " + sprintDomain(rr.Hostname) } type X25 struct { @@ -351,7 +347,7 @@ func (rr *RT) copy() RR { return &RT{*rr.Hdr.copyHeader(), rr.Preferen func (rr *RT) len() int { l := len(rr.Host) + 1; return rr.Hdr.len() + l + 2 } func (rr *RT) String() string { - return rr.Hdr.String() + strconv.Itoa(int(rr.Preference)) + " " + rr.Host + return rr.Hdr.String() + strconv.Itoa(int(rr.Preference)) + " " + sprintDomain(rr.Host) } type NS struct { @@ -364,7 +360,7 @@ func (rr *NS) len() int { l := len(rr.Ns) + 1; return rr.Hdr.len() + l func (rr *NS) copy() RR { return &NS{*rr.Hdr.copyHeader(), rr.Ns} } func (rr *NS) String() string { - return rr.Hdr.String() + rr.Ns + return rr.Hdr.String() + sprintDomain(rr.Ns) } type PTR struct { @@ -377,7 +373,7 @@ func (rr *PTR) copy() RR { return &PTR{*rr.Hdr.copyHeader(), rr.Ptr} } func (rr *PTR) len() int { l := len(rr.Ptr) + 1; return rr.Hdr.len() + l } func (rr *PTR) String() string { - return rr.Hdr.String() + rr.Ptr + return rr.Hdr.String() + sprintDomain(rr.Ptr) } type RP struct { @@ -391,7 +387,7 @@ func (rr *RP) copy() RR { return &RP{*rr.Hdr.copyHeader(), rr.Mbox, rr func (rr *RP) len() int { return rr.Hdr.len() + len(rr.Mbox) + 1 + len(rr.Txt) + 1 } func (rr *RP) String() string { - return rr.Hdr.String() + rr.Mbox + " " + rr.Txt + return rr.Hdr.String() + rr.Mbox + " " + sprintTxt([]string{rr.Txt}) } type SOA struct { @@ -411,7 +407,7 @@ func (rr *SOA) copy() RR { } func (rr *SOA) String() string { - return rr.Hdr.String() + rr.Ns + " " + rr.Mbox + + return rr.Hdr.String() + sprintDomain(rr.Ns) + " " + sprintDomain(rr.Mbox) + " " + strconv.FormatInt(int64(rr.Serial), 10) + " " + strconv.FormatInt(int64(rr.Refresh), 10) + " " + strconv.FormatInt(int64(rr.Retry), 10) + @@ -437,16 +433,109 @@ func (rr *TXT) copy() RR { return &TXT{*rr.Hdr.copyHeader(), cp} } -func (rr *TXT) String() string { - s := rr.Hdr.String() - for i, s1 := range rr.Txt { - if i > 0 { - s += " " + strconv.QuoteToASCII(s1) +func (rr *TXT) String() string { return rr.Hdr.String() + sprintTxt(rr.Txt) } + +func sprintDomain(s string) string { + src := []byte(s) + dst := make([]byte, 0, len(src)) + for i := 0; i < len(src); { + if i+1 < len(src) && src[i] == '\\' && src[i+1] == '.' { + dst = append(dst, src[i:i+2]...) + i += 2 } else { - s += strconv.QuoteToASCII(s1) + b, n := nextByte(src, i) + if n == 0 { + i++ // dangling back slash + } else if b == '.' { + dst = append(dst, b) + } else { + dst = appendDomainNameByte(dst, b) + } + i += n } } - return s + return string(dst) +} + +func sprintTxt(txt []string) string { + const maxLen = 256*4 + 3 + var out []byte + srcTmp := make([]byte, maxLen) + dstTmp := make([]byte, maxLen) + for i, s := range txt { + src := srcTmp[0:len(s)] + copy(src, s) + dst := dstTmp[0:0:maxLen] + if i > 0 { + dst = append(dst, ' ') + } + dst = append(dst, '"') + for j := 0; j < len(s); { + b, n := nextByte(src, j) + if n == 0 { + break + } + dst = appendTXTStringByte(dst, b) + j += n + } + dst = append(dst, '"') + out = append(out, dst...) + } + return string(out) +} + +func appendDomainNameByte(s []byte, b byte) []byte { + if b == '.' || b == '(' || b == ')' || b == ';' || b == ' ' || b == '\'' { + return append(s, '\\', b) + } + return appendTXTStringByte(s, b) +} + +func appendTXTStringByte(s []byte, b byte) []byte { + if b == '"' { + return append(s, `\"`...) + } else if b == '\\' { + return append(s, `\\`...) + } else if b == '\t' { + return append(s, `\t`...) + } else if b == '\r' { + return append(s, `\r`...) + } else if b == '\n' { + return append(s, `\n`...) + } else if b < ' ' || b > '~' { + return append(s, fmt.Sprintf("\\%03d", b)...) + } + return append(s, b) +} + +func nextByte(b []byte, offset int) (byte, int) { + if offset > len(b) { + return 0, 0 + } + if b[offset] != '\\' { + // not an escape sequence + return b[offset], 1 + } + switch len(b) - offset { + case 1: // dangling escape + return 0, 0 + case 2, 3: // too short to be \ddd + default: // maybe \ddd + if isDigit(b[offset+1]) && isDigit(b[offset+2]) && isDigit(b[offset+3]) { + return dddToByte(b[offset+1:]), 4 + } + } + // not \ddd, maybe a control char + switch b[offset+1] { + case 't': + return '\t', 2 + case 'r': + return '\r', 2 + case 'n': + return '\n', 2 + default: + return b[offset+1], 2 + } } func (rr *TXT) len() int { @@ -469,17 +558,7 @@ func (rr *SPF) copy() RR { return &SPF{*rr.Hdr.copyHeader(), cp} } -func (rr *SPF) String() string { - s := rr.Hdr.String() - for i, s1 := range rr.Txt { - if i > 0 { - s += " " + strconv.QuoteToASCII(s1) - } else { - s += strconv.QuoteToASCII(s1) - } - } - return s -} +func (rr *SPF) String() string { return rr.Hdr.String() + sprintTxt(rr.Txt) } func (rr *SPF) len() int { l := rr.Hdr.len() @@ -507,7 +586,7 @@ func (rr *SRV) String() string { return rr.Hdr.String() + strconv.Itoa(int(rr.Priority)) + " " + strconv.Itoa(int(rr.Weight)) + " " + - strconv.Itoa(int(rr.Port)) + " " + rr.Target + strconv.Itoa(int(rr.Port)) + " " + sprintDomain(rr.Target) } type NAPTR struct { @@ -577,7 +656,7 @@ func (rr *DNAME) copy() RR { return &DNAME{*rr.Hdr.copyHeader(), rr.Ta func (rr *DNAME) len() int { l := len(rr.Target) + 1; return rr.Hdr.len() + l } func (rr *DNAME) String() string { - return rr.Hdr.String() + rr.Target + return rr.Hdr.String() + sprintDomain(rr.Target) } type A struct { @@ -622,7 +701,7 @@ type PX struct { func (rr *PX) Header() *RR_Header { return &rr.Hdr } func (rr *PX) copy() RR { return &PX{*rr.Hdr.copyHeader(), rr.Preference, rr.Map822, rr.Mapx400} } func (rr *PX) String() string { - return rr.Hdr.String() + strconv.Itoa(int(rr.Preference)) + " " + rr.Map822 + " " + rr.Mapx400 + return rr.Hdr.String() + strconv.Itoa(int(rr.Preference)) + " " + sprintDomain(rr.Map822) + " " + sprintDomain(rr.Mapx400) } func (rr *PX) len() int { return rr.Hdr.len() + 2 + len(rr.Map822) + 1 + len(rr.Mapx400) + 1 } @@ -731,7 +810,7 @@ func (rr *RRSIG) String() string { " " + TimeToString(rr.Expiration) + " " + TimeToString(rr.Inception) + " " + strconv.Itoa(int(rr.KeyTag)) + - " " + rr.SignerName + + " " + sprintDomain(rr.SignerName) + " " + rr.Signature return s } @@ -755,7 +834,7 @@ func (rr *NSEC) copy() RR { } func (rr *NSEC) String() string { - s := rr.Hdr.String() + rr.NextDomain + s := rr.Hdr.String() + sprintDomain(rr.NextDomain) for i := 0; i < len(rr.TypeBitMap); i++ { s += " " + Type(rr.TypeBitMap[i]).String() } @@ -850,7 +929,7 @@ func (rr *KX) copy() RR { return &KX{*rr.Hdr.copyHeader(), rr.Preferen func (rr *KX) String() string { return rr.Hdr.String() + strconv.Itoa(int(rr.Preference)) + - " " + rr.Exchanger + " " + sprintDomain(rr.Exchanger) } type TA struct { @@ -886,7 +965,7 @@ func (rr *TALINK) len() int { return rr.Hdr.len() + len(rr.PreviousNam func (rr *TALINK) String() string { return rr.Hdr.String() + - " " + rr.PreviousName + " " + rr.NextName + " " + sprintDomain(rr.PreviousName) + " " + sprintDomain(rr.NextName) } type SSHFP struct { @@ -997,7 +1076,7 @@ type NSAPPTR struct { func (rr *NSAPPTR) Header() *RR_Header { return &rr.Hdr } func (rr *NSAPPTR) copy() RR { return &NSAPPTR{*rr.Hdr.copyHeader(), rr.Ptr} } -func (rr *NSAPPTR) String() string { return rr.Hdr.String() + rr.Ptr } +func (rr *NSAPPTR) String() string { return rr.Hdr.String() + sprintDomain(rr.Ptr) } func (rr *NSAPPTR) len() int { return rr.Hdr.len() + len(rr.Ptr) } type NSEC3 struct { @@ -1128,16 +1207,8 @@ func (rr *URI) copy() RR { } func (rr *URI) String() string { - s := rr.Hdr.String() + strconv.Itoa(int(rr.Priority)) + - " " + strconv.Itoa(int(rr.Weight)) - for i, s1 := range rr.Target { - if i > 0 { - s += " " + strconv.QuoteToASCII(s1) - } else { - s += strconv.QuoteToASCII(s1) - } - } - return s + return rr.Hdr.String() + strconv.Itoa(int(rr.Priority)) + + " " + strconv.Itoa(int(rr.Weight)) + sprintTxt(rr.Target) } func (rr *URI) len() int { @@ -1204,7 +1275,7 @@ func (rr *HIP) String() string { " " + rr.Hit + " " + rr.PublicKey for _, d := range rr.RendezvousServers { - s += " " + d + s += " " + sprintDomain(d) } return s } @@ -1231,17 +1302,7 @@ func (rr *NINFO) copy() RR { return &NINFO{*rr.Hdr.copyHeader(), cp} } -func (rr *NINFO) String() string { - s := rr.Hdr.String() - for i, s1 := range rr.ZSData { - if i > 0 { - s += " " + strconv.QuoteToASCII(s1) - } else { - s += strconv.QuoteToASCII(s1) - } - } - return s -} +func (rr *NINFO) String() string { return rr.Hdr.String() + sprintTxt(rr.ZSData) } func (rr *NINFO) len() int { l := rr.Hdr.len() @@ -1342,7 +1403,7 @@ func (rr *LP) copy() RR { return &LP{*rr.Hdr.copyHeader(), rr.Preferen func (rr *LP) len() int { return rr.Hdr.len() + 2 + len(rr.Fqdn) + 1 } func (rr *LP) String() string { - return rr.Hdr.String() + strconv.Itoa(int(rr.Preference)) + " " + rr.Fqdn + return rr.Hdr.String() + strconv.Itoa(int(rr.Preference)) + " " + sprintDomain(rr.Fqdn) } type EUI48 struct { @@ -1412,7 +1473,7 @@ type UINFO struct { func (rr *UINFO) Header() *RR_Header { return &rr.Hdr } func (rr *UINFO) copy() RR { return &UINFO{*rr.Hdr.copyHeader(), rr.Uinfo} } -func (rr *UINFO) String() string { return rr.Hdr.String() + strconv.QuoteToASCII(rr.Uinfo) } +func (rr *UINFO) String() string { return rr.Hdr.String() + sprintTxt([]string{rr.Uinfo}) } func (rr *UINFO) len() int { return rr.Hdr.len() + len(rr.Uinfo) + 1 } type EID struct { From 0c98da613d7abfb7b5e6c05fc975f7fcb1234b2b Mon Sep 17 00:00:00 2001 From: Andrew Tunnell-Jones Date: Sat, 1 Mar 2014 02:37:10 +0000 Subject: [PATCH 03/10] Fix bounds check in nextByte --- types.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types.go b/types.go index 6c8110f9..47c5e750 100644 --- a/types.go +++ b/types.go @@ -509,7 +509,7 @@ func appendTXTStringByte(s []byte, b byte) []byte { } func nextByte(b []byte, offset int) (byte, int) { - if offset > len(b) { + if offset >= len(b) { return 0, 0 } if b[offset] != '\\' { From 6fd4d29ced4d0a45434ed40f615df03cfcd94799 Mon Sep 17 00:00:00 2001 From: Andrew Tunnell-Jones Date: Sat, 1 Mar 2014 02:42:47 +0000 Subject: [PATCH 04/10] Fix bounds overflow in sprintTxt Passing excessively long strings (>256*4+3) to sprintTxt would result in overflowing src. Fix isn't as efficient but simplifies the code. --- types.go | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/types.go b/types.go index 47c5e750..19058c15 100644 --- a/types.go +++ b/types.go @@ -458,28 +458,23 @@ func sprintDomain(s string) string { } func sprintTxt(txt []string) string { - const maxLen = 256*4 + 3 var out []byte - srcTmp := make([]byte, maxLen) - dstTmp := make([]byte, maxLen) for i, s := range txt { - src := srcTmp[0:len(s)] - copy(src, s) - dst := dstTmp[0:0:maxLen] if i > 0 { - dst = append(dst, ' ') + out = append(out, ` "`...) + } else { + out = append(out, '"') } - dst = append(dst, '"') - for j := 0; j < len(s); { - b, n := nextByte(src, j) + bs := []byte(s) + for j := 0; j < len(bs); { + b, n := nextByte(bs, j) if n == 0 { break } - dst = appendTXTStringByte(dst, b) + out = appendTXTStringByte(out, b) j += n } - dst = append(dst, '"') - out = append(out, dst...) + out = append(out, '"') } return string(out) } From 3ba746b6ca56f1f889187551dc19c02e1da4e86c Mon Sep 17 00:00:00 2001 From: Andrew Tunnell-Jones Date: Sat, 1 Mar 2014 22:25:24 +0000 Subject: [PATCH 05/10] Convention is Txt not TXT in msg.go function names --- msg.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/msg.go b/msg.go index cf105f8d..20877e8e 100644 --- a/msg.go +++ b/msg.go @@ -450,7 +450,7 @@ Loop: return string(s), off1, nil } -func packTXT(txt []string, msg []byte, offset int, tmp []byte) (int, error) { +func packTxt(txt []string, msg []byte, offset int, tmp []byte) (int, error) { var err error if len(txt) == 0 { if offset >= len(msg) { @@ -463,7 +463,7 @@ func packTXT(txt []string, msg []byte, offset int, tmp []byte) (int, error) { if len(txt[i]) > len(tmp) { return offset, ErrBuf } - offset, err = packTXTString(txt[i], msg, offset, tmp) + offset, err = packTxtString(txt[i], msg, offset, tmp) if err != nil { return offset, err } @@ -471,7 +471,7 @@ func packTXT(txt []string, msg []byte, offset int, tmp []byte) (int, error) { return offset, err } -func packTXTString(s string, msg []byte, offset int, tmp []byte) (int, error) { +func packTxtString(s string, msg []byte, offset int, tmp []byte) (int, error) { lenByteOffset := offset if offset >= len(msg) { return offset, ErrBuf @@ -514,12 +514,12 @@ func packTXTString(s string, msg []byte, offset int, tmp []byte) (int, error) { return offset, nil } -func unpackTXT(msg []byte, offset, rdend int) ([]string, int, error) { +func unpackTxt(msg []byte, offset, rdend int) ([]string, int, error) { var err error var ss []string var s string for offset < rdend && err == nil { - s, offset, err = unpackTXTString(msg, offset) + s, offset, err = unpackTxtString(msg, offset) if err == nil { ss = append(ss, s) } @@ -527,7 +527,7 @@ func unpackTXT(msg []byte, offset, rdend int) ([]string, int, error) { return ss, offset, err } -func unpackTXTString(msg []byte, offset int) (string, int, error) { +func unpackTxtString(msg []byte, offset int) (string, int, error) { l := int(msg[offset]) if offset+l+1 > len(msg) { return "", offset, &Error{err: "TXT string truncated"} @@ -585,7 +585,7 @@ func packStructValue(val reflect.Value, msg []byte, off int, compression map[str if txtTmp == nil { txtTmp = make([]byte, 256*4+1) } - off, err = packTXT(fv.Interface().([]string), msg, off, txtTmp) + off, err = packTxt(fv.Interface().([]string), msg, off, txtTmp) if err != nil { return lenmsg, err } @@ -823,7 +823,7 @@ func packStructValue(val reflect.Value, msg []byte, off int, compression map[str if txtTmp == nil { txtTmp = make([]byte, 256*4+1) } - off, err = packTXTString(fv.String(), msg, off, txtTmp) + off, err = packTxtString(fv.String(), msg, off, txtTmp) if err != nil { return lenmsg, err } @@ -880,7 +880,7 @@ func unpackStructValue(val reflect.Value, msg []byte, off int) (off1 int, err er break } var txt []string - txt, off, err = unpackTXT(msg, off, rdend) + txt, off, err = unpackTxt(msg, off, rdend) if err != nil { return lenmsg, err } @@ -1218,7 +1218,7 @@ func unpackStructValue(val reflect.Value, msg []byte, off int) (off1 int, err er case `dns:"txt"`: fallthrough case "": - s, off, err = unpackTXTString(msg, off) + s, off, err = unpackTxtString(msg, off) } fv.SetString(s) } From 38d78bafe419ff4fe38b256526b07d45d463faa2 Mon Sep 17 00:00:00 2001 From: Andrew Tunnell-Jones Date: Sat, 1 Mar 2014 22:30:52 +0000 Subject: [PATCH 06/10] Escape @ when printing/unpacking domain names --- msg.go | 2 +- types.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/msg.go b/msg.go index 20877e8e..5e9e1bc8 100644 --- a/msg.go +++ b/msg.go @@ -403,7 +403,7 @@ Loop: } for j := off; j < off+c; j++ { switch b := msg[j]; b { - case '.', '(', ')', ';', ' ': + case '.', '(', ')', ';', ' ', '@': fallthrough case '"', '\\': s = append(s, '\\', b) diff --git a/types.go b/types.go index 19058c15..2b6ce8a4 100644 --- a/types.go +++ b/types.go @@ -480,7 +480,7 @@ func sprintTxt(txt []string) string { } func appendDomainNameByte(s []byte, b byte) []byte { - if b == '.' || b == '(' || b == ')' || b == ';' || b == ' ' || b == '\'' { + if b == '.' || b == '(' || b == ')' || b == ';' || b == ' ' || b == '\'' || b == '@' { return append(s, '\\', b) } return appendTXTStringByte(s, b) From 0a5cb5c80aaedfc10cddfba1ab7f331f9d443959 Mon Sep 17 00:00:00 2001 From: Andrew Tunnell-Jones Date: Sun, 2 Mar 2014 09:48:46 +0000 Subject: [PATCH 07/10] Update TSIG doc header to avoid godoc oddity For some reason godoc treats it as plain text because it contains "(TSIG)". --- tsig.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsig.go b/tsig.go index f11ab67d..f6654591 100644 --- a/tsig.go +++ b/tsig.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// TRANSACTION SIGNATURE (TSIG) +// TRANSACTION SIGNATURE // // An TSIG or transaction signature adds a HMAC TSIG record to each message sent. // The supported algorithms include: HmacMD5, HmacSHA1 and HmacSHA256. From 22f3256df47129b190b9be2cd8ddee3ab12d651c Mon Sep 17 00:00:00 2001 From: Andrew Tunnell-Jones Date: Sun, 2 Mar 2014 10:15:26 +0000 Subject: [PATCH 08/10] Bump up MaxCountScale in TestTXTRRQuick Increases number of test iterations which should cover more corner-cases. --- parse_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/parse_test.go b/parse_test.go index 017d287d..1cad6948 100644 --- a/parse_test.go +++ b/parse_test.go @@ -251,7 +251,8 @@ func TestTXTRRQuick(t *testing.T) { } return true } - if err := quick.Check(f, nil); err != nil { + c := &quick.Config{MaxCountScale: 10} + if err := quick.Check(f, c); err != nil { t.Error(err) } } From 588a52762d76391632c679d281284792907a06d3 Mon Sep 17 00:00:00 2001 From: Andrew Tunnell-Jones Date: Sun, 2 Mar 2014 10:37:07 +0000 Subject: [PATCH 09/10] Test byte escaping in Domain Names and TXT strings TestTXTRRQuick may not always cover these bytes so explicitly test that they're covered. --- parse_test.go | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/parse_test.go b/parse_test.go index 1cad6948..c797b5af 100644 --- a/parse_test.go +++ b/parse_test.go @@ -95,6 +95,47 @@ func TestDomainName(t *testing.T) { } } +func TestDomainNameAndTXTEscapes(t *testing.T) { + tests := []byte{'.', '(', ')', ';', ' ', '@', '"', '\\', '\t', '\r', '\n', 0, 255} + for _, b := range tests { + rrbytes := []byte{ + 1, b, 0, // owner + byte(TypeTXT >> 8), byte(TypeTXT), + byte(ClassINET >> 8), byte(ClassINET), + 0, 0, 0, 1, // TTL + 0, 2, 1, b, // Data + } + rr1, _, err := UnpackRR(rrbytes, 0) + if err != nil { + panic(err) + } + s := rr1.String() + rr2, err := NewRR(s) + if err != nil { + t.Logf("Error parsing unpacked RR's string: %v", err) + t.Logf(" Bytes: %v\n", rrbytes) + t.Logf("String: %v\n", s) + t.Fail() + } + repacked := make([]byte, len(rrbytes)) + if _, err := PackRR(rr2, repacked, 0, nil, false); err != nil { + t.Logf("Error packing parsed RR: %v", err) + t.Logf(" Original Bytes: %v\n", rrbytes) + t.Logf("Unpacked Struct: %V\n", rr1) + t.Logf(" Parsed Struct: %V\n", rr2) + t.Fail() + } + if !bytes.Equal(repacked, rrbytes) { + t.Log("Packed bytes don't match original bytes") + t.Logf(" Original bytes: %v", rrbytes) + t.Logf(" Packed bytes: %v", repacked) + t.Logf("Unpacked struct: %V", rr1) + t.Logf(" Parsed struct: %V", rr2) + t.Fail() + } + } +} + func GenerateDomain(r *rand.Rand, size int) []byte { dnLen := size % 70 // artificially limit size so there's less to intrepret if a failure occurs var dn []byte From bd189318ed161b05f601c07262aca5d9cbcb6294 Mon Sep 17 00:00:00 2001 From: Andrew Tunnell-Jones Date: Sun, 2 Mar 2014 10:38:46 +0000 Subject: [PATCH 10/10] Document domain name and TXT escaping behaviour --- dns.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/dns.go b/dns.go index 88d6f9cb..d1aadae6 100644 --- a/dns.go +++ b/dns.go @@ -83,6 +83,20 @@ // if t, ok := in.Answer[0].(*dns.TXT); ok { // // do something with t.Txt // } +// +// Domain Name and TXT Character String Representations +// +// Both domain names and TXT character strings are converted to presentation +// form both when unpacked and when converted to strings. +// +// For TXT character strings, tabs, carriage returns and line feeds will be +// converted to \t, \r and \n respectively. Back slashes and quotations marks +// will be escaped. Bytes below 32 and above 127 will be converted to \DDD +// form. +// +// For domain names, in addition to the above rules brackets, periods, +// spaces, semicolons and the at symbol are escaped. +// package dns import (