Allow using no-udp option in vhosts

Signed-off-by: Grigory Trenin <grigory.trenin@gmail.com>
This commit is contained in:
Grigory Trenin
2026-04-14 13:26:32 -04:00
parent 4a0a087996
commit db125634b8
14 changed files with 197 additions and 6 deletions
+2
View File
@@ -22,6 +22,8 @@
- Aligned default values for 'keepalive', 'rekey-time', 'cookie-timeout', - Aligned default values for 'keepalive', 'rekey-time', 'cookie-timeout',
'auth-timeout', 'ban-reset-time', 'max-ban-score', and 'auth-timeout', 'ban-reset-time', 'max-ban-score', and
'switch-to-tcp-timeout' with the values documented in sample.config 'switch-to-tcp-timeout' with the values documented in sample.config
- The `no-udp` option can now be used to disable DTLS for specific vhosts.
Previously it was only available in per-user or per-group config.
* Version 1.4.1 (released 2026-02-28) * Version 1.4.1 (released 2026-02-28)
+10
View File
@@ -677,6 +677,16 @@ route = 192.168.0.0/255.255.0.0
# [scope: vhost user] # [scope: vhost user]
no-route = 192.168.5.0/255.255.255.0 no-route = 192.168.5.0/255.255.255.0
# Whether to disable DTLS (UDP) for client connections. If set to true,
# the server will only accept TCP connections and will not negotiate DTLS.
# This can be useful for clients behind restrictive firewalls or NATs that
# have issues with UDP, or to enforce TCP-only operation for specific vhosts.
#
# This setting can be overridden per vhost, user, or group.
#
# [scope: vhost user]
#no-udp = false
# If set, the script /usr/libexec/ocserv-fw will be called to restrict # If set, the script /usr/libexec/ocserv-fw will be called to restrict
# the user to its allowed routes and prevent him from accessing # the user to its allowed routes and prevent him from accessing
# any other routes. In case of defaultroute, the no-routes are restricted. # any other routes. In case of defaultroute, the no-routes are restricted.
+2
View File
@@ -1308,6 +1308,8 @@ static int cfg_ini_handler(void *_ctx, const char *section, const char *name,
READ_STRING(config->camouflage_secret); READ_STRING(config->camouflage_secret);
} else if (strcmp(name, "camouflage_realm") == 0) { } else if (strcmp(name, "camouflage_realm") == 0) {
READ_STRING(config->camouflage_realm); READ_STRING(config->camouflage_realm);
} else if (strcmp(name, "no-udp") == 0) {
READ_TF(config->no_udp);
} else { } else {
if (reload == 0) if (reload == 0)
fprintf(stderr, fprintf(stderr,
+1 -1
View File
@@ -338,7 +338,7 @@ static void apply_default_config(sec_mod_instance_st *sec_mod_instance,
vhost_cfg_st *vhost = proc->vhost; vhost_cfg_st *vhost = proc->vhost;
if (!gc->has_no_udp) { if (!gc->has_no_udp) {
gc->no_udp = (vhost->perm_config.udp_port != 0) ? 0 : 1; gc->no_udp = vhost->perm_config.config->no_udp;
gc->has_no_udp = 1; gc->has_no_udp = 1;
} }
+10
View File
@@ -767,6 +767,16 @@ static int forward_udp_to_owner(main_server_st *s, struct listener_st *listener)
} }
if (proc_to_send != 0) { if (proc_to_send != 0) {
if (GETPCONFIG(s)->udp_port == 0 ||
proc_to_send->config->no_udp) {
mslog(s, proc_to_send, LOG_WARNING,
"Received UDP packet from %s while UDP is disabled for this client. "
"Possible cause: compromised worker or incorrect worker match.",
human_addr((struct sockaddr *)&cli_addr,
cli_addr_size, tbuf, sizeof(tbuf)));
goto fail;
}
UdpFdMsg msg = UDP_FD_MSG__INIT; UdpFdMsg msg = UDP_FD_MSG__INIT;
if (now - proc_to_send->udp_fd_receive_time <= if (now - proc_to_send->udp_fd_receive_time <=
+2
View File
@@ -381,6 +381,8 @@ struct cfg_st {
bool camouflage; /* [scope: vhost] */ bool camouflage; /* [scope: vhost] */
char *camouflage_secret; /* [scope: vhost] */ char *camouflage_secret; /* [scope: vhost] */
char *camouflage_realm; /* [scope: vhost] */ char *camouflage_realm; /* [scope: vhost] */
bool no_udp; /* [scope: vhost user] */
}; };
/* /*
+2 -2
View File
@@ -787,8 +787,8 @@ static int recv_cookie_auth_reply(worker_st *ws)
ws, msg->ipv6_local); ws, msg->ipv6_local);
} }
if (msg->config->no_udp != 0) if (msg->config->has_no_udp)
WSPCONFIG(ws)->udp_port = 0; WSCONFIG(ws)->no_udp = msg->config->no_udp;
/* routes */ /* routes */
if (check_if_default_route(msg->config->routes, if (check_if_default_route(msg->config->routes,
+2 -1
View File
@@ -2131,7 +2131,8 @@ static int connect_handler(worker_st *ws)
DTLS_ACTIVE(ws)->udp_state = UP_DISABLED; DTLS_ACTIVE(ws)->udp_state = UP_DISABLED;
DTLS_INACTIVE(ws)->udp_state = UP_DISABLED; DTLS_INACTIVE(ws)->udp_state = UP_DISABLED;
if (WSPCONFIG(ws)->udp_port != 0 && req->master_secret_set != 0) { if (WSPCONFIG(ws)->udp_port != 0 && !WSCONFIG(ws)->no_udp &&
req->master_secret_set != 0) {
memcpy(ws->master_secret, req->master_secret, TLS_MASTER_SIZE); memcpy(ws->master_secret, req->master_secret, TLS_MASTER_SIZE);
DTLS_ACTIVE(ws)->udp_state = UP_WAIT_FD; DTLS_ACTIVE(ws)->udp_state = UP_WAIT_FD;
DTLS_INACTIVE(ws)->udp_state = UP_WAIT_FD; DTLS_INACTIVE(ws)->udp_state = UP_WAIT_FD;
-1
View File
@@ -246,7 +246,6 @@ def main():
# are per-user only and don't appear in the global sample.config — skip those. # are per-user only and don't appear in the global sample.config — skip those.
PER_USER_ONLY = { PER_USER_ONLY = {
"iroute", "hostname", "explicit-ipv4", "explicit-ipv6", "iroute", "hostname", "explicit-ipv4", "explicit-ipv6",
"no-udp",
"ipv4-dns", "ipv6-dns", "ipv4-nbns", "ipv6-nbns", "ipv4-dns", "ipv6-dns", "ipv4-nbns", "ipv6-nbns",
} }
for opt in sorted(user_in_code): for opt in sorted(user_in_code):
+36
View File
@@ -0,0 +1,36 @@
auth = plain[@SRCDIR@/data/test-no-udp.passwd]
isolate-workers = @ISOLATE_WORKERS@
max-ban-score = 0
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-dns = 192.168.1.1
ipv4-network = @VPNNET@
ipv6-network = @VPNNET6@
ping-leases = false
[vhost:udp-restricted.example.com]
no-udp = true
auth = plain[passwd=@SRCDIR@/data/test-no-udp.passwd]
config-per-user = @SRCDIR@/user-config/
ca-cert = @SRCDIR@/certs/ca.pem
server-cert = @SRCDIR@/certs/server-cert.pem
server-key = @SRCDIR@/certs/server-key.pem
ipv4-network = @VPNNET@
ipv6-network = @VPNNET6@
+3
View File
@@ -0,0 +1,3 @@
noudp1:*:$5$FTX0hYx4Ns2EyUQJ$eSquiS6eIuqvS7lmVV37RTDGaoyK64l6UE8B370XZx.
noudp2:*:$5$O504PV8Po56eV95.$lMKsgC1qidOOSubv9J12Tq0YDtqygzAc7/fAWB/c8T3
noudp3:*:$5$pza6pL8iS67.T5jd$aWJuSF0GeoK3f5B8KaVoU4QtYKLARU2ktX3KAX6fuz8
+1 -1
View File
@@ -240,7 +240,7 @@ if get_option('root-tests')
'test-user-config', 'test-append-routes', 'test-ban', 'test-user-config', 'test-append-routes', 'test-ban',
'multiple-routes', 'json', 'test-udp-listen-host', 'multiple-routes', 'json', 'test-udp-listen-host',
'test-max-same-1', 'test-vhost-udp-port-inheritance', 'test-max-same-1', 'test-vhost-udp-port-inheritance',
'apple-ios', 'ipv6-iface', 'apple-ios', 'ipv6-iface', 'test-no-udp',
'disconnect-user', 'disconnect-user2', 'terminate-commands', 'disconnect-user', 'disconnect-user2', 'terminate-commands',
'ping-leases', 'test-ban-local', 'test-client-bypass-protocol', 'ping-leases', 'test-ban-local', 'test-client-bypass-protocol',
'ipv6-small-net', 'test-camouflage', 'test-camouflage-norealm', 'ipv6-small-net', 'test-camouflage', 'test-camouflage-norealm',
+125
View File
@@ -0,0 +1,125 @@
#!/bin/bash
#
# Copyright (C) 2026 Grigory Trenin
#
# 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/>.
#
# Test no-udp option
OCCTL="${OCCTL:-../src/occtl/occtl}"
OCCTL_SOCKET=./occtl-no-udp-$$.socket
SERV="${SERV:-../src/ocserv}"
srcdir=${srcdir:-.}
PIDFILE=ocserv-pid.$$.tmp
CLIPID=oc-pid.$$.tmp
PATH=${PATH}:/usr/sbin
OUTFILE=occtl-no-udp.$$.tmp
echo "Testing no-udp option..."
finish() {
set +e
echo " * Cleaning up..."
test -n "${PID}" && kill ${PID} >/dev/null 2>&1
test -n "${PIDFILE}" && rm -f ${PIDFILE} >/dev/null 2>&1
test -n "${CLIPID}" && kill $(cat ${CLIPID}) >/dev/null 2>&1
test -n "${CLIPID}" && rm -f ${CLIPID} >/dev/null 2>&1
test -n "${CONFIG}" && rm -f ${CONFIG} >/dev/null 2>&1
rm -f ${OUTFILE}
}
connect_vhost() {
VHOST=$1
USERNAME=$2
echo " * Connecting to ${VHOST} at ${ADDRESS}:${PORT}..."
( echo ${USERNAME} | ${CMDNS1} ${OPENCONNECT} ${VHOST}:${PORT} --resolve ${VHOST}:${ADDRESS} -u ${USERNAME} --servercert=pin-sha256:xp3scfzy3rOQsv9NcOve/8YVVv+pHr4qNCXEXrNl5s8= -s ${srcdir}/scripts/vpnc-script --pid-file=${CLIPID} --passwd-on-stdin -b )
if test $? != 0;then
fail $PID "Could not connect to server"
fi
${OCCTL} -s ${OCCTL_SOCKET} show user ${USERNAME} >${OUTFILE}
if test $? != 0;then
cat ${OUTFILE}
fail $PID "occtl show user ${USERNAME} failed!"
fi
grep -i "Username: ${USERNAME}" ${OUTFILE} >/dev/null
if test $? != 0;then
cat ${OUTFILE}
fail $PID "Username missing in occtl show user output"
fi
grep -i "vhost: ${VHOST}" ${OUTFILE} >/dev/null
if test $? != 0;then
cat ${OUTFILE}
fail $PID "Session associated with wrong vhost (expected: ${VHOST})"
fi
if grep -i "DTLS cipher:" ${OUTFILE} >/dev/null;then
HAVE_DTLS=1
else
HAVE_DTLS=0
fi
}
disconnect() {
kill "$(cat ${CLIPID})" 2>/dev/null || true
rm -f "${CLIPID}"
sleep 3
# Restore NS1 routing in case vpnc-script failed to run during server-initiated disconnect
reset_client_routes
}
. `dirname $0`/common.sh
. `dirname $0`/random-net.sh
. `dirname $0`/ns.sh
eval "${GETPORT}"
# Run server
update_config test-no-udp.config
if test "$VERBOSE" = 1;then
DEBUG="-d 3"
fi
${CMDNS2} ${SERV} -p ${PIDFILE} -f -c ${CONFIG} ${DEBUG} & PID=$!
sleep 4
echo "*** Test 1 - UDP enabled"
connect_vhost default noudp1
if test "${HAVE_DTLS}" = 0;then
fail $PID "DTLS session was not established with default vhost"
fi
disconnect
echo "*** Test 2 - UDP disabled per vhost"
connect_vhost udp-restricted.example.com noudp2
if test "${HAVE_DTLS}" = 1;then
fail $PID "DTLS session established despite being disabled for this vhost"
fi
disconnect
echo "*** Test 3 - UDP disabled per vhost, but enabled per-user"
connect_vhost udp-restricted.example.com noudp3
if test "${HAVE_DTLS}" = 0;then
fail $PID "DTLS session failed to establish despite user-level override"
fi
disconnect
exit 0
+1
View File
@@ -0,0 +1 @@
no-udp = false