travel router

This commit is contained in:
nick committed 2026-07-16 11:44:08 -07:00
1 parent d1ea7aaff2
commit 2658d7403d
10 files changed
+134 -12

No files matched your search

+1
View File
@@ -26,6 +26,7 @@ Oswald↔Trochalakis tunnel — this box never carries it.
| Oswald LAN | `192.168.7.0/24` | AH-OSWLDC |
| WG transfer | `172.31.255.0/24` | hub .1, Troch .2, Oswald .3 |
| OpenVPN clients | `172.31.8.0/24` | routed (not NATed) to both sites |
| WG travel routers | `172.31.9.0/24` | GL.iNet roaming peers, hub .1 ([docs](docs/TRAVEL-ROUTERS.md)) |
Public endpoint: `vpn.atlashorizon.net` — only 22/tcp (SSH), 51820/udp
(WireGuard) and 1194/udp (OpenVPN) are open. RADIUS binds loopback only;
+10
View File
@@ -39,6 +39,16 @@ technitium_admin_password: ""
# "" to disable registration.
vpn_dns_zone: roam.atlashorizon.net
# ── Travel routers (GL.iNet etc.) as roaming WireGuard peers ──
# Each gets a /32 from 172.31.9.0/24 (hub is 172.31.9.1). Generate the
# keypair on the router (or wg genkey) and paste the PUBLIC key here.
# The init job also registers <name>.<vpn_dns_zone> A + PTR records.
wg_travel_peers: []
#wg_travel_peers:
# - { name: glinet-1, public_key: "xxxx=", ip: 172.31.9.11 }
# - { name: glinet-2, public_key: "yyyy=", ip: 172.31.9.12 }
# - { name: glinet-3, public_key: "zzzz=", ip: 172.31.9.13 }
# ── Optional ──
# Pin for deterministic Duo updates (e.g. "6.5.1"); bumping it forces a
# refetch of the tarball on the next rebuild.
+13 -5
View File
@@ -219,15 +219,23 @@
ansible.builtin.debug:
msg: "RackNerd WG public key: {{ wg_pubkey.stdout }}"
- name: Deploy wg0.conf (rendered from host/wg0.conf.example)
- name: Deploy wg0.conf (rendered from host/wg0.conf.example + travel peers)
tags: [host, wireguard]
ansible.builtin.copy:
dest: /etc/wireguard/wg0.conf
mode: "0600"
content: "{{ lookup('file', playbook_dir ~ '/../host/wg0.conf.example')
| replace('<RACKNERD_WG_PRIVATE_KEY>', wg_key_raw.content | b64decode | trim)
| replace('<TROCHALAKIS_WG_PUBLIC_KEY>', troch_wg_public_key | default('<TROCHALAKIS_WG_PUBLIC_KEY>', true))
| replace('<OSWALD_WG_PUBLIC_KEY>', oswald_wg_public_key | default('<OSWALD_WG_PUBLIC_KEY>', true)) }}\n"
content: |
{{ lookup('file', playbook_dir ~ '/../host/wg0.conf.example')
| replace('<RACKNERD_WG_PRIVATE_KEY>', wg_key_raw.content | b64decode | trim)
| replace('<TROCHALAKIS_WG_PUBLIC_KEY>', troch_wg_public_key | default('<TROCHALAKIS_WG_PUBLIC_KEY>', true))
| replace('<OSWALD_WG_PUBLIC_KEY>', oswald_wg_public_key | default('<OSWALD_WG_PUBLIC_KEY>', true)) }}
{% for p in wg_travel_peers | default([]) %}
# ── Travel router: {{ p.name }} (roaming /32, appended by Ansible) ──
[Peer]
PublicKey = {{ p.public_key }}
AllowedIPs = {{ p.ip }}/32
{% endfor %}
no_log: true
notify: restart wireguard
+1
View File
@@ -19,6 +19,7 @@ RADIUS_SECRET={{ radius_secret }}
TECHNITIUM_ADMIN_PASSWORD={{ technitium_admin_password }}
VPN_DNS_ZONE={{ vpn_dns_zone | default('roam.atlashorizon.net') }}
VPN_TRAVEL_PEERS={% for p in wg_travel_peers | default([]) %}{{ p.name }}:{{ p.ip }}{{ "," if not loop.last else "" }}{% endfor %}
DUO_AUTHPROXY_VERSION={{ duo_authproxy_version | default('latest') }}
+3 -1
View File
@@ -29,7 +29,9 @@ services:
- DNS_SERVER_ADMIN_PASSWORD=${TECHNITIUM_ADMIN_PASSWORD}
- DNS_SERVER_WEB_SERVICE_HTTP_PORT=5380
- DNS_SERVER_RECURSION=UseSpecifiedNetworkACL
- DNS_SERVER_RECURSION_NETWORK_ACL=127.0.0.0/8,172.31.8.0/24,172.31.255.0/24,192.168.20.0/24,192.168.7.0/24
# NOTE: seeded on first start only — on an existing deploy, add new
# networks in the console instead (Settings -> Recursion).
- DNS_SERVER_RECURSION_NETWORK_ACL=127.0.0.0/8,172.31.8.0/24,172.31.9.0/24,172.31.255.0/24,192.168.20.0/24,192.168.7.0/24
volumes:
- ${TECHNITIUM_DIR:-./technitium}/config:/etc/dns
restart: unless-stopped
+7 -4
View File
@@ -13,7 +13,8 @@ Add the RackNerd peer to each site's existing WG config. The sites
[Peer] # RackNerd hub
PublicKey = <RACKNERD_WG_PUBLIC_KEY> # printed by host/setup.sh
Endpoint = vpn.atlashorizon.net:51820
AllowedIPs = 172.31.255.1/32, 172.31.8.0/24
# .8 = OpenVPN clients, .9 = travel routers (docs/TRAVEL-ROUTERS.md)
AllowedIPs = 172.31.255.1/32, 172.31.8.0/24, 172.31.9.0/24
PersistentKeepalive = 25
```
@@ -35,6 +36,7 @@ LAN router (or directly on the DCs):
| Destination | Via |
|------------------|------------------------|
| `172.31.8.0/24` | site WireGuard box |
| `172.31.9.0/24` | site WireGuard box |
| `172.31.255.1/32`| site WireGuard box |
Verify no asymmetry: `tracert 172.31.8.10` from a DC must leave via the
@@ -42,9 +44,10 @@ local WG box → RackNerd, never via the other site.
## 3. Windows Firewall / segmentation on the DCs
Allow from **`172.31.8.0/24`** (roaming clients, real source IPs): standard
AD client ports — DNS 53, Kerberos 88/464, LDAP 389/636, SMB 445, RPC 135 +
49152-65535, NTP 123, GC 3268/3269.
Allow from **`172.31.8.0/24`** (OpenVPN clients, real source IPs) and
**`172.31.9.0/24`** (travel routers): standard AD client ports — DNS 53,
Kerberos 88/464, LDAP 389/636, SMB 445, RPC 135 + 49152-65535, NTP 123,
GC 3268/3269.
Allow from **`172.31.255.1/32`** (the RackNerd host itself):
- TCP/UDP 53 → AHDC01, AHDC03, AH-OSWLDC (Technitium forwarder queries)
+65
View File
@@ -0,0 +1,65 @@
# Travel routers (GL.iNet) as roaming WireGuard peers
The three GL.iNet routers connect to the hub as always-on WireGuard
clients from `172.31.9.0/24` (hub = `172.31.9.1`). They are roaming
*clients*, not sites: they initiate to the hub, carry no LAN routes of
their own on the hub side, and NAT their attached devices behind their
single WG address — so the DCs see one IP per router.
Security model (be aware): unlike OpenVPN users there is **no Duo** here —
possession of the router's private key is the whole credential. Each
router has its own keypair; if one is lost, remove its entry from
`wg_travel_peers` and re-run the playbook — that revokes it completely.
## Hub side (all via Ansible)
1. Generate a keypair per router — in the GL.iNet UI (it shows the public
key) or manually: `wg genkey | tee priv | wg pubkey`.
2. Fill `wg_travel_peers` in `group_vars/vpnhub.yml`:
```yaml
wg_travel_peers:
- { name: glinet-1, public_key: "xxxx=", ip: 172.31.9.11 }
- { name: glinet-2, public_key: "yyyy=", ip: 172.31.9.12 }
- { name: glinet-3, public_key: "zzzz=", ip: 172.31.9.13 }
```
3. `ansible-playbook site.yml` — appends the peers to wg0.conf (restarts
wg0: brief blip for all tunnels), adds the firewall forwards, and
registers `<name>.roam.atlashorizon.net` A + PTR records.
4. **Existing deploys only**: add `172.31.9.0/24` to the Technitium
recursion ACL by hand (console → Settings → Recursion) — that setting
was seeded at first boot and isn't re-read from the compose env.
## Router side (paste into the GL.iNet WireGuard client UI)
```ini
[Interface]
PrivateKey = <this router's private key>
Address = 172.31.9.11/24 # .12 / .13 for the others
DNS = 172.31.9.1 # Technitium — AD names resolve
[Peer]
PublicKey = <RACKNERD_WG_PUBLIC_KEY> # printed by setup/playbook
Endpoint = vpn.atlashorizon.net:51820
# Split tunnel: both sites + the hub nets. Use 0.0.0.0/0 instead for
# full tunnel (all hotel traffic exits via the VPS).
AllowedIPs = 192.168.20.0/24, 192.168.7.0/24, 172.31.9.0/24, 172.31.255.1/32
PersistentKeepalive = 25
```
## Site side (once, both sites — same drill as the OpenVPN subnet)
- Add `172.31.9.0/24` to the RackNerd peer's AllowedIPs in each site's
WG config.
- Return route: `172.31.9.0/24` via the site WG box (LAN router or DCs).
- DC/Windows firewall: allow AD ports from `172.31.9.0/24`.
- Optional: AD Sites & Services subnet object for `172.31.9.0/24`.
## Verify
```bash
wg show wg0 # router peers: recent handshakes
ping 172.31.9.11 # from the hub
dig @127.0.0.1 -x 172.31.9.11 +short # glinet-1.roam.atlashorizon.net.
# from a device behind the router:
# ping ahdc01.ad.atlashorizon.net (resolves via 172.31.9.1, routes via hub)
```
+4
View File
@@ -43,6 +43,10 @@
-A FORWARD -i tun0 -o wg0 -s 172.31.8.0/24 -d 192.168.7.0/24 -j ACCEPT
-A FORWARD -i tun0 -o wg0 -s 172.31.8.0/24 -d 172.31.255.0/24 -j ACCEPT
# Travel routers (roaming WG peers, wg0 -> wg0) -> both sites
-A FORWARD -i wg0 -o wg0 -s 172.31.9.0/24 -d 192.168.20.0/24 -j ACCEPT
-A FORWARD -i wg0 -o wg0 -s 172.31.9.0/24 -d 192.168.7.0/24 -j ACCEPT
# Site -> client NEW connections (e.g. RDP/RSAT from a DC to a roaming
# laptop). Off by default; uncomment if you need it.
#-A FORWARD -i wg0 -o tun0 -d 172.31.8.0/24 -j ACCEPT
+10 -1
View File
@@ -3,7 +3,8 @@
# paste in each site's public key afterwards.
[Interface]
Address = 172.31.255.1/24
# .255 = site transfer net, .9 = roaming WG peers (travel routers)
Address = 172.31.255.1/24, 172.31.9.1/24
ListenPort = 51820
PrivateKey = <RACKNERD_WG_PRIVATE_KEY>
# wg-quick auto-adds routes for each peer's AllowedIPs — that is exactly
@@ -25,3 +26,11 @@ AllowedIPs = 172.31.255.3/32, 192.168.7.0/24
# Deliberately NOT listing the other site's LAN under either peer:
# Oswald<->Trochalakis traffic must stay on their direct tunnel, never
# hairpin through this box.
# ── Travel routers (roaming /32 peers from 172.31.9.0/24) ──
# The Ansible deploy appends these from the wg_travel_peers list in
# group_vars. For manual setups, copy this block per router:
#[Peer] # glinet-1
#PublicKey = <ROUTER_WG_PUBLIC_KEY>
#AllowedIPs = 172.31.9.11/32
# (no Endpoint — routers initiate and keep alive from behind NAT)
+20 -1
View File
@@ -93,7 +93,26 @@ ensure_primary_zone() {
if [ -n "${VPN_DNS_ZONE:-}" ]; then
ensure_primary_zone "$VPN_DNS_ZONE"
ensure_primary_zone "8.31.172.in-addr.arpa"
ensure_primary_zone "8.31.172.in-addr.arpa" # OpenVPN clients
ensure_primary_zone "9.31.172.in-addr.arpa" # travel routers
fi
# Static A + PTR for travel routers (VPN_TRAVEL_PEERS="name:ip,name:ip").
# overwrite=true keeps this idempotent across re-runs.
if [ -n "${VPN_TRAVEL_PEERS:-}" ] && [ -n "${VPN_DNS_ZONE:-}" ]; then
echo "$VPN_TRAVEL_PEERS" | tr ',' '\n' | while IFS=: read -r name ip; do
[ -n "$name" ] && [ -n "$ip" ] || continue
echo "registering travel router $name -> $ip"
curl -sf -H "$AUTH" -G "$API/zones/records/add" \
--data-urlencode "domain=$name.$VPN_DNS_ZONE" \
--data-urlencode "zone=$VPN_DNS_ZONE" \
--data-urlencode "type=A" \
--data-urlencode "ttl=3600" \
--data-urlencode "ipAddress=$ip" \
--data-urlencode "overwrite=true" \
--data-urlencode "ptr=true" \
--data-urlencode "createPtrZone=true" | api_ok
done
fi
echo "Technitium AD forwarder zones OK."