initial commit

This commit is contained in:
2025-12-26 18:42:04 +01:00
commit d8d9ddfc53
52 changed files with 16863 additions and 0 deletions
+242
View File
@@ -0,0 +1,242 @@
const std = @import("std");
const types = @import("types.zig");
const Name = @import("name.zig").Name;
const ResourceRecord = @import("record.zig").ResourceRecord;
const RData = @import("record.zig").RData;
const Allocator = std.mem.Allocator;
/// EDNS Option Codes
pub const OptionCode = enum(u16) {
LLQ = 1, // Long-Lived Queries
UL = 2, // Update Lease
NSID = 3, // Name Server Identifier
DAU = 5, // DNSSEC Algorithm Understood
DHU = 6, // DS Hash Understood
N3U = 7, // NSEC3 Hash Understood
ECS = 8, // Client Subnet
EXPIRE = 9, // Expire
COOKIE = 10, // DNS Cookie
TCP_KEEPALIVE = 11, // TCP Keepalive
PADDING = 12, // Padding
CHAIN = 13, // CHAIN
KEY_TAG = 14, // Key Tag
EXTENDED_ERROR = 15, // Extended DNS Error
CLIENT_TAG = 16, // Client Tag
SERVER_TAG = 17, // Server Tag
_,
};
/// EDNS Option
pub const EdnsOption = struct {
code: OptionCode,
data: []const u8,
pub fn deinit(self: *EdnsOption, allocator: Allocator) void {
allocator.free(self.data);
}
};
/// EDNS (OPT Record) Information
pub const Edns = struct {
/// Requestor's UDP payload size
udp_payload_size: u16,
/// Extended RCODE (upper 8 bits)
extended_rcode: u8,
/// EDNS version
version: u8,
/// DNSSEC OK flag
dnssec_ok: bool,
/// Other flags (Z field, 15 bits)
z: u15,
/// EDNS options
options: []EdnsOption,
allocator: ?Allocator,
pub fn init() Edns {
return Edns{
.udp_payload_size = types.EDNS_DEFAULT_SIZE,
.extended_rcode = 0,
.version = 0,
.dnssec_ok = false,
.z = 0,
.options = &[_]EdnsOption{},
.allocator = null,
};
}
/// Parse EDNS from an OPT resource record
pub fn fromOptRecord(record: ResourceRecord, allocator: Allocator) !Edns {
if (record.rtype != types.QType.OPT) {
return error.NotOptRecord;
}
// CLASS field contains UDP payload size
const udp_payload_size = @intFromEnum(record.class);
// TTL field contains extended RCODE and flags
const ttl = record.ttl;
const extended_rcode: u8 = @truncate((ttl >> 24) & 0xFF);
const version: u8 = @truncate((ttl >> 16) & 0xFF);
const flags: u16 = @truncate(ttl & 0xFFFF);
const dnssec_ok = (flags >> 15) & 1 == 1;
const z: u15 = @truncate(flags & 0x7FFF);
// Parse options from RDATA
var options = std.ArrayListUnmanaged(EdnsOption){};
errdefer {
for (options.items) |*opt| {
opt.deinit(allocator);
}
options.deinit(allocator);
}
const opt_data = switch (record.rdata) {
.opt => |data| data,
else => return error.InvalidOptRecord,
};
var pos: usize = 0;
while (pos + 4 <= opt_data.len) {
const code = std.mem.readInt(u16, opt_data[pos..][0..2], .big);
const length = std.mem.readInt(u16, opt_data[pos + 2 ..][0..2], .big);
pos += 4;
if (pos + length > opt_data.len) break;
const data = try allocator.dupe(u8, opt_data[pos .. pos + length]);
try options.append(allocator, EdnsOption{
.code = @enumFromInt(code),
.data = data,
});
pos += length;
}
return Edns{
.udp_payload_size = udp_payload_size,
.extended_rcode = extended_rcode,
.version = version,
.dnssec_ok = dnssec_ok,
.z = z,
.options = try options.toOwnedSlice(allocator),
.allocator = allocator,
};
}
/// Create an OPT resource record from this EDNS info
pub fn toOptRecord(self: Edns, allocator: Allocator) !ResourceRecord {
// Create root name (empty)
var root_name = try Name.fromString("", allocator);
errdefer root_name.deinit();
// Encode options to RDATA
var rdata_size: usize = 0;
for (self.options) |opt| {
rdata_size += 4 + opt.data.len;
}
var rdata = try allocator.alloc(u8, rdata_size);
errdefer allocator.free(rdata);
var pos: usize = 0;
for (self.options) |opt| {
std.mem.writeInt(u16, rdata[pos..][0..2], @intFromEnum(opt.code), .big);
std.mem.writeInt(u16, rdata[pos + 2 ..][0..2], @intCast(opt.data.len), .big);
@memcpy(rdata[pos + 4 .. pos + 4 + opt.data.len], opt.data);
pos += 4 + opt.data.len;
}
// Build TTL from extended RCODE and flags
var ttl: u32 = 0;
ttl |= @as(u32, self.extended_rcode) << 24;
ttl |= @as(u32, self.version) << 16;
if (self.dnssec_ok) {
ttl |= @as(u32, 1) << 15;
}
ttl |= @as(u32, self.z);
return ResourceRecord{
.name = root_name,
.rtype = types.QType.OPT,
.class = @enumFromInt(self.udp_payload_size),
.ttl = ttl,
.rdata = RData{ .opt = rdata },
};
}
pub fn deinit(self: *Edns) void {
if (self.allocator) |alloc| {
for (self.options) |*opt| {
var o = opt.*;
o.deinit(alloc);
}
alloc.free(self.options);
}
}
/// Check if DNSSEC validation is requested
pub fn wantsDnssec(self: Edns) bool {
return self.dnssec_ok;
}
/// Get the maximum UDP payload size the client can handle
pub fn getMaxPayloadSize(self: Edns) u16 {
return self.udp_payload_size;
}
};
/// Check if a packet has EDNS support by looking for OPT record in additional section
pub fn hasEdns(records: []const ResourceRecord) bool {
for (records) |record| {
if (record.rtype == types.QType.OPT) {
return true;
}
}
return false;
}
/// Find and extract EDNS info from additional records
pub fn extractEdns(records: []const ResourceRecord, allocator: Allocator) !?Edns {
for (records) |record| {
if (record.rtype == types.QType.OPT) {
return try Edns.fromOptRecord(record, allocator);
}
}
return null;
}
test "EDNS init" {
const testing = std.testing;
const edns = Edns.init();
try testing.expectEqual(@as(u16, types.EDNS_DEFAULT_SIZE), edns.udp_payload_size);
try testing.expectEqual(@as(u8, 0), edns.version);
try testing.expect(!edns.dnssec_ok);
}
test "EDNS to/from OPT record roundtrip" {
const testing = std.testing;
const allocator = testing.allocator;
var original = Edns{
.udp_payload_size = 1232,
.extended_rcode = 0,
.version = 0,
.dnssec_ok = true,
.z = 0,
.options = &[_]EdnsOption{},
.allocator = null,
};
var opt_record = try original.toOptRecord(allocator);
defer opt_record.deinit(allocator);
var parsed = try Edns.fromOptRecord(opt_record, allocator);
defer parsed.deinit();
try testing.expectEqual(original.udp_payload_size, parsed.udp_payload_size);
try testing.expectEqual(original.extended_rcode, parsed.extended_rcode);
try testing.expectEqual(original.version, parsed.version);
try testing.expectEqual(original.dnssec_ok, parsed.dnssec_ok);
}
+266
View File
@@ -0,0 +1,266 @@
const std = @import("std");
const types = @import("types.zig");
/// DNS Header (12 bytes)
/// Format:
/// 1 1 1 1 1 1
/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
/// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
/// | ID |
/// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
/// |QR| Opcode |AA|TC|RD|RA| Z | RCODE |
/// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
/// | QDCOUNT |
/// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
/// | ANCOUNT |
/// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
/// | NSCOUNT |
/// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
/// | ARCOUNT |
/// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
pub const Header = struct {
/// Transaction ID
id: u16,
/// Query/Response flag (0 = query, 1 = response)
qr: bool,
/// Operation code
opcode: types.OpCode,
/// Authoritative Answer flag
aa: bool,
/// Truncation flag
tc: bool,
/// Recursion Desired flag
rd: bool,
/// Recursion Available flag
ra: bool,
/// Reserved bit (must be zero)
z: bool,
/// Authenticated Data flag (DNSSEC - response was validated)
ad: bool,
/// Checking Disabled flag (DNSSEC - client doesn't want validation)
cd: bool,
/// Response code
rcode: types.RCode,
/// Number of questions
qdcount: u16,
/// Number of answers
ancount: u16,
/// Number of authority records
nscount: u16,
/// Number of additional records
arcount: u16,
pub const SIZE: usize = 12;
pub const ParseError = error{
BufferTooSmall,
};
/// Parse a DNS header from a buffer
pub fn parse(buffer: []const u8) ParseError!Header {
if (buffer.len < SIZE) {
return error.BufferTooSmall;
}
const id = std.mem.readInt(u16, buffer[0..2], .big);
const flags = std.mem.readInt(u16, buffer[2..4], .big);
const qdcount = std.mem.readInt(u16, buffer[4..6], .big);
const ancount = std.mem.readInt(u16, buffer[6..8], .big);
const nscount = std.mem.readInt(u16, buffer[8..10], .big);
const arcount = std.mem.readInt(u16, buffer[10..12], .big);
return Header{
.id = id,
.qr = (flags >> 15) & 1 == 1,
.opcode = @enumFromInt(@as(u4, @truncate((flags >> 11) & 0xF))),
.aa = (flags >> 10) & 1 == 1,
.tc = (flags >> 9) & 1 == 1,
.rd = (flags >> 8) & 1 == 1,
.ra = (flags >> 7) & 1 == 1,
.z = (flags >> 6) & 1 == 1,
.ad = (flags >> 5) & 1 == 1,
.cd = (flags >> 4) & 1 == 1,
.rcode = @enumFromInt(@as(u4, @truncate(flags & 0xF))),
.qdcount = qdcount,
.ancount = ancount,
.nscount = nscount,
.arcount = arcount,
};
}
/// Encode the header into a buffer
pub fn encode(self: Header, buffer: []u8) ParseError!void {
if (buffer.len < SIZE) {
return error.BufferTooSmall;
}
std.mem.writeInt(u16, buffer[0..2], self.id, .big);
var flags: u16 = 0;
flags |= @as(u16, @intFromBool(self.qr)) << 15;
flags |= @as(u16, @intFromEnum(self.opcode)) << 11;
flags |= @as(u16, @intFromBool(self.aa)) << 10;
flags |= @as(u16, @intFromBool(self.tc)) << 9;
flags |= @as(u16, @intFromBool(self.rd)) << 8;
flags |= @as(u16, @intFromBool(self.ra)) << 7;
flags |= @as(u16, @intFromBool(self.z)) << 6;
flags |= @as(u16, @intFromBool(self.ad)) << 5;
flags |= @as(u16, @intFromBool(self.cd)) << 4;
flags |= @as(u16, @intFromEnum(self.rcode));
std.mem.writeInt(u16, buffer[2..4], flags, .big);
std.mem.writeInt(u16, buffer[4..6], self.qdcount, .big);
std.mem.writeInt(u16, buffer[6..8], self.ancount, .big);
std.mem.writeInt(u16, buffer[8..10], self.nscount, .big);
std.mem.writeInt(u16, buffer[10..12], self.arcount, .big);
}
/// Create a response header from a query header
pub fn createResponse(query: Header, rcode: types.RCode) Header {
return Header{
.id = query.id,
.qr = true, // Response
.opcode = query.opcode,
.aa = false,
.tc = false,
.rd = query.rd,
.ra = true, // We support recursion
.z = false,
.ad = false, // Set by DNSSEC validation
.cd = query.cd, // Preserve client's CD flag
.rcode = rcode,
.qdcount = query.qdcount,
.ancount = 0,
.nscount = 0,
.arcount = 0,
};
}
/// Check if the response has authenticated data (DNSSEC validated)
pub fn isAuthenticated(self: Header) bool {
return self.ad;
}
/// Check if this is a query
pub fn isQuery(self: Header) bool {
return !self.qr;
}
/// Check if this is a response
pub fn isResponse(self: Header) bool {
return self.qr;
}
};
test "parse simple header" {
const testing = std.testing;
// Simple query header
const buffer = [_]u8{
0x00, 0x01, // ID = 1
0x01, 0x00, // Standard query, RD=1
0x00, 0x01, // QDCOUNT = 1
0x00, 0x00, // ANCOUNT = 0
0x00, 0x00, // NSCOUNT = 0
0x00, 0x00, // ARCOUNT = 0
};
const header = try Header.parse(&buffer);
try testing.expectEqual(@as(u16, 1), header.id);
try testing.expect(!header.qr); // Query
try testing.expectEqual(types.OpCode.Query, header.opcode);
try testing.expect(!header.aa);
try testing.expect(!header.tc);
try testing.expect(header.rd); // Recursion desired
try testing.expect(!header.ra);
try testing.expect(!header.z);
try testing.expect(!header.ad);
try testing.expect(!header.cd);
try testing.expectEqual(types.RCode.NoError, header.rcode);
try testing.expectEqual(@as(u16, 1), header.qdcount);
try testing.expectEqual(@as(u16, 0), header.ancount);
try testing.expectEqual(@as(u16, 0), header.nscount);
try testing.expectEqual(@as(u16, 0), header.arcount);
}
test "encode and decode header roundtrip" {
const testing = std.testing;
const original = Header{
.id = 0xABCD,
.qr = true,
.opcode = types.OpCode.Query,
.aa = true,
.tc = false,
.rd = true,
.ra = true,
.z = false,
.ad = true,
.cd = false,
.rcode = types.RCode.NoError,
.qdcount = 1,
.ancount = 2,
.nscount = 0,
.arcount = 1,
};
var buffer: [Header.SIZE]u8 = undefined;
try original.encode(&buffer);
const decoded = try Header.parse(&buffer);
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.aa, decoded.aa);
try testing.expectEqual(original.tc, decoded.tc);
try testing.expectEqual(original.rd, decoded.rd);
try testing.expectEqual(original.ra, decoded.ra);
try testing.expectEqual(original.z, decoded.z);
try testing.expectEqual(original.ad, decoded.ad);
try testing.expectEqual(original.cd, decoded.cd);
try testing.expectEqual(original.rcode, decoded.rcode);
try testing.expectEqual(original.qdcount, decoded.qdcount);
try testing.expectEqual(original.ancount, decoded.ancount);
try testing.expectEqual(original.nscount, decoded.nscount);
try testing.expectEqual(original.arcount, decoded.arcount);
}
test "create response from query" {
const testing = std.testing;
const query = Header{
.id = 0x1234,
.qr = false,
.opcode = types.OpCode.Query,
.aa = false,
.tc = false,
.rd = true,
.ra = false,
.z = false,
.ad = false,
.cd = false,
.rcode = types.RCode.NoError,
.qdcount = 1,
.ancount = 0,
.nscount = 0,
.arcount = 0,
};
const response = Header.createResponse(query, types.RCode.NoError);
try testing.expectEqual(query.id, response.id);
try testing.expect(response.qr); // Is response
try testing.expectEqual(query.opcode, response.opcode);
try testing.expectEqual(query.rd, response.rd);
try testing.expect(response.ra); // Recursion available
try testing.expectEqual(types.RCode.NoError, response.rcode);
}
test "buffer too small" {
const testing = std.testing;
const buffer = [_]u8{ 0x00, 0x01 }; // Only 2 bytes
try testing.expectError(error.BufferTooSmall, Header.parse(&buffer));
}
+540
View File
@@ -0,0 +1,540 @@
const std = @import("std");
const types = @import("types.zig");
const Allocator = std.mem.Allocator;
/// DNS Name with support for compression
pub const Name = struct {
/// Raw labels (without compression, each label prefixed with length)
labels: []const u8,
/// Allocator used for labels (null if labels are a view into packet)
allocator: ?Allocator,
pub const ParseError = error{
InvalidLabel,
NameTooLong,
LabelTooLong,
CompressionLoop,
InvalidPointer,
BufferTooSmall,
OutOfMemory,
};
pub const ParseResult = struct {
name: Name,
bytes_read: usize,
};
/// Parse a DNS name from a buffer with compression support (iterative, no recursion)
/// packet_start is the beginning of the entire DNS packet (for resolving pointers)
pub fn parse(buffer: []const u8, packet_start: []const u8, allocator: Allocator) ParseError!ParseResult {
// Pre-allocate to avoid multiple reallocations
var labels = try std.ArrayListUnmanaged(u8).initCapacity(allocator, types.MAX_NAME_LENGTH + 1);
errdefer labels.deinit(allocator);
var bytes_read: usize = 0;
var final_bytes_read: ?usize = null;
var total_length: usize = 0;
var jumps: usize = 0;
const max_jumps: usize = 128; // Prevent infinite compression loops
// Track visited offsets to detect loops more robustly
// Use a bitset for packet offsets up to 16KB (covers typical DNS packets)
// For offsets beyond 16KB, fall back to jump counter only
var visited: [2048]u8 = [_]u8{0} ** 2048; // 16384 bits = 16KB coverage
// Current parsing position - can switch buffers on pointer jumps
var current_buffer: []const u8 = buffer;
var pos: usize = 0;
while (true) {
if (pos >= current_buffer.len) {
return error.BufferTooSmall;
}
const len_byte = current_buffer[pos];
// Check for compression pointer (top 2 bits set)
if ((len_byte & types.COMPRESSION_POINTER_MASK) == types.COMPRESSION_POINTER_MASK) {
if (pos + 1 >= current_buffer.len) {
return error.BufferTooSmall;
}
// Store the bytes read before first pointer (only from original buffer)
if (final_bytes_read == null) {
final_bytes_read = bytes_read + 2;
}
// Get pointer offset (14 bits)
const ptr_high: u16 = @as(u16, len_byte & 0x3F) << 8;
const ptr_low: u16 = current_buffer[pos + 1];
const offset: usize = ptr_high | ptr_low;
// Validate pointer - must be within packet bounds
if (offset >= packet_start.len) {
return error.InvalidPointer;
}
// Check for compression loops using visited bitset
if (offset < 16384) {
const byte_idx = offset / 8;
const bit_idx: u3 = @intCast(offset % 8);
if ((visited[byte_idx] & (@as(u8, 1) << bit_idx)) != 0) {
return error.CompressionLoop;
}
visited[byte_idx] |= (@as(u8, 1) << bit_idx);
}
// Also enforce jump counter as secondary protection
jumps += 1;
if (jumps > max_jumps) {
return error.CompressionLoop;
}
// Jump to pointed location (iteratively, no recursion)
current_buffer = packet_start[offset..];
pos = 0;
continue;
}
// Normal label
const label_len = len_byte;
if (label_len == 0) {
// End of name
try labels.append(allocator, 0);
// Only add to bytes_read if we haven't jumped
if (final_bytes_read == null) {
bytes_read += 1;
}
break;
}
if (label_len > types.MAX_LABEL_LENGTH) {
return error.LabelTooLong;
}
if (pos + 1 + label_len > current_buffer.len) {
return error.BufferTooSmall;
}
total_length += label_len + 1; // +1 for dot
if (total_length > types.MAX_NAME_LENGTH) {
return error.NameTooLong;
}
// Copy label length and data
try labels.append(allocator, len_byte);
try labels.appendSlice(allocator, current_buffer[pos + 1 .. pos + 1 + label_len]);
pos += 1 + label_len;
// Only add to bytes_read if we haven't jumped
if (final_bytes_read == null) {
bytes_read += 1 + label_len;
}
}
return ParseResult{
.name = Name{
.labels = try labels.toOwnedSlice(allocator),
.allocator = allocator,
},
.bytes_read = final_bytes_read orelse bytes_read,
};
}
/// Parse a name from a string (e.g., "www.google.com")
pub fn fromString(str: []const u8, allocator: Allocator) ParseError!Name {
if (str.len > types.MAX_NAME_LENGTH) {
return error.NameTooLong;
}
var labels = std.ArrayListUnmanaged(u8){};
errdefer labels.deinit(allocator);
// Handle root domain
if (str.len == 0 or (str.len == 1 and str[0] == '.')) {
try labels.append(allocator, 0);
return Name{
.labels = try labels.toOwnedSlice(allocator),
.allocator = allocator,
};
}
var iter = std.mem.splitScalar(u8, str, '.');
while (iter.next()) |label| {
if (label.len == 0) continue; // Skip empty labels (trailing dot)
if (label.len > types.MAX_LABEL_LENGTH) {
return error.LabelTooLong;
}
try labels.append(allocator, @intCast(label.len));
try labels.appendSlice(allocator, label);
}
try labels.append(allocator, 0); // Null terminator
return Name{
.labels = try labels.toOwnedSlice(allocator),
.allocator = allocator,
};
}
/// Convert the name to a string (e.g., "www.google.com")
pub fn toString(self: Name, allocator: Allocator) ![]u8 {
var result = std.ArrayListUnmanaged(u8){};
errdefer result.deinit(allocator);
var pos: usize = 0;
var first = true;
while (pos < self.labels.len) {
const label_len = self.labels[pos];
if (label_len == 0) break;
if (!first) {
try result.append(allocator, '.');
}
first = false;
const label_start = pos + 1;
const label_end = label_start + label_len;
if (label_end > self.labels.len) break;
try result.appendSlice(allocator, self.labels[label_start..label_end]);
pos = label_end;
}
return result.toOwnedSlice(allocator);
}
/// Convert the name to a string using a pre-allocated buffer (no allocation)
/// Returns null if buffer is too small
pub fn toStringBuf(self: Name, buf: []u8) ?[]const u8 {
var pos: usize = 0;
var out_pos: usize = 0;
var first = true;
while (pos < self.labels.len) {
const label_len = self.labels[pos];
if (label_len == 0) break;
if (!first) {
if (out_pos >= buf.len) return null;
buf[out_pos] = '.';
out_pos += 1;
}
first = false;
const label_start = pos + 1;
const label_end = label_start + label_len;
if (label_end > self.labels.len) break;
if (out_pos + label_len > buf.len) return null;
@memcpy(buf[out_pos..][0..label_len], self.labels[label_start..label_end]);
out_pos += label_len;
pos = label_end;
}
return buf[0..out_pos];
}
/// Encode the name into a buffer (without compression)
pub fn encode(self: Name, buffer: []u8) ParseError!usize {
if (buffer.len < self.labels.len) {
return error.BufferTooSmall;
}
@memcpy(buffer[0..self.labels.len], self.labels);
return self.labels.len;
}
/// Get the encoded length of the name
pub fn encodedLen(self: Name) usize {
return self.labels.len;
}
/// Check if two names are equal (case-insensitive per DNS spec)
pub fn eql(self: Name, other: Name) bool {
if (self.labels.len != other.labels.len) return false;
var pos: usize = 0;
while (pos < self.labels.len) {
const len = self.labels[pos];
if (len != other.labels[pos]) return false;
if (len == 0) break;
const start = pos + 1;
const end = start + len;
if (end > self.labels.len) return false;
for (self.labels[start..end], other.labels[start..end]) |a, b| {
if (std.ascii.toLower(a) != std.ascii.toLower(b)) return false;
}
pos = end;
}
return true;
}
/// Get the number of labels in the name
pub fn labelCount(self: Name) usize {
var count: usize = 0;
var pos: usize = 0;
while (pos < self.labels.len) {
const len = self.labels[pos];
if (len == 0) break;
count += 1;
pos += 1 + len;
}
return count;
}
/// Check if this name is a subdomain of another
/// E.g., "www.example.com" is a subdomain of "example.com"
pub fn isSubdomainOf(self: Name, parent: Name) bool {
const self_count = self.labelCount();
const parent_count = parent.labelCount();
// Self must have more labels than parent to be a subdomain
if (self_count <= parent_count) return false;
// Skip the first (self_count - parent_count) labels in self
// Then the remaining labels should match parent exactly
const labels_to_skip = self_count - parent_count;
var self_pos: usize = 0;
var skipped: usize = 0;
// Skip labels in self
while (skipped < labels_to_skip and self_pos < self.labels.len) {
const len = self.labels[self_pos];
if (len == 0) break;
self_pos += 1 + len;
skipped += 1;
}
// Now compare remaining self labels with all parent labels
var parent_pos: usize = 0;
while (self_pos < self.labels.len and parent_pos < parent.labels.len) {
const self_len = self.labels[self_pos];
const parent_len = parent.labels[parent_pos];
// Both null terminators = match
if (self_len == 0 and parent_len == 0) return true;
// Length mismatch
if (self_len != parent_len) return false;
// Compare label content case-insensitively
const self_end = self_pos + 1 + self_len;
const parent_end = parent_pos + 1 + parent_len;
if (self_end > self.labels.len or parent_end > parent.labels.len) return false;
for (self.labels[self_pos + 1 .. self_end], parent.labels[parent_pos + 1 .. parent_end]) |a, b| {
if (std.ascii.toLower(a) != std.ascii.toLower(b)) return false;
}
self_pos = self_end;
parent_pos = parent_end;
}
return false;
}
/// Free the name's memory
pub fn deinit(self: *Name) void {
if (self.allocator) |alloc| {
alloc.free(self.labels);
}
}
};
/// Compression map for encoding names with compression
pub const CompressionMap = struct {
map: std.StringHashMapUnmanaged(u16),
allocator: Allocator,
pub fn init(allocator: Allocator) CompressionMap {
return CompressionMap{
.map = std.StringHashMapUnmanaged(u16){},
.allocator = allocator,
};
}
pub fn deinit(self: *CompressionMap) void {
var iter = self.map.keyIterator();
while (iter.next()) |key| {
self.allocator.free(key.*);
}
self.map.deinit(self.allocator);
}
pub fn lookup(self: *CompressionMap, name: []const u8) ?u16 {
return self.map.get(name);
}
pub fn insert(self: *CompressionMap, name: []const u8, offset: u16) !void {
const key = try self.allocator.dupe(u8, name);
errdefer self.allocator.free(key);
try self.map.put(self.allocator, key, offset);
}
};
test "parse simple name" {
const testing = std.testing;
const allocator = testing.allocator;
// www.google.com encoded as: \x03www\x06google\x03com\x00
const buffer = [_]u8{ 0x03, 'w', 'o', 'w', 0x06, 'g', 'o', 'o', 'g', 'l', 'e', 0x03, 'c', 'o', 'm', 0x00 };
const result = try Name.parse(&buffer, &buffer, allocator);
defer {
var name = result.name;
name.deinit();
}
try testing.expectEqual(@as(usize, 16), result.bytes_read);
const str = try result.name.toString(allocator);
defer allocator.free(str);
try testing.expectEqualStrings("wow.google.com", str);
}
test "parse name with compression" {
const testing = std.testing;
const allocator = testing.allocator;
// Packet with compression:
// Offset 0: header (12 bytes, simulated with zeros)
// Offset 12: \x03www\x06google\x03com\x00 (first name)
// Offset 28: \x03api + pointer to offset 16 (google.com)
var packet: [36]u8 = undefined;
@memset(&packet, 0);
// First name at offset 12: www.google.com
packet[12] = 0x03;
packet[13] = 'w';
packet[14] = 'w';
packet[15] = 'w';
packet[16] = 0x06;
packet[17] = 'g';
packet[18] = 'o';
packet[19] = 'o';
packet[20] = 'g';
packet[21] = 'l';
packet[22] = 'e';
packet[23] = 0x03;
packet[24] = 'c';
packet[25] = 'o';
packet[26] = 'm';
packet[27] = 0x00;
// Second name at offset 28: api + pointer to google.com (offset 16)
packet[28] = 0x03;
packet[29] = 'a';
packet[30] = 'p';
packet[31] = 'i';
packet[32] = 0xC0; // Compression pointer
packet[33] = 16; // Points to offset 16 (google.com)
const result = try Name.parse(packet[28..], &packet, allocator);
defer {
var name = result.name;
name.deinit();
}
try testing.expectEqual(@as(usize, 6), result.bytes_read); // \x03api + 2 byte pointer
const str = try result.name.toString(allocator);
defer allocator.free(str);
try testing.expectEqualStrings("api.google.com", str);
}
test "name from string" {
const testing = std.testing;
const allocator = testing.allocator;
var name = try Name.fromString("www.example.com", allocator);
defer name.deinit();
const str = try name.toString(allocator);
defer allocator.free(str);
try testing.expectEqualStrings("www.example.com", str);
try testing.expectEqual(@as(usize, 3), name.labelCount());
}
test "name from string with trailing dot" {
const testing = std.testing;
const allocator = testing.allocator;
var name = try Name.fromString("www.example.com.", allocator);
defer name.deinit();
const str = try name.toString(allocator);
defer allocator.free(str);
try testing.expectEqualStrings("www.example.com", str);
}
test "root name" {
const testing = std.testing;
const allocator = testing.allocator;
var name = try Name.fromString("", allocator);
defer name.deinit();
const str = try name.toString(allocator);
defer allocator.free(str);
try testing.expectEqualStrings("", str);
try testing.expectEqual(@as(usize, 0), name.labelCount());
}
test "name encode and decode roundtrip" {
const testing = std.testing;
const allocator = testing.allocator;
var original = try Name.fromString("test.example.org", allocator);
defer original.deinit();
var buffer: [256]u8 = undefined;
const encoded_len = try original.encode(&buffer);
const result = try Name.parse(buffer[0..encoded_len], buffer[0..encoded_len], allocator);
defer {
var name = result.name;
name.deinit();
}
try testing.expect(original.eql(result.name));
}
test "name equality case insensitive" {
const testing = std.testing;
const allocator = testing.allocator;
var name1 = try Name.fromString("WWW.EXAMPLE.COM", allocator);
defer name1.deinit();
var name2 = try Name.fromString("www.example.com", allocator);
defer name2.deinit();
try testing.expect(name1.eql(name2));
}
test "label too long" {
const testing = std.testing;
const allocator = testing.allocator;
// Create a label with 64 characters (max is 63)
const long_label = "a" ** 64 ++ ".example.com";
try testing.expectError(error.LabelTooLong, Name.fromString(long_label, allocator));
}
+518
View File
@@ -0,0 +1,518 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
pub const types = @import("types.zig");
pub const Header = @import("header.zig").Header;
pub const Name = @import("name.zig").Name;
pub const CompressionMap = @import("name.zig").CompressionMap;
pub const Question = @import("question.zig").Question;
pub const ResourceRecord = @import("record.zig").ResourceRecord;
pub const RData = @import("record.zig").RData;
pub const createARecord = @import("record.zig").createARecord;
pub const createAAAARecord = @import("record.zig").createAAAARecord;
pub const createCnameRecord = @import("record.zig").createCnameRecord;
pub const Edns = @import("edns.zig").Edns;
pub const hasEdns = @import("edns.zig").hasEdns;
pub const extractEdns = @import("edns.zig").extractEdns;
/// TTL for blocked/synthetic responses (in seconds)
pub const DENIED_RESPONSE_TTL: u32 = 60;
/// Complete DNS Packet
pub const Packet = struct {
header: Header,
questions: []Question,
answers: []ResourceRecord,
authority: []ResourceRecord,
additional: []ResourceRecord,
allocator: Allocator,
/// Raw bytes of the original packet (for forwarding)
raw_bytes: ?[]const u8 = null,
pub const ParseError = error{
BufferTooSmall,
InvalidLabel,
NameTooLong,
LabelTooLong,
CompressionLoop,
InvalidPointer,
OutOfMemory,
InvalidRData,
HeaderParseError,
};
/// Parse a complete DNS packet from a buffer
pub fn parse(buffer: []const u8, allocator: Allocator) ParseError!Packet {
// Parse header
const header = Header.parse(buffer) catch return error.HeaderParseError;
var offset: usize = Header.SIZE;
// Parse questions
var questions = try allocator.alloc(Question, header.qdcount);
errdefer {
for (questions) |*q| {
q.deinit();
}
allocator.free(questions);
}
var q_idx: usize = 0;
while (q_idx < header.qdcount) : (q_idx += 1) {
const result = try Question.parse(buffer[offset..], buffer, allocator);
questions[q_idx] = result.question;
offset += result.bytes_read;
}
// Parse answers
var answers = try allocator.alloc(ResourceRecord, header.ancount);
errdefer {
for (answers[0..]) |*a| {
a.deinit(allocator);
}
allocator.free(answers);
}
var a_idx: usize = 0;
while (a_idx < header.ancount) : (a_idx += 1) {
const result = try ResourceRecord.parse(buffer[offset..], buffer, allocator);
answers[a_idx] = result.record;
offset += result.bytes_read;
}
// Parse authority
var authority = try allocator.alloc(ResourceRecord, header.nscount);
errdefer {
for (authority[0..]) |*ns| {
ns.deinit(allocator);
}
allocator.free(authority);
}
var ns_idx: usize = 0;
while (ns_idx < header.nscount) : (ns_idx += 1) {
const result = try ResourceRecord.parse(buffer[offset..], buffer, allocator);
authority[ns_idx] = result.record;
offset += result.bytes_read;
}
// Parse additional
var additional = try allocator.alloc(ResourceRecord, header.arcount);
errdefer {
for (additional[0..]) |*ar| {
ar.deinit(allocator);
}
allocator.free(additional);
}
var ar_idx: usize = 0;
while (ar_idx < header.arcount) : (ar_idx += 1) {
const result = try ResourceRecord.parse(buffer[offset..], buffer, allocator);
additional[ar_idx] = result.record;
offset += result.bytes_read;
}
// Store raw bytes
const raw = try allocator.dupe(u8, buffer);
return Packet{
.header = header,
.questions = questions,
.answers = answers,
.authority = authority,
.additional = additional,
.allocator = allocator,
.raw_bytes = raw,
};
}
/// Encode the packet into a buffer
pub fn encode(self: Packet, buffer: []u8) ParseError!usize {
var offset: usize = 0;
// Update header counts
var header = self.header;
header.qdcount = @intCast(self.questions.len);
header.ancount = @intCast(self.answers.len);
header.nscount = @intCast(self.authority.len);
header.arcount = @intCast(self.additional.len);
// Encode header
header.encode(buffer[offset..]) catch return error.BufferTooSmall;
offset += Header.SIZE;
// Encode questions
for (self.questions) |q| {
const len = try q.encode(buffer[offset..]);
offset += len;
}
// Encode answers
for (self.answers) |a| {
const len = try a.encode(buffer[offset..], self.allocator);
offset += len;
}
// Encode authority
for (self.authority) |ns| {
const len = try ns.encode(buffer[offset..], self.allocator);
offset += len;
}
// Encode additional
for (self.additional) |ar| {
const len = try ar.encode(buffer[offset..], self.allocator);
offset += len;
}
return offset;
}
/// Free all packet memory
pub fn deinit(self: *Packet) void {
for (self.questions) |*q| {
q.deinit();
}
self.allocator.free(self.questions);
for (self.answers) |*a| {
a.deinit(self.allocator);
}
self.allocator.free(self.answers);
for (self.authority) |*ns| {
ns.deinit(self.allocator);
}
self.allocator.free(self.authority);
for (self.additional) |*ar| {
ar.deinit(self.allocator);
}
self.allocator.free(self.additional);
if (self.raw_bytes) |raw| {
self.allocator.free(raw);
}
}
/// Check if this packet is a query
pub fn isQuery(self: Packet) bool {
return self.header.isQuery();
}
/// Check if this packet is a response
pub fn isResponse(self: Packet) bool {
return self.header.isResponse();
}
/// Get the first question's domain name as a string
pub fn getQueryName(self: Packet) ![]u8 {
if (self.questions.len == 0) return error.NoQuestion;
return self.questions[0].name.toString(self.allocator);
}
pub const NoQuestionError = error{NoQuestion};
/// Get the first question's type
pub fn getQueryType(self: Packet) ?types.QType {
if (self.questions.len == 0) return null;
return self.questions[0].qtype;
}
/// Check if EDNS is present
pub fn hasEdnsSupport(self: Packet) bool {
return hasEdns(self.additional);
}
/// Get EDNS information if present
pub fn getEdns(self: Packet) !?Edns {
return extractEdns(self.additional, self.allocator);
}
/// Create a response packet from a query
pub fn createResponse(query: *const Packet, rcode: types.RCode, allocator: Allocator) !Packet {
const response_header = Header.createResponse(query.header, rcode);
// Clone questions
var questions = try allocator.alloc(Question, query.questions.len);
errdefer allocator.free(questions);
for (query.questions, 0..) |q, i| {
questions[i] = try q.clone(allocator);
}
return Packet{
.header = response_header,
.questions = questions,
.answers = try allocator.alloc(ResourceRecord, 0),
.authority = try allocator.alloc(ResourceRecord, 0),
.additional = try allocator.alloc(ResourceRecord, 0),
.allocator = allocator,
};
}
/// Create a denied response (returns 0.0.0.0 for A queries)
pub fn createDeniedResponse(query: *const Packet, allocator: Allocator) !Packet {
var response = try createResponse(query, types.RCode.NoError, allocator);
errdefer response.deinit();
if (query.questions.len > 0) {
const q = query.questions[0];
// Clone the name for the answer
const labels = try allocator.dupe(u8, q.name.labels);
const answer_name = Name{
.labels = labels,
.allocator = allocator,
};
var answers = try allocator.alloc(ResourceRecord, 1);
if (q.qtype == types.QType.A) {
answers[0] = createARecord(answer_name, DENIED_RESPONSE_TTL, [4]u8{ 0, 0, 0, 0 });
} else if (q.qtype == types.QType.AAAA) {
answers[0] = createAAAARecord(answer_name, DENIED_RESPONSE_TTL, [16]u8{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 });
} else {
// For other types, just return NXDOMAIN
answer_name.allocator.?.free(answer_name.labels);
allocator.free(answers);
response.header.rcode = types.RCode.NXDomain;
return response;
}
allocator.free(response.answers);
response.answers = answers;
response.header.ancount = 1;
}
return response;
}
/// Create an NXDOMAIN response
pub fn createNxdomainResponse(query: *const Packet, allocator: Allocator) !Packet {
return createResponse(query, types.RCode.NXDomain, allocator);
}
/// Create a safe search CNAME response that redirects to the safe domain
pub fn createSafeSearchResponse(query: *const Packet, safe_domain: []const u8, allocator: Allocator) !Packet {
var response = try createResponse(query, types.RCode.NoError, allocator);
errdefer response.deinit();
if (query.questions.len > 0) {
const q = query.questions[0];
// Clone the name for the answer
const labels = try allocator.dupe(u8, q.name.labels);
const answer_name = Name{
.labels = labels,
.allocator = allocator,
};
// Create the CNAME target
const target = try Name.fromString(safe_domain, allocator);
var answers = try allocator.alloc(ResourceRecord, 1);
answers[0] = createCnameRecord(answer_name, DENIED_RESPONSE_TTL, target);
allocator.free(response.answers);
response.answers = answers;
response.header.ancount = 1;
}
return response;
}
/// Create a SERVFAIL response
pub fn createServfailResponse(query: *const Packet, allocator: Allocator) !Packet {
return createResponse(query, types.RCode.ServFail, allocator);
}
/// Create a FORMERR response
pub fn createFormerrResponse(query: *const Packet, allocator: Allocator) !Packet {
return createResponse(query, types.RCode.FormErr, allocator);
}
};
/// Parse only the header from a buffer (for quick inspection)
pub fn parseHeaderOnly(buffer: []const u8) !Header {
return Header.parse(buffer);
}
/// Get the transaction ID from a buffer without full parsing
pub fn getTransactionId(buffer: []const u8) ?u16 {
if (buffer.len < 2) return null;
return std.mem.readInt(u16, buffer[0..2], .big);
}
test "parse simple query packet" {
const testing = std.testing;
const allocator = testing.allocator;
// Simple A query for www.google.com
const query = [_]u8{
// Header
0x00, 0x01, // ID = 1
0x01, 0x00, // Standard query, RD=1
0x00, 0x01, // QDCOUNT = 1
0x00, 0x00, // ANCOUNT = 0
0x00, 0x00, // NSCOUNT = 0
0x00, 0x00, // ARCOUNT = 0
// Question
0x03, 'w', 'w', 'w', // www
0x06, 'g', 'o', 'o', 'g', 'l', 'e', // google
0x03, 'c', 'o', 'm', // com
0x00, // null terminator
0x00, 0x01, // TYPE = A
0x00, 0x01, // CLASS = IN
};
var packet = try Packet.parse(&query, allocator);
defer packet.deinit();
try testing.expectEqual(@as(u16, 1), packet.header.id);
try testing.expect(packet.isQuery());
try testing.expect(!packet.isResponse());
try testing.expectEqual(@as(usize, 1), packet.questions.len);
try testing.expectEqual(@as(usize, 0), packet.answers.len);
const name = try packet.getQueryName();
defer allocator.free(name);
try testing.expectEqualStrings("www.google.com", name);
try testing.expectEqual(types.QType.A, packet.getQueryType().?);
}
test "parse response packet with A record" {
const testing = std.testing;
const allocator = testing.allocator;
// Response for example.com with A record 93.184.216.34
const response = [_]u8{
// Header
0x00, 0x02, // ID = 2
0x81, 0x80, // Response, RD=1, RA=1
0x00, 0x01, // QDCOUNT = 1
0x00, 0x01, // ANCOUNT = 1
0x00, 0x00, // NSCOUNT = 0
0x00, 0x00, // ARCOUNT = 0
// Question
0x07, 'e', 'x', 'a', 'm', 'p', 'l', 'e', // example
0x03, 'c', 'o', 'm', // com
0x00, // null terminator
0x00, 0x01, // TYPE = A
0x00, 0x01, // CLASS = IN
// Answer
0xC0, 0x0C, // Pointer to offset 12 (example.com)
0x00, 0x01, // TYPE = A
0x00, 0x01, // CLASS = IN
0x00, 0x00, 0x01, 0x2C, // TTL = 300
0x00, 0x04, // RDLENGTH = 4
0x5D, 0xB8, 0xD8, 0x22, // 93.184.216.34
};
var packet = try Packet.parse(&response, allocator);
defer packet.deinit();
try testing.expectEqual(@as(u16, 2), packet.header.id);
try testing.expect(packet.isResponse());
try testing.expectEqual(@as(usize, 1), packet.questions.len);
try testing.expectEqual(@as(usize, 1), packet.answers.len);
const answer = packet.answers[0];
try testing.expectEqual(types.QType.A, answer.rtype);
try testing.expectEqual(@as(u32, 300), answer.ttl);
const ip = answer.getIPv4().?;
try testing.expectEqual([4]u8{ 93, 184, 216, 34 }, ip);
}
test "create blocked response" {
const testing = std.testing;
const allocator = testing.allocator;
// Create a query
const query_bytes = [_]u8{
// Header
0x00, 0x03, // ID = 3
0x01, 0x00, // Standard query, RD=1
0x00, 0x01, // QDCOUNT = 1
0x00, 0x00, // ANCOUNT = 0
0x00, 0x00, // NSCOUNT = 0
0x00, 0x00, // ARCOUNT = 0
// Question
0x09, 'd', 'o', 'u', 'b', 'l', 'e', 'c', 'l', 'k', // doubleclk
0x03, 'n', 'e', 't', // net
0x00, // null terminator
0x00, 0x01, // TYPE = A
0x00, 0x01, // CLASS = IN
};
var query = try Packet.parse(&query_bytes, allocator);
defer query.deinit();
var denied = try Packet.createDeniedResponse(&query, allocator);
defer denied.deinit();
try testing.expect(denied.isResponse());
try testing.expectEqual(@as(u16, 3), denied.header.id);
try testing.expectEqual(@as(usize, 1), denied.answers.len);
const answer = denied.answers[0];
try testing.expectEqual(types.QType.A, answer.rtype);
try testing.expectEqual([4]u8{ 0, 0, 0, 0 }, answer.getIPv4().?);
}
test "encode and decode packet roundtrip" {
const testing = std.testing;
const allocator = testing.allocator;
// Simple query
const original_bytes = [_]u8{
// Header
0xAB, 0xCD, // ID
0x01, 0x00, // Standard query, RD=1
0x00, 0x01, // QDCOUNT = 1
0x00, 0x00, // ANCOUNT = 0
0x00, 0x00, // NSCOUNT = 0
0x00, 0x00, // ARCOUNT = 0
// Question
0x04, 't', 'e', 's', 't', // test
0x03, 'c', 'o', 'm', // com
0x00, // null terminator
0x00, 0x01, // TYPE = A
0x00, 0x01, // CLASS = IN
};
var original = try Packet.parse(&original_bytes, allocator);
defer original.deinit();
// Encode
var buffer: [512]u8 = undefined;
const encoded_len = try original.encode(&buffer);
// Decode
var decoded = try Packet.parse(buffer[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);
const orig_name = try original.getQueryName();
defer allocator.free(orig_name);
const dec_name = try decoded.getQueryName();
defer allocator.free(dec_name);
try testing.expectEqualStrings(orig_name, dec_name);
}
test "get transaction ID" {
const testing = std.testing;
const buffer = [_]u8{ 0x12, 0x34, 0x01, 0x00 };
try testing.expectEqual(@as(u16, 0x1234), getTransactionId(&buffer).?);
const short = [_]u8{0x12};
try testing.expect(getTransactionId(&short) == null);
}
+214
View File
@@ -0,0 +1,214 @@
const std = @import("std");
const types = @import("types.zig");
const Name = @import("name.zig").Name;
const Allocator = std.mem.Allocator;
/// DNS Question Section
/// Format:
/// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
/// | QNAME |
/// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
/// | QTYPE |
/// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
/// | QCLASS |
/// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
pub const Question = struct {
/// Query name
name: Name,
/// Query type
qtype: types.QType,
/// Query class
qclass: types.QClass,
pub const ParseError = error{
BufferTooSmall,
InvalidLabel,
NameTooLong,
LabelTooLong,
CompressionLoop,
InvalidPointer,
OutOfMemory,
};
pub const ParseResult = struct {
question: Question,
bytes_read: usize,
};
/// Parse a question from a buffer
pub fn parse(buffer: []const u8, packet_start: []const u8, allocator: Allocator) ParseError!ParseResult {
// Parse the name first
const name_result = try Name.parse(buffer, packet_start, allocator);
errdefer {
var n = name_result.name;
n.deinit();
}
const remaining = buffer[name_result.bytes_read..];
if (remaining.len < 4) {
var n = name_result.name;
n.deinit();
return error.BufferTooSmall;
}
const qtype = std.mem.readInt(u16, remaining[0..2], .big);
const qclass = std.mem.readInt(u16, remaining[2..4], .big);
return ParseResult{
.question = Question{
.name = name_result.name,
.qtype = @enumFromInt(qtype),
.qclass = @enumFromInt(qclass),
},
.bytes_read = name_result.bytes_read + 4,
};
}
/// Encode the question into a buffer
pub fn encode(self: Question, buffer: []u8) ParseError!usize {
var offset: usize = 0;
// Encode name
const name_len = try self.name.encode(buffer[offset..]);
offset += name_len;
if (buffer.len < offset + 4) {
return error.BufferTooSmall;
}
// Encode type and class
std.mem.writeInt(u16, buffer[offset..][0..2], @intFromEnum(self.qtype), .big);
offset += 2;
std.mem.writeInt(u16, buffer[offset..][0..2], @intFromEnum(self.qclass), .big);
offset += 2;
return offset;
}
/// Get the encoded length of the question
pub fn encodedLen(self: Question) usize {
return self.name.encodedLen() + 4; // name + qtype (2) + qclass (2)
}
/// Free the question's memory
pub fn deinit(self: *Question) void {
self.name.deinit();
}
/// Create a copy of the question
pub fn clone(self: Question, allocator: Allocator) !Question {
const labels = try allocator.dupe(u8, self.name.labels);
return Question{
.name = Name{
.labels = labels,
.allocator = allocator,
},
.qtype = self.qtype,
.qclass = self.qclass,
};
}
};
test "parse simple question" {
const testing = std.testing;
const allocator = testing.allocator;
// www.google.com A IN
const buffer = [_]u8{
0x03, 'w', 'w', 'w', // www
0x06, 'g', 'o', 'o', 'g', 'l', 'e', // google
0x03, 'c', 'o', 'm', // com
0x00, // null terminator
0x00, 0x01, // TYPE = A
0x00, 0x01, // CLASS = IN
};
const result = try Question.parse(&buffer, &buffer, allocator);
defer {
var q = result.question;
q.deinit();
}
try testing.expectEqual(@as(usize, 20), result.bytes_read);
try testing.expectEqual(types.QType.A, result.question.qtype);
try testing.expectEqual(types.QClass.IN, result.question.qclass);
const name_str = try result.question.name.toString(allocator);
defer allocator.free(name_str);
try testing.expectEqualStrings("www.google.com", name_str);
}
test "parse AAAA question" {
const testing = std.testing;
const allocator = testing.allocator;
// example.com AAAA IN
const buffer = [_]u8{
0x07, 'e', 'x', 'a', 'm', 'p', 'l', 'e', // example
0x03, 'c', 'o', 'm', // com
0x00, // null terminator
0x00, 0x1C, // TYPE = AAAA (28)
0x00, 0x01, // CLASS = IN
};
const result = try Question.parse(&buffer, &buffer, allocator);
defer {
var q = result.question;
q.deinit();
}
try testing.expectEqual(types.QType.AAAA, result.question.qtype);
try testing.expectEqual(types.QClass.IN, result.question.qclass);
}
test "encode and decode question roundtrip" {
const testing = std.testing;
const allocator = testing.allocator;
var name = try Name.fromString("test.example.org", allocator);
errdefer name.deinit();
const original = Question{
.name = name,
.qtype = types.QType.MX,
.qclass = types.QClass.IN,
};
defer {
var q = @as(Question, original);
q.deinit();
}
var buffer: [256]u8 = undefined;
const encoded_len = try original.encode(&buffer);
const result = try Question.parse(buffer[0..encoded_len], buffer[0..encoded_len], allocator);
defer {
var q = result.question;
q.deinit();
}
try testing.expectEqual(original.qtype, result.question.qtype);
try testing.expectEqual(original.qclass, result.question.qclass);
try testing.expect(original.name.eql(result.question.name));
}
test "question encoded length" {
const testing = std.testing;
const allocator = testing.allocator;
var name = try Name.fromString("www.example.com", allocator);
errdefer name.deinit();
const q = Question{
.name = name,
.qtype = types.QType.A,
.qclass = types.QClass.IN,
};
defer {
var question = @as(Question, q);
question.deinit();
}
// www(1+3) + example(1+7) + com(1+3) + null(1) + type(2) + class(2) = 21
try testing.expectEqual(@as(usize, 21), q.encodedLen());
}
+983
View File
@@ -0,0 +1,983 @@
const std = @import("std");
const types = @import("types.zig");
const Name = @import("name.zig").Name;
const Allocator = std.mem.Allocator;
/// Resource Record Data (tagged union)
pub const RData = union(enum) {
/// A record - IPv4 address (4 bytes)
a: [4]u8,
/// AAAA record - IPv6 address (16 bytes)
aaaa: [16]u8,
/// CNAME record - canonical name
cname: Name,
/// NS record - name server
ns: Name,
/// PTR record - pointer
ptr: Name,
/// MX record - mail exchange
mx: struct {
preference: u16,
exchange: Name,
},
/// TXT record - text strings
txt: []const []const u8,
/// SOA record - start of authority
soa: struct {
mname: Name,
rname: Name,
serial: u32,
refresh: u32,
retry: u32,
expire: u32,
minimum: u32,
},
/// SRV record - service locator
srv: struct {
priority: u16,
weight: u16,
port: u16,
target: Name,
},
/// OPT record (EDNS) - stored as raw data
opt: []const u8,
// DNSSEC record types
/// DNSKEY record - public key for DNSSEC
dnskey: struct {
flags: u16, // Zone key flag (bit 7), SEP flag (bit 15)
protocol: u8, // Must be 3
algorithm: u8, // DNSSEC algorithm number
public_key: []const u8,
},
/// DS record - Delegation Signer
ds: struct {
key_tag: u16,
algorithm: u8,
digest_type: u8,
digest: []const u8,
},
/// RRSIG record - signature over RRset
rrsig: struct {
type_covered: u16,
algorithm: u8,
labels: u8,
original_ttl: u32,
signature_expiration: u32,
signature_inception: u32,
key_tag: u16,
signer_name: Name,
signature: []const u8,
},
/// NSEC record - authenticated denial of existence
nsec: struct {
next_domain: Name,
type_bitmap: []const u8,
},
/// NSEC3 record - hashed authenticated denial
nsec3: struct {
hash_algorithm: u8,
flags: u8,
iterations: u16,
salt: []const u8,
next_hashed_owner: []const u8,
type_bitmap: []const u8,
},
/// NSEC3PARAM record - NSEC3 parameters
nsec3param: struct {
hash_algorithm: u8,
flags: u8,
iterations: u16,
salt: []const u8,
},
/// Unknown/raw record data
raw: []const u8,
pub fn deinit(self: *RData, allocator: Allocator) void {
switch (self.*) {
.cname => |*name| name.deinit(),
.ns => |*name| name.deinit(),
.ptr => |*name| name.deinit(),
.mx => |*mx| mx.exchange.deinit(),
.txt => |txt| {
for (txt) |s| {
allocator.free(s);
}
allocator.free(txt);
},
.soa => |*soa| {
soa.mname.deinit();
soa.rname.deinit();
},
.srv => |*srv| srv.target.deinit(),
.opt => |data| allocator.free(data),
// DNSSEC types
.dnskey => |*dk| allocator.free(dk.public_key),
.ds => |*ds| allocator.free(ds.digest),
.rrsig => |*rrsig| {
rrsig.signer_name.deinit();
allocator.free(rrsig.signature);
},
.nsec => |*nsec| {
nsec.next_domain.deinit();
allocator.free(nsec.type_bitmap);
},
.nsec3 => |*nsec3| {
allocator.free(nsec3.salt);
allocator.free(nsec3.next_hashed_owner);
allocator.free(nsec3.type_bitmap);
},
.nsec3param => |*np| allocator.free(np.salt),
.raw => |data| allocator.free(data),
.a, .aaaa => {},
}
}
};
/// DNS Resource Record
/// Format:
/// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
/// | NAME |
/// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
/// | TYPE |
/// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
/// | CLASS |
/// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
/// | TTL |
/// | |
/// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
/// | RDLENGTH |
/// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
/// | RDATA |
/// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
pub const ResourceRecord = struct {
name: Name,
rtype: types.QType,
class: types.QClass,
ttl: u32,
rdata: RData,
pub const ParseError = error{
BufferTooSmall,
InvalidLabel,
NameTooLong,
LabelTooLong,
CompressionLoop,
InvalidPointer,
OutOfMemory,
InvalidRData,
};
pub const ParseResult = struct {
record: ResourceRecord,
bytes_read: usize,
};
/// Parse a resource record from a buffer
pub fn parse(buffer: []const u8, packet_start: []const u8, allocator: Allocator) ParseError!ParseResult {
// Parse the name
const name_result = try Name.parse(buffer, packet_start, allocator);
errdefer {
var n = name_result.name;
n.deinit();
}
var offset = name_result.bytes_read;
const remaining = buffer[offset..];
if (remaining.len < 10) {
var n = name_result.name;
n.deinit();
return error.BufferTooSmall;
}
const rtype_val = std.mem.readInt(u16, remaining[0..2], .big);
const class_val = std.mem.readInt(u16, remaining[2..4], .big);
const ttl = std.mem.readInt(u32, remaining[4..8], .big);
const rdlength = std.mem.readInt(u16, remaining[8..10], .big);
offset += 10;
const rdata_buf = buffer[offset..];
if (rdata_buf.len < rdlength) {
var n = name_result.name;
n.deinit();
return error.BufferTooSmall;
}
const rtype: types.QType = @enumFromInt(rtype_val);
const rdata = try parseRData(rtype, rdata_buf[0..rdlength], packet_start, allocator);
errdefer {
var rd = rdata;
rd.deinit(allocator);
}
return ParseResult{
.record = ResourceRecord{
.name = name_result.name,
.rtype = rtype,
.class = @enumFromInt(class_val),
.ttl = ttl,
.rdata = rdata,
},
.bytes_read = offset + rdlength,
};
}
fn parseRData(rtype: types.QType, data: []const u8, packet_start: []const u8, allocator: Allocator) ParseError!RData {
switch (rtype) {
.A => {
if (data.len != 4) return error.InvalidRData;
return RData{ .a = data[0..4].* };
},
.AAAA => {
if (data.len != 16) return error.InvalidRData;
return RData{ .aaaa = data[0..16].* };
},
.CNAME => {
const name_result = try Name.parse(data, packet_start, allocator);
return RData{ .cname = name_result.name };
},
.NS => {
const name_result = try Name.parse(data, packet_start, allocator);
return RData{ .ns = name_result.name };
},
.PTR => {
const name_result = try Name.parse(data, packet_start, allocator);
return RData{ .ptr = name_result.name };
},
.MX => {
if (data.len < 3) return error.InvalidRData;
const preference = std.mem.readInt(u16, data[0..2], .big);
const name_result = try Name.parse(data[2..], packet_start, allocator);
return RData{
.mx = .{
.preference = preference,
.exchange = name_result.name,
},
};
},
.TXT => {
var strings = std.ArrayListUnmanaged([]const u8){};
errdefer {
for (strings.items) |s| {
allocator.free(s);
}
strings.deinit(allocator);
}
var pos: usize = 0;
while (pos < data.len) {
const str_len = data[pos];
pos += 1;
if (pos + str_len > data.len) return error.InvalidRData;
const str = try allocator.dupe(u8, data[pos .. pos + str_len]);
try strings.append(allocator, str);
pos += str_len;
}
return RData{ .txt = try strings.toOwnedSlice(allocator) };
},
.SOA => {
var offset: usize = 0;
const mname_result = try Name.parse(data, packet_start, allocator);
errdefer {
var n = mname_result.name;
n.deinit();
}
offset += mname_result.bytes_read;
const rname_result = try Name.parse(data[offset..], packet_start, allocator);
errdefer {
var n = rname_result.name;
n.deinit();
}
offset += rname_result.bytes_read;
if (data.len < offset + 20) return error.InvalidRData;
const serial = std.mem.readInt(u32, data[offset..][0..4], .big);
const refresh = std.mem.readInt(u32, data[offset + 4 ..][0..4], .big);
const retry = std.mem.readInt(u32, data[offset + 8 ..][0..4], .big);
const expire = std.mem.readInt(u32, data[offset + 12 ..][0..4], .big);
const minimum = std.mem.readInt(u32, data[offset + 16 ..][0..4], .big);
return RData{
.soa = .{
.mname = mname_result.name,
.rname = rname_result.name,
.serial = serial,
.refresh = refresh,
.retry = retry,
.expire = expire,
.minimum = minimum,
},
};
},
.SRV => {
if (data.len < 7) return error.InvalidRData;
const priority = std.mem.readInt(u16, data[0..2], .big);
const weight = std.mem.readInt(u16, data[2..4], .big);
const port = std.mem.readInt(u16, data[4..6], .big);
const target_result = try Name.parse(data[6..], packet_start, allocator);
return RData{
.srv = .{
.priority = priority,
.weight = weight,
.port = port,
.target = target_result.name,
},
};
},
.OPT => {
const opt_data = try allocator.dupe(u8, data);
return RData{ .opt = opt_data };
},
// DNSSEC record types
.DNSKEY => {
if (data.len < 4) return error.InvalidRData;
const flags = std.mem.readInt(u16, data[0..2], .big);
const protocol = data[2];
const algorithm = data[3];
const public_key = try allocator.dupe(u8, data[4..]);
return RData{ .dnskey = .{
.flags = flags,
.protocol = protocol,
.algorithm = algorithm,
.public_key = public_key,
} };
},
.DS => {
if (data.len < 4) return error.InvalidRData;
const key_tag = std.mem.readInt(u16, data[0..2], .big);
const algorithm = data[2];
const digest_type = data[3];
const digest = try allocator.dupe(u8, data[4..]);
return RData{ .ds = .{
.key_tag = key_tag,
.algorithm = algorithm,
.digest_type = digest_type,
.digest = digest,
} };
},
.RRSIG => {
// RRSIG format: type_covered(2) + algorithm(1) + labels(1) + original_ttl(4) +
// sig_expiration(4) + sig_inception(4) + key_tag(2) + signer_name + signature
if (data.len < 18) return error.InvalidRData;
const type_covered = std.mem.readInt(u16, data[0..2], .big);
const algorithm = data[2];
const labels = data[3];
const original_ttl = std.mem.readInt(u32, data[4..8], .big);
const signature_expiration = std.mem.readInt(u32, data[8..12], .big);
const signature_inception = std.mem.readInt(u32, data[12..16], .big);
const key_tag = std.mem.readInt(u16, data[16..18], .big);
const name_result = try Name.parse(data[18..], packet_start, allocator);
errdefer {
var n = name_result.name;
n.deinit();
}
const sig_start = 18 + name_result.bytes_read;
const signature = try allocator.dupe(u8, data[sig_start..]);
return RData{ .rrsig = .{
.type_covered = type_covered,
.algorithm = algorithm,
.labels = labels,
.original_ttl = original_ttl,
.signature_expiration = signature_expiration,
.signature_inception = signature_inception,
.key_tag = key_tag,
.signer_name = name_result.name,
.signature = signature,
} };
},
.NSEC => {
const name_result = try Name.parse(data, packet_start, allocator);
errdefer {
var n = name_result.name;
n.deinit();
}
const type_bitmap = try allocator.dupe(u8, data[name_result.bytes_read..]);
return RData{ .nsec = .{
.next_domain = name_result.name,
.type_bitmap = type_bitmap,
} };
},
.NSEC3 => {
// NSEC3 format: hash_algorithm(1) + flags(1) + iterations(2) + salt_length(1) +
// salt + hash_length(1) + hash + type_bitmap
if (data.len < 5) return error.InvalidRData;
const hash_algorithm = data[0];
const flags = data[1];
const iterations = std.mem.readInt(u16, data[2..4], .big);
const salt_length = data[4];
var offset: usize = 5;
if (data.len < offset + salt_length) return error.InvalidRData;
const salt = try allocator.dupe(u8, data[offset .. offset + salt_length]);
errdefer allocator.free(salt);
offset += salt_length;
if (data.len < offset + 1) return error.InvalidRData;
const hash_length = data[offset];
offset += 1;
if (data.len < offset + hash_length) return error.InvalidRData;
const next_hashed_owner = try allocator.dupe(u8, data[offset .. offset + hash_length]);
errdefer allocator.free(next_hashed_owner);
offset += hash_length;
const type_bitmap = try allocator.dupe(u8, data[offset..]);
return RData{ .nsec3 = .{
.hash_algorithm = hash_algorithm,
.flags = flags,
.iterations = iterations,
.salt = salt,
.next_hashed_owner = next_hashed_owner,
.type_bitmap = type_bitmap,
} };
},
.NSEC3PARAM => {
if (data.len < 5) return error.InvalidRData;
const hash_algorithm = data[0];
const flags = data[1];
const iterations = std.mem.readInt(u16, data[2..4], .big);
const salt_length = data[4];
if (data.len < 5 + salt_length) return error.InvalidRData;
const salt = try allocator.dupe(u8, data[5 .. 5 + salt_length]);
return RData{ .nsec3param = .{
.hash_algorithm = hash_algorithm,
.flags = flags,
.iterations = iterations,
.salt = salt,
} };
},
else => {
const raw_data = try allocator.dupe(u8, data);
return RData{ .raw = raw_data };
},
}
}
/// Encode the resource record into a buffer
pub fn encode(self: ResourceRecord, buffer: []u8, allocator: Allocator) ParseError!usize {
var offset: usize = 0;
// Encode name
const name_len = try self.name.encode(buffer[offset..]);
offset += name_len;
if (buffer.len < offset + 10) {
return error.BufferTooSmall;
}
// Encode type, class, ttl
std.mem.writeInt(u16, buffer[offset..][0..2], @intFromEnum(self.rtype), .big);
offset += 2;
std.mem.writeInt(u16, buffer[offset..][0..2], @intFromEnum(self.class), .big);
offset += 2;
std.mem.writeInt(u32, buffer[offset..][0..4], self.ttl, .big);
offset += 4;
// Reserve space for rdlength
const rdlength_offset = offset;
offset += 2;
// Encode rdata
const rdata_len = try self.encodeRData(buffer[offset..], allocator);
offset += rdata_len;
// Write rdlength
std.mem.writeInt(u16, buffer[rdlength_offset..][0..2], @intCast(rdata_len), .big);
return offset;
}
fn encodeRData(self: ResourceRecord, buffer: []u8, allocator: Allocator) ParseError!usize {
_ = allocator;
switch (self.rdata) {
.a => |addr| {
if (buffer.len < 4) return error.BufferTooSmall;
@memcpy(buffer[0..4], &addr);
return 4;
},
.aaaa => |addr| {
if (buffer.len < 16) return error.BufferTooSmall;
@memcpy(buffer[0..16], &addr);
return 16;
},
.cname => |name| {
return name.encode(buffer);
},
.ns => |name| {
return name.encode(buffer);
},
.ptr => |name| {
return name.encode(buffer);
},
.mx => |mx| {
if (buffer.len < 2) return error.BufferTooSmall;
std.mem.writeInt(u16, buffer[0..2], mx.preference, .big);
const name_len = try mx.exchange.encode(buffer[2..]);
return 2 + name_len;
},
.txt => |strings| {
var offset: usize = 0;
for (strings) |s| {
if (buffer.len < offset + 1 + s.len) return error.BufferTooSmall;
buffer[offset] = @intCast(s.len);
offset += 1;
@memcpy(buffer[offset .. offset + s.len], s);
offset += s.len;
}
return offset;
},
.soa => |soa| {
var offset: usize = 0;
offset += try soa.mname.encode(buffer[offset..]);
offset += try soa.rname.encode(buffer[offset..]);
if (buffer.len < offset + 20) return error.BufferTooSmall;
std.mem.writeInt(u32, buffer[offset..][0..4], soa.serial, .big);
offset += 4;
std.mem.writeInt(u32, buffer[offset..][0..4], soa.refresh, .big);
offset += 4;
std.mem.writeInt(u32, buffer[offset..][0..4], soa.retry, .big);
offset += 4;
std.mem.writeInt(u32, buffer[offset..][0..4], soa.expire, .big);
offset += 4;
std.mem.writeInt(u32, buffer[offset..][0..4], soa.minimum, .big);
offset += 4;
return offset;
},
.srv => |srv| {
if (buffer.len < 6) return error.BufferTooSmall;
std.mem.writeInt(u16, buffer[0..2], srv.priority, .big);
std.mem.writeInt(u16, buffer[2..4], srv.weight, .big);
std.mem.writeInt(u16, buffer[4..6], srv.port, .big);
const name_len = try srv.target.encode(buffer[6..]);
return 6 + name_len;
},
.opt => |data| {
if (buffer.len < data.len) return error.BufferTooSmall;
@memcpy(buffer[0..data.len], data);
return data.len;
},
// DNSSEC types
.dnskey => |dk| {
const min_len = 4 + dk.public_key.len;
if (buffer.len < min_len) return error.BufferTooSmall;
std.mem.writeInt(u16, buffer[0..2], dk.flags, .big);
buffer[2] = dk.protocol;
buffer[3] = dk.algorithm;
@memcpy(buffer[4 .. 4 + dk.public_key.len], dk.public_key);
return min_len;
},
.ds => |ds| {
const min_len = 4 + ds.digest.len;
if (buffer.len < min_len) return error.BufferTooSmall;
std.mem.writeInt(u16, buffer[0..2], ds.key_tag, .big);
buffer[2] = ds.algorithm;
buffer[3] = ds.digest_type;
@memcpy(buffer[4 .. 4 + ds.digest.len], ds.digest);
return min_len;
},
.rrsig => |rrsig| {
if (buffer.len < 18) return error.BufferTooSmall;
std.mem.writeInt(u16, buffer[0..2], rrsig.type_covered, .big);
buffer[2] = rrsig.algorithm;
buffer[3] = rrsig.labels;
std.mem.writeInt(u32, buffer[4..8], rrsig.original_ttl, .big);
std.mem.writeInt(u32, buffer[8..12], rrsig.signature_expiration, .big);
std.mem.writeInt(u32, buffer[12..16], rrsig.signature_inception, .big);
std.mem.writeInt(u16, buffer[16..18], rrsig.key_tag, .big);
const name_len = try rrsig.signer_name.encode(buffer[18..]);
const sig_start = 18 + name_len;
if (buffer.len < sig_start + rrsig.signature.len) return error.BufferTooSmall;
@memcpy(buffer[sig_start .. sig_start + rrsig.signature.len], rrsig.signature);
return sig_start + rrsig.signature.len;
},
.nsec => |nsec| {
const name_len = try nsec.next_domain.encode(buffer);
if (buffer.len < name_len + nsec.type_bitmap.len) return error.BufferTooSmall;
@memcpy(buffer[name_len .. name_len + nsec.type_bitmap.len], nsec.type_bitmap);
return name_len + nsec.type_bitmap.len;
},
.nsec3 => |nsec3| {
const min_len = 5 + nsec3.salt.len + 1 + nsec3.next_hashed_owner.len + nsec3.type_bitmap.len;
if (buffer.len < min_len) return error.BufferTooSmall;
buffer[0] = nsec3.hash_algorithm;
buffer[1] = nsec3.flags;
std.mem.writeInt(u16, buffer[2..4], nsec3.iterations, .big);
buffer[4] = @intCast(nsec3.salt.len);
var offset: usize = 5;
@memcpy(buffer[offset .. offset + nsec3.salt.len], nsec3.salt);
offset += nsec3.salt.len;
buffer[offset] = @intCast(nsec3.next_hashed_owner.len);
offset += 1;
@memcpy(buffer[offset .. offset + nsec3.next_hashed_owner.len], nsec3.next_hashed_owner);
offset += nsec3.next_hashed_owner.len;
@memcpy(buffer[offset .. offset + nsec3.type_bitmap.len], nsec3.type_bitmap);
return offset + nsec3.type_bitmap.len;
},
.nsec3param => |np| {
const min_len = 5 + np.salt.len;
if (buffer.len < min_len) return error.BufferTooSmall;
buffer[0] = np.hash_algorithm;
buffer[1] = np.flags;
std.mem.writeInt(u16, buffer[2..4], np.iterations, .big);
buffer[4] = @intCast(np.salt.len);
@memcpy(buffer[5 .. 5 + np.salt.len], np.salt);
return min_len;
},
.raw => |data| {
if (buffer.len < data.len) return error.BufferTooSmall;
@memcpy(buffer[0..data.len], data);
return data.len;
},
}
}
/// Free the record's memory
pub fn deinit(self: *ResourceRecord, allocator: Allocator) void {
self.name.deinit();
self.rdata.deinit(allocator);
}
/// Get the IPv4 address if this is an A record
pub fn getIPv4(self: ResourceRecord) ?[4]u8 {
return switch (self.rdata) {
.a => |addr| addr,
else => null,
};
}
/// Get the IPv6 address if this is an AAAA record
pub fn getIPv6(self: ResourceRecord) ?[16]u8 {
return switch (self.rdata) {
.aaaa => |addr| addr,
else => null,
};
}
/// Get the CNAME target if this is a CNAME record
pub fn getCname(self: ResourceRecord) ?Name {
return switch (self.rdata) {
.cname => |name| name,
else => null,
};
}
};
/// Create an A record
pub fn createARecord(name: Name, ttl: u32, ip: [4]u8) ResourceRecord {
return ResourceRecord{
.name = name,
.rtype = types.QType.A,
.class = types.QClass.IN,
.ttl = ttl,
.rdata = RData{ .a = ip },
};
}
/// Create an AAAA record
pub fn createAAAARecord(name: Name, ttl: u32, ip: [16]u8) ResourceRecord {
return ResourceRecord{
.name = name,
.rtype = types.QType.AAAA,
.class = types.QClass.IN,
.ttl = ttl,
.rdata = RData{ .aaaa = ip },
};
}
/// Create a CNAME record
pub fn createCnameRecord(name: Name, ttl: u32, target: Name) ResourceRecord {
return ResourceRecord{
.name = name,
.rtype = types.QType.CNAME,
.class = types.QClass.IN,
.ttl = ttl,
.rdata = RData{ .cname = target },
};
}
test "parse A record" {
const testing = std.testing;
const allocator = testing.allocator;
// example.com A 1.2.3.4 TTL=300
const buffer = [_]u8{
0x07, 'e', 'x', 'a', 'm', 'p', 'l', 'e', // example
0x03, 'c', 'o', 'm', // com
0x00, // null terminator
0x00, 0x01, // TYPE = A
0x00, 0x01, // CLASS = IN
0x00, 0x00, 0x01, 0x2C, // TTL = 300
0x00, 0x04, // RDLENGTH = 4
0x01, 0x02, 0x03, 0x04, // RDATA = 1.2.3.4
};
const result = try ResourceRecord.parse(&buffer, &buffer, allocator);
defer {
var r = result.record;
r.deinit(allocator);
}
try testing.expectEqual(types.QType.A, result.record.rtype);
try testing.expectEqual(types.QClass.IN, result.record.class);
try testing.expectEqual(@as(u32, 300), result.record.ttl);
const ip = result.record.getIPv4().?;
try testing.expectEqual([4]u8{ 1, 2, 3, 4 }, ip);
}
test "parse AAAA record" {
const testing = std.testing;
const allocator = testing.allocator;
// example.com AAAA 2001:db8::1 TTL=600
const buffer = [_]u8{
0x07, 'e', 'x', 'a', 'm', 'p', 'l', 'e', // example
0x03, 'c', 'o', 'm', // com
0x00, // null terminator
0x00, 0x1C, // TYPE = AAAA (28)
0x00, 0x01, // CLASS = IN
0x00, 0x00, 0x02, 0x58, // TTL = 600
0x00, 0x10, // RDLENGTH = 16
0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00, // 2001:0db8:0000:0000:
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, // 0000:0000:0000:0001
};
const result = try ResourceRecord.parse(&buffer, &buffer, allocator);
defer {
var r = result.record;
r.deinit(allocator);
}
try testing.expectEqual(types.QType.AAAA, result.record.rtype);
try testing.expectEqual(@as(u32, 600), result.record.ttl);
const ip = result.record.getIPv6().?;
try testing.expectEqual([16]u8{ 0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 }, ip);
}
test "parse CNAME record" {
const testing = std.testing;
const allocator = testing.allocator;
// www.example.com CNAME example.com TTL=3600
const buffer = [_]u8{
0x03, 'w', 'w', 'w', // www
0x07, 'e', 'x', 'a', 'm', 'p', 'l', 'e', // example
0x03, 'c', 'o', 'm', // com
0x00, // null terminator
0x00, 0x05, // TYPE = CNAME
0x00, 0x01, // CLASS = IN
0x00, 0x00, 0x0E, 0x10, // TTL = 3600
0x00, 0x0D, // RDLENGTH = 13
0x07, 'e', 'x', 'a', 'm', 'p', 'l', 'e', // example
0x03, 'c', 'o', 'm', // com
0x00, // null terminator
};
const result = try ResourceRecord.parse(&buffer, &buffer, allocator);
defer {
var r = result.record;
r.deinit(allocator);
}
try testing.expectEqual(types.QType.CNAME, result.record.rtype);
try testing.expectEqual(@as(u32, 3600), result.record.ttl);
const cname = result.record.getCname().?;
const cname_str = try cname.toString(allocator);
defer allocator.free(cname_str);
try testing.expectEqualStrings("example.com", cname_str);
}
test "parse TXT record" {
const testing = std.testing;
const allocator = testing.allocator;
// example.com TXT "v=spf1 -all" TTL=300
const buffer = [_]u8{
0x07, 'e', 'x', 'a', 'm', 'p', 'l', 'e', // example
0x03, 'c', 'o', 'm', // com
0x00, // null terminator
0x00, 0x10, // TYPE = TXT (16)
0x00, 0x01, // CLASS = IN
0x00, 0x00, 0x01, 0x2C, // TTL = 300
0x00, 0x0C, // RDLENGTH = 12
0x0B, // String length = 11
'v', '=', 's', 'p', 'f', '1', ' ', '-', 'a', 'l', 'l',
};
const result = try ResourceRecord.parse(&buffer, &buffer, allocator);
defer {
var r = result.record;
r.deinit(allocator);
}
try testing.expectEqual(types.QType.TXT, result.record.rtype);
switch (result.record.rdata) {
.txt => |strings| {
try testing.expectEqual(@as(usize, 1), strings.len);
try testing.expectEqualStrings("v=spf1 -all", strings[0]);
},
else => try testing.expect(false),
}
}
test "create and encode A record" {
const testing = std.testing;
const allocator = testing.allocator;
var name = try Name.fromString("test.example.com", allocator);
defer name.deinit();
const record = createARecord(name, 300, [4]u8{ 192, 168, 1, 1 });
var buffer: [256]u8 = undefined;
const encoded_len = try record.encode(&buffer, allocator);
// Parse it back
const result = try ResourceRecord.parse(buffer[0..encoded_len], buffer[0..encoded_len], allocator);
defer {
var r = result.record;
r.deinit(allocator);
}
try testing.expectEqual(types.QType.A, result.record.rtype);
try testing.expectEqual(@as(u32, 300), result.record.ttl);
try testing.expectEqual([4]u8{ 192, 168, 1, 1 }, result.record.getIPv4().?);
}
test "parse DNSKEY record" {
const testing = std.testing;
const allocator = testing.allocator;
// example.com DNSKEY 256 3 8 <public_key> TTL=3600
const buffer = [_]u8{
0x07, 'e', 'x', 'a', 'm', 'p', 'l', 'e', // example
0x03, 'c', 'o', 'm', // com
0x00, // null terminator
0x00, 0x30, // TYPE = DNSKEY (48)
0x00, 0x01, // CLASS = IN
0x00, 0x00, 0x0E, 0x10, // TTL = 3600
0x00, 0x08, // RDLENGTH = 8
0x01, 0x00, // flags = 256 (Zone Key)
0x03, // protocol = 3
0x08, // algorithm = 8 (RSASHA256)
0xDE, 0xAD, 0xBE, 0xEF, // public_key (dummy 4 bytes)
};
const result = try ResourceRecord.parse(&buffer, &buffer, allocator);
defer {
var r = result.record;
r.deinit(allocator);
}
try testing.expectEqual(types.QType.DNSKEY, result.record.rtype);
try testing.expectEqual(@as(u32, 3600), result.record.ttl);
switch (result.record.rdata) {
.dnskey => |dk| {
try testing.expectEqual(@as(u16, 256), dk.flags);
try testing.expectEqual(@as(u8, 3), dk.protocol);
try testing.expectEqual(@as(u8, 8), dk.algorithm);
try testing.expectEqual(@as(usize, 4), dk.public_key.len);
},
else => try testing.expect(false),
}
}
test "parse DS record" {
const testing = std.testing;
const allocator = testing.allocator;
// example.com DS 12345 8 2 <digest> TTL=86400
const buffer = [_]u8{
0x07, 'e', 'x', 'a', 'm', 'p', 'l', 'e', // example
0x03, 'c', 'o', 'm', // com
0x00, // null terminator
0x00, 0x2B, // TYPE = DS (43)
0x00, 0x01, // CLASS = IN
0x00, 0x01, 0x51, 0x80, // TTL = 86400
0x00, 0x08, // RDLENGTH = 8
0x30, 0x39, // key_tag = 12345
0x08, // algorithm = 8
0x02, // digest_type = 2 (SHA-256)
0xAB, 0xCD, 0xEF, 0x01, // digest (dummy 4 bytes)
};
const result = try ResourceRecord.parse(&buffer, &buffer, allocator);
defer {
var r = result.record;
r.deinit(allocator);
}
try testing.expectEqual(types.QType.DS, result.record.rtype);
switch (result.record.rdata) {
.ds => |ds| {
try testing.expectEqual(@as(u16, 12345), ds.key_tag);
try testing.expectEqual(@as(u8, 8), ds.algorithm);
try testing.expectEqual(@as(u8, 2), ds.digest_type);
try testing.expectEqual(@as(usize, 4), ds.digest.len);
},
else => try testing.expect(false),
}
}
test "parse NSEC3PARAM record" {
const testing = std.testing;
const allocator = testing.allocator;
// example.com NSEC3PARAM 1 0 10 <salt> TTL=0
const buffer = [_]u8{
0x07, 'e', 'x', 'a', 'm', 'p', 'l', 'e', // example
0x03, 'c', 'o', 'm', // com
0x00, // null terminator
0x00, 0x33, // TYPE = NSEC3PARAM (51)
0x00, 0x01, // CLASS = IN
0x00, 0x00, 0x00, 0x00, // TTL = 0
0x00, 0x07, // RDLENGTH = 7
0x01, // hash_algorithm = 1 (SHA-1)
0x00, // flags = 0
0x00, 0x0A, // iterations = 10
0x02, // salt_length = 2
0xAB, 0xCD, // salt
};
const result = try ResourceRecord.parse(&buffer, &buffer, allocator);
defer {
var r = result.record;
r.deinit(allocator);
}
try testing.expectEqual(types.QType.NSEC3PARAM, result.record.rtype);
switch (result.record.rdata) {
.nsec3param => |np| {
try testing.expectEqual(@as(u8, 1), np.hash_algorithm);
try testing.expectEqual(@as(u8, 0), np.flags);
try testing.expectEqual(@as(u16, 10), np.iterations);
try testing.expectEqual(@as(usize, 2), np.salt.len);
},
else => try testing.expect(false),
}
}
+223
View File
@@ -0,0 +1,223 @@
const std = @import("std");
/// DNS Query/Response types (RFC 1035 + extensions)
pub const QType = enum(u16) {
A = 1,
NS = 2,
MD = 3, // obsolete
MF = 4, // obsolete
CNAME = 5,
SOA = 6,
MB = 7,
MG = 8,
MR = 9,
NULL = 10,
WKS = 11,
PTR = 12,
HINFO = 13,
MINFO = 14,
MX = 15,
TXT = 16,
RP = 17,
AFSDB = 18,
X25 = 19,
ISDN = 20,
RT = 21,
NSAP = 22,
NSAP_PTR = 23,
SIG = 24,
KEY = 25,
PX = 26,
GPOS = 27,
AAAA = 28,
LOC = 29,
NXT = 30,
EID = 31,
NIMLOC = 32,
SRV = 33,
ATMA = 34,
NAPTR = 35,
KX = 36,
CERT = 37,
A6 = 38,
DNAME = 39,
SINK = 40,
OPT = 41, // EDNS
APL = 42,
DS = 43, // DNSSEC
SSHFP = 44,
IPSECKEY = 45,
RRSIG = 46, // DNSSEC
NSEC = 47, // DNSSEC
DNSKEY = 48, // DNSSEC
DHCID = 49,
NSEC3 = 50,
NSEC3PARAM = 51,
TLSA = 52,
SMIMEA = 53,
HIP = 55,
NINFO = 56,
RKEY = 57,
TALINK = 58,
CDS = 59,
CDNSKEY = 60,
OPENPGPKEY = 61,
CSYNC = 62,
ZONEMD = 63,
SVCB = 64,
HTTPS = 65,
SPF = 99,
UINFO = 100,
UID = 101,
GID = 102,
UNSPEC = 103,
NID = 104,
L32 = 105,
L64 = 106,
LP = 107,
EUI48 = 108,
EUI64 = 109,
TKEY = 249,
TSIG = 250,
IXFR = 251,
AXFR = 252,
MAILB = 253,
MAILA = 254,
ANY = 255,
URI = 256,
CAA = 257,
AVC = 258,
DOA = 259,
AMTRELAY = 260,
TA = 32768,
DLV = 32769,
_,
pub fn toString(self: QType) []const u8 {
return switch (self) {
.A => "A",
.NS => "NS",
.CNAME => "CNAME",
.SOA => "SOA",
.PTR => "PTR",
.MX => "MX",
.TXT => "TXT",
.AAAA => "AAAA",
.SRV => "SRV",
.OPT => "OPT",
// DNSSEC types
.DS => "DS",
.RRSIG => "RRSIG",
.NSEC => "NSEC",
.DNSKEY => "DNSKEY",
.NSEC3 => "NSEC3",
.NSEC3PARAM => "NSEC3PARAM",
.CDS => "CDS",
.CDNSKEY => "CDNSKEY",
.ANY => "ANY",
.HTTPS => "HTTPS",
.SVCB => "SVCB",
else => "UNKNOWN",
};
}
};
/// DNS Query Class
pub const QClass = enum(u16) {
IN = 1, // Internet
CS = 2, // CSNET (obsolete)
CH = 3, // Chaos
HS = 4, // Hesiod
NONE = 254,
ANY = 255,
_,
pub fn toString(self: QClass) []const u8 {
return switch (self) {
.IN => "IN",
.CH => "CH",
.HS => "HS",
.ANY => "ANY",
else => "UNKNOWN",
};
}
};
/// DNS Response Code
pub const RCode = enum(u4) {
NoError = 0, // No error
FormErr = 1, // Format error
ServFail = 2, // Server failure
NXDomain = 3, // Non-existent domain
NotImp = 4, // Not implemented
Refused = 5, // Query refused
YXDomain = 6, // Name exists when it should not
YXRRSet = 7, // RR set exists when it should not
NXRRSet = 8, // RR set does not exist
NotAuth = 9, // Not authorized
NotZone = 10, // Name not in zone
_,
pub fn toString(self: RCode) []const u8 {
return switch (self) {
.NoError => "NOERROR",
.FormErr => "FORMERR",
.ServFail => "SERVFAIL",
.NXDomain => "NXDOMAIN",
.NotImp => "NOTIMP",
.Refused => "REFUSED",
else => "UNKNOWN",
};
}
};
/// DNS Operation Code
pub const OpCode = enum(u4) {
Query = 0, // Standard query
IQuery = 1, // Inverse query (obsolete)
Status = 2, // Server status request
Notify = 4, // Zone change notification
Update = 5, // Dynamic update
_,
};
/// Maximum DNS name length (RFC 1035: 255 octets wire format, 253 chars in dot notation)
pub const MAX_NAME_LENGTH: usize = 253;
/// Maximum DNS label length (RFC 1035: 63 octets, 6-bit length field)
pub const MAX_LABEL_LENGTH: usize = 63;
/// Standard DNS UDP message size (RFC 1035 Section 4.2.1)
pub const DNS_UDP_SIZE: usize = 512;
/// EDNS0 default UDP payload size (RFC 6891, commonly 4096 for modern resolvers)
pub const EDNS_DEFAULT_SIZE: usize = 4096;
/// DNS header size in bytes (RFC 1035 Section 4.1.1: ID + flags + 4 counts)
pub const DNS_HEADER_SIZE: usize = 12;
/// Compression pointer mask - top 2 bits set indicates pointer (RFC 1035 Section 4.1.4)
pub const COMPRESSION_POINTER_MASK: u8 = 0xC0;
/// Maximum compression pointer offset (14-bit value, RFC 1035 Section 4.1.4)
pub const MAX_COMPRESSION_OFFSET: u16 = 0x3FFF;
test "QType values" {
const testing = std.testing;
try testing.expectEqual(@as(u16, 1), @intFromEnum(QType.A));
try testing.expectEqual(@as(u16, 28), @intFromEnum(QType.AAAA));
try testing.expectEqual(@as(u16, 5), @intFromEnum(QType.CNAME));
try testing.expectEqual(@as(u16, 41), @intFromEnum(QType.OPT));
}
test "QClass values" {
const testing = std.testing;
try testing.expectEqual(@as(u16, 1), @intFromEnum(QClass.IN));
try testing.expectEqual(@as(u16, 255), @intFromEnum(QClass.ANY));
}
test "RCode values" {
const testing = std.testing;
try testing.expectEqual(@as(u4, 0), @intFromEnum(RCode.NoError));
try testing.expectEqual(@as(u4, 3), @intFromEnum(RCode.NXDomain));
}