Skip to content

Commit

Permalink
Add std.net.dns for performing DNS queries
Browse files Browse the repository at this point in the history
This adds the std.net.dns module providing the Resolver type for
performing DNS queries. Currently the only type of query supported is
resolving a hostname into a list of IP addresses, but this may be
expanded upon in the future.

For FreeBSD and macOS, the implementation uses getaddrinfo(). For Linux,
if systemd-resolve is installed its varlink API is used with a fallback
to getaddrinfo(). The benefit of using the varlink API is that it allows
us to make use of Inko's non-blocking sockets.

This fixes #735.

Changelog: added
  • Loading branch information
yorickpeterse committed Jan 1, 2025
1 parent 505a78e commit ca243a5
Show file tree
Hide file tree
Showing 11 changed files with 760 additions and 18 deletions.
18 changes: 18 additions & 0 deletions std/src/std/libc.inko
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ 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 AI_ADDRCONFIG = sys.AI_ADDRCONFIG
let AI_V4MAPPED = sys.AI_V4MAPPED
let CLOCK_REALTIME = sys.CLOCK_REALTIME
let DT_DIR = sys.DT_DIR
let DT_LNK = sys.DT_LNK
Expand All @@ -25,6 +28,12 @@ let EACCES = sys.EACCES
let EADDRINUSE = sys.EADDRINUSE
let EADDRNOTAVAIL = sys.EADDRNOTAVAIL
let EAGAIN = sys.EAGAIN
let EAI_ADDRFAMILY = sys.EAI_ADDRFAMILY
let EAI_AGAIN = sys.EAI_AGAIN
let EAI_FAIL = sys.EAI_FAIL
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 +333,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
20 changes: 20 additions & 0 deletions std/src/std/libc/freebsd.inko
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import std.io (Error)
let AF_INET = 2
let AF_INET6 = 28
let AF_UNIX = 1
let AF_UNSPEC = 0
let AI_ADDRCONFIG = 0x400
let AI_V4MAPPED = 0x800
let CLOCK_REALTIME = 0
let DT_DIR = 4
let DT_LNK = 10
Expand All @@ -11,6 +14,12 @@ let EACCES = 13
let EADDRINUSE = 48
let EADDRNOTAVAIL = 49
let EAGAIN = 35
let EAI_ADDRFAMILY = 1
let EAI_AGAIN = 2
let EAI_FAIL = 4
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 +334,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
20 changes: 20 additions & 0 deletions std/src/std/libc/linux.inko
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ 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 AI_ADDRCONFIG = 0x20
let AI_V4MAPPED = 0x8
let AT_EMPTY_PATH = 0x1000
let AT_FDCWD = -0x64
let CLOCK_REALTIME = 0
Expand All @@ -15,6 +18,12 @@ let EACCES = 13
let EADDRINUSE = 98
let EADDRNOTAVAIL = 99
let EAGAIN = 11
let EAI_ADDRFAMILY = -9
let EAI_AGAIN = -3
let EAI_FAIL = -4
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 +393,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
20 changes: 20 additions & 0 deletions std/src/std/libc/mac.inko
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ 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 AI_ADDRCONFIG = 0x400
let AI_V4MAPPED = 0x800
let AT_FDCWD = -2
let CLOCK_REALTIME = 0
let COPYFILE_ALL = 15
Expand All @@ -15,6 +18,12 @@ let EACCES = 13
let EADDRINUSE = 48
let EADDRNOTAVAIL = 49
let EAGAIN = 35
let EAI_ADDRFAMILY = 1
let EAI_AGAIN = 2
let EAI_FAIL = 4
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 +330,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
194 changes: 194 additions & 0 deletions std/src/std/net/dns.inko
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
# Types for performing DNS queries.
#
# This module provides a `Resolver` type that is used for performing DNS
# queries, such as resolving a hostname to a list of IP addresses. For more
# information, refer to the documentation of the `Resolver` type.
import std.cmp (Equal)
import std.fmt (Format, Formatter)
import std.io
import std.net.ip (IpAddresses)
import std.string (ToString)
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)

# An error produced as part of a DNS query.
type pub inline enum Error {
# A hostname can't be resolved (NXDomain).
case InvalidHost

# The DNS server returned an error (e.g. ServFail) or produced an invalid
# response (e.g. systemd-resolve returning bogus data).
case ServerError

# Any other kind of error, such as a network timeout.
case Other(io.Error)
}

impl ToString for Error {
fn pub to_string -> String {
match self {
case InvalidHost -> "the hostname can't be resolved"
case ServerError -> 'the DNS server returned an error'
case Other(e) -> e.to_string
}
}
}

impl Format for Error {
fn pub fmt(formatter: mut Formatter) {
match self {
case InvalidHost -> formatter.tuple('InvalidHost').finish
case ServerError -> formatter.tuple('ServerError').finish
case Other(e) -> formatter.tuple('Other').field(e).finish
}
}
}

impl Equal[ref Error] for Error {
fn pub ==(other: ref Error) -> Bool {
match (self, other) {
case (InvalidHost, InvalidHost) -> true
case (ServerError, ServerError) -> true
case (Other(a), Other(b)) -> a == b
case _ -> false
}
}
}

# 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.
#
# Depending on the implementation of the resolver, the deadline might be
# ignored.
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 a list of IP addresses.
#
# Upon success an `std.net.ip.IpAddresses` is returned, storing the IP
# addresses in the order in which they should be used based on the underlying
# platform's preferences.
#
# # Errors
#
# If the host doesn't resolve to anything, an `Error.InvalidHost` error is
# returned.
#
# If the DNS server produced some sort of internal error (e.g. it's
# overloaded), an `Error.ServerError` is returned.
#
# For any other kind of error (e.g. a timeout), a `Error.Other` is returned
# that wraps an `std.io.Error` value.
#
# # Examples
#
# Resolving a hostname and using it to connect a socket:
#
# ```inko
# import std.net.dns (Resolver)
# import std.net.socket (TcpClient)
#
# let dns = Resolver.new
# let ips = dns.resolve('example.com').or_panic('DNS lookup failed')
#
# ips
# .try(fn (ip) { TcpClient.new(ip, port: 80) })
# .or_panic('failed to connect')
# ```
fn pub mut resolve(host: String) -> Result[IpAddresses, Error]
}

# A type for performing DNS queries.
#
# # Backends
#
# The `Resolver` type uses a different backend/implementation depending on the
# underlying platform. These are as follows:
#
# |=
# | Platform
# | Backend
# | Fallback
# |-
# | FreeBSD
# | `getaddrinfo()`
# | None
# |-
# | macOS
# | `getaddrinfo()`
# | None
# |-
# | Linux
# | systemd-resolve (using its [varlink](https://varlink.org/) API)
# | `getaddrinfo()`
#
# Due to the blocking nature of `getaddrinfo()`, it's possible for a call to
# this function to block the OS thread for a long period of time. While such
# threads are marked as blocking and are replaced with a backup thread whenever
# necessary, the number of available threads is fixed and thus it's possible to
# exhaust all these threads by performing many slow DNS queries.
#
# In contrast, the systemd-resolve backend is able to take advantage of Inko's
# non-blocking network IO and thus doesn't suffer from the same problem. For
# this reason it's _highly_ recommended to ensure systemd-resolve is available
# when deploying to a Linux environment. If systemd-resolve isn't available, the
# Linux implementation falls back to using `getaddrinfo()`.
#
# When using the `getaddrinfo()` backend, the following `ai_flags` flags are
# set:
#
# - `AI_ADDRCONFIG`
# - `AI_V4MAPPED`
#
# These flags are set to ensure consistent behaviour across platforms and libc
# implementations.
#
# # Deadlines
#
# A `Resolver` supports setting a deadline using `Resolver.timeout_after=`.
# However, this timeout might be ignored based on the backend in use. Most
# notably, `getaddrinfo()` backends don't support timeouts/deadlines and instead
# use a system-wide timeout, ignoring any deadlines set using
# `Resolver.timeout_after=`.
#
# # Examples
#
# Resolving a hostname and using it to connect a socket:
#
# ```inko
# import std.net.dns (Resolver)
# import std.net.socket (TcpClient)
#
# let dns = Resolver.new
# let ips = dns.resolve('example.com').or_panic('DNS lookup failed')
#
# ips.try(fn (ip) { TcpClient.new(ip, port: 80) }).or_panic('failed to connect')
# ```
type pub inline Resolver {
let @inner: Resolve

# Returns a new `Resolver`.
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[IpAddresses, Error] {
@inner.resolve(host)
}
}
Loading

0 comments on commit ca243a5

Please sign in to comment.