qa: add nfq based firewall test with live reload

(cherry picked from commit 49b1382a8b)

Include suricata config not part of original commit.

Ticket: #8409.
pull/15218/head
Victor Julien 6 months ago
parent f53a5d3b7a
commit 3239f6b24f

@ -1894,6 +1894,121 @@ jobs:
- run: |
./qa/unix.sh "suricata-verify/"
ubuntu-latest-namespace-ips:
name: Ubuntu 24.04 (IPS tests in namespaces)
runs-on: ubuntu-latest
needs: [prepare-deps, prepare-cbindgen]
container:
image: ubuntu:24.04
options: --privileged
steps:
- name: Cache ~/.cargo
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb
with:
path: ~/.cargo/registry
key: cargo-registry
- name: Determine number of CPUs
run: echo CPUS=$(nproc --all) >> $GITHUB_ENV
- name: Install dependencies
run: |
apt update
apt -y install \
libpcre2-dev \
build-essential \
autoconf \
automake \
llvm-19-dev \
clang-19 \
git \
hping3 \
hwloc \
libhwloc-dev \
jq \
inetutils-ping \
libc++-dev \
libc++abi-dev \
libtool \
libpcap-dev \
libnet1-dev \
libyaml-0-2 \
libyaml-dev \
libcap-ng-dev \
libcap-ng0 \
libmagic-dev \
libnetfilter-queue-dev \
libnetfilter-queue1 \
libnfnetlink-dev \
libnfnetlink0 \
libnuma-dev \
libhiredis-dev \
libjansson-dev \
libevent-dev \
libevent-pthreads-2.1-7 \
make \
parallel \
python3-yaml \
software-properties-common \
sudo \
zlib1g \
zlib1g-dev \
exuberant-ctags \
unzip \
curl \
time \
wget \
caddy \
ethtool \
iproute2 \
iptables \
tshark
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
- uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3
with:
name: prep
path: prep
# packaged Rust version is too old for coverage, so get from rustup. 1.85.1 matches
# LLVM 19
- name: Install Rust
run: curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain 1.85.1 -y
- uses: ./.github/actions/install-cbindgen
- run: ./autogen.sh
- run: ./configure --disable-shared --localstatedir=/var --prefix=/usr --sysconfdir=/etc --enable-nfqueue
env:
CC: "clang-19"
CXX: "clang++-19"
RUSTFLAGS: "-C instrument-coverage"
CFLAGS: "-fprofile-instr-generate -fcoverage-mapping -O0"
CXXFLAGS: "-fprofile-instr-generate -fcoverage-mapping -O0"
- run: make -j ${{ env.CPUS }}
env:
CC: "clang-19"
CXX: "clang++-19"
RUSTFLAGS: "-C instrument-coverage"
CFLAGS: "-fprofile-instr-generate -fcoverage-mapping -O0"
CXXFLAGS: "-fprofile-instr-generate -fcoverage-mapping -O0"
- run: |
./qa/live/netns/nfq-fw-netns-route.sh "autofp" "qa/live/netns/fw-netns.yaml"
env:
LLVM_PROFILE_FILE: "/tmp/nfq-fw-netns-route.profraw"
- run: llvm-profdata-19 merge -o nfq-fw-netns-route.profdata /tmp/nfq-fw-netns-route.profraw
- run: llvm-profdata-19 merge -o combined.profdata nfq-fw-netns-route.profdata
- run: llvm-cov-19 export ./src/suricata -instr-profile=combined.profdata -format=lcov --ignore-filename-regex="^(/github/home/.cargo/.*|/usr/.*|/rustc/.*)" --skip-branches > coverage.lcov
- name: Upload coverage.lcov artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f
with:
name: coverage-lcov-${{ github.job }}
path: coverage.lcov
- name: Upload coverage to Codecov
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de
with:
fail_ci_if_error: true
files: coverage.lcov
flags: netns
verbose: true
ubuntu-24-04-asan-afpdpdk:
name: Ubuntu 24.04 (afpacket and dpdk live tests with ASAN)
runs-on: ubuntu-latest

@ -0,0 +1,13 @@
# allow session setup
accept:hook tcp:all any any <> any 80 (flow:not_established; alert; sid:1021;)
# pass rest of the flow to
accept:hook tcp:all any any <> any 80 (flow:established; alert; sid:1023;)
#accept:hook ip:all any any <> any any (alert; sid:1024;)
# default drop
accept:hook http1:request_started any any -> any any (alert; sid:100;)
accept:hook http1:request_line any any -> any any (http.method; content:"GET"; http.uri; content:"/"; alert; sid:101;)
accept:tx http1:request_headers any any -> any any (http.user_agent; content:"wget"; nocase; alert; sid:102;)

@ -0,0 +1,13 @@
# allow session setup
accept:hook tcp:all any any <> any 80 (flow:not_established; alert; sid:1021;)
# pass rest of the flow to
accept:hook tcp:all any any <> any 80 (flow:established; alert; sid:1023;)
#accept:hook ip:all any any <> any any (alert; sid:1024;)
# default drop
accept:hook http1:request_started any any -> any any (alert; sid:100;)
accept:hook http1:request_line any any -> any any (http.method; bsize:3; urilen:>1; sid:201; alert;)
accept:tx http1:request_headers any any -> any any (http.user_agent; pcre:"/wget/i"; sid:202; alert;)

@ -0,0 +1,131 @@
%YAML 1.1
---
# Suricata configuration file. In addition to the comments describing all
# options in this file, full documentation can be found at:
# https://docs.suricata.io/en/latest/configuration/suricata-yaml.html
# This configuration file was generated by Suricata 9.0.0-dev.
suricata-version: "9.0"
##
## Step 1: Inform Suricata about your network
##
vars:
# more specific is better for alert accuracy and performance
address-groups:
HOME_NET: "[192.168.0.0/16,10.0.0.0/8,172.16.0.0/12]"
#HOME_NET: "[192.168.0.0/16]"
#HOME_NET: "[10.0.0.0/8]"
#HOME_NET: "[172.16.0.0/12]"
#HOME_NET: "any"
EXTERNAL_NET: "!$HOME_NET"
#EXTERNAL_NET: "any"
HTTP_SERVERS: "$HOME_NET"
SMTP_SERVERS: "$HOME_NET"
SQL_SERVERS: "$HOME_NET"
DNS_SERVERS: "$HOME_NET"
TELNET_SERVERS: "$HOME_NET"
AIM_SERVERS: "$EXTERNAL_NET"
DC_SERVERS: "$HOME_NET"
DNP3_SERVER: "$HOME_NET"
DNP3_CLIENT: "$HOME_NET"
MODBUS_CLIENT: "$HOME_NET"
MODBUS_SERVER: "$HOME_NET"
ENIP_CLIENT: "$HOME_NET"
ENIP_SERVER: "$HOME_NET"
port-groups:
HTTP_PORTS: "80"
SHELLCODE_PORTS: "!80"
ORACLE_PORTS: 1521
SSH_PORTS: 22
DNP3_PORTS: 20000
MODBUS_PORTS: 502
FILE_DATA_PORTS: "[$HTTP_PORTS,110,143]"
FTP_PORTS: 21
GENEVE_PORTS: 6081
VXLAN_PORTS: 4789
TEREDO_PORTS: 3544
SIP_PORTS: "[5060, 5061]"
##
## Step 2: Select outputs to enable
##
# Global stats configuration
stats:
enabled: yes
# The interval field (in seconds) controls the interval at
# which stats are updated in the log.
interval: 8
# Add decode events to stats.
decoder-events: true
# Decoder event prefix in stats. Has been 'decoder' before, but that leads
# to missing events in the eve.stats records. See issue #2225.
#decoder-events-prefix: "decoder.event"
# Add stream events as stats.
stream-events: true
exception-policy:
per-app-proto-errors: true # default: false. True will log errors for
# each app-proto. Warning: VERY verbose
outputs:
- eve-log:
enabled: yes
filetype: regular #regular|syslog|unix_dgram|unix_stream|redis
filename: eve.json
ethernet: true # log ethernet header in events when available
types:
- alert
- http:
extended: yes # enable this for extended logging information
- dns
- tls:
extended: yes # enable this for extended logging information
- files:
force-magic: no # force logging magic on all logged files
# force logging of checksums, available hash functions are md5,
# sha1 and sha256
#force-hash: [md5]
- drop:
alerts: yes # log alerts that caused drops
flows: all # start or all: 'start' logs only a single drop
verdict: yes
- stats:
totals: yes # stats for all threads merged together
threads: no # per thread stats
deltas: no # include delta values
# Don't log stats counters that are zero. Default: true
#null-values: false # False will NOT log stats counters: 0
- flow
unix-command:
enabled: auto
af-packet:
- interface: ptp-dut_client
cluster-id: 1
copy-iface: ptp-dut_server
- interface: ptp-dut_server
cluster-id: 2
copy-iface: ptp-dut_client
- interface: default
threads: 2
cluster-type: cluster_flow
copy-mode: ips
firewall:
# toggle to enable firewall mode
enabled: yes
# Firewall rule file are in their own path and are not managed
# by Suricata-Update.
rule-path: /etc/suricata/firewall/
# List of files with firewall rules. Order matters, files are loaded
# in order and rules are applied in that order (per state, see docs)
rule-files:
- firewall.rules

@ -0,0 +1,324 @@
#!/bin/bash
# Script to test live Firewall capabilities for NFQ.
#
# Uses 3 network namespaces:
# - client
# - server
# - dut
#
# Dut is where Suricata will run:
#
# [ client ]$clientif - $dutclientif[ dut ]$dutserverif - $serverif[ server ]
#
# By routing packets between the dut interfaces, Suricata becomes the router.
# Packets will be forwarded by the kernel, sent to Suricata via iptables NFQUEUE
# which can then verdict them.
# Call with following arguments:
# 1st: runmode string (single/autofp/workers)
# 2nd: suricata yaml to use
set -e
set -x
if [ $# -ne "2" ]; then
echo "ERROR call with 2 args: runmode (single/autofp/workers) and yaml"
exit 1;
fi
RUNMODE=$1
YAML=$2
# dump some info
echo "* printing some diagnostics..."
ip netns list
uname -a
ip r
echo "* printing some diagnostics... done"
clientns=client
serverns=server
dutns=dut
clientip="10.10.10.2/24"
clientnet="10.10.10.0/24"
serverip='10.10.20.2/24'
servernet="10.10.20.0/24"
dutclientip="10.10.10.1/24"
dutserverip='10.10.20.1/24'
clientif=client
serverif=server
dutclientif=dut_client
dutserverif=dut_server
echo "* removing old namespaces..."
NAMESPACES=$(ip netns list|cut -d' ' -f1)
for NS in $NAMESPACES; do
if [ $NS = $dutns ] || [ $NS = $clientns ] || [ $NS = $serverns ]; then
ip netns delete $NS
fi
done
echo "* removing old namespaces... done"
# remove eve.json from previous run
if [ -f eve.json ]; then
rm eve.json
fi
if [ -e ./rust/target/release/suricatasc ]; then
SURICATASC=./rust/target/release/suricatasc
else
SURICATASC=./rust/target/debug/suricatasc
fi
RES=0
# adding namespaces
echo "* creating namespaces..."
ip netns add $clientns
ip netns add $serverns
ip netns add $dutns
echo "* creating namespaces... done"
#diagnostics output
echo "* list namespaces..."
ip netns list
ip netns exec $clientns ip ad
ip netns exec $serverns ip ad
ip netns exec $dutns ip ad
echo "* list namespaces... done"
# create virtual ethernet link between client-dut and server-dut
# These are not yet mapped to a namespace
echo "* creating virtual ethernet devices..."
ip link add ptp-$clientif type veth peer name ptp-$dutclientif
ip link add ptp-$serverif type veth peer name ptp-$dutserverif
echo "* creating virtual ethernet devices...done"
echo "* list interface in global namespace..."
ip link
echo "* list interface in global namespace... done"
echo "* map virtual ethernet interfaces to their namespaces..."
ip link set ptp-$clientif netns $clientns
ip link set ptp-$serverif netns $serverns
ip link set ptp-$dutclientif netns $dutns
ip link set ptp-$dutserverif netns $dutns
echo "* map virtual ethernet interfaces to their namespaces... done"
echo "* list namespaces and interfaces within them..."
ip netns list
ip netns exec $clientns ip ad
ip netns exec $serverns ip ad
ip netns exec $dutns ip ad
echo "* list namespaces and interfaces within them... done"
# bring up interfaces. Client and server get IP's.
# Disable rx and tx csum offload on all sides.
echo "* setup client interface..."
iface=ptp-$clientif
ip netns exec $clientns ip addr add $clientip dev $iface
ip netns exec $clientns ip link set $iface up
echo "* setup client interface... done"
echo "* setup server interface..."
iface=ptp-$serverif
ip netns exec $serverns ip addr add $serverip dev $iface
ip netns exec $serverns ip link set $iface up
echo "* setup server interface... done"
echo "* setup dut interfaces..."
ip netns exec $dutns ip addr add $dutclientip dev ptp-$dutclientif
ip netns exec $dutns ip addr add $dutserverip dev ptp-$dutserverif
ip netns exec $dutns ip link set ptp-$dutclientif up
ip netns exec $dutns ip link set ptp-$dutserverif up
echo "* setup dut interfaces... done"
echo "* setup client/server routes..."
# routes:
#
# client can reach servernet through the client side ip of the dut
via_ip=$(echo $dutclientip|cut -f1 -d'/')
ip netns exec $clientns ip route add $servernet via $via_ip dev ptp-$clientif
#
# server can reach clientnet through the server side ip of the dut
via_ip=$(echo $dutserverip|cut -f1 -d'/')
ip netns exec $serverns ip route add $clientnet via $via_ip dev ptp-$serverif
echo "* setup client/server routes... done"
echo "* enabling forwarding in the dut..."
# forward all
ip netns exec $dutns sysctl net.ipv4.ip_forward=1
ip netns exec $dutns iptables -I FORWARD 1 -j NFQUEUE
echo "* enabling forwarding in the dut... done"
# set first rule file
cp qa/live/netns/firewall1-l3.rules firewall.rules
RULES="firewall.rules"
cat firewall.rules
echo "* starting Suricata in the \"dut\" namespace..."
# Start Suricata in the dut namespace, then SIGINT after 240 secords. Will
# close it earlier through the unix socket.
timeout --kill-after=300 --preserve-status 240 \
ip netns exec $dutns \
./src/suricata -c $YAML -l ./ -q 0 -v \
--set firewall.rule-path=. \
--set default-rule-path=. --runmode=$RUNMODE &
SURIPID=$!
sleep 10
echo "* starting Suricata... done"
echo "* starting tshark on in the server namespace..."
timeout --kill-after=240 --preserve-status 180 \
ip netns exec $serverns \
tshark -i ptp-$serverif -T json > tshark-server.json &
TSHARKSERVERPID=$!
sleep 5
echo "* starting tshark on in the server namespace... done, pid $TSHARKSERVERPID"
echo "* starting Caddy..."
# Start Caddy in the server namespace
timeout --kill-after=480 --preserve-status 240 \
ip netns exec $serverns \
caddy file-server --browse &
CADDYPID=$!
sleep 10
echo "* starting Caddy in the \"server\" namespace... done"
echo "* running curl in the \"client\" namespace..."
set +e
timeout --kill-after=30 --preserve-status 15 \
ip netns exec $clientns \
curl -O http://10.10.20.2/index.html
CURLRES=$?
set -e
echo "* running curl in the \"client\" namespace... done: $CURLRES"
echo "* running wget in the \"client\" namespace..."
set +e
timeout --kill-after=30 --preserve-status 15 \
ip netns exec $clientns \
wget http://10.10.20.2/index.html
WGETRES=$?
set -e
echo "* running wget in the \"client\" namespace... done"
ping_ip=$(echo $serverip|cut -f1 -d'/')
echo "* running ping $ping_ip in the \"client\" namespace..."
set +e
ip netns exec $clientns \
ping -c 10 $ping_ip
PINGRES=$?
set -e
echo "* running ping in the \"client\" namespace... done"
# pings should have been dropped, so ping reports error
if [ $PINGRES != 1 ]; then
echo "ERROR ping should have failed"
RES=1
fi
# first rulefile:
# expecting 2 of 101 because of the curl and wget requests
# expecting 1 of 102 because only wget is accepted
SID101=$(jq -c 'select(.alert.signature_id==101)' ./eve.json | wc -l)
SID102=$(jq -c 'select(.alert.signature_id==102)' ./eve.json | wc -l)
echo "SID101 $SID101 SID102 $SID102"
if [ $SID101 -ne 2 ]; then
echo "ERROR wrong alert count for sid 101: $SID101"
RES=1
fi
if [ $SID102 -ne 1 ]; then
echo "ERROR wrong alert count for sid 102: $SID102"
RES=1
fi
echo "* installing new firewall rules..."
cp qa/live/netns/firewall2-l3.rules firewall.rules
cat firewall.rules
echo "* installing new firewall rules... done"
echo "* issuing rule reload..."
ip netns exec $dutns \
${SURICATASC} -c "reload-rules" /var/run/suricata/suricata-command.socket
# give suricata time to reload
sleep 10
echo "* issuing rule reload... done"
echo "* running wget in the \"client\" namespace..."
set +e
timeout --kill-after=30 --preserve-status 15 \
ip netns exec $clientns \
wget http://10.10.20.2/index.html
WGETRES=$?
set -e
echo "* running wget in the \"client\" namespace... done"
sleep 10
echo "* shutting down tshark..."
kill -INT $TSHARKSERVERPID
wait $TSHARKSERVERPID
echo "* shutting down tshark... done"
# second rulefile (after reload)
SID201=$(jq -c 'select(.alert.signature_id==201)' ./eve.json | wc -l)
SID202=$(jq -c 'select(.alert.signature_id==202)' ./eve.json | wc -l)
echo "SID201 $SID201 SID202 $SID202"
ACCEPTED=$(jq -c 'select(.event_type == "stats")' ./eve.json | tail -n1 | jq '.stats.ips.accepted')
BLOCKED=$(jq -c 'select(.event_type == "stats")' ./eve.json | tail -n1 | jq '.stats.ips.blocked')
echo "ACCEPTED $ACCEPTED BLOCKED $BLOCKED"
if [ $ACCEPTED -eq 0 ]; then
echo "ERROR should have seen non-0 accepted"
RES=1
fi
if [ $BLOCKED -lt 10 ]; then
echo "ERROR should have seen 10+ blocked"
RES=1
fi
if [ $SID201 -ne 1 ]; then
echo "ERROR wrong alert count for sid 201: $SID201"
RES=1
fi
if [ $SID202 -ne 1 ]; then
echo "ERROR wrong alert count for sid 202: $SID202"
RES=1
fi
# validate that we didn't receive pings
SERVER_RECV_PING=$(jq -c '.[]' ./tshark-server.json|jq 'select(._source.layers.icmp."icmp.type"=="8")'|wc -l)
echo "* server pings received check (should be 0): $SERVER_RECV_PING"
if [ $SERVER_RECV_PING -ne 0 ]; then
jq '.[]' ./tshark-server.json | jq 'select(._source.layers.icmp)'
RES=1
fi
echo "* server pings received check... done"
echo "* shutting down..."
set +e
kill -INT $CADDYPID
wait $CADDYPID
CADDYRES=$?
set -e
ip netns exec $dutns \
${SURICATASC} -c "shutdown" /var/run/suricata/suricata-command.socket
wait $SURIPID
echo "* shutting down... done"
# Caddy sometimes exits uncleanly. Warn about it but otherwise
# it can be ignored.
if [ $CADDYRES -ne 0 ]; then
echo "WARNING Caddy exited with error $CADDYRES"
fi
echo "* dumping some stats..."
cat ./eve.json | jq -c 'select(.http)'|tail -n1|jq
cat ./eve.json | jq -c 'select(.stats)|.stats.ips'|tail -n1|jq
echo "* dumping some stats... done"
echo "* done: $RES"
exit $RES
Loading…
Cancel
Save