commit 0deaf9b34e69a5b774bc390103d5e31ce8c207b8
parent ba1f76e8a971de8879a59dac59f40488eba16abc
Author: Drew DeVault <sir@cmpwn.com>
Date: Mon, 3 Jun 2024 12:04:49 +0200
unix::resolvconf: rewrite and expand parser
Signed-off-by: Drew DeVault <sir@cmpwn.com>
Diffstat:
7 files changed, 316 insertions(+), 47 deletions(-)
diff --git a/net/dial/ip.ha b/net/dial/ip.ha
@@ -9,6 +9,7 @@ use net::udp;
fn dial_tcp(addr: str, service: str) (net::socket | error) = {
const result = resolve("tcp", addr, service)?;
const addrs = result.0, port = result.1;
+ defer free(addrs);
for (let i = 0z; i < len(addrs); i += 1) {
const addr = addrs[i];
match (tcp::connect(addr, port)) {
@@ -26,6 +27,7 @@ fn dial_tcp(addr: str, service: str) (net::socket | error) = {
fn dial_udp(addr: str, service: str) (net::socket | error) = {
const result = resolve("udp", addr, service)?;
const addrs = result.0, port = result.1;
+ defer free(addrs);
for (let i = 0z; i < len(addrs); i += 1) {
const addr = addrs[i];
match (udp::connect(addr, port)) {
diff --git a/net/dns/query.ha b/net/dns/query.ha
@@ -22,7 +22,7 @@ def timeout: time::duration = 3 * time::SECOND;
// If no DNS servers are provided, the system default servers (if any) are used.
export fn query(query: *message, servers: ip::addr...) (*message | error) = {
if (len(servers) == 0) {
- servers = resolvconf::load();
+ servers = resolvconf::load().nameservers;
};
if (len(servers) == 0) {
// Fall back to localhost
diff --git a/unix/resolvconf/README b/unix/resolvconf/README
@@ -1,4 +1,8 @@
-unix::resolvconf provides a (basic) reader for the resolv.conf file found on
-many Unix-like operating systems. This implementation only supports the
-"nameserver" directive, and it will return a list of IP addresses to use as
-nameservers.
+[[unix::resolvconf]] implements a parser for /etc/resolv.conf files which has
+feature parity with the resolv.conf format supported by glibc 2.36. However,
+most options are not supported by Hare internally, i.e. via [[net::dns]].
+
+The user may parse a resolv.conf file manually via the [[read]] and [[next]]
+functions. Additionally, this module maintains a global copy of the local
+resolv.conf file, parsed once at runtime and cached for the lifetime of the
+process, which is available via [[load]].
diff --git a/unix/resolvconf/errors.ha b/unix/resolvconf/errors.ha
@@ -0,0 +1,26 @@
+use errors;
+use encoding::utf8;
+use io;
+use net::ip;
+
+// The resolv.conf file is not well-formatted.
+export type invalid = !void;
+
+// Any error which can be raised by the resolv.conf parser.
+export type error = !(errors::error | io::error | utf8::invalid | ip::invalid | invalid);
+
+// Converts an [[error]] into a human-friendly representation.
+export fn strerror(err: error) const str = {
+ match (err) {
+ case invalid =>
+ return "resolv.conf is not well-formatted";
+ case let err: errors::error =>
+ return errors::strerror(err);
+ case let err: io::error =>
+ return io::strerror(err);
+ case let err: ip::invalid =>
+ return "Invalid IP address in /etc/resolv.conf";
+ case utf8::invalid =>
+ return "resolv.conf contains invalid UTF-8 data";
+ };
+};
diff --git a/unix/resolvconf/load.ha b/unix/resolvconf/load.ha
@@ -2,63 +2,73 @@
// (c) Hare authors <https://harelang.org>
use bufio;
+use fmt;
use io;
use memio;
use net::ip;
use os;
use strings;
-let cache: []ip::addr = [];
+let cache_valid = false;
+let cache: config = config {
+ options = DEFAULT_OPTIONS,
+ ...
+};
@fini fn fini() void = {
- free(cache);
+ if (!cache_valid) {
+ return;
+ };
+
+ strings::freeall(cache.search);
+ free(cache.nameservers);
+ free(cache.sortlist);
};
-// Reads a list of nameservers from resolv.conf. Aborts the program if the file
-// does not exist, is written in an invalid format, or if any other error
-// occurs.
-export fn load() []ip::addr = {
- // XXX: Would be cool if we could do this without allocating anything
- if (len(cache) != 0) {
- return cache;
+// Reads /etc/resolv.conf (or the platform-specific equivalent path) and returns
+// the configuration therein. If the file does not exist, or is poorly
+// formatted, returns the default resolver configuration.
+export fn load() *config = {
+ if (cache_valid) {
+ return &cache;
};
- const file = os::open(PATH)!;
+ const file = match (os::open("/etc/resolv.conf")) {
+ case let file: io::file =>
+ yield file;
+ case =>
+ cache_valid = true;
+ return &cache;
+ };
defer io::close(file)!;
- for (true) {
- const line = match (bufio::read_line(file)) {
- case io::EOF =>
- break;
- case let line: []u8 =>
- yield line;
- };
- defer free(line);
- if (len(line) == 0 || line[0] == '#') {
- continue;
- };
-
- const scanner = memio::fixed(line);
- const tok = match (bufio::read_tok(&scanner, ' ', '\t')!) {
- case io::EOF =>
- break;
- case let tok: []u8 =>
- yield tok;
- };
- defer free(tok);
- if (strings::fromutf8(tok)! != "nameserver") {
- continue;
- };
+ match (parse(&cache, file)) {
+ case let err: error =>
+ fmt::errorfln("Error parsing /etc/resolv.conf: {}",
+ strerror(err)): void;
+ return &cache;
+ case void =>
+ cache_valid = true;
+ return &cache;
+ };
+};
- const tok = match (bufio::read_tok(&scanner, ' ')!) {
- case io::EOF =>
- break;
- case let tok: []u8 =>
- yield tok;
+// Parses a resolv.conf-formatted file and populates the given config object.
+fn parse(conf: *config, in: io::handle) (void | error) = {
+ const rd = read(in);
+ for (const param => next(&rd)!) {
+ switch (param.name) {
+ case "nameserver" =>
+ append(conf.nameservers, param.value as ip::addr);
+ case "search" =>
+ strings::freeall(conf.search);
+ conf.search = strings::dupall(param.value as []str);
+ case "sortlist" =>
+ free(conf.sortlist);
+ conf.sortlist = alloc((param.value as []ip::subnet)...);
+ case "options" =>
+ conf.options = *(param.value as *options);
+ case => void;
};
- defer free(tok);
- append(cache, ip::parse(strings::fromutf8(tok)!)!);
};
-
- return cache;
};
diff --git a/unix/resolvconf/reader.ha b/unix/resolvconf/reader.ha
@@ -0,0 +1,180 @@
+// SPDX-License-Identifier: MPL-2.0
+// (c) Hare authors <https://harelang.org>
+
+use bufio;
+use io;
+use net::ip;
+use strconv;
+use strings;
+
+export type reader = struct {
+ scan: bufio::scanner,
+
+ // Only one of these is valid at a time (return values from [[next]] are
+ // borrowed from this).
+ union {
+ addr_list: []ip::addr,
+ subnet_list: []ip::subnet,
+ str_list: []str,
+ },
+
+ options: options,
+};
+
+// Reads an /etc/resolv.conf-formatted file from the provided I/O handle. Use
+// [[next]] to enumerate directives from the file and pass the return value to
+// [[finish]] to free resources associated with the reader.
+export fn read(in: io::handle) reader = {
+ return reader {
+ scan = bufio::newscanner(in),
+ ...
+ };
+};
+
+// Frees resources associated with a [[reader]].
+export fn finish(rd: *reader) void = {
+ bufio::finish(&rd.scan);
+ free(rd.addr_list);
+};
+
+// Reads the next [[parameter]] from a resolv.conf [[reader]]. The return value
+// is borrowed from the [[reader]].
+export fn next(rd: *reader) (parameter | io::EOF | error) = {
+ for (const line => bufio::scan_line(&rd.scan)?) {
+ if (strings::hasprefix(line, '#') || strings::hasprefix(line, ';')) {
+ continue;
+ };
+ if (len(line) == 0) {
+ continue;
+ };
+
+ const tok = strings::tokenize(line, " \t");
+
+ const name = match (strings::next_token(&tok)) {
+ case let name: str =>
+ yield name;
+ case done =>
+ continue;
+ };
+
+ const val = switch (name) {
+ case "nameserver" =>
+ yield parse_addr(rd, &tok)?;
+ case "search" =>
+ yield parse_str_list(rd, &tok)?;
+ case "sortlist" =>
+ yield parse_subnet_list(rd, &tok)?;
+ case "options" =>
+ yield parse_options(rd, &tok)?;
+ case =>
+ continue;
+ };
+
+ return parameter {
+ name = name,
+ value = val,
+ };
+ };
+
+ return io::EOF;
+};
+
+fn parse_addr(rd: *reader, tok: *strings::tokenizer) (value | error) = {
+ const addr = match (strings::next_token(tok)) {
+ case let addr: str =>
+ yield addr;
+ case done =>
+ return invalid;
+ };
+
+ return ip::parse(addr)?;
+};
+
+fn parse_subnet_list(rd: *reader, tok: *strings::tokenizer) (value | error) = {
+ rd.subnet_list = rd.subnet_list[..0];
+
+ for (const tok => strings::next_token(tok)) {
+ if (len(tok) == 0) {
+ continue;
+ };
+
+ const subnet = ip::parsecidr(tok)?;
+ append(rd.subnet_list, subnet);
+ };
+
+ return rd.subnet_list;
+};
+
+fn parse_str_list(rd: *reader, tok: *strings::tokenizer) (value | error) = {
+ rd.str_list = rd.str_list[..0];
+
+ for (const tok => strings::next_token(tok)) {
+ if (len(tok) == 0) {
+ continue;
+ };
+ append(rd.str_list, tok);
+ };
+
+ return rd.str_list;
+};
+
+fn parse_options(rd: *reader, tok: *strings::tokenizer) (value | error) = {
+ rd.options = DEFAULT_OPTIONS;
+ let opts = &rd.options;
+
+ for (const tok => strings::next_token(tok)) {
+ if (len(tok) == 0) {
+ continue;
+ };
+
+ const (name, val) = strings::cut(tok, ":");
+ switch (name) {
+ case "debug" =>
+ opts.debug = true;
+ case "ndots" =>
+ match (strconv::stou(val)) {
+ case let u: uint =>
+ opts.ndots = u;
+ case =>
+ return invalid;
+ };
+ case "timeout" =>
+ match (strconv::stou(val)) {
+ case let u: uint =>
+ opts.timeout = u;
+ case =>
+ return invalid;
+ };
+ case "attempts" =>
+ match (strconv::stou(val)) {
+ case let u: uint =>
+ opts.attempts = u;
+ case =>
+ return invalid;
+ };
+ case "rotate" =>
+ opts.rotate = true;
+ case "no-aaaa" =>
+ opts.no_aaaa = true;
+ case "no-check-names" =>
+ opts.no_check_names = true;
+ case "inet6" =>
+ opts.inet6 = true;
+ case "edns0" =>
+ opts.edns0 = true;
+ case "single-request" =>
+ opts.single_request = true;
+ case "no-tld-query" =>
+ opts.no_tld_query = true;
+ case "use-vc" =>
+ opts.use_vc = true;
+ case "no-reload" =>
+ opts.no_reload = true;
+ case "trust-ad" =>
+ opts.trust_ad = true;
+ case => void;
+ };
+ };
+
+ return opts;
+};
diff --git a/unix/resolvconf/types.ha b/unix/resolvconf/types.ha
@@ -0,0 +1,47 @@
+use net::ip;
+
+// A list of [[net::ip::subnet]]s.
+export type subnet_list = []ip::subnet;
+
+// Values set in an "options" directive.
+export type options = struct {
+ debug: bool,
+ ndots: uint,
+ timeout: uint,
+ attempts: uint,
+ rotate: bool,
+ no_aaaa: bool,
+ no_check_names: bool,
+ inet6: bool,
+ edns0: bool,
+ single_request: bool,
+ single_request_reopen: bool,
+ no_tld_query: bool,
+ use_vc: bool,
+ no_reload: bool,
+ trust_ad: bool,
+};
+
+def DEFAULT_OPTIONS = options {
+ ndots = 1,
+ timeout = 5,
+ attempts = 2,
+ ...
+};
+
+// The value associated with a configuration parameter.
+export type value = (ip::addr | subnet_list | *options | []str);
+
+// A configuration parameter from resolv.conf.
+export type parameter = struct {
+ name: const str,
+ value: value,
+};
+
+// A complete configuration parsed from resolv.conf.
+export type config = struct {
+ nameservers: []ip::addr,
+ search: []str,
+ sortlist: []ip::subnet,
+ options: options,
+};