P4 Switch + Deep Packet Inspection (DPI) example 2
[Topology]
Before that, please refer to https://github.com/mushorg/go-dpi to
install go-dpi in your environment.
Also, remember to use 2 network cards in your
environment. If you are using VirtualBox or VMware, please set these
two NICs to NAT mode.
[basic.p4]
#include <core.p4> #include <v1model.p4> const bit<16> TYPE_IPV4 = 0x800; typedef bit<9> egressSpec_t; typedef bit<48> macAddr_t; typedef bit<32> ip4Addr_t; struct intrinsic_metadata_t { bit<4> mcast_grp; bit<4> egress_rid; bit<16> mcast_hash; bit<32> lf_field_list; } header ethernet_t { bit<48> dstAddr; bit<48> srcAddr; bit<16> etherType; } header ipv4_t { bit<4> version; bit<4> ihl; bit<8> diffserv; bit<16> totalLen; bit<16> identification; bit<3> flags; bit<13> fragOffset; bit<8> ttl; bit<8> protocol; bit<16> hdrChecksum; ip4Addr_t srcAddr; ip4Addr_t dstAddr; } header tcp_t { bit<16> srcPort; bit<16> dstPort; bit<32> seqNo; bit<32> ackNo; bit<4> dataOffset; bit<3> res; bit<3> ecn; bit<6> ctrl; bit<16> window; bit<16> checksum; bit<16> urgentPtr; } struct metadata { @name(".intrinsic_metadata") intrinsic_metadata_t intrinsic_metadata; } struct headers { @name(".ethernet") ethernet_t ethernet; @name(".ipv4") ipv4_t ipv4; @name(".tcp") tcp_t tcp; } parser ParserImpl(packet_in packet, out headers hdr, inout metadata meta, inout standard_metadata_t standard_metadata) { @name(".start") state start { transition parse_ethernet; } @name(".parse_ethernet") state parse_ethernet { packet.extract(hdr.ethernet); transition select(hdr.ethernet.etherType) { TYPE_IPV4: parse_ipv4; default: accept; } } @name(".parse_ipv4")state parse_ipv4 { packet.extract(hdr.ipv4); transition select(hdr.ipv4.protocol) { 6: parse_tcp; default: accept; } } @name(".parse_tcp")state parse_tcp { packet.extract(hdr.tcp); transition accept; } } control ingress(inout headers hdr, inout metadata meta, inout standard_metadata_t standard_metadata) {
action drop() { mark_to_drop(); }
action ipv4_forward(macAddr_t dstAddr, egressSpec_t port) {
standard_metadata.egress_spec = port;
hdr.ethernet.dstAddr = dstAddr;
hdr.ipv4.ttl = hdr.ipv4.ttl - 1; }
table ipv4_lpm { key = {
hdr.ipv4.dstAddr: exact;
hdr.ethernet.dstAddr: exact;
standard_metadata.ingress_port: exact; } actions = { ipv4_forward; drop; NoAction; } size = 1024; default_action = NoAction(); }
apply { if (hdr.ipv4.isValid()) { ipv4_lpm.apply(); } } } control egress(inout headers hdr, inout metadata meta, inout standard_metadata_t standard_metadata) { action mydrop() { mark_to_drop(); }
table filter { key = { hdr.ipv4.dstAddr: exact; //hdr.ipv4.srcAddr: exact; hdr.tcp.dstPort: exact; //hdr.tcp.srcPort: exact; } actions = { mydrop; NoAction; } size = 1024; default_action = NoAction(); }
apply { filter.apply(); } } control DeparserImpl(packet_out packet, in headers hdr) { apply { packet.emit(hdr.ethernet); packet.emit(hdr.ipv4); packet.emit(hdr.tcp); } } control verifyChecksum(inout headers hdr, inout metadata meta) { apply { } } control computeChecksum(inout headers hdr, inout metadata meta) { apply { update_checksum(true, { hdr.ipv4.version, hdr.ipv4.ihl, hdr.ipv4.diffserv, hdr.ipv4.totalLen, hdr.ipv4.identification, hdr.ipv4.flags, hdr.ipv4.fragOffset, hdr.ipv4.ttl, hdr.ipv4.protocol, hdr.ipv4.srcAddr, hdr.ipv4.dstAddr }, hdr.ipv4.hdrChecksum, HashAlgorithm.csum16); } } V1Switch(ParserImpl(), verifyChecksum(), ingress(), egress(), computeChecksum(), DeparserImpl()) main; |
[test_topo.py]
import os from mininet.net import Mininet from mininet.topo import Topo from mininet.log import setLogLevel, info from mininet.cli import CLI from mininet.node import RemoteController from mininet.link import Intf, TCLink from p4_mininet import P4Switch, P4Host import argparse from time import sleep parser = argparse.ArgumentParser(description='Mininet demo') parser.add_argument('--behavioral-exe', help='Path to behavioral executable', type=str, action="store", required=False, default='simple_switch' ) parser.add_argument('--thrift-port', help='Thrift server port for table updates', type=int, action="store", default=9090) parser.add_argument('--num-hosts', help='Number of hosts to connect to switch', type=int, action="store", default=2) parser.add_argument('--mode', choices=['l2', 'l3'], type=str, default='l3') parser.add_argument('--json', help='Path to JSON config file', type=str, action="store", required=True) parser.add_argument('--pcap-dump', help='Dump packets on interfaces to pcap files', type=str, action="store", required=False, default=False) args = parser.parse_args() class SingleSwitchTopo(Topo): def __init__(self, sw_path, json_path, thrift_port, pcap_dump, **opts): Topo.__init__(self, **opts) switch1 = self.addSwitch('s1', sw_path = sw_path, json_path = json_path, thrift_port = thrift_port,cls = P4Switch ,pcap_dump = pcap_dump) #switch2 = self.addSwitch('s2', sw_path = sw_path, json_path = json_path, thrift_port = thrift_port + 1,cls = P4Switch ,pcap_dump = pcap_dump)
host1 = self.addHost('h1', mac = '00:00:00:00:00:01', ip = '10.0.0.1/24' ) host2 = self.addHost('h2', mac = '00:00:00:00:00:02', ip = '10.0.0.2/24' ) host3 = self.addHost('h3', mac = '00:00:00:00:00:03', ip = '10.0.0.3/24' ) self.addLink(host1, switch1, port1 = 0,
port2 = 1)
self.addLink(host2, switch1, port1 = 0, port2 = 2) self.addLink(host2, switch1,
port1 = 1, port2 = 4)
self.addLink(host3, switch1, port1 = 0, port2 = 3) #self.addLink(host2, switch2, port1 = 0, port2 = 1) #self.addLink(switch1, switch2)
def main(): topo = SingleSwitchTopo(args.behavioral_exe, args.json, args.thrift_port, args.pcap_dump) #controller1 = RemoteController('controller1', ip = '10.108.148.148') net = Mininet(topo = topo, host = P4Host, controller = None, link=TCLink) net.start() h1,h2,h3=net.get('h1','h2','h3') Intf('eth10', node=h2) h1.cmd("arp -s
10.0.0.2 00:00:00:00:00:02") h1.cmd("arp -s
10.0.0.3 00:00:00:00:00:03") h2.cmd("arp -s
10.0.0.1 00:00:00:00:00:01 -i eth0") h2.cmd("arp -s
10.0.0.3 00:00:00:00:00:03 -i h2-eth1") h3.cmd("arp -s 10.0.0.1
00:00:00:00:00:01") h3.cmd("arp -s
10.0.0.2 00:00:00:00:00:02") h2.cmd("ifconfig
h2-eth1 hw ether 00:00:00:00:00:04") h2.cmd("ip addr add
10.0.0.4/24 brd + dev h2-eth1") h2.cmd("ip route add
10.0.0.3/32 via 10.0.0.4") h2.cmd("ip route add
10.0.0.1/32 via 10.0.0.2") h2.cmd("echo 1 >
/proc/sys/net/ipv4/ip_forward") h2.cmd("/sbin/sysctl
-w net.ipv4.conf.eth0.accept_redirects=0") h2.cmd("/sbin/sysctl
-w net.ipv4.conf.eth0.send_redirects=0") h2.cmd("echo 0 >
/proc/sys/net/ipv4/conf/all/accept_redirects") h2.cmd("echo 0 >
/proc/sys/net/ipv4/conf/all/send_redirects") sleep(1) print('\033[0;32m'), print "Gotcha!" print('\033[0m') CLI(net) try: net.stop() except: print('\033[0;31m'), print('Stop error! Trying sudo mn -c') print('\033[0m') os.system('sudo mn -c') print('\033[0;32m'), print ('Stop successfully!') print('\033[0m') if __name__ == '__main__': setLogLevel('info') main() |
[cmd.txt]
table_add ipv4_lpm ipv4_forward 10.0.0.3 00:00:00:00:00:03 1 => 00:00:00:00:00:02 2 table_add ipv4_lpm ipv4_forward 10.0.0.3 00:00:00:00:00:02 4 => 00:00:00:00:00:03 3 table_add ipv4_lpm ipv4_forward 10.0.0.1 00:00:00:00:00:01 3 => 00:00:00:00:00:04 4 table_add ipv4_lpm ipv4_forward 10.0.0.1 00:00:00:00:00:01 2 => 00:00:00:00:00:01 1 |
[cmd_add.py]
import os os.system('sudo /home/vagrant/behavioral-model/targets/simple_switch/simple_switch_CLI --thrift-port=9090 < cmd.txt') |
[start_test_topo.py]
import os os.system("sudo python test_topo.py --behavioral-exe /home/vagrant/behavioral-model/targets/simple_switch/simple_switch --json basic.json") |
[p4_mininet.py]
# Copyright 2013-present Barefoot Networks, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # from mininet.net import Mininet from mininet.node import Switch, Host from mininet.log import setLogLevel, info, error, debug from mininet.moduledeps import pathCheck from sys import exit import os import tempfile import socket class P4Host(Host): def config(self, **params): r = super(Host, self).config(**params) self.defaultIntf().rename("eth0") for off in ["rx", "tx", "sg"]: cmd = "/sbin/ethtool --offload eth0 %s off" % off self.cmd(cmd) # disable IPv6 self.cmd("sysctl -w net.ipv6.conf.all.disable_ipv6=1") self.cmd("sysctl -w net.ipv6.conf.default.disable_ipv6=1") self.cmd("sysctl -w net.ipv6.conf.lo.disable_ipv6=1") return r def describe(self): print "**********" print self.name print "default interface: %s\t%s\t%s" %( self.defaultIntf().name, self.defaultIntf().IP(), self.defaultIntf().MAC() ) print "**********" class P4Switch(Switch): """P4 virtual switch""" device_id = 0 def __init__(self, name, sw_path = None, json_path = None, thrift_port = None, pcap_dump = False, log_console = False, verbose = False, device_id = None, enable_debugger = False, **kwargs): Switch.__init__(self, name, **kwargs) assert(sw_path) assert(json_path) # make sure that the provided sw_path is valid pathCheck(sw_path) # make sure that the provided JSON file exists if not os.path.isfile(json_path): error("Invalid JSON file.\n") exit(1) self.sw_path = sw_path self.json_path = json_path self.verbose = verbose logfile = "/tmp/p4s.{}.log".format(self.name) self.output = open(logfile, 'w') self.thrift_port = thrift_port self.pcap_dump = pcap_dump self.enable_debugger = enable_debugger self.log_console = log_console if device_id is not None: self.device_id = device_id P4Switch.device_id = max(P4Switch.device_id, device_id) else: self.device_id = P4Switch.device_id P4Switch.device_id += 1 self.nanomsg = "ipc:///tmp/bm-{}-log.ipc".format(self.device_id) @classmethod def setup(cls): pass def check_switch_started(self, pid): """While the process is running (pid exists), we check if the Thrift server has been started. If the Thrift server is ready, we assume that the switch was started successfully. This is only reliable if the Thrift server is started at the end of the init process""" while True: if not os.path.exists(os.path.join("/proc", str(pid))): return False sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(0.5) result = sock.connect_ex(("localhost", self.thrift_port)) if result == 0: return True def start(self, controllers): "Start up a new P4 switch" info("Starting P4 switch {}.\n".format(self.name)) args = [self.sw_path] for port, intf in self.intfs.items(): if not intf.IP(): args.extend(['-i', str(port) + "@" + intf.name]) #wuwzhs edit in 2017/11/10 #args.extend(['-i 3@veth1']) if self.pcap_dump: args.append("--pcap") # args.append("--useFiles") if self.thrift_port: args.extend(['--thrift-port', str(self.thrift_port)]) if self.nanomsg: args.extend(['--nanolog', self.nanomsg]) args.extend(['--device-id', str(self.device_id)]) P4Switch.device_id += 1 args.append(self.json_path) if self.enable_debugger: args.append("--debugger") if self.log_console: args.append("--log-console") logfile = "/tmp/p4s.{}.log".format(self.name) info(' '.join(args) + "\n") pid = None with tempfile.NamedTemporaryFile() as f: # self.cmd(' '.join(args) + ' > /dev/null 2>&1 &') self.cmd(' '.join(args) + ' >' + logfile + ' 2>&1 & echo $! >> ' + f.name) pid = int(f.read()) debug("P4 switch {} PID is {}.\n".format(self.name, pid)) if not self.check_switch_started(pid): error("P4 switch {} did not start correctly.\n".format(self.name)) exit(1) info("P4 switch {} has been started.\n".format(self.name)) def stop(self): "Terminate P4 switch." self.output.flush() self.cmd('kill %' + self.sw_path) self.cmd('wait') self.deleteIntfs() def attach(self, intf): "Connect a data port" assert(0) def detach(self, intf): "Disconnect a data port" assert(0) |
[example_app.go]
package main import ( "flag" "fmt" "os" "os/signal" "time" "github.com/google/gopacket" "github.com/google/gopacket/layers" "github.com/google/gopacket/pcap" "github.com/mushorg/go-dpi" "github.com/mushorg/go-dpi/types" "github.com/mushorg/go-dpi/utils" ) func main() { var ( count, idCount int protoCounts map[types.Protocol]int packetChannel <-chan gopacket.Packet err error )
var handle *pcap.Handle var deverr error //var mybuffer gopacket.SerializeBuffer //var myoptions gopacket.SerializeOptions protoCounts = make(map[types.Protocol]int) filename := flag.String("filename", "godpi_example/dumps/http.cap", "File to read packets from") device := flag.String("device", "", "Device to watch for packets") flag.Parse() if *device != "" { // check if interface was given handle, deverr = pcap.OpenLive(*device, 1024, false, time.Duration(-1)) if deverr != nil { fmt.Println("Error opening device:", deverr) return } packetChannel = gopacket.NewPacketSource(handle, handle.LinkType()).Packets() } else if _, ferr := os.Stat(*filename); !os.IsNotExist(ferr) { // check if file exists packetChannel, err = utils.ReadDumpFile(*filename) } else { fmt.Println("File does not exist:", *filename) return } initErrs := godpi.Initialize() if len(initErrs) != 0 { for _, err := range initErrs { fmt.Println(err) } return } defer func() { godpi.Destroy() fmt.Println() fmt.Println("Number of packets:", count) fmt.Println("Number of packets identified:", idCount) fmt.Println("Protocols identified:\n", protoCounts) }() signalChannel := make(chan os.Signal, 1) signal.Notify(signalChannel, os.Interrupt) intSignal := false if err != nil { fmt.Println("Error:", err) return } count = 0 for packet := range packetChannel { fmt.Printf("Packet #%d: ", count+1) flow, isNew := godpi.GetPacketFlow(packet) //fmt.Println(reflect.TypeOf(flow)) result := godpi.ClassifyFlow(flow) if result.Protocol != types.Unknown { fmt.Print(result) idCount++ protoCounts[result.Protocol]++ } else { fmt.Print("Could not identify") } if isNew { fmt.Println(" (new flow)") } else { fmt.Println() } select { case <-signalChannel: fmt.Println("Received interrupt signal") intSignal = true default: } if intSignal { break } count++ ipLayer := packet.Layer(layers.LayerTypeIPv4) if ipLayer != nil { fmt.Println("IPv4 layer detected.") ip, _ := ipLayer.(*layers.IPv4) fmt.Printf("From %s to %s\n", ip.SrcIP, ip.DstIP) fmt.Println("Protocol: ", ip.Protocol) fmt.Println() } tcpLayer := packet.Layer(layers.LayerTypeTCP) if tcpLayer != nil { fmt.Println("TCP layer detected.") tcp, _ := tcpLayer.(*layers.TCP) fmt.Printf("From port %d to %d\n", tcp.SrcPort, tcp.DstPort) fmt.Println() }
if result.Protocol
!="ICMP" {
fmt.Println("NOT ICMP PACKET")
mydevice := "h2-eth1"
myhandle, mydeverr := pcap.OpenLive(mydevice, 1024, false,
time.Duration(-1))
if mydeverr != nil {
fmt.Println("Error opening device:", mydeverr)
return
}
defer myhandle.Close()
buffer :=gopacket.NewSerializeBuffer()
var options gopacket.SerializeOptions
err :=gopacket.SerializePacket(buffer, options, packet)
if err != nil {
fmt.Printf("%s", err)
}
outgoingPacket := buffer.Bytes()
err = myhandle.WritePacketData(outgoingPacket)
if err != nil {
fmt.Printf("%s", err)
}
fmt.Println("send out one packet")
} } } |
[Execution]
Check whether there are two NICs in your VM.
Rename eth1 to eth10.
Start the emulation.
Open another terminal to set up the rules for p1.
Use xterm to open terminals for h1,h2,h3
In h2, use the dhclient eth10 to get the IP address for eth10.
Check the settings for h1
Check the settings for h2
Check the settings for h3
Run example_app
Setup the firewall in h2
http test (ok)
Ping test (fail…. Due to firewall)
Dr. Chih-Heng Ke (smallko@gmail.com)
Department of Computer Science and Information
Engineering,
National Quemoy University, Kinmen, Taiwan.