特権ポートとリモート認証


クライアントでのbindの実行

サーバー側では必ず bindを実行して、ソケットを指定の ポートと接続していましたが、クライアントでは bindは実行しませんでした。 なぜでしょう。それはクライアント側での connect や sendto の際に、OSが クライアント側のポートを自動的に適切に割り当ててくれるからです。
クライアント側に自動的に割当たられるポート番号は規約により1025番以降です。 OSはこの1025から順番に bind を試みて、空いているポートに bindします。
他方、Unix domainの通信の場合はクライアント側の Unix domain ソケットの名前は空でありません。

普通はクライアント側のポート番号や UNIX domain ソケットを気にする ことはありませんが、特殊な事情により特定の番号のポートや特定の PATHの UNIX domain ソケットを使ってクライアントにネットワーク通信を させたいことがあります。そのような場合に、クライアント側で bind を 明示的に実行します。

Local domain の場合は以下のようにして connect もしくは sendto の 前に bind を実行して指定の UNIX domain ソケットから発信するようにします。

#define CLINET_SOCKNAME "/tmp/c_udsock" unlink( CLIENT_SOCKNAME ); sockaddr_un c_addr = SOCKADDR_UN_INIT( AF_LOCAL, CLIENT_SOCKNAME ); bind( s, (sockaddr*)&c_addr, sizeof(c_addr) );
Internet domainの場合は以下のようにして connect もしくは sendto の 前に bind を実行して指定のポートから発信するようにします。
#define CLIENT_PORT htons(5001) sockaddr_in c_addr = SOCKADDR_IN_INIT( AF_INET, CLIENT_PORT, InAddr(htonl(INADDR_ANY)) ); if( bind( s, (sockaddr*)&c_addr, sizeof(c_addr) ) < 0 ){ perror("clinet bind"); exit(1); }
もちろん、このポート番号が空いていないとなりません。 一般ユーザーは 1024番以下のポートへの bind は禁止され、 5001番以降のポートへの bind が推奨されています。


特権ポートによるリモート認証の論理

さて、このようにクライアント側のポートを自分の好きな値にすることに 何の意味があるのでしょうか? 直前にも言及しましたが 一般ユーザーは1024番以下のポートに bindすることはできません。 逆に言えば root 権限ならば 1024番以下の空いているポートに bindすることが できるのです。これを特権ポートと呼びます。 一般ユーザーがクライアントプログラムを実行して そのプロセスがroot権限を持つように set uid bit を立てておくのです。

root権限を持つプロセスのプログラムというのは プログラマが細心の注意を払って作るものです。実行しているユーザーが誰なのか 調べたり、本当にその人なのかを確認したりします。 なのでrootのプロセスがネットワーク通信での他のマシンのプロセスに対して 言うことはきっと信用できると思われます。もちろんそのマシンの管理人が 信用できることが前提ですが。

これを逆に見れば、1024番以下のポートからクライアントがアクセスして きたのなら、これはroot権限をもつプロセスであり、もしそのクライアントホストが 信頼できるホストなら、そこのrootのプロセスは信頼でき、 このネットワーク通信でクライアントが通達してくることは 信用できる。という論理になるのです。

この信頼できるホストのリストというのは /etc/hosts.equiv です。 このファイルには、このサーバーが信頼するリモートのマシンと リモートのユーザー名が以下のような形式で列挙されています。

nicehost.somewhere.com niceguy reliable.somewhere.com
2番目の例はホスト reliable.somewhere.com の全ユーザーを信頼するということです。
サーバーにアカウントを持つユーザーの各自のホームディレクトリの下に 置く .rhostsというファイルには、そのユーザーが信頼するリモートのマシンと リモートのユーザー名を同様な形式で列挙することができます。

クライアントが果してこの信頼できるリストに載っているかどうかのチェックは iruserok関数が一手に行ってくれます。その使い方は後で説明します。

このようにリモートのクライアントを信頼して、そこのrootプロセスの ネットワーク通信を信頼して、種々の特別なサービスを提供するのです。 その例としては rlogin, rsh, rcp, popperのRPOP, youbinのメールヘッダー 通知機能、などがあります。


特権ポートのbind と、特権ポートからのacceptとリモート認証

特権ポートをbindするようなクライアント側の関数は以下のようになるでしょう。

#include <errno.h> int bind_privileged( int s ) { sockaddr_in c_addr = SOCKADDR_IN_INIT( AF_INET, htons(0), InAddr(htonl(INADDR_ANY)) ); for( u_short port=600; port<1024; port++ ){ c_addr.sin_port = htons(port); if( bind( s, (sockaddr*)&c_addr, sizeof(c_addr) ) == 0 ) return 0; if( errno != EADDRINUSE ){ return -1; } } errno = EADDRNOTAVAIL; return -1; }
1024以下と言いましたが、実際に特権ポートとして使うことが推奨されているのは 600番から1023番までのようです。

サーバー側で特権ポートからの接続をacceptする関数は以下のようになるでしょう。 /etc/hosts.equivで信頼できるホストからのアクセスのみを許すことにします。 ユーザー名はチェックしないことにします。

int accept_privileged( int sb ) { sockaddr_in c_addr; socklen_t len = sizeof(c_addr); int s; if( (s=accept( sb, (sockaddr*)&c_addr, &len )) < 0 ) return -1; u_short port = ntohs(c_addr.sin_port); if( port<600 || 1023<port ){ close(s); errno = ECONNREFUSED; return -1; } if( iruserok( c_addr.sin_addr.s_addr, 0, "nobody", "nobody" ) < 0 ){ close(s); errno = EACCES; return -1 } return s; }
iruserok関数の引数 "nobody" はちょっとしたズルです。本当はここには クライアントが通知してくるユーザー名を指定するべきなのです。でも 今はホストのユーザーごとのチェックはしないことにしているので これでいいのです。

iruserok関数の第1引数にクライアントのIPアドレスを u_long 型で与えます。 第3引数はクライアント側のユーザー名で、 /etc/hosts.equiv や .rhosts 中に 記述されたユーザー名と照合するために使われます。 第4引数はサーバー側のユーザー名で、サーバーのホームディレクトリの .rhosts のパスを作るために使われます。 第2引数に0を与えると、iruserok関数は /etc/hosts.equiv を先に調べて 該当しなければ .rhosts も次に調べます。第2引数に1を与えると、 iruserok関数は .rhosts だけを調べます。 これらの信頼リストにユーザー名が指定されていない場合は 第3引数と第4引数は同一ユーザー名でないとなりません。


目次

Copyright(C) by Naoki Watanabe. Jan 1st, 2001.
渡辺尚貴 naoki@cms.phys.s.u-tokyo.ac.jp