Merge branch 'tmp-lease' into 'master'

Use network address as IPv6 lease start

Closes #714

See merge request openconnect/ocserv!543
This commit is contained in:
Nikos Mavrogiannopoulos
2026-10-01 18:02:06 +00:00
16 changed files with 235 additions and 23 deletions
+6
View File
@@ -23,6 +23,12 @@
- IPv4 /31 networks (netmask 255.255.255.254) can now be used for leases as
point-to-point links (RFC 3021); previously no address could be leased from
them. The server takes the network address and the client the other one.
- The server-side IPv6 tunnel address is now the network address of
ipv6-network (previously the network address + 1). Firewall rules, DNS
settings or scripts that reference the server's ::1 tunnel address must be
updated. This makes /127 point-to-point networks (RFC 6164) lease the
expected address to the client, and a client can no longer be assigned
the server's address as part of the first subnet (#714)
* Version 1.5.0 (released 2026-06-07)
+23
View File
@@ -250,6 +250,29 @@ and `explicit-ipv4 = 192.168.1.0`, confirm the session is rejected (no IP).
**Links:** REQ-MAIN-NET-001, OC-PROTO-CONN-007 (server address first in the
network, SHOULD)
### REQ-MAIN-NET-006 — Server-side IPv6 TUN address is the network address
**Requirement:** `get_ipv6_lease()` MUST set the server-side (local) TUN
address `lip` to the network address of the configured `ipv6-network` (all
host bits zero) and MUST NOT lease to the client (`rip`) an address whose
`ipv6-subnet-prefix` subnet equals `lip`. IPv6 has no reserved network
address, so for a `/127` network (RFC 6164) the server takes the network
address and the client is leased the other one (`network + 1`). An explicit
per-user IPv6 address (`explicit-ipv6`) whose subnet equals `lip` MUST be
rejected with `ERR_NO_IP`.
**Strength:** MUST
**Status:** DERIVED
**Source:** src/ip-lease.c:442-443 (`lip`), src/ip-lease.c:447-481
(explicit IP), src/ip-lease.c:545 (pool allocation), src/ip-lease.c:150-164
(`is_ipv6_ok`)
**Acceptance:** local — with `ipv6-network = N/127`, confirm the server TUN
address is `N` and the client receives `N+1`; with the same `/127` and
`explicit-ipv6 = N`, confirm the session is rejected (no IP). With a wider
network (e.g. `/112`), confirm the server TUN address is `N` and an
`explicit-ipv6` equal to `N` is rejected.
**Links:** REQ-MAIN-NET-001, REQ-MAIN-NET-004, OC-PROTO-CONN-007 (server
address first in the network, SHOULD)
---
### REQ-MAIN-NET-005 — A user's iroutes are applied with `route-add-cmd` when the user connects, with `%{R}`, `%{RI}` and `%{D}` substituted
+4 -3
View File
@@ -366,8 +366,9 @@ test sources after `common.sh`:
same kind into `NAME`. For every allocated `NAME` it MUST set
`NAME_BASE`, the network address without its last all-zero octet
(IPv4, e.g. `10.22.134`) or group (IPv6, ending in `:`), and
`NAME_ADDR`, the first host address (`NAME_BASE` followed by `.1`
or `1`), and it MUST append `NAME` to `VPNNET_VARS` so that
`NAME_ADDR`, the server address: the first host address
(`NAME_BASE` followed by `.1`) for IPv4, and the network address
itself for IPv6 (`REQ-MAIN-NET-006`), and it MUST append `NAME` to `VPNNET_VARS` so that
`update_config` substitutes it (`REQ-GEN-TEST-008`). It MUST also
set `VPNADDR` and `VPNADDR6` to `VPNNET_ADDR` and `VPNNET6_ADDR`.
It MUST NOT modify `ADDRESS` or `CLI_ADDRESS`, so a test using
@@ -414,7 +415,7 @@ draw space is about 70,000 `/24` networks).
`random-vpnnet.sh`, call `alloc_vpnnet4 VPNNET2`, and confirm: `ADDRESS`
is unchanged and `CLI_ADDRESS` unset; `"${VPNNET_BASE}.0/24" = "$VPNNET"`
and `VPNADDR = VPNNET_ADDR = ${VPNNET_BASE}.1`; `VPNNET6` ends in `/112`
and `VPNADDR6 = ${VPNNET6_BASE}1`; `VPNNET2 != VPNNET`; and a template
and `VPNADDR6` equals the network address of `VPNNET6`; `VPNNET2 != VPNNET`; and a template
containing `@VPNNET2@ @VPNNET2_BASE@.9 @VPNNET2_ADDR@` is materialized by
`update_config` with those values. Source `random-net.sh` and confirm
`ADDRESS` and `CLI_ADDRESS` are set to addresses different from
@@ -113,9 +113,12 @@ each individually optional depending on what's been requested/negotiated
**Notes:** "X-CSTP-Address-IP6 ... prefix length is RECOMMENDED to be set to
127-bits" (RFC 6164) and "X-CSTP-Netmask ... RECOMMENDED the server address to
be the first in defined network" are both SHOULD-strength conventions, not
MUST — `[CANDIDATE: check whether ocserv's `ip-lease.c` (REQ-MAIN-NET-001)
follows the /127 and "server address first" recommendations for IPv6/IPv4
respectively.]`
MUST. ocserv follows "server address first": the IPv4 server address is
the network address + 1, or the network address for a `/31`
(REQ-MAIN-NET-004), and the IPv6 server address is the network address
(REQ-MAIN-NET-006). ocserv does not follow the /127 recommendation: the
prefix sent in `X-CSTP-Address-IP6` is `ipv6-subnet-prefix` (default 128),
not 127 — see REQ-PROTO-CONN-007.
---
+9 -6
View File
@@ -181,12 +181,15 @@ choice depends on `user_agent_type`, see REQ-PROTO-CFG-002); OCSERV
`X-CSTP-Split-Include*` header is sent and the client tunnels all traffic.
**Divergence**: the *choice of DNS header name* (`X-CSTP-DNS` vs
`X-CSTP-DNS-IP6`) for IPv6 entries is AnyConnect-vs-OpenConnect divergent — see
REQ-PROTO-CFG-002 for the dedicated entry. The "/127 for IPv6, server-address-
first for IPv4" RECOMMENDED conventions from OC-PROTO-CONN-007's Notes were
**not verified** against `src/ip-lease.c` in this pass — `[REVIEW]`: confirm
`ip-lease.c`'s allocation order against these SHOULD-strength conventions in a
follow-up.
**Links**: REQ-PROTO-CFG-001, REQ-PROTO-CFG-002, REQ-MAIN-NET-001
REQ-PROTO-CFG-002 for the dedicated entry. Of the SHOULD-strength conventions
in OC-PROTO-CONN-007's Notes, ocserv follows "server address first" for both
families (REQ-MAIN-NET-004 for IPv4, REQ-MAIN-NET-006 for IPv6). It diverges
from "/127 for IPv6": `X-CSTP-Address-IP6` carries `ipv6-subnet-prefix`
(`src/worker-vpn.c`, default 128, or e.g. 64 for a delegated subnet), because
ocserv leases a client its own subnet rather than one side of a shared `/127`
link. Informational.
**Links**: REQ-PROTO-CFG-001, REQ-PROTO-CFG-002, REQ-MAIN-NET-001,
REQ-MAIN-NET-004, REQ-MAIN-NET-006
---
+6 -1
View File
@@ -633,7 +633,12 @@ ipv4-netmask = 255.255.255.0
# An alternative way of specifying the network:
#ipv4-network = 192.168.1.0/24
# The IPv6 subnet that leases will be given from.
# The IPv6 subnet that leases will be given from. The server uses the
# network address (e.g., fda9:4efe:7e3b:03ea:: for the network below) for
# its side of the tunnel; versions prior to 1.5.1 used the network
# address + 1. A /127 network is suitable for point-to-point links
# (RFC 6164): the server takes the network address and the client the
# other address; this is typically useful as a per-user setting.
# [scope: vhost user]
#ipv6-network = fda9:4efe:7e3b:03ea::/48
-2
View File
@@ -439,9 +439,7 @@ static int get_ipv6_lease(main_server_st *s, struct proc_st *proc)
if (proc->ipv6 == NULL)
return ERR_MEM;
/* LIP = network address + 1 */
memcpy(&proc->ipv6->lip, &network, sizeof(struct sockaddr_in6));
SA_IN6_U8_P(&proc->ipv6->lip)[15] |= 1;
proc->ipv6->lip_len = sizeof(struct sockaddr_in6);
+28
View File
@@ -0,0 +1,28 @@
# Used by test-ipv6-p2p; the per-user files in user-config-ipv6-p2p/
# override ipv6-network to exercise /127 point-to-point networks.
auth = "plain[@SRCDIR@/data/test1.passwd]"
isolate-workers = @ISOLATE_WORKERS@
use-dbus = no
max-clients = 16
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
pid-file = /var/run/ocserv.pid
socket-file = /var/run/ocserv-socket
run-as-user = nobody
run-as-group = daemon
device = vpns
default-domain = example.com
ipv4-network = @VPNNET@
ipv6-network = @VPNNET6@
ping-leases = false
config-per-user = @CONFIG_PER_USER_DIR@/
occtl-socket-file = @OCCTL_SOCKET@
use-occtl = true
+1 -1
View File
@@ -1 +1 @@
explicit-ipv6 = @VPNNET6_BASE@1
explicit-ipv6 = @VPNNET6_BASE@0
+1
View File
@@ -0,0 +1 @@
ipv6-network = @VPNNET6_BASE@0/127
+2
View File
@@ -0,0 +1,2 @@
ipv6-network = @VPNNET6_BASE@2/127
explicit-ipv6 = @VPNNET6_BASE@2
+2
View File
@@ -0,0 +1,2 @@
ipv6-network = @VPNNET6B@
explicit-ipv6 = @VPNNET6B_BASE@1
+2 -1
View File
@@ -301,7 +301,8 @@ if get_option('root-tests')
'haproxy-connect', 'test-iroute', 'test-multi-cookie',
'test-pass-script', 'idle-timeout', 'idle-timeout-icmpv6', 'test-year-2038',
'test-cookie-timeout', 'test-cookie-timeout-2',
'test-explicit-ip', 'test-ipv4-p2p', 'test-cookie-invalidation',
'test-explicit-ip', 'test-ipv4-p2p', 'test-ipv6-p2p',
'test-cookie-invalidation',
'test-user-config', 'test-append-routes', 'test-ban',
'multiple-routes', 'json', 'test-udp-listen-host',
'test-max-same-1', 'test-vhost-udp-port-inheritance',
+3 -3
View File
@@ -27,9 +27,9 @@
# VPNADDR=192.168.1.1
#
# Provides:
# ${NSCMD1} - to run on NS1
# ${NSCMD2} - to run on NS2
# ${NSCMD3} - to run on NS3
# ${CMDNS1} - to run on NS1
# ${CMDNS2} - to run on NS2
# ${CMDNS3} - to run on NS3
#
# Cleanup is automatic via a trap
# Requires: finish() to be defined
+5 -3
View File
@@ -33,7 +33,8 @@
# to derive addresses and sub-networks within VPNNET
# VPNNET_ADDR - the first host address of VPNNET (also as VPNADDR)
# VPNNET6 - random IPv6 /112 network
# VPNNET6_BASE, VPNNET6_ADDR (also as VPNADDR6) - likewise for VPNNET6
# VPNNET6_BASE, VPNNET6_ADDR (also as VPNADDR6) - likewise for VPNNET6,
# except that VPNNET6_ADDR is the network address itself
# alloc_vpnnet4 NAME, alloc_vpnnet6 NAME - allocate a further network,
# distinct from the ones already allocated, into NAME,
# NAME_BASE and NAME_ADDR. Every allocated NAME is
@@ -115,13 +116,14 @@ _alloc_vpnnet() {
echo "VPN network $1: ${NETWORK}/${PREFIX} overlaps a local route, redrawing"
done
# the network address minus its last all-zero group or octet;
# for IPv6 it ends in ':', so BASE + host number is an address
# for IPv6 it ends in ':', so BASE + host number is an address.
# The IPv6 server address is the network address (REQ-MAIN-NET-006).
if test "$2" = 24; then
_base="${NETWORK%.0}"
_addr="${_base}.1"
else
_base="${NETWORK%0}"
_addr="${_base}1"
_addr="${NETWORK}"
fi
_VPNNETS_ALLOCATED="${_VPNNETS_ALLOCATED} ${NETWORK}"
VPNNET_VARS="${VPNNET_VARS} $1"
+137
View File
@@ -0,0 +1,137 @@
#!/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 <https://www.gnu.org/licenses/>.
# Tests that the server side of the IPv6 tunnel takes the network address:
# on a /127 point-to-point network (RFC 6164) the client is leased the
# other address, and an explicit IP equal to the network address is
# rejected while network + 1 is usable. See REQ-MAIN-NET-006.
#
# The server runs in its own network namespace (ns.sh) because the host
# namespace of CI containers may have IPv6 disabled, which makes setting
# the tun device's IPv6 address fail and the session fall back to IPv4.
SERV="${SERV:-../src/ocserv}"
srcdir=${srcdir:-.}
TMPFILE=outfile.$$
OCCTL_SOCKET=./occtl-ipv6-p2p-$$.socket
SERVERCERT=pin-sha256:xp3scfzy3rOQsv9NcOve/8YVVv+pHr4qNCXEXrNl5s8=
. `dirname $0`/common.sh
. `dirname $0`/random-net.sh
# a second, wider network for the explicit network + 1 case
alloc_vpnnet6 VPNNET6B
eval "${GETPORT}"
function finish {
set +e
test -n "${CPID}" && kill ${CPID} >/dev/null 2>&1
test -n "${PID}" && kill ${PID} >/dev/null 2>&1
rm -f ${TMPFILE} ${CONFIG}
rm -rf ${CONFIG_PER_USER_DIR}
}
. `dirname $0`/ns.sh
# connect <user>: on success leaves the client running as $CPID with its
# verbose output in $TMPFILE
connect()
{
user=$1
COOKIE=''
eval `echo "$user" | ${CMDNS1} $OPENCONNECT -q $ADDRESS:$PORT -u $user --servercert=$SERVERCERT --authenticate`
if [ -z "$COOKIE" ];then
return 1
fi
rm -f $TMPFILE
echo "$user" | ${CMDNS1} $OPENCONNECT -v $ADDRESS:$PORT -u $user -C "$COOKIE" --servercert=$SERVERCERT --script=/bin/true >$TMPFILE 2>&1 &
CPID=$!
sleep 3
if ! grep "Established DTLS" $TMPFILE >/dev/null 2>&1;then
kill $CPID 2>/dev/null
return 1
fi
return 0
}
# check_header <header> <expected value>
check_header()
{
got=`grep "$1:" $TMPFILE | head -1 | sed 's/.*'"$1"': *//' | tr -d '\r'`
if test "$got" != "$2";then
kill $CPID
fail $PID "FAIL: expected $1: $2, got '$got'"
fi
}
# check_server_ip <user> <expected server address>
check_server_ip()
{
got=`${OCCTL} -s ${OCCTL_SOCKET} show user $1 2>&1 | grep "P-t-P IPv6:" | head -1 | sed 's/.*P-t-P IPv6: *//' | awk '{print $1}'`
if test "$got" != "$2";then
kill $CPID
fail $PID "FAIL: expected P-t-P IPv6: $2, got '$got'"
fi
}
echo "Testing the server side IPv6 address and /127 point-to-point networks... "
update_config_dir user-config-ipv6-p2p
update_config test-ipv6-p2p.config
${CMDNS2} ${SERV} -d 1 -f -c "${CONFIG}" & PID=$!
wait_server $PID
echo -n "Connecting with a /127 network... "
connect test
if test $? != 0;then
cat $TMPFILE
fail $PID "FAIL: expected a lease from ${VPNNET6_BASE}0/127, got no connection"
fi
check_header X-CSTP-Address-IP6 ${VPNNET6_BASE}1/128
check_server_ip test ${VPNNET6_ADDR}
kill $CPID
echo ok
echo -n "Connecting with the server address of a /127 as explicit IP... "
connect test2
if test $? = 0;then
kill $CPID
fail $PID "FAIL: expected rejection of ${VPNNET6_BASE}2 (server address), got a connection"
fi
echo ok
echo -n "Connecting with network + 1 of a /112 as explicit IP... "
connect test3
if test $? != 0;then
cat $TMPFILE
fail $PID "FAIL: expected a connection with ${VPNNET6B_BASE}1, got none"
fi
check_header X-CSTP-Address-IP6 ${VPNNET6B_BASE}1/128
check_server_ip test3 ${VPNNET6B_ADDR}
kill $CPID
echo ok
kill $PID
wait
exit 0