Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b14b7723e | ||
|
|
5303779559 | ||
|
|
2dc4db440f | ||
|
|
45cea95728 | ||
|
|
dd2dfaaf2b |
No files matched your search
@@ -46,7 +46,7 @@ and as a Tailwind CSS page for a browser, and `/status` is the same status as JS
|
||||
much as `web.status_content` says, since the router is reachable from the internet and the
|
||||
operator decides how much it tells strangers: `basic`, the default because it tells the least,
|
||||
the overall status and the Client IP, `standard` also the package versions and the number of
|
||||
active and other services, and `full` also each service and the state of the site-to-site, road-warrior, OpenVPN and
|
||||
active and other services, and `full` also each service and the state of the site-to-site, IKEv2, OpenVPN and
|
||||
WireGuard features. The Client IP is the address the request came from, through the trusted
|
||||
proxies, so a person can see the address the router sees. Nothing comes from the configuration.
|
||||
`/status` names its content, so clients such as manage-ui and the Router Manager show what it
|
||||
@@ -394,8 +394,8 @@ failure, then listens. `vpnrouter_auth` turns every failure of the database into
|
||||
psycopg (Ubuntu ships only its pure-Python implementation) finds libpq through the dynamic linker
|
||||
cache, which dpkg refreshes with its deferred `ldconfig` only at the end of an installation, so a
|
||||
service started during the installation cannot import it yet; a failed import cannot be repeated
|
||||
in the same process either, as psycopg caches the lookup. The services therefore import psycopg
|
||||
plainly, and their units wait instead: `ExecStartPre=wait-for-libpq`, a shell script of each
|
||||
in the same process either, as psycopg caches the lookup. The services and `vpn-router-setup`
|
||||
therefore import psycopg plainly, and their units wait instead: `ExecStartPre=wait-for-libpq`, a shell script of each
|
||||
package, waits until `ldconfig -p` lists `libpq.so.5`, within `TimeoutStartSec=5min`. The
|
||||
maintainer scripts start the units with `systemctl --no-block` (debhelper's start is turned off),
|
||||
because a start that waited would hold dpkg before its `ldconfig`.
|
||||
@@ -458,7 +458,7 @@ refresh tokens after the operations of `vpnrouter_auth.REVOKING`.
|
||||
|
||||
Consequences: A Manager-only account cannot become a VPN user, as LDAP cannot change an entry's
|
||||
structural class and its password hash cannot be copied; it is removed and added again instead.
|
||||
It has no certificate, so OpenVPN and road-warrior access refuse it. The administrator's password
|
||||
It has no certificate, so OpenVPN and IKEv2 access refuse it. The administrator's password
|
||||
is on disk only until setup has run.
|
||||
|
||||
## ADR-19: The package has an LDAP schema of its own under a UUID OID
|
||||
@@ -655,8 +655,8 @@ one, so its name is the one the tunnel always had.
|
||||
- **Local settings per connection:** each connection has its own `local_cidrs` and
|
||||
`local_id_mode`, as each peer expects its own traffic selectors and identity. `wan.local_fqdn`
|
||||
stays the router's one name, used by the `fqdn` mode, resolved once for the `public_ip` mode,
|
||||
and used by HTTPS, road-warrior and OpenVPN. `local.cidrs` stays the subnets the router
|
||||
routes, masquerades and offers to road-warrior and OpenVPN clients.
|
||||
and used by HTTPS, IKEv2 and OpenVPN. `local.cidrs` stays the subnets the router
|
||||
routes, masquerades and offers to IKEv2 and OpenVPN clients.
|
||||
- **One file:** `vpn-router-setup` writes every connection that is ready to
|
||||
`/etc/swanctl/conf.d/site-to-site.conf`, with the proposals and timers the one tunnel had, and
|
||||
removes the file when none is. The NAT exemptions pair each connection's `local_cidrs` with its
|
||||
@@ -766,9 +766,9 @@ signs the requester's CSR, or makes the key and returns it once in a PKCS#12 fil
|
||||
the certificate, so a key it gave away cannot leak from it later. The PKCS#12 file uses 3DES and
|
||||
SHA-1 MACs, the encryption the key stores of Windows and macOS import. strongSwan reads only the
|
||||
first certificate of a PEM file, so each CA has a file of its own in `/etc/swanctl/x509ca`;
|
||||
road-warrior and site-to-site name their CA in `cacerts`, which restricts the trust chain to it.
|
||||
IKEv2 access and site-to-site name their CA in `cacerts`, which restricts the trust chain to it.
|
||||
|
||||
Consequences: The P2S CA's certificates are accepted by road-warrior and OpenVPN only, the site-to-site CA's by
|
||||
Consequences: The P2S CA's certificates are accepted by IKEv2 and OpenVPN only, the site-to-site CA's by
|
||||
site-to-site only. With an offline root, new issuing CAs and the root CRL need the offline step,
|
||||
and a router whose P2S CA waits has no VPN server certificate and so no P2S access. The router
|
||||
keeps the users' keys, as the OpenVPN profile embeds them.
|
||||
@@ -1198,7 +1198,7 @@ resolver share no address.
|
||||
|
||||
Status: Accepted
|
||||
|
||||
Context: macOS, iOS and iPadOS connect to the road-warrior access with their built-in IKEv2 client,
|
||||
Context: macOS, iOS and iPadOS connect to the IKEv2 access with their built-in IKEv2 client,
|
||||
configured by a configuration profile. The router keeps each user's certificate and key. Apple's
|
||||
clients read a PKCS#12 only when it is encrypted with 3DES and SHA-1, send one proposal per SA,
|
||||
and show an unsigned profile as unverified.
|
||||
@@ -1213,20 +1213,43 @@ Consequences: The password is never stored; the device asks for it when the prof
|
||||
A profile shows as unverified. A changed proposal of `road-warrior.conf` must be changed in the
|
||||
profile too. WireGuard is not part of it.
|
||||
|
||||
## ADR-45: Road-warrior clients get split DNS domains, p2s.dns_domains
|
||||
## ADR-45: IKEv2 clients get split DNS domains, p2s.dns_domains
|
||||
|
||||
Status: Accepted
|
||||
|
||||
Context: The road-warrior access is a split tunnel. macOS and iOS use the DNS server an IKEv2 VPN
|
||||
Context: The IKEv2 access is a split tunnel. macOS and iOS use the DNS server an IKEv2 VPN
|
||||
pushes only for queries scoped to the tunnel, so names only the router resolves, such as Azure's
|
||||
internal names, fail. RFC 8598 defines INTERNAL_DNS_DOMAIN (attribute 25): the client resolves the
|
||||
listed domains and their subdomains with the VPN's servers. strongSwan's pools take numeric
|
||||
attributes with string values; Apple profiles name such domains in `SupplementalMatchDomains`.
|
||||
|
||||
Decision: The setting `p2s.dns_domains` lists the domains; setup pushes each as attribute 25 of the
|
||||
road-warrior pool, and the Apple VPN profile gives them as match domains of the router's resolver,
|
||||
IKEv2 address pool, and the Apple VPN profile gives them as match domains of the router's resolver,
|
||||
the addresses setup records for the web service.
|
||||
|
||||
Consequences: Clients resolve the listed domains through the tunnel and everything else with their
|
||||
own resolvers. Clients that ignore attribute 25 keep the scoped behaviour; the Apple profile
|
||||
covers Apple devices. A changed list reaches an Apple device with a newly installed profile.
|
||||
|
||||
## ADR-46: vre opens files in the Shell page's editor over a socket checked by the peer's uid
|
||||
|
||||
Status: Accepted
|
||||
|
||||
Context: Administrators edit the router's files from the Router Manager's Shell page, in an editor
|
||||
of the page rather than a terminal editor. The command that names a file runs in the shell, as the
|
||||
console account. The web service runs as root, sandboxed with `ProtectHome=yes`, so it cannot see
|
||||
the accounts' home folders. Reading and writing as root would also bypass the account's
|
||||
permissions.
|
||||
|
||||
Decision: Each shell has a random session in `VPN_ROUTER_SHELL`. `vpn-router-editor` (`vre`) sends
|
||||
the session and an absolute path to the web service on `/run/vpn-router/editor.sock`, which anyone
|
||||
may connect to. The web service accepts a request only when the peer's uid (`SO_PEERCRED`) is the
|
||||
uid of the session's console account. It then tells the shell's client over the shell's WebSocket,
|
||||
and the page reads and saves the file through `/shell/editor/<session>/<id>`. Files are read and
|
||||
written by `editor-file`, run with `systemd-run --uid=<account> --pipe` outside the sandbox, as the
|
||||
shell is. A save carries the SHA-256 of the content it replaces and is refused when the file
|
||||
changed since. Files open only while their shell lives.
|
||||
|
||||
Consequences: The editor edits only what the console account may; a file only root may write is
|
||||
edited in the terminal. `vre` run under sudo is refused, as its uid is not the console account's.
|
||||
Each read and save starts a transient unit. Files are limited to 1 MiB of UTF-8 text.
|
||||
@@ -3,11 +3,11 @@
|
||||
A Debian package that configures a Linux host or virtual machine as a VPN router:
|
||||
|
||||
- site-to-site IKEv2 IPSec with pre-shared key or certificate authentication (strongSwan swanctl)
|
||||
- road-warrior (P2S) access using IKEv2 EAP-TLS
|
||||
- IKEv2 access using EAP-TLS
|
||||
- a two-level certificate authority: a root, which may be kept offline, and issuing CAs for the VPN
|
||||
clients, for site-to-site and for general use, with certificate templates, published certificates
|
||||
and CRLs, and an optional ACME server
|
||||
- OpenVPN (P2S) access with a certificate and a password per user, the users kept in a local
|
||||
- OpenVPN access with a certificate and a password per user, the users kept in a local
|
||||
LDAP directory and managed with `vpn-router-user` or the web service
|
||||
- optional WireGuard endpoint
|
||||
- routing and NAT for a protected subnet, TCP MSS clamping, and UFW firewall rules
|
||||
@@ -108,15 +108,15 @@ must have the type shown below; an unknown section or setting is an error.
|
||||
| `general` | `mode` | string | `manual` | How much you supply: `manual`, `interfaces` or `auto` |
|
||||
| `interfaces` | `external` | string | | Name of the NIC facing the untrusted network |
|
||||
| `interfaces` | `internal` | string | | Name of the NIC facing the protected network |
|
||||
| `wan` | `local_fqdn` | string | | This router's name or IP address: its IKE identity in `fqdn` mode, the name resolved in `public_ip` mode, and the name of HTTPS, road-warrior and OpenVPN |
|
||||
| `local` | `cidrs` | list | | Local subnets routed via the internal side, masqueraded to the external one, and offered to road-warrior and OpenVPN clients |
|
||||
| `wan` | `local_fqdn` | string | | This router's name or IP address: its IKE identity in `fqdn` mode, the name resolved in `public_ip` mode, and the name of HTTPS, IKEv2 and OpenVPN |
|
||||
| `local` | `cidrs` | list | | Local subnets routed via the internal side, masqueraded to the external one, and offered to IKEv2 and OpenVPN clients |
|
||||
| `local` | `int_addr` | string | | This host's address on the internal network |
|
||||
| `local` | `int_gateway_ip` | string | | Next hop on the internal side |
|
||||
| `site_to_site` | | object | | The site-to-site connections, each a name and an object of the settings below |
|
||||
| `p2s` | `enabled` | boolean | `false` | Road-warrior access |
|
||||
| `p2s` | `address_pool` | string | | CIDR assigned to road-warrior clients |
|
||||
| `p2s` | `dns_domains` | list | | The domains road-warrior clients resolve with the router's resolver (INTERNAL_DNS_DOMAIN, RFC 8598, and the match domains of the Apple VPN profile), at most 16 |
|
||||
| `p2s` | `dns_listen` | string | `internal` | The addresses of the resolver the road-warrior, OpenVPN and WireGuard clients are given: `internal`, `external` or `both` |
|
||||
| `p2s` | `enabled` | boolean | `false` | IKEv2 access |
|
||||
| `p2s` | `address_pool` | string | | CIDR assigned to IKEv2 clients |
|
||||
| `p2s` | `dns_domains` | list | | The domains IKEv2 clients resolve with the router's resolver (INTERNAL_DNS_DOMAIN, RFC 8598, and the match domains of the Apple VPN profile), at most 16 |
|
||||
| `p2s` | `dns_listen` | string | `internal` | The addresses of the resolver the IKEv2, OpenVPN and WireGuard clients are given: `internal`, `external` or `both` |
|
||||
| `wireguard` | `enabled` | boolean | `false` | WireGuard endpoint |
|
||||
| `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 |
|
||||
@@ -251,9 +251,9 @@ A minimal site-to-site configuration in `auto` mode:
|
||||
The router's IKE identity (`wan.local_fqdn` in `fqdn` mode) and the remote one (`id`) are
|
||||
an FQDN or an IPv4 or IPv6 address. They are written to the strongSwan configuration as given;
|
||||
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. IKEv2 and OpenVPN access need a name in `wan.local_fqdn`.
|
||||
|
||||
WireGuard peers are P2S users' devices: a VPN user of the router's directory may have up to 16
|
||||
WireGuard peers are VPN users' devices: a VPN user of the router's directory may have up to 16
|
||||
peers, each with its public key and its IPs, IPv4 addresses or networks, kept in the user's
|
||||
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,
|
||||
@@ -398,7 +398,7 @@ systemctl restart vpn-router-setup
|
||||
The service also runs at boot. With platform `none` it does nothing and says so.
|
||||
Otherwise it regenerates the configuration files, resolves the interfaces and their
|
||||
addresses as `mode` directs, applies routes, generates WireGuard keys and injects
|
||||
firewall rules. It provisions the user directory in `slapd` while OpenVPN or road-warrior
|
||||
firewall rules. It provisions the user directory in `slapd` while OpenVPN or IKEv2
|
||||
access is configured. It then starts `strongswan`, `wg-quick@wg0` and
|
||||
`openvpn-server@vpn-router` if the corresponding feature is configured, reloading or
|
||||
restarting them only when their inputs changed, and
|
||||
@@ -449,7 +449,7 @@ The root signs the issuing CAs only (path length 1), and the issuing CAs sign th
|
||||
(path length 0):
|
||||
|
||||
- `p2s`, named `pki.p2s_name`, signs the router's VPN server certificate, which IKEv2 and OpenVPN
|
||||
present, and the users' certificates. Road-warrior and OpenVPN accept client certificates of
|
||||
present, and the users' certificates. IKEv2 and OpenVPN accept client certificates of
|
||||
this CA only.
|
||||
- `s2s`, named `pki.s2s_name`, signs the router's site-to-site certificate and the certificates
|
||||
of its peers.
|
||||
@@ -652,7 +652,7 @@ certificate names:
|
||||
### The user directory
|
||||
|
||||
Once a platform is chosen, `vpn-router-setup` keeps a directory of users in the local slapd,
|
||||
whether or not OpenVPN or road-warrior access is enabled, so users can be managed before either
|
||||
whether or not OpenVPN or IKEv2 access is enabled, so users can be managed before either
|
||||
is switched on:
|
||||
|
||||
- its own database, `dc=vpn-router,dc=local` in `/var/lib/ldap/vpn-router`, next to the one the
|
||||
@@ -773,22 +773,22 @@ When the Router Manager is installed with an administrator (its debconf question
|
||||
Manager-only account, adds it to the group `admins`, which it creates when missing, and removes
|
||||
the file.
|
||||
|
||||
The certificate is also accepted by road-warrior (IKEv2 EAP-TLS) access, which checks
|
||||
The certificate is also accepted by IKEv2 (EAP-TLS) access, which checks
|
||||
certificates only, so `disable` does not affect it; `remove` revokes the certificate for both.
|
||||
|
||||
### Apple VPN profile
|
||||
|
||||
While `p2s.enabled` is `true` and `wan.local_fqdn` is set, `/users/<name>/apple-profile` and a
|
||||
user's own `/account/apple-profile` give an unsigned Apple configuration profile (`.mobileconfig`)
|
||||
that sets up road-warrior access in the built-in VPN client of macOS, iOS and iPadOS. It holds:
|
||||
that sets up IKEv2 access in the built-in VPN client of macOS, iOS and iPadOS. It holds:
|
||||
|
||||
- the P2S CA and the root CA, the root installed as a trusted root;
|
||||
- the user's certificate and key as a PKCS#12 encrypted with the password of the request, with
|
||||
3DES and SHA-1, the only encryption Apple's clients read; the device asks for the password when
|
||||
the profile is installed, and the router does not keep it;
|
||||
- an IKEv2 VPN to `wan.local_fqdn`, with the user name as the local identity, the certificate over
|
||||
EAP-TLS, and AES-256, SHA2-256 and group 14 for IKE and ESP, as the router's road-warrior
|
||||
connection takes them;
|
||||
EAP-TLS, and AES-256, SHA2-256 and group 14 for IKE and ESP, as the router's IKEv2
|
||||
connection (`road-warrior`) takes them;
|
||||
- with `p2s.dns_domains`, the router's resolver for those domains and their subdomains
|
||||
(`SupplementalMatchDomains`). macOS and iOS use the DNS server of a VPN that does not carry all
|
||||
traffic only for its match domains.
|
||||
@@ -892,7 +892,7 @@ deploying.
|
||||
which is how a deferred install is expressed. `generic` is the module to choose when the
|
||||
router should be configured but the platform adds nothing.
|
||||
|
||||
The shipped modules currently add nothing. Road-warrior, OpenVPN and WireGuard clients are given the
|
||||
The shipped modules currently add nothing. IKEv2, OpenVPN and WireGuard clients are given the
|
||||
router's addresses of `p2s.dns_listen` as their DNS servers, by default the internal address,
|
||||
the WireGuard clients in the configuration the Router Manager makes for a device (`DNS =`),
|
||||
where `vpn-router-resolver.service`, dnsmasq with `/etc/vpn-router/dnsmasq.conf`, answers and
|
||||
@@ -908,7 +908,7 @@ A module may define any of the following, and takes the default for anything it
|
||||
|
||||
| Name | Type | Effect |
|
||||
|---|---|---|
|
||||
| `EXTRA_P2S_DNS` | `str` | added to the road-warrior pool `dns` list |
|
||||
| `EXTRA_P2S_DNS` | `str` | added to the `dns` list of the IKEv2 address pool |
|
||||
| `EXTRA_LOCAL_TS` | `str` | added to `local_ts` on both connections |
|
||||
| `apply(ctx)` | function | called once the system is configured, to apply platform-specific state; must be idempotent |
|
||||
|
||||
@@ -941,7 +941,7 @@ configuration: no key, identity, subnet, or address but the client's. They show
|
||||
|---|---|
|
||||
| `basic` (default) | The overall status (`ok` or `degraded`), the time and the Client IP |
|
||||
| `standard` | Also the `vpn-router` version, the `vpn-router-manager` version or that it is not installed, and the number of active services and of services in any other state, without their names |
|
||||
| `full` | Also each service and its state, Docker, how many site-to-site connections are set up and how many of them are established (`site_to_site`: `connections` and `established`, `null` when swanctl cannot be asked), road-warrior, OpenVPN and WireGuard |
|
||||
| `full` | Also each service and its state, Docker, how many site-to-site connections are set up and how many of them are established (`site_to_site`: `connections` and `established`, `null` when swanctl cannot be asked), IKEv2, OpenVPN and WireGuard |
|
||||
|
||||
The Client IP is the address the request came from: the last entry of `X-Forwarded-For` for a
|
||||
request from one of `web.trusted_proxies`, by default the address the request came in on, so
|
||||
@@ -976,7 +976,7 @@ curl http://10.0.0.4:17081/
|
||||
| `/status` | The same status as JSON |
|
||||
| `/auth` | Signing in with a user's password and TOTP code, refreshing and signing out |
|
||||
| `/config` | The configuration without its secret options, and what the router resolved and runs, as JSON; needs a token |
|
||||
| `/users` | The users of OpenVPN and road-warrior access; needs a token |
|
||||
| `/users` | The users of OpenVPN and IKEv2 access; needs a token |
|
||||
| `/service-accounts` | The service accounts; needs a token |
|
||||
| `/groups` | The groups of users; needs a token |
|
||||
| `/site-to-site` | The site-to-site connections: list, add, change and remove; needs a token |
|
||||
@@ -989,14 +989,14 @@ curl http://10.0.0.4:17081/
|
||||
The status holds the state of `vpn-router-setup`, `slapd` and each PostgreSQL cluster unit
|
||||
(`postgresql@<version>-<cluster>`, or `postgresql` as `no cluster` without one), of
|
||||
`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
|
||||
IKEv2 access is configured, of `wg-quick@wg0` when WireGuard is, and of
|
||||
`openvpn-server@vpn-router` when OpenVPN is; the number of connected
|
||||
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
|
||||
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
|
||||
connections that are set up (`connections`) and of those established (`established`, `null` when
|
||||
swanctl cannot be asked), without their names; the number of connected road-warrior (IPsec P2S) clients; and
|
||||
swanctl cannot be asked), without their names; the number of connected IKEv2 clients; and
|
||||
the WireGuard peers with the age of their last handshake and their traffic. `status` is `ok` when
|
||||
all these services are active, and `degraded` otherwise.
|
||||
|
||||
@@ -1007,7 +1007,7 @@ curl http://10.0.0.4:7081/status
|
||||
|
||||
### Tokens and signing in
|
||||
|
||||
`/config`, `/users`, `/service-accounts`, `/groups`, `/site-to-site`, `/backup`, `/remote-access`, `/ca`, `/logs`, `/sites`, `/dns`, `/shell` and `/account` need a JWT in an `Authorization: Bearer` header, signed with
|
||||
`/config`, `/metrics/history`, `/metrics/settings`, `/users`, `/service-accounts`, `/groups`, `/site-to-site`, `/backup`, `/remote-access`, `/ca`, `/logs`, `/sites`, `/dns`, `/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).
|
||||
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
|
||||
@@ -1099,6 +1099,34 @@ off or the TOTP enrolment removed; the router then sends a close frame with the
|
||||
journal of `vpn-router-web` records who opened each shell, from which address, in which unit,
|
||||
when it ended and why, and each refused ticket or shell with the reason.
|
||||
|
||||
#### The editor of the shell
|
||||
|
||||
In the shell, `vpn-router-editor FILE...`, or its short name `vre`, opens files in the editor of
|
||||
the Router Manager's Shell page and returns at once:
|
||||
|
||||
```sh
|
||||
vre /etc/vpn-router/vpn-router.json ~/notes.txt
|
||||
```
|
||||
|
||||
- **How the files reach the page:** each shell has its session in `VPN_ROUTER_SHELL`. `vre`
|
||||
names each file, as an absolute path, with that session on the socket
|
||||
`/run/vpn-router/editor.sock`. The web service accepts it only from a process that runs as the
|
||||
shell's console account, and sends the page a text message `{"editor": "open", "session", "id", "path"}`
|
||||
over the shell's WebSocket.
|
||||
- **Reading and saving:** the page uses the routes below, which need the user's access token. The
|
||||
file is read and written as the console account by `/usr/lib/vpn-router/editor-file`, run with
|
||||
`systemd-run --uid --pipe`, so the account's permissions apply. A save writes the file in place,
|
||||
keeping its owner and mode, and creates a file that does not exist.
|
||||
- **Limits:** only regular files of UTF-8 text up to 1 MiB, and at most 20 open files per shell.
|
||||
- **Lifetime:** the files are open while the shell lives. `vre` outside the Router Manager's shell
|
||||
exits with status 2.
|
||||
|
||||
| Request | Body | Answer |
|
||||
| --- | --- | --- |
|
||||
| `GET /shell/editor/<session>/<id>` | | `{"path", "content", "version"}`, `version` the SHA-256 of the content |
|
||||
| `PUT /shell/editor/<session>/<id>` | `{"content", "version"}` | `{"path", "version"}`; 409 when the file no longer has `version` |
|
||||
| `DELETE /shell/editor/<session>/<id>` | | `{"path", "closed": true}` |
|
||||
|
||||
### The /config page
|
||||
|
||||
`/config` shows what `/` and `/status` leave out, for a client such as manage-ui:
|
||||
@@ -1206,7 +1234,7 @@ and answer bodies are JSON; a request body is at most 65536 bytes.
|
||||
| `POST /users/<name>/unlock` | | the user |
|
||||
| `DELETE /users/<name>` | | `{"name", "removed": true}` |
|
||||
| `GET /users/<name>/profile` | | `{"name", "profile"}`, the OpenVPN client profile |
|
||||
| `POST /users/<name>/apple-profile` | `{"password"}` | `{"name", "profile"}`, the Apple configuration profile of IKEv2 road-warrior access (see Apple VPN profile) |
|
||||
| `POST /users/<name>/apple-profile` | `{"password"}` | `{"name", "profile"}`, the Apple configuration profile of IKEv2 access (see Apple VPN profile) |
|
||||
| `POST /users/<name>/otp` | | `{"name", "secret", "uri"}`, a new TOTP secret; nothing is stored |
|
||||
| `PUT /users/<name>/otp` | `{"secret", "code"}` | the user, once the code matches the secret |
|
||||
| `DELETE /users/<name>/otp` | | the user |
|
||||
@@ -1216,7 +1244,7 @@ and answer bodies are JSON; a request body is at most 65536 bytes.
|
||||
A failure is `{"error": ...}` with 400 for an invalid request, 404 for a user that does not
|
||||
exist or is a service account, 409 for a user that exists and 500 otherwise. `/users` does not
|
||||
list service accounts; `/service-accounts` serves them. `/users` works whether or not OpenVPN or
|
||||
road-warrior access is enabled; the profile needs OpenVPN. Passwords, private keys and TOTP
|
||||
IKEv2 access is enabled; the profile needs OpenVPN. Passwords, private keys and TOTP
|
||||
secrets cross the internal network in plain HTTP, like the token.
|
||||
|
||||
```bash
|
||||
@@ -1264,7 +1292,7 @@ router manages them, their display name and description included. A failure is o
|
||||
| `PUT /account/passkeys` | `{"id", "credential", "label"?}`, `credential` the browser's answer as its `toJSON()` gives it; `label` at most 64 characters, `Passkey` without one | the user |
|
||||
| `DELETE /account/passkeys/<id>` | | the user |
|
||||
| `POST /account/profile` | `{"current_password", "current_code"}` | `{"name", "profile"}`, the user's OpenVPN client profile |
|
||||
| `POST /account/apple-profile` | `{"current_password", "current_code", "password"}` | `{"name", "profile"}`, the user's Apple configuration profile of IKEv2 road-warrior access (see Apple VPN profile) |
|
||||
| `POST /account/apple-profile` | `{"current_password", "current_code", "password"}` | `{"name", "profile"}`, the user's Apple configuration profile of IKEv2 access (see Apple VPN profile) |
|
||||
|
||||
`current_code` is needed only for a user enrolled for TOTP; removing a passkey needs neither. A wrong current password or code gets
|
||||
403 and counts as a failed login; slapd accepts each code once, so a code used to sign in is not
|
||||
@@ -1332,9 +1360,21 @@ The answer of `GET /backup` holds:
|
||||
|
||||
A change is made, checked and applied as for `/site-to-site`.
|
||||
|
||||
### The /metrics/settings API
|
||||
|
||||
`/metrics/settings` shows and changes the settings of the metrics service, with the same token as
|
||||
`/users`.
|
||||
|
||||
| Request | Body | Answer |
|
||||
| --- | --- | --- |
|
||||
| `GET /metrics/settings` | | `{"settings"}`: the `metrics` section without `otlp_password`, with `otlp_password_set` |
|
||||
| `PUT /metrics/settings` | any of the settings of the `metrics` section; an empty or absent `otlp_password` keeps the secret, and an empty `otlp_username` removes it | `{"settings"}` |
|
||||
|
||||
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`),
|
||||
`/remote-access` shows and changes the remote access systems, IKEv2 (`p2s`),
|
||||
OpenVPN (`openvpn`) and WireGuard (`wireguard`), with the same token as `/users`.
|
||||
|
||||
| Request | Body | Answer |
|
||||
@@ -1615,7 +1655,7 @@ The domains are added with `/dns` or the Router Manager, each with:
|
||||
|
||||
A domain inside another domain of the router is delegated from it with NS records. Setup writes the
|
||||
domains at each run, and after each change through `/dns`; the SOA's serial grows only when the
|
||||
records change. While P2S, OpenVPN or WireGuard is enabled, dnsmasq answers their clients on port 53
|
||||
records change. While IKEv2, OpenVPN or WireGuard is enabled, dnsmasq answers their clients on port 53
|
||||
of the addresses of `p2s.dns_listen`, by default the internal address, so `dns.listen` must not
|
||||
share an interface with it. The parent zone of each domain must delegate it to `dns.nameserver` and
|
||||
the secondaries, with glue records for name servers inside the domain.
|
||||
@@ -1636,7 +1676,7 @@ takes a sample of the router with `psutil`:
|
||||
- the free and the total space of `/`;
|
||||
- the bytes received and sent per second on the external and the internal interface;
|
||||
- the 1, 5 and 15 minute load averages;
|
||||
- the VPN clients of each enabled VPN system: road-warrior (IPsec P2S) and OpenVPN clients, and
|
||||
- the VPN clients of each enabled VPN system: IKEv2 and OpenVPN clients, and
|
||||
WireGuard peers with a handshake in the last 3 minutes;
|
||||
- for each site-to-site connection that is set up: whether it is up (1 or 0), the bytes and the
|
||||
packets per second received and sent through its child SAs since the previous sample, and the
|
||||
|
||||
@@ -1,3 +1,51 @@
|
||||
vpn-router (0.35.0-1) unstable; urgency=medium
|
||||
|
||||
feature: vre opens files from the Router Manager's shell in the editor of its Shell page.
|
||||
|
||||
* vpn-router-editor FILE..., or vre, in the shell of the Router Manager
|
||||
opens the files in the editor of the Shell page and returns at once;
|
||||
outside that shell it exits with status 2.
|
||||
* GET, PUT and DELETE /shell/editor/<session>/<id> read, save and close
|
||||
a file opened so, as the shell's console account; a save is refused
|
||||
with 409 when the file changed since it was read.
|
||||
* The editor takes regular files of UTF-8 text up to 1 MiB, at most 20
|
||||
per shell, while the shell lives.
|
||||
|
||||
-- Slawomir Koszewski <slawek@koszewscy.waw.pl> Thu, 29 Oct 2026 18:02:27 +0100
|
||||
|
||||
vpn-router (0.34.0-1) unstable; urgency=medium
|
||||
|
||||
feature: the web service shows and changes the settings of the metrics service at /metrics/settings, and OpenVPN starts on a new router, as setup installs CRLs that revoke nothing.
|
||||
|
||||
* GET and PUT /metrics/settings show and change the metrics section;
|
||||
otlp_password is never shown, an empty one keeps it, and an empty
|
||||
otlp_username removes it.
|
||||
* A CRL without revoked certificates is installed for strongSwan and
|
||||
OpenVPN.
|
||||
|
||||
-- Slawomir Koszewski <slawek@koszewscy.waw.pl> Wed, 28 Oct 2026 22:00:00 +0100
|
||||
|
||||
vpn-router (0.33.3-1) unstable; urgency=medium
|
||||
|
||||
fix: vpn-router-setup runs on a new installation once libpq can be found.
|
||||
|
||||
* The vpn-router-setup unit waits, with wait-for-libpq, until libpq is in
|
||||
the linker cache before it runs setup.
|
||||
* The package starts vpn-router-setup without waiting for it.
|
||||
|
||||
-- Slawomir Koszewski <slawek@koszewscy.waw.pl> Wed, 28 Oct 2026 21:00:00 +0100
|
||||
|
||||
vpn-router (0.33.2-1) unstable; urgency=medium
|
||||
|
||||
fix: the router's IPsec remote access is called IKEv2 instead of road-warrior.
|
||||
|
||||
* The README, the package description, the installer's questions, the
|
||||
status page, the messages of the web service and the examples say
|
||||
IKEv2 access and IKEv2 clients; the strongSwan connection keeps its
|
||||
name, road-warrior.
|
||||
|
||||
-- Slawomir Koszewski <slawek@koszewscy.waw.pl> Tue, 27 Oct 2026 23:00:00 +0100
|
||||
|
||||
vpn-router (0.33.1-1) unstable; urgency=medium
|
||||
|
||||
fix: the name server is the primary of any number of domains, each with secondary name servers and a switch for the records of the sites.
|
||||
|
||||
@@ -46,10 +46,10 @@ Suggests: caddy,
|
||||
pdns-server,
|
||||
pdns-backend-pgsql,
|
||||
proxmox-backup-client-static
|
||||
Description: Linux VPN router with IPSec, road-warrior, OpenVPN and WireGuard support
|
||||
Description: Linux VPN router with IPSec, IKEv2, OpenVPN and WireGuard support
|
||||
Configures a Linux host or virtual machine as a VPN router: site-to-site
|
||||
IKEv2 IPSec (strongSwan swanctl) with pre-shared keys or certificates,
|
||||
optional road-warrior access using EAP-TLS, optional OpenVPN with a
|
||||
optional IKEv2 access using EAP-TLS, optional OpenVPN with a
|
||||
certificate and a password per user, kept in a local LDAP directory, optional
|
||||
WireGuard, and the routing, NAT, MSS clamping and firewall rules a router
|
||||
needs. Its two-level CA, whose root may be kept offline, has issuing CAs for
|
||||
|
||||
@@ -17,6 +17,8 @@ src/usr/lib/vpn-router/install-packages usr/lib/vpn-router/
|
||||
src/usr/lib/vpn-router/webdav-auth usr/lib/vpn-router/
|
||||
src/usr/lib/vpn-router/openvpn-same-user usr/lib/vpn-router/
|
||||
src/usr/lib/vpn-router/openvpn-issuer usr/lib/vpn-router/
|
||||
src/usr/lib/vpn-router/editor-file usr/lib/vpn-router/
|
||||
src/usr/bin/vpn-router-editor usr/bin/
|
||||
src/usr/sbin/vpn-router-user usr/sbin/
|
||||
src/usr/sbin/vpn-router-group usr/sbin/
|
||||
src/usr/sbin/vpn-router-token usr/sbin/
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
usr/bin/vpn-router-editor usr/bin/vre
|
||||
@@ -71,9 +71,11 @@ case "$1" in
|
||||
# setting the forward policy all belong to vpn-router-setup, which does
|
||||
# them only once the configuration says this host is a router.
|
||||
|
||||
# The web service waits for libpq in the linker cache, which dpkg
|
||||
# refreshes after this script, so it is started without waiting for it.
|
||||
# The setup and web services wait for libpq in the linker cache, which
|
||||
# dpkg refreshes after this script, so they are started without waiting
|
||||
# for them.
|
||||
if [ -d /run/systemd/system ]; then
|
||||
systemctl --no-block restart vpn-router-setup.service >/dev/null 2>&1 || true
|
||||
systemctl --no-block restart vpn-router-web.service >/dev/null 2>&1 || true
|
||||
fi
|
||||
;;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
dh $@
|
||||
|
||||
override_dh_installsystemd:
|
||||
dh_installsystemd --name=vpn-router-setup
|
||||
dh_installsystemd --name=vpn-router-setup --no-start
|
||||
dh_installsystemd --name=vpn-router-docker
|
||||
dh_installsystemd --name=vpn-router-web --no-start
|
||||
dh_installsystemd --name=vpn-router-metrics --no-enable --no-start
|
||||
|
||||
@@ -4,7 +4,7 @@ Choices: none, generic, azure, gcp
|
||||
Default: none
|
||||
Description: Platform:
|
||||
Which platform this router runs on. The choice loads platform-specific
|
||||
additions, such as the provider DNS resolver for road-warrior clients.
|
||||
additions, such as the provider DNS resolver for IKEv2 clients.
|
||||
.
|
||||
none defers configuration entirely: the package installs its files and
|
||||
changes nothing on the machine. Choose it when the router will be configured
|
||||
@@ -58,7 +58,7 @@ Type: string
|
||||
Default:
|
||||
Description: Local router FQDN:
|
||||
Fully-qualified domain name of this router (for example
|
||||
router.example.com). Used as the road-warrior server identity and
|
||||
router.example.com). Used as the IKEv2 server identity and
|
||||
certificate common name.
|
||||
|
||||
Template: vpn-router/local_id_mode
|
||||
@@ -122,15 +122,15 @@ Description: Pre-shared key (PSK):
|
||||
Template: vpn-router/p2s_enabled
|
||||
Type: boolean
|
||||
Default: false
|
||||
Description: Enable road-warrior (P2S) access?
|
||||
Description: Enable IKEv2 access?
|
||||
If enabled, this router accepts IKEv2 EAP-TLS connections from individual
|
||||
clients, with certificates of the router's P2S issuing CA.
|
||||
|
||||
Template: vpn-router/p2s_address_pool
|
||||
Type: string
|
||||
Default:
|
||||
Description: Road-warrior address pool:
|
||||
CIDR block assigned to road-warrior clients (for example 172.16.0.0/24).
|
||||
Description: IKEv2 address pool:
|
||||
CIDR block assigned to IKEv2 clients (for example 172.16.0.0/24).
|
||||
|
||||
Template: vpn-router/pki_root_name
|
||||
Type: string
|
||||
|
||||
@@ -6,6 +6,7 @@ After=network-online.target docker.service postgresql.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStartPre=/usr/lib/vpn-router/wait-for-libpq
|
||||
ExecStart=/usr/lib/vpn-router/setup
|
||||
RemainAfterExit=yes
|
||||
|
||||
|
||||
Executable
+71
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/python3
|
||||
"""Open files in the editor of the Router Manager's Shell page, from the shell of that page; vre is its short name.
|
||||
|
||||
Each file is named to the router's web service over SOCKET with the shell's session,
|
||||
$VPN_ROUTER_SHELL, which the web service sets in the shells it starts; the Shell page then shows
|
||||
the file in a tab of its editor, where it is read and saved. The command returns at once. A
|
||||
relative path is taken from the current directory, and a file that does not exist is created when
|
||||
it is saved. The exit status is 0 when every file was opened, 1 when the web service refused one,
|
||||
with its reason, and 2 outside the Router Manager's shell or for an invalid request. -h or --help
|
||||
shows the usage.
|
||||
|
||||
vre FILE...
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
|
||||
#: The socket of the router's web service that opens files in the editor of a shell.
|
||||
SOCKET = '/run/vpn-router/editor.sock'
|
||||
|
||||
#: Seconds to wait for the web service's answer.
|
||||
TIMEOUT = 10
|
||||
|
||||
|
||||
class Parser(argparse.ArgumentParser):
|
||||
"""An argument parser whose errors end with status 2."""
|
||||
|
||||
def error(self, message):
|
||||
print(f'{self.prog}: {message}; see {self.prog} --help', file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
def opened(session, path):
|
||||
"""Ask the web service to open the file in the editor of the session; None, or the reason it refused."""
|
||||
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as connection:
|
||||
connection.settimeout(TIMEOUT)
|
||||
connection.connect(SOCKET)
|
||||
connection.sendall(json.dumps({'session': session, 'path': path}).encode() + b'\n')
|
||||
answer = json.loads(connection.makefile('rb').readline() or b'{}')
|
||||
return None if answer.get('ok') is True else str(answer.get('error', 'no answer'))
|
||||
|
||||
|
||||
def main(argv):
|
||||
prog = os.path.basename(sys.argv[0])
|
||||
parser = Parser(prog=prog, description="Open files in the editor of the Router Manager's Shell page.")
|
||||
parser.add_argument('files', nargs='+', metavar='FILE', help='a file to open')
|
||||
options = parser.parse_args(argv)
|
||||
session = os.environ.get('VPN_ROUTER_SHELL', '')
|
||||
if not session:
|
||||
print(f'{prog}: {prog} works only in the shell of the Router Manager', file=sys.stderr)
|
||||
return 2
|
||||
status = 0
|
||||
for name in options.files:
|
||||
path = os.path.abspath(os.path.expanduser(name))
|
||||
try:
|
||||
reason = opened(session, path)
|
||||
except (OSError, ValueError) as exc:
|
||||
reason = f'the web service cannot be reached: {exc}'
|
||||
if reason:
|
||||
print(f'{prog}: {path}: {reason}', file=sys.stderr)
|
||||
status = 1
|
||||
else:
|
||||
print(f'Opened {path} in the editor of the Shell page.')
|
||||
return status
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main(sys.argv[1:]))
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/python3
|
||||
"""Read or write a file of the editor of the Router Manager's Shell page, as the user it runs as.
|
||||
|
||||
The web service runs it with systemd-run --uid=<console account> --pipe, outside its sandbox, so the
|
||||
file is read and written with the rights of the shell's user. The version of a content is the
|
||||
hexadecimal SHA-256 of its bytes; a missing file has the version of the empty content.
|
||||
|
||||
editor-file read PATH print the content of the file; nothing for a missing file
|
||||
editor-file write PATH VERSION replace the content with standard input when the file still has
|
||||
VERSION, writing in place, so its owner, mode and links stay;
|
||||
a missing file is created
|
||||
|
||||
Only regular files of at most MAX_BYTES bytes of UTF-8 text are read and written. The exit status
|
||||
is 0 on success, 2 for a file that is refused, with the reason on standard error, 3 when the file
|
||||
does not have VERSION any more, and 1 for any other failure.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
import sys
|
||||
|
||||
#: The largest file the editor opens, in bytes.
|
||||
MAX_BYTES = 1024 * 1024
|
||||
|
||||
|
||||
class Refused(Exception):
|
||||
"""A file the editor does not open or write; status is the exit status."""
|
||||
|
||||
def __init__(self, message, status=2):
|
||||
super().__init__(message)
|
||||
self.status = status
|
||||
|
||||
|
||||
def text(data, path):
|
||||
"""The bytes when they are UTF-8 text of at most MAX_BYTES; Refused otherwise."""
|
||||
if len(data) > MAX_BYTES:
|
||||
raise Refused(f'{path} is larger than {MAX_BYTES // 1024} KiB')
|
||||
try:
|
||||
data.decode('utf-8')
|
||||
except UnicodeDecodeError:
|
||||
raise Refused(f'{path} is not UTF-8 text')
|
||||
return data
|
||||
|
||||
|
||||
def current(path):
|
||||
"""The content of the file, b'' when it does not exist; Refused for a file that is not a regular one."""
|
||||
if not os.path.lexists(path):
|
||||
return b''
|
||||
if not os.path.isfile(path):
|
||||
raise Refused(f'{path} is not a regular file')
|
||||
with open(path, 'rb') as handle:
|
||||
return text(handle.read(MAX_BYTES + 1), path)
|
||||
|
||||
|
||||
def main(argv):
|
||||
if len(argv) < 2 or (argv[0], len(argv)) not in (('read', 2), ('write', 3)):
|
||||
print('usage: editor-file read PATH | editor-file write PATH VERSION', file=sys.stderr)
|
||||
return 2
|
||||
path = argv[1]
|
||||
try:
|
||||
if argv[0] == 'read':
|
||||
sys.stdout.buffer.write(current(path))
|
||||
return 0
|
||||
content = text(sys.stdin.buffer.read(MAX_BYTES + 1), path)
|
||||
if hashlib.sha256(current(path)).hexdigest() != argv[2]:
|
||||
raise Refused(f'{path} changed since it was read', 3)
|
||||
with open(path, 'r+b' if os.path.lexists(path) else 'xb') as handle:
|
||||
handle.write(content)
|
||||
handle.truncate()
|
||||
handle.flush()
|
||||
os.fsync(handle.fileno())
|
||||
except Refused as refused:
|
||||
print(refused, file=sys.stderr)
|
||||
return refused.status
|
||||
except OSError as exc:
|
||||
print(f'{exc.filename or path}: {exc.strerror}', file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main(sys.argv[1:]))
|
||||
@@ -7,7 +7,7 @@ disables the unit.
|
||||
Every SAMPLE_SECONDS, on the minute, a sample is taken with psutil: the CPU utilization since the
|
||||
previous sample, the memory used and in total, the free and the total space of /, the bytes
|
||||
received and sent per second on the external and the internal interface, and the load averages;
|
||||
and the VPN clients of each enabled VPN system (vpnrouter_status): road-warrior (p2s) and
|
||||
and the VPN clients of each enabled VPN system (vpnrouter_status): IKEv2 (p2s) and
|
||||
OpenVPN clients and WireGuard peers with a recent handshake; and of each site-to-site connection
|
||||
that is set up, whether it is up, the bytes and packets per second through its child SAs since
|
||||
the previous sample, and how long its IKE SA has been up. The interfaces, the enabled systems and
|
||||
|
||||
@@ -1063,7 +1063,7 @@ def routing_ready(ctx):
|
||||
|
||||
|
||||
def p2s_ready(ctx):
|
||||
"""Road-warrior access needs its settings and the router's VPN server certificate, which the P2S CA issues."""
|
||||
"""IKEv2 access needs its settings and the router's VPN server certificate, which the P2S CA issues."""
|
||||
return bool(ctx['p2s_enabled'] and ctx['p2s_address_pool'] and ctx['local_fqdn'] and ctx['server_certificate'])
|
||||
|
||||
|
||||
|
||||
@@ -722,7 +722,7 @@ def install_crls(connection):
|
||||
crls = {}
|
||||
for name, file in SWANCTL_CA_FILES.items():
|
||||
entry = authority(connection, name)
|
||||
crls[name] = entry['crl'].public_bytes(serialization.Encoding.PEM) if entry and entry['crl'] else None
|
||||
crls[name] = entry['crl'].public_bytes(serialization.Encoding.PEM) if entry and entry['crl'] is not None else None
|
||||
changed |= write(X509CRL_DIR / file, crls[name], 0o644) if crls[name] else remove(X509CRL_DIR / file)
|
||||
if crls['root'] and crls['p2s']:
|
||||
changed |= write(OPENVPN_CRL, crls['root'] + crls['p2s'], 0o644)
|
||||
|
||||
@@ -10,7 +10,7 @@ adding one file here. Nothing in the core has to be edited.
|
||||
|
||||
A module may define any of:
|
||||
|
||||
EXTRA_P2S_DNS str, added to the road-warrior pool 'dns' list
|
||||
EXTRA_P2S_DNS str, added to the 'dns' list of the IKEv2 address pool
|
||||
EXTRA_LOCAL_TS str, added to 'local_ts' on both connections
|
||||
apply(ctx) called once the system has been configured, to apply
|
||||
platform-specific state. Must be idempotent. ctx is the
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
No platform-specific configuration is currently required.
|
||||
|
||||
The Azure platform resolver 168.63.129.16 is deliberately *not* advertised to
|
||||
road-warrior clients and *not* added to the traffic selectors. Clients are
|
||||
IKEv2 clients and *not* added to the traffic selectors. Clients are
|
||||
given the router's addresses of p2s.dns_listen as their DNS servers, where dnsmasq
|
||||
forwards to the router's own systemd-resolved. That resolves
|
||||
Azure-internal names without routing the wireserver address through the tunnel
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
No platform-specific configuration is currently required.
|
||||
|
||||
As on Azure, road-warrior clients are given the router itself as their DNS
|
||||
As on Azure, IKEv2 clients are given the router itself as their DNS
|
||||
server and the router forwards through its own systemd-resolved, so the
|
||||
metadata resolver does not belong in the pool or in the traffic selectors.
|
||||
"""
|
||||
@@ -814,7 +814,7 @@ def profile(connection, name):
|
||||
|
||||
|
||||
def apple_profile(connection, name, password):
|
||||
"""The Apple configuration profile (.mobileconfig, unsigned) of the user's IKEv2 road-warrior access: the root CA and
|
||||
"""The Apple configuration profile (.mobileconfig, unsigned) of the user's IKEv2 access: the root CA and
|
||||
the P2S CA, the user's certificate and key as a PKCS#12 encrypted with the password, and the VPN.
|
||||
|
||||
The connection matches road-warrior.conf: the router's wan.local_fqdn as address and identity, the user name, a
|
||||
@@ -828,7 +828,7 @@ def apple_profile(connection, name, password):
|
||||
cfg = vpnrouter.load_config()
|
||||
fqdn = cfg['wan']['local_fqdn'].strip()
|
||||
if not cfg['p2s']['enabled'] or not fqdn:
|
||||
raise Failure('IKEv2 road-warrior access is not enabled', 2)
|
||||
raise Failure('IKEv2 access is not enabled', 2)
|
||||
credentials = vpnrouter_pki.user_credentials(name)
|
||||
if credentials is None:
|
||||
raise Failure(f'user {name} has no certificate')
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/bin/sh
|
||||
# Wait until libpq is in the dynamic linker cache, where psycopg looks it up.
|
||||
# ExecStartPre= of vpn-router-web.service; TimeoutStartSec= of the unit bounds the wait.
|
||||
# ExecStartPre= of vpn-router-setup.service, vpn-router-web.service and vpn-router-metrics.service;
|
||||
# TimeoutStartSec= of the unit bounds the wait.
|
||||
|
||||
reported=
|
||||
until ldconfig -p | grep -q '^[[:space:]]*libpq\.so\.5 '; do
|
||||
|
||||
@@ -29,7 +29,7 @@ Pages:
|
||||
/backup the backup settings, the last run and the local archives: show, change and run
|
||||
now (see backup())
|
||||
/remote-access
|
||||
the settings of IKEv2 road-warrior, OpenVPN and WireGuard access and whether each
|
||||
the settings of IKEv2, 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
|
||||
settings, issue and revoke certificates, and take the certificate of an issuing CA
|
||||
@@ -41,13 +41,18 @@ Pages:
|
||||
a member of admins whose console login is on and who has a TOTP token: POST signs the user in again, unless
|
||||
the user used a shell within web.shell_auth_timeout minutes, for a one-time ticket
|
||||
(see shell_ticket()), GET with it is a WebSocket
|
||||
that carries the terminal (see Handler.shell())
|
||||
that carries the terminal (see Handler.shell()); /shell/editor/<session>/<id> reads, saves and
|
||||
closes a file the shell opened with vpn-router-editor (vre) over EDITOR_SOCKET (see
|
||||
shell_editor() and editor_request())
|
||||
/auth sign-in with a user's password and TOTP code or with a passkey, refresh and sign-out
|
||||
(see auth())
|
||||
/metrics/history
|
||||
the metrics the metrics service keeps, for a view: hour, day, month or year (see
|
||||
metrics_history())
|
||||
/logs the last lines of the journal of the router's units and of the files web.log_files lists
|
||||
/metrics/settings
|
||||
the settings of the metrics service: show and change, applied by vpn-router-setup (see
|
||||
metrics_settings())
|
||||
/logs the last lines of the journal of the router's units and of the files web.log_files lists
|
||||
(see logs())
|
||||
/sites the web sites Caddy serves: the settings of sites, list, add, change and remove (see sites()),
|
||||
and the files of each: list, download, upload, extract a zip archive, create a folder, rename and
|
||||
@@ -62,7 +67,7 @@ Pages:
|
||||
loopback address, where Caddy passes the requests of the HTTPS site; POST /oidc/login counts
|
||||
towards the limit of /auth calls
|
||||
|
||||
/status/full, /config, /metrics/history, /postgresql, /users, /service-accounts, /groups, /site-to-site, /backup, /remote-access, /ca, /logs, /sites, /dns, /shell and /account need an HS256 JWT in an Authorization: Bearer header signed
|
||||
/status/full, /config, /metrics/history, /metrics/settings, /postgresql, /users, /service-accounts, /groups, /site-to-site, /backup, /remote-access, /ca, /logs, /sites, /dns, /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;
|
||||
/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
|
||||
@@ -79,7 +84,7 @@ default, the overall status, the time and the client's address (its address as f
|
||||
below); standard also the package version, the installed version of vpn-router-manager
|
||||
(manager_version, null when it is not installed) and the number of active and other services; full
|
||||
whether each service runs, whether Docker is installed, how many of the site-to-site connections are established
|
||||
(site_to_site: connections and established, null when swanctl cannot be asked), and road-warrior (IPsec P2S), OpenVPN
|
||||
(site_to_site: connections and established, null when swanctl cannot be asked), and IKEv2, OpenVPN
|
||||
and WireGuard. No configuration data, key or connection name is shown, and no address but the client's.
|
||||
|
||||
The service first initializes, without listening: it makes the signing key, the OpenID Provider's key
|
||||
@@ -184,7 +189,7 @@ BODY_LIMIT = 65536
|
||||
#: 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')
|
||||
|
||||
#: The sections of the remote access systems /remote-access changes: IKEv2 road-warrior, OpenVPN and WireGuard.
|
||||
#: The sections of the remote access systems /remote-access changes: IKEv2, OpenVPN and WireGuard.
|
||||
REMOTE_ACCESS = ('p2s', 'openvpn', 'wireguard')
|
||||
|
||||
#: The HTTP status for each status of a vpnrouter_users.Failure; any other is 500.
|
||||
@@ -214,6 +219,23 @@ SHELL_CHECK_SECONDS = 60
|
||||
#: The largest WebSocket message accepted from a client, in bytes.
|
||||
MESSAGE_LIMIT = 65536
|
||||
|
||||
#: The socket vpn-router-editor (vre) names files on to open them in the editor of a shell (see editor_listen()).
|
||||
EDITOR_SOCKET = '/run/vpn-router/editor.sock'
|
||||
|
||||
#: The helper that reads and writes a file of the editor as the shell's user.
|
||||
EDITOR_FILE = '/usr/lib/vpn-router/editor-file'
|
||||
|
||||
#: The largest file the editor opens, in bytes, as editor-file allows; a body of PUT /shell/editor/... may hold its
|
||||
#: content escaped in JSON.
|
||||
EDITOR_MAX_BYTES = MIB
|
||||
EDITOR_BODY_LIMIT = 2 * EDITOR_MAX_BYTES + BODY_LIMIT
|
||||
|
||||
#: The most files a shell has open in its editor at once.
|
||||
EDITOR_FILES_MAX = 20
|
||||
|
||||
#: Seconds a reader of the editor socket and a run of editor-file may take.
|
||||
EDITOR_TIMEOUT = 30
|
||||
|
||||
#: The GUID of RFC 6455 that the Sec-WebSocket-Accept of a handshake is made with.
|
||||
WEBSOCKET_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
|
||||
|
||||
@@ -234,6 +256,10 @@ _shell_lock = threading.Lock()
|
||||
#: The last use of a shell of each user, a sign-in of POST /shell or a keystroke: user -> monotonic time.
|
||||
_shell_used = {}
|
||||
|
||||
#: The open shells by their session, which each shell has in VPN_ROUTER_SHELL: session -> {"user", "account", "uid",
|
||||
#: "send", the function that sends a WebSocket frame to its client, "files": {file id: path}}.
|
||||
_shell_sessions = {}
|
||||
|
||||
|
||||
def read_json(path):
|
||||
"""The object in a JSON file, or {} when the file is missing or holds no object."""
|
||||
@@ -490,7 +516,7 @@ def text_of(status):
|
||||
lines.append(f'Site to site: {"unknown" if established is None else established} of {connections} established')
|
||||
if 'p2s' in status:
|
||||
clients = status['p2s']['clients']
|
||||
lines.append(f'Road-warrior (IPsec P2S): {"unknown" if clients is None else f"{clients} connected"}')
|
||||
lines.append(f'IKEv2: {"unknown" if clients is None else f"{clients} connected"}')
|
||||
if 'openvpn' in status:
|
||||
clients = status['openvpn']['clients']
|
||||
lines.append(f'OpenVPN: {"unknown" if clients is None else f"{clients} connected"}')
|
||||
@@ -566,7 +592,7 @@ def users(state, method, path, body):
|
||||
DELETE /users/<name> remove the user and revoke the certificate
|
||||
GET /users/<name>/profile the OpenVPN client profile
|
||||
POST /users/<name>/apple-profile
|
||||
the Apple configuration profile of IKEv2 road-warrior access: {"password"}, which
|
||||
the Apple configuration profile of IKEv2 access: {"password"}, which
|
||||
encrypts the user's key in it; {"name", "profile"}
|
||||
POST /users/<name>/otp a new TOTP secret and its otpauth URI; nothing is stored
|
||||
PUT /users/<name>/otp store a TOTP secret: {"secret", "code"}, code a code of the secret
|
||||
@@ -1051,7 +1077,7 @@ def account(name, method, path, body):
|
||||
POST /account/profile the user's OpenVPN client profile: {"current_password", "current_code"?}; {"name",
|
||||
"profile"}
|
||||
POST /account/apple-profile
|
||||
the user's Apple configuration profile of IKEv2 road-warrior access:
|
||||
the user's Apple configuration profile of IKEv2 access:
|
||||
{"current_password", "current_code"?, "password"}, password encrypting the user's key in
|
||||
it; {"name", "profile"}
|
||||
|
||||
@@ -1351,6 +1377,39 @@ def metrics_history(view):
|
||||
return 500, {'error': str(exc.code)}
|
||||
|
||||
|
||||
def metrics_shown(settings):
|
||||
"""The metrics section as /metrics/settings shows it: its settings but otlp_password, with otlp_password_set."""
|
||||
return {**{option: value for option, value in settings.items() if option != 'otlp_password'},
|
||||
'otlp_password_set': bool(settings['otlp_password'])}
|
||||
|
||||
|
||||
def metrics_settings(method, body):
|
||||
"""Answer a /metrics/settings request: the HTTP status and the JSON document.
|
||||
|
||||
GET /metrics/settings {"settings"}: the metrics section without otlp_password, with otlp_password_set
|
||||
PUT /metrics/settings change any of the settings of the metrics section; an empty or absent otlp_password keeps
|
||||
the current secret, and an empty otlp_username removes it
|
||||
|
||||
A change is 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'}
|
||||
if method in ('GET', 'HEAD'):
|
||||
try:
|
||||
return 200, {'settings': metrics_shown(vpnrouter.load_config()['metrics'])}
|
||||
except SystemExit as exc:
|
||||
return 500, {'error': str(exc.code)}
|
||||
if method != 'PUT':
|
||||
return 404, {'error': f'no route {method} /metrics/settings'}
|
||||
body = dict(body or {})
|
||||
if not body.get('otlp_password'):
|
||||
body.pop('otlp_password', None)
|
||||
if body.get('otlp_username') == '':
|
||||
body['otlp_password'] = ''
|
||||
changed = section_change('metrics', body)
|
||||
return changed if isinstance(changed, tuple) else (200, {'settings': metrics_shown(changed)})
|
||||
|
||||
|
||||
def changes_admins(connection, method, parts, body):
|
||||
"""Whether a /users, /service-accounts or /groups request creates, renames to, changes or removes ADMIN_GROUP, or
|
||||
removes a member of it."""
|
||||
@@ -1503,15 +1562,134 @@ def take_ticket(name, ticket):
|
||||
return holder == name and expires > time.monotonic()
|
||||
|
||||
|
||||
def shell_command(account, unit):
|
||||
"""The command that runs the login shell of the system user account in the transient unit, on its terminal.
|
||||
def shell_command(account, unit, session):
|
||||
"""The command that runs the login shell of the system user account in the transient unit, on its terminal, with
|
||||
the shell's session in VPN_ROUTER_SHELL for vpn-router-editor.
|
||||
|
||||
setsid makes the terminal the controlling one of systemd-run, which passes the window size on; systemd-run starts
|
||||
the shell outside the web service's sandbox, in a PAM login session of the user.
|
||||
"""
|
||||
return ['setsid', '--ctty', 'systemd-run', '--quiet', '--collect', '--pty', '--send-sighup', f'--unit={unit}',
|
||||
f'--uid={account.pw_name}', '--property=PAMName=login', f'--working-directory={account.pw_dir}',
|
||||
'--setenv=TERM=xterm-256color', account.pw_shell or '/bin/sh', '-l']
|
||||
'--setenv=TERM=xterm-256color', f'--setenv=VPN_ROUTER_SHELL={session}', account.pw_shell or '/bin/sh', '-l']
|
||||
|
||||
|
||||
def editor_file(account, action, path, content=b'', version=''):
|
||||
"""Run editor-file as the system user account outside the sandbox: read the file, or write content to it when it
|
||||
still has version. The exit status of editor-file and its output, or its message on failure."""
|
||||
command = ['systemd-run', '--quiet', '--collect', '--pipe', f'--uid={account}', EDITOR_FILE, action, path]
|
||||
try:
|
||||
result = subprocess.run(command + ([version] if action == 'write' else []), input=content, capture_output=True,
|
||||
timeout=EDITOR_TIMEOUT)
|
||||
except (OSError, subprocess.TimeoutExpired) as exc:
|
||||
return 1, f'editor-file cannot be run: {exc}'.encode()
|
||||
return result.returncode, result.stdout if result.returncode == 0 else result.stderr.strip() or \
|
||||
f'editor-file ended with status {result.returncode}'.encode()
|
||||
|
||||
|
||||
def shell_editor(name, method, parts, body):
|
||||
"""Answer a request of a file open in the editor of one of the user's shells: the HTTP status and the JSON document.
|
||||
|
||||
GET /shell/editor/<session>/<id> {"path", "content", "version"}, version the SHA-256 of the content
|
||||
PUT /shell/editor/<session>/<id> {"content", "version"} writes the content when the file still has version,
|
||||
else 409; answers {"path", "version"}
|
||||
DELETE /shell/editor/<session>/<id> closes the file
|
||||
|
||||
The file is read and written as the shell's console account (editor_file). 404 for a session that is not a shell of
|
||||
the user or a file it does not have open.
|
||||
"""
|
||||
if len(parts) != 4:
|
||||
return 404, {'error': f'no route {method} /{"/".join(parts)}'}
|
||||
with _shell_lock:
|
||||
session = _shell_sessions.get(parts[2])
|
||||
path = session['files'].get(parts[3]) if session and session['user'] == name else None
|
||||
account = session['account'] if path else None
|
||||
if path is None:
|
||||
return 404, {'error': 'no such file is open in a shell of yours'}
|
||||
if method == 'DELETE':
|
||||
with _shell_lock:
|
||||
session['files'].pop(parts[3], None)
|
||||
return 200, {'path': path, 'closed': True}
|
||||
if method == 'GET':
|
||||
status, output = editor_file(account, 'read', path)
|
||||
if status != 0:
|
||||
return (400 if status == 2 else 500), {'error': output.decode(errors='replace')}
|
||||
return 200, {'path': path, 'content': output.decode(), 'version': hashlib.sha256(output).hexdigest()}
|
||||
if method != 'PUT':
|
||||
return 405, {'error': f'{method} is not allowed on a file of the editor'}
|
||||
if not isinstance(body, dict) or not isinstance(body.get('content'), str) or not isinstance(body.get('version'), str):
|
||||
return 400, {'error': 'the body must be {"content", "version"}'}
|
||||
content = body['content'].encode()
|
||||
if len(content) > EDITOR_MAX_BYTES:
|
||||
return 413, {'error': f'the content is larger than {EDITOR_MAX_BYTES // 1024} KiB'}
|
||||
status, output = editor_file(account, 'write', path, content, body['version'])
|
||||
if status != 0:
|
||||
return {2: 400, 3: 409}.get(status, 500), {'error': output.decode(errors='replace')}
|
||||
log(f'editor of {name} saved {path} as {account}')
|
||||
return 200, {'path': path, 'version': hashlib.sha256(content).hexdigest()}
|
||||
|
||||
|
||||
def editor_request(connection):
|
||||
"""Answer one request of vpn-router-editor on the editor socket: {"session", "path"} on a line opens the file in the
|
||||
editor of the shell of the session, whose client is sent {"editor": "open", "session", "id", "path"} as a text
|
||||
message.
|
||||
|
||||
The peer must run as the shell's console account and the path must be absolute. The answer is {"ok": true} or
|
||||
{"error"} on a line.
|
||||
"""
|
||||
try:
|
||||
connection.settimeout(EDITOR_TIMEOUT)
|
||||
_, uid, _ = struct.unpack('iII', connection.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED,
|
||||
struct.calcsize('iII')))
|
||||
request = json.loads(connection.makefile('rb').readline(4096) or b'null')
|
||||
if not isinstance(request, dict) or not isinstance(request.get('session'), str) or \
|
||||
not isinstance(request.get('path'), str):
|
||||
answer = {'error': 'the request must be {"session", "path"}'}
|
||||
elif not os.path.isabs(request['path']):
|
||||
answer = {'error': 'the path must be absolute'}
|
||||
else:
|
||||
with _shell_lock:
|
||||
session = _shell_sessions.get(request['session'])
|
||||
if session is None or session['uid'] != uid:
|
||||
answer = {'error': 'no shell of the Router Manager has this session; vre works only in its shell'}
|
||||
elif len(session['files']) >= EDITOR_FILES_MAX:
|
||||
answer = {'error': f'the editor has {EDITOR_FILES_MAX} files open; close one first'}
|
||||
else:
|
||||
file_id = secrets.token_urlsafe(8)
|
||||
session['files'][file_id] = request['path']
|
||||
answer = None
|
||||
if answer is None:
|
||||
session['send'](TEXT, json.dumps({'editor': 'open', 'session': request['session'], 'id': file_id,
|
||||
'path': request['path']}).encode())
|
||||
log(f'editor of {session["user"]} opened {request["path"]}')
|
||||
answer = {'ok': True}
|
||||
connection.sendall(json.dumps(answer).encode() + b'\n')
|
||||
except (OSError, ValueError, struct.error):
|
||||
pass
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
|
||||
def editor_listen():
|
||||
"""Listen on EDITOR_SOCKET, which any local user may connect to, answering each connection in a thread with
|
||||
editor_request; the socket of an earlier run is replaced. Does not return; a failure to listen is logged and the
|
||||
editor stays closed."""
|
||||
try:
|
||||
if os.path.lexists(EDITOR_SOCKET):
|
||||
os.unlink(EDITOR_SOCKET)
|
||||
listener = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
listener.bind(EDITOR_SOCKET)
|
||||
os.chmod(EDITOR_SOCKET, 0o666)
|
||||
listener.listen(16)
|
||||
except OSError as exc:
|
||||
log(f'the editor socket {EDITOR_SOCKET} cannot be opened: {exc}')
|
||||
return
|
||||
while True:
|
||||
try:
|
||||
connection, _ = listener.accept()
|
||||
except OSError:
|
||||
continue
|
||||
threading.Thread(target=editor_request, args=(connection,), name='editor request', daemon=True).start()
|
||||
|
||||
|
||||
def frame(opcode, payload):
|
||||
@@ -1676,8 +1854,10 @@ class Handler(BaseHTTPRequestHandler):
|
||||
|
||||
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
|
||||
terminal's output. The shell ends after web.shell_idle_minutes without a keystroke, when it exits, when the
|
||||
client closes the connection, or when a check of the user (SHELL_CHECK_SECONDS) finds its console login off or
|
||||
terminal's output; a text message of the server {"editor": "open", "session", "id", "path"} opens a file the
|
||||
shell named with vpn-router-editor (editor_request), which the client reads and saves with
|
||||
/shell/editor/<session>/<id> (shell_editor) while the shell lives. The shell ends after web.shell_idle_minutes
|
||||
without a keystroke, when it exits, when the client closes the connection, or when a check of the user (SHELL_CHECK_SECONDS) finds its console login off or
|
||||
its TOTP token gone; the server then closes it with the reason. Each opening, end and refusal is logged
|
||||
with its reason.
|
||||
"""
|
||||
@@ -1714,9 +1894,10 @@ class Handler(BaseHTTPRequestHandler):
|
||||
except KeyError:
|
||||
return refuse(500, f'the system does not know the account {login}; is nslcd running?')
|
||||
unit = f'vpn-router-shell-{secrets.token_hex(4)}'
|
||||
session = secrets.token_urlsafe(24)
|
||||
master, terminal = os.openpty()
|
||||
try:
|
||||
process = subprocess.Popen(shell_command(account, unit), stdin=terminal, stdout=terminal, stderr=terminal,
|
||||
process = subprocess.Popen(shell_command(account, unit, session), stdin=terminal, stdout=terminal, stderr=terminal,
|
||||
env={'PATH': '/usr/sbin:/usr/bin:/sbin:/bin'})
|
||||
except OSError as exc:
|
||||
os.close(master)
|
||||
@@ -1739,6 +1920,10 @@ class Handler(BaseHTTPRequestHandler):
|
||||
with lock:
|
||||
self.wfile.write(frame(opcode, payload))
|
||||
|
||||
with _shell_lock:
|
||||
_shell_sessions[session] = {'user': name, 'account': account.pw_name, 'uid': account.pw_uid, 'send': send,
|
||||
'files': {}}
|
||||
|
||||
def receive():
|
||||
"""Pass the client's keystrokes and window sizes to the terminal until it closes the connection."""
|
||||
try:
|
||||
@@ -1791,6 +1976,8 @@ class Handler(BaseHTTPRequestHandler):
|
||||
reason = 'the client closed the connection'
|
||||
except OSError:
|
||||
reason = 'the connection failed'
|
||||
with _shell_lock:
|
||||
_shell_sessions.pop(session, None)
|
||||
run('systemctl', 'stop', unit)
|
||||
try:
|
||||
send(CLOSE, (1000).to_bytes(2, 'big') + reason.encode()[:120])
|
||||
@@ -1819,7 +2006,8 @@ class Handler(BaseHTTPRequestHandler):
|
||||
directory = next((name for name in ('users', 'service-accounts', 'groups', 'account', 'site-to-site', 'backup',
|
||||
'ca', 'shell', 'remote-access', 'logs', 'sites', 'dns')
|
||||
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', '/metrics/settings', '/postgresql') or \
|
||||
directory is not None
|
||||
signing = path in ('/auth', '/auth/passkey', '/auth/passkey/complete', '/auth/refresh', '/auth/logout')
|
||||
oidc = (path == '/.well-known/openid-configuration' or path.startswith('/oidc/')) and \
|
||||
self.server.server_address[0] == vpnrouter_net.LOOPBACK
|
||||
@@ -1828,8 +2016,9 @@ class Handler(BaseHTTPRequestHandler):
|
||||
state = current() if protected or signing else {}
|
||||
claims = self.claims(state['key']) if protected and state['key'] else None
|
||||
# The body of a file upload is read as a stream, after the request is allowed.
|
||||
payload = self.request_body() if (protected or signing) and self.command not in ('GET', 'HEAD') and \
|
||||
not (files and self.command == 'PUT') else None
|
||||
payload = self.request_body(EDITOR_BODY_LIMIT if path.startswith('/shell/editor/') else BODY_LIMIT) \
|
||||
if (protected or signing) and self.command not in ('GET', 'HEAD') and not (files and self.command == 'PUT') \
|
||||
else None
|
||||
if claims is not None:
|
||||
self.principal = 'admin' if claims.get('admin') is True else claims.get('sub')
|
||||
if (protected or signing) and not state['key']:
|
||||
@@ -1861,8 +2050,14 @@ class Handler(BaseHTTPRequestHandler):
|
||||
query = urllib.parse.parse_qs(urllib.parse.urlsplit(self.path).query)
|
||||
code, document = metrics_history(query.get('range', ['hour'])[0])
|
||||
body, kind = json.dumps(document).encode() + b'\n', 'application/json'
|
||||
elif path == '/metrics/settings':
|
||||
code, document = metrics_settings(self.command, payload)
|
||||
body, kind = json.dumps(document, indent=2).encode() + b'\n', 'application/json'
|
||||
elif directory == 'shell' and (path, self.command) == ('/shell', 'GET'):
|
||||
return self.shell(claims.get('sub'))
|
||||
elif directory == 'shell' and path.startswith('/shell/editor/'):
|
||||
code, document = shell_editor(claims.get('sub'), self.command, path.strip('/').split('/'), payload)
|
||||
body, kind = json.dumps(document).encode() + b'\n', 'application/json'
|
||||
elif directory == 'shell':
|
||||
code, document = shell_ticket(claims.get('sub'), payload) if (path, self.command) == ('/shell', 'POST') \
|
||||
else (404, {'error': f'no route {self.command} {path}'})
|
||||
@@ -1949,14 +2144,15 @@ class Handler(BaseHTTPRequestHandler):
|
||||
return b''
|
||||
return self.rfile.read(size) if 0 < size <= BODY_LIMIT else b''
|
||||
|
||||
def request_body(self):
|
||||
"""The request body as JSON, None when there is none, or the string 'invalid'."""
|
||||
def request_body(self, limit=BODY_LIMIT):
|
||||
"""The request body as JSON, None when there is none, or the string 'invalid', also when it is larger than
|
||||
limit bytes."""
|
||||
length = self.headers.get('Content-Length')
|
||||
if not length:
|
||||
return None
|
||||
try:
|
||||
size = int(length)
|
||||
if not 0 <= size <= BODY_LIMIT:
|
||||
if not 0 <= size <= limit:
|
||||
return 'invalid'
|
||||
return json.loads(self.rfile.read(size) or b'null')
|
||||
except ValueError:
|
||||
@@ -2022,6 +2218,7 @@ def initialize():
|
||||
def main():
|
||||
signal.signal(signal.SIGTERM, lambda *_: sys.exit(0))
|
||||
initialize()
|
||||
threading.Thread(target=editor_listen, name='editor socket', daemon=True).start()
|
||||
try:
|
||||
vpnrouter_serve.serve(lambda: vpnrouter_net.web_listen(read_json(CONFIG_FILE), DEFAULT_PORT), Handler, log,
|
||||
DISCOVERY_SECONDS)
|
||||
|
||||
@@ -45,7 +45,7 @@ variable "vm_size" {
|
||||
}
|
||||
|
||||
variable "local_fqdn" {
|
||||
description = "FQDN of the router, used as the road-warrior/IKE identity and, when dns_zone_id is set, as the name of the A record created for it."
|
||||
description = "FQDN of the router, used as the IKE identity and, when dns_zone_id is set, as the name of the A record created for it."
|
||||
type = string
|
||||
}
|
||||
|
||||
@@ -89,13 +89,13 @@ variable "psk" {
|
||||
}
|
||||
|
||||
variable "p2s_enabled" {
|
||||
description = "Enable road-warrior (P2S) access."
|
||||
description = "Enable IKEv2 access."
|
||||
type = bool
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "p2s_address_pool" {
|
||||
description = "CIDR block assigned to road-warrior clients."
|
||||
description = "CIDR block assigned to IKEv2 clients."
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ variable "machine_type" {
|
||||
}
|
||||
|
||||
variable "local_fqdn" {
|
||||
description = "FQDN of the router, used as the road-warrior/IKE identity and, when dns_managed_zone is set, as the name of the A record created for it."
|
||||
description = "FQDN of the router, used as the IKE identity and, when dns_managed_zone is set, as the name of the A record created for it."
|
||||
type = string
|
||||
}
|
||||
|
||||
@@ -86,13 +86,13 @@ variable "psk" {
|
||||
}
|
||||
|
||||
variable "p2s_enabled" {
|
||||
description = "Enable road-warrior (P2S) access."
|
||||
description = "Enable IKEv2 access."
|
||||
type = bool
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "p2s_address_pool" {
|
||||
description = "CIDR block assigned to road-warrior clients."
|
||||
description = "CIDR block assigned to IKEv2 clients."
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user