P4 Switch + Deep Packet Inspection (DPI) example
[Topology]
H1-s1-H2
|
H3
In this lab, I will run a DPI application on H3 (go-dpi example). H2 will run a HTTP server (but not at 80 port). H1 will use curl to get the webpage from H2. When the packet go through the S1, the packet will clone a copy and send it to H3 for Deep Packet Inspection. When H3 detects that this is a HTTP protocol packet, H3 will send a rule to s1 to block any packet to H2 HTTP server port.
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 forward(egressSpec_t port) { standard_metadata.egress_spec = port; } table phy_forward
{
key = {
standard_metadata.ingress_port: exact; }
actions = {
forward;
drop; }
size = 1024; default_action = drop(); } @name(".do_copy") action do_copy()
{
clone3(CloneType.I2E, (bit<32>)32w250, { standard_metadata
}); } @name(".mycopy") table mycopy {
actions = {
do_copy; }
size = 1; default_action = do_copy(); } apply { mycopy.apply(); phy_forward.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.tcp.dstPort:
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 { } } V1Switch(ParserImpl(),
verifyChecksum(), ingress(), egress(), computeChecksum(), DeparserImpl())
main; |
[cmd.txt]
table_add phy_forward forward 1 => 2 table_add phy_forward forward 2 => 1 mirroring_add 250 3 |
[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 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)
host1 = self.addHost('h1', mac =
'00:00:00:00:00:01')
host2 = self.addHost('h2', mac =
'00:00:00:00:00:02')
host3 = self.addHost('h3', mac =
'00:00:00:00:00:03') self.addLink(host1, switch1, port1 = 0, port2 = 1) self.addLink(host2, switch1, port1 = 0, port2 = 2) self.addLink(host3, switch1, port1 = 0, port2 = 3) 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) net.start() h3=net.get('h3') Intf('eth10',
node=h3) 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() |
[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") |
[cmd_add.py]
import os os.system('sudo /home/vagrant/behavioral-model/targets/simple_switch/simple_switch_CLI --thrift-port=9090 < cmd.txt') |
[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] Note: 192.168.25.180 is the IP address of eth0 in my VM.
package main import ( "flag" "fmt" "os" "os/exec"
"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 ) 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) 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=="HTTP"
{
ip,
_ := ipLayer.(*layers.IPv4)
tcp, _ := tcpLayer.(*layers.TCP) a
:= fmt.Sprintf("echo \"table_add filter mydrop %s %d
=>\" | sudo
/home/vagrant/behavioral-model/targets/simple_switch/simple_switch_CLI
--thrift-ip=192.168.25.180 --thrift-port=9090",
ip.DstIP, tcp.DstPort)
fmt.Println(a)
out,err := exec.Command("bash","-c",
a).Output()
if err != nil {
fmt.Printf("%s", err)
}
output :=
string(out[:]) fmt.Println(output)
} } } |
[Execution]
Check whether there are two NICs in your VM. The IP in my VM is 192.168.25.180.
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 h3, use the dhclient eth10 to get the IP address for eth10.
Run example_app
Ping test.
First HTTP Test
Second HTTP Test (We can see that h1 cannot get any response from h2)
Dr. Chih-Heng Ke (smallko@gmail.com)
Department of Computer Science and
Information Engineering,
National Quemoy
University, Kinmen, Taiwan.