inko-lang / inko

A language for building concurrent software with confidence
http://inko-lang.org/
Mozilla Public License 2.0
866 stars 38 forks source link

Add module for resolving DNS names to IP addresses #735

Open yorickpeterse opened 1 month ago

yorickpeterse commented 1 month ago

Description

Inko's standard library should provide a std.net.dns module, which in turn provides types/methods for looking up DNS names and resolving them to IP addresses. Ideally we'd do so without using libc, by instead parsing /etc/resolv.conf ourselves and querying the DNS servers through the standard library. For macOS some special care is likely needed as it doesn't use resolv.conf (or at least considers it obsolete). What exactly is needed here I'm not yet sure of.

Related issues

Related work

yorickpeterse commented 1 month ago

For the initial setup I think we should just go with using getaddrinfo(). This works across all Unix platforms (including macOS), and is easier to implement compared to a pure-Inko DNS stack. At some point in the future we could add a pure Inko resolver, and only use it on e.g. Linux and FreeBSD.

yorickpeterse commented 1 month ago

Note to self: reverse DNS is done using getnameinfo().

yorickpeterse commented 1 month ago

A sketch of using getaddrinfo, without handling it blocking the OS thread:

dns.inko ```inko import std.fmt (fmt) import std.net.ip (IpAddress) import std.stdio (STDOUT) let AF_INET = 2 let AF_INET6 = 10 let SOCK_STREAM = 1 let SOCK_DGRAM = 2 let SOCK_RAW = 3 class extern InAddr { let @s_addr: UInt32 } class extern In6Addr { let @0: UInt8 let @1: UInt8 let @2: UInt8 let @3: UInt8 let @4: UInt8 let @5: UInt8 let @6: UInt8 let @7: UInt8 } class extern SockAddr { let @sa_family_t: UInt16 let @sa_data: Pointer[UInt8] } class extern SockAddrIn { let @sin_family: UInt16 let @sin_port: UInt16 let @sin_addr: InAddr } class extern SockAddrIn6 { let @sin6_family: UInt16 let @sin6_port: UInt16 let @sin6_flowinfo: UInt32 let @sin6_addr: In6Addr let @sin6_scope_id: UInt32 } class extern AddrInfo { let @ai_flags: Int32 let @ai_family: Int32 let @ai_socktype: Int32 let @ai_protocol: Int32 let @ai_addrlen: Int32 let @ai_addr: Pointer[SockAddr] let @ai_canonname: Pointer[UInt8] let @ai_next: Pointer[AddrInfo] } fn extern getaddrinfo( node: Pointer[UInt8], service: Pointer[UInt8], hints: Pointer[AddrInfo], res: Pointer[AddrInfo], ) -> Int32 fn extern freeaddrinfo(res: Pointer[AddrInfo]) class async Main { fn async main { let name = 'yorickpeterse.com' let port = '80' let addr = 0x0 as Pointer[AddrInfo] let stdout = STDOUT.new let hints = AddrInfo( ai_flags: 0 as Int32, ai_family: 0 as Int32, ai_socktype: SOCK_STREAM as Int32, ai_protocol: 0 as Int32, ai_addrlen: 0 as Int32, ai_addr: 0x0 as Pointer[SockAddr], ai_canonname: 0x0 as Pointer[UInt8], ai_next: 0x0 as Pointer[AddrInfo], ) match getaddrinfo(name.to_pointer, port.to_pointer, mut hints, mut addr) as Int { case 0 -> {} case n -> panic('getaddrinfo() failed: ${n}') } let mut current = addr while current as Int != 0x0 { match current.ai_addr.sa_family_t as Int { case AF_INET -> { let fam = current.ai_family as Int let proto = current.ai_protocol as Int let typ = current.ai_socktype as Int let addr = current.ai_addr as Pointer[SockAddrIn] let port = (addr.sin_port as Int).swap_bytes >>> 48 let raw = addr.sin_addr.s_addr as Int let ip = IpAddress.v4( raw as UInt8 as Int, raw >> 8 as UInt8 as Int, raw >> 16 as UInt8 as Int, raw >> 24 as UInt8 as Int, ) stdout.print( 'IPv4: ${ip}, port = ${port}, family = ${fam}, type = ${typ}, proto = ${proto}', ) } case AF_INET6 -> { let fam = current.ai_family as Int let proto = current.ai_protocol as Int let typ = current.ai_socktype as Int let addr = current.ai_addr as Pointer[SockAddrIn6] let port = (addr.sin6_port as Int).swap_bytes >>> 48 let ip = IpAddress.v6( addr.sin6_addr.0 as Int, addr.sin6_addr.1 as Int, addr.sin6_addr.2 as Int, addr.sin6_addr.3 as Int, addr.sin6_addr.4 as Int, addr.sin6_addr.5 as Int, addr.sin6_addr.6 as Int, addr.sin6_addr.7 as Int, ) stdout.print( 'IPv6: ${ip}, port = ${port}, family = ${fam}, type = ${typ}, proto = ${proto}', ) } case _ -> panic('unknown socket family') } current = current.ai_next } freeaddrinfo(addr) } } ```
yorickpeterse commented 1 month ago

getaddrinfo has apparently no way to specify a timeout/deadline for the DNS operation, meaning it can (in theory) hand indefinitely. This can lead to outages such as the one described at https://www.uber.com/en-NL/blog/denial-by-dns/.

yorickpeterse commented 1 month ago

Given the hairy nature of getaddrinfo(), I'm leaning more towards a pure-Inko DNS client. This means it won't support everything out of the box (e.g. mDNS might be tricky), but it might be good enough for common cases, and wouldn't suffer from the same issues as getaddrinfo(). We'd still need a way to fetch the name servers though, which at least on macOS isn't trivial.

yorickpeterse commented 1 month ago

For Linux and FreeBSD, parsing resolv.conf is easy enough. For macOS we could probably do something like https://gist.github.com/leiless/5dddcdfbb1d578bd96c44ff727b2cc05, assuming that code is legit.

yorickpeterse commented 1 month ago

Given the complexity this likely brings, I'll un-assign this from the 0.16.0 milestone for the time being.