Probably works
This commit is contained in:
10 files changed
+222
-31
No files matched your search
@@ -0,0 +1,4 @@
|
|||||||
|
site-a.yml
|
||||||
|
site-b.yml
|
||||||
|
.env
|
||||||
|
.DS_Store
|
||||||
@@ -5,18 +5,17 @@ is one TCP port on one host per side.
|
|||||||
|
|
||||||
```
|
```
|
||||||
ansible/
|
ansible/
|
||||||
├── inventory.ini
|
|
||||||
├── wg-relay.yml # the whole thing
|
├── wg-relay.yml # the whole thing
|
||||||
├── group_vars/all.yml # defaults
|
├── defaults.yml # defaults
|
||||||
├── host_vars/site-a.yml # Site A (Site B writes the mirror image)
|
├── vars/
|
||||||
|
│ ├── site-a.yml # Site A (Site B writes the mirror image)
|
||||||
|
│ └── site-b.yml
|
||||||
└── templates/
|
└── templates/
|
||||||
├── wg0.conf.j2
|
├── wg0.conf.j2
|
||||||
├── socat-relay.service.j2
|
├── socat-relay.service.j2
|
||||||
└── nftables-wgrelay.conf.j2
|
└── nftables-wgrelay.conf.j2
|
||||||
```
|
```
|
||||||
|
|
||||||
Needs `ansible-core` and nothing else — no collections to install.
|
|
||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
The relay VM never routes. `ip_forward` is pinned to 0 and the nftables
|
The relay VM never routes. `ip_forward` is pinned to 0 and the nftables
|
||||||
@@ -24,7 +23,7 @@ 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:
|
LAN. The only things listening are socat processes you declared:
|
||||||
|
|
||||||
- an **inbound** relay bound to the tunnel address, forwarding to one
|
- an **inbound** relay bound to the tunnel address, forwarding to one
|
||||||
`host:port` on the local LAN
|
`host:port` on the local LAN
|
||||||
- an **outbound** relay bound to the LAN address, forwarding down the tunnel
|
- 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
|
Local clients point at the relay VM's LAN address and never learn a tunnel
|
||||||
@@ -43,20 +42,48 @@ 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
|
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.
|
none is needed, since the half that protects you is the half you control.
|
||||||
|
|
||||||
## First run
|
## Prerequisites
|
||||||
|
|
||||||
1. Run it on each side with the peer key left as the placeholder. It
|
You can use the ansible repositories or another installation method if desired, all that matters is you have ansible
|
||||||
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
|
Needs `ansible-core`, no other packages or collections are needed. The playbook will install additional required packages
|
||||||
ansible-playbook -i inventory.ini wg-relay.yml --check --diff
|
|
||||||
ansible-playbook -i inventory.ini wg-relay.yml
|
```
|
||||||
|
sudo apt update && sudo apt install ansible
|
||||||
```
|
```
|
||||||
|
|
||||||
The playbook ends by asserting forwarding is off, waiting for each relay port
|
## Setup
|
||||||
to accept connections, and printing `wg show`.
|
|
||||||
|
The playbook and `defaults.yml` are identical at both sites. Everything that
|
||||||
|
differs lives in that site's vars file — copy `vars/site-a.yml` and edit:
|
||||||
|
|
||||||
|
- `relay_host` / `relay_user` — which box to configure. Set it to the relay
|
||||||
|
VM to run over SSH, or leave it as `localhost` to configure the machine
|
||||||
|
you're sitting on.
|
||||||
|
- `wg_address` — this side's tunnel address, as a `/32`.
|
||||||
|
- `wg_peers` — the other site's public key, endpoint and `/32`.
|
||||||
|
- `relays` — the inbound and outbound listeners.
|
||||||
|
|
||||||
|
Values in the vars file override `defaults.yml`, because `-e` outranks
|
||||||
|
everything else. Adding a site later means one more file in `vars/`; the
|
||||||
|
playbook doesn't change.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Run it at each site against that site's vars file. No inventory file needed.
|
||||||
|
|
||||||
|
```
|
||||||
|
ansible-playbook wg-relay.yml -e @vars/site-a.yml --check --diff
|
||||||
|
ansible-playbook wg-relay.yml -e @vars/site-a.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
Leave the peer's public key as the placeholder on the first run — it generates
|
||||||
|
a keypair and prints this side's public key. Exchange public keys, fill in
|
||||||
|
`wg_peers[].public_key` in each vars file, then run again. Finally, point the
|
||||||
|
local client at the relay VM's LAN address and port.
|
||||||
|
|
||||||
|
Each run ends by asserting forwarding is off, waiting for each relay port to
|
||||||
|
accept connections, and printing `wg show`.
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
@@ -65,8 +92,8 @@ to accept connections, and printing `wg show`.
|
|||||||
- Keys live in `/etc/wireguard/private.key` (0600, root-only). To manage them
|
- 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
|
centrally instead, set `wg_private_key` from Ansible Vault and the playbook
|
||||||
skips generation.
|
skips generation.
|
||||||
- Removing a relay from `host_vars` stops and deletes its systemd unit on the
|
- Removing a relay from the vars file stops and deletes its systemd unit on
|
||||||
next run — no orphaned listeners.
|
the next run — no orphaned listeners.
|
||||||
- If either uplink is PPPoE or tunnelled, drop `wg_mtu` to 1380. Sustained
|
- 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
|
bulk transfers are what find PMTU blackholes, and the symptom is a stall
|
||||||
mid-transfer rather than a clean failure.
|
mid-transfer rather than a clean failure.
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
---
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Defaults shared by every site. Do not edit this file per-site — put the
|
||||||
|
# differences in your site vars file (see vars/site-a.yml) and pass it with
|
||||||
|
# -e @vars/<site>.yml, which overrides everything here.
|
||||||
|
#
|
||||||
|
# Anything marked REQUIRED has no sensible default and must be set there.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
wg_interface: wg0
|
||||||
|
wg_listen_port: 51820
|
||||||
|
|
||||||
|
# 1420 leaves room for the WireGuard header inside a 1500-byte path.
|
||||||
|
# Drop to 1380 if either side is on PPPoE or a tunnelled uplink.
|
||||||
|
wg_mtu: 1420
|
||||||
|
|
||||||
|
# REQUIRED: this box's tunnel address, as a /32. Never a /24 — the mask is
|
||||||
|
# what stops wg-quick installing a route to the whole tunnel subnet.
|
||||||
|
wg_address: ""
|
||||||
|
|
||||||
|
# Leave undefined to have the playbook generate a keypair on first run and print
|
||||||
|
# the public key. Set it (from Ansible Vault) if you manage keys centrally.
|
||||||
|
# wg_private_key: "..."
|
||||||
|
|
||||||
|
# REQUIRED: the remote site.
|
||||||
|
# allowed_ips - their /32 and nothing wider. This is the kernel's source
|
||||||
|
# filter, not just a route.
|
||||||
|
# endpoint - omit entirely if they're behind CGNAT and dial out to you.
|
||||||
|
wg_peers: []
|
||||||
|
# - name: site-b
|
||||||
|
# public_key: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx="
|
||||||
|
# allowed_ips: 10.99.0.2/32
|
||||||
|
# endpoint: site-b.example.com:51820
|
||||||
|
# keepalive: 25
|
||||||
|
|
||||||
|
# REQUIRED: the socat listeners.
|
||||||
|
# direction: inbound - listens on the tunnel, forwards to your LAN
|
||||||
|
# direction: outbound - listens on your LAN, forwards down the tunnel
|
||||||
|
# source_allow - nftables source restriction for this listener
|
||||||
|
relays: []
|
||||||
|
# - name: in-site-b
|
||||||
|
# direction: inbound
|
||||||
|
# listen_address: 10.99.0.1
|
||||||
|
# listen_port: 9000
|
||||||
|
# target: 192.168.1.50:9000
|
||||||
|
# source_allow: 10.99.0.2/32
|
||||||
|
# - name: out-site-b
|
||||||
|
# direction: outbound
|
||||||
|
# listen_address: 192.168.1.10
|
||||||
|
# listen_port: 9001
|
||||||
|
# target: 10.99.0.2:9000
|
||||||
|
# source_allow: 192.168.1.50/32
|
||||||
|
|
||||||
|
# Unprivileged account the socat units run as. 'nogroup' is Debian/Ubuntu;
|
||||||
|
# on RHEL-family set both to 'nobody'.
|
||||||
|
socat_user: nobody
|
||||||
|
socat_group: nogroup
|
||||||
|
|
||||||
|
# Safety rails. Only flip these if you know why.
|
||||||
|
# forwarding_enabled: true turns this box into a router between the tunnel
|
||||||
|
# and your LAN, which defeats the point of the relay design.
|
||||||
|
wg_relay_forwarding_enabled: false
|
||||||
|
wg_relay_manage_nftables: true
|
||||||
@@ -9,10 +9,8 @@
|
|||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
|
|
||||||
# --- which box to configure ------------------------------------------------
|
# --- which box to configure ------------------------------------------------
|
||||||
# Set to the relay VM's address to configure it over SSH, or leave as
|
relay_host: localhost
|
||||||
# localhost to configure the machine you're running this on.
|
#relay_user: admin
|
||||||
relay_host: 192.168.1.10
|
|
||||||
relay_user: admin
|
|
||||||
|
|
||||||
# --- this side of the tunnel -----------------------------------------------
|
# --- this side of the tunnel -----------------------------------------------
|
||||||
wg_address: 10.99.0.1/32
|
wg_address: 10.99.0.1/32
|
||||||
@@ -22,8 +20,6 @@ wg_listen_port: 51820
|
|||||||
wg_peers:
|
wg_peers:
|
||||||
- name: site-b
|
- name: site-b
|
||||||
public_key: "PASTE_SITE_B_PUBLIC_KEY_HERE"
|
public_key: "PASTE_SITE_B_PUBLIC_KEY_HERE"
|
||||||
# Omit endpoint on whichever side isn't reachable from outside; the
|
|
||||||
# other side dials in and keepalives hold the path open.
|
|
||||||
endpoint: site-b.example.com:51820
|
endpoint: site-b.example.com:51820
|
||||||
allowed_ips: 10.99.0.2/32
|
allowed_ips: 10.99.0.2/32
|
||||||
keepalive: 25
|
keepalive: 25
|
||||||
@@ -10,8 +10,8 @@
|
|||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
|
|
||||||
# --- which box to configure ------------------------------------------------
|
# --- which box to configure ------------------------------------------------
|
||||||
relay_host: 10.0.0.10
|
relay_host: localhost
|
||||||
relay_user: admin
|
#
|
||||||
|
|
||||||
# --- this side of the tunnel -----------------------------------------------
|
# --- this side of the tunnel -----------------------------------------------
|
||||||
wg_address: 10.99.0.2/32
|
wg_address: 10.99.0.2/32
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
{{ ansible_managed | comment }}
|
||||||
|
|
||||||
|
table inet wgrelay
|
||||||
|
delete table inet wgrelay
|
||||||
|
|
||||||
|
table inet wgrelay {
|
||||||
|
chain input {
|
||||||
|
type filter hook input priority 0; policy accept;
|
||||||
|
|
||||||
|
ct state established,related accept
|
||||||
|
ct state invalid drop
|
||||||
|
|
||||||
|
# --- traffic arriving over the tunnel -------------------------------
|
||||||
|
# Ping across the tunnel is useful for debugging and harmless.
|
||||||
|
iifname "{{ wg_interface }}" icmp type echo-request accept
|
||||||
|
iifname "{{ wg_interface }}" icmpv6 type echo-request accept
|
||||||
|
|
||||||
|
{% for r in relays if r.direction == 'inbound' %}
|
||||||
|
# {{ r.name }}: only {{ r.source_allow }} may reach this listener
|
||||||
|
iifname "{{ wg_interface }}" ip saddr {{ r.source_allow }} tcp dport {{ r.listen_port }} accept
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
# Anything else off the tunnel is not ours. This is the rule that
|
||||||
|
# makes "one port, one host" true rather than aspirational.
|
||||||
|
iifname "{{ wg_interface }}" counter drop
|
||||||
|
|
||||||
|
# --- LAN-facing relay listeners -------------------------------------
|
||||||
|
{% for r in relays if r.direction == 'outbound' %}
|
||||||
|
iifname != "{{ wg_interface }}" ip saddr {{ r.source_allow }} tcp dport {{ r.listen_port }} accept
|
||||||
|
tcp dport {{ r.listen_port }} counter drop
|
||||||
|
{% endfor %}
|
||||||
|
}
|
||||||
|
|
||||||
|
chain forward {
|
||||||
|
type filter hook forward priority 0; policy drop;
|
||||||
|
# Nothing is routed through this box, by design. Combined with
|
||||||
|
# net.ipv4.ip_forward=0 this is belt and braces.
|
||||||
|
counter
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
{{ ansible_managed | comment }}
|
||||||
|
[Unit]
|
||||||
|
Description=Relay {{ item.name }} ({{ item.listen_address }}:{{ item.listen_port }} -> {{ item.target }})
|
||||||
|
After=network-online.target wg-quick@{{ wg_interface }}.service
|
||||||
|
Wants=network-online.target
|
||||||
|
{% if item.direction == 'inbound' %}
|
||||||
|
# Inbound listeners bind to the tunnel address, which doesn't exist until
|
||||||
|
# wg-quick has brought the interface up.
|
||||||
|
BindsTo=wg-quick@{{ wg_interface }}.service
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/bin/socat -d \
|
||||||
|
TCP-LISTEN:{{ item.listen_port }},bind={{ item.listen_address }},fork,reuseaddr,keepalive,keepidle=60,keepintvl=10,keepcnt=5 \
|
||||||
|
TCP:{{ item.target }},keepalive,keepidle=60,keepintvl=10,keepcnt=5
|
||||||
|
|
||||||
|
# Bind may fail for a few seconds after boot while wg0 settles; just retry.
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
# socat needs no privileges beyond binding a high port.
|
||||||
|
User={{ socat_user }}
|
||||||
|
Group={{ socat_group }}
|
||||||
|
NoNewPrivileges=yes
|
||||||
|
PrivateTmp=yes
|
||||||
|
PrivateDevices=yes
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=yes
|
||||||
|
ProtectKernelTunables=yes
|
||||||
|
ProtectKernelModules=yes
|
||||||
|
ProtectControlGroups=yes
|
||||||
|
RestrictAddressFamilies=AF_INET AF_INET6
|
||||||
|
RestrictNamespaces=yes
|
||||||
|
MemoryDenyWriteExecute=yes
|
||||||
|
CapabilityBoundingSet=
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{{ ansible_managed | comment }}
|
||||||
|
# Relay tunnel. This box forwards nothing; see the sysctl and nftables
|
||||||
|
# tasks in wg-relay.yml.
|
||||||
|
|
||||||
|
[Interface]
|
||||||
|
Address = {{ wg_address }}
|
||||||
|
ListenPort = {{ wg_listen_port }}
|
||||||
|
MTU = {{ wg_mtu }}
|
||||||
|
PrivateKey = {{ wg_relay_effective_private_key }}
|
||||||
|
|
||||||
|
{% for peer in wg_peers %}
|
||||||
|
# {{ peer.name }}
|
||||||
|
[Peer]
|
||||||
|
PublicKey = {{ peer.public_key }}
|
||||||
|
AllowedIPs = {{ peer.allowed_ips }}
|
||||||
|
{% if peer.endpoint is defined and peer.endpoint %}
|
||||||
|
Endpoint = {{ peer.endpoint }}
|
||||||
|
{% endif %}
|
||||||
|
{% if peer.preshared_key is defined and peer.preshared_key %}
|
||||||
|
PresharedKey = {{ peer.preshared_key }}
|
||||||
|
{% endif %}
|
||||||
|
PersistentKeepalive = {{ peer.keepalive | default(25) }}
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
+3
-5
@@ -8,12 +8,10 @@
|
|||||||
# ansible-playbook wg-relay.yml -e @vars/site-a.yml --check --diff
|
# ansible-playbook wg-relay.yml -e @vars/site-a.yml --check --diff
|
||||||
# ansible-playbook wg-relay.yml -e @vars/site-a.yml
|
# ansible-playbook wg-relay.yml -e @vars/site-a.yml
|
||||||
#
|
#
|
||||||
# No inventory file needed. Set relay_host to localhost (or leave it unset)
|
# No inventory file needed. Set relay_host to localhost or leave it unset
|
||||||
# to configure the machine you're running on. Defaults come from
|
# to configure the machine you're running on. Defaults come from
|
||||||
# defaults.yml; anything in the vars file wins, since -e outranks it.
|
# defaults.yml.
|
||||||
#
|
# The restrictions are enforced locally at each site.
|
||||||
# The restrictions are enforced locally at each site, so neither side can
|
|
||||||
# widen the other's exposure.
|
|
||||||
|
|
||||||
- name: Resolve which box to configure
|
- name: Resolve which box to configure
|
||||||
hosts: localhost
|
hosts: localhost
|
||||||
|
|||||||
Reference in new issue
Block a user