1#!/usr/bin/env python3
2
3# Copyright (c) Barefoot Networks, Inc.
4# Licensed under the Apache License, Version 2.0 (the "License")
5
6# Testing example for P4->EBPF compiler
7#
8# This program exercises the simple.c EBPF program
9# generated from the simple.p4 source file.
10
11import subprocess
12import ctypes
13import time
14import sys
15import os
16from bcc import BPF
17from pyroute2 import IPRoute, NSPopen, NetNS
18from netaddr import IPAddress
19
20### This part is a simple generic network simulaton toolkit
21
22class Base(object):
23    def __init__(self):
24        self.verbose = True
25
26    def message(self, *args):
27        if self.verbose:
28            print(*args)
29
30
31class Endpoint(Base):
32    # a network interface really
33    def __init__(self, ipaddress, ethaddress):
34        Base.__init__(self)
35        self.mac_addr = ethaddress
36        self.ipaddress = ipaddress
37        self.prefixlen = 24
38        self.parent = None
39
40    def __str__(self):
41        return "Endpoint " + str(self.ipaddress)
42
43    def set_parent(self, parent):
44        assert isinstance(parent, Node)
45        self.parent = parent
46
47    def get_ip_address(self):
48        return IPAddress(self.ipaddress)
49
50
51class Node(Base):
52    # Used to represent one of clt, sw, srv
53    # Each lives in its own namespace
54    def __init__(self, name):
55        Base.__init__(self)
56        self.name = name
57        self.endpoints = []
58        self.get_ns()  # as a side-effect creates namespace
59
60    def add_endpoint(self, endpoint):
61        assert isinstance(endpoint, Endpoint)
62        self.endpoints.append(endpoint)
63        endpoint.set_parent(self)
64
65    def __str__(self):
66        return "Node " + self.name
67
68    def get_ns_name(self):
69        return self.name
70
71    def get_ns(self):
72        nsname = self.get_ns_name()
73        ns = NetNS(nsname)
74        return ns
75
76    def remove(self):
77        ns = self.get_ns();
78        ns.close()
79        ns.remove()
80
81    def execute(self, command):
82        # Run a command in the node's namespace
83        # Return the command's exit code
84        self.message(self.name, "Executing", command)
85        nsn = self.get_ns_name()
86        pipe = NSPopen(nsn, command)
87        result = pipe.wait()
88        pipe.release()
89        return result
90
91    def set_arp(self, destination):
92        assert isinstance(destination, Endpoint)
93        command = ["arp", "-s", str(destination.ipaddress),
94                   str(destination.mac_addr)]
95        self.execute(command)
96
97
98class NetworkBase(Base):
99    def __init__(self):
100        Base.__init__(self)
101        self.ipr = IPRoute()
102        self.nodes = []
103
104    def add_node(self, node):
105        assert isinstance(node, Node)
106        self.nodes.append(node)
107
108    def get_interface_name(self, source, dest):
109        assert isinstance(source, Node)
110        assert isinstance(dest, Node)
111        interface_name = "veth-" + source.name + "-" + dest.name
112        return interface_name
113
114    def get_interface(self, ifname):
115        interfaces = self.ipr.link_lookup(ifname=ifname)
116        if len(interfaces) != 1:
117            raise Exception("Could not identify interface " + ifname)
118        ix = interfaces[0]
119        assert isinstance(ix, int)
120        return ix
121
122    def set_interface_ipaddress(self, node, ifname, address, mask):
123        # Ask a node to set the specified interface address
124        if address is None:
125            return
126
127        assert isinstance(node, Node)
128        command = ["ip", "addr", "add", str(address) + "/" + str(mask),
129                   "dev", str(ifname)]
130        result = node.execute(command)
131        assert(result == 0)
132
133    def create_link(self, src, dest):
134        assert isinstance(src, Endpoint)
135        assert isinstance(dest, Endpoint)
136
137        ifname = self.get_interface_name(src.parent, dest.parent)
138        destname = self.get_interface_name(dest.parent, src.parent)
139        self.ipr.link_create(ifname=ifname, kind="veth", peer=destname)
140
141        self.message("Create", ifname, "link")
142
143        # Set source endpoint information
144        ix = self.get_interface(ifname)
145        self.ipr.link("set", index=ix, address=src.mac_addr)
146        # push source endpoint into source namespace
147        self.ipr.link("set", index=ix,
148                      net_ns_fd=src.parent.get_ns_name(), state="up")
149        # Set interface ip address; seems to be
150        # lost of set prior to moving to namespace
151        self.set_interface_ipaddress(
152            src.parent, ifname, src.ipaddress , src.prefixlen)
153
154        # Sef destination endpoint information
155        ix = self.get_interface(destname)
156        self.ipr.link("set", index=ix, address=dest.mac_addr)
157        # push destination endpoint into the destination namespace
158        self.ipr.link("set", index=ix,
159                      net_ns_fd=dest.parent.get_ns_name(), state="up")
160        # Set interface ip address
161        self.set_interface_ipaddress(dest.parent, destname,
162                                     dest.ipaddress, dest.prefixlen)
163
164    def show_interfaces(self, node):
165        cmd = ["ip", "addr"]
166        if node is None:
167            # Run with no namespace
168            subprocess.call(cmd)
169        else:
170            # Run in node's namespace
171            assert isinstance(node, Node)
172            self.message("Enumerating all interfaces in ", node.name)
173            node.execute(cmd)
174
175    def delete(self):
176        self.message("Deleting virtual network")
177        for n in self.nodes:
178            n.remove()
179        self.ipr.close()
180
181
182### Here begins the concrete instantiation of the network
183# Network setup:
184# Each of these is a separate namespace.
185#
186#                        62:ce:1b:48:3e:61          a2:59:94:cf:51:09
187#      96:a4:85:fe:2a:11           62:ce:1b:48:3e:60
188#              /------------------\     /-----------------\
189#      ----------                 --------                ---------
190#      |  clt   |                 |  sw  |                |  srv  |
191#      ----------                 --------                ---------
192#       10.0.0.11                                         10.0.0.10
193#
194
195class SimulatedNetwork(NetworkBase):
196    def __init__(self):
197        NetworkBase.__init__(self)
198
199        self.client = Node("clt")
200        self.add_node(self.client)
201        self.client_endpoint = Endpoint("10.0.0.11", "96:a4:85:fe:2a:11")
202        self.client.add_endpoint(self.client_endpoint)
203
204        self.server = Node("srv")
205        self.add_node(self.server)
206        self.server_endpoint = Endpoint("10.0.0.10", "a2:59:94:cf:51:09")
207        self.server.add_endpoint(self.server_endpoint)
208
209        self.switch = Node("sw")
210        self.add_node(self.switch)
211        self.sw_clt_endpoint = Endpoint(None, "62:ce:1b:48:3e:61")
212        self.sw_srv_endpoint = Endpoint(None, "62:ce:1b:48:3e:60")
213        self.switch.add_endpoint(self.sw_clt_endpoint)
214        self.switch.add_endpoint(self.sw_srv_endpoint)
215
216    def run_method_in_node(self, node, method, args):
217        # run a method of the SimulatedNetwork class in a different namespace
218        # return the exit code
219        assert isinstance(node, Node)
220        assert isinstance(args, list)
221        torun = __file__
222        args.insert(0, torun)
223        args.insert(1, method)
224        return node.execute(args)  # runs the command argv[0] method args
225
226    def instantiate(self):
227        # Creates the various namespaces
228        self.message("Creating virtual network")
229
230        self.message("Create client-switch link")
231        self.create_link(self.client_endpoint, self.sw_clt_endpoint)
232
233        self.message("Create server-switch link")
234        self.create_link(self.server_endpoint, self.sw_srv_endpoint)
235
236        self.show_interfaces(self.client)
237        self.show_interfaces(self.server)
238        self.show_interfaces(self.switch)
239
240        self.message("Set ARP mappings")
241        self.client.set_arp(self.server_endpoint)
242        self.server.set_arp(self.client_endpoint)
243
244    def setup_switch(self):
245        # This method is run in the switch namespace.
246        self.message("Compiling and loading BPF program")
247
248        b = BPF(src_file="./simple.c", debug=0)
249        fn = b.load_func("ebpf_filter", BPF.SCHED_CLS)
250
251        self.message("BPF program loaded")
252
253        self.message("Discovering tables")
254        routing_tbl = b.get_table("routing")
255        routing_miss_tbl = b.get_table("ebpf_routing_miss")
256        cnt_tbl = b.get_table("cnt")
257
258        self.message("Hooking up BPF classifiers using TC")
259
260        interfname = self.get_interface_name(self.switch, self.server)
261        sw_srv_idx = self.get_interface(interfname)
262        self.ipr.tc("add", "ingress", sw_srv_idx, "ffff:")
263        self.ipr.tc("add-filter", "bpf", sw_srv_idx, ":1", fd=fn.fd,
264                    name=fn.name, parent="ffff:", action="ok", classid=1)
265
266        interfname = self.get_interface_name(self.switch, self.client)
267        sw_clt_idx = self.get_interface(interfname)
268        self.ipr.tc("add", "ingress", sw_clt_idx, "ffff:")
269        self.ipr.tc("add-filter", "bpf", sw_clt_idx, ":1", fd=fn.fd,
270                    name=fn.name, parent="ffff:", action="ok", classid=1)
271
272        self.message("Populating tables from the control plane")
273        cltip = self.client_endpoint.get_ip_address()
274        srvip = self.server_endpoint.get_ip_address()
275
276        # BCC does not support tbl.Leaf when the type contains a union,
277        # so we have to make up the value type manually.  Unfortunately
278        # these sizes are not portable...
279
280        class Forward(ctypes.Structure):
281            _fields_ = [("port", ctypes.c_ushort)]
282
283        class Nop(ctypes.Structure):
284            _fields_ = []
285
286        class Union(ctypes.Union):
287            _fields_ = [("nop", Nop),
288                        ("forward", Forward)]
289
290        class Value(ctypes.Structure):
291            _fields_ = [("action", ctypes.c_uint),
292                        ("u", Union)]
293
294        if False:
295            # This is how it should ideally be done, but it does not work
296            routing_tbl[routing_tbl.Key(int(cltip))] = routing_tbl.Leaf(
297                1, sw_clt_idx)
298            routing_tbl[routing_tbl.Key(int(srvip))] = routing_tbl.Leaf(
299                1, sw_srv_idx)
300        else:
301            v1 = Value()
302            v1.action = 1
303            v1.u.forward.port = sw_clt_idx
304
305            v2 = Value()
306            v2.action = 1;
307            v2.u.forward.port = sw_srv_idx
308
309            routing_tbl[routing_tbl.Key(int(cltip))] = v1
310            routing_tbl[routing_tbl.Key(int(srvip))] = v2
311
312        self.message("Dumping table contents")
313        for key, leaf in routing_tbl.items():
314            self.message(str(IPAddress(key.key_field_0)),
315                         leaf.action, leaf.u.forward.port)
316
317    def run(self):
318        self.message("Pinging server from client")
319        ping = ["ping", self.server_endpoint.ipaddress, "-c", "2"]
320        result = self.client.execute(ping)
321        if result != 0:
322            raise Exception("Test failed")
323        else:
324            print("Test succeeded!")
325
326    def prepare_switch(self):
327        self.message("Configuring switch")
328        # Re-invokes this script in the switch namespace;
329        # this causes the setup_switch method to be run in that context.
330        # This is the same as running self.setup_switch()
331        # but in the switch namespace
332        self.run_method_in_node(self.switch, "setup_switch", [])
333
334
335def compile(source, destination):
336    try:
337        status = subprocess.call(
338            "../compiler/p4toEbpf.py " + source + " -o " + destination,
339            shell=True)
340        if status < 0:
341            print("Child was terminated by signal", -status, file=sys.stderr)
342        else:
343            print("Child returned", status, file=sys.stderr)
344    except OSError as e:
345        print("Execution failed:", e, file=sys.stderr)
346        raise e
347
348def start_simulation():
349    compile("testprograms/simple.p4", "simple.c")
350    network = SimulatedNetwork()
351    network.instantiate()
352    network.prepare_switch()
353    network.run()
354    network.delete()
355    os.remove("simple.c")
356
357def main(argv):
358    print(str(argv))
359    if len(argv) == 1:
360        # Main entry point: start simulation
361        start_simulation()
362    else:
363        # We are invoked with some arguments (probably in a different namespace)
364        # First argument is a method name, rest are method arguments.
365        # Create a SimulatedNetwork and invoke the specified method with the
366        # specified arguments.
367        network = SimulatedNetwork()
368        methodname = argv[1]
369        arguments = argv[2:]
370        method = getattr(network, methodname)
371        method(*arguments)
372
373if __name__ == '__main__':
374    main(sys.argv)
375
376