From 9c6005f01cb239c34154be5dce465698d8cf3338 Mon Sep 17 00:00:00 2001 From: Hugo Gomes Date: Wed, 21 Jun 2023 23:50:12 +0100 Subject: [PATCH] part1 done --- README.md | 3 + dnsweekend/part1.go | 151 +++++++++++++++++++++++++++++++++++++++ dnsweekend/part1_test.go | 107 +++++++++++++++++++++++++++ go.mod | 3 + main.go | 9 +++ 5 files changed, 273 insertions(+) create mode 100644 README.md create mode 100644 dnsweekend/part1.go create mode 100644 dnsweekend/part1_test.go create mode 100644 go.mod create mode 100644 main.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..456876f --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Implement DNS in a weekend + +The code for Implement DNS in a Weekend. DNS in a Weekend is a project by Julia Evans', more info at https://implement-dns.wizardzines.com/ diff --git a/dnsweekend/part1.go b/dnsweekend/part1.go new file mode 100644 index 0000000..765f55b --- /dev/null +++ b/dnsweekend/part1.go @@ -0,0 +1,151 @@ +// Package dnsweekend https://implement-dns.wizardzines.com/ +package dnsweekend + +import ( + "bytes" + "encoding/binary" + "math/rand" + "net" + "strings" +) + +// RecordType is a 16 bit field +type RecordType uint16 + +const ( + A RecordType = 1 // A is a host address + NS RecordType = 2 // NS is an authoritative name server + AAAA RecordType = 28 // AAAA is a IPv6 host address +) + +// RecordClass is a 16 bit field +type RecordClass uint16 + +const ( + IN RecordClass = 1 // IN for Internet +) + +// DNSHeader (12 bytes) +type DNSHeader struct { + ID uint16 // ID + Flags uint16 // Flags (QR - 1 bit, Opcode - 4 bits, AA - 1 bit, TC - 1 bit, RD - 1 bit, RA - 1 bit, Z - 3 bits, RCODE - 4 bits) + QDCount uint16 // Question Count + ANCount uint16 // Answer Count + NSCount uint16 // Name Server Count + ARCount uint16 // Additional Record Count +} + +// DNSQuestion (variable length) +type DNSQuestion struct { + Name []byte // Name + Type uint16 // Type (A, AAAA, MX, NS, etc.) + Class uint16 // Class (IN, CH, HS, etc.) +} + +func (h *DNSHeader) toBytes() []byte { + bytes := make([]byte, 12) + binary.BigEndian.PutUint16(bytes[0:2], h.ID) + binary.BigEndian.PutUint16(bytes[2:4], h.Flags) + binary.BigEndian.PutUint16(bytes[4:6], h.QDCount) + binary.BigEndian.PutUint16(bytes[6:8], h.ANCount) + binary.BigEndian.PutUint16(bytes[8:10], h.NSCount) + binary.BigEndian.PutUint16(bytes[10:12], h.ARCount) + return bytes +} + +func (h *DNSHeader) setFlags(qr uint16, opcode uint16, aa uint16, tc uint16, rd uint16, ra uint16, z uint16, rcode uint16) { + const ( + QRBit = 15 + OpCodeBit = 11 + AAbit = 10 + TCbit = 9 + RDbit = 8 + RAbit = 7 + Zbit = 4 + ) + + h.Flags = (qr << QRBit) | + (opcode << OpCodeBit) | + (aa << AAbit) | + (tc << TCbit) | + (rd << RDbit) | + (ra << RAbit) | + (z << Zbit) | + rcode +} + +func (q *DNSQuestion) toBytes() []byte { + nameLength := len(q.Name) + questionSize := nameLength + 4 // 4 bytes for Type and Class + bytes := make([]byte, questionSize) + + copy(bytes, q.Name) + binary.BigEndian.PutUint16(bytes[nameLength:], q.Type) + binary.BigEndian.PutUint16(bytes[nameLength+2:], q.Class) + return bytes +} + +// DNS names are encoded as a sequence of labels, +// where each label consists of a length octet +// followed by that number of octets, and terminated +// e.g. www.google.com -> 3www6google3com +func encodeDNSName(domain string) []byte { + buffer := new(bytes.Buffer) + labels := strings.Split(domain, ".") + for _, label := range labels { + buffer.WriteByte(byte(len(label))) + buffer.WriteString(label) + } + buffer.WriteByte(0) + return buffer.Bytes() +} + +func buildQuery(domain string, recursionDesired bool, recordType RecordType) []byte { + id := rand.Intn(65535) + + header := DNSHeader{ + ID: uint16(id), + Flags: 0, + QDCount: 1, + } + + // Set RD flag to 1 if recursionDesired is true + if recursionDesired { + header.setFlags(0, 0, 0, 0, 1, 0, 0, 0) + } + + question := DNSQuestion{ + Name: encodeDNSName(domain), + Type: uint16(recordType), + Class: uint16(IN), + } + + buffer := new(bytes.Buffer) + buffer.Write(header.toBytes()) + buffer.Write(question.toBytes()) + return buffer.Bytes() +} + +// SendQuery sends a DNS query to Google's public DNS server +// and returns the response as a byte slice (or an error) +func SendQuery(domain string, nameserver string, recordType RecordType) ([]byte, error) { + conn, err := net.Dial("udp", nameserver) + if err != nil { + return nil, err + } + defer conn.Close() + + query := buildQuery(domain, false, recordType) + _, err = conn.Write(query) + if err != nil { + return nil, err + } + + resp := make([]byte, 1024) + _, err = conn.Read(resp) + if err != nil { + return nil, err + } + + return resp, nil +} diff --git a/dnsweekend/part1_test.go b/dnsweekend/part1_test.go new file mode 100644 index 0000000..1d2dd8e --- /dev/null +++ b/dnsweekend/part1_test.go @@ -0,0 +1,107 @@ +package dnsweekend + +import ( + "encoding/hex" + "reflect" + "testing" +) + +func TestDNSHeaderToBytes(t *testing.T) { + header := DNSHeader{ + ID: 0x1314, + Flags: 0, + QDCount: 1, + ANCount: 0, + NSCount: 0, + ARCount: 0, + } + + bytes := header.toBytes() + + expectedBytes := []byte{ + 0x13, 0x14, + 0x00, 0x00, + 0x00, 0x01, + 0x00, 0x00, + 0x00, 0x00, + 0x00, 0x00, + } + + if !reflect.DeepEqual(bytes, expectedBytes) { + t.Errorf("Expected %v, but got %v", expectedBytes, bytes) + } +} + +func TestDNSHeaderSetFlags(t *testing.T) { + header := DNSHeader{} + + header.setFlags(0, 0, 0, 0, 1, 0, 0, 0) + expectedFlags := uint16(0b100000000) // Binary: 1000111111111111 + if header.Flags != expectedFlags { + t.Errorf("Expected Flags to be %v, but got %v", expectedFlags, header.Flags) + } +} + +func TestEncodeDNSName(t *testing.T) { + tests := []struct { + domain string + expectedName []byte + }{ + { + domain: "www.google.com", + expectedName: []byte{3, 'w', 'w', 'w', 6, 'g', 'o', 'o', 'g', 'l', 'e', 3, 'c', 'o', 'm', 0}, + }, + { + domain: "example.com", + expectedName: []byte{7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0}, + }, + { + domain: "dnsweekend.com", + expectedName: []byte{10, 'd', 'n', 's', 'w', 'e', 'e', 'k', 'e', 'n', 'd', 3, 'c', 'o', 'm', 0}, + }, + } + + for _, test := range tests { + actualName := encodeDNSName(test.domain) + if !reflect.DeepEqual(actualName, test.expectedName) { + t.Errorf("Expected %v, but got %v", test.expectedName, actualName) + } + } +} + +func TestDNSQuestionToBytes(t *testing.T) { + question := DNSQuestion{ + Name: []byte{3, 'w', 'w', 'w', 6, 'g', 'o', 'o', 'g', 'l', 'e', 3, 'c', 'o', 'm'}, + Type: 1, + Class: 1, + } + + bytes := question.toBytes() + + expectedBytes := []byte{ + 3, 'w', 'w', 'w', + 6, 'g', 'o', 'o', 'g', 'l', 'e', + 3, 'c', 'o', 'm', + 0, 1, + 0, 1, + } + + if !reflect.DeepEqual(bytes, expectedBytes) { + t.Errorf("Expected %v, but got %v", expectedBytes, bytes) + } +} + +func TestSendQuery(t *testing.T) { + domain := "www.example.com" + resp, err := SendQuery(domain, "8.8.8.8:53", A) + if err != nil { + panic(err) + } + str := hex.EncodeToString(resp) + + // Run `sudo tcpdump -ni any port 53` + // And check that the output is in line with + // 08:31:19.676059 IP 192.168.1.173.62752 > 8.8.8.8.53: 45232+ A? www.example.com. (33) + // 08:31:19.694678 IP 8.8.8.8.53 > 192.168.1.173.62752: 45232 1/0/0 A 93.184.216.34 (49) + t.Logf("%s\n", str) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f260137 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module dns-weekend + +go 1.20.5 diff --git a/main.go b/main.go new file mode 100644 index 0000000..e27b35f --- /dev/null +++ b/main.go @@ -0,0 +1,9 @@ +package main + +import ( + "dns-weekend/dnsweekend" + "encoding/hex" + "fmt" + "os" +) +