diff --git a/docs/index.rst b/docs/index.rst index c3010fc8..abe8d527 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -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 diff --git a/docs/package/gluon-mesh-vpn-wireguard.rst b/docs/package/gluon-mesh-vpn-wireguard.rst new file mode 100755 index 00000000..fd78abe5 --- /dev/null +++ b/docs/package/gluon-mesh-vpn-wireguard.rst @@ -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 + diff --git a/package/features b/package/features index 1f0d825a..254fda92 100644 --- a/package/features +++ b/package/features @@ -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' diff --git a/package/gluon-config-mode-mesh-vpn/luasrc/lib/gluon/config-mode/reboot/0100-mesh-vpn.lua b/package/gluon-config-mode-mesh-vpn/luasrc/lib/gluon/config-mode/reboot/0100-mesh-vpn.lua index 2e154803..96baf1cc 100644 --- a/package/gluon-config-mode-mesh-vpn/luasrc/lib/gluon/config-mode/reboot/0100-mesh-vpn.lua +++ b/package/gluon-config-mode-mesh-vpn/luasrc/lib/gluon/config-mode/reboot/0100-mesh-vpn.lua @@ -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 diff --git a/package/gluon-mesh-vpn-wireguard/Makefile b/package/gluon-mesh-vpn-wireguard/Makefile new file mode 100755 index 00000000..5ba42d46 --- /dev/null +++ b/package/gluon-mesh-vpn-wireguard/Makefile @@ -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)) diff --git a/package/gluon-mesh-vpn-wireguard/check_site.lua b/package/gluon-mesh-vpn-wireguard/check_site.lua new file mode 100644 index 00000000..d9ec081c --- /dev/null +++ b/package/gluon-mesh-vpn-wireguard/check_site.lua @@ -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) diff --git a/package/gluon-mesh-vpn-wireguard/files/lib/gluon/mesh-vpn/wireguard b/package/gluon-mesh-vpn-wireguard/files/lib/gluon/mesh-vpn/wireguard new file mode 100644 index 00000000..e69de29b diff --git a/package/gluon-mesh-vpn-wireguard/files/lib/gluon/upgrade/406-wireguard-generate-key b/package/gluon-mesh-vpn-wireguard/files/lib/gluon/upgrade/406-wireguard-generate-key new file mode 100755 index 00000000..d19904b4 --- /dev/null +++ b/package/gluon-mesh-vpn-wireguard/files/lib/gluon/upgrade/406-wireguard-generate-key @@ -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 diff --git a/package/gluon-mesh-vpn-wireguard/files/lib/netifd/proto/gluon_wireguard.sh b/package/gluon-mesh-vpn-wireguard/files/lib/netifd/proto/gluon_wireguard.sh new file mode 100755 index 00000000..9de46687 --- /dev/null +++ b/package/gluon-mesh-vpn-wireguard/files/lib/netifd/proto/gluon_wireguard.sh @@ -0,0 +1,94 @@ +#!/bin/sh +# Copyright 2016-2017 Christof Schulze +# 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 +} diff --git a/package/gluon-mesh-vpn-wireguard/files/usr/bin/enable-all-wg-interfaces b/package/gluon-mesh-vpn-wireguard/files/usr/bin/enable-all-wg-interfaces new file mode 100755 index 00000000..4f5aa80f --- /dev/null +++ b/package/gluon-mesh-vpn-wireguard/files/usr/bin/enable-all-wg-interfaces @@ -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 + diff --git a/package/gluon-mesh-vpn-wireguard/files/usr/bin/gluon-mesh-vpn-wireguard-get-or-create-secret b/package/gluon-mesh-vpn-wireguard/files/usr/bin/gluon-mesh-vpn-wireguard-get-or-create-secret new file mode 100755 index 00000000..a4957578 --- /dev/null +++ b/package/gluon-mesh-vpn-wireguard/files/usr/bin/gluon-mesh-vpn-wireguard-get-or-create-secret @@ -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" diff --git a/package/gluon-mesh-vpn-wireguard/files/usr/bin/wg-broker-client b/package/gluon-mesh-vpn-wireguard/files/usr/bin/wg-broker-client new file mode 100755 index 00000000..c03e500b --- /dev/null +++ b/package/gluon-mesh-vpn-wireguard/files/usr/bin/wg-broker-client @@ -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" diff --git a/package/gluon-mesh-vpn-wireguard/files/usr/bin/wgcheck b/package/gluon-mesh-vpn-wireguard/files/usr/bin/wgcheck new file mode 100755 index 00000000..e439233d --- /dev/null +++ b/package/gluon-mesh-vpn-wireguard/files/usr/bin/wgcheck @@ -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 diff --git a/package/gluon-mesh-vpn-wireguard/files/usr/lib/micron.d/wgcheck b/package/gluon-mesh-vpn-wireguard/files/usr/lib/micron.d/wgcheck new file mode 100644 index 00000000..659937ce --- /dev/null +++ b/package/gluon-mesh-vpn-wireguard/files/usr/lib/micron.d/wgcheck @@ -0,0 +1 @@ +*/5 * * * * /usr/bin/wgcheck diff --git a/package/gluon-mesh-vpn-wireguard/luasrc/lib/gluon/upgrade/405-wireguard b/package/gluon-mesh-vpn-wireguard/luasrc/lib/gluon/upgrade/405-wireguard new file mode 100755 index 00000000..7dee045a --- /dev/null +++ b/package/gluon-mesh-vpn-wireguard/luasrc/lib/gluon/upgrade/405-wireguard @@ -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') diff --git a/package/gluon-web-admin/files/lib/gluon/config-mode/view/admin/info.html b/package/gluon-web-admin/files/lib/gluon/config-mode/view/admin/info.html index 5f3e9191..8aec0862 100644 --- a/package/gluon-web-admin/files/lib/gluon/config-mode/view/admin/info.html +++ b/package/gluon-web-admin/files/lib/gluon/config-mode/view/admin/info.html @@ -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) },