Files
ocserv/tests/test-cstp-send-hang
T
Dimitri Papadopoulos 34be9e368e tests: whichcommand -v
Use `command -v` instead of `which` because:
* It's built into the shell, avoiding an external dependency on the
  `which` package on Fedora 44.
* It works across all POSIX-compliant shells.
* It's standard across Linux distributions. For example, Fedora 44
  lacks the `which` package by default.

Signed-off-by: Dimitri Papadopoulos <3350651-DimitriPapadopoulos@users.noreply.gitlab.com>
2026-05-31 14:17:33 +02:00

159 lines
4.9 KiB
Bash
Executable File

#!/bin/bash
#
# 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/>.
# Regression test for issue #638: cstp_send() must not loop forever when
# the peer stops draining the TCP socket (e.g. a mobile client roaming
# between networks and the TCP connection going silently dead).
#
# The config sets no-udp=true so all VPN data travels over TLS/CSTP,
# ensuring that cstp_send() is on the critical path.
#
# Method:
# 1. Connect an openconnect client (TLS-only, no DTLS).
# 2. Freeze openconnect with SIGSTOP so it can no longer drain its TCP
# receive buffer.
# 3. Flood large ICMP packets from the server namespace toward the client
# VPN IP. Each packet enters the TUN, is read by the worker, and
# handed to cstp_send(); with a frozen receiver gnutls_record_send()
# returns GNUTLS_E_AGAIN.
# 4. Assert that the worker disconnects the session within
# DEFAULT_SOCKET_TIMEOUT + margin rather than looping forever.
OCCTL="${OCCTL:-../src/occtl/occtl}"
SERV="${SERV:-../src/ocserv}"
srcdir=${srcdir:-.}
PIDFILE=ocserv-pid.$$.tmp
CLIPID=oc-pid.$$.tmp
OCCTL_SOCKET=./occtl-cstp-hang-$$.socket
PATH=${PATH}:/usr/sbin
IP=$(command -v ip)
. "$(dirname "$0")"/common.sh
eval "${GETPORT}"
if test "$(id -u)" != "0"; then
echo "This test must be run as root"
exit 77
fi
if test -z "${IP}"; then
echo "no ip tool found"
exit 77
fi
echo "Testing that cstp_send() does not hang when the peer freezes (issue #638)..."
function finish {
echo " * Cleaning up..."
# Resume openconnect before killing it so cleanup_client_server works cleanly
test -f "${CLIPID}" && kill -CONT "$(cat "${CLIPID}")" >/dev/null 2>&1
cleanup_client_server
}
trap finish EXIT
. "$(dirname "$0")"/random-net.sh
. "$(dirname "$0")"/ns.sh
update_config cstp-send-hang.config
if test "$VERBOSE" = 1; then
DEBUG="-d 3"
fi
${CMDNS2} ${SERV} -p ${PIDFILE} -f -c ${CONFIG} ${DEBUG} & PID=$!
sleep 3
echo " * Connecting to ${ADDRESS}:${PORT} (TLS only, no DTLS)..."
echo "test" | ${CMDNS1} ${OPENCONNECT} \
${ADDRESS}:${PORT} \
-u test \
--passwd-on-stdin \
--servercert=pin-sha256:xp3scfzy3rOQsv9NcOve/8YVVv+pHr4qNCXEXrNl5s8= \
-s "${srcdir}/scripts/vpnc-script" \
--pid-file="${CLIPID}" \
-b
if test $? != 0; then
fail $PID "Could not connect to server"
fi
sleep 2
if test ! -f "${CLIPID}"; then
fail $PID "openconnect did not write pid file"
fi
echo " * Verifying VPN is up..."
${CMDNS1} ping -c 3 ${VPNADDR}
if test $? != 0; then
fail $PID "VPN ping failed — connection not established"
fi
# Obtain the IP address assigned to the client inside the VPN.
# cstp_send() is exercised when the worker sends packets destined for
# this address out through the TUN.
OCCTL_JSON=$(${OCCTL} -s "${OCCTL_SOCKET}" -j show user test 2>/dev/null)
if test "$VERBOSE" = 1; then
echo " * occtl JSON: ${OCCTL_JSON}"
fi
VPN_CLI_IP=$(echo "${OCCTL_JSON}" | jq -r '.[0].IPv4' 2>/dev/null)
if test -z "${VPN_CLI_IP}" || test "${VPN_CLI_IP}" = "null"; then
fail $PID "Could not determine client VPN IP from occtl (JSON: ${OCCTL_JSON})"
fi
echo " * Client VPN IP: ${VPN_CLI_IP}"
OC_PID=$(cat "${CLIPID}")
echo " * Freezing openconnect (simulating silent network loss)..."
kill -STOP "${OC_PID}"
# Flood large ICMP packets from the server namespace toward the client VPN IP.
# Each packet travels: server routing → TUN → worker → cstp_send() → TLS/TCP.
# Because openconnect is frozen the TCP receive buffer fills and
# gnutls_record_send() starts returning GNUTLS_E_AGAIN.
echo " * Flooding server→client to trigger GNUTLS_E_AGAIN in cstp_send()..."
${CMDNS2} ping -f -s 1400 -c 100000 ${VPN_CLI_IP} >/dev/null 2>&1 &
FLOODPID=$!
# Wait up to DEFAULT_SOCKET_TIMEOUT (10 s) + 10 s margin for the worker to
# hit the poll() deadline and disconnect. On unfixed code the worker loops
# forever and the user remains visible in occtl at the deadline.
echo " * Waiting for worker to time out and disconnect..."
DEADLINE=$(($(date +%s) + 20))
WORKER_GONE=false
while test "$(date +%s)" -lt "${DEADLINE}"; do
sleep 2
if ! ${OCCTL} -s "${OCCTL_SOCKET}" show user test >/dev/null 2>&1; then
WORKER_GONE=true
break
fi
done
kill "${FLOODPID}" 2>/dev/null
wait "${FLOODPID}" 2>/dev/null
if test "${WORKER_GONE}" != "true"; then
fail $PID "Worker did not disconnect frozen client within timeout (issue #638)"
fi
echo " * Worker disconnected frozen client as expected."
exit 0