diff --git a/docs/features/vpn.rst b/docs/features/vpn.rst index 6db2ecc6..88a84cbd 100644 --- a/docs/features/vpn.rst +++ b/docs/features/vpn.rst @@ -191,6 +191,16 @@ negative effects. Only when a previously connected node reboots the effect comes into play, as the gateway still knows about the old timestamp of the gluon node. +gluon-mesh-vpn-key-translate +"""""""""""""""""""""""""""" + +Many communities already possess a collection of active fastd-keys when they +plan migrating their community to WireGuard. +These public keys known on the server-side can be derived into their WireGuard +equivalent using `gluon-mesh-vpn-key-translate `__. +The routers do the necessary reencoding of the private key seamlessly +when updating firmware from fastd to the WireGuard variant. + Gateway / Supernode Configuration """"""""""""""""""""""""""""""""" diff --git a/package/gluon-mesh-vpn-wireguard/Makefile b/package/gluon-mesh-vpn-wireguard/Makefile index 61c53332..9d4e9a79 100644 --- a/package/gluon-mesh-vpn-wireguard/Makefile +++ b/package/gluon-mesh-vpn-wireguard/Makefile @@ -6,7 +6,13 @@ include ../gluon.mk define Package/gluon-mesh-vpn-wireguard TITLE:=Support for connecting meshes via wireguard - DEPENDS:=+gluon-core +libgluonutil +gluon-mesh-vpn-core +wireguard-tools +wgpeerselector +libubus + DEPENDS:=+gluon-core +libgluonutil +gluon-mesh-vpn-core +wireguard-tools +wgpeerselector +libubox +libubus +endef + +define Package/gluon-mesh-vpn-wireguard/install + $(Gluon/Build/Install) + $(INSTALL_DIR) $(1)/usr/sbin + $(INSTALL_BIN) $(PKG_BUILD_DIR)/gluon-hex-to-b64 $(1)/usr/sbin/ endef $(eval $(call BuildPackageGluon,gluon-mesh-vpn-wireguard)) diff --git a/package/gluon-mesh-vpn-wireguard/luasrc/lib/gluon/upgrade/400-mesh-vpn-wireguard b/package/gluon-mesh-vpn-wireguard/luasrc/lib/gluon/upgrade/400-mesh-vpn-wireguard index 18b5197b..b58ceb91 100755 --- a/package/gluon-mesh-vpn-wireguard/luasrc/lib/gluon/upgrade/400-mesh-vpn-wireguard +++ b/package/gluon-mesh-vpn-wireguard/luasrc/lib/gluon/upgrade/400-mesh-vpn-wireguard @@ -1,18 +1,63 @@ #!/usr/bin/lua local uci = require('simple-uci').cursor() +local unistd = require 'posix.unistd' +local util = require('gluon.util') local site = require 'gluon.site' +local sp = util.subprocess +local wait = require 'posix.sys.wait' -local private_key = uci:get("network_gluon-old", 'wg_mesh', "private_key") +local wg_private_key = uci:get("network_gluon-old", 'wg_mesh', "private_key") -if not private_key or not private_key:match("^" .. ("[%a%d+/]"):rep(42) .. "[AEIMQUYcgkosw480]=$") then - private_key = "generate" +local function valid_fastd_key(fastd_key) + return fastd_key and fastd_key:match(('%x'):rep(64)) end +local function valid_wireguard_key(wireguard_key) + return wireguard_key and wireguard_key:match("^" .. ("[%a%d+/]"):rep(42) .. "[AEIMQUYcgkosw480]=$") +end + +local function migrate_from_fastd_secret(fastd_secret) + local options = { + stdin = sp.PIPE, + stdout = sp.PIPE, + } + local pid, pipe = sp.popen('gluon-hex-to-b64', {}, options) + + if not pid then + return + end + + local inw = pipe.stdin + local out = pipe.stdout + + unistd.write(inw, string.format('%s\n', fastd_secret)) + unistd.close(inw) + + local wpid, status, code = wait.wait(pid) + if wpid and status == 'exited' and code == 0 then + local result = unistd.read(out, 44) + unistd.close(out) + return result + end +end + +if not valid_wireguard_key(wg_private_key) then + local fastd_secret = uci:get('fastd', 'mesh_vpn', 'secret') + if valid_fastd_key(fastd_secret) then + wg_private_key = migrate_from_fastd_secret(fastd_secret) + end +end + +if not valid_wireguard_key(wg_private_key) then + wg_private_key = "generate" +end + + uci:section('network', 'interface', 'wg_mesh', { proto = 'wireguard', fwmark = 1, - private_key = private_key, + private_key = wg_private_key, }) uci:section('network', 'interface', 'mesh_wg_mesh', { diff --git a/package/gluon-mesh-vpn-wireguard/src/Makefile b/package/gluon-mesh-vpn-wireguard/src/Makefile index 0b027848..ff847068 100644 --- a/package/gluon-mesh-vpn-wireguard/src/Makefile +++ b/package/gluon-mesh-vpn-wireguard/src/Makefile @@ -1,6 +1,9 @@ -all: respondd.so +all: respondd.so gluon-hex-to-b64 CFLAGS += -Wall -Werror-implicit-function-declaration +gluon-hex-to-b64: gluon-hex-to-b64.c + $(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) -Wall -o $@ $^ $(LDLIBS) -lubox + respondd.so: respondd.c $(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) -shared -fPIC -D_GNU_SOURCE -o $@ $^ $(LDLIBS) -lgluonutil -lubus diff --git a/package/gluon-mesh-vpn-wireguard/src/gluon-hex-to-b64.c b/package/gluon-mesh-vpn-wireguard/src/gluon-hex-to-b64.c new file mode 100644 index 00000000..0bca245e --- /dev/null +++ b/package/gluon-mesh-vpn-wireguard/src/gluon-hex-to-b64.c @@ -0,0 +1,101 @@ +// SPDX-FileCopyrightText: 2022 Jan-Niklas Burfeind +// SPDX-License-Identifier: BSD-2-Clause +// SPDX-FileContributor: read_hex() by Matthias Schiffer + +#include +#include +#include +#include +#include +#include + +/** + * how many blocks should be encoded at once - can be configured + */ +#define BLOCK_AMOUNT 32 + +/** + * smallest possible block size to encode in b64 without further contex + * is three bytes - do not change + */ +#define CHUNK_SIZE (3*BLOCK_AMOUNT) + +/** print usage info and exit as failed */ +static void usage(void) { + fprintf(stderr, "Usage: gluon-hex-to-b64\n"); + exit(1); +} + +/** + * read a string of hexadecimal characters and return them as bytes + * return false in case any non-hexadecimal characters are provided + * return true on success + */ +static bool read_hex(uint8_t key[CHUNK_SIZE], const char *hexstr) { + if (strspn(hexstr, "0123456789abcdefABCDEF") != strlen(hexstr)) + return false; + + size_t i; + for (i = 0; i < CHUNK_SIZE; i++) + sscanf(&hexstr[2 * i], "%02hhx", &key[i]); + + return true; +} + +int main(int argc, char *argv[]) { + if (argc != 1) + usage(); + + unsigned char hex_input[CHUNK_SIZE * 2 + 1]; + uint8_t as_bytes[CHUNK_SIZE]; + int byte_count; + int b64_buflen = B64_ENCODE_LEN(CHUNK_SIZE); + int b64_return; + size_t ret; + + char str[b64_buflen]; + + do { + ret = fread(hex_input, 1, sizeof(hex_input) - 1, stdin); + hex_input[ret] = '\0'; + + /* in case fread did not fill six characters */ + if (ret != sizeof(hex_input)-1) { + /* drop newline by replacing it with a null character */ + hex_input[strcspn(hex_input, "\n")] = 0; + + /* + * count length of resulting string and make sure it's even, + * as bytes are represented using pairs of hex characters + */ + ret = strlen(hex_input); + if (ret % 2 == 1) { + fprintf(stderr, "Error: Incomplete hex representation of a byte.\n"); + exit(EXIT_FAILURE); + } + } + + byte_count = ret / 2; + b64_buflen = B64_ENCODE_LEN(byte_count); + + /* in case read_hex fails due to invalid characters */ + if (!read_hex(as_bytes, hex_input)) { + fprintf(stderr, "Error: Invalid hexadecimal input.\n"); + exit(EXIT_FAILURE); + } + + b64_return = b64_encode(as_bytes, byte_count, str, b64_buflen); + + /* trailing '\0' is not counted by b64_encode(), so we subtract one character */ + if (b64_buflen - 1 != b64_return) { + fprintf(stderr, "Error: Encoding bytes as b64 failed.\n"); + exit(EXIT_FAILURE); + } + + printf("%s", str); + /* repeat until a non full block is read */ + } while (ret == sizeof(hex_input)-1); + printf("\n"); + + exit(EXIT_SUCCESS); +}