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 3, 2022
1 parent aa6ce9b commit 60f9d37
Show file tree
Hide file tree
Showing 10 changed files with 457 additions and 3 deletions.
9 changes: 6 additions & 3 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,12 @@ X509_ORGANIZATION_NAME=OpenWISP
X509_ORGANIZATION_UNIT_NAME=OpenWISP
X509_EMAIL=[email protected]
X509_COMMON_NAME=OpenWISP
# VPN
VPN_NAME=default
VPN_CLIENT_NAME=default-management-vpn
# WireGuard
WIREGUARD_VPN_DOMAIN=wireguard.openwisp.org
WIREGUARD_FLASK_HOST=0.0.0.0
WIREGUARD_FLASK_PORT=8081
WIREGUARD_FLASK_ENDPOINT=/trigger-update
WIREGUARD_FLASK_KEY='openwisp-wireguard-updater-auth-key'
# Developer
DEBUG_MODE=False
DJANGO_LOG_LEVEL=INFO
Expand Down
27 changes: 27 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,33 @@ services:
cap_add:
- NET_ADMIN

wireguard:
image: openwisp/openwisp-wireguard:latest
build:
context: images
dockerfile: openwisp_wireguard/Dockerfile
env_file:
- .env
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
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
8 changes: 8 additions & 0 deletions images/common/utils.sh
Original file line number Diff line number Diff line change
Expand Up @@ -239,3 +239,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
}
32 changes: 32 additions & 0 deletions images/openwisp_wireguard/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# 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

RUN rm /etc/cont-init.d/40-confs && rm /etc/services.d/wireguard -r
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 \
OPENWISP_USER=root
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('VXLAN_IPV4_METHOD', 'link-local')
VXLAN_IPV6_METHOD = os.environ.get('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}')
154 changes: 154 additions & 0 deletions images/openwisp_wireguard/update_wireguard.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
#!/bin/bash

if [ "$(whoami)" != "$OPENWISP_USER" ]; then
echo "Script should only be run by $OPENWISP_USER. Exiting!"
exit 9
fi

# make sure this directory is writable by the user which calls the script
CONF_DIR="/opt/openwisp"

# do not modify these vars
_VPN_URL_PATH="$API_INTERNAL/controller/vpn"
_VPN_CHECKSUM_URL="$_VPN_URL_PATH/checksum/$VPN_UUID/?key=$VPN_KEY"
_VPN_DOWNLOAD_URL="$_VPN_URL_PATH/download-config/$VPN_UUID/?key=$VPN_KEY"
_WORKING_DIR="$CONF_DIR/.openwisp"
_CHECKSUM_FILE="$_WORKING_DIR/checksum"
_TIMESTAMP_FILE="$_WORKING_DIR/timestamp"
_MANAGED_INTERFACE="$_WORKING_DIR/managed-interface"
_APPLIED_CONF_DIR="$_WORKING_DIR/current-conf"
_CONF_TAR="$_WORKING_DIR/conf.tar.gz"
_CURL="curl -s --show-error --fail"
if [ "$INSECURE_CURL" == true ]; then
_CURL = "$_CURL --insecure"
fi

mkdir -p $_WORKING_DIR
mkdir -p $_APPLIED_CONF_DIR

assert_exit_code() {
exit_code=$?
lineno=$(($1 - 1))
if [ "$exit_code" != "0" ]; then
echo "Line $lineno: Command returned non zero exit code: $exit_code"
exit $exit_code
fi
}

check_config() {
_latest_checksum=$($_CURL $_VPN_CHECKSUM_URL)
assert_exit_code $LINENO
if [ -f "$_CHECKSUM_FILE" ]; then
_current_checksum=$(cat $_CHECKSUM_FILE)
else
_current_checksum=""
fi

if [ "$_current_checksum" != "$_latest_checksum" ]; then
echo "Configuration changed, downloading new configuration..."
update_config
fi
}

clean_old_interface() {
echo "Bringing down old wireguard interface $managed_interface_name"
for old_conf_file in $_APPLIED_CONF_DIR/*.conf; do
[ -e "$old_conf_file" ] || continue
sudo wg-quick down $old_conf_file
done
rm $_APPLIED_CONF_DIR/*.conf
}

create_new_interface() {
echo "Bringing up new wireguard interface $interface"
sudo wg-quick up $file
}

update_config() {
# Set file permissions to 0660, otherwise wg will complain
# for having public configurations
umask 0117
$($_CURL $_VPN_DOWNLOAD_URL >"$_CONF_TAR")
assert_exit_code $LINENO
echo "Configuration downloaded, extracting it..."
tar -zxvf $_CONF_TAR -C $CONF_DIR >/dev/null
assert_exit_code $LINENO
if [ -e "$_MANAGED_INTERFACE" ]; then
managed_interface_name=$(cat "$_MANAGED_INTERFACE")
fi

for file in $CONF_DIR/*.conf; do
[ -e "$file" ] || continue
filename=$(basename $file)
interface="${filename%.*}"

# There is no managed_interface
if [ -z ${managed_interface_name+x} ]; then
create_new_interface
# Current managed interface is not present in new configuration
elif [ "$managed_interface_name" != "$interface" ]; then
clean_old_interface
assert_exit_code $LINENO
create_new_interface
assert_exit_code $LINENO
else
# Update the configuration of current managed interface
echo "Reloading wireguard interface $interface with config file $file..."
wg_conf_filename="$filename-wg"
sudo wg-quick strip "$CONF_DIR/$filename" >"$CONF_DIR/$wg_conf_filename"
assert_exit_code $LINENO
sudo wg syncconf $interface "$CONF_DIR/$wg_conf_filename"
assert_exit_code $LINENO
rm "$CONF_DIR/$wg_conf_filename"
fi
echo "$interface" >"$_MANAGED_INTERFACE"
mv -f "$file" "$_APPLIED_CONF_DIR/$filename"
assert_exit_code $LINENO
done

# Save checksum of applied configuration
echo $_latest_checksum >$_CHECKSUM_FILE

export VXLAN_IPV4_METHOD="{{ openwisp2_wireguard_vxlan_ipv4_method }}" \
VXLAN_IPV6_METHOD="{{ openwisp2_wireguard_vxlan_ipv6_method }}"
if [ -e "$CONF_DIR/vxlan.json" ]; then
"$CONF_DIR/update_vxlan.py" "$CONF_DIR/vxlan.json"
mv -f "$CONF_DIR/vxlan.json" "$_APPLIED_CONF_DIR/vxlan.json"
fi
}

bring_up_interface() {
for conf_file in $_APPLIED_CONF_DIR/*.conf; do
[ -e "$conf_file" ] || continue
sudo wg-quick up $conf_file || true
done
exit 0
}

watch_configuration_change() {
_REDIS_CMD="redis-cli -h $REDIS_HOST"
if [[ "$REDIS_PORT" ]]; then
_REDIS_CMD="$_REDIS_CMD -p $REDIS_PORT"
fi
if [[ "$REDIS_PASSWORD" ]]; then
_REDIS_CMD="$_REDIS_CMD -a $REDIS_PASSWORD -n 15"
fi
while true; do
if [ -f "$_TIMESTAMP_FILE" ]; then
local_timestamp=$(cat $_TIMESTAMP_FILE)
else
local_timestamp=""
fi
current_timestamp=$($_REDIS_CMD GET wg-$VPN_UUID)
if [ "$current_timestamp" != "$local_timestamp" ]; then
echo "Configuration reload triggered by the updater."
check_config
assert_exit_code $LINENO
# Save timestamp of applied configuration
echo $current_timestamp >$_TIMESTAMP_FILE
fi
sleep 3
done
}

"$@"
Loading

0 comments on commit 60f9d37

Please sign in to comment.