Sunday, July 31, 2016

Automatically documenting/generating OF pipelines

FAUCET now has code that automatically enforces correct documentation, of what OpenFlow matches are used in what table. Today, it looks like this (see end of this post).

This is very useful, because it enables you to correctly configure hardware that needs to know what matches are used in what table, in advance. It also lets you optimize the pipeline (for example, by removing matches from tables that don't need them, which can increase forwarding performance).

FAUCET could also support a P4 switch running an OF bridge, if that bridge supported these matches in these tables.

FAUCET's pipeline could use some further optimization (particularly of eth_src_table, which is quite "wide" - it could match a lot of things). This is ongoing.

        self.TABLE_MATCH_TYPES = {
            self.dp.vlan_table: (
                'in_port', 'vlan_vid', 'eth_src', 'eth_dst', 'eth_type'),
            # TODO: eth_src_table matches too many things. It should
            # be split further into two tables for IPv4/IPv6 entries.
            self.dp.eth_src_table: (
                'in_port', 'vlan_vid', 'eth_src', 'eth_dst', 'eth_type',
                'ip_proto',
                'icmpv6_type', 'ipv6_nd_target',
                'arp_tpa', 'ipv4_src'),
            self.dp.ipv4_fib_table: (
                'vlan_vid', 'eth_type', 'ip_proto',
                'ipv4_src', 'ipv4_dst'),
            self.dp.ipv6_fib_table: (
                'vlan_vid', 'eth_type', 'ip_proto',
                'icmpv6_type', 'ipv6_dst'),
            self.dp.eth_dst_table: (
                'vlan_vid', 'eth_dst'),
            self.dp.flood_table: (
                'vlan_vid', 'eth_dst'),
        }


Monday, July 18, 2016

Policy based forwarding with FAUCET

Sometimes, you want certain traffic to be taken out of the dataplane, and entirely diverted to another system (for example, you want to redirect all DHCP request broadcasts to only one DHCP server, or you want a DDoS system to perform deeper analysis).



FAUCET allows you to configure an ACL to divert any packet that can be matched by OpenFlow, to a port, and optionally have the destination address rewritten.


acls:
    1:
        - rule:
            dl_dst: "01:02:03:04:05:06"
            actions:
                output:
                    dl_dst: "06:06:06:06:06:06"
                    port: 2


In this example, any traffic with an Ethernet destination of 01:02:03:04:05:06, will be intercepted, will have its destination address rewritten to be 06:06:06:06:06:06, and then output port 2.

The match expression can match anything OpenFlow can; for example, you could match source or destination IP address.

Here's another example that matches DHCP requests and redirects them to port 1:


acls:
    1:
        - rule:
            dl_dst: "ff:ff:ff:ff:ff:ff"
            dl_type: 0x800
            nw_proto: 17
            nw_src: "0.0.0.0"
            nw_dst: "255.255.255.255"
            tp_src: 68
            tp_dst: 67
            actions:
                output:
                    port: 1


And output from an Allied Telesis switch that shows it working:

awplus#show openflow rules |include table_id=1
table_id=1, duration=69s, n_packets=1, n_bytes=377, priority=9099,udp,in_port=23,dl_dst=ff:ff:ff:ff:ff:ff,nw
_src=0.0.0.0,nw_dst=255.255.255.255,tp_src=68,tp_dst=67,actions=output:1
table_id=1, duration=69s, n_packets=8590, n_bytes=11026081, priority=9098,in_port=23,actions=goto_table:2
table_id=1, duration=69s, n_packets=0, n_bytes=0, priority=0,actions=drop

Routing protocol offload with FAUCET (FAUCET does BGP)

As with protocols like 802.1x, it is possible to offload routing protocols with FAUCET.

FAUCET now speaks basic BGP (it can both advertise its own IPv4 and IPv6 routes, and it can learn BGP routes). This this is sufficient to communicate with a more sophisticated route processor, like Quagga or BIRD - the external processor does the filtering and processing of community strings, etc - and exports the resulting RIB to FAUCET, which simply uses it for a FIB.



This implies further that FAUCET can now effectively speak any routing protocol that the external routing processor can speak, because the external processor has only to translate to simple BGP. 

Since FAUCET can do IPv4 and IPv6 routing in the dataplane itself, including nexthop resolution, dynamic routing can be done by the controlled OpenFlow switch. 

Tuesday, July 12, 2016

802.1x authentication on FAUCET (NFV offload of authentication).

In this post, we will discuss how to add 802.1x to any FAUCET controlled switch. This is a good example of how SDN can add functionality to a switch with just a controller upgrade - no changes to the dataplane are required.

802.1x is a standard for network authentication - whether a given device should be allowed access to the network (whether WiFi or wired).

Here's an overview of how it works with FAUCET.

  • A host, connected to the Ethernet switch, sends 802.1x authentication request packets to the switch. The switch passes them on to the controller/NFV's offload port.
  • A process, hostapd receives these packets and facilitates an authentication conversation with a RADIUS server (see here for an extremely detailed explanation).
  • Whether successful or unsuccessful, hostapd notifies another process on the same host, hostapd_cli.
  • A script, hostapd_trigger.py (see below) monitors hostapd_cli, and the FAUCET controller's log file. It correlates the MAC address of the host with the port the host is connected to on the switch, and then initiates the desired action (for example, opening a firewall port so the host can access the Internet - if successfully authenticated).



This implementation is a proof of concept of implementing the authentication entirely within the dataplane. The FAUCET controller does not participate or understand the 802.1x exchange and does not need to. Other services, like DNS and DHCP could also be run on the same host, in the same way. The trigger script can perform any action, including, modifying the FAUCET config file itself and HUPing the controller (for example, to add or remove an OpenFlow ACL to the host's port to authorize it to access the network).

For a more advanced solution including an administrative GUI, a system like PacketFence could be integrated. For example, PacketFence could also change FAUCET's config to put a problematic host in a quarantine VLAN.

There are some features being added to FAUCET to support better integration. For example, this proof of concept uses the FAUCET log file, but FAUCET will expose what it knows programmatically (specifically, what MAC addresses have been learned on what ports). There are also features to force all 802.1x, etc traffic to the offload port only, and more efficiently add or remove an ACL/VLAN from a port when requested.

hostapd takes a minimal configuration file, instructing it to listen on a port, communicate with a RADIUS server, and notify an external process of events:

interface=eth0.2001
driver=wired
logger_stdout=-1
logger_stdout_level=1
ieee8021x=1
eap_reauth_period=3600
eap_server=0
eapol_version=1
own_ip_addr=127.0.0.1
auth_server_addr=127.0.0.1
auth_server_port=1812
auth_server_shared_secret=SECRET

ctrl_interface=/var/run/hostapd

In this case it will listen on eth0.2001, which is a tagged port for FAUCET VLAN 2001. We will use a RADIUS server running on the same host as the controller (how to configure the RADIUS server can found above).

The trigger script follows. While a proof of concept it demonstrates how to correlate the host's MAC with a port and then call an external system (future versions of FAUCET will support proper programmatic access, as above).

#!/usr/bin/python

import fcntl
import os
import re
import subprocess
import time

PORTS = [5]
mac_to_port = {}
mac_authenticated = {}

faucet_log = open('/var/log/faucet/faucet.log')
fcntl.fcntl(faucet_log.fileno(), fcntl.F_SETFL, os.O_NONBLOCK)
hostapd_cli = subprocess.Popen(
    ['/usr/local/sbin/hostapd_cli', '-p', '/var/run/hostapd'],
    stdin=subprocess.PIPE,
    stdout=subprocess.PIPE,
    shell=True)
fcntl.fcntl(hostapd_cli.stdout, fcntl.F_SETFL, os.O_NONBLOCK)

while True:
   log = faucet_log.readline()
   hostapi_log = ''
   try:
       hostapi_log = hostapd_cli.stdout.readline()
   except IOError:
       pass
   status_change = False
   # did we learn a MAC on a port?
   if log:
       m = re.search('src:(\S+) in_port:(\S+)', log)
       if m:
           mac = m.group(1)
           in_port = int(m.group(2))
           if in_port in PORTS:
               if mac not in mac_to_port or mac_to_port[mac] != in_port:
                   mac_to_port[mac] = in_port
                   print 'mac %s is on port %u' % (mac, in_port)
                   status_change = True
       if not status_change:
           continue
   # was a MAC authenticated or de-authenticated
   if hostapi_log:
       connected_m = re.search('AP-STA-CONNECTED (\S+)', hostapi_log)
       disconnected_m = re.search('AP-STA-DISCONNECTED (\S+)', hostapi_log)
       if connected_m:
           mac = connected_m.group(1)
           mac_authenticated[mac] = True
           print 'mac %s authenticated' % mac
           status_change = True
       elif disconnected_m:
           mac = disconnected_m.group(1)
           mac_authenticated[mac] = False
           print 'mac %s not authenticated' % mac
           status_change = True
   if status_change:
       for mac, in_port in mac_to_port.iteritems():
           print mac, in_port,
           if mac in mac_authenticated:
               print mac_authenticated[mac]
               ## ADD TRIGGER TO EXTERNAL SYSTEM HERE
           else:
               print 'unknown'
   else:
       time.sleep(0.5)
       hostapd_cli.stdin.write('\n')