ローファイ日記

出てくるコード片、ぼくが書いたものは断りがない場合 MIT License としています http://udzura.mit-license.org/

プラグインを書いて学ぶGNU NSS

Linuxアドベントカレンダー13日目の月曜日の記事です。

qiita.com

今日は以前から気になっていたNSS pluginの仕様について調べてまとめ、簡単なものを書いてみようと思います。というのも、実は 同僚が 二人も NSS pluginを開発して公開しており、ずっと負けてられないなと思っていました。

GNU Name Service Switch (NSS) とは

さて、OSを使っていると、名前の正体が何者なのか、あるいはこのIDは人間がどういう名前をつけているのか、それぞれ確認する機会が頻繁にあります。

Linuxの場合、その「人間が使う名前と、OSが使うID」の相互解決をName Service Switchというものがやってくれます*1

Linuxを構築運用していて /etc/nsswitch.conf というファイルをいじった経験がある方も多いでしょう。そのマニュアルに どういう名前解決をする時にNSSが使われるかが一覧されています。例えば、passwd(ユーザ名→IDなど)、group、hosts(ホスト名→IPなど)などが代表的かと思います。

それと一緒に、どういう関数を読んだ時にどの設定が使われるか、という対応づけが分かるかと思います。

例えば上述のmanのhostsの項目には

Host names and numbers, used by gethostbyname(3) and related functions.

と書かれています。プログラムの中でホスト名を解決する関数である gethostbyname(3) と、似た機能の関数(例えば gethostbyname_r(3) など)を呼ぶ時にもnsswitchを参照し、どの順序でNSSプラグインを呼び出すかを決め、実際の名前解決を行います。

NSSの関数呼び出しを追いかける

実際どういう経緯で呼び出されているのでしょうか。観察してみます。

id(1) は、ユーザIDからユーザやグループに関するpasswd情報(ユーザ名、ホームディレクトリなど...)を引いてくれるコマンドです。

$ id
uid=1000(vagrant) gid=1000(vagrant) groups=1000(vagrant)

このコマンドは明らかにNSSを使っていそうですね。どういうプラグインを使っているかは、例えばstraceで追いかけられます。

$ strace id 9999 2>&1 | grep 'libnss'
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libnss_files.so.2", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libnss_systemd.so.2", O_RDONLY|O_CLOEXEC) = 3

glibcが用意している標準的なプラグインは、 libnss_XXX.so のような名前になります。nsswitch.confを確認すると、確かにpasswd情報については filessystemd をこの順番で参照するようになっています。

$ grep passwd /etc/nsswitch.conf 
passwd:         files systemd

なお、 id コマンドはバイナリにある情報からライブラリをリンクしているわけではなく、名前解決をするタイミングで動的にプラグインをロードしています。動的リンクといってもここは特殊かと思います。

$ ldd `which id`
        linux-vdso.so.1 (0x00007fff4f1fe000)
        libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007fcd546b5000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fcd544cb000)
        libpcre2-8.so.0 => /lib/x86_64-linux-gnu/libpcre2-8.so.0 (0x00007fcd5443b000)
        libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007fcd54435000)
        /lib64/ld-linux-x86-64.so.2 (0x00007fcd546fb000)
        libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fcd54413000)

さて、これらのNSSプラグイン共有ライブラリには _nss_XXX_getpwuid_r という関数が定義されており、NSSは getpwuid_r(3) を呼ぶタイミングで、順繰りにこれらの関数を呼んでいきます。

続いて、bpftraceを使って追いかけてみます*2

次のような trace.bt というファイルを作成します。

uprobe:/lib/x86_64-linux-gnu/libnss_files.so.2:_nss_files_getpwuid_r
{
  printf("called: %s(%d)\n", probe, arg0);
}
uretprobe:/lib/x86_64-linux-gnu/libnss_files.so.2:_nss_files_getpwuid_r
{
  printf("returns: %d\n", retval);
}

uprobe:/lib/x86_64-linux-gnu/libnss_systemd.so.2:_nss_systemd_getpwuid_r
{
  printf("called: %s(%d)\n", probe, arg0);
}
uretprobe:/lib/x86_64-linux-gnu/libnss_systemd.so.2:_nss_systemd_getpwuid_r
{
  printf("returns: %d\n", retval);
}

bpftraceを起動します。

$ sudo bpftrace trace.bt 
Attaching 4 probes...

別のターミナルで id コマンドを発行すると _nss_* な関数呼び出しと戻り値が記録されています。nsswitch.confのmanにある success が1に、 notfound が0に対応している感じですね。

$ id 1
uid=1(daemon) gid=1(daemon) groups=1(daemon)
$ id 9999
id: ‘9999’: no such user

....
$ sudo bpftrace trace.bt 
Attaching 4 probes...
called: uprobe:/lib/x86_64-linux-gnu/libnss_files.so.2:_nss_files_getpwuid_r(1)
returns: 1
called: uprobe:/lib/x86_64-linux-gnu/libnss_files.so.2:_nss_files_getpwuid_r(1)
returns: 1
called: uprobe:/lib/x86_64-linux-gnu/libnss_files.so.2:_nss_files_getpwuid_r(9999)
returns: 0
called: uprobe:/lib/x86_64-linux-gnu/libnss_systemd.so.2:_nss_systemd_getpwuid_r(9999)
returns: 0

NSSのプラグイン入門

ここまで読んで分かる通り、NSSのプラグインlibnss_${プラグイン名}.so という名前で、 _nss_${プラグイン名}_${対応する関数名} というシンボルをエクスポートしている共有ライブラリっぽい、ということがわかります。

実際どういう名前のライブラリにして、どういう関数の定義にしてシンボルはこうです、というのはGNU libcのマニュアルにあります。

www.gnu.org

が、いったん実例も眺めてみます。

名前解決にmDNSを使うためのnss-mdnsというプラグインがあるそうです。

github.com

この ソースコードを見る と、hostsプラグインを書くには、 gethostbyname_r gethostbyname[234]_r gethostbyaddr_r あたりに対応し、関数自体は enum nss_status を返すといいみたいだとわかります。

ところで例えば ping -c1 udzura.jp のようなコマンドを打ってみると、 gethostbyname4_r(3) を呼んでいるようですので、それだけ実装したプラグインを作ってみます。

$ sudo bpftrace -e '
uprobe:/lib/x86_64-linux-gnu/libnss_files.so.2:_nss_files_gethostbyname*_r
{
  printf("%s called probe %s\n", comm, probe);
}'
Attaching 4 probes...
ping called probe uprobe:/lib/x86_64-linux-gnu/libnss_files.so.2:_nss_files_gethostbyname4_r

とりあえず見様見真似で1関数だけ。

#include <nss.h>
#include <syslog.h>

enum nss_status _nss_logger_gethostbyname4_r(const char* name,
                                           struct gaih_addrtuple** pat,
                                           char* buffer, size_t buflen,
                                           int* errnop, int* h_errnop,
                                           int32_t* ttlp) {
  openlog("resolver.hello", 0, LOG_USER);
  syslog(LOG_NOTICE, "resolving: %s", name);
  return NSS_STATUS_NOTFOUND;
}

この辺の記述 を参考にしてビルドオプションを決めます。

$ gcc -shared -o libnss_logger.so.2 -Wl,-soname,libnss_logger.so.2 nss_logger.c

また、NSSは共有ライブラリ解決時には LD_LIBRARY_PATH 環境変数を参照してくれないようなので、 /usr/lib/x86_64-linux-gnu 配下にインストールしておきます。

$ sudo install ./libnss_logger.so.2 /usr/lib/x86_64-linux-gnu/

最後にnsswitchに、hosts解決時には logger プラグインを最優先させるよう指定します。

hosts:          logger files dns

これで適当に名前解決するプログラムを走らせると、syslogに名前解決した旨と解決しようとしたドメイン名が記録されます。

$ sudo tail -f /var/log/syslog | grep resolver
Dec  4 07:13:11 ubuntu-groovy resolver.hello: resolving: ubuntu-groovy
Dec  4 07:13:15 ubuntu-groovy resolver.hello: resolving: udzura.jp
Dec  4 07:13:20 ubuntu-groovy resolver.hello: resolving: ubuntu-groovy
...

ちなみに _nss_logger_gethostbyname4_r() 自体は絶対にnotfoundになるようにしており、解決の処理自体は次のプラグインに移譲されます。こうすることで言ってみれば「名前解決の監査」のような実装が簡単に作れるように思われました。

なお、既存のプラグインは大抵Cで書かれていますが、原理を理解すれば分かる通り他の言語でも、共有ライブラリを作成できる言語なら作成可能です。

Rust で書かれたプラグインの例です。

github.com


ちなみにNSSは本来プログラム側でリンクを想定していない共有ライブラリを動的に読み込む形になるため、プログラム側とNSSプラグイン側でリンクするライブラリが違ったり、関数名がコンフリクトしていると深遠なバグの原因になります。

例えば複雑な処理をしそうなNSSプラグインは、共有ライブラリ依存ではなく、C++やRustなどのランタイムやcrateの実装で書くという方法は回避の一つの策として有効かもしれません。

NSSでどういうトラブルが起こりうるかについては「続・玩式草子 ―戯れせんとや生まれけん―」の2020年8月〜9月の号がとても分かりやすく詳細なので、そちらも是非併せてお読みください。

gihyo.jp

gihyo.jp

まとめ

NSS について概要的な内容と、簡単なプラグインの作り方を書き残しました。普段隠れている割には、Linuxを使う上でほぼ絶対に避けられない仕様でもあるので、この知識が運用の何かの役に立つと嬉しいなと思います。

動作環境

本稿は以下の環境で確認しました。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 20.10
Release:        20.10
Codename:       groovy
$ uname -a
Linux ubuntu-groovy 5.8.0-63-generic #71-Ubuntu SMP Tue Jul 13 15:59:12 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

謝辞

この記事は @ten_forward さんにレビューをいただきました。ありがとうございました!*3

ten_forward さんのLinuxアドベントカレンダーは21日目です。また、明日は @kaizen_nagoya さんです。

*1:FreeBSDなどにもほぼ同じ仕組みがありますが、今回はLinuxのやつを取り上げると言うことで

*2:ltraceでできそうですがうまくいかず...。

*3:無論、内容の正確性等文責は全て私にあります。