467 lines
15 KiB
Zig
467 lines
15 KiB
Zig
const std = @import("std");
|
|
const testing = std.testing;
|
|
|
|
const packet = @import("packet");
|
|
const Header = @import("header").Header;
|
|
const Name = @import("name").Name;
|
|
const Question = @import("question").Question;
|
|
const types = @import("types");
|
|
|
|
// ============================================================================
|
|
// Real DNS packet captures for testing
|
|
// ============================================================================
|
|
|
|
/// Standard A query for google.com
|
|
/// Captured from: dig google.com A
|
|
const GOOGLE_A_QUERY = [_]u8{
|
|
// Header
|
|
0xAB, 0xCD, // ID: 0xABCD
|
|
0x01, 0x00, // Flags: standard query, RD=1
|
|
0x00, 0x01, // QDCOUNT: 1
|
|
0x00, 0x00, // ANCOUNT: 0
|
|
0x00, 0x00, // NSCOUNT: 0
|
|
0x00, 0x00, // ARCOUNT: 0
|
|
// Question: google.com A IN
|
|
0x06, 'g', 'o', 'o', 'g', 'l', 'e',
|
|
0x03, 'c', 'o', 'm',
|
|
0x00, // null terminator
|
|
0x00, 0x01, // QTYPE: A (1)
|
|
0x00, 0x01, // QCLASS: IN (1)
|
|
};
|
|
|
|
/// AAAA query for example.org
|
|
const EXAMPLE_AAAA_QUERY = [_]u8{
|
|
// Header
|
|
0x12, 0x34, // ID
|
|
0x01, 0x00, // Flags: standard query, RD=1
|
|
0x00, 0x01, // QDCOUNT: 1
|
|
0x00, 0x00, // ANCOUNT: 0
|
|
0x00, 0x00, // NSCOUNT: 0
|
|
0x00, 0x00, // ARCOUNT: 0
|
|
// Question: example.org AAAA IN
|
|
0x07, 'e', 'x', 'a', 'm', 'p', 'l', 'e',
|
|
0x03, 'o', 'r', 'g',
|
|
0x00,
|
|
0x00, 0x1C, // QTYPE: AAAA (28)
|
|
0x00, 0x01, // QCLASS: IN
|
|
};
|
|
|
|
/// Response with A record
|
|
const SIMPLE_A_RESPONSE = [_]u8{
|
|
// Header
|
|
0xAB, 0xCD, // ID
|
|
0x81, 0x80, // Flags: response, RD=1, RA=1
|
|
0x00, 0x01, // QDCOUNT: 1
|
|
0x00, 0x01, // ANCOUNT: 1
|
|
0x00, 0x00, // NSCOUNT: 0
|
|
0x00, 0x00, // ARCOUNT: 0
|
|
// Question: google.com A IN (with compression)
|
|
0x06, 'g', 'o', 'o', 'g', 'l', 'e',
|
|
0x03, 'c', 'o', 'm',
|
|
0x00,
|
|
0x00, 0x01, // QTYPE: A
|
|
0x00, 0x01, // QCLASS: IN
|
|
// Answer: A record using compression pointer
|
|
0xC0, 0x0C, // Name pointer to offset 12 (google.com)
|
|
0x00, 0x01, // TYPE: A
|
|
0x00, 0x01, // CLASS: IN
|
|
0x00, 0x00, 0x01, 0x2C, // TTL: 300 seconds
|
|
0x00, 0x04, // RDLENGTH: 4
|
|
0xD8, 0x3A, 0xD3, 0x8E, // RDATA: 216.58.211.142
|
|
};
|
|
|
|
/// Response with CNAME chain
|
|
const CNAME_RESPONSE = [_]u8{
|
|
// Header
|
|
0x55, 0x66, // ID
|
|
0x81, 0x80, // Flags: response, RD=1, RA=1
|
|
0x00, 0x01, // QDCOUNT: 1
|
|
0x00, 0x02, // ANCOUNT: 2 (CNAME + A)
|
|
0x00, 0x00, // NSCOUNT: 0
|
|
0x00, 0x00, // ARCOUNT: 0
|
|
// Question: www.example.com A IN
|
|
0x03, 'w', 'w', 'w',
|
|
0x07, 'e', 'x', 'a', 'm', 'p', 'l', 'e',
|
|
0x03, 'c', 'o', 'm',
|
|
0x00,
|
|
0x00, 0x01, // QTYPE: A
|
|
0x00, 0x01, // QCLASS: IN
|
|
// Answer 1: CNAME www.example.com -> example.com
|
|
0xC0, 0x0C, // Name pointer to www.example.com
|
|
0x00, 0x05, // TYPE: CNAME
|
|
0x00, 0x01, // CLASS: IN
|
|
0x00, 0x00, 0x0E, 0x10, // TTL: 3600
|
|
0x00, 0x02, // RDLENGTH: 2 (compression pointer)
|
|
0xC0, 0x10, // RDATA: pointer to example.com
|
|
// Answer 2: A record for example.com
|
|
0xC0, 0x10, // Name pointer to example.com
|
|
0x00, 0x01, // TYPE: A
|
|
0x00, 0x01, // CLASS: IN
|
|
0x00, 0x00, 0x01, 0x2C, // TTL: 300
|
|
0x00, 0x04, // RDLENGTH: 4
|
|
0x5D, 0xB8, 0xD8, 0x22, // RDATA: 93.184.216.34
|
|
};
|
|
|
|
/// Malformed packet - truncated header
|
|
const MALFORMED_TRUNCATED = [_]u8{
|
|
0x12, 0x34, // Only 2 bytes, header needs 12
|
|
};
|
|
|
|
/// Malformed packet - invalid compression pointer (loop)
|
|
const MALFORMED_COMPRESSION_LOOP = [_]u8{
|
|
// Header
|
|
0x00, 0x01,
|
|
0x01, 0x00,
|
|
0x00, 0x01,
|
|
0x00, 0x00,
|
|
0x00, 0x00,
|
|
0x00, 0x00,
|
|
// Question with self-referencing pointer
|
|
0xC0, 0x0C, // Points to itself
|
|
0x00, 0x01,
|
|
0x00, 0x01,
|
|
};
|
|
|
|
// ============================================================================
|
|
// Header Tests
|
|
// ============================================================================
|
|
|
|
test "Header.parse - standard query" {
|
|
const h = try Header.parse(&GOOGLE_A_QUERY);
|
|
|
|
try testing.expectEqual(@as(u16, 0xABCD), h.id);
|
|
try testing.expect(!h.qr); // Query, not response
|
|
try testing.expectEqual(types.OpCode.Query, h.opcode);
|
|
try testing.expect(!h.aa); // Not authoritative
|
|
try testing.expect(!h.tc); // Not truncated
|
|
try testing.expect(h.rd); // Recursion desired
|
|
try testing.expect(!h.ra); // Recursion not available (query)
|
|
try testing.expectEqual(types.RCode.NoError, h.rcode);
|
|
try testing.expectEqual(@as(u16, 1), h.qdcount);
|
|
try testing.expectEqual(@as(u16, 0), h.ancount);
|
|
try testing.expectEqual(@as(u16, 0), h.nscount);
|
|
try testing.expectEqual(@as(u16, 0), h.arcount);
|
|
}
|
|
|
|
test "Header.parse - standard response" {
|
|
const h = try Header.parse(&SIMPLE_A_RESPONSE);
|
|
|
|
try testing.expectEqual(@as(u16, 0xABCD), h.id);
|
|
try testing.expect(h.qr); // Response
|
|
try testing.expect(h.rd); // RD copied from query
|
|
try testing.expect(h.ra); // Recursion available
|
|
try testing.expectEqual(@as(u16, 1), h.qdcount);
|
|
try testing.expectEqual(@as(u16, 1), h.ancount);
|
|
}
|
|
|
|
test "Header.parse - buffer too small" {
|
|
const result = Header.parse(&MALFORMED_TRUNCATED);
|
|
try testing.expectError(error.BufferTooSmall, result);
|
|
}
|
|
|
|
test "Header.encode - roundtrip" {
|
|
const original = try Header.parse(&GOOGLE_A_QUERY);
|
|
|
|
var buf: [12]u8 = undefined;
|
|
original.encode(&buf);
|
|
|
|
const decoded = try Header.parse(&buf);
|
|
try testing.expectEqual(original.id, decoded.id);
|
|
try testing.expectEqual(original.qr, decoded.qr);
|
|
try testing.expectEqual(original.opcode, decoded.opcode);
|
|
try testing.expectEqual(original.rd, decoded.rd);
|
|
try testing.expectEqual(original.qdcount, decoded.qdcount);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Name Tests
|
|
// ============================================================================
|
|
|
|
test "Name.parse - simple domain" {
|
|
const allocator = testing.allocator;
|
|
const buffer = [_]u8{ 0x06, 'g', 'o', 'o', 'g', 'l', 'e', 0x03, 'c', 'o', 'm', 0x00 };
|
|
|
|
const result = try Name.parse(&buffer, &buffer, allocator);
|
|
defer result.name.deinit();
|
|
|
|
try testing.expectEqual(@as(usize, 12), result.bytes_read);
|
|
|
|
var str_buf: [256]u8 = undefined;
|
|
const str = result.name.toStringBuf(&str_buf).?;
|
|
try testing.expectEqualStrings("google.com", str);
|
|
}
|
|
|
|
test "Name.parse - root domain" {
|
|
const allocator = testing.allocator;
|
|
const buffer = [_]u8{0x00}; // Just null byte = root
|
|
|
|
const result = try Name.parse(&buffer, &buffer, allocator);
|
|
defer result.name.deinit();
|
|
|
|
try testing.expectEqual(@as(usize, 1), result.bytes_read);
|
|
|
|
var str_buf: [256]u8 = undefined;
|
|
const str = result.name.toStringBuf(&str_buf).?;
|
|
try testing.expectEqualStrings("", str);
|
|
}
|
|
|
|
test "Name.parse - compression pointer" {
|
|
const allocator = testing.allocator;
|
|
|
|
// Full packet with question followed by answer using compression
|
|
const result = try Name.parse(SIMPLE_A_RESPONSE[27..], &SIMPLE_A_RESPONSE, allocator);
|
|
defer result.name.deinit();
|
|
|
|
var str_buf: [256]u8 = undefined;
|
|
const str = result.name.toStringBuf(&str_buf).?;
|
|
try testing.expectEqualStrings("google.com", str);
|
|
}
|
|
|
|
test "Name.parse - max label length (63)" {
|
|
const allocator = testing.allocator;
|
|
var buffer: [67]u8 = undefined;
|
|
buffer[0] = 63; // Label length = max
|
|
for (1..64) |i| {
|
|
buffer[i] = 'a';
|
|
}
|
|
buffer[64] = 0x03;
|
|
buffer[65] = 'c';
|
|
buffer[66] = 'o';
|
|
// Would need more bytes for full domain, but testing max label
|
|
|
|
// This should work (63 is max label length)
|
|
const result = Name.parse(buffer[0..67], buffer[0..67], allocator);
|
|
if (result) |r| {
|
|
r.name.deinit();
|
|
} else |_| {
|
|
// May fail due to incomplete buffer, which is fine
|
|
}
|
|
}
|
|
|
|
test "Name.parse - label too long (64+)" {
|
|
const allocator = testing.allocator;
|
|
var buffer: [68]u8 = undefined;
|
|
buffer[0] = 64; // Label length > 63 is invalid (and not a pointer)
|
|
@memset(buffer[1..65], 'a');
|
|
buffer[65] = 0x00;
|
|
|
|
const result = Name.parse(&buffer, &buffer, allocator);
|
|
try testing.expectError(error.InvalidLabel, result);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Question Tests
|
|
// ============================================================================
|
|
|
|
test "Question.parse - A record query" {
|
|
const allocator = testing.allocator;
|
|
|
|
const result = try Question.parse(GOOGLE_A_QUERY[12..], &GOOGLE_A_QUERY, allocator);
|
|
defer result.question.deinit();
|
|
|
|
try testing.expectEqual(types.QType.A, result.question.qtype);
|
|
try testing.expectEqual(types.QClass.IN, result.question.qclass);
|
|
|
|
var str_buf: [256]u8 = undefined;
|
|
const domain = result.question.name.toStringBuf(&str_buf).?;
|
|
try testing.expectEqualStrings("google.com", domain);
|
|
}
|
|
|
|
test "Question.parse - AAAA record query" {
|
|
const allocator = testing.allocator;
|
|
|
|
const result = try Question.parse(EXAMPLE_AAAA_QUERY[12..], &EXAMPLE_AAAA_QUERY, allocator);
|
|
defer result.question.deinit();
|
|
|
|
try testing.expectEqual(types.QType.AAAA, result.question.qtype);
|
|
try testing.expectEqual(types.QClass.IN, result.question.qclass);
|
|
|
|
var str_buf: [256]u8 = undefined;
|
|
const domain = result.question.name.toStringBuf(&str_buf).?;
|
|
try testing.expectEqualStrings("example.org", domain);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Packet Tests
|
|
// ============================================================================
|
|
|
|
test "Packet.parse - simple query" {
|
|
const allocator = testing.allocator;
|
|
|
|
var pkt = try packet.Packet.parse(&GOOGLE_A_QUERY, allocator);
|
|
defer pkt.deinit();
|
|
|
|
try testing.expectEqual(@as(u16, 0xABCD), pkt.header.id);
|
|
try testing.expect(!pkt.header.qr);
|
|
try testing.expectEqual(@as(usize, 1), pkt.questions.len);
|
|
try testing.expectEqual(@as(usize, 0), pkt.answers.len);
|
|
|
|
var str_buf: [256]u8 = undefined;
|
|
const domain = pkt.questions[0].name.toStringBuf(&str_buf).?;
|
|
try testing.expectEqualStrings("google.com", domain);
|
|
try testing.expectEqual(types.QType.A, pkt.questions[0].qtype);
|
|
}
|
|
|
|
test "Packet.parse - response with A record" {
|
|
const allocator = testing.allocator;
|
|
|
|
var pkt = try packet.Packet.parse(&SIMPLE_A_RESPONSE, allocator);
|
|
defer pkt.deinit();
|
|
|
|
try testing.expect(pkt.header.qr);
|
|
try testing.expectEqual(@as(usize, 1), pkt.questions.len);
|
|
try testing.expectEqual(@as(usize, 1), pkt.answers.len);
|
|
|
|
// Check the A record
|
|
const answer = pkt.answers[0];
|
|
try testing.expectEqual(types.QType.A, answer.rtype);
|
|
try testing.expectEqual(@as(u32, 300), answer.ttl);
|
|
|
|
// Verify IP address
|
|
const ip = answer.getA().?;
|
|
try testing.expectEqual([4]u8{ 0xD8, 0x3A, 0xD3, 0x8E }, ip);
|
|
}
|
|
|
|
test "Packet.parse - CNAME chain" {
|
|
const allocator = testing.allocator;
|
|
|
|
var pkt = try packet.Packet.parse(&CNAME_RESPONSE, allocator);
|
|
defer pkt.deinit();
|
|
|
|
try testing.expectEqual(@as(usize, 2), pkt.answers.len);
|
|
|
|
// First answer should be CNAME
|
|
try testing.expectEqual(types.QType.CNAME, pkt.answers[0].rtype);
|
|
|
|
// Second answer should be A record
|
|
try testing.expectEqual(types.QType.A, pkt.answers[1].rtype);
|
|
|
|
const ip = pkt.answers[1].getA().?;
|
|
try testing.expectEqual([4]u8{ 0x5D, 0xB8, 0xD8, 0x22 }, ip);
|
|
}
|
|
|
|
test "Packet.parse - truncated packet" {
|
|
const allocator = testing.allocator;
|
|
|
|
const result = packet.Packet.parse(&MALFORMED_TRUNCATED, allocator);
|
|
try testing.expectError(error.HeaderParseError, result);
|
|
}
|
|
|
|
test "Packet.encode - roundtrip" {
|
|
const allocator = testing.allocator;
|
|
|
|
var original = try packet.Packet.parse(&GOOGLE_A_QUERY, allocator);
|
|
defer original.deinit();
|
|
|
|
var buf: [512]u8 = undefined;
|
|
const encoded_len = try original.encode(&buf);
|
|
|
|
var decoded = try packet.Packet.parse(buf[0..encoded_len], allocator);
|
|
defer decoded.deinit();
|
|
|
|
try testing.expectEqual(original.header.id, decoded.header.id);
|
|
try testing.expectEqual(original.questions.len, decoded.questions.len);
|
|
}
|
|
|
|
test "Packet.createBlockedResponse - creates valid response" {
|
|
const allocator = testing.allocator;
|
|
|
|
var query = try packet.Packet.parse(&GOOGLE_A_QUERY, allocator);
|
|
defer query.deinit();
|
|
|
|
var response = try packet.Packet.createBlockedResponse(&query, allocator);
|
|
defer response.deinit();
|
|
|
|
try testing.expect(response.header.qr); // Is response
|
|
try testing.expectEqual(query.header.id, response.header.id);
|
|
try testing.expectEqual(@as(usize, 1), response.answers.len);
|
|
|
|
// Should return 0.0.0.0 for blocked
|
|
const ip = response.answers[0].getA().?;
|
|
try testing.expectEqual([4]u8{ 0, 0, 0, 0 }, ip);
|
|
}
|
|
|
|
test "Packet.createNxdomainResponse - creates valid NXDOMAIN" {
|
|
const allocator = testing.allocator;
|
|
|
|
var query = try packet.Packet.parse(&GOOGLE_A_QUERY, allocator);
|
|
defer query.deinit();
|
|
|
|
var response = try packet.Packet.createNxdomainResponse(&query, allocator);
|
|
defer response.deinit();
|
|
|
|
try testing.expect(response.header.qr);
|
|
try testing.expectEqual(types.RCode.NXDomain, response.header.rcode);
|
|
try testing.expectEqual(@as(usize, 0), response.answers.len);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Edge Cases
|
|
// ============================================================================
|
|
|
|
test "DNS max name length (253 chars)" {
|
|
const allocator = testing.allocator;
|
|
|
|
// Build a name with max length: 63.63.63.63 = 253 chars + labels
|
|
var buffer: [512]u8 = undefined;
|
|
var pos: usize = 12; // Skip header
|
|
|
|
// Add 4 labels of 63 chars each
|
|
for (0..4) |_| {
|
|
buffer[pos] = 63;
|
|
pos += 1;
|
|
@memset(buffer[pos .. pos + 63], 'a');
|
|
pos += 63;
|
|
}
|
|
buffer[pos] = 0; // Null terminator
|
|
pos += 1;
|
|
|
|
// Add qtype and qclass
|
|
buffer[pos] = 0x00;
|
|
buffer[pos + 1] = 0x01;
|
|
buffer[pos + 2] = 0x00;
|
|
buffer[pos + 3] = 0x01;
|
|
pos += 4;
|
|
|
|
// Set up header
|
|
@memset(buffer[0..12], 0);
|
|
buffer[2] = 0x01; // RD=1
|
|
buffer[5] = 0x01; // QDCOUNT=1
|
|
|
|
const pkt_result = packet.Packet.parse(buffer[0..pos], allocator);
|
|
// This may fail due to name being too long (4*63=252 + dots)
|
|
if (pkt_result) |*pkt| {
|
|
pkt.deinit();
|
|
} else |_| {}
|
|
}
|
|
|
|
test "DNS various query types" {
|
|
const allocator = testing.allocator;
|
|
const query_types = [_]types.QType{ .A, .AAAA, .CNAME, .MX, .NS, .TXT, .SOA, .PTR, .SRV };
|
|
|
|
for (query_types) |qtype| {
|
|
var buf: [64]u8 = undefined;
|
|
@memset(buf[0..12], 0);
|
|
buf[2] = 0x01; // RD
|
|
buf[5] = 0x01; // QDCOUNT
|
|
|
|
// Simple question: a.b
|
|
buf[12] = 0x01;
|
|
buf[13] = 'a';
|
|
buf[14] = 0x01;
|
|
buf[15] = 'b';
|
|
buf[16] = 0x00;
|
|
|
|
const qtype_val = @intFromEnum(qtype);
|
|
buf[17] = @intCast((qtype_val >> 8) & 0xFF);
|
|
buf[18] = @intCast(qtype_val & 0xFF);
|
|
buf[19] = 0x00;
|
|
buf[20] = 0x01; // CLASS IN
|
|
|
|
var pkt = try packet.Packet.parse(buf[0..21], allocator);
|
|
defer pkt.deinit();
|
|
|
|
try testing.expectEqual(qtype, pkt.questions[0].qtype);
|
|
}
|
|
}
|