Inhalt

6. RPC-Programmierung II, / vertiefte Methoden

6.1 Debugging mit Raw RPC

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.

6.2 Aufbau eines Server-Dispatchers

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.

6.3 Server mit verschiedenen Versionen

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

6.4 Beispiel einer TCP-Anwendung

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

6.5 Techniken der asynchronen Kommunikation

Broadcasting

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

Batching

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:

  1. Als Transport muß TCP gewählt werden.
  2. XDR-Filter für Ergebnisse muß NULL sein.
  3. timeout muß Null sein.

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

Callback-Prozeduren

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.

6.6 Sicherheitsmechanismen

RPC bietet drei Stufen der sicheren Identifikation (Identification) und der Prüfung, ob diese Identifikation authentisch ist (Authentication):

  1. keine spezielle Sicherheitsmechanismen
  2. UNIX-spezifische Identifikation
  3. Diffie-Hellman/DES Authentizität

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.

UNIX-spezifische Identifikation

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.

Diffie-Hellman/DES Authentizität

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

6.7 Benutzung des inetd Dämons

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

6.8 RPC Error Bibliothek

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


Inhalt