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.

Inter-VLAN routing diagram
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.

/etc/faucet/faucet.yaml
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.

/etc/faucet/faucet.yaml
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.

/etc/faucet/faucet.yaml
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:

/etc/faucet/faucet.yaml
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