Skip to content

Commit

Permalink
[feature] Added support for WireGuard and VXLAN #225
Browse files Browse the repository at this point in the history
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
  • Loading branch information
pandafy committed Jun 15, 2022
1 parent f9bdb1c commit a01539c
Show file tree
Hide file tree
Showing 11 changed files with 490 additions and 3 deletions.
9 changes: 8 additions & 1 deletion .env
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# Essential
DASHBOARD_DOMAIN=dashboard.openwisp.org
API_DOMAIN=api.openwisp.org
VPN_DOMAIN=openvpn.openwisp.org
WIREGUARD_UPDATER_DOMAIN=wireguard.openwisp.org
EMAIL_DJANGO_DEFAULT=[email protected]
DB_USER=admin
DB_PASS=admin
Expand Down Expand Up @@ -42,6 +42,13 @@ X509_COMMON_NAME=OpenWISP
# VPN
VPN_NAME=default
VPN_CLIENT_NAME=default-management-vpn
# WireGuard
WIREGUARD_UPDATER_PORT=8081
WIREGUARD_UPDATER_ENDPOINT=/trigger-update
WIREGUARD_UPDATER_KEY=openwisp-wireguard-updater-auth-key
WIREGUARD_UPDATER_INSECURE_CURL=false
WIREGUARD_VXLAN_IPV4_METHOD=link-local
WIREGUARD_VXLAN_IPV6_METHOD=link-local
# Developer
DEBUG_MODE=False
DJANGO_LOG_LEVEL=INFO
Expand Down
33 changes: 33 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,13 +111,15 @@ services:
aliases:
- dashboard.internal
- api.internal
- wireguard_updater.internal
ports:
- "80:80"
- "443:443"
depends_on:
- dashboard
- api
- websocket
- wireguard_updater

freeradius:
image: openwisp/openwisp-freeradius:latest
Expand Down Expand Up @@ -158,6 +160,37 @@ services:
cap_add:
- NET_ADMIN

wireguard:
image: openwisp/openwisp-wireguard:latest
build:
context: images
dockerfile: openwisp_wireguard/Dockerfile
env_file:
- .env
volumes:
- /lib/modules:/lib/modules
ports:
- 51820:51820/udp
sysctls:
- net.ipv4.conf.all.src_valid_mark=1
cap_add:
- NET_ADMIN
- SYS_MODULE

wireguard_updater:
image: openwisp/openwisp-wireguard-updater:latest
build:
context: images
dockerfile: openwisp_wireguard_updater/Dockerfile
args:
WIREGUARD_UPDATER_APP_PORT: 8081
env_file:
- .env
networks:
default:
aliases:
- wireguard.internal

postgres:
image: mdillon/postgis:11-alpine
environment:
Expand Down
8 changes: 8 additions & 0 deletions images/common/init_command.sh
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ elif [ "$MODULE_NAME" = 'openvpn' ]; then
# docker container running, restarting would mean killing
# the container while supervisor helps only to restart the service!
supervisord --nodaemon --configuration supervisord.conf
elif [ "$MODULE_NAME" = 'wireguard' ]; then
if [[ -z "$VPN_UUID" || -z "$VPN_KEY" ]]; then
echo "You need to cofigure VPN_UUID and VPN_KEY environment varibales."
fi
wait_nginx_services
wireguard_setup
elif [ "$MODULE_NAME" = 'wireguard_updater' ]; then
start_uwsgi
elif [ "$MODULE_NAME" = 'nginx' ]; then
rm -rf /etc/nginx/conf.d/default.conf
if [ "$NGINX_CUSTOM_FILE" = 'True' ]; then
Expand Down
23 changes: 22 additions & 1 deletion images/common/utils.sh
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,19 @@ function create_prod_certs {
--domain ${API_DOMAIN} \
--email ${CERT_ADMIN_EMAIL}
fi
if [ ! -f /etc/letsencrypt/live/${WIREGUARD_UPDATER_DOMAIN}/privkey.pem ]; then
certbot certonly --standalone --noninteractive --agree-tos \
--rsa-key-size 4096 \
--domain ${WIREGUARD_UPDATER_DOMAIN} \
--email ${CERT_ADMIN_EMAIL}
fi
}

function create_dev_certs {
# Ensure required directories exist
mkdir -p /etc/letsencrypt/live/${DASHBOARD_DOMAIN}/
mkdir -p /etc/letsencrypt/live/${API_DOMAIN}/
mkdir -p /etc/letsencrypt/live/${WIREGUARD_UPDATER_DOMAIN}/
# Create self-signed certificates
if [ ! -f /etc/letsencrypt/live/${DASHBOARD_DOMAIN}/privkey.pem ]; then
openssl req -x509 -newkey rsa:4096 \
Expand All @@ -60,6 +67,12 @@ function create_dev_certs {
-out /etc/letsencrypt/live/${API_DOMAIN}/fullchain.pem \
-days 365 -nodes -subj '/CN=OpenWISP'
fi
if [ ! -f /etc/letsencrypt/live/${WIREGUARD_UPDATER_DOMAIN}/privkey.pem ]; then
openssl req -x509 -newkey rsa:4096 \
-keyout /etc/letsencrypt/live/${WIREGUARD_UPDATER_DOMAIN}/privkey.pem \
-out /etc/letsencrypt/live/${WIREGUARD_UPDATER_DOMAIN}/fullchain.pem \
-days 365 -nodes -subj '/CN=OpenWISP'
fi
}

function nginx_dev {
Expand Down Expand Up @@ -109,7 +122,7 @@ function ssl_http_behaviour {
function envsubst_create_config {
# Creates nginx configurations files for dashboard
# and api instances.
for application in DASHBOARD API; do
for application in DASHBOARD API WIREGUARD_UPDATER; do
eval export APP_SERVICE=\$${application}_APP_SERVICE
eval export APP_PORT=\$${application}_APP_PORT
eval export DOMAIN=\$${application}_${3}
Expand Down Expand Up @@ -239,3 +252,11 @@ function crl_download {
export CAid=$(psql -qAtc "SELECT ca_id FROM config_vpn where name='${VPN_NAME}';")
wget -qO revoked.crl --no-check-certificate ${DASHBOARD_INTERNAL}/admin/pki/ca/${CAid}.crl
}

function wireguard_setup {
bash /opt/openwisp/update_wireguard.sh bring_up_interface
bash /opt/openwisp/update_wireguard.sh check_config
echo "*/5 * * * * bash /opt/openwisp/update_wireguard.sh check_config" | sudo crontab
sudo cron
bash /opt/openwisp/update_wireguard.sh watch_configuration_change
}
6 changes: 5 additions & 1 deletion images/openwisp_nginx/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,17 @@ ENV MODULE_NAME=nginx \
DASHBOARD_APP_PORT=8000 \
API_APP_PORT=8001 \
WEBSOCKET_APP_PORT=8002 \
WIREGUARD_UPDATER_APP_PORT=8081 \
# Application Service Name
DASHBOARD_APP_SERVICE=dashboard \
API_APP_SERVICE=api \
WEBSOCKET_APP_SERVICE=websocket \
WIREGUARD_UPDATER_APP_SERVICE=wireguard_updater \
# Listen domains
DASHBOARD_DOMAIN=dashboard.example.com \
API_DOMAIN=api.example.com \
WIREGUARD_UPDATER_DOMAIN=wireguard_updater.example.com \
# Inter container communication domains
DASHBOARD_INTERNAL=dashboard.internal \
API_INTERNAL=api.internal
API_INTERNAL=api.internal \
WIREGUARD_UPDATER_INTERNAL=wireguard_updater.internal
36 changes: 36 additions & 0 deletions images/openwisp_wireguard/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# hadolint ignore=DL3007
FROM linuxserver/wireguard:latest

WORKDIR /opt/openwisp

RUN apt update && \
apt install -y sudo network-manager cron redis-tools wget && \
apt autoclean

# Remove services from the base image
RUN rm /etc/cont-init.d/40-confs && \
rm -r /etc/services.d/wireguard && \
rm -r /etc/services.d/coredns
RUN useradd --system --password '' --create-home --shell /bin/bash \
--gid root --groups sudo --uid 1001 openwisp
RUN echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
RUN chown -R openwisp:root /opt/openwisp

USER openwisp:root

COPY --chown=openwisp:root ./openwisp_wireguard/update_vxlan.py \
./openwisp_wireguard/update_wireguard.sh \
./common/init_command.sh \
./common/utils.sh \
./common/services.py /opt/openwisp/

CMD ["bash", "init_command.sh"]

EXPOSE 51820

ENV MODULE_NAME=wireguard \
DASHBOARD_INTERNAL=dashboard.internal \
API_INTERNAL=api.internal \
REDIS_HOST=redis \
REDIS_DATABASE=15 \
OPENWISP_USER=openwisp
129 changes: 129 additions & 0 deletions images/openwisp_wireguard/update_vxlan.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
#!/usr/bin/env python3

import json
import os
import subprocess
import sys

VXLAN_IPV4_METHOD = os.environ.get('WIREGUARD_VXLAN_IPV4_METHOD', 'link-local')
VXLAN_IPV6_METHOD = os.environ.get('WIREGUARD_VXLAN_IPV6_METHOD', 'link-local')

try:
peer_file_path = sys.argv[1]
except IndexError:
print('peer file must be passed as first argument', file=sys.stderr)
sys.exit(1)

try:
with open(peer_file_path, 'r') as peer_file:
contents = peer_file.read()
except FileNotFoundError as e:
print(e, file=sys.stderr)
sys.exit(2)

try:
peers = json.loads(contents)
assert isinstance(peers, list)
except Exception as e:
print(f'Error while parsing JSON file: {e}', file=sys.stderr)
sys.exit(3)


remote_peers = {}

for peer in peers:
remote_peers[f'vxlan-vxlan{peer["vni"]}'] = peer


class Nmcli:
@classmethod
def _exec_command(cls, command):
process = subprocess.Popen(
command.split(' '), stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
stdout, stderr = process.communicate()
if stderr:
raise ValueError(stderr)
return stdout.decode('utf8').strip()

@classmethod
def list_connections(cls, type=None):
output = cls._exec_command('nmcli connection show')
lines = output.split('\n')
connections = []
for line in lines[1:]:
parts = line.split()
connection = {
'name': parts[0].strip(),
'uuid': parts[1].strip(),
'type': parts[2].strip(),
'device': parts[3].strip(),
}
if not type or type and type == connection['type']:
connections.append(connection)
return connections

@classmethod
def get_connection(cls, connection):
output = cls._exec_command(f'sudo nmcli connection show {connection}')
data = {}
lines = output.split('\n')
for line in lines:
parts = line.split()
data[parts[0][:-1]] = parts[1]
return data

@classmethod
def get_local_vxlan_peers(cls):
peers = {}
vxlan_connections = cls.list_connections(type='vxlan')
for vxlan in vxlan_connections:
data = cls.get_connection(vxlan['uuid'])
peers[data['connection.id']] = {
'remote': data['vxlan.remote'],
'vni': int(data['vxlan.id']),
}
return peers

@classmethod
def add_connection(cls, ifname, vni, remote):
return cls._exec_command(
f'sudo nmcli connection add type vxlan ifname {ifname} '
f'id {vni} remote {remote} destination-port 4789 '
f'ipv4.method {VXLAN_IPV4_METHOD} ipv6.method {VXLAN_IPV6_METHOD}'
)

@classmethod
def edit_connection(cls, connection, vni, remote):
return cls._exec_command(
f'sudo nmcli connection modify {connection}'
f' vxlan.id {vni} vxlan.remote {remote}'
)

@classmethod
def delete_connection(cls, connection):
return cls._exec_command(f'sudo nmcli connection delete {connection}')


local_peers = Nmcli.get_local_vxlan_peers()


for connection_name, peer_data in local_peers.items():
if connection_name not in remote_peers:
Nmcli.delete_connection(connection_name)
print(f'Removed {connection_name}')


for connection_name, peer_data in remote_peers.items():
vni = peer_data['vni']
remote = peer_data['remote']
if connection_name not in local_peers:
Nmcli.add_connection(f'vxlan{vni}', vni, remote)
print(f'Added {connection_name}')
continue
elif peer_data == local_peers[connection_name]:
print(f'Skipping {connection_name}, already up to date')
continue
else:
Nmcli.edit_connection(connection_name, vni, remote)
print(f'Updated {connection_name}')
Loading

0 comments on commit a01539c

Please sign in to comment.