systemd-resolved のホスト名解決をちょっと調べてみた
すべてが 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 って何?状態だったのだが、とりあえずサンプルコードがあったので読んでみた。
いやいや、たかだかホスト名解決するためだけにこれ書かなきゃいけないのおかしいやろ。もうちょっと使いやすいライブラリとか無いんか?と思ったが軽く調べたところでは無さそうだった。これ絶対使わせる気ないやろ…
各種コマンド
まぁ、気を取り直して更にいろいろ調べたところ、とりあえず以下のようなコマンドで試せることは分かった。(ここまで調べるのに 5 万年かかった…)
$ 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 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 --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
だと何でも。2
(AF_INET
)だと IPv4、10
(AF_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
と言う呪文は引数や戻り値の型を表す文字である。i
は int32
、t
は uint64
、y
は byte
、s
は string
、a
は配列で要素型は続く文字で表される、()
は構造体で各フィールドはカッコ内の文字で表される。くわしくはこちら。
C 言語
次はサンプルを基に気張って 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 でも書いてみる。
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)
で書いてみる。
#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.53
は systemd-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
とか言うヤツが生えた。なるほどね。
...
hosts: files resolve [!UNAVAIL=return] dns
...
ところで man 8 nss-resolve
見ると nss-resolve
は files
より前がいいよって書いてあるんだけど、なんで後なんだろう。あと、フォールバック用に後ろに 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.2
と libnss_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
インタフェースのメソッドのうち ResolveHostname
と ResolveAddress
がそのまま公開されているっぽい。そのままって言っても JSON だけど。
つまり、man 8 systemd-resolved
の最初に書かれている 3 つのインタフェースのうち 2 番目の glibc のインタフェース使った場合には、1 番目の DBus インターフェイスとも 3 番目のローカル DNS スタブリスナインタフェースとも異なるインターフェイスで systemd-resolved
とやりとりしてるって事だ。なるほどね…
まぁ一応 systemd-resolved
に問い合わせることができたので許してやるか。
Go 言語
次にこれも 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=go
や GODEBUG=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
文字が付いていた。
$ 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
Varlink を C 言語で呼んでみる
ここまで来たらコマンドでお茶を濁すんじゃなくてプログラムでも書いてみたい。が、C 言語で JSON を扱うのはしんどいのでリクエストは単なる sprintf(3)
、レスポンスもそのまま puts(3)
でお茶を濁す。
#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}}
それっぽい結果が返って来てるのでヨシ!
Varlink を Go 言語で呼んでみる
C 言語で JSON はしんどいけど Go 言語なら何とかなるやろ、ってことで 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
ライフを!
Discussion