Skip to content

Commit 6fe8956

Browse files
committed
[feature] Added support for WireGuard and VXLAN #225
Added two images: - wireguard: image that runs WireGuard and VXLAN server - wireguard_updater: image that runs a Flask app that is used for triggering configuration update for WireGuard and VXLAN server Closes #225
1 parent aa6ce9b commit 6fe8956

File tree

11 files changed

+490
-3
lines changed

11 files changed

+490
-3
lines changed

.env

+8-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# Essential
55
DASHBOARD_DOMAIN=dashboard.openwisp.org
66
API_DOMAIN=api.openwisp.org
7-
VPN_DOMAIN=openvpn.openwisp.org
7+
WIREGUARD_UPDATER_DOMAIN=wireguard.openwisp.org
88
EMAIL_DJANGO_DEFAULT=example@example.org
99
DB_USER=admin
1010
DB_PASS=admin
@@ -42,6 +42,13 @@ X509_COMMON_NAME=OpenWISP
4242
# VPN
4343
VPN_NAME=default
4444
VPN_CLIENT_NAME=default-management-vpn
45+
# WireGuard
46+
WIREGUARD_UPDATER_PORT=8081
47+
WIREGUARD_UPDATER_ENDPOINT=/trigger-update
48+
WIREGUARD_UPDATER_KEY=openwisp-wireguard-updater-auth-key
49+
WIREGUARD_UPDATER_INSECURE_CURL=false
50+
WIREGUARD_VXLAN_IPV4_METHOD=link-local
51+
WIREGUARD_VXLAN_IPV6_METHOD=link-local
4552
# Developer
4653
DEBUG_MODE=False
4754
DJANGO_LOG_LEVEL=INFO

docker-compose.yml

+33
Original file line numberDiff line numberDiff line change
@@ -111,13 +111,15 @@ services:
111111
aliases:
112112
- dashboard.internal
113113
- api.internal
114+
- wireguard_updater.internal
114115
ports:
115116
- "80:80"
116117
- "443:443"
117118
depends_on:
118119
- dashboard
119120
- api
120121
- websocket
122+
- wireguard_updater
121123

122124
freeradius:
123125
image: openwisp/openwisp-freeradius:latest
@@ -158,6 +160,37 @@ services:
158160
cap_add:
159161
- NET_ADMIN
160162

163+
wireguard:
164+
image: openwisp/openwisp-wireguard:latest
165+
build:
166+
context: images
167+
dockerfile: openwisp_wireguard/Dockerfile
168+
env_file:
169+
- .env
170+
volumes:
171+
- /lib/modules:/lib/modules
172+
ports:
173+
- 51820:51820/udp
174+
sysctls:
175+
- net.ipv4.conf.all.src_valid_mark=1
176+
cap_add:
177+
- NET_ADMIN
178+
- SYS_MODULE
179+
180+
wireguard_updater:
181+
image: openwisp/openwisp-wireguard-updater:latest
182+
build:
183+
context: images
184+
dockerfile: openwisp_wireguard_updater/Dockerfile
185+
args:
186+
WIREGUARD_UPDATER_APP_PORT: 8081
187+
env_file:
188+
- .env
189+
networks:
190+
default:
191+
aliases:
192+
- wireguard.internal
193+
161194
postgres:
162195
image: mdillon/postgis:11-alpine
163196
environment:

images/common/init_command.sh

+8
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@ elif [ "$MODULE_NAME" = 'openvpn' ]; then
4545
# docker container running, restarting would mean killing
4646
# the container while supervisor helps only to restart the service!
4747
supervisord --nodaemon --configuration supervisord.conf
48+
elif [ "$MODULE_NAME" = 'wireguard' ]; then
49+
if [[ -z "$VPN_UUID" || -z "$VPN_KEY" ]]; then
50+
echo "You need to cofigure VPN_UUID and VPN_KEY environment varibales."
51+
fi
52+
wait_nginx_services
53+
wireguard_setup
54+
elif [ "$MODULE_NAME" = 'wireguard_updater' ]; then
55+
start_uwsgi
4856
elif [ "$MODULE_NAME" = 'nginx' ]; then
4957
rm -rf /etc/nginx/conf.d/default.conf
5058
if [ "$NGINX_CUSTOM_FILE" = 'True' ]; then

images/common/utils.sh

+22-1
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,19 @@ function create_prod_certs {
4141
--domain ${API_DOMAIN} \
4242
--email ${CERT_ADMIN_EMAIL}
4343
fi
44+
if [ ! -f /etc/letsencrypt/live/${WIREGUARD_UPDATER_DOMAIN}/privkey.pem ]; then
45+
certbot certonly --standalone --noninteractive --agree-tos \
46+
--rsa-key-size 4096 \
47+
--domain ${WIREGUARD_UPDATER_DOMAIN} \
48+
--email ${CERT_ADMIN_EMAIL}
49+
fi
4450
}
4551

4652
function create_dev_certs {
4753
# Ensure required directories exist
4854
mkdir -p /etc/letsencrypt/live/${DASHBOARD_DOMAIN}/
4955
mkdir -p /etc/letsencrypt/live/${API_DOMAIN}/
56+
mkdir -p /etc/letsencrypt/live/${WIREGUARD_UPDATER_DOMAIN}/
5057
# Create self-signed certificates
5158
if [ ! -f /etc/letsencrypt/live/${DASHBOARD_DOMAIN}/privkey.pem ]; then
5259
openssl req -x509 -newkey rsa:4096 \
@@ -60,6 +67,12 @@ function create_dev_certs {
6067
-out /etc/letsencrypt/live/${API_DOMAIN}/fullchain.pem \
6168
-days 365 -nodes -subj '/CN=OpenWISP'
6269
fi
70+
if [ ! -f /etc/letsencrypt/live/${WIREGUARD_UPDATER_DOMAIN}/privkey.pem ]; then
71+
openssl req -x509 -newkey rsa:4096 \
72+
-keyout /etc/letsencrypt/live/${WIREGUARD_UPDATER_DOMAIN}/privkey.pem \
73+
-out /etc/letsencrypt/live/${WIREGUARD_UPDATER_DOMAIN}/fullchain.pem \
74+
-days 365 -nodes -subj '/CN=OpenWISP'
75+
fi
6376
}
6477

6578
function nginx_dev {
@@ -109,7 +122,7 @@ function ssl_http_behaviour {
109122
function envsubst_create_config {
110123
# Creates nginx configurations files for dashboard
111124
# and api instances.
112-
for application in DASHBOARD API; do
125+
for application in DASHBOARD API WIREGUARD_UPDATER; do
113126
eval export APP_SERVICE=\$${application}_APP_SERVICE
114127
eval export APP_PORT=\$${application}_APP_PORT
115128
eval export DOMAIN=\$${application}_${3}
@@ -239,3 +252,11 @@ function crl_download {
239252
export CAid=$(psql -qAtc "SELECT ca_id FROM config_vpn where name='${VPN_NAME}';")
240253
wget -qO revoked.crl --no-check-certificate ${DASHBOARD_INTERNAL}/admin/pki/ca/${CAid}.crl
241254
}
255+
256+
function wireguard_setup {
257+
bash /opt/openwisp/update_wireguard.sh bring_up_interface
258+
bash /opt/openwisp/update_wireguard.sh check_config
259+
echo "*/5 * * * * bash /opt/openwisp/update_wireguard.sh check_config" | sudo crontab
260+
sudo cron
261+
bash /opt/openwisp/update_wireguard.sh watch_configuration_change
262+
}

images/openwisp_nginx/Dockerfile

+5-1
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,17 @@ ENV MODULE_NAME=nginx \
3939
DASHBOARD_APP_PORT=8000 \
4040
API_APP_PORT=8001 \
4141
WEBSOCKET_APP_PORT=8002 \
42+
WIREGUARD_UPDATER_APP_PORT=8081 \
4243
# Application Service Name
4344
DASHBOARD_APP_SERVICE=dashboard \
4445
API_APP_SERVICE=api \
4546
WEBSOCKET_APP_SERVICE=websocket \
47+
WIREGUARD_UPDATER_APP_SERVICE=wireguard_updater \
4648
# Listen domains
4749
DASHBOARD_DOMAIN=dashboard.example.com \
4850
API_DOMAIN=api.example.com \
51+
WIREGUARD_UPDATER_DOMAIN=wireguard_updater.example.com \
4952
# Inter container communication domains
5053
DASHBOARD_INTERNAL=dashboard.internal \
51-
API_INTERNAL=api.internal
54+
API_INTERNAL=api.internal \
55+
WIREGUARD_UPDATER_INTERNAL=wireguard_updater.internal

images/openwisp_wireguard/Dockerfile

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# hadolint ignore=DL3007
2+
FROM linuxserver/wireguard:latest
3+
4+
WORKDIR /opt/openwisp
5+
6+
RUN apt update && \
7+
apt install -y sudo network-manager cron redis-tools wget && \
8+
apt autoclean
9+
10+
# Remove services from the base image
11+
RUN rm /etc/cont-init.d/40-confs && \
12+
rm -r /etc/services.d/wireguard && \
13+
rm -r /etc/services.d/coredns
14+
RUN useradd --system --password '' --create-home --shell /bin/bash \
15+
--gid root --groups sudo --uid 1001 openwisp
16+
RUN echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
17+
RUN chown -R openwisp:root /opt/openwisp
18+
19+
USER openwisp:root
20+
21+
COPY --chown=openwisp:root ./openwisp_wireguard/update_vxlan.py \
22+
./openwisp_wireguard/update_wireguard.sh \
23+
./common/init_command.sh \
24+
./common/utils.sh \
25+
./common/services.py /opt/openwisp/
26+
27+
CMD ["bash", "init_command.sh"]
28+
29+
EXPOSE 51820
30+
31+
ENV MODULE_NAME=wireguard \
32+
DASHBOARD_INTERNAL=dashboard.internal \
33+
API_INTERNAL=api.internal \
34+
REDIS_HOST=redis \
35+
REDIS_DATABASE=15 \
36+
OPENWISP_USER=openwisp
+129
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
#!/usr/bin/env python3
2+
3+
import json
4+
import os
5+
import subprocess
6+
import sys
7+
8+
VXLAN_IPV4_METHOD = os.environ.get('WIREGUARD_VXLAN_IPV4_METHOD', 'link-local')
9+
VXLAN_IPV6_METHOD = os.environ.get('WIREGUARD_VXLAN_IPV6_METHOD', 'link-local')
10+
11+
try:
12+
peer_file_path = sys.argv[1]
13+
except IndexError:
14+
print('peer file must be passed as first argument', file=sys.stderr)
15+
sys.exit(1)
16+
17+
try:
18+
with open(peer_file_path, 'r') as peer_file:
19+
contents = peer_file.read()
20+
except FileNotFoundError as e:
21+
print(e, file=sys.stderr)
22+
sys.exit(2)
23+
24+
try:
25+
peers = json.loads(contents)
26+
assert isinstance(peers, list)
27+
except Exception as e:
28+
print(f'Error while parsing JSON file: {e}', file=sys.stderr)
29+
sys.exit(3)
30+
31+
32+
remote_peers = {}
33+
34+
for peer in peers:
35+
remote_peers[f'vxlan-vxlan{peer["vni"]}'] = peer
36+
37+
38+
class Nmcli:
39+
@classmethod
40+
def _exec_command(cls, command):
41+
process = subprocess.Popen(
42+
command.split(' '), stdout=subprocess.PIPE, stderr=subprocess.PIPE
43+
)
44+
stdout, stderr = process.communicate()
45+
if stderr:
46+
raise ValueError(stderr)
47+
return stdout.decode('utf8').strip()
48+
49+
@classmethod
50+
def list_connections(cls, type=None):
51+
output = cls._exec_command('nmcli connection show')
52+
lines = output.split('\n')
53+
connections = []
54+
for line in lines[1:]:
55+
parts = line.split()
56+
connection = {
57+
'name': parts[0].strip(),
58+
'uuid': parts[1].strip(),
59+
'type': parts[2].strip(),
60+
'device': parts[3].strip(),
61+
}
62+
if not type or type and type == connection['type']:
63+
connections.append(connection)
64+
return connections
65+
66+
@classmethod
67+
def get_connection(cls, connection):
68+
output = cls._exec_command(f'sudo nmcli connection show {connection}')
69+
data = {}
70+
lines = output.split('\n')
71+
for line in lines:
72+
parts = line.split()
73+
data[parts[0][:-1]] = parts[1]
74+
return data
75+
76+
@classmethod
77+
def get_local_vxlan_peers(cls):
78+
peers = {}
79+
vxlan_connections = cls.list_connections(type='vxlan')
80+
for vxlan in vxlan_connections:
81+
data = cls.get_connection(vxlan['uuid'])
82+
peers[data['connection.id']] = {
83+
'remote': data['vxlan.remote'],
84+
'vni': int(data['vxlan.id']),
85+
}
86+
return peers
87+
88+
@classmethod
89+
def add_connection(cls, ifname, vni, remote):
90+
return cls._exec_command(
91+
f'sudo nmcli connection add type vxlan ifname {ifname} '
92+
f'id {vni} remote {remote} destination-port 4789 '
93+
f'ipv4.method {VXLAN_IPV4_METHOD} ipv6.method {VXLAN_IPV6_METHOD}'
94+
)
95+
96+
@classmethod
97+
def edit_connection(cls, connection, vni, remote):
98+
return cls._exec_command(
99+
f'sudo nmcli connection modify {connection}'
100+
f' vxlan.id {vni} vxlan.remote {remote}'
101+
)
102+
103+
@classmethod
104+
def delete_connection(cls, connection):
105+
return cls._exec_command(f'sudo nmcli connection delete {connection}')
106+
107+
108+
local_peers = Nmcli.get_local_vxlan_peers()
109+
110+
111+
for connection_name, peer_data in local_peers.items():
112+
if connection_name not in remote_peers:
113+
Nmcli.delete_connection(connection_name)
114+
print(f'Removed {connection_name}')
115+
116+
117+
for connection_name, peer_data in remote_peers.items():
118+
vni = peer_data['vni']
119+
remote = peer_data['remote']
120+
if connection_name not in local_peers:
121+
Nmcli.add_connection(f'vxlan{vni}', vni, remote)
122+
print(f'Added {connection_name}')
123+
continue
124+
elif peer_data == local_peers[connection_name]:
125+
print(f'Skipping {connection_name}, already up to date')
126+
continue
127+
else:
128+
Nmcli.edit_connection(connection_name, vni, remote)
129+
print(f'Updated {connection_name}')

0 commit comments

Comments
 (0)