Userspace protocol implementation

This page describes the steps to follow in implementing basic message passing for an userspace protocol. As an actual example we'll use RSTP.

Kernel modifications

First of all, we have to define a dummy protocol type used in our custom protocol implementation. This has no meaning whatsoever with respect to the protocol fields of actual ethernet frames, it is only used in LiSA's internal logic. We include the type in net_switch.h:

#define ETH_P_RSTP 0x0022

Next, we have to create a list head for our switch socket. That requires adding a 'struct list_head' field to the net_switch_port structure in sw_private.h.

struct net_switch_port {
 
    //...
 
    struct list_head sock_rstp;
}

Now, using the type from net_switch.h and the list_head previously defined, we add an entry in the bind_switch_port routine found in sw_socket.c.

static int bind_switch_port (struct switch_sock *sws, struct net_switch_port *port, int proto) {
 
    struct list_head *lh;
 
    switch(proto) {
        //...
        case ETH_P_RSTP:
            lh = &port->sock_rstp;
            break;
    }
}

Define a new type in sw_socket.c so we can tell which messages belong to our protocol:

#define ETH_HDLC_RSTP 0x2008

Modify the sw_socket_filter function from sw_socket.c so it identifies and properly enqueues our protocol messages:

int sw_socket_filter(struct sk_buff *skb, struct net_switch_port *port) {
    int handled = 0;
    struct switch_sock *sw_sk;
 
    //...
 
    switch(ntohs(*(short *)(skb->data + 6))) {
        //...
        case ETH_HDLC_RSTP:
            list_for_each_entry_rcu(sw_sk, &port->sock_rstp, port_chain) {
                atomic_inc(&skb->users);
                handled |= sw_socket_enqueue(skb, port->dev, sw_sk);
            }
            break;
    }
 
    //...
}

Finally, initialize the list_head previously defined, in sw_ioctl.c :

static int sw_addif(struct net_device *dev) {
    //...
    INIT_LIST_HEAD(&port->sock_rstp);
    //...
}

These are all the modifications required in the kernel so that it identifies our protocol. Some protocolos (including RSTP) require more changes to the kernel because they need more information about the switch itself (e.g.: port states in RSTP). If the protocol you're implementing only does simple message passing then you're good to go, otherwise good luck :)

Userspace testing

Now it's time to see if everything we did before actually works. For that, you need two (interconnected) machines with the modified kernel installed since we're gonna send a message from one to the other.

Sender

Basically, we have to create our own frame and send it using a special type of socket. Before that, let's define a structure representing an Ethernet SNAP frame header, since that is what we're going to be using. (NOTE: most likely, this will be defined somewhere in a common userspace header probably under another name, so in a real implementation make sure you use that ; for now, for the purposes of this tutorial we'll create our own definition). Here is the structure of the SNAP frame header:

struct ethhdr {
    unsigned char dst_addr[ETH_ALEN];
    unsigned char src_addr[ETH_ALEN];
    unsigned short length;
    unsigned char dsap;
    unsigned char ssap;
    unsigned char control;
    unsigned char oui[3];
    unsigned short protocol_id;
} __attribute__ ((packed));

For a description of all the fields go to http://www.firewall.cx/ethernet-frames-802.3-snap.php . We'll attach this header to every frame we send. In this example, we have a 255 byte buffer we first fill with values from 0 to 255, after which we modify the first bytes according to the header.

int main(int argc, char **argv) {
    char buf[255];
    struct ethhdr *header;
    int i;
 
    for (i = 0; i < 255; i++)
        buf[i] = i;
 
    header = (struct ethhdr *)buf;
 
    /* suppose we have the destination and source MAC addresses stored
       in buffers 'destination' and 'source' */
    memcpy(header->dst_addr, destination, ETH_ALEN);
    memcpy(header->src_add, source, ETH_ALEN);
 
    // DSAP and SSAP fields are both 0xaa;
    header->dsap = header->ssap = 0xaa;
 
    // LLC frame type
    header->control = 0x03;
 
    // Organizational Unique Identifier (Cisco)
    header->oui[0] = 0x00;
    header->oui[1] = 0x00;
    header->oui[2] = 0x0c;
 
    // Protocol type
    header->protocol_id = htons(ETH_HDLC_RSTP);
 
    //...
}

The remaining bytes should be filled with whatever your protocol specifies. In this example, for simplicity, we'll leave the buffer as it is. Now that we have the frame ready, we should start creating a socket and then send the frame.

int main(int argc, char **argv) {
    int fd;
    struct sockaddr_sw addr;
 
    fd = socket(PF_SWITCH, SOCK_RAW, 0);
 
    memset(&addr, 0, sizeof(addr));
    addr.ssw_family = AF_SWITCH;
    strncpy(addr.ssw_if_name, "eth0", sizeof(addr.ssw_if_name) - 1);
    addr.ssw_proto = ETH_P_RSTP;
    bind(fd, (struct sockaddr *)&addr, sizeof(addr));
 
    //...
}

That concluded the socket setup so all we have to do is send the frame:

send(fd, buf, sizeof(buf), 0);
Receiver

The receiver has the exact same code as the sender, except it does a recv call instead of send, after setting up the socket. In the actual source code you'll find a few lines of code that output the received message, so you can see that everything works. Compile the two programs, run the sender on one machine and the receiver on the other, and you should see the output (on the receiver part). Make sure that the destination address in the frame matches the address of the receiver or else the frame won't reach the upper layers so you won't see any output.