Example userspace AF_RXRPC client usage

AF_RXRPC is implemented as a network protocol and, as such, is accessible from userspace through the Linux socket interface. A socket is opened and then multiple calls may be made over it using the ancillary data provided to sendmsg() and recvmsg() to determine the call.


First of all, an AF_RXRPC socket must be opened:

            client = socket(AF_RXRPC, SOCK_DGRAM, PF_INET);

The second argument is the transport socket type (which must be DGRAM for the moment) and the third argument specified the transport protocol (PF_INET or PF_INET6). This gets you a file descriptor that can be used to perform RPC message passing. You can then set the security on it:

            int sec = RXRPC_SECURITY_AUTH;
            ret = setsockopt(client, SOL_RXRPC, RXRPC_MIN_SECURITY_LEVEL, &sec, sizeof(sec));
            const char principal[] = "afs@GRAND.CENTRAL.ORG";
            ret = setsockopt(client, SOL_RXRPC, RXRPC_SECURITY_KEY, principal, strlen(principal));

The first statement sets the minimum security level required to one of checksum only (PLAIN), authenticated packet only (AUTH) or full packet encryption (ENCRYPT). The second statement sets the name of the key to be used.

An address can then be bound to the local endpoint. This is optional for a client, but if a specific address and/or port is required for the local UDP transport, then this must be used.

            memset(&my_addr, 0, sizeof(my_addr));
            my_addr.my_addr_family              = AF_RXRPC;
            my_addr.my_addr_service             = 0;
            my_addr.transport_type              = SOCK_DGRAM;
            my_addr.transport_len               = sizeof(my_addr.transport.sin);
            my_addr.transport.sin.sin_family    = AF_INET;
            my_addr.transport.sin.sin_port      = htons(7001);
            memcpy(&my_addr.transport.sin.sin_addr, &local_addr, 4);

            ret = bind(client, (struct sockaddr *)&my_addr, sizeof(my_addr));

For a service, .srx_service is where the service ID on which the caller wishes to listen is specified; this should be 0 for a purely client socket.

It should be noted that client sockets opened separately may share a UDP port inside the kernel with each other, but sharing a UDP socket between a service and a client or between a pair of services is forbidden.

We then need to set up the address of the server to which we're going to try to talk:

            peer_addr.peer_addr_family          = AF_RXRPC;
            peer_addr.peer_addr_service         = 52;
            peer_addr.transport_type            = SOCK_DGRAM;
            peer_addr.transport_len             = sizeof(peer_addr.transport.sin);
            peer_addr.transport.sin.sin_family  = AF_INET;
            peer_addr.transport.sin.sin_port    = htons(7003);
            memcpy(&peer_addr.transport.sin.sin_addr, &remote_addr, 4);

The .srx_service member holds the service ID that we wish to contact on the far port. .transport holds the transport socket parameters. The .transport_type and .transport_family must match the values given to socket as type and protocol. .transport.sin carries the target UDP address.

Call Initiation and Transmission Phase

Next we need to load up the ancillary data into the control buffer for sendmsg() to use in call multiplexing:

            ctrllen = 0;
            RXRPC_ADD_CALLID(control, ctrllen, 0x12345);

This includes userspace's identifier for the call we're about to launch (in this case 0x12345). It must be included in all subsequent sendmsg() calls and it will be included in the return from all subsequent recvmsg() calls. It is valid until recvmsg() returns MSG_EOR in conjuction with this call ID. The call identifier is an integer of size unsigned long and may be used to hold a pointer if that is convenient.

We then marshal the request data into a buffer:

            static char vlname[] = "root.cell";
            vllen = sizeof(vlname) - 1;
            param[0] = htonl(VLGETENTRYBYNAME);
            param[1] = htonl(vllen);
            padding = 0;

And then we call sendmsg() to send the data, inserting suitable padding to round strings up to four-byte alignment:

            iov[0].iov_len  = sizeof(param);
            iov[0].iov_base = param;
            iov[1].iov_len  = vllen;
            iov[1].iov_base = vlname;
            iov[2].iov_len  = 4 - (vllen & 3);
            iov[2].iov_base = &padding;
            ioc = (iov[2].iov_len > 0) ? 3 : 2;

            msg.msg_name            = (struct sockaddr *)&peer_addr;
            msg.msg_namelen         = sizeof(peer_addr);
            msg.msg_iov             = iov;
            msg.msg_iovlen          = ioc;
            msg.msg_control         = control;
            msg.msg_controllen      = ctrllen;
            msg.msg_flags           = 0;

            ret = sendmsg(client, &msg, 0);

Note that we pass MSG_MORE if we want to send more data. AF_RXRPC will collect the data into appropriately sized packets so that there is no wastage of sequence number space (there is a limit to the number of DATA packets that can be sent in each direction).

Reception Phase

We can listen for a response from the server:

            iov[0].iov_base = buffer;
            iov[0].iov_len = sizeof(buffer);

            msg.msg_name            = &rx_addr;
            msg.msg_namelen         = sizeof(rx_addr);
            msg.msg_iov             = iov;
            msg.msg_iovlen          = 1;
            msg.msg_control         = control;
            msg.msg_controllen      = sizeof(control);
            msg.msg_flags           = 0;

            ret = recvmsg(client, &msg, 0);

This will return 0 or the number of bytes read into the buffer if it successfully queried a call - even if that call failed - and -ENODATA if no calls were outstanding on a client socket. If a call did fail, the control message buffer will hold ancillary data indicating the reason. The ancillary data will also hold the call identifier. It should be noted that if several calls are in progress simultaneously upon a socket, consecutive calls to recvmsg() may return information from different calls. It is possible, however, to specify MSG_PEEK to find out what the next call will be without altering the call state.

If there is more data to be read because the call has not yet reached the end of its reception phase, then MSG_MORE will be returned.

If any terminal indication is given, then the recvmsg will have indicated MSG_EOR to indicate that the call identifier is now released and can be reused.

The contents of the control buffer then need to be parsed:

            struct cmsghdr *cmsg;
            unsigned long call_identifier;
            int abort_code, error;
            enum {
            } call_state = call_ongoing;

            for (cmsg = CMSG_FIRSTHDR(msg); cmsg; cmsg = CMSG_NXTHDR(msg, cmsg)) {
                    void *p = CMSG_DATA(cmsg);
                    int n = cmsg->cmsg_len - CMSG_ALIGN(sizeof(*cmsg));

                    if (cmsg->cmsg_level == SOL_RXRPC) {
                            switch (cmsg->cmsg_type) {
                            case RXRPC_USER_CALL_ID:
                                    memcpy(&call_identifier, p, sizeof(user_id));
                            case RXRPC_ABORT:
                                    memcpy(&abort_code, p, sizeof(abort_code));
                                    call_state = call_remotely_aborted;
                            case RXRPC_ACK:
                                    call_state = service_call_finally_acked;
                            case RXRPC_NET_ERROR:
                                    memcpy(&error, p, sizeof(error));
                                    errno = error;
                                    call_state = call_network_error;
                            case RXRPC_BUSY:
                                    call_state = call_busy;
                            case RXRPC_LOCAL_ERROR:
                                    memcpy(&error, p, sizeof(error));
                                    errno = error;
                                    call_state = call_error;

The above snippet parses the control buffer, extracting the call identifier along with the code of any abort received and any local or network error incurred. It also extracts the indication that the call was terminated because the server kept returning a busy packet and the indication that a service call received the final ACK from the remote client.

Aborting a Call

An active call may be aborted as long as recvmsg() hasn't yet indicated that the call has been terminated by flagging MSG_EOR. This is done by specifying the call identifier and the abort code in the control message:

            ctrllen = 0;
            RXRPC_ADD_CALLID(control, ctrllen, 0x12345);
            RXRPC_ADD_ABORT(control, ctrllen, 0x6789);

            msg.msg_name            = NULL;
            msg.msg_namelen         = 0;
            msg.msg_iov             = NULL;
            msg.msg_iovlen          = 0;
            msg.msg_control         = control;
            msg.msg_controllen      = ctrllen;
            msg.msg_flags           = 0;

            ret = sendmsg(client, &msg, 0);

Note that this still has to be followed with a call to recvmsg() to find out how the call actually ended (it's possible the call was aborted first from the other side, for example).