From bf1eaccf86086427ec2784ce2db7274184266131 Mon Sep 17 00:00:00 2001 From: DasSkelett Date: Tue, 16 Jan 2024 00:32:13 +0100 Subject: [PATCH] Support wgkex loadbalancing mechanism --- ffmuc-mesh-vpn-wireguard-vxlan/Makefile | 4 +- ffmuc-mesh-vpn-wireguard-vxlan/README.md | 73 +++++++------- .../gluon-mesh-wireguard-vxlan/checkuplink | 99 ++++++++++++++----- .../parse-wgkex-response.lua | 12 +++ 4 files changed, 128 insertions(+), 60 deletions(-) create mode 100644 ffmuc-mesh-vpn-wireguard-vxlan/luasrc/lib/gluon/gluon-mesh-wireguard-vxlan/parse-wgkex-response.lua diff --git a/ffmuc-mesh-vpn-wireguard-vxlan/Makefile b/ffmuc-mesh-vpn-wireguard-vxlan/Makefile index eb94d7d0..d74463ee 100644 --- a/ffmuc-mesh-vpn-wireguard-vxlan/Makefile +++ b/ffmuc-mesh-vpn-wireguard-vxlan/Makefile @@ -1,7 +1,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=ffmuc-mesh-vpn-wireguard-vxlan -PKG_VERSION:=1 +PKG_VERSION:=2 PKG_RELEASE:=1 PKG_MAINTAINER:=Annika Wickert @@ -11,7 +11,7 @@ include $(TOPDIR)/../package/gluon.mk define Package/ffmuc-mesh-vpn-wireguard-vxlan TITLE:=Support for connecting meshes via wireguard - DEPENDS:=+gluon-mesh-vpn-core +micrond +kmod-wireguard +wireguard-tools +ip-full + DEPENDS:=+gluon-mesh-vpn-core +micrond +kmod-wireguard +wireguard-tools +ip-full +lua-jsonc endef $(eval $(call BuildPackageGluon,ffmuc-mesh-vpn-wireguard-vxlan)) diff --git a/ffmuc-mesh-vpn-wireguard-vxlan/README.md b/ffmuc-mesh-vpn-wireguard-vxlan/README.md index 07625b1e..a48f9c09 100644 --- a/ffmuc-mesh-vpn-wireguard-vxlan/README.md +++ b/ffmuc-mesh-vpn-wireguard-vxlan/README.md @@ -1,53 +1,58 @@ # ffmuc-mesh-vpn-wireguard-vxlan -You can use this package for connecting with wireguard to the Freifunk Munich network. +This package adds support for WireGuard+VXLAN as Mesh VPN protocol stack as it is used in the Freifunk Munich network. + +### Dependencies + +This relies on [wgkex](https://github.com/freifunkMUC/wgkex), the FFMUC WireGuard key exchange broker running on the configured broker address. The broker programms the gateway to accept the WireGuard key which is transmitted during connection. +Starting with the key exchange API v2, the wgkex broker also returns WireGuard peer data for a gateway selected by the broker, which this package then configures as mesh VPN peer/endpoint. This can be enabled by setting the `loadbalancing` option to `1`. + +For the health-checks a webserver of some kind needs to listen to `HTTP GET` requests on the gateways. + +### How it works + +When `checkuplink` gets called (which happens every minute via cronjob), it checks if the gateway connection is still alive by calling `wget` and connecting to the WireGuard peer link address. If this address replies, we also start a `batctl ping` to the same address. If both checks succeed the connection just stays alive. + +If one of the checks above bails out with an error the reconnect cycle is started. This means `checkuplink` registers itself with `wireguard.broker` by sending the WireGuard public key over either HTTP or HTTPS (depending on the device support). +The broker responds with JSON data containing the gateway peer data (pubkey, address, port, allowed IPs aka link address). `checkuplink` adds the peer to the wg interface using this data, and sets up the VXLAN interface with the peer link address as remote endpoint. + +This script prefers to establish connections over IPv6 and falls back to IPv4 **only if there is no IPv6 default route**. + +### Configuration You should use something like the following in the site.conf: - + ``` mesh_vpn = { mtu = 1400, wireguard = { enabled = '1', - iface = 'mesh-vpn', + iface = 'wg_mesh_vpn', -- not 'mesh-vpn', which is used for the VXLAN interface limit = '1', -- actually unused - broker = 'broker.ffmuc.net/api/v1/wg/key/exchange', - peers = { - { - publickey ='N9uF5Gg1B5AqWrE9IuvDgzmQePhqhb8Em/HrRpAdnlY=', - endpoint ='ffkwsn01.freifunk-koenigswinter.de:30020', - link_address = 'fe80::f000:22ff:fe12:01', - }, - { - publickey ='liatbdT62FbPiDPHKBqXVzrEo6hc5oO5tmEKDMhMTlU=', - endpoint ='ffkwsn02.freifunk-koenigswinter.de:30020', - link_address = 'fe80::f000:22ff:fe12:02', - }, - { - publickey ='xakSGG39D1v90j3Z9eVWzojh6nDbnsVUc/RByVdcKB0=', - endpoint ='ffkwsn03.freifunk-koenigswinter.de:30020', - link_address = 'fe80::f000:22ff:fe12:07', - }, - + broker = 'broker.ffmuc.net', -- base path of broker, will be combined with API path + loadbalancing = '1' -- controls whether to use the loadbalancing/gateway assignment feature of the broker + peers = { -- only needed if 'loadbalancing = '0'' + { + publickey ='N9uF5Gg1B5AqWrE9IuvDgzmQePhqhb8Em/HrRpAdnlY=', + endpoint ='ffkwsn01.freifunk-koenigswinter.de:30020', + link_address = 'fe80::f000:22ff:fe12:01', + }, + { + publickey ='liatbdT62FbPiDPHKBqXVzrEo6hc5oO5tmEKDMhMTlU=', + endpoint ='ffkwsn02.freifunk-koenigswinter.de:30020', + link_address = 'fe80::f000:22ff:fe12:02', + }, + { + publickey ='xakSGG39D1v90j3Z9eVWzojh6nDbnsVUc/RByVdcKB0=', + endpoint ='ffkwsn03.freifunk-koenigswinter.de:30020', + link_address = 'fe80::f000:22ff:fe12:07', }, + }, }, - ``` And you should include the package in the site.mk of course! -### Dependencies - -This relies on [wgkex](https://github.com/freifunkMUC/wgkex) the FFMUC wireguard broker running on the configured broker address. The broker programms the gateway to accept the WireGuard key which is transmitted during connection. - -For the health-checks a webserver of some kind needs to listen to `HTTP GET` requests on the gateways. - -### How it works - -When `checkuplink` gets called (which happens every minute via cronjob), it checks if the gateway connection is still alive by calling `wget` and connecting to `wireguard.peer.peer_[number].link_address`. If this address replies we also start a `batctl ping` to the same address. If both checks succeed the connection just stays alive. - -If one of the checks above bails out with an error the reconnect cycle is started. Which means `checkuplink` registers itself with `wireguard.broker` by sending the WireGuard public_key over either http or https (depending on the device support). After the key was sent the script tries to randomely connect to one of the `wireguard.peer`. This script prefers to establish connections over IPv6 and falls back to IPv4 only if there is no IPv6 default route. - ### Interesting Links - [FFMUC: Half a year with WireGuard](https://www.slideshare.net/AnnikaWickert/ffmuc-half-a-year-with-wireguard) diff --git a/ffmuc-mesh-vpn-wireguard-vxlan/files/lib/gluon/gluon-mesh-wireguard-vxlan/checkuplink b/ffmuc-mesh-vpn-wireguard-vxlan/files/lib/gluon/gluon-mesh-wireguard-vxlan/checkuplink index 40905f5e..80f6daf8 100755 --- a/ffmuc-mesh-vpn-wireguard-vxlan/files/lib/gluon/gluon-mesh-wireguard-vxlan/checkuplink +++ b/ffmuc-mesh-vpn-wireguard-vxlan/files/lib/gluon/gluon-mesh-wireguard-vxlan/checkuplink @@ -28,7 +28,17 @@ clean_port() { echo "$1" | sed -r 's/:[0-9]+$|\[|\]//g' } -check_address_family() { +extract_port() { + local address_port="$1" + + # Remove brackets for IPv6 addresses + local address="$(echo "$address_port" | sed -e 's/\[//;s/\]//')" + + # Extract port number + echo "$address" | awk -F: '{print $NF}' +} + +resolve_host() { local peer_endpoint="$2" local gateway gateway="$(clean_port "$peer_endpoint")" @@ -36,14 +46,13 @@ check_address_family() { if ip -6 route show table 1 | grep -q 'default via' then local ipv6 - ipv6="$(gluon-wan nslookup "$gateway" | grep 'Address:\? [0-9]' | grep -E -o '([a-f0-9:]+:+)+[a-f0-9]+')" - echo "[$ipv6]$(echo "$peer_endpoint" | grep -E -oe ":[0-9]+$")" + ipv6="$(gluon-wan nslookup "$gateway" | grep 'Address:\? [0-9]' | grep -oE '([a-f0-9:]+:+)+[a-f0-9]+')" + echo "$ipv6" else local ipv4 ipv4="$(gluon-wan nslookup "$gateway" | grep 'Address:\? [0-9]' | grep -oE "\b([0-9]{1,3}\.){3}[0-9]{1,3}\b")" - echo "$ipv4$(echo "$peer_endpoint" | grep -E -oe ":[0-9]+$")" + echo "$ipv4" fi - } # Do we already have a private-key? If not generate one @@ -85,17 +94,6 @@ if [ "$(uci get wireguard.mesh_vpn.enabled)" = "true" ] || [ "$(uci get wireguar exit 3 fi - # Get the number of configured peers and randomly select one - NUMBER_OF_PEERS=$(uci -q show wireguard | grep -E -ce "peer_[0-9]+.endpoint") - PEER="$(awk -v min=1 -v max="$NUMBER_OF_PEERS" 'BEGIN{srand(); print int(min+rand()*(max-min+1))}')" - PEER_PUBLICKEY="$(uci get wireguard.peer_"$PEER".publickey)" - - logger -t checkuplink "Selected peer $PEER" - - endpoint="$(check_address_family "$PEER_PUBLICKEY" "$(uci get wireguard.peer_"$PEER".endpoint)")" - - logger -t checkuplink "Connecting to $endpoint" - # Delete Interfaces { ip link set nomaster dev mesh-vpn >/dev/null 2>&1 @@ -106,7 +104,7 @@ if [ "$(uci get wireguard.mesh_vpn.enabled)" = "true" ] || [ "$(uci get wireguar PUBLICKEY=$(uci get wireguard.mesh_vpn.privatekey | wg pubkey) SEGMENT=$(uci get gluon.core.domain) - # Push public key to broker, test for https and use if supported + # Push public key to broker and receive gateway data, test for https and use if supported ret=0 wget -q "https://[::1]" || ret=$? # returns Network Failure =4 if https exists @@ -116,7 +114,63 @@ if [ "$(uci get wireguard.mesh_vpn.enabled)" = "true" ] || [ "$(uci get wireguar else PROTO=https fi - gluon-wan wget -q -O- --post-data='{"domain": "'"$SEGMENT"'","public_key": "'"$PUBLICKEY"'"}' "$PROTO://$(uci get wireguard.mesh_vpn.broker)" + + if [ "$(uci get wireguard.mesh_vpn.loadbalancing)" == "true" ] || [ "$(uci get wireguard.mesh_vpn.loadbalancing)" == "1" ]; then + # Use /api/v2, get gateway peer details from broker response + WGKEX_BROKER="$PROTO://$(uci get wireguard.mesh_vpn.broker)/api/v2/wg/key/exchange" + logger -t checkuplink "Contacting wgkex broker $WGKEX_BROKER" + WGKEX_DATA=$(gluon-wan wget -q -O- --post-data='{"domain": "'"$SEGMENT"'","public_key": "'"$PUBLICKEY"'"}' "$WGKEX_BROKER") + if [ $? -eq 1 ]; then + logger -t checkuplink "Contacting wgkex broker failed, response: $WGKEX_DATA" + exit 1 + fi + + logger -t checkuplink "Got data from wgkex broker: $WGKEX_DATA" + + data=$(lua /lib/gluon/gluon-mesh-wireguard-vxlan/parse-wgkex-response.lua "$WGKEX_DATA") + if [ $? -eq 1 ]; then + logger -t checkuplink "Parsing wgkex broker data failed" + exit 1 + fi + + logger -t checkuplink "Parsed wgkex broker data" + + PEER_ADDRESS="$(echo "$data" | sed -n 1p)" + PEER_PORT="$(echo "$data" | sed -n 2p)" + PEER_PUBLICKEY="$(echo "$data" | sed -n 3p)" + PEER_LINKADDRESS=$(echo "$data" | sed -n 4p) + + PEER_ADDRESS="$(resolve_host "$PEER_ADDRESS")" + PEER_ENDPOINT="$(combine_ip_port "$PEER_ADDRESS" "$PEER_PORT")" + + else + # Use /api/v1, get gateway peer details from config + WGKEX_BROKER="$PROTO://$(uci get wireguard.mesh_vpn.broker)/api/v1/wg/key/exchange" + logger -t checkuplink "Contacting wgkex broker $WGKEX_BROKER" + gluon-wan wget -q -O- --post-data='{"domain": "'"$SEGMENT"'","public_key": "'"$PUBLICKEY"'"}' "$WGKEX_BROKER" + if [ $? -eq 1 ]; then + logger -t checkuplink "Contacting wgkex broker failed" + exit 1 + fi + + # Get the number of configured peers and randomly select one + NUMBER_OF_PEERS=$(uci -q show wireguard | grep -E -ce "peer_[0-9]+.endpoint") + PEER="$(awk -v min=1 -v max="$NUMBER_OF_PEERS" 'BEGIN{srand(); print int(min+rand()*(max-min+1))}')" + + logger -t checkuplink "Selected peer $PEER" + + PEER_HOSTPORT="$(uci get wireguard.peer_"$PEER".endpoint)" + PEER_HOST="$(clean_port "$PEER_HOSTPORT")" + PEER_ADDRESS="$(resolve_host "$PEER_HOST")" + PEER_PORT="$(extract_port "$PEER_HOSTPORT")" + PEER_ENDPOINT="$(combine_ip_port "$PEER_ADDRESS" "$PEER_PORT")" + + PEER_PUBLICKEY="$(uci get wireguard.peer_"$PEER".publickey)" + PEER_LINKADDRESS="$(uci get wireguard.peer_"$PEER".link_address)" + + fi + + logger -t checkuplink "Connecting to $PEER_ENDPOINT" # Bring up the wireguard interface ip link add dev "$MESH_VPN_IFACE" type wireguard @@ -127,11 +181,8 @@ if [ "$(uci get wireguard.mesh_vpn.enabled)" = "true" ] || [ "$(uci get wireguar LINKLOCAL="$(interface_linklocal)" # Add link-address and Peer - ip address add "${LINKLOCAL}"/64 dev "$MESH_VPN_IFACE" - if [ "$endpoint" = "" ]; then - endpoint=$(uci get wireguard.peer_"$PEER".endpoint) - fi - gluon-wan wg set "$MESH_VPN_IFACE" peer "$(uci get wireguard.peer_"$PEER".publickey)" persistent-keepalive 25 allowed-ips "$(uci get wireguard.peer_"$PEER".link_address)/128" endpoint "$endpoint" + ip address add "${LINKLOCAL}"/64 dev $MESH_VPN_IFACE + gluon-wan wg set "$MESH_VPN_IFACE" peer "$PEER_PUBLICKEY" persistent-keepalive 25 allowed-ips "$PEER_LINKADDRESS/128" endpoint "$PEER_ENDPOINT" # We need to allow incoming vxlan traffic on mesh iface sleep 10 @@ -144,7 +195,7 @@ if [ "$(uci get wireguard.mesh_vpn.enabled)" = "true" ] || [ "$(uci get wireguar fi # Bring up VXLAN - if ! ip link add mesh-vpn type vxlan id "$(lua -e 'print(tonumber(require("gluon.util").domain_seed_bytes("gluon-mesh-vpn-vxlan", 3), 16))')" local "${LINKLOCAL}" remote "$(uci get wireguard.peer_"$PEER".link_address)" dstport 8472 dev "$MESH_VPN_IFACE" + if ! ip link add mesh-vpn type vxlan id "$(lua -e 'print(tonumber(require("gluon.util").domain_seed_bytes("gluon-mesh-vpn-vxlan", 3), 16))')" local "${LINKLOCAL}" remote "$PEER_LINKADDRESS" dstport 8472 dev "$MESH_VPN_IFACE" then logger -p err -t checkuplink "Unable to create mesh-vpn interface" exit 2 diff --git a/ffmuc-mesh-vpn-wireguard-vxlan/luasrc/lib/gluon/gluon-mesh-wireguard-vxlan/parse-wgkex-response.lua b/ffmuc-mesh-vpn-wireguard-vxlan/luasrc/lib/gluon/gluon-mesh-wireguard-vxlan/parse-wgkex-response.lua new file mode 100644 index 00000000..b50aeee5 --- /dev/null +++ b/ffmuc-mesh-vpn-wireguard-vxlan/luasrc/lib/gluon/gluon-mesh-wireguard-vxlan/parse-wgkex-response.lua @@ -0,0 +1,12 @@ +local json = require 'jsonc' + +local input = assert(arg[1]) +local data = assert(json.parse(input)) +if not data.Endpoint or not data.Endpoint.Address or not data.Endpoint.Port + or not data.Endpoint.PublicKey or not data.Endpoint.AllowedIPs or not data.Endpoint.AllowedIPs[1] then + error("Malformed JSON response, missing required value") +end +print(data.Endpoint.Address) +print(data.Endpoint.Port) +print(data.Endpoint.PublicKey) +print(data.Endpoint.AllowedIPs[1])