Unter Raw RPC versteht man eine Testumgebung für Low-level RPC. Sie besteht aus einem Programm, das sowohl den Client- als auch der Server-Stub enthält. Ausgelassen wird die ganze Netzwerkkommunikation. Es folgt ein Beispiel:
#include <stdio.h>
#include <rpc/rpc.h>
#include <rpc/raw.h>
main ()
{
CLIENT *cp;
SVCXPRT *sp;
int ni, no;
struct timeval timeout = {0, 0};
sp = svcraw_create ();
svc_register (svc, 200000, 1, server, 0);
cp = clntraw_create (200000, 1);
clnt_call (cp, 1, xdr_int, &ni, xdr_int, &no, timeout);
printf ("input %d output %d\n", ni, no);
}
server (req, transp)
struct svc_req *req;
SVCXPRT *transp;
{
int n;
svc_getargs (transp, xdr_int, &n); /* get client data n */
n++;
svc_sendreply (transp, xdr_int, &n); /* pass n to the client */
}
Die Testumgebung erlaubt es den Client- und den Server-Stub, sowie die
Server-Prozedur beliebig auszubauen und ihre Funktionsweise ohne Netzwerk zu
prüfen. Nach abgeschlossenem Test kann ...raw_create()
durch
..._create()
ersetzt werden und das Programm auf Client und
Server aufgespaltet werden.
Alle anderen RPC-Routinen sind so gestaltet, daß sie in einem Prozeß, der
auch mit Standardwerkzeugen (sdb) debuggt werden kann, zusammenspielen.
Von den drei Middle-level Routinen registerrpc()
,
svc_run()
und callrpc()
wurde svc_run noch nicht
in einfachere Bestandteile zerlegt. Das
kann jedoch notwendig sein, wenn der Server zusätzlich zu der
Endlosschleife mit dem Warten auf einen Request und seiner Bearbeitung noch
weitere Aufgaben erledigen soll. Der vollständige Code für den standard
Dispatcher sieht wie folgt aus:
void svc_run ()
{
fd_set readfds;
int dtbsz = getdtablesize ();
for (;;) {
readfds = svc_fdset;
switch (select (dtbsz, &readfds, NULL, NULL, NULL)) {
case -1:
if (errno != EBADF)
continue;
perror ("select");
return;
case 0
continue;
default:
svc_getreqset (&readfds);
}
}
}
Hier sehen wir eine Endlosschleife mit dem Warten auf einen Request mit
select() und der Verzweigung zu der Service-Routine mit
svc_getreqset()
.
svc_getreqset()
ruft den eigentlichen Service auf und schickt seine
Ergebnisse an der Client zurück. In dieser Schleife kann der Benutzer
zusätzlichen Code einbauen, der meistens mit dem Warten auf andere
Ereignisse und
der Reaktion auf sie verknüpft ist. Durch die richtige Erweiterung der
Maske svc_fdset kann mithilfe von einem select()
auf RPC-Requests
und gleichzeitig auf andere Events gewartet werden.
svc_fdset ist eine globale read-only Variable aus der RPC-Bibliothek.
Damit der Server nach dem Auftragsempfang gleich wieder einen neuen
Auftrag empfangen kann, muß er mit der Auftragsbearbeitung einen
Kindprozeß betrauen. Damit der Dispatcher keine leeren Ergebnisse an den
Client zurückschickt bevor die entfernte Prozedur zu Ende abgelaufen ist,
muß fork()
im Dispatcher aufgerufen werden.
Wenn die entfernte Prozedur mehrere Versionen hat, was bei der Entwicklung oft der Fall ist, ist es sinnvoll, daß sie alle durch den gleichen Dispatcher bzw. mithilfe des Wrappers unterstützt werden, z.B.:
main () /* Server */
{
.
.
int proc();
.
.
svc_register (transp, PROGNR, VERS_SHORT, proc, IPPROTO_TCP);
svc_register (transp, PROGNR, VERS_LONG, proc, IPPROTO_TCP);
.
.
svc_run ();
}
proc (req, transp) /* wrapper */
struct svc_req *req;
SVCXPRT *transp;
{
extern short proc1(); /* procedures differ in ret value */
extern long proc2();
static short n1;
static long n2;
switch (req->rq_proc {
case NULLPROC:
.
case PROCNR:
switch (req->rq_vers {
case VERS_SHORT:
n1 = proc1 (req, transp);
svc_sendreply (transp, xdr_short, &n1);
break;
case VERS_LONG:
n2 = proc2 (req, transp);
svc_sendreply (transp, xdr_long, &n2);
break;
}
default:
svcerr_noproc (transp);
}
}
Ist das nicht der Fall, so kann der Client anhand der zurückgegebenen Fehlermeldung die unterstützte Version herausfinden:
main () /* Client */
{
struct rpc_err err;
short s;
long l;
.
.
cl = clnt_create (host, PROGNR, VERS2, "udp");
switch (clnt_call (cl, PROCNR, xdr_void, NULL, xdr_long,
&l, timeout)) {
case RPC_SUCCESS: /* version ok. */
printf ("%d\n", l);
break;
case RPC_PROGVERSMISMATCH: /* try former version */
clnt_geterr (cl, &err);
if (err.re_vers.high < VERS2) {
clnt_destroy (cl);
cl = clnt_create (host, PROGNR, VERS1, "udp");
clnt_call (cl, PROCNR, xdr_void, NULL, xdr_short,
&s, timeout);
printf ("%d\n", s);
}
break;
}
}
Das hier gezeigte Beispiel lehnt sich an die SUN rcp-Implementierung
an. Es zeigt eine typische TCP-Anwendung. Die stdin wird auf der Client
Seite gelesen, zum Server transportiert und dort von der entfernten Prozedur
rcp auf dem stdout geschrieben. Die Lese/Schreib-Operation übernimmt dabei
ein untypischer XDR-Filter xdr_rcp()
, der gegenüber ENCODE/DECODE
unsymmetrisch ist.
Der Client-Teil sieht so aus:
#include <stdio.h>
#include <netdb.h>
#include <rpc/rpc.h>
#include <sys/socket.h>
#include "rcp.h" /* with PROG-, VERS-, PROC */
main (argc, argv) /* client side */
int argc;
char *argv[];
{
int xdr_f();
int socket = RPC_ANYSOCK
struct sockaddr_in server;
struct hostent *hp;
struct timeval tio;
CLIENT *cl;
hp = gethostbyname (argv[1]);
bcopy (hp->h_addr, (caddr_t)&server.sin_addr), hp->h_length);
server.sin_family = AF_INET;
server.sin_port = 0;
cp = clnttcp_create (&server, PROG, VERS, &socket,
BUFSIZ, BUFSIZ);
tio.tv_sec = 20;
tio.tv_usec = 0;
clnt_call (cl, PROC, xdr_f, stdin, xdr_void, 0, tio);
clnt_destroy (cl);
}
Es folgt der Server-Teil:
#include <stdio.h>
#include <rpc/rpc.h>
#include "rcp.h"
main () /* server side */
{
SVCXPRT *transp;
int rpc();
transp = svctcp_create (RPC_ANYSOCK, BUFSIZ, BUFSIZ);
pmap_unset (PROG, VERS);
svc_register (transp, PROG, VERS, rpc, IPPROTO_TCP);
svc_run ();
}
rcp (req, transp) /* remote procedure */
struct svc_req *req;
SVCXPRT *transp;
{
extern int xdr_f();
switch (req->rq_proc) {
case NULLPROC :
svc_sendreply (transp, xdr_void, 0);
break;
case RPCPROC :
svc_getargs (transp, xdr_f, stdout);
svc_sendreply (transp, xdr_void, 0);
break;
default :
svcerr_noproc (transp);
break;
}
}
mit dem folgenden Filter:
#include <stdio.h>
#include <rpc/rpc.h>
xdr_rcp (xdr, fp) /* XDR-Filter */
XDR *xdr;
FILE *fp;
{
unsigned long size = 0;
char buf[BUFSIZ], *p = buf;
if (xdr->x_op == XDR_FREE)
return (1);
while (1) {
if (xdr->x_op == XDR_ENCODE) /* in client */
size = fread (p, sizeof (char), BUFSIZ, fp);
if (!xdr_bytes (xdr, &p, &size, BUFSIZ))
return (0);
if (size == 0)
return (1);
if (xdr->x_op == XDR_DECODE) /* in server */
fwrite (p, sizeof (char), size, fp);
}
}
Unter Broadcast-RPC versteht man die Situation, in der ein Client sich gleichzeitig an mehrere Server mit einem Call-Request wendet. Natürlich muß er mehrere Antworten erwarten können. Ein Thread kann jedoch nur eine Verbindung aufbauen. Verbindungsorientiere Transporte (TCP) kommen aus diesem Grunde nicht in Frage. Da Broadcasting meistens dann eingesetzt wird, wenn nicht genau bekannt ist, welcher Server welche Services anbietet, werden Fehlerantworten auf der Client Seite automatisch ausgefiltert, und das Client Programm bekommt darüber (z.B. falsche Versionsnummer) keine Information.
Parameter von Broadcast Call-Requests sind auf eine maximum transfer unit (MTU) Größe beschränkt. Sie beträgt für Ethernet 1,5 kByte, abzüglich des RPC-Header sind das 1,4 k. Die Antworten sind wie gewöhnt auf UDP-Packetgröße (z.Zt. 8,8k) beschränkt. Wie weit Broadcasting ausgestrahlt wird, hängt von dem Netzwerk ab. Für Ethernet sind alle Hosts im LAN die Empfänger. Weitere Verbreitung hängt von den Routern ab.
Die Call-Request-Pakete gehen, wie bei jedem Broadcasting, nur einmal
an die Server. Der Weg von Bild 11, in dem sich der Client nach dem Erhalt
der Portmapperinformation zum zweiten Mal, jetzt an den richtigen Port
wendet, wird ausgespart. Der Portmapper muß mithilfe von callit()
den richtigen Serverprozeß selbst anrufen.
------------------ -------------------------
| Client | | Server |
| | | |
| ------------ | ----------- ---------- |
| | | -------------------------> | Port | ---> | Port- | |
| | Client- | | ------- | 111 | <--- | mapper | |
| | | | | ----------- ---------- |
| | Programm | | callit() | | |
| | | | | ----------- ----------- |
| ------------ | ------> | Port | ---> | Server- | |
| | | a | <--- | program | |
| | ----------- ----------- |
| | | |
------------------ -------------------------
Bild 13
Broadcasting wird mit nur einem Aufruf clnt_broadcast()
anstelle
von clnt_call()
eingeschaltet. Damit werden Client Broadcast
Call-Requests
abgeschickt und Antworten empfangen. Die Synopsis sieht wie folgt aus:
enum clnt_stat clnt_broadcast (prognum, versnum, procnum
outproc, outdata, inproc, indata,
eachresult)
u_long prognum,
versnum,
procnum;
xdrproc_t outproc;
caddr_t outdata;
xdrproc_t inproc;
caddr_t indata;
bool_t (*eachresult)();
Dabei sind prog-, vers-, procnum die übliche Identifikation der
entfernten Prozedur, out/indata Aufruf- und Ergebnisdaten und out/inproc
die zugehörigen XDR-Filter. Bei jedem Empfang einer Antwort ruft
clnt_broadcast()
die Funktion eachresult()
mit
folgenden Parametern auf:
bool_t eachresult (resultp, raddr)
caddr_t resultp;
struct sockaddr_in raddr;
Diese Funktion muß der Benutzer selbst liefern. Sie muß so gebaut
sein, daß clnt_broadcast()
anhand ihres Returnwertes (Typ bool_t)
erkennen
kann, ob sie auf weitere Server-Antworten warten soll (FALSE) oder nicht
(TRUE). Bei TRUE kommt clnt_broadcast mit RPC_SUCCESS zurück. Bei einer
genügend großen Zahl der FALSE Ergebnisse von eachresult()
wiederholt die
Broadcast-Routine Call-Requests in den Zeitabständen von 4, 6, 8, 10, 12
und 14 Sek. Das ist bei nicht verläßlichem (unreliable) Transport wie UDP
sinnvoll. Nach zusammen 54 Sek. kommt clnt_broadcast() mit RPC_TIMEOUT
zurück. Ist eachresult()
ein Nullpointer, so wird zwar Broadcast
ausgesendet, jedoch auf keine Antworten gewartet.
resultp ist der Zeiger auf Ergebnisdaten nach ihrer XDR-Umwandlung mit
inproc()
, raddr die Adresse des jeweiligen Servers.
Broadcast-RPC wendet standardmässig den AUTH_UNIX Sicherheitsmechanismus an, den man auch nicht auf andere umschalten kann.
Es folgt ein Beispielprogramm, das es möglich macht, den richtigen Host im LAN festzustellen, der die gesuchte Prozedur als Server anbietet.
#include <sys/socket.h>
#include <netdb.h>
#include <strings.h>
static char host[HOSTNAME_SIZE+1];
char *findserver ()
{
int reply();
host[0] = 0; /* clean previous result */
if (clnt_broadcast (PROGNR, VERSNR, NULLPROC,
xdr_void, NULL, XDR_void, NULL, reply) == RPC_SUCCESS)
return (host);
return (NULL);
}
reply (data, server)
void *data;
struct sockaddr_in *server;
{
struct hostent *hostentp;
if (hostentp = gethostbyaddr (&server->sin_addr.s_addr,
sizeof (server->sin_addr.s_addr), AF_INET)) {
strncpy (host, hostentp->h_name, HOSTNAME_SIZE);
return (1);
}
return (0);
}
Unter Batching versteht man eine Situation, in der der Client eine Reihe von Aufträgen (»in Batch«) an den Server verschickt ohne auf die Ergebnisse zu warten. Sinnvollerweise soll der Server in diesem Fall auch keine Teilergebnisse liefern. Damit wird das Schema vom Bild 1 außer Kraft gesetzt: Der Client verliert keine Zeit und kann eventuelle Ergebnisse später abholen. Während die Aufträge abgearbeitet werden, kann der Client sogar andere RPC-Requests an den gleichen Server verschicken.
Die Aufträge werden in einem TCP-Puffer positioniert und mit einem
sicheren (reliable) Transport (TPC/IP) an den Server geleitet. Die letzte
Aktion geschieht oft mit einem write()
auf einmal. Damit wird die
Netzbelastung vermindert. Das Leeren des TCP-Puffers kann der Client mit
einem Remote-Call ohne Batching erreichen.
Um Batching »einzuschalten« muß der Client folgende Bedingungen erfüllen:
Die Bedingungen für kein Warten auf Antworten (2. & 3.) tretten auch oft ohne Batching (ohne TCP) auf, wenn z.B. der Timer-Client in Zeitintervallen unzuverlässige (unreliable) Messages mit dem Time-Update an den/die Server schickt und an den Antworten nicht interessiert ist. Solche Situationen (keine Antworten) bezeichnet man auch als nonblocking RPC.
Hier ist ein Beispiel für Batching:
#include <rpc/rpc.h>
#define NULL ((char *)0)
#define BSIZE 256
main (argc, argv)
int argc;
char *argv[];
{
struct timeval timeout;
register CLIENT *cl;
char buf[BSIZE], *s = buf;
cl = clnt_create (argv[1], PROGNR, VERSNR, "tcp");
timeout.tv_sec = 0;
timeout.tv_usec = 0;
while (scanf ("%s", s) != EOF)
clnt_call (cl, PROCNR, xdr_wrapstring, &s, NULL, NULL,
timeout);
/* flush the TCP-pipeline now */
timeout.tv_sec = 20;
clnt_call (cl, NULLPROC, xdr_void, NULL, xdr_void, NULL,
timeout);
clnt_destroy (cl);
}
Anders als in unserem Client-Server Modell ist es manchmal sinnvoll, daß der Server eine mehr aktive Rolle als Kommunikationspartner übernimmt und selbst Requests (Callbacks) an den bisherigen Client sendet. Dabei muß der bisherige Client eine Prozedur registriert haben und auf ihren Aufruf warten.
Ein Beispiel für eine solche Situation wäre ein entfernt ablaufendes Programm, das ein lokales Fenstersystem benutzt. Die Benutzereingabe findet lokal durch die Fenster statt. Sie läßt eine Aktion im entfernten Server aus, der nicht passiv antwortet, sondern selbst entscheidet welche Bildumformung im lokalen Client stattfinden soll und die zugehörige Fensterfunktion durch einen Callback aufruft.
In dem folgenden Beispiel muß die Callbackroutine jeweils neu registriert
werden. gettransient()
wählt die richtige Programnummer aus dem
vorübergehend (transient) besetzten Bereich zwischen 0x400000000 bis
0x5fffffff.
gettransient (protocol, vers, port)
int protocol;
u_long vers;
u_short port;
{
static u_long prognum = 0x40000000;
while (!pmap_set (prognum++, vers, protocol, port))
continue;
return (prognum - 1);
}
prognum ist static, um die Suche nicht jedes Mal von Anfang an zu starten.
pmap_set()
scheitert, solange eine Registrierung ungültig war.
#include <stdio.h>
#include <rpc/rpc.h>
#include "example.h"
callback (req, transp)
struct svc_req *req;
SVCXPRT *transp;
{
if (req->rq_proc == 1)
fprintf (stderr, "got callback\n");
svc_sendreply (transp, xdr_void, 0);
return (0);
}
main () /* client */
{
int prognum;
char hostname[256];
SVCXPRT *xprt;
gethostbytname (host, sizeof (hostname));
svcudp_create (RPC_ANYSOCK));
prognum = gettransient (IPPROTO_UDP, 1, xprt->xp_port);
svc_register (xprt, prognum, 1, callback, 0);
callrpc (hostname, EXAMPLEPROG, EXAMPLEVERS, CALLBACK,
xdr_int, &prognum, xdr_void, 0);
svc_run ();
}
Der dazugehörige Server sieht wie folgt aus:
#include <stdio.h>
#include <rpc/rpc.h>
#include <signal.h>
#include "example.h"
int pnum = -1;
char hostname[256];
char *getnewprog (pnump)
int *pnump;
{
pnum = *(int *)pnump;
return (NULL);
}
docallback ()
{
if (pnum == -1) {
signal (SIGALRM, docallback);
return (0);
}
if (callrpc (hostname, pnum, 1, 1,
xdr_void, 0, xdr_void, 0) != RPC_SUCCESS)
fprintf (stderr, "server error\n");
}
main () /* server */
{
gethostbytname (host, sizeof (hostname));
registerrpc (EXAMPLEPROG, EXAMPLEVERS, CALLBACK,
getnewprog, xdr_int, xdr_void);
signal (SIGALRM, docallback);
alarm (10);
svc_run ();
}
Das Beispiel ist nur in einem Rechner lauffähig, aber die Verteilung des angegebenen Codes auf zwei Computer bereitet keine Probleme.
Der Callback-Mechanismus birgt die Gefahr von Deadlocks in sich.
RPC bietet drei Stufen der sicheren Identifikation (Identification) und der Prüfung, ob diese Identifikation authentisch ist (Authentication):
Die Programmierschnittstelle zu den Sicherheitsmechanismen besteht aus zwei Feldern in der Service-Request Struktur: einem für die grobe Information über die eingesetzten Algorithmen (rq_cred) und einem für die algorithmusspezifischen Daten (rq_clntcred).
/* RPC service request */
struct svc_req {
u_long rq_prog;
u_long rq_vers;
u_long rq_proc;
struct opaque_auth rq_cred;
caddt_t rq_clntcred; /* read only */
};
mit
struct opaque_auth {
enum_t oa_flavor; /* style of credentials */
caddr_t oa_base; /* address of more auth stuff */
u_int oa_length; /* length of oa_base field */
Der Typ des zweiten Feldes (caddt_t) ist absichtlich ein allgemeiner
Zeiger, weil hier je nach Algorithmus Adressen von verschiedenen Strukturen
übertragen werden: Bei Stufe 2 - Identifizierungen (credential), bei Stufe
3 - Verifikationsfelder (verifier).
Darüberhinaus gibt es ein Feld in der CLIENT-Struktur: cl_auth. Selbstverständlich gehören zur vollständigen Interface-Definition noch die Angaben darüber, wie diese Felder von RPC gehandhabt werden.
In der ersten Sicherheitsstufe (defaultmäßig) wird oa_flavor in rq_cred mit AUTH_NULL und cl_auth mit authnone_create() automatisch vorbesetzt.
struct svc_req req;
CLIENT cl;
req.eq_cred->oa_flavor = AUTH_NONE;
cl.cl_auth = authnone_create();
Auch in diesem Fall besteht jedoch eine Identifizierung des Call-Requests
und des Reply-Messages nach Programm-, Version-, Prozedurnummer
etc., wie das aus der svc_req Struktur ersichtlich ist. Genauere Angaben
darüber stehen im Kapitel über das RPC Message Protokoll.
In der zweiten Stufe (UNIX-spezifische Identifikation) werden in dem rq_clntcred-Feld UNIX-spezifische Informationen wie der Rechnername, User-Id, Group-Id, etc. übertragen. Das dient nur einer zusätzlichen Identifikation. Die Sicherheit, daß sie nicht vorgetäuscht ist, gibt es nicht. Trotz der Verwendung einiger Namen mit »auth« auf dieser Stufe gibt es hier keine Authentizitätsprüfung.
Erst in der dritten Sicherheitsstufe unterliegt jede Message einer Prüfung, ob die angegebenen Identifikationen authentisch sind. Dazu wird die Methode von Diffie-Hellman verwendet. Der dabei benutzte Verschlüsselungsalgorithmus ist DES (Data Encryption Standard), der durch die Password-Verschlüsselung in UNIX grosse Verbreitung fand.
Der zweiter Vorteil dieser Stufe liegt in der betriebssystemunabhängigen User-Identifikation. Sie funktioniert auch auf Computer ohne UNIX, so z.B. bei MS-DOS, wo allein der Begriff eines Users zunächst nicht vorhanden ist. Hier werden zusätzliche Namen eingeführt, sog. Netnames, die jeden Kommunikationspartner auf jedem Computer eindeutig für alle Netzwerke identifizieren.
Die Einfachheit des Interfaces erlaubt fast beliebige Erweiterungen dieses Schemas durch höhere Sicherheitsstufen, mit anderen benutzerimplementierten Prüfungen und Verschlüsselungen, deren Informationen durch die dehnbaren rq_cred- und rq_clntcred-Felder übertragen werden. Zusätzlich besteht immer die Möglichkeit, die Sicherheitsmechanismen in höheren auf RPC aufsetzenden Protokollen zu implementieren. Das erweist sich oft als sinnvoll.
Die zweite Sicherheitsstufe (UNIX-spezifische Identifiakation) wird im Client durch die Besetzung der Variable cl_auth eingeschaltet, die nach der Erstellung des Client-Handles erfolgt:
CLIENT *clnt;
.
clnt = clntudp_create (address, prognum, versnum, waittime, sockptr);
clnt->cl_auth = authunix_create_default ();
Die Funktion authunix_create_default() besetzt automatisch
1. die oa_flavor mit AUTH_UNIX und
2. rq_clntcred mit dem Zeiger auf die aufgefüllte Struktur authunix_perms:
struct authunix_perms {
u_long aup_time; /* credential creation time */
char *aup_machname; /* client's host name */
int aup_uid; /* client's UNIX effective uid */
int aup_gid; /* client's current group id */
u_int aup_len; /* element length of aup_gids */
int *aup_gids; /* array of groups user is in */
Die Verwendung dieser Felder, die an den Dispatcher des entfernten Servers transferiert werden, soll folgendes Beispiel verdeutlichen, in dem die Serverdienste nur auf zwei User uid=104 und uid=105 beschränkt sind.
nuser (req, transp)
struct svc_req *req;
SVCXPRT *transp;
{
struct authunix_parms *up;
static int n;
if (req->rq_proc == NULLPROC) {
/*
** customary no auth-check for NULLPROC
*/
svc_send_reply (transp, xdr_void, 0);
return;
}
switch (req->rq_cred.oa_flavor) {
case AUTH_UNIX:
up = (struct authunix_parms *) req->rq_clnt_cred;
break;
case AUTH_NULL:
default:
/*
** only weak error if flavor type not appropriate
*/
svcerr_weakauth (transp);
return;
}
switch (req->rq_proc) {
case USERSPROG_NUM:
if (up->aup_uid != 104 ||
up->aup_uid != 105) {
svcerr_systemerr (transp); /* access denied - hard error */
return;
}
/* calculate n */
.
.
svc_send_reply (transp, xdr_u_long, &n);
return;
default:
svcerr_noproc (transp);
return;
}
}
Nach dem Einsatz der Felder dieser Sicherheitsstufe sollte der Client sie wieder mit:
auth_destroy (clnt->cl_auth);
zerstören und damit den belegten Speicherplatz freigeben.
In einem LAN bietet die UNIX-spezifische Identifikation meistens ausreichende Sicherheit, da die Vortäuschung eines anderen Partners den ursprünglichen Partner aus der Kommunikation nicht eliminiert. Das ist anders bei Gateways: Die richtigen Pakete können abgefangen werden, und dann an ihrer Stelle die falschen geschickt werden. Außer diesem Problem der »Schreibberechtigung« kann die »Leseberechtigung« durch Verschlüsselungen auf höheren Protokollebenen gut geregelt werden.
Die Diffie-Hellman Methode bietet die Sicherheit, daß die Pakete von dem angegeben Absender (Netname) kommen. Die Voraussetzung dafür ist bei RPC NIS mit publickey.byname und netid.byname Datenbasis. Dabei muß der öffentliche Schlüssel (publickey) mit DES-Encryption-Kit verschlüsselt werden. DES-Encryption-Kit ist durch Comecon-Beschränkungen nur in den Vereinigten Staaten zugelassen und in Europa vertriebenen UNIX-Systemen zur Zeit nicht vorhanden.
Die dritte Sicherheitsstufe wird durch:
CLIENT *clnt;
.
clnt = clntudp_create (address, prognum, versnum, waittime, sockptr);
clnt->cl_auth = authdes_create (netname, timecred, syncaddr, deskeyp);
eingeschaltet. Dabei ist netname der Netzname des Serverprozesses, der mit:
char *netname[MAXNETNAMELEN+1];
.
user2netname (netname, getuid (), domain);
oder
host2netname (netname, rhostname, domain);
berechnet werden kann. Timecred ist die Zeit in Sekunden, wie lange die
abgeschickte Message gültig ist. Syncaddr ist die Socketadresse, durch die
die Zeitsynchronisation erfolgen kann. Syncaddr kann auf NULL gesetzt
werden. Auf die Serveradresse gesetzt bewirkt sie, daß ohne Synchronisation
nur die Zeit des Servers verwendet wird. deskeyp ist eine ebenfalls
optionale Adresse des verwendeten DES Schlüssels.
Der so initialisierte Client verschickt im rq_clntcred-Feld die für DES charakteristischen Daten vom Typ struct authdes_cred. Sie können im Server wie folgt benutzt werden:
#include <sys/time.h>
#include <rpc/auth_des.h>
nuser (req, transp)
struct svc_req *req;
SVCXPRT *transp;
{
struct authdes_cred *dc;
int uid, gid, gidlen, gidlist[10];
.
.
switch (req->rq_cred.oa_flavor) {
case AUTH_DES:
dc = (struct authdes_cred *) req->rq_clnt_cred;
/*
** get user and group for this netname
*/
if (! netname2user (dc->adc_fullname.name,
&uid, &gid, &gidlen, gidlist)) {
fprintf (stderr, unknown user: %s\n",
dc->adc_fullname.name);
svcerr_systemerr (transp);
return;
}
break;
case AUTH_NULL:
default:
svcerr_weakauth (transp);
return;
}
.
.
}
Wie die meisten Dämon-Prozesse lassen sich RPC-Server vom Internet Services Dämon inetd(8) starten, der anhand des Konfigurationsfiles /etc/inetd.conf die dort spezifizierten Prozesse nur dann anstößt, wenn deren Services gebraucht werden. Die Einträge im config-File inetd.conf sehen wie folgt aus:
p_name/version dgram rpc/udp wait/nowait user server args
p_name/version stream rpc/tcp wait/nowait user server args
wobei version auch ein Versionsbereich, z.B. 1-3 sein kann. Bei der
inetd-Benutzung müssen zwei Punkte beachtet werden:
1. Der Server muß die Kontrolle an inetd explicit durch exit()
abgeben und dadurch die unendliche Schleife in svc_run()
beenden.
2. Die Aufrufparameter der Routinen für die Transporterzeugung müssen entsprechend geändert werden:
transp = svcudp_create (0);
transp = svctcp_create (0, 0, 0);
transp = svcfd_create (0, 0, 0);
svc_register (transp, PROGNUM, VERSION, service, 0);
RPC-Library stellt eine Reihe Error-Routinen zur Verfügung. Sie werden teilweise von anderen RPC-Funktionen aufgerufen oder können vom Benutzer selbst verwendet werden. Im ersten Fall ermöglicht die Kenntnis ihrer Namen und Parameter, daß sie vom Benutzer durch eigenen Code ersetzt werden. Das wird erreicht, indem man dem Linker eigene Routinen vor der RPC-Bibliothek voranstellt.
Die folgenden Routinen werden im Serverstub aufgerufen. Sie nehmen als
Parameter nur einen Zeiger auf eine SVCXPRT-Struktur und geben void zurück.
Als Ausnahme braucht svcerr_auth()
zwei Parameter. Die meisten
benachrichtigen auch den Client:
svcerr_auth () Verletzung eines Sicherheitsmechanismus, (zweiter
Parameter vom Typ enum auth_stat)
svcerr_decode () kann die übertragenen Parametern nicht dekodieren
svcerr_noproc () Prozedur mit dieser Nummer wird nicht angeboten
svcerr_noprog () Programm mit dieser Nummer wurde nicht registriert
svcerr_progvers () Programm mit dieser Version wurde nicht registriert
svcerr_systemerr () Systemfehler, jedoch von keinem Protokoll
svcerr_weakaout () Sicherheitsmechanismus zwar nicht verletzt, aber
die Sicherheitsstufe zu niedrig
Folgende Routinen werden im Clientstub aufgerufen:
void clnt_pcreateerror (str) Error bei Client-Erstellung (auf stderr)
char *clnt_spcreateerror (str) " , Meldung nur als Rückgabewert
void clnt_perrno (stat) stderr-Ausgabe über die Variable stat
char *clnt_sperrno (stat) ", Meldung nur als Rückgabewert
void clnt_perror (handle, str) Message über fehlerhaften clnt_call
char *clnt_sperror (handle, str) ", Meldung nur als Rückgabewert
Die Parameter sind von Typ:
char *str;
enum clnt_stat stat;
CLIENT *handle;
Die Variable clnt_stat (Ergebnis eines RPC-Calls) kann folgende Werte
annehmen, deren Bedeutung am Namen gut zu erkennen ist. Die genaue
Beschreibung befindet sich in den Manual Pages:
RPC_SUCCESS
RPC_CANTENCODEARGS
RPC_CANTDECODERES
RPC_CANTSEND
RPC_CANTRECV
RPC_TIMEDOUT
RPC_VERSMISMATCH Version der ganzen RPC-Bibliothek
RPC_AUTHERROR
RPC_PROGUNAVAIL
RPC_PROGVERSMISMATCH
RPC_PROCUNAVAIL
RPC_CANTDECODEARGS
RPC_SYSTEMERROR
RPC_UNKNOWNHOST
RPC_UNKNOWNPROTO
RPC_PMAPFAILURE
RPC_PROGNOTREGISTERED
RPC_FAILED