ONC+ Developer's Guide

Chapter 5 Advanced RPC Programming Techniques

This section addresses areas of occasional interest to developers using the lower-level interfaces of the RPC package. The topics are:

poll() on the Server Side

This section applies only to servers running RPC in single-threaded (default) mode.

A process that services RPC requests and performs some other activity cannot always call svc_run(). If the other activity periodically updates a data structure, the process can set a SIGALRM signal before calling svc_run(). This process enables the signal handler to process the data structure and return control to svc_run() when done.

A process can bypass svc_run() and access the dispatcher directly with the svc_getreqset() call. The process must be given the file descriptors of the transport endpoints associated with the programs being waited on. Then the process can have its own poll() that waits on both the RPC file descriptors and its own descriptors.

Example 5–1 shows svc_run(). svc_pollset is an array of pollfd structures that is derived, through a call to __rpc_select_to_poll(), from svc_fdset(). The array can change every time any RPC library routine is called because descriptors are constantly being opened and closed. svc_getreq_poll() is called when poll() determines that an RPC request has arrived on some RPC file descriptors.


Note –

The __rpc_dtbsize() and __rpc_select_to_poll() functions are not part of the SVID, but they are available in the libnsl library. The descriptions of these functions are included here so that you can create versions of these functions for non-Solaris implementations.


Given an fd_set pointer and the number of bits to check in it, the __rpc_select_to_poll function initializes the supplied pollfd array for RPC's use. RPC polls only for input events. The number of pollfd slots that were initialized is returned. The arguments for this function are:

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

The __rpc_dtbsize() function calls the getrlimit() function to determine the maximum value that the system can assign to a newly created file descriptor. The result is cached for efficiency.

For more information on the SVID routines in this section, see the rpc_svc_calls(3NSL) and poll(2) man pages.


Example 5–1 svc_run() and poll()

void
svc_run()
{
	int nfds;
	int dtbsize = __rpc_dtbsize();
	int i;
	struct pollfd svc_pollset[fd_setsize];

	for (;;) {
		/*
		 * Check whether there is any server fd on which we may have
		 * to wait.
		 */
		nfds = __rpc_select_to_poll(dtbsize, &svc_fdset,
		                            svc_pollset);
		if (nfds == 0)
			break;	/* None waiting, hence quit */

		switch (i = poll(svc_pollset, nfds, -1)) {
		case -1:
			/*
			 * We ignore all errors, continuing with the assumption
			 * that it was set by the signal handlers (or any
			 * other outside event) and not caused by poll().
			 */
		case 0:
			continue;
		default:
			svc_getreq_poll(svc_pollset, i);
		}
	}
}

Broadcast RPC

When an RPC broadcast is issued, a message is sent to all rpcbind daemons on a network. An rpcbind daemon with which the requested service is registered forwards the request to the server. The main differences between broadcast RPC and normal RPC calls are:

The following code example shows how rpc_broadcast() is used and describes its arguments.


Example 5–2 RPC Broadcast

/*
 * bcast.c: example of RPC broadcasting use.
 */
 
#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);
}

The function in Example 5–3 collects the replies to the broadcast. The normal operation is to collect either the first reply or all replies. bcast_proc() displays the IP address of the server that has responded. Because the function returns FALSE it continues to collect responses. The RPC client code continues to resend the broadcast until it times out.


Example 5–3 Collect Broadcast Replies

bool_t
bcast_proc(res, t_addr, nconf)
	void *res;									/* Nothing comes back */
	struct t_bind *t_addr;					/* Who sent us the reply */
	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);
}

If done is TRUE, then broadcasting stops and rpc_broadcast() returns successfully. Otherwise, the routine waits for another response. The request is rebroadcast after a few seconds of waiting. If no responses come back, the routine returns with RPC_TIMEDOUT.

Batching

RPC is designed so that clients send a call message and wait for servers to reply to the call. This procedure implies that a client is blocked while the server processes the call. This result is inefficient when the client does not need each message acknowledged.

RPC batching lets clients process asynchronously. RPC messages can be placed in a pipeline of calls to a server. Batching requires that:

Because the server does not respond to each call, the client can send new calls in parallel with the server processing previous calls. The transport can buffer many call messages and send them to the server in one write() system call. This buffering decreases interprocess communication overhead and the total time of a series of calls. The client should end with a nonbatched call to flush the pipeline.

The following code example shows the unbatched version of the client. It scans the character array, buf, for delimited strings and sends each string to the server.


Example 5–4 Unbatched Client

#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);
}

The following code example shows the batched version of the client. It does not wait after each string is sent to the server. It waits only for an ending response from the server.


Example 5–5 Batched Client

#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);
}

The following code example shows the dispatch portion of the batched server. Because the server sends no message, the clients are not notified of failures.


Example 5–6 Batched Server

#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");
				/* Tell caller an error occurred */
				svcerr_decode(transp);
				break;
			}
			/* Code here to render the string 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");
				/* Be silent in the face of protocol errors */
				break;
			}
			/* Code here to render string s, but send no reply! */
			break;
		default:
			svcerr_noproc(transp);
			return;
	}
	/* Now free string allocated while decoding arguments */
	svc_freeargs(transp, xdr_wrapstring, &s);
}


Note –

To illustrate the benefits of batching, Example 5–4 and Example 5–6 were completed to render the lines in a 25,144-line file. The rendering service throws away the lines. The batched version of the application is four times as fast as the unbatched version.


Authentication

Just as you can use different transports when creating RPC clients and servers, you can associate different “flavors” of authentication with RPC clients. The authentication subsystem of RPC is open ended. So, RPC can support many flavors of authentication. Appendix B, RPC Protocol and Language Specification further defines the authentication protocols.

Sun RPC currently supports the authentication flavors shown in the following table.

Table 5–1 Authentication Methods Supported by Sun RPC

Method 

Description 

AUTH_NONE

Default. No authentication performed. 

AUTH_SYS

An authentication flavor based on the process permissions authentication in the UNIX operating system. 

AUTH_SHORT

An alternate flavor of AUTH_SYS used by some servers for efficiency. Client programs using AUTH_SYS authentication can receive AUTH_SHORT response verifiers from some servers. See Appendix B, RPC Protocol and Language Specification for details.

AUTH_DES

An authentication flavor based on DES encryption techniques. 

AUTH_KERB

Version 5 Kerberos authentication based on DES framework. 

When a caller creates a new RPC client handle as in:

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

the appropriate client-creation routine sets the associated authentication handle to:

clnt->cl_auth = authnone_create();

If you create a new instance of authentication, you must destroy it with auth_destroy(clnt->cl_auth). This destruction conserves memory.

On the server side, the RPC package passes a request that has an arbitrary authentication style associated with it to the service-dispatch routine. The request handle passed to a service-dispatch routine contains the structure rq_cred. This structure is opaque, except for one field: the flavor of the authentication credentials.

/*
 * Authentication data
 */
struct opaque_auth {
   enum_t    oa_flavor;		/* style of credentials */
   caddr_t   oa_base;			/* address of more auth stuff */
   u_int     oa_length;		/* not to exceed MAX_AUTH_BYTES */
};

The RPC package guarantees the following structural requirements to the service-dispatch routine:

AUTH_SYS Authentication

The client can use AUTH_SYS style authentication (called AUTH_UNIX in previous releases) by setting clnt–>cl_auth after creating the RPC client handle:

clnt->cl_auth = authsys_create_default();

This setting causes each RPC call associated with clnt to carry with it the following credentials-authentication structure shown in the following example:


Example 5–7 AUTH_SYS Credential Structure

/*
 * AUTH_SYS flavor credentials.
 */
struct authsys_parms {
	u_long aup_time;			 /* credentials creation time */
	char *aup_machname;		 /* client's host name */
	uid_t aup_uid;				 /* client's effective uid */
	gid_t aup_gid;				 /* client's current group id */
	u_int aup_len;				 /* element length of aup_gids*/
	gid_t *aup_gids;			 /* array of groups user is in */
};

rpc.broadcast defaults to AUTH_SYS authentication.

The following example shows a server, with procedure RUSERPROC_1(), that returns the number of users on the network. As an example of authentication, the server checks AUTH_SYS credentials and does not service requests from callers with a uid of 16.


Example 5–8 Authentication Server

nuser(rqstp, transp)
	struct svc_req *rqstp;
	SVCXPRT *transp;
{
	struct authsys_parms *sys_cred;
	uid_t uid;
	unsigned int nusers;

	/* NULLPROC should never be authenticated */
	if (rqstp->rq_proc == NULLPROC) {
		if (!svc_sendreply( transp, xdr_void, (caddr_t) NULL))
			fprintf(stderr, "can't reply to RPC call\n");
		return;
	}

	/* now get the 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:
			/* make sure caller is allowed to call this proc */
			if (uid == 16) {
				svcerr_systemerr(transp);

				return;
			}
			/*
			 * Code here to compute the number of users and assign it
			 * to the variable nusers
			 */
			if (!svc_sendreply( transp, xdr_u_int, &nusers))
				fprintf(stderr, "can't reply to RPC call\n");
			return;
		default:
			svcerr_noproc(transp);
			return;
	}
}

Note the following points about the example:

The last point underscores the relation between the RPC authentication package and the services: RPC deals only with authentication and not with an individual service's access control. The services must establish access-control policies and reflect these policies as return statuses in their protocols.

AUTH_DES Authentication

Use AUTH_DES authentication for programs that require more security than AUTH_SYS provides. AUTH_SYS authentication is easy to defeat. For example, instead of using authsys_create_default(), a program can call authsys_create() and change the RPC authentication handle to give itself any desired user ID and host name.

AUTH_DES authentication requires keyserv() daemons to be running on both the server and client hosts. The NIS or NIS+ naming service must also be running. Users on these hosts need public/secret key pairs assigned by the network administrator in the publickey() database. The users must also have decrypted their secret keys with the keylogin() command. This decryption is normally done by login() unless the login password and secure-RPC password differ.

To use AUTH_DES authentication, a client must set its authentication handle appropriately, as shown in the following example.

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

The first argument is the network name or “net name” of the owner of the server process. Server processes are usually root processes, and you can get their net names with the following call;

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

servername points to the receiving string and server is the name of the host the server process is running on. If the server process was run by a non-root user, use the call user2netname() as follows:

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

serveruid() is the user ID of the server process. The last argument of both functions is the name of the domain that contains the server. NULL means “use the local domain name.”

The second argument of authdes_seccreate() is the lifetime (known also as the “window”) of the client's credential. In this example, a credential expires 60 seconds after the client makes an RPC call. If a program tries to reuse the credential, the server RPC subsystem recognizes that the credential has expired and does not service the request carrying the expired credential. If any program tries to reuse a credential within its lifetime, the process is rejected, because the server RPC subsystem saves credentials it has seen in the near past and does not serve duplicates.

The third argument of authdes_seccreate() is the name of the timehost used to synchronize clocks. AUTH_DES authentication requires that server and client agree on the time. Example 5–8 specifies synchronization with the server. A (char *)NULL says not to synchronize. Use this syntax only when you are sure that the client and server are already synchronized.

The fourth argument of authdes_seccreate() points to a DES encryption key to encrypt timestamps and data. If this argument is (char *)NULL, as it is in Example 5–8, a random key is chosen. The ah_key field of the authentication handle contains the key.

The server side is simpler than the client. The following example shows the server in Example 5–8 changed to use AUTH_DES.


Example 5–9 AUTH_DES Server

#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 should never be authenticated */
	if (rqstp->rq_proc == NULLPROC) {
		/* same as before */
	}
	/* now get the 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;
	}
	/* The rest is the same as before */

The routine netname2user() converts a network name, or “net name” of a user, to a local system ID. It also supplies group IDs, which are not used in this example.

AUTH_KERB Authentication

SunOS 5.0 and compatible versions include support for most client-side features of Kerberos 5 except klogin. AUTH_KERB is conceptually similar to AUTH_DES. The essential difference is that DES passes a network name and a DES-encrypted session key, while Kerberos passes the encrypted service ticket. This section describes other factors that affect implementation and interoperability.

Kerberos uses the concept of a time window in which its credentials are valid. It does not place restrictions on the clocks of the client or server. Specifically, the window is passed as an argument to authkerb_seccreate(). The window does not change. If a timehost is specified as an argument, the client side gets the time from the timehost and alters its timestamp by the difference in time. Various methods of time synchronization are available. See the kerberos_rpc man page for more information.

Kerberos users are identified by a primary name, instance, and realm. The RPC authentication code ignores the realm and instance, while the Kerberos library code does not. The assumption is that user names are the same between client and server. This enables a server to translate a primary name into user identification information. Two forms of well-known names are used (omitting the realm):

Kerberos uses cipher block chaining (CBC) mode when sending a full name credential, one that includes the ticket and window, and electronic code book (ECB) mode otherwise. CBC and ECB are two methods of DES encryption. The session key is used as the initial input vector for CBC mode. The following notation means that XDR is used on object as a type.

xdr_type(object)

The length in the next code section is the size, in bytes of the credential or verifier, rounded up to 4-byte units. The full name credential and verifier are obtained as follows:

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

After encryption with CBC with input vector equal to the session key, the output is two DES cipher blocks:

CB0
CB1.low
CB1.high

The credential is:

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

The verifier is:

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

The nickname exchange yields:

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

The nickname is encrypted with ECB to obtain ECB0, and the credential is:

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

The verifier is:

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

Authentication Using RPCSEC_GSS

A determined snoop can overcome the authentication flavors mentioned previously- AUTH_SYS, AUTH_DES, and AUTH_KERB. For this reason a new networking layer, the Generic Security Standard API, or GSS-API, was added, which RPC programmers can use. The GSS-API framework offers two extra services beyond authentication: integrity and privacy.


Note –

Currently, the GSS–API is exposed, and certain GSS-API features are visible through RPCSEC_GSS functions. See the GSS-API Programming Guide.


RPCSEC_GSS API

The RPCSEC_GSS security flavor enables ONC RPC applications to maximize the features of GSS-API. RPCSEC_GSS sits “on top” of the GSS-API layer as shown in the following figure.

Figure 5–1 GSS-API and RPCSEC_GSS Security Layers

RPSEC_GSS is between the application and GSS-API.

Using the programming interface for RPCSEC_GSS, ONC RPC applications can specify the following information:

Applications can obtain lists of valid QOPs and mechanisms through functions provided by RPCSEC_GSS. See Miscellaneous Functions. Developers should avoid hard-coding mechanisms and QOPs into their applications, so that the applications do not need to be modified to use new or different mechanisms and QOPs.


Note –

Historically, “security flavor” and “authentication flavor” have had the same meaning. With the introduction of RPCSEC_GSS, “flavor” now has a somewhat different sense. A flavor can now include a service integrity or privacy along with authentication, although currently RPCSEC_GSS is the only flavor that falls into this category.


Using RPCSEC_GSS, ONC RPC applications establish a security context with a peer, exchange data, and destroy the context, just as they do with other flavors. After a context is established, the application can change the QOP and service for each data unit sent.

For more information on RPCSEC_GSS, including RPCSEC_GSS data types, see the rpcsec_gss(3N) man page.

RPCSEC_GSS Routines

The following table summarizes RPCSEC_GSS commands. It is a general overview of RPCSEC_GSS functions, rather than a specific description of each one. For more information on each function, see its man page, or check the rpcsec_gss(3NSL) man page for an overview, including a list of RPCSEC_GSS data structures.

Table 5–2 RPCSEC_GSS Functions
ActionFunctionInputOutput
 Create a security contextrpc_gss_seccreate(3NSL) CLIENT handle, principal name, mechanism, QOP, service typeAUTH handle
 Change QOP, service type for contextrpc_gss_set_defaults(3NSL) Old QOP, service New QOP, service
 Show maximum size for data before security transformationrpc_gss_max_data_length(3NSL) (client side) Maximum data size allowed by transport Maximum pre-transformation data size
 Show maximum size for data before security transformationrpc_gss_svc_max_data_length(3NSL) (server side) Maximum data size allowed by transport Maximum pre-transformation data size
 Set name of principals for server to representrpc_gss_set_svc_name(3NSL) Principal name, RPC program, version #s TRUE if successful
 Fetch credentials of caller (client)rpc_gss_getcred(3NSL)Pointer to svc_req structure UNIX credentials, RPCSEC_GSS credentials, cookie
 Specify user-written callback function rpc_gss_set_callback(3NSL) Pointer to callback function TRUE if successful
 Create RPCSEC_GSS structure for principal names from unique parametersrpc_gss_get_principal_name(3NSL) Mechanism, user name, machine name, domain name RPCSEC_GSS principal name structure
 Fetch an error code when an RPCSEC_GSS routine failsrpc_gss_get_error(3NSL)

 

RPCSEC_GSS error number, errno if applicable
 Get strings for installed mechanismsrpc_gss_get_mechanisms(3NSL)

 

 List of valid mechanisms
 Get valid QOP stringsrpc_gss_get_mech_info(3NSL) Mechanism Valid QOPs for that mechanism
 Get the highest, lowest version numbers of RPCSEC_GSS supportedrpc_gss_get_versions(3NSL)

 

 Highest, lowest versions
 Check if a mechanism is installedrpc_gss_is_installed(3NSL) Mechanism TRUE if installed
 Convert ASCII mechanism to RPC object identifierrpc_gss_mech_to_oid(3NSL) Mechanism (as string) Mechanism (as OID)
 Convert ASCII QOP to integerrpc_gss_qop_to_num(3NSL) QOP (as string) QOP (as integer)

Creating a Context

You create contexts with the rpc_gss_seccreate() call. This function takes as its arguments:

This function returns an AUTH authentication handle. The following example shows how rpc_gss_seccreate() might be used to create a context using the Kerberos V5 security mechanism and the integrity service.


Example 5–10 rpc_gss_seccreate()

CLIENT *clnt;                    /* client handle */
char server_host[] = "foo";
char service_name[] = "nfs@eng.acme.com";
char mech[] = "kerberos_v5";

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);

. . .

Note the following points about the example:

For more information, see the rpc_gss_seccreate(3NSL) man page.

Changing Values and Destroying a Context

After a context has been set, the application might need to change QOP and service values for individual data units being transmitted. For example, if you want a program to encrypt a password but not a login name, you can use rpc_gss_set_defaults().


Example 5–11 rpc_gss_set_defaults ()

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

. . .

In this case, the security service is set to privacy. See Creating a Context. qop is a pointer to a string naming the new QOP.

Contexts are destroyed in the usual way, with auth_destroy().

For more information on changing service and QOP, see the rpc_gss_set_defaults(3NSL) man page.

Principal Names

You need both a client and a server principal name to establish and maintain a security context.

Setting Server Principal Names

A server needs to be told the names of the principals it represents when it starts up. A server can act as more than one principal. rpc_gss_set_svc_name() sets the name of the principals, as shown in the following code example.


Example 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;		/* time for which credential should be valid */

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

Kerberos ignores the req_time parameter. Other authentication systems might use it.

For more information, see the rpc_gss_set_svc_name(3NSL) man page.

Generating Client Principal Names

Servers need to be able to operate on a client's principal name. For example, you might need to compare a client's principal name to an access control list, or look up a UNIX credential for that client, if such a credential exists. Such principal names are kept in the form of a rpc_gss_principal_t structure pointer. See the rpcsec_gss(3NSL) man page for more on rpc_gss_principal_t. If a server is to compare a principal name it has received with the name of a known entity, the server needs to be able to generate a principal name in that form.

The rpc_gss_get_principal_name() call takes as input several parameters that uniquely identify an individual on a network, and generates a principal name as a rpc_gss_principal_t structure pointer, as shown in the following code example.


Example 5–13 rpc_gss_get_principal_name()

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

The arguments to rpc_gss_get_principal_name() are:

Each security mechanism requires different identifying parameters. For example, Kerberos V5 requires a user name and, only optionally, qualified node and domain names, which in Kerberos terms are host and realm names.

For more information, see the rpc_gss_get_principal_name(3NSL) man page.

Freeing Principal Names

Use the free() library call to free principal names.

Receiving Credentials at the Server

A server must be able to fetch the credentials of a client. The rpc_gss_getcred() function, shown in Example 5–14, enables the server to retrieve either UNIX credentials or RPCSEC_GSS credentials, or both. The function has two arguments that are set if the function is successful. One is a pointer to an rpc_gss_ucred_t structure, which contains the caller's UNIX credentials, if such exist:

typedef struct {
    uid_t   uid;          /* user ID */
    gid_t   gid;          /* group ID */
    short   gidlen; 
    git_t   *gidlist;     /* list of groups */
} rpc_gss_ucred_t;

The other argument is a pointer to a rpc_gss_raw_cred_t structure, which looks like this:

typedef struct {
	u_int  version;              /*RPCSEC_GS program version *mechanism;
          	char          *qop;
	rpc_gss_principal_t client_principal;  /* client principal name */
	char                   *svc_principal; /*server principal name */
	rpc_gss_service_t	  service;        /* privacy, integrity enum */
} rpc_gss_rawcred_t;

Because rpc_gss_rawcred_t contains both the client and server principal names, rpc_gss_getcred() can return them both. See Generating Client Principal Names for a description of the rpc_gss_principal_t structure and how it is created.

The following example is a simple server-side dispatch procedure, in which the server gets the credentials for the caller. The procedure gets the caller's UNIX credentials and then verifies the user's identity, using the mechanism, QOP, and service type found in the rpc_gss_rcred_t argument.


Example 5–14 Getting Credentials

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;
		}
		/*
		 * authenticate all other requests */
		 */
 
		switch (rqstp->rq_cred.oa_flavor) {
		case RPCSEC_GSS:
			/*
			 * get credential information
			 */
			rpc_gss_getcred(rqstp, &rcred, &ucred, NULL);
			/*
			* verify that the user is allowed to access
			* using received security parameters by
			* peeking into my config file
			*/
			if (!authenticate_user(ucred->uid, rcred->mechanism,
				rcred->qop, rcred->service)) {
				svcerr_weakauth(xprt);
				return;
			}
			break; 	/* allow the user in */
		default:
			svcerr_weakauth(xprt);
			return;
		} /* end switch */
 
		switch (rqstp->rq_proq) {
		case SERV_PROC1:
			. . .
		}
 
		/* usual request processing; send response ... */
 
		return;
 
}

For more information, see the rpc_gss_getcred(3NSL) man page.

Cookies

In Example 5–14, the last argument to rpc_gss_getcred() (here, a NULL) is a user-defined cookie, with a value on return of whatever was specified by the server when the context was created. This cookie, a 4-byte value, can be used in any way appropriate for the application. RPC does not interpret the cookie. For example, the cookie can be a pointer or index to a structure that represents the context initiator. Instead of computing this value for every request, the server computes it at context-creation time, saving on request-processing time.

Callbacks

Another opportunity to use cookies is with callbacks. By using the rpc_gss_set_callback() function, a server can specify a user-defined callback so that it knows when a context first gets used. The callback is invoked the first time a context is used for data exchanges, after the context is established for the specified program and version.

The user-defined callback routine takes the following form:

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);

The second and third arguments, deleg and gss_context, are GSS-API data types and are currently exposed. See the GSS-API Programming Guide for more information. Note that deleg is the identity of any delegated peer, while gss_context is a pointer to the GSS-API context. This pointer is necessary in case the program needs to perform GSS-API operations on the context, that is, to test for acceptance criteria. You have already seen the cookie argument.

The lock argument is a pointer to a rpc_gss_lock_t structure:

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

This parameter enables a server to enforce a particular QOP and service for the session. QOP and service are found in the rpc_gss_rawcred_t structure described in Example 5–14. A server should not change the values for service and QOP. When the user-defined callback is invoked, the locked field is set to FALSE. If the server sets locked to TRUE, only requests with QOP and service values that match the QOP and service values in the rpc_gss_rawcred_t structure are accepted.

For more information, see the rpc_gss_set_callback(3NSL) man page.

Maximum Data Size

Two functions, rpc_gss_max_data_length() on the client side, and rpc_gss_svc_max_data_length() on the server side, are useful in determining how large a piece of data can be before it is transformed by security measures and sent “over the wire.” A security transformation such as encryption usually changes the size of a piece of transmitted data, most often enlarging it. To make sure that data won't be enlarged past a usable size, these two functions return the maximum pre-transformation size for a given transport.

For more information, see the rpc_gss_max_data_length(3NSL) man page.

Miscellaneous Functions

You can use several functions for getting information about the installed security system.

Using these functions gives the programmer latitude in avoiding hard-coding security parameters in applications. (See Table 5–2 and the rpcsec_gss(3NSL) man page for a list of all RPCSEC_GSS functions.)

Associated Files

RPCSEC_GSS makes use of certain files to store information.

gsscred Table

When a server retrieves the client credentials associated with a request, the server can get either the client's principal name in the form of a rpc_gss_principal_t structure pointer or local UNIX credentials (UID) for that client. Services such as NFS require a local UNIX credential for access checking, but others might not. Those services can, for example, store the principal name directly in their own access control lists as a rpc_gss_principal_t structure.


Note –

The correspondence between a client's network credential (its principal name) and any local UNIX credential is not automatic. The local security administrator must be set up explicitly.


The gsscred file contains both the client's UNIX and network (for example, Kerberos V5) credentials. The network credential is the Hex-ASCII representation of the rpc_gss_principal_t structure. The gsscred file is accessed through XFN. Thus, this table can be implemented over files, NIS, or NIS+, or any future name service supported by XFN. In the XFN hierarchy, this table appears as this_org_unit/service/gsscred. Administrators can maintain the gsscred table with the use of the gsscred utility, which enables adding and deleting of users and mechanisms.

/etc/gss/qop and /etc/gss/mech

For convenience, RPCSEC_GSS uses string literals for representing mechanisms and quality of protection (QOP) parameters. The underlying mechanisms themselves, however, require mechanisms to be represented as object identifiers and QOPs as 32–bit integers. Additionally, for each mechanism, you need to specify the shared library that implements the services for that mechanism.

The /etc/gss/mech file stores the following information on all installed mechanisms on a system: the mechanism name, in ASCII; the mechanism's OID; the shared library implementing the services provided by this mechanism; and, optionally, the kernel module implementing the service. A sample line might look like this:


kerberos_v5   1.2.840.113554.1.2.2    gl/mech_krb5.so gl_kmech_krb5

For all mechanisms installed, the /etc/gss/qop file stores all the QOPs supported by each mechanism, both as an ASCII string and as its corresponding 32–bit integer.

Both /etc/gss/mech and /etc/gss/qop are created when security mechanisms are first installed on a given system.

Many of the in-kernel RPC routines use non-string values to represent mechanism and QOP. Therefore, applications can use the rpc_gss_mech_to_oid() and rpc_gss_qop_to_num() functions to get the non-string equivalents for these parameters, should they need to maximize use of those in-kernel routines.

Using Port Monitors

RPC servers can be started by port monitors such as inetd and listen. Port monitors listen for requests and spawn servers in response. The forked server process is passed the file descriptor 0 on which the request has been accepted. For inetd, when the server is done, it can exit immediately or wait a given interval for another service request. See also Appendix F, Writing a Port Monitor With the Service Access Facility (SAF).

For listen, servers should exit immediately after replying because listen() always spawns a new process. The following function call creates a SVCXPRT handle to be used by the services started by port monitors.

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

nconf is the netconfig structure of the transport from which the request is received.

Because the port monitors have already registered the service with rpcbind, the service does not need to register with rpcbind. The service must call svc_reg() to register the service procedure.

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

The netconfig structure is NULL to prevent svc_reg() from registering the service with rpcbind.

For connection-oriented transports, the following routine provides a lower level interface:

transp = svc_fd_create(0, recvsize, sendsize);

A 0 file descriptor is the first argument. You can set the value of recvsize and sendsize to any appropriate buffer size. A 0 for either argument causes a system default size to be chosen. Application servers that do not do any listening of their own use svc_fd_create().

Using inetd

Entries in /etc/inet/inetd.conf have different formats for socket-based, TLI-based, and RPC services. The format of inetd.conf entries for RPC services follows.

Table 5–3 RPC inetd Services

Service 

Description 

rpc_prog/vers

The name of an RPC program followed by a / and the version number or a range of version numbers. 

endpoint_type

One of dgram (for connectionless sockets), stream (for connection mode sockets), or tli (for TLI endpoints).

proto

May be * (for all supported transports), a net type, a net ID, or a comma separated list of net type and net ID. 

flags

Either wait or nowait.

user

Must exist in the effective passwd database. 

pathname

Full path name of the server daemon. 

args

Arguments to be passed to the daemon on invocation. 

For example:

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

For more information, see the inetd.conf(4) man page.

Using the Listener

Use pmadm to add RPC services:

pmadm -a -p pm_tag -s svctag -i id -v vers \

 	-m `nlsadmin -c command -D -R prog:vers`

The arguments are:

-a

adds a service

-p pm_tag

specifies a tag associated with the port monitor providing access to the service

-s svctag

server's identifying code

-i id

the /etc/passwd user name assigned to service svctag

-v ver

the version number for the port monitor's database file

-m

specifies the nlsadmin command to invoke the service. nlsadmin can have additional arguments. For example, to add version 1 of a remote program server named rusersd, a pmadm command would be:

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

The command is given root permissions, installed in version 4 of the listener database file, and is made available over TCP transports. Because of the complexity of the arguments and options to pmadm, use a command script or the menu system to add RPC services. To use the menu system, type sysadm ports and choose the -port_services option.

After adding a service, the listener must be re-initialized before the service is available. To do this, stop and restart the listener, as follows. rpcbind must be running.

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

For more information, such as how to set up the listener process, see the listen(1M), pmadm(1M), and sacadm(1M) man pages. Also see “How the TCP/IP Protocols Handle Data Communications” in System Administration Guide: IP Services.

Multiple Server Versions

By convention, the first version number of a program, PROG, is named PROGVERS_ORIG and the most recent version is named PROGVERS. Program version numbers must be assigned consecutively. Leaving a gap in the program version sequence can cause the search algorithm not to find a matching program version number that is defined.

Only the owner of a program should change version numbers. Adding a version number to a program that you do not own can cause severe problems when the owner increments the version number. Sun registers version numbers and answers questions about them (rpc@Sun.com).

Suppose a new version of the ruser program returns an unsigned short rather than an int. If you name this version RUSERSVERS_SHORT, a server that supports both versions would do a double register. Use the same server handle for both registrations.


Example 5–15 Server Handle for Two Versions of Single Routine

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);
}

Both versions can be performed by a single procedure, as shown in the following example.


Example 5–16 Procedure for Two Versions of Single Routine

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:
			/*
			 * Code here to compute the number of users
			 * and assign it to the variable 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;
}

Multiple Client Versions

Because different hosts can run different versions of RPC servers, a client should be capable of accommodating the variations. For example, one server can run the old version of RUSERSPROG(RUSERSVERS_ORIG) while another server runs the newer version (RUSERSVERS_SHORT).

If the version on a server does not match the version number in the client creation call, clnt_call() fails with an RPCPROGVERSMISMATCH error. You can get the version numbers supported by a server and then create a client handle with the appropriate version number. Use either the routine in the following example, or clnt_create_vers(). See the rpc(3NSL) man page for more details.


Example 5–17 RPC Versions on Client Side

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;							      /* set the time outs */
	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) {		/* Found latest version number */
		printf("num = %d\n", num_s);
		exit(0);
	}
	if (status != RPC_PROGVERSMISMATCH) {		/* Some other error */
		clnt_perror(clnt, "rusers");
		exit(1);
	}
	/* This version not supported */
	clnt_geterr(clnt, &rpcerr);
	maxvers = rpcerr.re_vers.high;		  /* highest version supported */
	minvers = rpcerr.re_vers.low;		     /*lowest version supported */

	if (RUSERSVERS_SHORT < minvers || RUSERSVERS_SHORT > maxvers)
{
			                           /* doesn't meet minimum standards */
		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)
			                 /* We found a version number we recognize */
		printf("num = %d\n", num_l);
	else {
		clnt_perror(clnt, "rusers");
		exit(1);
	}
}

Using Transient RPC Program Numbers

Occasionally, an application could use RPC program numbers that are generated dynamically. This technique could be used for implementing callback procedures, for example. In the callback example, a client program typically registers an RPC service using a dynamically generated, or transient, RPC program number. The program then passes this number on to a server along with a request. The server then calls back the client program using the transient RPC program number in order to supply the results.

This mechanism might be necessary if processing the client's request takes an excessive amount of time and the client cannot block, assuming it is single threaded. In this case, the server acknowledges the client's request, and calls back later with the results.

Another use of callbacks is to generate periodic reports from a server. The client makes an RPC call to start the reporting, and the server periodically calls back the client with reports using the transient RPC program number supplied by the client program.

Dynamically generated, or transient, RPC program numbers are in the transient range 0x40000000 - 0x5fffffff. The following routine creates a service based on a transient RPC program for a given transport type. The service handle and the transient RPC program number are returned. The caller supplies the service dispatch routine, the version, and the transport type.


Example 5–18 Transient RPC Program—Server Side

SVCXPRT *register_transient_prog(dispatch, program, version, netid)
	void (*dispatch)(); /* service dispatch routine */
	rpcproc_t *program;    /* returned transient RPC number */
	rpcvers_t version;     /* program version */
	char *netid;        /* transport 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);
}