Connection tracking tutorial
This tutorial will cover the use of the OVS connection tracking system (aka conntrack) in conjunction with Faucet.
We will explore using the conntrack module to implement:
Stateful firewall rules via conntrack ACLs
Source Network Address Translation (sNAT)
Prerequisites
A basic understanding of OVS connection tracking concepts. The OVS Conntrack Tutorial is a good starting point.
A good understanding of the previous tutorial topics (ACL tutorial, VLAN tutorial, Routing tutorial)
Install Faucet - Package installation steps 1 & 2
Install Open vSwitch - Connect your first datapath steps 1 & 2
Install the conntrack command line utility
sudo apt-get install conntrack
Useful Bash Functions - Copy and paste the following definitions into your bash terminal, or to make them persistent between sessions add them to the bottom of your .bashrc and run ‘source .bashrc’.
# Run command inside network namespace as_ns () { NAME=$1 NETNS=faucet-${NAME} shift sudo ip netns exec ${NETNS} $@ }
# Create network namespace create_ns () { NAME=$1 IP=$2 NETNS=faucet-${NAME} sudo ip netns add ${NETNS} sudo ip link add dev veth-${NAME} type veth peer name veth0 netns ${NETNS} sudo ip link set dev veth-${NAME} up as_ns ${NAME} ip link set dev lo up [ -n "${IP}" ] && as_ns ${NAME} ip addr add dev veth0 ${IP} as_ns ${NAME} ip link set dev veth0 up }
# Clean up namespaces, bridges and processes created during faucet tutorial cleanup () { for NETNS in $(sudo ip netns list | grep "faucet-" | awk '{print $1}'); do [ -n "${NETNS}" ] || continue NAME=${NETNS#faucet-} if [ -f "/run/dhclient-${NAME}.pid" ]; then # Stop dhclient sudo pkill -F "/run/dhclient-${NAME}.pid" fi if [ -f "/run/iperf3-${NAME}.pid" ]; then # Stop iperf3 sudo pkill -F "/run/iperf3-${NAME}.pid" fi if [ -f "/run/bird-${NAME}.pid" ]; then # Stop bird sudo pkill -F "/run/bird-${NAME}.pid" fi # Remove netns and veth pair sudo ip link delete veth-${NAME} sudo ip netns delete ${NETNS} done for isl in $(ip -o link show | awk -F': ' '{print $2}' | grep -oE "^l-br[0-9](_[0-9]*)?-br[0-9](_[0-9]*)?"); do # Delete inter-switch links sudo ip link delete dev $isl 2>/dev/null || true done for DNSMASQ in /run/dnsmasq-vlan*.pid; do [ -e "${DNSMASQ}" ] || continue # Stop dnsmasq sudo pkill -F "${DNSMASQ}" done # Remove faucet dataplane connection sudo ip link delete veth-faucet 2>/dev/null || true # Remove openvswitch bridges sudo ovs-vsctl --if-exists del-br br0 sudo ovs-vsctl --if-exists del-br br1 sudo ovs-vsctl --if-exists del-br br2 sudo ovs-vsctl --if-exists del-br br3 }
Run the cleanup script to remove old namespaces and switches:
cleanup
Stateful Firewall Rules
Let’s start with a single switch connected to two hosts in two different VLANs, reusing a setup from the Routing tutorial.
create_ns host1 10.0.0.1/24
create_ns host2 10.0.1.2/24
sudo ovs-vsctl add-br br0 \
-- set bridge br0 other-config:datapath-id=0000000000000001 \
-- set bridge br0 other-config:disable-in-band=true \
-- set bridge br0 fail_mode=secure \
-- add-port br0 veth-host1 -- set interface veth-host1 ofport_request=1 \
-- add-port br0 veth-host2 -- set interface veth-host2 ofport_request=2 \
-- set-controller br0 tcp:127.0.0.1:6653
We begin with the following Faucet configuration.
vlans:
vlan100:
vid: 100
faucet_vips: ["10.0.0.254/24"] # Faucet's virtual IP address for vlan100
faucet_mac: "00:00:00:00:00:11"
vlan200:
vid: 200
faucet_vips: ["10.0.1.254/24"] # Faucet's virtual IP address for vlan200
faucet_mac: "00:00:00:00:00:22"
dps:
sw1:
dp_id: 0x1
hardware: "Open vSwitch"
interfaces:
1:
name: "host1"
description: "host1 network namespace"
native_vlan: vlan100
2:
name: "host2"
description: "host2 network namespace"
native_vlan: vlan200
routers:
router-1: # Router name
vlans: [vlan100, vlan200] # Names of vlans to allow routing between
Now let’s signal Faucet to reload the configuration file, which simply enables and permits the two hosts to communicate.
sudo systemctl reload faucet
Add a default route on each host to set the gateway to the value we used for
faucet_vips
above.
as_ns host1 ip route add default via 10.0.0.254 dev veth0
as_ns host2 ip route add default via 10.0.1.254 dev veth0
By default and without any ACLs, traffic is now permitted in either direction between both hosts. We can show that by doing the following:
as_ns host1 ping 10.0.1.2
as_ns host2 ping 10.0.0.1
In this section we will be using Faucet as a gateway and stateful firewall between our two hosts. In this case, host1 is permitted to initiate connections to host2, but not vice-versa. We will implement stateful firewall rules to track egress connections from host1 to host2, allowing return packets from host2 to host1, but blocking new connections initiated by host2 to host1. This is accomplished by using a new ACL action option that we haven’t seen before.
ct |
Used to apply connection tracking to the specified flow. |
We will now restrict communication between the two hosts by adding a connection tracking ACL that permits egress communication from host1 to host2, but not vice-versa. Add the following ACLs to the configuration file.
acls:
conntrack_fw:
# Permit all ARP traffic such that hosts can resolve one another's MACs
- rule:
eth_type: 0x0806 # arp
actions:
allow: True
# Begin tracking ALL untracked IPv4 connections
- rule:
eth_type: 0x0800 # ipv4
ct_state: 0/0x20 # match -trk (untracked)
actions:
# Re-inject the tracked packet into the OpenFlow pipeline, containing
# additional connection metadata, to default table 0. The tracked packet
# is again evaluated by Faucet ACLs in table 0. The original, untracked
# packet is effectively dropped.
ct:
zone: 10 # arbitrary conntrack zone ID to match against later
table: 0
# Commit NEW IPv4 connections from host1 to host2
- rule:
eth_type: 0x0800 # ipv4
ipv4_src: 10.0.0.1
ipv4_dst: 10.0.1.2
ct_state: 0x21/0x21 # match +new - packets to establish a new connection
actions:
# Commit the connection to the connection tracking module which will be
# stored beyond the lifetime of packet in the pipeline.
ct:
zone: 10 # the same conntrack zone ID as above
flags: 1 # "commit" the new connection
table: 1 # implicit "allow" new connection packet(s) via faucet table 1
# Allow packets in either direction from existing connections initiated by
# host1 only
- rule:
eth_type: 0x0800 # ipv4
ct_zone: 10 # match packets associated with our conntrack zone ID
ct_state: 0x22/0x22 # match +est - packets in an established connection
actions:
allow: True
# Block all unwanted packets and new connections from host2 to host1
- rule:
eth_type: 0x0800 # ipv4
ipv4_src: 10.0.1.2
ipv4_dst: 10.0.0.1
actions:
allow: False
Be sure to also apply the new ACL to both ports in the data plane.
dps:
sw1:
1:
...
acls_in:
- conntrack_fw
2:
...
acls_in:
- conntrack_fw
Reload Faucet to apply the new configuration.
sudo systemctl reload faucet
The new conntrack related ACLs should have been added:
ovs-ofctl dump-flows br0 -O OpenFlow13 | grep =ct
We can debug how OVS interfaces with the conntrack module to deal with the tracked packet(s).
ovs-appctl ofproto/trace br0 in_port=1,tcp,nw_src=10.0.0.1,nw_dst=10.0.1.2
Our ping from host1 to host2 should continue to work, establishing an entry in the connection tracker.
as_ns host1 ping 10.0.1.2
An entry for the ping should now be visible in the kernel’s connection tracking table.
sudo conntrack -L | grep 10.0.1.2
However, ping and any other unrelated traffic from host2 to host1 is now denied.
ovs-appctl ofproto/trace br0 in_port=1,tcp,nw_src=10.0.1.2,nw_dst=10.0.0.1
as_ns host2 ping 10.0.0.1
More-complex ACL rules can be created to build out an entire stateful firewall. It is important to remember that ALL packets initially have a ct_state of -trk (untracked), and must be sent to the connection tracking module via a ct action. Packets then pass through the ACL(s) again, whereupon the ct_state and other fields can be matched against to achieve the desired behavior. In order to track (i.e. “remember”) a connection, a packet from the connection must first be “committed” to the conntrack module. Generally, it is best to do this for “new” egress connections in the permitted direction, which allows subsequent ACLs to match against packets for established (“est”) connections in either direction. The Connection Tracking Fields section of the ovs-fields(7) man page is a helpful reference in understanding what the various connection states mean.
Network Address Translation (NAT)
The connection tracking integration also allows changing the source/destination IP and/or ports of a given connection. This can be used to implement one-to-one or many-to-one sNAT (source-NAT) behavior seen in traditional NAT gateways.
We can extend our Stateful Firewall Rules ACL example to sNAT connections from host1 to host2. We will NAT host1’s IP to the Faucet VIP on its network, which is its gateway (default route). Connections observed from host2 will appear to be initiated by the Faucet VIP. This is accomplished by extending the ct action to include a nat configuration field.
NAT configuration key/values are based on the related Ryu configuration options.
The Firewalling Actions section of the ovs-actions(7) man page is a helpful reference to understand how the NAT action behaves.
Now we augment the ACLs from the previous example with an additional nat option, replacing them with the following:
acls:
conntrack_fw:
- rule:
eth_type: 0x0806 # arp
actions:
allow: True
- rule:
eth_type: 0x0800 # ipv4
ct_state: 0/0x20 # match -trk (untracked)
actions:
ct:
zone: 10
table: 0
- rule:
eth_type: 0x0800 # ipv4
ipv4_src: 10.0.0.1
ipv4_dst: 10.0.1.2
ct_state: 0x21/0x21 # match +new - packets to establish a new connection
actions:
ct:
zone: 10
flags: 1 # "commit" the new connection
table: 1
# sNAT the connection to the faucet VIP
nat:
flags: 1
range_ipv4_min: 10.0.0.254
range_ipv4_max: 10.0.0.254
- rule:
eth_type: 0x0800 # ipv4
ct_zone: 10
ct_state: 0x22/0x22 # match +est - packets in an established connection
actions:
ct:
zone: 10
flags: 1 # NAT must include "commit" - this is a NO-OP for existing connections
table: 1
# sNAT the packets in an existing connection appropriately according to their direction
nat:
flags: 1
- rule:
eth_type: 0x0800 # ipv4
ipv4_src: 10.0.1.2
ipv4_dst: 10.0.0.1
actions:
allow: False
Reload Faucet to apply the new configuration.
sudo systemctl reload faucet
We can now see how OVS + conntrack will NAT the packets:
ovs-appctl ofproto/trace br0 in_port=1,tcp,nw_src=10.0.0.1,nw_dst=10.0.1.2
Our ping from host1 to host2 should continue to work, establishing an entry in the connection tracker. This time, however, host1’s source IP of 10.0.0.1 gets NATed to the Faucet VIP of 10.0.0.254.
as_ns host1 ping 10.0.1.2
tcpdump -n -e -ttt -i veth-host2 host 10.0.0.254
sudo conntrack -L | grep 10.0.0.254