ocserv-fw-nftables: replace ipcalc with pure-shell mask_to_prefix

ipcalc was used only to convert dotted-decimal subnet masks to CIDR
prefix lengths (e.g. 255.255.0.0 -> 16), replaced with a POSIX shell
script.

Relates: #709

Signed-off-by: Nikos Mavrogiannopoulos <n.mavrogiannopoulos@gmail.com>
This commit is contained in:
Nikos Mavrogiannopoulos
2026-05-02 17:21:38 +02:00
parent dcb1b8d24e
commit 9c44e09356
5 changed files with 223 additions and 12 deletions
+1
View File
@@ -1,4 +1,5 @@
* Version 1.5.0 (unreleased)
- ocserv-fw-nftables: dropped runtime dependency on ipcalc/ipcalc-ng (#709)
- Fixed sudden disconnects after authentication for AnyConnect clients (#706)
- Vhosts now inherit configuration options from the default vhost if
they are not overridden (#705)
+2 -1
View File
@@ -40,7 +40,8 @@ endif
gperf = find_program('gperf', required: false)
protoc_c = find_program('protoc-c', required: false)
ipcalc = find_program(['ipcalc', 'ipcalc-ng'], required: true)
ipcalc = find_program(['ipcalc', 'ipcalc-ng'], required: false)
# Firewall script selection
_fw_opt = get_option('firewall-script')
fw_script_type = (_fw_opt == 'auto') ? 'nftables' : _fw_opt
+37 -11
View File
@@ -52,16 +52,6 @@
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')"
@@ -127,13 +117,49 @@ emit_route_rules() {
fi
}
# Convert a dotted-decimal subnet mask (e.g. 255.255.0.0) to a CIDR prefix
# length (16). Valid subnet masks are a closed set so a case table suffices.
# Returns 1 and logs an error for invalid masks.
mask_to_prefix() {
bits=0
seen_partial=0
n=0
IFS=.
for octet in $1; do
n=$((n+1))
if test "$seen_partial" = "1" && test "$octet" != "0"; then
logger -t ocserv-fw "invalid subnet mask: $1"
return 1
fi
case $octet in
255) bits=$((bits+8)) ;;
254) bits=$((bits+7)); seen_partial=1 ;;
252) bits=$((bits+6)); seen_partial=1 ;;
248) bits=$((bits+5)); seen_partial=1 ;;
240) bits=$((bits+4)); seen_partial=1 ;;
224) bits=$((bits+3)); seen_partial=1 ;;
192) bits=$((bits+2)); seen_partial=1 ;;
128) bits=$((bits+1)); seen_partial=1 ;;
0) seen_partial=1 ;;
*) logger -t ocserv-fw "invalid subnet mask: $1"
return 1 ;;
esac
done
if test "$n" != "4"; then
logger -t ocserv-fw "invalid subnet mask: $1"
return 1
fi
echo "$bits"
}
# 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}" ;;
*/*.*.*.*) prefix=$(mask_to_prefix "${1#*/}") || return 1
echo "${1%%/*}/${prefix}" ;;
*) echo "$1" ;;
esac
}
+1
View File
@@ -95,6 +95,7 @@ always_scripts = [
'ocpasswd-test',
'test-owasp-headers',
'test-replay',
'test-fw-normalize-route',
]
foreach s : always_scripts
+182
View File
@@ -0,0 +1,182 @@
#!/bin/sh
#
# Copyright (C) 2026 Nikos Mavrogiannopoulos
#
# This file is part of ocserv.
#
# ocserv 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.
#
# ocserv 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 program. If not, see <http://www.gnu.org/licenses/>.
# Unit test for the mask_to_prefix / normalize_route shell functions in
# ocserv-fw-nftables. The functions are extracted directly from the script
# so this test always exercises the real implementation.
srcdir=${srcdir:-.}
FW_SCRIPT="${srcdir}/../src/ocserv-fw-nftables"
if ! test -f "${FW_SCRIPT}"; then
echo "cannot find ${FW_SCRIPT}"
exit 1
fi
echo "Testing mask_to_prefix and normalize_route from ${FW_SCRIPT}..."
# Load the two functions directly from the firewall script. awk extracts
# each function body (from "funcname() {" to the closing "}" at column 0).
eval "$(awk '/^mask_to_prefix\(\)/,/^\}/' "${FW_SCRIPT}")"
eval "$(awk '/^normalize_route\(\)/,/^\}/' "${FW_SCRIPT}")"
if ! type mask_to_prefix > /dev/null 2>&1 || \
! type normalize_route > /dev/null 2>&1; then
echo "failed to load functions from ${FW_SCRIPT}"
exit 1
fi
# ---- test harness -----------------------------------------------------------
FAIL=0
check() {
input="$1"
expected="$2"
got=$(normalize_route "$input")
if test "$got" != "$expected"; then
echo "FAIL: normalize_route '$input' -> '$got', expected '$expected'"
FAIL=1
fi
}
check_mask() {
mask="$1"
expected="$2"
got=$(mask_to_prefix "$mask")
if test "$got" != "$expected"; then
echo "FAIL: mask_to_prefix '$mask' -> '$got', expected '$expected'"
FAIL=1
fi
}
# Expect mask_to_prefix to fail (return non-zero, produce no output)
check_mask_invalid() {
mask="$1"
got=$(mask_to_prefix "$mask" 2>/dev/null)
if test $? = 0 || test -n "$got"; then
echo "FAIL: mask_to_prefix '$mask' should have failed but returned '$got'"
FAIL=1
fi
}
# Expect normalize_route to fail (return non-zero, produce no output)
check_route_invalid() {
route="$1"
got=$(normalize_route "$route" 2>/dev/null)
if test $? = 0 || test -n "$got"; then
echo "FAIL: normalize_route '$route' should have failed but returned '$got'"
FAIL=1
fi
}
# ---- mask_to_prefix: all 33 valid prefix lengths ----------------------------
check_mask "0.0.0.0" 0
check_mask "128.0.0.0" 1
check_mask "192.0.0.0" 2
check_mask "224.0.0.0" 3
check_mask "240.0.0.0" 4
check_mask "248.0.0.0" 5
check_mask "252.0.0.0" 6
check_mask "254.0.0.0" 7
check_mask "255.0.0.0" 8
check_mask "255.128.0.0" 9
check_mask "255.192.0.0" 10
check_mask "255.224.0.0" 11
check_mask "255.240.0.0" 12
check_mask "255.248.0.0" 13
check_mask "255.252.0.0" 14
check_mask "255.254.0.0" 15
check_mask "255.255.0.0" 16
check_mask "255.255.128.0" 17
check_mask "255.255.192.0" 18
check_mask "255.255.224.0" 19
check_mask "255.255.240.0" 20
check_mask "255.255.248.0" 21
check_mask "255.255.252.0" 22
check_mask "255.255.254.0" 23
check_mask "255.255.255.0" 24
check_mask "255.255.255.128" 25
check_mask "255.255.255.192" 26
check_mask "255.255.255.224" 27
check_mask "255.255.255.240" 28
check_mask "255.255.255.248" 29
check_mask "255.255.255.252" 30
check_mask "255.255.255.254" 31
check_mask "255.255.255.255" 32
# ---- normalize_route: dotted-decimal mask conversion ------------------------
check "10.0.0.0/255.0.0.0" "10.0.0.0/8"
check "172.16.0.0/255.240.0.0" "172.16.0.0/12"
check "192.168.0.0/255.255.0.0" "192.168.0.0/16"
check "192.168.1.0/255.255.255.0" "192.168.1.0/24"
check "192.168.1.128/255.255.255.128" "192.168.1.128/25"
check "192.168.1.192/255.255.255.192" "192.168.1.192/26"
check "0.0.0.0/0.0.0.0" "0.0.0.0/0"
check "192.168.1.1/255.255.255.255" "192.168.1.1/32"
# ---- normalize_route: already-CIDR IPv4 passed through ---------------------
check "10.0.0.0/8" "10.0.0.0/8"
check "192.168.1.0/24" "192.168.1.0/24"
check "0.0.0.0/0" "0.0.0.0/0"
check "192.168.1.1/32" "192.168.1.1/32"
# ---- normalize_route: IPv6 passed through unchanged ------------------------
check "fd00::/48" "fd00::/48"
check "2001:db8::/32" "2001:db8::/32"
check "fd91:6d14:7241:dc6a::/112" "fd91:6d14:7241:dc6a::/112"
check "fc7d:b139:f7ba:5f53:c634::/80" "fc7d:b139:f7ba:5f53:c634::/80"
check "::/0" "::/0"
check "::1/128" "::1/128"
# ---- mask_to_prefix: invalid octet values -----------------------------------
check_mask_invalid "255.300.0.0"
check_mask_invalid "255.255.127.0"
check_mask_invalid "255.1.0.0"
# ---- mask_to_prefix: non-contiguous masks -----------------------------------
check_mask_invalid "254.192.128.0"
check_mask_invalid "255.0.255.0"
check_mask_invalid "0.255.0.0"
check_mask_invalid "255.255.0.255"
# ---- mask_to_prefix: wrong number of octets ---------------------------------
check_mask_invalid "255.255.0"
check_mask_invalid "255.255.255.0.0"
# ---- normalize_route: propagates mask_to_prefix failure ---------------------
check_route_invalid "10.0.0.0/255.0.255.0"
check_route_invalid "10.0.0.0/255.255.255.0.0"
# ---- result -----------------------------------------------------------------
if test "$FAIL" = "0"; then
echo "All tests passed."
exit 0
else
exit 1
fi