testing and refactors - i guess, should be looked over, also maybe benchmarks? but that can wait, as can the person requesting from this server

This commit is contained in:
LovroG05
2025-03-15 12:45:42 +01:00
parent 9f55f64577
commit 34cbb2dd55
10 changed files with 706 additions and 99 deletions

View File

@@ -4,10 +4,49 @@ This is my simple, barebones, **insecure** implementation of a simple DNS server
For now, it successfully recursively resolves A records. For now, it successfully recursively resolves A records.
Sends through correct TTL values. ## Features
### DNS Message Parsing & Handling
- [x] Can parse and respond to valid DNS queries
- [ ] Correctly handles headers (ID, flags, opcode, RCODE)
- [ ] Supports question section processing
- [ ] Correctly formats answers, authority, and additional sections
### Supported Query Types
- [x] A (IPv4 address)
- [ ] AAAA (IPv6 address)
- [ ] CNAME (Canonical name)
- [ ] NS (Nameserver)
- [ ] MX (Mail Exchange)
- [ ] TXT (Text records)
- [ ] PTR (Reverse lookups)
- [ ] SOA (Start of Authority)
### Query Processing
- [ ] Supports recursive queries (if implemented)
- [ ] Supports iterative queries (if acting as authoritative server)
- [ ] Correctly handles RD (Recursion Desired) flag
- [ ] Supports negative responses (NXDOMAIN, NODATA)
- [x] Supports wildcards (*.example.com)
### Caching & TTL
- [x] Correctly applies TTL from authoritative responses
- [x] Handles cache expiration and eviction
### Error Handling
- [ ] Responds correctly to malformed queries
- [ ] Returns correct RCODE values (e.g., SERVFAIL, REFUSED, NXDOMAIN)
- [ ] Handles timeouts and retransmissions
## Testing ## Testing
`go test dnsthingymagik/tests`
`dig @serverip domain.tld` `dig @serverip domain.tld`
Example: Example:

98
main.go
View File

@@ -1,106 +1,16 @@
package main package main
import ( import (
"dnsthingymagik/recordcache" "dnsthingymagik/server"
"dnsthingymagik/resolver"
"dnsthingymagik/resolver/entities"
"fmt"
"golang.org/x/net/dns/dnsmessage"
"log" "log"
"net"
"sync"
) )
func main() { func main() {
udpServer, err := net.ListenPacket("udp", ":53") s, err := server.NewServer(":53")
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
defer func(udpServer net.PacketConn) { defer s.Close()
err := udpServer.Close()
if err != nil {
log.Fatal(err)
}
}(udpServer)
cache := recordcache.NewCache() s.Start()
var wg sync.WaitGroup
for {
buf := make([]byte, 514)
n, addr, err := udpServer.ReadFrom(buf)
if err != nil {
log.Println(err)
}
wg.Add(1)
go process(udpServer, addr, buf[:n], cache, &wg)
}
//recordCache.recordcache.PurgeExpiredEntries()
}
func process(udp net.PacketConn, addr net.Addr, buf []byte, cache *recordcache.Cache, wg *sync.WaitGroup) {
defer wg.Done()
msg, err := resolver.PacketParser(buf)
if err != nil {
log.Println(err)
}
var faq []entities.Record
for _, q := range msg.Questions {
if q.Type == dnsmessage.TypeA {
nips, err := resolver.ResolveDN(q.Name, msg.Header.ID, dnsmessage.TypeA, cache)
if err != nil {
log.Println(err)
}
if len(nips) == 0 {
log.Printf("No IP found for %s", q.Name.String())
continue
}
faq = append(faq, nips...)
}
}
var answers []dnsmessage.Resource
for _, record := range faq {
answers = append(answers, dnsmessage.Resource{
Header: dnsmessage.ResourceHeader{
Name: record.Name,
Type: record.RType,
Class: record.Class,
TTL: record.TTL,
},
Body: &dnsmessage.AResource{
A: [4]byte{record.IP[0], record.IP[1], record.IP[2], record.IP[3]},
},
})
}
response := dnsmessage.Message{
Header: dnsmessage.Header{
ID: msg.Header.ID,
Response: true,
OpCode: msg.Header.OpCode,
Authoritative: false,
RecursionDesired: msg.Header.RecursionDesired,
RecursionAvailable: true,
RCode: dnsmessage.RCodeSuccess,
},
Questions: msg.Questions,
Answers: answers,
}
fmt.Println(response)
packed, err := response.Pack()
if err != nil {
log.Println(err)
}
_, err = udp.WriteTo(packed, addr)
if err != nil {
return
}
} }

153
server/Server.go Normal file
View File

@@ -0,0 +1,153 @@
package server
import (
"context"
"dnsthingymagik/server/recordcache"
"dnsthingymagik/server/resolver"
"dnsthingymagik/server/resolver/entities"
"golang.org/x/net/dns/dnsmessage"
"log"
"net"
"sync"
)
type Server struct {
udpServer net.PacketConn
cache *recordcache.Cache
wg sync.WaitGroup
shutdown context.CancelFunc
ctx context.Context
}
func NewServer(address string) (*Server, error) {
// Create context for shutdown
ctx, cancel := context.WithCancel(context.Background())
udpServer, err := net.ListenPacket("udp", address)
if err != nil {
return nil, err
}
return &Server{
udpServer: udpServer,
cache: recordcache.NewCache(),
ctx: ctx,
shutdown: cancel,
}, nil
}
// Start the DNS server to listen for queries.
func (s *Server) Start() {
log.Println("Starting DNS server on", s.udpServer.LocalAddr())
for {
select {
case <-s.ctx.Done():
// If the server is shutting down, stop accepting new requests
log.Println("Server shutting down...")
return
default:
buf := make([]byte, 514)
n, addr, err := s.udpServer.ReadFrom(buf)
if err != nil {
if netErr, ok := err.(*net.OpError); ok && netErr.Op == "read" {
// If the connection is closed, exit the loop
log.Println("Error reading from UDP connection, shutting down...")
return
}
log.Println("Error reading UDP packet:", err)
continue
}
s.wg.Add(1)
go s.process(addr, buf[:n])
}
}
}
// Gracefully close the server and all pending requests.
func (s *Server) Close() {
// Signal shutdown and close the UDP server connection
s.shutdown()
// Wait for all ongoing requests to be processed
s.wg.Wait()
err := s.udpServer.Close()
if err != nil {
log.Fatal("Error closing UDP server:", err)
}
log.Println("Server shut down gracefully")
}
// Process a single DNS query request.
func (s *Server) process(addr net.Addr, buf []byte) {
defer s.wg.Done()
// Parse incoming DNS query
msg, err := resolver.PacketParser(buf)
if err != nil {
log.Printf("Packet parsing error from %s: %v", addr, err)
return
}
var records []entities.Record
for _, q := range msg.Questions {
if q.Type == dnsmessage.TypeA {
nips, err := resolver.ResolveDN(q.Name, msg.Header.ID, dnsmessage.TypeA, s.cache)
if err != nil {
log.Printf("Resolution error from %s for %s: %v", addr, q.Name, err)
continue
}
if len(nips) == 0 {
log.Printf("No IP found for %s from %s", q.Name.String(), addr)
continue
}
records = append(records, nips...)
}
}
// Build DNS answer section
var answers []dnsmessage.Resource
for _, record := range records {
answers = append(answers, dnsmessage.Resource{
Header: dnsmessage.ResourceHeader{
Name: record.Name,
Type: record.RType,
Class: record.Class,
TTL: record.TTL,
},
Body: &dnsmessage.AResource{
A: [4]byte{record.IP[0], record.IP[1], record.IP[2], record.IP[3]},
},
})
}
// Prepare the response message
response := dnsmessage.Message{
Header: dnsmessage.Header{
ID: msg.Header.ID,
Response: true,
OpCode: msg.Header.OpCode,
Authoritative: false,
RecursionDesired: msg.Header.RecursionDesired,
RecursionAvailable: true,
RCode: dnsmessage.RCodeSuccess,
},
Questions: msg.Questions,
Answers: answers,
}
// Pack the response
packed, err := response.Pack()
if err != nil {
log.Printf("Response packing error for %s: %v", addr, err)
return
}
// Send the response back to the client
_, err = s.udpServer.WriteTo(packed, addr)
if err != nil {
log.Printf("Response write error to %s: %v", addr, err)
}
}

View File

@@ -1,7 +1,7 @@
package recordcache package recordcache
import ( import (
"dnsthingymagik/resolver/entities" "dnsthingymagik/server/resolver/entities"
"fmt" "fmt"
"golang.org/x/net/dns/dnsmessage" "golang.org/x/net/dns/dnsmessage"
"sync" "sync"

View File

@@ -1,9 +1,9 @@
package resolver package resolver
import ( import (
"dnsthingymagik/recordcache" "dnsthingymagik/server/recordcache"
"dnsthingymagik/resolver/entities" "dnsthingymagik/server/resolver/entities"
"dnsthingymagik/resolver/query" "dnsthingymagik/server/resolver/query"
"golang.org/x/net/dns/dnsmessage" "golang.org/x/net/dns/dnsmessage"
"log" "log"
"net" "net"

69
tests/QueryDNS.go Normal file
View File

@@ -0,0 +1,69 @@
package tests
import (
"dnsthingymagik/server"
"golang.org/x/net/dns/dnsmessage"
"net"
"testing"
"time"
)
func sendDNSQuery(t *testing.T, serverAddr, domain string, qType dnsmessage.Type, autostart bool) dnsmessage.Message {
if autostart {
s := startTestServer(t)
defer s.Close()
}
t.Helper()
conn, err := net.Dial("udp", serverAddr)
if err != nil {
t.Fatalf("Failed to connect to DNS server: %v", err)
}
defer conn.Close()
var msg dnsmessage.Message
msg.Header.ID = 1234
msg.Header.RecursionDesired = true
msg.Questions = []dnsmessage.Question{
{
Name: dnsmessage.MustNewName(domain),
Type: qType,
Class: dnsmessage.ClassINET,
},
}
query, err := msg.Pack()
if err != nil {
t.Fatalf("Failed to pack DNS query: %v", err)
}
_, err = conn.Write(query)
if err != nil {
t.Fatalf("Failed to send DNS query: %v", err)
}
buf := make([]byte, 512)
n, err := conn.Read(buf)
if err != nil {
t.Fatalf("Failed to read DNS response: %v", err)
}
var response dnsmessage.Message
err = response.Unpack(buf[:n])
if err != nil {
t.Fatalf("Failed to unpack DNS response: %v", err)
}
return response
}
// Setup Test Server
func startTestServer(t *testing.T) *server.Server {
server, err := server.NewServer(":53")
if err != nil {
t.Fatalf("Failed to start DNS server: %v", err)
}
go func() { server.Start() }()
time.Sleep(500 * time.Millisecond) // Allow server to initialize
return server
}

View File

@@ -0,0 +1,436 @@
package tests
import (
"golang.org/x/net/dns/dnsmessage"
"net"
"testing"
"time"
)
func Test_CorrectlyHandlesHeaders(t *testing.T) {
// Send a simple DNS query for a known domain
response := sendDNSQuery(t, "127.0.0.1:53", "govekar.net.", dnsmessage.TypeA, true)
if response.Header.RCode != dnsmessage.RCodeSuccess {
t.Errorf("Expected RCODE 0 (NoError), got %d", response.Header.RCode)
}
// Check that the query ID is the same as the request ID
if response.Header.ID != 12345 { // Replace with the ID used in your request (or dynamic query ID)
t.Errorf("Expected query ID to be 12345, but got %d", response.Header.ID)
}
// Check that the query response (qr) flag is set to 1, meaning it's a response
if response.Header.Response != true {
t.Errorf("Expected qr (query/response) flag to be set to 1, but it was 0")
}
// Check that the recursion desired (rd) flag is set to 1 in the query
if response.Header.RecursionDesired != true {
t.Errorf("Expected rd (recursion desired) flag to be set to 1, but it was 0")
}
// Check that recursion available (ra) flag is set to 1 in the response (assuming recursion is available)
if response.Header.RecursionAvailable != true {
t.Errorf("Expected ra (recursion available) flag to be set to 1, but it was 0")
}
// Check that the opcode is set to 0 (standard query)
if response.Header.OpCode != 0 {
t.Errorf("Expected opcode to be 0 (QUERY), but got %d", response.Header.OpCode)
}
// Optionally, check for the other flags and ensure that the query is well-formed
// (e.g., authority and additional sections should be empty in a standard query with no answer)
if len(response.Authorities) > 0 {
t.Errorf("Expected no authority section, but found %d entries", len(response.Authorities))
}
if len(response.Additionals) > 0 {
t.Errorf("Expected no additional section, but found %d entries", len(response.Additionals))
}
}
func Test_CorrectlyFormatsSections(t *testing.T) {
// Send a DNS query for a known domain (e.g., "govekar.net")
response := sendDNSQuery(t, "127.0.0.1:53", "govekar.net.", dnsmessage.TypeA, true)
// Test that the answer section is properly populated with an A record
if len(response.Answers) == 0 {
t.Error("Expected at least one answer, but got none")
} else {
// Check that the answer is an A record (IPv4 address)
for _, ans := range response.Answers {
if ans.Header.Type != dnsmessage.TypeA {
t.Errorf("Expected answer type to be A, but got %d", ans.Header.Type)
}
}
}
// Test the authority section, which should contain authoritative name server records if the server is authoritative
if len(response.Authorities) > 0 {
// Ensure that the authority section contains NS (Name Server) records
for _, auth := range response.Authorities {
if auth.Header.Type != dnsmessage.TypeNS {
t.Errorf("Expected authority record type to be NS, but got %d", auth.Header.Type)
}
}
} else {
t.Log("No authority section found (expected if server is not authoritative)")
}
// Test the additional section, which should contain A records for the name servers listed in the authority section
if len(response.Additionals) > 0 {
for _, add := range response.Additionals {
if add.Header.Type != dnsmessage.TypeA {
t.Errorf("Expected additional section record type to be A, but got %d", add.Header.Type)
}
}
}
// Verify if the additional section corresponds to the authority section, i.e., if NS records are followed by A records
for _, auth := range response.Authorities {
nsRecord := auth.Body.(*dnsmessage.NSResource).NS
if nsRecord.String() != "" {
found := false
for _, add := range response.Additionals {
if add.Header.Type == dnsmessage.TypeA {
// Check if the additional A record corresponds to the NS record
if add.Header.Name == nsRecord {
found = true
break
}
}
}
if !found {
t.Errorf("No corresponding A record found in additional section for NS record %s", nsRecord)
}
}
}
}
func Test_ARecord(t *testing.T) {
response := sendDNSQuery(t, "127.0.0.1:53", "govekar.net.", dnsmessage.TypeA, true)
if response.Header.RCode != dnsmessage.RCodeSuccess {
t.Errorf("Expected RCODE 0 (NoError), got %d", response.Header.RCode)
}
found := false
for _, ans := range response.Answers {
if ans.Header.Type == dnsmessage.TypeA {
found = true
break
}
}
if !found {
t.Errorf("Expected A record in response")
}
}
// Test AAAA Record (IPv6)
func Test_AAAARecord(t *testing.T) {
response := sendDNSQuery(t, "127.0.0.1:53", "govekar.net.", dnsmessage.TypeAAAA, true)
if response.Header.RCode != dnsmessage.RCodeSuccess {
t.Errorf("Expected RCODE 0 (NoError), got %d", response.Header.RCode)
}
found := false
for _, ans := range response.Answers {
if ans.Header.Type == dnsmessage.TypeAAAA {
found = true
break
}
}
if !found {
t.Errorf("Expected AAAA record in response")
}
}
// Test CNAME Record
func Test_CNAMERecord(t *testing.T) {
response := sendDNSQuery(t, "127.0.0.1:53", "www.govekar.net.", dnsmessage.TypeCNAME, true)
if response.Header.RCode != dnsmessage.RCodeSuccess {
t.Errorf("Expected RCODE 0 (NoError), got %d", response.Header.RCode)
}
found := false
for _, ans := range response.Answers {
if ans.Header.Type == dnsmessage.TypeCNAME {
found = true
break
}
}
if !found {
t.Errorf("Expected CNAME record in response")
}
}
// Test MX Record
func Test_MXRecord(t *testing.T) {
response := sendDNSQuery(t, "127.0.0.1:53", "govekar.net.", dnsmessage.TypeMX, true)
if response.Header.RCode != dnsmessage.RCodeSuccess {
t.Errorf("Expected RCODE 0 (NoError), got %d", response.Header.RCode)
}
found := false
for _, ans := range response.Answers {
if ans.Header.Type == dnsmessage.TypeMX {
found = true
break
}
}
if !found {
t.Errorf("Expected MX record in response")
}
}
// Test NS Record
func Test_NSRecord(t *testing.T) {
response := sendDNSQuery(t, "127.0.0.1:53", "govekar.net.", dnsmessage.TypeNS, true)
if response.Header.RCode != dnsmessage.RCodeSuccess {
t.Errorf("Expected RCODE 0 (NoError), got %d", response.Header.RCode)
}
found := false
for _, ans := range response.Answers {
if ans.Header.Type == dnsmessage.TypeNS {
found = true
break
}
}
if !found {
t.Errorf("Expected NS record in response")
}
}
// Test TXT Record
func Test_TXTRecord(t *testing.T) {
response := sendDNSQuery(t, "127.0.0.1:53", "govekar.net.", dnsmessage.TypeTXT, true)
if response.Header.RCode != dnsmessage.RCodeSuccess {
t.Errorf("Expected RCODE 0 (NoError), got %d", response.Header.RCode)
}
found := false
for _, ans := range response.Answers {
if ans.Header.Type == dnsmessage.TypeTXT {
found = true
break
}
}
if !found {
t.Errorf("Expected TXT record in response")
}
}
// Test PTR Record (Reverse DNS)
func Test_PTRRecord(t *testing.T) {
response := sendDNSQuery(t, "127.0.0.1:53", "4.3.2.1.in-addr.arpa.", dnsmessage.TypePTR, true)
if response.Header.RCode != dnsmessage.RCodeSuccess {
t.Errorf("Expected RCODE 0 (NoError), got %d", response.Header.RCode)
}
found := false
for _, ans := range response.Answers {
if ans.Header.Type == dnsmessage.TypePTR {
found = true
break
}
}
if !found {
t.Errorf("Expected PTR record in response")
}
}
// Test SOA Record
func Test_SOARecord(t *testing.T) {
response := sendDNSQuery(t, "127.0.0.1:53", "govekar.net.", dnsmessage.TypeSOA, true)
if response.Header.RCode != dnsmessage.RCodeSuccess {
t.Errorf("Expected RCODE 0 (NoError), got %d", response.Header.RCode)
}
found := false
for _, ans := range response.Answers {
if ans.Header.Type == dnsmessage.TypeSOA {
found = true
break
}
}
if !found {
t.Errorf("Expected SOA record in response")
}
}
/*// Test Recursive Queries
func Test_RecursiveQuery(t *testing.T) {
server := startTestServer(t)
defer server.Close()
response := sendDNSQuery(t, "127.0.0.1:53", "govekar.net.", dnsmessage.TypeA)
if !response.Header.RecursionAvailable {
t.Errorf("Expected RA (Recursion Available) flag to be set")
}
}
// Test Iterative Queries (If server is authoritative)
func Test_IterativeQuery(t *testing.T) {
server := startTestServer(t)
defer server.Close()
response := sendDNSQuery(t, "127.0.0.1:53", "govekar.net.", dnsmessage.TypeA)
if response.Header.RecursionAvailable {
t.Errorf("Server should not allow recursion for iterative queries")
}
}*/
// Test NXDOMAIN Response (Non-existent domain)
func Test_NXDOMAIN(t *testing.T) {
response := sendDNSQuery(t, "127.0.0.1:53", "nonexistent.govekar.net.", dnsmessage.TypeA, true)
if response.Header.RCode != dnsmessage.RCodeNameError {
t.Errorf("Expected NXDOMAIN (RCODE 3), got %d", response.Header.RCode)
}
}
/*// Test SERVFAIL Response (Server failure)
func Test_SERVFAIL(t *testing.T) {
server := startTestServer(t)
defer server.Close()
// Simulating a server error (invalid domain)
response := sendDNSQuery(t, "127.0.0.1:53", "serverfail.govekar.net.", dnsmessage.TypeA)
if response.Header.RCode != dnsmessage.RCodeServerFailure {
t.Errorf("Expected SERVFAIL (RCODE 2), got %d", response.Header.RCode)
}
}
// Test REFUSED Response (Query refused)
func Test_REFUSED(t *testing.T) {
server := startTestServer(t)
defer server.Close()
// Assuming server is configured to refuse certain domains
response := sendDNSQuery(t, "127.0.0.1:53", "forbidden.govekar.net.", dnsmessage.TypeA)
if response.Header.RCode != dnsmessage.RCodeRefused {
t.Errorf("Expected REFUSED (RCODE 5), got %d", response.Header.RCode)
}
}*/
func Test_WildcardQuery_NoWildcardRecord(t *testing.T) {
// Query a non-existent subdomain without a wildcard record
response := sendDNSQuery(t, "127.0.0.1:53", "random.govekar.net.", dnsmessage.TypeA, true)
// The response should be NXDOMAIN (Non-Existent Domain) if there's no wildcard
if response.Header.RCode != dnsmessage.RCodeNameError {
t.Errorf("Expected NXDOMAIN, but got RCode: %v", response.Header.RCode)
}
}
func Test_WildcardQuery_WithWildcardRecord(t *testing.T) {
// Query a non-existent subdomain but one that should match the wildcard
response := sendDNSQuery(t, "127.0.0.1:53", "random.test.govekar.net.", dnsmessage.TypeA, true)
// The response should return the IP defined by the wildcard record
if len(response.Answers) == 0 || response.Answers[0].Body.(*dnsmessage.AResource).A != [4]byte{192, 168, 1, 1} {
t.Errorf("Expected IP address from wildcard record, but got: %v", response.Answers)
}
}
// Test Caching Mechanism
func Test_CacheTTLExpiration(t *testing.T) {
s := startTestServer(t)
defer s.Close()
// First query to cache the record
response1 := sendDNSQuery(t, "127.0.0.1:53", "govekar.net.", dnsmessage.TypeA, false)
initialTTL := response1.Answers[0].Header.TTL
if initialTTL < 1 {
t.Errorf("TTL should be greater than 0")
}
// Wait for a short duration before expiration (e.g., 1/2 of TTL)
waitDuration := time.Duration(initialTTL/2) * time.Second
time.Sleep(waitDuration)
// Send the second query
response2 := sendDNSQuery(t, "127.0.0.1:53", "govekar.net.", dnsmessage.TypeA, false)
decreasedTTL := response2.Answers[0].Header.TTL
// Verify that the TTL has decreased (it should be lower than the initial TTL)
if decreasedTTL >= initialTTL {
t.Errorf("Expected TTL to decrease, but it didn't. Initial TTL: %d, Decreased TTL: %d", initialTTL, decreasedTTL)
}
// Ensure the TTL is still positive after the decrease
if decreasedTTL <= 0 {
t.Errorf("TTL should be greater than 0, but it is: %d", decreasedTTL)
}
}
// Test Malformed Query (Should return FORMAT ERROR)
func Test_MalformedQuery(t *testing.T) {
s := startTestServer(t)
defer s.Close()
// Sending invalid query data
conn, err := net.Dial("udp", "127.0.0.1:53")
if err != nil {
t.Fatalf("Failed to connect to DNS server: %v", err)
}
defer conn.Close()
// Set a timeout for the connection to avoid infinite waiting
err = conn.SetDeadline(time.Now().Add(5 * time.Second)) // Timeout after 5 seconds
if err != nil {
t.Fatalf("Failed to set timeout for connection: %v", err)
}
// Sending an invalid DNS query (malformed)
_, err = conn.Write([]byte{0x00, 0x01, 0x02}) // Invalid DNS query format
if err != nil {
t.Fatalf("Failed to send malformed query: %v", err)
}
buf := make([]byte, 512)
n, err := conn.Read(buf)
if err != nil {
// Check for timeout error (or any other read error)
if ne, ok := err.(net.Error); ok && ne.Timeout() {
t.Fatalf("Timeout waiting for response from DNS server: %v", err)
} else {
t.Fatalf("Failed to read DNS response: %v", err)
}
}
var response dnsmessage.Message
err = response.Unpack(buf[:n])
if err != nil {
t.Fatalf("Failed to unpack response: %v", err)
}
// Check if the server returned the correct format error (RCODE 1)
if response.Header.RCode != dnsmessage.RCodeFormatError {
t.Errorf("Expected FORMAT ERROR (RCODE 1), got %d", response.Header.RCode)
}
}
// Test Unknown Record Type Handling
func Test_UnknownRecordType(t *testing.T) {
response := sendDNSQuery(t, "127.0.0.1:53", "govekar.net.", dnsmessage.Type(99), true) // Unknown type
if response.Header.RCode != dnsmessage.RCodeNotImplemented {
t.Errorf("Expected NOTIMPLEMENTED (RCODE 4), got %d", response.Header.RCode)
}
}