Commit 701999f
2026-05-12 11:07:31
Changed files (7)
dns/answer.go
@@ -0,0 +1,33 @@
+package dns
+
+import "encoding/binary"
+
+// RR is a DNS Resource Record (an answer)
+// RFC 1035 section 3.2.1
+type RR struct {
+ Name string
+ Type uint16
+ Class uint16
+ TTL uint32 // Time to live in seconds
+ RData []byte // Actual record data, varies by type
+}
+
+// Pack serializes an RR to wire format
+func (rr *RR) Pack() []byte {
+ var buf []byte
+ buf = append(buf, PackDomain(rr.Name)...)
+
+ // Type, Class, TTL, RDLength, RData
+ tmp := make([]byte, 8)
+ binary.BigEndian.PutUint16(tmp[0:2], rr.Type)
+ binary.BigEndian.PutUint16(tmp[2:4], rr.Class)
+ binary.BigEndian.PutUint32(tmp[4:8], rr.TTL)
+ buf = append(buf, tmp...)
+
+ // RDLength + RData
+ rdLen := make([]byte, 2)
+ binary.BigEndian.PutUint16(rdLen, uint16(len(rr.RData)))
+ buf = append(buf, rdLen...)
+ buf = append(buf, rr.RData...)
+ return buf
+}
dns/domain.go
@@ -0,0 +1,96 @@
+package dns
+
+import (
+ "encoding/binary"
+ "fmt"
+ "strings"
+)
+
+// PackDomain encodes a domain name into DNS wire format
+// "ansari.wtf" → [6]ansari[3]wtf[0]
+func PackDomain(domain string) []byte {
+ // Remove trailing dot if present
+ domain = strings.TrimSuffix(domain, ".")
+
+ if domain == "" {
+ return []byte{0} // root
+ }
+
+ var buf []byte
+ labels := strings.SplitSeq(domain, ".")
+ for label := range labels {
+ if len(label) > 63 {
+ panic("label too long") // RFC limit: 63 chars per label
+ }
+ buf = append(buf, byte(len(label)))
+ buf = append(buf, []byte(label)...)
+ }
+ buf = append(buf, 0) // null terminator
+ return buf
+}
+
+// UnpackDomain decodes a DNS name from wire format
+// Returns the name and how many bytes were consumed
+// Also handles compression pointers (0xC0 prefix)
+func UnpackDomain(buf []byte, offset int) (string, int, error) {
+ var labels []string
+ visited := make(map[int]bool) // detect pointer loops
+ origOffset := offset
+ jumped := false
+ jumpOffset := 0
+
+ for {
+ if offset >= len(buf) {
+ return "", 0, fmt.Errorf("name parse out of bounds at offset %d", offset)
+ }
+
+ length := int(buf[offset])
+
+ // Check for compression pointer: top 2 bits are 11 (0xC0)
+ if length&0xC0 == 0xC0 {
+ if offset+1 >= len(buf) {
+ return "", 0, fmt.Errorf("compression pointer out of bounds")
+ }
+ // Pointer is 14-bit offset into the message
+ ptr := int(binary.BigEndian.Uint16(buf[offset:offset+2]) &^ 0xC000)
+
+ if visited[ptr] {
+ return "", 0, fmt.Errorf("compression pointer loop detected")
+ }
+ visited[ptr] = true
+
+ if !jumped {
+ jumpOffset = offset + 2 // after the pointer, this is where we resume
+ }
+ jumped = true
+ offset = ptr
+ continue
+ }
+
+ // Normal label
+ if length == 0 {
+ // End of name
+ offset++ // consume the null byte
+ break
+ }
+
+ offset++ // move past the length byte
+ if offset+length > len(buf) {
+ return "", 0, fmt.Errorf("label out of bounds")
+ }
+ labels = append(labels, string(buf[offset:offset+length]))
+ offset += length
+ }
+
+ name := strings.Join(labels, ".") + "."
+
+ // If we jumped, return the offset after the pointer (2 bytes)
+ // If we didn't jump, return where we ended up
+ consumed := offset - origOffset
+ if jumped {
+ consumed = jumpOffset - origOffset
+ }
+
+ return name, consumed, nil
+}
+
dns/header.go
@@ -0,0 +1,127 @@
+package dns
+
+import (
+ "encoding/binary"
+ "fmt"
+)
+
+/**
+Every DNS packet has this structure:
+
++---------------------+
+| Header | 12 bytes, always
++---------------------+
+| Question | variable
++---------------------+
+| Answer | variable (in responses)
++---------------------+
+| Authority | variable (we'll get to this)
++---------------------+
+| Additional | variable
++---------------------+
+
+1byte - 8bits
+Here its 12 bytes - 96 bits
+
+Which inturns split into 16bit per section so totally its 6 section as follows:
+
+0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
++--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+| ID |
++--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+|QR| Opcode |AA|TC|RD|RA| Z | RCODE |
++--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+| QDCOUNT |
++--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+| ANCOUNT |
++--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+| NSCOUNT |
++--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+| ARCOUNT |
++--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+
+ID - 16 Random ID, clients sets and server echoes
+QR - QUERY/RESPONSE 0/1
+Opcode - most case its 0 means 'QUERY'
+AA - AUTHORATIVE ANSWER - determine whether its from the owning zone source or from recursive resolver
+TC - Truncated - response is way big so try with TCP (0/1)
+RD - Recurion Desired - client asking server to recurse and give full resolution (0/1)
+RA - Recursion Available - whether server can recurse or not (0/1)
+RCODE - Response Code - 0 → OK, 3 → NXDOMAIN (domain doesn't exist), 2 → Server failure, 5 → Refused
+QDCOUNT,ANCOUNT,NSCOUNT,ARCOUNT - Number of Questions,Answers,Authority Records and Additional Records
+
+**/
+
+type Header struct {
+ ID uint16
+ Flags uint16
+ QDCount uint16
+ ANCount uint16
+ NSCount uint16
+ ARCount uint16
+}
+
+const (
+ FlagQR = 1 << 15
+ FlagAA = 1 << 10
+ FlagTC = 1 << 9
+ FlagRD = 1 << 8
+ FlagRA = 1 << 7
+ RCodeMask = 0x000F // Bottom 4 bits
+ OpcodeMask = 0x7800 // Bits 11-14
+)
+
+func (h *Header) IsItResponse() bool {
+ return h.Flags&FlagQR != 0
+}
+
+func (h *Header) SetResponse() {
+ h.Flags |= FlagQR
+}
+
+func (h *Header) SetAA() {
+ h.Flags |= FlagAA
+}
+
+func (h *Header) SetRD() {
+ h.Flags |= FlagRD
+}
+
+func (h *Header) SetRA() {
+ h.Flags |= FlagRA
+}
+
+func (h *Header) SetRCode(code uint8) {
+ h.Flags = (h.Flags &^ RCodeMask) | uint16(code)
+}
+
+func (h *Header) GetRCode() uint8 {
+ return uint8(h.Flags & RCodeMask)
+}
+
+// Pack serializes the header to 12 bytes (big-endian per RFC)
+func (h *Header) Pack() []byte {
+ buf := make([]byte, 12)
+ binary.BigEndian.PutUint16(buf[0:2], h.ID)
+ binary.BigEndian.PutUint16(buf[2:4], h.Flags)
+ binary.BigEndian.PutUint16(buf[4:6], h.QDCount)
+ binary.BigEndian.PutUint16(buf[6:8], h.ANCount)
+ binary.BigEndian.PutUint16(buf[8:10], h.NSCount)
+ binary.BigEndian.PutUint16(buf[10:12], h.ARCount)
+ return buf
+}
+
+// UnpackHeader parses a 12-byte DNS header
+func UnpackHeader(buf []byte) (*Header, error) {
+ if len(buf) < 12 {
+ return nil, fmt.Errorf("buffer too short for header: %d bytes", len(buf))
+ }
+ return &Header{
+ ID: binary.BigEndian.Uint16(buf[0:2]),
+ Flags: binary.BigEndian.Uint16(buf[2:4]),
+ QDCount: binary.BigEndian.Uint16(buf[4:6]),
+ ANCount: binary.BigEndian.Uint16(buf[6:8]),
+ NSCount: binary.BigEndian.Uint16(buf[8:10]),
+ ARCount: binary.BigEndian.Uint16(buf[10:12]),
+ }, nil
+}
dns/message.go
@@ -0,0 +1,58 @@
+package dns
+
+import "fmt"
+
+type Message struct {
+ Header Header
+ Questions []Question
+ Answers []RR
+}
+
+// Pack serializes the entire DNS message to wire format
+func (m *Message) Pack() ([]byte, error) {
+ var buf []byte
+
+ m.Header.QDCount = uint16(len(m.Questions))
+ m.Header.ANCount = uint16(len(m.Answers))
+ m.Header.NSCount = 0
+ m.Header.ARCount = 0
+
+ buf = append(buf, m.Header.Pack()...)
+
+ for _, q := range m.Questions {
+ buf = append(buf, q.Pack()...)
+ }
+
+ for _, rr := range m.Answers {
+ buf = append(buf, rr.Pack()...)
+ }
+
+ return buf, nil
+}
+
+// Unpack parses a raw DNS message from wire format
+func UnpackMessage(buf []byte) (*Message, error) {
+ if len(buf) < 12 {
+ return nil, fmt.Errorf("message too short: %d bytes", len(buf))
+ }
+
+ header, err := UnpackHeader(buf)
+ if err != nil {
+ return nil, err
+ }
+
+ msg := &Message{Header: *header}
+ offset := 12 // start after header
+
+ // Parse questions
+ for i := 0; i < int(header.QDCount); i++ {
+ q, consumed, err := UnpackQuestion(buf, offset)
+ if err != nil {
+ return nil, fmt.Errorf("question %d: %w", i, err)
+ }
+ msg.Questions = append(msg.Questions, *q)
+ offset += consumed
+ }
+
+ return msg, nil
+}
dns/question.go
@@ -0,0 +1,57 @@
+package dns
+
+import (
+ "encoding/binary"
+ "fmt"
+)
+
+// Record types per RFC 1035 + RFC 3596
+const (
+ TypeA = 1
+ TypeNS = 2
+ TypeCNAME = 5
+ TypeMX = 15
+ TypeTXT = 16
+ TypeAAAA = 28 // RFC 3596
+
+ ClassIN = 1 // Internet class
+)
+
+type Question struct {
+ Name string
+ Type uint16
+ Class uint16
+}
+
+// Pack serializes a question to wire format
+func (q *Question) Pack() []byte {
+ var buf []byte
+ buf = append(buf, PackDomain(q.Name)...)
+ buf = append(buf, 0, 0) // Type
+ buf = append(buf, 0, 0) // Class
+ binary.BigEndian.PutUint16(buf[len(buf)-4:], q.Type)
+ binary.BigEndian.PutUint16(buf[len(buf)-2:], q.Class)
+ return buf
+}
+
+// UnpackQuestion parses a question from wire format at offset
+func UnpackQuestion(buf []byte, offset int) (*Question, int, error) {
+ name, consumed, err := UnpackDomain(buf, offset)
+ if err != nil {
+ return nil, 0, fmt.Errorf("question name: %w", err)
+ }
+ offset += consumed
+
+ if offset+4 > len(buf) {
+ return nil, 0, fmt.Errorf("question too short for type/class")
+ }
+
+ qtype := binary.BigEndian.Uint16(buf[offset : offset+2])
+ qclass := binary.BigEndian.Uint16(buf[offset+2 : offset+4])
+
+ return &Question{
+ Name: name,
+ Type: qtype,
+ Class: qclass,
+ }, consumed + 4, nil
+}
dns/server.go
@@ -0,0 +1,82 @@
+package dns
+
+import (
+ "fmt"
+ "log"
+ "net"
+)
+
+type Handler func(req *Message) *Message
+
+type Server struct {
+ addr string
+ conn *net.UDPConn
+ handler Handler
+}
+
+func NewServer(addr string, handler Handler) *Server {
+ return &Server{
+ addr: addr,
+ handler: handler,
+ }
+}
+
+func (s *Server) Start() error {
+ udpAddr, err := net.ResolveUDPAddr("udp", s.addr)
+ if err != nil {
+ return fmt.Errorf("resolve addr: %w", err)
+ }
+
+ // Bind the UDP socket
+ conn, err := net.ListenUDP("udp", udpAddr)
+ if err != nil {
+ return fmt.Errorf("listen udp: %w", err)
+ }
+ s.conn = conn
+
+ log.Printf("DNS server listening on %s", s.addr)
+
+ // Read loop — runs forever
+ buf := make([]byte, 512) // RFC 1035: max UDP DNS message is 512 bytes
+ for {
+ n, clientAddr, err := conn.ReadFromUDP(buf)
+ if err != nil {
+ log.Printf("read error: %v", err)
+ continue
+ }
+
+ // Handle each request in a goroutine
+ // so slow requests don't block other clients
+ go s.handlePacket(buf[:n], clientAddr)
+ }
+}
+
+func (s *Server) handlePacket(buf []byte, clientAddr *net.UDPAddr) {
+ // Parse the incoming message
+ req, err := UnpackMessage(buf)
+ if err != nil {
+ log.Printf("failed to parse DNS message from %s: %v", clientAddr, err)
+ return
+ }
+
+ log.Printf("query from %s: %s type=%d", clientAddr, req.Questions[0].Name, req.Questions[0].Type)
+
+ // Call our handler to build a response
+ resp := s.handler(req)
+ if resp == nil {
+ return
+ }
+
+ // Serialize response to wire format
+ respBytes, err := resp.Pack()
+ if err != nil {
+ log.Printf("failed to pack response: %v", err)
+ return
+ }
+
+ // Send it back to the client
+ _, err = s.conn.WriteToUDP(respBytes, clientAddr)
+ if err != nil {
+ log.Printf("failed to send response to %s: %v", clientAddr, err)
+ }
+}
dns/txt.go
@@ -0,0 +1,66 @@
+package dns
+
+import "fmt"
+
+const (
+ maxTXTString = 255 // RFC 1035: each string max 255 bytes
+ maxUDPPayload = 512 // RFC 1035: max UDP DNS message
+)
+
+// PackTXT encodes a slice of strings into TXT RData wire format
+// Each string becomes a length-prefixed chunk
+// "hello" "world" → 0x05 h e l l o 0x05 w o r l d
+func PackTXT(strings []string) ([]byte, error) {
+ var rdata []byte
+
+ for _, s := range strings {
+ if len(s) > maxTXTString {
+ return nil, fmt.Errorf("TXT string exceeds 255 bytes: %d", len(s))
+ }
+ rdata = append(rdata, byte(len(s)))
+ rdata = append(rdata, []byte(s)...)
+ }
+
+ return rdata, nil
+}
+
+// ChunkText splits a long string into 255-byte chunks
+// ready to be passed into PackTXT
+func ChunkText(text string) []string {
+ var chunks []string
+
+ for len(text) > 0 {
+ if len(text) <= maxTXTString {
+ chunks = append(chunks, text)
+ break
+ }
+ // Try to split on a word boundary near the 255 limit
+ cutoff := maxTXTString
+ for cutoff > 200 && text[cutoff] != ' ' {
+ cutoff--
+ }
+ chunks = append(chunks, text[:cutoff])
+ text = text[cutoff+1:]
+ }
+
+ return chunks
+}
+
+// NewTXTRecord builds a complete RR with TXT type from a long string
+// It handles chunking automatically
+func NewTXTRecord(name string, ttl uint32, text string) (*RR, error) {
+ chunks := ChunkText(text)
+
+ rdata, err := PackTXT(chunks)
+ if err != nil {
+ return nil, fmt.Errorf("pack txt: %w", err)
+ }
+
+ return &RR{
+ Name: name,
+ Type: TypeTXT,
+ Class: ClassIN,
+ TTL: ttl,
+ RData: rdata,
+ }, nil
+}