From 566b21d76585c0d3525b7daecb003ce6e618c16a Mon Sep 17 00:00:00 2001 From: Drew DeVault Date: Wed, 27 Sep 2023 14:49:25 +0200 Subject: [PATCH] ev::dns: initial commit --- cmd/dnsclient/main.ha | 68 +++++++++++++++++++++++++++++++++++++++++++++++++++++ ev/dns/dns.ha | 142 +++++++++++++++++++++++++++++++++++++++++++++++++++++ diff --git a/cmd/dnsclient/main.ha b/cmd/dnsclient/main.ha new file mode 100644 index 0000000000000000000000000000000000000000..615eb880a7fb2d7f41990ab9e42e58821525c441 --- /dev/null +++ b/cmd/dnsclient/main.ha @@ -0,0 +1,68 @@ +use crypto::random; +use ev; +use edns = ev::dns; +use fmt; +use net::dns; +use net::ip; +use os; + +export fn main() void = { + const loop = ev::newloop()!; + defer ev::finish(&loop); + + let rand: []u8 = [0, 0]; + random::buffer(rand); + let id = *(&rand[0]: *u16); + + const domain = dns::parse_domain(os::args[1]); + defer free(domain); + + const query = dns::message { + header = dns::header { + id = id, + op = dns::op { + qr = dns::qr::QUERY, + opcode = dns::opcode::QUERY, + rd = true, + ... + }, + qdcount = 1, + ... + }, + questions = [ + dns::question { + qname = domain, + qtype = dns::qtype::A, + qclass = dns::qclass::IN, + }, + ], + ... + }; + + edns::query(&loop, &query, &querycb, &loop)!; + + for (ev::dispatch(&loop, -1)!) void; +}; + +fn querycb(user: nullable *opaque, r: (*dns::message | dns::error)) void = { + const loop = user: *ev::loop; + const resp = match (r) { + case let msg: *dns::message => + yield msg; + case let err: dns::error => + fmt::errorln("DNS error:", dns::strerror(err))!; + ev::stop(loop); + return; + }; + + for (let i = 0z; i < len(resp.answers); i += 1) { + match (resp.answers[i].rdata) { + case let addr: dns::aaaa => + fmt::println(ip::string(addr))!; + case let addr: dns::a => + fmt::println(ip::string(addr))!; + case => void; + }; + }; + ev::stop(loop); +}; diff --git a/ev/dns/dns.ha b/ev/dns/dns.ha new file mode 100644 index 0000000000000000000000000000000000000000..8d189e12bb89bacc0253d14afd008678f290ebf6 --- /dev/null +++ b/ev/dns/dns.ha @@ -0,0 +1,142 @@ +use errors; +use ev; +use net; +use net::dns; +use net::ip; +use net::udp; +use time; +use unix::resolvconf; + +// TODO: Let users customize this? +def TIMEOUT: time::duration = 3 * time::SECOND; + +// Callback for a [[query]] operation. +export type querycb = fn( + user: nullable *opaque, + r: (*dns::message | dns::error), +) void; + +type qstate = struct { + buf: [512]u8, + socket4: *ev::file, + socket6: *ev::file, + r4: ev::req, + r6: ev::req, + timer: *ev::file, + rid: u16, + cb: *querycb, + user: nullable *opaque, +}; + +// Performs a DNS query against the provided set of DNS servers, or the list of +// servers from /etc/resolv.conf if none are specified. The user must free the +// message passed to the callback with [[net::dns::message_free]]. +export fn query( + loop: *ev::loop, + query: *dns::message, + cb: *querycb, + user: nullable *opaque, + servers: ip::addr... +) (ev::req | dns::error | net::error | errors::error) = { + if (len(servers) == 0) { + servers = resolvconf::load(); + }; + if (len(servers) == 0) { + // Fall back to localhost + servers = [ip::LOCAL_V6, ip::LOCAL_V4]; + }; + + const socket4 = ev::listen_udp(loop, ip::ANY_V4, 0)?; + const socket6 = ev::listen_udp(loop, ip::ANY_V6, 0)?; + const timeout = ev::newtimer(loop, &timeoutcb, time::clock::MONOTONIC)?; + let state = alloc(qstate { + socket4 = socket4, + socket6 = socket6, + timer = timeout, + rid = query.header.id, + cb = cb, + user = user, + ... + }); + const z = dns::encode(state.buf, query)?; + ev::setuser(socket4, state); + ev::setuser(socket6, state); + ev::setuser(timeout, state); + ev::timer_configure(timeout, TIMEOUT, 0); + + // Note: the initial set of requests is sent directly through net::udp + // as it is assumed they can fit into the kernel's internal send buffer + // and will finish without blocking + const buf = state.buf[..z]; + for (let i = 0z; i < len(servers); i += 1) match (servers[i]) { + case ip::addr4 => + udp::sendto(ev::getfd(socket4), buf, servers[i], 53)?; + case ip::addr6 => + udp::sendto(ev::getfd(socket6), buf, servers[i], 53)?; + }; + + state.r4 = ev::recvfrom(socket4, &qrecvcb, state.buf); + state.r6 = ev::recvfrom(socket6, &qrecvcb, state.buf); + return ev::mkreq(&query_cancel, state); +}; + +fn query_cancel(req: *ev::req) void = { + const q = req.user: *qstate; + query_destroy(q); +}; + +fn query_destroy(q: *qstate) void = { + ev::cancel(&q.r4); + ev::cancel(&q.r6); + ev::close(q.socket4); + ev::close(q.socket6); + ev::close(q.timer); + free(q); +}; + +fn query_complete(q: *qstate, r: (*dns::message | dns::error)) void = { + const cb = q.cb; + const user = q.user; + query_destroy(q); + cb(user, r); +}; + +fn timeoutcb(file: *ev::file) void = { + const q = ev::getuser(file): *qstate; + query_complete(q, errors::timeout); +}; + +fn qrecvcb(file: *ev::file, r: ((size, ip::addr, u16) | net::error)) void = { + const q = ev::getuser(file): *qstate; + let req: *ev::req = if (file == q.socket4) &q.r4 else &q.r6; + *req = ev::req { ... }; + + const (z, addr, port) = match (r) { + case let r: (size, ip::addr, u16) => + yield r; + case let err: net::error => + query_complete(q, err); + return; + }; + + const resp = match (dns::decode(q.buf[..z])) { + case dns::format => + *req = ev::recvfrom(file, &qrecvcb, q.buf); + return; + case let msg: *dns::message => + yield msg; + }; + defer dns::message_free(resp); + + if (resp.header.id != q.rid || resp.header.op.qr != dns::qr::RESPONSE) { + *req = ev::recvfrom(file, &qrecvcb, q.buf); + return; + }; + + if (!resp.header.op.tc) { + query_complete(q, resp); + return; + }; + + abort(); // TODO: retry over TCP +}; -- 2.48.1