From 598bcf405ed9729ed317851028d53508dc252cb2 Mon Sep 17 00:00:00 2001 From: Nikos Mavrogiannopoulos Date: Sun, 8 Mar 2026 16:20:04 +0100 Subject: [PATCH] Added ocserv-fw for nftables This also introduces a basic functional test for ocserv-fw. Resolves: #397 Signed-off-by: Nikos Mavrogiannopoulos --- .codespellrc | 2 +- NEWS | 1 + doc/sample.config | 3 - meson.build | 10 + meson_options.txt | 1 + src/meson.build | 10 +- src/{ocserv-fw => ocserv-fw-iptables} | 0 src/ocserv-fw-nftables | 236 +++++++++++++++++ tests/meson.build | 1 + tests/test-fw-script | 349 ++++++++++++++++++++++++++ 10 files changed, 607 insertions(+), 6 deletions(-) rename src/{ocserv-fw => ocserv-fw-iptables} (100%) create mode 100755 src/ocserv-fw-nftables create mode 100755 tests/test-fw-script diff --git a/.codespellrc b/.codespellrc index f924a033..e2862854 100644 --- a/.codespellrc +++ b/.codespellrc @@ -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 diff --git a/NEWS b/NEWS index cc9b4466..f4a94be9 100644 --- a/NEWS +++ b/NEWS @@ -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) diff --git a/doc/sample.config b/doc/sample.config index 1b77ca5e..f9881e42 100644 --- a/doc/sample.config +++ b/doc/sample.config @@ -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. diff --git a/meson.build b/meson.build index 3b5777fd..ac848c71 100644 --- a/meson.build +++ b/meson.build @@ -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 diff --git a/meson_options.txt b/meson_options.txt index 2298d739..de30b68c 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -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)') diff --git a/src/meson.build b/src/meson.build index f7b75078..f0bed9a4 100644 --- a/src/meson.build +++ b/src/meson.build @@ -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', ) diff --git a/src/ocserv-fw b/src/ocserv-fw-iptables similarity index 100% rename from src/ocserv-fw rename to src/ocserv-fw-iptables diff --git a/src/ocserv-fw-nftables b/src/ocserv-fw-nftables new file mode 100755 index 00000000..31ac78ed --- /dev/null +++ b/src/ocserv-fw-nftables @@ -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 diff --git a/tests/meson.build b/tests/meson.build index 7ca92b31..1745bf8a 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -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', diff --git a/tests/test-fw-script b/tests/test-fw-script new file mode 100755 index 00000000..4f2d591c --- /dev/null +++ b/tests/test-fw-script @@ -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