From: Felix Fietkau Date: Sat, 14 Feb 2026 19:44:31 +0000 (+0000) Subject: wireguard-tools: rewrite proto handler in ucode X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=41bc454602f15ac81f7cc9933d079323614d6492;p=thirdparty%2Fopenwrt.git wireguard-tools: rewrite proto handler in ucode This fixes automatic config reload on peer changes Signed-off-by: Felix Fietkau --- diff --git a/package/network/utils/wireguard-tools/Makefile b/package/network/utils/wireguard-tools/Makefile index 12127eb12f6..b0b0ddc9674 100644 --- a/package/network/utils/wireguard-tools/Makefile +++ b/package/network/utils/wireguard-tools/Makefile @@ -57,7 +57,7 @@ define Package/wireguard-tools/install $(INSTALL_BIN) $(PKG_BUILD_DIR)/src/wg $(1)/usr/bin/ $(INSTALL_BIN) ./files/wireguard_watchdog $(1)/usr/bin/ $(INSTALL_DIR) $(1)/lib/netifd/proto/ - $(INSTALL_BIN) ./files/wireguard.sh $(1)/lib/netifd/proto/ + $(INSTALL_DATA) ./files/wireguard.uc $(1)/lib/netifd/proto/ endef $(eval $(call BuildPackage,wireguard-tools)) diff --git a/package/network/utils/wireguard-tools/files/wireguard.sh b/package/network/utils/wireguard-tools/files/wireguard.sh deleted file mode 100644 index fa8e2142b58..00000000000 --- a/package/network/utils/wireguard-tools/files/wireguard.sh +++ /dev/null @@ -1,169 +0,0 @@ -#!/bin/sh -# Copyright 2016-2017 Dan Luedtke -# Licensed to the public under the Apache License 2.0. -# shellcheck disable=SC2317 - -WG=/usr/bin/wg -if [ ! -x $WG ]; then - logger -t "wireguard" "error: missing wireguard-tools (${WG})" - exit 0 -fi - -[ -n "$INCLUDE_ONLY" ] || { - . /lib/functions.sh - . ../netifd-proto.sh - init_proto "$@" -} - -proto_wireguard_init_config() { - renew_handler=1 - peer_detect=1 - - proto_config_add_string "private_key" - proto_config_add_int "listen_port" - proto_config_add_int "mtu" - proto_config_add_string "fwmark" - proto_config_add_string "addresses" - - available=1 - no_proto_task=1 -} - -ensure_key_is_generated() { - local private_key - private_key="$(uci get network."$1".private_key)" - - if [ "$private_key" = "generate" ] || [ -z "$private_key" ]; then - private_key="$("${WG}" genkey)" - uci -q set network."$1".private_key="$private_key" && \ - uci -q commit network - fi -} - -proto_wireguard_setup() { - local config="$1" - - local private_key listen_port mtu fwmark addresses ip6prefix nohostroute tunlink - ensure_key_is_generated "${config}" - - config_load network - config_get private_key "${config}" "private_key" - config_get listen_port "${config}" "listen_port" - config_get addresses "${config}" "addresses" - config_get mtu "${config}" "mtu" - config_get fwmark "${config}" "fwmark" - config_get ip6prefix "${config}" "ip6prefix" - config_get nohostroute "${config}" "nohostroute" - config_get tunlink "${config}" "tunlink" - - # Add the link only if it didn't already exist - ip -br link show "${config}" >/dev/null 2>&1 || ip link add dev "${config}" type wireguard - - [ -n "${mtu}" ] && ip link set mtu "${mtu}" dev "${config}" - - proto_init_update "${config}" 1 - - # Build WireGuard configuration entirely in memory - local wg_config="[Interface]\n" - wg_config="${wg_config}PrivateKey=${private_key}\n" - [ -n "${listen_port}" ] && wg_config="${wg_config}ListenPort=${listen_port}\n" - [ -n "${fwmark}" ] && wg_config="${wg_config}FwMark=${fwmark}\n" - - # Collect peer configs into wg_config as well - local peer_config - peer_config="" - proto_wireguard_setup_peer_collect() { - local section="$1" - local peer_block - - config_get_bool route_allowed_ips "$section" "route_allowed_ips" 0 - config_get_bool disabled "$section" "disabled" 0 - [ "$disabled" = 1 ] && return; - config_get peer_key "$section" "public_key" - config_get peer_eph "$section" "endpoint_host" - config_get peer_port "$section" "endpoint_port" "51820" - config_get peer_a_ips "$section" "allowed_ips" - config_get peer_p_ka "$section" "persistent_keepalive" - config_get peer_psk "$section" "preshared_key" - - - [ "${peer_eph##*:}" != "$peer_eph" ] && peer_eph="[$peer_eph]" - peer_port=${peer_port:-51820} - - peer_block="\n[Peer]\n" - [ -n "${peer_key}" ] && peer_block="${peer_block}PublicKey=${peer_key}\n" - [ -n "${peer_psk}" ] && peer_block="${peer_block}PresharedKey=${peer_psk}\n" - [ -n "${peer_eph}" ] && peer_block="${peer_block}Endpoint=${peer_eph}${peer_port:+:$peer_port}\n" - [ -n "${peer_a_ips}" ] && peer_block="${peer_block}AllowedIPs=${peer_a_ips// /, }\n" - [ -n "${peer_p_ka}" ] && peer_block="${peer_block}PersistentKeepalive=${peer_p_ka}\n" - - [ -n "$peer_key" ] && peer_config="$peer_config$peer_block\n" - if [ $route_allowed_ips -ne 0 ]; then - for allowed_ip in $peer_a_ips; do - case "${allowed_ip}" in - *:*/*) proto_add_ipv6_route "${allowed_ip%%/*}" "${allowed_ip##*/}" ;; - *.*/*) proto_add_ipv4_route "${allowed_ip%%/*}" "${allowed_ip##*/}" ;; - *:*) proto_add_ipv6_route "${allowed_ip%%/*}" "128" ;; - *.*) proto_add_ipv4_route "${allowed_ip%%/*}" "32" ;; - esac - done - fi - - } - - config_foreach proto_wireguard_setup_peer_collect "wireguard_${config}" - - # Combine interface + peer config into one variable - wg_config="${wg_config}${peer_config}" - - # Apply configuration directly using wg syncconf via stdin - printf "%b" "$wg_config" | ${WG} syncconf "${config}" /dev/stdin - local WG_RETURN=$? - - if [ ${WG_RETURN} -ne 0 ]; then - echo "Could not sync WireGuard configuration" - sleep 5 - proto_setup_failed "${config}" - exit 1 - fi - - # Assign addresses - for address in ${addresses}; do - case "${address}" in - *:*/*) proto_add_ipv6_address "${address%%/*}" "${address##*/}" ;; - *.*/*) proto_add_ipv4_address "${address%%/*}" "${address##*/}" ;; - *:*) proto_add_ipv6_address "${address%%/*}" "128" ;; - *.*) proto_add_ipv4_address "${address%%/*}" "32" ;; - esac - done - - for prefix in ${ip6prefix}; do - proto_add_ipv6_prefix "$prefix" - done - - # Endpoint dependency tracking - if [ "${nohostroute}" != "1" ]; then - wg show "${config}" endpoints | \ - sed -E 's/\[?([0-9.:a-f]+)\]?:([0-9]+)/\1 \2/' | \ - while IFS=$'\t ' read -r key address port; do - [ -n "${port}" ] || continue - proto_add_host_dependency "${config}" "${address}" "${tunlink}" - done - fi - - proto_send_update "${config}" -} - -proto_wireguard_renew() { - local interface="$1" - proto_wireguard_setup "$interface" -} - -proto_wireguard_teardown() { - local config="$1" - ip link del dev "${config}" >/dev/null 2>&1 -} - -[ -n "$INCLUDE_ONLY" ] || { - add_protocol wireguard -} diff --git a/package/network/utils/wireguard-tools/files/wireguard.uc b/package/network/utils/wireguard-tools/files/wireguard.uc new file mode 100644 index 00000000000..4d520776c84 --- /dev/null +++ b/package/network/utils/wireguard-tools/files/wireguard.uc @@ -0,0 +1,255 @@ +#!/usr/bin/env ucode +'use strict'; + +import * as fs from 'fs'; + +const WG = '/usr/bin/wg'; + +function wg_exists() { + return fs.access(WG, fs.F_OK); +} + +function ensure_key_is_generated(cursor, section_name) { + let private_key = cursor.get('network', section_name, 'private_key'); + + if (!private_key || private_key == 'generate') { + let proc = fs.popen(`${WG} genkey`); + if (!proc) + return null; + + let generated_key = rtrim(proc.read('all')); + proc.close(); + + if (generated_key) { + cursor.set('network', section_name, 'private_key', generated_key); + cursor.commit('network'); + return generated_key; + } + } + + return private_key; +} + +function parse_address(addr) { + if (index(addr, ':') >= 0) { + if (index(addr, '/') >= 0) { + let parts = split(addr, '/'); + return { family: 6, address: parts[0], mask: int(parts[1]) }; + } + return { family: 6, address: addr, mask: 128 }; + } + + if (index(addr, '/') >= 0) { + let parts = split(addr, '/'); + return { family: 4, address: parts[0], mask: int(parts[1]) }; + } + + return { family: 4, address: addr, mask: 32 }; +} + +function load_peers(cursor, iface) { + let peers = []; + let peer_type = sprintf('wireguard_%s', iface); + + cursor.foreach('network', peer_type, (peer_section) => { + let disabled = peer_section.disabled; + if (disabled == '1') + return; + + let route_allowed_ips = peer_section.route_allowed_ips; + let peer_key = peer_section.public_key; + let peer_eph = peer_section.endpoint_host; + let peer_port = peer_section.endpoint_port ?? '51820'; + let peer_a_ips = peer_section.allowed_ips; + let peer_p_ka = peer_section.persistent_keepalive; + let peer_psk = peer_section.preshared_key; + + if (!peer_key) + return; + + let peer_data = { + public_key: peer_key, + preshared_key: peer_psk, + endpoint_host: peer_eph, + endpoint_port: peer_port, + allowed_ips: peer_a_ips, + persistent_keepalive: peer_p_ka, + route_allowed_ips: route_allowed_ips == '1' + }; + + push(peers, peer_data); + }); + + return peers; +} + +function proto_setup(proto) { + if (!wg_exists()) { + warn('WireGuard tools not found at ', WG, '\n'); + proto.setup_failed(); + return; + } + + let iface = proto.iface; + let config = proto.config; + + system(sprintf('ip link add dev %s type wireguard 2>/dev/null || true', iface)); + + if (config.mtu) + system(sprintf('ip link set mtu %d dev %s', int(config.mtu), iface)); + + let wg_config = '[Interface]\n'; + wg_config += sprintf('PrivateKey=%s\n', config.private_key); + + if (config.listen_port) + wg_config += sprintf('ListenPort=%d\n', int(config.listen_port)); + + if (config.fwmark) + wg_config += sprintf('FwMark=%s\n', config.fwmark); + + let ipv4_routes = []; + let ipv6_routes = []; + + for (let peer in config.peers) { + wg_config += '\n[Peer]\n'; + wg_config += sprintf('PublicKey=%s\n', peer.public_key); + + if (peer.preshared_key) + wg_config += sprintf('PresharedKey=%s\n', peer.preshared_key); + + if (peer.endpoint_host) { + let eph = peer.endpoint_host; + if (index(eph, ':') >= 0 && eph[0] != '[') + eph = sprintf('[%s]', eph); + wg_config += sprintf('Endpoint=%s:%s\n', eph, peer.endpoint_port); + } + + if (peer.allowed_ips) { + let allowed_list = type(peer.allowed_ips) == 'array' ? peer.allowed_ips : split(peer.allowed_ips, ' '); + wg_config += sprintf('AllowedIPs=%s\n', join(', ', allowed_list)); + + if (peer.route_allowed_ips) { + for (let allowed_ip in allowed_list) { + let addr_info = parse_address(allowed_ip); + let route = { target: addr_info.address, netmask: '' + addr_info.mask }; + if (addr_info.family == 6) + push(ipv6_routes, route); + else + push(ipv4_routes, route); + } + } + } + + if (peer.persistent_keepalive) + wg_config += sprintf('PersistentKeepalive=%s\n', peer.persistent_keepalive); + } + + let wg_proc = fs.popen(sprintf('%s syncconf %s /dev/stdin', WG, iface), 'w'); + if (!wg_proc) { + warn('Failed to run wg syncconf for ', iface, '\n'); + proto.setup_failed(); + return; + } + + wg_proc.write(wg_config); + let wg_result = wg_proc.close(); + + if (wg_result != 0) { + warn('wg syncconf failed for ', iface, '\n'); + proto.setup_failed(); + return; + } + + system(sprintf('ip link set up dev %s', iface)); + + let ipv4_addrs = []; + let ipv6_addrs = []; + + if (config.addresses) { + let addr_list = split(config.addresses, ' '); + for (let address in addr_list) { + let addr_info = parse_address(address); + let addr = { ipaddr: addr_info.address, mask: '' + addr_info.mask }; + if (addr_info.family == 6) + push(ipv6_addrs, addr); + else + push(ipv4_addrs, addr); + } + } + + let link_data = { + ifname: iface + }; + + if (length(ipv4_addrs) > 0) + link_data.ipaddr = ipv4_addrs; + + if (length(ipv6_addrs) > 0) + link_data.ip6addr = ipv6_addrs; + + if (length(ipv4_routes) > 0) + link_data.routes = ipv4_routes; + + if (length(ipv6_routes) > 0) + link_data.routes6 = ipv6_routes; + + if (config.ip6prefix) { + let prefix_list = split(config.ip6prefix, ' '); + if (length(prefix_list) > 0) + link_data.ip6prefix = prefix_list; + } + + if (config.nohostroute != '1') { + let endpoints_proc = fs.popen(sprintf('%s show %s endpoints', WG, iface)); + if (endpoints_proc) { + let endpoints_data = endpoints_proc.read('all'); + endpoints_proc.close(); + + let endpoint_lines = split(endpoints_data, '\n'); + for (let line in endpoint_lines) { + if (!line) + continue; + + let parts = split(rtrim(line), '\t'); + if (length(parts) < 2) + continue; + + let endpoint = parts[1]; + let addr_match = match(endpoint, regexp('\\[?([0-9.:a-f]+)\\]?:([0-9]+)')); + if (addr_match && length(addr_match) > 1) + proto.add_host_dependency(addr_match[1], config.tunlink); + } + } + } + + proto.update_link(true, link_data); +} + +function proto_teardown(proto) { + let iface = proto.iface; + system(sprintf('ip link del dev %s 2>/dev/null', iface)); + proto.update_link(false); +} + +function proto_renew(proto) { + proto_setup(proto); +} + +netifd.add_proto({ + available: true, + no_proto_task: true, + 'renew-handler': true, + name: 'wireguard', + + config: function(ctx) { + return { + ...ctx.data, + private_key: ensure_key_is_generated(ctx.uci, ctx.section), + peers: load_peers(ctx.uci, ctx.section) + }; + }, + + setup: proto_setup, + teardown: proto_teardown, + renew: proto_renew +});