-
-
Notifications
You must be signed in to change notification settings - Fork 82
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[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
- Loading branch information
Showing
10 changed files
with
457 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
|
||
"$@" |
Oops, something went wrong.