RFC 1050

RPC: Remote Procedure Call Protocol specification

πŸ“… published April 1988, from ietf

bestie, let's get into RFC 1050 πŸ¦‹πŸ’–

It's from the Network Working Group, RFC number 1050, dated April 1988 πŸ“…. The title is "RPC: Remote Procedure Call Protocol Specification" πŸŽ€. The author is Sun Microsystems, Inc., and no individual authors are listed, so the whole company gets the credit πŸ’…. It doesn't say it obsoletes or updates anything, so there's nothing to report there πŸ™ˆ.

Status of This Memo πŸ“œ

okay so this RFC describes a standard that Sun Microsystems and others are already using, and they want to propose it for the Internet to consider 🫢. It is not an Internet standard yet, so don't treat it like one just yet πŸ’€. You're free to pass this memo around, because distribution is unlimited 🌸.

1. Introduction ✨

so this doc specifies a message protocol that's used to implement Sun's Remote Procedure Call (RPC) package πŸ”₯. The messages are described using the eXternal Data Representation (XDR) language [9] 🧚. It assumes you already know XDR, and it doesn't try to convince you why RPC is useful πŸ’…. If you want a good background on it, the paper by Birrell and Nelson [1] is recommended as an excellent intro πŸ“šβœ¨.

2. Terminology πŸŽ€

here's the vocab. We're talking about servers, services, programs, procedures, clients, and versions πŸ’œ. A server is software where network services get implemented πŸ–₯️. A network service is a collection of one or more remote programs 🌈. A remote program implements one or more remote procedures, and those procedures, their parameters, and their results are written down in that program's protocol specification (see Appendix A for an example) πŸ“. Network clients are software that start remote procedure calls to services πŸ¦‹. A server can support more than one version of a remote program so it stays forward compatible as protocols change πŸ”₯.

for example, a network file service could be made of two programs πŸ’–. One program might handle high-level stuff like file system access control and locking πŸ”. The other could handle low-level file IO and have procedures like "read" and "write" πŸ“. A client machine of the network file service calls the procedures from those two programs on behalf of some user on that client machine 🫢.

3. The RPC Model 🧚

the remote procedure call model is kinda like a local procedure call πŸ’…. In the local case, the caller puts the arguments for a procedure in some well-specified place (like a result register) ✨. Then it transfers control to the procedure and eventually gets control back πŸ”. At that point the results get pulled from that well-specified place, and the caller keeps going 🌸.

the remote version is similar because one thread of control logically winds through two processes πŸ’œ. One is the caller's process and the other is a server's process πŸ–₯️. So the caller process sends a call message to the server process and then waits (blocks) for a reply message 😭. The call message holds the procedure's parameters, among other things πŸŽ€. The reply message holds the procedure's results, among other things 🎁. Once the reply shows up, the results get extracted and the caller's execution picks back up πŸ”₯.

on the server side, a process is dormant until a call message shows up 😴. When one arrives, the server extracts the procedure's parameters, computes the results, sends a reply message, and then waits for the next call message πŸ’–.

note that in this model, only one of the two processes is active at any given time πŸ‘€. But this model is just an example, okay? The RPC protocol doesn't place any restrictions on the concurrency model, and other options are possible 🌈. For example, an implementation could make RPC calls asynchronous, so the client can do useful work while it waits for the server's reply ⭐. Another option is for the server to spin up a task to handle an incoming request, so the server itself is free to receive other requests 🧚.

4. Transports and Semantics 🚚

the RPC protocol doesn't depend on transport protocols, meaning RPC doesn't care how a message gets from one process to another 🫢. It only deals with specifying and interpreting messages πŸ’….

it's really important to point out that RPC does not try to implement any kind of reliability 🚨. The application has to know what transport protocol is underneath RPC πŸ’œ. If it knows it's running on a reliable transport such as TCP/IP [6], then most of the work is already done for it πŸŽ€. But if it's running on an unreliable transport such as UDP/IP [7], it must implement its own retransmission and time-out policy, because the RPC layer doesn't provide this service 😭.

because RPC is transport independent, it doesn't attach specific semantics to the remote procedures or their execution πŸ¦‹. Semantics can be inferred from the underlying transport protocol, but they should also be explicitly specified πŸ”. Take RPC running on an unreliable transport like UDP/IP as an example. If an application retransmits RPC messages after short time-outs and gets no reply, the only thing it can infer is that the procedure was executed zero or more times πŸ™ˆ. If it does get a reply, it can infer that the procedure was executed at least once ✨.

a server may want to remember requests it already granted from a client, and not grant them again, to get some degree of execute-at-most-once semantics 🧠. It can do this by using the transaction ID packaged with every RPC request πŸ”₯. The main job of that transaction ID is letting the client RPC layer match replies to requests πŸ’–. A client application might choose to reuse its previous transaction ID when it retransmits a request πŸ”. The server application knows this, so it can remember the ID after granting a request and refuse to grant requests with the same ID, again for some degree of execute-at-most-once semantics πŸ’…. The server is not allowed to examine this ID in any other way except as a test for equality 🚫.

on the other hand, if you're using a reliable transport like TCP/IP, the application can infer from a reply message that the procedure was executed exactly once 🎯. But if it gets no reply message, it can't assume the remote procedure wasn't executed 😬. Even with a connection-oriented protocol like TCP, the application still needs time-outs and reconnection to handle server crashes πŸ’€.

there are other transport options besides datagram- or connection-oriented protocols 🌈. For example, a request-reply protocol such as VMTP [2] is perhaps the most natural transport for RPC πŸ¦‹.

Note: at Sun, RPC is currently implemented on top of both TCP/IP and UDP/IP transports πŸ“’.

5. Binding and Rendezvous Independence πŸŽ€

the act of binding a client to a service is NOT part of the remote procedure call specification πŸ™…β€β™€οΈ. That important and necessary job is left to some higher-level software. (That software may use RPC itself, see Appendix A) πŸ”„.

implementors should think of the RPC protocol as the jump-subroutine instruction ("JSR") of a network 🧠. The loader (binder) makes JSR useful, and the loader itself uses JSR to get its task done πŸ’œ. In the same way, the network makes RPC useful, and it uses RPC to get this task done too 🌸.

6. Authentication πŸ”

the RPC protocol provides the fields needed for a client to identify itself to a service, and vice-versa 🫢. Security and access control mechanisms can be built on top of this message authentication πŸ›‘οΈ. Several different authentication protocols can be supported 🌈. A field in the RPC header says which protocol is being used πŸŽ€. More info on specific authentication protocols is in section 9: "Authentication Protocols" πŸ“–.

7. RPC Protocol Requirements πŸ‘‘

the RPC protocol must provide for the following:

  • (1) Unique specification of a procedure to be called.
  • (2) Provisions for matching response messages to request messages.
  • (3) Provisions for authenticating the caller to service and vice-versa.

besides these requirements, features that detect the following are worth supporting, because of protocol roll-over errors, implementation bugs, user error, and network administration πŸ’…:

  • (1) RPC protocol mismatches.
  • (2) Remote program protocol version mismatches.
  • (3) Protocol errors (such as misspecification of a procedure's parameters).
  • (4) Reasons why remote authentication failed.
  • (5) Any other reasons why the desired procedure was not called.

7.1 RPC Programs and Procedures πŸ§šβ€β™€οΈ

the RPC call message has three unsigned fields: remote program number, remote program version number, and remote procedure number πŸ”’. Those three fields uniquely identify the procedure to be called 🎯. Program numbers are administered by some central authority (like Sun) 🏒. Once an implementor has a program number, they can implement their remote program, and the first implementation would most likely have version number 1 ✨. Since most new protocols evolve into better, stable, and mature protocols, the version field in the call message says which version of the protocol the caller is using πŸ¦‹. Version numbers make it possible to speak old and new protocols through the same server process 🫢.

the procedure number identifies the procedure to be called πŸ“. These numbers are documented in that specific program's protocol specification πŸ“š. For example, a file service's protocol specification might say that procedure number 5 is "read" and procedure number 12 is "write" πŸ“.

just like remote program protocols can change over several versions, the actual RPC message protocol could change too πŸ”„. So the call message also has the RPC version number in it, which is always equal to two for the version of RPC described here πŸ’–.

the reply message to a request message has enough info to tell apart these error conditions 🚨:

  • (1) The remote implementation of RPC does speak protocol version 2. The lowest and highest supported RPC version numbers are returned.
  • (2) The remote program is not available on the remote system.
  • (3) The remote program does not support the requested version number. The lowest and highest supported remote program version numbers are returned.
  • (4) The requested procedure number does not exist. (This is usually a caller side protocol or programming error.)
  • (5) The parameters to the remote procedure appear to be garbage from the server's point of view. (Again, this is usually caused by a disagreement about the protocol between client and service.)

7.2. Authentication πŸ”βœ¨

Okay so the RPC protocol has authentication built right in 🫢 The caller can prove who they are to the service, and the service can prove who it is back to the caller. It's giving mutual trust energy! A call message has two authentication fields, the credentials and the verifier. A reply message has just one, the response verifier. The RPC protocol spec defines all three of these fields as the same opaque type πŸ’…

enum auth_flavor {
   AUTH_NULL       = 0,
   AUTH_UNIX       = 1,
   AUTH_SHORT      = 2,
   AUTH_DES        = 3
   /* and more to be defined */
};

struct opaque_auth {
   auth_flavor flavor;
   opaque body<400>;
};

In simple English, any opaque_auth structure is an auth_flavor enumeration followed by bytes that the RPC protocol implementation treats as opaque πŸ¦‹ It doesn't peek inside. Nope, not its business πŸ™ˆ

The interpretation and semantics of the data in the authentication fields come from individual, independent authentication protocol specifications. (Section 9 defines the various authentication protocols.) So the RPC layer just carries the bag of bytes, and the auth spec tells you what the bytes actually mean 🧚

If authentication parameters get rejected, the response message contains information stating why they were rejected. So you're never left wondering what went wrong πŸ’œ

7.3. Program Number Assignment πŸ”’πŸŒˆ

Program numbers are handed out in groups of hexadecimal 20000000 (decimal 536870912), split up according to this chart:

             0 - 1fffffff   defined by Sun
      20000000 - 3fffffff   defined by user
      40000000 - 5fffffff   transient
      60000000 - 7fffffff   reserved
      80000000 - 9fffffff   reserved
      a0000000 - bfffffff   reserved
      c0000000 - dfffffff   reserved
      e0000000 - ffffffff   reserved

The first group is a range of numbers administered by Sun Microsystems, and it should be identical for all sites πŸ‘‘ The second range is for applications that belong to one particular site. It's mainly meant for debugging new programs πŸ“ When a site builds an application that might be of general interest, that application should be given an assigned number from the first range. The third group is for applications that generate program numbers dynamically ⭐ The final groups are reserved for future use, so please don't use them πŸ™…β€β™€οΈ

7.4. Other Uses of the RPC Protocol πŸ¦‹

The intended use of this protocol is calling remote procedures. Every call message gets matched with a response message, that's the main vibe. BUT the protocol itself is really just a message-passing protocol, so other (non-RPC) protocols can be built on top of it πŸ§šβ€β™€οΈ Sun currently uses, or perhaps abuses, the RPC message protocol for two of those non-RPC protocols: batching (or pipelining) and broadcast RPC. These two are discussed below but not defined there πŸ’€

7.4.1. Batching πŸ“¦

Batching lets a client send an arbitrarily large sequence of call messages to a server. Batching typically uses reliable byte stream protocols (like TCP/IP) for its transport 🌊 With batching, the client never waits for a reply from the server, and the server doesn't send replies to batch requests. Usually a sequence of batch calls ends with a legit RPC to flush the pipeline (with positive acknowledgement) ✨

7.4.2. Broadcast RPC πŸ“‘

With broadcast RPC-based protocols, the client sends a broadcast packet to the network and then waits for lots of replies πŸ™Œ Broadcast RPC uses unreliable, packet-based protocols (like UDP/IP) as its transports. Servers that support broadcast protocols only respond when the request is successfully processed, and they stay silent in the face of errors 🀫 Broadcast RPC uses the Port Mapper RPC service to make all of this work. (See Appendix A for more information.) πŸ—ΊοΈ

8. THE RPC MESSAGE PROTOCOL πŸ“œ

This section defines the RPC message protocol in the XDR data description language. The message is defined in a top-down style, so we start big and zoom in πŸ”

    enum msg_type {
       CALL  = 0,
       REPLY = 1
    };
    /*
     * A reply to a call message can take on two forms:
     * The message was either accepted or rejected.
     */
    enum reply_stat {
       MSG_ACCEPTED = 0,
       MSG_DENIED   = 1
    };

    /*
     * Given that a call message was accepted, the following is the
     * status of an attempt to call a remote procedure.
     */
    enum accept_stat {
       SUCCESS       = 0, /* RPC executed successfully       */
       PROG_UNAVAIL  = 1, /* remote hasn't exported program  */
       PROG_MISMATCH = 2, /* remote can't support version #  */
       PROC_UNAVAIL  = 3, /* program can't support procedure */
       GARBAGE_ARGS  = 4  /* procedure can't decode params   */
    };

    /*
     * Reasons why a call message was rejected:
     */
    enum reject_stat {
       RPC_MISMATCH = 0, /* RPC version number != 2          */
       AUTH_ERROR = 1    /* remote can't authenticate caller */
    };

    /*
     * Why authentication failed:
     */
    enum auth_stat {
       AUTH_BADCRED      = 1,  /* bad credentials (seal broken) */
       AUTH_REJECTEDCRED = 2,  /* client must begin new session */
       AUTH_BADVERF      = 3,  /* bad verifier (seal broken)    */
       AUTH_REJECTEDVERF = 4,  /* verifier expired or replayed  */
       AUTH_TOOWEAK      = 5   /* rejected for security reasons */
    };

    /*
     * The  RPC  message:
     * All messages start with a transaction identifier, xid,
     * followed by a two-armed discriminated union.  The union's
     * discriminant is a msg_type which switches to one of the two
     * types of the message.  The xid of a REPLY message always
     * matches that of the initiating CALL message.  NB: The xid
     * field is only used for clients  matching reply messages with
     * call messages or for servers detecting retransmissions; the
     * service side cannot treat this id as any type of sequence
     * number.
     */
    struct rpc_msg {
       unsigned int xid;
       union switch (msg_type mtype) {
       case CALL:
          call_body cbody;
       case REPLY:
          reply_body rbody;
       } body;
    };

    /*
     * Body of an RPC request call:
     * In version 2 of the RPC protocol specification, rpcvers must
     * be equal to 2.  The fields prog, vers, and proc specify the
     * remote program, its version number, and the procedure within
     * the remote program to be called.  After these fields are two
     * authentication parameters: cred (authentication credentials)
     * and verf (authentication verifier).  The two authentication
     * parameters are followed by the parameters to the remote
     * procedure, which are specified by the specific program
     * protocol.
     */
    struct call_body {
       unsigned int rpcvers;          /* must be equal to two (2) */
       unsigned int prog;
       unsigned int vers;
       unsigned int proc;
       opaque_auth cred;
       opaque_auth verf;
       /* procedure specific parameters start here */
    };

    /*
     * Body of a reply to an RPC request:
     * The call message was either accepted or rejected.
     */
    union reply_body switch (reply_stat stat) {
    case MSG_ACCEPTED:
       accepted_reply areply;
    case MSG_DENIED:
       rejected_reply rreply;
    } reply;

    /*

     * Reply to an RPC request that was accepted by the server:
     * there could be an error even though the request was accepted.
     * The first field is an authentication verifier that the server
     * generates in order to validate itself to the caller.  It is
     * followed by a union whose discriminant is an enum
     * accept_stat.  The SUCCESS arm of the union is protocol
     * specific.  The PROG_UNAVAIL, PROC_UNAVAIL, and GARBAGE_ARGS
     * arms of the union are void.  The PROG_MISMATCH arm specifies
     * the lowest and highest version numbers of the remote program
     * supported by the server.
     */
    struct accepted_reply {
       opaque_auth verf;
       union switch (accept_stat stat) {
       case SUCCESS:
          opaque results[0];
          /*
           * procedure-specific results start here
           */
        case PROG_MISMATCH:
           struct {
              unsigned int low;
              unsigned int high;
           } mismatch_info;
        default:
           /*
            * Void.  Cases include PROG_UNAVAIL, PROC_UNAVAIL,
            * and GARBAGE_ARGS.
            */
           void;
        } reply_data;
    };

    /*
     * Reply to an RPC request that was rejected by the server:
     * The request can be rejected for two reasons:  either the
     * server is not running a compatible version of the RPC
     * protocol (RPC_MISMATCH), or the server refuses to
     * authenticate the caller (AUTH_ERROR).  In case of an RPC
     * version mismatch, the server returns the lowest and highest
     * supported RPC version numbers.  In case of refused
     * authentication, failure status is returned.
     */
    union rejected_reply switch (reject_stat stat) {
    case RPC_MISMATCH:
       struct {
          unsigned int low;
          unsigned int high;

       } mismatch_info;
    case AUTH_ERROR:
       auth_stat stat;
    };

9. AUTHENTICATION PROTOCOLS πŸ›‘οΈ

As previously stated, authentication parameters are opaque, but they're open-ended to the rest of the RPC protocol 🌸 This section defines some "flavors" of authentication implemented at (and supported by) Sun. Other sites are free to invent new authentication types, using the same rules of flavor number assignment as program number assignment πŸ’–

9.1. Null Authentication πŸ«₯

Sometimes calls have to be made where the caller doesn't know who they are, or the server doesn't care who the caller is. In this case, the flavor value (the discriminant of the opaque_auth's union) of the RPC message's credentials, verifier, and response verifier is "AUTH_NULL". The bytes of the opaque_auth's body are undefined. It is recommended that the opaque length be zero πŸƒ

9.2 UNIX Authentication

okay bestie, this is where a caller can say "hi, it's me, here's who I am on my UNIX(tm) machine" πŸ–₯️✨ The credential's discriminant is AUTH_UNIX, and the opaque body of the credential encodes this structure:

     struct auth_unix {
        unsigned int stamp;
        string machinename<255>;
        unsigned int uid;
        unsigned int gid;
        unsigned int gids<10>;
     };

Here's the breakdown 🧚: stamp is an arbitrary ID that the caller's machine can generate, and machinename is the name of the caller's machine (like "krypton" 🌈). uid is the caller's effective user ID, and gid is the caller's effective group ID. gids is a counted array of groups that the caller is a member of. The verifier that goes along with these credentials should be AUTH_NULL, which we covered above πŸ’œ.

The response verifier in the server's reply can be either AUTH_NULL or AUTH_SHORT πŸ¦‹. Now AUTH_SHORT is the lowkey iconic part. The bytes of the response verifier's string encode an opaque structure, and that new structure can be sent back to the server instead of the original AUTH_UNIX credentials. The server keeps a cache that maps those shorthand structures back to the caller's original credentials πŸ—‚οΈ. So the caller saves network bandwidth and server CPU cycles by using the new, shorter credentials. Efficiency queen behavior πŸ‘‘πŸ’…

The server may flush the shorthand opaque structure at any time. If this happens, the remote procedure call message will be rejected due to an authentication error. The reason for the failure will be AUTH_REJECTEDCRED. At this point, the caller may wish to try the original AUTH_UNIX style of credentials. πŸ’€ That's the plot twist, so be ready to fall back to the original AUTH_UNIX credentials if you get AUTH_REJECTEDCRED.

9.3 DES Authentication

ngl, UNIX authentication has two big problems πŸ™ˆ:

      (1) The naming is too UNIX oriented.
      (2) There is no verifier, so credentials can easily be faked.

DES authentication is the glow-up that tries to fix both of these πŸ’– Let's go through them one at a time.

9.3.1 Naming

The first problem gets fixed by addressing the caller with a simple string of characters, instead of an operating-system-specific integer πŸŽ€. That string is called the "netname" (or network name) of the caller. The server isn't allowed to interpret the contents of the caller's name in any way other than to identify the caller. And so, netnames should be unique for every caller on the Internet 🌍.

Each operating system's DES authentication implementation is responsible for generating netnames for its users in a way that keeps them unique when they call remote servers. Operating systems already know how to tell apart users who are local to their system, so it's usually a simple thing to extend that mechanism across the network 🧠. For example, a UNIX user at Sun with user ID 515 might get this netname: "unix.515@sun.com". This netname has three parts that keep it unique. Going backwards: there's only one naming domain called "sun.com" on the Internet, and inside that domain there's only one UNIX user with user ID 515. But here's the twist 😳: another user on a different operating system, say VMS, in that same naming domain could coincidentally have the same user ID. So to keep those two users distinguishable, we add the operating system name. That means one user is "unix.515@sun.com" and the other is "vms.515@sun.com" πŸ’…

The first field is technically a naming method rather than an operating system name πŸ€“. It just so happens that right now there's almost a one-to-one match between naming methods and operating systems. If the world ever agreed on one naming standard, the first field could be the name of that standard instead of an operating system name ✨.

9.3.2 DES Authentication Verifiers

Unlike UNIX authentication, DES authentication does have a verifier 🫢, so the server can validate the client's credential (and vice-versa). The verifier's contents are mainly an encrypted timestamp πŸ”. The server decrypts that timestamp, and if it's close to the real time, then the client must have encrypted it correctly. The only way the client could have encrypted it correctly is by knowing the "conversation key" of the RPC session. And if the client knows the conversation key, then it has to be the real client πŸ‘‘.

The conversation key is a DES [5] key that the client generates and tells the server about in its very first RPC call πŸ—οΈ. The conversation key is encrypted with a public key scheme during that first transaction. The public key scheme DES authentication uses is Diffie-Hellman [3], with 128-bit keys. We'll get into the details of this encryption method later πŸ”œ.

The client and the server need the same idea of the current time for all of this to work ⏰. If network time synchronization can't be guaranteed, then the client can sync up with the server before starting the conversation, maybe by checking the Internet Time Server (TIME [4]) πŸ•°οΈ.

Now how the server decides whether a client timestamp is valid is kinda complicated 😭. For any transaction except the first one, the server just checks two things:

      (1) the timestamp is greater than the one previously seen from
          the same client.

      (2) the timestamp has not expired.

A timestamp is expired if the server's time is later than the client's timestamp plus something called the client's "window" πŸͺŸ. The "window" is a number the client passes (encrypted) to the server in its first transaction. Think of it as a lifetime for the credential ⏳.

That covers everything except the first transaction. In the first transaction, the server only checks that the timestamp hasn't expired. If that were the only check, though, the client could easily send random data in place of the timestamp and have a pretty good shot at getting through 🎲. So there's an extra check: the client sends an encrypted item in the first transaction called the "window verifier", which must equal the window minus 1, or the server will reject the credential 🚫.

The client has to check the verifier the server sends back, too, to make sure it's legit πŸ•΅οΈβ€β™€οΈ. The server sends back the encrypted timestamp it got from the client, minus one second. If the client gets anything different from that, it rejects it.

9.3.3 Nicknames and Clock Synchronization

After the first transaction, the server's DES authentication subsystem sends back an integer "nickname" in its verifier 🏷️. The client can use that nickname in later transactions instead of sending its netname, encrypted DES key, and window every single time. The nickname is most likely an index into a table on the server that stores each client's netname, decrypted DES key, and window πŸ“’.

Even though the client's and server's clocks were synced at first, they can drift out of sync again πŸ•. When that happens, the client's RPC subsystem will most likely get back RPC_AUTHERROR, and at that point it should resynchronize.

Here's a plot twist πŸ™ƒ: a client can still get RPC_AUTHERROR even when it's synced with the server. That's because the server's nickname table has a limited size, and it can flush entries whenever it wants. In that case, the client should resend its original credential, and the server will give it a new nickname πŸ”. If a server crashes, the entire nickname table gets flushed, and every client will have to resend its original credentials πŸ’”.

9.3.4 DES Authentication Protocol Specification (in XDR language)

Time for the spec itself πŸ“œπŸŽ€. Here it is in XDR language:

    /*
     * There are two kinds of credentials: one in which the client uses
     * its full network name, and one in which it uses its "nickname"
     * (just an unsigned integer) given to it by the server.  The
     * client must use its fullname in its first transaction with the
     * server, in which the server will return to the client its
     * nickname.  The client may use its nickname in all further
     * transactions with the server.  There is no requirement to use the
     * nickname, but it is wise to use it for performance reasons.
     */
    enum authdes_namekind {

       ADN_FULLNAME = 0,
       ADN_NICKNAME = 1
    };

    /*
     * A 64-bit block of encrypted DES data
     */
    typedef opaque des_block[8];

    /*
     * Maximum length of a network user's name
     */
    const MAXNETNAMELEN = 255;

    /*
     * A fullname contains the network name of the client, an encrypted
     * conversation key, and the window.  The window is actually a
     * lifetime for the credential.  If the time indicated in the
     * verifier timestamp plus the window has past, then the server
     * should expire the request and not grant it.  To insure that
     * requests are not replayed, the server should insist that
     * timestamps are greater than the previous one seen, unless it is
     * the  first transaction.  In the first transaction, the server
     * checks instead that the window verifier is one less than the
     * window.
     */
    struct authdes_fullname {
       string name<MAXNETNAMELEN>;  /* name of client                */
       des_block key;               /* PK encrypted conversation key */
       unsigned int window;         /* encrypted window              */
    };

    /*
     * A credential is either a fullname or a nickname
     */
    union authdes_cred switch (authdes_namekind adc_namekind) {
    case ADN_FULLNAME:
       authdes_fullname adc_fullname;
    case ADN_NICKNAME:
       unsigned int adc_nickname;
    };

    /*
     * A timestamp encodes the time since midnight, January 1, 1970.
     */
    struct timestamp {
         unsigned int seconds;    /* seconds          */
         unsigned int useconds;   /* and microseconds */

    };

    /*
     * Verifier: client variety
     * The window verifier is only used in the first transaction.  In
     * conjunction with a fullname credential, these items are packed
     * into the following structure before being encrypted:
     *
     * struct {
     *     adv_timestamp;        -- one DES block
     *     adc_fullname.window;  -- one half DES block
     *     adv_winverf;          -- one half DES block
     * }
     * This structure is encrypted using CBC mode encryption with an
     * input vector of zero.  All other encryptions of timestamps use
     * ECB mode encryption.
     */
    struct authdes_verf_clnt {
       timestamp adv_timestamp;    /* encrypted timestamp       */
       unsigned int adv_winverf;   /* encrypted window verifier */
    };

    /*
     * Verifier: server variety
     * The server returns (encrypted) the same timestamp the client
     * gave it minus one second.  It also tells the client its nickname
     * to be used in future transactions (unencrypted).
     */
    struct authdes_verf_svr {
       timestamp adv_timeverf;     /* encrypted verifier      */
       unsigned int adv_nickname;  /* new nickname for client */
    };

That's the whole DES spec for this chunk! 🌸 Next up, the rest of the protocol. πŸ’œ

9.3.5 Diffie-Hellman Encryption

Okay bestie, this scheme is so cute because two people can agree on a shared secret without ever saying it out loud πŸ«ΆπŸ”. It uses two constants, PROOT and MODULUS. For the DES authentication protocol, Sun picked these values:

const PROOT = 2;
const MODULUS = "b520985fb31fcaf75036701e37d8b857"; /* in hex */

Here's how it works, with an example. Say there are two people, A and B, who want to send each other encrypted messages πŸ’Œ. Each of them makes up a random "secret" key and keeps it to themselves. These are called SK(A) and SK(B). They also post a "public" key in a public directory, and those are PK(A) and PK(B). The public keys get computed like this:

PK(A) = ( PROOT ** SK(A) ) mod MODULUS
PK(B) = ( PROOT ** SK(B) ) mod MODULUS

The ** is just exponentiation πŸ€“. Now the magic part: A and B can both land on the same "common" key, called CK(A, B), without ever revealing their secret keys. πŸͺ„

A computes:

CK(A, B) = ( PK(B) ** SK(A)) mod MODULUS

while B computes:

CK(A, B) = ( PK(A) ** SK(B)) mod MODULUS

These two come out equal:

(PK(B) ** SK(A)) mod MODULUS = (PK(A) ** SK(B)) mod MODULUS

To make the math easier to follow, we drop the "mod MODULUS" parts and just assume modulo arithmetic is happening:

PK(B) ** SK(A) = PK(A) ** SK(B)

Then we swap PK(B) for what B computed earlier, and PK(A) for what A computed:

((PROOT ** SK(B)) ** SK(A) = (PROOT ** SK(A)) ** SK(B)

which leads to:

PROOT ** (SK(A) * SK(B)) = PROOT ** (SK(A) * SK(B))

Both sides match, and that's the whole trick ✨.

Now, the common key isn't used to encrypt the timestamps. Nope! It's only used to encrypt a conversation key, and that conversation key is what encrypts the timestamps πŸ”‘. Why the extra step? The common key gets used as little as possible, because it might one day get broken. If a conversation key gets broken instead, that's a much smaller oof, since conversations are pretty short-lived πŸ™ˆ.

There's one more wrinkle. The conversation key is encrypted using 56-bit DES keys, but the common key is 128 bits. So we have to shrink it down. We take the middle-most 8-bytes from the common key, then add parity to the lower order bit of each byte. That gives us a 56-bit key with 8 bits of parity 🧩. Iconic engineering, honestly.


10. RECORD MARKING STANDARD

10. RECORD MARKING STANDARD 🧾

When RPC messages ride on top of a byte stream protocol like TCP/IP, you really want a way to tell one message from the next 🫠. Otherwise you can't spot user protocol errors or recover from them. That's called record marking (RM). Sun uses this RM/TCP/IP transport for sending RPC messages over TCP streams, and one RPC message fits into one RM record. Simple and clean πŸ’….

A record is made up of one or more record fragments. Each record fragment is a four-byte header followed by 0 to (2**31)-1 bytes of fragment data πŸ“¦. The header bytes encode an unsigned binary number, and just like XDR integers, the byte order goes from highest to lowest. That one number carries two values:

  • A boolean that says whether this fragment is the last fragment of the record. A bit value of 1 means yes, this is the last fragment.
  • A 31-bit unsigned binary value, which is the length in bytes of the fragment's data.

The boolean lives in the highest-order bit of the header, and the length takes up the 31 low-order bits πŸ”’.

Heads up: this record specification is not in XDR standard form! It's its own thing, so don't go assuming the usual XDR rules apply here πŸ™ƒ.

11. THE RPC LANGUAGE

11. THE RPC LANGUAGE πŸŽ€

Just like XDR data types needed a formal language, the procedures that work on those XDR data types need one too. Enter the RPC Language! πŸŽ‰ It's an extension of the XDR language, and the easiest way to get the vibe is to look at an example.


11.1 An Example Service Described in the RPC Language

Here's a simple ping program, described in the RPC Language πŸ“:

/*
 * Simple ping program
 */
program PING_PROG {
   /*
    * Latest and greatest version
    */
   version PING_VERS_PINGBACK {
      void
      PINGPROC_NULL(void) = 0;

      /*
       * Ping the caller, return the round-trip time
       * (in microseconds).  Returns -1 if the operation
       * timed out.

       */
      int
      PINGPROC_PINGBACK(void) = 1;
   } = 2;

   /*
    * Original version
    */
   version PING_VERS_ORIG {
      void
      PINGPROC_NULL(void) = 0;
   } = 1;
} = 1;

const PING_VERS = 2;      /* latest version */

The first version listed is PING_VERS_PINGBACK, and it has two procedures: PINGPROC_NULL and PINGPROC_PINGBACK. PINGPROC_NULL takes no arguments and returns nothing, but it's super handy for measuring round-trip times from the client to the server and back again ⏱️. By convention, procedure 0 of any RPC protocol should have the same semantics and should never need any kind of authentication πŸ”“. The second procedure lets the client ask the server to do a reverse ping back to the client. It returns how many microseconds that operation took 🌸.

The next version, PING_VERS_ORIG, is the original version of the protocol, and it doesn't include PINGPROC_PINGBACK. It's there for compatibility with older client programs. As the program matures, it may get dropped from the protocol entirely πŸ‚.


11.1 The RPC Language Specification

The RPC Language is identical to the XDR language, with one addition: a program-def, which is defined below πŸ“.

program-def:
   "program" identifier "{"
       version-def
       version-def *
   "}" "=" constant ";"

version-def:
   "version" identifier "{"
       procedure-def
       procedure-def *
   "}" "=" constant ";"

procedure-def:

   type-specifier identifier "(" type-specifier ")"
   "=" constant ";"

11.2 Syntax Notes

Here are the rules you need to know when you're writing this stuff 🧐:

(1) The following keywords get added, and you can't use them as identifiers: "program" and "version" 🚫.

(2) A version name can't show up more than once within the scope of a program definition. The same goes for version numbers: a version number can't appear more than once within the scope of a program definition either.

(3) A procedure name can't show up more than once within the scope of a version definition. Same deal for procedure numbers, which also can't repeat within the scope of a version definition.

(4) Program identifiers share a name space with constant and type identifiers. So they all have to play nice together 🀝.

(5) Only unsigned constants can be assigned to programs, versions, and procedures.


APPENDIX A: PORT MAPPER PROGRAM PROTOCOL

APPENDIX A: PORT MAPPER PROGRAM PROTOCOL πŸ—ΊοΈ

The port mapper program maps RPC program and version numbers to transport-specific port numbers. This is what makes dynamic binding of remote programs possible πŸ¦‹.

That's super important, because the range of reserved port numbers is very small, but the number of potential remote programs is huge. So instead, you only run the port mapper on a reserved port. Then you can find out the port numbers of all the other remote programs just by asking the port mapper πŸ™Œ.

The port mapper also helps out with broadcast RPC. A given RPC program will usually have different port number bindings on different machines, so you can't just broadcast straight to all of those programs. The port mapper, though, has a fixed port number. So to broadcast to a given program, the client sends its message to the port mapper at the broadcast address. Each port mapper that picks up the broadcast then calls the local service the client asked for. When the port mapper gets the reply from that local service, it sends the reply back to the client πŸ“£.


A.1 Port Mapper Protocol Specification (in RPC Language)

Here's the full port mapper spec, written in the RPC Language 🧾:

const PMAP_PORT = 111;      /* portmapper port number */
/*
 * A mapping of (program, version, protocol) to port number
 */
struct mapping {
   unsigned int prog;
   unsigned int vers;
   unsigned int prot;
   unsigned int port;
};

/*
 * Supported values for the "prot" field
 */
const IPPROTO_TCP = 6;      /* protocol number for TCP/IP */
const IPPROTO_UDP = 17;     /* protocol number for UDP/IP */

/*
 * A list of mappings
 */
struct *pmaplist {
   mapping map;
   pmaplist next;
};
/*
 * Arguments to callit
 */
struct call_args {
   unsigned int prog;
   unsigned int vers;
   unsigned int proc;
   opaque args<>;
};
/*
 * Results of callit
 */
struct call_result {
   unsigned int port;
   opaque res<>;
};

/*
 * Port mapper procedures
 */
program PMAP_PROG {
   version PMAP_VERS {
      void
      PMAPPROC_NULL(void)         = 0;
      bool
      PMAPPROC_SET(mapping)       = 1;

      bool
      PMAPPROC_UNSET(mapping)     = 2;

      unsigned int
      PMAPPROC_GETPORT(mapping)   = 3;

      pmaplist
      PMAPPROC_DUMP(void)         = 4;

      call_result
      PMAPPROC_CALLIT(call_args)  = 5;
   } = 2;
} = 100000;

A.2. Port Mapper Operation

okay so the portmapper is basically the bouncer for two protocols, UDP/IP and TCP/IP πŸ’… you reach it by knocking on assigned port number 111 (SUNRPC [8]) on either of them πŸšͺ✨ here's the full lineup of its procedures, bestie 🫢

PMAPPROC_NULL:

This one does literally nothing πŸ’€ by convention, procedure zero of any protocol takes no parameters and returns no results, so it's giving "just checking if you're awake" energy πŸ™ˆ

PMAPPROC_SET:

okay this is the registration moment πŸŽ€ when a program first shows up on a machine, it signs itself up with the port mapper on that same machine. it hands over its program number "prog", version number "vers", transport protocol number "prot", and the port "port" it's waiting on for service requests πŸ¦‹ the procedure gives back a boolean, TRUE if the mapping got set up and FALSE otherwise. and here's the drama: it will NOT establish a mapping if one already exists for the tuple "(prog, vers, prot)" πŸ’€ no double-booking allowed, periodt πŸ‘‘

PMAPPROC_UNSET:

when a program goes offline, it should unregister itself from the port mapper on the same machine πŸŒ™ the parameters and results mean exactly the same thing as they do for "PMAPPROC_SET" πŸ” and the protocol and port number fields in the argument get ignored, so you can just skip them πŸ’œ

PMAPPROC_GETPORT:

this is the lookup queen πŸ‘‘ give it a program number "prog", version number "vers", and transport protocol number "prot", and it tells you the port that program is listening on for call requests 🧚 a port value of zeros means the program never registered, so it's not there, not me being dramatic πŸ’… the "port" field of the argument is ignored here too 🌸

PMAPPROC_DUMP:

the tea spiller 🍡 this procedure lists every single entry in the port mapper's database. it takes no parameters and returns a list of program, version, protocol, and port values πŸ“œ full gossip, no cap ✨

PMAPPROC_CALLIT:

okay this one is lowkey iconic πŸ”₯ it lets a caller reach another remote procedure on the same machine WITHOUT needing to know that procedure's port number πŸ¦‹ it's meant for supporting broadcasts to arbitrary remote programs through the well-known port mapper's port πŸ“‘ the parameters "prog", "vers", "proc", and the bytes of "args" are the program number, version number, procedure number, and parameters of the remote procedure πŸ’–

a few things to keep in mind, bestie 🫢

  1. this procedure only sends a response if the remote procedure was successfully executed, and it's totally silent (no response) otherwise 🀫
  2. the port mapper talks to the remote program using UDP/IP only 🌐

and then the payoff: the procedure returns the remote program's port number, and the bytes of results are the results of the remote procedure 🎁 we ate that, iykyk πŸ’…


REFERENCES πŸ“š

  • [1] Birrel, A. D., and Nelson, B. J., "Implementing Remote Procedure Calls", XEROX CSL-83-7, October 1983. 🧠
  • [2] Cheriton, D., "VMTP: Versatile Message Transaction Protocol", Version 0.7, RFC-1045, Stanford University, February 1988. πŸ¦‹
  • [3] Diffie & Hellman, "Net Directions in Cryptography", IEEE Transactions on Information Theory IT-22, November 1976. πŸ”
  • [4] Postel, J., and Harrenstien, K., "Time Protocol", RFC-868, Network Information Center, SRI, May 1983. ⏰
  • [5] National Bureau of Standards, "Data Encryption Standard", Federal Information Processing Standards Publication 46, January 1977. πŸ—οΈ
  • [6] Postel, J., "Transmission Control Protocol - DARPA Internet Program Protocol Specification", RFC-793; Network Information Center, SRI, September 1981. 🌊
  • [7] Postel, J., "User Datagram Protocol", RFC-768, Network Information Center, SRI, August 1980. πŸ“¦
  • [8] Reynolds, J. and Postel, J.; "Assigned Numbers", RFC-1010, Network Information Center, SRI, May 1987. πŸ”’
  • [9] Sun Microsystems; "XDR: External Data Representation Standard", RFC-1014; Sun Microsystems, June 1987. 🌸
πŸ“¦ where this page comes from