From 7e9831193b841ebe6961b019e91cb17eaa9f6aff Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Mon, 30 Dec 2024 19:56:38 +0100 Subject: [PATCH] WIP: add std.net.dns for DNS lookups This fixes https://github.com/inko-lang/inko/issues/735. Changelog: added --- std/src/std/libc.inko | 1 + std/src/std/libc/freebsd.inko | 1 + std/src/std/libc/linux.inko | 1 + std/src/std/libc/mac.inko | 1 + std/src/std/net/dns.inko | 26 +++++++ std/src/std/sys/linux/dns.inko | 124 +++++++++++++++++++++++++++++++++ std/src/std/sys/unix/dns.inko | 71 +++++++++++++++++++ std/src/std/sys/unix/net.inko | 44 +++++++----- 8 files changed, 252 insertions(+), 17 deletions(-) create mode 100644 std/src/std/net/dns.inko create mode 100644 std/src/std/sys/linux/dns.inko create mode 100644 std/src/std/sys/unix/dns.inko diff --git a/std/src/std/libc.inko b/std/src/std/libc.inko index 73f297dc..0a4925e5 100644 --- a/std/src/std/libc.inko +++ b/std/src/std/libc.inko @@ -17,6 +17,7 @@ import std.libc.mac (self as sys) if mac let AF_INET = sys.AF_INET let AF_INET6 = sys.AF_INET6 let AF_UNIX = sys.AF_UNIX +let AF_UNSPEC = sys.AF_UNSPEC let CLOCK_REALTIME = sys.CLOCK_REALTIME let DT_DIR = sys.DT_DIR let DT_LNK = sys.DT_LNK diff --git a/std/src/std/libc/freebsd.inko b/std/src/std/libc/freebsd.inko index 0c892bc6..3d4ea341 100644 --- a/std/src/std/libc/freebsd.inko +++ b/std/src/std/libc/freebsd.inko @@ -2,6 +2,7 @@ import std.io (Error) let AF_INET = 2 let AF_INET6 = 28 +let AF_UNSPEC = 0 let AF_UNIX = 1 let CLOCK_REALTIME = 0 let DT_DIR = 4 diff --git a/std/src/std/libc/linux.inko b/std/src/std/libc/linux.inko index 52d554cd..7a020169 100644 --- a/std/src/std/libc/linux.inko +++ b/std/src/std/libc/linux.inko @@ -4,6 +4,7 @@ import std.libc.linux.arm64 (self as arch) if arm64 let AF_INET = 2 let AF_INET6 = 10 +let AF_UNSPEC = 0 let AF_UNIX = 1 let AT_EMPTY_PATH = 0x1000 let AT_FDCWD = -0x64 diff --git a/std/src/std/libc/mac.inko b/std/src/std/libc/mac.inko index c6158813..2e46d0da 100644 --- a/std/src/std/libc/mac.inko +++ b/std/src/std/libc/mac.inko @@ -4,6 +4,7 @@ import std.libc.mac.arm64 (self as sys) if arm64 let AF_INET = 2 let AF_INET6 = 30 +let AF_UNSPEC = 0 let AF_UNIX = 1 let AT_FDCWD = -2 let CLOCK_REALTIME = 0 diff --git a/std/src/std/net/dns.inko b/std/src/std/net/dns.inko new file mode 100644 index 00000000..d50c955f --- /dev/null +++ b/std/src/std/net/dns.inko @@ -0,0 +1,26 @@ +import std.io (Error) +import std.net.ip (IpAddress) +import std.sys.linux.dns (self as sys) if linux +import std.sys.unix.dns (self as sys) if mac +import std.sys.unix.dns (self as sys) if freebsd + +# A type that can resolve DNS queries, such as resolving a hostname into a list +# of IP addresses. +trait Resolve { + fn pub mut resolve(host: String) -> Result[Array[IpAddress], Error] +} + +# A type for resolving DNS queries. +type pub inline Resolver { + let @inner: Resolve + + fn pub static new -> Resolver { + Resolver(sys.resolver) + } +} + +impl Resolve for Resolver { + fn pub mut resolve(host: String) -> Result[Array[IpAddress], Error] { + @inner.resolve(host) + } +} diff --git a/std/src/std/sys/linux/dns.inko b/std/src/std/sys/linux/dns.inko new file mode 100644 index 00000000..75eed3f6 --- /dev/null +++ b/std/src/std/sys/linux/dns.inko @@ -0,0 +1,124 @@ +import std.io (Buffer, Error) +import std.json (Json) +import std.libc +import std.net.dns (Resolve) +import std.net.ip (IpAddress) +import std.net.socket (UnixClient) +import std.sys.unix.dns (resolver as system_resolver) +import std.sys.unix.net (self as net) + +# The varlink method to use for resolving a hostname. +let RESOLVE_HOST = 'io.systemd.Resolve.ResolveHostname' + +# The path to the systemd varlink socket. +let VARLINK_SOCKET = '/run/systemd/resolve/io.systemd.Resolve' + +# The amount of bytes to read from a socket in a single call. +let READ_SIZE = 8 * 1024 + +# Returns a new `ResolveHostname` message. +fn resolve_host(host: String) -> String { + let msg = Map.new + let params = Map.new + + params.set('name', Json.String(host)) + params.set('family', Json.Int(libc.AF_UNSPEC)) + msg.set('method', Json.String(RESOLVE_HOST)) + msg.set('parameters', Json.Object(params)) + Json.Object(msg).to_string +} + +# Parses the response of the `ResolveHostname` call. +fn parse_resolve_host_response(json: Json) -> Array[IpAddress] { + json + .query + .key('parameters') + .key('addresses') + .as_array + .get + .iter + .select_map(fn (val) { + let fam = try val.query.key('family').as_int + let capa = if fam == libc.AF_INET6 { 16 } else { 4 } + let addr = try val.query.key('address').as_array.then(fn (nums) { + nums + .iter + .try_reduce(ByteArray.with_capacity(capa), fn (bytes, num) { + try num.query.as_int.ok_or(nil).map(fn (v) { bytes.push(v) }) + Result.Ok(bytes) + }) + .ok + }) + + match fam { + case libc.AF_INET if addr.size == 4 -> { + Option.Some(net.parse_v4_address(addr.to_pointer as Pointer[UInt8])) + } + case libc.AF_INET6 if addr.size == 16 -> { + Option.Some(net.parse_v6_address(addr.to_pointer as Pointer[UInt16])) + } + case _ -> Option.None + } + }) + .to_array +} + +# A resolver that uses systemd-resolve's through its varlink +# (https://varlink.org/) protocol. +type SystemdResolver { + let @socket: UnixClient + let @buffer: ByteArray + + fn static new -> Option[SystemdResolver] { + let sock = try UnixClient.new(VARLINK_SOCKET.to_path).ok + let buf = ByteArray.new + + Option.Some(SystemdResolver(socket: sock, buffer: buf)) + } +} + +impl Resolve for SystemdResolver { + fn pub mut resolve(host: String) -> Result[Array[IpAddress], Error] { + @buffer.append(resolve_host(host)) + @buffer.push(0) + try @socket.write_bytes(@buffer) + @buffer.clear + + # Read until the trailing NULL byte. + loop { + match try @socket.read(into: @buffer, size: READ_SIZE) { + case 0 -> break + case _ if @buffer.last.or(-1) == 0 -> { + @buffer.pop + break + } + case _ -> {} + } + } + + # At this point it doesn't really matter what error we throw in case of + # invalid JSON, because outside of unexpected systemd changes we're unlikely + # to ever throw an error in the first place. + let res = Json + .parse(Buffer.new(@buffer)) + .ok + .map(fn (v) { parse_resolve_host_response(v) }) + .ok_or(Error.InvalidData) + + @buffer.clear + res + } +} + +fn inline resolver -> Resolve { + # If systemd-resolve is present then we try to use its varlink interface. In + # the rare case that the socket is available but for some reason we can't + # connect to it, we fall back to using the system resolver. + if VARLINK_SOCKET.to_path.file? { + SystemdResolver.new.map(fn (r) { r as Resolve }).or_else(fn { + system_resolver + }) + } else { + system_resolver + } +} diff --git a/std/src/std/sys/unix/dns.inko b/std/src/std/sys/unix/dns.inko new file mode 100644 index 00000000..de60f9b5 --- /dev/null +++ b/std/src/std/sys/unix/dns.inko @@ -0,0 +1,71 @@ +import std.io (Error) +import std.libc.linux (self as linux) +import std.net.dns (Resolve) +import std.net.ip (IpAddress) +import std.sys.unix.net (self as sys) if unix + +# TODO: push into std.libc +type extern AddrInfo { + let @ai_flags: Int32 + let @ai_family: Int32 + let @ai_socktype: Int32 + let @ai_protocol: Int32 + let @ai_addrlen: UInt64 + let @ai_addr: Pointer[linux.SockAddr] + let @ai_canonname: Pointer[UInt8] + let @ai_next: Pointer[AddrInfo] +} + +# TODO: push into std.libc +fn extern getaddrinfo( + node: Pointer[UInt8], + service: Pointer[UInt8], + hints: Pointer[AddrInfo], + res: Pointer[AddrInfo], +) -> Int32 + +# TODO: push into std.libc +fn extern freeaddrinfo(addr: Pointer[AddrInfo]) + +type Resolver { + fn inline static new -> Resolver { + Resolver() + } +} + +impl Resolve for Resolver { + fn pub mut resolve(host: String) -> Result[Array[IpAddress], Error] { + let hints = AddrInfo( + ai_flags: 0 as Int32, + ai_family: 0 as Int32, + ai_socktype: linux.SOCK_STREAM as Int32, + ai_protocol: 0 as Int32, + ai_addrlen: 0 as UInt64, + ai_addr: 0x0 as Pointer[linux.SockAddr], + ai_canonname: 0x0 as Pointer[UInt8], + ai_next: 0x0 as Pointer[AddrInfo], + ) + let list = 0x0 as Pointer[AddrInfo] + let res = getaddrinfo(host.to_pointer, ''.to_pointer, mut hints, mut list) + + # TODO: better error handling + if res as Int != 0 { panic('getaddrinfo(3) failed') } + + let mut cur = list + let ips = [] + + while cur as Int != 0 { + let addr_ptr = cur.ai_addr as Pointer[linux.SockAddrStorage] + + ips.push(sys.parse_ip_socket_address(addr_ptr).0) + cur = cur.ai_next + } + + freeaddrinfo(list) + Result.Ok(ips) + } +} + +fn inline resolver -> Resolve { + Resolver.new as Resolve +} diff --git a/std/src/std/sys/unix/net.inko b/std/src/std/sys/unix/net.inko index b589eb52..2f0bd7c9 100644 --- a/std/src/std/sys/unix/net.inko +++ b/std/src/std/sys/unix/net.inko @@ -200,6 +200,28 @@ fn inline connect( Result.Ok(nil) } +fn inline parse_v4_address(pointer: Pointer[UInt8]) -> IpAddress { + let a = pointer.0 as Int + let b = ptr.add(pointer, 1).0 as Int + let c = ptr.add(pointer, 2).0 as Int + let d = ptr.add(pointer, 3).0 as Int + + IpAddress.v4(a, b, c, d) +} + +fn inline parse_v6_address(pointer: Pointer[UInt16]) -> IpAddress { + let a = net.htons(pointer.0 as Int) + let b = net.htons(ptr.add(pointer, 1).0 as Int) + let c = net.htons(ptr.add(pointer, 2).0 as Int) + let d = net.htons(ptr.add(pointer, 3).0 as Int) + let e = net.htons(ptr.add(pointer, 4).0 as Int) + let f = net.htons(ptr.add(pointer, 5).0 as Int) + let g = net.htons(ptr.add(pointer, 6).0 as Int) + let h = net.htons(ptr.add(pointer, 7).0 as Int) + + IpAddress.v6(a, b, c, d, e, f, g, h) +} + fn inline parse_ip_socket_address( address: Pointer[sys_libc.SockAddrStorage], ) -> (IpAddress, Int) { @@ -207,28 +229,16 @@ fn inline parse_ip_socket_address( case libc.AF_INET -> { let addr_ptr = address as Pointer[sys_libc.SockAddrIn] let port = net.htons(addr_ptr.sin_port as Int) - let ip_ptr = (mut addr_ptr.sin_addr) as Pointer[UInt8] - let a = ip_ptr.0 as Int - let b = ptr.add(ip_ptr, 1).0 as Int - let c = ptr.add(ip_ptr, 2).0 as Int - let d = ptr.add(ip_ptr, 3).0 as Int + let ip = parse_v4_address((mut addr_ptr.sin_addr) as Pointer[UInt8]) - (IpAddress.v4(a, b, c, d), port) + (ip, port) } case libc.AF_INET6 -> { let addr_ptr = address as Pointer[sys_libc.SockAddrIn6] let port = net.htons(addr_ptr.sin6_port as Int) - let ip_ptr = (mut addr_ptr.sin6_addr0) as Pointer[UInt16] - let a = net.htons(ip_ptr.0 as Int) - let b = net.htons(ptr.add(ip_ptr, 1).0 as Int) - let c = net.htons(ptr.add(ip_ptr, 2).0 as Int) - let d = net.htons(ptr.add(ip_ptr, 3).0 as Int) - let e = net.htons(ptr.add(ip_ptr, 4).0 as Int) - let f = net.htons(ptr.add(ip_ptr, 5).0 as Int) - let g = net.htons(ptr.add(ip_ptr, 6).0 as Int) - let h = net.htons(ptr.add(ip_ptr, 7).0 as Int) - - (IpAddress.v6(a, b, c, d, e, f, g, h), port) + let ip = parse_v6_address((mut addr_ptr.sin6_addr0) as Pointer[UInt16]) + + (ip, port) } case other -> address_family_error(other) }