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