Skip to content

Commit

Permalink
part1 done
Browse files Browse the repository at this point in the history
  • Loading branch information
hugo-gomes committed Jun 21, 2023
0 parents commit 9c6005f
Show file tree
Hide file tree
Showing 5 changed files with 273 additions and 0 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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/
151 changes: 151 additions & 0 deletions dnsweekend/part1.go
Original file line number Diff line number Diff line change
@@ -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
}
107 changes: 107 additions & 0 deletions dnsweekend/part1_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module dns-weekend

go 1.20.5
9 changes: 9 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package main

import (
"dns-weekend/dnsweekend"
"encoding/hex"
"fmt"
"os"
)

0 comments on commit 9c6005f

Please sign in to comment.