a go dns packet parser

initial commit

seiso.moe 4cec10a5

+21
LICENSE
···
+
MIT License
+
+
Copyright (c) [2024] [blu]
+
+
Permission is hereby granted, free of charge, to any person obtaining a copy
+
of this software and associated documentation files (the "Software"), to deal
+
in the Software without restriction, including without limitation the rights
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+
copies of the Software, and to permit persons to whom the Software is
+
furnished to do so, subject to the following conditions:
+
+
The above copyright notice and this permission notice shall be included in all
+
copies or substantial portions of the Software.
+
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+
SOFTWARE.
+1
README.md
···
+
# Magna
+92
domain_name.go
···
+
package magna
+
+
import (
+
"encoding/binary"
+
"strings"
+
)
+
+
// decode_domain decodes a domain name from a buffer starting at offset.
+
// It returns the domain name along with the offset and error.
+
func decode_domain(buf []byte, offset int) (string, int, error) {
+
labels := make([]string, 0)
+
seen_offsets := make(map[int]struct{})
+
has_jumped := false
+
prev_offset := 0
+
+
var length uint8
+
var label []byte
+
var sec byte
+
var err error
+
for {
+
length, offset, err = getU8(buf, offset)
+
if err != nil {
+
return "", len(buf), err
+
}
+
+
if length == 0 {
+
break
+
}
+
+
if length&0xC0 == 0xC0 {
+
sec, offset, err = getU8(buf, offset)
+
if err != nil {
+
return "", len(buf), err
+
}
+
+
if !has_jumped {
+
prev_offset = offset
+
has_jumped = true
+
}
+
+
if _, found := seen_offsets[offset]; found {
+
return "", len(buf), &DomainCompressionError{}
+
}
+
+
seen_offsets[offset] = struct{}{}
+
offset = int(sec)
+
} else {
+
if length > 63 {
+
return "", len(buf), &InvalidLabelError{Length: int(length)}
+
}
+
+
label, offset, err = getSlice(buf, offset, int(length))
+
if err != nil {
+
return "", len(buf), err
+
}
+
+
labels = append(labels, string(label))
+
}
+
}
+
+
if has_jumped {
+
offset = prev_offset
+
}
+
+
return strings.Join(labels, "."), offset, nil
+
}
+
+
// encode_domain returns the bytes of the input bytes appened with the encoded domain name.
+
func encode_domain(bytes []byte, domain_name string, offsets *map[string]uint8) []byte {
+
pos := uint8(len(bytes))
+
+
labels := strings.Split(domain_name, ".")
+
for i, label := range labels {
+
remaining_labels := strings.Join(labels[i:], ".")
+
+
if offset, found := (*offsets)[remaining_labels]; found {
+
pointer := 0xC000 | uint16(offset)
+
bytes = binary.BigEndian.AppendUint16(bytes, pointer)
+
+
return bytes
+
}
+
+
bytes = append(bytes, uint8(len(label)))
+
bytes = append(bytes, []byte(label)...)
+
+
(*offsets)[remaining_labels] = pos
+
pos += 1 + uint8(len(label))
+
}
+
+
bytes = append(bytes, 0)
+
return bytes
+
}
+58
domain_test.go
···
+
package magna
+
+
import (
+
"testing"
+
)
+
+
func TestDecodeDomain(t *testing.T) {
+
buf := []byte{
+
0x03, 0x63, 0x6f, 0x6d, 0x00,
+
}
+
+
domain, offset, err := decode_domain(buf, 0)
+
assert_eq(t, "com", domain)
+
assert_eq(t, 5, offset)
+
assert_no_error(t, err)
+
}
+
+
func TestDecodeDomainWithCompression(t *testing.T) {
+
buf := []byte{
+
0x03, 0x63, 0x6f, 0x6d, 0x00, 0x01, 0x63, 0xC0, 0x00,
+
}
+
+
domain, offset, err := decode_domain(buf, 5)
+
assert_eq(t, "c.com", domain)
+
assert_eq(t, 9, offset)
+
assert_no_error(t, err)
+
}
+
+
func TestDecodeDomainWithCompressionLoop(t *testing.T) {
+
buf := []byte{
+
0x03, 0x63, 0x6f, 0x6d, 0xC0, 0x00,
+
}
+
+
domain, offset, err := decode_domain(buf, 0)
+
assert_eq(t, "", domain)
+
assert_eq(t, 6, offset)
+
assert_error(t, err)
+
}
+
+
func FuzzDecodeDomain(f *testing.F) {
+
testcases := [][]byte{
+
{
+
0x03, 0x63, 0x6f, 0x6d, 0x00,
+
},
+
{
+
0x03, 0x63, 0x6f, 0x6d, 0x00, 0x01, 0x63, 0xC0, 0x00,
+
},
+
{
+
0x03, 0x63, 0x6f, 0x6d, 0xC0, 0x00,
+
},
+
}
+
for _, tc := range testcases {
+
f.Add(tc) // Use f.Add to provide a seed corpus
+
}
+
f.Fuzz(func(t *testing.T, msg []byte) {
+
decode_domain(msg, 0)
+
})
+
}
+40
errors.go
···
+
package magna
+
+
import (
+
"fmt"
+
)
+
+
// BufferOverflowError represents an error when there is a buffer overflow.
+
type BufferOverflowError struct {
+
Length int
+
Offset int
+
}
+
+
func (e *BufferOverflowError) Error() string {
+
return fmt.Sprintf("magna: offset %d is past the buffer length %d", e.Offset, e.Length)
+
}
+
+
// InvalidLabelError represents an error when an invalid label length is encountered.
+
type InvalidLabelError struct {
+
Length int
+
}
+
+
func (e *InvalidLabelError) Error() string {
+
return fmt.Sprintf("magna: received invalid label length %d", e.Length)
+
}
+
+
// DomainCompressionError represents an error related to domain compression.
+
type DomainCompressionError struct{}
+
+
func (e *DomainCompressionError) Error() string {
+
return "magna: loop detected in domain compression"
+
}
+
+
// MagnaError represents a generic error with a custom message.
+
type MagnaError struct {
+
Message string
+
}
+
+
func (e *MagnaError) Error() string {
+
return fmt.Sprintf("magna: %s", e.Message)
+
}
+3
go.mod
···
+
module code.kiri.systems/magna
+
+
go 1.22.3
+89
header.go
···
+
package magna
+
+
import "encoding/binary"
+
+
// Decode decodes the header from the bytes.
+
func (h *Header) Decode(buf []byte, offset int) (int, error) {
+
var err error
+
h.ID, offset, err = getU16(buf, offset)
+
if err != nil {
+
return len(buf), err
+
}
+
+
flags, offset, err := getU16(buf, offset)
+
if err != nil {
+
return len(buf), err
+
}
+
+
h.QDCount, offset, err = getU16(buf, offset)
+
if err != nil {
+
return len(buf), err
+
}
+
+
h.ANCount, offset, err = getU16(buf, offset)
+
if err != nil {
+
return len(buf), err
+
}
+
+
h.NSCount, offset, err = getU16(buf, offset)
+
if err != nil {
+
return len(buf), err
+
}
+
+
h.ARCount, offset, err = getU16(buf, offset)
+
if err != nil {
+
return len(buf), err
+
}
+
+
h.QR = ((flags >> 15) & 0x01) == 1
+
h.OPCode = OPCode((flags >> 11) & 0x0F)
+
h.AA = ((flags >> 10) & 0x01) == 1
+
h.TC = ((flags >> 9) & 0x01) == 1
+
h.RD = ((flags >> 8) & 0x01) == 1
+
h.RA = ((flags >> 7) & 0x01) == 1
+
h.Z = uint8((flags >> 4) & 0x07)
+
h.RCode = RCode((flags & 0x0F))
+
+
return offset, nil
+
}
+
+
// Encode encodes the header packet to the byte representation.
+
func (h *Header) Encode() []byte {
+
var bytes []byte
+
+
bytes = binary.BigEndian.AppendUint16(bytes, h.ID)
+
+
var flags uint16
+
if h.QR {
+
flags |= 1 << 15
+
}
+
+
flags |= uint16(h.OPCode) << 11
+
+
if h.AA {
+
flags |= 1 << 10
+
}
+
+
if h.TC {
+
flags |= 1 << 9
+
}
+
+
if h.RD {
+
flags |= 1 << 8
+
}
+
+
if h.RA {
+
flags |= 1 << 7
+
}
+
+
flags |= uint16(h.Z) << 4
+
flags |= uint16(h.RCode)
+
+
bytes = binary.BigEndian.AppendUint16(bytes, flags)
+
bytes = binary.BigEndian.AppendUint16(bytes, h.QDCount)
+
bytes = binary.BigEndian.AppendUint16(bytes, h.ANCount)
+
bytes = binary.BigEndian.AppendUint16(bytes, h.NSCount)
+
bytes = binary.BigEndian.AppendUint16(bytes, h.ARCount)
+
+
return bytes
+
}
+105
header_test.go
···
+
package magna
+
+
import (
+
"reflect"
+
"testing"
+
)
+
+
func assert_eq(t *testing.T, expected any, actual any) {
+
if reflect.TypeOf(expected) != reflect.TypeOf(actual) {
+
t.Errorf("expected type: %T\t actual type: %T\n", expected, actual)
+
}
+
+
if expected != actual {
+
t.Fatalf("expected: %#v\t actual: %#v\n", expected, actual)
+
}
+
}
+
+
func assert_no_error(t *testing.T, err error) {
+
if err != nil {
+
t.Fatalf("err is not nil: %v\n", err)
+
}
+
}
+
+
func assert_error(t *testing.T, err error) {
+
if err == nil {
+
t.Fatalf("err is not nil: %v\n", err)
+
}
+
}
+
+
func TestHeaderDecode(t *testing.T) {
+
bytes := []byte{
+
0x01, 0x02, // ID
+
0xaa, 0xaa, // QR, Opcode, AA, TC, RD, RA, Z, RCODE
+
0x00, 0x01, // QDCOUNT
+
0x00, 0x02, // ANCOUNT
+
0x00, 0x03, // NSCOUNT
+
0x00, 0x04, // ARCOUNT
+
}
+
+
var header Header
+
offset, err := header.Decode(bytes, 0)
+
if err != nil {
+
t.Errorf("error should be nil\n")
+
}
+
+
if offset != 12 {
+
t.Errorf("offset should be 12 not %v\n", offset)
+
}
+
+
assert_eq(t, header.ID, uint16(258))
+
assert_eq(t, header.QR, true)
+
assert_eq(t, header.OPCode, OPCode(5))
+
assert_eq(t, header.AA, false)
+
assert_eq(t, header.TC, true)
+
assert_eq(t, header.RD, false)
+
assert_eq(t, header.RA, true)
+
assert_eq(t, header.Z, uint8(0b010))
+
assert_eq(t, header.RCode, RCode(0b1010))
+
assert_eq(t, header.QDCount, uint16(1))
+
assert_eq(t, header.ANCount, uint16(2))
+
assert_eq(t, header.NSCount, uint16(3))
+
assert_eq(t, header.ARCount, uint16(4))
+
}
+
+
func TestHeaderEncode(t *testing.T) {
+
bytes := []byte{
+
0x01, 0x02, // ID
+
0xaa, 0xaa, // QR, Opcode, AA, TC, RD, RA, Z, RCODE
+
0x00, 0x01, // QDCOUNT
+
0x00, 0x02, // ANCOUNT
+
0x00, 0x03, // NSCOUNT
+
0x00, 0x04, // ARCOUNT
+
}
+
+
var header Header
+
_, err := header.Decode(bytes, 0)
+
assert_no_error(t, err)
+
+
actual := header.Encode()
+
for i := 0; i < len(bytes); i++ {
+
if bytes[i] != actual[i] {
+
t.Fatal(bytes, actual)
+
}
+
}
+
}
+
+
func FuzzDecodeHeader(f *testing.F) {
+
testcases := [][]byte{
+
{
+
0x01, 0x02,
+
0xaa, 0xaa,
+
0x00, 0x01,
+
0x00, 0x02,
+
0x00, 0x03,
+
0x00, 0x04,
+
},
+
}
+
for _, tc := range testcases {
+
f.Add(tc) // Use f.Add to provide a seed corpus
+
}
+
var header Header
+
f.Fuzz(func(t *testing.T, msg []byte) {
+
header.Decode(msg, 0)
+
})
+
}
+2
magna.go
···
+
// Package magna implements DNS message encoding/decoding.
+
package magna
+76
message.go
···
+
package magna
+
+
// Decode decodes a DNS packet.
+
func (m *Message) Decode(buf []byte) (err error) {
+
offset, err := m.Header.Decode(buf, 0)
+
if err != nil {
+
return err
+
}
+
+
for x := 0; x < int(m.Header.QDCount); x++ {
+
var question Question
+
offset, err = question.Decode(buf, offset)
+
if err != nil {
+
return err
+
}
+
+
m.Question = append(m.Question, question)
+
}
+
+
for x := 0; x < int(m.Header.ANCount); x++ {
+
var rr ResourceRecord
+
offset, err = rr.Decode(buf, offset)
+
if err != nil {
+
return err
+
}
+
+
m.Answer = append(m.Answer, rr)
+
}
+
+
for x := 0; x < int(m.Header.NSCount); x++ {
+
var rr ResourceRecord
+
offset, err = rr.Decode(buf, offset)
+
if err != nil {
+
return err
+
}
+
+
m.Authority = append(m.Authority, rr)
+
}
+
+
for x := 0; x < int(m.Header.ARCount); x++ {
+
var rr ResourceRecord
+
offset, err = rr.Decode(buf, offset)
+
if err != nil {
+
return err
+
}
+
+
m.Additional = append(m.Additional, rr)
+
}
+
+
return nil
+
}
+
+
// Encode encodes a message to a DNS packet.
+
// TODO: set truncation bit if over 512 and udp is protocol
+
func (m *Message) Encode() []byte {
+
m.offsets = make(map[string]uint8)
+
bytes := m.Header.Encode()
+
+
for _, question := range m.Question {
+
bytes = question.Encode(bytes, &m.offsets)
+
}
+
+
for _, answer := range m.Answer {
+
bytes = answer.Encode(bytes, &m.offsets)
+
}
+
+
for _, authority := range m.Authority {
+
bytes = authority.Encode(bytes, &m.offsets)
+
}
+
+
for _, additional := range m.Additional {
+
bytes = additional.Encode(bytes, &m.offsets)
+
}
+
+
return bytes
+
}
+83
message_test.go
···
+
package magna
+
+
import (
+
"testing"
+
)
+
+
func TestMessageDecode(t *testing.T) {
+
bytes := []byte{
+
0x8e, 0x19, 0x81, 0x80, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x04, 0x6e, 0x65,
+
0x77, 0x73, 0x0b, 0x79, 0x63, 0x6f, 0x6d, 0x62, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x03,
+
0x63, 0x6f, 0x6d, 0x00, 0x00, 0x01, 0x00, 0x01, 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00,
+
0x00, 0x00, 0x01, 0x00, 0x04, 0xd1, 0xd8, 0xe6, 0xcf,
+
}
+
+
var msg Message
+
msg.Decode(bytes)
+
assert_eq(t, uint16(0x8e19), msg.Header.ID)
+
assert_eq(t, true, msg.Header.QR)
+
assert_eq(t, OPCode(0), msg.Header.OPCode)
+
assert_eq(t, false, msg.Header.AA)
+
assert_eq(t, false, msg.Header.TC)
+
assert_eq(t, true, msg.Header.RD)
+
assert_eq(t, true, msg.Header.RA)
+
assert_eq(t, uint8(0), msg.Header.Z)
+
assert_eq(t, RCode(0), msg.Header.RCode)
+
assert_eq(t, uint16(1), msg.Header.QDCount)
+
assert_eq(t, uint16(1), msg.Header.ANCount)
+
assert_eq(t, uint16(0), msg.Header.NSCount)
+
assert_eq(t, uint16(0), msg.Header.ARCount)
+
+
assert_eq(t, 1, len(msg.Question))
+
+
question := msg.Question[0]
+
assert_eq(t, "news.ycombinator.com", question.QName)
+
assert_eq(t, DNSType(1), question.QType)
+
assert_eq(t, DNSClass(1), question.QClass)
+
+
assert_eq(t, 1, len(msg.Answer))
+
answer := msg.Answer[0]
+
assert_eq(t, answer.Name, "news.ycombinator.com")
+
assert_eq(t, DNSType(1), answer.RType)
+
assert_eq(t, DNSClass(1), answer.RClass)
+
assert_eq(t, uint32(1), answer.TTL)
+
assert_eq(t, uint16(4), answer.RDLength)
+
}
+
+
func TestMessageEncode(t *testing.T) {
+
bytes := []byte{
+
0x8e, 0x19, 0x81, 0x80, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x04, 0x6e, 0x65,
+
0x77, 0x73, 0x0b, 0x79, 0x63, 0x6f, 0x6d, 0x62, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x03,
+
0x63, 0x6f, 0x6d, 0x00, 0x00, 0x01, 0x00, 0x01, 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00,
+
0x00, 0x00, 0x01, 0x00, 0x04, 0xd1, 0xd8, 0xe6, 0xcf,
+
}
+
+
var msg Message
+
err := msg.Decode(bytes)
+
assert_no_error(t, err)
+
+
actual := msg.Encode()
+
for i := 0; i < len(bytes); i++ {
+
if bytes[i] != actual[i] {
+
t.Fatal(bytes, actual)
+
}
+
}
+
}
+
+
func FuzzDecodeMessage(f *testing.F) {
+
testcases := [][]byte{
+
{
+
0x8e, 0x19, 0x81, 0x80, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x04, 0x6e, 0x65,
+
0x77, 0x73, 0x0b, 0x79, 0x63, 0x6f, 0x6d, 0x62, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x03,
+
0x63, 0x6f, 0x6d, 0x00, 0x00, 0x01, 0x00, 0x01, 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00,
+
0x00, 0x00, 0x01, 0x00, 0x04, 0xd1, 0xd8, 0xe6, 0xcf,
+
},
+
}
+
for _, tc := range testcases {
+
f.Add(tc) // Use f.Add to provide a seed corpus
+
}
+
f.Fuzz(func(t *testing.T, msg []byte) {
+
var m Message
+
m.Decode(msg)
+
})
+
}
+34
question.go
···
+
package magna
+
+
import "encoding/binary"
+
+
// Decode decodes a question from buf at the offset
+
func (q *Question) Decode(buf []byte, offset int) (int, error) {
+
var err error
+
q.QName, offset, err = decode_domain(buf, offset)
+
if err != nil {
+
return offset, err
+
}
+
+
qtype, offset, err := getU16(buf, offset)
+
if err != nil {
+
return offset, err
+
}
+
+
qclass, offset, err := getU16(buf, offset)
+
if err != nil {
+
return offset, err
+
}
+
+
q.QType = DNSType(qtype)
+
q.QClass = DNSClass(qclass)
+
return offset, nil
+
}
+
+
// Encode serializes a Question into bytes, using a map to handle domain name compression offsets.
+
func (q *Question) Encode(bytes []byte, offsets *map[string]uint8) []byte {
+
bytes = encode_domain(bytes, q.QName, offsets)
+
bytes = binary.BigEndian.AppendUint16(bytes, uint16(q.QType))
+
bytes = binary.BigEndian.AppendUint16(bytes, uint16(q.QClass))
+
return bytes
+
}
+421
resource_record.go
···
+
package magna
+
+
import (
+
"encoding/binary"
+
"net"
+
"strings"
+
)
+
+
func (a *A) Decode(buf []byte, offset int, rdlength int) (int, error) {
+
bytes, offset, err := getSlice(buf, offset, rdlength)
+
if err != nil {
+
return offset, err
+
}
+
+
a.Address = net.IP(bytes)
+
return offset, err
+
}
+
+
func (a *A) Encode(bytes []byte, offsets *map[string]uint8) []byte {
+
return append(bytes, a.Address.To4()...)
+
}
+
+
func (ns *NS) Decode(buf []byte, offset int, rdlength int) (int, error) {
+
var err error
+
ns.NSDName, offset, err = decode_domain(buf, offset)
+
if err != nil {
+
return offset, err
+
}
+
+
return offset, err
+
}
+
+
func (ns *NS) Encode(bytes []byte, offsets *map[string]uint8) []byte {
+
return append(bytes, encode_domain(bytes, ns.NSDName, offsets)...)
+
}
+
+
func (md *MD) Decode(buf []byte, offset int, rdlength int) (int, error) {
+
var err error
+
md.MADName, offset, err = decode_domain(buf, offset)
+
if err != nil {
+
return offset, err
+
}
+
+
return offset, err
+
}
+
+
func (md *MD) Encode(bytes []byte, offsets *map[string]uint8) []byte {
+
return append(bytes, encode_domain(bytes, md.MADName, offsets)...)
+
}
+
+
func (mf *MF) Decode(buf []byte, offset int, rdlength int) (int, error) {
+
var err error
+
mf.MADName, offset, err = decode_domain(buf, offset)
+
if err != nil {
+
return offset, err
+
}
+
+
return offset, err
+
}
+
+
func (mf *MF) Encode(bytes []byte, offsets *map[string]uint8) []byte {
+
return append(bytes, encode_domain(bytes, mf.MADName, offsets)...)
+
}
+
+
func (c *CNAME) Decode(buf []byte, offset int, rdlength int) (int, error) {
+
var err error
+
c.CName, offset, err = decode_domain(buf, offset)
+
if err != nil {
+
return offset, err
+
}
+
+
return offset, err
+
}
+
+
func (c *CNAME) Encode(bytes []byte, offsets *map[string]uint8) []byte {
+
return append(bytes, encode_domain(bytes, c.CName, offsets)...)
+
}
+
+
func (soa *SOA) Decode(buf []byte, offset int, rdlength int) (int, error) {
+
var err error
+
soa.MName, offset, err = decode_domain(buf, offset)
+
if err != nil {
+
return offset, err
+
}
+
+
soa.RName, offset, err = decode_domain(buf, offset)
+
if err != nil {
+
return offset, err
+
}
+
+
soa.Serial, offset, err = getU32(buf, offset)
+
if err != nil {
+
return offset, err
+
}
+
+
soa.Refresh, offset, err = getU32(buf, offset)
+
if err != nil {
+
return offset, err
+
}
+
+
soa.Retry, offset, err = getU32(buf, offset)
+
if err != nil {
+
return offset, err
+
}
+
+
soa.Expire, offset, err = getU32(buf, offset)
+
if err != nil {
+
return offset, err
+
}
+
+
soa.Minimum, offset, err = getU32(buf, offset)
+
if err != nil {
+
return offset, err
+
}
+
+
return offset, err
+
}
+
+
func (soa *SOA) Encode(bytes []byte, offsets *map[string]uint8) []byte {
+
bytes = append(bytes, encode_domain(bytes, soa.MName, offsets)...)
+
bytes = append(bytes, encode_domain(bytes, soa.RName, offsets)...)
+
bytes = binary.BigEndian.AppendUint32(bytes, soa.Serial)
+
bytes = binary.BigEndian.AppendUint32(bytes, soa.Refresh)
+
bytes = binary.BigEndian.AppendUint32(bytes, soa.Retry)
+
bytes = binary.BigEndian.AppendUint32(bytes, soa.Expire)
+
bytes = binary.BigEndian.AppendUint32(bytes, soa.Minimum)
+
+
return bytes
+
}
+
+
func (mb *MB) Decode(buf []byte, offset int, rdlength int) (int, error) {
+
madname, offset, err := decode_domain(buf, offset)
+
if err != nil {
+
return offset, err
+
}
+
+
mb.MADName = string(madname)
+
return offset, err
+
}
+
+
func (mb *MB) Encode(bytes []byte, offsets *map[string]uint8) []byte {
+
return append(bytes, encode_domain(bytes, mb.MADName, offsets)...)
+
}
+
+
func (mg *MG) Decode(buf []byte, offset int, rdlength int) (int, error) {
+
var err error
+
mg.MGMName, offset, err = decode_domain(buf, offset)
+
if err != nil {
+
return offset, err
+
}
+
+
return offset, err
+
}
+
+
func (mg *MG) Encode(bytes []byte, offsets *map[string]uint8) []byte {
+
return append(bytes, encode_domain(bytes, mg.MGMName, offsets)...)
+
}
+
+
func (mr *MR) Decode(buf []byte, offset int, rdlength int) (int, error) {
+
var err error
+
mr.NEWName, offset, err = decode_domain(buf, offset)
+
if err != nil {
+
return offset, err
+
}
+
+
return offset, err
+
}
+
+
func (mr *MR) Encode(bytes []byte, offsets *map[string]uint8) []byte {
+
return append(bytes, encode_domain(bytes, mr.NEWName, offsets)...)
+
}
+
+
func (null *NULL) Decode(buf []byte, offset int, rdlength int) (int, error) {
+
var err error
+
null.Anything, offset, err = getSlice(buf, offset, int(rdlength))
+
if err != nil {
+
return offset, err
+
}
+
+
return offset, err
+
}
+
+
func (null *NULL) Encode(bytes []byte, offsets *map[string]uint8) []byte {
+
return append(bytes, null.Anything...)
+
}
+
+
func (wks *WKS) Decode(buf []byte, offset int, rdlength int) (int, error) {
+
address, offset, err := getSlice(buf, offset, 4)
+
if err != nil {
+
return offset, err
+
}
+
+
protocol, offset, err := getU8(buf, offset)
+
if err != nil {
+
return offset, err
+
}
+
+
wks.Address = net.IP(address)
+
wks.Protocol = protocol
+
wks.BitMap, offset, err = getSlice(buf, offset, offset+int(rdlength)-5)
+
if err != nil {
+
return offset, err
+
}
+
+
return offset, err
+
}
+
+
func (wks *WKS) Encode(bytes []byte, offsets *map[string]uint8) []byte {
+
bytes = append(bytes, wks.Address.To4()...)
+
bytes = append(bytes, wks.Protocol)
+
bytes = append(bytes, wks.BitMap...)
+
+
return bytes
+
}
+
+
func (ptr *PTR) Decode(buf []byte, offset int, rdlength int) (int, error) {
+
var err error
+
ptr.PTRDName, offset, err = decode_domain(buf, offset)
+
if err != nil {
+
return offset, err
+
}
+
+
return offset, err
+
}
+
+
func (ptr *PTR) Encode(bytes []byte, offsets *map[string]uint8) []byte {
+
return append(bytes, encode_domain(bytes, ptr.PTRDName, offsets)...)
+
}
+
+
func (hinfo *HINFO) Decode(buf []byte, offset int, rdlength int) (int, error) {
+
input, offset, err := getSlice(buf, offset, int(rdlength))
+
if err != nil {
+
return offset, err
+
}
+
+
parts := strings.SplitN(string(input), " ", 2)
+
if len(parts) != 2 {
+
return offset, &MagnaError{Message: "HINFO expected 2 values separated by a space"}
+
}
+
+
hinfo.CPU = parts[0]
+
hinfo.OS = parts[1]
+
return offset, err
+
}
+
+
func (hinfo *HINFO) Encode(bytes []byte, offsets *map[string]uint8) []byte {
+
bytes = append(bytes, []byte(hinfo.CPU)...)
+
bytes = append(bytes, ' ')
+
bytes = append(bytes, []byte(hinfo.OS)...)
+
return bytes
+
}
+
+
func (minfo *MINFO) Decode(buf []byte, offset int, rdlength int) (int, error) {
+
var err error
+
+
minfo.RMailBx, offset, err = decode_domain(buf, offset)
+
if err != nil {
+
return offset, err
+
}
+
+
minfo.EMailBx, offset, err = decode_domain(buf, offset)
+
if err != nil {
+
return offset, err
+
}
+
+
return offset, err
+
}
+
+
func (minfo *MINFO) Encode(bytes []byte, offsets *map[string]uint8) []byte {
+
bytes = append(bytes, encode_domain(bytes, minfo.RMailBx, offsets)...)
+
bytes = append(bytes, encode_domain(bytes, minfo.EMailBx, offsets)...)
+
+
return bytes
+
}
+
+
func (mx *MX) Decode(buf []byte, offset int, rdlength int) (int, error) {
+
var err error
+
mx.Preference, offset, err = getU16(buf, offset)
+
if err != nil {
+
return offset, err
+
}
+
+
mx.Exchange, offset, err = decode_domain(buf, offset)
+
if err != nil {
+
return offset, err
+
}
+
+
return offset, err
+
}
+
+
func (mx *MX) Encode(bytes []byte, offsets *map[string]uint8) []byte {
+
bytes = binary.BigEndian.AppendUint16(bytes, mx.Preference)
+
bytes = append(bytes, encode_domain(bytes, mx.Exchange, offsets)...)
+
+
return bytes
+
}
+
+
func (txt *TXT) Decode(buf []byte, offset int, rdlength int) (int, error) {
+
bytes, offset, err := getSlice(buf, offset, rdlength)
+
if err != nil {
+
return offset, err
+
}
+
+
txt.TxtData = string(bytes)
+
return offset, err
+
}
+
+
func (txt *TXT) Encode(bytes []byte, offsets *map[string]uint8) []byte {
+
return append(bytes, []byte(txt.TxtData)...)
+
}
+
+
func (r *Reserved) Decode(buf []byte, offset int, rdlength int) (int, error) {
+
var err error
+
r.Bytes, offset, err = getSlice(buf, offset, int(rdlength))
+
if err != nil {
+
return offset, err
+
}
+
+
return offset, err
+
}
+
+
func (r *Reserved) Encode(bytes []byte, offsets *map[string]uint8) []byte {
+
return append(bytes, r.Bytes...)
+
}
+
+
// Decode decodes a resource record from buf at the offset.
+
func (r *ResourceRecord) Decode(buf []byte, offset int) (int, error) {
+
name, offset, err := decode_domain(buf, offset)
+
if err != nil {
+
return offset, err
+
}
+
r.Name = name
+
+
rtype, offset, err := getU16(buf, offset)
+
if err != nil {
+
return offset, err
+
}
+
r.RType = DNSType(rtype)
+
+
rclass, offset, err := getU16(buf, offset)
+
if err != nil {
+
return offset, err
+
}
+
r.RClass = DNSClass(rclass)
+
+
r.TTL, offset, err = getU32(buf, offset)
+
if err != nil {
+
return offset, err
+
}
+
+
r.RDLength, offset, err = getU16(buf, offset)
+
if err != nil {
+
return offset, err
+
}
+
+
switch r.RType {
+
case 1:
+
r.RData = &A{}
+
case 2:
+
r.RData = &NS{}
+
case 3:
+
r.RData = &MD{}
+
case 4:
+
r.RData = &MF{}
+
case 5:
+
r.RData = &CNAME{}
+
case 6:
+
r.RData = &SOA{}
+
case 7:
+
r.RData = &MB{}
+
case 8:
+
r.RData = &MG{}
+
case 9:
+
r.RData = &MR{}
+
case 10:
+
r.RData = &NULL{}
+
case 11:
+
r.RData = &WKS{}
+
case 12:
+
r.RData = &PTR{}
+
case 13:
+
r.RData = &HINFO{}
+
case 14:
+
r.RData = &MINFO{}
+
case 15:
+
r.RData = &MX{}
+
case 16:
+
r.RData = &TXT{}
+
default:
+
r.RData = &Reserved{}
+
}
+
+
if r.RData != nil {
+
offset, err = r.RData.Decode(buf, offset, int(r.RDLength))
+
if err != nil {
+
return offset, err
+
}
+
}
+
+
return offset, nil
+
}
+
+
// Encode encdoes a resource record and returns the input bytes appened.
+
func (r *ResourceRecord) Encode(bytes []byte, offsets *map[string]uint8) []byte {
+
bytes = encode_domain(bytes, r.Name, offsets)
+
bytes = binary.BigEndian.AppendUint16(bytes, uint16(r.RType))
+
bytes = binary.BigEndian.AppendUint16(bytes, uint16(r.RClass))
+
bytes = binary.BigEndian.AppendUint32(bytes, r.TTL)
+
+
second_dinner := make([]byte, len(bytes))
+
copy(second_dinner, bytes)
+
start := len(second_dinner)
+
second_dinner = r.RData.Encode(second_dinner, offsets)
+
end := len(second_dinner)
+
data := second_dinner[start:end]
+
+
bytes = binary.BigEndian.AppendUint16(bytes, uint16(end-start))
+
bytes = append(bytes, data...)
+
+
return bytes
+
}
+325
types.go
···
+
package magna
+
+
import (
+
"fmt"
+
"net"
+
)
+
+
type (
+
// An OPCode specifies the kind of query.
+
OPCode int
+
+
// A RCode specifies the response code.
+
RCode int
+
+
// A DNSType specifies the type of the query.
+
DNSType uint16
+
+
// A DNSClass specifies the class of the query.
+
DNSClass uint16
+
)
+
+
const (
+
QUERY OPCode = iota
+
IQUERY
+
STATUS
+
)
+
+
func (op OPCode) String() string {
+
switch op {
+
case QUERY:
+
return "QUERY"
+
case IQUERY:
+
return "IQUERY"
+
case STATUS:
+
return "STATUS"
+
default:
+
return fmt.Sprintf("RESERVED(%d)", int(op))
+
}
+
}
+
+
const (
+
NOERROR RCode = iota
+
FORMERR
+
SERVFAIL
+
NXDOMAIN
+
NOTIMP
+
REFUSED
+
)
+
+
func (r RCode) String() string {
+
switch r {
+
case NOERROR:
+
return "NOERROR"
+
case FORMERR:
+
return "FORMERR"
+
case SERVFAIL:
+
return "SERVFAIL"
+
case NXDOMAIN:
+
return "NXDOMAIN"
+
case NOTIMP:
+
return "NOTIMP"
+
case REFUSED:
+
return "REFUSED"
+
default:
+
return fmt.Sprintf("RESERVED(%d)", int(r))
+
}
+
}
+
+
const (
+
AType DNSType = iota + 1
+
NSType
+
MDType
+
MFType
+
CNAMEType
+
SOAType
+
MBType
+
MGType
+
MRType
+
NULLType
+
WKSType
+
PTRType
+
HINFOType
+
MINFOType
+
MXType
+
TXTType
+
+
AXFRType = 252
+
MAILBType = 253
+
MAILAType = 254
+
AllType = 255
+
)
+
+
func (t DNSType) String() string {
+
switch t {
+
case AType:
+
return "A"
+
case NSType:
+
return "NS"
+
case MDType:
+
return "MD"
+
case MFType:
+
return "MF"
+
case CNAMEType:
+
return "CNAME"
+
case SOAType:
+
return "SOA"
+
case MBType:
+
return "MB"
+
case MGType:
+
return "MG"
+
case MRType:
+
return "MR"
+
case NULLType:
+
return "NULL"
+
case WKSType:
+
return "WKS"
+
case PTRType:
+
return "PTR"
+
case HINFOType:
+
return "HINFO"
+
case MINFOType:
+
return "MINFO"
+
case MXType:
+
return "MX"
+
case TXTType:
+
return "TXT"
+
case AXFRType:
+
return "AXFR"
+
case MAILBType:
+
return "MAILB"
+
case MAILAType:
+
return "MAILA"
+
case AllType:
+
return "All"
+
default:
+
return fmt.Sprintf("RESERVED(%d)", int(t))
+
}
+
}
+
+
const (
+
IN DNSClass = iota + 1
+
CS
+
CH
+
HS
+
+
ANY = 255
+
)
+
+
func (c DNSClass) String() string {
+
switch c {
+
case IN:
+
return "IN"
+
case CS:
+
return "CS"
+
case CH:
+
return "CH"
+
case HS:
+
return "HS"
+
case ANY:
+
return "*"
+
default:
+
return fmt.Sprintf("RESERVED(%d)", int(c))
+
}
+
}
+
+
// A Message represents the single format supported by the DNS protocol.
+
type Message struct {
+
Header Header // Header contains metadata about the message.
+
Question []Question // Question contains a slice of questions.
+
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.
+
offsets map[string]uint8
+
}
+
+
// A Header represents the metadata information of a DNS packet.
+
type Header struct {
+
ID uint16 // ID is a 16 bit identifier.
+
QR bool // QR (query response) specifies if the packet is a query (false) or response (true).
+
OPCode OPCode // OPCode is a 4bit value that specifies the kind of the query.
+
AA bool // AA (Authoritative Answer) specifies if the responding name server is the authoritative server.
+
TC bool // TC (Truncation) specifies if the packet was truncated due to length.
+
RD bool // RD (Recursion Desired) specifies that the client wants to recurse.
+
RA bool // RA (Recursion Available) specifies that the server is available to recurse.
+
Z uint8 // Z Reserved for future use
+
RCode RCode // RCode is a 4 bit field representing the response error.
+
QDCount uint16 // QDCount specifies number of entries in the Question slice.
+
ANCount uint16 // ANCount specifies number of entries in the Answer slice.
+
NSCount uint16 // NSCount specifies number of entries in the Authority slice.
+
ARCount uint16 // ARCount specifies number of entries in the Additional slice.
+
}
+
+
// A Question represent a question in the DNS packet.
+
type Question struct {
+
QName string // QName domain name being questioned.
+
QType DNSType // QType specifies the type of the question.
+
QClass DNSClass // QClass specifies the class of the question.
+
}
+
+
// A ResourceRecordData represents the RDATA field of the resource record.
+
// Decode:
+
//
+
// []byte - DNS packet
+
// int - offset in []byte to start parsing resource record data
+
// int - length of the resource record field
+
//
+
// Encode:
+
//
+
// []byte - DNS Packet
+
// *map[string]uint8 - map containing labels and offsets for domain name compression
+
type ResourceRecordData interface {
+
Decode([]byte, int, int) (int, error)
+
Encode([]byte, *map[string]uint8) []byte
+
}
+
+
// ResourceRecord represents DNS records.
+
type ResourceRecord struct {
+
Name string // a domain label
+
RType DNSType // the type of the record
+
RClass DNSClass // the class of the record
+
TTL uint32 // the time to live of the record in seconds
+
RDLength uint16 // the length of the record in number of bytes
+
RData ResourceRecordData // the data of the record
+
}
+
+
// A represents a 32 bit Internet Address.
+
type A struct {
+
Address net.IP
+
}
+
+
// NS represents
+
type NS struct {
+
NSDName string
+
}
+
+
// MD represents the mail agent for a domain.
+
// XXX: Obsolete
+
type MD struct {
+
MADName string
+
}
+
+
// MF represents a host which has a mail agent for a domain.
+
// XXX: Obsolete
+
type MF struct {
+
MADName string
+
}
+
+
// CNAME represents the canonical name for the owner.
+
type CNAME struct {
+
CName string
+
}
+
+
// SOA represents
+
type SOA struct {
+
MName string // MName represents the name server that was the original source of data for a zone.
+
RName string // RName represents a mailbox of the person responsible for this zone.
+
Serial uint32 // Serial is a 32 bit version number of the original copy of the zone.
+
Refresh uint32 // Refresh is a 32 bit time interval before the zone should be refreshed.
+
Retry uint32 // Retry is a 32 bit time interval that should elapse before a failed refresh is retried.
+
Expire uint32 // Expire is the upper limit on the time interval that can elapse before the zone is no longer authoritative.
+
Minimum uint32 // Minimum is the minimum TTL field that should be exported with any RR from this zone.
+
}
+
+
// MB represents the host with a specified mailbox.
+
type MB struct {
+
MADName string
+
}
+
+
// MG represents a mailbox which is configured for MGMName.
+
type MG struct {
+
MGMName string
+
}
+
+
// MR specifies the proper rename of a mailbox.
+
type MR struct {
+
NEWName string
+
}
+
+
// NULL can be anything as long as its under 65535 octets.
+
type NULL struct {
+
Anything []byte
+
}
+
+
// WKS specifies well known services supported by a protocol.
+
type WKS struct {
+
Address net.IP // Address is a 32 bit Internet address.
+
Protocol uint8 // Protocol is a 8 bit IP protocol number.
+
BitMap []byte // BitMap is a variable length bit map must be multiple of 8 bits.
+
}
+
+
// PTR specifies to a location in the domain name space.
+
type PTR struct {
+
PTRDName string
+
}
+
+
// HINFO represents the general information about a server.
+
type HINFO struct {
+
CPU string // CPU specifies the CPU type.
+
OS string // OS specifies the operating system.
+
}
+
+
// MINFO represents mailing list information.
+
type MINFO struct {
+
RMailBx string // RMailBx specifies the mailbox for a mailing list.
+
EMailBx string // EMailBx specifes a mailbox for receiving errors.
+
}
+
+
// MX represents a host acting as a mail exchange.
+
type MX struct {
+
Preference uint16 // Preference specifies the preference with lower values being prefered.
+
Exchange string // Exchange specifies host acting for the name.
+
}
+
+
// TXT represents one or more character strings.
+
type TXT struct {
+
TxtData string
+
}
+
+
// Reserved represents a record that is not yet implemented.
+
type Reserved struct {
+
Bytes []byte
+
}
+45
utils.go
···
+
package magna
+
+
import (
+
"encoding/binary"
+
)
+
+
// getU8 returns the first byte from a byte array at offset.
+
func getU8(buf []byte, offset int) (uint8, int, error) {
+
next_offset := offset + 1
+
if next_offset > len(buf) {
+
return 0, len(buf), &BufferOverflowError{Length: len(buf), Offset: next_offset}
+
}
+
+
return buf[offset], next_offset, nil
+
}
+
+
// getU16 returns the bigEndian uint16 from a byte array at offset.
+
func getU16(buf []byte, offset int) (uint16, int, error) {
+
next_offset := offset + 2
+
if next_offset > len(buf) {
+
return 0, len(buf), &BufferOverflowError{Length: len(buf), Offset: next_offset}
+
}
+
+
return binary.BigEndian.Uint16(buf[offset:]), next_offset, nil
+
}
+
+
// getU32 returns the bigEndian uint32 from a byte array at offset.
+
func getU32(buf []byte, offset int) (uint32, int, error) {
+
next_offset := offset + 4
+
if next_offset > len(buf) {
+
return 0, len(buf), &BufferOverflowError{Length: len(buf), Offset: next_offset}
+
}
+
+
return binary.BigEndian.Uint32(buf[offset:]), next_offset, nil
+
}
+
+
// getSlice returns a slice of bytes from a byte array at an offset and of length.
+
func getSlice(buf []byte, offset int, length int) ([]byte, int, error) {
+
next_offset := offset + length
+
if next_offset > len(buf) {
+
return nil, len(buf), &BufferOverflowError{Length: len(buf), Offset: next_offset}
+
}
+
+
return buf[offset:next_offset], next_offset, nil
+
}
+69
utils_test.go
···
+
package magna
+
+
import (
+
"testing"
+
)
+
+
func TestU8(t *testing.T) {
+
buf := []byte{
+
0x01,
+
}
+
+
actual, offset, err := getU8(buf, 0)
+
assert_eq(t, uint8(1), actual)
+
assert_eq(t, 1, offset)
+
assert_no_error(t, err)
+
+
actual, offset, err = getU8(buf, 1)
+
assert_eq(t, uint8(0), actual)
+
assert_eq(t, 1, offset)
+
assert_error(t, err)
+
}
+
+
func TestU16(t *testing.T) {
+
buf := []byte{
+
0x00, 0x01,
+
}
+
+
actual, offset, err := getU16(buf, 0)
+
assert_eq(t, uint16(1), actual)
+
assert_eq(t, 2, offset)
+
assert_no_error(t, err)
+
+
actual, offset, err = getU16(buf, 1)
+
assert_eq(t, uint16(0), actual)
+
assert_eq(t, 2, offset)
+
assert_error(t, err)
+
}
+
+
func TestU32(t *testing.T) {
+
buf := []byte{
+
0x00, 0x00, 0x00, 0x01,
+
}
+
+
actual, offset, err := getU32(buf, 0)
+
assert_eq(t, uint32(1), actual)
+
assert_eq(t, 4, offset)
+
assert_no_error(t, err)
+
+
actual, offset, err = getU32(buf, 1)
+
assert_eq(t, uint32(0), actual)
+
assert_eq(t, 4, offset)
+
assert_error(t, err)
+
}
+
+
func TestSlice(t *testing.T) {
+
buf := []byte{
+
0x62, 0x6c, 0x75,
+
}
+
+
actual, offset, err := getSlice(buf, 0, 3)
+
assert_eq(t, "blu", string(actual))
+
assert_eq(t, 3, offset)
+
assert_no_error(t, err)
+
+
actual, offset, err = getSlice(buf, 0, 4)
+
assert_eq(t, "", string(actual))
+
assert_eq(t, 3, offset)
+
assert_error(t, err)
+
}