diff --git a/tests/bandwidth b/tests/bandwidth new file mode 100755 index 00000000..f580383d --- /dev/null +++ b/tests/bandwidth @@ -0,0 +1,157 @@ +#!/bin/bash +# +# Copyright (C) 2025 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 . +# + +# Tests rx-data-per-sec / tx-data-per-sec bandwidth throttling. +# +# Strategy: the config limits both directions to 100 KB/s (100,000 bytes/s +# = ~0.8 Mbits/s). On a loopback/namespace link iperf3 achieves hundreds +# of Mbits/s without throttling, so even a generous 5x upper bound +# (4 Mbits/s) gives a clear signal that the throttle is active. +# We test both upload (exercises rx-data-per-sec) and download +# (exercises tx-data-per-sec) directions. + +OCCTL="${OCCTL:-../src/occtl/occtl}" +SERV="${SERV:-../src/ocserv}" +srcdir=${srcdir:-.} +PIDFILE=ocserv-pid.$$.tmp +CLIPID=oc-pid.$$.tmp +PATH=${PATH}:/usr/sbin +IP=$(which ip) + +. `dirname $0`/common.sh + +eval "${GETPORT}" + +if test -z "${IP}"; then + echo "no IP tool is present" + exit 77 +fi + +if test "$(id -u)" != "0"; then + echo "This test must be run as root" + exit 77 +fi + +if ! iperf3 --version >/dev/null 2>&1; then + echo "iperf3 is not available" + exit 77 +fi + +if ! python3 -c "" 2>/dev/null; then + echo "python3 is not available" + exit 77 +fi + +echo "Testing bandwidth throttling..." + +function finish { + set +e + echo " * Cleaning up..." + # Kill the client first so the worker detects the disconnect and calls + # __gcov_dump() before exiting cleanly — avoids SIGKILL from the server's + # terminate_server() racing with cstp_send() in the worker. + test -n "${CLIPID}" && test -f "${CLIPID}" && kill $(cat ${CLIPID}) >/dev/null 2>&1 + test -n "${CLIPID}" && rm -f ${CLIPID} >/dev/null 2>&1 + sleep 2 + test -n "${PID}" && kill ${PID} >/dev/null 2>&1 + test -n "${PIDFILE}" && rm -f ${PIDFILE} >/dev/null 2>&1 + test -n "${CONFIG}" && rm -f ${CONFIG} >/dev/null 2>&1 +} +trap finish EXIT + +OCCTL_SOCKET=./occtl-bandwidth-$$.socket +USERNAME=test + +# Rate limit as configured in bandwidth.config (bytes/sec) +RATE_BYTES=100000 +# Upper bound: 5x the limit in bits/sec +MAX_BITS=$(( RATE_BYTES * 8 * 5 )) + +. `dirname $0`/random-net.sh +. `dirname $0`/ns.sh + +update_config bandwidth.config +if test "$VERBOSE" = 1; then + DEBUG="-d 3" +fi + +${CMDNS2} ${SERV} -p ${PIDFILE} -f -c ${CONFIG} ${DEBUG} & +PID=$! + +sleep 4 + +echo " * Getting cookie from ${ADDRESS}:${PORT}..." +( echo "test" | ${CMDNS1} ${OPENCONNECT} ${ADDRESS}:${PORT} -u ${USERNAME} \ + --servercert=pin-sha256:xp3scfzy3rOQsv9NcOve/8YVVv+pHr4qNCXEXrNl5s8= \ + --cookieonly ) +if test $? != 0; then + echo "Could not get cookie from server" + exit 1 +fi + +echo " * Connecting to ${ADDRESS}:${PORT}..." +( echo "test" | ${CMDNS1} ${OPENCONNECT} ${ADDRESS}:${PORT} -u ${USERNAME} \ + --servercert=pin-sha256:xp3scfzy3rOQsv9NcOve/8YVVv+pHr4qNCXEXrNl5s8= \ + -s ${srcdir}/scripts/vpnc-script --pid-file=${CLIPID} \ + --passwd-on-stdin -b ) +if test $? != 0; then + echo "Could not connect to server" + exit 1 +fi + +set -e +echo " * Pinging remote address" +${CMDNS1} ping -c 3 ${VPNADDR} + +# --- Upload test (client -> server, exercises rx-data-per-sec) --- + +${CMDNS2} iperf3 -s -D -1 +sleep 2 + +echo " * Testing upload throttle (rx-data-per-sec)..." +TX_BITS=$(${CMDNS1} iperf3 -t 5 -J -c ${VPNADDR} | \ + python3 -c "import sys,json; d=json.load(sys.stdin); \ + print(int(d['end']['sum_sent']['bits_per_second']))") +echo " upload: ${TX_BITS} bits/s (limit ~$(( RATE_BYTES * 8 )) bits/s, max allowed ${MAX_BITS} bits/s)" + +if test "${TX_BITS}" -gt "${MAX_BITS}"; then + echo "FAIL: upload ${TX_BITS} bits/s exceeds 5x throttle limit ${MAX_BITS} bits/s" + exit 1 +fi + +# --- Download test (server -> client, exercises tx-data-per-sec) --- + +set +e; eval "${GETPORT}"; set -e +${CMDNS2} iperf3 -s -D -1 -p ${PORT} +sleep 2 + +echo " * Testing download throttle (tx-data-per-sec)..." +RX_BITS=$(${CMDNS1} iperf3 -t 5 -R -J -c ${VPNADDR} -p ${PORT} | \ + python3 -c "import sys,json; d=json.load(sys.stdin); \ + print(int(d['end']['sum_received']['bits_per_second']))") +echo " download: ${RX_BITS} bits/s (limit ~$(( RATE_BYTES * 8 )) bits/s, max allowed ${MAX_BITS} bits/s)" + +if test "${RX_BITS}" -gt "${MAX_BITS}"; then + echo "FAIL: download ${RX_BITS} bits/s exceeds 5x throttle limit ${MAX_BITS} bits/s" + exit 1 +fi + +echo "Bandwidth throttling is working correctly" +exit 0 diff --git a/tests/data/bandwidth.config b/tests/data/bandwidth.config new file mode 100644 index 00000000..e3ed7136 --- /dev/null +++ b/tests/data/bandwidth.config @@ -0,0 +1,54 @@ +auth = "plain[@SRCDIR@/data/test1.passwd]" + +isolate-workers = @ISOLATE_WORKERS@ + +max-ban-score = 0 + +use-dbus = no + +max-clients = 16 + +listen-proxy-proto = false + +max-same-clients = 2 + +tcp-port = @PORT@ +udp-port = @PORT@ + +keepalive = 32400 + +dpd = 440 + +try-mtu-discovery = false + +server-cert = @SRCDIR@/certs/server-cert.pem +server-key = @SRCDIR@/certs/server-key.pem + +tls-priorities = "PERFORMANCE:%SERVER_PRECEDENCE:%COMPAT" + +auth-timeout = 40 + +socket-file = ./ocserv-socket + +occtl-socket-file = @OCCTL_SOCKET@ +use-occtl = true + +run-as-user = @USERNAME@ +run-as-group = @GROUP@ + +device = vpns + +default-domain = example.com + +ipv4-network = @VPNNET@ +ipv4-dns = 192.168.1.1 + +ipv6-network = @VPNNET6@ + +ping-leases = false + +# Limit to 100 KB/s in each direction. On a loopback/namespace link +# unthrottled iperf3 reaches hundreds of Mbits/s, so even a generous +# 5x tolerance gives a clear pass/fail signal. +rx-data-per-sec = 100000 +tx-data-per-sec = 100000 diff --git a/tests/meson.build b/tests/meson.build index bd4fe24a..979df0c6 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -225,7 +225,7 @@ if get_option('root-tests') 'vhost-traffic', 'defvhost-traffic', 'session-timeout', 'test-occtl', 'no-ipv6-ocv3', # cipher / traffic tests - 'traffic', 'lz4-compression', 'lzs-compression', + 'traffic', 'bandwidth', 'lz4-compression', 'lzs-compression', 'aes256-cipher', 'aes128-cipher', 'oc-aes256-gcm-cipher', 'oc-aes128-gcm-cipher', 'ac-aes128-gcm-cipher', 'ac-aes256-gcm-cipher',