net: Add a protocol 'bypass' command for convs
authorBarret Rhoden <brho@cs.berkeley.edu>
Fri, 16 Dec 2016 15:05:56 +0000 (10:05 -0500)
committerBarret Rhoden <brho@cs.berkeley.edu>
Tue, 10 Jan 2017 00:01:40 +0000 (19:01 -0500)
This allows userspace to claim a {protocol, port} tuple, and the kernel
bypasses its protocol layer, delivering the raw IP packets to userspace.
It actually doesn't need to be a specific port - just so long as the
protocol knows how to match inbound flows to the conversations at input
time.

Userspace can then either do its own protocol processing (e.g., user-level
TCP on a port-by-port basis), or it can forward it on to a VM.  This is a
building block for a simple NAT that we can build in the VMM.

The kernel still handles IP, like a router, and will send packets to any
IP.  The IP stack still runs - the outbound packets will have Akaros's TTL,
TOS, and fragment bits set.  There actually is a little protocol processing
that goes on during input, specific to the actual protocol.  For instance,
TCP and UDP check the checksum.  I can turn this off if we want, but doing
it like this allows us to use the NIC's xsum calculations.  Not a big deal.

Here's an example of a simple UDP echo server in action (in Qemu, TUN/TAP
networking):

(request, linux->akaros)
56:f3:f8:db:83:ba > 52:54:00:12:34:56, ethertype IPv4 (0x0800), length 48:
(tos 0x0, ttl 64, id 7607, offset 0, flags [DF], proto UDP (17), length 34)
    10.0.2.2.23 > 10.0.2.243.23: UDP, length 6

0x0000:  4500 0022 1db7 4000 4011 0420 0a00 0202  E.."..@.@.......
0x0010:  0a00 02f3 0017 0017 000e 1914 6865 6c6c  ............hell
0x0020:  6f0a                                     o.

(response, akaros->linux)
52:54:00:12:34:56 > 56:f3:f8:db:83:ba, ethertype IPv4 (0x0800), length 64:
(tos 0x0, ttl 255, id 4, offset 0, flags [none], proto UDP (17), length 50)
    10.0.2.243.23 > 10.0.2.2.23: UDP, length 6

0x0000:  4500 0032 0004 0000 ff11 a2c2 0a00 02f3  E..2............
0x0010:  0a00 0202 0017 0017 000e a2d3 6865 6c6c  ............hell
0x0020:  6f0a 0000 0000 0000 0000 0000 0000 0000  o...............
0x0030:  0000                                     ..

Interesting tidbits:
- We use the default TTL of akaros/Plan 9, not of whatever userspace gave
  us.  Same goes for TOS.

- Plan 9 didn't set the Dont_Frag bits.  That actually got zeroed before it
  went to userspace, and never got set on the way back.  Probably an
idiosyncrasy of the networking stack.

- The length of the IP packet is 50 for the response (64 with ethernet),
  but it was 34 for the request (48 with ether).  Akaros userspace got that
50 bytes on the inbound packet.  Userspace read 50 bytes, then it responded
with 50 bytes (of IP).  That 64 bytes of ethernet is set pretty early on,
in the NIC.  It's actually what the NIC (igbe) sees for the packet's
length.  Maybe it was QEMU's virtualization or the TUN/TAP that did that.

Signed-off-by: Barret Rhoden <brho@cs.berkeley.edu>
kern/include/ip.h
kern/src/net/devip.c
kern/src/net/tcp.c
kern/src/net/udp.c

index 442c472..2a73332 100644 (file)
@@ -64,6 +64,7 @@ enum {
        Announced = 2,
        Connecting = 3,
        Connected = 4,
+       Bypass = 5,
 };
 
 enum {
@@ -98,6 +99,8 @@ struct conv {
        int inuse;                                      /* opens of listen/data/ctl */
        int length;
        int state;
+       struct queue *rq_save;          /* rq created by proto, saved during bypass */
+       struct queue *wq_save;          /* wq created by proto, saved during bypass */
 
        /* udp specific */
        int headers;                            /* data src/dst headers in udp */
@@ -298,6 +301,7 @@ struct Proto {
        void (*connect)(struct conv *, char **, int);
        void (*announce)(struct conv *, char **, int);
        void (*bind)(struct conv *, char **, int);
+       void (*bypass)(struct conv *, char **, int);
        int (*state) (struct conv *, char *unused_char_p_t, int);
        void (*create) (struct conv *);
        void (*close) (struct conv *);
@@ -396,8 +400,10 @@ struct Proto *Fsrcvpcol(struct Fs *, uint8_t unused_uint8_t);
 struct Proto *Fsrcvpcolx(struct Fs *, uint8_t unused_uint8_t);
 void Fsstdconnect(struct conv *, char **, int);
 void Fsstdannounce(struct conv *, char **, int);
+void Fsstdbypass(struct conv *, char **, int);
 void Fsstdbind(struct conv *, char **, int);
 uint32_t scalednconv(void);
+void bypass_or_drop(struct conv *cv, struct block *bp);
 
 /*
  *  logging
index ed069c5..38e3b74 100644 (file)
@@ -83,6 +83,7 @@ enum {
        Shiftproto = Logtype + Logconv,
 
        Nfs = 32,
+       BYPASS_QMAX = 64 * MiB,
 };
 #define TYPE(x)        ( ((uint32_t)(x).path) & Masktype )
 #define CONV(x)        ( (((uint32_t)(x).path) >> Shiftconv) & Maskconv )
@@ -99,6 +100,8 @@ extern void pktmediumlink(void);
 extern char *eve;
 static long ndbwrite(struct Fs *, char *unused_char_p_t, uint32_t, int);
 static void closeconv(struct conv *);
+static void setup_proto_qio_bypass(struct conv *cv);
+static void undo_proto_qio_bypass(struct conv *cv);
 
 static struct conv *chan2conv(struct chan *chan)
 {
@@ -698,6 +701,8 @@ static void closeconv(struct conv *cv)
 
        cv->r = NULL;
        cv->rgen = 0;
+       if (cv->state == Bypass)
+               undo_proto_qio_bypass(cv);
        cv->p->close(cv);
        cv->state = Idle;
        qunlock(&cv->qlock);
@@ -748,7 +753,6 @@ static long ipread(struct chan *ch, void *a, long n, int64_t off)
        long rv;
        struct Fs *f;
        uint32_t offset = off;
-       size_t sofar;
 
        f = ipfs[ch->dev];
 
@@ -808,7 +812,10 @@ static long ipread(struct chan *ch, void *a, long n, int64_t off)
                        buf = kzmalloc(Statelen, 0);
                        x = f->p[PROTO(ch->qid)];
                        c = x->conv[CONV(ch->qid)];
-                       sofar = (*x->state) (c, buf, Statelen - 2);
+                       if (c->state == Bypass)
+                               snprintf(buf, Statelen, "Bypassed\n");
+                       else
+                               (*x->state)(c, buf, Statelen - 2);
                        rv = readstr(offset, p, n, buf);
                        kfree(buf);
                        return rv;
@@ -878,7 +885,8 @@ static void setluniqueport(struct conv *c, int lport)
                        break;
                if (xp == c)
                        continue;
-               if ((xp->state == Connected || xp->state == Announced)
+               if ((xp->state == Connected || xp->state == Announced
+                                           || xp->state == Bypass)
                        && xp->lport == lport
                        && xp->rport == c->rport
                        && ipcmp(xp->raddr, c->raddr) == 0
@@ -1142,6 +1150,136 @@ static void bindctlmsg(struct Proto *x, struct conv *c, struct cmdbuf *cb)
                x->bind(c, cb->f, cb->nf);
 }
 
+/* Helper, called by protocols to use the bypass.
+ *
+ * This is a bit nasty due to the overall nastiness of #ip.  We need to lock
+ * before checking the state and hold the qlock throughout, because a concurrent
+ * closeconv() could tear down the bypass.  Specifically, it could free the
+ * bypass queues.  The root issue is that conversation lifetimes are not managed
+ * well.
+ *
+ * If we fail, it's our responsibility to consume (free) the block(s). */
+void bypass_or_drop(struct conv *cv, struct block *bp)
+{
+       qlock(&cv->qlock);
+       if (cv->state == Bypass)
+               qpass(cv->rq, bp);
+       else
+               freeblist(bp);
+       qunlock(&cv->qlock);
+}
+
+/* Push the block directly to the approprite ipoput function.
+ *
+ * It's the protocol's responsibility (and thus ours here) to make sure there is
+ * at least the right amount of the IP header in the block (ipoput{4,6} assumes
+ * it has the right amount, and the other protocols account for the IP header in
+ * their own header).
+ *
+ * For the TTL and TOS, we just use the default ones.  If we want, we could look
+ * into the actual block and see what the user wanted, though we're bypassing
+ * the protocol layer, not the IP layer. */
+static void proto_bypass_kick(void *arg, struct block *bp)
+{
+       struct conv *cv = (struct conv*)arg;
+       uint8_t vers_nibble;
+       struct Fs *f;
+
+       f = cv->p->f;
+
+       bp = pullupblock(bp, 1);
+       if (!bp)
+               error(EINVAL, "Proto bypass unable to pullup a byte!");
+       vers_nibble = *(uint8_t*)bp->rp & 0xf0;
+       switch (vers_nibble) {
+       case IP_VER4:
+               bp = pullupblock(bp, IPV4HDR_LEN);
+               if (!bp)
+                       error(EINVAL, "Proto bypass unable to pullup v4 header");
+               ipoput4(f, bp, FALSE, MAXTTL, DFLTTOS, NULL);
+               break;
+       case IP_VER6:
+               bp = pullupblock(bp, IPV6HDR_LEN);
+               if (!bp)
+                       error(EINVAL, "Proto bypass unable to pullup v6 header");
+               ipoput6(f, bp, FALSE, MAXTTL, DFLTTOS, NULL);
+               break;
+       default:
+               error(EINVAL, "Proto bypass block had unknown IP version 0x%x",
+                     vers_nibble);
+       }
+}
+
+/* Sets up cv for the protocol bypass.  We use different queues for two reasons:
+ * 1) To be protocol independent.  For instance, TCP and UDP could use very
+ * different QIO styles.
+ * 2) To set up our own kick/bypass method.  Note how udpcreate() and here uses
+ * qbypass() (just blast it out), while TCP uses qopen() with a kick.  TCP still
+ * follows queuing discipline.
+ *
+ * It's like we are our own protocol, the bypass protocol, when it comes to how
+ * we interact with qio.  The conv still is of the real protocol type (e.g.
+ * TCP).
+ *
+ * Note that we can't free the old queues.  The way #ip works, the queues are
+ * created when the conv is created, but the conv is never freed.  It's like a
+ * slab allocator that never frees objects, but just reinitializes them a
+ * little.
+ *
+ * For the queues, we're basically like UDP:
+ * - We take packets for rq and drop on overflow.
+ * - rq is also Qmsg, but we also have Qcoalesce, to ignore out zero-len blocks
+ * - We kick for our outbound (wq) messages.
+ *
+ * Note that Qmsg can drop parts of packets.  It's up to the user to read
+ * enough.  If they didn't read enough, the extra is dropped.  This is similar
+ * to SOCK_DGRAM and recvfrom().  Minus major changes, there's no nice way to
+ * get individual messages with read().  Userspace using the bypass will need to
+ * find out the MTU of the NIC the IP stack is attached to, and make sure to
+ * read in at least that amount each time. */
+static void setup_proto_qio_bypass(struct conv *cv)
+{
+       cv->rq_save = cv->rq;
+       cv->wq_save = cv->wq;
+       cv->rq = qopen(BYPASS_QMAX, Qmsg | Qcoalesce, 0, 0);
+       cv->wq = qbypass(proto_bypass_kick, cv);
+}
+
+static void undo_proto_qio_bypass(struct conv *cv)
+{
+       qfree(cv->rq);
+       qfree(cv->wq);
+       cv->rq = cv->rq_save;
+       cv->wq = cv->wq_save;
+       cv->rq_save = NULL;
+       cv->wq_save = NULL;
+}
+
+void Fsstdbypass(struct conv *cv, char *argv[], int argc)
+{
+       memset(cv->raddr, 0, sizeof(cv->raddr));
+       cv->rport = 0;
+       switch (argc) {
+       case 2:
+               setladdrport(cv, argv[1], 1);
+               break;
+       default:
+               error(EINVAL, "Bad args (was %d, need 2) to bypass", argc);
+       }
+}
+
+static void bypassctlmsg(struct Proto *x, struct conv *cv, struct cmdbuf *cb)
+{
+       if (!x->bypass)
+               error(EFAIL, "Protocol %s does not support bypass", x->name);
+       /* The protocol needs to set the port (usually by calling Fsstdbypass) and
+        * then do whatever it needs to make sure it can find the conv again during
+        * receive (usually by adding to a hash table). */
+       x->bypass(cv, cb->f, cb->nf);
+       setup_proto_qio_bypass(cv);
+       cv->state = Bypass;
+}
+
 static void shutdownctlmsg(struct conv *cv, struct cmdbuf *cb)
 {
        if (cb->nf < 2)
@@ -1236,6 +1374,8 @@ static long ipwrite(struct chan *ch, void *v, long n, int64_t off)
                                announcectlmsg(x, c, cb);
                        else if (strcmp(cb->f[0], "bind") == 0)
                                bindctlmsg(x, c, cb);
+                       else if (strcmp(cb->f[0], "bypass") == 0)
+                               bypassctlmsg(x, c, cb);
                        else if (strcmp(cb->f[0], "shutdown") == 0)
                                shutdownctlmsg(c, cb);
                        else if (strcmp(cb->f[0], "ttl") == 0)
index 83a1817..597d923 100644 (file)
@@ -501,6 +501,14 @@ static void tcpannounce(struct conv *c, char **argv, int argc)
        Fsconnected(c, NULL);
 }
 
+static void tcpbypass(struct conv *cv, char **argv, int argc)
+{
+       struct tcppriv *tpriv = cv->p->priv;
+
+       Fsstdbypass(cv, argv, argc);
+       iphtadd(&tpriv->ht, cv);
+}
+
 static void tcpshutdown(struct conv *c, int how)
 {
        Tcpctl *tcb = (Tcpctl*)c->ptcl;
@@ -1988,6 +1996,12 @@ void tcpiput(struct Proto *tcp, struct Ipifc *unused, struct block *bp)
                        return;
                }
 
+               s = iphtlook(&tpriv->ht, source, seg.source, dest, seg.dest);
+               if (s && s->state == Bypass) {
+                       bypass_or_drop(s, bp);
+                       return;
+               }
+
                /* trim the packet to the size claimed by the datagram */
                length -= hdrlen + TCP4_PKT;
                bp = trimblock(bp, hdrlen + TCP4_PKT, length);
@@ -2029,6 +2043,12 @@ void tcpiput(struct Proto *tcp, struct Ipifc *unused, struct block *bp)
                        return;
                }
 
+               s = iphtlook(&tpriv->ht, source, seg.source, dest, seg.dest);
+               if (s && s->state == Bypass) {
+                       bypass_or_drop(s, bp);
+                       return;
+               }
+
                /* trim the packet to the size claimed by the datagram */
                length -= hdrlen;
                bp = trimblock(bp, hdrlen + TCP6_PKT, length);
@@ -2040,8 +2060,7 @@ void tcpiput(struct Proto *tcp, struct Ipifc *unused, struct block *bp)
                }
        }
 
-       /* Look for a matching conversation */
-       s = iphtlook(&tpriv->ht, source, seg.source, dest, seg.dest);
+       /* s, the conv matching the n-tuple, was set above */
        if (s == NULL) {
                netlog(f, Logtcp, "iphtlook failed\n");
 reset:
@@ -3196,6 +3215,7 @@ void tcpinit(struct Fs *fs)
        tcp->name = "tcp";
        tcp->connect = tcpconnect;
        tcp->announce = tcpannounce;
+       tcp->bypass = tcpbypass;
        tcp->ctl = tcpctl;
        tcp->state = tcpstate;
        tcp->create = tcpcreate;
index 19ddfc1..faf78fb 100644 (file)
@@ -173,6 +173,14 @@ static void udpannounce(struct conv *c, char **argv, int argc)
        iphtadd(&upriv->ht, c);
 }
 
+static void udpbypass(struct conv *cv, char **argv, int argc)
+{
+       Udppriv *upriv = cv->p->priv;
+
+       Fsstdbypass(cv, argv, argc);
+       iphtadd(&upriv->ht, cv);
+}
+
 static void udpcreate(struct conv *c)
 {
        c->rq = qopen(128 * 1024, Qmsg, 0, 0);
@@ -467,6 +475,12 @@ void udpiput(struct Proto *udp, struct Ipifc *ifc, struct block *bp)
                freeblist(bp);
                return;
        }
+
+       if (c->state == Bypass) {
+               bypass_or_drop(c, bp);
+               return;
+       }
+
        ucb = (Udpcb *) c->ptcl;
 
        if (c->state == Announced) {
@@ -656,6 +670,7 @@ void udpinit(struct Fs *fs)
        udp->name = "udp";
        udp->connect = udpconnect;
        udp->announce = udpannounce;
+       udp->bypass = udpbypass;
        udp->ctl = udpctl;
        udp->state = udpstate;
        udp->create = udpcreate;