gluon-mesh-vpn-wireguard: initial support

This commit is contained in:
Christof Schulze 2018-08-18 23:01:16 +02:00
parent b7e8ad33a4
commit 81b728b704
16 changed files with 391 additions and 2 deletions

View File

@ -65,6 +65,7 @@ Several Freifunk communities in Germany use Gluon as the foundation of their Fre
package/gluon-ebtables-source-filter
package/gluon-hoodselector
package/gluon-mesh-batman-adv
package/gluon-mesh-vpn-wireguard
package/gluon-radv-filterd
package/gluon-scheduled-domain-switch
package/gluon-web-admin

View File

@ -0,0 +1,53 @@
gluon-mesh-vpn-wireguard
========================
This package allows WireGuard [1] to be used in Gluon. WireGuard establishes
VPN connections on OSI layer 3 allowing increased throughput in comparison with
fastd for mesh protocols that operate on layer 3 too.
When starting WireGuard, the system requires some entropy. It is recommended to
use haveged to avoid long startup times.
[1] https://wireguard.io
site.conf
---------
This is similar to the fastd-based mesh_vpn structure.
Example::
mesh_vpn = {
mtu = 1374,
wireguard = {
enabled = true,
groups = {
backbone = {
limit = 2,
peers = {
gw02 = {
enabled = true,
key = 'bog2DzyiC0Os7y1GloEw0afb8bLdZ9SzVQCd44Eock4=',
remote = 'gw02.babel.ffm.freifunk.net',
broker_port = 40000,
},
},
},
},
},
}
Server Side Configuration
-------------------------
* The wireguard private key must be deployed, and the derived Public Key has to be in site.conf
* The wg-broker-server script must be running on the server and be listening on
the broker_port
* The node must be able to reach the server using TCP-Port broker_port and it
must be able to communicate with the server using one UDP port between 40000
and 41000.
On dockerhub there is an image klausdieter371/wg-docker integrating the
server-side components. Please refer to its documentation to set up the server
part. The Code and Documentation are kept here:
https://github.com/FreifunkMD/wg-docker

View File

@ -9,7 +9,7 @@ packages 'web-wizard' \
packages 'web-wizard & autoupdater' \
'gluon-config-mode-autoupdater'
packages 'web-wizard & (mesh-vpn-fastd | mesh-vpn-tunneldigger)' \
packages 'web-wizard & (mesh-vpn-fastd | mesh-vpn-tunneldigger | mesh-vpn-wireguard)' \
'gluon-config-mode-mesh-vpn'

View File

@ -40,7 +40,7 @@ elseif has_fastd then
elseif has_wireguard then
local wireguard_enabled = uci:get_bool("wireguard", "mesh_vpn", "enabled")
if wireguard_enabled then
local secret = util.trim(util.exec("/usr/bin/gluon-mesh-vpn-wireguard-get-or-create-secret"))
local secret = uci:get("wireguard", "mesh_vpn", "secret")
pubkey = util.trim(util.exec("/usr/bin/wg pubkey < " .. secret))
msg = site_i18n._translate('gluon-config-mode:pubkey')
else

View File

@ -0,0 +1,19 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=gluon-mesh-vpn-wireguard
PKG_VERSION:=3
include ../gluon.mk
PKG_CONFIG_DEPENDS += $(GLUON_I18N_CONFIG)
define Package/gluon-mesh-vpn-wireguard
TITLE:=WireGuard Mesh VPN Support
DEPENDS:=+gluon-core +gluon-mesh-vpn-core +kmod-wireguard +jsonfilter +wireguard-tools +micrond +@BUSYBOX_CONFIG_TIMEOUT
endef
define Package/gluon-mesh-vpn-wireguard/description
Support layer 3 mesh-vpn connection using wireguard. This can be used by mesh-protocols working on OSI layer 3.
endef
$(eval $(call BuildPackageGluon,gluon-mesh-vpn-wireguard))

View File

@ -0,0 +1,17 @@
local function check_peer(k)
need_alphanumeric_key(k)
need_string_match(in_domain(extend(k, {'key'})), '[%w]+=*')
need_string_match(in_domain(extend(k, {'remote'})), '[%w_-.]')
need_number(in_domain(extend(k, {'broker_port'})), false)
end
local function check_group(k)
need_alphanumeric_key(k)
need_number(extend(k, {'limit'}), false)
need_table(extend(k, {'peers'}), check_peer, false)
need_table(extend(k, {'groups'}), check_group, false)
end
need_table({'mesh_vpn', 'wireguard', 'groups'}, check_group)

View File

@ -0,0 +1,7 @@
#!/bin/sh
touch /etc/config/gluon_mesh_vpn_wireguard
uci set gluon_mesh_vpn_wireguard.mesh_vpn="backbone"
if ! uci get gluon_mesh_vpn_wireguard.mesh_vpn.secret 2>/dev/null| wg pubkey 2>/dev/null; then
uci set gluon_mesh_vpn_wireguard.mesh_vpn.secret="generate"
fi

View File

@ -0,0 +1,94 @@
#!/bin/sh
# Copyright 2016-2017 Christof Schulze <christof@christofschulze.com>
# Licensed to the public under the Apache License 2.0.
. /lib/functions.sh
. ../netifd-proto.sh
init_proto "$@"
proto_gluon_wireguard_init_config() {
no_device=1
available=1
renew_handler=1
}
proto_gluon_wireguard_renew() {
local config="$1"
echo "wireguard RENEW: $*"
ifdown "$config"
ifup "$config"
}
proto_gluon_wireguard_setup() {
local config="$1"
ifname="$(uci get "network.$config.ifname")" # we need uci here because nodevice=1 means the device is not part of the ubus structure
local peer_limit=$(gluon-show-site |jsonfilter -e $.mesh_vpn.wireguard.groups.backbone.limit)
if [[ $(wg show all latest-handshakes |wc -l) -ge "$peer_limit" ]]; then
echo "not establishing another connection, we already have $peer_limit connections." >&2
ip link del "$ifname"
ifdown "$config"
exit 1
fi
(
flock -n 9
if [[ $(uci get gluon.mesh_vpn.enabled) -eq 1 ]]; then
ip link del "$ifname"
ip link add dev "$ifname" type wireguard
ip link set mtu "$(gluon-show-site | jsonfilter -e $.mesh_vpn.mtu)" dev "$ifname"
ip link set multicast on dev "$ifname"
mkdir -p /var/gluon/mesh-vpn-wireguard
secretfile=/var/gluon/mesh-vpn-wireguard/secret
secret=$(gluon-mesh-vpn-wireguard-get-or-create-secret)
echo "$secret" > "$secretfile"
pubkey=$(echo "$secret"| wg pubkey)
gwname=${config##*_}
peer=${gwname%?}
peer_config=$(gluon-show-site |jsonfilter -e "$.mesh_vpn.wireguard.groups.backbone.peers.$peer")
remote=$(jsonfilter -s "$peer_config" -e "$.remote")
brokerport=$(jsonfilter -s "$peer_config" -e "$.broker_port")
peer_key=$(jsonfilter -s "$peer_config" -e "$.key")
remoteport=$(/usr/bin/wg-broker-client "$ifname" "$pubkey" "$remote" "$brokerport")
if [[ "$remoteport" == "FULL" ]]; then
echo "wireguard server $remote is not accepting additional connections. Closing this interface" >&2
ip link del "$ifname"
exit 1
elif [[ "$remoteport" == "ERROR" ]]; then
echo "error when setting up wireguard connection for $ifname" >&2
ip link del "$ifname"
exit 1
elif [[ -z "$remoteport" ]]; then
echo "error when setting up wireguard connection for $ifname - no response from broker: $remote" >&2
ip link del "$ifname"
exit 1
fi
gluon-wan wg set "$ifname" private-key "$secretfile" peer "$peer_key" endpoint "$remote:$remoteport" allowed-ips ::/0 persistent-keepalive 25
ip link set dev "$ifname" up
ip -6 route add fe80::/64 dev "$ifname" proto kernel metric 256 pref medium table local
proto_init_update "$ifname" 1
proto_send_update "$config"
fi
) 9>"/var/lock/wireguard_proto_${ifname}.lock" || ifdown "$config"
}
proto_gluon_wireguard_teardown() {
local config="$1"
echo teardown config: "$config"
ifname=$(uci get "network.$config.ifname") # we need uci here because nodevice=1 means the device is not part of the ubus structure
ip link del "$ifname"
}
[[ -n "$INCLUDE_ONLY" ]] || {
add_protocol gluon_wireguard
}

View File

@ -0,0 +1,19 @@
#!/bin/sh
get_down_wg_backbone_interfaces() {
ubus -S call network.interface dump | jsonfilter -e '@.interface[@.up=false && @.proto="gluon_wireguard"].interface'
}
is_wan_up() {
ubus -S call network.interface dump | jsonfilter -e '@.interface[@.up=true && @.interface="wan"].up'
}
if is_wan_up >/dev/null; then
if [[ $(uci get gluon.mesh_vpn.enabled) == "1" ]]; then
for i in $(get_down_wg_backbone_interfaces)
do
ifup "$i"
done
fi
fi

View File

@ -0,0 +1,9 @@
#!/bin/sh
secret=$(uci get gluon_mesh_vpn_wireguard.mesh_vpn.secret)
if [[ "$secret" = "generate" ]]; then
secret="$(wg genkey)"
uci set gluon_mesh_vpn_wireguard.mesh_vpn.secret="$secret"
uci commit gluon_mesh_vpn_wireguard
fi
echo "$secret"

View File

@ -0,0 +1,61 @@
#!/bin/sh
timeout=10
run_broker() {
local interface="$1"
local pubkey="$2"
local remote="$3"
local brokerport="$4"
local port
local interval=5
localtime=$(date +%s)
# sleeping on stdin keeps the sockets open in nc, allowing us to receive a
# reply. Unfortunately this means all requests take $timeout seconds even
# if the server is faster
peer_reply="$( { echo '{"version":1, "pubkey":"'"$pubkey"'"}'; sleep $timeout; } | gluon-wan timeout $timeout nc "$remote" "$brokerport" | tail -n1)"
if [[ "x$peer_reply" != "x" ]]; then
port=$(jsonfilter -s "$peer_reply" -e "@.port")
peer_time=$(jsonfilter -s "$peer_reply" -e "@.time")
difference=0
if [[ $peer_time -gt $localtime ]]; then
difference=$((peer_time - localtime))
else
difference=$((localtime - peer_time))
fi
if [[ "x$peer_time" != "x" && $difference -gt 240 ]]; then
# local clock differs a lot from the peer clock.
# assuming ntp is working only when a tunnel is established we need to
# set the clock to something in the proximity of the correct time.
# Let's assume peer_time for now. ntpd will handle the rest
formatted_time=$(date -d "@$peer_time" +%Y%m%d%H%M.%S)
date -s "$formatted_time" >/dev/null
fi
if [[ -z $port ]]; then
error=$(jsonfilter -s "$peer_reply" -e "@.error")
if [[ -n $error ]]; then
reason=$(jsonfilter -s "$peer_reply" -e "@.error.reason")
ecode=$(jsonfilter -s "$peer_reply" -e "@.error.code")
echo "received error [$ecode] from host $remote: $reason" >&2
if [[ "$ecode" == "1" ]]; then
echo FULL
return 1
fi
fi
fi
echo "$port"
return 0
else
echo "Received no reply from peer $remote" >&2
echo "ERROR"
return 255
fi
}
run_broker "$1" "$2" "$3" "$4"

View File

@ -0,0 +1,40 @@
#!/bin/sh
curtime=$(date +%s)
get_wg_interfaces() {
ubus -S call network.interface dump | jsonfilter -e '@.interface[@.up=true && @.proto="gluon_wireguard"].l3_device'
}
get_connection_count() {
ubus -S call network.interface dump | jsonfilter -e '@.interface[@.up=true && @.proto="gluon_wireguard" && @].l3_device' | wc -l
}
get_interface_from_ifname() {
ubus -S call network.interface dump | jsonfilter -e "@.interface[@.proto=\"gluon_wireguard\" && @.l3_device=\"$1\"].interface"
}
# purge wg interface that have terminated
for i in $(get_wg_interfaces)
do
line=$(wg show "$i" latest-handshakes)
if [[ -n "${line}" ]]; then
latest=$(echo "${line}"| awk '{print $2}')
diff=$((curtime-latest))
if [[ $diff -gt 600 ]]; then
ifdown "$(get_interface_from_ifname "${i}")"
fi
else
ifdown "$(get_interface_from_ifname "${i}")"
fi
done
# in case less than our peer-limit connections is "up", start all wg interfaces that are currently down
if [[ "$(uci get gluon.mesh_vpn.enabled)" == "1" ]] &&
[[ $(get_connection_count) -lt $(gluon-show-site |jsonfilter -e $.mesh_vpn.wireguard.groups.backbone.limit) ]]; then
if [[ $(get_connection_count) -gt 0 ]]; then
# it is ok to wait for a backup vpn connection. This sleep spreads the load for the servers
sleep "$(awk 'BEGIN{srand();print int(rand()*180)}')"
fi
/usr/bin/enable-all-wg-interfaces
fi

View File

@ -0,0 +1 @@
*/5 * * * * /usr/bin/wgcheck

View File

@ -0,0 +1,61 @@
#!/usr/bin/lua
local site = require 'gluon.site'
local uci = require('simple-uci').cursor()
local iputil = require 'gluon.iputil'
local sysconfig = require 'gluon.sysconfig'
local add_groups
local function generate_section_name(peer)
return "mesh_vpn_wg_" .. peer
end
local function generate_wg_iface_name(peer)
return "mesh-vpn-" .. peer
end
local function add_peer(name, count)
local ip = iputil.mac_to_ip("fe80::/64", sysconfig.primary_mac, 0x00, count)
uci:section('network', 'interface', generate_section_name(name) .. 'm', {
type = 'wireguard',
ifname = generate_wg_iface_name(count),
proto = 'gluon_mesh',
})
uci:section('network', 'interface', generate_section_name(name) .. 's', {
ip6addr = ip,
ifname = generate_wg_iface_name(count),
proto = 'static',
})
uci:section('network', 'interface', generate_section_name(name) .. 'w', {
ifname = generate_wg_iface_name(count),
proto = 'gluon_wireguard',
})
end
local function add_group(name, config)
local count=0
if config.peers then
for peername, _ in pairs(config.peers) do
add_peer(peername, count)
count = count + 1
end
end
add_groups(name, config.groups)
end
-- declared local above
function add_groups(prefix, groups)
if groups then
for name, group in pairs(groups) do
add_group(prefix .. '_' .. name, group)
end
end
end
add_groups('mesh_vpn', site.mesh_vpn.wireguard.groups())
uci:save('network')

View File

@ -18,6 +18,13 @@
pubkey = nil
end
end
local wg_meshvpn_enabled = uci:get_bool("wireguard", "mesh_vpn_backbone", "enabled")
if wg_meshvpn_enabled then
pubkey = util.trim(util.exec('/usr/bin/wg pubkey < $(uci get wireguard.mesh_vpn.secret)'))
if pubkey == '' then
pubkey = nil
end
end
local values = {
{ _('Hostname'), pretty_hostname.get(uci) },