Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57e8bbcffe | ||
|
|
9ca8fce14e | ||
|
|
74c24d7bc6 | ||
|
|
770332728e |
No files matched your search
@@ -1176,8 +1176,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 road-warrior 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: Road-warrior clients get split DNS domains, p2s.dns_domains
|
||||||
|
|
||||||
|
Status: Accepted
|
||||||
|
|
||||||
|
Context: The road-warrior access is a split tunnel. macOS and iOS use the DNS server an IKEv2 VPN
|
||||||
|
pushes only for queries scoped to the tunnel, so names only the router resolves, such as Azure's
|
||||||
|
internal names, fail. RFC 8598 defines INTERNAL_DNS_DOMAIN (attribute 25): the client resolves the
|
||||||
|
listed domains and their subdomains with the VPN's servers. strongSwan's pools take numeric
|
||||||
|
attributes with string values; Apple profiles name such domains in `SupplementalMatchDomains`.
|
||||||
|
|
||||||
|
Decision: The setting `p2s.dns_domains` lists the domains; setup pushes each as attribute 25 of the
|
||||||
|
road-warrior pool, and the Apple VPN profile gives them as match domains of the router's resolver,
|
||||||
|
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.
|
||||||
@@ -115,7 +115,8 @@ must have the type shown below; an unknown section or setting is an error.
|
|||||||
| `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` | Road-warrior access |
|
||||||
| `p2s` | `address_pool` | string | | CIDR assigned to road-warrior clients |
|
| `p2s` | `address_pool` | string | | CIDR assigned to road-warrior 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 road-warrior clients resolve with the router's resolver (INTERNAL_DNS_DOMAIN, RFC 8598, and the match domains of the Apple VPN profile), at most 16 |
|
||||||
|
| `p2s` | `dns_listen` | string | `internal` | The addresses of the resolver the road-warrior, OpenVPN and WireGuard clients are given: `internal`, `external` or `both` |
|
||||||
| `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 |
|
||||||
@@ -776,6 +777,26 @@ the file.
|
|||||||
The certificate is also accepted by road-warrior (IKEv2 EAP-TLS) access, which checks
|
The certificate is also accepted by road-warrior (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 road-warrior access in the built-in VPN client of macOS, iOS and iPadOS. It holds:
|
||||||
|
|
||||||
|
- the P2S CA and the root CA, the root installed as a trusted root;
|
||||||
|
- the user's certificate and key as a PKCS#12 encrypted with the password of the request, with
|
||||||
|
3DES and SHA-1, the only encryption Apple's clients read; the device asks for the password when
|
||||||
|
the profile is installed, and the router does not keep it;
|
||||||
|
- an IKEv2 VPN to `wan.local_fqdn`, with the user name as the local identity, the certificate over
|
||||||
|
EAP-TLS, and AES-256, SHA2-256 and group 14 for IKE and ESP, as the router's road-warrior
|
||||||
|
connection takes them;
|
||||||
|
- 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 +893,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. Road-warrior, 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
|
||||||
@@ -1180,6 +1202,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 road-warrior 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 |
|
||||||
@@ -1236,6 +1259,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 road-warrior 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
|
||||||
@@ -1310,7 +1335,7 @@ 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
|
||||||
@@ -1575,7 +1600,7 @@ missing, without the recommended bind backend.
|
|||||||
|
|
||||||
Records added with `/dns` or the Router Manager (A, AAAA, CNAME, TXT, MX, SRV, CAA) are served
|
Records added with `/dns` or the Router Manager (A, AAAA, CNAME, TXT, MX, SRV, CAA) are served
|
||||||
too, but not those of a name and type the zone's own records have. Setup writes the zone at each
|
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,
|
run; the SOA's serial grows only when the records change. While P2S, OpenVPN or WireGuard is enabled,
|
||||||
dnsmasq answers their clients on port 53 of the addresses of `p2s.dns_listen`, by default the
|
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 must delegate `dns.zone` to `dns.nameserver`, with glue records for
|
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
|
||||||
a name server inside the zone.
|
a name server inside the zone.
|
||||||
|
|||||||
@@ -1,3 +1,52 @@
|
|||||||
|
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.
|
||||||
|
|||||||
@@ -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']
|
||||||
@@ -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': []},
|
||||||
@@ -147,6 +149,10 @@ 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.zone and dns.nameserver.
|
||||||
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)
|
||||||
|
|
||||||
@@ -584,15 +590,21 @@ def validate(cfg):
|
|||||||
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 +1002,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,
|
||||||
@@ -1236,7 +1251,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):
|
||||||
|
|||||||
@@ -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 road-warrior 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 road-warrior 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}'
|
||||||
|
|
||||||
|
|||||||
@@ -565,7 +565,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 road-warrior 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 +595,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 +893,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 +907,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 +1048,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 road-warrior 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 +1065,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 +1082,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'):
|
||||||
|
|||||||
@@ -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 %}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user