1package dns
2
3import (
4 "encoding/binary"
5 "fmt"
6 "strings"
7)
8
9// PackDomain encodes a domain name into DNS wire format
10// "ansari.wtf" → [6]ansari[3]wtf[0]
11func PackDomain(domain string) []byte {
12 // Remove trailing dot if present
13 domain = strings.TrimSuffix(domain, ".")
14
15 if domain == "" {
16 return []byte{0} // root
17 }
18
19 var buf []byte
20 labels := strings.SplitSeq(domain, ".")
21 for label := range labels {
22 if len(label) > 63 {
23 panic("label too long") // RFC limit: 63 chars per label
24 }
25 buf = append(buf, byte(len(label)))
26 buf = append(buf, []byte(label)...)
27 }
28 buf = append(buf, 0) // null terminator
29 return buf
30}
31
32// UnpackDomain decodes a DNS name from wire format
33// Returns the name and how many bytes were consumed
34// Also handles compression pointers (0xC0 prefix)
35func UnpackDomain(buf []byte, offset int) (string, int, error) {
36 var labels []string
37 visited := make(map[int]bool) // detect pointer loops
38 origOffset := offset
39 jumped := false
40 jumpOffset := 0
41
42 for {
43 if offset >= len(buf) {
44 return "", 0, fmt.Errorf("name parse out of bounds at offset %d", offset)
45 }
46
47 length := int(buf[offset])
48
49 // Check for compression pointer: top 2 bits are 11 (0xC0)
50 if length&0xC0 == 0xC0 {
51 if offset+1 >= len(buf) {
52 return "", 0, fmt.Errorf("compression pointer out of bounds")
53 }
54 // Pointer is 14-bit offset into the message
55 ptr := int(binary.BigEndian.Uint16(buf[offset:offset+2]) &^ 0xC000)
56
57 if visited[ptr] {
58 return "", 0, fmt.Errorf("compression pointer loop detected")
59 }
60 visited[ptr] = true
61
62 if !jumped {
63 jumpOffset = offset + 2 // after the pointer, this is where we resume
64 }
65 jumped = true
66 offset = ptr
67 continue
68 }
69
70 // Normal label
71 if length == 0 {
72 // End of name
73 offset++ // consume the null byte
74 break
75 }
76
77 offset++ // move past the length byte
78 if offset+length > len(buf) {
79 return "", 0, fmt.Errorf("label out of bounds")
80 }
81 labels = append(labels, string(buf[offset:offset+length]))
82 offset += length
83 }
84
85 name := strings.Join(labels, ".") + "."
86
87 // If we jumped, return the offset after the pointer (2 bytes)
88 // If we didn't jump, return where we ended up
89 consumed := offset - origOffset
90 if jumped {
91 consumed = jumpOffset - origOffset
92 }
93
94 return name, consumed, nil
95}
96