diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 07:10:00 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 07:10:00 +0000 |
commit | 4ba2b326284765e942044db13a7f0dae702bec93 (patch) | |
tree | cbdfaec33eed4f3a970c54cd10e8ddfe3003b3b1 /xdp-filter | |
parent | Initial commit. (diff) | |
download | xdp-tools-4ba2b326284765e942044db13a7f0dae702bec93.tar.xz xdp-tools-4ba2b326284765e942044db13a7f0dae702bec93.zip |
Adding upstream version 1.3.1.upstream/1.3.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'xdp-filter')
-rw-r--r-- | xdp-filter/.gitignore | 3 | ||||
-rw-r--r-- | xdp-filter/Makefile | 24 | ||||
-rw-r--r-- | xdp-filter/README.org | 294 | ||||
-rw-r--r-- | xdp-filter/common_kern_user.h | 28 | ||||
-rw-r--r-- | xdp-filter/extract_features.sh | 37 | ||||
-rw-r--r-- | xdp-filter/tests/common.py | 56 | ||||
-rw-r--r-- | xdp-filter/tests/test-xdp-filter.sh | 391 | ||||
-rw-r--r-- | xdp-filter/tests/test_basic.py | 279 | ||||
-rw-r--r-- | xdp-filter/tests/test_slow.py | 127 | ||||
-rw-r--r-- | xdp-filter/xdp-filter.8 | 371 | ||||
-rw-r--r-- | xdp-filter/xdp-filter.c | 1097 | ||||
-rw-r--r-- | xdp-filter/xdpfilt_alw_all.c | 10 | ||||
-rw-r--r-- | xdp-filter/xdpfilt_alw_eth.c | 10 | ||||
-rw-r--r-- | xdp-filter/xdpfilt_alw_ip.c | 10 | ||||
-rw-r--r-- | xdp-filter/xdpfilt_alw_tcp.c | 10 | ||||
-rw-r--r-- | xdp-filter/xdpfilt_alw_udp.c | 10 | ||||
-rw-r--r-- | xdp-filter/xdpfilt_dny_all.c | 10 | ||||
-rw-r--r-- | xdp-filter/xdpfilt_dny_eth.c | 10 | ||||
-rw-r--r-- | xdp-filter/xdpfilt_dny_ip.c | 10 | ||||
-rw-r--r-- | xdp-filter/xdpfilt_dny_tcp.c | 10 | ||||
-rw-r--r-- | xdp-filter/xdpfilt_dny_udp.c | 10 | ||||
-rw-r--r-- | xdp-filter/xdpfilt_prog.h | 257 |
22 files changed, 3064 insertions, 0 deletions
diff --git a/xdp-filter/.gitignore b/xdp-filter/.gitignore new file mode 100644 index 0000000..87a8a62 --- /dev/null +++ b/xdp-filter/.gitignore @@ -0,0 +1,3 @@ +*.ll +xdp-filter +prog_features.h diff --git a/xdp-filter/Makefile b/xdp-filter/Makefile new file mode 100644 index 0000000..0d54e8e --- /dev/null +++ b/xdp-filter/Makefile @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: (GPL-2.0 OR BSD-2-Clause) + +XDP_TARGETS := xdpfilt_dny_udp xdpfilt_dny_tcp xdpfilt_dny_ip \ + xdpfilt_dny_eth xdpfilt_dny_all \ + xdpfilt_alw_udp xdpfilt_alw_tcp xdpfilt_alw_ip \ + xdpfilt_alw_eth xdpfilt_alw_all + +TOOL_NAME := xdp-filter +USER_TARGETS := xdp-filter +EXTRA_DEPS := xdpfilt_prog.h +MAN_PAGE := xdp-filter.8 +TEST_FILE := tests/test-xdp-filter.sh +TEST_FILE_DEPS := $(wildcard tests/*.py) + +USER_GEN := prog_features.h +EXTRA_USER_DEPS := $(USER_GEN) + +LIB_DIR = ../lib + +include $(LIB_DIR)/common.mk + +prog_features.h: ${XDP_TARGETS:=.o} extract_features.sh + $(Q)sh extract_features.sh $^ > $@ || ( ret=$$?; rm -f $@; exit $$ret ) + diff --git a/xdp-filter/README.org b/xdp-filter/README.org new file mode 100644 index 0000000..3a4df60 --- /dev/null +++ b/xdp-filter/README.org @@ -0,0 +1,294 @@ +#+EXPORT_FILE_NAME: xdp-filter +#+TITLE: xdp-filter +#+MAN_CLASS_OPTIONS: :section-id "8\" \"DATE\" \"VERSION\" \"A simple XDP-powered packet filter" +# This file serves both as a README on github, and as the source for the man +# page; the latter through the org-mode man page export support. +# . +# To export the man page, simply use the org-mode exporter; (require 'ox-man) if +# it's not available. There's also a Makefile rule to export it. + +* xdp-filter - a simple XDP-powered packet filter + +XDP-filter is a packet filtering utility powered by XDP. It is deliberately +simple and so does not have the same matching capabilities as, e.g., netfilter. +Instead, thanks to XDP, it can achieve very high drop rates: tens of millions of +packets per second on a single CPU core. + +** Running xdp-filter +The syntax for running xdp-filter is: + +#+begin_src sh +xdp-filter COMMAND [options] + +Where COMMAND can be one of: + load - load xdp-filter on an interface + unload - unload xdp-filter from an interface + port - add a port to the filter list + ip - add an IP address to the filter list + ether - add an Ethernet MAC address to the filter list + status - show current xdp-filter status + poll - poll statistics output + help - show the list of available commands +#+end_src + +Each command, and its options are explained below. Or use =xdp-filter COMMAND +--help= to see the options for each command. + +* The LOAD command +To use =xdp-filter=, it must first be loaded onto an interface. This is +accomplished with the =load= command, which takes the name of the interface as a +parameter, and optionally allows specifying the features that should be +included. By default all features are loaded, but de-selecting some features can +speed up the packet matching, and increase performance by a substantial amount. + +The syntax for the =load= command is: + +=xdp-filter load [options] <ifname>= + +Where =<ifname>= is the name of the interface to load =xdp-filter= onto, and +must be specified. The supported options are: + +** -m, --mode <mode> +Specifies which mode to load the XDP program to be loaded in. The valid values +are 'native', which is the default in-driver XDP mode, 'skb', which causes the +so-called /skb mode/ (also known as /generic XDP/) to be used, or 'hw' which +causes the program to be offloaded to the hardware. + +** -p, --policy <policy> +This sets the policy =xdp-filter= applies to packets *not* matched by any of the +filter rules. The default is /allow/, in which packets not matching any rules +are allowed to pass. The other option is /deny/, in which *all* packets are +dropped *except* those matched by the filter options. + +=xdp-filter= cannot be loaded simultaneously in /deny/ and /allow/ policy modes +on the system. Note that loading =xdp-filter= in /deny/ mode will drop all +traffic on the interface until suitable allow rules are installed, so some care +is needed to avoid being locked out of a remote system. + +** -f, --features <feats> +Use this option to select which features to include when loaded =xdp-filter=. +The default is to load all available features. So select individual features +specify one or more of these: + + * *tcp*: Support filtering on TCP port number + * *udp*: Support filtering on UDP port number + * *ipv6*: Support filtering on IPv6 addresses + * *ipv4*: Support filtering on IPv4 addresses + * *ethernet*: Support filtering on Ethernet MAC addresses + +Specify multiple features by separating them with a comma. E.g.: =tcp,udp,ipv6=. + +** -v, --verbose +Enable debug logging. Specify twice for even more verbosity. + +** -h, --help +Display a summary of the available options + +* The UNLOAD command +The =unload= command unloads =xdp-filter= from one (or all) interfaces, and +cleans up the program state. + +The syntax for the =load= command is: + +=xdp-filter unload [options] <ifname>= + +Where =<ifname>= is the name of the interface to unload =xdp-filter= from, and +must be specified unless the *--all* option is used. The supported options are: + +** -a, --all +Specify this option to remove =xdp-filter= from all interfaces it was loaded +onto. If this option is specified, no =<ifname>= is needed. + +This option can also be used to clean up all =xdp-filter= state if the XDP +program(s) were unloaded by other means. + +** -k, --keep-maps +Specify this option to prevent =xdp-filter= from clearing its map state. By +default, all BPF maps no longer needed by any loaded program are removed. +However, this will also remove the contents of the maps (the filtering rules), +so this option can be used to keep the maps around so the rules persist until +=xdp-filter= is loaded again. + +** -v, --verbose +Enable debug logging. Specify twice for even more verbosity. + +** -h, --help +Display a summary of the available options + +* The PORT command +Use the =port= command to add a TCP or UDP port to the =xdp-filter= match list. +For this to work, =xdp-filter= must be loaded with either the *udp* or the *tcp* +feature (or both) on at least one interface. + +The syntax for the =port= command is: + +=xdp-filter port [options] <port>= + +Where =<port>= is the port number to add (or remove if the *--remove* is +specified). The supported options are: + +** -r, --remove +Remove the port instead of adding it. + +** -m, --mode <mode> +Select filtering mode. Valid options are *src* and *dst*, both of which may be +specified as =src,dst=. If *src* is specified, the port number will added as a +/source port/ match, while if *dst* is specified, the port number will be added +as a /destination port/ match. If both are specified, a packet will be matched +if *either* its source or destination port is the specified port number. + +** -p, --proto <proto> +Specify one (or both) of *udp* and/or *tcp* to match UDP or TCP ports, +respectively. + +** -s, --status +If this option is specified, the current list of matched ports will be printed +after inserting the port number. Otherwise, nothing will be printed. + +** -v, --verbose +Enable debug logging. Specify twice for even more verbosity. + +** -h, --help +Display a summary of the available options + + +* The IP command +Use the =ip= command to add an IPv6 or an IPv4 address to the =xdp-filter= match +list. + +The syntax for the =ip= command is: + +=xdp-filter ip [options] <ip>= + +Where =<ip>= is the IP address to add (or remove if the *--remove* is +specified). Either IPv4 or IPv6 addresses can be specified, but =xdp-filter= +must be loaded with the corresponding features (*ipv4* and *ipv6*, +respectively). The supported options are: + +** -r, --remove +Remove the IP address instead of adding it. + +** -m, --mode <mode> +Select filtering mode. Valid options are *src* and *dst*, both of which may be +specified as =src,dst=. If *src* is specified, the IP address will added as a +/source IP/ match, while if *dst* is specified, the IP address will be added +as a /destination IP/ match. If both are specified, a packet will be matched +if *either* its source or destination IP is the specified IP address. + +** -s, --status +If this option is specified, the current list of matched ips will be printed +after inserting the IP address. Otherwise, nothing will be printed. + +** -v, --verbose +Enable debug logging. Specify twice for even more verbosity. + +** -h, --help +Display a summary of the available options + +* The ETHER command +Use the =ether= command to add an Ethernet MAC address to the =xdp-filter= match +list. For this to work, =xdp-filter= must be loaded with either the *ethernet* +feature on at least one interface. + +The syntax for the =ether= command is: + +=xdp-filter ether [options] <addr>= + +Where =<addr>= is the MAC address to add (or remove if the *--remove* is +specified). The supported options are: + +** -r, --remove +Remove the MAC address instead of adding it. + +** -m, --mode <mode> +Select filtering mode. Valid options are *src* and *dst*, both of which may be +specified as =src,dst=. If *src* is specified, the MAC address will added as a +/source MAC/ match, while if *dst* is specified, the MAC address will be added +as a /destination MAC/ match. If both are specified, a packet will be matched +if *either* its source or destination MAC is the specified MAC address. + +** -s, --status +If this option is specified, the current list of matched ips will be printed +after inserting the MAC address. Otherwise, nothing will be printed. + +** -v, --verbose +Enable debug logging. Specify twice for even more verbosity. + +** -h, --help +Display a summary of the available options + +* The STATUS command +The =status= command prints the current status of =xdp-filter=: Which interfaces +it is loaded on, the current list of rules, and some statistics for how many +packets have been processed in total, and how many times each rule has been hit. + +The syntax for the =status= command is: + +=xdp-filter status [options]= + +Where the supported options are: + +** -v, --verbose +Enable debug logging. Specify twice for even more verbosity. + +** -h, --help +Display a summary of the available options + +* The POLL command +The =poll= command periodically polls the =xdp-filter= statistics map and prints +out the total number of packets and bytes processed by =xdp-filter=, as well as +the number in the last polling interval, converted to packets (and bytes) per +second. This can be used to inspect the performance of =xdp-filter=, and to +compare the performance of the different feature sets selectable by the =load= +parameter. + +The syntax for the =poll= command is: + +=xdp-filter poll [options]= + +Where the supported options are: + +** -i, --interval <interval> +The polling interval, in milliseconds. Defaults to 1000 (1 second). + +** -v, --verbose +Enable debug logging. Specify twice for even more verbosity. + +** -h, --help +Display a summary of the available options + +* Examples + +To filter all packets arriving on port 80 on eth0, issue the +following commands: + +#+begin_src sh +# xdp-filter load eth0 -f tcp,udp +# xdp-filter port 80 +#+end_src + +To filter all packets *except* those from IP address fc00:dead:cafe::1 issue the +following commands (careful, this can lock you out of remote access!): + +#+begin_src sh +# xdp-filter load eth0 -f ipv6 -p deny +# xdp-filter ip fc00:dead:cafe::1 -m src +#+end_src + +To allow packets from *either* IP fc00:dead:cafe::1 *or* arriving on port 22, +issue the following (careful, this can lock you out of remote access!): + +#+begin_src sh +# xdp-filter load eth0 -f ipv6,tcp -p deny +# xdp-filter port 22 +# xdp-filter ip fc00:dead:cafe::1 -m src +#+end_src + +* BUGS + +Please report any bugs on Github: https://github.com/xdp-project/xdp-tools/issues + +* AUTHOR + +xdp-filter was written by Toke Høiland-Jørgensen and Jesper Dangaard Brouer. +This man page was written by Toke Høiland-Jørgensen. diff --git a/xdp-filter/common_kern_user.h b/xdp-filter/common_kern_user.h new file mode 100644 index 0000000..a7da1af --- /dev/null +++ b/xdp-filter/common_kern_user.h @@ -0,0 +1,28 @@ +#ifndef COMMON_KERN_USER_H +#define COMMON_KERN_USER_H + +#define FEAT_TCP (1<<0) +#define FEAT_UDP (1<<1) +#define FEAT_IPV6 (1<<2) +#define FEAT_IPV4 (1<<3) +#define FEAT_ETHERNET (1<<4) +#define FEAT_ALL (FEAT_TCP|FEAT_UDP|FEAT_IPV6|FEAT_IPV4|FEAT_ETHERNET) +#define FEAT_ALLOW (1<<5) +#define FEAT_DENY (1<<6) + +#define MAP_FLAG_SRC (1<<0) +#define MAP_FLAG_DST (1<<1) +#define MAP_FLAG_TCP (1<<2) +#define MAP_FLAG_UDP (1<<3) +#define MAP_FLAGS (MAP_FLAG_SRC|MAP_FLAG_DST|MAP_FLAG_TCP|MAP_FLAG_UDP) + +#define COUNTER_SHIFT 6 + +#define MAP_NAME_PORTS filter_ports +#define MAP_NAME_IPV4 filter_ipv4 +#define MAP_NAME_IPV6 filter_ipv6 +#define MAP_NAME_ETHERNET filter_ethernet + +#include "xdp/xdp_stats_kern_user.h" + +#endif diff --git a/xdp-filter/extract_features.sh b/xdp-filter/extract_features.sh new file mode 100644 index 0000000..9cfb5e9 --- /dev/null +++ b/xdp-filter/extract_features.sh @@ -0,0 +1,37 @@ +#!/bin/sh + +cat<<EOF +#ifndef PROG_FEATURES_H +#define PROG_FEATURES_H +struct prog_feature { + char *prog_name; + __u32 features; +}; + +static struct prog_feature prog_features[] = { +EOF + +for f in $*; do + featstring=$(readelf -x features $f 2>/dev/null) + [ "$?" -ne "0" ] && continue + + found=0 + for w in $featstring; do + if [ "$w" = "0x00000000" ]; then + found=1 + else + if [ "$found" -eq "1" ]; then + feats=$w + break + fi + fi + done + + echo " {\"$f\", 0x$feats}," +done + +cat<<EOF + {} +}; +#endif +EOF diff --git a/xdp-filter/tests/common.py b/xdp-filter/tests/common.py new file mode 100644 index 0000000..4848f31 --- /dev/null +++ b/xdp-filter/tests/common.py @@ -0,0 +1,56 @@ +import os +import subprocess + +from xdp_test_harness.xdp_case import XDPCase, usingCustomLoader +from xdp_test_harness.utils import XDPFlag + + +XDP_FILTER = os.environ.get("XDP_FILTER", "xdp-filter") + + +def get_mode_string(xdp_mode: XDPFlag): + if xdp_mode == XDPFlag.SKB_MODE: + return "skb" + if xdp_mode == XDPFlag.DRV_MODE: + return "native" + if xdp_mode == XDPFlag.HW_MODE: + return "hw" + return None + + +@usingCustomLoader +class Base(XDPCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls.src_port = 60001 + cls.dst_port = 60002 + cls.to_send = cls.generate_default_packets( + src_port=cls.src_port, dst_port=cls.dst_port) + cls.to_send6 = cls.generate_default_packets( + src_port=cls.src_port, dst_port=cls.dst_port, use_inet6=True) + + def arrived(self, packets, result): + self.assertPacketsIn(packets, result.captured_local) + for i in result.captured_remote: + self.assertPacketContainerEmpty(i) + + def not_arrived(self, packets, result): + self.assertPacketsNotIn(packets, result.captured_local) + for i in result.captured_remote: + self.assertPacketContainerEmpty(i) + + def setUp(self): + subprocess.check_output([ + XDP_FILTER, "load", + self.get_contexts().get_local_main().iface, + "--mode", get_mode_string( + self.get_contexts().get_local_main().xdp_mode + ) + ], stderr=subprocess.STDOUT) + + def tearDown(self): + subprocess.check_output([ + XDP_FILTER, "unload", "--all" + ], stderr=subprocess.STDOUT) diff --git a/xdp-filter/tests/test-xdp-filter.sh b/xdp-filter/tests/test-xdp-filter.sh new file mode 100644 index 0000000..1d24c31 --- /dev/null +++ b/xdp-filter/tests/test-xdp-filter.sh @@ -0,0 +1,391 @@ +XDP_LOADER=${XDP_LOADER:-./xdp-loader} +XDP_FILTER=${XDP_FILTER:-./xdp-filter} +ALL_TESTS="test_load test_print test_output_remove test_ports_allow test_ports_deny test_ipv6_allow test_ipv6_deny test_ipv4_allow test_ipv4_deny test_ether_allow test_ether_deny test_python_basic test_python_slow" + +try_feat() +{ + local output + + feat=$1 + prog=$2 + shift 2 + + output=$($XDP_FILTER load $NS --features $feat "$@" -v 2>&1) + ret=$? + if [ "$ret" -ne "0" ]; then + return $ret + fi + echo "$output" + regex="Found prog '$prog'" + if ! [[ $output =~ $regex ]]; then + echo + echo "Couldn't find '$regex' in output for feat $feat" >&2 + return 1 + fi + check_run $XDP_FILTER unload $NS -v +} + +test_load() +{ + + declare -a FEATS=(tcp udp ipv4 ipv6 ethernet all) + declare -a PROGS_D=(xdpfilt_dny_tcp.o xdpfilt_dny_udp.o xdpfilt_dny_ip.o xdpfilt_dny_ip.o xdpfilt_dny_eth.o xdpfilt_dny_all.o) + declare -a PROGS_A=(xdpfilt_alw_tcp.o xdpfilt_alw_udp.o xdpfilt_alw_ip.o xdpfilt_alw_ip.o xdpfilt_alw_eth.o xdpfilt_alw_all.o) + local len=${#FEATS[@]} + + for (( i=0; i<$len; i++ )); do + if ! try_feat ${FEATS[$i]} ${PROGS_A[$i]}; then + return 1 + fi + if ! try_feat ${FEATS[$i]} ${PROGS_A[$i]} --mode skb; then + return 1 + fi + if ! try_feat ${FEATS[$i]} ${PROGS_D[$i]} --policy deny; then + return 1 + fi + if ! try_feat ${FEATS[$i]} ${PROGS_D[$i]} --policy deny --mode skb; then + return 1 + fi + done + + if [ -d /sys/fs/bpf/xdp-filter ]; then + die "/sys/fs/bpf/xdp-filter still exists!" + fi +} + +check_packet() +{ + local filter="$1" + local command="$2" + local expect="$3" + echo "Checking command '$command' filter '$filter'" + PID=$(start_background tcpdump --immediate-mode -epni $NS "$filter") + echo "Started listener as $PID" + ns_exec bash -c "$command" + sleep 1 + output=$(stop_background $PID) + echo "$output" + + if [[ "$expect" == "OK" ]]; then + regex="[1-9] packets? captured" + else + regex="0 packets captured" + fi + + if [[ "$output" =~ $regex ]]; then + echo "Packet check $expect SUCCESS" + return 0 + else + echo "Packet check $expect FAILURE" + exit 1 + fi +} + +check_port() +{ + local type=$1 + local port=$2 + local expect=$3 + echo "$type port $port $expect" + [[ "$type" == "tcp" ]] && command="nc -w 1 -z $OUTSIDE_IP6 $port" + [[ "$type" == "udp" ]] && command="echo test | nc -w 1 -u $OUTSIDE_IP6 $port" + + check_packet "$type dst port $port" "$command" $expect +} + +test_ports_allow() +{ + local TEST_PORT=10000 + + # default allow mode + check_run $XDP_FILTER load -f udp,tcp $NS -v + check_port tcp $TEST_PORT OK + check_port udp $TEST_PORT OK + check_run $XDP_FILTER port $TEST_PORT -v + check_port tcp $TEST_PORT FAIL + check_port tcp $[TEST_PORT+1] OK + check_port udp $TEST_PORT FAIL + check_port udp $[TEST_PORT+1] OK + check_run $XDP_FILTER port -r $TEST_PORT -v + check_port tcp $TEST_PORT OK + check_port udp $TEST_PORT OK + check_run $XDP_FILTER unload $NS -v +} + +test_ports_deny() +{ + local TEST_PORT=10000 + # default deny mode + check_run $XDP_FILTER load -p deny -f udp,tcp $NS -v + check_port tcp $TEST_PORT FAIL + check_port udp $TEST_PORT FAIL + check_run $XDP_FILTER port $TEST_PORT -v + check_port tcp $TEST_PORT OK + check_port tcp $[TEST_PORT+1] FAIL + check_port udp $TEST_PORT OK + check_port udp $[TEST_PORT+1] FAIL + check_run $XDP_FILTER port -r $TEST_PORT -v + check_port tcp $TEST_PORT FAIL + check_port udp $TEST_PORT FAIL + check_run $XDP_FILTER unload $NS -v +} + +check_ping6() +{ + check_packet "dst $OUTSIDE_IP6" "$PING6 -c 1 $OUTSIDE_IP6" $1 +} + +test_ipv6_allow() +{ + check_ping6 OK + check_run $XDP_FILTER load -f ipv6 $NS -v + check_run $XDP_FILTER ip $OUTSIDE_IP6 + check_ping6 FAIL + check_run $XDP_FILTER ip -r $OUTSIDE_IP6 + check_ping6 OK + check_run $XDP_FILTER ip -m src $INSIDE_IP6 + check_ping6 FAIL + check_run $XDP_FILTER ip -m src -r $INSIDE_IP6 + check_ping6 OK + check_run $XDP_FILTER unload $NS -v +} + +test_ipv6_deny() +{ + check_ping6 OK + check_run $XDP_FILTER load -p deny -f ipv6 $NS -v + check_run $XDP_FILTER ip $OUTSIDE_IP6 + check_ping6 OK + check_run $XDP_FILTER ip -r $OUTSIDE_IP6 + check_ping6 FAIL + check_run $XDP_FILTER ip -m src $INSIDE_IP6 + check_ping6 OK + check_run $XDP_FILTER ip -m src -r $INSIDE_IP6 + check_ping6 FAIL + check_run $XDP_FILTER unload $NS -v +} + +check_ping4() +{ + check_packet "dst $OUTSIDE_IP4" "ping -c 1 $OUTSIDE_IP4" $1 +} + +test_ipv4_allow() +{ + check_ping4 OK + check_run $XDP_FILTER load -f ipv4 $NS -v + check_run $XDP_FILTER ip $OUTSIDE_IP4 + check_ping4 FAIL + check_run $XDP_FILTER ip -r $OUTSIDE_IP4 + check_ping4 OK + check_run $XDP_FILTER ip -m src $INSIDE_IP4 + check_ping4 FAIL + check_run $XDP_FILTER ip -m src -r $INSIDE_IP4 + check_ping4 OK + check_run $XDP_FILTER unload $NS -v +} + +test_ipv4_deny() +{ + check_ping4 OK + check_run $XDP_FILTER load -p deny -f ipv4 $NS -v + check_run $XDP_FILTER ip $OUTSIDE_IP4 + check_ping4 OK + check_run $XDP_FILTER ip -r $OUTSIDE_IP4 + check_ping4 FAIL + check_run $XDP_FILTER ip -m src $INSIDE_IP4 + check_ping4 OK + check_run $XDP_FILTER ip -m src -r $INSIDE_IP4 + check_ping4 FAIL + check_run $XDP_FILTER unload $NS -v +} + +test_ether_allow() +{ + check_ping6 OK + check_run $XDP_FILTER load -f ethernet $NS -v + check_run $XDP_FILTER ether $OUTSIDE_MAC + check_ping6 FAIL + check_run $XDP_FILTER ether -r $OUTSIDE_MAC + check_ping6 OK + check_run $XDP_FILTER ether -m src $INSIDE_MAC + check_ping6 FAIL + check_run $XDP_FILTER ether -m src -r $INSIDE_MAC + check_ping6 OK + check_run $XDP_FILTER unload $NS -v +} + +test_ether_deny() +{ + check_ping6 OK + check_run $XDP_FILTER load -p deny -f ethernet $NS -v + check_run $XDP_FILTER ether $OUTSIDE_MAC + check_ping6 OK + check_run $XDP_FILTER ether -r $OUTSIDE_MAC + check_ping6 FAIL + check_run $XDP_FILTER ether -m src $INSIDE_MAC + check_ping6 OK + check_run $XDP_FILTER ether -m src -r $INSIDE_MAC + check_ping6 FAIL + check_run $XDP_FILTER unload $NS -v +} + +check_status() +{ + local match + local output + match="$1" + output=$($XDP_FILTER status) + + if echo "$output" | grep -q $match; then + echo "Output check for $match SUCCESS" + return 0 + else + echo "Output check for $match FAILURE" + echo "Output: $output" + exit 1 + fi +} + +check_status_no_match() +{ + local match + local output + match="$1" + output=$($XDP_FILTER status) + + if echo "$output" | grep -q $match; then + echo "Output check for no $match FAILURE" + echo "Output: $output" + exit 1 + else + echo "Output check for no $match SUCCESS" + return 0 + fi +} + +test_print() +{ + check_run $XDP_FILTER load $NS -v + check_run $XDP_FILTER ether aa:bb:cc:dd:ee:ff + check_status "aa:bb:cc:dd:ee:ff" + check_run $XDP_FILTER ip 1.2.3.4 + check_status "1.2.3.4" + check_run $XDP_FILTER ip aa::bb + check_status "aa::bb" + check_run $XDP_FILTER port 100 + check_status "100.*dst,tcp,udp" + check_run $XDP_FILTER unload $NS -v +} + +check_port_removal_from_all() +{ + local command_options=$1 + local expected_output=$2 + + local TEST_PORT=54321 + + check_run $XDP_FILTER port $TEST_PORT -p tcp,udp -m src,dst + check_status "$TEST_PORT.*src,dst,tcp,udp" + + check_run $XDP_FILTER port $TEST_PORT $command_options -r + if [[ -z "$expected_output" ]]; then + check_status_no_match "$TEST_PORT" + else + check_status "$TEST_PORT.*$expected_output" + fi +} + +test_output_remove() +{ + check_run $XDP_FILTER load $NS -v + + # Remove only one mode/proto. + check_port_removal_from_all "-m src" "dst,tcp,udp" + check_port_removal_from_all "-m dst" "src,tcp,udp" + check_port_removal_from_all "-p udp" "src,dst,tcp" + check_port_removal_from_all "-p tcp" "src,dst,udp" + + # Remove one from each. + check_port_removal_from_all "-m src -p udp" "dst,tcp" + check_port_removal_from_all "-m src -p tcp" "dst,udp" + check_port_removal_from_all "-m dst -p udp" "src,tcp" + check_port_removal_from_all "-m dst -p tcp" "src,udp" + + # Remove everything. + check_port_removal_from_all "" "" + check_port_removal_from_all "-m src,dst" "" + check_port_removal_from_all "-p tcp,udp" "" + check_port_removal_from_all "-m src,dst -p tcp,udp" "" + + + check_run $XDP_FILTER unload $NS -v +} + +get_python() +{ + if [[ -z "${PYTHON:-}" ]]; then + local -a possible=(python3 python) + local -a available + + local found=0 + for i in "${possible[@]}"; do + PYTHON=$(which $i) + if [[ $? -eq 0 ]]; then + found=1 + break + fi + done + if [[ found -eq 0 ]]; then + return 1 + fi + fi + + $PYTHON -c "import xdp_test_harness" &> /dev/null + if [[ $? -ne 0 ]]; then + # Libraries are not installed. + return 1 + fi + + echo "$PYTHON" +} + +run_python_test() +{ + local module="$1" + local module_path + local python + + module_path="$(realpath --relative-to=. "$TOOL_TESTS_DIR" | sed "s/\//./g")" + if [[ $? -ne 0 ]] || [[ $module_path == "." ]]; then + return "$SKIPPED_TEST" + fi + + python="$(get_python)" + if [[ $? -ne 0 ]]; then + return "$SKIPPED_TEST" + fi + + $python -m xdp_test_harness.runner client "$module_path"."$module" + if [[ $? -ne 0 ]]; then + return 1 + fi + + return 0 +} + +test_python_basic() +{ + run_python_test test_basic +} + +test_python_slow() +{ + run_python_test test_slow +} + +cleanup_tests() +{ + $XDP_FILTER unload $NS >/dev/null 2>&1 + $XDP_LOADER unload $NS --all >/dev/null 2>&1 +} diff --git a/xdp-filter/tests/test_basic.py b/xdp-filter/tests/test_basic.py new file mode 100644 index 0000000..b0e72e3 --- /dev/null +++ b/xdp-filter/tests/test_basic.py @@ -0,0 +1,279 @@ +import subprocess +import os +import signal + +import unittest +import scapy +from scapy.all import (Ether, Packet, IP, IPv6, Raw, + UDP, TCP, IPv6ExtHdrRouting) + +from xdp_test_harness.xdp_case import XDPCase, usingCustomLoader +from xdp_test_harness.utils import XDPFlag + +from . common import XDP_FILTER, Base, get_mode_string + + +@usingCustomLoader +class LoadUnload(XDPCase): + def setUp(self): + self.msg = "WARNING: All tests that follow will likely provide false result.\n" + + def run_wrap(self, cmd): + r = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + self.msg += "command: '" + str(cmd) + "'\n" + self.msg += "stdout: '" + r.stdout.decode().strip() + "'\n" + if r.stderr is not None: + self.msg += "stderr: '" + r.stderr.decode().strip() + "'\n" + self.msg += "\n" + return r.returncode == 0 + + def load(self, mode=None): + return self.run_wrap([ + XDP_FILTER, "load", + self.get_contexts().get_local_main().iface, + "--verbose", + "--mode", get_mode_string( + mode if mode else self.get_contexts().get_local_main().xdp_mode + ) + ]) + + def unload(self): + return self.run_wrap([ + XDP_FILTER, "unload", + self.get_contexts().get_local_main().iface, + "--verbose" + ]) + + def test_load_once(self): + self.assertFalse(self.unload(), self.msg) + self.assertTrue(self.load(), self.msg) + self.assertTrue(self.unload(), self.msg) + self.assertFalse(self.unload(), self.msg) + + def test_load_twice(self): + self.assertFalse(self.unload(), self.msg) + self.assertTrue(self.load(), self.msg) + self.assertFalse(self.load(), self.msg) + self.assertTrue(self.unload(), self.msg) + self.assertFalse(self.unload(), self.msg) + + def test_load_hw(self): + self.assertFalse(self.unload(), self.msg) + self.load(mode=XDPFlag.HW_MODE), self.msg + self.unload(), self.msg + self.assertFalse(self.unload(), self.msg) + + +class DirectBase: + def drop_generic(self, address, target, use_inet6=False): + to_send = self.to_send6 if use_inet6 else self.to_send + + self.arrived(to_send, self.send_packets(to_send)) + + subprocess.run([XDP_FILTER, target, address, + "--mode", self.get_mode()]) + + self.not_arrived(to_send, self.send_packets(to_send)) + + subprocess.run([XDP_FILTER, target, address, + "--mode", self.get_mode(), + "--remove"]) + + self.arrived(to_send, self.send_packets(to_send)) + + def test_none_specified(self): + self.arrived(self.to_send, self.send_packets(self.to_send)) + + def test_ether(self): + self.drop_generic(self.get_device().ether, "ether") + + def test_ip(self): + self.drop_generic(self.get_device().inet, "ip") + + def test_port(self): + self.drop_generic(str(self.get_port()), "port") + + @unittest.skipIf(XDPCase.get_contexts().get_local_main().inet6 is None or + XDPCase.get_contexts().get_remote_main().inet6 is None, + "no inet6 address available") + def test_ipv6(self): + self.drop_generic(self.get_device().inet6, "ip", use_inet6=True) + + +class BaseSrc: + def get_device(self): + return self.get_contexts().get_remote_main() + + def get_port(self): + return self.src_port + + def get_mode(self): + return "src" + + +class BaseDst: + def get_device(self): + return self.get_contexts().get_local_main() + + def get_port(self): + return self.dst_port + + def get_mode(self): + return "dst" + + +class BaseInvert: + def setUp(self): + subprocess.run([ + XDP_FILTER, "load", + "--policy", "deny", + self.get_contexts().get_local_main().iface, + "--mode", get_mode_string( + self.get_contexts().get_local_main().xdp_mode + ) + ]) + + arrived = Base.not_arrived + not_arrived = Base.arrived + + +class DirectDropSrc(Base, DirectBase, BaseSrc): + pass + + +class DirectPassSrc(Base, DirectBase, BaseSrc, BaseInvert): + pass + + +class DirectDropDst(Base, DirectBase, BaseDst): + pass + + +class DirectPassDst(Base, DirectBase, BaseDst, BaseInvert): + pass + + +class IPv6ExtensionHeader(Base): + def generic(self, extensions): + packets = [Ether() / + IPv6() / extensions / + UDP(dport=55555)] * 5 + + self.arrived(packets, self.send_packets(packets)) + + subprocess.run([XDP_FILTER, + "port", "55555", + "--mode", "dst"]) + self.not_arrived(packets, self.send_packets(packets)) + + subprocess.run([XDP_FILTER, + "port", "55555", + "--mode", "dst", + "--remove"]) + self.arrived(packets, self.send_packets(packets)) + + def test_routing(self): + self.generic(scapy.layers.inet6.IPv6ExtHdrRouting()) + + def test_hop_by_hop(self): + self.generic(scapy.layers.inet6.IPv6ExtHdrHopByHop()) + + def test_destination_options(self): + self.generic(scapy.layers.inet6.IPv6ExtHdrDestOpt()) + + def test_fragment(self): + self.generic(scapy.layers.inet6.IPv6ExtHdrFragment()) + + +class IPv4ToIPv6Mapping(Base): + def setUp(self): + super().setUp() + + inet = self.get_contexts().get_local_main().inet + + self.address_explicit = "::ffff:" + inet + + inet6_split = [format(int(i), "02x") for i in inet.split(".")] + self.address_converted = "::ffff:" + \ + inet6_split[0] + inet6_split[1] + ":" + \ + inet6_split[2] + inet6_split[3] + + self.packets = self.generate_default_packets( + dst_inet=self.address_explicit, use_inet6=True) + self.packets += self.generate_default_packets( + dst_inet=self.address_converted, use_inet6=True) + + def test_filter_explicit_address(self): + self.arrived(self.packets, self.send_packets(self.packets)) + + subprocess.run([XDP_FILTER, + "ip", self.address_explicit, + "--mode", "dst"]) + self.not_arrived(self.packets, self.send_packets(self.packets)) + + subprocess.run([XDP_FILTER, + "ip", self.address_explicit, + "--mode", "dst", + "--remove"]) + self.arrived(self.packets, self.send_packets(self.packets)) + + def test_filter_converted_address(self): + self.arrived(self.packets, self.send_packets(self.packets)) + + subprocess.run([XDP_FILTER, + "ip", self.address_converted, + "--mode", "dst"]) + self.not_arrived(self.packets, self.send_packets(self.packets)) + + subprocess.run([XDP_FILTER, + "ip", self.address_converted, + "--mode", "dst", + "--remove"]) + self.arrived(self.packets, self.send_packets(self.packets)) + + +class Status(Base): + def setUp(self): + pass + + def load(self, features): + return subprocess.run([ + XDP_FILTER, "load", + self.get_contexts().get_local_main().iface, + "--mode", get_mode_string( + self.get_contexts().get_local_main().xdp_mode + ), + "--features", features, + ]) + + def get_status(self): + return subprocess.run( + [XDP_FILTER, "status"], capture_output=True + ).stdout.decode() + + def test_ethernet_feature(self): + self.load("ethernet") + self.check_status("ether", self.get_contexts().get_local_main().ether) + + def test_ipv4_feature(self): + self.load("ipv4") + self.check_status("ip", self.get_contexts().get_local_main().inet) + + def test_udp_feature(self): + self.load("udp") + self.check_status("port", str(self.dst_port)) + + def test_all_features(self): + self.load("all") + self.check_status("ether", self.get_contexts().get_local_main().ether) + self.check_status("ip", self.get_contexts().get_local_main().inet) + self.check_status("port", str(self.dst_port)) + + def check_status(self, subcommand, address): + self.assertEqual(self.get_status().find(address), -1) + + subprocess.run([XDP_FILTER, subcommand, address]) + self.assertNotEqual(self.get_status().find(address), -1) + + subprocess.run([XDP_FILTER, subcommand, address, "--remove"]) + self.assertEqual(self.get_status().find(address), -1) diff --git a/xdp-filter/tests/test_slow.py b/xdp-filter/tests/test_slow.py new file mode 100644 index 0000000..3a1c91f --- /dev/null +++ b/xdp-filter/tests/test_slow.py @@ -0,0 +1,127 @@ +import subprocess +import os +import signal + +import unittest +from scapy.all import (Ether, Packet, IP, IPv6, Raw, + UDP, TCP, IPv6ExtHdrRouting) + +from xdp_test_harness.xdp_case import XDPCase, usingCustomLoader +from xdp_test_harness.utils import XDPFlag + +from . common import Base, XDP_FILTER, get_mode_string + + +class ManyAddresses(Base): + def format_number(self, number, + delimiter, format_string, + part_size, parts_amount): + splitted = [] + + while number > 0: + splitted.append(int(number % (1 << part_size))) + number = number >> part_size + + assert(len(splitted) <= parts_amount) + if (len(splitted) < parts_amount): + splitted += [0] * (parts_amount - len(splitted)) + + splitted.reverse() + + return delimiter.join(format(s, format_string) for s in splitted) + + def generate_addresses(self, + delimiter, format_string, parts_amount, full_size): + AMOUNT = 257 + + bits = parts_amount * full_size + + for gen_number in range(0, (1 << bits) - 1, int((1 << bits) / AMOUNT)): + yield self.format_number(gen_number, delimiter, + format_string, parts_amount, full_size) + + def filter_addresses(self, name, + delimiter, format_string, parts_amount, full_size): + summed = 0 + for address in self.generate_addresses(delimiter, format_string, + parts_amount, full_size): + summed += 1 + subprocess.run([XDP_FILTER, name, address, "--mode", "dst"]) + + output = subprocess.check_output([XDP_FILTER, "status"]) + # Each address is on a separate line. + self.assertGreaterEqual(len(output.splitlines()), summed) + + def get_invalid_address(self, name, + delimiter, format_string, + parts_amount, full_size): + """ + Try to add addresses to xdp-filter, + return address that does not get added. + """ + + last_length = subprocess.check_output([XDP_FILTER, "status"]) + for address in self.generate_addresses(delimiter, format_string, + parts_amount, full_size): + new_length = subprocess.check_output( + [XDP_FILTER, name, address, "--mode", "dst", "--status"]) + + if new_length == last_length: + return address + last_length = new_length + + return None + + def test_ip_arrive(self): + missing = self.get_invalid_address("ip", ".", "d", 8, 4) + + if missing is None: + return + + to_send = self.generate_default_packets(dst_inet=missing) + res = self.send_packets(to_send) + self.not_arrived(to_send, res) + + def test_ether_arrive(self): + missing = self.get_invalid_address("ether", ":", "02x", 8, 6) + + if missing is None: + return + + to_send = self.generate_default_packets(dst_ether=missing) + res = self.send_packets(to_send) + self.not_arrived(to_send, res) + + def test_port_arrive(self): + missing = self.get_invalid_address("port", "", "d", 16, 1) + + if missing is None: + return + + to_send = self.generate_default_packets(dst_port=missing) + res = self.send_packets(to_send) + self.not_arrived(to_send, res) + + def test_ip_status(self): + self.filter_addresses("ip", ".", "d", 8, 4) + + def test_port_status(self): + self.filter_addresses("port", "", "d", 16, 1) + + def test_ether_status(self): + self.filter_addresses("ether", ":", "02x", 8, 6) + + +class ManyAddressesInverted(ManyAddresses): + def setUp(self): + subprocess.run([ + XDP_FILTER, "load", + "--policy", "deny", + self.get_contexts().get_local_main().iface, + "--mode", get_mode_string( + self.get_contexts().get_local_main().xdp_mode + ) + ]) + + arrived = Base.not_arrived + not_arrived = Base.arrived diff --git a/xdp-filter/xdp-filter.8 b/xdp-filter/xdp-filter.8 new file mode 100644 index 0000000..c2db9b5 --- /dev/null +++ b/xdp-filter/xdp-filter.8 @@ -0,0 +1,371 @@ +.TH "xdp-filter" "8" "SEPTEMBER 5, 2022" "V1.3.1" "A simple XDP-powered packet filter" + +.SH "NAME" +xdp-filter \- a simple XDP-powered packet filter +.SH "SYNOPSIS" +.PP +XDP-filter is a packet filtering utility powered by XDP. It is deliberately +simple and so does not have the same matching capabilities as, e.g., netfilter. +Instead, thanks to XDP, it can achieve very high drop rates: tens of millions of +packets per second on a single CPU core. + +.SS "Running xdp-filter" +.PP +The syntax for running xdp-filter is: + +.RS +.nf +\fCxdp-filter COMMAND [options] + +Where COMMAND can be one of: + load - load xdp-filter on an interface + unload - unload xdp-filter from an interface + port - add a port to the filter list + ip - add an IP address to the filter list + ether - add an Ethernet MAC address to the filter list + status - show current xdp-filter status + poll - poll statistics output + help - show the list of available commands +\fP +.fi +.RE + +.PP +Each command, and its options are explained below. Or use \fIxdp\-filter COMMAND +\-\-help\fP to see the options for each command. + +.SH "The LOAD command" +.PP +To use \fIxdp\-filter\fP, it must first be loaded onto an interface. This is +accomplished with the \fIload\fP command, which takes the name of the interface as a +parameter, and optionally allows specifying the features that should be +included. By default all features are loaded, but de-selecting some features can +speed up the packet matching, and increase performance by a substantial amount. + +.PP +The syntax for the \fIload\fP command is: + +.PP +\fIxdp\-filter load [options] <ifname>\fP + +.PP +Where \fI<ifname>\fP is the name of the interface to load \fIxdp\-filter\fP onto, and +must be specified. The supported options are: + +.SS "-m, --mode <mode>" +.PP +Specifies which mode to load the XDP program to be loaded in. The valid values +are 'native', which is the default in-driver XDP mode, 'skb', which causes the +so-called \fIskb mode\fP (also known as \fIgeneric XDP\fP) to be used, or 'hw' which +causes the program to be offloaded to the hardware. + +.SS "-p, --policy <policy>" +.PP +This sets the policy \fIxdp\-filter\fP applies to packets \fBnot\fP matched by any of the +filter rules. The default is \fIallow\fP, in which packets not matching any rules +are allowed to pass. The other option is \fIdeny\fP, in which \fBall\fP packets are +dropped \fBexcept\fP those matched by the filter options. + +.PP +\fIxdp\-filter\fP cannot be loaded simultaneously in \fIdeny\fP and \fIallow\fP policy modes +on the system. Note that loading \fIxdp\-filter\fP in \fIdeny\fP mode will drop all +traffic on the interface until suitable allow rules are installed, so some care +is needed to avoid being locked out of a remote system. + +.SS "-f, --features <feats>" +.PP +Use this option to select which features to include when loaded \fIxdp\-filter\fP. +The default is to load all available features. So select individual features +specify one or more of these: + +.IP \(bu 4 +\fBtcp\fP: Support filtering on TCP port number +.IP \(bu 4 +\fBudp\fP: Support filtering on UDP port number +.IP \(bu 4 +\fBipv6\fP: Support filtering on IPv6 addresses +.IP \(bu 4 +\fBipv4\fP: Support filtering on IPv4 addresses +.IP \(bu 4 +\fBethernet\fP: Support filtering on Ethernet MAC addresses + +.PP +Specify multiple features by separating them with a comma. E.g.: \fItcp,udp,ipv6\fP. + +.SS "-v, --verbose" +.PP +Enable debug logging. Specify twice for even more verbosity. + +.SS "-h, --help" +.PP +Display a summary of the available options + +.SH "The UNLOAD command" +.PP +The \fIunload\fP command unloads \fIxdp\-filter\fP from one (or all) interfaces, and +cleans up the program state. + +.PP +The syntax for the \fIload\fP command is: + +.PP +\fIxdp\-filter unload [options] <ifname>\fP + +.PP +Where \fI<ifname>\fP is the name of the interface to unload \fIxdp\-filter\fP from, and +must be specified unless the \fB--all\fP option is used. The supported options are: + +.SS "-a, --all" +.PP +Specify this option to remove \fIxdp\-filter\fP from all interfaces it was loaded +onto. If this option is specified, no \fI<ifname>\fP is needed. + +.PP +This option can also be used to clean up all \fIxdp\-filter\fP state if the XDP +program(s) were unloaded by other means. + +.SS "-k, --keep-maps" +.PP +Specify this option to prevent \fIxdp\-filter\fP from clearing its map state. By +default, all BPF maps no longer needed by any loaded program are removed. +However, this will also remove the contents of the maps (the filtering rules), +so this option can be used to keep the maps around so the rules persist until +\fIxdp\-filter\fP is loaded again. + +.SS "-v, --verbose" +.PP +Enable debug logging. Specify twice for even more verbosity. + +.SS "-h, --help" +.PP +Display a summary of the available options + +.SH "The PORT command" +.PP +Use the \fIport\fP command to add a TCP or UDP port to the \fIxdp\-filter\fP match list. +For this to work, \fIxdp\-filter\fP must be loaded with either the \fBudp\fP or the \fBtcp\fP +feature (or both) on at least one interface. + +.PP +The syntax for the \fIport\fP command is: + +.PP +\fIxdp\-filter port [options] <port>\fP + +.PP +Where \fI<port>\fP is the port number to add (or remove if the \fB--remove\fP is +specified). The supported options are: + +.SS "-r, --remove" +.PP +Remove the port instead of adding it. + +.SS "-m, --mode <mode>" +.PP +Select filtering mode. Valid options are \fBsrc\fP and \fBdst\fP, both of which may be +specified as \fIsrc,dst\fP. If \fBsrc\fP is specified, the port number will added as a +\fIsource port\fP match, while if \fBdst\fP is specified, the port number will be added +as a \fIdestination port\fP match. If both are specified, a packet will be matched +if \fBeither\fP its source or destination port is the specified port number. + +.SS "-p, --proto <proto>" +.PP +Specify one (or both) of \fBudp\fP and/or \fBtcp\fP to match UDP or TCP ports, +respectively. + +.SS "-s, --status" +.PP +If this option is specified, the current list of matched ports will be printed +after inserting the port number. Otherwise, nothing will be printed. + +.SS "-v, --verbose" +.PP +Enable debug logging. Specify twice for even more verbosity. + +.SS "-h, --help" +.PP +Display a summary of the available options + + +.SH "The IP command" +.PP +Use the \fIip\fP command to add an IPv6 or an IPv4 address to the \fIxdp\-filter\fP match +list. + +.PP +The syntax for the \fIip\fP command is: + +.PP +\fIxdp\-filter ip [options] <ip>\fP + +.PP +Where \fI<ip>\fP is the IP address to add (or remove if the \fB--remove\fP is +specified). Either IPv4 or IPv6 addresses can be specified, but \fIxdp\-filter\fP +must be loaded with the corresponding features (\fBipv4\fP and \fBipv6\fP, +respectively). The supported options are: + +.SS "-r, --remove" +.PP +Remove the IP address instead of adding it. + +.SS "-m, --mode <mode>" +.PP +Select filtering mode. Valid options are \fBsrc\fP and \fBdst\fP, both of which may be +specified as \fIsrc,dst\fP. If \fBsrc\fP is specified, the IP address will added as a +\fIsource IP\fP match, while if \fBdst\fP is specified, the IP address will be added +as a \fIdestination IP\fP match. If both are specified, a packet will be matched +if \fBeither\fP its source or destination IP is the specified IP address. + +.SS "-s, --status" +.PP +If this option is specified, the current list of matched ips will be printed +after inserting the IP address. Otherwise, nothing will be printed. + +.SS "-v, --verbose" +.PP +Enable debug logging. Specify twice for even more verbosity. + +.SS "-h, --help" +.PP +Display a summary of the available options + +.SH "The ETHER command" +.PP +Use the \fIether\fP command to add an Ethernet MAC address to the \fIxdp\-filter\fP match +list. For this to work, \fIxdp\-filter\fP must be loaded with either the \fBethernet\fP +feature on at least one interface. + +.PP +The syntax for the \fIether\fP command is: + +.PP +\fIxdp\-filter ether [options] <addr>\fP + +.PP +Where \fI<addr>\fP is the MAC address to add (or remove if the \fB--remove\fP is +specified). The supported options are: + +.SS "-r, --remove" +.PP +Remove the MAC address instead of adding it. + +.SS "-m, --mode <mode>" +.PP +Select filtering mode. Valid options are \fBsrc\fP and \fBdst\fP, both of which may be +specified as \fIsrc,dst\fP. If \fBsrc\fP is specified, the MAC address will added as a +\fIsource MAC\fP match, while if \fBdst\fP is specified, the MAC address will be added +as a \fIdestination MAC\fP match. If both are specified, a packet will be matched +if \fBeither\fP its source or destination MAC is the specified MAC address. + +.SS "-s, --status" +.PP +If this option is specified, the current list of matched ips will be printed +after inserting the MAC address. Otherwise, nothing will be printed. + +.SS "-v, --verbose" +.PP +Enable debug logging. Specify twice for even more verbosity. + +.SS "-h, --help" +.PP +Display a summary of the available options + +.SH "The STATUS command" +.PP +The \fIstatus\fP command prints the current status of \fIxdp\-filter\fP: Which interfaces +it is loaded on, the current list of rules, and some statistics for how many +packets have been processed in total, and how many times each rule has been hit. + +.PP +The syntax for the \fIstatus\fP command is: + +.PP +\fIxdp\-filter status [options]\fP + +.PP +Where the supported options are: + +.SS "-v, --verbose" +.PP +Enable debug logging. Specify twice for even more verbosity. + +.SS "-h, --help" +.PP +Display a summary of the available options + +.SH "The POLL command" +.PP +The \fIpoll\fP command periodically polls the \fIxdp\-filter\fP statistics map and prints +out the total number of packets and bytes processed by \fIxdp\-filter\fP, as well as +the number in the last polling interval, converted to packets (and bytes) per +second. This can be used to inspect the performance of \fIxdp\-filter\fP, and to +compare the performance of the different feature sets selectable by the \fIload\fP +parameter. + +.PP +The syntax for the \fIpoll\fP command is: + +.PP +\fIxdp\-filter poll [options]\fP + +.PP +Where the supported options are: + +.SS "-i, --interval <interval>" +.PP +The polling interval, in milliseconds. Defaults to 1000 (1 second). + +.SS "-v, --verbose" +.PP +Enable debug logging. Specify twice for even more verbosity. + +.SS "-h, --help" +.PP +Display a summary of the available options + +.SH "Examples" +.PP +To filter all packets arriving on port 80 on eth0, issue the +following commands: + +.RS +.nf +\fC# xdp-filter load eth0 -f tcp,udp +# xdp-filter port 80 +\fP +.fi +.RE + +.PP +To filter all packets \fBexcept\fP those from IP address fc00:dead:cafe::1 issue the +following commands (careful, this can lock you out of remote access!): + +.RS +.nf +\fC# xdp-filter load eth0 -f ipv6 -p deny +# xdp-filter ip fc00:dead:cafe::1 -m src +\fP +.fi +.RE + +.PP +To allow packets from \fBeither\fP IP fc00:dead:cafe::1 \fBor\fP arriving on port 22, +issue the following (careful, this can lock you out of remote access!): + +.RS +.nf +\fC# xdp-filter load eth0 -f ipv6,tcp -p deny +# xdp-filter port 22 +# xdp-filter ip fc00:dead:cafe::1 -m src +\fP +.fi +.RE + +.SH "BUGS" +.PP +Please report any bugs on Github: \fIhttps://github.com/xdp-project/xdp-tools/issues\fP + +.SH "AUTHOR" +.PP +xdp-filter was written by Toke Høiland-Jørgensen and Jesper Dangaard Brouer. +This man page was written by Toke Høiland-Jørgensen. diff --git a/xdp-filter/xdp-filter.c b/xdp-filter/xdp-filter.c new file mode 100644 index 0000000..0396a1a --- /dev/null +++ b/xdp-filter/xdp-filter.c @@ -0,0 +1,1097 @@ +/* SPDX-License-Identifier: GPL-2.0 */ + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <errno.h> +#include <stdbool.h> +#include <unistd.h> +#include <fcntl.h> +#include <inttypes.h> + +#include <bpf/bpf.h> +#include <bpf/libbpf.h> +#include <xdp/libxdp.h> +#include <arpa/inet.h> + +#include <linux/if_ether.h> + +#include "params.h" +#include "logging.h" +#include "util.h" +#include "stats.h" +#include "common_kern_user.h" +#include "prog_features.h" + +#define NEED_RLIMIT (20 * 1024 * 1024) /* 10 Mbyte */ +#define PROG_NAME "xdp-filter" + +struct flag_val map_flags_all[] = { + {"src", MAP_FLAG_SRC}, + {"dst", MAP_FLAG_DST}, + {"tcp", MAP_FLAG_TCP}, + {"udp", MAP_FLAG_UDP}, + {} +}; + +struct flag_val map_flags_srcdst[] = { + {"src", MAP_FLAG_SRC}, + {"dst", MAP_FLAG_DST}, + {} +}; + +struct flag_val map_flags_tcpudp[] = { + {"tcp", MAP_FLAG_TCP}, + {"udp", MAP_FLAG_UDP}, + {} +}; + +static char *find_prog_file(__u32 features) +{ + struct prog_feature *feat; + + if (!features) + return NULL; + + for (feat = prog_features; feat->prog_name; feat++) { + if ((ntohl(feat->features) & features) == features) + return strdup(feat->prog_name); + } + return NULL; +} + +static __u32 find_features(const char *progname) +{ + struct prog_feature *feat; + + for (feat = prog_features; feat->prog_name; feat++) { + if (is_prefix(progname, feat->prog_name)) + return ntohl(feat->features); + } + return 0; +} + +static int map_get_counter_flags(int fd, void *key, __u64 *counter, __u8 *flags) +{ + /* For percpu maps, userspace gets a value per possible CPU */ + int nr_cpus = libbpf_num_possible_cpus(); + __u64 sum_ctr = 0; + int i, err = 0; + __u64 *values; + + if (nr_cpus < 0) + return nr_cpus; + + values = calloc(nr_cpus, sizeof(*values)); + if (!values) + return -ENOMEM; + + if ((bpf_map_lookup_elem(fd, key, values)) != 0) { + err = -ENOENT; + goto out; + } + + /* Sum values from each CPU */ + for (i = 0; i < nr_cpus; i++) { + __u8 flg = values[i] & MAP_FLAGS; + + if (!flg) { + err = -ENOENT; /* not set */ + goto out; + } + *flags = flg; + sum_ctr += values[i] >> COUNTER_SHIFT; + } + *counter = sum_ctr; + +out: + free(values); + return err; +} + +static int map_set_flags(int fd, void *key, __u8 flags, bool delete_empty) +{ + /* For percpu maps, userspace gets a value per possible CPU */ + int nr_cpus = libbpf_num_possible_cpus(); + __u64 *values; + int i, err; + + if (nr_cpus < 0) + return nr_cpus; + + values = calloc(nr_cpus, sizeof(*values)); + if (!values) + return -ENOMEM; + + if (bpf_map_lookup_elem(fd, key, values) != 0) { + memset(values, 0, sizeof(*values) * nr_cpus); + } else if (!flags && delete_empty) { + pr_debug("Deleting empty map value from flags %u\n", flags); + + err = bpf_map_delete_elem(fd, key); + if (err) { + err = -errno; + pr_warn("Couldn't delete value from state map: %s\n", + strerror(-err)); + } + goto out; + } + + for (i = 0; i < nr_cpus; i++) + values[i] = flags ? (values[i] & ~MAP_FLAGS) | (flags & MAP_FLAGS) : 0; + + pr_debug("Setting new map value %" PRIu64 " from flags %u\n", + (uint64_t)values[0], flags); + + err = bpf_map_update_elem(fd, key, values, 0); + if (err) { + err = -errno; + if (err == -E2BIG) + pr_warn("Couldn't add entry: state map is full\n"); + else + pr_warn("Unable to update state map: %s\n", strerror(-err)); + } + +out: + free(values); + return err; +} + +static int get_iface_features(__unused const struct iface *iface, + struct xdp_program *prog, + __unused enum xdp_attach_mode mode, void *arg) +{ + __u32 *all_feats = arg; + + *all_feats |= find_features(xdp_program__name(prog)); + return 0; +} + +static int get_used_features(const char *pin_root_path, __u32 *feats) +{ + __u32 all_feats = 0; + int err; + + err = iterate_pinned_programs(pin_root_path, get_iface_features, + &all_feats); + if (err && err != -ENOENT) + return err; + + *feats = all_feats; + return 0; +} + +static const struct loadopt { + bool help; + struct iface iface; + unsigned int features; + enum xdp_attach_mode mode; + unsigned int policy_mode; +} defaults_load = { + .features = FEAT_ALL, + .mode = XDP_MODE_NATIVE, + .policy_mode = FEAT_ALLOW, +}; + +struct flag_val load_features[] = { + {"tcp", FEAT_TCP}, + {"udp", FEAT_UDP}, + {"ipv6", FEAT_IPV6}, + {"ipv4", FEAT_IPV4}, + {"ethernet", FEAT_ETHERNET}, + {"all", FEAT_ALL}, + {} +}; + +struct flag_val print_features[] = { + {"tcp", FEAT_TCP}, + {"udp", FEAT_UDP}, + {"ipv6", FEAT_IPV6}, + {"ipv4", FEAT_IPV4}, + {"ethernet", FEAT_ETHERNET}, + {"allow", FEAT_ALLOW}, + {"deny", FEAT_DENY}, + {} +}; + +struct enum_val xdp_modes[] = { + {"native", XDP_MODE_NATIVE}, + {"skb", XDP_MODE_SKB}, + {"hw", XDP_MODE_HW}, + {NULL, 0} +}; + +struct enum_val policy_modes[] = { + {"allow", FEAT_ALLOW}, + {"deny", FEAT_DENY}, + {NULL, 0} +}; + +static struct prog_option load_options[] = { + DEFINE_OPTION("mode", OPT_ENUM, struct loadopt, mode, + .short_opt = 'm', + .typearg = xdp_modes, + .metavar = "<mode>", + .help = "Load XDP program in <mode>; default native"), + DEFINE_OPTION("policy", OPT_ENUM, struct loadopt, policy_mode, + .short_opt = 'p', + .typearg = policy_modes, + .metavar = "<policy>", + .help = "Policy for unmatched packets; default allow"), + DEFINE_OPTION("dev", OPT_IFNAME, struct loadopt, iface, + .positional = true, + .metavar = "<ifname>", + .required = true, + .help = "Load on device <ifname>"), + DEFINE_OPTION("features", OPT_FLAGS, struct loadopt, features, + .short_opt = 'f', + .metavar = "<feats>", + .typearg = load_features, + .help = "Features to enable; default all"), + END_OPTIONS +}; + +int do_load(const void *cfg, const char *pin_root_path) +{ + char errmsg[STRERR_BUFSIZE], featbuf[100]; + const struct loadopt *opt = cfg; + struct xdp_program *p = NULL; + int err = EXIT_SUCCESS; + unsigned int features; + __u32 used_feats; + char *filename; + DECLARE_LIBBPF_OPTS(bpf_object_open_opts, opts, + .pin_root_path = pin_root_path); + DECLARE_LIBXDP_OPTS(xdp_program_opts, xdp_opts, 0); + + if (opt->mode == XDP_MODE_HW) { + pr_warn("xdp-filter does not support offloading.\n"); + return EXIT_FAILURE; + } + + err = get_used_features(pin_root_path, &used_feats); + if (err) { + pr_warn("Error getting list of loaded programs: %s\n", + strerror(-err)); + return err; + } + + features = opt->features; + if (opt->policy_mode == FEAT_DENY && used_feats & FEAT_ALLOW) { + pr_warn("xdp-filter is already loaded in allow policy mode. " + "Unload before loading in deny mode.\n"); + return EXIT_FAILURE; + } else if (opt->policy_mode == FEAT_ALLOW && used_feats & FEAT_DENY) { + pr_warn("xdp-filter is already loaded in deny policy mode. " + "Unload before loading in allow mode.\n"); + return EXIT_FAILURE; + } + features |= opt->policy_mode; + + err = get_pinned_program(&opt->iface, pin_root_path, NULL, &p); + if (!err) { + pr_warn("xdp-filter is already loaded on %s\n", opt->iface.ifname); + xdp_program__close(p); + return EXIT_FAILURE; + } + + print_flags(featbuf, sizeof(featbuf), print_features, features); + pr_debug("Looking for eBPF program with features %s\n", featbuf); + + filename = find_prog_file(features); + if (!filename) { + pr_warn("Couldn't find an eBPF program with the requested feature set!\n"); + return EXIT_FAILURE; + } + + pr_debug("Found prog '%s' matching feature set to be loaded on interface '%s'.\n", + filename, opt->iface.ifname); + + /* libbpf spits out a lot of unhelpful error messages while loading. + * Silence the logging so we can provide our own messages instead; this + * is a noop if verbose logging is enabled. + */ + silence_libbpf_logging(); + +retry: + xdp_opts.find_filename = filename; + xdp_opts.opts = &opts; + /* prog_name is NULL, so choose the first program in object */ + p = xdp_program__create(&xdp_opts); + err = libxdp_get_error(p); + if (err) { + if (err == -EPERM && !double_rlimit()) + goto retry; + + libxdp_strerror(err, errmsg, sizeof(errmsg)); + pr_warn("Couldn't load BPF program: %s(%d)\n", errmsg, err); + p = NULL; + goto out; + } + + err = attach_xdp_program(p, &opt->iface, opt->mode, pin_root_path); + if (err) { + if (err == -EPERM && !double_rlimit()) { + xdp_program__close(p); + goto retry; + } + + libxdp_strerror(err, errmsg, sizeof(errmsg)); + pr_warn("Couldn't attach XDP program on iface '%s': %s(%d)\n", + opt->iface.ifname, errmsg, err); + goto out; + } + +out: + if (p) + xdp_program__close(p); + free(filename); + return err; +} + +static int remove_unused_maps(const char *pin_root_path, __u32 features) +{ + int dir_fd, err = 0; + + dir_fd = open(pin_root_path, O_DIRECTORY); + if (dir_fd < 0) { + if (errno == ENOENT) + return 0; + err = -errno; + pr_warn("Unable to open pin directory %s: %s\n", + pin_root_path, strerror(-err)); + goto out; + } + + if (!(features & (FEAT_TCP | FEAT_UDP))) { + err = unlink_pinned_map(dir_fd, textify(MAP_NAME_PORTS)); + if (err) + goto out; + } + + if (!(features & FEAT_IPV4)) { + err = unlink_pinned_map(dir_fd, textify(MAP_NAME_IPV4)); + if (err) + goto out; + } + + if (!(features & FEAT_IPV6)) { + err = unlink_pinned_map(dir_fd, textify(MAP_NAME_IPV6)); + if (err) + goto out; + } + + if (!(features & FEAT_ETHERNET)) { + err = unlink_pinned_map(dir_fd, textify(MAP_NAME_ETHERNET)); + if (err) + goto out; + } + + if (!features) { + char buf[PATH_MAX]; + + err = unlink_pinned_map(dir_fd, textify(XDP_STATS_MAP_NAME)); + if (err) + goto out; + + close(dir_fd); + dir_fd = -1; + + err = try_snprintf(buf, sizeof(buf), "%s/%s", pin_root_path, "programs"); + if (err) + goto out; + + pr_debug("Removing program directory %s\n", buf); + err = rmdir(buf); + if (err) { + err = -errno; + pr_warn("Unable to rmdir: %s\n", strerror(-err)); + goto out; + } + + pr_debug("Removing pinning directory %s\n", pin_root_path); + err = rmdir(pin_root_path); + if (err) { + err = -errno; + pr_warn("Unable to rmdir: %s\n", strerror(-err)); + goto out; + } + } + +out: + if (dir_fd >= 0) + close(dir_fd); + + return err; +} + +static int remove_iface_program(const struct iface *iface, + struct xdp_program *prog, + enum xdp_attach_mode mode, void *arg) +{ + char errmsg[STRERR_BUFSIZE], buf[100]; + char *pin_root_path = arg; + __u32 feats; + int err; + + feats = find_features(xdp_program__name(prog)); + if (!feats) { + pr_warn("Unrecognised XDP program on interface %s. Not removing.\n", + iface->ifname); + return -ENOENT; + } + + print_flags(buf, sizeof(buf), print_features, feats); + pr_debug("Removing XDP program with features %s from iface %s\n", + buf, iface->ifname); + + err = detach_xdp_program(prog, iface, mode, pin_root_path); + if (err) { + libxdp_strerror(err, errmsg, sizeof(errmsg)); + pr_warn("Removing XDP program on iface %s failed (%d): %s\n", + iface->ifname, -err, errmsg); + } + + return err; +} + + +static const struct unloadopt { + bool all; + bool keep; + struct iface iface; +} defaults_unload = {}; + +static struct prog_option unload_options[] = { + DEFINE_OPTION("dev", OPT_IFNAME, struct unloadopt, iface, + .positional = true, + .metavar = "<ifname>", + .help = "Unload from device <ifname>"), + DEFINE_OPTION("all", OPT_BOOL, struct unloadopt, all, + .short_opt = 'a', + .help = "Unload from all interfaces"), + DEFINE_OPTION("keep-maps", OPT_BOOL, struct unloadopt, keep, + .short_opt = 'k', + .help = "Don't destroy unused maps after unloading"), + END_OPTIONS +}; + +int do_unload(const void *cfg, const char *pin_root_path) +{ + const struct unloadopt *opt = cfg; + enum xdp_attach_mode mode; + struct xdp_program *prog; + int err = EXIT_SUCCESS; + char buf[100]; + __u32 feats; + DECLARE_LIBBPF_OPTS(bpf_object_open_opts, opts, + .pin_root_path = pin_root_path); + + if (opt->all) { + pr_debug("Removing xdp-filter from all interfaces\n"); + err = iterate_pinned_programs(pin_root_path, + remove_iface_program, + (void *)pin_root_path); + if (err && err != -ENOENT) + goto out; + goto clean_maps; + } + + if (!opt->iface.ifindex) { + pr_warn("Must specify ifname or --all\n"); + err = EXIT_FAILURE; + goto out; + } + + err = get_pinned_program(&opt->iface, pin_root_path, &mode, &prog); + if (err) { + pr_warn("xdp-filter is not loaded on %s\n", opt->iface.ifname); + err = EXIT_FAILURE; + goto out; + } + + err = remove_iface_program(&opt->iface, prog, mode, + (void *)pin_root_path); + if (err) + goto out; + +clean_maps: + if (opt->keep) { + pr_debug("Not removing pinned maps because of --keep-maps option\n"); + goto out; + } + + pr_debug("Checking map usage and removing unused maps\n"); + err = get_used_features(pin_root_path, &feats); + if (err) + goto out; + + print_flags(buf, sizeof(buf), print_features, feats); + pr_debug("Features still being used: %s\n", feats ? buf : "none"); + + err = remove_unused_maps(pin_root_path, feats); + if (err) + goto out; + +out: + return err; +} + +int print_ports(int map_fd) +{ + __u32 map_key = -1, prev_key = 0; + int err; + + printf("Filtered ports:\n"); + printf(" %-40s Mode Hit counter\n", ""); + FOR_EACH_MAP_KEY (err, map_fd, map_key, prev_key) { + char buf[100]; + __u64 counter; + __u8 flags = 0; + + err = map_get_counter_flags(map_fd, &map_key, &counter, &flags); + if (err == -ENOENT) + continue; + else if (err) + return err; + + print_flags(buf, sizeof(buf), map_flags_all, flags); + printf(" %-40u %-15s %" PRIu64 "\n", ntohs(map_key), buf, + (uint64_t)counter); + } + return 0; +} + +static const struct portopt { + unsigned int mode; + unsigned int proto; + __u16 port; + bool print_status; + bool remove; +} defaults_port = {}; + +static struct prog_option port_options[] = { + DEFINE_OPTION("port", OPT_U16, struct portopt, port, + .positional = true, + .metavar = "<port>", + .required = true, + .help = "Port to add or remove"), + DEFINE_OPTION("remove", OPT_BOOL, struct portopt, remove, + .short_opt = 'r', + .help = "Remove port instead of adding"), + DEFINE_OPTION("mode", OPT_FLAGS, struct portopt, mode, + .short_opt = 'm', + .metavar = "<mode>", + .typearg = map_flags_srcdst, + .help = "Filter mode; default dst"), + DEFINE_OPTION("proto", OPT_FLAGS, struct portopt, proto, + .short_opt = 'p', + .metavar = "<proto>", + .typearg = map_flags_tcpudp, + .help = "Protocol to filter; default tcp,udp"), + DEFINE_OPTION("status", OPT_BOOL, struct portopt, print_status, + .short_opt = 's', + .help = "Print status of filtered ports after changing"), + END_OPTIONS +}; + + +int do_port(const void *cfg, const char *pin_root_path) +{ + int map_fd = -1, err = EXIT_SUCCESS; + char modestr[100], protostr[100]; + const struct portopt *opt = cfg; + unsigned int proto = opt->proto; + unsigned int mode = opt->mode; + struct bpf_map_info info = {}; + __u8 flags = 0; + __u64 counter; + __u32 map_key; + + map_fd = get_pinned_map_fd(pin_root_path, textify(MAP_NAME_PORTS), &info); + if (map_fd < 0) { + pr_warn("Couldn't find port filter map; is xdp-filter loaded " + "with the right features (udp and/or tcp)?\n"); + err = EXIT_FAILURE; + goto out; + } + pr_debug("Found map with fd %d for map id %d\n", map_fd, info.id); + + map_key = htons(opt->port); + + err = map_get_counter_flags(map_fd, &map_key, &counter, &flags); + if (err && err != -ENOENT) + goto out; + + if (opt->remove) { + if (mode == 0 && proto == 0) { + mode = MAP_FLAG_SRC | MAP_FLAG_DST; + proto = MAP_FLAG_TCP | MAP_FLAG_UDP; + } + + flags &= ~(mode | proto); + } else { + if (mode == 0) + mode = MAP_FLAG_DST; + if (proto == 0) + proto = MAP_FLAG_TCP | MAP_FLAG_UDP; + + flags |= mode | proto; + } + + print_flags(modestr, sizeof(modestr), map_flags_srcdst, mode); + print_flags(protostr, sizeof(protostr), map_flags_tcpudp, proto); + pr_debug("%s %s port %u mode %s\n", opt->remove ? "Removing" : "Adding", + protostr, opt->port, modestr); + + if (!(flags & (MAP_FLAG_DST | MAP_FLAG_SRC)) || + !(flags & (MAP_FLAG_TCP | MAP_FLAG_UDP))) + flags = 0; + + err = map_set_flags(map_fd, &map_key, flags, false); + if (err) + goto out; + + if (opt->print_status) { + err = print_ports(map_fd); + if (err) + goto out; + } + +out: + if (map_fd >= 0) + close(map_fd); + return err; +} + +int __print_ips(int map_fd, int af) +{ + struct ip_addr map_key = { .af = af }, prev_key = {}; + int err; + + FOR_EACH_MAP_KEY (err, map_fd, map_key.addr, prev_key.addr) { + char flagbuf[100], addrbuf[100]; + __u8 flags = 0; + __u64 counter; + + err = map_get_counter_flags(map_fd, &map_key.addr, &counter, &flags); + if (err == -ENOENT) + continue; + else if (err) + return err; + + print_flags(flagbuf, sizeof(flagbuf), map_flags_srcdst, flags); + print_addr(addrbuf, sizeof(addrbuf), &map_key); + printf(" %-40s %-15s %" PRIu64 "\n", addrbuf, flagbuf, + (uint64_t)counter); + } + + return 0; +} + +int print_ips() +{ + int map_fd4 = -1, map_fd6 = -1; + char pin_root_path[PATH_MAX]; + int err = 0; + + err = get_bpf_root_dir(pin_root_path, sizeof(pin_root_path), PROG_NAME, + true); + if (err) + goto out; + + map_fd6 = get_pinned_map_fd(pin_root_path, textify(MAP_NAME_IPV6), NULL); + map_fd4 = get_pinned_map_fd(pin_root_path, textify(MAP_NAME_IPV4), NULL); + if (map_fd4 < 0 && map_fd6 < 0) { + err = -ENOENT; + goto out; + } + + printf("Filtered IP addresses:\n"); + printf(" %-40s Mode Hit counter\n", ""); + + if (map_fd6 >= 0) { + err = __print_ips(map_fd6, AF_INET6); + if (err) + goto out; + } + + if (map_fd4 >= 0) + err = __print_ips(map_fd4, AF_INET); + +out: + if (map_fd4 >= 0) + close(map_fd4); + if (map_fd6 >= 0) + close(map_fd6); + return err; +} + + +static int __do_address(const char *pin_root_path, + const char *map_name, const char *feat_name, + void *map_key, bool remove, int mode) +{ + int map_fd = -1, err = 0; + __u8 flags = 0; + __u64 counter; + + map_fd = get_pinned_map_fd(pin_root_path, map_name, NULL); + if (map_fd < 0) { + pr_warn("Couldn't find filter map; is xdp-filter loaded " + "with the %s feature?\n", feat_name); + err = -ENOENT; + goto out; + } + + err = map_get_counter_flags(map_fd, map_key, &counter, &flags); + if (err && err != -ENOENT) + goto out; + + if (remove) + flags &= ~mode; + else + flags |= mode; + + err = map_set_flags(map_fd, map_key, flags, true); + if (err) + goto out; + +out: + return err ?: map_fd; +} + +static const struct ipopt { + unsigned int mode; + struct ip_addr addr; + bool print_status; + bool remove; +} defaults_ip = { + .mode = MAP_FLAG_DST, +}; + +static struct prog_option ip_options[] = { + DEFINE_OPTION("addr", OPT_IPADDR, struct ipopt, addr, + .positional = true, + .metavar = "<addr>", + .required = true, + .help = "Address to add or remove"), + DEFINE_OPTION("remove", OPT_BOOL, struct ipopt, remove, + .short_opt = 'r', + .help = "Remove address instead of adding"), + DEFINE_OPTION("mode", OPT_FLAGS, struct ipopt, mode, + .short_opt = 'm', + .metavar = "<mode>", + .typearg = map_flags_srcdst, + .help = "Filter mode; default dst"), + DEFINE_OPTION("status", OPT_BOOL, struct ipopt, print_status, + .short_opt = 's', + .help = "Print status of filtered addresses after changing"), + END_OPTIONS +}; + +static int do_ip(const void *cfg, const char *pin_root_path) +{ + int map_fd = -1, err = EXIT_SUCCESS; + char modestr[100], addrstr[100]; + const struct ipopt *opt = cfg; + struct ip_addr addr = opt->addr; + bool v6; + + print_flags(modestr, sizeof(modestr), map_flags_srcdst, opt->mode); + print_addr(addrstr, sizeof(addrstr), &opt->addr); + pr_debug("%s addr %s mode %s\n", opt->remove ? "Removing" : "Adding", + addrstr, modestr); + + v6 = (opt->addr.af == AF_INET6); + + map_fd = __do_address(pin_root_path, + v6 ? textify(MAP_NAME_IPV6) : textify(MAP_NAME_IPV4), + v6 ? "ipv6" : "ipv4", + &addr.addr, opt->remove, opt->mode); + if (map_fd < 0) { + err = map_fd; + goto out; + } + + if (opt->print_status) { + err = print_ips(); + if (err) + goto out; + } + +out: + if (map_fd >= 0) + close(map_fd); + return err; +} + +int print_ethers(int map_fd) +{ + struct mac_addr map_key = {}, prev_key = {}; + int err; + + printf("Filtered MAC addresses:\n"); + printf(" %-40s Mode Hit counter\n", ""); + FOR_EACH_MAP_KEY (err, map_fd, map_key, prev_key) { + char modebuf[100], addrbuf[100]; + __u8 flags = 0; + __u64 counter; + + err = map_get_counter_flags(map_fd, &map_key, &counter, &flags); + if (err == -ENOENT) + continue; + else if (err) + return err; + + print_flags(modebuf, sizeof(modebuf), map_flags_srcdst, flags); + print_macaddr(addrbuf, sizeof(addrbuf), &map_key); + printf(" %-40s %-15s %" PRIu64 "\n", addrbuf, modebuf, + (uint64_t)counter); + } + return 0; +} + +static const struct etheropt { + unsigned int mode; + struct mac_addr addr; + bool print_status; + bool remove; +} defaults_ether = { + .mode = MAP_FLAG_DST, +}; + +static struct prog_option ether_options[] = { + DEFINE_OPTION("addr", OPT_MACADDR, struct etheropt, addr, + .positional = true, + .metavar = "<addr>", + .required = true, + .help = "Address to add or remove"), + DEFINE_OPTION("remove", OPT_BOOL, struct etheropt, remove, + .short_opt = 'r', + .help = "Remove address instead of adding"), + DEFINE_OPTION("mode", OPT_FLAGS, struct etheropt, mode, + .short_opt = 'm', + .metavar = "<mode>", + .typearg = map_flags_srcdst, + .help = "Filter mode; default dst"), + DEFINE_OPTION("status", OPT_BOOL, struct etheropt, print_status, + .short_opt = 's', + .help = "Print status of filtered addresses after changing"), + END_OPTIONS +}; + +static int do_ether(const void *cfg, const char *pin_root_path) +{ + int err = EXIT_SUCCESS, map_fd = -1; + const struct etheropt *opt = cfg; + struct mac_addr addr = opt->addr; + char modestr[100], addrstr[100]; + + print_flags(modestr, sizeof(modestr), map_flags_srcdst, opt->mode); + print_macaddr(addrstr, sizeof(addrstr), &opt->addr); + pr_debug("%s addr %s mode %s\n", opt->remove ? "Removing" : "Adding", + addrstr, modestr); + + map_fd = __do_address(pin_root_path, textify(MAP_NAME_ETHERNET), + "ethernet", &addr.addr, opt->remove, opt->mode); + if (map_fd < 0) { + err = map_fd; + goto out; + } + + if (opt->print_status) { + err = print_ethers(map_fd); + if (err) + goto out; + } + +out: + if (map_fd >= 0) + close(map_fd); + return err; +} + +static struct prog_option status_options[] = { END_OPTIONS }; + +int print_iface_status(const struct iface *iface, struct xdp_program *prog, + enum xdp_attach_mode mode, __unused void *arg) +{ + __u32 feat = 0; + int err; + printf("%s\n", xdp_program__name(prog)); + + err = get_iface_features(iface, prog, XDP_MODE_UNSPEC, &feat); + if (err) + return err; + + if (feat) { + char featbuf[100]; + char namebuf[100]; + + print_flags(featbuf, sizeof(featbuf), print_features, feat); + snprintf(namebuf, sizeof(namebuf), "%s (%s mode)", iface->ifname, + get_enum_name(xdp_modes, mode)); + printf(" %-40s %s\n", namebuf, featbuf); + } + return 0; +} + +int do_status(__unused const void *cfg, const char *pin_root_path) +{ + int err = EXIT_SUCCESS, map_fd = -1; + struct bpf_map_info info = {}; + struct stats_record rec = {}; + + map_fd = get_pinned_map_fd(pin_root_path, textify(XDP_STATS_MAP_NAME), &info); + if (map_fd < 0) { + err = map_fd; + pr_warn("Couldn't find stats map. Maybe xdp-filter is not loaded?\n"); + goto out; + } + rec.stats[XDP_DROP].enabled = true; + rec.stats[XDP_PASS].enabled = true; + rec.stats[XDP_ABORTED].enabled = true; + + err = stats_collect(map_fd, info.type, &rec); + if (err) + goto out; + + printf("CURRENT XDP-FILTER STATUS:\n\n"); + printf("Aggregate per-action statistics:\n"); + err = stats_print_one(&rec); + if (err) + goto out; + printf("\n"); + + printf("Loaded on interfaces:\n"); + printf(" %-40s Enabled features\n", ""); + + err = iterate_pinned_programs(pin_root_path, print_iface_status, + NULL); + if (err) + goto out; + printf("\n"); + + map_fd = get_pinned_map_fd(pin_root_path, textify(MAP_NAME_PORTS), NULL); + if (map_fd >= 0) { + err = print_ports(map_fd); + if (err) + goto out; + printf("\n"); + close(map_fd); + map_fd = -1; + } + + err = print_ips(); + if (err && err != -ENOENT) + goto out; + + printf("\n"); + + map_fd = get_pinned_map_fd(pin_root_path, textify(MAP_NAME_ETHERNET), NULL); + if (map_fd >= 0) { + err = print_ethers(map_fd); + if (err) + goto out; + } + + printf("\n"); + +out: + if (map_fd >= 0) + close(map_fd); + return err; +} + +static const struct pollopt { + __u32 interval; +} defaults_poll = { .interval = 1000 }; + +static struct prog_option poll_options[] = { + DEFINE_OPTION("interval", OPT_U32, struct pollopt, interval, + .short_opt = 'i', + .metavar = "<interval>", + .help = "Polling interval in milliseconds (default 1000)"), + END_OPTIONS +}; + +int do_poll(const void *cfg, const char *pin_root_path) +{ + int err = 0, map_fd = -1; + const struct pollopt *opt = cfg; + bool exit = false; + + if (!opt->interval) { + err = -EINVAL; + pr_warn("Can't use a polling interval of 0\n"); + goto out; + } + + map_fd = get_pinned_map_fd(pin_root_path, textify(XDP_STATS_MAP_NAME), NULL); + if (map_fd < 0) { + err = map_fd; + pr_warn("Couldn't find stats map. Maybe xdp-filter is not loaded?\n"); + goto out; + } + + prog_lock_release(0); + err = stats_poll(map_fd, opt->interval, &exit, + pin_root_path, textify(XDP_STATS_MAP_NAME)); + if (err) { + pr_warn("Error polling statistics: %s\n", strerror(-err)); + goto out; + } + +out: + return err ? EXIT_FAILURE : EXIT_SUCCESS; +} + +int do_help(__unused const void *cfg, __unused const char *pin_root_path) +{ + fprintf(stderr, + "Usage: xdp-filter COMMAND [options]\n" + "\n" + "COMMAND can be one of:\n" + " load - load xdp-filter on an interface\n" + " unload - unload xdp-filter from an interface\n" + " port - add a port to the filter list\n" + " ip - add an IP address to the filter list\n" + " ether - add an Ethernet MAC address to the filter list\n" + " status - show current xdp-filter status\n" + " poll - poll statistics output\n" + " help - show this help message\n" + "\n" + "Use 'xdp-filter COMMAND --help' to see options for each command\n"); + return -1; +} + +static const struct prog_command cmds[] = { + DEFINE_COMMAND(load, "Load xdp-filter on an interface"), + DEFINE_COMMAND(unload, "Unload xdp-filter from an interface"), + DEFINE_COMMAND(port, "Add or remove ports from xdp-filter"), + DEFINE_COMMAND(ip, "Add or remove IP addresses from xdp-filter"), + DEFINE_COMMAND(ether, "Add or remove MAC addresses from xdp-filter"), + DEFINE_COMMAND(poll, "Poll xdp-filter statistics"), + DEFINE_COMMAND_NODEF(status, "Show xdp-filter status"), + { .name = "help", .func = do_help, .no_cfg = true }, + END_COMMANDS +}; + +union all_opts { + struct loadopt load; + struct unloadopt unload; + struct portopt port; + struct ipopt ip; + struct etheropt ether; + struct pollopt poll; +}; + +int main(int argc, char **argv) +{ + if (argc > 1) + return dispatch_commands(argv[1], argc - 1, argv + 1, cmds, + sizeof(union all_opts), PROG_NAME, true); + + return do_help(NULL, NULL); +} diff --git a/xdp-filter/xdpfilt_alw_all.c b/xdp-filter/xdpfilt_alw_all.c new file mode 100644 index 0000000..5d1588b --- /dev/null +++ b/xdp-filter/xdpfilt_alw_all.c @@ -0,0 +1,10 @@ +/* SPDX-License-Identifier: GPL-2.0 */ + +#define FILT_MODE_ALLOW +#define FILT_MODE_ETHERNET +#define FILT_MODE_IPV4 +#define FILT_MODE_IPV6 +#define FILT_MODE_UDP +#define FILT_MODE_TCP +#define FUNCNAME xdpfilt_alw_all +#include "xdpfilt_prog.h" diff --git a/xdp-filter/xdpfilt_alw_eth.c b/xdp-filter/xdpfilt_alw_eth.c new file mode 100644 index 0000000..62327bd --- /dev/null +++ b/xdp-filter/xdpfilt_alw_eth.c @@ -0,0 +1,10 @@ +/* SPDX-License-Identifier: GPL-2.0 */ + +#define FILT_MODE_ALLOW +#define FILT_MODE_ETHERNET +#undef FILT_MODE_IPV4 +#undef FILT_MODE_IPV6 +#undef FILT_MODE_UDP +#undef FILT_MODE_TCP +#define FUNCNAME xdpfilt_alw_eth +#include "xdpfilt_prog.h" diff --git a/xdp-filter/xdpfilt_alw_ip.c b/xdp-filter/xdpfilt_alw_ip.c new file mode 100644 index 0000000..3b7f72f --- /dev/null +++ b/xdp-filter/xdpfilt_alw_ip.c @@ -0,0 +1,10 @@ +/* SPDX-License-Identifier: GPL-2.0 */ + +#define FILT_MODE_ALLOW +#undef FILT_MODE_ETHERNET +#define FILT_MODE_IPV4 +#define FILT_MODE_IPV6 +#undef FILT_MODE_UDP +#undef FILT_MODE_TCP +#define FUNCNAME xdpfilt_alw_ip +#include "xdpfilt_prog.h" diff --git a/xdp-filter/xdpfilt_alw_tcp.c b/xdp-filter/xdpfilt_alw_tcp.c new file mode 100644 index 0000000..2c32ec3 --- /dev/null +++ b/xdp-filter/xdpfilt_alw_tcp.c @@ -0,0 +1,10 @@ +/* SPDX-License-Identifier: GPL-2.0 */ + +#define FILT_MODE_ALLOW +#undef FILT_MODE_ETHERNET +#undef FILT_MODE_IPV4 +#undef FILT_MODE_IPV6 +#undef FILT_MODE_UDP +#define FILT_MODE_TCP +#define FUNCNAME xdpfilt_alw_tcp +#include "xdpfilt_prog.h" diff --git a/xdp-filter/xdpfilt_alw_udp.c b/xdp-filter/xdpfilt_alw_udp.c new file mode 100644 index 0000000..d9b7cf3 --- /dev/null +++ b/xdp-filter/xdpfilt_alw_udp.c @@ -0,0 +1,10 @@ +/* SPDX-License-Identifier: GPL-2.0 */ + +#define FILT_MODE_ALLOW +#undef FILT_MODE_ETHERNET +#undef FILT_MODE_IPV4 +#undef FILT_MODE_IPV6 +#define FILT_MODE_UDP +#undef FILT_MODE_TCP +#define FUNCNAME xdpfilt_alw_udp +#include "xdpfilt_prog.h" diff --git a/xdp-filter/xdpfilt_dny_all.c b/xdp-filter/xdpfilt_dny_all.c new file mode 100644 index 0000000..05e006a --- /dev/null +++ b/xdp-filter/xdpfilt_dny_all.c @@ -0,0 +1,10 @@ +/* SPDX-License-Identifier: GPL-2.0 */ + +#define FILT_MODE_DENY +#define FILT_MODE_ETHERNET +#define FILT_MODE_IPV4 +#define FILT_MODE_IPV6 +#define FILT_MODE_UDP +#define FILT_MODE_TCP +#define FUNCNAME xdpfilt_dny_all +#include "xdpfilt_prog.h" diff --git a/xdp-filter/xdpfilt_dny_eth.c b/xdp-filter/xdpfilt_dny_eth.c new file mode 100644 index 0000000..237715c --- /dev/null +++ b/xdp-filter/xdpfilt_dny_eth.c @@ -0,0 +1,10 @@ +/* SPDX-License-Identifier: GPL-2.0 */ + +#define FILT_MODE_DENY +#define FILT_MODE_ETHERNET +#undef FILT_MODE_IPV4 +#undef FILT_MODE_IPV6 +#undef FILT_MODE_UDP +#undef FILT_MODE_TCP +#define FUNCNAME xdpfilt_dny_eth +#include "xdpfilt_prog.h" diff --git a/xdp-filter/xdpfilt_dny_ip.c b/xdp-filter/xdpfilt_dny_ip.c new file mode 100644 index 0000000..94447ea --- /dev/null +++ b/xdp-filter/xdpfilt_dny_ip.c @@ -0,0 +1,10 @@ +/* SPDX-License-Identifier: GPL-2.0 */ + +#define FILT_MODE_DENY +#undef FILT_MODE_ETHERNET +#define FILT_MODE_IPV4 +#define FILT_MODE_IPV6 +#undef FILT_MODE_UDP +#undef FILT_MODE_TCP +#define FUNCNAME xdpfilt_dny_ip +#include "xdpfilt_prog.h" diff --git a/xdp-filter/xdpfilt_dny_tcp.c b/xdp-filter/xdpfilt_dny_tcp.c new file mode 100644 index 0000000..d8a78b4 --- /dev/null +++ b/xdp-filter/xdpfilt_dny_tcp.c @@ -0,0 +1,10 @@ +/* SPDX-License-Identifier: GPL-2.0 */ + +#define FILT_MODE_DENY +#undef FILT_MODE_ETHERNET +#undef FILT_MODE_IPV4 +#undef FILT_MODE_IPV6 +#undef FILT_MODE_UDP +#define FILT_MODE_TCP +#define FUNCNAME xdpfilt_dny_tcp +#include "xdpfilt_prog.h" diff --git a/xdp-filter/xdpfilt_dny_udp.c b/xdp-filter/xdpfilt_dny_udp.c new file mode 100644 index 0000000..2393232 --- /dev/null +++ b/xdp-filter/xdpfilt_dny_udp.c @@ -0,0 +1,10 @@ +/* SPDX-License-Identifier: GPL-2.0 */ + +#define FILT_MODE_DENY +#undef FILT_MODE_ETHERNET +#undef FILT_MODE_IPV4 +#undef FILT_MODE_IPV6 +#define FILT_MODE_UDP +#undef FILT_MODE_TCP +#define FUNCNAME xdpfilt_dny_udp +#include "xdpfilt_prog.h" diff --git a/xdp-filter/xdpfilt_prog.h b/xdp-filter/xdpfilt_prog.h new file mode 100644 index 0000000..fbc781e --- /dev/null +++ b/xdp-filter/xdpfilt_prog.h @@ -0,0 +1,257 @@ +/* SPDX-License-Identifier: GPL-2.0 */ + +/* XDP filter program fragment. This header file contains the full-featured + * program, split up with ifdefs. The actual program files xdpfilt_*.c + * include this file with different #defines to create the + * different eBPF program sections that include only the needed features. + */ + +#ifndef __XDPFILT_PROG_H +#define __XDPFILT_PROG_H + +#include <linux/bpf.h> +#include <linux/in.h> +#include <bpf/bpf_helpers.h> +#include <xdp/xdp_helpers.h> + +#include "common_kern_user.h" + +/* Defines xdp_stats_map */ +#include "xdp/xdp_stats_kern.h" +#include "xdp/parsing_helpers.h" + +#ifdef FILT_MODE_DENY +#define VERDICT_HIT XDP_PASS +#define VERDICT_MISS XDP_DROP +#define FEATURE_OPMODE FEAT_DENY +#else +#define VERDICT_HIT XDP_DROP +#define VERDICT_MISS XDP_PASS +#define FEATURE_OPMODE FEAT_ALLOW +#endif + +#define CHECK_RET(ret) \ + do { \ + if ((ret) < 0) { \ + action = XDP_ABORTED; \ + goto out; \ + } \ + } while (0) + +#define CHECK_VERDICT(type, param) \ + do { \ + if ((action = lookup_verdict_##type(param)) != VERDICT_MISS) \ + goto out; \ + } while (0) + +#define CHECK_MAP(map, key, mask) \ + do { \ + __u64 *value; \ + value = bpf_map_lookup_elem(map, key); \ + if ((value) && (*(value) & (mask)) == (mask)) { \ + *value += (1 << COUNTER_SHIFT); \ + return VERDICT_HIT; \ + } \ + } while (0) + +#if defined(FILT_MODE_TCP) || defined(FILT_MODE_UDP) +struct { + __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY); + __uint(max_entries, 65536); + __type(key, __u32); + __type(value, __u64); + __uint(pinning, LIBBPF_PIN_BY_NAME); +} MAP_NAME_PORTS SEC(".maps"); + +#ifdef FILT_MODE_TCP +static int __always_inline lookup_verdict_tcp(struct tcphdr *tcphdr) +{ + __u32 key; + + key = tcphdr->dest; + CHECK_MAP(&filter_ports, &key, MAP_FLAG_DST | MAP_FLAG_TCP); + key = tcphdr->source; + CHECK_MAP(&filter_ports, &key, MAP_FLAG_SRC | MAP_FLAG_TCP); + return VERDICT_MISS; +} +#define FEATURE_TCP FEAT_TCP +#else +#define FEATURE_TCP 0 +#endif + +#ifdef FILT_MODE_UDP +static int __always_inline lookup_verdict_udp(struct udphdr *udphdr) +{ + __u32 key; + + key = udphdr->dest; + CHECK_MAP(&filter_ports, &key, MAP_FLAG_DST | MAP_FLAG_UDP); + key = udphdr->source; + CHECK_MAP(&filter_ports, &key, MAP_FLAG_SRC | MAP_FLAG_UDP); + return VERDICT_MISS; +} +#define FEATURE_UDP FEAT_UDP +#else +#define FEATURE_UDP 0 +#endif + +#else +#define FEATURE_UDP 0 +#define FEATURE_TCP 0 +#endif /* TCP || UDP */ + +#ifdef FILT_MODE_IPV4 +struct { + __uint(type, BPF_MAP_TYPE_PERCPU_HASH); + __uint(max_entries, 10000); + __type(key, __u32); + __type(value, __u64); + __uint(pinning, LIBBPF_PIN_BY_NAME); +} MAP_NAME_IPV4 SEC(".maps"); + +static int __always_inline lookup_verdict_ipv4(struct iphdr *iphdr) +{ + __u32 addr; + addr = iphdr->daddr; + CHECK_MAP(&filter_ipv4, &addr, MAP_FLAG_DST); + addr = iphdr->saddr; + CHECK_MAP(&filter_ipv4, &addr, MAP_FLAG_SRC); + return VERDICT_MISS; +} + +#define CHECK_VERDICT_IPV4(param) CHECK_VERDICT(ipv4, param) +#define FEATURE_IPV4 FEAT_IPV4 +#else +#define FEATURE_IPV4 0 +#define CHECK_VERDICT_IPV4(param) +#endif /* FILT_MODE_IPV4 */ + +#ifdef FILT_MODE_IPV6 +struct { + __uint(type, BPF_MAP_TYPE_PERCPU_HASH); + __uint(max_entries, 10000); + __type(key, struct in6_addr); + __type(value, __u64); + __uint(pinning, LIBBPF_PIN_BY_NAME); +} MAP_NAME_IPV6 SEC(".maps"); + +static int __always_inline lookup_verdict_ipv6(struct ipv6hdr *ipv6hdr) +{ + struct in6_addr addr; + + addr = ipv6hdr->daddr; + CHECK_MAP(&filter_ipv6, &addr, MAP_FLAG_DST); + addr = ipv6hdr->saddr; + CHECK_MAP(&filter_ipv6, &addr, MAP_FLAG_SRC); + return VERDICT_MISS; +} + +#define CHECK_VERDICT_IPV6(param) CHECK_VERDICT(ipv6, param) +#define FEATURE_IPV6 FEAT_IPV6 +#else +#define FEATURE_IPV6 0 +#define CHECK_VERDICT_IPV6(param) +#endif /* FILT_MODE_IPV6 */ + +#ifdef FILT_MODE_ETHERNET +struct ethaddr { + __u8 addr[ETH_ALEN]; +}; + +struct { + __uint(type, BPF_MAP_TYPE_PERCPU_HASH); + __uint(max_entries, 10000); + __type(key, struct ethaddr); + __type(value, __u64); + __uint(pinning, LIBBPF_PIN_BY_NAME); +} MAP_NAME_ETHERNET SEC(".maps"); + +static int __always_inline lookup_verdict_ethernet(struct ethhdr *eth) +{ + struct ethaddr addr = {}; + + __builtin_memcpy(&addr, eth->h_dest, sizeof(addr)); + CHECK_MAP(&filter_ethernet, &addr, MAP_FLAG_DST); + __builtin_memcpy(&addr, eth->h_source, sizeof(addr)); + CHECK_MAP(&filter_ethernet, &addr, MAP_FLAG_SRC); + return VERDICT_MISS; +} + +#define CHECK_VERDICT_ETHERNET(param) CHECK_VERDICT(ethernet, param) +#define FEATURE_ETHERNET FEAT_ETHERNET +#else +#define FEATURE_ETHERNET 0 +#define CHECK_VERDICT_ETHERNET(param) +#endif /* FILT_MODE_ETHERNET */ + +#ifndef FUNCNAME +#define FUNCNAME xdp_filt_unknown +#endif + +struct { + __uint(priority, 10); + __uint(XDP_PASS, 1); +} XDP_RUN_CONFIG(FUNCNAME); + +SEC("xdp") +int FUNCNAME(struct xdp_md *ctx) +{ + void *data_end = (void *)(long)ctx->data_end; + void *data = (void *)(long)ctx->data; + __u32 action = VERDICT_MISS; /* Default action */ + struct hdr_cursor nh; + struct ethhdr *eth; + int eth_type; + + nh.pos = data; + eth_type = parse_ethhdr(&nh, data_end, ð); + CHECK_RET(eth_type); + CHECK_VERDICT_ETHERNET(eth); + +#if defined(FILT_MODE_IPV4) || defined(FILT_MODE_IPV6) || \ + defined(FILT_MODE_TCP) || defined(FILT_MODE_UDP) + struct iphdr *iphdr; + struct ipv6hdr *ipv6hdr; + int ip_type; + if (eth_type == bpf_htons(ETH_P_IP)) { + ip_type = parse_iphdr(&nh, data_end, &iphdr); + CHECK_RET(ip_type); + + CHECK_VERDICT_IPV4(iphdr); + } else if (eth_type == bpf_htons(ETH_P_IPV6)) { + ip_type = parse_ip6hdr(&nh, data_end, &ipv6hdr); + CHECK_RET(ip_type); + + CHECK_VERDICT_IPV6(ipv6hdr); + } else { + goto out; + } + +#ifdef FILT_MODE_UDP + struct udphdr *udphdr; + if (ip_type == IPPROTO_UDP) { + CHECK_RET(parse_udphdr(&nh, data_end, &udphdr)); + CHECK_VERDICT(udp, udphdr); + } +#endif /* FILT_MODE_UDP */ + +#ifdef FILT_MODE_TCP + struct tcphdr *tcphdr; + if (ip_type == IPPROTO_TCP) { + CHECK_RET(parse_tcphdr(&nh, data_end, &tcphdr)); + CHECK_VERDICT(tcp, tcphdr); + } +#endif /* FILT_MODE_TCP*/ +#endif /* FILT_MODE_{IPV4,IPV6,TCP,UDP} */ +out: + return xdp_stats_record_action(ctx, action); +} + +char _license[] SEC("license") = "GPL"; +__u32 _features SEC("features") = (FEATURE_ETHERNET | FEATURE_IPV4 | + FEATURE_IPV6 | FEATURE_UDP | FEATURE_TCP | + FEATURE_OPMODE); + +#else +#error "Multiple includes of xdpfilt_prog.h" +#endif // include guard |