Skip to content

Commit

Permalink
initial
Browse files Browse the repository at this point in the history
  • Loading branch information
mateusz834 committed May 11, 2023
1 parent c5a1d96 commit 4cdcd8a
Show file tree
Hide file tree
Showing 7 changed files with 1,396 additions and 0 deletions.
26 changes: 26 additions & 0 deletions binary.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package dnsmsg

//TODO: use binary.BigEndian in entire package (because of lower cost (golang/go#42958)) (but probably after golang/go#54097 gets fixed)

func unpackUint16(b []byte) uint16 {
_ = b[1]
return uint16(b[0])<<8 | uint16(b[1])
}

func unpackUint32(b []byte) uint32 {
_ = b[3]
return uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3])
}

func appendUint16(b []byte, v uint16) []byte {
return append(b, byte(v>>8), byte(v))
}

func appendUint32(b []byte, v uint32) []byte {
return append(b,
byte(v>>24),
byte(v>>16),
byte(v>>8),
byte(v),
)
}
192 changes: 192 additions & 0 deletions builder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
package dnsmsg

import (
"errors"
)

func MakeQuery[T name](msg []byte, id uint16, flags Flags, q Question[T]) []byte {
// Header
msg = appendUint16(msg, id)
msg = appendUint16(msg, uint16(flags))
msg = appendUint16(msg, 1)
msg = appendUint16(msg, 0)
msg = appendUint16(msg, 0)
msg = appendUint16(msg, 0)

// Question
appendName(msg, q.Name)

msg = appendUint16(msg, uint16(q.Type))
msg = appendUint16(msg, uint16(q.Class))
return msg
}

func MakeQueryWithEDNS0[T name](msg []byte, id uint16, flags Flags, q Question[T], ends0 EDNS0) []byte {
// Header
msg = appendUint16(msg, id)
msg = appendUint16(msg, uint16(flags))
msg = appendUint16(msg, 1)
msg = appendUint16(msg, 0)
msg = appendUint16(msg, 0)
msg = appendUint16(msg, 1)

// Question
appendName(msg, q.Name)
msg = appendUint16(msg, uint16(q.Type))
msg = appendUint16(msg, uint16(q.Class))

// EDNS0
msg = append(msg, 0) // root name
msg = appendUint16(msg, uint16(TypeOPT))
msg = appendUint16(msg, ends0.Payload)

// TODO: support rest of EDNS0 stuff.
msg = appendUint32(msg, 0)
msg = appendUint16(msg, 0)
return msg
}

func appendName[T name](buf []byte, n T) []byte {
switch n := any(n).(type) {
case Name:
return appendEscapedName(buf, n.name)
case ParserName:
return n.appendRawName(buf)
default:
panic("appendName: unsupported name type")
}
}

var errInvalidName = errors.New("invalid name")

type Name struct {
name string
}

func NewName(name string) (Name, error) {
if !isValidEscapedName(name) {
return Name{}, errInvalidName
}
return Name{name: name}, nil
}

func isValidEscapedName(m string) bool {
if m == "" {
return false
}

if m == "." {
return true
}

labelLength := 0
nameLength := 0
inEscape := false
rooted := false

for i := 0; i < len(m); i++ {
char := m[i]
rooted = false

switch char {
case '.':
if inEscape {
labelLength++
inEscape = false
continue
}
if labelLength == 0 || labelLength > maxLabelLength {
return false
}
rooted = true
nameLength += labelLength + 1
labelLength = 0
case '\\':
inEscape = !inEscape
if !inEscape {
labelLength++
}
default:
if inEscape && isDigit(char) {
if len(m[i:]) < 3 || !isDigit(m[i+1]) || !isDigit(m[i+2]) {
return false
}
if _, ok := decodeDDD([3]byte{char, m[i+1], m[i+2]}); !ok {
return false
}
i += 2
}
inEscape = false
labelLength++
}
}

nameLength += labelLength

if inEscape {
return false
}

if nameLength > 254 || nameLength == 254 && !rooted {
return false
}

return true
}

func appendEscapedName(buf []byte, m string) []byte {
labelLength := byte(0)

labelIndex := len(buf)
buf = append(buf, 0)

for i := 0; i < len(m); i++ {
char := m[i]
switch char {
case '.':
buf[labelIndex] = labelLength
labelLength = 0
labelIndex = len(buf)
buf = append(buf, 0)
case '\\':
if isDigit(m[i+1]) {
labelLength++
ddd, _ := decodeDDD([3]byte{m[i+1], m[i+2], m[i+3]})
buf = append(buf, ddd)
i += 3
continue
}
buf = append(buf, m[i+1])
i += 1
labelLength++
default:
labelLength++
buf = append(buf, char)
}
}

if labelLength != 0 {
buf[labelIndex] = labelLength
}

if buf[len(buf)-1] != 0 {
buf = append(buf, 0)
}

return buf
}

func isDigit(char byte) bool {
return char >= '0' && char <= '9'
}

func decodeDDD(ddd [3]byte) (uint8, bool) {
ddd[0] -= '0'
ddd[1] -= '0'
ddd[2] -= '0'
num := uint16(ddd[0])*100 + uint16(ddd[1])*10 + uint16(ddd[2])
if num > 255 {
return 0, false
}
return uint8(num), true
}
145 changes: 145 additions & 0 deletions builder_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package dnsmsg

import (
"fmt"
"strings"
"testing"
)

var (
escapes = "\\.\\223\\.\\\\"
escapesCharCount = 4
label54 = escapes + strings.Repeat("a", 54-2*escapesCharCount) + escapes
label63 = escapes + strings.Repeat("a", 63-2*escapesCharCount) + escapes
label64 = escapes + strings.Repeat("a", 64-2*escapesCharCount) + escapes
)

var newNameTests = []struct {
name string
ok bool
diferentAsString bool
}{
{name: "", ok: false},
{name: ".", ok: true},
{name: "com.", ok: true},
{name: "com", ok: true},

{name: "go.dev", ok: true},
{name: "go.dev.", ok: true},
{name: "www.go.dev", ok: true},
{name: "www.go.dev.", ok: true},

{name: "www..go.dev", ok: false},
{name: ".www.go.dev", ok: false},
{name: "..www.go.dev", ok: false},
{name: "www.go.dev..", ok: false},

{name: "www.go.dev\\.", ok: true},
{name: "www.go.dev\\..", ok: true},
{name: "www.go.dev\\...", ok: false},
{name: "www\\..go.dev", ok: true},
{name: "www\\...go.dev", ok: false},

{name: "\\\\www.go.dev.", ok: true},
{name: "\\\\www.go.dev.", ok: true},
{name: "www.go.dev\\\\\\.", ok: true},
{name: "www.go.dev\\\\\\.", ok: true},
{name: "\\ww\\ w.go.dev", ok: true, diferentAsString: true},
{name: "ww\\w.go.dev", ok: true, diferentAsString: true},
{name: "www.go.dev\\\\", ok: true},

{name: "\\223www.go.dev", ok: true},
{name: "\\000www.go.dev", ok: true},
{name: "\\255www.go.dev", ok: true},

{name: "\\256www.go.dev", ok: false},
{name: "\\999www.go.dev", ok: false},
{name: "\\12www.go.dev", ok: false},
{name: "\\1www.go.dev", ok: false},
{name: "www.go.dev\\223", ok: true},
{name: "www.go.dev\\12", ok: false},
{name: "www.go.dev\\1", ok: false},
{name: "www.go.dev\\", ok: false},

{name: label63 + ".go.dev", ok: true},
{name: label64 + ".go.dev", ok: false},

// 253B non-rooted name.
{
name: fmt.Sprintf("%[1]v.%[1]v.%[1]v.%v.go.dev", label63, label54),
ok: true,
},

// 254B rooted name.
{
name: fmt.Sprintf("%[1]v.%[1]v.%[1]v.%v.go.dev.", label63, label54),
ok: true,
},

// 254B non-rooted name.
{
name: fmt.Sprintf("%[1]v.%[1]v.%[1]v.%va.go.dev", label63, label54),
ok: false,
},

// 255B rooted name.
{
name: fmt.Sprintf("%[1]v.%[1]v.%[1]v.%va.go.dev.", label63, label54),
ok: false,
},
}

func TestNewName(t *testing.T) {
for _, v := range newNameTests {
_, err := NewName(v.name)
expectErr := errInvalidName
if v.ok {
expectErr = nil
}
if expectErr != err {
t.Errorf("'%v' got error: %v, expected: %v", v.name, err, expectErr)
}
}
}

func TestAppendEscapedName(t *testing.T) {
for _, v := range newNameTests {
n, err := NewName(v.name)
if err != nil {
continue
}

packedName := appendEscapedName(nil, v.name)

p, err := NewParser(packedName)
if err != nil {
continue
}

name := ParserName{m: &p, nameStart: 0}
_, err = name.unpack()
if err != nil {
t.Errorf("'%v' failed while unpacking packed name: %v\n\traw: %v", v.name, err, packedName)
continue
}

if !name.EqualName(n) {
t.Errorf("'%v' ParserName is not equal to name\n\traw: %v", v.name, packedName)
continue
}

if v.diferentAsString {
continue
}

expectName := v.name
dotAtEnd := expectName[len(expectName)-1] == '.'
if !dotAtEnd || (len(expectName) > 2 && dotAtEnd && expectName[len(expectName)-2] == '\\') {
expectName += "."
}

if name := name.String(); name != expectName {
t.Errorf("'%v' got name: %v, expected: %v\n\traw: %v", v.name, name, expectName, packedName)
}
}
}
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/mateusz834/dnsmsg

go 1.20
Loading

0 comments on commit 4cdcd8a

Please sign in to comment.