Skip to content

Commit 60f9d37

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 60f9d37

File tree

10 files changed

+457
-3
lines changed

10 files changed

+457
-3
lines changed

.env

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,12 @@ X509_ORGANIZATION_NAME=OpenWISP
3939
X509_ORGANIZATION_UNIT_NAME=OpenWISP
4040
4141
X509_COMMON_NAME=OpenWISP
42-
# VPN
43-
VPN_NAME=default
44-
VPN_CLIENT_NAME=default-management-vpn
42+
# WireGuard
43+
WIREGUARD_VPN_DOMAIN=wireguard.openwisp.org
44+
WIREGUARD_FLASK_HOST=0.0.0.0
45+
WIREGUARD_FLASK_PORT=8081
46+
WIREGUARD_FLASK_ENDPOINT=/trigger-update
47+
WIREGUARD_FLASK_KEY='openwisp-wireguard-updater-auth-key'
4548
# Developer
4649
DEBUG_MODE=False
4750
DJANGO_LOG_LEVEL=INFO

docker-compose.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,33 @@ services:
158158
cap_add:
159159
- NET_ADMIN
160160

161+
wireguard:
162+
image: openwisp/openwisp-wireguard:latest
163+
build:
164+
context: images
165+
dockerfile: openwisp_wireguard/Dockerfile
166+
env_file:
167+
- .env
168+
ports:
169+
- 51820:51820/udp
170+
sysctls:
171+
- net.ipv4.conf.all.src_valid_mark=1
172+
cap_add:
173+
- NET_ADMIN
174+
- SYS_MODULE
175+
176+
wireguard_updater:
177+
image: openwisp/openwisp-wireguard-updater:latest
178+
build:
179+
context: images
180+
dockerfile: openwisp_wireguard_updater/Dockerfile
181+
env_file:
182+
- .env
183+
networks:
184+
default:
185+
aliases:
186+
- wireguard.internal
187+
161188
postgres:
162189
image: mdillon/postgis:11-alpine
163190
environment:

images/common/init_command.sh

Lines changed: 8 additions & 0 deletions
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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,3 +239,11 @@ function crl_download {
239239
export CAid=$(psql -qAtc "SELECT ca_id FROM config_vpn where name='${VPN_NAME}';")
240240
wget -qO revoked.crl --no-check-certificate ${DASHBOARD_INTERNAL}/admin/pki/ca/${CAid}.crl
241241
}
242+
243+
function wireguard_setup {
244+
bash /opt/openwisp/update_wireguard.sh bring_up_interface
245+
bash /opt/openwisp/update_wireguard.sh check_config
246+
echo "*/5 * * * * bash /opt/openwisp/update_wireguard.sh check_config" | sudo crontab
247+
sudo cron
248+
bash /opt/openwisp/update_wireguard.sh watch_configuration_change
249+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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+
RUN rm /etc/cont-init.d/40-confs && rm /etc/services.d/wireguard -r
11+
RUN useradd --system --password '' --create-home --shell /bin/bash \
12+
--gid root --groups sudo --uid 1001 openwisp
13+
RUN echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
14+
RUN chown -R openwisp:root /opt/openwisp
15+
16+
USER openwisp:root
17+
18+
COPY --chown=openwisp:root ./openwisp_wireguard/update_vxlan.py \
19+
./openwisp_wireguard/update_wireguard.sh \
20+
./common/init_command.sh \
21+
./common/utils.sh \
22+
./common/services.py /opt/openwisp/
23+
24+
CMD ["bash", "init_command.sh"]
25+
26+
EXPOSE 51820
27+
28+
ENV MODULE_NAME=wireguard \
29+
DASHBOARD_INTERNAL=dashboard.internal \
30+
API_INTERNAL=api.internal \
31+
REDIS_HOST=redis \
32+
OPENWISP_USER=root
Lines changed: 129 additions & 0 deletions
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('VXLAN_IPV4_METHOD', 'link-local')
9+
VXLAN_IPV6_METHOD = os.environ.get('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}')
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
#!/bin/bash
2+
3+
if [ "$(whoami)" != "$OPENWISP_USER" ]; then
4+
echo "Script should only be run by $OPENWISP_USER. Exiting!"
5+
exit 9
6+
fi
7+
8+
# make sure this directory is writable by the user which calls the script
9+
CONF_DIR="/opt/openwisp"
10+
11+
# do not modify these vars
12+
_VPN_URL_PATH="$API_INTERNAL/controller/vpn"
13+
_VPN_CHECKSUM_URL="$_VPN_URL_PATH/checksum/$VPN_UUID/?key=$VPN_KEY"
14+
_VPN_DOWNLOAD_URL="$_VPN_URL_PATH/download-config/$VPN_UUID/?key=$VPN_KEY"
15+
_WORKING_DIR="$CONF_DIR/.openwisp"
16+
_CHECKSUM_FILE="$_WORKING_DIR/checksum"
17+
_TIMESTAMP_FILE="$_WORKING_DIR/timestamp"
18+
_MANAGED_INTERFACE="$_WORKING_DIR/managed-interface"
19+
_APPLIED_CONF_DIR="$_WORKING_DIR/current-conf"
20+
_CONF_TAR="$_WORKING_DIR/conf.tar.gz"
21+
_CURL="curl -s --show-error --fail"
22+
if [ "$INSECURE_CURL" == true ]; then
23+
_CURL = "$_CURL --insecure"
24+
fi
25+
26+
mkdir -p $_WORKING_DIR
27+
mkdir -p $_APPLIED_CONF_DIR
28+
29+
assert_exit_code() {
30+
exit_code=$?
31+
lineno=$(($1 - 1))
32+
if [ "$exit_code" != "0" ]; then
33+
echo "Line $lineno: Command returned non zero exit code: $exit_code"
34+
exit $exit_code
35+
fi
36+
}
37+
38+
check_config() {
39+
_latest_checksum=$($_CURL $_VPN_CHECKSUM_URL)
40+
assert_exit_code $LINENO
41+
if [ -f "$_CHECKSUM_FILE" ]; then
42+
_current_checksum=$(cat $_CHECKSUM_FILE)
43+
else
44+
_current_checksum=""
45+
fi
46+
47+
if [ "$_current_checksum" != "$_latest_checksum" ]; then
48+
echo "Configuration changed, downloading new configuration..."
49+
update_config
50+
fi
51+
}
52+
53+
clean_old_interface() {
54+
echo "Bringing down old wireguard interface $managed_interface_name"
55+
for old_conf_file in $_APPLIED_CONF_DIR/*.conf; do
56+
[ -e "$old_conf_file" ] || continue
57+
sudo wg-quick down $old_conf_file
58+
done
59+
rm $_APPLIED_CONF_DIR/*.conf
60+
}
61+
62+
create_new_interface() {
63+
echo "Bringing up new wireguard interface $interface"
64+
sudo wg-quick up $file
65+
}
66+
67+
update_config() {
68+
# Set file permissions to 0660, otherwise wg will complain
69+
# for having public configurations
70+
umask 0117
71+
$($_CURL $_VPN_DOWNLOAD_URL >"$_CONF_TAR")
72+
assert_exit_code $LINENO
73+
echo "Configuration downloaded, extracting it..."
74+
tar -zxvf $_CONF_TAR -C $CONF_DIR >/dev/null
75+
assert_exit_code $LINENO
76+
if [ -e "$_MANAGED_INTERFACE" ]; then
77+
managed_interface_name=$(cat "$_MANAGED_INTERFACE")
78+
fi
79+
80+
for file in $CONF_DIR/*.conf; do
81+
[ -e "$file" ] || continue
82+
filename=$(basename $file)
83+
interface="${filename%.*}"
84+
85+
# There is no managed_interface
86+
if [ -z ${managed_interface_name+x} ]; then
87+
create_new_interface
88+
# Current managed interface is not present in new configuration
89+
elif [ "$managed_interface_name" != "$interface" ]; then
90+
clean_old_interface
91+
assert_exit_code $LINENO
92+
create_new_interface
93+
assert_exit_code $LINENO
94+
else
95+
# Update the configuration of current managed interface
96+
echo "Reloading wireguard interface $interface with config file $file..."
97+
wg_conf_filename="$filename-wg"
98+
sudo wg-quick strip "$CONF_DIR/$filename" >"$CONF_DIR/$wg_conf_filename"
99+
assert_exit_code $LINENO
100+
sudo wg syncconf $interface "$CONF_DIR/$wg_conf_filename"
101+
assert_exit_code $LINENO
102+
rm "$CONF_DIR/$wg_conf_filename"
103+
fi
104+
echo "$interface" >"$_MANAGED_INTERFACE"
105+
mv -f "$file" "$_APPLIED_CONF_DIR/$filename"
106+
assert_exit_code $LINENO
107+
done
108+
109+
# Save checksum of applied configuration
110+
echo $_latest_checksum >$_CHECKSUM_FILE
111+
112+
export VXLAN_IPV4_METHOD="{{ openwisp2_wireguard_vxlan_ipv4_method }}" \
113+
VXLAN_IPV6_METHOD="{{ openwisp2_wireguard_vxlan_ipv6_method }}"
114+
if [ -e "$CONF_DIR/vxlan.json" ]; then
115+
"$CONF_DIR/update_vxlan.py" "$CONF_DIR/vxlan.json"
116+
mv -f "$CONF_DIR/vxlan.json" "$_APPLIED_CONF_DIR/vxlan.json"
117+
fi
118+
}
119+
120+
bring_up_interface() {
121+
for conf_file in $_APPLIED_CONF_DIR/*.conf; do
122+
[ -e "$conf_file" ] || continue
123+
sudo wg-quick up $conf_file || true
124+
done
125+
exit 0
126+
}
127+
128+
watch_configuration_change() {
129+
_REDIS_CMD="redis-cli -h $REDIS_HOST"
130+
if [[ "$REDIS_PORT" ]]; then
131+
_REDIS_CMD="$_REDIS_CMD -p $REDIS_PORT"
132+
fi
133+
if [[ "$REDIS_PASSWORD" ]]; then
134+
_REDIS_CMD="$_REDIS_CMD -a $REDIS_PASSWORD -n 15"
135+
fi
136+
while true; do
137+
if [ -f "$_TIMESTAMP_FILE" ]; then
138+
local_timestamp=$(cat $_TIMESTAMP_FILE)
139+
else
140+
local_timestamp=""
141+
fi
142+
current_timestamp=$($_REDIS_CMD GET wg-$VPN_UUID)
143+
if [ "$current_timestamp" != "$local_timestamp" ]; then
144+
echo "Configuration reload triggered by the updater."
145+
check_config
146+
assert_exit_code $LINENO
147+
# Save timestamp of applied configuration
148+
echo $current_timestamp >$_TIMESTAMP_FILE
149+
fi
150+
sleep 3
151+
done
152+
}
153+
154+
"$@"

0 commit comments

Comments
 (0)