ONC+ 開発ガイド

第 5 章 RPC プログラミングの高度なテクニック

この節では、RPC の下位レベルインタフェースを使用するさまざまな開発テクニックを説明します。この章で説明する項目を次に示します。

サーバー側の poll( )

この節で説明する内容は、(デフォルトの) シングルスレッドのモードで RPC を実行する場合にだけ適用されます。

RPC 要求をサービスしたり、その他のアクティビティを実行したりするプロセスでは、svc_run() を呼び出せない場合があります。他のアクティビティで定期的にデータ構造を更新する場合は、プロセスから svc_run() を呼び出す前に SIGALRM 信号をセットできます。そうすると、シグナルハンドラがデータ構造を処理してから svc_run() に制御を戻します。

プロセスから svc_run() をバイパスして直接ディスパッチャにアクセスするには、svc_getreqset() を呼び出します。プロセスには、待っているプログラムに結合したトランスポート端点のファイル記述子が指定されている必要があります。その場合、プロセスは自分で poll() を呼び出して、RPC ファイル記述子と自身の記述子の両方で要求を待つことができます。

例 5–1 には svc_run() を示します。 svc_pollset は、_rpc_select_to_poll() の呼び出しを通して svc_fdset() から派生した pollfd 構造体の配列です。この配列は、RPC ライブラリルーチンのどれかが呼び出されるたびに変わる可能性があります。そのたびに記述子がオープンされ、クローズされるからです。poll() がいくつかの RPC ファイル記述子への RPC 要求の到着を確認すると、svc_getreq_poll() が呼び出されます。


注 –

__rpc_dtbsize()__rpc_select_to_poll () は、SVID の一部ではありませんが、libnsl ライブラリで使用できます。Solaris 以外でも実行できるように、これらの関数を作成するために、関数の仕様を説明します。


ビットフラグとして fd_set ポインタとチェックすべきビット数が指定されます。 関数 __rpc_select_to_poll では、指定された pollfd 配列を RPC が使用するために初期化するようにします。RPC は、入力イベントだけをポーリングします。初期化された pollfd スロット数が返されます。この関数の引数は次のとおりです。

int __rpc_select_to_poll(int fdmax, fd_set *fdset,
					struct pollfd *pollset)

関数 __rpc_dtbsize() は、getrlimit() 関数を呼び出し、新しく作成された記述子にシステムが割り当てる最大値を決定します。結果は、効率化のためにキャッシュされます。

この節の SVID ルーチンについての詳細は、rpc_svc_calls(3NSL) および poll(2) のマニュアルページを参照してください。


例 5–1 svc_run()poll()

void
svc_run()
{
 int nfds;
	int dtbsize = __rpc_dtbsize();
	int i;
	struct pollfd svc_pollset[fd_setsize];
 
	for (;;) {
		/*
		 * 要求待ちするサーバー fd があるかどうかをチェック
 	 */
		nfds = __rpc_select_to_poll(dtbsize, &svc_fdset,
 	                            svc_pollset);
		if (nfds == 0)
 		break;	/* 要求待ちの fd がないので終了 */
 
		switch (i = poll(svc_pollset, nfds, -1)) {
		case -1:
	 	/*
			 * エラーが起こった場合は、poll() ではなく、シグナルハンドラなど
 		 *  外部イベントによるものと考えて、無視して継続する
			 */
		case 0:
			continue;
		default:
	 	svc_getreq_poll(svc_pollset, i);
		}
	}
}
 

ブロードキャスト RPC

RPC のブロードキャストが要求されると、メッセージはネットワーク上の rpcbind デーモンに送られます。要求されたサービスが登録されている rpcbind デーモンは、その要求をサーバーに送ります。ブロードキャスト RPC と通常の RPC 呼び出しとの主な相違点を次に示します。

次に、rpc_broadcast() の使用方法を示し、引数を説明します。


例 5–2 RPC ブロードキャスト

/*
 * bcast.c: RPC ブロードキャストの使用例
 */
 
#include <stdio.h>
#include <rpc/rpc.h>
 
main(argc, argv)
 int argc;
	char *argv[];
{
	enum clnt_stat rpc_stat;
	rpcprog_t prognum;
	rpcvers_t vers;
	struct rpcent *re;
 
	if(argc != 3) {
		fprintf(stderr, "usage : %s RPC_PROG VERSION\n", argv[0]);
		exit(1);
	}
	if (isdigit( *argv[1]))
		prognum = atoi(argv[1]);
	else {
		re = getrpcbyname(argv[1]);
		if (! re) {
			fprintf(stderr, "Unknown RPC service %s\n", argv[1]);
 		exit(1);
		}
		prognum = re->r_number;
 }
	vers = atoi(argv[2]);
	rpc_stat = rpc_broadcast(prognum, vers, NULLPROC, xdr_void,
	           (char *)NULL, xdr_void, (char *)NULL, bcast_proc,
NULL);
	if ((rpc_stat != RPC_SUCCESS) && (rpc_stat != RPC_TIMEDOUT)) {
	 fprintf(stderr, "broadcast failed: %s\n",
		         clnt_sperrno(rpc_stat));
 	exit(1);
	}
	exit(0);
}
 
 

例 5–3 の関数では、ブロードキャストに対する応答を収集します。通常は、最初の応答だけを取り出すか、応答をすべて収集します。bcast_proc() は、応答を返したサーバーの IP アドレスを表示します。この関数は FALSE を返して応答の収集を続けます。したがって、RPC クライアントコードはタイムアウトになるまでブロードキャストを再送信し続けます。


例 5–3 ブロードキャストへの応答の収集

bool_t
bcast_proc(res, t_addr, nconf)
 void *res;									/* 応答なし */
	struct t_bind *t_addr;					/* 応答したアドレス */
	struct netconfig *nconf;
{
 register struct hostent *hp;
	char *naddr;
 
	naddr = taddr2naddr(nconf, &taddr->addr);
	if (naddr == (char *) NULL) {
		fprintf(stderr,"Responded: unknown\n");
 } else {
		fprintf(stderr,"Responded: %s\n", naddr);
 	free(naddr);
	}
	return(FALSE);
} 

TRUE が返されるとブロードキャストは終了し、rpc_broadcast() は正常終了します。FALSE が返された場合は、次の応答を待ちます。数秒間待ってから、要求が再びブロードキャストされます。応答が返されない場合は、rpc_broadcast()RPC_TIMEDOUT を返します。

バッチ処理

RPC の設計方針では、クライアントは呼び出しメッセージを送信して、サーバーがそれに応答するのを待ちます。すなわち、サーバーが要求を処理する間、クライアントは停止していることになります。これは、クライアントが各メッセージへの応答を待つ必要がないときには非効率です。

RPC のバッチ処理を使用すると、クライアントは非同期に処理を進めることができます。RPC メッセージは呼び出しパイプラインに入れてサーバーに送られます。バッチ処理では次のことが必要になります。

サーバーはそれぞれの呼び出しに対しては応答しないので、クライアントは、サーバーが前の呼び出しを処理している間に平行して次の呼び出しを送信できます。トランスポートは複数の呼び出しメッセージをバッファリングし、システムコール write() で一度にサーバーに送信します。このバッファリングにより、プロセス間通信のオーバヘッドが減少し、一連の呼び出しに要する総時間が短縮されます。クライアントは終了前に、パイプラインをフラッシュする呼び出しをバッチにしないで実行します。

次に、バッチ処理を使用しないクライアント側プログラムを示します。文字配列 buf を走査して文字列を順に取り出し、1 つずつサーバーに送信します。


例 5–4 バッチ処理を使用しないクライアントプログラム

#include <stdio.h>

#include <rpc/rpc.h>
#include "windows.h"
 
main(argc, argv)
	int argc;
	char **argv;
{
	struct timeval total_timeout;
	register CLIENT *client;
	enum clnt_stat clnt_stat;
	char buf[1000], *s = buf;
 
	if ((client = clnt_create( argv[1], WINDOWPROG, WINDOWVERS,
								"circuit_v")) == (CLIENT *) NULL) {
		clnt_pcreateerror("clnt_create");
		exit(1);
	}
 
	total_timeout.tv_sec = 20;
	total_timeout.tv_usec = 0;
	while (scanf( "%s", s ) != EOF) {
		if (clnt_call(client, RENDERSTRING, xdr_wrapstring, &s,
		   xdr_void, (caddr_t) NULL, total_timeout) != RPC_SUCCESS) {
			clnt_perror(client, "rpc");
			exit(1);
		}
	}
 
	clnt_destroy( client );
	exit(0);
}

次に、バッチ処理を使用したクライアント側プログラムを示します。各文字列を送信した後に応答は待ちません。サーバーからの終了応答だけを待ちます。


例 5–5 バッチ処理を使用するクライアントプログラム

#include <stdio.h>

#include <rpc/rpc.h>
#include "windows.h"
 
main(argc, argv)
	int argc;
	char **argv;
{
	struct timeval total_timeout;
	register CLIENT *client;
	enum clnt_stat clnt_stat;
	char buf[1000], *s = buf;
 
	if ((client = clnt_create( argv[1], WINDOWPROG, WINDOWVERS,
									"circuit_v")) == (CLIENT *) NULL) {
		clnt_pcreateerror("clnt_create");
		exit(1);
	}
	timerclear(&total_timeout);
	while (scanf("%s", s) != EOF)
		clnt_call(client, RENDERSTRING_BATCHED, xdr_wrapstring,
		           &s, xdr_void, (caddr_t) NULL, total_timeout);
	/* Now flush the pipeline */
	total_timeout.tv_sec = 20;
	clnt_stat = clnt_call(client, NULLPROC, xdr_void,
	         (caddr_t) NULL, xdr_void, (caddr_t) NULL,
total_timeout);
	if (clnt_stat != RPC_SUCCESS) {
		clnt_perror(client, "rpc");
		exit(1);
	}
	clnt_destroy(client);
	exit(0);
}

次に、バッチ処理を使用した場合のサーバーのディスパッチ部分を示します。サーバーは、メッセージを送信しないので、クライアント側は、失敗に気付きません。


例 5–6 バッチ処理を行うサーバー

#include <stdio.h>
#include <rpc/rpc.h>
#include "windows.h"
 
void
windowdispatch(rqstp, transp)
	struct svc_req *rqstp;
	SVCXPRT *transp;
{
 char    *s = NULL;
 
	switch(rqstp->rq_proc) {
 	case NULLPROC:
			if (!svc_sendreply( transp, xdr_void, NULL))
 			fprintf(stderr, "can't reply to RPC call\n");
			return;
 	case RENDERSTRING:
			if (!svc_getargs( transp, xdr_wrapstring, &s)) {
				fprintf(stderr, "can't decode arguments\n");
 			/* 呼び出し側にエラーを通知 */
				svcerr_decode(transp);
 			break;
			}
			/* 文字列 s を処理するコード */
 		if (!svc_sendreply( transp, xdr_void, (caddr_t) NULL))
	 		fprintf( stderr, "can't reply to RPC call\n");
			break;
 	case RENDERSTRING_BATCHED:
			if (!svc_getargs(transp, xdr_wrapstring, &s)) {
				fprintf(stderr, "can't decode arguments\n");
 			/* プロトコルエラーのため何も返さない */
				break;
			}
 		/* 文字列 s を処理するコード。ただし応答はしない。 */
			break;
 	default:
			svcerr_noproc(transp);
			return;
 }
	/* 引数の復号化で割り当てた文字列を解放 */
	svc_freeargs(transp, xdr_wrapstring, &s);
}


注 –

バッチ処理によるパフォーマンスの向上を調べるために、例 5–4例 5–6 で 25144 行のファイルを処理しました。このサービスは、ファイルの各行を引き渡すだけの簡単なサービスです。バッチ処理を使用した方が、使用しない場合の 4 倍の速さで終了しました。


認証

RPC のクライアントとサーバーを作成するときにさまざまなトランスポートを指定できるように、RPC クライアントにもさまざまなタイプの認証メカニズムを採用できます。RPC の認証サブシステムはオープンエンド型です。したがって、認証はさまざな使用法がサポートされます。認証プロトコルは、付録 B 「RPC プロトコルおよび言語の仕様」 で詳細に定義されています。

次の表で、RPC が現在サポートしている認証タイプを示します。

表 5–1 RPC が現在サポートしている認証タイプ

メソッド 

説明 

AUTH_NONE

デフォルト。認証は実行されない 

AUTH_SYS

UNIX オペレーティングシステムのプロセスアクセス権を基にした認証タイプ 

AUTH_SHORT

サーバーによっては効率向上のため AUTH_SYS の代わりに AUTH_SHORT を使用できる。AUTH_SYS 認証を使用するクライアントプログラムは、サーバーからの AUTH_SHORT 応答ベリファイアを受信できる。詳細は、 付録 B 「RPC プロトコルおよび言語の仕様」 を参照してください。

AUTH_DES

DES 暗号化技法を基にした認証タイプ 

AUTH_KERB

DES フレームワークを基にした Version 5 Kerberos 認証形式 

呼び出し側が次の方法で RPC クライアントハンドルを新規作成する場合を考えます。

clnt = clnt_create(host, prognum, versnum, nettype);

この場合対応するクライアント作成ルーチンが次のように認証ハンドルを設定します。

clnt->cl_auth = authnone_create();
 
 

新たな認証インスタンスを作成するときは、auth_destroy(clnt->cl_auth) を使用して現在のインスタンスを破棄します。この操作はメモリーの節約のために必 要です。

サーバー側では、RPC パッケージがサービスディスパッチルーチンに、任意の認証スタイルが結合されている要求を渡します。サービスディスパッチルーチンに渡された要求ハンドルには、rq_cred という構造体が入っています。その構成は、認証資格タイプを示すフィールドを除いて、ユーザーから隠されています。

/*
 * 認証データ
 */
struct opaque_auth {
   enum_t    oa_flavor;		/* 資格スタイル */
   caddr_t   oa_base;			/* より詳細な認証データのアドレス */
   u_int     oa_length; 	/* 最大 MAX_AUTH_BYTES まで */
};

RPC パッケージでは、サービスディスパッチルーチンに対して次のことを保証しています。

AUTH_SYS タイプの認証

クライアント側で AUTH_SYS (旧バージョンでは AUTH_UNIX) タイプの認証を使用 するには、RPC クライアントハンドルの作成後に clnt–>cl_auth を次のように設定します。

clnt->cl_auth = authsys_create_default();

以降は、この clnt を使用した RPC 呼び出しでは、次に示す資格 - 認証構造体が渡されます。


例 5–7 AUTH_SYS タイプの資格 - 認証構造体

/*
 * AUTH_SYS タイプの資格
 */
struct authsys_parms {
	u_long aup_time;		/* 資格作成時刻 */
 char *aup_machname;			/* クライアント側のホスト名 */
	uid_t aup_uid;	 		/* クライアント側の実効 uid */
	gid_t aup_gid;			/* クライアント側の現在のグループ ID */
	u_int aup_len;			/* aup_gids の配列の長さ */
 gid_t *aup_gids;			/* ユーザーが所属するグループの配列 */
};

rpc.broadcast では、デフォルトで AUTH_SYS タイプの認証になります。

次に、手続きを使用し、ネットワーク上のユーザー数を返すサーバープログラムである RUSERPROC_1 () を示します。認証の例として AUTH_SYS タイプの資格をチェックし、呼び出し側の uid16 の場合は要求に応じないようにしてあります。


例 5–8 認証データをチェックするサーバープログラム

nuser(rqstp, transp)
	struct svc_req *rqstp;
 SVCXPRT *transp;
{
	struct authsys_parms *sys_cred;
 uid_t uid;
	unsigned int nusers;
 
	/* NULLPROC の場合は認証データなし */
	if (rqstp->rq_proc == NULLPROC) {
 	if (!svc_sendreply( transp, xdr_void, (caddr_t) NULL))
		 fprintf(stderr, "can't reply to RPC call\n");
		return;
 }
 
	/* ここで uid を取得 */
	switch(rqstp->rq_cred.oa_flavor) {
		case AUTH_SYS:
			sys_cred = (struct authsys_parms *) rqstp->rq_clntcred;
			uid = sys_cred->aup_uid;
 		break;
		default:
			svcerr_weakauth(transp);
 		return;
	}
	switch(rqstp->rq_proc) {
 	case RUSERSPROC_1:
		/* 呼び出し側が、この手続きの呼び出し資格を持っているかどうか確認 */
 		if (uid == 16) {
				svcerr_systemerr(transp);
 
	 		return;
			}
			/*
			 * ユーザー数を求めて変数 nusers に設定するコード
			 */
			if (!svc_sendreply( transp, xdr_u_int, &nusers))
				fprintf(stderr, "can't reply to RPC call\n");
			return;
		default:
 		svcerr_noproc(transp);
			return;
	}
} 

次の点に注意してください。

最後の点で重要なのは、RPC の認証パッケージとサービスの関係です。RPC は認証を処理しますが、個々のサービスへのアクセス制御は行いません。サービス自体でアクセス制御の方針を決め、それがプロトコル内で戻り値として反映されるようにしなければなりません。

AUTH_DES タイプの認証

AUTH_SYS タイプより厳しいセキュリティレベルが要求されるプログラムでは、AUTH_DES タイプの認証を使用します。AUTH_SYS タイプは AUTH_DES タイプに簡単に変更できます。たとえば、authsys_create_default() を使用する代わりに、プログラムから authsys_create() を呼び出し、RPC 認証ハンドルを変更して目的のユーザー ID とホスト名を設定することができます。

AUTH_DES タイプの認証を使用するには、サーバー側とクライアント側の両方のホストで、keyserv() デーモンが実行されている必要があります。また、NIS または NIS+ ネームサービスも実行されている必要があります。両方のホスト上のユーザーに対してネットワーク管理者が割り当てた公開鍵 / 秘密鍵ペアが、publickey() のデータベースに入っていなければなりません。ユーザーは keylogin() のコマンドを実行して自分の秘密鍵を暗号化しておく必要があります。通常ログインパスワードと Secure RPC パスワードが同一の場合には、これを login() で行います。

AUTH_DES タイプの認証を使用するには、クライアントが認証ハンドルを正しく設定しなければなりません。その例を次に示します。

cl->cl_auth = authdes_seccreate(servername, 60, server,
 					       (char *)NULL);
 

最初の引数は、サーバープロセスのネットワーク名か、サーバープロセスの所有者のネット名です。サーバープロセスは通常 root プロセスで、次の関数呼び出しでネット名を得ることができます。

char servername[MAXNETNAMELEN];
host2netname(servername, server, (char *)NULL);

servername は受信文字列へのポインタで、server はサーバープロセスが実行されているホスト名です。サーバープロセスがスーパーユーザー以外のユーザーから起動されている場合は、次のように user2netname() を呼び出します。

char servername[MAXNETNAMELEN];
user2netname(servername, serveruid(), (char *)NULL);

serveruid() はサーバープロセスのユーザー ID です。どちらの関数も最後の引数は、サーバーを含むドメイン名です。NULL を指定すると、ローカルドメイン名が使用されます。

authdes_seccreate() の第 2 引数では、このクライアントの資格の存在時間 (ウィンドウとも呼ばれる) を指定します。この例では 60 秒が指定されているので、この資格はクライアント側が RPC 呼び出しを行なってから、60 秒間で失われます。プログラムから再びこの資格を使用しようとしても、サーバー側の RPC サブシステムは、資格がすでに失われていることを知って、資格を失ったクライアントからの要求に答えません。また資格の存在時間中に別のプログラムがその資格を再使用しようとしても拒否されます。サーバー側の RPC サブシステムが最近作成された資格を保存していて、重複して使用できないようにするためです。

authdes_seccreate() の第 3 引数は、クロックを同期させる timehost 名です。AUTH_DES タイプの認証を使用するには、サーバーとクライアントの時間が一致していなければなりません。例 5–8 では、サーバーに同期させています。(char *)NULL と指定すると同期しません。この指定は、クライアントとサーバーがすでに同期していることが確実な場合にだけ行なってください。

authdes_seccreate() の第 4 引数は、タイムスタンプとデータとを暗号化するための DES 暗号化キーへのポインタです。例 5–8 のように、 (char *)NULLchar *)NULL と指定した場合は、ランダムキーが選択されます。このキーは、認証ハンドルの ah_key フィールドに入っています。

サーバー側はクライアント側より簡単です。次に、例 5–8 のサーバーを AUTH_DES タイプの認証を使用するように変更したものを示します。


例 5–9 AUTH_DES タイプの認証を使用するサーバー

#include <rpc/rpc.h>
	...
 ...
nuser(rqstp, transp)
	struct svc_req *rqstp;
 SVCXPRT *transp;
{
	struct authdes_cred *des_cred;
 uid_t uid;
	gid_t gid;
	int gidlen;
 gid_t gidlist[10];
 
	/* NULLPROC の場合は認証データなし */
 if (rqstp->rq_proc == NULLPROC) {
		/* 元のプログラムと同じ */
 }
	/* ここで uid を取得 */
	switch(rqstp->rq_cred.oa_flavor) {
		case AUTH_DES:
			des_cred = (struct authdes_cred *) rqstp->rq_clntcred;
			if (! netname2user( des_cred->adc_fullname.name, &uid,
			                    &gid, &gidlen, gidlist)) {
				fprintf(stderr, "unknown user: %s\n",
 			         des_cred->adc_fullname.name);
				svcerr_systemerr(transp);
 			return;
			}
			break;
		default:
 		svcerr_weakauth(transp);
			return;
	}
 /* 以降は元のプログラムと同じ */

netname2user() ルーチンは、ネットワーク名 (またはユーザーの netname) をローカルシステム ID に変換することに注意してください。このルーチンはグループ ID も返します (この例では使用していません)。

AUTH_KERB 認証形式

SunOS 5.x は、klogin 以外の Kerberos V5 の大部分のクライアント側機能をサポートします。AUTH_KERBAUTH_DES と概念的に同じです。主な違いは、DES がネットワーク名と暗号化された DES セッションキーを引き渡すのに対し、Kerberos は、暗号化されたサービスチケットを引き渡すことです。実装状態と相互運用性に影響を及ぼすその他の要因については、このあとで説明します。

Kerberos はその資格が有効である時間ウィンドウの概念を使用します。クライアントまたはサーバーのクロックを制限しません。具体的には、windowauthkerb_seccreate() に引数として渡します。この場合、ウィンドウは変わりません。timehostauthkerb_seccreate() の引数として指定されると、クライアント側は timehost から時刻を取得して、時刻の差異によってタイムスタンプを変更します。時刻を同期化するには、さまざまな方法が使用できます。詳細は、 kerberos_rpc のマニュアルページを参照してください。

Kerberos ユーザーは、一次名、インスタンス、領域によって識別されます。RPC 認証コードは、領域とインスタンスを無視しますが、Kerberos ライブラリコードは無視しません。ユーザー名は、クライアントとサーバー間で同じであると仮定します。これによって、サーバーは一次名をユーザー ID 情報に変換することができます。周知の名前として 2 つの書式が使用されます (領域は省略されます)。

Kerberos は、完全資格名 (チケットとウィンドウを含むもの) の送信時に暗号文ブロックチェイン (CBC: Cipher Block Chaining) モード、それ以外の場合は、電子コードブック (ECB: Electronic Code Book) モードを使用します。CBC と ECB は、DES 暗号化のための 2 つの方法です。セッションキーは、CBC モードに対する初期入力ベクトルとして使用されます。次の表記では、XDR が objecttype とみなして使用されることを示しています。

xdr_type(object)

次のコードセクションの長さ (資格またはベリファイアのバイト数) を、4 バイト単位に丸めたサイズで表されます。完全資格名およびベリファイアは、次のようになります。

xdr_long(timestamp.seconds)
xdr_long(timestamp.useconds)
xdr_long(window)
xdr_long(window - 1)

セッションキーに等しい入力ベクトルを持つ CBC で暗号化を行うと、出力結果は次のような 2 つの DES 暗号化ブロックになります。

CB0
CB1.low
CB1.high

資格は、次のようになります。

xdr_long(AUTH_KERB)
xdr_long(length)
xdr_enum(AKN_FULLNAME)
xdr_bytes(ticket)
xdr_opaque(CB1.high) 

ベリファイアは、次のようになります。

xdr_long(AUTH_KERB)
xdr_long(length)
xdr_opaque(CB0)
xdr_opaque(CB1.low) 

ニックネーム交換によって、次のように生成されます。

xdr_long(timestamp.seconds)
xdr_long(timestamp.useconds) 

ニックネームは、ECB によって暗号化され、ECB0 と資格を得ます。

xdr_long(AUTH_KERB)
xdr_long(length)
xdr_enum(AKN_NICKNAME)
xdr_opaque(akc_nickname)
 

ベリファイアは、次のようになります。

xdr_long(AUTH_KERB)
xdr_long(length)
xdr_opaque(ECB0)
xdr_opaque(0) 

RPCSEC_GSS を使用した認証

上述の認証タイプ (AUTH_SYSAUTH_DES、 および AUTH_KERB) は、1 つの決まった見方で同じように扱うことができます。このため、新しいネットワーキング階層、Generic Security Standard API (汎用セキュリティ規格API)、すなわち GSS-API が追加されており、RPC プログラマが利用可能です。GSS-API のフレームワークでは、認証に加え次の 2 つの「サービス」が提供されています。 それは、完全性とプライバシです。


注 –

現在、GSS–API はまだ発表されていません。ただし、特定の GSS-API 機能は RPCSEC_GSS の機能を通じて参照できます。詳細は、 『GSS-API のプログラミング』を参照してください。


RPCSEC_GSS API

RPCSEC_GSS API セキュリティタイプを使用すると、ONC RPC アプリケーションは、GSS-API の機能を最大に生かすことができます。RPCSEC_GSS は、次の図のように、GSS-API 階層の「最上部」に位置しています。

図 5–1 GSS-API と RPCSEC-GSS のセキュリティ階層

Graphic

RPCSEC-GSS のプログラミングインタフェースを使用する場合は、ONC RPC アプリケーションは以下の項目を指定できます。

アプリケーションは、RPCSEC_GSS によって提供される関数により、QOP およびメカニズムのリストを入手できます。その他の関数 を参照してください。 開発者は、メカニズムと QOP をハードコード化し、使用するアプリケーション内に埋め込むことは避けてください。そうすれば、新しい、または異なるメカニズムおよび QOP を使用するためにアプリケーションを修正する必要はありません。


注 –

これまでは、「セキュリティタイプ」と「認証タイプ」は同じものを表していました。RPCSEC_GSS の導入によって、「タイプ」は現在、多少異なる意味を持ちます。タイプには、認証とともにサービス (一貫性またはプライバシ) を含むことができるようになりました。ただし、現在のところは RPCSEC_GSS がそれに該当する唯一のタイプということになります。


RPCSEC_GSS を使用すると、ONC RPC アプリケーションは、他のタイプを使用して行う場合と同様に、ピアにセキュリティコンテキストを確立し、データを交換してこのコンテキストを破棄します。一度コンテキストが確立されると、アプリケーションは、送信したデータユニットごとに QOP およびサービスを変更できます。

RPCSEC_GSS データタイプを含む RPCSEC_GSS の詳細については、rpcsec_gss(3NSL) のマニュアルページを参照してください。

RPCSEC_GSS ルーチン

次の表は、RPCSEC_GSS コマンドを要約したものです。この表では、各関数の個別の説明ではなく、RPCSEC_GSS 関数の全般的な概要を示しています。各関数の詳細については、該当するマニュアルページを参照するか、RPCSEC_GSS データ構造のリストなどの概要が記載された、rpcsec_gss(3NSL) のマニュアルページを参照してください。

表 5–2 RPCSEC_GSS 関数
処理 関数 入力 出力
 セキュリティコンテキストの作成 rpc_gss_seccreate(3NSL) クライアントのハンドル、主体名、メカニズム、QOP、サービスタイプAUTH ハンドル
 コンテキストの QOP とサービスタイプの変更 rpc_gss_set_defaults(3NSL) 古い QOP とサービス 新しい QOP とサービス
 セキュリティの変換前に、データの最大サイズを示す rpc_gss_max_data_length(3NSL) (クライアント側)  伝送できる最大データサイズ 変換前の最大データサイズ
 セキュリティの変換前に、データの最大サイズを示すrpc_gss_svc_max_data_length(3NSL) (サーバー側) 伝送できる最大データサイズ 変換前の最大データサイズ
 表示するサーバーの主体名を設定する rpc_gss_set_svc_name(3NSL) 主体名 , RPC プログラム、バージョン番号 正常に完了した場合は TRUE
 呼び出し側 (クライアント) の資格を得る rpc_gss_getcred(3NSL)svc_req 構造体へのポインタ UNIX 資格、RPCSEC_GSS 資格、cookie
 (ユーザーの作成した) コールバック関数を指定する rpc_gss_set_callback(3NSL) コールバック関数へのポインタ 正常に完了した場合は TRUE
 固有のパラメータから主体名の RPCSEC_GSS 構造体を作成する rpc_gss_get_principal_name(3NSL) メカニズム、ユーザー名、マシン名、ドメイン名 RPCSEC_GSS 主体名の構造体
 RPCSEC_GSS ルーチンが失敗した場合にエラーコードを得る rpc_gss_get_error(3NSL)

 

 RPCSEC_GSS エラー番号、該当する場合には errno
 インストールされているメカニズムの文字列を入手する rpc_gss_get_mechanisms(3NSL)

 

 有効なメカニズムのリスト
 有効な QOP 文字列を入手する rpc_gss_get_mech_info(3NSL) メカニズム そのメカニズムの有効な QOP
 サポートされている RPCSEC_GSS の最大および最小のバージョン番号を得る rpc_gss_get_versions(3NSL)

 

 最大および最小のバージョン番号
 メカニズムが導入されているかどうかをチェックする rpc_gss_is_installed(3NSL) メカニズム インストールされている場合は TRUE
 ASCII メカニズムを RPC オブジェクト識別子に変換する rpc_gss_mech_to_oid(3NSL) メカニズム (文字列で) メカニズム (OID で)
 ASCII QOP を整数に変換する rpc_gss_qop_to_num(3NSL) QOP (文字列で) QOP (整数で)

コンテキストの作成

コンテキストは、rpc_gss_seccreate() を使用して作成します。この関数では引数として次のものをとります。

この関数は、AUTH 認証ハンドルを返します。次のプログラムでは、Kerberos v5 セキュリテメカニズムと完全性サービスを使用したコンテキストを作成する場合、rpc_gss_seccreate() がどのように使用されるかを示しています。


例 5–10 rpc_gss_seccreate()

 
CLIENT *clnt;						/* クライアントハンドル */
char server_host[] = "foo";
char service_name[] = "nfs@machine.eng.company.com";
char mech[] = "kerberosv5";
 
clnt = clnt_create(server_host, SERVER_PROG, SERV_VERS, "netpath");
clnt->clnt_auth = rpc_gss_seccreate(clnt, service_name, mech,
                          rpc_gss_svc_integrity, NULL, NULL, NULL); 

. . .

次の点に注意してください。

詳細については、rpc_gss_seccreate(3NSL) のマニュアルページを参照してください。

値の変更とコンテキストの破棄

コンテキストが設定されると、アプリケーションは伝送される個々のデータユニットの QOP およびサービス値を変更する必要がある場合があります。たとえば、プログラムのパスワードは暗号化したいがログイン名は暗号化したくない場合。これは、次のように rpc_gss_set_defaults() を使用すると実行できます。


例 5–11 rpc_gss_set_defaults ()

 
rpc_gss_set_defaults(clnt->clnt_auth, rpc_gss_svc_privacy, qop);
 
. . .

この場合、セキュリティサービスはプライバシに設定されます。 コンテキストの作成 を参照してください。 ここで、qop は新しい QOP の名前を表わす文字列へのポインタです。

コンテキストは、通常どおり、auth_destroy() を使用して破棄します。

QOP とサービスの変更に関する詳細は、 rpc_gss_set_defaults(3NSL) のマニュアルページを参照してください。

主体名

セキュリティコンテキストを確立し、保持するには、次の 2 つのタイプの主体名が必要です。

サーバー主体名の設定

サーバーは、起動時に、そのサーバーを表わす主体名を指定する必要があります 。1 つのサーバーが、複数の主体として機能する場合もあります。次のプログラムで示すように、 rpc_gss_set_svc_name() を使用してサーバー主体名の設定をします。


例 5–12 rpc_gss_set_svc_name()

char *principal, *mechanism;
u_int req_time;

principal = "nfs@eng.acme.com";
mechanism = "kerberos_v5";
req_time = 10000;		/* 資格の有効時間 */

rpc_gss_set_svc_name(principal, mechanism, req_time, SERV_PROG, SERV_VERS);

Kerberos は、req_time パラメータを無視します。他の認証システムでは、このパラメータを使用する場合があります。

詳細については、rpc_gss_set_svc_name(3NSL) のマニュアルページを参照してください。

クライアント主体名の作成

サーバーは、クライアントの主体名で稼動する必要があります。 たとえば、クライアントの主体名をアクセス制御リストと比較するため、またはクライアントの UNIX 資格が存在する場合にはそれを検出するために必要です。このような主体名は、rpc_gss_principal_t 構造体ポインタとして保存されます。 rpc_gss_principal_t についての詳細は、rpcsec_gss(3NSL) のマニュアルページを参照してください。 サーバーが、受信した主体名を既知のエンティティの名前と比較する必要がある場合、サーバーは、この形式で主体名を生成する必要があります。

次のプログラムで示すように、rpc_gss_get_principal_name() 呼び出しでは、ネットワーク上で個人を識別するパラメータをいくつか入力し、rpc_gss_principal_t 構造体ポインタとして主体名を生成します。


例 5–13 rpc_gss_get_principal_name()

rpc_gss_principal_t *principal;
rpc_gss_get_principal_name(principal, mechanism, name, node, domain);
. . .

rpc_gss_get_principal_name() への引数は、次のとおりです。

各セキュリティメカニズムには、別々の識別パラメータが必要です。たとえば、Kerberos V5 にはユーザー名が必ず必要です。また、オプションの場合に限り、修飾されたノード名とドメイン名が必要です (Kerberos 用語では、ホスト名と領域名)。

詳細については、rpc_gss_get_principal_name(3NSL) のマニュアルページを参照してください。

主体名の解放

主体名は、free() ライブラリコールを使用して解放します。

サーバーで資格を受信する

サーバーは、クライアントの資格を獲得できなければなりません。 例 5–14 で示すように、 rpc_gss_getcred() 関数を使用すると、サーバーは UNIX 資格、または RPCSEC_GSS 資格のいずれか (またはこの両方) を検索できます。 これは、この関数が正常に終了した場合に設定された 2 つの引数によって実行されます。このうち1つは、呼び出し側の UNIX 資格が組み込まれた rpc_gss_ucred_t 構造体 (存在する場合) へのポインタになります。

typedef struct {
    uid_t   uid;          /* ユーザー ID */
    gid_t   gid;          /* グループ ID */
    short   gidlen;       
    git_t   *gidlist;     /* グループのリスト */
} rpc_gss_ucred_t;

もう 1 つの引数は、次のような、rpc_gss_raw_cred_t 構造体へのポインタです。

typedef struct {
	u_int  version;              /* RPCSEC_GSS プログラムバージョン */ 
	char          *mechanism;
          	char          *qop;
	rpc_gss_principal_t client_principal;  /* クライアント主体名 */
	char                   *svc_principal; /* サーバー主体名 */
	rpc_gss_service_t	  service;        /* プライバシ、完全性 enum */
} rpc_gss_rawcred_t;

rpc_gss_rawcred_t にはクライアントとサーバーの両方の主体名が組み込まれているため、rpc_gss_getcred() は両方の名前を返します。 rpc_gss_principal_t 構造体の解説と作成方法については、クライアント主体名の作成 を参照してください。

次のプログラムは、単純なサーバー側のディスパッチ手続きの例です。サーバーは、呼び出し側の資格を入手しています。この手続きでは、呼び出し側の UNIX 資格を入手してから、次に rpc_gss_rcred_t 引数内で検出されたメカニズム、QOP、サービスタイプを使用して、ユーザーの識別情報 (ID) を確認します。


例 5–14 資格の入手

static void server_prog(struct svc_req *rqstp, SVCXPRT *xprt)
{
		rpc_gss_ucred_t *ucred;
		rpc_gss_rawcred_t *rcred;
 
		if (rqst->rq_proq == NULLPROC) {
			svc_sendreply(xprt, xdr_void, NULL);
			return;
		}
		/*
		 * 他の全ての要求を認証する */
		 */
 
		switch (rqstp->rq_cred.oa_flavor) {
		case RPCSEC_GSS:
			/*
			 * 資格情報を取得する
			 */
			rpc_gss_getcred(rqstp, &rcred, &ucred, NULL);
			/*
			* 設定ファイルを参照してセキュリティパラメータを
			* 使用することでユーザーにアクセスが許可されている
			* ことを確認する
			*/
			if (!authenticate_user(ucred->uid, rcred->mechanism,
				rcred->qop, rcred->service)) {
				svcerr_weakauth(xprt);
				return;
			}
			break; 	/* ユーザーに許可する */
		default:
			svcerr_weakauth(xprt);
			return;
		} /* スイッチの終り */
 
		switch (rqstp->rq_proq) {
		case SERV_PROC1:
			. . .
		}
 
		/* 通常の要求処理 ; 応答を送る ... */
 
		return;
 
}

詳細については、rpc_gss_getcred(3NSL) のマニュアルページを参照してください。

Cookies

例 5–14 では、rpc_gss_getcred () への最後の引数 (ここでは NULL になっている) は、ユーザー定義の cookie です。このコンテキストの作成時にサーバーによってどのような値が指定されていても、このユーザー定義の値が戻されます。この cookie は 4 バイトの値で、そのアプリケーションに適したあらゆる方法で使用されます。RPC はこれを解釈しません。たとえば、 cookie は、コンテキストの起動元を示す構造体へのポインタまたはインデックスになることができます。また、各要求ごとにこの値を計算する代わりに、サーバーがコンテキスト作成時にこの値を計算します。このため、要求の処理時間が削減されます。

コールバック

これ以外に cookie が使用される場所は、コールバックです。サーバーは、rpc_gss_set_callback() 関数を使用することにより、ユーザー定義のコールバックを指定して、コンテキストが最初に使用された時を認知できます。コールバックは、コンテキストが指定されたプログラムとバージョン用に確立されたあとに、そのコンテキストがデータ交換に最初に使用された時に呼び出されます。

ユーザー定義のコールバックルーチンは、以下のような形式になります。

bool_t callback (struct svc_req *req,  gss_cred_id_t deleg,
gss_ctx_id_t  gss_context rpc_gss_lock_t *
lock void ** cookie);

2 番めと 3 番めの引数 deleg gss_context は、GSS-API データタイプで、現在はまだ公開されていません。詳細については、『GSS-API のプログラミング』 を参照してください。 deleg は委譲されたピアを識別する情報になり、一方 gss_context は GSS-API コンテキストへのポインタになります。プログラムが GSS-API 処理をこのコンテキスト上で実行する必要がある場合、つまり受信条件のテストをする場合に、このポインタが必要となります。 cookie 引数については、すでに説明しました。

lock 引数は、以下のように rpc_gss_lock_t 構造体へのポインタです。

 
typedef struct {
 	bool_t 						locked;
		rpc_gss_rawcred_t		*raw_cred;
} rpc_gss_lock_t;

このパラメータを使用すると、サーバーはセッションに対し強制的に特定の QOP とサービスを実行できます。例 5–14 に記載したように、QOP とサービスは、rpc_gss_rawcred_t 構造体内で検出できます。サーバーは、サービスと QOP の値を変更する必要はありません。ユーザー定義のコールバックが呼び出されると、lockedFALSE に設定されます。サーバーが、lockedTRUE に設定すると、QOP とサービスの値が、rpc_gss_rawcred_t 構造体内の値と一致する要求だけが受理されます。

詳細は、rpc_gss_set_callback(3NSL) のマニュアルページを参照してください。

最大データサイズ

rpc_gss_max_data_length()rpc_gss_svc_max_data_length() の 2 つの関数は、1 つのデータが、セキュリティ測度によって変換され「ワイヤを通じて」送信される前に、そのデータの大きさを判別する場合に便利です。つまり、暗号化などのセキュリティ変換により、通常、伝送される 1 つのデータのサイズは変更され、通常は大きくなります。データが使用できるサイズ以上に大きくならないよう に、これら 2 つの関数(前者はクライアント側バージョンで、後者はサーバー側バージョン) により、指定されたトランスポートの変換前の最大サイズが戻されます。

詳細については、rpc_gss_max_data_length(3NSL) のマニュアルページを参照してください。

その他の関数

関数の中には、導入されたセキュリティシステムに関する情報を入手する場合に使用できるものもあります。

これらの関数を使用することによって、プログラマは、アプリケーション内のセキュリティパラメータのハードコード化を避けることができます 。(RPCSEC_GSS 関数については、表 5–2 および rpcsec_gss(3NSL) マニュアルページを参照してください)。

関連ファイル

RPCSEC_GSS は各種のファイルを使用して情報を保存します。

gsscredテーブル

サーバーが要求に関連するクライアントの資格を検索すると、サーバーはクライアントの主体名 (rpc_gss_principal_t 構造体ポインタの形式)、またはクライアントのローカル UNIX 資格 (UID) のいずれかを入手できます。NFS 要求などのサービス では、アクセス検査に必要なローカル UNIX 資格が必要ですが、他の資格は必要ありません。これらのサービスでは、たとえば主体名は、 rpc_gss_principal_t 構造体として直接、独自のアクセス制御リスト内に格納できます。


注 –

クライアントのネットワーク資格 (その主体名) とローカルUNIX 資格間の対応は自動的に行われません。これは、ローカルのセキュリティ管理者が明示的に設定する必要があります。


gsscred ファイルには、クライアントの UNIX 資格とネットワーク(たとえば、Kerberos V5) 資格の両方が入っています。ネットワーク資格は、rpc_gss_principal_t 構造体が 16 進 ASCII 表現されています。gsscred ファイルは、XFN を通じてアクセスされます。したがって、このテーブルは、ファイル、NIS、NIS+、あるいは XFN によってサポートされる将来のネームサービス上に実装可能となります。XFN 階層では、このテーブルは this_org_unit/service/gsscred として表示されます。システム管理者は、ユーザーやマシンを追加したり削除したりできる gsscred ユーティリティを利用して、gsscred テーブルの保守管理を実行できます。

/etc/gss/qop と /etc/gss/mech

便宜上、RPCSEC_GSS では、メカニズムと保護の質 (QOP) パラメータを表示するためにリテラルの文字列を使用します。ただし、基本的なメカニズム自体では、メカニズムをオブジェクト識別子として、QOP は 32 ビット整数として表示する必要があります。また、各メカニズムごとに、そのメカニズムのサービスが実装された共有ライブラリを指定する必要があります。

/etc/gss/mech ファイルには、システム上に導入されたすべてのメカニズムに関する情報が保存されています。ASCII 形式によるメカニズム名、そのメカニズムの OID、このメカニズムによって提供されるサービスが実装された共有ライブラリ名、さらに、オプションとして、サービスが実装されたカーネルモジュール名です。次に例を示します。


kerberos_v5   1.2.840.113554.1.2.2    gl/mech_krb5.so gl_kmech_krb5

/etc/gss/qop ファイルには、導入されたすべてのメカニズム用に、各メカニズムがサポートするすべての QOP が、ASCII 文字列とそれに対応する 32 ビット整数の両方で格納されます。

/etc/gss/mech/etc/gss/qop は、両方とも指定されたシステムにセキュリティメカニズムが最初に導入されたときに作成されます。

多くのカーネル内 RPC ルーチンでは、文字列ではない値によって、メカニズムと QOP が表現されています。したがって、アプリケーションは、これらのカーネル内ルーチンを利用したい場合には、rpc_gss_mech_to_oid ()rpc_gss_qop_to_num() 関数を使用して、文字列ではない値で表現された、これらのパラメータを入手します。

ポートモニターの使用

RPC サーバーは、inetdlisten のようなポートモニターから起動できます。ポートモニターは、要求が来ているかどうか監視し、要求が来ればそれに応じてサーバーを生成します。生成されたサーバープロセスには、要求を受信したファイル記述子 0 が渡されます。inetd の場合、サーバーは処理を終えるとすぐに終了するか、次の要求がくる場合に備えて指定された時間だけ待ってから終了します。 付録 F 「SAF を使用したポートモニタープログラムの作成」も参照してください。

listen の場合は常に新たなプロセスが生成されるため、サーバーは応答を返したらすぐに終了しなければなりません。次に示す関数呼び出しでは、ポートモニターから起動されるサービスで使用する SVCXPRT ハンドルが作成されます。

transp = svc_tli_create(0, nconf, (struct t_bind *)NULL, 0, 0)

ここで、nconf は要求を受信したトランスポートの netconfig 構造体です。

サービスはポートモニターによりすでに rpcbind で登録されているので、登録する必要はありません。ただし、次のように svc_reg() を呼び出してサービス手続きを登録する必要があります。

svc_reg(transp, PROGNUM, VERSNUM, dispatch,(struct netconfig *)NULL)

ここでは netconfig 構造体として NULL を渡し、svc_reg()rpcbind を呼び出してサービスを登録しないようにしています。

接続型トランスポートの場合は、次のルーチンにより下位レベルインタフェースが提供されます。

transp = svc_fd_create(0, recvsize, sendsize);

最初の引数ではファイル記述子 0 を指定します。recvsizesendsize には、適当なバッファサイズを指定できます。どちらの引数も 0 とすると、システムのデフォルト値が使用されます。自分で監視を行わないアプリケーションサーバーの場合は、svc_fd_create() を使用します。

inetd の使用

/etc/inet/inetd.conf のエントリ形式は、ソケットサービス、TLI サービス、RPC サービスによってそれぞれ異なります。RPC サービスの場合の inetd.conf のエントリ形式は次のようになります。

表 5–3 RPC inetdサービス

サービス 

説明 

rpc_prog/vers

RPC プログラム名に / とバージョン番号 (またはバージョン番号の範囲) を付けたもの 

endpoint_type

dgram (非接続型ソケット)、stream (接続型ソケット)、tli (TLI 端点) のどれか

proto

* (サポートされているトランスポートすべてを意味する)、nettype、netid のどれか。または、nettype と netid をコンマで区切ったリスト  

flags

wait または nowait のどちらか

user

有効な passwd データベースに存在しているユーザー

pathname

サーバーデーモンへのフルパス名 

args

デーモンの呼び出し時に渡される引数 

次に、エントリの例を示します。

rquotad/1 tli rpc/udp wait root /usr/lib/nfs/rquotad rquotad

詳細については、inetd.conf(4) のマニュアルページを参照してください。

リスナーの使用

次に示すように pmadm を使用して RPC サービスを追加します。


pmadm -a -p pm_tag -s  svctag -i id -v vers  \
 
	-m `nlsadmin -c command -D -R prog:vers`

次に、コマンド行引数を示します。

-a

サービスの追加

-p pm_tag

サービスへのアクセスを提供するポートモニターに結合したタグの指定

-s svctag

サーバーの ID コード

-i id

サービス svctag に割り当てられた /etc/passwd 内のユーザー名

-v ver

ポートモニターのデータベースファイルのバージョン番号

-m

サービスを呼び出す nlsadmin コマンドを指定。nlsadmin コマンドには引数を渡すことができます。たとえば、rusersd という名前のリモートプログラムサーバーのバージョン 1 を追加する場合は、pmadm コマンドは次のようになります。


# pmadm -a -p tcp -s rusers -i root -v 4 \
 
-m `nlsadmin -c /usr/sbin/rpc.ruserd -D -R 100002:1`

このコマンドでは、root パーミッションが指定され、listener データベースファイルのバージョン 4 でインストールされ、TCP トランスポート上で使用可能になります。pmadm の引数やオプションは複雑であるため、RPC サービスはコマンドスクリプトでもメニューシステムでも追加できます。メニューシステムを使用するには、sysadm ports と入力して、port_services オプションを選択します。

サービスを追加した場合は、その後リスナーを再初期化してサービスを利用可能にしなければなりません。そのためには、次のようにリスナーを一度止めてから再起動します。このとき rpcbind が実行されている必要があります。

# sacadm -k -p pmtag
# sacadm -s -p pmtag

リスナープロセスの設定などについての詳細は、 listen(1M)pmadm(1M)、および sacadm(1M) のマニュアルページを参照してください。 また、『Solaris のシステム管理 (IP サービス)』の「TCP/IP プロトコルがデータ通信を行う方法」も参照してください。

サーバーのバージョン

一般に、プログラム PROG の最初のバージョンは PROGVERS_ORIG とし、最新バージョンは PROGVERS と命名します。プログラムのバージョン番号は続き番号で割り当てなければなりません。バージョン番号に飛ばされた番号があると、検索したときに定義済みのバージョン番号を探し出せないようなことが起こります。

バージョン番号を変更できるのは、自分が所有しているファイルだけです。自分が所有していないプログラムのバージョン番号を追加したりすると、そのプログラムの所有者がバージョン番号を追加するときに重大な問題が起こります。バージョン番号の登録やご質問はご購入先へお問い合わせ下さい。

ruser プログラムの新バージョンが、int ではなく unsigned short を返すように変更されたとします。新バージョンの名前を RUSERSVERS_SHORT とすると、新旧の 2 つのバージョンをサポートするサーバーは二重登録することになります。次のように、どちらの登録でも同じサーバーハンドルを使用します。


例 5–15 同一ルーチンの 2 つのバージョンのためのサーバーハンドル

if (!svc_reg(transp, RUSERSPROG, RUSERSVERS_ORIG, 
 					nuser, nconf))
{
	fprintf(stderr, "can't register RUSER service\n");
	exit(1);
}
if (!svc_reg(transp, RUSERSPROG, RUSERSVERS_SHORT, nuser,
			 		nconf)) {
	fprintf(stderr, "can't register RUSER service\n");
 exit(1);
}

次のように、1 つの手続きで両バージョンを実行できます。


例 5–16 両バージョンを使用するサーバー

void
nuser(rqstp, transp)
	struct svc_req *rqstp;
	SVCXPRT *transp;
{
 unsigned int nusers;
	unsigned short nusers2;
 switch(rqstp->rq_proc) {
		case NULLPROC:
	 	if (!svc_sendreply( transp, xdr_void, 0))
				fprintf(stderr, "can't reply to RPC call\n");
			return;
		case RUSERSPROC_NUM:
			/*
			 * ユーザー数を求めて変数 nusers に設定するコード
			 */
		switch(rqstp->rq_vers) {
 		case RUSERSVERS_ORIG:
				if (! svc_sendreply( transp, xdr_u_int, &nusers))
					fprintf(stderr, "can't reply to RPC call\n");
 			break;
			case RUSERSVERS_SHORT:
				nusers2 = nusers;
				if (! svc_sendreply( transp, xdr_u_short, &nusers2))
 				fprintf(stderr, "can't reply to RPC call\n");
				break;
 	}
		default:
			svcerr_noproc(transp);
 		return;
	}
	return;
}

クライアントのバージョン

異なるホストでは RPC サーバーの異なるバージョンが実行されている可能性があるので、クライアントはさまざまなバージョンに対応できるようにしなければなりません。たとえば、あるサーバーでは旧バージョン RUSERSPROG(RUSERSVERS_ORIG) が実行されており、別のサーバーでは最新バージョン RUSERSPROG(RUSERSVERS_SHORT) が実行されているとします。

サーバーのバージョンがクライアント作成ルーチン clnt_call() で指定したバージョン番号と一致しない場合は、clnt_call() から RPCPROGVERSMISMATCH というエラーが返されます。サーバーがサポートしているバージョン番号を取り出して、正しいバージョン番号をもつクライアントハンドルを作成することもできます。そのためには、次に示すルーチンを使用するか、clnt_create_vers () を使用します。詳細については、rpc(3NSL) のマニュアルページを参照してください。


例 5–17 クライアント側での RPC バージョン選択

main()
{
	enum clnt_stat status;
 u_short num_s;
	u_int num_l;
	struct rpc_err rpcerr;
	int maxvers, minvers;
	CLIENT *clnt;
 
	clnt = clnt_create("remote", RUSERSPROG, RUSERSVERS_SHORT,
			             "datagram_v");
	if (clnt == (CLIENT *) NULL) {
 	clnt_pcreateerror("unable to create client handle");
		exit(1);
 }
	to.tv_sec = 10;				/* タイムアウト値を設定 */
	to.tv_usec = 0;
 
	status = clnt_call(clnt, RUSERSPROC_NUM, xdr_void,
	                  (caddr_t) NULL, xdr_u_short, 
                  (caddr_t)&num_s, to);
	if (status == RPC_SUCCESS) {   /* 最新バージョン番号が見つかった場合 */
		printf("num = %d\n", num_s);
 	exit(0);
	}
	if (status != RPC_PROGVERSMISMATCH) {		/* その他のエラー */
		clnt_perror(clnt, "rusers");
 	exit(1);
	}
	/* 指定したバージョンがサポートされていない場合 */
 clnt_geterr(clnt, &rpcerr);
	maxvers = rpcerr.re_vers.high;   /* サポートされている最新バージョン */
	minvers = rpcerr.re_vers.low;
               /* サポートされている最も古いバージョン */
	if (RUSERSVERS_SHORT < minvers || RUSERSVERS_SHORT> maxvers)
{
			                     /* サポート範囲内にない場合 */
		clnt_perror(clnt, "version mismatch");
		exit(1);
	}
	(void) clnt_control(clnt, CLSET_VERSION, RUSERSVERS_ORIG);
	status = clnt_call(clnt, RUSERSPROC_NUM, xdr_void,
			 (caddr_t) NULL, xdr_u_int, (caddr_t)&num_l, to);
	if (status == RPC_SUCCESS)
 		               /* 識別できるバージョン番号が見つかった場合 */
		printf("num = %d\n", num_l);
	else {
		clnt_perror(clnt, "rusers");
 	exit(1);
	}
}
 

一時的な RPC プログラム番号の使用

場合によっては、動的に生成される RPC プログラム番号をアプリケーションが使用すると便利なことがあります。たとえば、コールバック手続きを実装する場合などです。コールバックでは、クライアントプログラムは通常、動的に生成される、つまり一時的な RPC プログラム番号を使用して RPC サービスを登録します。そして、その番号を要求とともにサーバーに渡します。次にサーバーは一時的な RPC プログラム番号を使用してクライアントプログラムをコールバックし、結果を返します。

クライアントの要求を処理するのにかなりの時間がかかり、クライアントが停止できない場合 (シングルスレッドであると思われるとき) には、この機構が必要になります。このような場合、サーバーはクライアントの要求を認識し、あとで結果とともにコールバックを行います。

コールバックを使用する別の例としては、サーバーから定期的なレポートを生成する場合があります。クライアントは RPC 呼び出しを行い、報告を開始します。そしてサーバーはクライアントプログラムが提供する一時的な RPC プログラム番号を使用して、定期的にレポートとともにクライアントをコールバックします。

動的に生成される一時的な RPC 番号は、0x40000000 から 0x5fffffff の範囲です。次に示すルーチンは指定されるトランスポートタイプ用に、一時的な RPC プログラムに基づいてサービスを作成します。サービスハンドルと一時的な RPC プログラム番号が返されます。呼び出し側はサービスディスパッチルーチン、バージョンタイプ、トランスポートタイプを提供します。


例 5–18 一時的な RPC プログラム - サーバー側

SVCXPRT *
register_transient_prog(dispatch, program, version, netid)
	void (*dispatch)(); /* サービスディスパッチルーチン */
	rpcproc_t  *program;    /* 一時的な RPC 番号が返される */
 rpcvers_t version;     /* プログラムバージョン */
	char *netid;        /* トランスポート id */
{
	SVCXPRT  *transp;
 struct netconfig *nconf;
	rpcprog_t prognum;
 if ((nconf = getnetconfigent(netid)) == (struct netconfig
*)NULL)
 	return ((SVCXPRT *)NULL);
	if ((transp = svc_tli_create(RPC_ANYFD, nconf,
				(struct t_bind *)NULL, 0, 0)) == (SVCXPRT *)NULL) {
		freenetconfigent(nconf);
		return ((SVCXPRT *)NULL);
	}
	prognum = 0x40000000;
 while (prognum < 0x60000000 && svc_reg(transp, prognum,
version,
 								dispatch, nconf) == 0) {
		prognum++;
 }
	freenetconfigent(nconf);
	if (prognum>= 0x60000000) {
		svc_destroy(transp);
		return ((SVCXPRT *)NULL);
	}
	*program = prognum;
 return (transp);
}