config: add scope annotations and validation script

Add machine-readable [scope: X] annotations to doc/sample.config and
src/vpn.h struct fields to document which config options are permanent,
global-only, per-vhost, or per-user/group overridable.

Scope vocabulary:
  global (non-reloadable)  -- in perm_cfg_st; requires restart; cannot differ per vhost
  vhost (non-reloadable)   -- in perm_cfg_st; requires restart; can differ per vhost
  global            -- in cfg_st; reloadable; cannot be set in [vhost:] sections
  vhost             -- in cfg_st; reloadable; can differ per vhost
  vhost user        -- in cfg_st; reloadable; also overridable per user/group

Add tests/check-config-scope.py: a script that cross-checks the annotations
against the actual code:
  (a) every option in sample.config has a [scope:] annotation
  (b-c) [global] options match error_on_vhost() calls in config.c
  (d-e) [vhost user] options match handlers in src/sup-config/file.c
  (f) every field in cfg_st and perm_cfg_st has a [scope:] comment

Also fix a pre-existing bug in src/sup-config/file.c: the tunnel-all-dns
option was compared using an underscore ("tunnel_all_dns") instead of
the correct dash ("tunnel-all-dns"), silently ignoring the per-user
setting.

Signed-off-by: Nikos Mavrogiannopoulos <n.mavrogiannopoulos@gmail.com>
This commit is contained in:
Nikos Mavrogiannopoulos
2026-04-14 18:57:45 +02:00
parent 62a5f72864
commit 611eb00527
8 changed files with 647 additions and 162 deletions
+5
View File
@@ -24,6 +24,11 @@ variables:
-Droot-tests=false
JOBS: 6
config-scope-check:
stage: preliminaries
script:
- python3 tests/check-config-scope.py
Signoff:
stage: preliminaries
script:
+6 -2
View File
@@ -1,4 +1,10 @@
* Version 1.4.2 (unreleased)
- Added VIRTUAL HOSTS and CONFIGURATION sections to ocserv(8) man page;
documented options scope.
- Setting a global-only option inside a [vhost:] section is now a hard
configuration error (previously it was ignored). Options are
annotated with their scope in doc/sample.config; see ocserv(8) for a
description of the configuration scope.
- radius: added group-separator option to auth configuration, allowing
the separator used in OU= Class attributes to be set to 'semicolon'
(default) or 'comma', to support Freeradius deployments (#428)
@@ -9,8 +15,6 @@
- Replaced autoconf/automake build system with meson (#699)
- Added nftables-based ocserv-fw; requires ipcalc-ng/ipcalc (#397)
- No longer need to duplicate global options in virtual hosts (#698)
- Added VIRTUAL HOSTS section to ocserv(8) man page; documented global options
that cannot be overridden at vhost level
- Fixed a bug where a correct password was rejected after a wrong password
attempt in the same session (#323)
- Aligned the default values for 'dpd' and 'mobile-dpd' options with
+59 -20
View File
@@ -80,6 +80,65 @@ server.
Output version of program and exit.
## CONFIGURATION
Each configuration option has a **scope** that determines where it may appear.
**Scope** takes one of three values:
* *global* — the option applies to the entire server and may only appear at
the top level of the configuration file. Placing it inside a `[vhost:]`
section is a configuration error that prevents the server from starting
(e.g. `max-clients`, `device`, `route-add-cmd`).
* *vhost* — the option may appear at the top level applying to the default
virtual host, or inside a specific `[vhost:]` (e.g. `tls-priorities`,
`banner`, `cookie-timeout`).
* *vhost user* — the option may appear at the top level, in a `[vhost:]` section,
and also in per-user or per-group supplemental configuration files
(e.g. `routes`, `iroutes`, `no-udp`).
The description of each option in the configuration file carries a `[scope:]`
annotation that identifies its scope.
Furthermore, certain configuration options specified are not-reloadable and a
change only takes effect when restarting the server. These options are marked
as `(non-reloadable)` in their scope annotation.
## PER-USER AND PER-GROUP CONFIGURATION
Options with *vhost user* scope may be further overridden for individual users or
groups through supplemental INI-format configuration files. The relevant
directories are set with `config-per-user` and `config-per-group` in the main
configuration file.
When a user connects, ocserv looks for a file named after their username in
the per-user directory and a file named after their group in the per-group
directory. Values found there override the corresponding values from the
active virtual host configuration for that session only; they do not affect
other connected users.
Options not specified within a virtual host, or within a per-user/group file,
fall back to the global configuration value, or to the built-in default if
not set globally.
## VIRTUAL HOSTS
Ocserv supports virtual hosts, allowing a single instance to serve multiple
domains with different configurations. This feature operates similarly to
virtual hosts in Apache or Nginx — when clients connect requesting a specific
domain name (via TLS SNI), the server selects the corresponding virtual host
configuration. If no matching virtual host is found, the connection falls back
to the global configuration.
Virtual host sections are introduced in the configuration file with a header
of the form:
[vhost:www.example.com]
All options that follow (until the next section header or end of file) apply
only to that virtual host.
## AUTHENTICATION
Users can be authenticated in multiple ways, which are explained in the following
paragraphs. Connected users can be managed using the _occtl_ tool.
@@ -243,26 +302,6 @@ should be generated as follows.
--load-ca-certificate ca-cert.pem \
--template crl.tmpl --outfile crl.pem
## VIRTUAL HOSTS
Ocserv supports virtual hosts, allowing a single instance to serve multiple
domains with different configurations. This feature operates similarly to
virtual hosts in Apache or Nginx - when clients connect requesting a specific
domain name (via TLS SNI), the server selects the corresponding virtual host
configuration. If no matching virtual host is found, the connection falls back
to the global configuration.
The global configuration can be thought of as a default virtual host that is
used when an incoming connection does not match any explicitly defined virtual
host.
Options not specified within a virtual host are initialized to their default
values. Default value is typically zero or an empty string, unless an option's
description states otherwise.
Note that certain options have global scope (affecting the entire server) and
cannot be specified within virtual hosts. Such options are only recognized
in the global configuration.
## FILES
### ocserv's configuration file format
+119
View File
@@ -47,6 +47,7 @@
# to have been issued within the provided number of seconds. That option is used to
# restrict logins even if the KDC provides long time TGT tickets.
# [scope: vhost (non-reloadable)]
#auth = "pam"
#auth = "pam[gid-min=1000]"
#auth = "plain[passwd=./sample.passwd,otp=./sample.otp]"
@@ -59,6 +60,7 @@ auth = "plain[passwd=./sample.passwd]"
# will be sufficient to login, irrespective of the main 'auth' entries.
# When multiple options are present, they are OR composed (any of them
# succeeding allows login).
# [scope: vhost (non-reloadable)]
#enable-auth = "certificate"
#enable-auth = "gssapi"
#enable-auth = "gssapi[keytab=/etc/key.tab,require-local-user-map=true,tgt-freshness-time=900]"
@@ -73,50 +75,62 @@ auth = "plain[passwd=./sample.passwd]"
# PAM.
#
# Only one accounting method can be specified.
# [scope: vhost (non-reloadable)]
#acct = "radius[config=/etc/radiusclient/radiusclient.conf]"
# Use listen-host to limit to a specific IP or to the IPs of a provided
# hostname.
# This option is global and cannot be set at vhost level.
# [scope: global (non-reloadable)]
#listen-host = [IP|HOSTNAME]
# Use udp-listen-host to limit udp to a specific IP or to the IPs of a provided
# hostname. if not set, listen-host will be used.
# This option is global and cannot be set at vhost level.
# [scope: global (non-reloadable)]
#udp-listen-host = [IP|HOSTNAME]
# When the server has a dynamic DNS address (that may change),
# should set that to true to ask the client to resolve again on
# reconnects.
# [scope: vhost]
#listen-host-is-dyndns = true
# move the listen socket within the specified network namespace
# [scope: global (non-reloadable)]
# listen-netns = "foo"
# TCP and UDP port number (not configurable per vhost)
# [scope: global (non-reloadable)]
tcp-port = 443
# [scope: global (non-reloadable)]
udp-port = 443
# The user the worker processes will be run as. This should be a dedicated
# unprivileged user (e.g., 'ocserv') and no other services should run as this
# user.
# These settings are global and cannot be set at vhost level.
# [scope: global (non-reloadable)]
run-as-user = nobody
# [scope: global (non-reloadable)]
run-as-group = daemon
# socket file used for IPC with occtl. You only need to set that,
# if you use more than a single server.
# This option is global and cannot be set at vhost level.
# [scope: global (non-reloadable)]
#occtl-socket-file = /var/run/occtl.socket
# socket file used for server IPC (worker-main), will be appended with .PID
# It must be accessible within the chroot environment (if any), so it is best
# specified relatively to the chroot directory.
# This option is global and cannot be set at vhost level.
# [scope: global (non-reloadable)]
socket-file = /var/run/ocserv-socket
# The default server directory. Does not require any devices present.
# This option is global and cannot be set at vhost level.
# [scope: global (non-reloadable)]
#chroot-dir = /var/lib/ocserv
# The key and the certificates of the server
@@ -132,6 +146,7 @@ socket-file = /var/run/ocserv-socket
# certificate renewal (they are checked and reloaded periodically;
# a SIGHUP signal to main server will force reload).
# [scope: vhost (non-reloadable)]
#server-cert = /etc/ocserv/server-cert.pem
#server-key = /etc/ocserv/server-key.pem
server-cert = ../tests/certs/server-cert.pem
@@ -141,26 +156,32 @@ server-key = ../tests/certs/server-key.pem
# versions of GnuTLS for supporting DHE ciphersuites.
# Can be generated using:
# certtool --generate-dh-params --outfile /etc/ocserv/dh.pem
# [scope: vhost (non-reloadable)]
#dh-params = /etc/ocserv/dh.pem
# In case PKCS #11, TPM or encrypted keys are used the PINs should be available
# in files. The srk-pin-file is applicable to TPM keys only, and is the
# storage root key.
# [scope: vhost (non-reloadable)]
#pin-file = /etc/ocserv/pin.txt
# [scope: vhost (non-reloadable)]
#srk-pin-file = /etc/ocserv/srkpin.txt
# The password or PIN needed to unlock the key in server-key file.
# Only needed if the file is encrypted or a PKCS #11 object. This
# is an alternative method to pin-file.
# [scope: vhost (non-reloadable)]
#key-pin = 1234
# The SRK PIN for TPM.
# This is an alternative method to srk-pin-file.
# [scope: vhost (non-reloadable)]
#srk-pin = 1234
# The Certificate Authority that will be used to verify
# client certificates (public keys) if certificate authentication
# is set.
# [scope: vhost (non-reloadable)]
#ca-cert = /etc/ocserv/ca.pem
ca-cert = ../tests/certs/ca.pem
@@ -168,6 +189,7 @@ ca-cert = ../tests/certs/ca.pem
# processes. Typically this should not be set as the number of processes
# is determined automatically by the initially set maximum number of clients.
# This option is global and cannot be set at vhost level.
# [scope: global (non-reloadable)]
#sec-mod-scale = 4
@@ -190,22 +212,27 @@ ca-cert = ../tests/certs/ca.pem
# disabling that option and report the failures you, along with system and debugging
# information at: https://gitlab.com/openconnect/ocserv/issues
# This option is global and cannot be set at vhost level.
# [scope: global]
isolate-workers = true
# A banner to be displayed on clients after connection
# [scope: vhost]
#banner = "Welcome"
# A banner to be displayed on clients before connection
# [scope: vhost]
#pre-login-banner = "Welcome"
# Limit the number of clients. Unset or set to zero if unknown. In
# that case the maximum value is ~8k clients.
# This option is global and cannot be set at vhost level.
# [scope: global]
#max-clients = 1024
max-clients = 16
# Limit the number of identical clients (i.e., users connecting
# multiple times). Unset or set to zero for unlimited.
# [scope: vhost user]
max-same-clients = 2
# When the server receives connections from a proxy, like haproxy
@@ -215,6 +242,7 @@ max-same-clients = 2
# and v2 versions of proxy protocol are supported, the v2 version
# is recommended as it is more efficient in parsing.
# This option is global and cannot be set at vhost level.
# [scope: global]
#listen-proxy-proto = true
# Rate limit the number of incoming connections to one every X milliseconds
@@ -222,21 +250,25 @@ max-same-clients = 2
# makes the server more resilient (and prevents connection failures) on
# multiple concurrent connections. Set to zero for no limit.
# This option is global and cannot be set at vhost level.
# [scope: global]
rate-limit-ms = 100
# Stats report time. The number of seconds after which each
# worker process will report its usage statistics (number of
# bytes transferred etc). This is useful when accounting like
# radius is in use.
# [scope: vhost user]
#stats-report-time = 360
# Stats reset time. The period of time statistics kept by main/sec-mod
# processes will be reset. These are the statistics shown by cmd
# 'occtl show stats'. For daily: 86400, weekly: 604800
# This is unrelated to stats-report-time.
# [scope: global (non-reloadable)]
server-stats-reset-time = 604800
# Keepalive in seconds
# [scope: vhost user]
keepalive = 32400
# Dead peer detection in seconds.
@@ -244,6 +276,7 @@ keepalive = 32400
# needs to be short enough to prevent the NAT disassociating
# his UDP session from the port number. Otherwise the client
# could have his UDP connection stalled, for several minutes.
# [scope: vhost user]
dpd = 90
# Dead peer detection for mobile clients. That needs to
@@ -251,6 +284,7 @@ dpd = 90
# often by the DPD messages, and save battery.
# The mobile clients are distinguished from the header
# 'X-AnyConnect-Identifier-Platform'.
# [scope: vhost user]
mobile-dpd = 1800
# If using DTLS, and no UDP traffic is received for this
@@ -259,14 +293,17 @@ mobile-dpd = 1800
# in the case that there is a NAT and the UDP translation
# was deleted. If this is unset, do not attempt to use this
# recovery mechanism.
# [scope: vhost]
switch-to-tcp-timeout = 25
# MTU discovery (DPD must be enabled)
# This option is global and cannot be set at vhost level.
# [scope: global]
try-mtu-discovery = false
# To enable load-balancer connection draining, set server-drain-ms to a value
# higher than your load-balancer health probe interval.
# [scope: global]
#server-drain-ms = 15000
# If you have a certificate from a CA that provides an OCSP
@@ -276,12 +313,14 @@ try-mtu-discovery = false
# You can update this response periodically using:
# ocsptool --ask --load-cert=your_cert --load-issuer=your_ca --outfile response
# Make sure that you replace the following file in an atomic way.
# [scope: vhost]
#ocsp-response = /etc/ocserv/ocsp.der
# The object identifier that will be used to read the user ID in the client
# certificate. The object identifier should be part of the certificate's DN
# Useful OIDs are:
# CN = 2.5.4.3, UID = 0.9.2342.19200300.100.1.1, SAN(rfc822name)
# [scope: vhost]
cert-user-oid = 0.9.2342.19200300.100.1.1
# The object identifier that will be used to read the user group in the
@@ -289,23 +328,33 @@ cert-user-oid = 0.9.2342.19200300.100.1.1
# DN. If the user may belong to multiple groups, then use multiple such fields
# in the certificate's DN. Useful OIDs are:
# OU (organizational unit) = 2.5.4.11
# [scope: vhost]
#cert-group-oid = 2.5.4.11
# The revocation list of the certificates issued by the 'ca-cert' above.
# See the manual to generate an empty CRL initially. The CRL will be reloaded
# periodically when ocserv detects a change in the file. To force a reload use
# SIGHUP.
# [scope: vhost]
#crl = /etc/ocserv/crl.pem
# Uncomment this to enable compression negotiation (LZS, LZ4).
# To increase security it is recommended to keep it disabled;
# see https://ocserv.openconnect-vpn.net/technical.html for rationale.
# [scope: vhost]
#compression = false
# Set the priority order of compression algorithms. Takes a colon-separated
# list of algorithms (e.g., "lz4:lzs"). This is a global setting and cannot
# be set per virtual host.
# [scope: global]
#compression-algo-priority = lz4:lzs
# Set the minimum size under which a packet will not be compressed.
# That is to allow low-latency for VoIP packets. The default size
# is 256 bytes. Modify it if the clients typically use compression
# as well of VoIP with codecs that exceed the default value.
# [scope: vhost]
#no-compress-limit = 256
# GnuTLS priority string; note that SSL 3.0 is disabled by default
@@ -322,6 +371,7 @@ cert-user-oid = 0.9.2342.19200300.100.1.1
# difference with AES_128_CBC_SHA1 (the default for anyconnect clients)
# in your system.
# [scope: vhost]
tls-priorities = "NORMAL:%SERVER_PRECEDENCE:%COMPAT:-VERS-SSL3.0:-VERS-TLS1.0:-VERS-TLS1.1"
# More combinations in priority strings are available, check
@@ -334,24 +384,29 @@ tls-priorities = "NORMAL:%SERVER_PRECEDENCE:%COMPAT:-VERS-SSL3.0:-VERS-TLS1.0:-V
# cipher as the primary TLS channel.Note also, that this option implies
# that the dtls-legacy option is false; this option cannot be enforced
# in the legacy/compat protocol.
# [scope: vhost]
#match-tls-dtls-ciphers = true
# The time (in seconds) that a client is allowed to stay connected prior
# to authentication
# [scope: global]
auth-timeout = 240
# The time (in seconds) that a client is allowed to stay idle (no traffic)
# before being disconnected. Unset to disable.
# [scope: vhost user]
#idle-timeout = 1200
# The time (in seconds) that a client is allowed to stay connected
# Unset to disable. When set a client will be disconnected after being
# continuously connected for this amount of time, and its cookies will
# be invalidated (i.e., re-authentication will be required).
# [scope: vhost user]
#session-timeout = 86400
# The time (in seconds) that a mobile client is allowed to stay idle (no
# traffic) before being disconnected. Unset to disable.
# [scope: vhost user]
#mobile-idle-timeout = 2400
# Banning clients in ocserv works with a point system. IP addresses
@@ -365,18 +420,24 @@ auth-timeout = 240
# are global and cannot be set at vhost level.
#
# Set to zero to disable.
# [scope: global]
max-ban-score = 80
# The duration (in seconds) an IP address remains banned
# after exceeding max-ban-score.
# [scope: global]
ban-time = 300
# The time (in seconds) that all score kept for a client is reset.
# [scope: global]
ban-reset-time = 1200
# In case you'd like to change the default points.
# [scope: global]
#ban-points-wrong-password = 10
# [scope: global]
#ban-points-connection = 1
# [scope: global]
#ban-points-kkdcp = 1
# Cookie timeout (in seconds)
@@ -386,22 +447,26 @@ ban-reset-time = 1200
# the user's connected time, and after user disconnection it
# remains active for this amount of time. That setting should allow a
# reasonable amount of time for roaming between different networks.
# [scope: vhost]
cookie-timeout = 300
# If this is enabled (not recommended) the cookies will stay
# valid even after a user manually disconnects, and until they
# expire. This may improve roaming with some broken clients.
# [scope: vhost]
#persistent-cookies = true
# Whether roaming is allowed, i.e., if true a cookie is
# restricted to a single IP address and cannot be reused
# from a different IP.
# [scope: vhost user]
deny-roaming = false
# ReKey time (in seconds)
# ocserv will ask the client to refresh keys periodically once
# this amount of seconds is elapsed. Set to zero to disable (note
# that, some clients fail if rekey is disabled).
# [scope: vhost]
rekey-time = 172800
# ReKey method
@@ -411,6 +476,7 @@ rekey-time = 172800
# new-tunnel: Will instruct the client to discard and re-establish the channel.
# Use this option only if the connecting clients have issues with the ssl
# option.
# [scope: vhost]
rekey-method = ssl
# Script to call when a client connects and obtains an IP.
@@ -436,27 +502,33 @@ rekey-method = ssl
# These scripts are global and cannot be set at vhost level.
# They receive the virtual host name via the VHOST environment variable.
# [scope: global]
#connect-script = /usr/bin/myscript
# [scope: global]
#disconnect-script = /usr/bin/myscript
# This script is to be called when the client's advertised hostname becomes
# available. It will contain REASON with "host-update" value and the
# variable REMOTE_HOSTNAME in addition to the connect variables.
# [scope: global]
#host-update-script = /usr/bin/myhostnamescript
# UTMP
# Register the connected clients to utmp. This will allow viewing
# the connected clients using the command 'who'.
# This option is global and cannot be set at vhost level.
# [scope: global]
#use-utmp = true
# Whether to enable support for the occtl tool (i.e., either through D-BUS,
# or via a unix socket).
# This option is global and cannot be set at vhost level.
# [scope: global]
use-occtl = true
# PID file. It can be overridden in the command line.
# [scope: global (non-reloadable)]
pid-file = /var/run/ocserv.pid
# Log Level. Ocserv sends the logging messages to standard error
@@ -471,6 +543,7 @@ pid-file = /var/run/ocserv.pid
# 4 http
# 8 sensitive
# 9 TLS
# [scope: global (non-reloadable)]
log-level = 2
# Set the protocol-defined priority (SO_PRIORITY) for packets to
@@ -478,10 +551,12 @@ log-level = 2
# priority. Alternatively this can be used to set the IP Type-
# Of-Service, by setting it to a hexadecimal number (e.g., 0x20).
# This can be set per user/group or globally.
# [scope: vhost user]
#net-priority = 3
# Set the VPN worker process into a specific cgroup. This is Linux
# specific and can be set per user/group or globally.
# [scope: vhost user]
#cgroup = "cpuset,cpu:test"
#
@@ -490,14 +565,17 @@ log-level = 2
# The name to use for the tun device
# This option is global and cannot be set at vhost level.
# [scope: global]
device = vpns
# Whether the generated IPs will be predictable, i.e., IP stays the
# same for the same user when possible.
# [scope: vhost]
predictable-ips = true
# The default domain to be advertised. Multiple domains (functional on
# openconnect clients) can be provided in a space separated list.
# [scope: vhost]
default-domain = example.com
#default-domain = "example.com one.example.com"
@@ -509,36 +587,44 @@ default-domain = example.com
# Note that, you could use addresses from a subnet of your LAN network if you
# enable [proxy arp in the LAN interface](http://ocserv.openconnect-vpn.net/recipes-ocserv-pseudo-bridge.html);
# in that case it is recommended to set ping-leases to true.
# [scope: vhost user]
ipv4-network = 192.168.1.0
# [scope: vhost user]
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.
# [scope: vhost user]
#ipv6-network = fda9:4efe:7e3b:03ea::/48
# Specify the size of the network to provide to clients. It is
# generally recommended to provide clients with a /64 network in
# IPv6, but any subnet may be specified. To provide clients only
# with a single IP use the prefix 128.
# [scope: vhost user]
#ipv6-subnet-prefix = 128
#ipv6-subnet-prefix = 64
# Whether to tunnel all DNS queries via the VPN. This is the default
# when a default route is set.
# [scope: vhost user]
#tunnel-all-dns = true
# The advertised DNS server. Use multiple lines for
# multiple servers.
# [scope: vhost user]
# dns = fc00::4be0
dns = 192.168.1.2
# The NBNS server (if any)
# [scope: vhost user]
#nbns = 192.168.1.3
# The domains over which the provided DNS should be used. Use
# multiple lines for multiple domains.
# [scope: vhost user]
#split-dns = example.com
# Prior to leasing any IP from the pool ping it to verify that
@@ -546,12 +632,14 @@ dns = 192.168.1.2
# Only set to true, if there can be occupied addresses in the
# IP range for leases.
# This option is global and cannot be set at vhost level.
# [scope: global]
ping-leases = false
# Use this option to set a link MTU value to the incoming
# connections. Unset to use the default MTU of the TUN device.
# Note that the MTU is negotiated using the value set and the
# value sent by the peer.
# [scope: vhost user]
#mtu = 1420
# Unset to enable bandwidth restrictions (in bytes/second). The
@@ -559,12 +647,15 @@ ping-leases = false
# The RX direction refers to received data on the server from the
# VPN client, and the TX refers to transmitted data by the server
# to the client.
# [scope: vhost user]
#rx-data-per-sec = 40000
# [scope: vhost user]
#tx-data-per-sec = 40000
# The number of packets (of MTU size) that are available in
# the output buffer. The default is low to improve latency.
# Setting it higher will improve throughput.
# [scope: vhost]
#output-buffer = 10
# Routes to be forwarded to the client. If you need the
@@ -575,6 +666,7 @@ ping-leases = false
# comment out all routes from the server, or use the special keyword
# 'default'.
# [scope: vhost user]
route = 10.10.10.0/255.255.255.0
route = 192.168.0.0/255.255.0.0
#route = fef4:db8:1000:1001::/64
@@ -583,6 +675,7 @@ route = 192.168.0.0/255.255.0.0
# Subsets of the routes above that will not be routed by
# the server.
# [scope: vhost user]
no-route = 192.168.5.0/255.255.255.0
# If set, the script /usr/libexec/ocserv-fw will be called to restrict
@@ -590,12 +683,14 @@ no-route = 192.168.5.0/255.255.255.0
# any other routes. In case of defaultroute, the no-routes are restricted.
# All the routes applied by ocserv can be reverted using /usr/libexec/ocserv-fw
# --removeall. This option can be set globally or in the per-user configuration.
# [scope: vhost user]
#restrict-user-to-routes = true
# This option implies restrict-user-to-routes set to true. If set, the
# script /usr/libexec/ocserv-fw will be called to restrict the user to
# access specific ports in the network. This option can be set globally
# or in the per-user configuration.
# [scope: vhost user]
#restrict-user-to-ports = "tcp(443), tcp(80), udp(443), sctp(99), tcp(583), icmp(), icmpv6()"
# You could also use negation, i.e., block the user from accessing these ports only.
@@ -604,6 +699,7 @@ no-route = 192.168.5.0/255.255.255.0
# When set to true, all client's iroutes are made visible to all
# connecting clients except for the ones offering them. This option
# only makes sense if config-per-user is set.
# [scope: vhost]
#expose-iroutes = true
# Groups that a client is allowed to select from.
@@ -617,22 +713,26 @@ no-route = 192.168.5.0/255.255.255.0
# Add multiple entries for multiple groups. Note that the
# groups are global and cannot be overridden per vhost.
# The group may be followed by a user-friendly name in brackets.
# [scope: vhost]
#select-group = group1
#select-group = group2[My special group]
# Allow easy selection of the authgroup by the user by enabling
# a URL such as vpn.example.com/group_name that the user can
# connect, that will not prompt for the group. Default is disabled.
# [scope: vhost]
#select-group-by-url = false
# The name of the (virtual) group that if selected it would assign the user
# to its default group.
# [scope: vhost]
#default-select-group = DEFAULT
# Instead of specifying manually all the allowed groups, you may instruct
# ocserv to scan all available groups and include the full list. This
# option will scan on startup available groups (e.g., from the password file,
# or PAM) and offer these.
# [scope: vhost]
#auto-select-group = true
# Configuration files that will be applied per user connection or
@@ -654,12 +754,16 @@ no-route = 192.168.5.0/255.255.255.0
# hostname to override any proposed by the user. Note also, that, any
# routes, no-routes, DNS or NBNS servers present will overwrite the global ones.
# [scope: vhost]
#config-per-user = /etc/ocserv/config-per-user/
# [scope: vhost]
#config-per-group = /etc/ocserv/config-per-group/
# When config-per-xxx is specified and there is no group or user that
# matches, then utilize the following configuration.
# [scope: vhost]
#default-user-config = /etc/ocserv/defaults/user.conf
# [scope: vhost]
#default-group-config = /etc/ocserv/defaults/group.conf
# The system command to use to setup a route. %{R} will be replaced with the
@@ -669,11 +773,14 @@ no-route = 192.168.5.0/255.255.255.0
# like 192.168.2.0/255.255.255.0 and %{RI} 192.168.2.0/24 (the argument of iroute).
# These settings are global and cannot be set at vhost level.
# [scope: global]
#route-add-cmd = "ip route add %{R} dev %{D}"
# [scope: global]
#route-del-cmd = "ip route delete %{R} dev %{D}"
# This option allows one to forward a proxy. The special keywords '%{U}'
# and '%{G}', if present will be replaced by the username and group name.
# [scope: vhost]
#proxy-url = http://example.com/
#proxy-url = http://example.com/%{U}/
@@ -689,6 +796,7 @@ no-route = 192.168.5.0/255.255.255.0
#
# The following option is available in ocserv, when compiled with GSSAPI support.
# [scope: vhost]
#kkdcp = "SERVER-PATH KERBEROS-REALM PROTOCOL@SERVER:PORT"
#kkdcp = "/KdcProxy KERBEROS.REALM udp@127.0.0.1:88"
#kkdcp = "/KdcProxy KERBEROS.REALM tcp@127.0.0.1:88"
@@ -715,6 +823,7 @@ no-route = 192.168.5.0/255.255.255.0
# set to true).
# (2) This option cannot be set per-user or per-group; only the global
# version is being sent to client.
# [scope: vhost user]
#user-profile = profile.xml
#
@@ -725,6 +834,7 @@ no-route = 192.168.5.0/255.255.255.0
# will not require clients to present their certificate on every TLS
# connection. It must be set to true to support legacy CISCO clients
# and openconnect clients < 7.08. When set to true, it implies dtls-legacy = true.
# [scope: vhost]
cisco-client-compat = true
# This option allows one to disable the DTLS-PSK negotiation (enabled by default).
@@ -732,6 +842,7 @@ cisco-client-compat = true
# the pre-draft-DTLS negotiation inherited from AnyConnect. It allows the
# DTLS channel to negotiate its ciphers and the DTLS protocol version.
# This option is global and cannot be set at vhost level.
# [scope: global]
#dtls-psk = false
# This option allows one to disable the legacy DTLS negotiation (enabled by default,
@@ -739,17 +850,20 @@ cisco-client-compat = true
# The legacy DTLS uses a pre-draft version of the DTLS protocol and was
# from AnyConnect protocol. It has several limitations, that are addressed
# by the dtls-psk protocol supported by openconnect 7.08+.
# [scope: vhost]
dtls-legacy = true
# This option will enable the settings needed for Cisco SVC IPPhone clients
# to connect. It implies dtls-legacy = true and tls-priorities is changed to
# only the ciphers the device supports.
# [scope: vhost]
cisco-svc-client-compat = false
# This option will enable the X-CSTP-Client-Bypass-Protocol (disabled by default).
# If the server has not configured an IPv6 or IPv4 address pool, enabling this option
# will instruct the client to bypass the server for that IP protocol. The option is
# currently only understood by Anyconnect clients.
# [scope: vhost user]
client-bypass-protocol = false
# The following options are related to server camouflage (hidden service)
@@ -759,15 +873,18 @@ client-bypass-protocol = false
# With "camouflage" enabled, connection to the VPN can be established only if the client provided a specific
# "secret string" in the connection URL, e.g. "https://example.com/?mysecretkey",
# otherwise the server will return HTTP error for all requests.
# [scope: vhost]
camouflage = false
# The URL prefix that should be set on the client (after '?' sign) to pass through the camouflage check,
# e.g. in case of 'mysecretkey', the server URL on the client should be like "https://example.com/?mysecretkey".
# [scope: vhost]
camouflage_secret = "mysecretkey"
# Defines the realm (browser prompt) for HTTP authentication.
# If no realm is set, the server will return 404 Not found error instead of 401 Unauthorized.
# Better change it from the default value to avoid fingerprinting.
# [scope: vhost]
camouflage_realm = "Restricted Content"
#Advanced options
@@ -778,6 +895,7 @@ camouflage_realm = "Restricted Content"
# this may help others, please send your settings and reason to
# the openconnect mailing list. The special keywords '%{U}'
# and '%{G}', if present will be replaced by the username and group name.
# [scope: vhost]
#custom-header = "X-My-Header: hi there"
@@ -802,6 +920,7 @@ ipv4-netmask = 255.255.255.0
cert-user-oid = 0.9.2342.19200300.100.1.1
# HTTP headers
# [scope: vhost]
included-http-headers = Strict-Transport-Security: max-age=31536000 ; includeSubDomains
included-http-headers = X-Frame-Options: deny
included-http-headers = X-Content-Type-Options: nosniff
+9 -6
View File
@@ -727,7 +727,8 @@ struct ini_ctx_st {
static bool error_on_vhost(const char *vname, const char *oname)
{
if (vname) {
fprintf(stderr, ERRSTR
fprintf(stderr,
ERRSTR
"'%s' cannot be set inside a virtual host section\n",
oname);
return true;
@@ -926,11 +927,13 @@ static int cfg_ini_handler(void *_ctx, const char *section, const char *name,
} else if (strcmp(name, "socket-file") == 0) {
if (error_on_vhost(vhost->name, "socket-file"))
return 0;
PREAD_STRING(pool, vhost->perm_config.socket_file_prefix);
PREAD_STRING(pool,
vhost->perm_config.socket_file_prefix);
} else if (strcmp(name, "occtl-socket-file") == 0) {
if (error_on_vhost(vhost->name, "occtl-socket-file"))
return 0;
PREAD_STRING(pool, vhost->perm_config.occtl_socket_file);
PREAD_STRING(pool,
vhost->perm_config.occtl_socket_file);
} else if (strcmp(name, "chroot-dir") == 0) {
if (error_on_vhost(vhost->name, "chroot-dir"))
return 0;
@@ -938,7 +941,8 @@ static int cfg_ini_handler(void *_ctx, const char *section, const char *name,
} else if (strcmp(name, "server-stats-reset-time") == 0) {
/* cannot be modified as it would require sec-mod to
* re-read configuration too */
if (error_on_vhost(vhost->name, "server-stats-reset-time"))
if (error_on_vhost(vhost->name,
"server-stats-reset-time"))
return 0;
READ_NUMERIC(vhost->perm_config.stats_reset_time);
} else if (strcmp(name, "pid-file") == 0) {
@@ -1061,8 +1065,7 @@ static int cfg_ini_handler(void *_ctx, const char *section, const char *name,
#if defined(OCSERV_WORKER_PROCESS)
if (switch_comp_priority(pool, value) == 0) {
fprintf(stderr,
WARNSTR
"invalid compression modstring %s\n",
WARNSTR "invalid compression modstring %s\n",
value);
}
#endif
+1 -1
View File
@@ -113,7 +113,7 @@ static int group_cfg_ini_handler(void *_ctx, const char *section,
} else if (strcmp(name, "restrict-user-to-routes") == 0) {
READ_TF(msg->config->restrict_user_to_routes,
msg->config->has_restrict_user_to_routes);
} else if (strcmp(name, "tunnel_all_dns") == 0) {
} else if (strcmp(name, "tunnel-all-dns") == 0) {
READ_TF(msg->config->tunnel_all_dns,
msg->config->has_tunnel_all_dns);
} else if (strcmp(name, "deny-roaming") == 0) {
+152 -133
View File
@@ -213,212 +213,231 @@ typedef struct kkdcp_st {
unsigned int realms_size;
} kkdcp_st;
/*
* Scope tags for config options:
* [scope: global] -- reloadable; error if set in a [vhost:X] section
* [scope: vhost] -- reloadable; can differ per virtual host
* [scope: vhost user] -- reloadable; settable per-vhost and overridable per-user/group
* Options in perm_cfg_st are permanent (require restart); see tags there.
*/
struct cfg_st {
unsigned int is_dyndns;
unsigned int listen_proxy_proto;
unsigned int stats_report_time;
unsigned int is_dyndns; /* [scope: vhost] */
unsigned int listen_proxy_proto; /* [scope: global] */
unsigned int stats_report_time; /* [scope: vhost user] */
kkdcp_st *kkdcp;
unsigned int kkdcp_size;
kkdcp_st *kkdcp; /* [scope: vhost] */
unsigned int kkdcp_size; /* [scope: vhost] */
char *cert_user_oid; /* The OID that will be used to extract the username */
char *cert_group_oid; /* The OID that will be used to extract the groupname */
char *cert_user_oid; /* [scope: vhost] The OID that will be used to extract the username */
char *cert_group_oid; /* [scope: vhost] The OID that will be used to extract the groupname */
gnutls_certificate_request_t cert_req;
char *priorities;
gnutls_certificate_request_t cert_req; /* [scope: vhost] */
char *priorities; /* [scope: vhost] */
#ifdef ENABLE_COMPRESSION
unsigned int enable_compression;
unsigned int enable_compression; /* [scope: vhost] */
unsigned int
no_compress_limit; /* under this size (in bytes) of data there will be no compression */
no_compress_limit; /* [scope: vhost] under this size (in bytes) of data there will be no compression */
#endif
char *banner;
char *pre_login_banner;
char *ocsp_response; /* file with the OCSP response */
char *default_domain; /* domain to be advertised */
char *banner; /* [scope: vhost] */
char *pre_login_banner; /* [scope: vhost] */
char *ocsp_response; /* [scope: vhost] file with the OCSP response */
char *default_domain; /* [scope: vhost] domain to be advertised */
char **group_list; /* select_group */
unsigned int group_list_size;
char **group_list; /* [scope: vhost] select_group */
unsigned int group_list_size; /* [scope: vhost] */
char **friendly_group_list; /* the same size as group_list_size */
char **friendly_group_list; /* [scope: vhost] the same size as group_list_size */
unsigned int select_group_by_url;
unsigned int auto_select_group;
char *default_select_group;
unsigned int select_group_by_url; /* [scope: vhost] */
unsigned int auto_select_group; /* [scope: vhost] */
char *default_select_group; /* [scope: vhost] */
char **custom_header;
size_t custom_header_size;
char **custom_header; /* [scope: vhost] */
size_t custom_header_size; /* [scope: vhost] */
char **split_dns;
size_t split_dns_size;
char **split_dns; /* [scope: vhost user] */
size_t split_dns_size; /* [scope: vhost user] */
/* http headers to include */
char **included_http_headers;
size_t included_http_headers_size;
char **included_http_headers; /* [scope: vhost] */
size_t included_http_headers_size; /* [scope: vhost] */
unsigned int
append_routes; /* whether to append global routes to per-user config */
append_routes; /* [scope: vhost] whether to append global routes to per-user config */
unsigned int
restrict_user_to_routes; /* whether the firewall script will be run for the user */
restrict_user_to_routes; /* [scope: vhost user] whether the firewall script will be run for the user */
unsigned int
deny_roaming; /* whether a cookie is restricted to a single IP */
time_t cookie_timeout; /* in seconds */
time_t session_timeout; /* in seconds */
deny_roaming; /* [scope: vhost user] whether a cookie is restricted to a single IP */
time_t cookie_timeout; /* [scope: vhost] in seconds */
time_t session_timeout; /* [scope: vhost user] in seconds */
unsigned int
persistent_cookies; /* whether cookies stay valid after disconnect */
persistent_cookies; /* [scope: vhost] whether cookies stay valid after disconnect */
time_t rekey_time; /* in seconds */
unsigned int rekey_method; /* REKEY_METHOD_ */
time_t rekey_time; /* [scope: vhost] in seconds */
unsigned int rekey_method; /* [scope: vhost] REKEY_METHOD_ */
time_t ban_time; /* duration IP remains banned after hitting max_ban_score -> in seconds */
time_t ban_time; /* [scope: global] duration IP remains banned after hitting max_ban_score -> in seconds */
unsigned int
max_ban_score; /* the score allowed before a user is banned (see vpn.h) */
int ban_reset_time;
max_ban_score; /* [scope: global] the score allowed before a user is banned (see vpn.h) */
int ban_reset_time; /* [scope: global] */
unsigned int ban_points_wrong_password;
unsigned int ban_points_connect;
unsigned int ban_points_kkdcp;
unsigned int ban_points_wrong_password; /* [scope: global] */
unsigned int ban_points_connect; /* [scope: global] */
unsigned int ban_points_kkdcp; /* [scope: global] */
/* when using the new PSK DTLS negotiation make sure that
* the negotiated DTLS cipher/mac matches the TLS cipher/mac. */
unsigned int match_dtls_and_tls;
unsigned int dtls_psk; /* whether to enable DTLS-PSK */
unsigned int dtls_legacy; /* whether to enable DTLS-LEGACY */
unsigned int isolate; /* whether seccomp should be enabled or not */
unsigned int auth_timeout; /* timeout of HTTP auth */
unsigned int idle_timeout; /* timeout when idle */
unsigned int mobile_idle_timeout; /* timeout when a mobile is idle */
unsigned int match_dtls_and_tls; /* [scope: vhost] */
unsigned int dtls_psk; /* [scope: global] whether to enable DTLS-PSK */
unsigned int
switch_to_tcp_timeout; /* length of no traffic period to automatically switch to TCP */
unsigned int keepalive;
unsigned int dpd;
unsigned int mobile_dpd;
unsigned int max_clients;
unsigned int max_same_clients;
unsigned int use_utmp;
unsigned int tunnel_all_dns;
unsigned int
use_occtl; /* whether support for the occtl tool will be enabled */
dtls_legacy; /* [scope: vhost] whether to enable DTLS-LEGACY */
unsigned int try_mtu; /* MTU discovery enabled */
unsigned int cisco_client_compat; /* do not require client certificate,
unsigned int
isolate; /* [scope: global] whether seccomp should be enabled or not */
unsigned int auth_timeout; /* [scope: global] timeout of HTTP auth */
unsigned int idle_timeout; /* [scope: vhost user] timeout when idle */
unsigned int
mobile_idle_timeout; /* [scope: vhost user] timeout when a mobile is idle */
unsigned int
switch_to_tcp_timeout; /* [scope: vhost] length of no traffic period to automatically switch to TCP */
unsigned int keepalive; /* [scope: vhost user] */
unsigned int dpd; /* [scope: vhost user] */
unsigned int mobile_dpd; /* [scope: vhost user] */
unsigned int max_clients; /* [scope: global] */
unsigned int max_same_clients; /* [scope: vhost user] */
unsigned int use_utmp; /* [scope: global] */
unsigned int tunnel_all_dns; /* [scope: vhost user] */
unsigned int
use_occtl; /* [scope: global] whether support for the occtl tool will be enabled */
unsigned int try_mtu; /* [scope: global] MTU discovery enabled */
unsigned int
cisco_client_compat; /* [scope: vhost] do not require client certificate,
* and allow auth to complete in different
* TCP sessions. */
unsigned int
cisco_svc_client_compat; /* force allowed ciphers and disable dtls-legacy */
cisco_svc_client_compat; /* [scope: vhost] force allowed ciphers and disable dtls-legacy */
unsigned int
rate_limit_ms; /* if non zero force a connection every rate_limit milliseconds if ocserv-sm is heavily loaded */
rate_limit_ms; /* [scope: global] if non zero force a connection every rate_limit milliseconds if ocserv-sm is heavily loaded */
unsigned int
ping_leases; /* non zero if we need to ping prior to leasing */
ping_leases; /* [scope: global] non zero if we need to ping prior to leasing */
unsigned int
server_drain_ms; /* how long to wait after we stop accepting new connections before closing old connections */
server_drain_ms; /* [scope: global] how long to wait after we stop accepting new connections before closing old connections */
size_t rx_per_sec;
size_t tx_per_sec;
unsigned int net_priority;
size_t rx_per_sec; /* [scope: vhost user] */
size_t tx_per_sec; /* [scope: vhost user] */
unsigned int net_priority; /* [scope: vhost user] */
char *crl;
char *crl; /* [scope: vhost] */
unsigned int output_buffer;
unsigned int default_mtu;
unsigned int predictable_ips; /* boolean */
unsigned int output_buffer; /* [scope: vhost] */
unsigned int default_mtu; /* [scope: vhost user] */
unsigned int predictable_ips; /* [scope: vhost] boolean */
char *route_add_cmd;
char *route_del_cmd;
char *route_add_cmd; /* [scope: global] */
char *route_del_cmd; /* [scope: global] */
char *connect_script;
char *host_update_script;
char *disconnect_script;
char *connect_script; /* [scope: global] */
char *host_update_script; /* [scope: global] */
char *disconnect_script; /* [scope: global] */
char *cgroup;
char *proxy_url;
char *cgroup; /* [scope: vhost user] */
char *proxy_url; /* [scope: vhost] */
#ifdef ANYCONNECT_CLIENT_COMPAT
char *xml_config_file;
char *xml_config_hash;
char *xml_config_file; /* [scope: vhost user] */
char *xml_config_hash; /* [scope: vhost user] */
#endif
unsigned int client_bypass_protocol;
unsigned int client_bypass_protocol; /* [scope: vhost user] */
/* additional configuration files */
char *per_group_dir;
char *per_user_dir;
char *default_group_conf;
char *default_user_conf;
char *per_group_dir; /* [scope: vhost] */
char *per_user_dir; /* [scope: vhost] */
char *default_group_conf; /* [scope: vhost] */
char *default_user_conf; /* [scope: vhost] */
bool gssapi_no_local_user_map;
bool gssapi_no_local_user_map; /* [scope: vhost] */
/* known iroutes - only sent to the users who are not registering them
*/
char **known_iroutes;
size_t known_iroutes_size;
/* known iroutes - only sent to the users who are not registering them */
char **known_iroutes; /* [scope: vhost] */
size_t known_iroutes_size; /* [scope: vhost] */
FwPortSt **fw_ports;
size_t n_fw_ports;
FwPortSt **fw_ports; /* [scope: vhost user] */
size_t n_fw_ports; /* [scope: vhost user] */
/* the tun network */
struct vpn_st network;
struct vpn_st
network; /* [scope: vhost user] dns/routes/network sub-fields */
/* holds a usage count of holders of pointers in this struct */
int *usage_count;
int *usage_count; /* [scope: vhost] */
bool camouflage;
char *camouflage_secret;
char *camouflage_realm;
bool camouflage; /* [scope: vhost] */
char *camouflage_secret; /* [scope: vhost] */
char *camouflage_realm; /* [scope: vhost] */
};
/*
* Permanent config (perm_cfg_st): requires server restart to change.
* Scope tags:
* [scope: global (non-reloadable)] -- cannot differ per virtual host
* [scope: vhost (non-reloadable)] -- can differ per virtual host
*/
struct perm_cfg_st {
/* gets reloaded */
struct cfg_st *config;
/* stuff here don't change on reload */
auth_struct_st auth[MAX_AUTH_METHODS];
unsigned int auth_methods;
acct_struct_st acct;
unsigned int sup_config_type; /* one of SUP_CONFIG_ */
auth_struct_st
auth[MAX_AUTH_METHODS]; /* [scope: vhost (non-reloadable)] */
unsigned int auth_methods; /* [scope: vhost (non-reloadable)] */
acct_struct_st acct; /* [scope: vhost (non-reloadable)] */
unsigned int
sup_config_type; /* [scope: vhost (non-reloadable)] one of SUP_CONFIG_ */
char *chroot_dir; /* where the xml files are served from */
char *occtl_socket_file;
char *socket_file_prefix;
char *chroot_dir; /* [scope: global (non-reloadable)] where the xml files are served from */
char *occtl_socket_file; /* [scope: global (non-reloadable)] */
char *socket_file_prefix; /* [scope: global (non-reloadable)] */
uid_t uid;
gid_t gid;
uid_t uid; /* [scope: global (non-reloadable)] */
gid_t gid; /* [scope: global (non-reloadable)] */
char *key_pin;
char *srk_pin;
char *key_pin; /* [scope: vhost (non-reloadable)] */
char *srk_pin; /* [scope: vhost (non-reloadable)] */
char *pin_file;
char *srk_pin_file;
char **cert;
size_t cert_size;
char **key;
size_t key_size;
char *pin_file; /* [scope: vhost (non-reloadable)] */
char *srk_pin_file; /* [scope: vhost (non-reloadable)] */
char **cert; /* [scope: vhost (non-reloadable)] */
size_t cert_size; /* [scope: vhost (non-reloadable)] */
char **key; /* [scope: vhost (non-reloadable)] */
size_t key_size; /* [scope: vhost (non-reloadable)] */
#ifdef ANYCONNECT_CLIENT_COMPAT
char *cert_hash;
char *cert_hash; /* [scope: vhost (non-reloadable)] */
#endif
unsigned int stats_reset_time;
unsigned int foreground;
unsigned int no_chdir;
unsigned int log_level;
unsigned int log_stderr;
unsigned int syslog;
unsigned int stats_reset_time; /* [scope: global (non-reloadable)] */
unsigned int foreground; /* [scope: global (non-reloadable)] */
unsigned int no_chdir; /* [scope: global (non-reloadable)] */
unsigned int log_level; /* [scope: global (non-reloadable)] */
unsigned int log_stderr; /* [scope: global (non-reloadable)] */
unsigned int syslog; /* [scope: global (non-reloadable)] */
unsigned int pr_dumpable;
unsigned int pr_dumpable; /* [scope: global (non-reloadable)] */
char *ca;
char *dh_params_file;
char *ca; /* [scope: vhost (non-reloadable)] */
char *dh_params_file; /* [scope: vhost (non-reloadable)] */
char *listen_host;
char *udp_listen_host;
char *listen_netns_name;
unsigned int port;
unsigned int udp_port;
char *listen_host; /* [scope: global (non-reloadable)] */
char *udp_listen_host; /* [scope: global (non-reloadable)] */
char *listen_netns_name; /* [scope: global (non-reloadable)] */
unsigned int port; /* [scope: global (non-reloadable)] */
unsigned int udp_port; /* [scope: global (non-reloadable)] */
unsigned int sec_mod_scale;
unsigned int sec_mod_scale; /* [scope: global (non-reloadable)] */
/* for testing ocserv only */
unsigned int debug_no_secmod_stats;
unsigned int debug_no_secmod_stats; /* [scope: global (non-reloadable)] */
/* attic, where old config allocated values are stored */
struct list_head attic;
+296
View File
@@ -0,0 +1,296 @@
#!/usr/bin/env python3
"""
Validate that [scope:] annotations in doc/sample.config are consistent
with the actual config parsing code in src/config.c and src/sup-config/file.c.
Scope vocabulary:
global (non-reloadable) -- perm_cfg_st; requires restart; cannot differ per vhost
vhost (non-reloadable) -- perm_cfg_st; requires restart; can differ per vhost
global -- cfg_st; reloadable; cannot be set in [vhost:] sections
vhost -- cfg_st; reloadable; can differ per vhost
vhost user -- cfg_st; reloadable; per-vhost AND per-user/group overridable
Checks performed:
(a) Every option in sample.config has a [scope: ...] annotation.
(b) Every option annotated [... global] has an error_on_vhost() call in
config.c (or is a permanent-global option in perm_cfg_st, which never
reaches error_on_vhost).
(c) Every error_on_vhost() option in config.c is annotated [... global] in
sample.config.
(d) Every option annotated [...user...] is handled in src/sup-config/file.c.
(e) Every option in src/sup-config/file.c is annotated [...user...] in
sample.config.
(f) Every option in sample.config annotated [...user...] is also annotated
[...vhost...] (user-overridable options must also be settable per-vhost).
(g) Every field in struct cfg_st and struct perm_cfg_st in src/vpn.h has a
[scope: ...] inline comment.
Exit code: 0 on success, 1 if any errors are found.
"""
import re
import sys
import os
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(SCRIPT_DIR)
SAMPLE_CONFIG = os.path.join(ROOT, "doc", "sample.config")
CONFIG_C = os.path.join(ROOT, "src", "config.c")
SUP_CONFIG_FILE = os.path.join(ROOT, "src", "sup-config", "file.c")
VPN_H = os.path.join(ROOT, "src", "vpn.h")
def read_file(path):
with open(path, "r") as f:
return f.read()
# ---------------------------------------------------------------------------
# Step 1: Parse doc/sample.config
# Returns dict: {option_name: scope_string or None}
# ---------------------------------------------------------------------------
def extract_sample_config_scopes(text):
"""
For each option line (has '=' and is not a pure comment/blank),
look backwards for the nearest '# [scope: ...]' line.
"""
lines = text.splitlines()
scopes = {}
for i, line in enumerate(lines):
# Strip leading '#' and whitespace to get the raw content
stripped = line.lstrip()
if stripped.startswith("#"):
after_hash = stripped[1:]
# Only treat as a potential option if 0 or 1 spaces follow '#'.
# Two or more spaces indicate an example buried inside a prose
# comment block (e.g. "# CN = 2.5.4.3" or "# kdc = ...").
if after_hash.startswith(" "):
continue
stripped = after_hash.lstrip()
# Is it an option line? Must contain ' = ' or '= ' and not be empty
if "=" not in stripped or stripped.startswith("["):
continue
# Skip pure comment lines (the line itself starts with '#' then a word)
raw = line.lstrip()
if raw == "" or raw.startswith("###"):
continue
# Extract option name: everything before the first '='
eq_pos = stripped.find("=")
opt = stripped[:eq_pos].strip()
# Reject options that look like key=value inside a comment or contain spaces badly
if not re.match(r'^[a-zA-Z][a-zA-Z0-9_-]*$', opt):
continue
# Avoid duplicates — keep first occurrence (the active/commented-first)
if opt in scopes:
continue
# Walk backwards looking for a '# [scope: ...]' tag
scope = None
for j in range(i - 1, max(i - 10, -1), -1):
prev = lines[j].strip()
m = re.match(r'^#\s*\[scope:\s*([^\]]+)\]', prev)
if m:
scope = m.group(1).strip()
break
# Stop if we hit a blank line or a non-comment line
if prev == "" or (prev and not prev.startswith("#")):
break
scopes[opt] = scope
return scopes
# ---------------------------------------------------------------------------
# Step 2: Parse src/config.c — options that call error_on_vhost()
# ---------------------------------------------------------------------------
def extract_error_on_vhost_options(text):
"""Return set of option names passed to error_on_vhost()."""
# Pattern: error_on_vhost(vhost->name, "option-name")
pattern = re.compile(r'error_on_vhost\s*\([^,]+,\s*"([^"]+)"')
return set(pattern.findall(text))
# ---------------------------------------------------------------------------
# Step 3: Parse src/sup-config/file.c — options handled per-user/group
# ---------------------------------------------------------------------------
def extract_sup_config_options(text):
"""Return set of option names handled in the per-user/group file parser."""
pattern = re.compile(r'strcmp\s*\(\s*name\s*,\s*"([^"]+)"')
return set(pattern.findall(text))
# ---------------------------------------------------------------------------
# Step 4: Parse src/vpn.h — check that cfg_st and perm_cfg_st fields
# all have [scope: ...] annotations.
# ---------------------------------------------------------------------------
def check_vpn_h_annotations(text):
"""
Returns list of field declarations in cfg_st and perm_cfg_st
that are missing a [scope: ...] inline comment.
"""
missing = []
# Find cfg_st body
for struct_name in ("struct cfg_st", "struct perm_cfg_st"):
m = re.search(re.escape(struct_name) + r'\s*\{', text)
if not m:
continue
start = m.end()
# Find matching closing brace
depth = 1
pos = start
while pos < len(text) and depth > 0:
if text[pos] == '{':
depth += 1
elif text[pos] == '}':
depth -= 1
pos += 1
body = text[start:pos - 1]
# Find field declarations: lines ending in ';' that look like declarations
for line in body.splitlines():
stripped = line.strip()
# Skip blank, comment-only, preprocessor, and struct/union lines
if (not stripped or stripped.startswith("/*") or stripped.startswith("//")
or stripped.startswith("#") or stripped.startswith("}")
or stripped.startswith("{") or stripped.startswith("struct ")
or stripped.startswith("typedef ")):
continue
# Must end with ';'
if not stripped.endswith(";"):
continue
# Skip lines that are closing braces with semicolons e.g. '} name;'
if stripped.startswith("}"):
continue
# Check for [scope: ...] annotation
if "[scope:" not in line:
# Extract a clean field name for reporting
# Remove array/pointer noise
name_m = re.search(r'\b(\w+)\s*(?:\[[^\]]*\])?\s*;', stripped)
field = name_m.group(1) if name_m else stripped[:40]
missing.append(f" {struct_name}: field '{field}' has no [scope:] annotation")
return missing
# ---------------------------------------------------------------------------
# Main validation
# ---------------------------------------------------------------------------
def main():
errors = []
sample_text = read_file(SAMPLE_CONFIG)
config_c_text = read_file(CONFIG_C)
sup_config_text = read_file(SUP_CONFIG_FILE)
vpn_h_text = read_file(VPN_H)
# Parse
sample_scopes = extract_sample_config_scopes(sample_text)
global_in_code = extract_error_on_vhost_options(config_c_text)
user_in_code = extract_sup_config_options(sup_config_text)
# Non-reloadable options live in perm_cfg_st and never go through
# error_on_vhost(); they are enforced structurally. Collect from sample.config.
perm_global_opts = {
opt for opt, scope in sample_scopes.items()
if scope and "non-reloadable" in scope
}
# (a) All options in sample.config have a scope annotation
for opt, scope in sorted(sample_scopes.items()):
if scope is None:
errors.append(
f"sample.config: '{opt}' has no [scope:] annotation"
)
# (b) [global] options (reloadable only) must have error_on_vhost in config.c;
# [global (non-reloadable)] options live in perm_cfg_st and are exempt.
for opt, scope in sorted(sample_scopes.items()):
if scope and "global" in scope and "non-reloadable" not in scope:
if opt not in global_in_code:
errors.append(
f"sample.config: '{opt}' is annotated [{scope}] but "
f"has no error_on_vhost() call in config.c"
)
# (c) Every error_on_vhost option must be annotated [... global] in sample.config
for opt in sorted(global_in_code):
scope = sample_scopes.get(opt)
if scope is None:
errors.append(
f"config.c: error_on_vhost('{opt}') but option is missing "
f"from sample.config annotations"
)
elif "global" not in scope:
errors.append(
f"config.c: error_on_vhost('{opt}') but sample.config "
f"annotates it [{scope}] (expected '... global')"
)
# (d) [...user...] options must be in sup-config/file.c
for opt, scope in sorted(sample_scopes.items()):
if scope and "user" in scope:
if opt not in user_in_code:
errors.append(
f"sample.config: '{opt}' is annotated [{scope}] but is not "
f"handled in src/sup-config/file.c"
)
# (e) Every option in sup-config/file.c must be annotated [...user...] in sample.config
# Some options (like 'iroute', 'hostname', 'explicit-ipv4', 'explicit-ipv6')
# are per-user only and don't appear in the global sample.config — skip those.
PER_USER_ONLY = {
"iroute", "hostname", "explicit-ipv4", "explicit-ipv6",
"no-udp",
"ipv4-dns", "ipv6-dns", "ipv4-nbns", "ipv6-nbns",
}
for opt in sorted(user_in_code):
if opt in PER_USER_ONLY:
continue
scope = sample_scopes.get(opt)
if scope is None:
errors.append(
f"sup-config/file.c: handles '{opt}' but option is missing "
f"from sample.config annotations"
)
elif "user" not in scope:
errors.append(
f"sup-config/file.c: handles '{opt}' but sample.config "
f"annotates it [{scope}] (expected '... user ...')"
)
# (f) Every [...user...] option in sample.config must also be [...vhost...]
for opt, scope in sorted(sample_scopes.items()):
if scope and "user" in scope and "vhost" not in scope:
errors.append(
f"sample.config: '{opt}' is annotated [{scope}] but "
f"user-overridable options must also be settable per-vhost "
f"(expected '[scope: vhost user]')"
)
# (g) vpn.h struct field annotations
missing_annot = check_vpn_h_annotations(vpn_h_text)
if missing_annot:
errors.append(
"src/vpn.h: the following fields are missing [scope:] annotations:"
)
errors.extend(missing_annot)
# Report
if errors:
print("config-scope-check: FAILED")
for e in errors:
print(f" ERROR: {e}")
return 1
print(f"config-scope-check: OK ({len(sample_scopes)} options checked)")
return 0
if __name__ == "__main__":
sys.exit(main())