Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1f314d8c1 | ||
|
|
7d8892b8c4 | ||
|
|
4491377d9b | ||
|
|
f1d9c3bc36 | ||
|
|
015fba50ca | ||
|
|
9255d56276 | ||
|
|
07dbe91d0c |
No files matched your search
@@ -913,7 +913,7 @@ ticket gets the shell. The WebSocket of RFC 6455 is implemented on the standard
|
|||||||
web service's `http.server`, server side only, without extensions. `systemd-run --pty` starts the
|
web service's `http.server`, server side only, without extensions. `systemd-run --pty` starts the
|
||||||
shell in a transient unit with a PAM login session, outside the sandbox, and `setsid --ctty`
|
shell in a transient unit with a PAM login session, outside the sandbox, and `setsid --ctty`
|
||||||
makes the terminal its controlling terminal so window sizes reach the shell. A shell idle for
|
makes the terminal its controlling terminal so window sizes reach the shell. A shell idle for
|
||||||
`web.shell_idle_minutes` is closed, and the journal records every opening and end. The Router
|
`web.shell_idle_minutes` is closed, and the journal records every opening, end and refusal. The Router
|
||||||
Manager checks the session and the WebSocket's origin, and relays the bytes to the router after
|
Manager checks the session and the WebSocket's origin, and relays the bytes to the router after
|
||||||
both handshakes.
|
both handshakes.
|
||||||
|
|
||||||
@@ -949,3 +949,23 @@ under the administrator's name. The directory becomes part of the system's name
|
|||||||
nslcd, and the system knows only the accounts with console login on. A user enrolled for TOTP types
|
nslcd, and the system knows only the accounts with console login on. A user enrolled for TOTP types
|
||||||
the code after the password at the SSH and sudo prompts. Homes are created at the first login and
|
the code after the password at the SSH and sudo prompts. Homes are created at the first login and
|
||||||
are not removed with the user. A password is changed through the directory, not with `passwd`.
|
are not removed with the user. A password is changed through the directory, not with `passwd`.
|
||||||
|
|
||||||
|
## ADR-35: WireGuard peers are the devices of the VPN users, kept in the directory
|
||||||
|
|
||||||
|
Context: The WireGuard endpoint could be switched on from the Router Manager, but its peers were
|
||||||
|
written by hand into `wg0.conf`, so nobody could connect without SSH. A peer is a P2S client like
|
||||||
|
an OpenVPN or IKEv2 user, and one person may have several devices.
|
||||||
|
|
||||||
|
Decision: A VPN user's entry holds the user's WireGuard peers in the multi-valued attribute
|
||||||
|
`vpnRouterWireGuardPeer`, one value `<public key> <IP>[,<IP>...]` per device; the router never
|
||||||
|
sees a private key. `PUT /users/<name>` replaces a user's peers after checking the keys, which
|
||||||
|
are unique, and the IPs, which do not overlap another peer's or cover the router's address. Setup
|
||||||
|
reads the peers of the users who are not disabled from the directory and writes them as a marked
|
||||||
|
block of `[Peer]` sections into `wg0.conf`; sections outside the block, added by hand, stay. A
|
||||||
|
change of the peers, and disabling, enabling or removing a user with peers, restart setup. The
|
||||||
|
Router Manager adds and removes peers on the WireGuard tab of Remote access management, where the
|
||||||
|
endpoint is, choosing the user for each.
|
||||||
|
|
||||||
|
Consequences: Peers follow their users: disabling a user drops the user's devices, and the
|
||||||
|
directory's backup holds them. A peer cannot exist without a VPN user. A change restarts
|
||||||
|
`wg-quick@wg0` through setup, which briefly interrupts the other peers.
|
||||||
@@ -114,7 +114,7 @@ must have the type shown below; an unknown section or setting is an error.
|
|||||||
| `p2s` | `enabled` | boolean | `false` | Road-warrior access |
|
| `p2s` | `enabled` | boolean | `false` | Road-warrior access |
|
||||||
| `p2s` | `address_pool` | string | | CIDR assigned to road-warrior clients |
|
| `p2s` | `address_pool` | string | | CIDR assigned to road-warrior clients |
|
||||||
| `wireguard` | `enabled` | boolean | `false` | WireGuard endpoint |
|
| `wireguard` | `enabled` | boolean | `false` | WireGuard endpoint |
|
||||||
| `wireguard` | `address` | string | | Address and prefix for `wg0`, for example `192.168.200.1/24` |
|
| `wireguard` | `address` | string | | The router's own address on `wg0` with the prefix of the peers' network, for example `192.168.200.1/24`; the network and the broadcast address of a network larger than /31 are refused |
|
||||||
| `wireguard` | `listen_port` | integer | `51820` | UDP port |
|
| `wireguard` | `listen_port` | integer | `51820` | UDP port |
|
||||||
| `openvpn` | `enabled` | boolean | `false` | OpenVPN access for users |
|
| `openvpn` | `enabled` | boolean | `false` | OpenVPN access for users |
|
||||||
| `openvpn` | `address_pool` | string | | IPv4 CIDR assigned to OpenVPN clients |
|
| `openvpn` | `address_pool` | string | | IPv4 CIDR assigned to OpenVPN clients |
|
||||||
@@ -237,9 +237,17 @@ an FQDN or an IPv4 or IPv6 address. They are written to the strongSwan configura
|
|||||||
strongSwan reads an address as an IP identity and a name as an FQDN, so a leading `@` is not needed
|
strongSwan reads an address as an IP identity and a name as an FQDN, so a leading `@` is not needed
|
||||||
and is refused. Road-warrior and OpenVPN access need a name in `wan.local_fqdn`.
|
and is refused. Road-warrior and OpenVPN access need a name in `wan.local_fqdn`.
|
||||||
|
|
||||||
WireGuard peers are added by hand in `/etc/wireguard/wg0.conf`. The package owns the
|
WireGuard peers are P2S users' devices: a VPN user of the router's directory may have up to 16
|
||||||
`[Interface]` section of that file and rewrites it when the settings above change, but it
|
peers, each with its public key and its IPs, IPv4 addresses or networks, kept in the user's
|
||||||
carries every `[Peer]` section across untouched.
|
entry (`vpnRouterWireGuardPeer`) and set with `PUT /users/<name>` (`wireguard_peers`) or on the
|
||||||
|
WireGuard tab of the Router Manager's Remote access management. A key is used by one peer only,
|
||||||
|
the IPs of two peers may not overlap, and no peer's IPs cover the router's own address of
|
||||||
|
`wireguard.address`. The package owns the `[Interface]` section of `/etc/wireguard/wg0.conf` and,
|
||||||
|
between the lines `# BEGIN` and `# END WireGuard peers of the users of the router's directory`, a
|
||||||
|
`[Peer]` section for each peer of a user who is not disabled; setup rewrites them when the
|
||||||
|
settings or the peers change, and a change of a user's peers, disabling, enabling or removing a
|
||||||
|
user with peers restarts setup. Every other `[Peer]` section, added by hand, is carried across
|
||||||
|
untouched. While the directory cannot be reached, the users' peers in the file stay as they are.
|
||||||
|
|
||||||
Every feature is independent. Leaving a group empty simply means that feature is not
|
Every feature is independent. Leaving a group empty simply means that feature is not
|
||||||
configured, and its generated files are removed.
|
configured, and its generated files are removed.
|
||||||
@@ -885,7 +893,7 @@ The status holds the state of `vpn-router-setup`, `slapd` and each PostgreSQL cl
|
|||||||
`vpn-router-manager` when that package is installed, of `strongswan` when site-to-site or
|
`vpn-router-manager` when that package is installed, of `strongswan` when site-to-site or
|
||||||
road-warrior access is configured, of `wg-quick@wg0` when WireGuard is, and of
|
road-warrior access is configured, of `wg-quick@wg0` when WireGuard is, and of
|
||||||
`openvpn-server@vpn-router` when OpenVPN is; the number of connected
|
`openvpn-server@vpn-router` when OpenVPN is; the number of connected
|
||||||
OpenVPN clients, from the server's status file, which OpenVPN rewrites every minute; the package version and,
|
OpenVPN clients, from the server's status file, which OpenVPN rewrites every 5 seconds; the package version and,
|
||||||
as `manager_version`, the installed version of `vpn-router-manager` (`null`, shown as not installed, without it); whether
|
as `manager_version`, the installed version of `vpn-router-manager` (`null`, shown as not installed, without it); whether
|
||||||
Docker is installed, and when it is, the state of `docker.service` and of `vpn-router-docker`
|
Docker is installed, and when it is, the state of `docker.service` and of `vpn-router-docker`
|
||||||
(the Docker rules), which do not change `status`; as `site_to_site`, the number of site-to-site
|
(the Docker rules), which do not change `status`; as `site_to_site`, the number of site-to-site
|
||||||
@@ -901,7 +909,7 @@ curl http://10.0.0.4:7081/status
|
|||||||
|
|
||||||
### Tokens and signing in
|
### Tokens and signing in
|
||||||
|
|
||||||
`/config`, `/users`, `/service-accounts`, `/groups`, `/site-to-site`, `/shell` and `/account` need a JWT in an `Authorization: Bearer` header, signed with
|
`/config`, `/users`, `/service-accounts`, `/groups`, `/site-to-site`, `/backup`, `/remote-access`, `/ca`, `/shell` and `/account` need a JWT in an `Authorization: Bearer` header, signed with
|
||||||
HS256 by the key the web service makes on its first run, `/etc/vpn-router/web.key` (root only).
|
HS256 by the key the web service makes on its first run, `/etc/vpn-router/web.key` (root only).
|
||||||
The key never leaves the router; while it does not exist these paths and `/auth` are not served
|
The key never leaves the router; while it does not exist these paths and `/auth` are not served
|
||||||
(404). A missing, expired or wrongly signed token, or one of another algorithm, gets 401 with
|
(404). A missing, expired or wrongly signed token, or one of another algorithm, gets 401 with
|
||||||
@@ -984,7 +992,7 @@ the router are the terminal's output. The shell is a PAM login session of the co
|
|||||||
run by `systemd-run` in a transient unit `vpn-router-shell-<id>`; sudo works as for SSH. It is closed after `web.shell_idle_minutes` without a keystroke, when the shell
|
run by `systemd-run` in a transient unit `vpn-router-shell-<id>`; sudo works as for SSH. It is closed after `web.shell_idle_minutes` without a keystroke, when the shell
|
||||||
exits, or when the connection closes; the router then sends a close frame with the reason. The
|
exits, or when the connection closes; the router then sends a close frame with the reason. The
|
||||||
journal of `vpn-router-web` records who opened each shell, from which address, in which unit,
|
journal of `vpn-router-web` records who opened each shell, from which address, in which unit,
|
||||||
when it ended and why.
|
when it ended and why, and each refused ticket or shell with the reason.
|
||||||
|
|
||||||
### The /config page
|
### The /config page
|
||||||
|
|
||||||
@@ -998,7 +1006,7 @@ when it ended and why.
|
|||||||
| `wireguard` | The public key and port of `wg0`, and each peer's public key, endpoint, allowed IPs, handshake age and traffic; no private or preshared key |
|
| `wireguard` | The public key and port of `wg0`, and each peer's public key, endpoint, allowed IPs, handshake age and traffic; no private or preshared key |
|
||||||
| `docker` | `docker.exposed_ports` and the rules of the `VPN-ROUTER-DOCKER` chain |
|
| `docker` | `docker.exposed_ports` and the rules of the `VPN-ROUTER-DOCKER` chain |
|
||||||
| `postgresql` | `clusters`: each PostgreSQL cluster of `pg_lsclusters` with its version, name, port, status and data directory, and for one that is online its `settings`: `server_version`, `listen_addresses`, `port`, `max_connections`, `shared_buffers`, `effective_cache_size`, `work_mem`, `password_encryption`, `ssl` and `timezone`; `null` when `pg_lsclusters` fails |
|
| `postgresql` | `clusters`: each PostgreSQL cluster of `pg_lsclusters` with its version, name, port, status and data directory, and for one that is online its `settings`: `server_version`, `listen_addresses`, `port`, `max_connections`, `shared_buffers`, `effective_cache_size`, `work_mem`, `password_encryption`, `ssl` and `timezone`; `null` when `pg_lsclusters` fails |
|
||||||
| `caddy` | The installed Caddy: `version`, and `source`, `upstream` once Caddy's repository is configured and `ubuntu` otherwise; `null` when Caddy is not installed |
|
| `caddy` | The installed Caddy: `version`, `source`, `upstream` once Caddy's repository is configured and `ubuntu` otherwise, and `service`, the state of `caddy.service`; `null` when Caddy is not installed |
|
||||||
|
|
||||||
### The /postgresql page
|
### The /postgresql page
|
||||||
|
|
||||||
@@ -1017,9 +1025,9 @@ and answer bodies are JSON; a request body is at most 65536 bytes.
|
|||||||
|
|
||||||
| Request | Body | Answer |
|
| Request | Body | Answer |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `GET /users` | | `{"users": [{"name", "manager_only", "service", "internal", "enabled", "locked", "certificate", "otp", "console", "first_name", "last_name", "email"}, ...]}`; `console` is `null` or `{"enabled", "account", "local", "uid"}` |
|
| `GET /users` | | `{"users": [{"name", "manager_only", "service", "internal", "enabled", "locked", "certificate", "otp", "console", "wireguard_peers", "first_name", "last_name", "email"}, ...], "wireguard": {"enabled", "address"}}`; `console` is `null` or `{"enabled", "account", "local", "uid"}`, `wireguard_peers` a list of `{"public_key", "ips"}`, and `wireguard` the endpoint's settings, whose network the peers' addresses come from |
|
||||||
| `POST /users` | `{"name", "password", "first_name", "last_name", "email"}` for a VPN user; `{"name", "password", "email"?}` for a Manager-only account | 201 and the user |
|
| `POST /users` | `{"name", "password", "first_name", "last_name", "email"}` for a VPN user; `{"name", "password", "email"?}` for a Manager-only account | 201 and the user |
|
||||||
| `PUT /users/<name>` | any of `{"first_name", "last_name", "email"}`; for a Manager-only account only `{"email"}`, `""` removing it | the user |
|
| `PUT /users/<name>` | any of `{"first_name", "last_name", "email", "wireguard_peers"}`; `wireguard_peers`, `[{"public_key", "ips"}]`, replaces a VPN user's WireGuard peers (see Configuration); for a Manager-only account only `{"email"}`, `""` removing it | the user |
|
||||||
| `PUT /users/<name>/password` | `{"password": ...}` | the user |
|
| `PUT /users/<name>/password` | `{"password": ...}` | the user |
|
||||||
| `POST /users/<name>/disable` | | the user |
|
| `POST /users/<name>/disable` | | the user |
|
||||||
| `POST /users/<name>/enable` | | the user |
|
| `POST /users/<name>/enable` | | the user |
|
||||||
@@ -1142,6 +1150,23 @@ The answer of `GET /backup` holds:
|
|||||||
|
|
||||||
A change is made, checked and applied as for `/site-to-site`.
|
A change is made, checked and applied as for `/site-to-site`.
|
||||||
|
|
||||||
|
### The /remote-access API
|
||||||
|
|
||||||
|
`/remote-access` shows and changes the remote access systems, IKEv2 road-warrior (`p2s`),
|
||||||
|
OpenVPN (`openvpn`) and WireGuard (`wireguard`), with the same token as `/users`.
|
||||||
|
|
||||||
|
| Request | Body | Answer |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `GET /remote-access` | | `{"p2s", "openvpn", "wireguard", "running", "local_fqdn", "wireguard_server"}`; `wireguard_server` is `{"public_key", "endpoint", "allowed_ips"}`: the router's WireGuard public key (`null` before setup made it), `<wan.local_fqdn>:<wireguard.listen_port>` (`null` without a name) and the networks a peer reaches through the tunnel, the router's local CIDRs and the WireGuard network |
|
||||||
|
| `PUT /remote-access/<system>` | any of the settings of the section `p2s`, `openvpn` or `wireguard` | the section |
|
||||||
|
|
||||||
|
`running` tells for each system whether setup runs it. IKEv2 and OpenVPN run once enabled with an
|
||||||
|
address pool, `wan.local_fqdn` set and the router's VPN server certificate issued by the P2S CA;
|
||||||
|
WireGuard once enabled with an address. A system enabled without its address pool or address is
|
||||||
|
refused. A change is made, checked and applied as for `/site-to-site`. The router's firewall
|
||||||
|
follows the settings; a cloud firewall in front of the router, such as an Azure network security
|
||||||
|
group, must allow the OpenVPN and WireGuard ports itself.
|
||||||
|
|
||||||
### The /ca API
|
### The /ca API
|
||||||
|
|
||||||
`/ca` manages the PKI (see Certificates) with the same token as `/users`, with the module
|
`/ca` manages the PKI (see Certificates) with the same token as `/users`, with the module
|
||||||
|
|||||||
@@ -1,3 +1,78 @@
|
|||||||
|
vpn-router (0.25.3-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
fix: /config gives the state of caddy.service with the installed Caddy, for the Router Manager's Web page.
|
||||||
|
|
||||||
|
* caddy of /config is {"version", "source", "service"}.
|
||||||
|
|
||||||
|
-- Slawomir Koszewski <slawek@koszewscy.waw.pl> Tue, 03 Nov 2026 05:00:00 +0100
|
||||||
|
|
||||||
|
vpn-router (0.25.2-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
fix: wireguard.address must be the router's own address in its network, not the network or broadcast address, from which the peers' addresses are told apart.
|
||||||
|
|
||||||
|
* A wireguard.address that is the network or the broadcast address of a
|
||||||
|
network larger than /31, such as 192.168.232.0/24, is refused with the
|
||||||
|
first address of the network as the example.
|
||||||
|
|
||||||
|
-- Slawomir Koszewski <slawek@koszewscy.waw.pl> Tue, 03 Nov 2026 02:30:00 +0100
|
||||||
|
|
||||||
|
vpn-router (0.25.1-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
fix: /remote-access gives the router's WireGuard public key, endpoint and allowed IPs, which a peer's configuration needs.
|
||||||
|
|
||||||
|
* GET /remote-access answers wireguard_server: {"public_key",
|
||||||
|
"endpoint", "allowed_ips"}.
|
||||||
|
|
||||||
|
-- Slawomir Koszewski <slawek@koszewscy.waw.pl> Tue, 03 Nov 2026 01:30:00 +0100
|
||||||
|
|
||||||
|
vpn-router (0.25.0-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
feature: a VPN user has WireGuard peers, each a device with its public key and IPs, kept in the directory and written into wg0.conf by setup.
|
||||||
|
|
||||||
|
* PUT /users/<name> takes wireguard_peers, [{"public_key", "ips"}],
|
||||||
|
which replaces the user's peers; keys are unique and IPs may not
|
||||||
|
overlap another peer's or cover the router's WireGuard address.
|
||||||
|
* GET /users answers each user's wireguard_peers and the WireGuard
|
||||||
|
endpoint's enabled and address.
|
||||||
|
* Setup writes a [Peer] section into wg0.conf for each peer of a user
|
||||||
|
who is not disabled, in a marked block, and keeps the peers added by
|
||||||
|
hand; changing a user's peers, disabling, enabling or removing a user
|
||||||
|
with peers applies at once.
|
||||||
|
|
||||||
|
-- Slawomir Koszewski <slawek@koszewscy.waw.pl> Tue, 03 Nov 2026 00:00:00 +0100
|
||||||
|
|
||||||
|
vpn-router (0.24.1-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
fix: the status gives the number of connected OpenVPN clients at most 5 seconds old instead of up to a minute.
|
||||||
|
|
||||||
|
* The OpenVPN server writes its status file every 5 seconds, so a
|
||||||
|
refresh of the status shows clients that connected or left since.
|
||||||
|
|
||||||
|
-- Slawomir Koszewski <slawek@koszewscy.waw.pl> Mon, 02 Nov 2026 23:30:00 +0100
|
||||||
|
|
||||||
|
vpn-router (0.24.0-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
feature: /remote-access of the web service shows and changes the settings of IKEv2 road-warrior, OpenVPN and WireGuard access and whether each runs, for the Router Manager's Remote access page.
|
||||||
|
|
||||||
|
* GET /remote-access answers the p2s, openvpn and wireguard sections,
|
||||||
|
which of them setup runs, and wan.local_fqdn; PUT
|
||||||
|
/remote-access/<system> changes one section and restarts setup.
|
||||||
|
* p2s.enabled and openvpn.enabled need their address_pool, and
|
||||||
|
wireguard.enabled its address.
|
||||||
|
|
||||||
|
-- Slawomir Koszewski <slawek@koszewscy.waw.pl> Mon, 02 Nov 2026 23:00:00 +0100
|
||||||
|
|
||||||
|
vpn-router (0.23.2-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
fix: the shell of a user's own POSIX account opens on the first setup that sets up console login, and the journal records each refused shell with the reason.
|
||||||
|
|
||||||
|
* Setup restarts vpn-router-web when it adds ldap to the name service
|
||||||
|
switch, so the web service finds the POSIX accounts of the directory.
|
||||||
|
* The journal of vpn-router-web records each refused ticket and shell,
|
||||||
|
with the user, the address and the reason.
|
||||||
|
|
||||||
|
-- Slawomir Koszewski <slawek@koszewscy.waw.pl> Mon, 02 Nov 2026 22:00:00 +0100
|
||||||
|
|
||||||
vpn-router (0.23.1-1) unstable; urgency=medium
|
vpn-router (0.23.1-1) unstable; urgency=medium
|
||||||
|
|
||||||
fix: the shell runs as the console account of each member of admins, a local account or the user's own POSIX account in the directory, which also signs in with SSH and sudo, instead of one web.shell_user.
|
fix: the shell runs as the console account of each member of admins, a local account or the user's own POSIX account in the directory, which also signs in with SSH and sudo, instead of one web.shell_user.
|
||||||
|
|||||||
@@ -555,8 +555,8 @@ def manage_console(cfg):
|
|||||||
|
|
||||||
The local group vpnrouter_ldap.CONSOLE_GROUP is created when missing, and sudo lets its members run any command
|
The local group vpnrouter_ldap.CONSOLE_GROUP is created when missing, and sudo lets its members run any command
|
||||||
with their password. nslcd serves the directory's posixAccounts with console login on through the local socket,
|
with their password. nslcd serves the directory's posixAccounts with console login on through the local socket,
|
||||||
and checks their passwords by binding as the user; the passwd line of the name service switch gains ldap, and PAM
|
and checks their passwords by binding as the user; the passwd line of the name service switch gains ldap, which
|
||||||
creates a home at the first login. While ssh.ldap_login is true, sshd accepts the password of the group's members;
|
restarts the web service, and PAM creates a home at the first login. While ssh.ldap_login is true, sshd accepts the password of the group's members;
|
||||||
a drop-in sshd -t refuses is removed again.
|
a drop-in sshd -t refuses is removed again.
|
||||||
"""
|
"""
|
||||||
group = vpnrouter_ldap.CONSOLE_GROUP
|
group = vpnrouter_ldap.CONSOLE_GROUP
|
||||||
@@ -601,6 +601,9 @@ def manage_console(cfg):
|
|||||||
for line in lines]
|
for line in lines]
|
||||||
if switched != lines:
|
if switched != lines:
|
||||||
NSSWITCH.write_text(''.join(switched), encoding='utf-8')
|
NSSWITCH.write_text(''.join(switched), encoding='utf-8')
|
||||||
|
# A running process keeps the name service switch it read first, so the web service's shell would not find
|
||||||
|
# the console accounts of the directory until it restarts.
|
||||||
|
run(['systemctl', 'try-restart', 'vpn-router-web.service'], check=False)
|
||||||
|
|
||||||
if 'pam_mkhomedir' not in PAM_SESSION.read_text(encoding='utf-8'):
|
if 'pam_mkhomedir' not in PAM_SESSION.read_text(encoding='utf-8'):
|
||||||
run(['pam-auth-update', '--enable', 'mkhomedir'], check=False)
|
run(['pam-auth-update', '--enable', 'mkhomedir'], check=False)
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ if str(LIB_DIR) not in sys.path:
|
|||||||
import vpnrouter_ldap
|
import vpnrouter_ldap
|
||||||
import vpnrouter_pki
|
import vpnrouter_pki
|
||||||
import vpnrouter_platforms
|
import vpnrouter_platforms
|
||||||
|
import vpnrouter_status
|
||||||
from vpnrouter_net import DEFAULT_LISTEN, LISTEN_CHOICES, LOOPBACK, ConfigError, detect_interfaces, interface_addresses, ip_output
|
from vpnrouter_net import DEFAULT_LISTEN, LISTEN_CHOICES, LOOPBACK, ConfigError, detect_interfaces, interface_addresses, ip_output
|
||||||
|
|
||||||
CONFIG_FILE = pathlib.Path('/etc/vpn-router/vpn-router.json')
|
CONFIG_FILE = pathlib.Path('/etc/vpn-router/vpn-router.json')
|
||||||
@@ -37,6 +38,11 @@ SITE_TO_SITE = SWANCTL_CONF_D / 'site-to-site.conf'
|
|||||||
ROAD_WARRIOR = SWANCTL_CONF_D / 'road-warrior.conf'
|
ROAD_WARRIOR = SWANCTL_CONF_D / 'road-warrior.conf'
|
||||||
P2S_FORWARDER = pathlib.Path('/etc/systemd/resolved.conf.d/p2s-forwarder.conf')
|
P2S_FORWARDER = pathlib.Path('/etc/systemd/resolved.conf.d/p2s-forwarder.conf')
|
||||||
WG_CONF = pathlib.Path('/etc/wireguard/wg0.conf')
|
WG_CONF = pathlib.Path('/etc/wireguard/wg0.conf')
|
||||||
|
#: The router's WireGuard public key, which setup writes with its private key.
|
||||||
|
WG_PUBLIC_KEY = pathlib.Path('/etc/wireguard/wg0.pub')
|
||||||
|
#: The lines around the [Peer] sections of the users' WireGuard peers in WG_CONF, which setup writes from the directory.
|
||||||
|
WG_PEERS_BEGIN = "# BEGIN WireGuard peers of the users of the router's directory"
|
||||||
|
WG_PEERS_END = "# END WireGuard peers of the users of the router's directory"
|
||||||
OPENVPN_CONF = pathlib.Path('/etc/openvpn/server/vpn-router.conf')
|
OPENVPN_CONF = pathlib.Path('/etc/openvpn/server/vpn-router.conf')
|
||||||
OPENVPN_LDAP = pathlib.Path('/etc/openvpn/server/vpn-router-ldap.conf')
|
OPENVPN_LDAP = pathlib.Path('/etc/openvpn/server/vpn-router-ldap.conf')
|
||||||
|
|
||||||
@@ -435,6 +441,9 @@ def validate(cfg):
|
|||||||
raise ValueError
|
raise ValueError
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise ConfigError(f'openvpn.address_pool: {openvpn_pool!r} is not an IPv4 network')
|
raise ConfigError(f'openvpn.address_pool: {openvpn_pool!r} is not an IPv4 network')
|
||||||
|
for system, option in (('p2s', 'address_pool'), ('openvpn', 'address_pool'), ('wireguard', 'address')):
|
||||||
|
if _get(cfg, system, 'enabled') and not _get(cfg, system, option):
|
||||||
|
raise ConfigError(f'{system}.enabled needs {system}.{option}')
|
||||||
_check_address(_get(cfg, 'local', 'int_addr'), 'local.int_addr')
|
_check_address(_get(cfg, 'local', 'int_addr'), 'local.int_addr')
|
||||||
_check_address(_get(cfg, 'local', 'int_gateway_ip'), 'local.int_gateway_ip')
|
_check_address(_get(cfg, 'local', 'int_gateway_ip'), 'local.int_gateway_ip')
|
||||||
|
|
||||||
@@ -449,10 +458,16 @@ def validate(cfg):
|
|||||||
wg_address = _get(cfg, 'wireguard', 'address')
|
wg_address = _get(cfg, 'wireguard', 'address')
|
||||||
if wg_address:
|
if wg_address:
|
||||||
try:
|
try:
|
||||||
ipaddress.ip_interface(wg_address)
|
interface = ipaddress.ip_interface(wg_address)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise ConfigError(
|
raise ConfigError(
|
||||||
f'wireguard.address: {wg_address!r} is not an address with prefix')
|
f'wireguard.address: {wg_address!r} is not an address with prefix')
|
||||||
|
# The router's own address on wg0, from which the peers' addresses of the same network are told apart.
|
||||||
|
network = interface.network
|
||||||
|
if network.num_addresses > 2 and interface.ip in (network.network_address, network.broadcast_address):
|
||||||
|
raise ConfigError(
|
||||||
|
f'wireguard.address: {wg_address} is the network or broadcast address of {network}; give the '
|
||||||
|
f'router\'s own address in it, such as {next(network.hosts())}/{network.prefixlen}')
|
||||||
|
|
||||||
port = _get(cfg, 'wireguard', 'listen_port')
|
port = _get(cfg, 'wireguard', 'listen_port')
|
||||||
if not 1 <= port <= 65535:
|
if not 1 <= port <= 65535:
|
||||||
@@ -883,6 +898,8 @@ def build_context(cfg, public_ip=None):
|
|||||||
'wg_enabled': _get(cfg, 'wireguard', 'enabled'),
|
'wg_enabled': _get(cfg, 'wireguard', 'enabled'),
|
||||||
'wg_address': _get(cfg, 'wireguard', 'address'),
|
'wg_address': _get(cfg, 'wireguard', 'address'),
|
||||||
'wg_listen_port': _get(cfg, 'wireguard', 'listen_port'),
|
'wg_listen_port': _get(cfg, 'wireguard', 'listen_port'),
|
||||||
|
'wg_peers_begin': WG_PEERS_BEGIN,
|
||||||
|
'wg_peers_end': WG_PEERS_END,
|
||||||
'openvpn_enabled': _get(cfg, 'openvpn', 'enabled'),
|
'openvpn_enabled': _get(cfg, 'openvpn', 'enabled'),
|
||||||
'openvpn_pool': openvpn_pool,
|
'openvpn_pool': openvpn_pool,
|
||||||
'openvpn_network': str(openvpn_network.network_address) if openvpn_network else '',
|
'openvpn_network': str(openvpn_network.network_address) if openvpn_network else '',
|
||||||
@@ -893,6 +910,8 @@ def build_context(cfg, public_ip=None):
|
|||||||
'openvpn_protocol': _get(cfg, 'openvpn', 'protocol'),
|
'openvpn_protocol': _get(cfg, 'openvpn', 'protocol'),
|
||||||
'openvpn_device': OPENVPN_DEVICE,
|
'openvpn_device': OPENVPN_DEVICE,
|
||||||
'openvpn_ldap_conf': str(OPENVPN_LDAP),
|
'openvpn_ldap_conf': str(OPENVPN_LDAP),
|
||||||
|
'openvpn_status': vpnrouter_status.OPENVPN_STATUS,
|
||||||
|
'openvpn_status_seconds': vpnrouter_status.OPENVPN_STATUS_SECONDS,
|
||||||
'p2s_dns_list': _split_list(_join(local_addrs, _split_list(extras['EXTRA_P2S_DNS']))),
|
'p2s_dns_list': _split_list(_join(local_addrs, _split_list(extras['EXTRA_P2S_DNS']))),
|
||||||
'openvpn_ca': str(vpnrouter_pki.OPENVPN_CA),
|
'openvpn_ca': str(vpnrouter_pki.OPENVPN_CA),
|
||||||
'openvpn_crl': str(vpnrouter_pki.OPENVPN_CRL),
|
'openvpn_crl': str(vpnrouter_pki.OPENVPN_CRL),
|
||||||
@@ -975,27 +994,38 @@ def render(env, ctx, template_name, dest, mode):
|
|||||||
|
|
||||||
|
|
||||||
def existing_peers(path):
|
def existing_peers(path):
|
||||||
"""Return the [Peer] sections of an existing WireGuard config.
|
"""The block of the users' peers and the [Peer] sections added by hand of an existing WireGuard config, each ''
|
||||||
|
without one.
|
||||||
|
|
||||||
Peers are added by the operator by hand, so re-rendering the [Interface]
|
The block lies between WG_PEERS_BEGIN and WG_PEERS_END; the [Peer] sections outside it are the operator's, which
|
||||||
section must not discard them.
|
re-rendering the file keeps.
|
||||||
"""
|
"""
|
||||||
path = pathlib.Path(path)
|
path = pathlib.Path(path)
|
||||||
if not path.is_file():
|
if not path.is_file():
|
||||||
return ''
|
return '', ''
|
||||||
lines = path.read_text(encoding='utf-8').splitlines(keepends=True)
|
text = path.read_text(encoding='utf-8')
|
||||||
|
block = ''
|
||||||
|
begin, end = text.find(WG_PEERS_BEGIN), text.find(WG_PEERS_END)
|
||||||
|
if begin != -1 and end > begin:
|
||||||
|
block = text[begin:end + len(WG_PEERS_END)]
|
||||||
|
text = text[:begin] + text[end + len(WG_PEERS_END):]
|
||||||
|
lines = text.splitlines(keepends=True)
|
||||||
for index, line in enumerate(lines):
|
for index, line in enumerate(lines):
|
||||||
if line.strip().lower() == '[peer]':
|
if line.strip().lower() == '[peer]':
|
||||||
return ''.join(lines[index:])
|
return block, ''.join(lines[index:])
|
||||||
return ''
|
return block, ''
|
||||||
|
|
||||||
|
|
||||||
def render_wg_conf(env, ctx, dest=WG_CONF, mode=0o600):
|
def render_wg_conf(env, ctx, dest=WG_CONF, mode=0o600):
|
||||||
"""Render wg0.conf, carrying any operator-defined peers across."""
|
"""Render wg0.conf: the [Interface], the block of the users' peers of ctx's wg_peers, the block the file holds while
|
||||||
content = env.get_template('wg0.conf.j2').render(ctx)
|
wg_peers is None, and the peers the operator added by hand, carried across."""
|
||||||
peers = existing_peers(dest)
|
content = env.get_template('wg0.conf.j2').render(ctx).rstrip('\n') + '\n'
|
||||||
if peers:
|
block, own = existing_peers(dest)
|
||||||
content = content.rstrip('\n') + '\n\n' + peers
|
if ctx['wg_peers'] is not None:
|
||||||
|
block = env.get_template('wg0-peers.conf.j2').render(ctx).strip('\n') if ctx['wg_peers'] else ''
|
||||||
|
for part in (block, own):
|
||||||
|
if part.strip():
|
||||||
|
content += '\n' + part.strip('\n') + '\n'
|
||||||
|
|
||||||
dest = pathlib.Path(dest)
|
dest = pathlib.Path(dest)
|
||||||
if dest.exists() and dest.read_text(encoding='utf-8') == content:
|
if dest.exists() and dest.read_text(encoding='utf-8') == content:
|
||||||
@@ -1064,6 +1094,13 @@ def configure(cfg=None, public_ip=None):
|
|||||||
notices = [f'the PKI is left as it is: the directory cannot be reached: {exc}']
|
notices = [f'the PKI is left as it is: the directory cannot be reached: {exc}']
|
||||||
for notice in notices:
|
for notice in notices:
|
||||||
warn(notice)
|
warn(notice)
|
||||||
|
# The WireGuard peers of the users; None keeps those of wg0.conf while the directory cannot be reached.
|
||||||
|
ctx['wg_peers'] = None
|
||||||
|
if wg_ready(ctx):
|
||||||
|
try:
|
||||||
|
ctx['wg_peers'] = vpnrouter_ldap.wireguard_peers()
|
||||||
|
except ldap.LDAPError as exc:
|
||||||
|
warn(f'the WireGuard peers of the users are left as they are: the directory cannot be reached: {exc}')
|
||||||
# What needs the router's own certificates.
|
# What needs the router's own certificates.
|
||||||
ctx['server_certificate'] = vpnrouter_pki.VPN_SERVER_CERT.is_file()
|
ctx['server_certificate'] = vpnrouter_pki.VPN_SERVER_CERT.is_file()
|
||||||
for connection in ctx['connections']:
|
for connection in ctx['connections']:
|
||||||
|
|||||||
@@ -66,7 +66,8 @@ OVERLAYS = ('ppolicy', 'otp')
|
|||||||
#: authorization scopes, of which service marks a service account, displayName and
|
#: authorization scopes, of which service marks a service account, displayName and
|
||||||
#: description, the labels of a service account, and vpnRouterConsoleLogin and
|
#: description, the labels of a service account, and vpnRouterConsoleLogin and
|
||||||
#: vpnRouterConsoleAccount, whether the user may use a console and the local account it runs as,
|
#: vpnRouterConsoleAccount, whether the user may use a console and the local account it runs as,
|
||||||
#: without which it runs as the user's own posixAccount. vpnRouterAuthority is a CA of the PKI: its
|
#: without which it runs as the user's own posixAccount, and vpnRouterWireGuardPeer, each a
|
||||||
|
#: WireGuard peer of the user as "<public key> <IP>[,<IP>...]". vpnRouterAuthority is a CA of the PKI: its
|
||||||
#: certificate and CRL (cACertificate and certificateRevocationList of RFC 4523), its key, the
|
#: certificate and CRL (cACertificate and certificateRevocationList of RFC 4523), its key, the
|
||||||
#: CSR it waits with for the offline root, the number of its last CRL, where the CRL came from,
|
#: CSR it waits with for the offline root, the number of its last CRL, where the CRL came from,
|
||||||
#: and when it was retired. vpnRouterCertificate is a certificate a CA issued, named by its
|
#: and when it was retired. vpnRouterCertificate is a certificate a CA issued, named by its
|
||||||
@@ -97,11 +98,13 @@ SCHEMA_ATTRIBUTE_TYPES = (
|
|||||||
"EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE )",
|
"EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE )",
|
||||||
f"( {SCHEMA_OID}.1.14 NAME 'vpnRouterConsoleAccount' DESC 'VPN router: the local account the console of the user runs as' "
|
f"( {SCHEMA_OID}.1.14 NAME 'vpnRouterConsoleAccount' DESC 'VPN router: the local account the console of the user runs as' "
|
||||||
f"{_STRING} )",
|
f"{_STRING} )",
|
||||||
|
f"( {SCHEMA_OID}.1.15 NAME 'vpnRouterWireGuardPeer' DESC 'VPN router: a WireGuard peer of the user, its public key and IPs' "
|
||||||
|
"EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{1024} )",
|
||||||
)
|
)
|
||||||
SCHEMA_OBJECT_CLASSES = (
|
SCHEMA_OBJECT_CLASSES = (
|
||||||
f"( {SCHEMA_OID}.2.1 NAME 'vpnRouterAccount' DESC 'VPN router: properties of a user or a Manager-only account' "
|
f"( {SCHEMA_OID}.2.1 NAME 'vpnRouterAccount' DESC 'VPN router: properties of a user or a Manager-only account' "
|
||||||
"SUP top AUXILIARY MAY ( mail $ vpnRouterScope $ displayName $ description $ vpnRouterConsoleLogin $ "
|
"SUP top AUXILIARY MAY ( mail $ vpnRouterScope $ displayName $ description $ vpnRouterConsoleLogin $ "
|
||||||
"vpnRouterConsoleAccount ) )",
|
"vpnRouterConsoleAccount $ vpnRouterWireGuardPeer ) )",
|
||||||
f"( {SCHEMA_OID}.2.2 NAME 'vpnRouterAuthority' DESC 'VPN router: a CA of the PKI' SUP top STRUCTURAL MUST cn "
|
f"( {SCHEMA_OID}.2.2 NAME 'vpnRouterAuthority' DESC 'VPN router: a CA of the PKI' SUP top STRUCTURAL MUST cn "
|
||||||
"MAY ( cACertificate $ certificateRevocationList $ vpnRouterPrivateKey $ vpnRouterCertificateRequest $ "
|
"MAY ( cACertificate $ certificateRevocationList $ vpnRouterPrivateKey $ vpnRouterCertificateRequest $ "
|
||||||
"vpnRouterCRLNumber $ vpnRouterCRLSource $ vpnRouterRetired ) )",
|
"vpnRouterCRLNumber $ vpnRouterCRLSource $ vpnRouterRetired ) )",
|
||||||
@@ -116,6 +119,9 @@ TOTP_ALGORITHM = '1.2.840.113549.2.7'
|
|||||||
TOTP_PERIOD = 30
|
TOTP_PERIOD = 30
|
||||||
TOTP_WINDOW = 1
|
TOTP_WINDOW = 1
|
||||||
|
|
||||||
|
#: The value of pwdAccountLockedTime that disables a user until it is enabled again.
|
||||||
|
DISABLED = '000001010000Z'
|
||||||
|
|
||||||
#: The indexes of the database.
|
#: The indexes of the database.
|
||||||
INDEXES = ('objectClass eq', 'uid eq', 'uidNumber eq', 'vpnRouterConsoleLogin eq')
|
INDEXES = ('objectClass eq', 'uid eq', 'uidNumber eq', 'vpnRouterConsoleLogin eq')
|
||||||
|
|
||||||
@@ -287,6 +293,23 @@ def _ensure_manager(connection):
|
|||||||
MANAGER_SECRET.write_text(password, encoding='utf-8')
|
MANAGER_SECRET.write_text(password, encoding='utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
def wireguard_peers():
|
||||||
|
"""The WireGuard peers of the users who are not disabled, read as root: [(user, public key, [IP, ...])], sorted."""
|
||||||
|
connection = root()
|
||||||
|
try:
|
||||||
|
found = connection.search_s(USERS, ldap.SCOPE_ONELEVEL,
|
||||||
|
f'(&(vpnRouterWireGuardPeer=*)(!(pwdAccountLockedTime={DISABLED})))',
|
||||||
|
['uid', 'vpnRouterWireGuardPeer'])
|
||||||
|
finally:
|
||||||
|
connection.unbind_s()
|
||||||
|
peers = []
|
||||||
|
for _, attributes in found:
|
||||||
|
for value in attributes['vpnRouterWireGuardPeer']:
|
||||||
|
key, _, ips = value.decode().partition(' ')
|
||||||
|
peers.append((attributes['uid'][0].decode(), key, ips.split(',')))
|
||||||
|
return sorted(peers)
|
||||||
|
|
||||||
|
|
||||||
def provision():
|
def provision():
|
||||||
"""Create or bring up to date the modules, the package's schema, database, ACLs, password policy, entries and service account."""
|
"""Create or bring up to date the modules, the package's schema, database, ACLs, password policy, entries and service account."""
|
||||||
connection = root()
|
connection = root()
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ COMMAND_TIMEOUT = 5
|
|||||||
|
|
||||||
OPENVPN_STATUS = '/run/openvpn-server/status-vpn-router.log'
|
OPENVPN_STATUS = '/run/openvpn-server/status-vpn-router.log'
|
||||||
|
|
||||||
|
#: Seconds between two writes of OPENVPN_STATUS by the OpenVPN server, whose configuration sets it (OpenVPN's default is 60).
|
||||||
|
OPENVPN_STATUS_SECONDS = 5
|
||||||
|
|
||||||
#: Seconds since its last handshake within which a WireGuard peer counts as connected.
|
#: Seconds since its last handshake within which a WireGuard peer counts as connected.
|
||||||
WIREGUARD_CONNECTED_SECONDS = 180
|
WIREGUARD_CONNECTED_SECONDS = 180
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ optional description and at least one member; a group cannot be empty, so the la
|
|||||||
cannot leave it and a user who is the last member of a group cannot be removed. Groups only
|
cannot leave it and a user who is the last member of a group cannot be removed. Groups only
|
||||||
record membership; nothing on the router reads them.
|
record membership; nothing on the router reads them.
|
||||||
|
|
||||||
|
A VPN user may have WireGuard peers, each a device with its public key and its IPs, in
|
||||||
|
vpnRouterWireGuardPeer; vpn-router-setup writes a [Peer] of wg0.conf for each peer of a user who
|
||||||
|
is not disabled.
|
||||||
|
|
||||||
A user may have a TOTP token: the user entry itself is an oathTOTPUser and oathTOTPToken whose
|
A user may have a TOTP token: the user entry itself is an oathTOTPUser and oathTOTPToken whose
|
||||||
oathTOTPToken points to itself, with the secret in oathSecret and the parameters of
|
oathTOTPToken points to itself, with the secret in oathSecret and the parameters of
|
||||||
vpnrouter_ldap.TOTP_PARAMS. Enrolment stores nothing until a code of the new secret is
|
vpnrouter_ldap.TOTP_PARAMS. Enrolment stores nothing until a code of the new secret is
|
||||||
@@ -48,6 +52,7 @@ import calendar
|
|||||||
import contextlib
|
import contextlib
|
||||||
import fcntl
|
import fcntl
|
||||||
import grp
|
import grp
|
||||||
|
import ipaddress
|
||||||
import pwd
|
import pwd
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -78,7 +83,9 @@ LABELS = (('display_name', 'displayName'), ('description', 'description'))
|
|||||||
#: The attributes read for a user.
|
#: The attributes read for a user.
|
||||||
ATTRIBUTES = ['uid', 'objectClass', 'pwdAccountLockedTime', 'givenName', 'sn', 'mail', 'oathTOTPToken', 'vpnRouterScope',
|
ATTRIBUTES = ['uid', 'objectClass', 'pwdAccountLockedTime', 'givenName', 'sn', 'mail', 'oathTOTPToken', 'vpnRouterScope',
|
||||||
'displayName', 'description', 'userCertificate;binary', 'vpnRouterConsoleLogin', 'vpnRouterConsoleAccount',
|
'displayName', 'description', 'userCertificate;binary', 'vpnRouterConsoleLogin', 'vpnRouterConsoleAccount',
|
||||||
'uidNumber']
|
'uidNumber', 'vpnRouterWireGuardPeer']
|
||||||
|
#: The most WireGuard peers of one user.
|
||||||
|
WIREGUARD_PEERS_MAX = 16
|
||||||
#: The vpnRouterScope of a service account.
|
#: The vpnRouterScope of a service account.
|
||||||
SERVICE_SCOPE = 'service'
|
SERVICE_SCOPE = 'service'
|
||||||
#: The object classes of a VPN user and of a Manager-only account.
|
#: The object classes of a VPN user and of a Manager-only account.
|
||||||
@@ -94,7 +101,7 @@ GROUP_ATTRIBUTES = ['cn', 'description', 'member']
|
|||||||
#: The longest group description, in characters.
|
#: The longest group description, in characters.
|
||||||
DESCRIPTION_MAX = 256
|
DESCRIPTION_MAX = 256
|
||||||
#: The value of pwdAccountLockedTime that disables an account until it is enabled.
|
#: The value of pwdAccountLockedTime that disables an account until it is enabled.
|
||||||
LOCKED = b'000001010000Z'
|
LOCKED = vpnrouter_ldap.DISABLED.encode()
|
||||||
#: The group whose members manage the router through the web service.
|
#: The group whose members manage the router through the web service.
|
||||||
ADMIN_GROUP = 'admins'
|
ADMIN_GROUP = 'admins'
|
||||||
#: The digits of a TOTP code given to login().
|
#: The digits of a TOTP code given to login().
|
||||||
@@ -244,6 +251,7 @@ def summary(attributes):
|
|||||||
'certificate': 'userCertificate;binary' in attributes,
|
'certificate': 'userCertificate;binary' in attributes,
|
||||||
'otp': 'oathTOTPToken' in attributes,
|
'otp': 'oathTOTPToken' in attributes,
|
||||||
'console': console_of(attributes),
|
'console': console_of(attributes),
|
||||||
|
'wireguard_peers': peers_of(attributes),
|
||||||
}
|
}
|
||||||
for key, attribute in PERSON + LABELS:
|
for key, attribute in PERSON + LABELS:
|
||||||
values = attributes.get(attribute)
|
values = attributes.get(attribute)
|
||||||
@@ -267,6 +275,62 @@ def console_of(attributes):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def peers_of(attributes):
|
||||||
|
"""The WireGuard peers of an entry as the answers show them: [{public_key, ips}], in the order they are stored."""
|
||||||
|
peers = []
|
||||||
|
for value in attributes.get('vpnRouterWireGuardPeer', []):
|
||||||
|
key, _, ips = value.decode().partition(' ')
|
||||||
|
peers.append({'public_key': key, 'ips': ips.split(',')})
|
||||||
|
return peers
|
||||||
|
|
||||||
|
|
||||||
|
def checked_peers(connection, name, peers):
|
||||||
|
"""The values of vpnRouterWireGuardPeer for the user's WireGuard peers, [{public_key, ips}], when they are valid.
|
||||||
|
|
||||||
|
A public key is 32 bytes in base64, used by no other peer of any user; each peer has at least one IP, an IPv4
|
||||||
|
address or network, which no other peer of any user covers and which does not cover the router's own address of
|
||||||
|
wireguard.address. A user has at most WIREGUARD_PEERS_MAX peers.
|
||||||
|
"""
|
||||||
|
if not isinstance(peers, list) or len(peers) > WIREGUARD_PEERS_MAX:
|
||||||
|
raise Failure(f'wireguard_peers must be a list of at most {WIREGUARD_PEERS_MAX} peers', 2)
|
||||||
|
router = vpnrouter.load_config()['wireguard']['address'].strip()
|
||||||
|
taken = [(other['name'], key, network)
|
||||||
|
for other in list_users(connection)['users'] if other['name'] != name
|
||||||
|
for peer in other['wireguard_peers'] for key in [peer['public_key']]
|
||||||
|
for network in (ipaddress.ip_network(ip) for ip in peer['ips'])]
|
||||||
|
values = []
|
||||||
|
for peer in peers:
|
||||||
|
key = peer.get('public_key') if isinstance(peer, dict) else None
|
||||||
|
ips = peer.get('ips') if isinstance(peer, dict) else None
|
||||||
|
try:
|
||||||
|
if not isinstance(key, str) or len(base64.b64decode(key.strip(), validate=True)) != 32:
|
||||||
|
raise ValueError
|
||||||
|
except ValueError:
|
||||||
|
raise Failure(f'{key!r} is not a WireGuard public key: 32 bytes in base64', 2)
|
||||||
|
key = key.strip()
|
||||||
|
if not isinstance(ips, list) or not ips or not all(isinstance(ip, str) for ip in ips):
|
||||||
|
raise Failure(f'the peer {key} needs at least one IP', 2)
|
||||||
|
networks = []
|
||||||
|
for ip in ips:
|
||||||
|
try:
|
||||||
|
network = ipaddress.ip_network(ip.strip(), strict=True)
|
||||||
|
except ValueError:
|
||||||
|
raise Failure(f'{ip!r} is not an IPv4 address or network of the peer {key}', 2)
|
||||||
|
if network.version != 4:
|
||||||
|
raise Failure(f'{ip!r} of the peer {key} is not IPv4', 2)
|
||||||
|
if router and ipaddress.ip_interface(router).ip in network:
|
||||||
|
raise Failure(f'{network} of the peer {key} covers the router\'s own address {router}', 2)
|
||||||
|
for owner, other_key, other in taken:
|
||||||
|
if other_key == key:
|
||||||
|
raise Failure(f'the key {key} is a peer of {owner}', 4)
|
||||||
|
if network.overlaps(other):
|
||||||
|
raise Failure(f'{network} of the peer {key} overlaps {other} of {owner}', 4)
|
||||||
|
networks.append(network)
|
||||||
|
taken += [(name, key, network) for network in networks]
|
||||||
|
values.append(f'{key} {",".join(str(network) for network in networks)}'.encode())
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
def classes(attributes):
|
def classes(attributes):
|
||||||
"""The object classes of an entry, in lower case."""
|
"""The object classes of an entry, in lower case."""
|
||||||
return {value.decode().lower() for value in attributes.get('objectClass', [])}
|
return {value.decode().lower() for value in attributes.get('objectClass', [])}
|
||||||
@@ -412,7 +476,8 @@ def add(connection, name, password, values, service=False):
|
|||||||
|
|
||||||
|
|
||||||
def update(connection, name, values):
|
def update(connection, name, values):
|
||||||
"""Change the person attributes values gives; the cn follows the first and the last name.
|
"""Change the person attributes values gives, the cn following the first and the last name, and of a VPN user the
|
||||||
|
WireGuard peers, wireguard_peers, which replace the user's peers (checked_peers).
|
||||||
|
|
||||||
A Manager-only account has only the e-mail address, which '' removes; a service account also its labels
|
A Manager-only account has only the e-mail address, which '' removes; a service account also its labels
|
||||||
(update_service).
|
(update_service).
|
||||||
@@ -421,6 +486,8 @@ def update(connection, name, values):
|
|||||||
if service(current):
|
if service(current):
|
||||||
return update_service(connection, name, values)
|
return update_service(connection, name, values)
|
||||||
if manager_only(current):
|
if manager_only(current):
|
||||||
|
if values.get('wireguard_peers') is not None:
|
||||||
|
raise Failure(f'user {name} is a Manager-only account, which has no VPN access and so no WireGuard peers', 2)
|
||||||
if values.get('first_name') is not None or values.get('last_name') is not None:
|
if values.get('first_name') is not None or values.get('last_name') is not None:
|
||||||
raise Failure(f'user {name} is a Manager-only account, which has no first or last name', 2)
|
raise Failure(f'user {name} is a Manager-only account, which has no first or last name', 2)
|
||||||
email = values.get('email')
|
email = values.get('email')
|
||||||
@@ -433,16 +500,30 @@ def update(connection, name, values):
|
|||||||
connection.modify_s(dn_of(name), [(ldap.MOD_REPLACE, 'mail', [person({'email': email}, required=False)['mail'].encode()])])
|
connection.modify_s(dn_of(name), [(ldap.MOD_REPLACE, 'mail', [person({'email': email}, required=False)['mail'].encode()])])
|
||||||
return summary(entry(connection, name))
|
return summary(entry(connection, name))
|
||||||
details = person(values, required=False)
|
details = person(values, required=False)
|
||||||
if not details:
|
peers = values.get('wireguard_peers')
|
||||||
raise Failure('give at least one of first_name, last_name and email', 2)
|
if not details and peers is None:
|
||||||
names = {attribute: current[attribute][0].decode() for attribute in ('givenName', 'sn') if current.get(attribute)}
|
raise Failure('give at least one of first_name, last_name, email and wireguard_peers', 2)
|
||||||
names.update({attribute: details[attribute] for attribute in ('givenName', 'sn') if attribute in details})
|
changes = []
|
||||||
changes = [(ldap.MOD_REPLACE, attribute, [value.encode()]) for attribute, value in details.items()]
|
if details:
|
||||||
changes.append((ldap.MOD_REPLACE, 'cn', [common_name(names, name).encode()]))
|
names = {attribute: current[attribute][0].decode() for attribute in ('givenName', 'sn') if current.get(attribute)}
|
||||||
|
names.update({attribute: details[attribute] for attribute in ('givenName', 'sn') if attribute in details})
|
||||||
|
changes += [(ldap.MOD_REPLACE, attribute, [value.encode()]) for attribute, value in details.items()]
|
||||||
|
changes.append((ldap.MOD_REPLACE, 'cn', [common_name(names, name).encode()]))
|
||||||
|
if peers is not None:
|
||||||
|
if 'vpnrouteraccount' not in classes(current):
|
||||||
|
changes.append((ldap.MOD_ADD, 'objectClass', [b'vpnRouterAccount']))
|
||||||
|
changes.append((ldap.MOD_REPLACE, 'vpnRouterWireGuardPeer', checked_peers(connection, name, peers)))
|
||||||
connection.modify_s(dn_of(name), changes)
|
connection.modify_s(dn_of(name), changes)
|
||||||
|
if peers is not None:
|
||||||
|
apply_wireguard()
|
||||||
return summary(entry(connection, name))
|
return summary(entry(connection, name))
|
||||||
|
|
||||||
|
|
||||||
|
def apply_wireguard():
|
||||||
|
"""Have vpn-router-setup write the WireGuard peers of the users into wg0.conf, without waiting for it."""
|
||||||
|
subprocess.run(['systemctl', 'restart', '--no-block', 'vpn-router-setup.service'], capture_output=True)
|
||||||
|
|
||||||
|
|
||||||
def update_service(connection, name, values):
|
def update_service(connection, name, values):
|
||||||
"""Change any of the e-mail address and the labels (labels) of a service account that values gives; '' removes one."""
|
"""Change any of the e-mail address and the labels (labels) of a service account that values gives; '' removes one."""
|
||||||
if not service(entry(connection, name)):
|
if not service(entry(connection, name)):
|
||||||
@@ -466,14 +547,22 @@ def passwd(connection, name, password):
|
|||||||
|
|
||||||
|
|
||||||
def disable(connection, name):
|
def disable(connection, name):
|
||||||
if entry(connection, name).get('pwdAccountLockedTime') != [LOCKED]:
|
"""Lock the user out; the WireGuard peers of a disabled user are left out of wg0.conf."""
|
||||||
|
current = entry(connection, name)
|
||||||
|
if current.get('pwdAccountLockedTime') != [LOCKED]:
|
||||||
connection.modify_s(dn_of(name), [(ldap.MOD_REPLACE, 'pwdAccountLockedTime', [LOCKED])])
|
connection.modify_s(dn_of(name), [(ldap.MOD_REPLACE, 'pwdAccountLockedTime', [LOCKED])])
|
||||||
|
if 'vpnRouterWireGuardPeer' in current:
|
||||||
|
apply_wireguard()
|
||||||
return summary(entry(connection, name))
|
return summary(entry(connection, name))
|
||||||
|
|
||||||
|
|
||||||
def enable(connection, name):
|
def enable(connection, name):
|
||||||
if entry(connection, name).get('pwdAccountLockedTime') == [LOCKED]:
|
"""Let the user in again, with the WireGuard peers."""
|
||||||
|
current = entry(connection, name)
|
||||||
|
if current.get('pwdAccountLockedTime') == [LOCKED]:
|
||||||
connection.modify_s(dn_of(name), [(ldap.MOD_DELETE, 'pwdAccountLockedTime', None)])
|
connection.modify_s(dn_of(name), [(ldap.MOD_DELETE, 'pwdAccountLockedTime', None)])
|
||||||
|
if 'vpnRouterWireGuardPeer' in current:
|
||||||
|
apply_wireguard()
|
||||||
return summary(entry(connection, name))
|
return summary(entry(connection, name))
|
||||||
|
|
||||||
|
|
||||||
@@ -579,6 +668,8 @@ def remove(connection, name):
|
|||||||
raise Failure(f'the certificate cannot be revoked: {failure}', failure.status)
|
raise Failure(f'the certificate cannot be revoked: {failure}', failure.status)
|
||||||
connection.delete_s(dn_of(name))
|
connection.delete_s(dn_of(name))
|
||||||
end_sessions(current)
|
end_sessions(current)
|
||||||
|
if 'vpnRouterWireGuardPeer' in current:
|
||||||
|
apply_wireguard()
|
||||||
return {'name': name, 'removed': True}
|
return {'name': name, 'removed': True}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ Pages:
|
|||||||
applied by vpn-router-setup (see site_to_site())
|
applied by vpn-router-setup (see site_to_site())
|
||||||
/backup the backup settings, the last run and the local archives: show, change and run
|
/backup the backup settings, the last run and the local archives: show, change and run
|
||||||
now (see backup())
|
now (see backup())
|
||||||
|
/remote-access
|
||||||
|
the settings of IKEv2 road-warrior, OpenVPN and WireGuard access and whether each
|
||||||
|
runs: show and change, applied by vpn-router-setup (see remote_access())
|
||||||
/ca the PKI: its settings, the root and issuing CAs, the certificates issued; change the
|
/ca the PKI: its settings, the root and issuing CAs, the certificates issued; change the
|
||||||
settings, issue and revoke certificates, and take the certificate of an issuing CA
|
settings, issue and revoke certificates, and take the certificate of an issuing CA
|
||||||
and a root CRL signed offline (see ca())
|
and a root CRL signed offline (see ca())
|
||||||
@@ -46,7 +49,7 @@ Pages:
|
|||||||
the databases of each PostgreSQL cluster on the router with their sizes, as JSON (see
|
the databases of each PostgreSQL cluster on the router with their sizes, as JSON (see
|
||||||
postgresql_databases()); /config has the clusters' settings
|
postgresql_databases()); /config has the clusters' settings
|
||||||
|
|
||||||
/status/full, /config, /metrics/history, /postgresql, /users, /service-accounts, /groups, /site-to-site, /backup, /ca, /shell and /account need an HS256 JWT in an Authorization: Bearer header signed
|
/status/full, /config, /metrics/history, /postgresql, /users, /service-accounts, /groups, /site-to-site, /backup, /remote-access, /ca, /shell and /account need an HS256 JWT in an Authorization: Bearer header signed
|
||||||
with vpnrouter_auth.WEB_KEY (see vpnrouter_auth.verify). A token of /auth names a user in sub;
|
with vpnrouter_auth.WEB_KEY (see vpnrouter_auth.verify). A token of /auth names a user in sub;
|
||||||
/account serves that user, and the other paths need a member of vpnrouter_users.ADMIN_GROUP. A
|
/account serves that user, and the other paths need a member of vpnrouter_users.ADMIN_GROUP. A
|
||||||
token of vpn-router-token has the admin claim and names no user; it may call every path but
|
token of vpn-router-token has the admin claim and names no user; it may call every path but
|
||||||
@@ -86,6 +89,7 @@ import collections
|
|||||||
import configparser
|
import configparser
|
||||||
import fcntl
|
import fcntl
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import ipaddress
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import pwd
|
import pwd
|
||||||
@@ -150,6 +154,9 @@ BODY_LIMIT = 65536
|
|||||||
#: The settings of a site_to_site connection that /site-to-site adds and changes.
|
#: The settings of a site_to_site connection that /site-to-site adds and changes.
|
||||||
CONNECTION_SETTINGS = ('addrs', 'id', 'cidrs', 'local_cidrs', 'local_id_mode', 'auth', 'psk', 'remote_ca')
|
CONNECTION_SETTINGS = ('addrs', 'id', 'cidrs', 'local_cidrs', 'local_id_mode', 'auth', 'psk', 'remote_ca')
|
||||||
|
|
||||||
|
#: The sections of the remote access systems /remote-access changes: IKEv2 road-warrior, OpenVPN and WireGuard.
|
||||||
|
REMOTE_ACCESS = ('p2s', 'openvpn', 'wireguard')
|
||||||
|
|
||||||
#: The HTTP status for each status of a vpnrouter_users.Failure; any other is 500.
|
#: The HTTP status for each status of a vpnrouter_users.Failure; any other is 500.
|
||||||
USER_STATUS = {2: 400, 3: 404, 4: 409, 5: 401}
|
USER_STATUS = {2: 400, 3: 404, 4: 409, 5: 401}
|
||||||
|
|
||||||
@@ -396,7 +403,7 @@ def postgresql_databases():
|
|||||||
|
|
||||||
def details(state):
|
def details(state):
|
||||||
"""What /config shows: the configuration without its secrets, and what the router resolved and runs; caddy is the
|
"""What /config shows: the configuration without its secrets, and what the router resolved and runs; caddy is the
|
||||||
installed Caddy, {version, source}, null when it is not installed."""
|
installed Caddy, {version, source, service}, service the state of caddy.service, null when it is not installed."""
|
||||||
configuration = state.get('configuration', {})
|
configuration = state.get('configuration', {})
|
||||||
caddy = package_version('caddy')
|
caddy = package_version('caddy')
|
||||||
return {
|
return {
|
||||||
@@ -413,8 +420,8 @@ def details(state):
|
|||||||
},
|
},
|
||||||
'postgresql': postgresql_configuration(),
|
'postgresql': postgresql_configuration(),
|
||||||
# Caddy is upstream's once its repository is configured (vpn-router-caddy).
|
# Caddy is upstream's once its repository is configured (vpn-router-caddy).
|
||||||
'caddy': {'version': caddy, 'source': 'upstream' if vpnrouter.CADDY_APT_SOURCE.is_file() else 'ubuntu'}
|
'caddy': {'version': caddy, 'source': 'upstream' if vpnrouter.CADDY_APT_SOURCE.is_file() else 'ubuntu',
|
||||||
if caddy else None,
|
'service': unit_state('caddy.service')} if caddy else None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -501,10 +508,12 @@ def html_of(status):
|
|||||||
def users(state, method, path, body):
|
def users(state, method, path, body):
|
||||||
"""Answer a /users request with vpnrouter_users: the HTTP status and the JSON document.
|
"""Answer a /users request with vpnrouter_users: the HTTP status and the JSON document.
|
||||||
|
|
||||||
GET /users the users
|
GET /users the users, and {"enabled", "address"} of wireguard, whose network their
|
||||||
|
WireGuard peers take their addresses from
|
||||||
POST /users add a user: {"name", "password", "first_name", "last_name", "email"};
|
POST /users add a user: {"name", "password", "first_name", "last_name", "email"};
|
||||||
without the names a Manager-only account
|
without the names a Manager-only account
|
||||||
PUT /users/<name> change any of {"first_name", "last_name", "email"}
|
PUT /users/<name> change any of {"first_name", "last_name", "email", "wireguard_peers"};
|
||||||
|
wireguard_peers, [{"public_key", "ips"}], replaces the user's peers
|
||||||
PUT /users/<name>/password set the password: {"password": ...}
|
PUT /users/<name>/password set the password: {"password": ...}
|
||||||
POST /users/<name>/disable lock the user out of OpenVPN
|
POST /users/<name>/disable lock the user out of OpenVPN
|
||||||
POST /users/<name>/enable let the user in again
|
POST /users/<name>/enable let the user in again
|
||||||
@@ -527,7 +536,8 @@ def users(state, method, path, body):
|
|||||||
name = parts[1] if len(parts) > 1 else None
|
name = parts[1] if len(parts) > 1 else None
|
||||||
actions = {
|
actions = {
|
||||||
('GET', 1, None): lambda connection: {
|
('GET', 1, None): lambda connection: {
|
||||||
'users': [user for user in vpnrouter_users.list_users(connection)['users'] if not user['service']]},
|
'users': [user for user in vpnrouter_users.list_users(connection)['users'] if not user['service']],
|
||||||
|
'wireguard': {option: vpnrouter.load_config()['wireguard'][option] for option in ('enabled', 'address')}},
|
||||||
('POST', 1, None): lambda connection: vpnrouter_users.add(connection, body.get('name'), body.get('password'), body),
|
('POST', 1, None): lambda connection: vpnrouter_users.add(connection, body.get('name'), body.get('password'), body),
|
||||||
('PUT', 2, None): lambda connection: vpnrouter_users.update(connection, name, body),
|
('PUT', 2, None): lambda connection: vpnrouter_users.update(connection, name, body),
|
||||||
('PUT', 3, 'password'): lambda connection: vpnrouter_users.passwd(connection, name, body.get('password')),
|
('PUT', 3, 'password'): lambda connection: vpnrouter_users.passwd(connection, name, body.get('password')),
|
||||||
@@ -814,6 +824,68 @@ def backup(method, path, body):
|
|||||||
return 200, {'settings': backup_settings(cfg['backup'])}
|
return 200, {'settings': backup_settings(cfg['backup'])}
|
||||||
|
|
||||||
|
|
||||||
|
def wireguard_server(cfg, state):
|
||||||
|
"""What a WireGuard peer's configuration needs of the router: {public_key, endpoint, allowed_ips}; public_key is
|
||||||
|
null before setup made the key pair, endpoint, <wan.local_fqdn>:<wireguard.listen_port>, null without a name, and
|
||||||
|
allowed_ips the networks reached through the tunnel, the router's local CIDRs and the WireGuard network."""
|
||||||
|
try:
|
||||||
|
public_key = vpnrouter.WG_PUBLIC_KEY.read_text(encoding='utf-8').strip() or None
|
||||||
|
except OSError:
|
||||||
|
public_key = None
|
||||||
|
fqdn = cfg['wan']['local_fqdn'].strip()
|
||||||
|
address = cfg['wireguard']['address'].strip()
|
||||||
|
networks = section(state, 'network').get('local_cidrs')
|
||||||
|
allowed = [cidr for cidr in networks if isinstance(cidr, str)] if isinstance(networks, list) else []
|
||||||
|
if address:
|
||||||
|
allowed.append(str(ipaddress.ip_interface(address).network))
|
||||||
|
return {'public_key': public_key, 'endpoint': f'{fqdn}:{cfg["wireguard"]["listen_port"]}' if fqdn else None,
|
||||||
|
'allowed_ips': list(dict.fromkeys(allowed))}
|
||||||
|
|
||||||
|
|
||||||
|
def remote_access(state, method, path, body):
|
||||||
|
"""Answer a /remote-access request: the HTTP status and the JSON document.
|
||||||
|
|
||||||
|
GET /remote-access {"p2s", "openvpn", "wireguard", "running", "local_fqdn", "wireguard_server"}: the
|
||||||
|
settings of the three remote access systems (REMOTE_ACCESS), whether setup runs
|
||||||
|
each (running: {p2s, openvpn, wireguard}), wan.local_fqdn, which IKEv2 and OpenVPN
|
||||||
|
need with the router's VPN server certificate, and what a WireGuard peer's
|
||||||
|
configuration needs of the router (wireguard_server)
|
||||||
|
PUT /remote-access/<system> change any of the settings of the system's section; answers the section
|
||||||
|
|
||||||
|
A change is made as /site-to-site makes one, and applied by restarting vpn-router-setup without waiting for it.
|
||||||
|
"""
|
||||||
|
if body == 'invalid' or (body is not None and not isinstance(body, dict)):
|
||||||
|
return 400, {'error': 'the body must be a JSON object'}
|
||||||
|
body = body or {}
|
||||||
|
parts = path.strip('/').split('/')
|
||||||
|
try:
|
||||||
|
if (method, len(parts)) == ('GET', 1):
|
||||||
|
cfg = vpnrouter.load_config()
|
||||||
|
return 200, {**{system: cfg[system] for system in REMOTE_ACCESS},
|
||||||
|
'running': {system: bool(state.get(system)) for system in REMOTE_ACCESS},
|
||||||
|
'local_fqdn': cfg['wan']['local_fqdn'],
|
||||||
|
'wireguard_server': wireguard_server(cfg, state)}
|
||||||
|
if method != 'PUT' or len(parts) != 2 or parts[1] not in REMOTE_ACCESS:
|
||||||
|
return 404, {'error': f'no route {method} {path}'}
|
||||||
|
system = parts[1]
|
||||||
|
settings = {option: body[option] for option in vpnrouter.SCHEMA[system] if option in body}
|
||||||
|
with vpnrouter.config_lock():
|
||||||
|
data = vpnrouter.read_settings()
|
||||||
|
current = data.get(system)
|
||||||
|
data[system] = {**(current if isinstance(current, dict) else {}), **settings}
|
||||||
|
cfg = vpnrouter.with_defaults(data)
|
||||||
|
vpnrouter.validate(cfg)
|
||||||
|
vpnrouter.save_config(data)
|
||||||
|
except vpnrouter.ConfigError as exc:
|
||||||
|
return 400, {'error': str(exc)}
|
||||||
|
except OSError as exc:
|
||||||
|
return 500, {'error': f'the configuration cannot be written: {exc}'}
|
||||||
|
except SystemExit as exc:
|
||||||
|
return 500, {'error': str(exc.code)}
|
||||||
|
run('systemctl', 'restart', '--no-block', 'vpn-router-setup.service')
|
||||||
|
return 200, cfg[system]
|
||||||
|
|
||||||
|
|
||||||
def ca(method, path, body):
|
def ca(method, path, body):
|
||||||
"""Answer a /ca request with vpnrouter_pki: the HTTP status and the JSON document.
|
"""Answer a /ca request with vpnrouter_pki: the HTTP status and the JSON document.
|
||||||
|
|
||||||
@@ -1193,9 +1265,11 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
The ticket must come from POST /shell for the user named by the token. Binary messages of the client are its
|
The ticket must come from POST /shell for the user named by the token. Binary messages of the client are its
|
||||||
keystrokes, a text message {"cols", "rows"} sets the window size, and binary messages of the server are the
|
keystrokes, a text message {"cols", "rows"} sets the window size, and binary messages of the server are the
|
||||||
terminal's output. The shell ends after web.shell_idle_minutes without a keystroke, when it exits, or when the
|
terminal's output. The shell ends after web.shell_idle_minutes without a keystroke, when it exits, or when the
|
||||||
client closes the connection; the server then closes it with the reason. Each opening and end is logged.
|
client closes the connection; the server then closes it with the reason. Each opening, end and refusal is logged
|
||||||
|
with its reason.
|
||||||
"""
|
"""
|
||||||
def refuse(code, message):
|
def refuse(code, message):
|
||||||
|
log(f'shell of {name} from {self.client()} refused: {message}')
|
||||||
body = json.dumps({'error': message}).encode() + b'\n'
|
body = json.dumps({'error': message}).encode() + b'\n'
|
||||||
self.send_response(code)
|
self.send_response(code)
|
||||||
self.send_header('Content-Type', 'application/json')
|
self.send_header('Content-Type', 'application/json')
|
||||||
@@ -1317,7 +1391,7 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
if self.command not in ('GET', 'HEAD'):
|
if self.command not in ('GET', 'HEAD'):
|
||||||
self.close_connection = True
|
self.close_connection = True
|
||||||
directory = next((name for name in ('users', 'service-accounts', 'groups', 'account', 'site-to-site', 'backup',
|
directory = next((name for name in ('users', 'service-accounts', 'groups', 'account', 'site-to-site', 'backup',
|
||||||
'ca', 'shell')
|
'ca', 'shell', 'remote-access')
|
||||||
if path == f'/{name}' or path.startswith(f'/{name}/')), None)
|
if path == f'/{name}' or path.startswith(f'/{name}/')), None)
|
||||||
protected = path in ('/status/full', '/config', '/metrics/history', '/postgresql') or directory is not None
|
protected = path in ('/status/full', '/config', '/metrics/history', '/postgresql') or directory is not None
|
||||||
signing = path in ('/auth', '/auth/refresh', '/auth/logout')
|
signing = path in ('/auth', '/auth/refresh', '/auth/logout')
|
||||||
@@ -1360,6 +1434,8 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
elif directory == 'shell':
|
elif directory == 'shell':
|
||||||
code, document = shell_ticket(claims.get('sub'), payload) if (path, self.command) == ('/shell', 'POST') \
|
code, document = shell_ticket(claims.get('sub'), payload) if (path, self.command) == ('/shell', 'POST') \
|
||||||
else (404, {'error': f'no route {self.command} {path}'})
|
else (404, {'error': f'no route {self.command} {path}'})
|
||||||
|
if code != 200:
|
||||||
|
log(f'shell ticket of {claims.get("sub")} from {self.client()} refused: {document["error"]}')
|
||||||
body, kind = json.dumps(document, indent=2).encode() + b'\n', 'application/json'
|
body, kind = json.dumps(document, indent=2).encode() + b'\n', 'application/json'
|
||||||
elif directory == 'account':
|
elif directory == 'account':
|
||||||
code, document = account(claims['sub'], self.command, path, payload)
|
code, document = account(claims['sub'], self.command, path, payload)
|
||||||
@@ -1367,6 +1443,9 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
elif directory == 'site-to-site':
|
elif directory == 'site-to-site':
|
||||||
code, document = site_to_site(self.command, path, payload)
|
code, document = site_to_site(self.command, path, payload)
|
||||||
body, kind = json.dumps(document, indent=2).encode() + b'\n', 'application/json'
|
body, kind = json.dumps(document, indent=2).encode() + b'\n', 'application/json'
|
||||||
|
elif directory == 'remote-access':
|
||||||
|
code, document = remote_access(state, self.command, path, payload)
|
||||||
|
body, kind = json.dumps(document, indent=2).encode() + b'\n', 'application/json'
|
||||||
elif directory == 'backup':
|
elif directory == 'backup':
|
||||||
code, document = backup(self.command, path, payload)
|
code, document = backup(self.command, path, payload)
|
||||||
body, kind = json.dumps(document, indent=2).encode() + b'\n', 'application/json'
|
body, kind = json.dumps(document, indent=2).encode() + b'\n', 'application/json'
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ push "route {{ network }} {{ netmask }}"
|
|||||||
push "dhcp-option DNS {{ address }}"
|
push "dhcp-option DNS {{ address }}"
|
||||||
{% endfor -%}
|
{% endfor -%}
|
||||||
|
|
||||||
|
status {{ openvpn_status }} {{ openvpn_status_seconds }}
|
||||||
|
|
||||||
keepalive 10 120
|
keepalive 10 120
|
||||||
{% if openvpn_protocol == 'udp' -%}
|
{% if openvpn_protocol == 'udp' -%}
|
||||||
explicit-exit-notify 1
|
explicit-exit-notify 1
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{{ wg_peers_begin }}
|
||||||
|
{% for user, key, ips in wg_peers -%}
|
||||||
|
[Peer]
|
||||||
|
# {{ user }}
|
||||||
|
PublicKey = {{ key }}
|
||||||
|
AllowedIPs = {{ ips | join(', ') }}
|
||||||
|
|
||||||
|
{% endfor -%}
|
||||||
|
{{ wg_peers_end }}
|
||||||
Reference in new issue
Block a user