Add example for BPF+tc rate limiting
* Adding an example to address #59, simulation of bandwidth sharing
Signed-off-by: Brenden Blanco <bblanco@plumgrid.com>
diff --git a/examples/tc_neighbor_sharing.c b/examples/tc_neighbor_sharing.c
new file mode 100644
index 0000000..6ba7d35
--- /dev/null
+++ b/examples/tc_neighbor_sharing.c
@@ -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
index 0000000..5add98d
--- /dev/null
+++ b/examples/tc_neighbor_sharing.py
@@ -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()
+
+