From 3239f6b24fa4df8867bf72c334102d24e450de28 Mon Sep 17 00:00:00 2001 From: Victor Julien Date: Thu, 26 Mar 2026 10:31:21 +0100 Subject: [PATCH] qa: add nfq based firewall test with live reload (cherry picked from commit 49b1382a8b413b2da67cac17c18ff296a632d6d3) Include suricata config not part of original commit. Ticket: #8409. --- .github/workflows/builds.yml | 115 ++++++++++ qa/live/netns/firewall1-l3.rules | 13 ++ qa/live/netns/firewall2-l3.rules | 13 ++ qa/live/netns/fw-netns.yaml | 131 +++++++++++ qa/live/netns/nfq-fw-netns-route.sh | 324 ++++++++++++++++++++++++++++ 5 files changed, 596 insertions(+) create mode 100644 qa/live/netns/firewall1-l3.rules create mode 100644 qa/live/netns/firewall2-l3.rules create mode 100644 qa/live/netns/fw-netns.yaml create mode 100755 qa/live/netns/nfq-fw-netns-route.sh diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml index c80516fe7f..3afe164d21 100644 --- a/.github/workflows/builds.yml +++ b/.github/workflows/builds.yml @@ -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 diff --git a/qa/live/netns/firewall1-l3.rules b/qa/live/netns/firewall1-l3.rules new file mode 100644 index 0000000000..78f41a6290 --- /dev/null +++ b/qa/live/netns/firewall1-l3.rules @@ -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;) + diff --git a/qa/live/netns/firewall2-l3.rules b/qa/live/netns/firewall2-l3.rules new file mode 100644 index 0000000000..3722a83be6 --- /dev/null +++ b/qa/live/netns/firewall2-l3.rules @@ -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;) + diff --git a/qa/live/netns/fw-netns.yaml b/qa/live/netns/fw-netns.yaml new file mode 100644 index 0000000000..c899941429 --- /dev/null +++ b/qa/live/netns/fw-netns.yaml @@ -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 diff --git a/qa/live/netns/nfq-fw-netns-route.sh b/qa/live/netns/nfq-fw-netns-route.sh new file mode 100755 index 0000000000..060211cdfd --- /dev/null +++ b/qa/live/netns/nfq-fw-netns-route.sh @@ -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