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 7e98311
Show file tree
Hide file tree
Showing 8 changed files with 252 additions and 17 deletions.
1 change: 1 addition & 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 Down
1 change: 1 addition & 0 deletions std/src/std/libc/freebsd.inko
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions std/src/std/libc/linux.inko
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions std/src/std/libc/mac.inko
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 26 additions & 0 deletions std/src/std/net/dns.inko
Original file line number Diff line number Diff line change
@@ -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)
}
}
124 changes: 124 additions & 0 deletions std/src/std/sys/linux/dns.inko
Original file line number Diff line number Diff line change
@@ -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
}
}
71 changes: 71 additions & 0 deletions std/src/std/sys/unix/dns.inko
Original file line number Diff line number Diff line change
@@ -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
}
44 changes: 27 additions & 17 deletions std/src/std/sys/unix/net.inko
Original file line number Diff line number Diff line change
Expand Up @@ -200,35 +200,45 @@ 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) {
match address.ss_family as Int {
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)
}
Expand Down

0 comments on commit 7e98311

Please sign in to comment.