systemd-resolvedのDNSStubListenerを使ってgolangでmDNSの名前解決をする

· ·

golangでmDNSの名前解決をするのは結構クセがあります。

前置き 🔗

golangのDNSによる名前解決はポータビリティを高めるためにgolangによる独自実装となっており、いくつかの条件によって名前解決の結果が他の言語の場合と異なります。

その条件の1つがmDNSを使った場合で、末尾が .local の場合はfallbackOrderへ切り替わるようになっています。

src/net/conf.go#215

このfallbackOrderは、CGO_ENABLED=1 の場合は最終的に src/net/cgo_unix.go#153 でgetaddrinfoを使って解決されるため、他の言語と同様の結果となります。

問題は CGO_ENABLED=0 の場合で、その場合は最終的にhostsファイルかgolangによるDNSクライアント実装 src/net/dnsclient_unix.go#L258 によって名前解決が行われ、mDNSによる名前解決は行われなくなります。

私はLAN内のサーバーの名前解決をmDNSに任せているのですが、上記の問題によりzabbix-agent2や、prometheusのexporterでmDNSでの名前解決が出来ない問題が発生しました。

本題 🔗

そういうわけで、mDNSを使った環境ではgolangでの名前解決に問題があります。

それを解決するためにsystemd-resolvedのDNSStubListenerを使うという方法を取ります。

他のDNSサーバーを使う際に厄介者扱いされがちなsystemd-resolvedですが、DNSによる名前解決以外にmDNSやLLMNRによる名前解決も提供しているため、golangでのmDNSの名前解決に使ってしまおうというわけです。

mDNSによる名前解決をするためには、systemd-networkdにて該当interfaceからmDNSの名前解決をするかの設定が必要なためsystemd-networkd側の設定をします。

[Match]
Name=eth0
[Network]
DHCP=yes
IPv6AcceptRA=yes
# このinterfaceでmDNSによる名前解決を行うかどうか。
MulticastDNS=resolve
LLMNR=no
[DHCPv4]
UseDNS=no
[DHCPv6]
UseDNS=no
[IPv6AcceptRA]
UseDNS=no

該当interfaceからmDNSによる名前解決が行われるかどうかは以下のコマンドで確認できます。

resolvectl mdns

続いてsystemd-resolvedの設定をします。

[Resolve]
# 上位のDNSサーバーを指定する。
DNS=1.1.1.1
# DNSStubListenerでmDNSによる名前解決を行うかどうか。
# yesの場合、mDNSサーバーとしても動作する。
MulticastDNS=resolve
# 今回の主目的。127.0.0.53:53でsystemd-resolvedがDNSサーバーとして動いてくれる。
DNSStubListener=yes

以上の設定が終わったらsystemd-networkdとsystemd-resolvedを再起動します。

systemctl restart systemd-networkd
systemctl restart systemd-resolved

systemd-resolvedのDNSStubListenerを使うように /etc/resolv.conf にシンボリックリンクを張ります。

ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf

この状態でgolangによるmDNSでの名前解決が出来れば成功です。

package main

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

func main() {
    addrs, err := net.LookupHost("mapoyon.local")
    if err != nil {
        os.Exit(1)
    }
    fmt.Printf("%v\n", addrs)
    os.Exit(0)
}
CGO_ENABLED=0 go run main.go
# => [192.168.0.20]

その他 🔗

getaddrinfoによるmDNSの名前解決について 🔗

mDNSのドメインが問い合わせられた場合、nsswitchによって最終的にavahi-daemonに問い合わせが行われます。

そのため、avahi-daemonが起動していない場合mDNSによる名前解決が出来ません。

dhclientが/etc/resolv.confを書き換えてしまう場合 🔗

以下のようなスクリプトを配置することで無効化できる。

echo '#!/bin/sh
make_resolv_conf() { : ; }
' > /etc/dhcp/dhclient-enter-hooks.d/disable-resolved-enter
chmod +x /etc/dhcp/dhclient-enter-hooks.d/disable-resolved-enter

NetworkManagerが/etc/resolv.confを書き換えてしまう場合 🔗

NetworkManagerのデフォルト設定では/etc/resolv.confがシンボリックリンクの場合書き換えないようになっているはずだが、明示的に無効化したい場合はdns=noneにする。

echo '[main]
dns=none' > /etc/NetworkManager/conf.d/disable-resolved-enter.conf