├── .gitignore ├── amneziawg-tools ├── Makefile └── files │ ├── amneziawg.sh │ └── amneziawg_watchdog ├── kmod-amneziawg ├── Makefile ├── files │ └── 000-initial-amneziawg.patch └── src │ └── Makefile └── luci-proto-amneziawg ├── Makefile ├── htdocs └── luci-static │ └── resources │ ├── protocol │ └── amneziawg.js │ └── view │ └── amneziawg │ └── status.js └── root └── usr └── share ├── luci └── menu.d │ └── luci-proto-amneziawg.json └── rpcd ├── acl.d └── luci-amneziawg.json └── ucode └── luci.amneziawg /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | -------------------------------------------------------------------------------- /amneziawg-tools/Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2016-2019 Jason A. Donenfeld 3 | # Copyright (C) 2016 Baptiste Jonglez 4 | # Copyright (C) 2016-2017 Dan Luedtke 5 | # 6 | # This is free software, licensed under the GNU General Public License v2. 7 | # See /LICENSE for more information. 8 | 9 | include $(TOPDIR)/rules.mk 10 | 11 | PKG_NAME:=amneziawg-tools 12 | PKG_VERSION:=1.0.20240213 13 | PKG_RELEASE:=1 14 | 15 | PKG_SOURCE:=v$(PKG_VERSION).tar.gz 16 | PKG_SOURCE_URL:=https://github.com/amnezia-vpn/amneziawg-tools/archive/refs/tags/ 17 | PKG_HASH:=4bde122630c9ddb1ec013c3e958f2c613b9eea56834674dda92fcb423c6f4d10 18 | 19 | PKG_MAINTAINER:=Amnezia Admin 20 | PKG_LICENSE:=GPL-2.0 21 | PKG_LICENSE_FILES:=COPYING 22 | 23 | PKG_BUILD_DIR=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION) 24 | 25 | include $(INCLUDE_DIR)/package.mk 26 | 27 | MAKE_PATH:=src 28 | MAKE_VARS += PLATFORM=linux 29 | 30 | define Package/amneziawg-tools 31 | CATEGORY:=Network 32 | URL:=https://amnezia.org/ 33 | MAINTAINER:=Amnezia Admin 34 | TITLE:=AmneziaWG userspace control program (amneziawg) 35 | DEPENDS:= \ 36 | +@BUSYBOX_CONFIG_IP \ 37 | +@BUSYBOX_CONFIG_FEATURE_IP_LINK 38 | endef 39 | 40 | define Package/amneziawg-tools/description 41 | Amnezia VPN — simple and free app to run a self-hosted VPN with 42 | high privacy requirements. 43 | 44 | This package provides the userspace control program for AmneziaWG, 45 | `amneziawg`, a netifd protocol helper, and a re-resolve watchdog script. 46 | endef 47 | 48 | define Package/amneziawg-tools/install 49 | $(INSTALL_DIR) $(1)/usr/bin/ 50 | $(INSTALL_BIN) $(PKG_BUILD_DIR)/src/wg $(1)/usr/bin/awg 51 | $(INSTALL_BIN) ./files/amneziawg_watchdog $(1)/usr/bin/ 52 | $(INSTALL_DIR) $(1)/lib/netifd/proto/ 53 | $(INSTALL_BIN) ./files/amneziawg.sh $(1)/lib/netifd/proto/ 54 | endef 55 | 56 | $(eval $(call BuildPackage,amneziawg-tools)) 57 | -------------------------------------------------------------------------------- /amneziawg-tools/files/amneziawg.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Copyright 2016-2017 Dan Luedtke 3 | # Licensed to the public under the Apache License 2.0. 4 | 5 | # shellcheck disable=SC1091,SC3003,SC3043 6 | 7 | WG=/usr/bin/awg 8 | if [ ! -x $WG ]; then 9 | logger -t "amneziawg" "error: missing amneziawg-tools (${WG})" 10 | exit 0 11 | fi 12 | 13 | [ -n "$INCLUDE_ONLY" ] || { 14 | . /lib/functions.sh 15 | . ../netifd-proto.sh 16 | init_proto "$@" 17 | } 18 | 19 | proto_amneziawg_init_config() { 20 | proto_config_add_string "private_key" 21 | proto_config_add_int "listen_port" 22 | proto_config_add_int "mtu" 23 | proto_config_add_string "fwmark" 24 | proto_config_add_int "awg_jc" 25 | proto_config_add_int "awg_jmin" 26 | proto_config_add_int "awg_jmax" 27 | proto_config_add_int "awg_s1" 28 | proto_config_add_int "awg_s2" 29 | proto_config_add_int "awg_h1" 30 | proto_config_add_int "awg_h2" 31 | proto_config_add_int "awg_h3" 32 | proto_config_add_int "awg_h4" 33 | # shellcheck disable=SC2034 34 | available=1 35 | # shellcheck disable=SC2034 36 | no_proto_task=1 37 | } 38 | 39 | proto_amneziawg_is_kernel_mode() { 40 | if [ ! -e /sys/module/amneziawg ]; then 41 | modprobe amneziawg >/dev/null 2>&1 || true 42 | 43 | if [ -e /sys/module/amneziawg ]; then 44 | return 0 45 | else 46 | if ! command -v "${WG_QUICK_USERSPACE_IMPLEMENTATION:-amneziawg-go}" >/dev/null; then 47 | ret=$? 48 | echo "Please install either kernel module (kmod-amneziawg package) or user-space implementation in /usr/bin/amneziawg-go." 49 | exit $ret 50 | else 51 | return 1 52 | fi 53 | fi 54 | else 55 | return 0 56 | fi 57 | } 58 | 59 | proto_amneziawg_setup_peer() { 60 | local peer_config="$1" 61 | 62 | local disabled 63 | local public_key 64 | local preshared_key 65 | local allowed_ips 66 | local route_allowed_ips 67 | local endpoint_host 68 | local endpoint_port 69 | local persistent_keepalive 70 | 71 | config_get_bool disabled "${peer_config}" "disabled" 0 72 | config_get public_key "${peer_config}" "public_key" 73 | config_get preshared_key "${peer_config}" "preshared_key" 74 | config_get allowed_ips "${peer_config}" "allowed_ips" 75 | config_get_bool route_allowed_ips "${peer_config}" "route_allowed_ips" 0 76 | config_get endpoint_host "${peer_config}" "endpoint_host" 77 | config_get endpoint_port "${peer_config}" "endpoint_port" 78 | config_get persistent_keepalive "${peer_config}" "persistent_keepalive" 79 | 80 | if [ "${disabled}" -eq 1 ]; then 81 | # skip disabled peers 82 | return 0 83 | fi 84 | 85 | if [ -z "$public_key" ]; then 86 | echo "Skipping peer config $peer_config because public key is not defined." 87 | return 0 88 | fi 89 | 90 | echo "[Peer]" >> "${wg_cfg}" 91 | echo "PublicKey=${public_key}" >> "${wg_cfg}" 92 | if [ "${preshared_key}" ]; then 93 | echo "PresharedKey=${preshared_key}" >> "${wg_cfg}" 94 | fi 95 | for allowed_ip in $allowed_ips; do 96 | echo "AllowedIPs=${allowed_ip}" >> "${wg_cfg}" 97 | done 98 | if [ "${endpoint_host}" ]; then 99 | case "${endpoint_host}" in 100 | *:*) 101 | endpoint="[${endpoint_host}]" 102 | ;; 103 | *) 104 | endpoint="${endpoint_host}" 105 | ;; 106 | esac 107 | if [ "${endpoint_port}" ]; then 108 | endpoint="${endpoint}:${endpoint_port}" 109 | else 110 | endpoint="${endpoint}:51820" 111 | fi 112 | echo "Endpoint=${endpoint}" >> "${wg_cfg}" 113 | fi 114 | if [ "${persistent_keepalive}" ]; then 115 | echo "PersistentKeepalive=${persistent_keepalive}" >> "${wg_cfg}" 116 | fi 117 | 118 | if [ "${route_allowed_ips}" -ne 0 ]; then 119 | for allowed_ip in ${allowed_ips}; do 120 | case "${allowed_ip}" in 121 | *:*/*) 122 | proto_add_ipv6_route "${allowed_ip%%/*}" "${allowed_ip##*/}" 123 | ;; 124 | *.*/*) 125 | proto_add_ipv4_route "${allowed_ip%%/*}" "${allowed_ip##*/}" 126 | ;; 127 | *:*) 128 | proto_add_ipv6_route "${allowed_ip%%/*}" "128" 129 | ;; 130 | *.*) 131 | proto_add_ipv4_route "${allowed_ip%%/*}" "32" 132 | ;; 133 | esac 134 | done 135 | fi 136 | } 137 | 138 | ensure_key_is_generated() { 139 | local private_key 140 | private_key="$(uci get network."$1".private_key)" 141 | 142 | if [ "$private_key" = "generate" ]; then 143 | local ucitmp 144 | oldmask="$(umask)" 145 | umask 077 146 | ucitmp="$(mktemp -d)" 147 | private_key="$("${WG}" genkey)" 148 | uci -q -t "$ucitmp" set network."$1".private_key="$private_key" && \ 149 | uci -q -t "$ucitmp" commit network 150 | rm -rf "$ucitmp" 151 | umask "$oldmask" 152 | fi 153 | } 154 | 155 | proto_amneziawg_setup() { 156 | local config="$1" 157 | local wg_dir="/tmp/wireguard" 158 | local wg_cfg="${wg_dir}/${config}" 159 | 160 | local private_key 161 | local listen_port 162 | local addresses 163 | local mtu 164 | local fwmark 165 | local ip6prefix 166 | local nohostroute 167 | local tunlink 168 | 169 | # Amnezia WG specific parameters 170 | local awg_jc 171 | local awg_jmin 172 | local awg_jmax 173 | local awg_s1 174 | local awg_s2 175 | local awg_h1 176 | local awg_h2 177 | local awg_h3 178 | local awg_h4 179 | 180 | ensure_key_is_generated "${config}" 181 | 182 | config_load network 183 | config_get private_key "${config}" "private_key" 184 | config_get listen_port "${config}" "listen_port" 185 | config_get addresses "${config}" "addresses" 186 | config_get mtu "${config}" "mtu" 187 | config_get fwmark "${config}" "fwmark" 188 | config_get ip6prefix "${config}" "ip6prefix" 189 | config_get nohostroute "${config}" "nohostroute" 190 | config_get tunlink "${config}" "tunlink" 191 | 192 | config_get awg_jc "${config}" "awg_jc" 193 | config_get awg_jmin "${config}" "awg_jmin" 194 | config_get awg_jmax "${config}" "awg_jmax" 195 | config_get awg_s1 "${config}" "awg_s1" 196 | config_get awg_s2 "${config}" "awg_s2" 197 | config_get awg_h1 "${config}" "awg_h1" 198 | config_get awg_h2 "${config}" "awg_h2" 199 | config_get awg_h3 "${config}" "awg_h3" 200 | config_get awg_h4 "${config}" "awg_h4" 201 | 202 | if proto_amneziawg_is_kernel_mode; then 203 | logger -t "amneziawg" "info: using kernel-space kmod-amneziawg for ${WG}" 204 | ip link del dev "${config}" 2>/dev/null 205 | ip link add dev "${config}" type amneziawg 206 | else 207 | logger -t "amneziawg" "info: using user-space amneziawg-go for ${WG}" 208 | rm -f "/var/run/wireguard/${config}.sock" 209 | amneziawg-go "${config}" 210 | fi 211 | 212 | if [ "${mtu}" ]; then 213 | ip link set mtu "${mtu}" dev "${config}" 214 | fi 215 | 216 | proto_init_update "${config}" 1 217 | 218 | umask 077 219 | mkdir -p "${wg_dir}" 220 | echo "[Interface]" > "${wg_cfg}" 221 | echo "PrivateKey=${private_key}" >> "${wg_cfg}" 222 | if [ "${listen_port}" ]; then 223 | echo "ListenPort=${listen_port}" >> "${wg_cfg}" 224 | fi 225 | if [ "${fwmark}" ]; then 226 | echo "FwMark=${fwmark}" >> "${wg_cfg}" 227 | fi 228 | # AWG 229 | if [ "${awg_jc}" ]; then 230 | echo "Jc = ${awg_jc}" >> "${wg_cfg}" 231 | fi 232 | if [ "${awg_jmin}" ]; then 233 | echo "Jmin = ${awg_jmin}" >> "${wg_cfg}" 234 | fi 235 | if [ "${awg_jmax}" ]; then 236 | echo "Jmax = ${awg_jmax}" >> "${wg_cfg}" 237 | fi 238 | if [ "${awg_s1}" ]; then 239 | echo "S1 = ${awg_s1}" >> "${wg_cfg}" 240 | fi 241 | if [ "${awg_s2}" ]; then 242 | echo "S2 = ${awg_s2}" >> "${wg_cfg}" 243 | fi 244 | if [ "${awg_h1}" ]; then 245 | echo "H1 = ${awg_h1}" >> "${wg_cfg}" 246 | fi 247 | if [ "${awg_h2}" ]; then 248 | echo "H2 = ${awg_h2}" >> "${wg_cfg}" 249 | fi 250 | if [ "${awg_h3}" ]; then 251 | echo "H3 = ${awg_h3}" >> "${wg_cfg}" 252 | fi 253 | if [ "${awg_h4}" ]; then 254 | echo "H4 = ${awg_h4}" >> "${wg_cfg}" 255 | fi 256 | 257 | config_foreach proto_amneziawg_setup_peer "amneziawg_${config}" 258 | 259 | # apply configuration file 260 | ${WG} setconf "${config}" "${wg_cfg}" 261 | WG_RETURN=$? 262 | 263 | rm -f "${wg_cfg}" 264 | 265 | if [ ${WG_RETURN} -ne 0 ]; then 266 | sleep 5 267 | proto_setup_failed "${config}" 268 | exit 1 269 | fi 270 | 271 | for address in ${addresses}; do 272 | case "${address}" in 273 | *:*/*) 274 | proto_add_ipv6_address "${address%%/*}" "${address##*/}" 275 | ;; 276 | *.*/*) 277 | proto_add_ipv4_address "${address%%/*}" "${address##*/}" 278 | ;; 279 | *:*) 280 | proto_add_ipv6_address "${address%%/*}" "128" 281 | ;; 282 | *.*) 283 | proto_add_ipv4_address "${address%%/*}" "32" 284 | ;; 285 | esac 286 | done 287 | 288 | for prefix in ${ip6prefix}; do 289 | proto_add_ipv6_prefix "$prefix" 290 | done 291 | 292 | # endpoint dependency 293 | if [ "${nohostroute}" != "1" ]; then 294 | # shellcheck disable=SC2034 295 | ${WG} show "${config}" endpoints | \ 296 | sed -E 's/\[?([0-9.:a-f]+)\]?:([0-9]+)/\1 \2/' | \ 297 | while IFS=$'\t ' read -r key address port; do 298 | [ -n "${port}" ] || continue 299 | proto_add_host_dependency "${config}" "${address}" "${tunlink}" 300 | done 301 | fi 302 | 303 | proto_send_update "${config}" 304 | } 305 | 306 | proto_amneziawg_teardown() { 307 | local config="$1" 308 | proto_amneziawg_check_installed 309 | if proto_amneziawg_is_kernel_mode; then 310 | ip link del dev "${config}" >/dev/null 2>&1 311 | else 312 | rm -f "/var/run/amneziawg/${config}.sock" 313 | fi 314 | } 315 | 316 | [ -n "$INCLUDE_ONLY" ] || { 317 | add_protocol amneziawg 318 | } 319 | -------------------------------------------------------------------------------- /amneziawg-tools/files/amneziawg_watchdog: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # SPDX-License-Identifier: GPL-2.0 3 | # 4 | # Copyright (C) 2018 Aleksandr V. Piskunov . 5 | # Copyright (C) 2015-2018 Jason A. Donenfeld . All Rights Reserved. 6 | # 7 | # This watchdog script tries to re-resolve hostnames for inactive WireGuard peers. 8 | # Use it for peers with a frequently changing dynamic IP. 9 | # persistent_keepalive must be set, recommended value is 25 seconds. 10 | # 11 | # Run this script from cron every minute: 12 | # echo '* * * * * /usr/bin/wireguard_watchdog' >> /etc/crontabs/root 13 | 14 | # shellcheck disable=SC1091,SC3043 15 | 16 | . /lib/functions.sh 17 | 18 | check_peer_activity() { 19 | local cfg="$1" 20 | local iface="$2" 21 | local disabled 22 | local public_key 23 | local endpoint_host 24 | local endpoint_port 25 | local persistent_keepalive 26 | local last_handshake 27 | local idle_seconds 28 | 29 | config_get_bool disabled "${cfg}" "disabled" 0 30 | config_get public_key "${cfg}" "public_key" 31 | config_get endpoint_host "${cfg}" "endpoint_host" 32 | config_get endpoint_port "${cfg}" "endpoint_port" 33 | 34 | if [ "${disabled}" -eq 1 ]; then 35 | # skip disabled peers 36 | return 0 37 | fi 38 | 39 | persistent_keepalive=$(awg show "${iface}" persistent-keepalive | grep "${public_key}" | awk '{print $2}') 40 | 41 | # only process peers with endpoints and keepalive set 42 | [ -z "${endpoint_host}" ] && return 0; 43 | if [ -z "${persistent_keepalive}" ] || [ "${persistent_keepalive}" = "off" ]; then return 0; fi 44 | 45 | # skip IP addresses 46 | # check taken from packages/net/ddns-scripts/files/dynamic_dns_functions.sh 47 | local IPV4_REGEX="[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}" 48 | local IPV6_REGEX="\(\([0-9A-Fa-f]\{1,4\}:\)\{1,\}\)\(\([0-9A-Fa-f]\{1,4\}\)\{0,1\}\)\(\(:[0-9A-Fa-f]\{1,4\}\)\{1,\}\)" 49 | local IPV4 IPV6 50 | IPV4="$(echo "${endpoint_host}" | grep -m 1 -o "$IPV4_REGEX$")" # do not detect ip in 0.0.0.0.example.com 51 | IPV6="$(echo "${endpoint_host}" | grep -m 1 -o "$IPV6_REGEX")" 52 | [ -n "${IPV4}${IPV6}" ] && return 0; 53 | 54 | # re-resolve endpoint hostname if not responding for too long 55 | last_handshake=$(awg show "${iface}" latest-handshakes | grep "${public_key}" | awk '{print $2}') 56 | [ -z "${last_handshake}" ] && return 0; 57 | idle_seconds=$(($(date +%s)-last_handshake)) 58 | [ ${idle_seconds} -lt 150 ] && return 0; 59 | logger -t "amneziawg_monitor" "${iface} endpoint ${endpoint_host}:${endpoint_port} is not responding for ${idle_seconds} seconds, trying to re-resolve hostname" 60 | awg set "${iface}" peer "${public_key}" endpoint "${endpoint_host}:${endpoint_port}" 61 | } 62 | 63 | # query ubus for all active wireguard interfaces 64 | wg_ifaces=$(ubus -S call network.interface dump | jsonfilter -e '@.interface[@.up=true]' | jsonfilter -a -e '@[@.proto="amneziawg"].interface' | tr "\n" " ") 65 | 66 | # check every peer in every active wireguard interface 67 | config_load network 68 | for iface in $wg_ifaces; do 69 | config_foreach check_peer_activity "amneziawg_${iface}" "${iface}" 70 | done 71 | -------------------------------------------------------------------------------- /kmod-amneziawg/Makefile: -------------------------------------------------------------------------------- 1 | include $(TOPDIR)/rules.mk 2 | 3 | include $(INCLUDE_DIR)/kernel.mk 4 | 5 | PKG_NAME:=kmod-amneziawg 6 | PKG_VERSION:=0.0.1 7 | PKG_RELEASE:=1 8 | 9 | include $(INCLUDE_DIR)/package.mk 10 | 11 | define KernelPackage/amneziawg 12 | SECTION:=kernel 13 | CATEGORY:=Kernel Modules 14 | SUBMENU:=Network Support 15 | URL:=https://amnezia.org/ 16 | MAINTAINER:=Amnezia Admin 17 | TITLE:=AmneziaWG Kernel Module 18 | FILES:=$(PKG_BUILD_DIR)/amneziawg.ko 19 | DEPENDS:= \ 20 | +kmod-udptunnel4 \ 21 | +kmod-udptunnel6 \ 22 | +kmod-crypto-lib-chacha20poly1305 \ 23 | +kmod-crypto-lib-curve25519 24 | endef 25 | 26 | define Build/Prepare 27 | cp -fr $(LINUX_DIR)/drivers/net/wireguard/{*.c,*.h,selftest/} $(PKG_BUILD_DIR) 28 | mkdir -p $(PKG_BUILD_DIR)/uapi 29 | cp -f $(LINUX_DIR)/include/uapi/linux/wireguard.h $(PKG_BUILD_DIR)/uapi/ 30 | for patch in `ls files/*`; do \ 31 | patch -d $(PKG_BUILD_DIR)/ -F3 -t -p0 -i "$$$$(pwd)/$$$${patch}"; \ 32 | done 33 | cp -f src/Makefile $(PKG_BUILD_DIR) 34 | endef 35 | 36 | define Build/Compile 37 | $(MAKE) -C "$(LINUX_DIR)" \ 38 | $(KERNEL_MAKE_FLAGS) \ 39 | M="$(PKG_BUILD_DIR)" \ 40 | EXTRA_CFLAGS="$(BUILDFLAGS)" \ 41 | modules 42 | endef 43 | 44 | $(eval $(call KernelPackage,amneziawg)) 45 | -------------------------------------------------------------------------------- /kmod-amneziawg/files/000-initial-amneziawg.patch: -------------------------------------------------------------------------------- 1 | diff --git cookie.c cookie.c 2 | index 8b7d1fe..3120094 100644 3 | --- cookie.c 4 | +++ cookie.c 5 | @@ -179,13 +179,13 @@ void wg_cookie_add_mac_to_packet(void *message, size_t len, 6 | 7 | void wg_cookie_message_create(struct message_handshake_cookie *dst, 8 | struct sk_buff *skb, __le32 index, 9 | - struct cookie_checker *checker) 10 | + struct cookie_checker *checker, u32 message_type) 11 | { 12 | struct message_macs *macs = (struct message_macs *) 13 | ((u8 *)skb->data + skb->len - sizeof(*macs)); 14 | u8 cookie[COOKIE_LEN]; 15 | 16 | - dst->header.type = cpu_to_le32(MESSAGE_HANDSHAKE_COOKIE); 17 | + dst->header.type = cpu_to_le32(message_type); 18 | dst->receiver_index = index; 19 | get_random_bytes_wait(dst->nonce, COOKIE_NONCE_LEN); 20 | 21 | diff --git cookie.h cookie.h 22 | index c4bd61c..2b50660 100644 23 | --- cookie.h 24 | +++ cookie.h 25 | @@ -52,7 +52,7 @@ void wg_cookie_add_mac_to_packet(void *message, size_t len, 26 | 27 | void wg_cookie_message_create(struct message_handshake_cookie *src, 28 | struct sk_buff *skb, __le32 index, 29 | - struct cookie_checker *checker); 30 | + struct cookie_checker *checker, u32 message_type); 31 | void wg_cookie_message_consume(struct message_handshake_cookie *src, 32 | struct wg_device *wg); 33 | 34 | diff --git device.c device.c 35 | index 062490f..40c4f1c 100644 36 | --- device.c 37 | +++ device.c 38 | @@ -377,6 +377,11 @@ static int wg_newlink(struct net *src_net, struct net_device *dev, 39 | */ 40 | dev->priv_destructor = wg_destruct; 41 | 42 | + wg->advanced_security_config.init_packet_magic_header = MESSAGE_HANDSHAKE_INITIATION; 43 | + wg->advanced_security_config.response_packet_magic_header = MESSAGE_HANDSHAKE_RESPONSE; 44 | + wg->advanced_security_config.cookie_packet_magic_header = MESSAGE_HANDSHAKE_COOKIE; 45 | + wg->advanced_security_config.transport_packet_magic_header = MESSAGE_DATA; 46 | + 47 | pr_debug("%s: Interface created\n", dev->name); 48 | return ret; 49 | 50 | @@ -473,3 +478,118 @@ void wg_device_uninit(void) 51 | #endif 52 | rcu_barrier(); 53 | } 54 | + 55 | +int wg_device_handle_post_config(struct net_device *dev, struct amnezia_config *asc) 56 | +{ 57 | + struct wg_device *wg = netdev_priv(dev); 58 | + bool a_sec_on = false; 59 | + int ret = 0; 60 | + 61 | + if (!asc->advanced_security_enabled) 62 | + goto out; 63 | + 64 | + if (asc->junk_packet_count < 0) { 65 | + net_dbg_ratelimited("%s: JunkPacketCount should be non negative\n", dev->name); 66 | + ret = -EINVAL; 67 | + } 68 | + 69 | + wg->advanced_security_config.junk_packet_count = asc->junk_packet_count; 70 | + if (asc->junk_packet_count != 0) 71 | + a_sec_on = true; 72 | + 73 | + wg->advanced_security_config.junk_packet_min_size = asc->junk_packet_min_size; 74 | + if (asc->junk_packet_min_size != 0) 75 | + a_sec_on = true; 76 | + 77 | + if (asc->junk_packet_count > 0 && asc->junk_packet_min_size == asc->junk_packet_max_size) 78 | + asc->junk_packet_max_size++; 79 | + 80 | + if (asc->junk_packet_max_size >= MESSAGE_MAX_SIZE) { 81 | + wg->advanced_security_config.junk_packet_min_size = 0; 82 | + wg->advanced_security_config.junk_packet_max_size = 1; 83 | + 84 | + net_dbg_ratelimited("%s: JunkPacketMaxSize: %d; should be smaller than maxSegmentSize: %d\n", 85 | + dev->name, asc->junk_packet_max_size, 86 | + MESSAGE_MAX_SIZE); 87 | + ret = -EINVAL; 88 | + } else if (asc->junk_packet_max_size < asc->junk_packet_min_size) { 89 | + net_dbg_ratelimited("%s: maxSize: %d; should be greater than minSize: %d\n", 90 | + dev->name, asc->junk_packet_max_size, 91 | + asc->junk_packet_min_size); 92 | + ret = -EINVAL; 93 | + } else 94 | + wg->advanced_security_config.junk_packet_max_size = asc->junk_packet_max_size; 95 | + 96 | + if (asc->junk_packet_max_size != 0) 97 | + a_sec_on = true; 98 | + 99 | + if (asc->init_packet_junk_size + MESSAGE_INITIATION_SIZE >= MESSAGE_MAX_SIZE) { 100 | + net_dbg_ratelimited("%s: init header size (%d) + junkSize (%d) should be smaller than maxSegmentSize: %d\n", 101 | + dev->name, MESSAGE_INITIATION_SIZE, 102 | + asc->init_packet_junk_size, MESSAGE_MAX_SIZE); 103 | + ret = -EINVAL; 104 | + } else 105 | + wg->advanced_security_config.init_packet_junk_size = asc->init_packet_junk_size; 106 | + 107 | + if (asc->init_packet_junk_size != 0) 108 | + a_sec_on = true; 109 | + 110 | + if (asc->response_packet_junk_size + MESSAGE_RESPONSE_SIZE >= MESSAGE_MAX_SIZE) { 111 | + net_dbg_ratelimited("%s: response header size (%d) + junkSize (%d) should be smaller than maxSegmentSize: %d\n", 112 | + dev->name, MESSAGE_RESPONSE_SIZE, 113 | + asc->response_packet_junk_size, MESSAGE_MAX_SIZE); 114 | + ret = -EINVAL; 115 | + } else 116 | + wg->advanced_security_config.response_packet_junk_size = asc->response_packet_junk_size; 117 | + 118 | + if (asc->response_packet_junk_size != 0) 119 | + a_sec_on = true; 120 | + 121 | + if (asc->init_packet_magic_header > MESSAGE_DATA) { 122 | + a_sec_on = true; 123 | + wg->advanced_security_config.init_packet_magic_header = asc->init_packet_magic_header; 124 | + } 125 | + 126 | + if (asc->response_packet_magic_header > MESSAGE_DATA) { 127 | + a_sec_on = true; 128 | + wg->advanced_security_config.response_packet_magic_header = asc->response_packet_magic_header; 129 | + } 130 | + 131 | + if (asc->cookie_packet_magic_header > MESSAGE_DATA) { 132 | + a_sec_on = true; 133 | + wg->advanced_security_config.cookie_packet_magic_header = asc->cookie_packet_magic_header; 134 | + } 135 | + 136 | + if (asc->transport_packet_magic_header > MESSAGE_DATA) { 137 | + a_sec_on = true; 138 | + wg->advanced_security_config.transport_packet_magic_header = asc->transport_packet_magic_header; 139 | + } 140 | + 141 | + if (wg->advanced_security_config.init_packet_magic_header == wg->advanced_security_config.response_packet_magic_header || 142 | + wg->advanced_security_config.init_packet_magic_header == wg->advanced_security_config.cookie_packet_magic_header || 143 | + wg->advanced_security_config.init_packet_magic_header == wg->advanced_security_config.transport_packet_magic_header || 144 | + wg->advanced_security_config.response_packet_magic_header == wg->advanced_security_config.cookie_packet_magic_header || 145 | + wg->advanced_security_config.response_packet_magic_header == wg->advanced_security_config.transport_packet_magic_header || 146 | + wg->advanced_security_config.cookie_packet_magic_header == wg->advanced_security_config.transport_packet_magic_header) { 147 | + net_dbg_ratelimited("%s: magic headers should differ; got: init:%d; recv:%d; unde:%d; tran:%d\n", 148 | + dev->name, 149 | + wg->advanced_security_config.init_packet_magic_header, 150 | + wg->advanced_security_config.response_packet_magic_header, 151 | + wg->advanced_security_config.cookie_packet_magic_header, 152 | + wg->advanced_security_config.transport_packet_magic_header); 153 | + ret = -EINVAL; 154 | + } 155 | + 156 | + if (MESSAGE_INITIATION_SIZE + wg->advanced_security_config.init_packet_junk_size == 157 | + MESSAGE_RESPONSE_SIZE + wg->advanced_security_config.response_packet_junk_size) { 158 | + net_dbg_ratelimited("%s: new init size:%d; and new response size:%d; should differ\n", 159 | + dev->name, 160 | + MESSAGE_INITIATION_SIZE + asc->init_packet_junk_size, 161 | + MESSAGE_RESPONSE_SIZE + asc->response_packet_junk_size); 162 | + ret = -EINVAL; 163 | + } 164 | + 165 | + wg->advanced_security_config.advanced_security_enabled = a_sec_on; 166 | +out: 167 | + return ret; 168 | +} 169 | diff --git device.h device.h 170 | index 43c7ceb..89e946c 100644 171 | --- device.h 172 | +++ device.h 173 | @@ -37,6 +37,19 @@ struct prev_queue { 174 | atomic_t count; 175 | }; 176 | 177 | +struct amnezia_config { 178 | + bool advanced_security_enabled; 179 | + u16 junk_packet_count; 180 | + u16 junk_packet_min_size; 181 | + u16 junk_packet_max_size; 182 | + u16 init_packet_junk_size; 183 | + u16 response_packet_junk_size; 184 | + u32 init_packet_magic_header; 185 | + u32 response_packet_magic_header; 186 | + u32 cookie_packet_magic_header; 187 | + u32 transport_packet_magic_header; 188 | +}; 189 | + 190 | struct wg_device { 191 | struct net_device *dev; 192 | struct crypt_queue encrypt_queue, decrypt_queue, handshake_queue; 193 | @@ -50,6 +63,7 @@ struct wg_device { 194 | struct allowedips peer_allowedips; 195 | struct mutex device_update_lock, socket_update_lock; 196 | struct list_head device_list, peer_list; 197 | + struct amnezia_config advanced_security_config; 198 | atomic_t handshake_queue_len; 199 | unsigned int num_peers, device_update_gen; 200 | u32 fwmark; 201 | @@ -58,5 +72,6 @@ struct wg_device { 202 | 203 | int wg_device_init(void); 204 | void wg_device_uninit(void); 205 | +int wg_device_handle_post_config(struct net_device *dev, struct amnezia_config *asc); 206 | 207 | #endif /* _WG_DEVICE_H */ 208 | diff --git main.c main.c 209 | index 5506738..b45253d 100644 210 | --- main.c 211 | +++ main.c 212 | @@ -9,9 +9,7 @@ 213 | #include "queueing.h" 214 | #include "ratelimiter.h" 215 | #include "netlink.h" 216 | - 217 | -#include 218 | - 219 | +#include "uapi/wireguard.h" 220 | #include "crypto/zinc.h" 221 | 222 | #include 223 | @@ -52,7 +50,7 @@ static int __init wg_mod_init(void) 224 | if (ret < 0) 225 | goto err_netlink; 226 | 227 | - pr_info("WireGuard " WIREGUARD_VERSION " loaded. See www.wireguard.com for information.\n"); 228 | + pr_info("WireGuard " WIREGUARD_VERSION " (Amnezia VPN) loaded. See www.wireguard.com for information.\n"); 229 | pr_info("Copyright (C) 2015-2019 Jason A. Donenfeld . All Rights Reserved.\n"); 230 | 231 | return 0; 232 | @@ -78,7 +76,7 @@ static void __exit wg_mod_exit(void) 233 | module_init(wg_mod_init); 234 | module_exit(wg_mod_exit); 235 | MODULE_LICENSE("GPL v2"); 236 | -MODULE_DESCRIPTION("WireGuard secure network tunnel"); 237 | +MODULE_DESCRIPTION("WireGuard (Amnezia VPN) secure network tunnel"); 238 | MODULE_AUTHOR("Jason A. Donenfeld "); 239 | MODULE_VERSION(WIREGUARD_VERSION); 240 | MODULE_ALIAS_RTNL_LINK(KBUILD_MODNAME); 241 | diff --git messages.h messages.h 242 | index 1d1ed18..42cd054 100644 243 | --- messages.h 244 | +++ messages.h 245 | @@ -117,6 +117,14 @@ enum message_alignments { 246 | MESSAGE_MINIMUM_LENGTH = message_data_len(0) 247 | }; 248 | 249 | +enum message_size { 250 | + MESSAGE_INITIATION_SIZE = sizeof(struct message_handshake_initiation), 251 | + MESSAGE_RESPONSE_SIZE = sizeof(struct message_handshake_response), 252 | + MESSAGE_COOKIE_REPLY_SIZE = sizeof(struct message_handshake_cookie), 253 | + MESSAGE_TRANSPORT_SIZE = sizeof(struct message_data), 254 | + MESSAGE_MAX_SIZE = 65535 255 | +}; 256 | + 257 | #define SKB_HEADER_LEN \ 258 | (max(sizeof(struct iphdr), sizeof(struct ipv6hdr)) + \ 259 | sizeof(struct udphdr) + NET_SKB_PAD) 260 | diff --git netlink.c netlink.c 261 | index e3420e0..1d03aef 100644 262 | --- netlink.c 263 | +++ netlink.c 264 | @@ -9,9 +9,7 @@ 265 | #include "socket.h" 266 | #include "queueing.h" 267 | #include "messages.h" 268 | - 269 | -#include 270 | - 271 | +#include "uapi/wireguard.h" 272 | #include 273 | #include 274 | #include 275 | @@ -27,7 +25,16 @@ static const struct nla_policy device_policy[WGDEVICE_A_MAX + 1] = { 276 | [WGDEVICE_A_FLAGS] = { .type = NLA_U32 }, 277 | [WGDEVICE_A_LISTEN_PORT] = { .type = NLA_U16 }, 278 | [WGDEVICE_A_FWMARK] = { .type = NLA_U32 }, 279 | - [WGDEVICE_A_PEERS] = { .type = NLA_NESTED } 280 | + [WGDEVICE_A_PEERS] = { .type = NLA_NESTED }, 281 | + [WGDEVICE_A_JC] = { .type = NLA_U16 }, 282 | + [WGDEVICE_A_JMIN] = { .type = NLA_U16 }, 283 | + [WGDEVICE_A_JMAX] = { .type = NLA_U16 }, 284 | + [WGDEVICE_A_S1] = { .type = NLA_U16 }, 285 | + [WGDEVICE_A_S2] = { .type = NLA_U16 }, 286 | + [WGDEVICE_A_H1] = { .type = NLA_U32 }, 287 | + [WGDEVICE_A_H2] = { .type = NLA_U32 }, 288 | + [WGDEVICE_A_H3] = { .type = NLA_U32 }, 289 | + [WGDEVICE_A_H4] = { .type = NLA_U32 } 290 | }; 291 | 292 | static const struct nla_policy peer_policy[WGPEER_A_MAX + 1] = { 293 | @@ -233,7 +240,25 @@ static int wg_get_device_dump(struct sk_buff *skb, struct netlink_callback *cb) 294 | wg->incoming_port) || 295 | nla_put_u32(skb, WGDEVICE_A_FWMARK, wg->fwmark) || 296 | nla_put_u32(skb, WGDEVICE_A_IFINDEX, wg->dev->ifindex) || 297 | - nla_put_string(skb, WGDEVICE_A_IFNAME, wg->dev->name)) 298 | + nla_put_string(skb, WGDEVICE_A_IFNAME, wg->dev->name) || 299 | + nla_put_u16(skb, WGDEVICE_A_JC, 300 | + wg->advanced_security_config.junk_packet_count) || 301 | + nla_put_u16(skb, WGDEVICE_A_JMIN, 302 | + wg->advanced_security_config.junk_packet_min_size) || 303 | + nla_put_u16(skb, WGDEVICE_A_JMAX, 304 | + wg->advanced_security_config.junk_packet_max_size) || 305 | + nla_put_u16(skb, WGDEVICE_A_S1, 306 | + wg->advanced_security_config.init_packet_junk_size) || 307 | + nla_put_u16(skb, WGDEVICE_A_S2, 308 | + wg->advanced_security_config.response_packet_junk_size) || 309 | + nla_put_u32(skb, WGDEVICE_A_H1, 310 | + wg->advanced_security_config.init_packet_magic_header) || 311 | + nla_put_u32(skb, WGDEVICE_A_H2, 312 | + wg->advanced_security_config.response_packet_magic_header) || 313 | + nla_put_u32(skb, WGDEVICE_A_H3, 314 | + wg->advanced_security_config.cookie_packet_magic_header) || 315 | + nla_put_u32(skb, WGDEVICE_A_H4, 316 | + wg->advanced_security_config.transport_packet_magic_header)) 317 | goto out; 318 | 319 | down_read(&wg->static_identity.lock); 320 | @@ -494,6 +519,7 @@ out: 321 | static int wg_set_device(struct sk_buff *skb, struct genl_info *info) 322 | { 323 | struct wg_device *wg = lookup_interface(info->attrs, skb); 324 | + struct amnezia_config *asc = kzalloc(sizeof(*asc), GFP_KERNEL); 325 | u32 flags = 0; 326 | int ret; 327 | 328 | @@ -538,6 +564,51 @@ static int wg_set_device(struct sk_buff *skb, struct genl_info *info) 329 | goto out; 330 | } 331 | 332 | + if (info->attrs[WGDEVICE_A_JC]) { 333 | + asc->advanced_security_enabled = true; 334 | + asc->junk_packet_count = nla_get_u16(info->attrs[WGDEVICE_A_JC]); 335 | + } 336 | + 337 | + if (info->attrs[WGDEVICE_A_JMIN]) { 338 | + asc->advanced_security_enabled = true; 339 | + asc->junk_packet_min_size = nla_get_u16(info->attrs[WGDEVICE_A_JMIN]); 340 | + } 341 | + 342 | + if (info->attrs[WGDEVICE_A_JMAX]) { 343 | + asc->advanced_security_enabled = true; 344 | + asc->junk_packet_max_size = nla_get_u16(info->attrs[WGDEVICE_A_JMAX]); 345 | + } 346 | + 347 | + if (info->attrs[WGDEVICE_A_S1]) { 348 | + asc->advanced_security_enabled = true; 349 | + asc->init_packet_junk_size = nla_get_u16(info->attrs[WGDEVICE_A_S1]); 350 | + } 351 | + 352 | + if (info->attrs[WGDEVICE_A_S2]) { 353 | + asc->advanced_security_enabled = true; 354 | + asc->response_packet_junk_size = nla_get_u16(info->attrs[WGDEVICE_A_S2]); 355 | + } 356 | + 357 | + if (info->attrs[WGDEVICE_A_H1]) { 358 | + asc->advanced_security_enabled = true; 359 | + asc->init_packet_magic_header = nla_get_u32(info->attrs[WGDEVICE_A_H1]); 360 | + } 361 | + 362 | + if (info->attrs[WGDEVICE_A_H2]) { 363 | + asc->advanced_security_enabled = true; 364 | + asc->response_packet_magic_header = nla_get_u32(info->attrs[WGDEVICE_A_H2]); 365 | + } 366 | + 367 | + if (info->attrs[WGDEVICE_A_H3]) { 368 | + asc->advanced_security_enabled = true; 369 | + asc->cookie_packet_magic_header = nla_get_u32(info->attrs[WGDEVICE_A_H3]); 370 | + } 371 | + 372 | + if (info->attrs[WGDEVICE_A_H4]) { 373 | + asc->advanced_security_enabled = true; 374 | + asc->transport_packet_magic_header = nla_get_u32(info->attrs[WGDEVICE_A_H4]); 375 | + } 376 | + 377 | if (flags & WGDEVICE_F_REPLACE_PEERS) 378 | wg_peer_remove_all(wg); 379 | 380 | @@ -591,13 +662,14 @@ skip_set_private_key: 381 | goto out; 382 | } 383 | } 384 | - ret = 0; 385 | + ret = wg_device_handle_post_config(wg->dev, asc); 386 | 387 | out: 388 | mutex_unlock(&wg->device_update_lock); 389 | rtnl_unlock(); 390 | dev_put(wg->dev); 391 | out_nodev: 392 | + kfree(asc); 393 | if (info->attrs[WGDEVICE_A_PRIVATE_KEY]) 394 | memzero_explicit(nla_data(info->attrs[WGDEVICE_A_PRIVATE_KEY]), 395 | nla_len(info->attrs[WGDEVICE_A_PRIVATE_KEY])); 396 | diff --git noise.c noise.c 397 | index baf455e..9a4e8e0 100644 398 | --- noise.c 399 | +++ noise.c 400 | @@ -484,7 +484,7 @@ static void tai64n_now(u8 output[NOISE_TIMESTAMP_LEN]) 401 | 402 | bool 403 | wg_noise_handshake_create_initiation(struct message_handshake_initiation *dst, 404 | - struct noise_handshake *handshake) 405 | + struct noise_handshake *handshake, u32 message_type) 406 | { 407 | u8 timestamp[NOISE_TIMESTAMP_LEN]; 408 | u8 key[NOISE_SYMMETRIC_KEY_LEN]; 409 | @@ -501,7 +501,7 @@ wg_noise_handshake_create_initiation(struct message_handshake_initiation *dst, 410 | if (unlikely(!handshake->static_identity->has_identity)) 411 | goto out; 412 | 413 | - dst->header.type = cpu_to_le32(MESSAGE_HANDSHAKE_INITIATION); 414 | + dst->header.type = cpu_to_le32(message_type); 415 | 416 | handshake_init(handshake->chaining_key, handshake->hash, 417 | handshake->remote_static); 418 | @@ -634,7 +634,7 @@ out: 419 | } 420 | 421 | bool wg_noise_handshake_create_response(struct message_handshake_response *dst, 422 | - struct noise_handshake *handshake) 423 | + struct noise_handshake *handshake, u32 message_type) 424 | { 425 | u8 key[NOISE_SYMMETRIC_KEY_LEN]; 426 | bool ret = false; 427 | @@ -650,7 +650,7 @@ bool wg_noise_handshake_create_response(struct message_handshake_response *dst, 428 | if (handshake->state != HANDSHAKE_CONSUMED_INITIATION) 429 | goto out; 430 | 431 | - dst->header.type = cpu_to_le32(MESSAGE_HANDSHAKE_RESPONSE); 432 | + dst->header.type = cpu_to_le32(message_type); 433 | dst->receiver_index = handshake->remote_index; 434 | 435 | /* e */ 436 | diff --git noise.h noise.h 437 | index c527253..300d9d4 100644 438 | --- noise.h 439 | +++ noise.h 440 | @@ -118,13 +118,13 @@ void wg_noise_precompute_static_static(struct wg_peer *peer); 441 | 442 | bool 443 | wg_noise_handshake_create_initiation(struct message_handshake_initiation *dst, 444 | - struct noise_handshake *handshake); 445 | + struct noise_handshake *handshake, u32 message_type); 446 | struct wg_peer * 447 | wg_noise_handshake_consume_initiation(struct message_handshake_initiation *src, 448 | struct wg_device *wg); 449 | 450 | bool wg_noise_handshake_create_response(struct message_handshake_response *dst, 451 | - struct noise_handshake *handshake); 452 | + struct noise_handshake *handshake, u32 message_type); 453 | struct wg_peer * 454 | wg_noise_handshake_consume_response(struct message_handshake_response *src, 455 | struct wg_device *wg); 456 | diff --git receive.c receive.c 457 | index 214889e..d6566e6 100644 458 | --- receive.c 459 | +++ receive.c 460 | @@ -33,25 +33,51 @@ static void update_rx_stats(struct wg_peer *peer, size_t len) 461 | 462 | #define SKB_TYPE_LE32(skb) (((struct message_header *)(skb)->data)->type) 463 | 464 | -static size_t validate_header_len(struct sk_buff *skb) 465 | +static size_t validate_header_len(struct sk_buff *skb, struct wg_device *wg) 466 | { 467 | if (unlikely(skb->len < sizeof(struct message_header))) 468 | return 0; 469 | - if (SKB_TYPE_LE32(skb) == cpu_to_le32(MESSAGE_DATA) && 470 | + if (SKB_TYPE_LE32(skb) == cpu_to_le32(wg->advanced_security_config.transport_packet_magic_header) && 471 | skb->len >= MESSAGE_MINIMUM_LENGTH) 472 | return sizeof(struct message_data); 473 | - if (SKB_TYPE_LE32(skb) == cpu_to_le32(MESSAGE_HANDSHAKE_INITIATION) && 474 | - skb->len == sizeof(struct message_handshake_initiation)) 475 | - return sizeof(struct message_handshake_initiation); 476 | - if (SKB_TYPE_LE32(skb) == cpu_to_le32(MESSAGE_HANDSHAKE_RESPONSE) && 477 | - skb->len == sizeof(struct message_handshake_response)) 478 | - return sizeof(struct message_handshake_response); 479 | - if (SKB_TYPE_LE32(skb) == cpu_to_le32(MESSAGE_HANDSHAKE_COOKIE) && 480 | - skb->len == sizeof(struct message_handshake_cookie)) 481 | - return sizeof(struct message_handshake_cookie); 482 | + if (SKB_TYPE_LE32(skb) == cpu_to_le32(wg->advanced_security_config.init_packet_magic_header) && 483 | + skb->len == MESSAGE_INITIATION_SIZE) 484 | + return MESSAGE_INITIATION_SIZE; 485 | + if (SKB_TYPE_LE32(skb) == cpu_to_le32(wg->advanced_security_config.response_packet_magic_header) && 486 | + skb->len == MESSAGE_RESPONSE_SIZE) 487 | + return MESSAGE_RESPONSE_SIZE; 488 | + if (SKB_TYPE_LE32(skb) == cpu_to_le32(wg->advanced_security_config.cookie_packet_magic_header) && 489 | + skb->len == MESSAGE_COOKIE_REPLY_SIZE) 490 | + return MESSAGE_COOKIE_REPLY_SIZE; 491 | return 0; 492 | } 493 | 494 | +void prepare_advanced_secured_message(struct sk_buff *skb, struct wg_device *wg) 495 | +{ 496 | + u32 assumed_type = SKB_TYPE_LE32(skb); 497 | + u32 assumed_offset; 498 | + 499 | + if (wg->advanced_security_config.advanced_security_enabled) { 500 | + if (skb->len == MESSAGE_INITIATION_SIZE + wg->advanced_security_config.init_packet_junk_size) { 501 | + assumed_type = cpu_to_le32(wg->advanced_security_config.init_packet_magic_header); 502 | + assumed_offset = wg->advanced_security_config.init_packet_junk_size; 503 | + } else if (skb->len == MESSAGE_RESPONSE_SIZE + wg->advanced_security_config.response_packet_junk_size) { 504 | + assumed_type = cpu_to_le32(wg->advanced_security_config.response_packet_magic_header); 505 | + assumed_offset = wg->advanced_security_config.response_packet_junk_size; 506 | + } else 507 | + return; 508 | + 509 | + if (unlikely(assumed_offset <= 0) || unlikely(!pskb_may_pull(skb, assumed_offset))) 510 | + return; 511 | + 512 | + skb_pull(skb, assumed_offset); 513 | + 514 | + if (SKB_TYPE_LE32(skb) != assumed_type) { 515 | + skb_push(skb, assumed_offset); 516 | + } 517 | + } 518 | +} 519 | + 520 | static int prepare_skb_header(struct sk_buff *skb, struct wg_device *wg) 521 | { 522 | size_t data_offset, data_len, header_len; 523 | @@ -87,7 +113,8 @@ static int prepare_skb_header(struct sk_buff *skb, struct wg_device *wg) 524 | if (unlikely(skb->len != data_len)) 525 | /* Final len does not agree with calculated len */ 526 | return -EINVAL; 527 | - header_len = validate_header_len(skb); 528 | + prepare_advanced_secured_message(skb, wg); 529 | + header_len = validate_header_len(skb, wg); 530 | if (unlikely(!header_len)) 531 | return -EINVAL; 532 | __skb_push(skb, data_offset); 533 | @@ -109,7 +136,7 @@ static void wg_receive_handshake_packet(struct wg_device *wg, 534 | bool packet_needs_cookie; 535 | bool under_load; 536 | 537 | - if (SKB_TYPE_LE32(skb) == cpu_to_le32(MESSAGE_HANDSHAKE_COOKIE)) { 538 | + if (SKB_TYPE_LE32(skb) == cpu_to_le32(wg->advanced_security_config.cookie_packet_magic_header)) { 539 | net_dbg_skb_ratelimited("%s: Receiving cookie response from %pISpfsc\n", 540 | wg->dev->name, skb); 541 | wg_cookie_message_consume( 542 | @@ -139,8 +166,7 @@ static void wg_receive_handshake_packet(struct wg_device *wg, 543 | return; 544 | } 545 | 546 | - switch (SKB_TYPE_LE32(skb)) { 547 | - case cpu_to_le32(MESSAGE_HANDSHAKE_INITIATION): { 548 | + if (SKB_TYPE_LE32(skb) == cpu_to_le32(wg->advanced_security_config.init_packet_magic_header)) { 549 | struct message_handshake_initiation *message = 550 | (struct message_handshake_initiation *)skb->data; 551 | 552 | @@ -160,9 +186,8 @@ static void wg_receive_handshake_packet(struct wg_device *wg, 553 | wg->dev->name, peer->internal_id, 554 | &peer->endpoint.addr); 555 | wg_packet_send_handshake_response(peer); 556 | - break; 557 | } 558 | - case cpu_to_le32(MESSAGE_HANDSHAKE_RESPONSE): { 559 | + if (SKB_TYPE_LE32(skb) == cpu_to_le32(wg->advanced_security_config.response_packet_magic_header)) { 560 | struct message_handshake_response *message = 561 | (struct message_handshake_response *)skb->data; 562 | 563 | @@ -193,8 +218,6 @@ static void wg_receive_handshake_packet(struct wg_device *wg, 564 | */ 565 | wg_packet_send_keepalive(peer); 566 | } 567 | - break; 568 | - } 569 | } 570 | 571 | if (unlikely(!peer)) { 572 | @@ -559,10 +582,10 @@ void wg_packet_receive(struct wg_device *wg, struct sk_buff *skb) 573 | { 574 | if (unlikely(prepare_skb_header(skb, wg) < 0)) 575 | goto err; 576 | - switch (SKB_TYPE_LE32(skb)) { 577 | - case cpu_to_le32(MESSAGE_HANDSHAKE_INITIATION): 578 | - case cpu_to_le32(MESSAGE_HANDSHAKE_RESPONSE): 579 | - case cpu_to_le32(MESSAGE_HANDSHAKE_COOKIE): { 580 | + 581 | + if (SKB_TYPE_LE32(skb) == cpu_to_le32(wg->advanced_security_config.init_packet_magic_header) || 582 | + SKB_TYPE_LE32(skb) == cpu_to_le32(wg->advanced_security_config.response_packet_magic_header) || 583 | + SKB_TYPE_LE32(skb) == cpu_to_le32(wg->advanced_security_config.cookie_packet_magic_header)) { 584 | int cpu, ret = -EBUSY; 585 | 586 | if (unlikely(!rng_is_initialized())) 587 | @@ -575,23 +598,20 @@ void wg_packet_receive(struct wg_device *wg, struct sk_buff *skb) 588 | } else 589 | ret = ptr_ring_produce_bh(&wg->handshake_queue.ring, skb); 590 | if (ret) { 591 | - drop: 592 | +drop: 593 | net_dbg_skb_ratelimited("%s: Dropping handshake packet from %pISpfsc\n", 594 | - wg->dev->name, skb); 595 | + wg->dev->name, skb); 596 | goto err; 597 | } 598 | atomic_inc(&wg->handshake_queue_len); 599 | cpu = wg_cpumask_next_online(&wg->handshake_queue.last_cpu); 600 | /* Queues up a call to packet_process_queued_handshake_packets(skb): */ 601 | queue_work_on(cpu, wg->handshake_receive_wq, 602 | - &per_cpu_ptr(wg->handshake_queue.worker, cpu)->work); 603 | - break; 604 | - } 605 | - case cpu_to_le32(MESSAGE_DATA): 606 | + &per_cpu_ptr(wg->handshake_queue.worker, cpu)->work); 607 | + } else if (SKB_TYPE_LE32(skb) == cpu_to_le32(wg->advanced_security_config.transport_packet_magic_header)) { 608 | PACKET_CB(skb)->ds = ip_tunnel_get_dsfield(ip_hdr(skb), skb); 609 | wg_packet_consume_data(wg, skb); 610 | - break; 611 | - default: 612 | + } else { 613 | WARN(1, "Non-exhaustive parsing of packet header lead to unknown packet type!\n"); 614 | goto err; 615 | } 616 | diff --git send.c send.c 617 | index 2b19344..c96d2a2 100644 618 | --- send.c 619 | +++ send.c 620 | @@ -15,13 +15,24 @@ 621 | #include 622 | #include 623 | #include 624 | +#include 625 | #include 626 | #include 627 | #include 628 | 629 | +u32 wg_get_random_u32_inclusive(u32 floor, u32 ceil) 630 | +{ 631 | + u32 diff = ceil - floor + 1; 632 | + return floor + (get_random_u32() % diff); 633 | +} 634 | + 635 | static void wg_packet_send_handshake_initiation(struct wg_peer *peer) 636 | { 637 | struct message_handshake_initiation packet; 638 | + struct wg_device *wg = peer->device; 639 | + void *buffer; 640 | + u8 ds; 641 | + u16 junk_packet_count, junk_packet_size; 642 | 643 | if (!wg_birthdate_has_expired(atomic64_read(&peer->last_sent_handshake), 644 | REKEY_TIMEOUT)) 645 | @@ -32,14 +43,37 @@ static void wg_packet_send_handshake_initiation(struct wg_peer *peer) 646 | peer->device->dev->name, peer->internal_id, 647 | &peer->endpoint.addr); 648 | 649 | - if (wg_noise_handshake_create_initiation(&packet, &peer->handshake)) { 650 | + if (wg->advanced_security_config.advanced_security_enabled) { 651 | + junk_packet_count = wg->advanced_security_config.junk_packet_count; 652 | + buffer = kzalloc(wg->advanced_security_config.junk_packet_max_size, GFP_KERNEL); 653 | + 654 | + while (junk_packet_count-- > 0) { 655 | + junk_packet_size = (u16) wg_get_random_u32_inclusive( 656 | + wg->advanced_security_config.junk_packet_min_size, 657 | + wg->advanced_security_config.junk_packet_max_size); 658 | + 659 | + get_random_bytes(buffer, junk_packet_size); 660 | + get_random_bytes(&ds, 1); 661 | + wg_socket_send_buffer_to_peer(peer, buffer, junk_packet_size, ds); 662 | + } 663 | + 664 | + kfree(buffer); 665 | + } 666 | + 667 | + if (wg_noise_handshake_create_initiation(&packet, &peer->handshake, wg->advanced_security_config.init_packet_magic_header)) { 668 | wg_cookie_add_mac_to_packet(&packet, sizeof(packet), peer); 669 | wg_timers_any_authenticated_packet_traversal(peer); 670 | wg_timers_any_authenticated_packet_sent(peer); 671 | atomic64_set(&peer->last_sent_handshake, 672 | ktime_get_coarse_boottime_ns()); 673 | - wg_socket_send_buffer_to_peer(peer, &packet, sizeof(packet), 674 | - HANDSHAKE_DSCP); 675 | + 676 | + if (wg->advanced_security_config.advanced_security_enabled) { 677 | + wg_socket_send_junked_buffer_to_peer(peer, &packet, sizeof(packet), 678 | + HANDSHAKE_DSCP, wg->advanced_security_config.init_packet_junk_size); 679 | + } else { 680 | + wg_socket_send_buffer_to_peer(peer, &packet, sizeof(packet), 681 | + HANDSHAKE_DSCP); 682 | + } 683 | wg_timers_handshake_initiated(peer); 684 | } 685 | } 686 | @@ -86,13 +120,14 @@ out: 687 | void wg_packet_send_handshake_response(struct wg_peer *peer) 688 | { 689 | struct message_handshake_response packet; 690 | + struct wg_device *wg = peer->device; 691 | 692 | atomic64_set(&peer->last_sent_handshake, ktime_get_coarse_boottime_ns()); 693 | net_dbg_ratelimited("%s: Sending handshake response to peer %llu (%pISpfsc)\n", 694 | peer->device->dev->name, peer->internal_id, 695 | &peer->endpoint.addr); 696 | 697 | - if (wg_noise_handshake_create_response(&packet, &peer->handshake)) { 698 | + if (wg_noise_handshake_create_response(&packet, &peer->handshake, wg->advanced_security_config.response_packet_magic_header)) { 699 | wg_cookie_add_mac_to_packet(&packet, sizeof(packet), peer); 700 | if (wg_noise_handshake_begin_session(&peer->handshake, 701 | &peer->keypairs)) { 702 | @@ -101,9 +136,16 @@ void wg_packet_send_handshake_response(struct wg_peer *peer) 703 | wg_timers_any_authenticated_packet_sent(peer); 704 | atomic64_set(&peer->last_sent_handshake, 705 | ktime_get_coarse_boottime_ns()); 706 | - wg_socket_send_buffer_to_peer(peer, &packet, 707 | - sizeof(packet), 708 | - HANDSHAKE_DSCP); 709 | + if (wg->advanced_security_config.advanced_security_enabled) { 710 | + wg_socket_send_junked_buffer_to_peer(peer, &packet, 711 | + sizeof(packet), 712 | + HANDSHAKE_DSCP, 713 | + wg->advanced_security_config.response_packet_junk_size); 714 | + } else { 715 | + wg_socket_send_buffer_to_peer(peer, &packet, 716 | + sizeof(packet), 717 | + HANDSHAKE_DSCP); 718 | + } 719 | } 720 | } 721 | } 722 | @@ -117,7 +159,7 @@ void wg_packet_send_handshake_cookie(struct wg_device *wg, 723 | net_dbg_skb_ratelimited("%s: Sending cookie response for denied handshake message for %pISpfsc\n", 724 | wg->dev->name, initiating_skb); 725 | wg_cookie_message_create(&packet, initiating_skb, sender_index, 726 | - &wg->cookie_checker); 727 | + &wg->cookie_checker, wg->advanced_security_config.cookie_packet_magic_header); 728 | wg_socket_send_buffer_as_reply_to_skb(wg, initiating_skb, &packet, 729 | sizeof(packet)); 730 | } 731 | @@ -160,7 +202,7 @@ static unsigned int calculate_skb_padding(struct sk_buff *skb) 732 | return padded_size - last_unit; 733 | } 734 | 735 | -static bool encrypt_packet(struct sk_buff *skb, struct noise_keypair *keypair) 736 | +static bool encrypt_packet(u32 message_type, struct sk_buff *skb, struct noise_keypair *keypair) 737 | { 738 | unsigned int padding_len, plaintext_len, trailer_len; 739 | struct scatterlist sg[MAX_SKB_FRAGS + 8]; 740 | @@ -204,7 +246,7 @@ static bool encrypt_packet(struct sk_buff *skb, struct noise_keypair *keypair) 741 | */ 742 | skb_set_inner_network_header(skb, 0); 743 | header = (struct message_data *)skb_push(skb, sizeof(*header)); 744 | - header->header.type = cpu_to_le32(MESSAGE_DATA); 745 | + header->header.type = cpu_to_le32(message_type); 746 | header->key_idx = keypair->remote_index; 747 | header->counter = cpu_to_le64(PACKET_CB(skb)->nonce); 748 | pskb_put(skb, trailer, trailer_len); 749 | @@ -291,6 +333,7 @@ void wg_packet_encrypt_worker(struct work_struct *work) 750 | struct crypt_queue *queue = container_of(work, struct multicore_worker, 751 | work)->ptr; 752 | struct sk_buff *first, *skb, *next; 753 | + struct wg_device *wg; 754 | simd_context_t simd_context; 755 | 756 | simd_get(&simd_context); 757 | @@ -298,7 +341,10 @@ void wg_packet_encrypt_worker(struct work_struct *work) 758 | enum packet_state state = PACKET_STATE_CRYPTED; 759 | 760 | skb_list_walk_safe(first, skb, next) { 761 | - if (likely(encrypt_packet(skb, 762 | + wg = PACKET_PEER(first)->device; 763 | + 764 | + if (likely(encrypt_packet(wg->advanced_security_config.transport_packet_magic_header, 765 | + skb, 766 | PACKET_CB(first)->keypair, 767 | &simd_context))) { 768 | wg_reset_packet(skb, true); 769 | diff --git socket.c socket.c 770 | index 9e0af93..2dd574f 100644 771 | --- socket.c 772 | +++ socket.c 773 | @@ -200,6 +200,18 @@ int wg_socket_send_buffer_to_peer(struct wg_peer *peer, void *buffer, 774 | return wg_socket_send_skb_to_peer(peer, skb, ds); 775 | } 776 | 777 | +int wg_socket_send_junked_buffer_to_peer(struct wg_peer *peer, void *buffer, 778 | + size_t len, u8 ds, u16 junk_size) 779 | +{ 780 | + int ret; 781 | + void *new_buffer = kzalloc(len + junk_size, GFP_KERNEL); 782 | + get_random_bytes(new_buffer, junk_size); 783 | + memcpy(new_buffer + junk_size, buffer, len); 784 | + ret = wg_socket_send_buffer_to_peer(peer, new_buffer, len + junk_size, ds); 785 | + kfree(new_buffer); 786 | + return ret; 787 | +} 788 | + 789 | int wg_socket_send_buffer_as_reply_to_skb(struct wg_device *wg, 790 | struct sk_buff *in_skb, void *buffer, 791 | size_t len) 792 | diff --git socket.h socket.h 793 | index bab5848..e4e3f96 100644 794 | --- socket.h 795 | +++ socket.h 796 | @@ -16,6 +16,8 @@ void wg_socket_reinit(struct wg_device *wg, struct sock *new4, 797 | struct sock *new6); 798 | int wg_socket_send_buffer_to_peer(struct wg_peer *peer, void *data, 799 | size_t len, u8 ds); 800 | +int wg_socket_send_junked_buffer_to_peer(struct wg_peer *peer, void *data, 801 | + size_t len, u8 ds, u16 junk_size); 802 | int wg_socket_send_skb_to_peer(struct wg_peer *peer, struct sk_buff *skb, 803 | u8 ds); 804 | int wg_socket_send_buffer_as_reply_to_skb(struct wg_device *wg, 805 | diff --git uapi/wireguard.h uapi/wireguard.h 806 | index ae88be1..f6698e8 100644 807 | --- uapi/wireguard.h 808 | +++ uapi/wireguard.h 809 | @@ -131,7 +131,7 @@ 810 | #ifndef _WG_UAPI_WIREGUARD_H 811 | #define _WG_UAPI_WIREGUARD_H 812 | 813 | -#define WG_GENL_NAME "wireguard" 814 | +#define WG_GENL_NAME "amneziawg" 815 | #define WG_GENL_VERSION 1 816 | 817 | #define WG_KEY_LEN 32 818 | @@ -157,6 +157,15 @@ enum wgdevice_attribute { 819 | WGDEVICE_A_LISTEN_PORT, 820 | WGDEVICE_A_FWMARK, 821 | WGDEVICE_A_PEERS, 822 | + WGDEVICE_A_JC, 823 | + WGDEVICE_A_JMIN, 824 | + WGDEVICE_A_JMAX, 825 | + WGDEVICE_A_S1, 826 | + WGDEVICE_A_S2, 827 | + WGDEVICE_A_H1, 828 | + WGDEVICE_A_H2, 829 | + WGDEVICE_A_H3, 830 | + WGDEVICE_A_H4, 831 | __WGDEVICE_A_LAST 832 | }; 833 | #define WGDEVICE_A_MAX (__WGDEVICE_A_LAST - 1) 834 | -------------------------------------------------------------------------------- /kmod-amneziawg/src/Makefile: -------------------------------------------------------------------------------- 1 | # WIREGUARD_VERSION = 1.0.0-awg 2 | 3 | ccflags-y := -D'pr_fmt(fmt)=KBUILD_MODNAME ": " fmt' 4 | # ccflags-y += -D'WIREGUARD_VERSION="$(WIREGUARD_VERSION)"' 5 | # ccflags-y += -DDEBUG 6 | amneziawg-y := main.o 7 | amneziawg-y += noise.o 8 | amneziawg-y += device.o 9 | amneziawg-y += peer.o 10 | amneziawg-y += timers.o 11 | amneziawg-y += queueing.o 12 | amneziawg-y += send.o 13 | amneziawg-y += receive.o 14 | amneziawg-y += socket.o 15 | amneziawg-y += peerlookup.o 16 | amneziawg-y += allowedips.o 17 | amneziawg-y += ratelimiter.o 18 | amneziawg-y += cookie.o 19 | amneziawg-y += netlink.o 20 | obj-m := amneziawg.o 21 | -------------------------------------------------------------------------------- /luci-proto-amneziawg/Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2016 Dan Luedtke 3 | # 4 | # This is free software, licensed under the Apache License, Version 2.0 . 5 | # 6 | 7 | include $(TOPDIR)/rules.mk 8 | 9 | PKG_MAINTAINER:=Amnezia Admin 10 | PKG_VERSION:=0.0.1-1 11 | LUCI_TITLE:=AmneziaWG Web UI 12 | LUCI_DESCRIPTION:=Provides Web UI for AmneziaWG 13 | LUCI_DEPENDS:=+luci-base +amneziawg-tools +ucode 14 | LUCI_PKGARCH:=all 15 | 16 | PKG_PROVIDES:=luci-proto-amneziawg 17 | 18 | include $(TOPDIR)/feeds/luci/luci.mk 19 | 20 | # call BuildPackage - OpenWrt buildroot signature 21 | -------------------------------------------------------------------------------- /luci-proto-amneziawg/htdocs/luci-static/resources/protocol/amneziawg.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | "require fs"; 3 | "require ui"; 4 | "require dom"; 5 | "require uci"; 6 | "require rpc"; 7 | "require form"; 8 | "require network"; 9 | "require validation"; 10 | 11 | var generateKey = rpc.declare({ 12 | object: "luci.amneziawg", 13 | method: "generateKeyPair", 14 | expect: { keys: {} }, 15 | }); 16 | 17 | var getPublicAndPrivateKeyFromPrivate = rpc.declare({ 18 | object: "luci.amneziawg", 19 | method: "getPublicAndPrivateKeyFromPrivate", 20 | params: ["privkey"], 21 | expect: { keys: {} }, 22 | }); 23 | 24 | var generatePsk = rpc.declare({ 25 | object: "luci.amneziawg", 26 | method: "generatePsk", 27 | expect: { psk: "" }, 28 | }); 29 | 30 | var qrIcon = 31 | ''; 32 | 33 | function validateBase64(section_id, value) { 34 | if (value.length == 0) return true; 35 | 36 | if ( 37 | value.length != 44 || 38 | !value.match( 39 | /^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/ 40 | ) 41 | ) 42 | return _("Invalid Base64 key string"); 43 | 44 | if (value[43] != "=") return _("Invalid Base64 key string"); 45 | 46 | return true; 47 | } 48 | 49 | var stubValidator = { 50 | factory: validation, 51 | apply: function (type, value, args) { 52 | if (value != null) this.value = value; 53 | 54 | return validation.types[type].apply(this, args); 55 | }, 56 | assert: function (condition) { 57 | return !!condition; 58 | }, 59 | }; 60 | 61 | function generateDescription(name, texts) { 62 | return E("li", { style: "color: inherit;" }, [ 63 | E("span", name), 64 | E( 65 | "ul", 66 | texts.map(function (text) { 67 | return E("li", { style: "color: inherit;" }, text); 68 | }) 69 | ), 70 | ]); 71 | } 72 | 73 | function invokeQREncode(data, code) { 74 | return fs 75 | .exec_direct("/usr/bin/qrencode", [ 76 | "--inline", 77 | "--8bit", 78 | "--type=SVG", 79 | "--output=-", 80 | "--", 81 | data, 82 | ]) 83 | .then(function (svg) { 84 | code.style.opacity = ""; 85 | dom.content( 86 | code, 87 | Object.assign(E(svg), { style: "width:100%;height:auto" }) 88 | ); 89 | }) 90 | .catch(function (error) { 91 | code.style.opacity = ""; 92 | 93 | if (L.isObject(error) && error.name == "NotFoundError") { 94 | dom.content(code, [ 95 | Object.assign(E(qrIcon), { 96 | style: "width:32px;height:32px;opacity:.2", 97 | }), 98 | E( 99 | "p", 100 | _( 101 | "The %sqrencode%s package is required for generating an QR code image of the configuration." 102 | ).format("", "") 103 | ), 104 | ]); 105 | } else { 106 | dom.content(code, [ 107 | _("Unable to generate QR code: %s").format( 108 | L.isObject(error) ? error.message : error 109 | ), 110 | ]); 111 | } 112 | }); 113 | } 114 | 115 | var cbiKeyPairGenerate = form.DummyValue.extend({ 116 | cfgvalue: function (section_id, value) { 117 | return E( 118 | "button", 119 | { 120 | class: "btn", 121 | click: ui.createHandlerFn( 122 | this, 123 | function (section_id, ev) { 124 | var prv = this.section.getUIElement(section_id, "private_key"), 125 | pub = this.section.getUIElement(section_id, "public_key"), 126 | map = this.map; 127 | 128 | if ( 129 | (prv.getValue() || pub.getValue()) && 130 | !confirm(_("Do you want to replace the current keys?")) 131 | ) 132 | return; 133 | 134 | return generateKey().then(function (keypair) { 135 | prv.setValue(keypair.priv); 136 | pub.setValue(keypair.pub); 137 | map.save(null, true); 138 | }); 139 | }, 140 | section_id 141 | ), 142 | }, 143 | [_("Generate new key pair")] 144 | ); 145 | }, 146 | }); 147 | 148 | function handleWindowDragDropIgnore(ev) { 149 | ev.preventDefault(); 150 | } 151 | 152 | return network.registerProtocol("amneziawg", { 153 | getI18n: function () { 154 | return _("AmneziaWG VPN"); 155 | }, 156 | 157 | getIfname: function () { 158 | return this._ubus("l3_device") || this.sid; 159 | }, 160 | 161 | getOpkgPackage: function () { 162 | return "amneziawg-tools"; 163 | }, 164 | 165 | isFloating: function () { 166 | return true; 167 | }, 168 | 169 | isVirtual: function () { 170 | return true; 171 | }, 172 | 173 | getDevices: function () { 174 | return null; 175 | }, 176 | 177 | containsDevice: function (ifname) { 178 | return network.getIfnameOf(ifname) == this.getIfname(); 179 | }, 180 | 181 | renderFormOptions: function (s) { 182 | var o, ss, ss2; 183 | 184 | // -- general --------------------------------------------------------------------- 185 | 186 | o = s.taboption( 187 | "general", 188 | form.Value, 189 | "private_key", 190 | _("Private Key"), 191 | _("Required. Base64-encoded private key for this interface.") 192 | ); 193 | o.password = true; 194 | o.validate = validateBase64; 195 | o.rmempty = false; 196 | 197 | var serverName = this.getIfname(); 198 | 199 | o = s.taboption( 200 | "general", 201 | form.Value, 202 | "public_key", 203 | _("Public Key"), 204 | _("Base64-encoded public key of this interface for sharing.") 205 | ); 206 | o.rmempty = false; 207 | o.write = function () { 208 | /* write nothing */ 209 | }; 210 | 211 | o.load = function (section_id) { 212 | var privKey = 213 | s.formvalue(section_id, "private_key") || 214 | uci.get("network", section_id, "private_key"); 215 | 216 | return getPublicAndPrivateKeyFromPrivate(privKey).then( 217 | function (keypair) { 218 | return keypair.pub || ""; 219 | }, 220 | function (error) { 221 | return _("Error getting PublicKey"); 222 | }, 223 | this 224 | ); 225 | }; 226 | 227 | s.taboption("general", cbiKeyPairGenerate, "_gen_server_keypair", " "); 228 | 229 | o = s.taboption( 230 | "general", 231 | form.Value, 232 | "listen_port", 233 | _("Listen Port"), 234 | _("Optional. UDP port used for outgoing and incoming packets.") 235 | ); 236 | o.datatype = "port"; 237 | o.placeholder = _("random"); 238 | o.optional = true; 239 | 240 | o = s.taboption( 241 | "general", 242 | form.DynamicList, 243 | "addresses", 244 | _("IP Addresses"), 245 | _("Recommended. IP addresses of the AmneziaWG interface.") 246 | ); 247 | o.datatype = "ipaddr"; 248 | o.optional = true; 249 | 250 | o = s.taboption( 251 | "general", 252 | form.Flag, 253 | "nohostroute", 254 | _("No Host Routes"), 255 | _("Optional. Do not create host routes to peers.") 256 | ); 257 | o.optional = true; 258 | 259 | o = s.taboption( 260 | "general", 261 | form.Button, 262 | "_import", 263 | _("Import configuration"), 264 | _("Imports settings from an existing AmneziaWG configuration file") 265 | ); 266 | o.inputtitle = _("Load configuration…"); 267 | o.onclick = function () { 268 | return ss.handleConfigImport("full"); 269 | }; 270 | 271 | // -- advanced -------------------------------------------------------------------- 272 | 273 | o = s.taboption( 274 | "advanced", 275 | form.Value, 276 | "mtu", 277 | _("MTU"), 278 | _("Optional. Maximum Transmission Unit of tunnel interface.") 279 | ); 280 | o.datatype = "range(0,8940)"; 281 | o.placeholder = "1420"; 282 | o.optional = true; 283 | 284 | o = s.taboption( 285 | "advanced", 286 | form.Value, 287 | "fwmark", 288 | _("Firewall Mark"), 289 | _( 290 | "Optional. 32-bit mark for outgoing encrypted packets. Enter value in hex, starting with %s." 291 | ).format("0x") 292 | ); 293 | o.optional = true; 294 | o.validate = function (section_id, value) { 295 | if (value.length > 0 && !value.match(/^0x[a-fA-F0-9]{1,8}$/)) 296 | return _("Invalid hexadecimal value"); 297 | 298 | return true; 299 | }; 300 | 301 | // AmneziaWG 302 | 303 | try { 304 | s.tab( 305 | "amneziawg", 306 | _("AmneziaWG Settings"), 307 | _( 308 | "Further information about AmneziaWG interfaces and peers at %s." 309 | ).format("amnezia.org") 310 | ); 311 | } catch (e) {} 312 | 313 | o = s.taboption( 314 | "amneziawg", 315 | form.Value, 316 | "awg_jc", 317 | _("Jc"), 318 | _("Junk packet count.") 319 | ); 320 | o.datatype = "uinteger"; 321 | o.optional = true; 322 | 323 | o = s.taboption( 324 | "amneziawg", 325 | form.Value, 326 | "awg_jmin", 327 | _("Jmin"), 328 | _("Junk packet minimum size.") 329 | ); 330 | o.datatype = "uinteger"; 331 | o.optional = true; 332 | 333 | o = s.taboption( 334 | "amneziawg", 335 | form.Value, 336 | "awg_jmax", 337 | _("Jmax"), 338 | _("Junk packet maximum size.") 339 | ); 340 | o.datatype = "uinteger"; 341 | o.optional = true; 342 | 343 | o = s.taboption( 344 | "amneziawg", 345 | form.Value, 346 | "awg_s1", 347 | _("S1"), 348 | _("Handshake initiation packet junk header size.") 349 | ); 350 | o.datatype = "uinteger"; 351 | o.optional = true; 352 | 353 | o = s.taboption( 354 | "amneziawg", 355 | form.Value, 356 | "awg_s2", 357 | _("S2"), 358 | _("Handshake response packet junk header size.") 359 | ); 360 | o.datatype = "uinteger"; 361 | o.optional = true; 362 | 363 | o = s.taboption( 364 | "amneziawg", 365 | form.Value, 366 | "awg_h1", 367 | _("H1"), 368 | _("Handshake initiation packet type header.") 369 | ); 370 | o.datatype = "uinteger"; 371 | o.optional = true; 372 | 373 | o = s.taboption( 374 | "amneziawg", 375 | form.Value, 376 | "awg_h2", 377 | _("H2"), 378 | _("Handshake response packet type header.") 379 | ); 380 | o.datatype = "uinteger"; 381 | o.optional = true; 382 | 383 | o = s.taboption( 384 | "amneziawg", 385 | form.Value, 386 | "awg_h3", 387 | _("H3"), 388 | _("Handshake cookie packet type header.") 389 | ); 390 | o.datatype = "uinteger"; 391 | o.optional = true; 392 | 393 | o = s.taboption( 394 | "amneziawg", 395 | form.Value, 396 | "awg_h4", 397 | _("H4"), 398 | _("Transport packet type header.") 399 | ); 400 | o.datatype = "uinteger"; 401 | o.optional = true; 402 | 403 | // -- peers ----------------------------------------------------------------------- 404 | 405 | try { 406 | s.tab( 407 | "peers", 408 | _("Peers"), 409 | _( 410 | "Further information about AmneziaWG interfaces and peers at %s." 411 | ).format("amnezia.org") 412 | ); 413 | } catch (e) {} 414 | 415 | o = s.taboption( 416 | "peers", 417 | form.SectionValue, 418 | "_peers", 419 | form.GridSection, 420 | "amneziawg_%s".format(s.section) 421 | ); 422 | o.depends("proto", "amneziawg"); 423 | 424 | ss = o.subsection; 425 | ss.anonymous = true; 426 | ss.addremove = true; 427 | ss.addbtntitle = _("Add peer"); 428 | ss.nodescriptions = true; 429 | ss.modaltitle = _("Edit peer"); 430 | 431 | ss.handleDragConfig = function (ev) { 432 | ev.stopPropagation(); 433 | ev.preventDefault(); 434 | ev.dataTransfer.dropEffect = "copy"; 435 | }; 436 | 437 | ss.handleDropConfig = function (mode, ev) { 438 | var file = ev.dataTransfer.files[0], 439 | nodes = ev.currentTarget, 440 | input = nodes.querySelector("textarea"), 441 | reader = new FileReader(); 442 | 443 | if (file) { 444 | reader.onload = function (rev) { 445 | input.value = rev.target.result.trim(); 446 | ss.handleApplyConfig(mode, nodes, file.name, ev); 447 | }; 448 | 449 | reader.readAsText(file); 450 | } 451 | 452 | ev.stopPropagation(); 453 | ev.preventDefault(); 454 | }; 455 | 456 | ss.parseConfig = function (data) { 457 | var lines = String(data).split(/(\r?\n)+/), 458 | section = null, 459 | config = { peers: [] }, 460 | s; 461 | 462 | for (var i = 0; i < lines.length; i++) { 463 | var line = lines[i].replace(/#.*$/, "").trim(); 464 | 465 | if (line.match(/^\[(\w+)\]$/)) { 466 | section = RegExp.$1.toLowerCase(); 467 | 468 | if (section == "peer") config.peers.push((s = {})); 469 | else s = config; 470 | } else if (section && line.match(/^(\w+)\s*=\s*(.+)$/)) { 471 | var key = RegExp.$1, 472 | val = RegExp.$2.trim(); 473 | 474 | if (val.length) s[section + "_" + key.toLowerCase()] = val; 475 | } 476 | } 477 | 478 | if (config.interface_address) { 479 | config.interface_address = config.interface_address.split(/[, ]+/); 480 | 481 | for (var i = 0; i < config.interface_address.length; i++) 482 | if (!stubValidator.apply("ipaddr", config.interface_address[i])) 483 | return _("Address setting is invalid"); 484 | } 485 | 486 | if (config.interface_dns) { 487 | config.interface_dns = config.interface_dns.split(/[, ]+/); 488 | 489 | for (var i = 0; i < config.interface_dns.length; i++) 490 | if ( 491 | !stubValidator.apply("ipaddr", config.interface_dns[i], ["nomask"]) 492 | ) 493 | return _("DNS setting is invalid"); 494 | } 495 | 496 | if ( 497 | !config.interface_privatekey || 498 | validateBase64(null, config.interface_privatekey) !== true 499 | ) 500 | return _("PrivateKey setting is missing or invalid"); 501 | 502 | if (!stubValidator.apply("port", config.interface_listenport || "0")) 503 | return _("ListenPort setting is invalid"); 504 | 505 | for (var i = 0; i < config.peers.length; i++) { 506 | var pconf = config.peers[i]; 507 | 508 | if ( 509 | pconf.peer_publickey != null && 510 | validateBase64(null, pconf.peer_publickey) !== true 511 | ) 512 | return _("PublicKey setting is invalid"); 513 | 514 | if ( 515 | pconf.peer_presharedkey != null && 516 | validateBase64(null, pconf.peer_presharedkey) !== true 517 | ) 518 | return _("PresharedKey setting is invalid"); 519 | 520 | if (pconf.peer_allowedips) { 521 | pconf.peer_allowedips = pconf.peer_allowedips.split(/[, ]+/); 522 | 523 | for (var j = 0; j < pconf.peer_allowedips.length; j++) 524 | if (!stubValidator.apply("ipaddr", pconf.peer_allowedips[j])) 525 | return _("AllowedIPs setting is invalid"); 526 | } else { 527 | pconf.peer_allowedips = ["0.0.0.0/0", "::/0"]; 528 | } 529 | 530 | if (pconf.peer_endpoint) { 531 | var host_port = 532 | pconf.peer_endpoint.match(/^\[([a-fA-F0-9:]+)\]:(\d+)$/) || 533 | pconf.peer_endpoint.match(/^(.+):(\d+)$/); 534 | 535 | if ( 536 | !host_port || 537 | !stubValidator.apply("host", host_port[1]) || 538 | !stubValidator.apply("port", host_port[2]) 539 | ) 540 | return _("Endpoint setting is invalid"); 541 | 542 | pconf.peer_endpoint = [host_port[1], host_port[2]]; 543 | } 544 | 545 | if ( 546 | pconf.peer_persistentkeepalive == "off" || 547 | pconf.peer_persistentkeepalive == "0" 548 | ) 549 | delete pconf.peer_persistentkeepalive; 550 | 551 | if (!stubValidator.apply("port", pconf.peer_persistentkeepalive || "0")) 552 | return _("PersistentKeepAlive setting is invalid"); 553 | } 554 | 555 | return config; 556 | }; 557 | 558 | ss.handleApplyConfig = function (mode, nodes, comment, ev) { 559 | var input = nodes.querySelector("textarea").value, 560 | error = nodes.querySelector(".alert-message"), 561 | cancel = nodes.nextElementSibling.querySelector(".btn"), 562 | config = this.parseConfig(input); 563 | 564 | if (typeof config == "string") { 565 | error.firstChild.data = _("Cannot parse configuration: %s").format( 566 | config 567 | ); 568 | error.style.display = "block"; 569 | return; 570 | } 571 | 572 | if (mode == "full") { 573 | var prv = s.formvalue(s.section, "private_key"); 574 | 575 | if ( 576 | prv && 577 | prv != config.interface_privatekey && 578 | !confirm( 579 | _("Overwrite the current settings with the imported configuration?") 580 | ) 581 | ) 582 | return; 583 | 584 | return getPublicAndPrivateKeyFromPrivate(config.interface_privatekey) 585 | .then(function (keypair) { 586 | s.getOption("private_key") 587 | .getUIElement(s.section) 588 | .setValue(keypair.priv); 589 | s.getOption("public_key") 590 | .getUIElement(s.section) 591 | .setValue(keypair.pub); 592 | s.getOption("listen_port") 593 | .getUIElement(s.section) 594 | .setValue(config.interface_listenport || ""); 595 | s.getOption("addresses") 596 | .getUIElement(s.section) 597 | .setValue(config.interface_address); 598 | s.getOption("awg_jc") 599 | .getUIElement(s.section) 600 | .setValue(config.awg_jc); 601 | s.getOption("awg_jmin") 602 | .getUIElement(s.section) 603 | .setValue(config.awg_jmin); 604 | s.getOption("awg_jmax") 605 | .getUIElement(s.section) 606 | .setValue(config.awg_jmax); 607 | s.getOption("awg_s1") 608 | .getUIElement(s.section) 609 | .setValue(config.awg_s1); 610 | s.getOption("awg_s2") 611 | .getUIElement(s.section) 612 | .setValue(config.awg_s2); 613 | s.getOption("awg_h1") 614 | .getUIElement(s.section) 615 | .setValue(config.awg_h1); 616 | s.getOption("awg_h2") 617 | .getUIElement(s.section) 618 | .setValue(config.awg_h2); 619 | s.getOption("awg_h3") 620 | .getUIElement(s.section) 621 | .setValue(config.awg_h3); 622 | s.getOption("awg_h4") 623 | .getUIElement(s.section) 624 | .setValue(config.awg_h4); 625 | 626 | if (config.interface_dns) 627 | s.getOption("dns") 628 | .getUIElement(s.section) 629 | .setValue(config.interface_dns); 630 | 631 | for (var i = 0; i < config.peers.length; i++) { 632 | var pconf = config.peers[i]; 633 | var sid = uci.add("network", "amneziawg_" + s.section); 634 | 635 | uci.sections( 636 | "network", 637 | "amneziawg_" + s.section, 638 | function (peer) { 639 | if (peer.public_key == pconf.peer_publickey) 640 | uci.remove("network", peer[".name"]); 641 | } 642 | ); 643 | 644 | uci.set( 645 | "network", 646 | sid, 647 | "description", 648 | comment || _("Imported peer configuration") 649 | ); 650 | uci.set("network", sid, "public_key", pconf.peer_publickey); 651 | uci.set("network", sid, "preshared_key", pconf.peer_presharedkey); 652 | uci.set("network", sid, "allowed_ips", pconf.peer_allowedips); 653 | uci.set( 654 | "network", 655 | sid, 656 | "persistent_keepalive", 657 | pconf.peer_persistentkeepalive 658 | ); 659 | 660 | if (pconf.peer_endpoint) { 661 | uci.set( 662 | "network", 663 | sid, 664 | "endpoint_host", 665 | pconf.peer_endpoint[0] 666 | ); 667 | uci.set( 668 | "network", 669 | sid, 670 | "endpoint_port", 671 | pconf.peer_endpoint[1] 672 | ); 673 | } 674 | } 675 | 676 | return s.map.save(null, true); 677 | }) 678 | .then(function () { 679 | cancel.click(); 680 | }); 681 | } else { 682 | return getPublicAndPrivateKeyFromPrivate(config.interface_privatekey) 683 | .then(function (keypair) { 684 | var sid = uci.add("network", "amneziawg_" + s.section); 685 | var pub = s.formvalue(s.section, "public_key"); 686 | 687 | uci.sections("network", "amneziawg_" + s.section, function (peer) { 688 | if (peer.public_key == keypair.pub) 689 | uci.remove("network", peer[".name"]); 690 | }); 691 | 692 | uci.set( 693 | "network", 694 | sid, 695 | "description", 696 | comment || _("Imported peer configuration") 697 | ); 698 | uci.set("network", sid, "public_key", keypair.pub); 699 | uci.set("network", sid, "private_key", keypair.priv); 700 | 701 | for (var i = 0; i < config.peers.length; i++) { 702 | var pconf = config.peers[i]; 703 | 704 | if (pconf.peer_publickey == pub) { 705 | uci.set( 706 | "network", 707 | sid, 708 | "preshared_key", 709 | pconf.peer_presharedkey 710 | ); 711 | uci.set("network", sid, "allowed_ips", pconf.peer_allowedips); 712 | uci.set( 713 | "network", 714 | sid, 715 | "persistent_keepalive", 716 | pconf.peer_persistentkeepalive 717 | ); 718 | break; 719 | } 720 | } 721 | 722 | return s.map.save(null, true); 723 | }) 724 | .then(function () { 725 | cancel.click(); 726 | }); 727 | } 728 | }; 729 | 730 | ss.handleConfigImport = function (mode) { 731 | var mapNode = ss.getActiveModalMap(), 732 | headNode = mapNode.parentNode.querySelector("h4"), 733 | parent = this.map; 734 | 735 | var nodes = E( 736 | "div", 737 | { 738 | dragover: this.handleDragConfig, 739 | drop: this.handleDropConfig.bind(this, mode), 740 | }, 741 | [ 742 | E( 743 | [], 744 | mode == "full" 745 | ? [ 746 | E( 747 | "p", 748 | _( 749 | "Drag or paste a valid %s file below to configure the local AmneziaWG interface." 750 | ).format("*.conf") 751 | ), 752 | ] 753 | : [ 754 | E( 755 | "p", 756 | _( 757 | "Paste or drag a AmneziaWG configuration (commonly %s) from another system below to create a matching peer entry allowing that system to connect to the local AmneziaWG interface." 758 | ).format("wg0.conf") 759 | ), 760 | E( 761 | "p", 762 | _( 763 | "To fully configure the local AmneziaWG interface from an existing (e.g. provider supplied) configuration file, use the %sconfiguration import%s instead." 764 | ).format( 765 | '', 766 | "" 767 | ) 768 | ), 769 | ] 770 | ), 771 | E("p", [ 772 | E("textarea", { 773 | placeholder: 774 | mode == "full" 775 | ? _("Paste or drag supplied AmneziaWG configuration file…") 776 | : _( 777 | "Paste or drag AmneziaWG peer configuration (%s) file…" 778 | ).format("wg0.conf"), 779 | style: "height:5em;width:100%; white-space:pre", 780 | }), 781 | ]), 782 | E( 783 | "div", 784 | { 785 | class: "alert-message", 786 | style: "display:none", 787 | }, 788 | [""] 789 | ), 790 | ] 791 | ); 792 | 793 | var cancelFn = function () { 794 | nodes.parentNode.removeChild(nodes.nextSibling); 795 | nodes.parentNode.removeChild(nodes); 796 | mapNode.classList.remove("hidden"); 797 | mapNode.nextSibling.classList.remove("hidden"); 798 | headNode.removeChild(headNode.lastChild); 799 | window.removeEventListener("dragover", handleWindowDragDropIgnore); 800 | window.removeEventListener("drop", handleWindowDragDropIgnore); 801 | }; 802 | 803 | var a = nodes.querySelector("a.full-import"); 804 | 805 | if (a) { 806 | a.addEventListener( 807 | "click", 808 | ui.createHandlerFn(this, function (mode) { 809 | cancelFn(); 810 | this.handleConfigImport("full"); 811 | }) 812 | ); 813 | } 814 | 815 | mapNode.classList.add("hidden"); 816 | mapNode.nextElementSibling.classList.add("hidden"); 817 | 818 | headNode.appendChild( 819 | E("span", [ 820 | " » ", 821 | mode == "full" ? _("Import configuration") : _("Import as peer"), 822 | ]) 823 | ); 824 | mapNode.parentNode.appendChild( 825 | E( 826 | [], 827 | [ 828 | nodes, 829 | E( 830 | "div", 831 | { 832 | class: "right", 833 | }, 834 | [ 835 | E( 836 | "button", 837 | { 838 | class: "btn", 839 | click: cancelFn, 840 | }, 841 | [_("Cancel")] 842 | ), 843 | " ", 844 | E( 845 | "button", 846 | { 847 | class: "btn primary", 848 | click: ui.createHandlerFn( 849 | this, 850 | "handleApplyConfig", 851 | mode, 852 | nodes, 853 | null 854 | ), 855 | }, 856 | [_("Import settings")] 857 | ), 858 | ] 859 | ), 860 | ] 861 | ) 862 | ); 863 | 864 | window.addEventListener("dragover", handleWindowDragDropIgnore); 865 | window.addEventListener("drop", handleWindowDragDropIgnore); 866 | }; 867 | 868 | ss.renderSectionAdd = function (/* ... */) { 869 | var nodes = this.super("renderSectionAdd", arguments); 870 | 871 | nodes.appendChild( 872 | E( 873 | "button", 874 | { 875 | class: "btn", 876 | click: ui.createHandlerFn(this, "handleConfigImport", "peer"), 877 | }, 878 | [_("Import configuration as peer…")] 879 | ) 880 | ); 881 | 882 | return nodes; 883 | }; 884 | 885 | ss.renderSectionPlaceholder = function () { 886 | return E("em", _("No peers defined yet.")); 887 | }; 888 | 889 | o = ss.option( 890 | form.Flag, 891 | "disabled", 892 | _("Peer disabled"), 893 | _("Enable / Disable peer. Restart amneziawg interface to apply changes.") 894 | ); 895 | o.modalonly = true; 896 | o.optional = true; 897 | 898 | o = ss.option( 899 | form.Value, 900 | "description", 901 | _("Description"), 902 | _("Optional. Description of peer.") 903 | ); 904 | o.placeholder = "My Peer"; 905 | o.datatype = "string"; 906 | o.optional = true; 907 | o.width = "30%"; 908 | o.textvalue = function (section_id) { 909 | var dis = ss.getOption("disabled"), 910 | pub = ss.getOption("public_key"), 911 | prv = ss.getOption("private_key"), 912 | psk = ss.getOption("preshared_key"), 913 | name = this.cfgvalue(section_id), 914 | key = pub.cfgvalue(section_id); 915 | 916 | var desc = [ 917 | E("p", [name ? E("span", [name]) : E("em", [_("Untitled peer")])]), 918 | ]; 919 | 920 | if (dis.cfgvalue(section_id) == "1") 921 | desc.push( 922 | E( 923 | "span", 924 | { 925 | class: "ifacebadge", 926 | "data-tooltip": _("AmneziaWG peer is disabled"), 927 | }, 928 | [ 929 | E("em", [ 930 | _( 931 | "Disabled", 932 | "Label indicating that AmneziaWG peer is disabled" 933 | ), 934 | ]), 935 | ] 936 | ), 937 | " " 938 | ); 939 | 940 | if (!key || !pub.isValid(section_id)) { 941 | desc.push( 942 | E( 943 | "span", 944 | { 945 | class: "ifacebadge", 946 | "data-tooltip": _("Public key is missing"), 947 | }, 948 | [ 949 | E("em", [ 950 | _( 951 | "Key missing", 952 | "Label indicating that AmneziaWG peer lacks public key" 953 | ), 954 | ]), 955 | ] 956 | ) 957 | ); 958 | } else { 959 | desc.push( 960 | E( 961 | "span", 962 | { 963 | class: "ifacebadge", 964 | "data-tooltip": _( 965 | "Public key: %h", 966 | "Tooltip displaying full AmneziaWG peer public key" 967 | ).format(key), 968 | }, 969 | [E("code", [key.replace(/^(.{5}).+(.{6})$/, "$1…$2")])] 970 | ), 971 | " ", 972 | prv.cfgvalue(section_id) && prv.isValid(section_id) 973 | ? E( 974 | "span", 975 | { 976 | class: "ifacebadge", 977 | "data-tooltip": _("Private key present"), 978 | }, 979 | [ 980 | _( 981 | "Private", 982 | "Label indicating that AmneziaWG peer private key is stored" 983 | ), 984 | ] 985 | ) 986 | : "", 987 | " ", 988 | psk.cfgvalue(section_id) && psk.isValid(section_id) 989 | ? E( 990 | "span", 991 | { 992 | class: "ifacebadge", 993 | "data-tooltip": _("Preshared key in use"), 994 | }, 995 | [_("PSK", "Label indicating that AmneziaWG peer uses a PSK")] 996 | ) 997 | : "" 998 | ); 999 | } 1000 | 1001 | return E([], desc); 1002 | }; 1003 | 1004 | function handleKeyChange(ev, section_id, value) { 1005 | var prv = this.section.getUIElement(section_id, "private_key"), 1006 | btn = this.map.findElement(".btn.qr-code"); 1007 | 1008 | btn.disabled = !prv.isValid() || !prv.getValue(); 1009 | } 1010 | 1011 | o = ss.option( 1012 | form.Value, 1013 | "public_key", 1014 | _("Public Key"), 1015 | _("Required. Public key of the AmneziaWG peer.") 1016 | ); 1017 | o.modalonly = true; 1018 | o.validate = validateBase64; 1019 | o.onchange = handleKeyChange; 1020 | 1021 | o = ss.option( 1022 | form.Value, 1023 | "private_key", 1024 | _("Private Key"), 1025 | _( 1026 | "Optional. Private key of the AmneziaWG peer. The key is not required for establishing a connection but allows generating a peer configuration or QR code if available. It can be removed after the configuration has been exported." 1027 | ) 1028 | ); 1029 | o.modalonly = true; 1030 | o.validate = validateBase64; 1031 | o.onchange = handleKeyChange; 1032 | o.password = true; 1033 | 1034 | o = ss.option(cbiKeyPairGenerate, "_gen_peer_keypair", " "); 1035 | o.modalonly = true; 1036 | 1037 | o = ss.option( 1038 | form.Value, 1039 | "preshared_key", 1040 | _("Preshared Key"), 1041 | _( 1042 | "Optional. Base64-encoded preshared key. Adds in an additional layer of symmetric-key cryptography for post-quantum resistance." 1043 | ) 1044 | ); 1045 | o.modalonly = true; 1046 | o.validate = validateBase64; 1047 | o.password = true; 1048 | 1049 | o = ss.option(form.DummyValue, "_gen_psk", " "); 1050 | o.modalonly = true; 1051 | o.cfgvalue = function (section_id, value) { 1052 | return E( 1053 | "button", 1054 | { 1055 | class: "btn", 1056 | click: ui.createHandlerFn( 1057 | this, 1058 | function (section_id, ev) { 1059 | var psk = this.section.getUIElement(section_id, "preshared_key"), 1060 | map = this.map; 1061 | 1062 | if ( 1063 | psk.getValue() && 1064 | !confirm(_("Do you want to replace the current PSK?")) 1065 | ) 1066 | return; 1067 | 1068 | return generatePsk().then(function (key) { 1069 | psk.setValue(key); 1070 | map.save(null, true); 1071 | }); 1072 | }, 1073 | section_id 1074 | ), 1075 | }, 1076 | [_("Generate preshared key")] 1077 | ); 1078 | }; 1079 | 1080 | o = ss.option( 1081 | form.DynamicList, 1082 | "allowed_ips", 1083 | _("Allowed IPs"), 1084 | _( 1085 | "Optional. IP addresses and prefixes that this peer is allowed to use inside the tunnel. Usually the peer's tunnel IP addresses and the networks the peer routes through the tunnel." 1086 | ) 1087 | ); 1088 | o.datatype = "ipaddr"; 1089 | o.textvalue = function (section_id) { 1090 | var ips = L.toArray(this.cfgvalue(section_id)), 1091 | list = []; 1092 | 1093 | for (var i = 0; i < ips.length; i++) { 1094 | if (i > 7) { 1095 | list.push( 1096 | E( 1097 | "em", 1098 | { 1099 | class: "ifacebadge cbi-tooltip-container", 1100 | }, 1101 | [ 1102 | _( 1103 | "+ %d more", 1104 | "Label indicating further amount of allowed ips" 1105 | ).format(ips.length - i), 1106 | E( 1107 | "span", 1108 | { 1109 | class: "cbi-tooltip", 1110 | }, 1111 | [ 1112 | E( 1113 | "ul", 1114 | ips.map(function (ip) { 1115 | return E("li", [ 1116 | E("span", { class: "ifacebadge" }, [ip]), 1117 | ]); 1118 | }) 1119 | ), 1120 | ] 1121 | ), 1122 | ] 1123 | ) 1124 | ); 1125 | 1126 | break; 1127 | } 1128 | 1129 | list.push(E("span", { class: "ifacebadge" }, [ips[i]])); 1130 | } 1131 | 1132 | if (!list.length) list.push("*"); 1133 | 1134 | return E( 1135 | "span", 1136 | { style: "display:inline-flex;flex-wrap:wrap;gap:.125em" }, 1137 | list 1138 | ); 1139 | }; 1140 | 1141 | o = ss.option( 1142 | form.Flag, 1143 | "route_allowed_ips", 1144 | _("Route Allowed IPs"), 1145 | _("Optional. Create routes for Allowed IPs for this peer.") 1146 | ); 1147 | o.modalonly = true; 1148 | 1149 | o = ss.option( 1150 | form.Value, 1151 | "endpoint_host", 1152 | _("Endpoint Host"), 1153 | _( 1154 | "Optional. Host of peer. Names are resolved prior to bringing up the interface." 1155 | ) 1156 | ); 1157 | o.placeholder = "vpn.example.com"; 1158 | o.datatype = "host"; 1159 | o.textvalue = function (section_id) { 1160 | var host = this.cfgvalue(section_id), 1161 | port = this.section.cfgvalue(section_id, "endpoint_port"); 1162 | 1163 | return host && port 1164 | ? "%h:%d".format(host, port) 1165 | : host 1166 | ? "%h:*".format(host) 1167 | : port 1168 | ? "*:%d".format(port) 1169 | : "*"; 1170 | }; 1171 | 1172 | o = ss.option( 1173 | form.Value, 1174 | "endpoint_port", 1175 | _("Endpoint Port"), 1176 | _("Optional. Port of peer.") 1177 | ); 1178 | o.modalonly = true; 1179 | o.placeholder = "51820"; 1180 | o.datatype = "port"; 1181 | 1182 | o = ss.option( 1183 | form.Value, 1184 | "persistent_keepalive", 1185 | _("Persistent Keep Alive"), 1186 | _( 1187 | "Optional. Seconds between keep alive messages. Default is 0 (disabled). Recommended value if this device is behind a NAT is 25." 1188 | ) 1189 | ); 1190 | o.modalonly = true; 1191 | o.datatype = "range(0,65535)"; 1192 | o.placeholder = "0"; 1193 | 1194 | o = ss.option( 1195 | form.DummyValue, 1196 | "_keyops", 1197 | _("Configuration Export"), 1198 | _("Generates a configuration suitable for import on a AmneziaWG peer") 1199 | ); 1200 | 1201 | o.modalonly = true; 1202 | 1203 | o.createPeerConfig = function (section_id, endpoint, ips) { 1204 | var pub = s.formvalue(s.section, "public_key"), 1205 | port = s.formvalue(s.section, "listen_port") || "51820", 1206 | jc = s.formvalue; 1207 | (prv = this.section.formvalue(section_id, "private_key")), 1208 | (psk = this.section.formvalue(section_id, "preshared_key")), 1209 | (eport = this.section.formvalue(section_id, "endpoint_port")), 1210 | (keep = this.section.formvalue(section_id, "persistent_keepalive")); 1211 | 1212 | // If endpoint is IPv6 we must escape it with [] 1213 | if (endpoint.indexOf(":") > 0) { 1214 | endpoint = "[" + endpoint + "]"; 1215 | } 1216 | 1217 | return [ 1218 | "[Interface]", 1219 | "PrivateKey = " + prv, 1220 | eport ? "ListenPort = " + eport : "# ListenPort not defined", 1221 | "", 1222 | "[Peer]", 1223 | "PublicKey = " + pub, 1224 | psk ? "PresharedKey = " + psk : "# PresharedKey not used", 1225 | ips && ips.length 1226 | ? "AllowedIPs = " + ips.join(", ") 1227 | : "# AllowedIPs not defined", 1228 | endpoint 1229 | ? "Endpoint = " + endpoint + ":" + port 1230 | : "# Endpoint not defined", 1231 | keep 1232 | ? "PersistentKeepAlive = " + keep 1233 | : "# PersistentKeepAlive not defined", 1234 | ].join("\n"); 1235 | }; 1236 | 1237 | o.handleGenerateQR = function (section_id, ev) { 1238 | var mapNode = ss.getActiveModalMap(), 1239 | headNode = mapNode.parentNode.querySelector("h4"), 1240 | configGenerator = this.createPeerConfig.bind(this, section_id), 1241 | parent = this.map; 1242 | 1243 | return Promise.all([ 1244 | network.getWANNetworks(), 1245 | network.getWAN6Networks(), 1246 | L.resolveDefault(uci.load("ddns")), 1247 | L.resolveDefault(uci.load("system")), 1248 | parent.save(null, true), 1249 | ]).then(function (data) { 1250 | var hostnames = []; 1251 | 1252 | uci.sections("ddns", "service", function (s) { 1253 | if (typeof s.lookup_host == "string" && s.enabled == "1") 1254 | hostnames.push(s.lookup_host); 1255 | }); 1256 | 1257 | uci.sections("system", "system", function (s) { 1258 | if (typeof s.hostname == "string" && s.hostname.indexOf(".") > 0) 1259 | hostnames.push(s.hostname); 1260 | }); 1261 | 1262 | for (var i = 0; i < data[0].length; i++) 1263 | hostnames.push.apply( 1264 | hostnames, 1265 | data[0][i].getIPAddrs().map(function (ip) { 1266 | return ip.split("/")[0]; 1267 | }) 1268 | ); 1269 | 1270 | for (var i = 0; i < data[1].length; i++) 1271 | hostnames.push.apply( 1272 | hostnames, 1273 | data[1][i].getIP6Addrs().map(function (ip) { 1274 | return ip.split("/")[0]; 1275 | }) 1276 | ); 1277 | 1278 | var ips = ["0.0.0.0/0", "::/0"]; 1279 | 1280 | var qrm, qrs, qro; 1281 | 1282 | qrm = new form.JSONMap( 1283 | { config: { endpoint: hostnames[0], allowed_ips: ips } }, 1284 | null, 1285 | _( 1286 | "The generated configuration can be imported into a AmneziaWG client application to set up a connection towards this device." 1287 | ) 1288 | ); 1289 | qrm.parent = parent; 1290 | 1291 | qrs = qrm.section(form.NamedSection, "config"); 1292 | 1293 | function handleConfigChange(ev, section_id, value) { 1294 | var code = this.map.findElement(".qr-code"), 1295 | conf = this.map.findElement(".client-config"), 1296 | endpoint = this.section.getUIElement(section_id, "endpoint"), 1297 | ips = this.section.getUIElement(section_id, "allowed_ips"); 1298 | 1299 | if (this.isValid(section_id)) { 1300 | conf.firstChild.data = configGenerator( 1301 | endpoint.getValue(), 1302 | ips.getValue() 1303 | ); 1304 | code.style.opacity = ".5"; 1305 | 1306 | invokeQREncode(conf.firstChild.data, code); 1307 | } 1308 | } 1309 | 1310 | qro = qrs.option( 1311 | form.Value, 1312 | "endpoint", 1313 | _("Connection endpoint"), 1314 | _( 1315 | "The public hostname or IP address of this system the peer should connect to. This usually is a static public IP address, a static hostname or a DDNS domain." 1316 | ) 1317 | ); 1318 | qro.datatype = "or(ipaddr,hostname)"; 1319 | hostnames.forEach(function (hostname) { 1320 | qro.value(hostname); 1321 | }); 1322 | qro.onchange = handleConfigChange; 1323 | 1324 | qro = qrs.option( 1325 | form.DynamicList, 1326 | "allowed_ips", 1327 | _("Allowed IPs"), 1328 | _( 1329 | "IP addresses that are allowed inside the tunnel. The peer will accept tunnelled packets with source IP addresses matching this list and route back packets with matching destination IP." 1330 | ) 1331 | ); 1332 | qro.datatype = "ipaddr"; 1333 | qro.default = ips; 1334 | ips.forEach(function (ip) { 1335 | qro.value(ip); 1336 | }); 1337 | qro.onchange = handleConfigChange; 1338 | 1339 | qro = qrs.option(form.DummyValue, "output"); 1340 | qro.renderWidget = function () { 1341 | var peer_config = configGenerator(hostnames[0], ips); 1342 | 1343 | var node = E( 1344 | "div", 1345 | { 1346 | style: 1347 | "display:flex;flex-wrap:wrap;align-items:center;gap:.5em;width:100%", 1348 | }, 1349 | [ 1350 | E( 1351 | "div", 1352 | { 1353 | class: "qr-code", 1354 | style: "width:320px;flex:0 1 320px;text-align:center", 1355 | }, 1356 | [E("em", { class: "spinning" }, [_("Generating QR code…")])] 1357 | ), 1358 | E( 1359 | "pre", 1360 | { 1361 | class: "client-config", 1362 | style: "flex:1;white-space:pre;overflow:auto", 1363 | click: function (ev) { 1364 | var sel = window.getSelection(), 1365 | range = document.createRange(); 1366 | 1367 | range.selectNodeContents(ev.currentTarget); 1368 | 1369 | sel.removeAllRanges(); 1370 | sel.addRange(range); 1371 | }, 1372 | }, 1373 | [peer_config] 1374 | ), 1375 | ] 1376 | ); 1377 | 1378 | invokeQREncode(peer_config, node.firstChild); 1379 | 1380 | return node; 1381 | }; 1382 | 1383 | return qrm.render().then(function (nodes) { 1384 | mapNode.classList.add("hidden"); 1385 | mapNode.nextElementSibling.classList.add("hidden"); 1386 | 1387 | headNode.appendChild(E("span", [" » ", _("Generate configuration")])); 1388 | mapNode.parentNode.appendChild( 1389 | E( 1390 | [], 1391 | [ 1392 | nodes, 1393 | E( 1394 | "div", 1395 | { 1396 | class: "right", 1397 | }, 1398 | [ 1399 | E( 1400 | "button", 1401 | { 1402 | class: "btn", 1403 | click: function () { 1404 | nodes.parentNode.removeChild(nodes.nextSibling); 1405 | nodes.parentNode.removeChild(nodes); 1406 | mapNode.classList.remove("hidden"); 1407 | mapNode.nextSibling.classList.remove("hidden"); 1408 | headNode.removeChild(headNode.lastChild); 1409 | }, 1410 | }, 1411 | [_("Back to peer configuration")] 1412 | ), 1413 | ] 1414 | ), 1415 | ] 1416 | ) 1417 | ); 1418 | 1419 | if (!s.formvalue(s.section, "listen_port")) { 1420 | nodes.appendChild( 1421 | E("div", { class: "alert-message" }, [ 1422 | E("p", [ 1423 | _( 1424 | "No fixed interface listening port defined, peers might not be able to initiate connections to this AmneziaWG instance!" 1425 | ), 1426 | ]), 1427 | ]) 1428 | ); 1429 | } 1430 | }); 1431 | }); 1432 | }; 1433 | 1434 | o.cfgvalue = function (section_id, value) { 1435 | var privkey = this.section.cfgvalue(section_id, "private_key"); 1436 | 1437 | return E( 1438 | "button", 1439 | { 1440 | class: "btn qr-code", 1441 | style: "display:inline-flex;align-items:center;gap:.5em", 1442 | click: ui.createHandlerFn(this, "handleGenerateQR", section_id), 1443 | disabled: privkey ? null : "", 1444 | }, 1445 | [ 1446 | Object.assign(E(qrIcon), { style: "width:22px;height:22px" }), 1447 | _("Generate configuration…"), 1448 | ] 1449 | ); 1450 | }; 1451 | }, 1452 | 1453 | deleteConfiguration: function () { 1454 | uci.sections("network", "amneziawg_%s".format(this.sid), function (s) { 1455 | uci.remove("network", s[".name"]); 1456 | }); 1457 | }, 1458 | }); 1459 | -------------------------------------------------------------------------------- /luci-proto-amneziawg/htdocs/luci-static/resources/view/amneziawg/status.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 'require view'; 3 | 'require rpc'; 4 | 'require poll'; 5 | 'require dom'; 6 | 'require ui'; 7 | 8 | 9 | var callGetWgInstances = rpc.declare({ 10 | object: 'luci.amneziawg', 11 | method: 'getWgInstances' 12 | }); 13 | 14 | function timestampToStr(timestamp) { 15 | if (timestamp < 1) 16 | return _('Never', 'No AmneziaWG peer handshake yet'); 17 | 18 | var seconds = (Date.now() / 1000) - timestamp; 19 | var ago; 20 | 21 | if (seconds < 60) 22 | ago = _('%ds ago').format(seconds); 23 | else if (seconds < 3600) 24 | ago = _('%dm ago').format(seconds / 60); 25 | else if (seconds < 86401) 26 | ago = _('%dh ago').format(seconds / 3600); 27 | else 28 | ago = _('over a day ago'); 29 | 30 | return (new Date(timestamp * 1000)).toUTCString() + ' (' + ago + ')'; 31 | } 32 | 33 | function handleInterfaceDetails(iface) { 34 | ui.showModal(_('Instance Details'), [ 35 | ui.itemlist(E([]), [ 36 | _('Name'), iface.name, 37 | _('Public Key'), E('code', [ iface.public_key ]), 38 | _('Listen Port'), iface.listen_port, 39 | _('Firewall Mark'), iface.fwmark != 'off' ? iface.fwmark : E('em', _('none')) 40 | ]), 41 | E('div', { 'class': 'right' }, [ 42 | E('button', { 43 | 'class': 'btn cbi-button', 44 | 'click': ui.hideModal 45 | }, [ _('Dismiss') ]) 46 | ]) 47 | ]); 48 | } 49 | 50 | function handlePeerDetails(peer) { 51 | ui.showModal(_('Peer Details'), [ 52 | ui.itemlist(E([]), [ 53 | _('Description'), peer.name, 54 | _('Public Key'), E('code', [ peer.public_key ]), 55 | _('Endpoint'), peer.endpoint, 56 | _('Allowed IPs'), (Array.isArray(peer.allowed_ips) && peer.allowed_ips.length) ? peer.allowed_ips.join(', ') : E('em', _('none')), 57 | _('Received Data'), '%1024mB'.format(peer.transfer_rx), 58 | _('Transmitted Data'), '%1024mB'.format(peer.transfer_tx), 59 | _('Latest Handshake'), timestampToStr(+peer.latest_handshake), 60 | _('Keep-Alive'), (peer.persistent_keepalive != 'off') ? _('every %ds', 'AmneziaWG keep alive interval').format(+peer.persistent_keepalive) : E('em', _('none')), 61 | ]), 62 | E('div', { 'class': 'right' }, [ 63 | E('button', { 64 | 'class': 'btn cbi-button', 65 | 'click': ui.hideModal 66 | }, [ _('Dismiss') ]) 67 | ]) 68 | ]); 69 | } 70 | 71 | function renderPeerTable(instanceName, peers) { 72 | var t = new L.ui.Table( 73 | [ 74 | _('Peer'), 75 | _('Endpoint'), 76 | _('Data Received'), 77 | _('Data Transmitted'), 78 | _('Latest Handshake') 79 | ], 80 | { 81 | id: 'peers-' + instanceName 82 | }, 83 | E('em', [ 84 | _('No peers connected') 85 | ]) 86 | ); 87 | 88 | t.update(peers.map(function(peer) { 89 | return [ 90 | [ 91 | peer.name || '', 92 | E('div', { 93 | 'style': 'cursor:pointer', 94 | 'click': ui.createHandlerFn(this, handlePeerDetails, peer) 95 | }, [ 96 | E('p', [ 97 | peer.name ? E('span', [ peer.name ]) : E('em', [ _('Untitled peer') ]) 98 | ]), 99 | E('span', { 100 | 'class': 'ifacebadge hide-sm', 101 | 'data-tooltip': _('Public key: %h', 'Tooltip displaying full AmneziaWG peer public key').format(peer.public_key) 102 | }, [ 103 | E('code', [ peer.public_key.replace(/^(.{5}).+(.{6})$/, '$1…$2') ]) 104 | ]) 105 | ]) 106 | ], 107 | peer.endpoint, 108 | [ +peer.transfer_rx, '%1024mB'.format(+peer.transfer_rx) ], 109 | [ +peer.transfer_tx, '%1024mB'.format(+peer.transfer_tx) ], 110 | [ +peer.latest_handshake, timestampToStr(+peer.latest_handshake) ] 111 | ]; 112 | })); 113 | 114 | return t.render(); 115 | } 116 | 117 | return view.extend({ 118 | renderIfaces: function(ifaces) { 119 | var res = [ 120 | E('h2', [ _('AmneziaWG Status') ]) 121 | ]; 122 | 123 | for (var instanceName in ifaces) { 124 | res.push( 125 | E('h3', [ _('Instance "%h"', 'AmneziaWG instance heading').format(instanceName) ]), 126 | E('p', { 127 | 'style': 'cursor:pointer', 128 | 'click': ui.createHandlerFn(this, handleInterfaceDetails, ifaces[instanceName]) 129 | }, [ 130 | E('span', { 'class': 'ifacebadge' }, [ 131 | E('img', { 'src': L.resource('icons', 'tunnel.png') }), 132 | '\xa0', 133 | instanceName 134 | ]), 135 | E('span', { 'style': 'opacity:.8' }, [ 136 | ' · ', 137 | _('Port %d', 'AmneziaWG listen port').format(ifaces[instanceName].listen_port), 138 | ' · ', 139 | E('code', { 'click': '' }, [ ifaces[instanceName].public_key ]) 140 | ]) 141 | ]), 142 | renderPeerTable(instanceName, ifaces[instanceName].peers) 143 | ); 144 | } 145 | 146 | if (res.length == 1) 147 | res.push(E('p', { 'class': 'center', 'style': 'margin-top:5em' }, [ 148 | E('em', [ _('No AmneziaWG interfaces configured.') ]) 149 | ])); 150 | 151 | return E([], res); 152 | }, 153 | 154 | render: function() { 155 | poll.add(L.bind(function () { 156 | return callGetWgInstances().then(L.bind(function(ifaces) { 157 | dom.content( 158 | document.querySelector('#view'), 159 | this.renderIfaces(ifaces) 160 | ); 161 | }, this)); 162 | }, this), 5); 163 | 164 | return E([], [ 165 | E('h2', [ _('AmneziaWG Status') ]), 166 | E('p', { 'class': 'center', 'style': 'margin-top:5em' }, [ 167 | E('em', [ _('Loading data…') ]) 168 | ]) 169 | ]); 170 | }, 171 | 172 | handleReset: null, 173 | handleSaveApply: null, 174 | handleSave: null 175 | }); 176 | -------------------------------------------------------------------------------- /luci-proto-amneziawg/root/usr/share/luci/menu.d/luci-proto-amneziawg.json: -------------------------------------------------------------------------------- 1 | { 2 | "admin/status/amneziawg": { 3 | "title": "AmneziaWG", 4 | "order": 92, 5 | "action": { 6 | "type": "view", 7 | "path": "amneziawg/status" 8 | }, 9 | "depends": { 10 | "acl": [ "luci-proto-amneziawg" ], 11 | "uci": { "network": true } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /luci-proto-amneziawg/root/usr/share/rpcd/acl.d/luci-amneziawg.json: -------------------------------------------------------------------------------- 1 | { 2 | "luci-proto-amneziawg": { 3 | "description": "Grant access to LuCI AmneziaWG procedures", 4 | "read": { 5 | "file": { 6 | "/usr/bin/qrencode --inline --8bit --type=SVG --output=- -- *": [ "exec" ] 7 | }, 8 | "ubus": { 9 | "luci.amneziawg": [ 10 | "getWgInstances" 11 | ] 12 | }, 13 | "uci": [ "network" ] 14 | }, 15 | "write": { 16 | "ubus": { 17 | "luci.amneziawg": [ 18 | "generateKeyPair", 19 | "getPublicAndPrivateKeyFromPrivate", 20 | "generatePsk" 21 | ] 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /luci-proto-amneziawg/root/usr/share/rpcd/ucode/luci.amneziawg: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Jo-Philipp Wich 2 | // Licensed to the public under the Apache License 2.0. 3 | 4 | 'use strict'; 5 | 6 | import { cursor } from 'uci'; 7 | import { popen } from 'fs'; 8 | 9 | 10 | function shellquote(s) { 11 | return `'${replace(s ?? '', "'", "'\\''")}'`; 12 | } 13 | 14 | function command(cmd) { 15 | return trim(popen(cmd)?.read?.('all')); 16 | } 17 | 18 | 19 | const methods = { 20 | generatePsk: { 21 | call: function() { 22 | return { psk: command('awg genpsk 2>/dev/null') }; 23 | } 24 | }, 25 | 26 | generateKeyPair: { 27 | call: function() { 28 | const priv = command('awg genkey 2>/dev/null'); 29 | const pub = command(`echo ${shellquote(priv)} | awg pubkey 2>/dev/null`); 30 | 31 | return { keys: { priv, pub } }; 32 | } 33 | }, 34 | 35 | getPublicAndPrivateKeyFromPrivate: { 36 | args: { privkey: "privkey" }, 37 | call: function(req) { 38 | const priv = req.args?.privkey; 39 | const pub = command(`echo ${shellquote(priv)} | awg pubkey 2>/dev/null`); 40 | 41 | return { keys: { priv, pub } }; 42 | } 43 | }, 44 | 45 | getWgInstances: { 46 | call: function() { 47 | const data = {}; 48 | let last_device; 49 | let qr_pubkey = {}; 50 | 51 | const uci = cursor(); 52 | const wg_dump = popen("awg show all dump 2>/dev/null"); 53 | 54 | if (wg_dump) { 55 | uci.load("network"); 56 | 57 | for (let line = wg_dump.read('line'); length(line); line = wg_dump.read('line')) { 58 | const record = split(rtrim(line, '\n'), '\t'); 59 | 60 | if (last_device != record[0]) { 61 | last_device = record[0]; 62 | data[last_device] = { 63 | name: last_device, 64 | public_key: record[2], 65 | listen_port: record[3], 66 | fwmark: record[4], 67 | peers: [] 68 | }; 69 | 70 | if (!length(record[2]) || record[2] == '(none)') 71 | qr_pubkey[last_device] = ''; 72 | else 73 | qr_pubkey[last_device] = `PublicKey = ${record[2]}`; 74 | } 75 | else { 76 | let peer_name; 77 | 78 | uci.foreach('network', `amneziawg_${last_device}`, (s) => { 79 | if (s.public_key == record[1]) 80 | peer_name = s.description; 81 | }); 82 | 83 | const peer = { 84 | name: peer_name, 85 | public_key: record[1], 86 | endpoint: record[3], 87 | allowed_ips: [], 88 | latest_handshake: record[5], 89 | transfer_rx: record[6], 90 | transfer_tx: record[7], 91 | persistent_keepalive: record[8] 92 | }; 93 | 94 | if (record[3] != '(none)' && length(record[4])) 95 | push(peer.allowed_ips, ...split(record[4], ',')); 96 | 97 | push(data[last_device].peers, peer); 98 | } 99 | } 100 | } 101 | 102 | return data; 103 | } 104 | } 105 | }; 106 | 107 | return { 'luci.amneziawg': methods }; 108 | --------------------------------------------------------------------------------