🐔

systemd-resolved のホスト名解決をちょっと調べてみた

2025/01/09に公開

すべてが S になる

init に始まり今やすべては systemd に飲み込まれつつある。ホスト名解決も例外ではなく、systemd-resolved が使えるようになってからだいぶ経ち、標準でインストールされるディストリビューションもある。(最近は増えてきてるのか?)

しかし、ふと systemd-resolved 良く知らなんな、と言うかそもそもアプリケーションにおけるホスト名解決の方法自体良く知らんな、と言うことに気付いたので、ちょっと調べてみた。

systemd-resolved を使ったホスト名解決方法

systemd-resolved が提供するホスト名解決のためのインタフェースは man 8 systemd-resolved の最初に書いてある。曰くざっくり以下のような感じ。

  • D-Bus を通したネイティブな全部入りインターフェイス。非同期でも使えるし全機能つかえるからこれを使うのが推奨だよ。
  • glibc の getaddrinfo(3) とかその関連の gethostbyname(3) とかのインタフェース。Linux を超えて広く使われてるけと DNSSEC validation status 情報を公開してないし(ここよくわかってない)、非同期では使えないよ。
  • ローカル DNS スタブリスナインタフェース。resolv.conf(5) を読んで直接 DNS プロトコルを喋るプログラムのためのものだけど、リンクローカルアドレスとか LLMNR とかみたいないろんなものに対応できないので上 2 つのいずれかを使うことを強くお勧めするよ。

と言うことで、それぞれ軽~く覗いてみる。

D-Bus インタフェース

D-Bus が推奨だよ、と言われてもそもそも D-Bus って何?状態だったのだが、とりあえずサンプルコードがあったので読んでみた。

https://wiki.freedesktop.org/www/Software/systemd/writing-resolver-clients/#resolvingahostname

いやいや、たかだかホスト名解決するためだけにこれ書かなきゃいけないのおかしいやろ。もうちょっと使いやすいライブラリとか無いんか?と思ったが軽く調べたところでは無さそうだった。これ絶対使わせる気ないやろ…

各種コマンド

まぁ、気を取り直して更にいろいろ調べたところ、とりあえず以下のようなコマンドで試せることは分かった。(ここまで調べるのに 5 万年かかった…)

busctl(1) を使った場合
$ busctl call --json=short \
              org.freedesktop.resolve1 /org/freedesktop/resolve1 \
              org.freedesktop.resolve1.Manager ResolveHostname isit \
  	      0 www.google.com 0 0
# 結果
{"type":"a(iiay)st","data":[[[2,2,[172,217,26,228]],[2,10,[36,4,104,0,64,4,8,16,0,0,0,0,0,0,32,4]]],"www.google.com",1048577]}
gdbus(1) を使った場合
$ gdbus call -y \
             -d org.freedesktop.resolve1 -o /org/freedesktop/resolve1 \
             -m org.freedesktop.resolve1.Manager.ResolveHostname \
	     0 www.google.com 0 0
# 結果
([(2, 2, [byte 0xac, 0xd9, 0xaf, 0x44]), (2, 10, [0x24, 0x04, 0x68, 0x00, 0x40, 0x04, 0x08, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x04])], 'www.google.com', uint64 1048577)
dbus-send(1) を使った場合
$ dbus-send --system --print-reply \
            --dest=org.freedesktop.resolve1 /org/freedesktop/resolve1 \
	    org.freedesktop.resolve1.Manager.ResolveHostname \
	    int32:0 string:www.google.com int32:0 uint64:0
# 結果
method return time=1706544632.643961 sender=:1.0 -> destination=:1.20 serial=17 reply_serial=2
   array [
      struct {
         int32 2
         int32 10
         array of bytes [
            24 04 68 00 40 04 08 22 00 00 00 00 00 00 20 04
         ]
      }
      struct {
         int32 2
         int32 2
         array of bytes [
            ac d9 af 44
         ]
      }
   ]
   string "www.google.com"
   uint64 1048577

このサンプルの引数はどれも 0 www.google.com 0 0 だが、意味はそれぞれ以下の通り。

  • 0:ネットワークインタフェースの番号(ip a とかで出るヤツ)だが、普通は 0(グローバル)で良い。リンクローカルアドレスとかのためのものらしい。(良く分かってない…)
  • www.google.com:解決するホスト名。これが本命。
  • 0:アドレスファミリ。0 だと何でも。2AF_INET)だと IPv4、10AF_INET6)だと IPv6。
  • 0:フラグ。普通は 0 でいいが、キャッシュ使うな(0x1000)とかネットワーク使うな(0x8000)とか指定できる。詳しくはこちら

また、戻り値は dbus-send(1)[1] の結果が多少は見やすいかもしれない。(busctl(1) はデフォルトの出力形式が絶望的に見づらかったので --json=short を付けてしまった…)

  • 最初の配列(array)が解決結果のアドレス。各要素は以下の 3 つのフィールドからなる。
    • 最初はネットワークインタフェースの番号。引数と同じ。
    • 2 番目はアドレスファミリ。これも引数と同じ。
    • 最後はアドレス。バイト配列になってるのでちと(だいぶ?)見づらい。
  • 2 番目の文字列は canonical name、つまり CNAME、ホントの名前だ。検索したホスト名が CNAME レコードに当たった場合とかに本来の名前が返ってくる。
  • 最後はフラグ。引数と同じなので詳しくはこちら

ResolveHostname とかのメソッドの詳細はこちらに書いてあった。

ちなみに busctl(1) のコマンドライン引数の isit や結果の a(iiay)st と言う呪文は引数や戻り値の型を表す文字である。iint32tuint64ybytesstringa は配列で要素型は続く文字で表される、() は構造体で各フィールドはカッコ内の文字で表される。くわしくはこちら

C 言語

次はサンプルを基に気張って C 言語でコードを書いてみる。

dbus.c
#include <arpa/inet.h>
#include <stdio.h>
#include <systemd/sd-bus.h>

int main(int argc, char **argv)
{
    int r;

    __attribute__((cleanup(sd_bus_flush_close_unrefp)))
    sd_bus *bus = NULL;
    r = sd_bus_open_system(&bus);
    if (r < 0) {
        fprintf(stderr, "Failed to open system bus: %s\n", strerror(-r));
        return 1;
    }

    for (int i = 1; i < argc; ++i) {
        puts(argv[i]);

        __attribute__((cleanup(sd_bus_message_unrefp)))
        sd_bus_message *reply = NULL;
        __attribute__((cleanup(sd_bus_error_free)))
        sd_bus_error error = SD_BUS_ERROR_NULL;
        r = sd_bus_call_method(bus,
            "org.freedesktop.resolve1", "/org/freedesktop/resolve1",
            "org.freedesktop.resolve1.Manager", "ResolveHostname",
            &error, &reply, "isit",
            0, argv[i], AF_UNSPEC, UINT64_C(0));
        if (r < 0) {
            fprintf(stderr, "Failed to resolve hostnme: %s\n", error.message);
            continue;
        }

        r = sd_bus_message_enter_container(reply, 'a', "(iiay)");
        if (r < 0) {
            goto parse_failure;
        }

        for (;;) {
            r = sd_bus_message_enter_container(reply, 'r', "iiay");
            if (r < 0) {
                goto parse_failure;
            }
            if (r == 0) {
                break;
            }

            int32_t ifindex, family;
            r = sd_bus_message_read(reply, "ii", &ifindex, &family);
            if (r < 0) {
                goto parse_failure;
            }

            const void *data;
            size_t length;
            r = sd_bus_message_read_array(reply, 'y', &data, &length);
            if (r < 0) {
                goto parse_failure;
            }

            r = sd_bus_message_exit_container(reply);
            if (r < 0) {
                goto parse_failure;
            }

            char buf[INET6_ADDRSTRLEN];
            printf("%d %s\n", ifindex, inet_ntop(family, data, buf, sizeof(buf)));
        }

        r = sd_bus_message_exit_container(reply);
        if (r < 0) {
            goto parse_failure;
        }

        const char *canonical;
        uint64_t flags;
        r = sd_bus_message_read(reply, "st", &canonical, &flags);
        if (r < 0) {
            goto parse_failure;
        }

        printf("canonname: %s\n", canonical);
        printf("flags: %#lx\n", flags);
        continue;

parse_failure:
        fprintf(stderr, "Parse failure: %s\n", strerror(-r));
    }
}

な、長い、やっぱり使わせる気無いよな…
そもそも DBus へのアクセスを素で書くのは厳しすぎるので libsystemd とか言うライブラリ使ってる(サンプルも使ってた)上に、更に GCC 拡張の cleanup 属性まで使ってるんだが…
てか cleanup 属性初めて知った、これいいな…

Go 言語

やはり時代は Go、と言うことで Go でも書いてみる。

dbus.go
package main

import (
    "fmt"
    "net"
    "os"

    "github.com/godbus/dbus/v5"
)

type AddressInfo struct {
    Ifindex int32
    Family  int32
    IP      net.IP
}

func main() {
    conn, err := dbus.ConnectSystemBus()
    if err != nil {
        fmt.Printf("ConnectSystemBus error:%v\n", err)
        os.Exit(1)
    }
    defer conn.Close()

    obj := conn.Object("org.freedesktop.resolve1", "/org/freedesktop/resolve1")

    for _, host := range os.Args[1:] {
        fmt.Println(host)
        call := obj.Call("org.freedesktop.resolve1.Manager.ResolveHostname", 0,
            int32(0), host, int32(0), uint64(0))

        var addrs []AddressInfo
        var canonical string
        var flags uint64
        if err := call.Store(&addrs, &canonical, &flags); err != nil {
            fmt.Printf("Store error: %v\n", err)
            continue
        }

        fmt.Printf("canonname: %s\n", canonical)
        fmt.Printf("flags: %#x\n", flags)
        for _, addr := range addrs {
            fmt.Printf("%d %d %s\n", addr.Ifindex, addr.Family, addr.IP)
        }
    }
}

これもちょっと(だいぶ?)ズルして DBus のやりとりのために github.com/godbus/dbus なるモジュールを使ってしまったが、これならまぁ DBus 経由で呼び出してもいいかもしれない。やっぱり Go はラクだな。
でも github.com/godbus/dbus は標準モジュールじゃないので、DBus での名前解決自体が Golang の標準パッケージから使えるようになるのは厳しそうかな…

一応結果も。

$ ./dbus www.google.com www.yahoo.com
# 結果
www.google.com
canonname: www.google.com
flags: 0x100001
2 10 2404:6800:4004:822::2004
2 2 142.251.42.132
www.yahoo.com
canonname: me-ycpi-cf-www.g06.yahoodns.net
flags: 0x100001
2 10 2406:2000:a4:807::
2 10 2406:2000:a4:807::1
2 2 180.222.119.247
2 2 180.222.119.248

うん、ちゃんと動いてるっぽい。

DBus インタフェースは厳しめ…

とりあえず呼び出せはしたが、C 言語のプログラムからこのインタフェースを呼び出すのは大変厳しそうだというのは分かった。Golang で github.com/godbus/dbus 使ってもいいならまぁ使っても悪くはないかな、と言った感じだ。

glibc のインタフェース

このインタフェースはお馴染みの古き良き gethostbyname(3) とその新しめの仲間(?)の getaddrinfo(3) なので、まぁ大丈夫っしょ、とか考えてたら甘かった…

てか gethostbyname(3) とかって deprecated になってるじゃね~か、知らんかった…orz

C 言語

とりあえず IPv6 も取れるように getaddrinfo(3) で書いてみる。

addrinfo.c
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <stdio.h>

int main(int argc, char const **argv)
{
    const struct addrinfo hints = {
        .ai_socktype = SOCK_RAW,
        .ai_flags = AI_CANONNAME,
    };

    for (int i = 1; i < argc; ++i) {
        struct addrinfo *res;
        int ret = getaddrinfo(argv[i], NULL, &hints, &res);
        if (ret != 0) {
            printf("getaddrinfo error: %s\n", gai_strerror(ret));
            continue;
        }

        printf("%s\ncanonname: %s\n", argv[i], res->ai_canonname);
        for (struct addrinfo *tmp = res; tmp != NULL; tmp = tmp->ai_next) {
            char host[INET6_ADDRSTRLEN];
            int ret = getnameinfo(tmp->ai_addr, tmp->ai_addrlen, host, \
	                          sizeof(host), NULL, 0, NI_NUMERICHOST);
            if (ret != 0) {
                printf("getnameinfo error: %s\n", gai_strerror(ret));
                continue;
            }
            printf("%d %s\n", tmp->ai_family, host);

        }

        freeaddrinfo(res);
    }
}

アドレスを文字列化するのに最初 inet_ntop(3) を使おうとしたが IPv4 と IPv6 両対応にするには微妙に使いづらかったので、両対応が簡単な getnameinfo(3) を使ってしまった。getaddrinfo(3) の結果に getnameinfo(3) を使うのはどうなのよと思わなくもないが、これが一番簡単そうだったのでしょうがない。

てか inet_ntoa(3) も deprecated かよ、完全に時代に取り残されてるな…orz

気を取り直して試しに実行してみる。

$ ./addrinfo www.google.com www.yahoo.com
# 結果
www.google.com
canonname: www.google.com
2 142.250.196.132
10 2404:6800:4004:811::2004
www.yahoo.com
canonname: me-ycpi-cf-www.g06.yahoodns.net
2 180.222.119.248
2 180.222.119.247
10 2406:2000:a4:807::
10 2406:2000:a4:807::1

正しく動いているようだ。
が、これに strace(1) を使ってみたところ、以下のような出力(抜粋)となった。

...
socket(AF_INET, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_IP) = 3
connect(3, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("127.0.0.53")}, 16) = 0
sendmmsg(3, [{msg_hdr={msg_name=NULL, msg_namelen=0, msg_iov=[{iov_base="\273%\1 \0\1\0\0\0\0\0\1\3www\6google\3com\0\0\1\0\1"..., iov_len=43}], msg_iovlen=1, msg_controllen=0, msg_flags=0}, msg_len=43}, {msg_hdr={msg_name=NULL, msg_namelen=0, msg_iov=[{iov_base="\326.\1 \0\1\0\0\0\0\0\1\3www\6google\3com\0\0\34\0\1"..., iov_len=43}], msg_iovlen=1, msg_controllen=0, msg_flags=0}, msg_len=43}], 2, MSG_NOSIGNAL) = 2
recvfrom(3, "\326.\201\200\0\1\0\1\0\0\0\1\3www\6google\3com\0\0\34\0\1"..., 2048, 0, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("127.0.0.53")}, [28 => 16]) = 71
recvfrom(3, "\273%\201\200\0\1\0\1\0\0\0\1\3www\6google\3com\0\0\1\0\1"..., 65536, 0, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("127.0.0.53")}, [28 => 16]) = 59
close(3)                                = 0
...

え、てっきり DBus で問い合わせてるんかと思ってたのに、これ普通に DNS で問い合わせしてるだけやん…(127.0.0.53systemd-resolved のローカルスタブリスナアドレス)

man はちゃんと読め

と思ったら、ちゃんと man 8 systemd-resolved に書いてあった。

glibc でホスト名解決する時に systemd-resolved 使うんなら nss-resolve(8) 入れてね。

マジか、今まで知らないでそのまま使ってた…orz
てか Ubuntu さんデフォルトで systemd-resolved インストールされてるんだから nss-resolve も入れといてくれてもいいんじゃないの?と思わなくもない。入れとくと何か困ることでもあるんかな…

と言うことで、sudo apt install libnss-resolve すると(Ubuntu のパッケージ名は nss-resolve じゃなくて libnss-resolve だった)、nsswitch.conf(5)resolve とか言うヤツが生えた。なるほどね。

/etc/nsswitch.conf
...
hosts:          files resolve [!UNAVAIL=return] dns
...

ところで man 8 nss-resolve 見ると nss-resolvefiles より前がいいよって書いてあるんだけど、なんで後なんだろう。あと、フォールバック用に後ろに dns 書いた方がいいとも書いてあるんだけど、/etc/resolv.conf がスタブモードだと結局 systemd-resolved 向いちゃうからフォールバックにならなくない?

まぁいいや。dpkg -L libnss-resolve で見てみると、実体は /usr/lib/x86_64-linux-gnu/libnss_resolve.so.2 のようだ。

$ dpkg -L libnss-resolve
# 結果
/.
/usr
/usr/lib
/usr/lib/x86_64-linux-gnu
/usr/lib/x86_64-linux-gnu/libnss_resolve.so.2
/usr/share
/usr/share/doc
/usr/share/doc/libnss-resolve
/usr/share/doc/libnss-resolve/copyright
/usr/share/lintian
/usr/share/lintian/overrides
/usr/share/lintian/overrides/libnss-resolve
/usr/share/man
/usr/share/man/man8
/usr/share/man/man8/nss-resolve.8.gz
/usr/share/doc/libnss-resolve/NEWS.Debian.gz
/usr/share/doc/libnss-resolve/changelog.Debian.gz
/usr/share/man/man8/libnss_resolve.so.2.8.gz

nsswitch.conf(5) に書かれているように、SERVICE って言う解決法は libnss_SERVICE.so.X って言う名前の共有ライブラリで提供されるようなので、辻褄はあってるな。が、ちょっと覗いてみたら libnss_files.so.2libnss_dns.so.2 は有用なシンボル何も定義されてなくて、コイツらは実はダミーで実体は libc.so.6 にあるらしい。なぜだ…

あと、/usr/share/lintian/overrides って何?と思ったら lintian(1) って言うパッケージチェッカ的なモノがあるのね、知らんかった…(またすぐ忘れそう…)

ではもう一度動かす。

...
socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0) = 3
connect(3, {sa_family=AF_UNIX, sun_path="/run/systemd/resolve/io.systemd.Resolve"}, 42) = 0
sendto(3, "{\"method\":\"io.systemd.Resolve.Re"..., 97, MSG_DONTWAIT|MSG_NOSIGNAL, NULL, 0) = 97
recvfrom(3, 0x7fc72582d010, 135152, MSG_DONTWAIT, NULL, NULL) = -1 EAGAIN (Resource temporarily unavailable)
recvfrom(3, "{\"parameters\":{\"addresses\":[{\"if"..., 135152, MSG_DONTWAIT, NULL, NULL) = 198
close(3)                                = 0
...

あるぇ~?何かやっぱり DBus じゃないんだけど…
Unix Domain Socket のパス名は /run/systemd/resolve/io.systemd.Resolve とかなってるので明らかに systemd-resolved に行ってる感じではあるけど(この名前で騙し討ちとか無いよね?)、リクエストも何となく JSON っぽいし、コイツは一体何者なんだ?

仕方ないので glibc とか systemd-resolved のソースを grep してみると、それっぽいのが出てきた。曰く Varlink なるインタフェースを使ってやりとりしているらしい。

なんぞそれ?と systemd-resolved のドキュメントを軽く調べてみるもドキュメント化はされてなさそう。なさそうではあるんだが、どうもソースを見る限り DBus で提供されている org.freedesktop.resolve1.Manager インタフェースのメソッドのうち ResolveHostnameResolveAddress がそのまま公開されているっぽい。そのままって言っても JSON だけど。

つまり、man 8 systemd-resolved の最初に書かれている 3 つのインタフェースのうち 2 番目の glibc のインタフェース使った場合には、1 番目の DBus インターフェイスとも 3 番目のローカル DNS スタブリスナインタフェースとも異なるインターフェイスで systemd-resolved とやりとりしてるって事だ。なるほどね…

まぁ一応 systemd-resolved に問い合わせることができたので許してやるか。

Go 言語

次にこれも Go でも書いてみる。

addrinfo.go
package main

import (
    "fmt"
    "net"
    "os"
)

func main() {
    for _, hostname := range os.Args[1:] {
        fmt.Println(hostname)
        ips, err := net.LookupIP(hostname)
        if err != nil {
            fmt.Printf("LookupIP error:%v\n", err)
            continue
        }
        for _, ip := range ips {
            fmt.Println(ip)
        }
    }
}

パッと見 getaddrinfo(3) 使ってないが、ここに説明がある通りいくつかの条件で getaddrinfo(3) が使われる。

今回の場合は libnss-resolve をインストールしたことで nsswitch.conf(5)resolve が生えていて、コイツは pure Go の実装では処理できないので cgo を使って getaddrinfo(3) を呼び出す方の実装に切り替わっている。

と言う訳で実行してみる。

./addrinfo www.google.com www.yahoo.com
www.google.com
142.251.222.4
2404:6800:4004:821::2004
www.yahoo.com
180.222.119.247
180.222.119.248
2406:2000:a4:807::
2406:2000:a4:807::1

解決はされているが、getaddrinfo(3) 使われてるか分からんな。GODEBUG でその辺の情報がログにでるらしいので付けてやってみる。

$ GODEBUG=netdns=2 ./addrinfo www.google.com www.yahoo.com
www.google.com
go package net: confVal.netCgo = false  netGo = false
go package net: dynamic selection of DNS resolver
go package net: hostLookupOrder(www.google.com) = cgo
142.251.42.132
2404:6800:4004:821::2004
www.yahoo.com
go package net: hostLookupOrder(www.yahoo.com) = cgo
180.222.119.247
180.222.119.248
2406:2000:a4:807::
2406:2000:a4:807::1

hostLookupOrder(www.google.com) = cgo と書かれているので、想定通り cgo を使って解決しているようだ。

cgo を使わないように CGO_ENABLED=0 を指定してビルドすると resolve は無視されて pure Go 実装が使われるようだ。実際試したらそうなった。

$ CGO_ENABLED=0 go build addrinfo.go
$ GODEBUG=netdns=2 ./addrinfo www.google.com www.yahoo.com
www.google.com
go package net: confVal.netCgo = false  netGo = false
go package net: cgo resolver not supported; using Go's DNS resolver
go package net: hostLookupOrder(www.google.com) = files,dns
216.58.220.132
2404:6800:4004:826::2004
www.yahoo.com
go package net: hostLookupOrder(www.yahoo.com) = files,dns
180.222.119.248
180.222.119.247
2406:2000:a4:807::1
2406:2000:a4:807::

cgo resolver not supported; using Go's DNS resolver なので cgo 無いので Go でやるよ、ってことだな。hostLookupOrder(www.google.com) = files,dns ってことで確かに resolve は無視されている。

逆に、ビルドタグで netcgo を指定してビルドすると resolve が無くても cgo が使われた。

$ go build -tags netcgo addrinfo.go
$ GODEBUG=netdns=2 ./addrinfo www.google.com www.yahoo.com
www.google.com
go package net: confVal.netCgo = true  netGo = false
go package net: using cgo DNS resolver
go package net: hostLookupOrder(www.google.com) = cgo
142.251.42.164
2404:6800:4004:827::2004
www.yahoo.com
go package net: hostLookupOrder(www.yahoo.com) = cgo
180.222.119.248
180.222.119.247
2406:2000:a4:807::1
2406:2000:a4:807::

後から見ると分かりづらいが、この時は nsswitch.conf(5) から resolve は削除して試してみている。ビルドタグ指定すると confVal.netCgo = true とか出るんやな。

あとは実行時に GODEBUG=netdns=goGODEBUG=netdns=cgo を指定する事でもコントロール可能だった。

$ GODEBUG=netdns=go+2 ./addrinfo www.google.com www.yahoo.com
www.google.com
go package net: confVal.netCgo = false  netGo = true
go package net: GODEBUG setting forcing use of Go's resolver
go package net: hostLookupOrder(www.google.com) = files,dns
216.58.220.132
2404:6800:4004:821::2004
www.yahoo.com
go package net: hostLookupOrder(www.yahoo.com) = files,dns
180.222.119.248
180.222.119.247
2406:2000:a4:807::1
2406:2000:a4:807::

これも後から見ると分かりづらいが、この時は nsswitch.conf(5)resolve を復活させてビルドも普通にしてから試してみている。GODEBUG setting forcing use of Go's resolver なので、無理やり Go でやるよってことだな。

OK、Go のホスト名解決の挙動もざっくりと分かったような気がする。

せっかく Varlink とか言うインタフェースが使われてるのが分かったのでちょっと試してみたくなった。

まずは正しく理解できてるか分からないので、さっくりとコマンドで試してみる。

varlink
$ echo -e '{
  "method":"io.systemd.Resolve.ResolveHostname",
  "parameters":{
    "name":"www.google.com"
  }
}\0' | nc -UN /run/systemd/resolve/io.systemd.Resolve
# 結果
{"parameters":{"addresses":[{"ifindex":2,"family":10,"address":[36,4,104,0,64,4,8,40,0,0,0,0,0,0,32,4]},{"ifindex":2,"family":2,"address":[172,217,174,100]}],"name":"www.google.com","flags":1048577}}

おっ、ちゃんと出た。

Varlink と言うのは method に文字列でメソッド名、parameters にオブジェクトでパラメータを送り付けると結果が parameters にオブジェクトで返って来るらしい。気を付けなきゃいけないのは送り付ける JSON の後ろに NUL 文字(\0)を付けないといけないことか。最初付けずに試したら結果が返ってこなくてちょっと悩んだ。あと、パッと見分からないけど実はレスポンスの最後にも NUL 文字が付いていた。

varlink
$ echo -e '{
  "method":"io.systemd.Resolve.ResolveHostname",
  "parameters":{
    "name":"www.google.com"
  }
}\0' | nc -UN /run/systemd/resolve/io.systemd.Resolve | hd
# 結果
00000000  7b 22 70 61 72 61 6d 65  74 65 72 73 22 3a 7b 22  |{"parameters":{"|
00000010  61 64 64 72 65 73 73 65  73 22 3a 5b 7b 22 69 66  |addresses":[{"if|
00000020  69 6e 64 65 78 22 3a 32  2c 22 66 61 6d 69 6c 79  |index":2,"family|
00000030  22 3a 31 30 2c 22 61 64  64 72 65 73 73 22 3a 5b  |":10,"address":[|
00000040  33 36 2c 34 2c 31 30 34  2c 30 2c 36 34 2c 34 2c  |36,4,104,0,64,4,|
00000050  38 2c 31 35 2c 30 2c 30  2c 30 2c 30 2c 30 2c 30  |8,15,0,0,0,0,0,0|
00000060  2c 33 32 2c 34 5d 7d 2c  7b 22 69 66 69 6e 64 65  |,32,4]},{"ifinde|
00000070  78 22 3a 32 2c 22 66 61  6d 69 6c 79 22 3a 32 2c  |x":2,"family":2,|
00000080  22 61 64 64 72 65 73 73  22 3a 5b 32 31 36 2c 35  |"address":[216,5|
00000090  38 2c 32 32 30 2c 31 33  32 5d 7d 5d 2c 22 6e 61  |8,220,132]}],"na|
000000a0  6d 65 22 3a 22 77 77 77  2e 67 6f 6f 67 6c 65 2e  |me":"www.google.|
000000b0  63 6f 6d 22 2c 22 66 6c  61 67 73 22 3a 31 30 34  |com","flags":104|
000000c0  38 35 37 37 7d 7d 00                              |8577}}.|
000000c7

ここまで来たらコマンドでお茶を濁すんじゃなくてプログラムでも書いてみたい。が、C 言語で JSON を扱うのはしんどいのでリクエストは単なる sprintf(3)、レスポンスもそのまま puts(3) でお茶を濁す。

varlink.c
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <errno.h>
#include <string.h>

int main(int argc, char **argv)
{
    int fd = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0);
    if (fd < 0) {
        printf("connect error:%s\n", strerror(errno));
        return 1;
    }
    struct sockaddr_un sa = {
        .sun_family = AF_UNIX,
        .sun_path = "/run/systemd/resolve/io.systemd.Resolve",
    };
    int ret = connect(fd, (const struct sockaddr *)&sa, sizeof(sa));
    if (ret != 0) {
        printf("connect error:%s\n", strerror(errno));
        return 1;
    }
    for (int i = 1; i < argc; ++i) {
        char buf[1024];
        int len = sprintf(buf,
            "{"
                "\"method\":\"io.systemd.Resolve.ResolveHostname\","
                "\"parameters\":{"
                    "\"name\":\"%s\""
                "}"
            "}",
            argv[i]);
        int write_len = write(fd, buf, len + 1);
        if (write_len != len + 1) {
            printf("write error(%s)\n", strerror(errno));
            continue;
        }
        int read_len = read(fd, buf, sizeof(buf));
        if (read_len <= 0) {
            printf("read error(%s)\n", strerror(errno));
            continue;
        }
        puts(buf);
    }
}

で、呼んでみた。

$ ./varlink www.google.com www.yahoo.com
{"parameters":{"addresses":[{"ifindex":2,"family":2,"address":[216,58,220,132]},{"ifindex":2,"family":10,"address":[36,4,104,0,64,4,8,19,0,0,0,0,0,0,32,4]}],"name":"www.google.com","flags":1048577}}
{"parameters":{"addresses":[{"ifindex":2,"family":10,"address":[36,6,32,0,0,164,8,7,0,0,0,0,0,0,0,0]},{"ifindex":2,"family":10,"address":[36,6,32,0,0,164,8,7,0,0,0,0,0,0,0,1]},{"ifindex":2,"family":2,"address":[180,222,119,248]},{"ifindex":2,"family":2,"address":[180,222,119,247]}],"name":"me-ycpi-cf-www.g06.yahoodns.net","flags":1048577}}

それっぽい結果が返って来てるのでヨシ!

C 言語で JSON はしんどいけど Go 言語なら何とかなるやろ、ってことで Go 言語でも書いてみる。

varlink.go
package main

import (
    "fmt"
    "net"
    "os"
    "encoding/json"
)

type ReplyError struct {
    Parameters map[string]any
    Error string
}

type Reply struct {
    Parameters struct {
        Name string
        Addresses []struct {
            Address []byte
            Family int32
            Ifindex int32
        }
        Flags uint64
    }
}

func main() {
    conn, err := net.Dial("unix", "/run/systemd/resolve/io.systemd.Resolve")
    if err != nil {
        fmt.Printf("error: %v\n", err)
        os.Exit(1)
    }
    defer conn.Close()

    for _, arg := range os.Args[1:] {
        fmt.Println(arg)

        req := "{" +
            `"method":"io.systemd.Resolve.ResolveHostname",` +
            `"parameters":{` +
                `"name":"` + arg + `"` +
            `}` +
        "}\000"
        if _, err = conn.Write([]byte(req)); err != nil {
            fmt.Printf("Write error: %v\n", err)
            continue
        }

        buf := make([]byte, 1024)
        read_len, err := conn.Read(buf)
        if err != nil {
            fmt.Printf("Read error: %v\n", err)
            continue
        }

        var res ReplyError
        if err := json.Unmarshal(buf[:read_len - 1], &res); err != nil {
            fmt.Printf("Decode error: %v\n", err)
            continue
        }
        if res.Error != "" {
            fmt.Printf("Method call error: %s(%+v)\n", res.Error, res.Parameters)
            continue
        }

        var data Reply
        if err := json.Unmarshal(buf[:read_len - 1], &data); err != nil {
            fmt.Printf("Decode error: %v\n", err)
            continue
        }
        fmt.Printf("name: %s\n", data.Parameters.Name)
        for i, val := range data.Parameters.Addresses {
            fmt.Printf("%d: family:%d, ifindex:%d, address:%s\n",
                i, val.Family, val.Ifindex, net.IP(val.Address))
        }
        fmt.Printf("flags: %#x\n", data.Parameters.Flags)
    }
}

何か思ってたよりは面倒だった。てか address は型的には []byte だけど net.IP[]byte だからと言って net.IP に直接 Unmarshal はできないのか、…(Go 言語エアプ的感想

で、動かしてみる。

$ ./varlink www.google.com www.yahoo.com
www.google.com
name: www.google.com
0: family:2, ifindex:2, address:142.251.222.4
1: family:10, ifindex:2, address:2404:6800:4004:818::2004
flags: 0x100001
www.yahoo.com
name: me-ycpi-cf-www.g06.yahoodns.net
0: family:2, ifindex:2, address:180.222.119.247
1: family:2, ifindex:2, address:180.222.119.248
2: family:10, ifindex:2, address:2406:2000:a4:807::1
3: family:10, ifindex:2, address:2406:2000:a4:807::
flags: 0x100001

ちゃんと動いてるっぽい。

本格的にやるとなるとエラーハンドリングとかいろいろ大変なのかもしれないが、これなら Pure Go 実装に取り込んでくれても良くない?

あれ?DBus って要る?

Varlink あるんなら DBus じゃなくても良くない?プロトコル的にはこっちは JSON、DBus はバイナリだから DBus の方が効率的なのかな?でも DBus はdbus-daemon(1)[2] が中継してるからなぁ…

う~ん、分からん…

おわりに

と言う訳で systemd-resolved のホスト名解決をちょっとだけ調べてみたが、なかなか複雑で明日には何もかも忘れそうだ。

それでは、良い systemd ライフを!

脚注
  1. コイツは man7 にマニュアルが無かった… ↩︎

  2. コイツも man7 にマニュアルが無かった… ↩︎

Discussion