From f69cd160562a4beb29fc0596e47538e71d4d33df Mon Sep 17 00:00:00 2001 From: Iurii Egorov Date: Fri, 6 Sep 2024 12:39:46 +0300 Subject: [PATCH] Auth notifications for unknown peers --- contrib/peer-approver/README | 24 ++ contrib/peer-approver/accounts.csv | 2 + contrib/peer-approver/approve.sh | 26 ++ contrib/peer-approver/notification-listener.c | 263 ++++++++++++++++++ src/uapi/linux/linux/wireguard.h | 23 ++ 5 files changed, 338 insertions(+) create mode 100644 contrib/peer-approver/README create mode 100644 contrib/peer-approver/accounts.csv create mode 100755 contrib/peer-approver/approve.sh create mode 100644 contrib/peer-approver/notification-listener.c diff --git a/contrib/peer-approver/README b/contrib/peer-approver/README new file mode 100644 index 0000000..c162df9 --- /dev/null +++ b/contrib/peer-approver/README @@ -0,0 +1,24 @@ +=== Dynamic peers authentication example === + +This example shows how to utilize netlink's multicast notifications +in AmneziaWG kernel module to provide dynamic peer authentication. + +To compile it, you must install some pre-requisites: + +```shell +apt-get install build-essential pkg-config libnl-3-dev libnl-genl-3-dev +``` + +After that, build example with the following command: + +```shell +gcc notification-listener.c $(pkg-config --cflags --libs libnl-3.0 libnl-genl-3.0) -o notification-listener +``` + +Bring up AWG interface with `awg-quick` as usually, edit `accounts.csv` file accordingly to your needs and then run: + +```shell +sudo ./notification-listener ./approve.sh ./accounts.csv +``` + +### **PLEASE NOTE: THIS EXAMPLE AS WELL AS OVERALL DYNAMIC AUTHENTICATION MECHANISM AND LEGACY CLIENTS' SUPPORT IN AMNEZIAWG IS SPONSORED BY [WINDSCRIBE LIMITED](https://windscribe.com)** \ No newline at end of file diff --git a/contrib/peer-approver/accounts.csv b/contrib/peer-approver/accounts.csv new file mode 100644 index 0000000..bebbff7 --- /dev/null +++ b/contrib/peer-approver/accounts.csv @@ -0,0 +1,2 @@ +Public Key,Allowed Ips,PSK +/Ca5004uiLJVBqSPaBUKg5zBszO9qbzEUCWmVkelkjY=,"10.8.1.10/32",E37VXqGtGvwftop/uFsbZcIO76Ox1kMmB6Sz/JoIw2I= \ No newline at end of file diff --git a/contrib/peer-approver/approve.sh b/contrib/peer-approver/approve.sh new file mode 100755 index 0000000..b4ecfe9 --- /dev/null +++ b/contrib/peer-approver/approve.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +ACCOUNTS_FILE=$1 +INTERFACE_NAME=$2 +PUBLIC_KEY=$3 +ENDPOINT=$4 +ADVANCED_SECURITY=$5 + +ACCOUNT_STR=`grep "${PUBLIC_KEY}" "${ACCOUNTS_FILE}"` + +if [ "${ACCOUNT_STR}" == "" ]; then + echo "Public key not found in accounts file!" + exit 255 +fi + +ACCOUNT=(${ACCOUNT_STR//,/ }) +ALLOWED_IPS=$(echo ${ACCOUNT[1]}|tr -d '"') +PSK=$(echo ${ACCOUNT[2]}|tr -d '"') +PSK_FILE=$(tempfile) +echo "${PSK}" > "${PSK_FILE}" + +awg set "${INTERFACE_NAME}" peer "${PUBLIC_KEY}" allowed-ips "${ALLOWED_IPS}" endpoint "${ENDPOINT}" allowed-ips "${ALLOWED_IPS}" preshared-key "${PSK_FILE}" advanced-security "${ADVANCED_SECURITY}" +EXIT_CODE=$? + +rm -f "{$PSK_FILE}" +exit ${EXIT_CODE} diff --git a/contrib/peer-approver/notification-listener.c b/contrib/peer-approver/notification-listener.c new file mode 100644 index 0000000..a264673 --- /dev/null +++ b/contrib/peer-approver/notification-listener.c @@ -0,0 +1,263 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../src/uapi/linux/linux/wireguard.h" + +#define prerr(...) fprintf(stderr, "Error: " __VA_ARGS__) + +#define WG_KEY_LEN 32 +#define WG_KEY_LEN_BASE64 ((((WG_KEY_LEN) + 2) / 3) * 4 + 1) + +static struct nl_sock *sk = NULL; +static char **cb_argv; +static int cb_argc; + +static int cleanup_and_exit(int ret) +{ + if (sk != NULL) + nl_socket_free(sk); + exit(ret); +} + +static void signal_handler(int sig) +{ + cleanup_and_exit(EXIT_SUCCESS); +} + +static inline void encode_base64(char dest[static 4], const uint8_t src[static 3]) +{ + const uint8_t input[] = { (src[0] >> 2) & 63, ((src[0] << 4) | (src[1] >> 4)) & 63, ((src[1] << 2) | (src[2] >> 6)) & 63, src[2] & 63 }; + + for (unsigned int i = 0; i < 4; ++i) + dest[i] = input[i] + 'A' + + (((25 - input[i]) >> 8) & 6) + - (((51 - input[i]) >> 8) & 75) + - (((61 - input[i]) >> 8) & 15) + + (((62 - input[i]) >> 8) & 3); + +} + +void key_to_base64(char base64[static WG_KEY_LEN_BASE64], const uint8_t key[static WG_KEY_LEN]) +{ + unsigned int i; + + for (i = 0; i < WG_KEY_LEN / 3; ++i) + encode_base64(&base64[i * 4], &key[i * 3]); + encode_base64(&base64[i * 4], (const uint8_t[]){ key[i * 3 + 0], key[i * 3 + 1], 0 }); + base64[WG_KEY_LEN_BASE64 - 2] = '='; + base64[WG_KEY_LEN_BASE64 - 1] = '\0'; +} + +static char *key(const uint8_t key[static WG_KEY_LEN]) +{ + static char base64[WG_KEY_LEN_BASE64]; + + key_to_base64(base64, key); + return base64; +} + +static char *endpoint(const struct sockaddr *addr) +{ + char host[4096 + 1]; + char service[512 + 1]; + static char buf[sizeof(host) + sizeof(service) + 4]; + int ret; + socklen_t addr_len = 0; + + memset(buf, 0, sizeof(buf)); + if (addr->sa_family == AF_INET) + addr_len = sizeof(struct sockaddr_in); + else if (addr->sa_family == AF_INET6) + addr_len = sizeof(struct sockaddr_in6); + + ret = getnameinfo(addr, addr_len, host, sizeof(host), service, sizeof(service), NI_DGRAM | NI_NUMERICSERV | NI_NUMERICHOST); + if (ret) { + strncpy(buf, gai_strerror(ret), sizeof(buf) - 1); + buf[sizeof(buf) - 1] = '\0'; + } else + snprintf(buf, sizeof(buf), (addr->sa_family == AF_INET6 && strchr(host, ':')) ? "[%s]:%s" : "%s:%s", host, service); + return buf; +} + +static int get_ifname(struct nlattr *tb[], char **ifname) +{ + if (tb[WGDEVICE_A_IFNAME] == NULL) + return -1; + *ifname = nla_data(tb[WGDEVICE_A_IFNAME]); + return 0; +} + +static int get_pubkey(struct nlattr *peer[], char **pubkey) +{ + if (peer[WGPEER_A_PUBLIC_KEY] == NULL) + return -1; + *pubkey = key(nla_data(peer[WGPEER_A_PUBLIC_KEY])); + return 0; +} + +static int get_endpoint(struct nlattr *peer[], char **endpoint_ip) +{ + if (peer[WGPEER_A_ENDPOINT] == NULL) + return -1; + *endpoint_ip = endpoint(nla_data(peer[WGPEER_A_ENDPOINT])); + return 0; +} + +static int run_callback(char *ifname, char *pubkey, char *endpoint_ip, bool advanced_security) +{ + char** new_argv = malloc((cb_argc + 2) * sizeof *new_argv); + + new_argv[0] = cb_argv[1]; + for (int i = 2; i < cb_argc - 3; i++) { + new_argv[i - 1] = cb_argv[i]; + } + new_argv[cb_argc - 4] = ifname; + new_argv[cb_argc - 3] = pubkey; + new_argv[cb_argc - 2] = endpoint_ip; + new_argv[cb_argc - 1] = (advanced_security ? "on\0" : "off\0"); + new_argv[cb_argc] = NULL; + + int child_pid = fork(), ret; + if (child_pid < 0) { + prerr("failed to spawn child process: %d\n", child_pid); + return child_pid; + } else if (child_pid == 0) { + execv(cb_argv[1], new_argv); + exit(0); + } else { + waitpid(child_pid, &ret, 0); + } + + free(new_argv); + return ret; +} + +static int netlink_callback(struct nl_msg *msg, void *arg) +{ + struct nlmsghdr *ret_hdr = nlmsg_hdr(msg); + struct genlmsghdr *gnlh = nlmsg_data(ret_hdr); + struct nlattr *tb[WGDEVICE_A_MAX + 1]; + struct nlattr *peer[WGPEER_A_MAX + 1]; + + nla_parse(tb, WGDEVICE_A_MAX, genlmsg_attrdata(gnlh, 0), genlmsg_attrlen(gnlh, 0), NULL); + + char *ifname, *pubkey, *endpoint_ip; + bool advanced_security = false; + int cb_ret; + + switch (gnlh->cmd) { + case WG_CMD_UNKNOWN_PEER: + if (get_ifname(tb, &ifname) < 0) { + prerr("unknown interface name!\n"); + return NL_SKIP; + } + if (nla_parse_nested(peer, WGPEER_A_MAX, tb[WGDEVICE_A_PEER], NULL)) { + prerr("failed to parse nested peer!\n"); + return NL_SKIP; + } + if (get_pubkey(peer, &pubkey)) { + prerr("invalid public key!\n"); + return NL_SKIP; + } + if (get_endpoint(peer, &endpoint_ip)) { + prerr("invalid endpoint!\n"); + return NL_SKIP; + } + if (nla_get_flag(peer[WGPEER_A_ADVANCED_SECURITY])) { + advanced_security = true; + } + if (cb_ret = run_callback(ifname, pubkey, endpoint_ip, advanced_security)) { + prerr("failed to execute callback script: %d!\n", cb_ret); + return NL_SKIP; + } + printf("Callback executed successfully.\n"); + break; + default: + return NL_SKIP; + } + + return 0; +} + +int main(int argc, char *argv[]) +{ + int ret; + int sk_fd; + fd_set rfds; + + if (argc < 2) { + prerr("usage: %s \n", argv[0]); + cleanup_and_exit(EXIT_FAILURE); + } + + cb_argc = argc + 3; + cb_argv = argv; + + signal(SIGTERM, signal_handler); + signal(SIGINT, signal_handler); + + sk = nl_socket_alloc(); + if (sk == NULL) { + prerr("unable to allocate Netlink socket!\n"); + exit(EXIT_FAILURE); + } + + ret = genl_connect(sk); + if (ret < 0) { + prerr("no connect %d!\n", ret); + cleanup_and_exit(EXIT_FAILURE); + } + + printf("Netlink socket connected.\n"); + + ret = genl_ctrl_resolve_grp(sk, WG_GENL_NAME, WG_MULTICAST_GROUP_AUTH); + if (ret < 0) { + prerr("auth group not found %d!\n", ret); + cleanup_and_exit(EXIT_FAILURE); + } + + ret = nl_socket_add_membership(sk, ret); + if (ret < 0) { + prerr("unable to join multicast group %d!\n", ret); + cleanup_and_exit(EXIT_FAILURE); + } + + nl_socket_disable_seq_check(sk); + ret = nl_socket_modify_cb(sk, NL_CB_VALID, NL_CB_CUSTOM, netlink_callback, NULL); + if (ret < 0) { + prerr("unable to register callback %d!\n", ret); + cleanup_and_exit(EXIT_FAILURE); + } + + while (1) { + FD_ZERO(&rfds); + + sk_fd = nl_socket_get_fd(sk); + FD_SET(sk_fd, &rfds); + + ret = select(sk_fd + 1, &rfds, NULL, NULL, NULL); + if (ret < 0) + break; + + ret = nl_recvmsgs_default(sk); + if (ret < 0) { + prerr("error receiving message %d!\n", ret); + cleanup_and_exit(EXIT_FAILURE); + } + } + + cleanup_and_exit(EXIT_FAILURE); +} \ No newline at end of file diff --git a/src/uapi/linux/linux/wireguard.h b/src/uapi/linux/linux/wireguard.h index bb1c214..8d38c90 100644 --- a/src/uapi/linux/linux/wireguard.h +++ b/src/uapi/linux/linux/wireguard.h @@ -129,6 +129,25 @@ * of a peer, it likely should not be specified in subsequent fragments. * * If an error occurs, NLMSG_ERROR will reply containing an errno. + * + * WG_CMD_UNKNOWN_PEER + * ---------------------- + * + * This command is sent on the multicast group WG_MULTICAST_GROUP_AUTH + * when the initiation message received from a peer with an unknown public + * key. + * The kernel will send a single message containing the + * following tree of nested items: + * + * WGDEVICE_A_IFINDEX: NLA_U32 + * WGDEVICE_A_IFNAME: NLA_NUL_STRING, maxlen IFNAMSIZ - 1 + * WGDEVICE_A_PEER: NLA_NESTED + * WGPEER_A_PUBLIC_KEY: NLA_EXACT_LEN, len WG_KEY_LEN + * WGPEER_A_ENDPOINT: NLA_MIN_LEN(struct sockaddr), struct sockaddr_in or struct sockaddr_in6 + * WGPEER_A_ADVANCED_SECURITY: flag indicating that advanced security + * techniques provided by AmneziaWG should + * be used. + * */ #ifndef _WG_UAPI_WIREGUARD_H @@ -139,9 +158,12 @@ #define WG_KEY_LEN 32 +#define WG_MULTICAST_GROUP_AUTH "auth" + enum wg_cmd { WG_CMD_GET_DEVICE, WG_CMD_SET_DEVICE, + WG_CMD_UNKNOWN_PEER, __WG_CMD_MAX }; #define WG_CMD_MAX (__WG_CMD_MAX - 1) @@ -169,6 +191,7 @@ enum wgdevice_attribute { WGDEVICE_A_H2, WGDEVICE_A_H3, WGDEVICE_A_H4, + WGDEVICE_A_PEER, __WGDEVICE_A_LAST }; #define WGDEVICE_A_MAX (__WGDEVICE_A_LAST - 1)