initial commit
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
Reference in New Issue
Block a user