Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5303779559 | ||
|
|
2dc4db440f | ||
|
|
45cea95728 | ||
|
|
dd2dfaaf2b | ||
|
|
1697e1aa38 | ||
|
|
57e8bbcffe | ||
|
|
9ca8fce14e | ||
|
|
74c24d7bc6 | ||
|
|
770332728e |
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
|
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,
|
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
|
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
|
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.
|
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
|
`/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
|
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
|
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
|
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
|
in the same process either, as psycopg caches the lookup. The services and `vpn-router-setup`
|
||||||
plainly, and their units wait instead: `ExecStartPre=wait-for-libpq`, a shell script of each
|
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
|
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),
|
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`.
|
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
|
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.
|
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.
|
is on disk only until setup has run.
|
||||||
|
|
||||||
## ADR-19: The package has an LDAP schema of its own under a UUID OID
|
## 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 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`
|
`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,
|
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
|
and used by HTTPS, IKEv2 and OpenVPN. `local.cidrs` stays the subnets the router
|
||||||
routes, masquerades and offers to road-warrior and OpenVPN clients.
|
routes, masquerades and offers to IKEv2 and OpenVPN clients.
|
||||||
- **One file:** `vpn-router-setup` writes every connection that is ready to
|
- **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
|
`/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
|
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
|
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
|
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`;
|
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,
|
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
|
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.
|
keeps the users' keys, as the OpenVPN profile embeds them.
|
||||||
@@ -1141,8 +1141,9 @@ later. S3 and Azure Blob clients are not served.
|
|||||||
|
|
||||||
Status: Accepted
|
Status: Accepted
|
||||||
|
|
||||||
Context: Sites get subdomains of a selected domain, delegated to the router, without a registrar
|
Context: The router hosts authoritative domains as their primary, with optional secondary name
|
||||||
change per site. Ubuntu has BIND, Knot and PowerDNS; only PowerDNS's gpgsql backend reads records
|
servers, and a domain can give the sites their records without a registrar change per site. Ubuntu
|
||||||
|
has BIND, Knot and PowerDNS; only PowerDNS's gpgsql backend reads records
|
||||||
from PostgreSQL, so changes are rows, without zone files or reloads. systemd-resolved's stub holds
|
from PostgreSQL, so changes are rows, without zone files or reloads. systemd-resolved's stub holds
|
||||||
127.0.0.53 and 127.0.0.54, and the remote access clients' resolver answered on the external
|
127.0.0.53 and 127.0.0.54, and the remote access clients' resolver answered on the external
|
||||||
addresses, the addresses an authoritative server needs. The clients reach the internal address
|
addresses, the addresses an authoritative server needs. The clients reach the internal address
|
||||||
@@ -1151,15 +1152,25 @@ just as well, through the tunnel when it is in `local.cidrs` (ADR-43).
|
|||||||
Decision: While `dns.enabled` is true, setup runs `pdns.service` with
|
Decision: While `dns.enabled` is true, setup runs `pdns.service` with
|
||||||
`/etc/powerdns/pdns.d/vpn-router.conf`, written before the packages are installed, on the
|
`/etc/powerdns/pdns.d/vpn-router.conf`, written before the packages are installed, on the
|
||||||
addresses of `dns.listen`, with the gpgsql backend on the database `vpn-router-dns` and PowerDNS's
|
addresses of `dns.listen`, with the gpgsql backend on the database `vpn-router-dns` and PowerDNS's
|
||||||
own schema. It writes one zone, `dns.zone`, through the procedure `dns_apply` of
|
own schema, and `primary=yes`. The domains, their secondaries and manual records are rows of the
|
||||||
`/usr/share/vpn-router/sql/dns.sql`: SOA, NS, A and AAAA records of `dns.addresses` for the apex,
|
router's tables in that database, like the sites in `vpn-router-sites`, not settings of
|
||||||
the name server and every site host name in the zone, and manual records administrators add; the
|
`vpn-router.json`: they are many and change through the web service. `dns.nameserver` and
|
||||||
serial grows only when the records change. The remote access clients' resolver answers on the
|
`dns.addresses` stay settings, the router's one identity as the primary NS of every domain. Setup
|
||||||
addresses of the new setting `p2s.dns_listen`, by default the internal one; a `dns.listen` that
|
writes each domain as a MASTER zone through the procedure `dns_apply` of
|
||||||
shares an interface with it is refused while P2S or OpenVPN is enabled.
|
`/usr/share/vpn-router/sql/dns.sql`: SOA, NS of the router and the secondaries, A and AAAA records
|
||||||
|
of `dns.addresses` for the apex, the name server and, with the domain's site records switch, every
|
||||||
|
site host name whose closest domain it is, and the manual records; the serial grows only when the
|
||||||
|
records change, which makes PowerDNS notify. A secondary is a name, its NS, and an address, which
|
||||||
|
goes to the domain's `ALSO-NOTIFY` and `ALLOW-AXFR-FROM` metadata, so the global
|
||||||
|
`allow-axfr-ips` stays the loopback default. A host name belongs to its closest domain only, and a
|
||||||
|
domain inside another is delegated from it with NS records, so a name is never answered by two
|
||||||
|
zones. The remote access clients' resolver answers on the addresses of the setting
|
||||||
|
`p2s.dns_listen`, by default the internal one; a `dns.listen` that shares an interface with it is
|
||||||
|
refused while P2S or OpenVPN is enabled.
|
||||||
|
|
||||||
Consequences: The zone follows the sites at each setup run. One zone only, without DNSSEC or
|
Consequences: The domains follow the sites at each setup run. Transfers are authorized by address,
|
||||||
transfers; the delegation is made at the parent zone by hand.
|
without TSIG, and there is no DNSSEC; the delegation of each domain is made at its parent zone by
|
||||||
|
hand.
|
||||||
|
|
||||||
## ADR-43: The remote access clients' resolver is dnsmasq, not systemd-resolved
|
## ADR-43: The remote access clients' resolver is dnsmasq, not systemd-resolved
|
||||||
|
|
||||||
@@ -1176,8 +1187,46 @@ Decision: `vpn-router-resolver.service` runs dnsmasq, bound to the addresses of
|
|||||||
only (`bind-interfaces`), forwarding to resolved's stub on 127.0.0.53, without `/etc/hosts` and
|
only (`bind-interfaces`), forwarding to resolved's stub on 127.0.0.53, without `/etc/hosts` and
|
||||||
without forwarding names that have no dot (`domain-needed`). resolved keeps its loopback stubs
|
without forwarding names that have no dot (`domain-needed`). resolved keeps its loopback stubs
|
||||||
only and stays the router's own resolver, with the upstream servers of the platform. Setup writes
|
only and stays the router's own resolver, with the upstream servers of the platform. Setup writes
|
||||||
`/etc/vpn-router/dnsmasq.conf` while IKEv2 or OpenVPN runs and removes the earlier drop-in.
|
`/etc/vpn-router/dnsmasq.conf` while IKEv2, OpenVPN or WireGuard runs, opens port 53 there for
|
||||||
|
their client networks, and removes the earlier drop-in.
|
||||||
|
|
||||||
Consequences: Clients resolve the names resolved resolves, Azure's internal names included, but
|
Consequences: Clients resolve the names resolved resolves, Azure's internal names included, but
|
||||||
not the router's synthesized ones. The package depends on dnsmasq-base. PowerDNS and the clients'
|
not the router's synthesized ones. The package depends on dnsmasq-base. PowerDNS and the clients'
|
||||||
resolver share no address.
|
resolver share no address.
|
||||||
|
|
||||||
|
## ADR-44: Apple VPN profiles are unsigned IKEv2 profiles with a PKCS#12 encrypted by a password given per download
|
||||||
|
|
||||||
|
Status: Accepted
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Decision: The web service builds the profile with `plistlib` and python3-cryptography: the P2S CA
|
||||||
|
and the root, the user's PKCS#12 encrypted with a password the caller gives with each request,
|
||||||
|
and an IKEv2 payload matching `road-warrior.conf` (EAP-TLS with the certificate, AES-256,
|
||||||
|
SHA2-256, group 14). The profile is not signed. Admins get any user's profile through `/users`,
|
||||||
|
users their own through `/account` with their current password and code.
|
||||||
|
|
||||||
|
Consequences: The password is never stored; the device asks for it when the profile is installed.
|
||||||
|
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: IKEv2 clients get split DNS domains, p2s.dns_domains
|
||||||
|
|
||||||
|
Status: Accepted
|
||||||
|
|
||||||
|
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
|
||||||
|
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.
|
||||||
@@ -3,11 +3,11 @@
|
|||||||
A Debian package that configures a Linux host or virtual machine as a VPN router:
|
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)
|
- 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
|
- 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
|
clients, for site-to-site and for general use, with certificate templates, published certificates
|
||||||
and CRLs, and an optional ACME server
|
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
|
LDAP directory and managed with `vpn-router-user` or the web service
|
||||||
- optional WireGuard endpoint
|
- optional WireGuard endpoint
|
||||||
- routing and NAT for a protected subnet, TCP MSS clamping, and UFW firewall rules
|
- routing and NAT for a protected subnet, TCP MSS clamping, and UFW firewall rules
|
||||||
@@ -15,7 +15,7 @@ A Debian package that configures a Linux host or virtual machine as a VPN router
|
|||||||
- metrics of the router kept in PostgreSQL, served for an OpenTelemetry Collector and pushed
|
- metrics of the router kept in PostgreSQL, served for an OpenTelemetry Collector and pushed
|
||||||
over OTLP
|
over OTLP
|
||||||
- static web sites served by Caddy, uploaded through the web service or WebDAV, and an optional
|
- static web sites served by Caddy, uploaded through the web service or WebDAV, and an optional
|
||||||
authoritative name server (PowerDNS) for a zone of the sites
|
primary name server (PowerDNS) of the router's domains, with secondaries and records for the sites
|
||||||
|
|
||||||
Nothing in the package is specific to a cloud provider. It is built on the external and
|
Nothing in the package is specific to a cloud provider. It is built on the external and
|
||||||
internal NIC names, and a `mode` setting says how much of the rest you are supplying and
|
internal NIC names, and a `mode` setting says how much of the rest you are supplying and
|
||||||
@@ -108,14 +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` |
|
| `general` | `mode` | string | `manual` | How much you supply: `manual`, `interfaces` or `auto` |
|
||||||
| `interfaces` | `external` | string | | Name of the NIC facing the untrusted network |
|
| `interfaces` | `external` | string | | Name of the NIC facing the untrusted network |
|
||||||
| `interfaces` | `internal` | string | | Name of the NIC facing the protected 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 |
|
| `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 road-warrior and OpenVPN clients |
|
| `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_addr` | string | | This host's address on the internal network |
|
||||||
| `local` | `int_gateway_ip` | string | | Next hop on the internal side |
|
| `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 |
|
| `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` | `enabled` | boolean | `false` | IKEv2 access |
|
||||||
| `p2s` | `address_pool` | string | | CIDR assigned to road-warrior clients |
|
| `p2s` | `address_pool` | string | | CIDR assigned to IKEv2 clients |
|
||||||
| `p2s` | `dns_listen` | string | `internal` | The addresses of the resolver the road-warrior and OpenVPN clients are given: `internal`, `external` or `both` |
|
| `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` | `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` | `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 |
|
||||||
@@ -146,10 +147,9 @@ must have the type shown below; an unknown section or setting is an error.
|
|||||||
| `sites` | `upload_max_mb` | integer | `512` | The largest upload of a site's file or zip archive, in MiB, 1 to 10240 |
|
| `sites` | `upload_max_mb` | integer | `512` | The largest upload of a site's file or zip archive, in MiB, 1 to 10240 |
|
||||||
| `sites` | `webdav` | boolean | `false` | WebDAV of the sites' folders, which needs `https.enabled` and `wan.local_fqdn` (see WebDAV) |
|
| `sites` | `webdav` | boolean | `false` | WebDAV of the sites' folders, which needs `https.enabled` and `wan.local_fqdn` (see WebDAV) |
|
||||||
| `sites` | `webdav_port` | integer | `7445` | HTTPS port of WebDAV |
|
| `sites` | `webdav_port` | integer | `7445` | HTTPS port of WebDAV |
|
||||||
| `dns` | `enabled` | boolean | `false` | The name server of `dns.zone`, which needs `dns.zone`, `dns.nameserver` and `dns.addresses` (see Name server) |
|
| `dns` | `enabled` | boolean | `false` | The primary name server of the router's domains, which needs `dns.nameserver` and `dns.addresses` (see Name server) |
|
||||||
| `dns` | `zone` | string | | The zone the name server answers for |
|
| `dns` | `nameserver` | string | | The name server's host name, the primary NS of every domain |
|
||||||
| `dns` | `nameserver` | string | | The name server's host name |
|
| `dns` | `addresses` | list | | The public addresses of the name server, the domains' apexes and the sites' host names |
|
||||||
| `dns` | `addresses` | list | | The public addresses of the zone's apex, the name server and the sites' host names |
|
|
||||||
| `dns` | `listen` | string | `external` | The interfaces the name server answers on: `internal`, `external` or `both` |
|
| `dns` | `listen` | string | `external` | The interfaces the name server answers on: `internal`, `external` or `both` |
|
||||||
| `dns` | `ttl` | integer | `300` | TTL of the records, 60 to 86400 seconds |
|
| `dns` | `ttl` | integer | `300` | TTL of the records, 60 to 86400 seconds |
|
||||||
| `pki` | `root_name` | string | `VPN Router Root CA` | Common name of the root CA, when the router creates it |
|
| `pki` | `root_name` | string | `VPN Router Root CA` | Common name of the root CA, when the router creates it |
|
||||||
@@ -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
|
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;
|
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
|
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
|
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
|
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,
|
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.
|
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
|
Otherwise it regenerates the configuration files, resolves the interfaces and their
|
||||||
addresses as `mode` directs, applies routes, generates WireGuard keys and injects
|
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
|
access is configured. It then starts `strongswan`, `wg-quick@wg0` and
|
||||||
`openvpn-server@vpn-router` if the corresponding feature is configured, reloading or
|
`openvpn-server@vpn-router` if the corresponding feature is configured, reloading or
|
||||||
restarting them only when their inputs changed, and
|
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):
|
(path length 0):
|
||||||
|
|
||||||
- `p2s`, named `pki.p2s_name`, signs the router's VPN server certificate, which IKEv2 and OpenVPN
|
- `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.
|
this CA only.
|
||||||
- `s2s`, named `pki.s2s_name`, signs the router's site-to-site certificate and the certificates
|
- `s2s`, named `pki.s2s_name`, signs the router's site-to-site certificate and the certificates
|
||||||
of its peers.
|
of its peers.
|
||||||
@@ -652,7 +652,7 @@ certificate names:
|
|||||||
### The user directory
|
### The user directory
|
||||||
|
|
||||||
Once a platform is chosen, `vpn-router-setup` keeps a directory of users in the local slapd,
|
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:
|
is switched on:
|
||||||
|
|
||||||
- its own database, `dc=vpn-router,dc=local` in `/var/lib/ldap/vpn-router`, next to the one the
|
- its own database, `dc=vpn-router,dc=local` in `/var/lib/ldap/vpn-router`, next to the one the
|
||||||
@@ -773,9 +773,29 @@ 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
|
Manager-only account, adds it to the group `admins`, which it creates when missing, and removes
|
||||||
the file.
|
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.
|
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 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 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.
|
||||||
|
|
||||||
|
The password is 8 to 128 characters. A user asking for the own profile also gives the current
|
||||||
|
password and, when enrolled, a code.
|
||||||
|
|
||||||
### Console login and SSH
|
### Console login and SSH
|
||||||
|
|
||||||
A member of `admins` enrolled for TOTP may have console login: the Router Manager's Shell page
|
A member of `admins` enrolled for TOTP may have console login: the Router Manager's Shell page
|
||||||
@@ -872,10 +892,11 @@ deploying.
|
|||||||
which is how a deferred install is expressed. `generic` is the module to choose when the
|
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.
|
router should be configured but the platform adds nothing.
|
||||||
|
|
||||||
The shipped modules currently add nothing. Road-warrior and OpenVPN 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,
|
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
|
where `vpn-router-resolver.service`, dnsmasq with `/etc/vpn-router/dnsmasq.conf`, answers and
|
||||||
the firewall opens port 53 for the client pools, so no provider resolver has to be advertised or
|
the firewall opens port 53 for the client pools and the WireGuard network, so no provider resolver has to be advertised or
|
||||||
routed through the tunnel on any platform. dnsmasq forwards to `systemd-resolved` on
|
routed through the tunnel on any platform. dnsmasq forwards to `systemd-resolved` on
|
||||||
127.0.0.53, which keeps only its loopback stubs, and forwards no name without a dot, so the
|
127.0.0.53, which keeps only its loopback stubs, and forwards no name without a dot, so the
|
||||||
router's own host name, `_gateway` and `localhost` are not answered to the clients. The clients reach
|
router's own host name, `_gateway` and `localhost` are not answered to the clients. The clients reach
|
||||||
@@ -887,7 +908,7 @@ A module may define any of the following, and takes the default for anything it
|
|||||||
|
|
||||||
| Name | Type | Effect |
|
| 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 |
|
| `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 |
|
| `apply(ctx)` | function | called once the system is configured, to apply platform-specific state; must be idempotent |
|
||||||
|
|
||||||
@@ -920,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 |
|
| `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 |
|
| `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
|
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
|
request from one of `web.trusted_proxies`, by default the address the request came in on, so
|
||||||
@@ -955,7 +976,7 @@ curl http://10.0.0.4:17081/
|
|||||||
| `/status` | The same status as JSON |
|
| `/status` | The same status as JSON |
|
||||||
| `/auth` | Signing in with a user's password and TOTP code, refreshing and signing out |
|
| `/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 |
|
| `/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 |
|
| `/service-accounts` | The service accounts; needs a token |
|
||||||
| `/groups` | The groups of users; 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 |
|
| `/site-to-site` | The site-to-site connections: list, add, change and remove; needs a token |
|
||||||
@@ -968,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
|
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
|
(`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
|
`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-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,
|
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
|
||||||
connections that are set up (`connections`) and of those established (`established`, `null` when
|
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
|
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.
|
all these services are active, and `degraded` otherwise.
|
||||||
|
|
||||||
@@ -986,7 +1007,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`, `/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).
|
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
|
||||||
@@ -1155,10 +1176,15 @@ curl -X PUT -H "Authorization: Bearer $TOKEN" --data-binary @site.zip \
|
|||||||
|
|
||||||
| Request | Answer |
|
| Request | Answer |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `GET /dns` | `{"settings", "installed", "running", "records", "manual", "problem"}`: the `dns` section, whether PowerDNS is installed and runs, the records it serves, the manual records, and why they cannot be read, `null` when they can |
|
| `GET /dns` | `{"settings", "installed", "running", "domains", "problem"}`: the `dns` section, whether PowerDNS is installed and runs, the domains, each `{"name", "sites", "ttl", "secondaries", "manual", "records"}` with its secondaries `{"nameserver", "address"}`, its manual records and the records it serves, and why they cannot be read, `null` when they can |
|
||||||
| `PUT /dns` | Changes any of the settings of the `dns` section |
|
| `PUT /dns` | Changes any of the settings of the `dns` section |
|
||||||
| `POST /dns/records` | Adds a manual record: `{"name", "type", "content", "ttl"?}`; `name` is relative to `dns.zone`, `@` for the zone itself, or absolute with a final dot, and `ttl` is `dns.ttl` without it |
|
| `POST /dns/domains` | Adds a domain: `{"name", "sites"?, "ttl"?}`; an existing name is 409 |
|
||||||
| `DELETE /dns/records?name=&type=&content=` | Removes the manual records of the name and type, only the one of `content` when it is given |
|
| `PUT /dns/domains/<domain>` | Changes `sites` or `ttl` of the domain |
|
||||||
|
| `DELETE /dns/domains/<domain>` | Removes the domain with its secondaries and manual records |
|
||||||
|
| `POST /dns/domains/<domain>/secondaries` | Adds a secondary name server, or changes its address: `{"nameserver", "address"}` |
|
||||||
|
| `DELETE /dns/domains/<domain>/secondaries/<nameserver>` | Removes the secondary name server |
|
||||||
|
| `POST /dns/domains/<domain>/records` | Adds a manual record: `{"name", "type", "content", "ttl"?}`; `name` is relative to the domain, `@` for the domain itself, or absolute with a final dot, and `ttl` is the domain's without it |
|
||||||
|
| `DELETE /dns/domains/<domain>/records?name=&type=&content=` | Removes the manual records of the name and type, only the one of `content` when it is given |
|
||||||
|
|
||||||
A TXT content without quotes is quoted; MX is `<preference> <host>`, SRV `<priority> <weight>
|
A TXT content without quotes is quoted; MX is `<preference> <host>`, SRV `<priority> <weight>
|
||||||
<port> <host>` and CAA `<flags> <tag> "<value>"`. A CNAME must be its name's only record.
|
<port> <host>` and CAA `<flags> <tag> "<value>"`. A CNAME must be its name's only record.
|
||||||
@@ -1180,6 +1206,7 @@ and answer bodies are JSON; a request body is at most 65536 bytes.
|
|||||||
| `POST /users/<name>/unlock` | | the user |
|
| `POST /users/<name>/unlock` | | the user |
|
||||||
| `DELETE /users/<name>` | | `{"name", "removed": true}` |
|
| `DELETE /users/<name>` | | `{"name", "removed": true}` |
|
||||||
| `GET /users/<name>/profile` | | `{"name", "profile"}`, the OpenVPN client profile |
|
| `GET /users/<name>/profile` | | `{"name", "profile"}`, the OpenVPN client 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 |
|
| `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 |
|
| `PUT /users/<name>/otp` | `{"secret", "code"}` | the user, once the code matches the secret |
|
||||||
| `DELETE /users/<name>/otp` | | the user |
|
| `DELETE /users/<name>/otp` | | the user |
|
||||||
@@ -1189,7 +1216,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
|
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
|
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
|
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.
|
secrets cross the internal network in plain HTTP, like the token.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -1236,6 +1263,8 @@ router manages them, their display name and description included. A failure is o
|
|||||||
| `POST /account/passkeys` | `{"current_password", "current_code"}` | `{"id", "options"}`: a pending registration, valid 2 minutes, and the options of `navigator.credentials.create` in their JSON form |
|
| `POST /account/passkeys` | `{"current_password", "current_code"}` | `{"id", "options"}`: a pending registration, valid 2 minutes, and the options of `navigator.credentials.create` in their JSON form |
|
||||||
| `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 |
|
| `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 |
|
| `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 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
|
`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
|
403 and counts as a failed login; slapd accepts each code once, so a code used to sign in is not
|
||||||
@@ -1303,14 +1332,26 @@ 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 /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
|
### 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`.
|
OpenVPN (`openvpn`) and WireGuard (`wireguard`), with the same token as `/users`.
|
||||||
|
|
||||||
| Request | Body | Answer |
|
| 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 |
|
| `GET /remote-access` | | `{"p2s", "openvpn", "wireguard", "running", "local_fqdn", "wireguard_server"}`; `wireguard_server` is `{"public_key", "endpoint", "allowed_ips", "dns"}`: the router's WireGuard public key (`null` before setup made it), `<wan.local_fqdn>:<wireguard.listen_port>` (`null` without a name), the networks a peer reaches through the tunnel, the router's local CIDRs and the WireGuard network, and the addresses of the remote access clients' resolver (`[]` while it does not run) |
|
||||||
| `PUT /remote-access/<system>` | any of the settings of the section `p2s`, `openvpn` or `wireguard` | the section |
|
| `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
|
`running` tells for each system whether setup runs it. IKEv2 and OpenVPN run once enabled with an
|
||||||
@@ -1558,9 +1599,9 @@ vpn-config-edit set --restart sites.webdav true
|
|||||||
### Name server
|
### Name server
|
||||||
|
|
||||||
While `dns.enabled` is `true`, `vpn-router-setup` runs PowerDNS (`pdns.service`) as the
|
While `dns.enabled` is `true`, `vpn-router-setup` runs PowerDNS (`pdns.service`) as the
|
||||||
authoritative name server of one zone, `dns.zone`, on port 53 of the addresses of `dns.listen`
|
primary name server of the router's domains, on port 53 of the addresses of `dns.listen`
|
||||||
(`internal`, `external`, the default, or `both`), which the firewall opens for UDP and TCP. The
|
(`internal`, `external`, the default, or `both`), which the firewall opens for UDP and TCP. The
|
||||||
records are in the database `vpn-router-dns` (password in `/etc/vpn-router/dns-database.secret`),
|
domains and records are in the database `vpn-router-dns` (password in `/etc/vpn-router/dns-database.secret`),
|
||||||
read by PowerDNS's gpgsql backend; setup writes the settings
|
read by PowerDNS's gpgsql backend; setup writes the settings
|
||||||
`/etc/powerdns/pdns.d/vpn-router.conf` (root, group `pdns`, 0640) and installs `pdns-server` and
|
`/etc/powerdns/pdns.d/vpn-router.conf` (root, group `pdns`, 0640) and installs `pdns-server` and
|
||||||
`pdns-backend-pgsql` from the Ubuntu archive with `vpn-router-install@dns.service` when they are
|
`pdns-backend-pgsql` from the Ubuntu archive with `vpn-router-install@dns.service` when they are
|
||||||
@@ -1568,21 +1609,31 @@ missing, without the recommended bind backend.
|
|||||||
|
|
||||||
| Setting | Meaning |
|
| Setting | Meaning |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `dns.zone` | The zone, such as `sites.example.com` |
|
| `dns.nameserver` | The name server's host name, the primary NS and the SOA's primary of every domain |
|
||||||
| `dns.nameserver` | The name server's host name, the zone's NS and the SOA's primary |
|
| `dns.addresses` | The public addresses of the domains' apexes, of the name server in a domain it is inside, and of the sites' host names |
|
||||||
| `dns.addresses` | The public addresses of the zone's apex, of the name server when it is in the zone, and of every host name of an enabled site in the zone |
|
| `dns.ttl` | The TTL of the records and the SOA's negative TTL of a domain without its own, 60 to 86400 seconds, 300 by default |
|
||||||
| `dns.ttl` | The TTL of the records and the SOA's negative TTL, 60 to 86400 seconds, 300 by default |
|
|
||||||
|
|
||||||
Records added with `/dns` or the Router Manager (A, AAAA, CNAME, TXT, MX, SRV, CAA) are served
|
The domains are added with `/dns` or the Router Manager, each with:
|
||||||
too, but not those of a name and type the zone's own records have. Setup writes the zone at each
|
|
||||||
run; the SOA's serial grows only when the records change. While P2S or OpenVPN is enabled,
|
- **Site records**: every host name of an enabled site gets A and AAAA records of `dns.addresses`
|
||||||
dnsmasq answers their clients on port 53 of the addresses of `p2s.dns_listen`, by default the
|
in the closest domain it is in, when that domain has site records;
|
||||||
internal address, so `dns.listen` must not share an interface with it. The parent zone must delegate `dns.zone` to `dns.nameserver`, with glue records for
|
- **TTL**: the TTL of its records, `dns.ttl` when not set;
|
||||||
a name server inside the zone.
|
- **Secondaries**: name servers, each a host name, which becomes an NS of the domain, and the
|
||||||
|
address it transfers the domain from. The router notifies that address of every change and
|
||||||
|
allows it zone transfers (AXFR); a secondary's name inside the domain gets an A or AAAA record of
|
||||||
|
that address;
|
||||||
|
- **Manual records** (A, AAAA, CNAME, TXT, MX, SRV, CAA), served too, but not those of a name and
|
||||||
|
type the domain's own records have.
|
||||||
|
|
||||||
|
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 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.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
vpn-config-edit set dns.zone sites.example.com
|
vpn-config-edit set dns.nameserver ns1.example.com
|
||||||
vpn-config-edit set dns.nameserver ns1.sites.example.com
|
|
||||||
vpn-config-edit set dns.addresses 203.0.113.10
|
vpn-config-edit set dns.addresses 203.0.113.10
|
||||||
vpn-config-edit set --restart dns.enabled true
|
vpn-config-edit set --restart dns.enabled true
|
||||||
```
|
```
|
||||||
@@ -1597,7 +1648,7 @@ takes a sample of the router with `psutil`:
|
|||||||
- the free and the total space of `/`;
|
- the free and the total space of `/`;
|
||||||
- the bytes received and sent per second on the external and the internal interface;
|
- the bytes received and sent per second on the external and the internal interface;
|
||||||
- the 1, 5 and 15 minute load averages;
|
- 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;
|
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
|
- 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
|
packets per second received and sent through its child SAs since the previous sample, and the
|
||||||
|
|||||||
@@ -1,3 +1,103 @@
|
|||||||
|
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.
|
||||||
|
|
||||||
|
* The domains, their secondaries and manual records are kept in the
|
||||||
|
vpn-router-dns database and changed through /dns/domains; dns.zone is
|
||||||
|
removed.
|
||||||
|
* Each domain is served as a primary zone: the router and its
|
||||||
|
secondaries are its NS, and each secondary's address is notified of
|
||||||
|
changes and may transfer the zone.
|
||||||
|
* A domain with site records gives the host names of the enabled sites
|
||||||
|
in it A and AAAA records of dns.addresses; a domain inside another is
|
||||||
|
delegated from it.
|
||||||
|
* Manual records belong to a domain; those of the single zone are
|
||||||
|
dropped.
|
||||||
|
|
||||||
|
-- Slawomir Koszewski <slawek@koszewscy.waw.pl> Tue, 27 Oct 2026 20:00:00 +0100
|
||||||
|
|
||||||
|
vpn-router (0.33.0-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
feature: road-warrior clients resolve the domains of the new setting p2s.dns_domains with the router's resolver.
|
||||||
|
|
||||||
|
* p2s.dns_domains lists up to 16 domains; setup pushes them to IKEv2
|
||||||
|
clients as INTERNAL_DNS_DOMAIN (RFC 8598, attribute 25) of the
|
||||||
|
road-warrior pool.
|
||||||
|
* The Apple VPN profile names them as match domains of the router's
|
||||||
|
resolver, which macOS and iOS use for those domains.
|
||||||
|
|
||||||
|
-- Slawomir Koszewski <slawek@koszewscy.waw.pl> Tue, 27 Oct 2026 18:00:00 +0100
|
||||||
|
|
||||||
|
vpn-router (0.32.1-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
fix: a VPN user downloads the own OpenVPN client profile through /account, with the current password and code.
|
||||||
|
|
||||||
|
* POST /account/profile answers the user's OpenVPN client profile; it
|
||||||
|
needs the current password and, when enrolled, a code.
|
||||||
|
|
||||||
|
-- Slawomir Koszewski <slawek@koszewscy.waw.pl> Tue, 27 Oct 2026 16:00:00 +0100
|
||||||
|
|
||||||
|
vpn-router (0.32.0-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
feature: the web service gives Apple VPN profiles that set up IKEv2 road-warrior access on macOS, iOS and iPadOS.
|
||||||
|
|
||||||
|
* POST /users/<name>/apple-profile and POST /account/apple-profile answer
|
||||||
|
an unsigned .mobileconfig with the P2S CA and the root CA, the user's
|
||||||
|
certificate and key as a PKCS#12 encrypted with the password of the
|
||||||
|
request, and an IKEv2 VPN to wan.local_fqdn with EAP-TLS and the
|
||||||
|
proposals of the road-warrior connection.
|
||||||
|
* A user's own profile needs the current password and, when enrolled, a
|
||||||
|
code.
|
||||||
|
|
||||||
|
-- Slawomir Koszewski <slawek@koszewscy.waw.pl> Tue, 27 Oct 2026 14:00:00 +0100
|
||||||
|
|
||||||
|
vpn-router (0.31.3-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
fix: WireGuard clients may use the router's DNS resolver, which runs while WireGuard does, and /remote-access names its addresses for their configuration.
|
||||||
|
|
||||||
|
* The firewall opens port 53 of the remote access clients' resolver for
|
||||||
|
the WireGuard network, as it does for the IKEv2 and OpenVPN pools.
|
||||||
|
* The resolver runs while WireGuard is the only remote access enabled.
|
||||||
|
* GET /remote-access answers the resolver's addresses as
|
||||||
|
wireguard_server.dns.
|
||||||
|
* dns.listen may not share an interface with p2s.dns_listen while
|
||||||
|
WireGuard is enabled either.
|
||||||
|
|
||||||
|
-- Slawomir Koszewski <slawek@koszewscy.waw.pl> Tue, 27 Oct 2026 12:00:00 +0100
|
||||||
|
|
||||||
vpn-router (0.31.2-1) unstable; urgency=medium
|
vpn-router (0.31.2-1) unstable; urgency=medium
|
||||||
|
|
||||||
fix: dnsmasq answers the remote access clients' DNS queries, so they no longer get the router's own host name with all its addresses from systemd-resolved.
|
fix: dnsmasq answers the remote access clients' DNS queries, so they no longer get the router's own host name with all its addresses from systemd-resolved.
|
||||||
|
|||||||
@@ -46,10 +46,10 @@ Suggests: caddy,
|
|||||||
pdns-server,
|
pdns-server,
|
||||||
pdns-backend-pgsql,
|
pdns-backend-pgsql,
|
||||||
proxmox-backup-client-static
|
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
|
Configures a Linux host or virtual machine as a VPN router: site-to-site
|
||||||
IKEv2 IPSec (strongSwan swanctl) with pre-shared keys or certificates,
|
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
|
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
|
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
|
needs. Its two-level CA, whose root may be kept offline, has issuing CAs for
|
||||||
|
|||||||
@@ -71,9 +71,11 @@ case "$1" in
|
|||||||
# setting the forward policy all belong to vpn-router-setup, which does
|
# setting the forward policy all belong to vpn-router-setup, which does
|
||||||
# them only once the configuration says this host is a router.
|
# them only once the configuration says this host is a router.
|
||||||
|
|
||||||
# The web service waits for libpq in the linker cache, which dpkg
|
# The setup and web services wait for libpq in the linker cache, which
|
||||||
# refreshes after this script, so it is started without waiting for it.
|
# dpkg refreshes after this script, so they are started without waiting
|
||||||
|
# for them.
|
||||||
if [ -d /run/systemd/system ]; then
|
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
|
systemctl --no-block restart vpn-router-web.service >/dev/null 2>&1 || true
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
dh $@
|
dh $@
|
||||||
|
|
||||||
override_dh_installsystemd:
|
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-docker
|
||||||
dh_installsystemd --name=vpn-router-web --no-start
|
dh_installsystemd --name=vpn-router-web --no-start
|
||||||
dh_installsystemd --name=vpn-router-metrics --no-enable --no-start
|
dh_installsystemd --name=vpn-router-metrics --no-enable --no-start
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Choices: none, generic, azure, gcp
|
|||||||
Default: none
|
Default: none
|
||||||
Description: Platform:
|
Description: Platform:
|
||||||
Which platform this router runs on. The choice loads platform-specific
|
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
|
none defers configuration entirely: the package installs its files and
|
||||||
changes nothing on the machine. Choose it when the router will be configured
|
changes nothing on the machine. Choose it when the router will be configured
|
||||||
@@ -58,7 +58,7 @@ Type: string
|
|||||||
Default:
|
Default:
|
||||||
Description: Local router FQDN:
|
Description: Local router FQDN:
|
||||||
Fully-qualified domain name of this router (for example
|
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.
|
certificate common name.
|
||||||
|
|
||||||
Template: vpn-router/local_id_mode
|
Template: vpn-router/local_id_mode
|
||||||
@@ -122,15 +122,15 @@ Description: Pre-shared key (PSK):
|
|||||||
Template: vpn-router/p2s_enabled
|
Template: vpn-router/p2s_enabled
|
||||||
Type: boolean
|
Type: boolean
|
||||||
Default: false
|
Default: false
|
||||||
Description: Enable road-warrior (P2S) access?
|
Description: Enable IKEv2 access?
|
||||||
If enabled, this router accepts IKEv2 EAP-TLS connections from individual
|
If enabled, this router accepts IKEv2 EAP-TLS connections from individual
|
||||||
clients, with certificates of the router's P2S issuing CA.
|
clients, with certificates of the router's P2S issuing CA.
|
||||||
|
|
||||||
Template: vpn-router/p2s_address_pool
|
Template: vpn-router/p2s_address_pool
|
||||||
Type: string
|
Type: string
|
||||||
Default:
|
Default:
|
||||||
Description: Road-warrior address pool:
|
Description: IKEv2 address pool:
|
||||||
CIDR block assigned to road-warrior clients (for example 172.16.0.0/24).
|
CIDR block assigned to IKEv2 clients (for example 172.16.0.0/24).
|
||||||
|
|
||||||
Template: vpn-router/pki_root_name
|
Template: vpn-router/pki_root_name
|
||||||
Type: string
|
Type: string
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ After=network-online.target docker.service postgresql.service
|
|||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=oneshot
|
Type=oneshot
|
||||||
|
ExecStartPre=/usr/lib/vpn-router/wait-for-libpq
|
||||||
ExecStart=/usr/lib/vpn-router/setup
|
ExecStart=/usr/lib/vpn-router/setup
|
||||||
RemainAfterExit=yes
|
RemainAfterExit=yes
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ disables the unit.
|
|||||||
Every SAMPLE_SECONDS, on the minute, a sample is taken with psutil: the CPU utilization since the
|
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
|
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;
|
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
|
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
|
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
|
the previous sample, and how long its IKE SA has been up. The interfaces, the enabled systems and
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ from vpnrouter import fail, warn
|
|||||||
BEFORE_RULES = pathlib.Path('/etc/ufw/before.rules')
|
BEFORE_RULES = pathlib.Path('/etc/ufw/before.rules')
|
||||||
UFW_DEFAULTS = pathlib.Path('/etc/default/ufw')
|
UFW_DEFAULTS = pathlib.Path('/etc/default/ufw')
|
||||||
WG_DIR = pathlib.Path('/etc/wireguard')
|
WG_DIR = pathlib.Path('/etc/wireguard')
|
||||||
WEB_STATE = pathlib.Path('/run/vpn-router/web.json')
|
WEB_STATE = vpnrouter.WEB_STATE
|
||||||
|
|
||||||
#: The state, the current step and the failure of the last run, which the bootstrap services show.
|
#: The state, the current step and the failure of the last run, which the bootstrap services show.
|
||||||
SETUP_STATE = pathlib.Path('/run/vpn-router/setup.json')
|
SETUP_STATE = pathlib.Path('/run/vpn-router/setup.json')
|
||||||
@@ -243,7 +243,8 @@ def filter_block(ctx):
|
|||||||
lines += ['# OPENVPN RULES END']
|
lines += ['# OPENVPN RULES END']
|
||||||
|
|
||||||
pools = [pool for ready, pool in ((vpnrouter.p2s_ready(ctx), ctx['p2s_address_pool']),
|
pools = [pool for ready, pool in ((vpnrouter.p2s_ready(ctx), ctx['p2s_address_pool']),
|
||||||
(vpnrouter.openvpn_ready(ctx), ctx['openvpn_pool'])) if ready]
|
(vpnrouter.openvpn_ready(ctx), ctx['openvpn_pool']),
|
||||||
|
(vpnrouter.wg_ready(ctx), ctx['wg_network'])) if ready]
|
||||||
# The remote access clients' resolver answers on the addresses of p2s.dns_listen.
|
# The remote access clients' resolver answers on the addresses of p2s.dns_listen.
|
||||||
if pools and ctx['p2s_resolver_addrs']:
|
if pools and ctx['p2s_resolver_addrs']:
|
||||||
lines += ['', '# P2S DNS RULES START']
|
lines += ['', '# P2S DNS RULES START']
|
||||||
@@ -820,12 +821,12 @@ def manage_webdav(ctx):
|
|||||||
|
|
||||||
|
|
||||||
def manage_dns(cfg, ctx):
|
def manage_dns(cfg, ctx):
|
||||||
"""Run PowerDNS, the name server of dns.zone, on the addresses of dns.listen while DNS is enabled, and stop it when
|
"""Run PowerDNS, the primary name server of the domains of vpnrouter_dns, on the addresses of dns.listen while DNS
|
||||||
not; its packages are installed when they are missing.
|
is enabled, and stop it when not; its packages are installed when they are missing.
|
||||||
|
|
||||||
The settings, vpnrouter_dns.PDNS_CONF, are written before the packages are installed, so the server their
|
The settings, vpnrouter_dns.PDNS_CONF, are written before the packages are installed, so the server their
|
||||||
installation starts answers on those addresses only, beside systemd-resolved's stub on 127.0.0.53. The zone gets
|
installation starts answers on those addresses only, beside systemd-resolved's stub on 127.0.0.53. The domains
|
||||||
the host names of the enabled sites, and keeps those it has while the sites cannot be read.
|
with site records get the host names of the enabled sites, and keep those they have while the sites cannot be read.
|
||||||
"""
|
"""
|
||||||
if not vpnrouter.dns_ready(ctx):
|
if not vpnrouter.dns_ready(ctx):
|
||||||
if unit_is_active(vpnrouter_dns.UNIT):
|
if unit_is_active(vpnrouter_dns.UNIT):
|
||||||
@@ -953,6 +954,7 @@ def refresh_web(cfg, ctx):
|
|||||||
'internal_address': ctx['int_addr'],
|
'internal_address': ctx['int_addr'],
|
||||||
'internal_gateway': ctx['int_gateway_ip'],
|
'internal_gateway': ctx['int_gateway_ip'],
|
||||||
'local_cidrs': ctx['local_cidr_list'],
|
'local_cidrs': ctx['local_cidr_list'],
|
||||||
|
'client_dns': ctx['p2s_resolver_addrs'] if vpnrouter.RESOLVER_CONF.exists() else [],
|
||||||
'site_to_site': [{
|
'site_to_site': [{
|
||||||
'name': connection['name'],
|
'name': connection['name'],
|
||||||
'local_identity': connection['local_id'],
|
'local_identity': connection['local_id'],
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ from vpnrouter_net import DEFAULT_LISTEN, LISTEN_CHOICES, LOOPBACK, ConfigError,
|
|||||||
|
|
||||||
CONFIG_FILE = pathlib.Path('/etc/vpn-router/vpn-router.json')
|
CONFIG_FILE = pathlib.Path('/etc/vpn-router/vpn-router.json')
|
||||||
CONFIG_LOCK = pathlib.Path('/run/vpn-router/config.lock')
|
CONFIG_LOCK = pathlib.Path('/run/vpn-router/config.lock')
|
||||||
|
#: What setup resolved and runs, for the web service (setup's refresh_web).
|
||||||
|
WEB_STATE = pathlib.Path('/run/vpn-router/web.json')
|
||||||
TEMPLATE_DIR = pathlib.Path('/usr/share/vpn-router/templates')
|
TEMPLATE_DIR = pathlib.Path('/usr/share/vpn-router/templates')
|
||||||
|
|
||||||
SWANCTL_CONF_D = pathlib.Path('/etc/swanctl/conf.d')
|
SWANCTL_CONF_D = pathlib.Path('/etc/swanctl/conf.d')
|
||||||
@@ -56,7 +58,7 @@ SCHEMA = {
|
|||||||
'wan': {'local_fqdn': ''},
|
'wan': {'local_fqdn': ''},
|
||||||
'local': {'cidrs': [], 'int_addr': '', 'int_gateway_ip': ''},
|
'local': {'cidrs': [], 'int_addr': '', 'int_gateway_ip': ''},
|
||||||
'site_to_site': {},
|
'site_to_site': {},
|
||||||
'p2s': {'enabled': False, 'address_pool': '', 'dns_listen': 'internal'},
|
'p2s': {'enabled': False, 'address_pool': '', 'dns_listen': 'internal', 'dns_domains': []},
|
||||||
'wireguard': {'enabled': False, 'address': '', 'listen_port': 51820},
|
'wireguard': {'enabled': False, 'address': '', 'listen_port': 51820},
|
||||||
'openvpn': {'enabled': False, 'address_pool': '', 'port': 1194, 'protocol': 'udp'},
|
'openvpn': {'enabled': False, 'address_pool': '', 'port': 1194, 'protocol': 'udp'},
|
||||||
'docker': {'mode': 'auto', 'exposed_ports': []},
|
'docker': {'mode': 'auto', 'exposed_ports': []},
|
||||||
@@ -67,7 +69,7 @@ SCHEMA = {
|
|||||||
'https': {'enabled': True, 'email': '', 'web_port': 7444, 'manager_port': 7443, 'caddy_source': 'ubuntu'},
|
'https': {'enabled': True, 'email': '', 'web_port': 7444, 'manager_port': 7443, 'caddy_source': 'ubuntu'},
|
||||||
'oidc': {'enabled': False},
|
'oidc': {'enabled': False},
|
||||||
'sites': {'enabled': False, 'upload_max_mb': 512, 'webdav': False, 'webdav_port': 7445},
|
'sites': {'enabled': False, 'upload_max_mb': 512, 'webdav': False, 'webdav_port': 7445},
|
||||||
'dns': {'enabled': False, 'zone': '', 'nameserver': '', 'addresses': [], 'listen': 'external', 'ttl': 300},
|
'dns': {'enabled': False, 'nameserver': '', 'addresses': [], 'listen': 'external', 'ttl': 300},
|
||||||
'pki': {'root_name': 'VPN Router Root CA', 'root_days': 7300, 'ca_days': 3650, 'key': 'rsa-4096',
|
'pki': {'root_name': 'VPN Router Root CA', 'root_days': 7300, 'ca_days': 3650, 'key': 'rsa-4096',
|
||||||
'crl_days': 7, 'root_crl_days': 180, 'publish': ['http'], 'p2s_name': 'VPN Router P2S CA',
|
'crl_days': 7, 'root_crl_days': 180, 'publish': ['http'], 'p2s_name': 'VPN Router P2S CA',
|
||||||
's2s_name': 'VPN Router Site-to-Site CA', 'issuing_cas': {}, 'templates': {},
|
's2s_name': 'VPN Router Site-to-Site CA', 'issuing_cas': {}, 'templates': {},
|
||||||
@@ -144,9 +146,13 @@ UPLOAD_MAX_MB = 10240
|
|||||||
#: The shortest and the longest dns.ttl, in seconds.
|
#: The shortest and the longest dns.ttl, in seconds.
|
||||||
DNS_TTL_RANGE = (60, 86400)
|
DNS_TTL_RANGE = (60, 86400)
|
||||||
|
|
||||||
#: A DNS name without a wildcard, in lowercase, of dns.zone and dns.nameserver.
|
#: A DNS name without a wildcard, in lowercase, of dns.nameserver and the domains of the name server.
|
||||||
ZONE_NAME = re.compile(r'^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+$')
|
ZONE_NAME = re.compile(r'^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+$')
|
||||||
|
|
||||||
|
#: A domain of p2s.dns_domains, one label or more, and the most domains.
|
||||||
|
DNS_DOMAIN = re.compile(r'^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$')
|
||||||
|
DNS_DOMAINS_MAX = 16
|
||||||
|
|
||||||
#: The ports of the bootstrap services: the Router Manager's and the router's.
|
#: The ports of the bootstrap services: the Router Manager's and the router's.
|
||||||
BOOTSTRAP_PORTS = (17080, 17081)
|
BOOTSTRAP_PORTS = (17080, 17081)
|
||||||
|
|
||||||
@@ -573,26 +579,31 @@ def validate(cfg):
|
|||||||
|
|
||||||
dns = cfg['dns']
|
dns = cfg['dns']
|
||||||
if dns['enabled']:
|
if dns['enabled']:
|
||||||
for option in ('zone', 'nameserver'):
|
if not ZONE_NAME.match(dns['nameserver'].strip()):
|
||||||
if not ZONE_NAME.match(dns[option].strip()):
|
raise ConfigError(f'dns.nameserver: {dns["nameserver"]!r} is not a DNS name in lowercase')
|
||||||
raise ConfigError(f'dns.{option}: {dns[option]!r} is not a DNS name in lowercase')
|
|
||||||
if not _get_list(cfg, 'dns', 'addresses'):
|
if not _get_list(cfg, 'dns', 'addresses'):
|
||||||
raise ConfigError('dns.enabled needs dns.addresses, the public addresses of the zone')
|
raise ConfigError('dns.enabled needs dns.addresses, the public addresses of the name server and the sites')
|
||||||
for item in _get_list(cfg, 'dns', 'addresses'):
|
for item in _get_list(cfg, 'dns', 'addresses'):
|
||||||
_check_address(item, 'dns.addresses')
|
_check_address(item, 'dns.addresses')
|
||||||
if dns['listen'] not in LISTEN_CHOICES:
|
if dns['listen'] not in LISTEN_CHOICES:
|
||||||
raise ConfigError(f'dns.listen: {dns["listen"]!r} is not one of {", ".join(LISTEN_CHOICES)}')
|
raise ConfigError(f'dns.listen: {dns["listen"]!r} is not one of {", ".join(LISTEN_CHOICES)}')
|
||||||
if not DNS_TTL_RANGE[0] <= dns['ttl'] <= DNS_TTL_RANGE[1]:
|
if not DNS_TTL_RANGE[0] <= dns['ttl'] <= DNS_TTL_RANGE[1]:
|
||||||
raise ConfigError(f'dns.ttl: {dns["ttl"]} is not between {DNS_TTL_RANGE[0]} and {DNS_TTL_RANGE[1]} seconds')
|
raise ConfigError(f'dns.ttl: {dns["ttl"]} is not between {DNS_TTL_RANGE[0]} and {DNS_TTL_RANGE[1]} seconds')
|
||||||
|
dns_domains = _get_list(cfg, 'p2s', 'dns_domains')
|
||||||
|
if len(dns_domains) > DNS_DOMAINS_MAX or len(set(dns_domains)) != len(dns_domains):
|
||||||
|
raise ConfigError(f'p2s.dns_domains: at most {DNS_DOMAINS_MAX} different domains')
|
||||||
|
for item in dns_domains:
|
||||||
|
if not DNS_DOMAIN.match(item):
|
||||||
|
raise ConfigError(f'p2s.dns_domains: {item!r} is not a DNS name in lowercase')
|
||||||
resolver_listen = _get(cfg, 'p2s', 'dns_listen')
|
resolver_listen = _get(cfg, 'p2s', 'dns_listen')
|
||||||
if resolver_listen not in LISTEN_CHOICES:
|
if resolver_listen not in LISTEN_CHOICES:
|
||||||
raise ConfigError(f'p2s.dns_listen: {resolver_listen!r} is not one of {", ".join(LISTEN_CHOICES)}')
|
raise ConfigError(f'p2s.dns_listen: {resolver_listen!r} is not one of {", ".join(LISTEN_CHOICES)}')
|
||||||
sides = {'internal': {'internal'}, 'external': {'external'}, 'both': {'internal', 'external'}}
|
sides = {'internal': {'internal'}, 'external': {'external'}, 'both': {'internal', 'external'}}
|
||||||
if dns['enabled'] and (_get(cfg, 'p2s', 'enabled') or _get(cfg, 'openvpn', 'enabled')) and \
|
remote_access = any(_get(cfg, system, 'enabled') for system in ('p2s', 'openvpn', 'wireguard'))
|
||||||
sides[dns['listen']] & sides[resolver_listen]:
|
if dns['enabled'] and remote_access and sides[dns['listen']] & sides[resolver_listen]:
|
||||||
raise ConfigError(f'dns.listen: port 53 of the {" and ".join(sorted(sides[dns["listen"]] & sides[resolver_listen]))} '
|
raise ConfigError(f'dns.listen: port 53 of the {" and ".join(sorted(sides[dns["listen"]] & sides[resolver_listen]))} '
|
||||||
f'addresses is the resolver of the remote access clients (p2s.dns_listen {resolver_listen}) '
|
f'addresses is the resolver of the remote access clients (p2s.dns_listen {resolver_listen}) '
|
||||||
'while p2s.enabled or openvpn.enabled is true')
|
'while p2s.enabled, openvpn.enabled or wireguard.enabled is true')
|
||||||
|
|
||||||
for option in ('root_name', 'p2s_name', 's2s_name'):
|
for option in ('root_name', 'p2s_name', 's2s_name'):
|
||||||
value = _get(cfg, 'pki', option)
|
value = _get(cfg, 'pki', option)
|
||||||
@@ -990,9 +1001,12 @@ def build_context(cfg, public_ip=None):
|
|||||||
'p2s_address_pool': _get(cfg, 'p2s', 'address_pool'),
|
'p2s_address_pool': _get(cfg, 'p2s', 'address_pool'),
|
||||||
'public_ip': public_ip or '',
|
'public_ip': public_ip or '',
|
||||||
'p2s_resolver_addrs': resolver_addrs,
|
'p2s_resolver_addrs': resolver_addrs,
|
||||||
|
'p2s_dns_domains': _get_list(cfg, 'p2s', 'dns_domains'),
|
||||||
'p2s_dns': _join(resolver_addrs, _split_list(extras['EXTRA_P2S_DNS'])),
|
'p2s_dns': _join(resolver_addrs, _split_list(extras['EXTRA_P2S_DNS'])),
|
||||||
'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_network': str(ipaddress.ip_interface(_get(cfg, 'wireguard', 'address')).network)
|
||||||
|
if _get(cfg, 'wireguard', 'address') else '',
|
||||||
'wg_listen_port': _get(cfg, 'wireguard', 'listen_port'),
|
'wg_listen_port': _get(cfg, 'wireguard', 'listen_port'),
|
||||||
'wg_peers_begin': WG_PEERS_BEGIN,
|
'wg_peers_begin': WG_PEERS_BEGIN,
|
||||||
'wg_peers_end': WG_PEERS_END,
|
'wg_peers_end': WG_PEERS_END,
|
||||||
@@ -1049,7 +1063,7 @@ def routing_ready(ctx):
|
|||||||
|
|
||||||
|
|
||||||
def p2s_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'])
|
return bool(ctx['p2s_enabled'] and ctx['p2s_address_pool'] and ctx['local_fqdn'] and ctx['server_certificate'])
|
||||||
|
|
||||||
|
|
||||||
@@ -1078,7 +1092,7 @@ def webdav_ready(ctx):
|
|||||||
|
|
||||||
|
|
||||||
def dns_ready(ctx):
|
def dns_ready(ctx):
|
||||||
"""The name server answers for dns.zone while dns.enabled is true."""
|
"""The name server answers for its domains while dns.enabled is true."""
|
||||||
return bool(ctx['dns_enabled'])
|
return bool(ctx['dns_enabled'])
|
||||||
|
|
||||||
|
|
||||||
@@ -1236,7 +1250,7 @@ def configure(cfg=None, public_ip=None):
|
|||||||
# its loopback stub only, so its records of the router's own name stay on the router.
|
# its loopback stub only, so its records of the router's own name stay on the router.
|
||||||
if remove(P2S_FORWARDER):
|
if remove(P2S_FORWARDER):
|
||||||
changed.add('resolved')
|
changed.add('resolved')
|
||||||
if (p2s_ready(ctx) or openvpn_ready(ctx)) and ctx['p2s_resolver_addrs']:
|
if (p2s_ready(ctx) or openvpn_ready(ctx) or wg_ready(ctx)) and ctx['p2s_resolver_addrs']:
|
||||||
if render(env, ctx, 'dnsmasq.conf.j2', RESOLVER_CONF, 0o644):
|
if render(env, ctx, 'dnsmasq.conf.j2', RESOLVER_CONF, 0o644):
|
||||||
changed.add('resolver')
|
changed.add('resolver')
|
||||||
elif remove(RESOLVER_CONF):
|
elif remove(RESOLVER_CONF):
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
"""The router's authoritative name server, PowerDNS with its gpgsql backend, for vpn-router-setup and the web service.
|
"""The router's authoritative name server, PowerDNS with its gpgsql backend, for vpn-router-setup and the web service.
|
||||||
|
|
||||||
The name server answers for one zone, dns.zone, on the addresses of the interfaces of dns.listen. Its records are in
|
The name server is the primary of its domains and answers on the addresses of the interfaces of dns.listen. The
|
||||||
the vpn-router-dns database, which provision() creates with its role and the password in DATABASE_SECRET, and fills with
|
domains, their secondary name servers and the records an administrator added (manual records) are in the
|
||||||
|
vpn-router-dns database, which provision() creates with its role and the password in DATABASE_SECRET, and fills with
|
||||||
the schema of pdns-backend-pgsql, PDNS_SCHEMA; DNS_SQL defines the router's own tables and procedures, and the first
|
the schema of pdns-backend-pgsql, PDNS_SCHEMA; DNS_SQL defines the router's own tables and procedures, and the first
|
||||||
connection of a process loads it. apply() writes the zone: its SOA, its NS, dns.nameserver, A and AAAA records of
|
connection of a process loads it.
|
||||||
dns.addresses for the zone's apex, for the name server when it is in the zone, and for every host name of an enabled
|
|
||||||
site in the zone, and the records an administrator added (manual records), but those of a name and type the records
|
|
||||||
before them have. The serial of the SOA grows only when the records change.
|
|
||||||
|
|
||||||
Every failure of the database raises DatabaseError; a record that cannot be added raises Failure.
|
apply() writes every domain as a primary zone: its SOA with dns.nameserver, its NS, dns.nameserver and the name
|
||||||
|
servers of its secondaries, the NS of the domains directly below it, A and AAAA records of dns.addresses for its apex,
|
||||||
|
for those name servers inside it that are the router, and, when the domain has site records, for every host name of an
|
||||||
|
enabled site whose closest domain it is, and the manual records, but those of a name and type the records before them
|
||||||
|
have. A secondary's address is notified of every change (ALSO-NOTIFY) and may transfer the zone (ALLOW-AXFR-FROM); the
|
||||||
|
serial of the SOA grows only when the records change.
|
||||||
|
|
||||||
|
Every failure of the database raises DatabaseError; a request that cannot be done raises Failure.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
@@ -19,6 +24,7 @@ import re
|
|||||||
import psycopg
|
import psycopg
|
||||||
from psycopg.rows import dict_row
|
from psycopg.rows import dict_row
|
||||||
|
|
||||||
|
import vpnrouter
|
||||||
import vpnrouter_auth
|
import vpnrouter_auth
|
||||||
|
|
||||||
DATABASE = 'vpn-router-dns'
|
DATABASE = 'vpn-router-dns'
|
||||||
@@ -38,7 +44,9 @@ GROUP = 'pdns'
|
|||||||
#: The types of the manual records.
|
#: The types of the manual records.
|
||||||
TYPES = ('A', 'AAAA', 'CNAME', 'TXT', 'MX', 'SRV', 'CAA')
|
TYPES = ('A', 'AAAA', 'CNAME', 'TXT', 'MX', 'SRV', 'CAA')
|
||||||
|
|
||||||
#: The most manual records.
|
#: The most domains, secondaries of a domain, and manual records of a domain.
|
||||||
|
DOMAINS_MAX = 100
|
||||||
|
SECONDARIES_MAX = 8
|
||||||
MANUAL_MAX = 1000
|
MANUAL_MAX = 1000
|
||||||
|
|
||||||
#: The refresh, retry and expire times of the SOA, in seconds.
|
#: The refresh, retry and expire times of the SOA, in seconds.
|
||||||
@@ -65,7 +73,8 @@ class DatabaseError(Exception):
|
|||||||
|
|
||||||
|
|
||||||
class Failure(Exception):
|
class Failure(Exception):
|
||||||
"""A record that cannot be added or removed; status is 2 invalid, 3 no such record."""
|
"""A request that cannot be done; status is 2 invalid, 3 no such domain, secondary or record, 4 the domain
|
||||||
|
exists."""
|
||||||
|
|
||||||
def __init__(self, message, status=2):
|
def __init__(self, message, status=2):
|
||||||
super().__init__(message)
|
super().__init__(message)
|
||||||
@@ -173,87 +182,210 @@ def check_record(zone, record):
|
|||||||
return {'name': name, 'type': kind, 'content': content, 'ttl': ttl}
|
return {'name': name, 'type': kind, 'content': content, 'ttl': ttl}
|
||||||
|
|
||||||
|
|
||||||
def manual():
|
def closest(zones, name):
|
||||||
"""The manual records."""
|
"""The longest of the zones the name is in, None when it is in none."""
|
||||||
with database() as connection:
|
return max((zone for zone in zones if in_zone(zone, name)), key=len, default=None)
|
||||||
return connection.execute('SELECT * FROM dns_manual_list()').fetchall()
|
|
||||||
|
|
||||||
|
|
||||||
def add(cfg, record):
|
def check_domain(name):
|
||||||
"""Add the manual record, or set the TTL of the same one; the record."""
|
"""The domain name in lowercase without a final dot; Failure when it is not a DNS name."""
|
||||||
zone = cfg['dns']['zone'].strip()
|
if not isinstance(name, str) or not vpnrouter.ZONE_NAME.match(name.strip().lower().rstrip('.')):
|
||||||
if not zone:
|
raise Failure(f'{name!r} is not a domain name')
|
||||||
raise Failure('dns.zone is not set')
|
return name.strip().lower().rstrip('.')
|
||||||
record = check_record(zone, record)
|
|
||||||
record['ttl'] = record['ttl'] or cfg['dns']['ttl']
|
|
||||||
|
def check_ttl(ttl):
|
||||||
|
if ttl is not None and (isinstance(ttl, bool) or not isinstance(ttl, int) or
|
||||||
|
not vpnrouter.DNS_TTL_RANGE[0] <= ttl <= vpnrouter.DNS_TTL_RANGE[1]):
|
||||||
|
raise Failure(f'ttl must be {vpnrouter.DNS_TTL_RANGE[0]} to {vpnrouter.DNS_TTL_RANGE[1]} seconds')
|
||||||
|
return ttl
|
||||||
|
|
||||||
|
|
||||||
|
def domain(connection, name):
|
||||||
|
"""The domain; Failure with status 3 when there is none."""
|
||||||
|
row = next((row for row in connection.execute('SELECT * FROM dns_domains_list()').fetchall()
|
||||||
|
if row['name'] == name), None)
|
||||||
|
if row is None:
|
||||||
|
raise Failure(f'no domain {name}', 3)
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
def listing():
|
||||||
|
"""Every domain with its secondaries, its manual records and the records the name server answers for it."""
|
||||||
with database() as connection:
|
with database() as connection:
|
||||||
records = connection.execute('SELECT * FROM dns_manual_list()').fetchall()
|
secondaries = connection.execute('SELECT * FROM dns_secondaries_list()').fetchall()
|
||||||
same = [row for row in records if (row['name'], row['type']) == (record['name'], record['type'])]
|
manual = connection.execute('SELECT * FROM dns_records_list()').fetchall()
|
||||||
others = [row for row in records if row['name'] == record['name'] and row['type'] != record['type']]
|
return [{**row,
|
||||||
if len(records) >= MANUAL_MAX and record not in records:
|
'secondaries': [{key: item[key] for key in ('nameserver', 'address')}
|
||||||
raise Failure(f'the zone has {MANUAL_MAX} manual records')
|
for item in secondaries if item['domain'] == row['name']],
|
||||||
if record['type'] == 'CNAME' and (others or [row for row in same if row['content'] != record['content']]):
|
'manual': [{key: item[key] for key in ('name', 'type', 'content', 'ttl')}
|
||||||
|
for item in manual if item['domain'] == row['name']],
|
||||||
|
'records': connection.execute('SELECT * FROM dns_served(%s)', (row['name'],)).fetchall()}
|
||||||
|
for row in connection.execute('SELECT * FROM dns_domains_list()').fetchall()]
|
||||||
|
|
||||||
|
|
||||||
|
def domain_add(body):
|
||||||
|
"""Add the domain {name, sites, ttl?}; the domain."""
|
||||||
|
if not isinstance(body, dict) or not isinstance(body.get('sites', False), bool):
|
||||||
|
raise Failure('a domain is an object of name, sites and ttl')
|
||||||
|
name, sites, ttl = check_domain(body.get('name')), body.get('sites', False), check_ttl(body.get('ttl'))
|
||||||
|
with database() as connection:
|
||||||
|
names = [row['name'] for row in connection.execute('SELECT * FROM dns_domains_list()').fetchall()]
|
||||||
|
if name in names:
|
||||||
|
raise Failure(f'the domain {name} exists', 4)
|
||||||
|
if len(names) >= DOMAINS_MAX:
|
||||||
|
raise Failure(f'the name server has {DOMAINS_MAX} domains')
|
||||||
|
connection.execute('CALL dns_domain_put(%s, %s, %s)', (name, sites, ttl))
|
||||||
|
return {'name': name, 'sites': sites, 'ttl': ttl}
|
||||||
|
|
||||||
|
|
||||||
|
def domain_change(name, body):
|
||||||
|
"""Change the sites and ttl of the domain; the domain."""
|
||||||
|
if not isinstance(body, dict) or not set(body) <= {'sites', 'ttl'} or \
|
||||||
|
not isinstance(body.get('sites', False), bool):
|
||||||
|
raise Failure('a change of a domain is an object of sites and ttl')
|
||||||
|
with database() as connection:
|
||||||
|
row = domain(connection, name)
|
||||||
|
sites, ttl = body.get('sites', row['sites']), check_ttl(body.get('ttl', row['ttl']))
|
||||||
|
connection.execute('CALL dns_domain_put(%s, %s, %s)', (name, sites, ttl))
|
||||||
|
return {'name': name, 'sites': sites, 'ttl': ttl}
|
||||||
|
|
||||||
|
|
||||||
|
def domain_remove(name):
|
||||||
|
"""Remove the domain with its secondaries and manual records."""
|
||||||
|
with database() as connection:
|
||||||
|
domain(connection, name)
|
||||||
|
connection.execute('CALL dns_domain_remove(%s)', (name,))
|
||||||
|
return {'name': name}
|
||||||
|
|
||||||
|
|
||||||
|
def secondary_add(name, body):
|
||||||
|
"""Add the secondary name server {nameserver, address} of the domain, or set its address; the secondary."""
|
||||||
|
if not isinstance(body, dict):
|
||||||
|
raise Failure('a secondary is an object of nameserver and address')
|
||||||
|
nameserver = body.get('nameserver')
|
||||||
|
if not isinstance(nameserver, str) or not vpnrouter.ZONE_NAME.match(nameserver.strip().lower().rstrip('.')):
|
||||||
|
raise Failure(f'{nameserver!r} is not a name server name')
|
||||||
|
nameserver = nameserver.strip().lower().rstrip('.')
|
||||||
|
try:
|
||||||
|
address = str(ipaddress.ip_address(str(body.get('address', '')).strip()))
|
||||||
|
except ValueError as exc:
|
||||||
|
raise Failure(f'{body.get("address")!r} is not an IP address') from exc
|
||||||
|
with database() as connection:
|
||||||
|
domain(connection, name)
|
||||||
|
others = [row for row in connection.execute('SELECT * FROM dns_secondaries_list()').fetchall()
|
||||||
|
if row['domain'] == name and row['nameserver'] != nameserver]
|
||||||
|
if len(others) >= SECONDARIES_MAX:
|
||||||
|
raise Failure(f'the domain {name} has {SECONDARIES_MAX} secondaries')
|
||||||
|
connection.execute('CALL dns_secondary_put(%s, %s, %s)', (name, nameserver, address))
|
||||||
|
return {'domain': name, 'nameserver': nameserver, 'address': address}
|
||||||
|
|
||||||
|
|
||||||
|
def secondary_remove(name, nameserver):
|
||||||
|
"""Remove the secondary name server of the domain."""
|
||||||
|
with database() as connection:
|
||||||
|
domain(connection, name)
|
||||||
|
if not any(row['domain'] == name and row['nameserver'] == nameserver
|
||||||
|
for row in connection.execute('SELECT * FROM dns_secondaries_list()').fetchall()):
|
||||||
|
raise Failure(f'no secondary {nameserver} of {name}', 3)
|
||||||
|
connection.execute('CALL dns_secondary_remove(%s, %s)', (name, nameserver))
|
||||||
|
return {'domain': name, 'nameserver': nameserver}
|
||||||
|
|
||||||
|
|
||||||
|
def add(cfg, name, record):
|
||||||
|
"""Add the manual record to the domain, or set the TTL of the same one; the record. A name in a domain below it
|
||||||
|
belongs to that domain."""
|
||||||
|
with database() as connection:
|
||||||
|
row = domain(connection, name)
|
||||||
|
record = check_record(name, record)
|
||||||
|
record['ttl'] = record['ttl'] or row['ttl'] or cfg['dns']['ttl']
|
||||||
|
zones = [item['name'] for item in connection.execute('SELECT * FROM dns_domains_list()').fetchall()]
|
||||||
|
owner = closest(zones, record['name'])
|
||||||
|
if owner != name:
|
||||||
|
raise Failure(f'{record["name"]} is in the domain {owner}')
|
||||||
|
records = [item for item in connection.execute('SELECT * FROM dns_records_list()').fetchall()
|
||||||
|
if item['domain'] == name]
|
||||||
|
same = [item for item in records if (item['name'], item['type']) == (record['name'], record['type'])]
|
||||||
|
others = [item for item in records if item['name'] == record['name'] and item['type'] != record['type']]
|
||||||
|
if len(records) >= MANUAL_MAX and not same:
|
||||||
|
raise Failure(f'the domain {name} has {MANUAL_MAX} manual records')
|
||||||
|
if record['type'] == 'CNAME' and (others or [item for item in same if item['content'] != record['content']]):
|
||||||
raise Failure(f'{record["name"]} has other records; a CNAME record must be its only record')
|
raise Failure(f'{record["name"]} has other records; a CNAME record must be its only record')
|
||||||
if any(row['type'] == 'CNAME' for row in others):
|
if any(item['type'] == 'CNAME' for item in others):
|
||||||
raise Failure(f'{record["name"]} has a CNAME record, which must be its only record')
|
raise Failure(f'{record["name"]} has a CNAME record, which must be its only record')
|
||||||
connection.execute('CALL dns_manual_put(%s, %s, %s, %s)',
|
connection.execute('CALL dns_record_put(%s, %s, %s, %s, %s)',
|
||||||
(record['name'], record['type'], record['content'], record['ttl']))
|
(name, record['name'], record['type'], record['content'], record['ttl']))
|
||||||
return record
|
return record
|
||||||
|
|
||||||
|
|
||||||
def remove(cfg, name, kind, content):
|
def remove(name, record_name, kind, content):
|
||||||
"""Remove the manual records of the name and type, only the one of the content when it is given."""
|
"""Remove the manual records of the name and type in the domain, only the one of the content when it is given."""
|
||||||
zone = cfg['dns']['zone'].strip()
|
full = absolute(name, record_name)
|
||||||
full = absolute(zone, name)
|
|
||||||
with database() as connection:
|
with database() as connection:
|
||||||
found = [row for row in connection.execute('SELECT * FROM dns_manual_list()').fetchall()
|
domain(connection, name)
|
||||||
if (row['name'], row['type']) == (full, kind) and content in (None, row['content'])]
|
found = [row for row in connection.execute('SELECT * FROM dns_records_list()').fetchall()
|
||||||
|
if (row['domain'], row['name'], row['type']) == (name, full, kind)
|
||||||
|
and content in (None, row['content'])]
|
||||||
if not found:
|
if not found:
|
||||||
raise Failure(f'no {kind} record of {full}', 3)
|
raise Failure(f'no {kind} record of {full}', 3)
|
||||||
connection.execute('CALL dns_manual_remove(%s, %s, %s)', (full, kind, content))
|
connection.execute('CALL dns_record_remove(%s, %s, %s, %s)', (name, full, kind, content))
|
||||||
return {'name': full, 'type': kind, 'removed': len(found)}
|
return {'name': full, 'type': kind, 'removed': len(found)}
|
||||||
|
|
||||||
|
|
||||||
def served(zone):
|
def records(cfg, row, zones, secondaries, hosts, manual_records):
|
||||||
"""The records of the zone as the name server answers them, [] when it serves none."""
|
"""The records of the domain row besides its SOA, as (name, type, content, ttl): the NS of the domain and of the
|
||||||
with database() as connection:
|
domains directly below it, the A and AAAA records of dns.addresses for its apex, dns.nameserver when it is inside
|
||||||
return connection.execute('SELECT * FROM dns_served(%s)', (zone,)).fetchall()
|
and, with site records, each host name whose closest domain it is, the glue of the secondaries inside it, and its
|
||||||
|
manual records. secondaries maps a domain to its secondaries."""
|
||||||
|
|
||||||
def records(cfg, hosts, manual_records):
|
|
||||||
"""The records of the zone besides its SOA, as (name, type, content, ttl): its NS, the A and AAAA records of
|
|
||||||
dns.addresses for the apex, the name server and each host name in the zone, and the manual records."""
|
|
||||||
dns = cfg['dns']
|
dns = cfg['dns']
|
||||||
zone, nameserver, ttl = dns['zone'].strip(), dns['nameserver'].strip(), dns['ttl']
|
zone, nameserver, ttl = row['name'], dns['nameserver'].strip(), row['ttl'] or dns['ttl']
|
||||||
addresses = [ipaddress.ip_address(item.strip()) for item in dns['addresses'] if item.strip()]
|
addresses = [ipaddress.ip_address(item.strip()) for item in dns['addresses'] if item.strip()]
|
||||||
found = [(zone, 'NS', nameserver, ttl)]
|
children = [name for name in zones
|
||||||
|
if name != zone and closest([item for item in zones if item != name], name) == zone]
|
||||||
|
found = [(name, 'NS', server, ttl) for name in [zone, *children]
|
||||||
|
for server in [nameserver, *(item['nameserver'] for item in secondaries.get(name, []))]]
|
||||||
for name in dict.fromkeys([zone, *([nameserver] if in_zone(zone, nameserver) else []),
|
for name in dict.fromkeys([zone, *([nameserver] if in_zone(zone, nameserver) else []),
|
||||||
*(host for host in hosts if in_zone(zone, host))]):
|
*(host for host in hosts if row['sites'] and closest(zones, host) == zone)]):
|
||||||
found += [(name, 'A' if address.version == 4 else 'AAAA', str(address), ttl) for address in addresses]
|
found += [(name, 'A' if address.version == 4 else 'AAAA', str(address), ttl) for address in addresses]
|
||||||
|
for item in (item for name in [zone, *children] for item in secondaries.get(name, [])):
|
||||||
|
if in_zone(zone, item['nameserver']) and item['nameserver'] != nameserver:
|
||||||
|
address = ipaddress.ip_address(item['address'])
|
||||||
|
found.append((item['nameserver'], 'A' if address.version == 4 else 'AAAA', str(address), ttl))
|
||||||
taken = {(name, kind) for name, kind, _, _ in found}
|
taken = {(name, kind) for name, kind, _, _ in found}
|
||||||
found += [(row['name'], row['type'], row['content'], row['ttl']) for row in manual_records
|
found += [(item['name'], item['type'], item['content'], item['ttl']) for item in manual_records
|
||||||
if (row['name'], row['type']) not in taken]
|
if (item['name'], item['type']) not in taken]
|
||||||
return list(dict.fromkeys(found))
|
return list(dict.fromkeys(found))
|
||||||
|
|
||||||
|
|
||||||
def apply(cfg, hosts):
|
def apply(cfg, hosts):
|
||||||
"""Make the name server serve dns.zone with records(), or no zone while dns.enabled is false."""
|
"""Make the name server the primary of every domain with records(), and of none while dns.enabled is false."""
|
||||||
with database() as connection:
|
with database() as connection:
|
||||||
if not cfg['dns']['enabled']:
|
if not cfg['dns']['enabled']:
|
||||||
connection.execute('CALL dns_clear()')
|
connection.execute('CALL dns_retain(%s::text[])', ([],))
|
||||||
return
|
return
|
||||||
dns = cfg['dns']
|
dns = cfg['dns']
|
||||||
zone = dns['zone'].strip()
|
domains = connection.execute('SELECT * FROM dns_domains_list()').fetchall()
|
||||||
rows = records(cfg, hosts, connection.execute('SELECT * FROM dns_manual_list()').fetchall())
|
zones = [row['name'] for row in domains]
|
||||||
soa = f'{dns["nameserver"].strip()} hostmaster.{zone} {{serial}} {SOA_TIMES} {dns["ttl"]}'
|
secondaries, manual = {}, {}
|
||||||
connection.execute('CALL dns_apply(%s, %s, %s, %s, %s, %s, %s)',
|
for item in connection.execute('SELECT * FROM dns_secondaries_list()').fetchall():
|
||||||
(zone, soa, dns['ttl'], [row[0] for row in rows], [row[1] for row in rows],
|
secondaries.setdefault(item['domain'], []).append(item)
|
||||||
[row[2] for row in rows], [row[3] for row in rows]))
|
for item in connection.execute('SELECT * FROM dns_records_list()').fetchall():
|
||||||
|
manual.setdefault(item['domain'], []).append(item)
|
||||||
|
for row in domains:
|
||||||
|
zone, ttl = row['name'], row['ttl'] or dns['ttl']
|
||||||
|
rows = records(cfg, row, zones, secondaries, hosts, manual.get(zone, []))
|
||||||
|
soa = f'{dns["nameserver"].strip()} hostmaster.{zone} {{serial}} {SOA_TIMES} {ttl}'
|
||||||
|
connection.execute('CALL dns_apply(%s, %s, %s, %s::text[], %s::text[], %s::text[], %s::integer[], '
|
||||||
|
'%s::text[])',
|
||||||
|
(zone, soa, ttl, [item[0] for item in rows], [item[1] for item in rows],
|
||||||
|
[item[2] for item in rows], [item[3] for item in rows],
|
||||||
|
[item['address'] for item in secondaries.get(zone, [])]))
|
||||||
|
connection.execute('CALL dns_retain(%s::text[])', (zones,))
|
||||||
|
|
||||||
|
|
||||||
def config_text(addresses):
|
def config_text(addresses):
|
||||||
"""The settings of PowerDNS: the gpgsql backend on the database of the name server, and the addresses it answers
|
"""The settings of PowerDNS: the gpgsql backend on the database of the name server, primary operation, and the
|
||||||
on."""
|
addresses it answers on."""
|
||||||
password = DATABASE_SECRET.read_text(encoding='utf-8').strip()
|
password = DATABASE_SECRET.read_text(encoding='utf-8').strip()
|
||||||
return '\n'.join([
|
return '\n'.join([
|
||||||
'# Written by vpn-router-setup from /etc/vpn-router/vpn-router.json; edits are overwritten.',
|
'# Written by vpn-router-setup from /etc/vpn-router/vpn-router.json; edits are overwritten.',
|
||||||
@@ -262,6 +394,7 @@ def config_text(addresses):
|
|||||||
f'gpgsql-dbname={DATABASE}',
|
f'gpgsql-dbname={DATABASE}',
|
||||||
f'gpgsql-user={DATABASE}',
|
f'gpgsql-user={DATABASE}',
|
||||||
f'gpgsql-password={password}',
|
f'gpgsql-password={password}',
|
||||||
|
'primary=yes',
|
||||||
f'local-address={", ".join(addresses)}',
|
f'local-address={", ".join(addresses)}',
|
||||||
'local-port=53',
|
'local-port=53',
|
||||||
'',
|
'',
|
||||||
|
|||||||
@@ -722,7 +722,7 @@ def install_crls(connection):
|
|||||||
crls = {}
|
crls = {}
|
||||||
for name, file in SWANCTL_CA_FILES.items():
|
for name, file in SWANCTL_CA_FILES.items():
|
||||||
entry = authority(connection, name)
|
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)
|
changed |= write(X509CRL_DIR / file, crls[name], 0o644) if crls[name] else remove(X509CRL_DIR / file)
|
||||||
if crls['root'] and crls['p2s']:
|
if crls['root'] and crls['p2s']:
|
||||||
changed |= write(OPENVPN_CRL, crls['root'] + crls['p2s'], 0o644)
|
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:
|
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
|
EXTRA_LOCAL_TS str, added to 'local_ts' on both connections
|
||||||
apply(ctx) called once the system has been configured, to apply
|
apply(ctx) called once the system has been configured, to apply
|
||||||
platform-specific state. Must be idempotent. ctx is the
|
platform-specific state. Must be idempotent. ctx is the
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
No platform-specific configuration is currently required.
|
No platform-specific configuration is currently required.
|
||||||
|
|
||||||
The Azure platform resolver 168.63.129.16 is deliberately *not* advertised to
|
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
|
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
|
forwards to the router's own systemd-resolved. That resolves
|
||||||
Azure-internal names without routing the wireserver address through the tunnel
|
Azure-internal names without routing the wireserver address through the tunnel
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
No platform-specific configuration is currently required.
|
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
|
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.
|
metadata resolver does not belong in the pool or in the traffic selectors.
|
||||||
"""
|
"""
|
||||||
@@ -65,16 +65,22 @@ import fcntl
|
|||||||
import grp
|
import grp
|
||||||
import ipaddress
|
import ipaddress
|
||||||
import json
|
import json
|
||||||
|
import plistlib
|
||||||
import pwd
|
import pwd
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import time
|
import time
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
import uuid
|
||||||
|
|
||||||
import ldap
|
import ldap
|
||||||
import ldap.filter
|
import ldap.filter
|
||||||
import ldap.modlist
|
import ldap.modlist
|
||||||
import pyotp
|
import pyotp
|
||||||
|
from cryptography import x509
|
||||||
|
from cryptography.hazmat.primitives import hashes, serialization
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import ec, rsa
|
||||||
|
from cryptography.hazmat.primitives.serialization import pkcs12
|
||||||
|
|
||||||
import vpnrouter
|
import vpnrouter
|
||||||
import vpnrouter_ldap
|
import vpnrouter_ldap
|
||||||
@@ -807,6 +813,80 @@ def profile(connection, name):
|
|||||||
return {'name': name, 'profile': text}
|
return {'name': name, 'profile': text}
|
||||||
|
|
||||||
|
|
||||||
|
def apple_profile(connection, name, password):
|
||||||
|
"""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
|
||||||
|
DNS name of the certificate, as the local identity, the certificate over EAP-TLS, and AES-256, SHA2-256 and group 14
|
||||||
|
for IKE and ESP. With p2s.dns_domains, the device resolves those domains with the router's resolver. The PKCS#12 uses 3DES and SHA-1, the only encryption Apple's clients read; they ask for the
|
||||||
|
password when the profile is installed.
|
||||||
|
"""
|
||||||
|
entry(connection, name)
|
||||||
|
if not isinstance(password, str) or not PASSWORD_MIN <= len(password) <= PASSWORD_MAX:
|
||||||
|
raise Failure(f'the password of the profile must have {PASSWORD_MIN} to {PASSWORD_MAX} characters', 2)
|
||||||
|
cfg = vpnrouter.load_config()
|
||||||
|
fqdn = cfg['wan']['local_fqdn'].strip()
|
||||||
|
if not cfg['p2s']['enabled'] or not fqdn:
|
||||||
|
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')
|
||||||
|
try:
|
||||||
|
chain = x509.load_pem_x509_certificates(vpnrouter_pki.bundle('p2s')['pem'].encode())
|
||||||
|
except vpnrouter_pki.Failure as failure:
|
||||||
|
raise Failure(str(failure), 4)
|
||||||
|
cert = x509.load_pem_x509_certificate(credentials[0])
|
||||||
|
key = serialization.load_pem_private_key(credentials[1], password=None)
|
||||||
|
encryption = (serialization.PrivateFormat.PKCS12.encryption_builder().kdf_rounds(2048)
|
||||||
|
.key_cert_algorithm(pkcs12.PBES.PBESv1SHA1And3KeyTripleDESCBC).hmac_hash(hashes.SHA1())
|
||||||
|
.build(password.encode()))
|
||||||
|
bundle = pkcs12.serialize_key_and_certificates(name.encode(), key, cert, None, encryption)
|
||||||
|
certificate_type = ('RSA' if isinstance(key, rsa.RSAPrivateKey)
|
||||||
|
else 'ECDSA384' if isinstance(key, ec.EllipticCurvePrivateKey) and key.curve.key_size == 384
|
||||||
|
else 'ECDSA256')
|
||||||
|
identifier = f'{".".join(reversed(fqdn.split(".")))}.vpn-router.{name}'
|
||||||
|
ids = {kind: str(uuid.uuid5(uuid.NAMESPACE_URL, f'{identifier}.{kind}')) for kind in ('profile', 'pkcs12', 'vpn')}
|
||||||
|
proposal = {'EncryptionAlgorithm': 'AES-256', 'IntegrityAlgorithm': 'SHA2-256', 'DiffieHellmanGroup': 14,
|
||||||
|
'LifeTimeInMinutes': 60}
|
||||||
|
payloads = [{'PayloadType': 'com.apple.security.root' if index == len(chain) - 1 else 'com.apple.security.pkcs1',
|
||||||
|
'PayloadVersion': 1, 'PayloadIdentifier': f'{identifier}.ca{index}',
|
||||||
|
'PayloadUUID': str(uuid.uuid5(uuid.NAMESPACE_URL, f'{identifier}.ca{index}')),
|
||||||
|
'PayloadDisplayName': vpnrouter_pki.common_name(item),
|
||||||
|
'PayloadContent': item.public_bytes(serialization.Encoding.DER)}
|
||||||
|
for index, item in enumerate(chain)]
|
||||||
|
payloads.append({'PayloadType': 'com.apple.security.pkcs12', 'PayloadVersion': 1,
|
||||||
|
'PayloadIdentifier': f'{identifier}.pkcs12', 'PayloadUUID': ids['pkcs12'],
|
||||||
|
'PayloadDisplayName': f'{name} ({fqdn})', 'PayloadContent': bundle})
|
||||||
|
# The router's resolver, which setup records, answers the names of p2s.dns_domains; the others stay with the device's.
|
||||||
|
try:
|
||||||
|
servers = json.loads(vpnrouter.WEB_STATE.read_text(encoding='utf-8'))['network']['client_dns']
|
||||||
|
except (OSError, ValueError, KeyError, TypeError):
|
||||||
|
servers = []
|
||||||
|
domains = [item.strip() for item in cfg['p2s']['dns_domains'] if item.strip()]
|
||||||
|
dns = {'DNS': {'ServerAddresses': servers, 'SupplementalMatchDomains': domains}} if servers and domains else {}
|
||||||
|
payloads.append({
|
||||||
|
**dns,
|
||||||
|
'PayloadType': 'com.apple.vpn.managed', 'PayloadVersion': 1, 'PayloadIdentifier': f'{identifier}.vpn',
|
||||||
|
'PayloadUUID': ids['vpn'], 'PayloadDisplayName': f'VPN Router {fqdn}', 'UserDefinedName': f'VPN Router {fqdn}',
|
||||||
|
'VPNType': 'IKEv2',
|
||||||
|
'IKEv2': {
|
||||||
|
'RemoteAddress': fqdn, 'RemoteIdentifier': fqdn, 'LocalIdentifier': name,
|
||||||
|
'AuthenticationMethod': 'Certificate', 'ExtendedAuthEnabled': 1, 'PayloadCertificateUUID': ids['pkcs12'],
|
||||||
|
'CertificateType': certificate_type,
|
||||||
|
'ServerCertificateIssuerCommonName': vpnrouter_pki.common_name(chain[0]), 'ServerCertificateCommonName': fqdn,
|
||||||
|
'IKESecurityAssociationParameters': {**proposal, 'LifeTimeInMinutes': 480},
|
||||||
|
'ChildSecurityAssociationParameters': proposal,
|
||||||
|
'EnablePFS': 0, 'DeadPeerDetectionRate': 'Medium', 'DisableMOBIKE': 0, 'DisableRedirect': 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
document = {'PayloadType': 'Configuration', 'PayloadVersion': 1, 'PayloadIdentifier': identifier,
|
||||||
|
'PayloadUUID': ids['profile'], 'PayloadDisplayName': f'VPN Router {fqdn} ({name})',
|
||||||
|
'PayloadDescription': f'IKEv2 access of {name} to {fqdn}', 'PayloadRemovalDisallowed': False,
|
||||||
|
'PayloadContent': payloads}
|
||||||
|
return {'name': name, 'profile': plistlib.dumps(document).decode()}
|
||||||
|
|
||||||
|
|
||||||
def group_dn(name):
|
def group_dn(name):
|
||||||
return f'cn={name},{vpnrouter_ldap.GROUPS}'
|
return f'cn={name},{vpnrouter_ldap.GROUPS}'
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# Wait until libpq is in the dynamic linker cache, where psycopg looks it up.
|
# 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=
|
reported=
|
||||||
until ldconfig -p | grep -q '^[[:space:]]*libpq\.so\.5 '; do
|
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
|
/backup the backup settings, the last run and the local archives: show, change and run
|
||||||
now (see backup())
|
now (see backup())
|
||||||
/remote-access
|
/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())
|
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
|
||||||
@@ -47,13 +47,16 @@ Pages:
|
|||||||
/metrics/history
|
/metrics/history
|
||||||
the metrics the metrics service keeps, for a view: hour, day, month or year (see
|
the metrics the metrics service keeps, for a view: hour, day, month or year (see
|
||||||
metrics_history())
|
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())
|
(see logs())
|
||||||
/sites the web sites Caddy serves: the settings of sites, list, add, change and remove (see sites()),
|
/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
|
and the files of each: list, download, upload, extract a zip archive, create a folder, rename and
|
||||||
remove (see Handler.site_files()); a member of a site's editors group may use the site's files
|
remove (see Handler.site_files()); a member of a site's editors group may use the site's files
|
||||||
/dns the name server of dns.zone: its settings, the records it serves, and the manual records: add and
|
/dns the primary name server of the router's domains: its settings, and the domains: add, change and
|
||||||
remove (see dns())
|
remove, each with its secondaries, the records it serves, and its manual records (see dns())
|
||||||
/postgresql
|
/postgresql
|
||||||
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
|
||||||
@@ -62,7 +65,7 @@ Pages:
|
|||||||
loopback address, where Caddy passes the requests of the HTTPS site; POST /oidc/login counts
|
loopback address, where Caddy passes the requests of the HTTPS site; POST /oidc/login counts
|
||||||
towards the limit of /auth calls
|
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;
|
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
|
||||||
@@ -79,7 +82,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
|
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
|
(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
|
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.
|
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
|
The service first initializes, without listening: it makes the signing key, the OpenID Provider's key
|
||||||
@@ -184,7 +187,7 @@ 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.
|
#: The sections of the remote access systems /remote-access changes: IKEv2, OpenVPN and WireGuard.
|
||||||
REMOTE_ACCESS = ('p2s', 'openvpn', '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.
|
||||||
@@ -490,7 +493,7 @@ def text_of(status):
|
|||||||
lines.append(f'Site to site: {"unknown" if established is None else established} of {connections} established')
|
lines.append(f'Site to site: {"unknown" if established is None else established} of {connections} established')
|
||||||
if 'p2s' in status:
|
if 'p2s' in status:
|
||||||
clients = status['p2s']['clients']
|
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:
|
if 'openvpn' in status:
|
||||||
clients = status['openvpn']['clients']
|
clients = status['openvpn']['clients']
|
||||||
lines.append(f'OpenVPN: {"unknown" if clients is None else f"{clients} connected"}')
|
lines.append(f'OpenVPN: {"unknown" if clients is None else f"{clients} connected"}')
|
||||||
@@ -565,7 +568,10 @@ def users(state, method, path, body):
|
|||||||
POST /users/<name>/unlock end a lockout after failed logins
|
POST /users/<name>/unlock end a lockout after failed logins
|
||||||
DELETE /users/<name> remove the user and revoke the certificate
|
DELETE /users/<name> remove the user and revoke the certificate
|
||||||
GET /users/<name>/profile the OpenVPN client profile
|
GET /users/<name>/profile the OpenVPN client profile
|
||||||
POST /users/<name>/otp a new TOTP secret and its otpauth URI; nothing is stored
|
POST /users/<name>/apple-profile
|
||||||
|
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
|
PUT /users/<name>/otp store a TOTP secret: {"secret", "code"}, code a code of the secret
|
||||||
DELETE /users/<name>/otp remove the TOTP token
|
DELETE /users/<name>/otp remove the TOTP token
|
||||||
DELETE /users/<name>/passkeys remove all the user's passkeys
|
DELETE /users/<name>/passkeys remove all the user's passkeys
|
||||||
@@ -592,6 +598,8 @@ def users(state, method, path, body):
|
|||||||
('POST', 3, 'unlock'): lambda connection: vpnrouter_users.unlock(connection, name),
|
('POST', 3, 'unlock'): lambda connection: vpnrouter_users.unlock(connection, name),
|
||||||
('DELETE', 2, None): lambda connection: vpnrouter_users.remove(connection, name),
|
('DELETE', 2, None): lambda connection: vpnrouter_users.remove(connection, name),
|
||||||
('GET', 3, 'profile'): lambda connection: vpnrouter_users.profile(connection, name),
|
('GET', 3, 'profile'): lambda connection: vpnrouter_users.profile(connection, name),
|
||||||
|
('POST', 3, 'apple-profile'): lambda connection: vpnrouter_users.apple_profile(connection, name,
|
||||||
|
body.get('password')),
|
||||||
('POST', 3, 'otp'): lambda connection: vpnrouter_users.otp_new(connection, name),
|
('POST', 3, 'otp'): lambda connection: vpnrouter_users.otp_new(connection, name),
|
||||||
('PUT', 3, 'otp'): lambda connection: vpnrouter_users.otp_enrol(connection, name, body.get('secret'), body.get('code')),
|
('PUT', 3, 'otp'): lambda connection: vpnrouter_users.otp_enrol(connection, name, body.get('secret'), body.get('code')),
|
||||||
('DELETE', 3, 'otp'): lambda connection: vpnrouter_users.otp_remove(connection, name),
|
('DELETE', 3, 'otp'): lambda connection: vpnrouter_users.otp_remove(connection, name),
|
||||||
@@ -888,9 +896,10 @@ def backup(method, path, body):
|
|||||||
|
|
||||||
|
|
||||||
def wireguard_server(cfg, state):
|
def wireguard_server(cfg, state):
|
||||||
"""What a WireGuard peer's configuration needs of the router: {public_key, endpoint, allowed_ips}; public_key is
|
"""What a WireGuard peer's configuration needs of the router: {public_key, endpoint, allowed_ips, dns}; public_key is
|
||||||
null before setup made the key pair, endpoint, <wan.local_fqdn>:<wireguard.listen_port>, null without a name, and
|
null before setup made the key pair, endpoint, <wan.local_fqdn>:<wireguard.listen_port>, null without a name,
|
||||||
allowed_ips the networks reached through the tunnel, the router's local CIDRs and the WireGuard network."""
|
allowed_ips the networks reached through the tunnel, the router's local CIDRs and the WireGuard network, and dns the
|
||||||
|
addresses of the remote access clients' resolver, [] while it does not run."""
|
||||||
try:
|
try:
|
||||||
public_key = vpnrouter.WG_PUBLIC_KEY.read_text(encoding='utf-8').strip() or None
|
public_key = vpnrouter.WG_PUBLIC_KEY.read_text(encoding='utf-8').strip() or None
|
||||||
except OSError:
|
except OSError:
|
||||||
@@ -901,8 +910,10 @@ def wireguard_server(cfg, state):
|
|||||||
allowed = [cidr for cidr in networks if isinstance(cidr, str)] if isinstance(networks, list) else []
|
allowed = [cidr for cidr in networks if isinstance(cidr, str)] if isinstance(networks, list) else []
|
||||||
if address:
|
if address:
|
||||||
allowed.append(str(ipaddress.ip_interface(address).network))
|
allowed.append(str(ipaddress.ip_interface(address).network))
|
||||||
|
dns = section(state, 'network').get('client_dns')
|
||||||
return {'public_key': public_key, 'endpoint': f'{fqdn}:{cfg["wireguard"]["listen_port"]}' if fqdn else None,
|
return {'public_key': public_key, 'endpoint': f'{fqdn}:{cfg["wireguard"]["listen_port"]}' if fqdn else None,
|
||||||
'allowed_ips': list(dict.fromkeys(allowed))}
|
'allowed_ips': list(dict.fromkeys(allowed)),
|
||||||
|
'dns': [address for address in dns if isinstance(address, str)] if isinstance(dns, list) else []}
|
||||||
|
|
||||||
|
|
||||||
def remote_access(state, method, path, body):
|
def remote_access(state, method, path, body):
|
||||||
@@ -1040,8 +1051,15 @@ def account(name, method, path, body):
|
|||||||
PUT /account/passkeys store the passkey: {"id", "credential", "label"?}, credential the browser's answer
|
PUT /account/passkeys store the passkey: {"id", "credential", "label"?}, credential the browser's answer
|
||||||
DELETE /account/passkeys/<id>
|
DELETE /account/passkeys/<id>
|
||||||
remove a passkey
|
remove a passkey
|
||||||
|
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 access:
|
||||||
|
{"current_password", "current_code"?, "password"}, password encrypting the user's key in
|
||||||
|
it; {"name", "profile"}
|
||||||
|
|
||||||
A change but the removal of a passkey needs the current password and, for an enrolled user, a current code; a wrong
|
A change but the removal of a passkey, and the profiles, need the current password and, for an enrolled user, a
|
||||||
|
current code; a wrong
|
||||||
one is 403 and counts as a failed login. A password change revokes the user's refresh tokens.
|
one is 403 and counts as a failed login. A password change revokes the user's refresh tokens.
|
||||||
"""
|
"""
|
||||||
if body == 'invalid' or (body is not None and not isinstance(body, dict)):
|
if body == 'invalid' or (body is not None and not isinstance(body, dict)):
|
||||||
@@ -1050,12 +1068,14 @@ def account(name, method, path, body):
|
|||||||
route = (method, path.rstrip('/'))
|
route = (method, path.rstrip('/'))
|
||||||
removing = method == 'DELETE' and path.startswith('/account/passkeys/') and len(path.strip('/').split('/')) == 3
|
removing = method == 'DELETE' and path.startswith('/account/passkeys/') and len(path.strip('/').split('/')) == 3
|
||||||
if not removing and route not in (('GET', '/account'), ('PUT', '/account/password'), ('POST', '/account/otp'),
|
if not removing and route not in (('GET', '/account'), ('PUT', '/account/password'), ('POST', '/account/otp'),
|
||||||
('PUT', '/account/otp'), ('POST', '/account/passkeys'), ('PUT', '/account/passkeys')):
|
('PUT', '/account/otp'), ('POST', '/account/passkeys'), ('PUT', '/account/passkeys'),
|
||||||
|
('POST', '/account/profile'), ('POST', '/account/apple-profile')):
|
||||||
return 404, {'error': f'no route {method} {path}'}
|
return 404, {'error': f'no route {method} {path}'}
|
||||||
try:
|
try:
|
||||||
if route == ('PUT', '/account/password'):
|
if route == ('PUT', '/account/password'):
|
||||||
vpnrouter_users.check_password(body.get('password'))
|
vpnrouter_users.check_password(body.get('password'))
|
||||||
if route in (('PUT', '/account/password'), ('PUT', '/account/otp'), ('POST', '/account/passkeys')):
|
if route in (('PUT', '/account/password'), ('PUT', '/account/otp'), ('POST', '/account/passkeys'),
|
||||||
|
('POST', '/account/profile'), ('POST', '/account/apple-profile')):
|
||||||
vpnrouter_users.login(name, body.get('current_password'), body.get('current_code'))
|
vpnrouter_users.login(name, body.get('current_password'), body.get('current_code'))
|
||||||
with vpnrouter_users.session() as connection:
|
with vpnrouter_users.session() as connection:
|
||||||
if route == ('GET', '/account'):
|
if route == ('GET', '/account'):
|
||||||
@@ -1065,6 +1085,10 @@ def account(name, method, path, body):
|
|||||||
document = vpnrouter_users.passwd(connection, name, body['password'])
|
document = vpnrouter_users.passwd(connection, name, body['password'])
|
||||||
elif route == ('POST', '/account/otp'):
|
elif route == ('POST', '/account/otp'):
|
||||||
document = vpnrouter_users.otp_new(connection, name)
|
document = vpnrouter_users.otp_new(connection, name)
|
||||||
|
elif route == ('POST', '/account/profile'):
|
||||||
|
document = vpnrouter_users.profile(connection, name)
|
||||||
|
elif route == ('POST', '/account/apple-profile'):
|
||||||
|
document = vpnrouter_users.apple_profile(connection, name, body.get('password'))
|
||||||
elif route == ('POST', '/account/passkeys'):
|
elif route == ('POST', '/account/passkeys'):
|
||||||
document = vpnrouter_passkeys.register_begin(connection, name)
|
document = vpnrouter_passkeys.register_begin(connection, name)
|
||||||
elif route == ('PUT', '/account/passkeys'):
|
elif route == ('PUT', '/account/passkeys'):
|
||||||
@@ -1243,44 +1267,69 @@ def site_file_action(name, target, body):
|
|||||||
def dns(method, path, query, body):
|
def dns(method, path, query, body):
|
||||||
"""Answer a /dns request with vpnrouter_dns: the HTTP status and the JSON document.
|
"""Answer a /dns request with vpnrouter_dns: the HTTP status and the JSON document.
|
||||||
|
|
||||||
GET /dns {"settings", "installed", "running", "records", "manual", "problem"}: the dns section of the
|
GET /dns {"settings", "installed", "running", "domains", "problem"}: the dns section of the
|
||||||
configuration, whether PowerDNS is installed and runs, the records the name server serves,
|
configuration, whether PowerDNS is installed and runs, the domains, each {"name", "sites",
|
||||||
the manual records, and why the records cannot be read, null when they can
|
"ttl", "secondaries", "manual", "records"} with its secondaries {"nameserver", "address"},
|
||||||
|
its manual records and the records the name server serves, and why the domains cannot be
|
||||||
|
read, null when they can
|
||||||
PUT /dns change any of the settings of the dns section
|
PUT /dns change any of the settings of the dns section
|
||||||
POST /dns/records add a manual record: {"name", "type", "content", "ttl"?}; name is relative to dns.zone, @
|
POST /dns/domains add a domain: {"name", "sites"?, "ttl"?}; sites gives the host names of the enabled sites
|
||||||
for the zone itself, or absolute with a final dot; ttl is dns.ttl without it
|
in it records; ttl is dns.ttl when null or absent; an existing name is 409
|
||||||
DELETE /dns/records remove the manual records of name= and type=, with content= only that one
|
PUT /dns/domains/<domain>
|
||||||
|
change "sites" or "ttl" of the domain
|
||||||
|
DELETE /dns/domains/<domain>
|
||||||
|
remove the domain with its secondaries and manual records
|
||||||
|
POST /dns/domains/<domain>/secondaries
|
||||||
|
add a secondary name server of the domain, or change its address: {"nameserver", "address"}
|
||||||
|
DELETE /dns/domains/<domain>/secondaries/<nameserver>
|
||||||
|
remove the secondary name server
|
||||||
|
POST /dns/domains/<domain>/records
|
||||||
|
add a manual record: {"name", "type", "content", "ttl"?}; name is relative to the domain,
|
||||||
|
@ for the domain itself, or absolute with a final dot; ttl is the domain's without it
|
||||||
|
DELETE /dns/domains/<domain>/records
|
||||||
|
remove the manual records of name= and type=, with content= only that one
|
||||||
|
|
||||||
A change is applied by restarting vpn-router-setup without waiting for 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)):
|
if body == 'invalid' or (body is not None and not isinstance(body, dict)):
|
||||||
return 400, {'error': 'the body must be a JSON object'}
|
return 400, {'error': 'the body must be a JSON object'}
|
||||||
body = body or {}
|
body = body or {}
|
||||||
|
parts = path.strip('/').split('/')
|
||||||
|
route = (method, *parts[:2], *parts[3:4])
|
||||||
try:
|
try:
|
||||||
cfg = vpnrouter.load_config()
|
cfg = vpnrouter.load_config()
|
||||||
if (method, path) == ('GET', '/dns'):
|
if route == ('GET', 'dns'):
|
||||||
served, manual, problem = [], [], None
|
domains, problem = [], None
|
||||||
try:
|
try:
|
||||||
manual = vpnrouter_dns.manual()
|
domains = vpnrouter_dns.listing()
|
||||||
served = vpnrouter_dns.served(cfg['dns']['zone'].strip())
|
|
||||||
except vpnrouter_dns.DatabaseError as exc:
|
except vpnrouter_dns.DatabaseError as exc:
|
||||||
problem = str(exc)
|
problem = str(exc)
|
||||||
return 200, {'settings': cfg['dns'], 'installed': vpnrouter_dns.PDNS_SERVER.exists(),
|
return 200, {'settings': cfg['dns'], 'installed': vpnrouter_dns.PDNS_SERVER.exists(),
|
||||||
'running': unit_state(vpnrouter_dns.UNIT) == 'active', 'records': served, 'manual': manual,
|
'running': unit_state(vpnrouter_dns.UNIT) == 'active', 'domains': domains,
|
||||||
'problem': problem}
|
'problem': problem}
|
||||||
if (method, path) == ('PUT', '/dns'):
|
if route == ('PUT', 'dns'):
|
||||||
changed = section_change('dns', body)
|
changed = section_change('dns', body)
|
||||||
return changed if isinstance(changed, tuple) else (200, {'settings': changed})
|
return changed if isinstance(changed, tuple) else (200, {'settings': changed})
|
||||||
if (method, path) == ('POST', '/dns/records'):
|
if route == ('POST', 'dns', 'domains') and len(parts) == 2:
|
||||||
document, code = vpnrouter_dns.add(cfg, body), 201
|
document, code = vpnrouter_dns.domain_add(body), 201
|
||||||
elif (method, path) == ('DELETE', '/dns/records'):
|
elif route == ('PUT', 'dns', 'domains') and len(parts) == 3:
|
||||||
|
document, code = vpnrouter_dns.domain_change(parts[2], body), 200
|
||||||
|
elif route == ('DELETE', 'dns', 'domains') and len(parts) == 3:
|
||||||
|
document, code = vpnrouter_dns.domain_remove(parts[2]), 200
|
||||||
|
elif route == ('POST', 'dns', 'domains', 'secondaries') and len(parts) == 4:
|
||||||
|
document, code = vpnrouter_dns.secondary_add(parts[2], body), 201
|
||||||
|
elif route == ('DELETE', 'dns', 'domains', 'secondaries') and len(parts) == 5:
|
||||||
|
document, code = vpnrouter_dns.secondary_remove(parts[2], parts[4]), 200
|
||||||
|
elif route == ('POST', 'dns', 'domains', 'records') and len(parts) == 4:
|
||||||
|
document, code = vpnrouter_dns.add(cfg, parts[2], body), 201
|
||||||
|
elif route == ('DELETE', 'dns', 'domains', 'records') and len(parts) == 4:
|
||||||
content = query.get('content', [None])[0]
|
content = query.get('content', [None])[0]
|
||||||
document = vpnrouter_dns.remove(cfg, query.get('name', [''])[0], query.get('type', [''])[0], content)
|
document = vpnrouter_dns.remove(parts[2], query.get('name', [''])[0], query.get('type', [''])[0], content)
|
||||||
code = 200
|
code = 200
|
||||||
else:
|
else:
|
||||||
return 404, {'error': f'no route {method} {path}'}
|
return 404, {'error': f'no route {method} {path}'}
|
||||||
except vpnrouter_dns.Failure as failure:
|
except vpnrouter_dns.Failure as failure:
|
||||||
return (404 if failure.status == 3 else 400), {'error': str(failure)}
|
return {3: 404, 4: 409}.get(failure.status, 400), {'error': str(failure)}
|
||||||
except vpnrouter_dns.DatabaseError as exc:
|
except vpnrouter_dns.DatabaseError as exc:
|
||||||
return 500, {'error': str(exc)}
|
return 500, {'error': str(exc)}
|
||||||
except SystemExit as exc:
|
except SystemExit as exc:
|
||||||
@@ -1305,6 +1354,39 @@ def metrics_history(view):
|
|||||||
return 500, {'error': str(exc.code)}
|
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):
|
def changes_admins(connection, method, parts, body):
|
||||||
"""Whether a /users, /service-accounts or /groups request creates, renames to, changes or removes ADMIN_GROUP, or
|
"""Whether a /users, /service-accounts or /groups request creates, renames to, changes or removes ADMIN_GROUP, or
|
||||||
removes a member of it."""
|
removes a member of it."""
|
||||||
@@ -1773,7 +1855,8 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
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', 'remote-access', 'logs', 'sites', 'dns')
|
'ca', 'shell', 'remote-access', 'logs', 'sites', 'dns')
|
||||||
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', '/metrics/settings', '/postgresql') or \
|
||||||
|
directory is not None
|
||||||
signing = path in ('/auth', '/auth/passkey', '/auth/passkey/complete', '/auth/refresh', '/auth/logout')
|
signing = path in ('/auth', '/auth/passkey', '/auth/passkey/complete', '/auth/refresh', '/auth/logout')
|
||||||
oidc = (path == '/.well-known/openid-configuration' or path.startswith('/oidc/')) and \
|
oidc = (path == '/.well-known/openid-configuration' or path.startswith('/oidc/')) and \
|
||||||
self.server.server_address[0] == vpnrouter_net.LOOPBACK
|
self.server.server_address[0] == vpnrouter_net.LOOPBACK
|
||||||
@@ -1815,6 +1898,9 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
query = urllib.parse.parse_qs(urllib.parse.urlsplit(self.path).query)
|
query = urllib.parse.parse_qs(urllib.parse.urlsplit(self.path).query)
|
||||||
code, document = metrics_history(query.get('range', ['hour'])[0])
|
code, document = metrics_history(query.get('range', ['hour'])[0])
|
||||||
body, kind = json.dumps(document).encode() + b'\n', 'application/json'
|
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'):
|
elif directory == 'shell' and (path, self.command) == ('/shell', 'GET'):
|
||||||
return self.shell(claims.get('sub'))
|
return self.shell(claims.get('sub'))
|
||||||
elif directory == 'shell':
|
elif directory == 'shell':
|
||||||
|
|||||||
@@ -89,7 +89,6 @@
|
|||||||
},
|
},
|
||||||
"dns": {
|
"dns": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"zone": "",
|
|
||||||
"nameserver": "",
|
"nameserver": "",
|
||||||
"addresses": [],
|
"addresses": [],
|
||||||
"listen": "external",
|
"listen": "external",
|
||||||
|
|||||||
@@ -1,48 +1,106 @@
|
|||||||
-- The zone the router's name server (PowerDNS, gpgsql backend) answers for (vpnrouter_dns), in the vpn-router-dns
|
-- The domains the router's name server (PowerDNS, gpgsql backend) is the primary of (vpnrouter_dns), in the
|
||||||
-- database, whose domains and records tables are PowerDNS's own schema.
|
-- vpn-router-dns database, whose domains, records and domainmetadata tables are PowerDNS's own schema.
|
||||||
-- The whole file runs as one transaction, serialized by an advisory lock, so that two processes can load it at once.
|
-- The whole file runs as one transaction, serialized by an advisory lock, so that two processes can load it at once.
|
||||||
|
|
||||||
SELECT pg_advisory_xact_lock(hashtext('vpn-router dns schema'));
|
SELECT pg_advisory_xact_lock(hashtext('vpn-router dns schema'));
|
||||||
|
|
||||||
-- A record an administrator added to the zone, besides those of the zone itself and of the sites.
|
DROP TABLE IF EXISTS dns_manual_records;
|
||||||
CREATE TABLE IF NOT EXISTS dns_manual_records (
|
|
||||||
|
-- A domain the router serves as its primary name server; sites says whether the host names of the enabled sites in
|
||||||
|
-- it get records, ttl is the TTL of its records, null for dns.ttl.
|
||||||
|
CREATE TABLE IF NOT EXISTS dns_domains (
|
||||||
|
name text PRIMARY KEY,
|
||||||
|
sites boolean NOT NULL,
|
||||||
|
ttl integer
|
||||||
|
);
|
||||||
|
|
||||||
|
-- A secondary name server of a domain: its NS name and the address it transfers the domain from.
|
||||||
|
CREATE TABLE IF NOT EXISTS dns_secondaries (
|
||||||
|
domain text NOT NULL REFERENCES dns_domains (name) ON DELETE CASCADE,
|
||||||
|
nameserver text NOT NULL,
|
||||||
|
address inet NOT NULL,
|
||||||
|
PRIMARY KEY (domain, nameserver)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- A record an administrator added to a domain, besides those of the domain itself and of the sites.
|
||||||
|
CREATE TABLE IF NOT EXISTS dns_records (
|
||||||
|
domain text NOT NULL REFERENCES dns_domains (name) ON DELETE CASCADE,
|
||||||
name text NOT NULL,
|
name text NOT NULL,
|
||||||
type text NOT NULL,
|
type text NOT NULL,
|
||||||
content text NOT NULL,
|
content text NOT NULL,
|
||||||
ttl integer NOT NULL,
|
ttl integer NOT NULL,
|
||||||
PRIMARY KEY (name, type, content)
|
PRIMARY KEY (domain, name, type, content)
|
||||||
);
|
);
|
||||||
|
|
||||||
-- The digest of the records last written to the zone and the serial of its SOA.
|
-- The digest of the records last written to a zone and the serial of its SOA.
|
||||||
CREATE TABLE IF NOT EXISTS dns_state (
|
CREATE TABLE IF NOT EXISTS dns_state (
|
||||||
zone text PRIMARY KEY,
|
zone text PRIMARY KEY,
|
||||||
digest text NOT NULL,
|
digest text NOT NULL,
|
||||||
serial bigint NOT NULL
|
serial bigint NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION dns_manual_list()
|
CREATE OR REPLACE FUNCTION dns_domains_list()
|
||||||
RETURNS TABLE (name text, type text, content text, ttl integer)
|
RETURNS TABLE (name text, sites boolean, ttl integer)
|
||||||
LANGUAGE sql STABLE AS $$
|
LANGUAGE sql STABLE AS $$
|
||||||
SELECT name, type, content, ttl FROM dns_manual_records ORDER BY name, type, content;
|
SELECT name, sites, ttl FROM dns_domains ORDER BY name;
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
-- Add the record, or set its TTL.
|
-- Add the domain, or set its sites and TTL.
|
||||||
CREATE OR REPLACE PROCEDURE dns_manual_put(p_name text, p_type text, p_content text, p_ttl integer)
|
CREATE OR REPLACE PROCEDURE dns_domain_put(p_name text, p_sites boolean, p_ttl integer)
|
||||||
LANGUAGE sql AS $$
|
LANGUAGE sql AS $$
|
||||||
INSERT INTO dns_manual_records VALUES (p_name, p_type, p_content, p_ttl)
|
INSERT INTO dns_domains VALUES (p_name, p_sites, p_ttl)
|
||||||
ON CONFLICT (name, type, content) DO UPDATE SET ttl = p_ttl;
|
ON CONFLICT (name) DO UPDATE SET sites = p_sites, ttl = p_ttl;
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
-- Remove the records of the name and type, all of them without content.
|
-- Remove the domain with its secondaries and records.
|
||||||
CREATE OR REPLACE PROCEDURE dns_manual_remove(p_name text, p_type text, p_content text)
|
CREATE OR REPLACE PROCEDURE dns_domain_remove(p_name text)
|
||||||
LANGUAGE sql AS $$
|
LANGUAGE sql AS $$
|
||||||
DELETE FROM dns_manual_records WHERE name = p_name AND type = p_type AND (p_content IS NULL OR content = p_content);
|
DELETE FROM dns_domains WHERE name = p_name;
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
-- Make the zone the only one served and give it the records, its SOA first, whose content has {serial} for the serial.
|
CREATE OR REPLACE FUNCTION dns_secondaries_list()
|
||||||
-- The serial grows, to at least the current Unix time, only when the records differ from those last written.
|
RETURNS TABLE (domain text, nameserver text, address text)
|
||||||
|
LANGUAGE sql STABLE AS $$
|
||||||
|
SELECT domain, nameserver, host(address) FROM dns_secondaries ORDER BY domain, nameserver;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- Add the secondary of the domain, or set its address.
|
||||||
|
CREATE OR REPLACE PROCEDURE dns_secondary_put(p_domain text, p_nameserver text, p_address inet)
|
||||||
|
LANGUAGE sql AS $$
|
||||||
|
INSERT INTO dns_secondaries VALUES (p_domain, p_nameserver, p_address)
|
||||||
|
ON CONFLICT (domain, nameserver) DO UPDATE SET address = p_address;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE OR REPLACE PROCEDURE dns_secondary_remove(p_domain text, p_nameserver text)
|
||||||
|
LANGUAGE sql AS $$
|
||||||
|
DELETE FROM dns_secondaries WHERE domain = p_domain AND nameserver = p_nameserver;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION dns_records_list()
|
||||||
|
RETURNS TABLE (domain text, name text, type text, content text, ttl integer)
|
||||||
|
LANGUAGE sql STABLE AS $$
|
||||||
|
SELECT domain, name, type, content, ttl FROM dns_records ORDER BY domain, name, type, content;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- Add the record to the domain, or set its TTL.
|
||||||
|
CREATE OR REPLACE PROCEDURE dns_record_put(p_domain text, p_name text, p_type text, p_content text, p_ttl integer)
|
||||||
|
LANGUAGE sql AS $$
|
||||||
|
INSERT INTO dns_records VALUES (p_domain, p_name, p_type, p_content, p_ttl)
|
||||||
|
ON CONFLICT (domain, name, type, content) DO UPDATE SET ttl = p_ttl;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- Remove the records of the name and type in the domain, all of them without content.
|
||||||
|
CREATE OR REPLACE PROCEDURE dns_record_remove(p_domain text, p_name text, p_type text, p_content text)
|
||||||
|
LANGUAGE sql AS $$
|
||||||
|
DELETE FROM dns_records
|
||||||
|
WHERE domain = p_domain AND name = p_name AND type = p_type AND (p_content IS NULL OR content = p_content);
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- Serve the zone as its primary and give it the records, its SOA first, whose content has {serial} for the serial,
|
||||||
|
-- and the addresses of its secondaries, which are notified of its changes and may transfer it. The serial grows, to
|
||||||
|
-- at least the current Unix time, only when the records differ from those last written.
|
||||||
CREATE OR REPLACE PROCEDURE dns_apply(p_zone text, p_soa text, p_ttl integer, p_names text[], p_types text[],
|
CREATE OR REPLACE PROCEDURE dns_apply(p_zone text, p_soa text, p_ttl integer, p_names text[], p_types text[],
|
||||||
p_contents text[], p_ttls integer[])
|
p_contents text[], p_ttls integer[], p_secondaries text[])
|
||||||
LANGUAGE plpgsql AS $$
|
LANGUAGE plpgsql AS $$
|
||||||
DECLARE
|
DECLARE
|
||||||
v_domain integer;
|
v_domain integer;
|
||||||
@@ -50,12 +108,17 @@ DECLARE
|
|||||||
v_digest text := md5(concat_ws('|', p_soa, p_ttl, array_to_string(p_names, ','), array_to_string(p_types, ','),
|
v_digest text := md5(concat_ws('|', p_soa, p_ttl, array_to_string(p_names, ','), array_to_string(p_types, ','),
|
||||||
array_to_string(p_contents, ','), array_to_string(p_ttls, ',')));
|
array_to_string(p_contents, ','), array_to_string(p_ttls, ',')));
|
||||||
BEGIN
|
BEGIN
|
||||||
DELETE FROM domains WHERE name <> p_zone;
|
|
||||||
DELETE FROM dns_state WHERE zone <> p_zone;
|
|
||||||
SELECT id INTO v_domain FROM domains WHERE name = p_zone;
|
SELECT id INTO v_domain FROM domains WHERE name = p_zone;
|
||||||
IF v_domain IS NULL THEN
|
IF v_domain IS NULL THEN
|
||||||
INSERT INTO domains (name, type) VALUES (p_zone, 'NATIVE') RETURNING id INTO v_domain;
|
INSERT INTO domains (name, type) VALUES (p_zone, 'MASTER') RETURNING id INTO v_domain;
|
||||||
ELSIF EXISTS (SELECT 1 FROM dns_state WHERE zone = p_zone AND digest = v_digest) THEN
|
ELSE
|
||||||
|
UPDATE domains SET type = 'MASTER' WHERE id = v_domain AND type <> 'MASTER';
|
||||||
|
END IF;
|
||||||
|
DELETE FROM domainmetadata WHERE domain_id = v_domain AND kind IN ('ALSO-NOTIFY', 'ALLOW-AXFR-FROM');
|
||||||
|
INSERT INTO domainmetadata (domain_id, kind, content)
|
||||||
|
SELECT v_domain, kind, address FROM unnest(p_secondaries) AS address,
|
||||||
|
unnest(ARRAY['ALSO-NOTIFY', 'ALLOW-AXFR-FROM']) AS kind;
|
||||||
|
IF EXISTS (SELECT 1 FROM dns_state WHERE zone = p_zone AND digest = v_digest) THEN
|
||||||
RETURN;
|
RETURN;
|
||||||
END IF;
|
END IF;
|
||||||
SELECT greatest(coalesce(max(serial) + 1, 1), extract(epoch FROM now())::bigint) INTO v_serial
|
SELECT greatest(coalesce(max(serial) + 1, 1), extract(epoch FROM now())::bigint) INTO v_serial
|
||||||
@@ -71,11 +134,11 @@ BEGIN
|
|||||||
END;
|
END;
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
-- Serve no zone.
|
-- Serve only the zones of the list, none for an empty one.
|
||||||
CREATE OR REPLACE PROCEDURE dns_clear()
|
CREATE OR REPLACE PROCEDURE dns_retain(p_zones text[])
|
||||||
LANGUAGE sql AS $$
|
LANGUAGE sql AS $$
|
||||||
DELETE FROM domains;
|
DELETE FROM domains WHERE name <> ALL (p_zones);
|
||||||
DELETE FROM dns_state;
|
DELETE FROM dns_state WHERE zone <> ALL (p_zones);
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
-- The records of the zone as they are served.
|
-- The records of the zone as they are served.
|
||||||
|
|||||||
@@ -39,5 +39,9 @@ pools {
|
|||||||
rw-pool {
|
rw-pool {
|
||||||
addrs = {{ p2s_address_pool }}
|
addrs = {{ p2s_address_pool }}
|
||||||
dns = {{ p2s_dns }}
|
dns = {{ p2s_dns }}
|
||||||
|
{%- if p2s_dns_domains %}
|
||||||
|
# INTERNAL_DNS_DOMAIN (RFC 8598): the domains the clients resolve with the DNS servers above.
|
||||||
|
25 = {{ p2s_dns_domains | join(', ') }}
|
||||||
|
{%- endif %}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -45,7 +45,7 @@ variable "vm_size" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
variable "local_fqdn" {
|
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
|
type = string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,13 +89,13 @@ variable "psk" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
variable "p2s_enabled" {
|
variable "p2s_enabled" {
|
||||||
description = "Enable road-warrior (P2S) access."
|
description = "Enable IKEv2 access."
|
||||||
type = bool
|
type = bool
|
||||||
default = false
|
default = false
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "p2s_address_pool" {
|
variable "p2s_address_pool" {
|
||||||
description = "CIDR block assigned to road-warrior clients."
|
description = "CIDR block assigned to IKEv2 clients."
|
||||||
type = string
|
type = string
|
||||||
default = ""
|
default = ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ variable "machine_type" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
variable "local_fqdn" {
|
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
|
type = string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,13 +86,13 @@ variable "psk" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
variable "p2s_enabled" {
|
variable "p2s_enabled" {
|
||||||
description = "Enable road-warrior (P2S) access."
|
description = "Enable IKEv2 access."
|
||||||
type = bool
|
type = bool
|
||||||
default = false
|
default = false
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "p2s_address_pool" {
|
variable "p2s_address_pool" {
|
||||||
description = "CIDR block assigned to road-warrior clients."
|
description = "CIDR block assigned to IKEv2 clients."
|
||||||
type = string
|
type = string
|
||||||
default = ""
|
default = ""
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user