Skip to content

Resolve a hostname

Source · io.systemd.Resolve · systemd v257+ · Unprivileged

A tiny resolvectl query: resolve one or more names through systemd-resolved and print every address. It shows the everyday shape of an sdvarlink program — dial, call, handle typed errors — and how to fall back when the service is missing.

Run it

go run ./examples/resolve-hostname github.com systemd.io nope.invalid
Output (illustrative)
github.com               140.82.121.3
systemd.io               185.199.108.153
systemd.io               185.199.109.153
systemd.io               185.199.110.153
systemd.io               185.199.111.153
nope.invalid: no such host

The code

examples/resolve-hostname/main.go
// Command resolve-hostname resolves names through systemd-resolved's
// io.systemd.Resolve interface, like `resolvectl query`.
//
//  go run ./examples/resolve-hostname github.com systemd.io
package main

import (
    "context"
    "errors"
    "fmt"
    "log"
    "net/netip"
    "os"
    "time"

    "github.com/rossedman/sdvarlink/systemd/v257/resolve"
    "github.com/rossedman/sdvarlink/varlink"
)

func main() {
    if len(os.Args) < 2 {
        log.Fatal("usage: resolve-hostname NAME...")
    }
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    c, err := resolve.Dial(ctx)
    if errors.Is(err, varlink.ErrUnsupported) {
        log.Fatal("systemd-resolved is not running (or systemd is older than v257)")
    }
    if err != nil {
        log.Fatal(err)
    }
    defer c.Close()

    for _, name := range os.Args[1:] {
        out, err := c.ResolveHostname(ctx, resolve.ResolveHostnameIn{Name: name})
        var noRR *resolve.NoSuchResourceRecordError
        switch {
        case errors.As(err, &noRR):
            fmt.Printf("%s: no such host\n", name)
            continue
        case err != nil:
            log.Fatalf("%s: %v", name, err)
        }
        for _, a := range out.Addresses {
            fmt.Printf("%-24s %s\n", out.Name, toAddr(a.Address))
        }
    }
}

// toAddr converts resolved's byte arrays (4 or 16 ints) to an address.
func toAddr(b []int64) netip.Addr {
    raw := make([]byte, len(b))
    for i, v := range b {
        raw[i] = byte(v)
    }
    addr, _ := netip.AddrFromSlice(raw)
    return addr
}

How it works

  • resolve.Dial(ctx) connects to resolve.DefaultSocket. If resolved isn't running, or systemd predates the interface, the error matches varlink.ErrUnsupported, which the program turns into a friendly message.
  • ResolveHostname takes a ResolveHostnameIn; only Name is required. Set Family: varlink.Ptr(int64(syscall.AF_INET)) to ask for IPv4 only.
  • Typed errors. A name that doesn't exist comes back as *resolve.NoSuchResourceRecordError; errors.As (or errors.Is(err, resolve.ErrNoSuchResourceRecord)) separates it from real failures.
  • Addresses arrive as JSON arrays of byte values ([]int64), which toAddr converts to a netip.Addr.

See also