オフィシャルサイト ucspi-tcp (D.J.Bernstein)

tcpserver (ucspi-tcp)

同じく D.J.Bernstein 作の qmail を研究している時に出会った、 TCP クライアント-サーバコネクション制御ユーティリティ。 xinetd (inetd) と tcp-wrappers の機能を併せ持ち、特定のサービスへのコネクションの同時成立数を規制したり、クライアントを制限したり、コネクションの成立と同時に目的のデーモンへ環境変数を渡したりできる。起動スクリプトを少し工夫すれば、通常 xinetd 経由でしか起動できない様々なデーモンを、安全に rcスクリプトから起動させることが可能だ。

目次

インストール

本体
ucspi-tcp 0.88 tcpserver を含むパッケージ。
パッチ

個別に入手することも可能(だったが今はほとんどリンク切れ)。 netqmail-1.05 あるいは 1.06 のソースの other_patches ディレクトリに含まれているのでそれらを使用する。

glibc errno glibcが 2.3.1 以降の場合必須。つまり、最近のほとんどの Linux リリースでは、これを当ててコンパイル時のマクロ指定を更新してやらないとコンパイルできない。
ucspi-rss
または
ucspi-tcp-0.88.a_record
ucspi-tcp 付属プログラムのひとつ rblsmtpd を改良する。 SMTPデーモンと併せて使用する可能性があるなら当てておいたほうがいい。 rblsmtpd は、RBLチェックプログラム (リンク集を参照のこと)。qmail-1.03 開発時点では、不正ホスト情報をテキストで提供するRBLが多かったが、現在ではDNSのAレコード問い合わせ式が主流。このパッチは、rblsmtpd を Aレコード問い合わせに対応させる。
ucspi-tcp-0.88.a_record
ucspi-tcp-0.88.nobase
上記 ucspi-rss を当てない場合 (Aレコードサーバを使わない場合) にのみこの2つを適用すべし。 rblsmtpd がデフォルトで問い合わせに行く RBL rbl.maps.vix.com は、もはやテキスト形式をサポートしていない。 nobaseパッチは、 -r オプションが明示されない場合にもそこを見に行こうとしていた rblsmtpd の挙動を止めさせる。

インストール

1. ソースの展開

user$ tar xzvf ucspi-tcp-0.88.tar.gz

2. パッチ当て

user$ cd ucspi-tcp-0.88
user$ patch -p1 < /path/to/the/patch

3. コンパイルとインストール

user$ make
user$ su
root# make setup check

使い方

ルールファイル

tcpserver のルールファイルは、 tcp-wrappershosts.allow, hosts.deny と、 xinetd の設定ファイルを併せたような役割をする。まず、テキストファイルにディレクティブを書き、それを tcprules というコマンドでバイナリフォーマットの cdb データベースファイルに変換、そのファイルを tcpserver 起動時に -x オプションで読み込ませることで利用する。

どこに置いても構わないのだが、筆者の慣例で、 root 所有の /etc/service ディレクトリに置くという仮定で解説を進める。

ルールテキストの作成

ルールは、

[address]:[instruction]

というフォーマットの1行で成る。行頭から行末まで、スペースを紛れ込ませてはいけない。ルールが複数行あれば、上から見ていって一番先にマッチしたルールが適用される。

address

USER@=HOSTNAME あるいは USER@IP のようにクライアントのユーザ名やホスト名を用いることも可能だが、インターネットサーバで利用する上では、ユーザ名を知るには互いに IDENTサービスポートを開けている必要があって現実的でないし、信用できる形でホスト名を利用するには接続の度に 2回の DNS問い合わせ (正引きと逆引き) が発生することになるので滅多に使わない。よって、ここでも IPアドレスのみを取り扱うことにする。
IPアドレスは、ドットやハイフォンを使ってワイルドカード的な指定や範囲指定もできる。以下に例を示す;

192.168.1.1: ずばり 192.168.1.1 そのもの
192.168.1.: 192.168.1.0 から 255
192.168.1.1-14: 192.168.1.1 から 14
192.168.2-15.: 192.168.2.0 から 192.168.15.255
: アドレスを無しにすると全てのアドレスにマッチ
instruction

address にマッチした時に適用する指示。 allow (許可する)、 deny (拒否する) の他に、目的のプログラムへ送る環境変数を定義することもできる。Linux で標準的な env に例えれば `env LANG=en man syslog' のようなことをやっているのだと考えることができる。例を示す;

allow,RELAYCLIENT="" 許可するとともに環境変数 RELAYCLIENT をデーモンに渡す
allow,VARA=%hoge%,VARB=/foo/ カンマ区切りで複数指定することも可能。また、クォートは " である必要はなく、前後を同じ記号で挟みさえすればよい

cdbへの変換

tcprules コマンドを使う。 tcprulesSTDIN から入力を読み取るので、通常はリダイレクトを使用する。

tcprules  変換後cdb名    変換時tempファイル名    < ルールテキスト名

実際の作業例:

root# cd /etc/service
root# tcprules tcp.smtp.cdb tcp.smtp.tmp < tcp.smtp

tcpserverの起動書式

tcpserver [option] [host_or_IP] [port] [program]

option部

主なオプション 意味
-u -g 目的のプログラムを起動する際の UID, GID を指定
-x 読み込むルールファイル (cdb) を指定
-H 相手アドレスの DNS名前解決をしない。つまり $TCPREMOTEHOST を取得しようとしない
-R 相手ユーザ名を問い合わせない。つまり $TCPREMOTEINFO (IDENT情報) を問い合わせない
-l localname サーバ自身のホストネームを DNS 検索せず、代わりに localname で指定した名前を「リゾルブ済み」のものとして解釈する。余計な DNS 検索を抑制できる。引数には、よく localhost またはそれと同義な 0 (ゼロ) を指定する
-c n 同時に成立できるコネクションの最大数を n 本に制限。デフォルトは 40
-v Verbose モード (エラーやステータスをプリント)

host_or_IP部

接続を受け入れる自分のホスト名または IPアドレス。つまり、通常は、接続を受け入れるネットワークインターフェースを指定する意味合いを持つ。例えば localhost と指定すると 127.0.0.1 への要求だけに応ずるし、/etc/hosts ファイルで定義してある自分のホスト名を指定すれば、そのインターフェースからの要求にだけ応じる。 0 にすると全てのインターフェースで待ち受けることになり、実際的にはそうすることが多い。

port部

待ち受けるポート。ポートナンバーそのものか、 /etc/services ファイルで定義されたサービス名を指定する。 0 にすると、空いているポートを使う (滅多にやらない)。

program部

tcpserver が接続を受け入れた暁に実行する、目的のプログラムtcpserver そのものは常駐するようにはできていないので、末尾に & を付けてバックグラウンドジョブ化する必要がある (そうでない例を筆者は今のところ知らない)。

実例集

この章では、主に、通常 xinetd 経由で起動されるようなデーモンを、 tcpserver を使って init.d の rcスクリプト (SysVinit) で起動させる形に切り替えた、 RedHat Linux や Fedora Core での実例を紹介する。目的はそれぞれだが、 rc起動にすると

といった利点がある。

予備知識

xinetd起動を無効にする

これを忘れると、tcpserver で起動しようとした時に "Address already in use" というエラーが出て起動できない。 RedHat 系のディストリビューションでは、 xinetd 経由で呼び出されるプログラムの規定は、 /etc/xinetd.d/ 下の、そのサービスをファイル名とした各ファイルで定義されている。 rcスクリプト起動に切り替える場合には、当該のファイルの中の

disable=no

disable=yes

に変更した上で、xinetd をリスタートする必要がある。

コネクションログ

サーバアプリケーション自体がロギング能力を備えている場合には頓着する必要はないが、そうでない場合には、 tcpserver 起動文の最後でロギングユーティリティへ出力をパイプ渡ししてやる。渡し先のユーティリティは、Linux標準の logger を使う方法と、 qmail に付属する splogger を使うやり方がある。

loggerの場合

firebird の接続ログを daemon ファシリティの info レベルで syslog へ送る例

tcpserver -v ... ... 2>&1 | logger -p daemon.info -t firebird & 
sploggerの場合
tcpserver -v ... ... 2>&1 | splogger telnetd 10 & 

& の手前の 10 は、ファシリティを数字で表したもの (10authprivファシリティ)。省略も可能で、省略した場合のデフォルトは 2 つまり mail ファシリティとなる。数字表記のログファシリティコードと mail, secure, daemon といった "名称" との対応は /usr/include/syslog.h で定義されている。

PID探知スクリプト (detectpid)

tcpserver 経由でバックグラウンドプロセス化したプログラムは、 ps の捉えるコマンド名がどれもこれも "tcpserver" となるためか、 daemon ファンクション (/etc/rc.d/init.d/functions スクリプトで定義されている) では PID が特定できず、 PID ファイル /var/run/xxx.pid の書き出しに失敗してしまうことがある。その結果、一応起動はできても、いざ "service xxxx stop" や "/etc/rc.d/init.d/xxxx stop" で止めようとしても、PID ファイルがないため killproc 関数が動作せず、デーモンが停止できないという状況に陥る。これは、tcpserver コマンド直後に ps の出力を awkgrep でフルイに掛けて自力で PID を見つけ、その値を PIDファイルへ書き出せば解決できる。

ただし、そうしたルーティンを一個一個の rcファイルに書くのは無駄が多く、ミスにもつながるので、筆者は detectpid という別スクリプトファイルにして、 様々な rcスクリプトから利用している。以下に紹介する rcスクリプトでは、 detectpid スクリプトを /usr/local/bin/ へコピーして実行ビットが立ててあることが必須条件になっている。

※ 2007/1/9 detectpidスクリプトを更新し (Ver.2.0)、引数の数の制限をなくした。何かの都合で古いバージョンが必要な人のために Ver.1.0 も一応保持しておく。ただし 1.0 では引数 (検索文字列) は常に 2つでなくてはならない点に注意。

detectpid の使い方:

detectpid {psから探し出すためのフィルタ文字列1} [フィルタ文字列2] [フィルタ文字列3...]
戻り値: PID

例) qmail-smtpd の場合:

detectpid qmail-smtpd tcpserver

ps の仕様やバージョンによるのか、ps の出力形態が多少異なることがあるため、最適なフィルタ文字列は違ってくるかもしれない (例えば RedHat 7.2 と FedoraCore 1 とでは違った)。例えば "tcpserver" と "smtp" を与えてみるなど、いくつかトライしてもらえば PID は必ず探知できるはずだ。なお、引数は正規表現で与えることもできる。ただしその場合、引数が AWK に渡る以前にシェルの解釈を受けるということを頭に置いて、正規表現特殊文字は上手くエスケープしてやらなければならない。

実例

qmail-smtpd/qmail-pop3d

qmail の解説ページでみっちりと説明しているので、そちらを参照していただきたい。もちろん detectpid が必須。

telnetd

起動スクリプトの中核、つまり tcpserver を telnet ポート要求に対して待機させて、要求が来たら telnet サーバ実行ファイルを呼ぶようにさせるコマンドは、下記のようになる。

tcpserver -H -R -l 0 -v -c 5 \
 -x /etc/service/tcp.telnet.cdb 0 telnet \
 in.telnetd 2>&1 | /var/qmail/bin/splogger telnetd 10 &

chkconfig 対応の rcスクリプトの完成版 がこれ。もちろん detectpid が必須。

VSFTPD

tcpserver コマンド部分は以下のようになる。

tcpserver -H -R -l 0 -v -c 5 \
 -x /etc/service/tcp.ftp.cdb 0 ftp \
 vsftpd 2>&1 | /var/qmail/bin/splogger ftpd 10 &

vsftpd の場合、クライアントのアドレスに応じて動作をダイナミックに変えることもできる。 vsftpd はプロセス毎に環境変数 VSFTPD_LOAD_CONF にセットされている主設定ファイルを読み込むので、下のような tcpserver ルールを用意しておけば;

127.0.0.1:allow
192.168.0.:allow
:allow,VSFTPD_LOAD_CONF="/etc/vsftpd/vsftpd.conf.anon"

外からの接続ではアノニマス、 LAN内部からの接続ではシステムユーザのログイン、といった具合に動作を切り替えられる。完成例を置いておくが、vsftpd 自体の設定も絡んでいて複雑なので、必ず VSFTPDのページも一読願いたい。もちろん detectpid が必須。

Firebird CS (Classic Server)

起動部分は下記のようになる。

tcpserver -H -R -l 0 -c 50 -u firebird -g firebird \
 -x /etc/service/tcp.firebird.cdb 0 gds_db \
 fb_inet_server 2>&1 | logger -p daemon.info -t firebird &

rcスクリプト化したものはこれ。もちろん detectpid 必須

簡易版 何にでもなれるTCP試験サーバスクリプト(旧)

tcpserver の基本動作はディスクリプタ 0 でネットワークからの入力を読み取り、ディスクリプタ 1 でネットワークへ出力するというものなので、簡単なシェルスクリプトを組んで、特定のポートで TCP コネクション待ち受けてクライアントへ某かのレスポンスを返すサーバプログラムを作成することもできる。例えば、特定の TCP ポートへの疎通試験ツールとしての用途が考えられる。試しに作成したのが、下記の実に簡易なサーバスクリプトだ。

multiserver@ は、以下のようなコールの仕様とした;

multiserver@ [-p LISTEN_PORT] {start|stop|list}
引数 アクション
LISTEN_PORT 待ち受けポート (数字での指定のみ想定)。省略した場合には multiserver@ スクリプト内で定義した $DEFAULTPORT (デフォルトは 23 つまり telnet) で待ち受ける。
start LISTEN_PORT で待ち受ける tcpserver プロセスを生成する。
stop LISTEN_PORT で待ち受けている tcpserver プロセスに TERM シグナルを送る、つまり停止する。
list 現在待機している tcpserver プロセスの待ち受けポート番号をリスト表示する。-p オプションは、指定したとしても無視される。

改良版 サーバ/クライアントプログラム作成例 (tcpresponder/tcpnotify)

上記を少し改良して、クライアント - サーバ間での連絡に使えるようにしたのがこの tcpresponder ツールだ。下記のファイルで構成されている。

使い方

準備:

  1. tcpresponder 用のログディレクトリを作る。お仕着せで使うなら /var/log/tcpresponder/ を作っておく。
  2. /etc/sysconfig/tcpresponder ファイルを確認し、待ち受けるTCPポート番号を変えたければ DEFAULTPORT 変える。デフォルトは 48023 とした。
  3. クライアントプログラム tcpnotify の冒頭付近にある REMHOST をサーバの IP に書き換える。また、項番2 で待ち受けポートをカスタマイズした場合は、PORT をそれに合わせる。
  4. tcpresponder から呼び出される respondercmd はクライアントの IP に基づいたフィルタを組み込んでいるので、冒頭付近の case 分岐内のマッチングIP を実際の接続クライアントの IP に合わせる。

実行:

[tcpresponderサーバ]

--SysVinit版--

/etc/init.d/tcpresponder [-p LISTEN_PORT] {start|stop|status}

通常は `-p 待ち受けポート' は不要で、DEFAULTPORT が使われる。-p オプションはパートタイム的に他のポートでも上げられるように残しただけだ。PIDファイルやロックファイルはポート番号毎に作られるようにしたので、同時に上げることも可能。「準備」でも書いたように、respondercmd で定義していないクライアントIP から接続が来たり、メッセージの頭に `TCPRESPONSE=' が付いていなかったり(ナンチャッテプロトコルですナ)、メッセージが空だった場合には、即座に接続を切り、その旨をログに残す。

tcpserver は、クライアントからの接続が確立すると、引数のプログラムを起動し、ネットワークからの入力をプログラムのディスクリプタ 0 へ注ぎ込み、プログラムからディスクリプタ 1 への出力をネットワークへ送る。respondercmd スクリプトはそのことに基づいて書いてある。respondercmd の `# Here, do whatever you want. Below is a sample procedure.' 以降はお好みの処理に書き換えて使っていただきたい。

--Upstart版--

{start|stop|status} tcpresponder [port=LISTEN_PORT]

引数として、この他に tcpcmd=COMMANDtcpopt="tcpserver_OPTION " を指定して挙動を変えることもできる。ただし、異なるポートで待つインスタンスを幾つも同時に立ち上げたい場合は、/etc/init/tcpresponder.conf を次のように書き換える必要がある。

#start on started rc RUNLEVEL=[2345]    <--自動起動しないようコメントアウト
stop on started rc RUNLEVEL=[S016]
 
instance $port        <--これを挿入する
console output
 
script
    [ -f /etc/sysconfig/tcpresponder ] && . /etc/sysconfig/tcpresponder
    : ${tcpopt:=$TCPOPT}
    : ${port:=$DEFAULTPORT}
    : ${tcpcmd:=$TCPCMD}
 
    /usr/local/bin/tcpserver $tcpopt $port $tcpcmd
end script

上記のように複数立ち上げ用に書き換えた場合は、Upstart の特性上、起動時に port=xxx だけは必ず付けなくてはならない(だから自動起動はできない)。

[tcpnotifyクライアント]

tcpnotify MESSAGE TO SEND 

tcpclient は、サーバとの接続が確立すると、引数のプログラムを起動し、プログラムからディスクリプタ 7 への出力をネットワークへ送り、ネットワークからの入力をディスクリプタ 6 で受け取る。tcpsend はそれに基づいた非常に単純なシェルスクリプトだ。サーバからの返答を受け取る必要がなければ tcpsend の中の `cat <&6' は削除してしまっても構わない。tcpnotify はメッセージの頭に必ず `TCPRESPONSE=' を付けてからサーバへ送信する。