-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 9c6005f
Showing
5 changed files
with
273 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
module dns-weekend | ||
|
||
go 1.20.5 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package main | ||
|
||
import ( | ||
"dns-weekend/dnsweekend" | ||
"encoding/hex" | ||
"fmt" | ||
"os" | ||
) | ||
|