Add example for BPF+tc rate limiting
authorBrenden Blanco <bblanco@plumgrid.com>
Tue, 16 Jun 2015 20:50:16 +0000 (13:50 -0700)
committerBrenden Blanco <bblanco@plumgrid.com>
Tue, 16 Jun 2015 20:50:16 +0000 (13:50 -0700)
* Adding an example to address #59, simulation of bandwidth sharing

Signed-off-by: Brenden Blanco <bblanco@plumgrid.com>
examples/tc_neighbor_sharing.c [new file with mode: 0644]
examples/tc_neighbor_sharing.py [new file with mode: 0755]

diff --git a/examples/tc_neighbor_sharing.c b/examples/tc_neighbor_sharing.c
new file mode 100644 (file)
index 0000000..6ba7d35
--- /dev/null
@@ -0,0 +1,61 @@
+// Copyright (c) PLUMgrid, Inc.
+// Licensed under the Apache License, Version 2.0 (the "License")
+
+#include <bcc/proto.h>
+
+struct ipkey {
+  u32 client_ip;
+};
+
+BPF_TABLE("hash", struct ipkey, int, learned_ips, 1024);
+
+// trivial action
+int pass(struct __sk_buff *skb) {
+  return 1;
+}
+
+// Process each wan packet, and determine if the packet is in the IP
+// table or not. Learned IPs are rate-limited and unclassified are not.
+// returns: > 0 when an IP is known
+//          = 0 when an IP is not known, or non-IP traffic
+int classify_wan(struct __sk_buff *skb) {
+  BEGIN(ethernet);
+  PROTO(ethernet) {
+    switch (ethernet->type) {
+      case 0x0800: goto ip;
+    }
+    goto EOP;
+  }
+  PROTO(ip) {
+    u32 dip = ip->dst;
+    struct ipkey key = {.client_ip=dip};
+    int *val = learned_ips.lookup(&key);
+    if (val)
+      return *val;
+    goto EOP;
+  }
+EOP:
+  return 0;
+}
+
+// Process each neighbor packet, and store the source IP in the learned table.
+// Mark the inserted entry with a non-zero value to be used by the classify_wan
+// lookup.
+int classify_neighbor(struct __sk_buff *skb) {
+  BEGIN(ethernet);
+  PROTO(ethernet) {
+    switch (ethernet->type) {
+      case 0x0800: goto ip;
+    }
+    goto EOP;
+  }
+  PROTO(ip) {
+    u32 sip = ip->src;
+    struct ipkey key = {.client_ip=sip};
+    int val = 1;
+    learned_ips.lookup_or_init(&key, &val);
+    goto EOP;
+  }
+EOP:
+  return 1;
+}
diff --git a/examples/tc_neighbor_sharing.py b/examples/tc_neighbor_sharing.py
new file mode 100755 (executable)
index 0000000..5add98d
--- /dev/null
@@ -0,0 +1,122 @@
+#!/usr/bin/env python
+# Copyright (c) PLUMgrid, Inc.
+# Licensed under the Apache License, Version 2.0 (the "License")
+
+# This example shows how a combination of BPF programs can be used to perform
+# per-IP classification and rate limiting. The simulation in this example
+# shows an example where N+M devices are combined and use 1 WAN. Traffic sent
+# from/to the "neighbor" devices have their combined bandwidth capped at
+# 128kbit, and the rest of the traffic can use an additional 1Mbit.
+
+# This works by sharing a map between various tc ingress filters, each with
+# a related set of bpf functions attached. The map stores a list of dynamically
+# learned ip addresses that were seen on the neighbor devices and should be
+# throttled.
+
+#                          /------------\                        |
+# neigh1 --|->->->->->->->-|            |                        |
+# neigh2 --|->->->->->->->-|    <-128kb-|        /------\        |
+# neigh3 --|->->->->->->->-|            |  wan0  | wan  |        |
+#          | ^             |   br100    |-<-<-<--| sim  |        |
+#          | clsfy_neigh() |            |   ^    \------/        |
+# lan1 ----|->->->->->->->-|    <--1Mb--|   |                    |
+# lan2 ----|->->->->->->->-|            |   classify_wan()       |
+#            ^             \------------/                        |
+#            pass()                                              |
+
+
+from bpf import BPF
+from pyroute2 import IPRoute, NetNS, IPDB, NSPopen
+import sys
+from time import sleep
+
+ipr = IPRoute()
+ipdb = IPDB(nl=ipr)
+b = BPF(src_file="tc_neighbor_sharing.c", debug=0)
+
+wan_fn = b.load_func("classify_wan", BPF.SCHED_CLS)
+pass_fn = b.load_func("pass", BPF.SCHED_CLS)
+neighbor_fn = b.load_func("classify_neighbor", BPF.SCHED_CLS)
+
+num_neighbors = 3
+num_locals = 2
+
+# class to build the simulation network
+class SharedNetSimulation(object):
+
+    def __init__(self):
+        self.ipdbs = []
+        self.namespaces = []
+        self.processes = []
+
+        # Create the wan namespace, and attach an ingress filter for throttling
+        # inbound (download) traffic
+        (self.wan, wan_if) = self._create_ns("wan0", "172.16.1.5/24", None)
+        ipr.tc("add", "ingress", wan_if["index"], "ffff:")
+        ipr.tc("add-filter", "bpf", wan_if["index"], ":1", fd=wan_fn.fd,
+               prio=1, name=wan_fn.name, parent="ffff:", action="drop",
+               classid=1, rate="128kbit", burst=1024 * 32, mtu=16 * 1024)
+        ipr.tc("add-filter", "bpf", wan_if["index"], ":2", fd=pass_fn.fd,
+               prio=2, name=pass_fn.name, parent="ffff:", action="drop",
+               classid=2, rate="1024kbit", burst=1024 * 32, mtu=16 * 1024)
+        self.wan_if = wan_if
+
+    # helper function to create a namespace and a veth connecting it
+    def _create_ns(self, name, ipaddr, fn):
+        ns_ipdb = IPDB(nl=NetNS(name))
+        ipdb.create(ifname="%sa" % name, kind="veth", peer="%sb" % name).commit()
+        with ipdb.interfaces["%sb" % name] as v:
+            v.net_ns_fd = ns_ipdb.nl.netns
+        with ipdb.interfaces["%sa" % name] as v:
+            v.up()
+        with ns_ipdb.interfaces["%sb" % name] as v:
+            v.ifname = "eth0"
+            v.add_ip("%s" % ipaddr)
+            v.up()
+        ifc = ipdb.interfaces["%sa" % name]
+        if fn:
+            ipr.tc("add", "ingress", ifc["index"], "ffff:")
+            ipr.tc("add-filter", "bpf", ifc["index"], ":1", fd=fn.fd, name=fn.name,
+                   parent="ffff:", action="ok", classid=1)
+        self.ipdbs.append(ns_ipdb)
+        self.namespaces.append(ns_ipdb.nl)
+        cmd = ["iperf", "-s", "-B", ipaddr.split("/")[0]]
+        self.processes.append(NSPopen(ns_ipdb.nl.netns, cmd))
+        return (ns_ipdb, ifc)
+
+    # start the namespaces that compose the network, interconnect them with the bridge,
+    # and attach the tc filters
+    def start(self):
+        neighbor_list = []
+        local_list = []
+        for i in range(0, num_neighbors):
+            neighbor_list.append(self._create_ns("neighbor%d" % i, "172.16.1.%d/24" % (i + 100), neighbor_fn))
+        for i in range(0, num_locals):
+            local_list.append(self._create_ns("local%d" % i, "172.16.1.%d/24" % (i + 150), pass_fn))
+
+        with ipdb.create(ifname="br100", kind="bridge") as br100:
+            for x in neighbor_list:
+                br100.add_port(x[1])
+            for x in local_list:
+                br100.add_port(x[1])
+            br100.add_port(self.wan_if)
+            br100.up()
+
+    def release(self):
+        for p in self.processes: p.kill(); p.release()
+        for db in self.ipdbs: db.release()
+        for ns in self.namespaces: ns.remove()
+
+try:
+    sim = SharedNetSimulation()
+    sim.start()
+    print("Network ready. Create a shell in the wan0 namespace and test with iperf")
+    print(" e.g.: ip netns exec wan0 iperf -t 2 -c 172.16.1.100")
+    input("Press enter when finished: ")
+finally:
+    if "sim" in locals(): sim.release()
+    if "br100" in ipdb.interfaces: ipdb.interfaces.br100.remove().commit()
+    sleep(2)
+    ipdb.release()
+
+