mirror of
https://codeberg.org/ziglang/zig.git
synced 2025-12-06 05:44:20 +00:00
770 lines
29 KiB
Zig
770 lines
29 KiB
Zig
//! An already-validated host name. A valid host name:
|
|
//! * Has length less than or equal to `max_len`.
|
|
//! * Is valid UTF-8.
|
|
//! * Lacks ASCII characters other than alphanumeric, '-', and '.'.
|
|
const HostName = @This();
|
|
|
|
const builtin = @import("builtin");
|
|
const native_os = builtin.os.tag;
|
|
|
|
const std = @import("../../std.zig");
|
|
const Io = std.Io;
|
|
const IpAddress = Io.net.IpAddress;
|
|
const Ip6Address = Io.net.Ip6Address;
|
|
const assert = std.debug.assert;
|
|
const Stream = Io.net.Stream;
|
|
|
|
/// Externally managed memory. Already checked to be valid.
|
|
bytes: []const u8,
|
|
|
|
pub const max_len = 255;
|
|
|
|
pub const InitError = error{
|
|
NameTooLong,
|
|
InvalidHostName,
|
|
};
|
|
|
|
pub fn init(bytes: []const u8) InitError!HostName {
|
|
if (bytes.len > max_len) return error.NameTooLong;
|
|
if (!std.unicode.utf8ValidateSlice(bytes)) return error.InvalidHostName;
|
|
for (bytes) |byte| {
|
|
if (!std.ascii.isAscii(byte) or byte == '.' or byte == '-' or std.ascii.isAlphanumeric(byte)) {
|
|
continue;
|
|
}
|
|
return error.InvalidHostName;
|
|
}
|
|
return .{ .bytes = bytes };
|
|
}
|
|
|
|
/// TODO add a retry field here
|
|
pub const LookupOptions = struct {
|
|
port: u16,
|
|
/// Must have at least length 2.
|
|
addresses_buffer: []IpAddress,
|
|
canonical_name_buffer: *[max_len]u8,
|
|
/// `null` means either.
|
|
family: ?IpAddress.Family = null,
|
|
};
|
|
|
|
pub const LookupError = error{
|
|
UnknownHostName,
|
|
ResolvConfParseFailed,
|
|
InvalidDnsARecord,
|
|
InvalidDnsAAAARecord,
|
|
InvalidDnsCnameRecord,
|
|
NameServerFailure,
|
|
} || Io.Timestamp.Error || IpAddress.BindError || Io.File.OpenError || Io.File.Reader.Error || Io.Cancelable;
|
|
|
|
pub const LookupResult = struct {
|
|
/// How many `LookupOptions.addresses_buffer` elements are populated.
|
|
addresses_len: usize,
|
|
canonical_name: HostName,
|
|
|
|
pub const empty: LookupResult = .{
|
|
.addresses_len = 0,
|
|
.canonical_name = undefined,
|
|
};
|
|
};
|
|
|
|
pub fn lookup(host_name: HostName, io: Io, options: LookupOptions) LookupError!LookupResult {
|
|
const name = host_name.bytes;
|
|
assert(name.len <= max_len);
|
|
assert(options.addresses_buffer.len >= 2);
|
|
|
|
if (native_os == .windows) @compileError("TODO");
|
|
if (builtin.link_libc) @compileError("TODO");
|
|
if (native_os == .linux) {
|
|
if (options.family != .ip6) {
|
|
if (IpAddress.parseIp4(name, options.port)) |addr| {
|
|
options.addresses_buffer[0] = addr;
|
|
return .{ .addresses_len = 1, .canonical_name = copyCanon(options.canonical_name_buffer, name) };
|
|
} else |_| {}
|
|
}
|
|
if (options.family != .ip4) {
|
|
if (IpAddress.parseIp6(name, options.port)) |addr| {
|
|
options.addresses_buffer[0] = addr;
|
|
return .{ .addresses_len = 1, .canonical_name = copyCanon(options.canonical_name_buffer, name) };
|
|
} else |_| {}
|
|
}
|
|
{
|
|
const result = try lookupHosts(host_name, io, options);
|
|
if (result.addresses_len > 0) return sortLookupResults(options, result);
|
|
}
|
|
{
|
|
// RFC 6761 Section 6.3.3
|
|
// Name resolution APIs and libraries SHOULD recognize
|
|
// localhost names as special and SHOULD always return the IP
|
|
// loopback address for address queries and negative responses
|
|
// for all other query types.
|
|
|
|
// Check for equal to "localhost(.)" or ends in ".localhost(.)"
|
|
const localhost = if (name[name.len - 1] == '.') "localhost." else "localhost";
|
|
if (std.mem.endsWith(u8, name, localhost) and
|
|
(name.len == localhost.len or name[name.len - localhost.len] == '.'))
|
|
{
|
|
var i: usize = 0;
|
|
if (options.family != .ip6) {
|
|
options.addresses_buffer[i] = .{ .ip4 = .loopback(options.port) };
|
|
i += 1;
|
|
}
|
|
if (options.family != .ip4) {
|
|
options.addresses_buffer[i] = .{ .ip6 = .loopback(options.port) };
|
|
i += 1;
|
|
}
|
|
const canon_name = "localhost";
|
|
const canon_name_dest = options.canonical_name_buffer[0..canon_name.len];
|
|
canon_name_dest.* = canon_name.*;
|
|
return sortLookupResults(options, .{
|
|
.addresses_len = i,
|
|
.canonical_name = .{ .bytes = canon_name_dest },
|
|
});
|
|
}
|
|
}
|
|
{
|
|
const result = try lookupDnsSearch(host_name, io, options);
|
|
if (result.addresses_len > 0) return sortLookupResults(options, result);
|
|
}
|
|
return error.UnknownHostName;
|
|
}
|
|
@compileError("unimplemented");
|
|
}
|
|
|
|
fn sortLookupResults(options: LookupOptions, result: LookupResult) !LookupResult {
|
|
const addresses = options.addresses_buffer[0..result.addresses_len];
|
|
// No further processing is needed if there are fewer than 2 results or
|
|
// if there are only IPv4 results.
|
|
if (addresses.len < 2) return result;
|
|
const all_ip4 = for (addresses) |a| switch (a) {
|
|
.ip4 => continue,
|
|
.ip6 => break false,
|
|
} else true;
|
|
if (all_ip4) return result;
|
|
|
|
// RFC 3484/6724 describes how destination address selection is
|
|
// supposed to work. However, to implement it requires making a bunch
|
|
// of networking syscalls, which is unnecessarily high latency,
|
|
// especially if implemented serially. Furthermore, rules 3, 4, and 7
|
|
// have excessive runtime and code size cost and dubious benefit.
|
|
//
|
|
// Therefore, this logic sorts only using values available without
|
|
// doing any syscalls, relying on the calling code to have a
|
|
// meta-strategy such as attempting connection to multiple results at
|
|
// once and keeping the fastest response while canceling the others.
|
|
|
|
const S = struct {
|
|
pub fn lessThan(s: @This(), lhs: IpAddress, rhs: IpAddress) bool {
|
|
return sortKey(s, lhs) < sortKey(s, rhs);
|
|
}
|
|
|
|
fn sortKey(s: @This(), a: IpAddress) i32 {
|
|
_ = s;
|
|
var da6: Ip6Address = .{
|
|
.port = 65535,
|
|
.bytes = undefined,
|
|
};
|
|
switch (a) {
|
|
.ip6 => |ip6| {
|
|
da6.bytes = ip6.bytes;
|
|
da6.interface = ip6.interface;
|
|
},
|
|
.ip4 => |ip4| {
|
|
da6.bytes[0..12].* = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff".*;
|
|
da6.bytes[12..].* = ip4.bytes;
|
|
},
|
|
}
|
|
const da6_scope: i32 = da6.scope();
|
|
const da6_prec: i32 = da6.policy().prec;
|
|
var key: i32 = 0;
|
|
key |= da6_prec << 20;
|
|
key |= (15 - da6_scope) << 16;
|
|
return key;
|
|
}
|
|
};
|
|
std.mem.sort(IpAddress, addresses, @as(S, .{}), S.lessThan);
|
|
return result;
|
|
}
|
|
|
|
fn lookupDnsSearch(host_name: HostName, io: Io, options: LookupOptions) LookupError!LookupResult {
|
|
const rc = ResolvConf.init(io) catch return error.ResolvConfParseFailed;
|
|
|
|
// Count dots, suppress search when >=ndots or name ends in
|
|
// a dot, which is an explicit request for global scope.
|
|
const dots = std.mem.countScalar(u8, host_name.bytes, '.');
|
|
const search_len = if (dots >= rc.ndots or std.mem.endsWith(u8, host_name.bytes, ".")) 0 else rc.search_len;
|
|
const search = rc.search_buffer[0..search_len];
|
|
|
|
var canon_name = host_name.bytes;
|
|
|
|
// Strip final dot for canon, fail if multiple trailing dots.
|
|
if (std.mem.endsWith(u8, canon_name, ".")) canon_name.len -= 1;
|
|
if (std.mem.endsWith(u8, canon_name, ".")) return error.UnknownHostName;
|
|
|
|
// Name with search domain appended is set up in `canon_name`. This
|
|
// both provides the desired default canonical name (if the requested
|
|
// name is not a CNAME record) and serves as a buffer for passing the
|
|
// full requested name to `lookupDns`.
|
|
@memcpy(options.canonical_name_buffer[0..canon_name.len], canon_name);
|
|
options.canonical_name_buffer[canon_name.len] = '.';
|
|
var it = std.mem.tokenizeAny(u8, search, " \t");
|
|
while (it.next()) |token| {
|
|
@memcpy(options.canonical_name_buffer[canon_name.len + 1 ..][0..token.len], token);
|
|
const lookup_canon_name = options.canonical_name_buffer[0 .. canon_name.len + 1 + token.len];
|
|
const result = try lookupDns(io, lookup_canon_name, &rc, options);
|
|
if (result.addresses_len > 0) return sortLookupResults(options, result);
|
|
}
|
|
|
|
const lookup_canon_name = options.canonical_name_buffer[0..canon_name.len];
|
|
return lookupDns(io, lookup_canon_name, &rc, options);
|
|
}
|
|
|
|
fn lookupDns(io: Io, lookup_canon_name: []const u8, rc: *const ResolvConf, options: LookupOptions) LookupError!LookupResult {
|
|
const family_records: [2]struct { af: IpAddress.Family, rr: u8 } = .{
|
|
.{ .af = .ip6, .rr = std.posix.RR.A },
|
|
.{ .af = .ip4, .rr = std.posix.RR.AAAA },
|
|
};
|
|
var query_buffers: [2][280]u8 = undefined;
|
|
var answer_buffer: [2 * 512]u8 = undefined;
|
|
var queries_buffer: [2][]const u8 = undefined;
|
|
var answers_buffer: [2][]const u8 = undefined;
|
|
var nq: usize = 0;
|
|
var answer_buffer_i: usize = 0;
|
|
|
|
for (family_records) |fr| {
|
|
if (options.family != fr.af) {
|
|
const entropy = std.crypto.random.array(u8, 2);
|
|
const len = writeResolutionQuery(&query_buffers[nq], 0, lookup_canon_name, 1, fr.rr, entropy);
|
|
queries_buffer[nq] = query_buffers[nq][0..len];
|
|
nq += 1;
|
|
}
|
|
}
|
|
|
|
var ip4_mapped: [ResolvConf.max_nameservers]IpAddress = undefined;
|
|
var any_ip6 = false;
|
|
for (rc.nameservers(), &ip4_mapped) |*ns, *m| {
|
|
m.* = .{ .ip6 = .fromAny(ns.*) };
|
|
any_ip6 = any_ip6 or ns.* == .ip6;
|
|
}
|
|
var socket = s: {
|
|
if (any_ip6) ip6: {
|
|
const ip6_addr: IpAddress = .{ .ip6 = .unspecified(0) };
|
|
const socket = ip6_addr.bind(io, .{ .ip6_only = true, .mode = .dgram }) catch |err| switch (err) {
|
|
error.AddressFamilyUnsupported => break :ip6,
|
|
else => |e| return e,
|
|
};
|
|
break :s socket;
|
|
}
|
|
any_ip6 = false;
|
|
const ip4_addr: IpAddress = .{ .ip4 = .unspecified(0) };
|
|
const socket = try ip4_addr.bind(io, .{ .mode = .dgram });
|
|
break :s socket;
|
|
};
|
|
defer socket.close(io);
|
|
|
|
const mapped_nameservers = if (any_ip6) ip4_mapped[0..rc.nameservers_len] else rc.nameservers();
|
|
const queries = queries_buffer[0..nq];
|
|
const answers = answers_buffer[0..queries.len];
|
|
var answers_remaining = answers.len;
|
|
for (answers) |*answer| answer.len = 0;
|
|
|
|
// boottime is chosen because time the computer is suspended should count
|
|
// against time spent waiting for external messages to arrive.
|
|
var now_ts = try Io.Timestamp.now(io, .boottime);
|
|
const final_ts = now_ts.addDuration(.fromSeconds(rc.timeout_seconds));
|
|
const attempt_duration: Io.Duration = .{
|
|
.nanoseconds = std.time.ns_per_s * @as(usize, rc.timeout_seconds) / rc.attempts,
|
|
};
|
|
|
|
send: while (now_ts.compare(.lt, final_ts)) : (now_ts = try Io.Timestamp.now(io, .boottime)) {
|
|
const max_messages = queries_buffer.len * ResolvConf.max_nameservers;
|
|
{
|
|
var message_buffer: [max_messages]Io.net.OutgoingMessage = undefined;
|
|
var message_i: usize = 0;
|
|
for (queries, answers) |query, *answer| {
|
|
if (answer.len != 0) continue;
|
|
for (mapped_nameservers) |*ns| {
|
|
message_buffer[message_i] = .{
|
|
.address = ns,
|
|
.data_ptr = query.ptr,
|
|
.data_len = query.len,
|
|
};
|
|
message_i += 1;
|
|
}
|
|
}
|
|
_ = io.vtable.netSend(io.userdata, socket.handle, message_buffer[0..message_i], .{});
|
|
}
|
|
|
|
const timeout: Io.Timeout = .{ .deadline = now_ts.addDuration(attempt_duration) };
|
|
|
|
while (true) {
|
|
var message_buffer: [max_messages]Io.net.IncomingMessage = undefined;
|
|
const buf = answer_buffer[answer_buffer_i..];
|
|
const recv_err, const recv_n = socket.receiveManyTimeout(io, &message_buffer, buf, .{}, timeout);
|
|
for (message_buffer[0..recv_n]) |*received_message| {
|
|
const reply = received_message.data;
|
|
// Ignore non-identifiable packets.
|
|
if (reply.len < 4) continue;
|
|
|
|
// Ignore replies from addresses we didn't send to.
|
|
const ns = for (mapped_nameservers) |*ns| {
|
|
if (received_message.from.eql(ns)) break ns;
|
|
} else {
|
|
continue;
|
|
};
|
|
|
|
// Find which query this answer goes with, if any.
|
|
const query, const answer = for (queries, answers) |query, *answer| {
|
|
if (reply[0] == query[0] and reply[1] == query[1]) break .{ query, answer };
|
|
} else {
|
|
continue;
|
|
};
|
|
if (answer.len != 0) continue;
|
|
|
|
// Only accept positive or negative responses; retry immediately on
|
|
// server failure, and ignore all other codes such as refusal.
|
|
switch (reply[3] & 15) {
|
|
0, 3 => {
|
|
answer.* = reply;
|
|
answer_buffer_i += reply.len;
|
|
answers_remaining -= 1;
|
|
if (answer_buffer.len - answer_buffer_i == 0) break :send;
|
|
if (answers_remaining == 0) break :send;
|
|
},
|
|
2 => {
|
|
var retry_message: Io.net.OutgoingMessage = .{
|
|
.address = ns,
|
|
.data_ptr = query.ptr,
|
|
.data_len = query.len,
|
|
};
|
|
_ = io.vtable.netSend(io.userdata, socket.handle, (&retry_message)[0..1], .{});
|
|
continue;
|
|
},
|
|
else => continue,
|
|
}
|
|
}
|
|
if (recv_err) |err| switch (err) {
|
|
error.Canceled => return error.Canceled,
|
|
error.Timeout => continue :send,
|
|
else => continue,
|
|
};
|
|
}
|
|
} else {
|
|
return error.NameServerFailure;
|
|
}
|
|
|
|
var addresses_len: usize = 0;
|
|
var canonical_name: ?HostName = null;
|
|
|
|
for (answers) |answer| {
|
|
var it = DnsResponse.init(answer) catch {
|
|
// TODO accept a diagnostics struct and append warnings
|
|
continue;
|
|
};
|
|
while (it.next() catch {
|
|
// TODO accept a diagnostics struct and append warnings
|
|
continue;
|
|
}) |record| switch (record.rr) {
|
|
std.posix.RR.A => {
|
|
const data = record.packet[record.data_off..][0..record.data_len];
|
|
if (data.len != 4) return error.InvalidDnsARecord;
|
|
options.addresses_buffer[addresses_len] = .{ .ip4 = .{
|
|
.bytes = data[0..4].*,
|
|
.port = options.port,
|
|
} };
|
|
addresses_len += 1;
|
|
},
|
|
std.posix.RR.AAAA => {
|
|
const data = record.packet[record.data_off..][0..record.data_len];
|
|
if (data.len != 16) return error.InvalidDnsAAAARecord;
|
|
options.addresses_buffer[addresses_len] = .{ .ip6 = .{
|
|
.bytes = data[0..16].*,
|
|
.port = options.port,
|
|
} };
|
|
addresses_len += 1;
|
|
},
|
|
std.posix.RR.CNAME => {
|
|
_, canonical_name = expand(record.packet, record.data_off, options.canonical_name_buffer) catch
|
|
return error.InvalidDnsCnameRecord;
|
|
},
|
|
else => continue,
|
|
};
|
|
}
|
|
|
|
if (addresses_len != 0) return .{
|
|
.addresses_len = addresses_len,
|
|
.canonical_name = canonical_name orelse .{ .bytes = lookup_canon_name },
|
|
};
|
|
|
|
return error.NameServerFailure;
|
|
}
|
|
|
|
fn lookupHosts(host_name: HostName, io: Io, options: LookupOptions) !LookupResult {
|
|
const file = Io.File.openAbsolute(io, "/etc/hosts", .{}) catch |err| switch (err) {
|
|
error.FileNotFound,
|
|
error.NotDir,
|
|
error.AccessDenied,
|
|
=> return .empty,
|
|
|
|
else => |e| return e,
|
|
};
|
|
defer file.close(io);
|
|
|
|
var line_buf: [512]u8 = undefined;
|
|
var file_reader = file.reader(io, &line_buf);
|
|
return lookupHostsReader(host_name, options, &file_reader.interface) catch |err| switch (err) {
|
|
error.ReadFailed => return file_reader.err.?,
|
|
};
|
|
}
|
|
|
|
fn lookupHostsReader(host_name: HostName, options: LookupOptions, reader: *Io.Reader) error{ReadFailed}!LookupResult {
|
|
var addresses_len: usize = 0;
|
|
var canonical_name: ?HostName = null;
|
|
while (true) {
|
|
const line = reader.takeDelimiterExclusive('\n') catch |err| switch (err) {
|
|
error.StreamTooLong => {
|
|
// Skip lines that are too long.
|
|
_ = reader.discardDelimiterInclusive('\n') catch |e| switch (e) {
|
|
error.EndOfStream => break,
|
|
error.ReadFailed => return error.ReadFailed,
|
|
};
|
|
continue;
|
|
},
|
|
error.ReadFailed => return error.ReadFailed,
|
|
error.EndOfStream => break,
|
|
};
|
|
var split_it = std.mem.splitScalar(u8, line, '#');
|
|
const no_comment_line = split_it.first();
|
|
|
|
var line_it = std.mem.tokenizeAny(u8, no_comment_line, " \t");
|
|
const ip_text = line_it.next() orelse continue;
|
|
var first_name_text: ?[]const u8 = null;
|
|
while (line_it.next()) |name_text| {
|
|
if (std.mem.eql(u8, name_text, host_name.bytes)) {
|
|
if (first_name_text == null) first_name_text = name_text;
|
|
break;
|
|
}
|
|
} else continue;
|
|
|
|
if (canonical_name == null) {
|
|
if (HostName.init(first_name_text.?)) |name_text| {
|
|
if (name_text.bytes.len <= options.canonical_name_buffer.len) {
|
|
const canonical_name_dest = options.canonical_name_buffer[0..name_text.bytes.len];
|
|
@memcpy(canonical_name_dest, name_text.bytes);
|
|
canonical_name = .{ .bytes = canonical_name_dest };
|
|
}
|
|
} else |_| {}
|
|
}
|
|
|
|
if (options.family != .ip6) {
|
|
if (IpAddress.parseIp4(ip_text, options.port)) |addr| {
|
|
options.addresses_buffer[addresses_len] = addr;
|
|
addresses_len += 1;
|
|
if (options.addresses_buffer.len - addresses_len == 0) return .{
|
|
.addresses_len = addresses_len,
|
|
.canonical_name = canonical_name orelse copyCanon(options.canonical_name_buffer, ip_text),
|
|
};
|
|
} else |_| {}
|
|
}
|
|
if (options.family != .ip4) {
|
|
if (IpAddress.parseIp6(ip_text, options.port)) |addr| {
|
|
options.addresses_buffer[addresses_len] = addr;
|
|
addresses_len += 1;
|
|
if (options.addresses_buffer.len - addresses_len == 0) return .{
|
|
.addresses_len = addresses_len,
|
|
.canonical_name = canonical_name orelse copyCanon(options.canonical_name_buffer, ip_text),
|
|
};
|
|
} else |_| {}
|
|
}
|
|
}
|
|
if (canonical_name == null) assert(addresses_len == 0);
|
|
return .{
|
|
.addresses_len = addresses_len,
|
|
.canonical_name = canonical_name orelse undefined,
|
|
};
|
|
}
|
|
|
|
fn copyCanon(canonical_name_buffer: *[max_len]u8, name: []const u8) HostName {
|
|
const dest = canonical_name_buffer[0..name.len];
|
|
@memcpy(dest, name);
|
|
return .{ .bytes = dest };
|
|
}
|
|
|
|
/// Writes DNS resolution query packet data to `w`; at most 280 bytes.
|
|
fn writeResolutionQuery(q: *[280]u8, op: u4, dname: []const u8, class: u8, ty: u8, entropy: [2]u8) usize {
|
|
// This implementation is ported from musl libc.
|
|
// A more idiomatic "ziggy" implementation would be welcome.
|
|
var name = dname;
|
|
if (std.mem.endsWith(u8, name, ".")) name.len -= 1;
|
|
assert(name.len <= 253);
|
|
const n = 17 + name.len + @intFromBool(name.len != 0);
|
|
|
|
// Construct query template - ID will be filled later
|
|
q[0..2].* = entropy;
|
|
@memset(q[2..n], 0);
|
|
q[2] = @as(u8, op) * 8 + 1;
|
|
q[5] = 1;
|
|
@memcpy(q[13..][0..name.len], name);
|
|
var i: usize = 13;
|
|
var j: usize = undefined;
|
|
while (q[i] != 0) : (i = j + 1) {
|
|
j = i;
|
|
while (q[j] != 0 and q[j] != '.') : (j += 1) {}
|
|
// TODO determine the circumstances for this and whether or
|
|
// not this should be an error.
|
|
if (j - i - 1 > 62) unreachable;
|
|
q[i - 1] = @intCast(j - i);
|
|
}
|
|
q[i + 1] = ty;
|
|
q[i + 3] = class;
|
|
return n;
|
|
}
|
|
|
|
pub const ExpandError = error{InvalidDnsPacket} || InitError;
|
|
|
|
/// Decompresses a DNS name.
|
|
///
|
|
/// Returns number of bytes consumed from `packet` starting at `i`,
|
|
/// along with the expanded `HostName`.
|
|
///
|
|
/// Asserts `buffer` is has length at least `max_len`.
|
|
pub fn expand(noalias packet: []const u8, start_i: usize, noalias dest_buffer: []u8) ExpandError!struct { usize, HostName } {
|
|
const dest = dest_buffer[0..max_len];
|
|
|
|
var i = start_i;
|
|
var dest_i: usize = 0;
|
|
var len: ?usize = null;
|
|
|
|
// Detect reference loop using an iteration counter.
|
|
for (0..packet.len / 2) |_| {
|
|
if (i >= packet.len) return error.InvalidDnsPacket;
|
|
|
|
const c = packet[i];
|
|
if ((c & 0xc0) != 0) {
|
|
if (i + 1 >= packet.len) return error.InvalidDnsPacket;
|
|
const j: usize = (@as(usize, c & 0x3F) << 8) | packet[i + 1];
|
|
if (j >= packet.len) return error.InvalidDnsPacket;
|
|
if (len == null) len = (i + 2) - start_i;
|
|
i = j;
|
|
} else if (c != 0) {
|
|
if (dest_i != 0) {
|
|
dest[dest_i] = '.';
|
|
dest_i += 1;
|
|
}
|
|
const label_len: usize = c;
|
|
if (i + 1 + label_len > packet.len) return error.InvalidDnsPacket;
|
|
if (dest_i + label_len + 1 > dest.len) return error.InvalidDnsPacket;
|
|
@memcpy(dest[dest_i..][0..label_len], packet[i + 1 ..][0..label_len]);
|
|
dest_i += label_len;
|
|
i += 1 + label_len;
|
|
} else {
|
|
dest[dest_i] = 0;
|
|
dest_i += 1;
|
|
return .{
|
|
len orelse i - start_i + 1,
|
|
try .init(dest[0..dest_i]),
|
|
};
|
|
}
|
|
}
|
|
return error.InvalidDnsPacket;
|
|
}
|
|
|
|
pub const DnsResponse = struct {
|
|
bytes: []const u8,
|
|
bytes_index: u32,
|
|
answers_remaining: u16,
|
|
|
|
pub const Answer = struct {
|
|
rr: u8,
|
|
packet: []const u8,
|
|
data_off: u32,
|
|
data_len: u16,
|
|
};
|
|
|
|
pub const Error = error{InvalidDnsPacket};
|
|
|
|
pub fn init(r: []const u8) Error!DnsResponse {
|
|
if (r.len < 12) return error.InvalidDnsPacket;
|
|
if ((r[3] & 15) != 0) return .{ .bytes = r, .bytes_index = 3, .answers_remaining = 0 };
|
|
var i: u32 = 12;
|
|
var query_count = std.mem.readInt(u16, r[4..6], .big);
|
|
while (query_count != 0) : (query_count -= 1) {
|
|
while (i < r.len and r[i] -% 1 < 127) i += 1;
|
|
if (r.len - i < 6) return error.InvalidDnsPacket;
|
|
i = i + 5 + @intFromBool(r[i] != 0);
|
|
}
|
|
return .{
|
|
.bytes = r,
|
|
.bytes_index = i,
|
|
.answers_remaining = std.mem.readInt(u16, r[6..8], .big),
|
|
};
|
|
}
|
|
|
|
pub fn next(dr: *DnsResponse) Error!?Answer {
|
|
if (dr.answers_remaining == 0) return null;
|
|
dr.answers_remaining -= 1;
|
|
const r = dr.bytes;
|
|
var i = dr.bytes_index;
|
|
while (i < r.len and r[i] -% 1 < 127) i += 1;
|
|
if (r.len - i < 12) return error.InvalidDnsPacket;
|
|
i = i + 1 + @intFromBool(r[i] != 0);
|
|
const len = std.mem.readInt(u16, r[i + 8 ..][0..2], .big);
|
|
if (i + 10 + len > r.len) return error.InvalidDnsPacket;
|
|
defer dr.bytes_index = i + 10 + len;
|
|
return .{
|
|
.rr = r[i + 1],
|
|
.packet = r,
|
|
.data_off = i + 10,
|
|
.data_len = len,
|
|
};
|
|
}
|
|
};
|
|
|
|
pub const ConnectTcpError = LookupError || IpAddress.ConnectTcpError;
|
|
|
|
pub fn connectTcp(host_name: HostName, io: Io, port: u16) ConnectTcpError!Stream {
|
|
var addresses_buffer: [32]IpAddress = undefined;
|
|
|
|
const results = try lookup(host_name, .{
|
|
.port = port,
|
|
.addresses_buffer = &addresses_buffer,
|
|
.canonical_name_buffer = &.{},
|
|
});
|
|
const addresses = addresses_buffer[0..results.addresses_len];
|
|
|
|
if (addresses.len == 0) return error.UnknownHostName;
|
|
|
|
for (addresses) |addr| {
|
|
return addr.connectTcp(io) catch |err| switch (err) {
|
|
error.ConnectionRefused => continue,
|
|
else => |e| return e,
|
|
};
|
|
}
|
|
return error.ConnectionRefused;
|
|
}
|
|
|
|
pub const ResolvConf = struct {
|
|
attempts: u32,
|
|
ndots: u32,
|
|
timeout_seconds: u32,
|
|
nameservers_buffer: [max_nameservers]IpAddress,
|
|
nameservers_len: usize,
|
|
search_buffer: [max_len]u8,
|
|
search_len: usize,
|
|
|
|
pub const max_nameservers = 3;
|
|
|
|
/// Returns `error.StreamTooLong` if a line is longer than 512 bytes.
|
|
fn init(io: Io) !ResolvConf {
|
|
var rc: ResolvConf = .{
|
|
.nameservers_buffer = undefined,
|
|
.nameservers_len = 0,
|
|
.search_buffer = undefined,
|
|
.search_len = 0,
|
|
.ndots = 1,
|
|
.timeout_seconds = 5,
|
|
.attempts = 2,
|
|
};
|
|
|
|
const file = Io.File.openAbsolute(io, "/etc/resolv.conf", .{}) catch |err| switch (err) {
|
|
error.FileNotFound,
|
|
error.NotDir,
|
|
error.AccessDenied,
|
|
=> {
|
|
try addNumeric(&rc, io, "127.0.0.1", 53);
|
|
return rc;
|
|
},
|
|
|
|
else => |e| return e,
|
|
};
|
|
defer file.close(io);
|
|
|
|
var line_buf: [512]u8 = undefined;
|
|
var file_reader = file.reader(io, &line_buf);
|
|
parse(&rc, io, &file_reader.interface) catch |err| switch (err) {
|
|
error.ReadFailed => return file_reader.err.?,
|
|
else => |e| return e,
|
|
};
|
|
return rc;
|
|
}
|
|
|
|
const Directive = enum { options, nameserver, domain, search };
|
|
const Option = enum { ndots, attempts, timeout };
|
|
|
|
fn parse(rc: *ResolvConf, io: Io, reader: *Io.Reader) !void {
|
|
while (reader.takeSentinel('\n')) |line_with_comment| {
|
|
const line = line: {
|
|
var split = std.mem.splitScalar(u8, line_with_comment, '#');
|
|
break :line split.first();
|
|
};
|
|
var line_it = std.mem.tokenizeAny(u8, line, " \t");
|
|
|
|
const token = line_it.next() orelse continue;
|
|
switch (std.meta.stringToEnum(Directive, token) orelse continue) {
|
|
.options => while (line_it.next()) |sub_tok| {
|
|
var colon_it = std.mem.splitScalar(u8, sub_tok, ':');
|
|
const name = colon_it.first();
|
|
const value_txt = colon_it.next() orelse continue;
|
|
const value = std.fmt.parseInt(u8, value_txt, 10) catch |err| switch (err) {
|
|
error.Overflow => 255,
|
|
error.InvalidCharacter => continue,
|
|
};
|
|
switch (std.meta.stringToEnum(Option, name) orelse continue) {
|
|
.ndots => rc.ndots = @min(value, 15),
|
|
.attempts => rc.attempts = @min(value, 10),
|
|
.timeout => rc.timeout_seconds = @min(value, 60),
|
|
}
|
|
},
|
|
.nameserver => {
|
|
const ip_txt = line_it.next() orelse continue;
|
|
try addNumeric(rc, io, ip_txt, 53);
|
|
},
|
|
.domain, .search => {
|
|
const rest = line_it.rest();
|
|
@memcpy(rc.search_buffer[0..rest.len], rest);
|
|
rc.search_len = rest.len;
|
|
},
|
|
}
|
|
} else |err| switch (err) {
|
|
error.EndOfStream => if (reader.bufferedLen() != 0) return error.EndOfStream,
|
|
else => |e| return e,
|
|
}
|
|
|
|
if (rc.nameservers_len == 0) {
|
|
try addNumeric(rc, io, "127.0.0.1", 53);
|
|
}
|
|
}
|
|
|
|
fn addNumeric(rc: *ResolvConf, io: Io, name: []const u8, port: u16) !void {
|
|
assert(rc.nameservers_len < rc.nameservers_buffer.len);
|
|
rc.nameservers_buffer[rc.nameservers_len] = try .resolve(io, name, port);
|
|
rc.nameservers_len += 1;
|
|
}
|
|
|
|
fn nameservers(rc: *const ResolvConf) []const IpAddress {
|
|
return rc.nameservers_buffer[0..rc.nameservers_len];
|
|
}
|
|
};
|
|
|
|
test ResolvConf {
|
|
const input =
|
|
\\# Generated by resolvconf
|
|
\\nameserver 1.0.0.1
|
|
\\nameserver 1.1.1.1
|
|
\\nameserver fe80::e0e:76ff:fed4:cf22%eno1
|
|
\\options edns0
|
|
\\
|
|
;
|
|
var reader: Io.Reader = .fixed(input);
|
|
|
|
var rc: ResolvConf = .{
|
|
.nameservers_buffer = undefined,
|
|
.nameservers_len = 0,
|
|
.search_buffer = undefined,
|
|
.search_len = 0,
|
|
.ndots = 1,
|
|
.timeout_seconds = 5,
|
|
.attempts = 2,
|
|
};
|
|
|
|
try rc.parse(std.testing.io, &reader);
|
|
try std.testing.expectEqual(3, rc.nameservers().len);
|
|
}
|