83 lines
3.6 KiB
Markdown
83 lines
3.6 KiB
Markdown
# wg-relay
|
|
|
|
A WireGuard tunnel between two sites where the only thing reachable across it
|
|
is one TCP port on one host per side.
|
|
|
|
```
|
|
ansible/
|
|
├── inventory.ini
|
|
├── wg-relay.yml # the whole thing
|
|
├── group_vars/all.yml # defaults
|
|
├── host_vars/site-a.yml # Site A (Site B writes the mirror image)
|
|
└── templates/
|
|
├── wg0.conf.j2
|
|
├── socat-relay.service.j2
|
|
└── nftables-wgrelay.conf.j2
|
|
```
|
|
|
|
Needs `ansible-core` and nothing else — no collections to install.
|
|
|
|
## How it works
|
|
|
|
The relay VM never routes. `ip_forward` is pinned to 0 and the nftables
|
|
forward chain policy is `drop`, so there is no path from the tunnel to the
|
|
LAN. The only things listening are socat processes you declared:
|
|
|
|
- an **inbound** relay bound to the tunnel address, forwarding to one
|
|
`host:port` on the local LAN
|
|
- an **outbound** relay bound to the LAN address, forwarding down the tunnel
|
|
|
|
Local clients point at the relay VM's LAN address and never learn a tunnel
|
|
exists, so nothing on them needs reconfiguring if the relay changes.
|
|
|
|
Three layers have to agree before a packet gets anywhere:
|
|
|
|
1. **WireGuard** — `AllowedIPs` is a `/32`, which the kernel enforces as a
|
|
source filter. A peer that forges a source address outside its own `/32`
|
|
is dropped before any userspace sees it.
|
|
2. **nftables** — the tunnel interface ends in an explicit `drop`; only the
|
|
declared relay ports, from the declared source, get through.
|
|
3. **socat** — forwards to exactly one `host:port`, running as `nobody`.
|
|
|
|
Each side's restrictions live entirely on its own machine. There is no
|
|
mechanism for one site to prove to the other how its half is configured — and
|
|
none is needed, since the half that protects you is the half you control.
|
|
|
|
## First run
|
|
|
|
1. Run it on each side with the peer key left as the placeholder. It
|
|
generates a keypair and prints the public key.
|
|
2. Exchange public keys, fill in `wg_peers[].public_key`, run again.
|
|
3. Point the local client at the relay VM's LAN address and port.
|
|
|
|
```bash
|
|
ansible-playbook -i inventory.ini wg-relay.yml --check --diff
|
|
ansible-playbook -i inventory.ini wg-relay.yml
|
|
```
|
|
|
|
The playbook ends by asserting forwarding is off, waiting for each relay port
|
|
to accept connections, and printing `wg show`.
|
|
|
|
## Notes
|
|
|
|
- The `wg0.conf` task is `no_log: true` because the file contains the private
|
|
key; `--diff` won't show it changing. That's deliberate.
|
|
- Keys live in `/etc/wireguard/private.key` (0600, root-only). To manage them
|
|
centrally instead, set `wg_private_key` from Ansible Vault and the playbook
|
|
skips generation.
|
|
- Removing a relay from `host_vars` stops and deletes its systemd unit on the
|
|
next run — no orphaned listeners.
|
|
- If either uplink is PPPoE or tunnelled, drop `wg_mtu` to 1380. Sustained
|
|
bulk transfers are what find PMTU blackholes, and the symptom is a stall
|
|
mid-transfer rather than a clean failure.
|
|
- `socat_group: nogroup` is Debian/Ubuntu. On RHEL-family set it to `nobody`.
|
|
- Whichever side is behind CGNAT should omit `endpoint` and let the other side
|
|
dial in; `PersistentKeepalive` holds the path open. If *both* sides are
|
|
behind CGNAT, neither can reach the other directly and this design doesn't
|
|
apply as-is.
|
|
- The relay is TCP only. A UDP service needs `UDP-LISTEN`/`UDP` in the socat
|
|
unit and a matching `udp dport` rule in the nftables template.
|
|
- Protocols that embed their own hostname or redirect clients to a second
|
|
address (anything doing virtual-host-style routing) need that address to
|
|
resolve on the client side too — a relay changes the address clients
|
|
connect to, and some protocols notice. |