a go dns packet parser

feature: add parsing of edns records according to rfc2671

+4 -1
README.md
···
this is a go package for packing/unpacking dns packets.
## Spec
[x] 103{4,5} - DNS standard
···
[ ] 2065 - DNSSEC (updated in later RFCs)
-
[ ] 2671 - EDNS record
[ ] 2782 - SRV record
···
this is a go package for packing/unpacking dns packets.
+
> which we expect to be so popular that it would be a waste of wire space
+
- rfc2671 4.2
+
## Spec
[x] 103{4,5} - DNS standard
···
[ ] 2065 - DNSSEC (updated in later RFCs)
+
[x] 2671 - EDNS record
[ ] 2782 - SRV record
+26
message.go
···
// Decode decodes a DNS packet.
func (m *Message) Decode(buf []byte) (err error) {
offset, err := m.Header.Decode(buf, 0)
if err != nil {
return fmt.Errorf("failed to decode message header: %w", err)
···
return fmt.Errorf("failed to decode additional record #%d: %w", i+1, err)
}
m.Additional = append(m.Additional, rr)
}
return nil
···
// Decode decodes a DNS packet.
func (m *Message) Decode(buf []byte) (err error) {
+
// gets checked when parsing additional records
+
m.HasEDNS = false
+
offset, err := m.Header.Decode(buf, 0)
if err != nil {
return fmt.Errorf("failed to decode message header: %w", err)
···
return fmt.Errorf("failed to decode additional record #%d: %w", i+1, err)
}
+
if rr.RType == OPTType {
+
opt, ok := rr.RData.(*OPT)
+
if !ok {
+
// this should never fail
+
return fmt.Errorf("unable to parse RData as OPT")
+
}
+
+
m.HasEDNS = true
+
m.ExtendedRCode = uint8(rr.TTL >> 24 & 0xFF000000)
+
m.EDNSVersion = uint8((rr.TTL & 0x00FF0000) >> 16)
+
m.EDNSFlags = uint16(rr.TTL & 0x0000FFFF)
+
m.EDNSOptions = opt.Options
+
m.UDPSize = uint16(rr.RClass)
+
}
+
m.Additional = append(m.Additional, rr)
+
}
+
+
if !m.HasEDNS {
+
m.ExtendedRCode = 0
+
m.EDNSVersion = 0
+
m.EDNSFlags = 0
+
m.EDNSOptions = make([]EDNSOption, 0)
+
m.UDPSize = 512 // default in rfc-1035 section 2.3.4.
}
return nil
+46
message_test.go
···
wantErrType: &BufferOverflowError{},
wantErrMsg: "failed to decode answer record #1:",
},
}
for _, tt := range tests {
···
assert.Equal(t, tt.expected.Answer, m.Answer, "Answer section mismatch")
assert.Equal(t, tt.expected.Authority, m.Authority, "Authority section mismatch")
assert.Equal(t, tt.expected.Additional, m.Additional, "Additional section mismatch")
}
})
}
···
wantErrType: &BufferOverflowError{},
wantErrMsg: "failed to decode answer record #1:",
},
+
{
+
name: "EDNS Record",
+
input: []byte{0xea, 0x7c, 0x1, 0x20, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x6, 0x6c, 0x6f, 0x62, 0x73, 0x74, 0x65, 0x2, 0x72, 0x73, 0x0, 0x0, 0x1, 0x0, 0x1, 0x0, 0x0, 0x29, 0x4, 0xd0, 0x0, 0x64, 0x0, 0x0, 0x0, 0x8, 0x0, 0x64, 0x0, 0x4, 0x66, 0x6f, 0x6f, 0xa},
+
wantErr: false,
+
expected: Message{
+
Header: Header{
+
ID: 0xea7c,
+
QR: false,
+
OPCode: 0,
+
RD: true,
+
RCode: 0,
+
QDCount: 1,
+
ARCount: 1,
+
},
+
Question: []Question{
+
{
+
QName: "lobste.rs",
+
QType: AType,
+
QClass: IN,
+
},
+
},
+
Answer: []ResourceRecord{},
+
Additional: []ResourceRecord{
+
{
+
Name: "",
+
RType: OPTType,
+
RClass: 1232,
+
TTL: 6553600,
+
RDLength: 8,
+
RData: &OPT{
+
[]EDNSOption{
+
{
+
Code: uint16(100),
+
Data: []byte("foo\n"),
+
},
+
},
+
},
+
},
+
},
+
Authority: []ResourceRecord{},
+
},
+
},
}
for _, tt := range tests {
···
assert.Equal(t, tt.expected.Answer, m.Answer, "Answer section mismatch")
assert.Equal(t, tt.expected.Authority, m.Authority, "Authority section mismatch")
assert.Equal(t, tt.expected.Additional, m.Additional, "Additional section mismatch")
+
+
b, err := m.Encode()
+
assert.NoError(t, err, "Expected no error on round trip")
+
assert.Equal(t, tt.input, b, "Expected equal inputs on round trip")
}
})
}
+78 -5
resource_record.go
···
return len(buf), fmt.Errorf("x25 record: string segment length %d exceeds RDLENGTH boundary %d", strLen, endOffset)
}
-
strBytes, return_offset, err := getSlice(buf, nextOffsetAfterLen, int(strLen))
if err != nil {
return len(buf), fmt.Errorf("x25 record: failed to read string data (length %d): %w", strLen, err)
}
···
}
}
-
return return_offset, nil
}
func (x *X25) Encode(bytes []byte, offsets *map[string]uint16) ([]byte, error) {
···
return fmt.Sprintf("%d %s", rt.Preference, rt.IntermediateHost)
}
func (r *Reserved) Decode(buf []byte, offset int, rdlength int) (int, error) {
var err error
r.Bytes, offset, err = getSlice(buf, offset, int(rdlength))
···
r.RData = &ISDN{}
case 21:
r.RData = &RT{}
default:
r.RData = &Reserved{}
}
···
bytes = binary.BigEndian.AppendUint16(bytes, uint16(r.RClass))
bytes = binary.BigEndian.AppendUint32(bytes, r.TTL)
-
rdata_start := len(bytes)
bytes = binary.BigEndian.AppendUint16(bytes, 0)
bytes, err = r.RData.Encode(bytes, offsets)
if err != nil {
return nil, fmt.Errorf("rr encode: failed to encode RData for %s (%s): %w", r.Name, r.RType.String(), err)
}
-
rdata_length := uint16(len(bytes) - rdata_start - 2)
-
binary.BigEndian.PutUint16(bytes[rdata_start:rdata_start+2], rdata_length)
return bytes, nil
}
···
return len(buf), fmt.Errorf("x25 record: string segment length %d exceeds RDLENGTH boundary %d", strLen, endOffset)
}
+
strBytes, offset, err := getSlice(buf, nextOffsetAfterLen, int(strLen))
if err != nil {
return len(buf), fmt.Errorf("x25 record: failed to read string data (length %d): %w", strLen, err)
}
···
}
}
+
return offset, nil
}
func (x *X25) Encode(bytes []byte, offsets *map[string]uint16) ([]byte, error) {
···
return fmt.Sprintf("%d %s", rt.Preference, rt.IntermediateHost)
}
+
func (opt *OPT) Decode(buf []byte, offset int, rdlength int) (int, error) {
+
// s := offset
+
opt.Options = make([]EDNSOption, 0)
+
+
if rdlength == 0 {
+
return offset, nil
+
}
+
+
endOffset := offset + rdlength
+
curOffset := offset
+
for curOffset < endOffset {
+
// need 4 bytes for both code and length
+
if offset+4 > endOffset {
+
return offset, fmt.Errorf("OPT record: truncated option header at offset: %d", offset)
+
}
+
+
optCode, offset, err := getU16(buf, offset)
+
if err != nil {
+
return offset, fmt.Errorf("OPT Record: failed to read option code: %w", err)
+
}
+
+
optLength, offset, err := getU16(buf, offset)
+
if err != nil {
+
return offset, fmt.Errorf("OPT Record: failed to read option length: %w", err)
+
}
+
+
if offset+int(optLength) > endOffset {
+
return offset, fmt.Errorf("OPT Record: failed to read option data of length: %d", offset)
+
}
+
+
optData, offset, err := getSlice(buf, offset, int(optLength))
+
if err != nil {
+
return offset, fmt.Errorf("OPT Record: failed to read option data: %w", err)
+
}
+
+
opt.Options = append(opt.Options, EDNSOption{
+
Code: optCode,
+
Data: optData,
+
})
+
+
curOffset = offset
+
}
+
+
return offset, nil
+
}
+
+
func (opt *OPT) Encode(bytes []byte, offsets *map[string]uint16) ([]byte, error) {
+
for _, opt := range opt.Options {
+
bytes = binary.BigEndian.AppendUint16(bytes, opt.Code)
+
bytes = binary.BigEndian.AppendUint16(bytes, uint16(len(opt.Data)))
+
bytes = append(bytes, opt.Data...)
+
}
+
+
return bytes, nil
+
}
+
+
func (opt OPT) String() string {
+
var result strings.Builder
+
result.WriteString("OPT [")
+
for i, o := range opt.Options {
+
if i < len(opt.Options)-1 {
+
result.WriteString(fmt.Sprintf("%d <%x>,", o.Code, o.Data))
+
} else {
+
result.WriteString(fmt.Sprintf("%d <%x>,", o.Code, o.Data))
+
}
+
}
+
result.WriteString("]")
+
+
return result.String()
+
}
+
func (r *Reserved) Decode(buf []byte, offset int, rdlength int) (int, error) {
var err error
r.Bytes, offset, err = getSlice(buf, offset, int(rdlength))
···
r.RData = &ISDN{}
case 21:
r.RData = &RT{}
+
case 41:
+
r.RData = &OPT{}
default:
r.RData = &Reserved{}
}
···
bytes = binary.BigEndian.AppendUint16(bytes, uint16(r.RClass))
bytes = binary.BigEndian.AppendUint32(bytes, r.TTL)
+
rdataStart := len(bytes)
bytes = binary.BigEndian.AppendUint16(bytes, 0)
bytes, err = r.RData.Encode(bytes, offsets)
if err != nil {
return nil, fmt.Errorf("rr encode: failed to encode RData for %s (%s): %w", r.Name, r.RType.String(), err)
}
+
rdataLength := uint16(len(bytes) - rdataStart - 2)
+
binary.BigEndian.PutUint16(bytes[rdataStart:rdataStart+2], rdataLength)
return bytes, nil
}
+25
types.go
···
NXDOMAIN
NOTIMP
REFUSED
)
func (r RCode) String() string {
···
return "NOTIMP"
case REFUSED:
return "REFUSED"
default:
return fmt.Sprintf("RESERVED(%d)", int(r))
}
···
X25Type = 19
ISDNType = 20
RTType = 21
AXFRType = 252
MAILBType = 253
···
return "ISDN"
case RTType:
return "RT"
case AXFRType:
return "AXFR"
case MAILBType:
···
Answer []ResourceRecord // Answer contains a slice of resource records.
Authority []ResourceRecord // Authority contains a slice of resource records.
Additional []ResourceRecord // Additional contains a slice of resource records.
// offsets is a map of domains pointing to the seen offset used
// in domain compression.
···
type RT struct {
Preference uint16
IntermediateHost string
}
// Reserved represents a record that is not yet implemented.
···
NXDOMAIN
NOTIMP
REFUSED
+
+
BADVERS = 16
)
func (r RCode) String() string {
···
return "NOTIMP"
case REFUSED:
return "REFUSED"
+
case BADVERS:
+
return "BADVERS"
default:
return fmt.Sprintf("RESERVED(%d)", int(r))
}
···
X25Type = 19
ISDNType = 20
RTType = 21
+
+
OPTType = 41
AXFRType = 252
MAILBType = 253
···
return "ISDN"
case RTType:
return "RT"
+
case OPTType:
+
return "OPT"
case AXFRType:
return "AXFR"
case MAILBType:
···
Answer []ResourceRecord // Answer contains a slice of resource records.
Authority []ResourceRecord // Authority contains a slice of resource records.
Additional []ResourceRecord // Additional contains a slice of resource records.
+
+
// EDNS information
+
HasEDNS bool // If the DNS message detected EDNS
+
ExtendedRCode uint8 // first 8 bits to use if edns.
+
EDNSVersion uint8 // EDNS version of sender.
+
EDNSFlags uint16 // EDNS specific flag.
+
EDNSOptions []EDNSOption // EDNS options.
+
UDPSize uint16 // Max UDP size of sender.
// offsets is a map of domains pointing to the seen offset used
// in domain compression.
···
type RT struct {
Preference uint16
IntermediateHost string
+
}
+
+
type EDNSOption struct {
+
Code uint16
+
Data []byte
+
}
+
+
type OPT struct {
+
Options []EDNSOption
}
// Reserved represents a record that is not yet implemented.