Added ocserv-fw for nftables

This also introduces a basic functional test for ocserv-fw.

Resolves: #397

Signed-off-by: Nikos Mavrogiannopoulos <n.mavrogiannopoulos@gmail.com>
This commit is contained in:
Nikos Mavrogiannopoulos
2026-03-29 16:55:12 +02:00
parent e21359716b
commit 598bcf405e
10 changed files with 607 additions and 6 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
[codespell]
skip = ./.git,aclocal.m4,./autom4te.cache,./build-aux,config.log,ccan,pcl,*.pem
skip = ./.git,aclocal.m4,./autom4te.cache,./build-aux,config.log,ccan,pcl,*.pem,./src/ocserv-fw-nftables,./tests/test-fw-script
ignore-words-list = alloced,ans,anull,bu,gir,siz,te
uri-ignore-words-list = gord,ist
+1
View File
@@ -4,6 +4,7 @@
commands that disconnect users and invalidate their session cookies,
preventing automatic reconnection (#689)
- Replaced autoconf/automake build system with meson (#699)
- Added nftables-based ocserv-fw; requires ipcalc-ng/ipcalc (#397)
* Version 1.4.1 (released 2026-02-28)
-3
View File
@@ -562,9 +562,6 @@ route = 192.168.0.0/255.255.0.0
no-route = 192.168.5.0/255.255.255.0
# Note the that following two firewalling options currently are available
# in Linux systems with iptables software.
# If set, the script /usr/libexec/ocserv-fw will be called to restrict
# the user to its allowed routes and prevent him from accessing
# any other routes. In case of defaultroute, the no-routes are restricted.
+10
View File
@@ -41,6 +41,16 @@ endif
gperf = find_program('gperf', required: false)
protoc_c = find_program('protoc-c')
ipcalc = find_program(['ipcalc', 'ipcalc-ng'], required: true)
nft_prog = find_program('nft', required: false)
# Firewall script selection
_fw_opt = get_option('firewall-script')
if _fw_opt == 'auto'
fw_script_type = nft_prog.found() ? 'nftables' : 'iptables'
else
fw_script_type = _fw_opt
endif
message('firewall script: ' + fw_script_type)
# --------------------------------------------------------------------------
# Required dependencies
+1
View File
@@ -25,3 +25,4 @@ option('tun-tests', type: 'boolean', value: true, description: 'En
option('kerberos-tests', type: 'boolean', value: false, description: 'Enable tests requiring a Kerberos setup')
option('with-werror', type: 'boolean', value: false, description: 'Treat compiler warnings as errors')
option('pager', type: 'string', value: 'less', description: 'Default pager for occtl (empty string for none)')
option('firewall-script', type: 'combo', choices: ['auto', 'nftables', 'iptables'], value: 'auto', description: 'Firewall script backend (auto prefers nftables if nft is found)')
+8 -2
View File
@@ -290,8 +290,14 @@ executable('ocserv-worker', worker_sources,
# ocserv-fw script
# --------------------------------------------------------------------------
install_data('ocserv-fw',
install_dir: get_option('libexecdir'),
if fw_script_type == 'nftables'
_fw_src = 'ocserv-fw-nftables'
else
_fw_src = 'ocserv-fw-iptables'
endif
install_data(_fw_src,
rename: 'ocserv-fw',
install_dir: get_option('libexecdir'),
install_mode: 'rwxr-xr-x',
)
+236
View File
@@ -0,0 +1,236 @@
#!/bin/sh
#
# Copyright (C) 2026 Nikos Mavrogiannopoulos
#
# This file is part of ocserv.
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this file; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
# Input is from environment:
#
# OCSERV_RESTRICT_TO_ROUTES: If set to '1' the user should be restricted
# to accessing the OCSERV_ROUTES and prevented from accessing
# OCSERV_NO_ROUTES.
#
# OCSERV_ROUTES: A space separated list of IPv4 and IPv6 routes to
# which the user has access. If empty or not set the
# user has default route.
#
# OCSERV_ROUTES4: A version of OCSERV_ROUTES with IPv4 addresses only.
# OCSERV_ROUTES6: A version of OCSERV_ROUTES with IPv6 addresses only.
#
# OCSERV_NO_ROUTES: A space separated list of IPv4 and IPv6 routes to
# which the user has NO access.
#
# OCSERV_NO_ROUTES4: A version of OCSERV_NO_ROUTES with IPv4 addresses only.
# OCSERV_NO_ROUTES6: A version of OCSERV_NO_ROUTES with IPv6 addresses only.
#
# OCSERV_DNS: A space-separated list of DNS servers the user has access to.
# OCSERV_DNS4: A version of OCSERV_DNS with IPv4 addresses only.
# OCSERV_DNS6: A version of OCSERV_DNS with IPv6 addresses only.
#
# OCSERV_DENY_PORTS: A space-separated list of port types and ports that the user
# should be denied access to. An example of the format is:
# "tcp 443 udp 312 sctp 999 icmp all esp all icmpv6 all"
#
# OCSERV_ALLOW_PORTS: A space-separated list of port types and ports that the user
# should be granted access to. If set the user must be denied access
# to any other ports. An example of the format is:
# "tcp 443 udp 312 sctp 999 icmp all esp all icmpv6 all"
PATH=/sbin:/usr/sbin:$PATH
IPCALC=$(which ipcalc-ng 2>/dev/null)
if test -z "${IPCALC}"; then
IPCALC=$(which ipcalc 2>/dev/null)
fi
if test -z "${IPCALC}"; then
logger -t ocserv-fw "ipcalc or ipcalc-ng is required but not found"
exit 1
fi
# nft table names cannot contain hyphens, dots, or other non-identifier chars
TABLE="ocserv_$(echo "${DEVICE}" | sed 's/[^a-zA-Z0-9_]/_/g')"
if test "$1" = "--removeall"; then
_tables=$(nft list tables inet 2>/dev/null | sed -n 's/^table inet \(ocserv_[^ ]*\).*/\1/p')
for t in $_tables; do
nft delete table inet "$t" 2>/dev/null || true
done
exit 0
fi
execute_next_script() {
if test -n "${OCSERV_NEXT_SCRIPT}"; then
TMP_SCRIPT="${OCSERV_NEXT_SCRIPT}"
unset OCSERV_NEXT_SCRIPT
/bin/sh "${TMP_SCRIPT}"
fi
}
if test "${REASON}" = "disconnect"; then
nft delete table inet "${TABLE}" 2>/dev/null || true
execute_next_script
exit 0
fi
if test "${REASON}" != "connect"; then
logger -t ocserv-fw "unknown reason ${REASON}"
exit 1
fi
set -e
# Emit route restriction rules into the current nft chain context.
# When OCSERV_RESTRICT_TO_ROUTES=1: denied routes are rejected, allowed routes
# are accepted, and traffic not matching any route is rejected or accepted
# depending on whether an explicit allow-list (OCSERV_ROUTES) was given.
# Without route restriction, all traffic is accepted.
emit_route_rules() {
if test "${OCSERV_RESTRICT_TO_ROUTES}" = "1"; then
if test -n "${OCSERV_NO_ROUTES4}"; then
no4=$(routes_to_nft $OCSERV_NO_ROUTES4)
printf ' iif "%s" ip daddr { %s } reject\n' "${DEVICE}" "${no4}"
fi
if test -n "${OCSERV_NO_ROUTES6}"; then
no6=$(routes_to_nft $OCSERV_NO_ROUTES6)
printf ' iif "%s" ip6 daddr { %s } reject\n' "${DEVICE}" "${no6}"
fi
if test -n "$OCSERV_ROUTES"; then
if test -n "$OCSERV_ROUTES4"; then
r4=$(routes_to_nft $OCSERV_ROUTES4)
printf ' iif "%s" ip daddr { %s } accept\n' "${DEVICE}" "${r4}"
fi
if test -n "$OCSERV_ROUTES6"; then
r6=$(routes_to_nft $OCSERV_ROUTES6)
printf ' iif "%s" ip6 daddr { %s } accept\n' "${DEVICE}" "${r6}"
fi
printf ' iif "%s" reject\n' "${DEVICE}"
else
printf ' iif "%s" accept\n' "${DEVICE}"
fi
else
printf ' iif "%s" accept\n' "${DEVICE}"
fi
}
# Convert a route that may use a dotted-decimal subnet mask
# (e.g. 10.0.0.0/255.0.0.0, as ocserv normalises IPv4 routes) to
# CIDR prefix-length notation (10.0.0.0/8) required by nftables.
# IPv6 routes and routes already in CIDR notation are passed through.
normalize_route() {
case "$1" in
*/*.*.*.*) eval $(${IPCALC} -p "$1"); echo "${1%%/*}/${PREFIX}" ;;
*) echo "$1" ;;
esac
}
# Convert a space-separated list of routes to a comma-separated list
# in CIDR notation for use in nftables inline sets.
routes_to_nft() {
_sep=""
for _r in $@; do
printf '%s%s' "${_sep}" "$(normalize_route "${_r}")"
_sep=","
done
echo ""
}
# Remove any leftover table for this device (must be outside the atomic block)
nft delete table inet "${TABLE}" 2>/dev/null || true
# Build and apply the complete table definition atomically
{
printf 'table inet %s {\n chain ocserv_fwd {\n' "${TABLE}"
# priority filter-10 ensures ocserv rules evaluate before firewalld (priority 0)
printf ' type filter hook forward priority filter - 10; policy accept;\n'
printf ' oif "%s" ct state established,related accept\n' "${DEVICE}"
# DNS — collect addresses into a single inline set per address family
if test -n "${OCSERV_DNS4}"; then
dns4=$(echo $OCSERV_DNS4 | sed 's/ /,/g')
printf ' iif "%s" ip daddr { %s } udp dport 53 ct state new accept\n' \
"${DEVICE}" "${dns4}"
printf ' iif "%s" ip daddr { %s } tcp dport 53 ct state new,established accept\n' \
"${DEVICE}" "${dns4}"
fi
if test -n "${OCSERV_DNS6}"; then
dns6=$(echo $OCSERV_DNS6 | sed 's/ /,/g')
printf ' iif "%s" ip6 daddr { %s } udp dport 53 ct state new accept\n' \
"${DEVICE}" "${dns6}"
printf ' iif "%s" ip6 daddr { %s } tcp dport 53 ct state new,established accept\n' \
"${DEVICE}" "${dns6}"
fi
# Port restrictions
if test -n "${OCSERV_DENY_PORTS}"; then
# Denied ports are rejected directly; route restriction follows for
# the remaining traffic.
set -- ${OCSERV_DENY_PORTS}
while test $# -gt 1; do
proto=$1
port=$2
case "$proto" in
icmp) printf ' iif "%s" ip protocol icmp reject\n' "${DEVICE}" ;;
icmpv6) printf ' iif "%s" ip6 nexthdr icmpv6 reject\n' "${DEVICE}" ;;
esp) printf ' iif "%s" meta l4proto esp reject\n' "${DEVICE}" ;;
*) printf ' iif "%s" meta l4proto %s th dport %s reject\n' \
"${DEVICE}" "$proto" "$port" ;;
esac
shift 2
done
elif test -n "${OCSERV_ALLOW_PORTS}"; then
# Allowed ports jump to the route-restriction chain; everything else
# is rejected. This ensures route restriction (which restrict-user-to-ports
# implies) is evaluated for each allowed port.
set -- ${OCSERV_ALLOW_PORTS}
while test $# -gt 1; do
proto=$1
port=$2
case "$proto" in
icmp) printf ' iif "%s" ip protocol icmp jump ocserv_rt_%s\n' "${DEVICE}" "${TABLE}" ;;
icmpv6) printf ' iif "%s" ip6 nexthdr icmpv6 jump ocserv_rt_%s\n' "${DEVICE}" "${TABLE}" ;;
esp) printf ' iif "%s" meta l4proto esp jump ocserv_rt_%s\n' "${DEVICE}" "${TABLE}" ;;
*) printf ' iif "%s" meta l4proto %s th dport %s jump ocserv_rt_%s\n' \
"${DEVICE}" "$proto" "$port" "${TABLE}" ;;
esac
shift 2
done
printf ' iif "%s" reject\n' "${DEVICE}"
fi
# Route restrictions for DENY_PORTS and no-port-restriction cases.
# For ALLOW_PORTS the route restrictions live in the ocserv_rt_${TABLE} chain below.
if test -z "${OCSERV_ALLOW_PORTS}"; then
emit_route_rules
fi
printf ' }\n'
# Named chain jumped to by ALLOW_PORTS rules; applies route restriction
# so that both port and route policies are enforced simultaneously.
if test -n "${OCSERV_ALLOW_PORTS}"; then
printf ' chain ocserv_rt_%s {\n' "${TABLE}"
emit_route_rules
printf ' }\n'
fi
printf '}\n'
} | nft -f -
execute_next_script
exit 0
+1
View File
@@ -213,6 +213,7 @@ endif
if get_option('root-tests')
root_scripts = [
'test-fw-script',
'haproxy-connect', 'test-iroute', 'test-multi-cookie',
'test-pass-script', 'idle-timeout',
'test-cookie-timeout', 'test-cookie-timeout-2',
+349
View File
@@ -0,0 +1,349 @@
#!/bin/bash
#
# Copyright (C) 2026 Nikos Mavrogiannopoulos
#
# This file is part of ocserv.
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this file; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
# Test that ocserv-fw-iptables and/or ocserv-fw-nftables install and remove
# firewall rules correctly for each supported feature combination.
#
# When FW_SCRIPT is set, only that script is tested.
# Otherwise, all scripts for which the required tool is present are tested.
#
# Exits 77 (skip) only when run without root.
srcdir=${srcdir:-.}
DEVICE="octest0"
if test "$(id -u)" != "0"; then
echo "Skipping: requires root"
exit 77
fi
cleanup_script() {
local script="$1"
REASON=disconnect DEVICE=$DEVICE sh "$script" 2>/dev/null || true
REASON=disconnect DEVICE="${DEVICE}a" sh "$script" 2>/dev/null || true
REASON=disconnect DEVICE="${DEVICE}b" sh "$script" 2>/dev/null || true
sh "$script" --removeall 2>/dev/null || true
}
cleanup() {
for script in $SCRIPTS_TO_TEST; do
cleanup_script "$script"
done
ip link del "$DEVICE" 2>/dev/null || true
ip link del "${DEVICE}a" 2>/dev/null || true
ip link del "${DEVICE}b" 2>/dev/null || true
}
trap cleanup EXIT
# Build the list of scripts to test
if test -n "$FW_SCRIPT"; then
SCRIPTS_TO_TEST="$FW_SCRIPT"
else
SCRIPTS_TO_TEST=""
for s in ../src/ocserv-fw-iptables ../src/ocserv-fw-nftables; do
case "$s" in
*nftables) tool=nft ;;
*) tool=iptables ;;
esac
if command -v "$tool" >/dev/null 2>&1; then
SCRIPTS_TO_TEST="$SCRIPTS_TO_TEST $s"
fi
done
if test -z "$SCRIPTS_TO_TEST"; then
echo "FAIL: neither iptables nor nft found"
exit 1
fi
fi
fail() {
echo "FAIL: $1"
exit 1
}
dump_rules() {
case "$TOOL" in
nft) nft list ruleset 2>/dev/null ;;
iptables) iptables-save 2>/dev/null; ip6tables-save 2>/dev/null ;;
esac
}
check_rules_exist() {
local desc="$1"
local pattern="$2"
dump_rules | grep -qE -- "$pattern" || fail "expected rule not found: $desc (pattern: $pattern)"
}
check_rules_absent() {
local desc="$1"
local pattern="$2"
dump_rules | grep -qE -- "$pattern" && fail "unexpected rule still present: $desc (pattern: $pattern)" || true
}
run_tests() {
# FW_SCRIPT and TOOL must be set by the caller
ip link add dev "$DEVICE" type dummy 2>/dev/null || true
ip link set "$DEVICE" up
echo "Testing firewall script: $FW_SCRIPT (tool: $TOOL)"
# --- Test 1: basic connect / disconnect ---
echo -n " Test 1: basic connect/disconnect ... "
REASON=connect DEVICE=$DEVICE sh "$FW_SCRIPT"
case "$TOOL" in
nft)
check_rules_exist "ESTABLISHED/RELATED rule" "ct state (established,related|related,established) accept"
;;
iptables)
check_rules_exist "ESTABLISHED/RELATED rule" "RELATED,ESTABLISHED"
;;
esac
REASON=disconnect DEVICE=$DEVICE sh "$FW_SCRIPT"
case "$TOOL" in
nft)
check_rules_absent "nft table for device" "table inet ocserv_${DEVICE}"
;;
iptables)
check_rules_absent "iptables rules for device" "${DEVICE}.*ocserv-fw|ocserv-fw.*${DEVICE}"
;;
esac
echo "ok"
# --- Test 2: DNS allow rules ---
echo -n " Test 2: DNS rules ... "
REASON=connect DEVICE=$DEVICE OCSERV_DNS4="192.0.2.1" OCSERV_DNS6="2001:db8::1" sh "$FW_SCRIPT"
case "$TOOL" in
nft)
check_rules_exist "IPv4 DNS accept rule" "192\.0\.2\.1.*dport 53.*accept"
check_rules_exist "IPv6 DNS accept rule" "2001:db8::1.*dport 53.*accept"
;;
iptables)
check_rules_exist "IPv4 DNS accept rule" "192\.0\.2\.1.*--dport 53"
check_rules_exist "IPv6 DNS accept rule" "2001:db8::1.*--dport 53"
;;
esac
REASON=disconnect DEVICE=$DEVICE sh "$FW_SCRIPT"
echo "ok"
# --- Test 3: RESTRICT_TO_ROUTES=1 with explicit routes ---
echo -n " Test 3: route restriction with explicit routes ... "
REASON=connect DEVICE=$DEVICE \
OCSERV_RESTRICT_TO_ROUTES=1 \
OCSERV_ROUTES="10.0.0.0/8 fd00::/8" \
OCSERV_ROUTES4="10.0.0.0/8" \
OCSERV_ROUTES6="fd00::/8" \
sh "$FW_SCRIPT"
case "$TOOL" in
nft)
check_rules_exist "IPv4 route accept rule" "10\.0\.0\.0/8.*accept"
check_rules_exist "IPv6 route accept rule" "fd00::/8.*accept"
check_rules_exist "default reject rule" "iif \"${DEVICE}\" reject"
;;
iptables)
check_rules_exist "IPv4 route accept rule" "10\.0\.0\.0/8.*ACCEPT"
check_rules_exist "IPv6 route accept rule" "fd00::/8.*ACCEPT"
check_rules_exist "default REJECT rule" "${DEVICE}.*REJECT"
;;
esac
REASON=disconnect DEVICE=$DEVICE sh "$FW_SCRIPT"
echo "ok"
# --- Test 4: RESTRICT_TO_ROUTES=1 with NO_ROUTES (deny-list) ---
echo -n " Test 4: route restriction with no-routes deny-list ... "
REASON=connect DEVICE=$DEVICE \
OCSERV_RESTRICT_TO_ROUTES=1 \
OCSERV_NO_ROUTES="10.1.0.0/16" \
OCSERV_NO_ROUTES4="10.1.0.0/16" \
sh "$FW_SCRIPT"
case "$TOOL" in
nft)
check_rules_exist "IPv4 no-route reject rule" "10\.1\.0\.0/16.*reject"
check_rules_exist "allow-all fall-through" "iif \"${DEVICE}\" accept"
;;
iptables)
check_rules_exist "IPv4 no-route REJECT rule" "10\.1\.0\.0/16.*REJECT"
check_rules_exist "allow-all fall-through" "${DEVICE}.*ACCEPT"
;;
esac
REASON=disconnect DEVICE=$DEVICE sh "$FW_SCRIPT"
echo "ok"
# --- Test 5: RESTRICT_TO_ROUTES=1 with no routes at all ---
echo -n " Test 5: route restriction with no routes (allow-all) ... "
REASON=connect DEVICE=$DEVICE \
OCSERV_RESTRICT_TO_ROUTES=1 \
sh "$FW_SCRIPT"
case "$TOOL" in
nft)
check_rules_exist "allow-all fall-through" "iif \"${DEVICE}\" accept"
;;
iptables)
check_rules_exist "allow-all fall-through" "${DEVICE}.*ACCEPT"
;;
esac
REASON=disconnect DEVICE=$DEVICE sh "$FW_SCRIPT"
echo "ok"
# --- Test 6: RESTRICT_TO_ROUTES=1 with dotted-decimal masks (allow-list) ---
# ocserv always normalises IPv4 routes to dotted-decimal notation
# (e.g. 10.0.0.0/255.0.0.0) before passing them to the fw script.
# The nftables script must convert these to CIDR before use.
echo -n " Test 6: route restriction with dotted-decimal subnet masks (allow-list) ... "
REASON=connect DEVICE=$DEVICE \
OCSERV_RESTRICT_TO_ROUTES=1 \
OCSERV_ROUTES="10.0.0.0/255.0.0.0" \
OCSERV_ROUTES4="10.0.0.0/255.0.0.0" \
sh "$FW_SCRIPT"
case "$TOOL" in
nft)
check_rules_exist "IPv4 route accept rule in CIDR" "10\.0\.0\.0/8.*accept"
check_rules_exist "default reject rule" "iif \"${DEVICE}\" reject"
;;
iptables)
check_rules_exist "IPv4 route accept rule" "10\.0\.0\.0.*ACCEPT"
check_rules_exist "default REJECT rule" "${DEVICE}.*REJECT"
;;
esac
REASON=disconnect DEVICE=$DEVICE sh "$FW_SCRIPT"
echo "ok"
# --- Test 7: RESTRICT_TO_ROUTES=1 with dotted-decimal masks (deny-list) ---
echo -n " Test 7: route restriction with dotted-decimal subnet masks (deny-list) ... "
REASON=connect DEVICE=$DEVICE \
OCSERV_RESTRICT_TO_ROUTES=1 \
OCSERV_NO_ROUTES="10.1.0.0/255.255.0.0" \
OCSERV_NO_ROUTES4="10.1.0.0/255.255.0.0" \
sh "$FW_SCRIPT"
case "$TOOL" in
nft)
check_rules_exist "IPv4 no-route reject rule in CIDR" "10\.1\.0\.0/16.*reject"
check_rules_exist "allow-all fall-through" "iif \"${DEVICE}\" accept"
;;
iptables)
check_rules_exist "IPv4 no-route REJECT rule" "10\.1\.0\.0.*REJECT"
check_rules_exist "allow-all fall-through" "${DEVICE}.*ACCEPT"
;;
esac
REASON=disconnect DEVICE=$DEVICE sh "$FW_SCRIPT"
echo "ok"
# --- Test 8: DENY_PORTS ---
echo -n " Test 8: deny ports ... "
REASON=connect DEVICE=$DEVICE \
OCSERV_DENY_PORTS="tcp 443" \
sh "$FW_SCRIPT"
case "$TOOL" in
nft)
check_rules_exist "tcp port 443 reject" "dport 443 reject"
;;
iptables)
check_rules_exist "tcp port 443 REJECT" "--dport 443.*REJECT"
;;
esac
REASON=disconnect DEVICE=$DEVICE sh "$FW_SCRIPT"
echo "ok"
# --- Test 9: ALLOW_PORTS ---
echo -n " Test 9: allow ports ... "
REASON=connect DEVICE=$DEVICE \
OCSERV_ALLOW_PORTS="tcp 443" \
sh "$FW_SCRIPT"
case "$TOOL" in
nft)
check_rules_exist "tcp port 443 jump to route chain" "dport 443 jump"
check_rules_exist "default reject after allow-ports" "iif \"${DEVICE}\" reject"
;;
iptables)
check_rules_exist "tcp port 443 jump to per-device chain" "--dport 443.*FORWARD-ocserv-fw-${DEVICE}"
check_rules_exist "default REJECT after allow-ports" "${DEVICE}.*REJECT"
;;
esac
REASON=disconnect DEVICE=$DEVICE sh "$FW_SCRIPT"
echo "ok"
# --- Test 10: --removeall clears all rules ---
echo -n " Test 10: --removeall ... "
ip link add dev "${DEVICE}a" type dummy 2>/dev/null || true
ip link set "${DEVICE}a" up
ip link add dev "${DEVICE}b" type dummy 2>/dev/null || true
ip link set "${DEVICE}b" up
REASON=connect DEVICE="${DEVICE}a" sh "$FW_SCRIPT"
REASON=connect DEVICE="${DEVICE}b" sh "$FW_SCRIPT"
sh "$FW_SCRIPT" --removeall
case "$TOOL" in
nft)
check_rules_absent "any ocserv nft tables" "table inet ocserv_"
;;
iptables)
check_rules_absent "any ocserv iptables rules" "comment ocserv-fw"
;;
esac
echo "ok"
# final cleanup for this script
cleanup_script "$FW_SCRIPT"
ip link del "${DEVICE}a" 2>/dev/null || true
ip link del "${DEVICE}b" 2>/dev/null || true
}
for FW_SCRIPT in $SCRIPTS_TO_TEST; do
case "$FW_SCRIPT" in
*nftables) TOOL=nft ;;
*) TOOL=iptables ;;
esac
run_tests
done
echo "All tests passed."
exit 0