名前解決サービスを自作する

## はじめに

### 名前解決サービスって？

皆さんが今お使いのネットワーク（LAN）には、「example」という名前のPCは接続されているでしょうか？ 臨時使用の仮想マシンを作る際に foo とか hoge の様な名前のマシンを構築することはあるかもしれませんが、普通はそんなコンピューター名にはしないと思います。また、`/etc/hosts`に「example」という名前に関する定義はあるでしょうか？ たとえば、`127.0.0.1 example`と書かれていれば、`ping example`を実行すると、自マシンからのping結果が返ってくるはずです。

ここから先は、「example」という名のホストは存在せず、`/etc/hosts`にもそのような定義はなく、`ping example`を実行すると、`ping: unknown host example`という結果が表示される前提で話を進めます。

ホスト名からIPアドレスを検索することを名前解決といいます。代表的なものとしては、「hostsファイル」や「dnsリゾルバ」などがあります。何に対して問い合わせるかは自動的に決定されます。それはnsswitch（Name Service Switch）という機能によって提供されます。まず、この設定ファイルを覗いてみましょう。`/etc/nsswitch.conf`を表示してみます。

```
soramimi@alice:~$ cat /etc/nsswitch.conf 
# /etc/nsswitch.conf
#
# Example configuration of GNU Name Service Switch functionality.
# If you have the `glibc-doc-reference' and `info' packages installed, try:
# `info libc "Name Service Switch"' for information about this file.

passwd:         compat
group:          compat
shadow:         compat
gshadow:        files

hosts:          files mdns4_minimal [NOTFOUND=return] dns
networks:       files

protocols:      db files
services:       db files
ethers:         db files
rpc:            db files

netgroup:       nis
```
中程に、`hosts:`という定義があり、その先頭が`files`となっています。続いて`mdns4`または`mdns4_minimal`、そして`dns`と書かれています。Sambaがインストールされた環境であれば、`wins`というのもあるかもしれません。

先頭の`files`は、最初に`/etc/hosts`の定義から検索することを指定します。`mdns`系はOSの構成によっては無いかもしれませんが、最近のOSでは標準で利用できるケースが多いようです。これは、mDNS（Multicast DNS）といって、macOSで標準となっている名前解決機能です。Windowsでも、Appleから提供されている「Bonjour for Windows」をインストールすると利用できますし、そもそも Windows 10 では標準でmDNSに対応しているそうなので、正しく設定されていれば、`ping [ホスト名].local`のように実行すると、名前解決に成功し、pingの結果が表示されると思います。最後の`dns`は、インターネットの名前解決のために、DNSサーバに問い合わせを行う指定です。

ホスト名とIPアドレスが予め決まっているのであれば、`/etc/hosts`に定義するのが最も簡単で普通のやり方です。hosts以外のファイル形式やデータベースなどを利用して、プログラムによって動的に名前解決を行いたい場合は、名前解決ライブラリを自作して`nsswitch.conf`に登録すれば、好きなようにできます。

## 作ります

本記事ではC言語を利用して名前解決ライブラリを開発します。その実体は共有ライブラリ（shared object）です。

名前解決機能は、OSの動作の根幹と言っても過言ではありませんから、OSに組み込むにはroot権限が必要ですし、プログラムに欠陥があれば、場合によっては重大なセキュリティーホールとなりえますので、そこは注意してください。

### ソースコード

Makefileを含んだプログラム全体は下記リポジトリに置きました。

https://github.com/soramimi/libnss-example

たった1個のC言語のソースファイルにたった2個の関数があるだけです。2つ目の関数は1つ目の関数を呼んでいるだけなので、実質、1つの関数しかありません。

``` main.c
#include <string.h>
#include <nss.h>
#include <netdb.h>
#include <stdlib.h>
#include <errno.h>
#include <arpa/inet.h>

#define ALIGN(idx) do { \
	if (idx % sizeof(void*)) \
	idx += (sizeof(void*) - idx % sizeof(void*)); /* Align on 32 bit boundary */ \
	} while(0)

enum nss_status _nss_example_gethostbyname_r(const char *name, struct hostent *result, char *buffer, size_t buflen, int *errnop, int *h_errnop)
{
	size_t idx, astart;

	if (strcmp(name, "example") == 0) {
		char const *host = "127.0.0.1";

		*(char**)buffer = NULL;
		result->h_aliases = (char**) buffer;
		idx = sizeof(char*);

		strcpy(buffer + idx, name);
		result->h_name = buffer + idx;
		idx += strlen(name) + 1;
		ALIGN(idx);

		result->h_addrtype = AF_INET;
		result->h_length = sizeof(uint32_t);

		struct in_addr addr;

		inet_pton(AF_INET, host, &addr);

		astart = idx;
		memcpy(buffer+astart, &addr.s_addr, sizeof(uint32_t));
		idx += sizeof(uint32_t);

		result->h_addr_list = (char**)(buffer + idx);
		result->h_addr_list[0] = buffer + astart;
		result->h_addr_list[1] = NULL;

		return NSS_STATUS_SUCCESS;
	}

	*errnop = EINVAL;
	*h_errnop = NO_RECOVERY;
	return NSS_STATUS_UNAVAIL;

}

enum nss_status _nss_example_gethostbyname2_r(const char *name, int af, struct hostent *result, char *buffer, size_t buflen, int *errnop, int *h_errnop)
{
	if (af != AF_INET) {
		*errnop = EAGAIN;
		*h_errnop = NO_RECOVERY;
		return NSS_STATUS_TRYAGAIN;
	} else {
		return _nss_example_gethostbyname_r(name, result, buffer, buflen, errnop, h_errnop);
	}
}
```

### 動作の仕組み

やっていることは、問い合わせの名前が「example」だったとき、その結果として「127.0.0.1」を返す、ただそれだけです。nsswitch特有の構造体に結果を格納する処理は一見面倒そうですが、この部分は先人の成果を丸パクリしただけとなっています。私が参考にした元プログラムは[これ](https://github.com/goneri/libnss-openvpn)です。

### コンパイル

コンパイルします。

```
soramimi@alice:~/develop/libnss-example$ make
gcc   -g -O2 -Wall -Wpointer-arith -fPIC -c -o main.o main.c
gcc  -g -O2 -Wall -Wpointer-arith -shared -Wl,-soname,libnss_example.so.2 -Wl,-z,defs -o libnss_example.so.2 main.o  
```

`main.c`をコンパイルして、`libnss_example.so.2`を作成しています。共有ライブラリ（shared object）なので、`.so`という拡張子がついています。最後の`.2`というのは、APIバージョンの様なものと考えてください。これ以外にすると動かないので、必ず`.2`を最後に付けます。

### インストール

次に、インストールします。

```
soramimi@alice:~/develop/libnss-example$ sudo make install
install -m755 -d /usr/lib/
install -m644 libnss_example.so.2 /usr/lib/libnss_example.so.2
```

単純に`/usr/lib/`の中にコピーしているだけです。

### 設定ファイルの変更

`/etc/nsswitch.conf`を編集します。`hosts:`の定義の最後に`example`を追加したら完了です。

```
...
hosts:          files mdns4_minimal [NOTFOUND=return] dns example
...
```

## 実行します

pingを実行して、結果が返ってくることを確認します。

```
soramimi@alice:~$ ping example
PING example (127.0.0.1) 56(84) bytes of data.
64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.034 ms
64 bytes from localhost (127.0.0.1): icmp_seq=2 ttl=64 time=0.033 ms
64 bytes from localhost (127.0.0.1): icmp_seq=3 ttl=64 time=0.029 ms
```

## 自分用に改造するなら

自前の名前解決ライブラリを作る場合、たとえば、`libnss_example.so.2`ではなく`libnss_hogehoge.so.2`にするのであれば、`main.c`と`Makefile`に含まれる`example`をすべて`hogehoge`に置換します。2つの関数名もそれぞれ`_nss_hogehoge_gethostbyname_r`と`_nss_hogehoge_gethostbyname2_r`になります。そして、関数で受け取った`name`に返答すべきIPアドレスがあれば、`struct in_addr addr`にアドレスを設定して、構造体に書き込み、`NSS_STATUS_SUCCESS`を返します。

コンパイルしてインストールしたら、`/etc/nsswitch.conf`の`hosts:`の定義に、新しく作ったライブラリの名前を追加します。