C MAGAZINE Linux programming Tips
第4回 tcp wrappers活用法
■はじめに
今回は、IPアドレスレベルのフィルタリングの実装のひとつであるtcp wrappersに関するセキュリティー的なお話をしたいと思います。
ご存知の通り、Linuxディストリビューションは、とても便利なネットワークサーバーがインターネット/イントラネット用途問わず、 数多く含まれています。もちろんsocketプログラミングなどの知識があれば簡単にネットワークサーバーを開発することもできます。 いろいろなネットワークサーバーを使えることは非常に便利なことですが、 逆に不正アクセスなどの危険に常にさらされていることにもなります。例えば、 ゲートウエイマシンのインターネット側で portmapや bootpなどのポートを開けておくことは不正アクセスの原因になるかも知れません。
ゲートウエイマシンと言ってしまえば大げさかも知れませんが、 Linuxマシンからプロバイダーに接続したときもゲートウエイと同じ状況で、 自宅のローカルネットワークにサービスしていたつもりのサービスが世界中で利用できてしまうことになりかねません。実際、 このことを利用したクラッキングのようなことはおこっているようです。 また特定のホストのみにサービスを提供したいという要求もあるでしょう。何らかのアクセスコントロール機構は必要不可欠です。
今回は、アクセスコントロール機構の実装のひとつであるtcp wrappersについてのおおまかな概要と内部動作に説明し、 ライブラリ関数の実装方法について説明します。
■tcp wrappersについて
アクセスコントロールといってもいろんなレベルの実装があります。tcp wrappersはその一種で、 簡単にいうと接続元のIPアドレスとアクセスコントロールリストを比較して妥当なクライアントかどうか判断するようなものです。 tcp wrappersは古くからある実装で、ほとんどのLinuxディストリビューションに含まれています。インターネットスーパーサーバー 「inetd」 から起動されるサーバーや一部のサーバーは、tcp wrappersが提供する機能を利用します。
tcp wrappersを利用するサーバーは、同一のアクセスコントロールリストでアクセス制御が可能になります。一方、 samba, apache, nfsd, xinetdなどは、アクセスコントロール機能を持っていますが、 tcp wrappersの機能を利用していないのでアクセスコントロールリストは各々の設定ファイルに記述することになります。
tcp wrappersは、inetdから起動されるサービスのためのラッパープログラム 「tcpd」、 アクセスコントロール機能を提供するCライブラリー 「libwrap」とちょっとしたプログラムを提供します。 後で説明するライブラリーlibwrapの機能を使うプログラムとしては、(コンパイルオプションにもよりますが)portmap, sshd(openssh)などがあげられます。またtcpdについては、 inetd.confを見ると分かりますがほとんどのサーバーがtcpd経由なことを確認できるでしょう。
[indet.confの一部]
telnet stream tcp nowait root /usr/sbin/tcpd in.telnetd -h
実装面の話をすると、特定のアドレスに対してのみサービスを提供するような実装は比較的簡単です。 ソケットプログラミングにおいて、接続元のアドレス情報の取得は可能なので、 そのアドレスが妥当かどうか判別するコードを埋め込めば良いわけです。 以下は listen()後のサーバープログラムの処理の一部です。このコードでは、 接続元のホスト名が zeus.kernel.orgであるかどうかの判断で処理を分岐させるようになっています。
....
int fd2;
struct hostent *peer_host;
struct sockaddr_in peer_sin;
int len_peer_sin;
if((fd2 = accept(fd, (struct sockaddr *)&caddr, &len)) < 0) {
perror("accept");
exit(1);
}
len_peer_sin = sizeof(peer_sin);
getpeername(fd2,(struct sockaddr *)&peer_sin,&len);
peer_host = gethostbyaddr((char *)&peer_sin.sin_addr.s_addr,
sizeof(peer_sin.sin_addr),AF_INET);
if( strcmp( peer_host->h_name, "zeus.kernel.org") == 0 ) {
// access granted
} else {
// access denied
}
.....
|
上記のように、アクセスコントロールはこのような簡単な実装でも可能になります。 tcp wrappersの説明をしているのにtcp wrappersを否定するようなことを言っていますが、 おそらく上記のようなアクセスコントロール(リスト)がスタティックにハードコードされている実装はないでしょう(そう願います)。 tcp wrappersのアクセスコントロールリストは、テキストファイル形式で比較的記述性の高いものです。 もちろんアクセスコントロールリストの構文解析ルーチンとマッチングのルーチンは提供されるのでまじめに一から実装するよりは楽でしょう。
■アクセスコントロールリスト
tcp wrappersでは、/etc/hosts.allow, /etc/hosts.denyの二つのファイルでアクセスコントロールリストを定義します。 基本的に、各行の書式は以下のルールに従います。
daemon_list : client_list [ : option [ : option ... ] ]
tcp wrappersのアクセスコントロールリストの記述ルールを全て説明すると他の内容を書くスペースがなくなりそうなので いくつかの具体例と要点のみの説明にします。詳しくは、hosts_access(5), hosts_options(5)のman ページを参考にしてください。
まずはローカルループバックの以外のアクセスを全て拒否する設定。基本的に全てはここからスタートする。
[hosts.allow]
ALL: 127.0.0.1
[hosts.deny]
ALL: ALL
次にsshdを全てのホストに対して開ける設定とさらにhosts.denyを使わなくてもすむようにALL:ALL:denyをhosts.allowで指定する。
[hosts.allow]
ALL: 127.0.0.1
sshd: ALL
ALL: ALL: deny
さらに上記に加え、telnetを内部のインターフェイス(192.168.0.1)からのみアクセス可能にし、 black.list.org以外は全てftpアクセス可能にする設定です。
[hosts.allow]
ALL: 127.0.0.1
in.telnetd@192.168.0.1 : ALL
in.ftpd : ALL EXCEPT black.list.org
sshd: ALL
ALL: ALL: deny
最後に、black.list.orgからのアクセスがあった場合、相手にメッセージを返す設定を紹介します(ちょっと意地悪)。
[hosts.allow]
ALL: 127.0.0.1
in.telnetd@192.168.0.1 : ALL
in.ftpd : ALL EXCEPT black.list.org
in.ftpd : black.list.org : twist /bin/echo you are black-listed.
sshd: ALL
ALL: ALL: deny
damon_list
deamon_listは、デーモン名を空白で区切ったリストになります。 また各デーモン名は
deamon@host_pattern
のようにしてネットワークインタフェースが2つ以上ある場合に、 どのインタフェースからの要求であるかを明示的に指定できます。 deamon_listは後述するワイルドカードが適用でき、 host_patternには後述するホストパターンが適用できます。
client_list
client_listは、クライアントのホスト名、IPアドレスなどを空白で区切ったリストになります。deamon_list同様に、 ワイルドカード、ホストパターンが適用
option
optionは、allow, denyの属性を指定したり、マッチングによってshellコマンドを起動するような指定が可能です。
パターン
deamon_list, client_listには、次のテーブルの例で示しているようなパターンを記述可能です。
マッチングパターン
| パターン |
マッチング対象(例) |
| zeus.kernel.org |
zeus.kernel.orgにのみマッチ |
| 209.10.41.242 |
209.10.41.242にのみマッチ |
| .kernel.org |
www.kernel.orgなどにマッチ |
| 209.10.41. |
209.10.41.0から209.10.41.255にマッチ |
| 209.10.41./255.255.255.0 |
209.10.41.0から209.10.41.255にマッチ |
ワイルドカード
daemon_list, client_listには、次のようなワイルドカード(or 演算子)の指定が可能です(一部省略)。
ワイルドカード
| ワイドカード |
説明 |
| ALL |
全てのホストにマッチ |
| LOCAL |
ドットを持たない全てのホストにマッチ |
| PARANOID |
名前とアドレスが一致していない全てのホストにマッチ |
| EXCEPT |
演算子: list1 EXCEPT list2のように指定。list2にマッチするものを除きlist1にマッチするものにマッチ |
■ライブラリーlibwrap.aの使い方
ライブラリーlibwrapは、tcp wrappersが提供するアクセスコントロールリストの構文解析とマッチング処理などが まとまったC言語用の(スタティック)ライブラリーです。自分で作成したサーバープログラムにアクセスコントロール機能したい場合に便利です。
基本的な使用手順としては、クライアントからの接続要求を受けた後 (access()関数のあとに)、要求のあったデーモン、 クライアントのIPアドレスなどのクライアント情報をもとに request_init()関数でリクエスト情報の構造体を初期化し、 hosts_access()関数でアクセスコントロールリストとマッチするかテストする。例えば、以下のような実装の流れになります。 ここで注意したいのは、request_init()関数を発行する前に少なくともクライアントのIPアドレスやホスト名を取っておく必要があります。
struct sockaddr_in caddr;
int fd1,fd2;
int len;
struct request_info request;
char ipstr[20];
....
if ((fd2 = accept(fd1, (struct sockaddr *)&caddr, &len)) < 0) {
....
strncpy(ipstr,inet_ntoa( caddr.sin_addr), 16);
request_init(&request,
RQ_DAEMON,"hoge",
RQ_CLIENT_ADDR, ipstr, NULL);
if( hosts_access(&request) ){
do access granted
} else {
do access denied
}
|
request_init()関数で使用可能なキーの一覧を以下のテーブルで示しておきます。また一度設定したrequest_info構造体の値を変更する場合は、 request_set()関数が使用できます(あまりつかわれませんが)。
request_init()で有効なキー
| キー |
変数型 |
説明 |
|
RQ_FILE |
int |
リクエストに関連付けられているFD |
|
RQ_CLIENT_NAME |
char* |
クライアントホスト名 |
|
RQ_CLIENT_ADDR |
char* |
クライアントIPアドレス(printable) |
|
RQ_CLIENT_SIN |
struct sockaddr_in * |
クライアントソケットアドレス(ネットワークバイトオーダー) |
|
RQ_SERVER_NAME |
char* |
サーバー(エンドポイント)ホスト名 |
|
RQ_SERVER_ADDR |
char* |
サーバー(エンドポイント)IPアドレス(printable) |
|
RQ_SERVER_SIN |
struct sockaddr_in * |
サーバー(エンドポイント)ソッケトアドレス(ネットワークバイトオーダー) |
|
RQ_DAEMON |
char* |
デーモン(サービス)名 |
|
RQ_USER |
char* |
リクエスト元のユーザ |
ライブラリーlibwrapを用いたサーバーのサンプルコード hoge.cを用意しておきました。このプログラムは、 アクセスコントロールリストの内容に応じて標準出力に アクセス情報を出力するようなものです。 アクセスコントロール機構の実装を理解するための最低限のコードになっています。
コンパイル方法は、
# cc -Wall -o hoge hoge.c -lwrap -lnsl
では実験に入りましょう。まずはネットワーク環境を正しく設定してください。 次に、hosts.allowを次のように編集します。
[hosts.allow]
hoge: localhost
ALL:ALL:deny
ではサーバーを起動しましょう。
$ ./hoge
クライアントは、telnetです。別なターミナルを開いて次のように実行します。
$ telnet localhost 1115
$ telnet mymachine 1115
正しく動作していれば、サーバーを起動しておいたターミナルで次のような出力を見ることができます (port番号は出力例とは異なっていても問題ありません)。
connect from localhost (127.0.0.1) port 4465
refused connect from mymachine.mydomain (192.168.0.10) port 4466
実験のコツが掴めたなら、hosts.allowの設定内容をいろいろ変更してみて反応がどうなるか確かめてみましょう。
[hoge.c]
#include <stdio.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/uio.h>
#include <unistd.h>
#include <sys/param.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <tcpd.h>
#include <syslog.h>
#define PORTNO 1115
int allow_severity = LOG_INFO;
int deny_severity = LOG_WARNING;
int main(int argc, char *argv[])
{
struct sockaddr_in caddr;
struct sockaddr_in saddr;
int fd,len;
if ((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket");
exit(1);
}
bzero((char *)&saddr, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
saddr.sin_port = htons(PORTNO);
if (bind(fd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0){
perror("bind");
exit(1);
}
if (listen(fd, 1) < 0) {
perror("listen");
exit(1);
}
len = sizeof(caddr);
for(;;) {
int fd2;
struct hostent *peer_host;
struct sockaddr_in peer_sin;
int len_peer_sin;
struct request_info request;
if((fd2 = accept(fd, (struct sockaddr *)&caddr, &len)) < 0) {
perror("accept");
exit(1);
}
len_peer_sin = sizeof(peer_sin);
getpeername(fd2,(struct sockaddr *)&peer_sin,&len);
peer_host = gethostbyaddr((char *)&peer_sin.sin_addr.s_addr,
sizeof(peer_sin.sin_addr),AF_INET);
request_init(&request,
RQ_DAEMON,"hoge",
RQ_CLIENT_NAME,peer_host->h_name, NULL);
if( hosts_access(&request) ){
printf("connect from %.100s (%.20s) port %d\n",
peer_host->h_name,
inet_ntoa(peer_sin.sin_addr),
ntohs(peer_sin.sin_port) );
} else {
printf("refused connect from %.100s (%.20s) port %d\n",
peer_host->h_name,
inet_ntoa(peer_sin.sin_addr),
ntohs(peer_sin.sin_port) );
}
close(fd2);
}
close(fd);
return 0;
}
|
■おまけ
tcp wrappersに関するちょっとしたTipsを数点紹介しておきます。
hosts_ctl
サンプルコード hoge.cでは、request_init()関数とhosts_access()関数の組合せを使っていましたが、 機能限定で一つの関数 hosts_ctl() でアクセスチェックを行なうことができます(大体の用途はこれでよいはず)。
int hosts_ctl(char *daemon,char *name,char *addr,char *user)
|
ちなみにこの関数は、マンページ hosts_access(5)には記述されていますが、ヘッダーには定義されていません。
実際にこの関数は、何のことはない内部処理は以下のようになっています。
int hosts_ctl(daemon, name, addr, user)
char *daemon;
char *name;
char *addr;
char *user;
{
struct request_info request;
return (hosts_access(request_init(&request,
RQ_DAEMON, daemon,
RQ_CLIENT_NAME, name,
RQ_CLIENT_ADDR, addr,
RQ_USER, user,
0)));
}
|
strtok
manページ hosts_access(5)を見ると何やら以下のような怪しげなことが書かれています。
BUGS
hosts_access() uses the strtok() library function. This
may interfere with other code that relies on strtok().
|
サーバーに対してltraceしたり、nm /usr/lib/libwrap.aなどしたら分かりますが、 (コンパイルオプションによりますが)実際にstrtok()は呼ばれません。 BUGSなのは、man ページのほうかもしれません。
freebsd-inetd
[tcp wrappersについて]のとことで若干嘘を言っています。 inetdから起動されるデーモンは tcpd 経由で起動されることによりアクセスコントロールされるようなことを言っていますが、 正しくありません。Linuxでよくつかわれる netkit-baseのinetd場合は、 この説明でもよろしいですが、freebsdのinetdなどは、tcpdを使わず、libwrap.aの関数を呼ぶようになっています。
catサーバー
inetdの変な使い方を紹介しておきます。まずinetd.confに以下のエントリーを追加してください。
cpuinfo stream tcp nowait root /usr/sbin/tcpd /bin/cat /proc/cpuinfo
/etc/servicesにも一つエントリーを追加し、
cpuinfo 8765/tcp
/etc/hosts.allowを以下のエントリーを追加してください。
cat : your-machine
そしておもむろに、your-machine上からinetdが動いているサーバーのポート 8765にtelnetしてみてください。
$ telnet server-machine 8765