Skip to content

Commit

Permalink
WIP: add std.net.dns for DNS lookups
Browse files Browse the repository at this point in the history
This fixes #735.

Changelog: added
  • Loading branch information
yorickpeterse committed Dec 30, 2024
1 parent 505a78e commit bd8108f
Show file tree
Hide file tree
Showing 9 changed files with 356 additions and 17 deletions.
13 changes: 13 additions & 0 deletions std/src/std/libc.inko
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -25,6 +26,9 @@ let EACCES = sys.EACCES
let EADDRINUSE = sys.EADDRINUSE
let EADDRNOTAVAIL = sys.EADDRNOTAVAIL
let EAGAIN = sys.EAGAIN
let EAI_NONAME = sys.EAI_NONAME
let EAI_SERVICE = sys.EAI_SERVICE
let EAI_SYSTEM = sys.EAI_SYSTEM
let EBADF = sys.EBADF
let EBUSY = sys.EBUSY
let ECONNABORTED = sys.ECONNABORTED
Expand Down Expand Up @@ -324,6 +328,15 @@ fn extern gmtime_r(time: Pointer[Int64], result: Pointer[Tm]) -> Pointer[Tm]

fn extern localtime_r(time: Pointer[Int64], result: Pointer[Tm]) -> Pointer[Tm]

fn extern getaddrinfo(
node: Pointer[UInt8],
service: Pointer[UInt8],
hints: Pointer[sys.AddrInfo],
res: Pointer[sys.AddrInfo],
) -> Int32

fn extern freeaddrinfo(addr: Pointer[sys.AddrInfo])

# Returns the type of a directory entry.
fn inline dirent_type(pointer: Pointer[sys.Dirent]) -> Int {
sys.dirent_type(pointer)
Expand Down
15 changes: 15 additions & 0 deletions std/src/std/libc/freebsd.inko
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import std.io (Error)
let AF_INET = 2
let AF_INET6 = 28
let AF_UNIX = 1
let AF_UNSPEC = 0
let CLOCK_REALTIME = 0
let DT_DIR = 4
let DT_LNK = 10
Expand All @@ -11,6 +12,9 @@ let EACCES = 13
let EADDRINUSE = 48
let EADDRNOTAVAIL = 49
let EAGAIN = 35
let EAI_NONAME = 8
let EAI_SERVICE = 9
let EAI_SYSTEM = 11
let EBADF = 9
let EBUSY = 16
let ECONNABORTED = 53
Expand Down Expand Up @@ -325,6 +329,17 @@ type extern SockAddrStorage {
let @__val14: UInt64
}

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_canonname: Pointer[UInt8]
let @ai_addr: Pointer[SockAddr]
let @ai_next: Pointer[AddrInfo]
}

fn extern fchmod(fd: Int32, mode: UInt16) -> Int32

fn extern fstat(fd: Int32, buf: Pointer[StatBuf]) -> Int32
Expand Down
15 changes: 15 additions & 0 deletions std/src/std/libc/linux.inko
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import std.libc.linux.arm64 (self as arch) if arm64
let AF_INET = 2
let AF_INET6 = 10
let AF_UNIX = 1
let AF_UNSPEC = 0
let AT_EMPTY_PATH = 0x1000
let AT_FDCWD = -0x64
let CLOCK_REALTIME = 0
Expand All @@ -15,6 +16,9 @@ let EACCES = 13
let EADDRINUSE = 98
let EADDRNOTAVAIL = 99
let EAGAIN = 11
let EAI_NONAME = -2
let EAI_SERVICE = -8
let EAI_SYSTEM = -11
let EBADF = 9
let EBUSY = 16
let ECONNABORTED = 103
Expand Down Expand Up @@ -384,6 +388,17 @@ type extern SockAddrStorage {
let @__val14: UInt64
}

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[SockAddr]
let @ai_canonname: Pointer[UInt8]
let @ai_next: Pointer[AddrInfo]
}

fn extern fchmod(fd: Int32, mode: UInt16) -> Int32

fn extern syscall(number: Int32, ...) -> Int32
Expand Down
15 changes: 15 additions & 0 deletions std/src/std/libc/mac.inko
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import std.libc.mac.arm64 (self as sys) if arm64
let AF_INET = 2
let AF_INET6 = 30
let AF_UNIX = 1
let AF_UNSPEC = 0
let AT_FDCWD = -2
let CLOCK_REALTIME = 0
let COPYFILE_ALL = 15
Expand All @@ -15,6 +16,9 @@ let EACCES = 13
let EADDRINUSE = 48
let EADDRNOTAVAIL = 49
let EAGAIN = 35
let EAI_NONAME = 8
let EAI_SERVICE = 9
let EAI_SYSTEM = 11
let EBADF = 9
let EBUSY = 16
let ECONNABORTED = 53
Expand Down Expand Up @@ -321,6 +325,17 @@ type extern SockAddrStorage {
let @__val14: UInt64
}

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_canonname: Pointer[UInt8]
let @ai_addr: Pointer[SockAddr]
let @ai_next: Pointer[AddrInfo]
}

fn extern chmod(path: Pointer[UInt8], mode: UInt16) -> Int32

fn extern fsync(fd: Int32) -> Int32
Expand Down
44 changes: 44 additions & 0 deletions std/src/std/net/dns.inko
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
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
import std.time (ToInstant)

# A type that can resolve DNS queries, such as resolving a hostname into a list
# of IP addresses.
trait Resolve {
# Sets the point in time after which IO operations must time out.
fn pub mut timeout_after=[T: ToInstant](deadline: ref T)

# Resets the deadline stored in `self`.
fn pub mut reset_deadline

# Resolves the given hostname into an array of IP addresses.
#
# If the underlying IO operations fail or time out, an `Error` is returned.
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 timeout_after=[T: ToInstant](deadline: ref T) {
@inner.timeout_after = deadline
}

fn pub mut reset_deadline {
@inner.reset_deadline
}

fn pub mut resolve(host: String) -> Result[Array[IpAddress], Error] {
@inner.resolve(host)
}
}
137 changes: 137 additions & 0 deletions std/src/std/sys/linux/dns.inko
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
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)
import std.time (ToInstant)

let RESOLVE_HOST = 'io.systemd.Resolve.ResolveHostname'
let VARLINK_SOCKET = '/run/systemd/resolve/io.systemd.Resolve'
let READ_SIZE = 8 * 1024
let NX_DOMAIN = 3

# 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) -> Result[Array[IpAddress], Error] {
let params = json.query.key('parameters')

if json.query.key('error').as_string.some? {
match params.key('rcode').as_int {
case Some(NX_DOMAIN) or None -> throw Error.NotFound
case Some(e) -> throw Error.Other(e)
}
}

params.key('addresses').as_array.get.iter.try_reduce([], fn (vals, val) {
let fam = try val.query.key('family').as_int.ok_or(Error.InvalidData)
let capa = if fam == libc.AF_INET6 { 16 } else { 4 }
let addr = try val
.query
.key('address')
.as_array
.ok_or(Error.InvalidData)
.then(fn (nums) {
nums.iter.try_reduce(ByteArray.with_capacity(capa), fn (bytes, num) {
try num.query.as_int.ok_or(Error.InvalidData).map(fn (v) {
bytes.push(v)
})
Result.Ok(bytes)
})
})

let ptr = addr.to_pointer
let ip = match fam {
case libc.AF_INET if addr.size == 4 -> {
net.parse_v4_address(ptr as Pointer[UInt8])
}
case libc.AF_INET6 if addr.size == 16 -> {
net.parse_v6_address(ptr as Pointer[UInt16])
}
case _ -> throw Error.InvalidData
}

vals.push(ip)
Result.Ok(vals)
})
}

# A resolver that uses systemd-resolve's through its
# [varlink](https://varlink.org/) API.
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 timeout_after=[T: ToInstant](deadline: ref T) {
@socket.socket.timeout_after = deadline
}

fn pub mut reset_deadline {
@socket.socket.reset_deadline
}

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 _ -> {}
}
}

# The JSON itself should always be valid, so we don't need to handle parsing
# errors in a better way.
let res = Json
.parse(Buffer.new(@buffer))
.map_error(fn (_) { Error.InvalidData })
.then(fn (v) { parse_resolve_host_response(v) })

@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.exists? {
match SystemdResolver.new {
case Some(r) -> return r as Resolve
case _ -> {}
}
}

system_resolver
}
70 changes: 70 additions & 0 deletions std/src/std/sys/unix/dns.inko
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import std.io (Error, start_blocking, stop_blocking)
import std.libc
import std.libc.freebsd (self as sys_libc) if freebsd
import std.libc.linux (self as sys_libc) if linux
import std.libc.mac (self as sys_libc) if mac
import std.net.dns (Resolve)
import std.net.ip (IpAddress)
import std.sys.unix.net (self as sys_net) if unix
import std.time (ToInstant)

# A resolver that uses `getaddrinfo()`.
type Resolver {
fn inline static new -> Resolver {
Resolver()
}
}

impl Resolve for Resolver {
fn pub mut timeout_after=[T: ToInstant](deadline: ref T) {
# getaddrinfo() doesn't support timeouts.
}

fn pub mut reset_deadline {
# getaddrinfo() doesn't support timeouts.
}

fn pub mut resolve(host: String) -> Result[Array[IpAddress], Error] {
let hints = sys_libc.AddrInfo(
ai_flags: 0 as Int32,
ai_family: 0 as Int32,
ai_socktype: libc.SOCK_STREAM as Int32,
ai_protocol: 0 as Int32,
ai_addrlen: 0 as UInt64,
ai_addr: 0x0 as Pointer[sys_libc.SockAddr],
ai_canonname: 0x0 as Pointer[UInt8],
ai_next: 0x0 as Pointer[sys_libc.AddrInfo],
)
let list = 0x0 as Pointer[sys_libc.AddrInfo]

start_blocking

let ptr = host.to_pointer
let res = libc.getaddrinfo(ptr, ''.to_pointer, mut hints, mut list)
let errno = stop_blocking

match res as Int {
case 0 -> {}
case libc.EAI_NONAME or libc.EAI_SERVICE -> throw Error.NotFound
case libc.EAI_SYSTEM -> throw Error.from_os_error(errno)
case e -> throw Error.Other(e)
}

let mut cur = list
let ips = []

while cur as Int != 0 {
let addr_ptr = cur.ai_addr as Pointer[sys_libc.SockAddrStorage]

ips.push(sys_net.parse_ip_socket_address(addr_ptr).0)
cur = cur.ai_next
}

libc.freeaddrinfo(list)
Result.Ok(ips)
}
}

fn inline resolver -> Resolve {
Resolver.new as Resolve
}
Loading

0 comments on commit bd8108f

Please sign in to comment.