Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions scenarios/wireguard/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
server/runtime/*
!server/runtime/.gitkeep

client/wg0.conf

*.key
*.conf.generated
118 changes: 118 additions & 0 deletions scenarios/wireguard/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
## WireGuard External Service Example

This scenario connects one external service to NSE2 through a WireGuard tunnel.
NSE2 runs the WireGuard server alongside the example nodes `n1` and `n2`.
The external host runs the WireGuard client and an application that shares the client's network namespace.

This example uses the [LinuxServer.io WireGuard container](https://github.com/linuxserver/docker-wireguard).
Refer to its documentation for additional configuration options and container requirements.


```mermaid
flowchart LR
subgraph NSE2["NSE2 Host"]
n1["Node n1"]
n2["Node n2"]
server["WireGuard Server<br/>Transparent Proxy"]

n1 <--> server
n2 <--> server
end

subgraph External["External Host"]
client["WireGuard Client"]
app["External Application"]
client --- app
end

server <-->|"WireGuard Tunnel"| client
```

Traffic addressed to either of the `wg-server` Docker addresses is DNATed by `server/setup-iptables.sh` to the external peer's tunnel address.
The external application shares the WireGuard client's network namespace and therefore requires no additional routing configuration.

This example supports one external WireGuard peer.

### Configuration

Before starting the server, review the following values in `compose.yml`:

- `SERVERURL` must be an address through which the external host can reach the NSE2 host. Use `host.docker.internal` when testing both sides on the same machine.
- `SERVERPORT` must match the UDP port published by the `wg-server` service.
- `ALLOWEDIPS` must include every NSE2 network that the external service should be able to reach through the tunnel.


### Setup

Start the NSE2-side services:

```sh
docker compose up -d
```

The WireGuard server generates its keys and the external peer configuration
under `server/runtime`. The generated client configuration is:

```text
server/runtime/peer_external/peer_external.conf
```

Copy this file to `client/wg0.conf` on the external host:

```sh
scp server/runtime/peer_external/peer_external.conf \
user@external-host:/path/to/examples/wireguard/client/wg0.conf
```

The configuration contains the external peer's private key and must not be
committed to Git. Restrict its permissions on the external host:

```sh
chmod 600 client/wg0.conf
```

Start the WireGuard client and external application:

```sh
docker compose -f compose-client.yml up -d
```


### Testing

Check that the tunnel has established a handshake:

```sh
docker compose exec wg-server wg show
docker compose -f compose-client.yml exec wg-client wg show
```

The example application listens on port 80. Test it from both NSE2 nodes:

```sh
docker compose exec n1 curl --fail http://172.30.0.2
docker compose exec n2 curl --fail http://172.31.0.2
```

Both requests should return:

```text
Hello from external WireGuard service!
```

Within the Compose networks, Docker DNS can resolve the WireGuard server by its service name.
The application can therefore also be reached through `wg-server`:

```sh
docker compose exec n1 curl --fail http://wg-server
docker compose exec n2 curl --fail http://wg-server
```

The generated client configuration routes the NSE2 Docker networks listed in `ALLOWEDIPS` through the WireGuard tunnel.
It also uses the DNS server configured by the WireGuard container, allowing the external side to resolve and connect to `n1` and `n2` by name.

```sh
docker compose -f compose-client.yml exec wg-client ping n1
docker compose -f compose-client.yml exec wg-client ping n2
```

40 changes: 40 additions & 0 deletions scenarios/wireguard/compose-client.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: wireguard-external-service-client

services:
wg-client:
image: lscr.io/linuxserver/wireguard:latest
container_name: external-wg-client
cap_add:
- NET_ADMIN
- SYS_MODULE
environment:
PUID: ${PUID:-1000}
PGID: ${PGID:-1000}
TZ: ${TZ:-Etc/UTC}
volumes:
- ./client/wg0.conf:/config/wg_confs/wg0.conf:ro
- /lib/modules:/lib/modules:ro
sysctls:
- net.ipv4.conf.all.src_valid_mark=1
restart: unless-stopped
# Optional. Only needed when server and client run on the same host. Then
# the SERVER_URL in the .env needs to be `host.docker.internal` since that
# resolves to localhost of the host.
extra_hosts:
- "host.docker.internal:host-gateway"

app:
image: alpine:latest
container_name: external-app
network_mode: service:wg-client
command:
- sh
- -c
- |
apk add --no-cache python3
mkdir -p /var/www
echo '<html><body><h1>Hello from external WireGuard service!</h1></body></html>' > /var/www/index.html
python3 -m http.server 80 --directory /var/www
depends_on:
- wg-client
restart: unless-stopped
86 changes: 86 additions & 0 deletions scenarios/wireguard/compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
name: wireguard-external-service-scenario

services:
wg-server:
image: lscr.io/linuxserver/wireguard:latest
container_name: wg-server
cap_add:
- NET_ADMIN
- SYS_MODULE
environment:
PUID: 1000
PGID: 1000
TZ: Etc/UTC
PEERS: external
PERSISTENTKEEPALIVE_PEERS: external

# Address and port written to the generated client configuration.
# Use host.docker.internal for same-host testing; otherwise, use an
# address reachable from the external host.
SERVERURL: host.docker.internal
SERVERPORT: 51820

# Networks the generated client configuration routes through WireGuard.
# Add any NSE2 network that the external service must be able to reach.
ALLOWEDIPS: 10.192.122.0/24,172.30.0.0/24,172.31.0.0/24

INTERNAL_SUBNET: 10.192.122.0
volumes:
- ./server/runtime:/config
- ./server/templates/server.conf:/config/templates/server.conf:ro
- ./server/setup-iptables.sh:/config/setup-iptables.sh:ro
- /lib/modules:/lib/modules:ro
ports:
# The published UDP port must match SERVERPORT.
- "51820:51820/udp"
sysctls:
- net.ipv4.conf.all.src_valid_mark=1
restart: unless-stopped
networks:
wg_n1:
ipv4_address: 172.30.0.2
wg_n2:
ipv4_address: 172.31.0.2
n1:
container_name: n1
hostname: n1
cap_add:
- NET_ADMIN
privileged: 'true'
volumes:
- ./compose.yml:/compose.yml:ro
environment:
NODE_ID: 1
TYPE: Host
networks:
wg_n1:
ipv4_address: 172.30.0.3
entrypoint: sh -c "apk add iproute2 bash curl && tail -f /dev/null"
image: alpine

n2:
container_name: n2
hostname: n2
cap_add:
- NET_ADMIN
privileged: 'true'
volumes:
- ./compose.yml:/compose.yml:ro
environment:
NODE_ID: 2
TYPE: Host
networks:
wg_n2:
ipv4_address: 172.31.0.3
entrypoint: sh -c "apk add iproute2 bash curl && tail -f /dev/null"
image: alpine

networks:
wg_n1:
ipam:
config:
- subnet: 172.30.0.0/24
wg_n2:
ipam:
config:
- subnet: 172.31.0.0/24
Empty file.
Loading