From 0e5dc22f720e812d95c5ae9e95e96b2e92ece6e9 Mon Sep 17 00:00:00 2001 From: Linus Nordberg Date: Tue, 1 Oct 2019 00:57:42 +0200 Subject: Server side lease handling --- Makefile | 6 +- common.c | 464 +++++++++++++----------------- common.h | 42 ++- ipm.c | 216 ++++++++++++++ ipm.h | 21 ++ lease.c | 342 +++++++++++++++++----- lease.h | 35 ++- tests/clientsh.bash | 269 ++++++++++++++++++ tests/netsh.sh | 89 +++--- wg-dynamic-client.c | 550 +++++++++++++----------------------- wg-dynamic-server.c | 798 +++++++++++++++++++++++++--------------------------- 11 files changed, 1645 insertions(+), 1187 deletions(-) create mode 100644 ipm.c create mode 100644 ipm.h create mode 100755 tests/clientsh.bash diff --git a/Makefile b/Makefile index 7d7a327..dc381c3 100644 --- a/Makefile +++ b/Makefile @@ -43,10 +43,10 @@ COMPILE.c = @echo " CC $@"; COMPILE.c += $(BUILT_IN_COMPILE.c) endif -all: wg-dynamic-server +all: wg-dynamic-server wg-dynamic-client -wg-dynamic-client: wg-dynamic-client.o netlink.o common.o -wg-dynamic-server: wg-dynamic-server.o netlink.o radix-trie.o common.o random.o lease.o +wg-dynamic-client: wg-dynamic-client.o netlink.o common.o ipm.o +wg-dynamic-server: wg-dynamic-server.o netlink.o radix-trie.o common.o random.o lease.o ipm.o ifneq ($(V),1) clean: diff --git a/common.c b/common.c index 3edf872..5dc229b 100644 --- a/common.c +++ b/common.c @@ -20,6 +20,60 @@ #include "common.h" #include "dbg.h" +union kvalues { + uint32_t u32; + struct wg_combined_ip ip; + char errmsg[256]; +}; + +static void request_ip(enum wg_dynamic_key key, union kvalues kv, void **dest) +{ + struct wg_combined_ip *ip = &kv.ip; + struct wg_dynamic_request_ip *r = (struct wg_dynamic_request_ip *)*dest; + + switch (key) { + case WGKEY_REQUEST_IP: + BUG_ON(*dest); + *dest = calloc(1, sizeof(struct wg_dynamic_request_ip)); + if (!*dest) + fatal("calloc()"); + + break; + case WGKEY_IPV4: + memcpy(&r->ipv4, &ip->ip4, sizeof r->ipv4); + r->cidrv4 = ip->cidr; + r->has_ipv4 = true; + break; + case WGKEY_IPV6: + memcpy(&r->ipv6, &ip->ip6, sizeof r->ipv6); + r->cidrv6 = ip->cidr; + r->has_ipv6 = true; + break; + case WGKEY_LEASESTART: + r->start = kv.u32; + break; + case WGKEY_LEASETIME: + r->leasetime = kv.u32; + break; + case WGKEY_ERRNO: + r->wg_errno = kv.u32; + break; + case WGKEY_ERRMSG: + r->errmsg = strdup(kv.errmsg); + break; + default: + debug("Invalid key %d, aborting\n", key); + BUG(); + } +} + +static void (*const deserialize_fptr[])(enum wg_dynamic_key key, + union kvalues kv, void **dest) = { + NULL, + NULL, + request_ip, +}; + static bool parse_ip_cidr(struct wg_combined_ip *ip, char *value) { uintmax_t res; @@ -50,281 +104,253 @@ static bool parse_ip_cidr(struct wg_combined_ip *ip, char *value) return true; } -static struct wg_dynamic_attr *parse_value(enum wg_dynamic_key key, char *value) +static bool parse_value(enum wg_dynamic_key key, char *str, union kvalues *kv) { - struct wg_dynamic_attr *attr; - size_t len; char *endptr; uintmax_t uresult; - union { - uint32_t uint32; - char errmsg[72]; - struct wg_combined_ip ip; - } data = { 0 }; + struct wg_combined_ip *ip; switch (key) { case WGKEY_IPV4: - len = sizeof data.ip; - data.ip.family = AF_INET; - if (!parse_ip_cidr(&data.ip, value)) - return NULL; - - break; case WGKEY_IPV6: - len = sizeof data.ip; - data.ip.family = AF_INET6; - if (!parse_ip_cidr(&data.ip, value)) - return NULL; + ip = &kv->ip; + ip->family = (key == WGKEY_IPV4) ? AF_INET : AF_INET6; + if (!parse_ip_cidr(ip, str)) + return false; break; + case WGKEY_REQUEST_IP: case WGKEY_LEASESTART: case WGKEY_LEASETIME: case WGKEY_ERRNO: - len = sizeof data.uint32; - uresult = strtoumax(value, &endptr, 10); + uresult = strtoumax(str, &endptr, 10); if (uresult > UINT32_MAX || *endptr != '\0') - return NULL; - data.uint32 = (uint32_t)uresult; + return false; + kv->u32 = (uint32_t)uresult; break; case WGKEY_ERRMSG: - strncpy(data.errmsg, value, sizeof data.errmsg - 1); - data.errmsg[sizeof data.errmsg - 1] = '\0'; - len = MIN(sizeof data.errmsg, - strlen(value) + 1); /* Copying the NUL byte too. */ - + strncpy(kv->errmsg, str, sizeof kv->errmsg); + kv->errmsg[sizeof kv->errmsg - 1] = '\0'; break; default: debug("Invalid key %d, aborting\n", key); BUG(); } - attr = malloc(sizeof(struct wg_dynamic_attr) + len); - if (!attr) - fatal("malloc()"); - - attr->len = len; - attr->key = key; - attr->next = NULL; - memcpy(&attr->value, &data, len); - - return attr; + return true; } static enum wg_dynamic_key parse_key(char *key) { - for (enum wg_dynamic_key e = 1; e < ARRAY_SIZE(WG_DYNAMIC_KEY); ++e) + for (enum wg_dynamic_key e = 2; e < ARRAY_SIZE(WG_DYNAMIC_KEY); ++e) if (!strcmp(key, WG_DYNAMIC_KEY[e])) return e; return WGKEY_UNKNOWN; } -/* Consumes one full line from buf, or up to MAX_LINESIZE-1 bytes if no newline - * character was found. - * If req != NULL then we expect to parse a command and will set cmd and version - * of req accordingly, while *attr will be set to NULL. - * Otherwise we expect to parse a normal key=value pair, that will be stored - * in a newly allocated wg_dynamic_attr, pointed to by *attr. +/* Consumes one full line from buf, or up to MAX_LINESIZE bytes if no newline + * character was found. If less then MAX_LINESIZE bytes are available, a new + * buffer will be allocated and req->buf and req->len set accordingly. * * Return values: * > 0 : Amount of bytes consumed (<= MAX_LINESIZE) + * = 0 : Consumed len bytes; need more for a full line * < 0 : Error - * = 0 : End of message */ static ssize_t parse_line(unsigned char *buf, size_t len, - struct wg_dynamic_attr **attr, - struct wg_dynamic_request *req) + struct wg_dynamic_request *req, + enum wg_dynamic_key *key, union kvalues *kv) { unsigned char *line_end, *key_end; - enum wg_dynamic_key key; ssize_t line_len; - char *endptr; - uintmax_t res; - line_end = memchr(buf, '\n', len > MAX_LINESIZE ? MAX_LINESIZE : len); + line_end = memchr(buf, '\n', MIN(len, MAX_LINESIZE)); if (!line_end) { if (len >= MAX_LINESIZE) return -E2BIG; - *attr = malloc(sizeof(struct wg_dynamic_attr) + len); - if (!*attr) + req->len = len; + req->buf = malloc(len); + if (!req->buf) fatal("malloc()"); - (*attr)->key = WGKEY_INCOMPLETE; - (*attr)->len = len; - (*attr)->next = NULL; - memcpy((*attr)->value, buf, len); - - return len; + memcpy(req->buf, buf, len); + return 0; } - if (line_end == buf) - return 0; /* \n\n - end of message */ + if (line_end == buf) { + *key = WGKEY_EOMSG; + return 1; + } *line_end = '\0'; line_len = line_end - buf + 1; key_end = memchr(buf, '=', line_len - 1); - if (!key_end) + if (!key_end || key_end == buf) return -EINVAL; *key_end = '\0'; - key = parse_key((char *)buf); - if (key == WGKEY_UNKNOWN) - return -ENOENT; - - if (req) { - if (key >= WGKEY_ENDCMD) - return -ENOENT; - - *attr = NULL; - res = strtoumax((char *)key_end + 1, &endptr, 10); - - if (res > UINT32_MAX || *endptr != '\0') - return -EINVAL; - - req->cmd = key; - req->version = (uint32_t)res; + *key = parse_key((char *)buf); + if (*key == WGKEY_UNKNOWN) + return line_len; - if (req->version != 1) - return -EPROTONOSUPPORT; - } else { - if (key <= WGKEY_ENDCMD) - return -ENOENT; - - *attr = parse_value(key, (char *)key_end + 1); - if (!*attr) - return -EINVAL; - } + if (!parse_value(*key, (char *)key_end + 1, kv)) + return -EINVAL; return line_len; } -static int parse_request(struct wg_dynamic_request *req, unsigned char *buf, - size_t len) +static ssize_t parse_request(struct wg_dynamic_request *req, unsigned char *buf, + size_t len) { - struct wg_dynamic_attr *attr; - size_t offset = 0; - ssize_t ret; + ssize_t ret, offset = 0; + size_t addlen = 0; + enum wg_dynamic_key key; + union kvalues kv; + void (*deserialize)(enum wg_dynamic_key key, union kvalues kv, + void **dest); if (memchr(buf, '\0', len)) return -EINVAL; /* don't allow null bytes */ - if (req->last && req->last->key == WGKEY_INCOMPLETE) { - len += req->last->len; + if (req->len > 0 && req->buf) { + len += req->len; - memmove(buf + req->last->len, buf, len); - memcpy(buf, req->last->value, req->last->len); - free(req->last); + memmove(buf + req->len, buf, len); + memcpy(buf, req->buf, req->len); + addlen = req->len; + free(req->buf); + req->buf = NULL; + req->len = 0; + } - if (req->first == req->last) { - req->first = NULL; - req->last = NULL; - } else { - attr = req->first; - while (attr->next != req->last) - attr = attr->next; + if (req->cmd == WGKEY_UNKNOWN) { + ret = parse_line(buf, len, req, &req->cmd, &kv); + if (ret <= 0) + return ret; - attr->next = NULL; - req->last = attr; - } + req->version = kv.u32; + if (req->cmd >= WGKEY_ENDCMD || req->cmd <= WGKEY_EOMSG || + req->version != 1) + return -EPROTONOSUPPORT; + + len -= ret; + offset += ret; + + deserialize = deserialize_fptr[req->cmd]; + deserialize(req->cmd, kv, &req->result); + } else { + deserialize = deserialize_fptr[req->cmd]; } while (len > 0) { - ret = parse_line(buf + offset, len, &attr, - req->cmd == WGKEY_UNKNOWN ? req : NULL); + ret = parse_line(buf + offset, len, req, &key, &kv); if (ret <= 0) - return ret; /* either error or message complete */ + return ret; len -= ret; offset += ret; - if (!attr) - continue; - if (!req->first) - req->first = attr; - else - req->last->next = attr; + if (key == WGKEY_EOMSG) + return offset - addlen; + else if (key == WGKEY_UNKNOWN) + continue; + else if (key <= WGKEY_ENDCMD) + return -EINVAL; - req->last = attr; + deserialize(key, kv, &req->result); } - return 1; + return 0; } -bool handle_request(struct wg_dynamic_request *req, - bool (*success)(struct wg_dynamic_request *), - bool (*error)(struct wg_dynamic_request *, int)) +ssize_t handle_request(int fd, struct wg_dynamic_request *req, + unsigned char buf[RECV_BUFSIZE + MAX_LINESIZE], + size_t *remaining) { - ssize_t bytes; - int ret; - unsigned char buf[RECV_BUFSIZE + MAX_LINESIZE]; + ssize_t bytes, processed; - while (1) { - bytes = read(req->fd, buf, RECV_BUFSIZE); - if (bytes < 0) { - if (errno == EWOULDBLOCK || errno == EAGAIN) - break; + do { + if (*remaining > 0) + bytes = *remaining; + else + bytes = read(fd, buf, RECV_BUFSIZE); - // TODO: handle EINTR + if (bytes < 0) { + if (errno == EWOULDBLOCK || errno == EAGAIN || + errno == EINTR) + return 0; - debug("Reading from socket %d failed: %s\n", req->fd, + debug("Reading from socket %d failed: %s\n", fd, strerror(errno)); - return true; + return -1; } else if (bytes == 0) { - debug("Peer disconnected unexpectedly\n"); - return true; + return -1; } - ret = parse_request(req, buf, bytes); - if (ret < 0) - return error(req, -ret); - else if (ret == 0) - return success(req); - } + processed = parse_request(req, buf, bytes); + if (processed < 0) + return processed; /* Parsing error */ + } while (processed == 0); + + *remaining = bytes - processed; + memmove(buf, buf + processed, *remaining); - return false; + return 1; } -bool send_message(struct wg_dynamic_request *req, const void *buf, size_t len) +void free_wg_dynamic_request(struct wg_dynamic_request *req) { - size_t offset = 0; + BUG_ON(req->buf || req->len); - while (1) { - ssize_t written = write(req->fd, buf + offset, len - offset); - if (written < 0) { - if (errno == EWOULDBLOCK || errno == EAGAIN) - break; + req->cmd = WGKEY_UNKNOWN; + req->version = 0; + if (req->result) { + free(((struct wg_dynamic_request_ip *)req->result)->errmsg); + free(req->result); + req->result = NULL; + } +} - // TODO: handle EINTR +size_t serialize_request_ip(bool send, char *buf, size_t len, + struct wg_dynamic_request_ip *rip) +{ + size_t off = 0; + char addrbuf[INET6_ADDRSTRLEN]; - debug("Writing to socket %d failed: %s\n", req->fd, - strerror(errno)); - return true; - } + if (send) + print_to_buf(buf, len, &off, "request_ip=1\n"); - offset += written; - if (offset == len) - return true; - } + if (rip->has_ipv4) { + if (!inet_ntop(AF_INET, &rip->ipv4, addrbuf, sizeof addrbuf)) + fatal("inet_ntop()"); - debug("Socket %d blocking on write with %lu bytes left, postponing\n", - req->fd, len - offset); + print_to_buf(buf, len, &off, "ipv4=%s/32\n", addrbuf); + } - if (!req->buf) { - req->buflen = len - offset; - req->buf = malloc(req->buflen); - if (!req->buf) - fatal("malloc()"); + if (rip->has_ipv6) { + if (!inet_ntop(AF_INET6, &rip->ipv6, addrbuf, sizeof addrbuf)) + fatal("inet_ntop()"); - memcpy(req->buf, buf + offset, req->buflen); - } else { - req->buflen = len - offset; - memmove(req->buf, buf + offset, req->buflen); + print_to_buf(buf, len, &off, "ipv6=%s/128\n", addrbuf); } - return false; + if (rip->start && rip->leasetime) + print_to_buf(buf, len, &off, "leasestart=%u\nleasetime=%u\n", + rip->start, rip->leasetime); + + if (rip->errmsg) + print_to_buf(buf, len, &off, "errmsg=%s\n", rip->errmsg); + + if (!send) + print_to_buf(buf, len, &off, "errno=%u\n", rip->wg_errno); + + print_to_buf(buf, len, &off, "\n"); + + return off; } void print_to_buf(char *buf, size_t bufsize, size_t *offset, char *fmt, ...) @@ -345,106 +371,8 @@ void print_to_buf(char *buf, size_t bufsize, size_t *offset, char *fmt, ...) *offset += n; } -uint32_t current_time() -{ - struct timespec tp; - if (clock_gettime(CLOCK_REALTIME, &tp)) - fatal("clock_gettime(CLOCK_REALTIME)"); - return tp.tv_sec; -} - -void close_connection(struct wg_dynamic_request *req) -{ - struct wg_dynamic_attr *prev, *cur = req->first; - - if (close(req->fd)) - debug("Failed to close socket\n"); - - while (cur) { - prev = cur; - cur = cur->next; - free(prev); - } - - req->cmd = WGKEY_UNKNOWN; - req->version = 0; - req->fd = -1; - free(req->buf); - req->buf = NULL; - req->buflen = 0; - req->first = NULL; - req->last = NULL; -} - bool is_link_local(unsigned char *addr) { /* TODO: check if the remaining 54 bits are 0 */ return IN6_IS_ADDR_LINKLOCAL(addr); } - -void iface_get_all_addrs(uint8_t family, mnl_cb_t data_cb, void *cb_data) -{ - struct mnl_socket *nl; - char buf[MNL_SOCKET_BUFFER_SIZE]; - struct nlmsghdr *nlh; - /* TODO: rtln-addr-dump from libmnl uses rtgenmsg here? */ - struct ifaddrmsg *ifaddr; - int ret; - unsigned int seq, portid; - - nl = mnl_socket_open(NETLINK_ROUTE); - if (nl == NULL) - fatal("mnl_socket_open"); - - if (mnl_socket_bind(nl, 0, MNL_SOCKET_AUTOPID) < 0) - fatal("mnl_socket_bind"); - - /* You'd think that we could just request addresses from a specific - * interface, via NLM_F_MATCH or something, but we can't. See also: - * https://marc.info/?l=linux-netdev&m=132508164508217 - */ - seq = time(NULL); - portid = mnl_socket_get_portid(nl); - nlh = mnl_nlmsg_put_header(buf); - nlh->nlmsg_type = RTM_GETADDR; - nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP; - nlh->nlmsg_seq = seq; - ifaddr = mnl_nlmsg_put_extra_header(nlh, sizeof(struct ifaddrmsg)); - ifaddr->ifa_family = family; - - if (mnl_socket_sendto(nl, nlh, nlh->nlmsg_len) < 0) - fatal("mnl_socket_sendto"); - - do { - ret = mnl_socket_recvfrom(nl, buf, sizeof(buf)); - if (ret <= MNL_CB_STOP) - break; - ret = mnl_cb_run(buf, ret, seq, portid, data_cb, cb_data); - } while (ret > 0); - - if (ret == -1) - fatal("mnl_cb_run/mnl_socket_recvfrom"); - - mnl_socket_close(nl); -} - -int data_attr_cb(const struct nlattr *attr, void *data) -{ - const struct nlattr **tb = data; - int type = mnl_attr_get_type(attr); - - /* skip unsupported attribute in user-space */ - if (mnl_attr_type_valid(attr, IFA_MAX) < 0) - return MNL_CB_OK; - - switch (type) { - case IFA_ADDRESS: - if (mnl_attr_validate(attr, MNL_TYPE_BINARY) < 0) { - perror("mnl_attr_validate"); - return MNL_CB_ERROR; - } - break; - } - tb[type] = attr; - return MNL_CB_OK; -} diff --git a/common.h b/common.h index fcb0f0e..b1d14af 100644 --- a/common.h +++ b/common.h @@ -15,20 +15,17 @@ #include "netlink.h" #define MAX_CONNECTIONS 16 - #define MAX_LINESIZE 4096 - #define RECV_BUFSIZE 8192 - #define MAX_RESPONSE_SIZE 8192 static const char WG_DYNAMIC_ADDR[] = "fe80::"; static const uint16_t WG_DYNAMIC_PORT = 970; /* ASCII sum of "wireguard" */ - -#define WG_DYNAMIC_LEASETIME 10 /* NOTE: 10s is good for testing purposes */ +#define WG_DYNAMIC_DEFAULT_LEASETIME 3600 #define ITEMS \ E(WGKEY_UNKNOWN, "") /* must be the first entry */ \ + E(WGKEY_EOMSG, "") \ /* CMD START */ \ E(WGKEY_REQUEST_IP, "request_ip") \ E(WGKEY_ENDCMD, "") \ @@ -62,21 +59,21 @@ static const char *const WG_DYNAMIC_ERR[] = { ITEMS }; #undef E #undef ITEMS -struct wg_dynamic_attr { - enum wg_dynamic_key key; - size_t len; - struct wg_dynamic_attr *next; - unsigned char value[]; -}; - struct wg_dynamic_request { enum wg_dynamic_key cmd; uint32_t version; - int fd; - wg_key pubkey; unsigned char *buf; - size_t buflen; - struct wg_dynamic_attr *first, *last; + size_t len; /* <= MAX_LINESIZE */ + void *result; +}; + +struct wg_dynamic_request_ip { + struct in_addr ipv4; + struct in6_addr ipv6; + uint8_t cidrv4, cidrv6; + uint32_t leasetime, start, wg_errno; + bool has_ipv4, has_ipv6; + char *errmsg; }; struct wg_combined_ip { @@ -90,15 +87,12 @@ struct wg_combined_ip { #define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0])) +ssize_t handle_request(int fd, struct wg_dynamic_request *req, + unsigned char buf[RECV_BUFSIZE + MAX_LINESIZE], + size_t *remaining); void free_wg_dynamic_request(struct wg_dynamic_request *req); -bool handle_request(struct wg_dynamic_request *req, - bool (*success)(struct wg_dynamic_request *), - bool (*error)(struct wg_dynamic_request *, int)); -bool send_message(struct wg_dynamic_request *req, const void *buf, size_t len); +size_t serialize_request_ip(bool include_header, char *buf, size_t len, + struct wg_dynamic_request_ip *rip); void print_to_buf(char *buf, size_t bufsize, size_t *offset, char *fmt, ...); -uint32_t current_time(); -void close_connection(struct wg_dynamic_request *req); bool is_link_local(unsigned char *addr); -void iface_get_all_addrs(uint8_t family, mnl_cb_t data_cb, void *cb_data); -int data_attr_cb(const struct nlattr *attr, void *data); #endif diff --git a/ipm.c b/ipm.c new file mode 100644 index 0000000..81135bb --- /dev/null +++ b/ipm.c @@ -0,0 +1,216 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2019 WireGuard LLC. All Rights Reserved. + */ + +#include +#include +#include +#include +#include + +#include "dbg.h" +#include "common.h" + +struct mnl_cb_data { + uint32_t ifindex; + struct wg_combined_ip *ip; + bool ip_found; + bool duplicate; +}; + +static struct mnl_socket *nl = NULL; + +static int iface_update(uint16_t cmd, uint16_t flags, uint32_t ifindex, + const uint8_t *addr, uint8_t cidr, sa_family_t family) +{ + char buf[MNL_SOCKET_BUFFER_SIZE]; + struct nlmsghdr *nlh; + unsigned int seq, portid; + struct ifaddrmsg *ifaddr; + int ret; + + portid = mnl_socket_get_portid(nl); + nlh = mnl_nlmsg_put_header(buf); + nlh->nlmsg_type = cmd; + nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | flags; + nlh->nlmsg_seq = seq = time(NULL); + ifaddr = mnl_nlmsg_put_extra_header(nlh, sizeof(struct ifaddrmsg)); + ifaddr->ifa_family = family; + ifaddr->ifa_prefixlen = cidr; + ifaddr->ifa_scope = RT_SCOPE_UNIVERSE; + ifaddr->ifa_index = ifindex; + mnl_attr_put(nlh, IFA_LOCAL, family == AF_INET ? 4 : 16, addr); + + if (mnl_socket_sendto(nl, nlh, nlh->nlmsg_len) < 0) + return -1; + + do { + ret = mnl_socket_recvfrom(nl, buf, sizeof(buf)); + if (ret <= MNL_CB_STOP) + break; + ret = mnl_cb_run(buf, ret, seq, portid, NULL, NULL); + } while (ret > 0); + + if (ret == -1) + return -1; + + return 0; +} + +static int data_attr_cb(const struct nlattr *attr, void *data) +{ + const struct nlattr **tb = data; + int type = mnl_attr_get_type(attr); + + /* skip unsupported attribute in user-space */ + if (mnl_attr_type_valid(attr, IFA_MAX) < 0) + return MNL_CB_OK; + + switch (type) { + case IFA_ADDRESS: + if (mnl_attr_validate(attr, MNL_TYPE_BINARY) < 0) { + perror("mnl_attr_validate"); + return MNL_CB_ERROR; + } + break; + } + tb[type] = attr; + return MNL_CB_OK; +} + +static int data_cb(const struct nlmsghdr *nlh, void *data) +{ + struct nlattr *tb[IFA_MAX + 1] = {}; + struct ifaddrmsg *ifa = mnl_nlmsg_get_payload(nlh); + struct mnl_cb_data *cb_data = (struct mnl_cb_data *)data; + + if (ifa->ifa_index != cb_data->ifindex) + return MNL_CB_OK; + + if (ifa->ifa_scope != RT_SCOPE_LINK) + return MNL_CB_OK; + + mnl_attr_parse(nlh, sizeof(*ifa), data_attr_cb, tb); + + if (!tb[IFA_ADDRESS]) + return MNL_CB_OK; + + if (cb_data->ip_found) { + cb_data->duplicate = true; + return MNL_CB_OK; + } + + memcpy(cb_data->ip, mnl_attr_get_payload(tb[IFA_ADDRESS]), + ifa->ifa_family == AF_INET ? 4 : 16); + cb_data->ip->cidr = ifa->ifa_prefixlen; + cb_data->ip->family = ifa->ifa_family; + + char out[INET6_ADDRSTRLEN]; + inet_ntop(ifa->ifa_family, cb_data->ip, out, sizeof(out)); + debug("index=%d, family=%d, addr=%s\n", ifa->ifa_index, ifa->ifa_family, + out); + + cb_data->ip_found = true; + + return MNL_CB_OK; +} + +static int iface_get_all_addrs(uint8_t family, void *cb_data) +{ + char buf[MNL_SOCKET_BUFFER_SIZE]; + struct nlmsghdr *nlh; + /* TODO: rtln-addr-dump from libmnl uses rtgenmsg here? */ + struct ifaddrmsg *ifaddr; + int ret; + unsigned int seq, portid; + + /* You'd think that we could just request addresses from a specific + * interface, via NLM_F_MATCH or something, but we can't. See also: + * https://marc.info/?l=linux-netdev&m=132508164508217 + */ + seq = time(NULL); + portid = mnl_socket_get_portid(nl); + nlh = mnl_nlmsg_put_header(buf); + nlh->nlmsg_type = RTM_GETADDR; + nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP; + nlh->nlmsg_seq = seq; + ifaddr = mnl_nlmsg_put_extra_header(nlh, sizeof(struct ifaddrmsg)); + ifaddr->ifa_family = family; + + if (mnl_socket_sendto(nl, nlh, nlh->nlmsg_len) < 0) + return -1; + + do { + ret = mnl_socket_recvfrom(nl, buf, sizeof(buf)); + if (ret <= MNL_CB_STOP) + break; + ret = mnl_cb_run(buf, ret, seq, portid, data_cb, cb_data); + } while (ret > 0); + + if (ret == -1) + return -1; + + return 0; +} + +void ipm_init() +{ + nl = mnl_socket_open(NETLINK_ROUTE); + if (nl == NULL) + fatal("mnl_socket_open()"); + + if (mnl_socket_bind(nl, 0, MNL_SOCKET_AUTOPID) < 0) + fatal("mnl_socket_bind()"); +} + +void ipm_free() +{ + if (nl) + mnl_socket_close(nl); +} + +int ipm_newaddr_v4(uint32_t ifindex, const struct in_addr *ip) +{ + return iface_update(RTM_NEWADDR, NLM_F_REPLACE | NLM_F_CREATE, ifindex, + (uint8_t *)ip, 32, AF_INET); +} + +int ipm_newaddr_v6(uint32_t ifindex, const struct in6_addr *ip) +{ + return iface_update(RTM_NEWADDR, NLM_F_REPLACE | NLM_F_CREATE, ifindex, + (uint8_t *)ip, 128, AF_INET6); +} + +int ipm_deladdr_v4(uint32_t ifindex, const struct in_addr *ip) +{ + return iface_update(RTM_DELADDR, 0, ifindex, (uint8_t *)ip, 32, + AF_INET); +} + +int ipm_deladdr_v6(uint32_t ifindex, const struct in6_addr *ip) +{ + return iface_update(RTM_DELADDR, 0, ifindex, (uint8_t *)ip, 128, + AF_INET6); +} + +int ipm_getlladdr(uint32_t ifindex, struct wg_combined_ip *addr) +{ + struct mnl_cb_data cb_data = { + .ifindex = ifindex, + .ip = addr, + .ip_found = false, + .duplicate = false, + }; + + if (iface_get_all_addrs(AF_INET6, &cb_data)) + return -1; + + if (!cb_data.ip_found) + return -2; + + if (cb_data.duplicate) + return -3; + + return 0; +} diff --git a/ipm.h b/ipm.h new file mode 100644 index 0000000..b10feac --- /dev/null +++ b/ipm.h @@ -0,0 +1,21 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2019 WireGuard LLC. All Rights Reserved. + */ + +#ifndef __IPM_H__ +#define __IPM_H__ + +#include + +#include "common.h" + +void ipm_init(); +void ipm_free(); +int ipm_newaddr_v4(uint32_t ifindex, const struct in_addr *ip); +int ipm_newaddr_v6(uint32_t ifindex, const struct in6_addr *ip); +int ipm_deladdr_v4(uint32_t ifindex, const struct in_addr *ip); +int ipm_deladdr_v6(uint32_t ifindex, const struct in6_addr *ip); +int ipm_getlladdr(uint32_t ifindex, struct wg_combined_ip *addr); + +#endif diff --git a/lease.c b/lease.c index 8a48a27..0ff37d4 100644 --- a/lease.c +++ b/lease.c @@ -14,6 +14,7 @@ #include #include #include +#include #include "common.h" #include "dbg.h" @@ -90,10 +91,13 @@ void leases_free() ipp_free(&pool); } -struct wg_dynamic_lease *new_lease(wg_key pubkey, uint32_t leasetime, - struct in_addr *ipv4, struct in6_addr *ipv6) +struct wg_dynamic_lease *set_lease(const char *devname, wg_key pubkey, + uint32_t leasetime, + const struct in6_addr *lladdr, + const struct in_addr *ipv4, + const struct in6_addr *ipv6) { - struct wg_dynamic_lease *lease, *parent; + struct wg_dynamic_lease *current, *new; uint64_t index_l; uint32_t index, index_h; struct timespec tp; @@ -101,34 +105,79 @@ struct wg_dynamic_lease *new_lease(wg_key pubkey, uint32_t leasetime, int ret; bool wants_ipv4 = !ipv4 || ipv4->s_addr; bool wants_ipv6 = !ipv6 || !IN6_IS_ADDR_UNSPECIFIED(ipv6); + bool ipv4_extended = false; + bool ipv6_extended = false; - lease = malloc(sizeof *lease); - if (!lease) - fatal("malloc()"); +#if DEBUG + char ipv4_asc[INET_ADDRSTRLEN], ipv6_asc[INET6_ADDRSTRLEN]; + wg_key_b64_string pubkey_asc; + wg_key_to_base64(pubkey_asc, pubkey); +#endif + + new = calloc(1, sizeof *new); + if (!new) + fatal("calloc()"); + + current = get_leases(pubkey); - if (wants_ipv4 && !pool.total_ipv4) - return NULL; /* no ipv4 addresses available */ + if (lease_is_valid(current)) { + if (current->ipv4.s_addr) { + if (ipv4 && ipv4->s_addr == current->ipv4.s_addr) { +#if DEBUG + inet_ntop(AF_INET, ¤t->ipv4, ipv4_asc, + INET_ADDRSTRLEN); + debug("extending %s\n", ipv4_asc); +#endif + new->ipv4 = current->ipv4; + ipv4_extended = true; + } + } + if (!IN6_IS_ADDR_UNSPECIFIED(¤t->ipv6)) { + if (ipv6 && IN6_ARE_ADDR_EQUAL(ipv6, ¤t->ipv6)) { +#if DEBUG + inet_ntop(AF_INET6, ¤t->ipv6, ipv6_asc, + INET6_ADDRSTRLEN); + debug("extending %s\n", ipv6_asc); +#endif + new->ipv6 = current->ipv6; + ipv6_extended = true; + } + } + } - if (wants_ipv6 && !pool.totalh_ipv6 && !pool.totall_ipv6) - return NULL; /* no ipv6 addresses available */ +#if DEBUG + if (ipv4) + inet_ntop(AF_INET, ipv4, ipv4_asc, INET_ADDRSTRLEN); + if (ipv6) + inet_ntop(AF_INET6, ipv6, ipv6_asc, INET6_ADDRSTRLEN); +#endif - if (wants_ipv4) { - if (!ipv4) { + if (wants_ipv4 && !ipv4_extended) { + if (!pool.total_ipv4) { + debug("IPv4 pool empty\n"); + } else if (!ipv4) { index = random_bounded(pool.total_ipv4 - 1); debug("new_lease(v4): %u of %u\n", index, pool.total_ipv4); - ipp_addnth_v4(&pool, &lease->ipv4, index); + ipp_addnth_v4(&pool, &new->ipv4, index); } else { - if (ipp_add_v4(&pool, ipv4, 32)) - return NULL; + debug("wants %s: ", ipv4_asc); + + if (!ipp_add_v4(&pool, ipv4, 32)) { + debug("allocated\n"); - memcpy(&lease->ipv4, ipv4, sizeof *ipv4); + new->ipv4 = *ipv4; + } else { + debug("not free\n"); + } } } - if (wants_ipv6) { - if (!ipv6) { + if (wants_ipv6 && !ipv6_extended) { + if (!pool.totalh_ipv6 && !pool.totall_ipv6) { + debug("IPv6 pool empty\n"); + } else if (!ipv6) { if (pool.totalh_ipv6 > 0) { index_l = random_bounded(UINT64_MAX); index_h = random_bounded(pool.totalh_ipv6 - 1); @@ -139,26 +188,49 @@ struct wg_dynamic_lease *new_lease(wg_key pubkey, uint32_t leasetime, debug("new_lease(v6): %u:%ju of %u:%ju\n", index_h, index_l, pool.totalh_ipv6, pool.totall_ipv6); - ipp_addnth_v6(&pool, &lease->ipv6, index_l, index_h); + ipp_addnth_v6(&pool, &new->ipv6, index_l, index_h); } else { - if (ipp_add_v6(&pool, ipv6, 128)) { - if (!ipv4 || ipv4->s_addr) - ipp_del_v4(&pool, ipv4, 32); + debug("wants %s: ", ipv6_asc); - return NULL; - } + if (!ipp_add_v6(&pool, ipv6, 128)) { + debug("allocated\n"); - memcpy(&lease->ipv6, ipv6, sizeof *ipv6); + new->ipv6 = *ipv6; + } else { + debug("not free\n"); + } } } + if (!wants_ipv4 && !ipv4_extended && ipv4 && ipv4->s_addr) { + debug("releasing %s\n", ipv4_asc); + + if (ipp_del_v4(&pool, &new->ipv4, 32)) + die("ipp_del_v4()\n"); + new->ipv4 = (struct in_addr){ 0 }; + } + + if (!wants_ipv6 && !ipv6_extended && ipv6 && + !IN6_IS_ADDR_UNSPECIFIED(ipv6)) { + debug("releasing %s\n", ipv6_asc); + + if (ipp_del_v6(&pool, &new->ipv6, 128)) + die("ipp_del_v6()\n"); + new->ipv6 = (struct in6_addr){ 0 }; + } + + if (!new->ipv4.s_addr && IN6_IS_ADDR_UNSPECIFIED(&new->ipv6)) { + free(new); + return NULL; + } + + new->lladdr = *lladdr; + if (clock_gettime(CLOCK_REALTIME, &tp)) fatal("clock_gettime(CLOCK_REALTIME)"); - - lease->start_real = tp.tv_sec; - lease->start_mono = get_monotonic_time(); - lease->leasetime = leasetime; - lease->next = NULL; + new->start_real = tp.tv_sec; + new->start_mono = get_monotonic_time(); + new->leasetime = leasetime; wg_key *pubcopy = malloc(sizeof(wg_key)); if (!pubcopy) @@ -169,21 +241,23 @@ struct wg_dynamic_lease *new_lease(wg_key pubkey, uint32_t leasetime, if (ret < 0) { fatal("kh_put()"); } else if (ret == 0) { - parent = kh_value(leases_ht, k); - while (parent->next) - parent = parent->next; - - parent->next = lease; - } else { - kh_value(leases_ht, k) = lease; + BUG_ON(!current); + BUG_ON(kh_value(leases_ht, k) != current); + debug("freeing lease: %s\n", lease_to_str(current)); + free(current); } + kh_value(leases_ht, k) = new; + + update_allowed_ips(devname, pubkey, new); - if (lease->start_mono + lease->leasetime < gexpires) - gexpires = lease->start_mono + lease->leasetime; + debug("new lease: %s\n", lease_to_str(new)); + + if (new->start_mono + new->leasetime < gexpires) + gexpires = new->start_mono + new->leasetime; /* TODO: add record to file */ - return lease; + return new; } struct wg_dynamic_lease *get_leases(wg_key pubkey) @@ -196,55 +270,158 @@ struct wg_dynamic_lease *get_leases(wg_key pubkey) return kh_val(leases_ht, k); } -bool extend_lease(struct wg_dynamic_lease *lease, uint32_t leasetime) +struct allowedips_update { + wg_key peer_pubkey; + struct in6_addr lladdr; + struct in_addr ipv4; + struct in6_addr ipv6; +}; + +#if DEBUG +static char *updates_to_str(const struct allowedips_update *u) +{ + static char buf[4096]; + wg_key_b64_string pubkey_asc; + char ll[INET6_ADDRSTRLEN], v4[INET_ADDRSTRLEN], v6[INET6_ADDRSTRLEN]; + + if (!u) + return "(null)"; + + wg_key_to_base64(pubkey_asc, u->peer_pubkey); + inet_ntop(AF_INET, &u->ipv4, v4, sizeof v4); + inet_ntop(AF_INET6, &u->ipv6, v6, sizeof v6); + inet_ntop(AF_INET6, &u->lladdr, ll, sizeof ll); + snprintf(buf, sizeof buf, "(%p) [%s] %s [%s]", u, ll, v4, v6); + + return buf; +} +#endif + +static void update_allowed_ips_bulk(const char *devname, + const struct allowedips_update *updates, + int nupdates) +{ + wg_peer peers[WG_DYNAMIC_LEASE_CHUNKSIZE] = { 0 }; + wg_allowedip allowedips[3 * WG_DYNAMIC_LEASE_CHUNKSIZE] = { 0 }; + wg_device dev = { 0 }; + wg_peer **pp = &dev.first_peer; + + int peer_idx = 0; + int allowedips_idx = 0; + for (int i = 0; i < nupdates; i++) { + debug("setting allowedips for %s\n", + updates_to_str(&updates[i])); + + peers[peer_idx].flags |= WGPEER_REPLACE_ALLOWEDIPS; + memcpy(peers[peer_idx].public_key, updates[i].peer_pubkey, + sizeof(wg_key)); + wg_allowedip **aipp = &peers[peer_idx].first_allowedip; + + if (!IN6_IS_ADDR_UNSPECIFIED(&updates[i].lladdr)) { + allowedips[allowedips_idx] = (wg_allowedip){ + .family = AF_INET6, + .cidr = 128, + .ip6 = updates[i].lladdr, + }; + *aipp = &allowedips[allowedips_idx]; + aipp = &allowedips[allowedips_idx].next_allowedip; + ++allowedips_idx; + } + if (updates[i].ipv4.s_addr) { + allowedips[allowedips_idx] = (wg_allowedip){ + .family = AF_INET, + .cidr = 32, + .ip4 = updates[i].ipv4, + }; + *aipp = &allowedips[allowedips_idx]; + aipp = &allowedips[allowedips_idx].next_allowedip; + ++allowedips_idx; + } + if (!IN6_IS_ADDR_UNSPECIFIED(&updates[i].ipv6)) { + allowedips[allowedips_idx] = (wg_allowedip){ + .family = AF_INET6, + .cidr = 128, + .ip6 = updates[i].ipv6, + }; + *aipp = &allowedips[allowedips_idx]; + ++allowedips_idx; + } + + *pp = &peers[peer_idx]; + pp = &peers[peer_idx].next_peer; + ++peer_idx; + } + + strncpy(dev.name, devname, sizeof(dev.name) - 1); + if (wg_set_device(&dev)) + fatal("wg_set_device()"); +} + +void update_allowed_ips(const char *devname, wg_key peer_pubkey, + const struct wg_dynamic_lease *lease) { - UNUSED(lease); - UNUSED(leasetime); - return false; + struct allowedips_update update; + + memcpy(update.peer_pubkey, peer_pubkey, sizeof(wg_key)); + update.lladdr = lease->lladdr; + update.ipv4 = lease->ipv4; + update.ipv6 = lease->ipv6; + + update_allowed_ips_bulk(devname, &update, 1); } -int leases_refresh() +int leases_refresh(const char *devname) { time_t cur_time = get_monotonic_time(); + struct allowedips_update updates[WG_DYNAMIC_LEASE_CHUNKSIZE] = { 0 }; if (cur_time < gexpires) return MIN(INT_MAX / 1000, gexpires - cur_time); gexpires = TIME_T_MAX; + int i = 0; for (khint_t k = kh_begin(leases_ht); k != kh_end(leases_ht); ++k) { if (!kh_exist(leases_ht, k)) continue; - - struct wg_dynamic_lease **pp = &kh_val(leases_ht, k), *tmp; - while (*pp) { - struct in_addr *ipv4 = &(*pp)->ipv4; - struct in6_addr *ipv6 = &(*pp)->ipv6; - time_t expires = (*pp)->start_mono + (*pp)->leasetime; - if (cur_time >= expires) { - if (ipv4->s_addr) - ipp_del_v4(&pool, ipv4, 32); - - if (!IN6_IS_ADDR_UNSPECIFIED(ipv6)) - ipp_del_v6(&pool, ipv6, 128); - - tmp = *pp; - *pp = (*pp)->next; - free(tmp); - } else { - if (expires < gexpires) - gexpires = expires; - - pp = &(*pp)->next; + struct wg_dynamic_lease *lease = kh_val(leases_ht, k); + BUG_ON(!lease); + time_t expires = lease->start_mono + lease->leasetime; + if (cur_time >= expires) { + if (lease->ipv4.s_addr) + ipp_del_v4(&pool, &lease->ipv4, 32); + + if (!IN6_IS_ADDR_UNSPECIFIED(&lease->ipv6)) + ipp_del_v6(&pool, &lease->ipv6, 128); + + memcpy(updates[i].peer_pubkey, kh_key(leases_ht, k), + sizeof(wg_key)); + updates[i].lladdr = lease->lladdr; + +#if DEBUG + wg_key_b64_string pubkey_asc; + wg_key_to_base64(pubkey_asc, updates[i].peer_pubkey); + debug("Peer losing its lease: %s\n", pubkey_asc); +#endif + ++i; + if (i == WG_DYNAMIC_LEASE_CHUNKSIZE) { + update_allowed_ips_bulk(devname, updates, i); + i = 0; + memset(updates, 0, sizeof updates); } - } - if (!kh_val(leases_ht, k)) { + free(lease); free((char *)kh_key(leases_ht, k)); kh_del(leaseht, leases_ht, k); + } else { + if (expires < gexpires) + gexpires = expires; } } + if (i) + update_allowed_ips_bulk(devname, updates, i); + return MIN(INT_MAX / 1000, gexpires - cur_time); } @@ -347,3 +524,32 @@ void leases_update_pools(struct mnl_socket *nlsock) if (ret == -1 && errno != EAGAIN && errno != EWOULDBLOCK) fatal("mnl_socket_recvfrom()"); } + +bool lease_is_valid(const struct wg_dynamic_lease *lease) +{ + if (!lease) + return false; + + if (get_monotonic_time() >= lease->start_mono + lease->leasetime) + return false; + + return true; +} + +#ifdef DEBUG +char *lease_to_str(const struct wg_dynamic_lease *l) +{ + static char buf[4096]; + char ll[INET6_ADDRSTRLEN], v4[INET_ADDRSTRLEN], v6[INET6_ADDRSTRLEN]; + + if (!l) + return "(null)"; + + inet_ntop(AF_INET6, &l->lladdr, ll, sizeof ll); + inet_ntop(AF_INET, &l->ipv4, v4, sizeof v4); + inet_ntop(AF_INET6, &l->ipv6, v6, sizeof v6); + snprintf(buf, sizeof buf, "(%p) [%s] %s [%s]", l, ll, v4, v6); + + return buf; +} +#endif diff --git a/lease.h b/lease.h index 3e1402d..e1be72f 100644 --- a/lease.h +++ b/lease.h @@ -13,13 +13,15 @@ #include "common.h" #include "netlink.h" +#define WG_DYNAMIC_LEASE_CHUNKSIZE 256 + struct wg_dynamic_lease { time_t start_real; time_t start_mono; uint32_t leasetime; /* in seconds */ struct in_addr ipv4; struct in6_addr ipv6; - struct wg_dynamic_lease *next; + struct in6_addr lladdr; }; /* @@ -34,11 +36,16 @@ void leases_init(char *fname, struct mnl_socket *nlsock); void leases_free(); /* - * Creates a new lease and returns a pointer to it, or NULL if either we ran out - * of assignable IPs or the requested IP is already taken. + * Creates a new lease and returns a pointer to it, or NULL if either + * we ran out of assignable IPs or the requested IP's are already + * taken. Frees currently held lease, if any. Updates allowedips for + * the peer. */ -struct wg_dynamic_lease *new_lease(wg_key pubkey, uint32_t leasetime, - struct in_addr *ipv4, struct in6_addr *ipv6); +struct wg_dynamic_lease *set_lease(const char *devname, wg_key pubkey, + uint32_t leasetime, + const struct in6_addr *lladdr, + const struct in_addr *ipv4, + const struct in6_addr *ipv6); /* * Returns all leases belonging to pubkey, or NULL if there are none. @@ -54,11 +61,27 @@ bool extend_lease(struct wg_dynamic_lease *lease, uint32_t leasetime); /* Refreshes all leases, meaning expired ones will be removed. Returns the * amount of seconds until the next lease will expire, or at most INT_MAX/1000. */ -int leases_refresh(); +int leases_refresh(const char *devname); + +/* + * Updates allowedips for peer_pubkey on devname, adding what's in + * lease (including lladdr), removing all others. + */ +void update_allowed_ips(const char *devname, wg_key peer_pubkey, + const struct wg_dynamic_lease *lease); /* * Updates all pools with information from the mnl socket nlsock. */ void leases_update_pools(struct mnl_socket *nlsock); +/* + * Return true if lease is !NULL and has not expired. + */ +bool lease_is_valid(const struct wg_dynamic_lease *lease); + +#ifdef DEBUG +char *lease_to_str(const struct wg_dynamic_lease *l); +#endif + #endif diff --git a/tests/clientsh.bash b/tests/clientsh.bash new file mode 100755 index 0000000..fef58cf --- /dev/null +++ b/tests/clientsh.bash @@ -0,0 +1,269 @@ +#! /bin/bash + +set -e + +exec 3>&1 + +DEBUG=1 +netnsprefix="$1"; shift # wg-test-$PID +server_public="$1"; shift + +netnsn() { echo $netnsprefix-$1; } +pretty() { echo -e "\x1b[32m\x1b[1m[+] ${1:+NS$1: }${2}\x1b[0m" >&3; } +pp() { pretty "" "$*"; "$@"; } +maybe_exec() { if [[ $BASHPID -eq $$ ]]; then "$@"; else exec "$@"; fi; } +nn() { + [[ "$1" != "-q" ]] && pretty $n "$*" || shift + local netns=$(netnsn $1) n=$1; shift; + maybe_exec ip netns exec $netns "$@"; +} +ipn() { + [[ "$1" != "-q" ]] && pretty $n "ip $*" || shift + local netns=$(netnsn $1) n=$1; shift; + ip -n $netns "$@"; +} + +ns= + +cleanup() { + set +e + exec 2>/dev/null + + for n in $ns; do ipn $n link del dev wg0; done + + local to_kill="$(for n in $ns; do $(ip netns pids $(netnsn $n)); done)" + [[ -n $to_kill ]] && kill $to_kill + + for n in $ns; do pp ip netns del $(netnsn $n); done + + exit +} + +trap cleanup EXIT + +setup_client_peer() { + local n=$1; shift + ns+=" $n" + + pp ip netns add $(netnsn $n) + ipn 0 link add dev wg0 type wireguard + ipn 0 link set wg0 netns $(netnsn $n) + + privkey=$(wg genkey) + pubkey=$(wg pubkey <<< $privkey) + + ipn $n addr add fe80::badc:0ffe:e0dd:$n/64 dev wg0 + nn $n wg set wg0 \ + private-key <(echo $privkey) \ + listen-port $n \ + peer $server_public \ + allowed-ips 0.0.0.0/0,::/0 + + ipn $n link set up dev wg0 + ipn $n route add fe80::/128 dev wg0 + + nn $n wg set wg0 peer "$server_public" endpoint [::1]:1 + nn 1 wg set wg0 peer "$pubkey" \ + allowed-ips fe80::badc:0ffe:e0dd:$n/128 \ + endpoint [::1]:$n + + nn $n ping6 -c 10 -f -W 1 fe80::%wg0 + nn 1 ping6 -c 10 -f -W 1 fe80::badc:0ffe:e0dd:$n%wg0 +} + +# Positive check -- verify that $1 is allowed on server. +check_alowedips() { + local n="$1"; shift + local pubkey="$1"; shift + local ip="$1"; shift + + [[ -z "$ip" ]] && return 0 + + nn -q 1 wg show wg0 allowed-ips | + while read -r _pubkey _ips; do + [[ "$_pubkey" = "$pubkey" ]] || continue + for _ip in $_ips; do + [[ "$_ip" = "$ip" ]] && return 0 + done + done && return 0 + + pretty $n "Missing $ip in allowedips" + return 1 +} + +declare -a IPV4 +declare -a IPV6 +declare -a LEASESTART +declare -a LEASETIME +declare -a ERRNO + +send_cmd() { + local n=$1; shift + local REQ="$1"; shift + + # - It would have been nice to use /dev/tcp/fe80::%w0/970 instead + # of nc, but we need to use a specific source port. + eval $( + printf $REQ | nn -q $n ncat -p 970 fe80::%wg0 970 | + while read -r line; do + key="${line%%=*}" + value="${line#*=}" + case "$key" in + ipv4) echo IPV4[$n]="$value"; continue ;; + ipv6) echo IPV6[$n]="$value"; continue ;; + leasestart) echo LEASESTART[$n]="$value"; continue ;; + leasetime) echo LEASETIME[$n]="$value"; continue ;; + errno) echo ERRNO[$n]="$value"; continue ;; + esac + done + ) +} + +req() { + local n=$1; shift + local ipv4_req= + [ $# -gt 0 ] && [ -n "$1" ] && { ipv4_req="ipv4=$1\n"; shift; } + [ "$ipv4_req" = "ipv4=-\n" ] && ipv4_req="ipv4=\n" + local ipv6_req= + [ $# -gt 0 ] && [ -n "$1" ] && { ipv6_req="ipv6=$1\n"; shift; } + [ "$ipv6_req" = "ipv6=-\n" ] && ipv6_req="ipv6=\n" + + IPV4[$n]= + IPV6[$n]= + LEASESTART[$n]= + LEASETIME[$n]= + ERRNO[$n]= + + REQ="request_ip=1\n${ipv4_req}${ipv6_req}\n" + send_cmd $n "$REQ" +} + +req_check() { + local n=$1 + + req $* + + pubkey=$(nn -q $n wg show wg0 public-key) + check_alowedips $n "$pubkey" "${IPV4[$n]}" + check_alowedips $n "$pubkey" "${IPV6[$n]}" +} + +run_k_at_random() { + local NCLIENTS=$1; shift + local k=$1; shift + local n=10 + local i + + if [[ $NCLIENTS -gt $k ]]; then + n=$(( 10 + $RANDOM % ($NCLIENTS - $k) )) + fi + + for i in $(seq $n $(( $n + $k - 1))); do + case $(( $RANDOM % 9 )) in + 0) req_check $i "" "" ; continue ;; + 1) req_check $i "" "-" ; continue ;; + 2) req_check $i "-" "" ; continue ;; + 3) req_check $i "-" "-" ; continue ;; + + 4) req_check $i ${IPV4[$i]} "" ; continue ;; + 5) req_check $i ${IPV4[$i]} "-" ; continue ;; + 6) req_check $i "" ${IPV6[$i]} ; continue ;; + 7) req_check $i "-" ${IPV6[$i]} ; continue ;; + + 8) req_check $i ${IPV4[$i]} ${IPV6[$i]} ; continue;; + esac + done +} + +test_random() { + local NCLIENTS=$1; shift + local k=$1; shift + local n + + for n in $(seq 10 $(( 9+$NCLIENTS ))); do setup_client_peer $n; done + + # NOTE: When running this script a second time, doing cleanup as + # we exit the first invocation, ncat is hanging on receiving the + # response. tcpdump shows that the response is indeed sent by the + # server. + for n in $(seq 10 $(( 9+$NCLIENTS ))); do + req_check $n + #t=$(( 1 + $RANDOM % 2 )) + #sleep $t + done + + while sleep 1; do + if [ $(( $RANDOM % 100 )) -lt 50 ]; then + run_k_at_random $NCLIENTS $k + fi + done +} + +fail() { + echo "FAIL \"$1\"" + exit 1 +} + +test_case_1() { + # One client -- 3. + setup_client_peer 3 + + pretty 3 "Badly formed request => errno=1 -- EXPECTED FAILURE: errno=2" + send_cmd 3 "ip_request=\n\n" + [[ ${ERRNO[3]} = 2 ]] || fail "errno: ${ERRNO[3]}" + + ## Check disabled 2019-09-27. Enable again when ipp_add_v4() and + ## ipp_add_v6() have checks. + #pretty 3 "Request an address we won't get => errno=2" + #req 3 "1.1.1.0/32" "-" + #[[ ${ERRNO[3]} = 2 ]] || fail "errno: ${ERRNO[3]}" + + pretty "" "SUCCESS\n" +} + +test_case_2() { + # Two clients -- 4 and 5. + for i in 4 5; do setup_client_peer $i; done + + pretty 4 "Any v4, any v6" + req_check 4 + [[ ${ERRNO[4]} = 0 ]] || fail "errno: ${ERRNO[4]}" + local C4_FIRST_V4=${IPV4[4]} + local C4_FIRST_V6=${IPV6[4]} + + pretty 4 "Extend v4, extend v6" + req_check 4 $C4_FIRST_V4 $C4_FIRST_V6 + [[ ${ERRNO[4]} = 0 ]] || fail "errno: ${ERRNO[4]}" + [[ ${IPV4[4]} = $C4_FIRST_V4 ]] || fail "ipv4: ${IPV4[4]}" + [[ ${IPV6[4]} = $C4_FIRST_V6 ]] || fail "ipv6: ${IPV6[4]}" + + pretty 4 "Extend v4, drop v6" + req_check 4 $C4_FIRST_V4 "-" + [[ ${ERRNO[4]} = 0 ]] || fail "errno: ${ERRNO[4]}" + [[ ${IPV4[4]} = $C4_FIRST_V4 ]] || fail "ipv4: ${IPV4[4]}" + [[ -z "${IPV6[4]}" ]] || fail "ipv6: ${IPV6[4]}" + + pretty 5 "Requesting the v4 of client 4 and no v6 => errno=0 and no addrs" + req 5 $C4_FIRST_V4 "-" + [[ ${ERRNO[5]} = 2 ]] || fail "errno: ${ERRNO[5]}" + [[ -z "${IPV4[5]}" ]] || fail "ipv4 not empty: ${IPV4[5]}" + [[ -z "${IPV6[5]}" ]] || fail "ipv6 not empty: ${IPV6[5]}" + + pretty 5 "Wait for lease to expire and try again" + pp sleep ${LEASETIME[4]} + req_check 5 $C4_FIRST_V4 "-" + [[ ${ERRNO[5]} = 0 ]] || fail "errno: ${ERRNO[5]}" + [[ ${IPV4[5]} = $C4_FIRST_V4 ]] || fail "ipv4: ${IPV4[5]}" + [[ -z "${IPV6[5]}" ]] || fail "ipv6 not empty: ${IPV6[5]}" + + pretty "" "SUCCESS\n" +} + +test_case_1 +test_case_2 + +N_RANDOM=20 +K_RANDOM=4 +[ $# -gt 0 ] && { N_RANDOM=$1; shift; } +[ $# -gt 0 ] && { K_RANDOM=$1; shift; } +test_random $N_RANDOM $K_RANDOM diff --git a/tests/netsh.sh b/tests/netsh.sh index 0376c14..6737d29 100755 --- a/tests/netsh.sh +++ b/tests/netsh.sh @@ -6,83 +6,84 @@ set -e exec 3>&1 + +[ $# -ge 1 ] && ( n_clients="$1"; shift; ) + export WG_HIDE_KEYS=never -netns0="wg-test-$$-0" -netns1="wg-test-$$-1" -netns2="wg-test-$$-2" +netnsn() { echo wg-test-$$-$1; } pretty() { echo -e "\x1b[32m\x1b[1m[+] ${1:+NS$1: }${2}\x1b[0m" >&3; } pp() { pretty "" "$*"; "$@"; } maybe_exec() { if [[ $BASHPID -eq $$ ]]; then "$@"; else exec "$@"; fi; } -n0() { pretty 0 "$*"; maybe_exec ip netns exec $netns0 "$@"; } -n1() { pretty 1 "$*"; maybe_exec ip netns exec $netns1 "$@"; } -n2() { pretty 2 "$*"; maybe_exec ip netns exec $netns2 "$@"; } -ip0() { pretty 0 "ip $*"; ip -n $netns0 "$@"; } -ip1() { pretty 1 "ip $*"; ip -n $netns1 "$@"; } -ip2() { pretty 2 "ip $*"; ip -n $netns2 "$@"; } +nn() { local netns=$(netnsn $1) n=$1; shift; pretty $n "$*"; maybe_exec ip netns exec $netns "$@"; } +ipn() { local netns=$(netnsn $1) n=$1; shift; pretty $n "ip $*"; ip -n $netns "$@"; } + +ns="0 1 2" cleanup() { set +e exec 2>/dev/null - ip0 link del dev wg0 - ip1 link del dev wg0 - ip2 link del dev wg0 - local to_kill="$(ip netns pids $netns0) $(ip netns pids $netns1) $(ip netns pids $netns2)" + + for n in $ns; do ipn $n link del dev wg0; done + + local to_kill="$(for n in $ns; do $(ip netns pids $(netnsn $n)); done)" [[ -n $to_kill ]] && kill $to_kill - pp ip netns del $netns0 - pp ip netns del $netns1 - pp ip netns del $netns2 + + for n in $ns; do pp ip netns del $(netnsn $n); done + exit } trap cleanup EXIT -ip netns del $netns0 2>/dev/null || true -ip netns del $netns1 2>/dev/null || true -ip netns del $netns2 2>/dev/null || true -pp ip netns add $netns0 -pp ip netns add $netns1 -pp ip netns add $netns2 -ip0 link set up dev lo - -ip0 link add dev wg0 type wireguard -ip0 link set wg0 netns $netns1 -ip0 link add dev wg0 type wireguard -ip0 link set wg0 netns $netns2 +pp ip netns add $(netnsn 0) +pp ip netns add $(netnsn 1) +pp ip netns add $(netnsn 2) +ipn 0 link set up dev lo + +ipn 0 link add dev wg0 type wireguard +ipn 0 link set wg0 netns $(netnsn 1) +ipn 0 link add dev wg0 type wireguard +ipn 0 link set wg0 netns $(netnsn 2) server_private=$(wg genkey) server_public=$(wg pubkey <<< $server_private) client_private=$(wg genkey) client_public=$(wg pubkey <<< $client_private) configure_peers() { - ip1 addr add fe80::/64 dev wg0 - ip2 addr add fe80::badc:0ffe:e0dd:f00d/128 dev wg0 + ipn 1 addr add fe80::/64 dev wg0 + ipn 2 addr add fe80::badc:0ffe:e0dd:f00d/128 dev wg0 - n1 wg set wg0 \ + nn 1 wg set wg0 \ private-key <(echo $server_private) \ listen-port 1 \ peer $client_public \ allowed-ips fe80::badc:0ffe:e0dd:f00d/128 - n2 wg set wg0 \ + nn 2 wg set wg0 \ private-key <(echo $client_private) \ listen-port 2 \ peer $server_public \ allowed-ips 0.0.0.0/0,::/0 - ip1 link set up dev wg0 - ip2 link set up dev wg0 + ipn 1 link set up dev wg0 + ipn 2 link set up dev wg0 - ip2 route add fe80::/128 dev wg0 - ip1 route add 192.168.4.0/28 dev wg0 - ip1 route add 192.168.73.0/27 dev wg0 - ip1 route add 2001:db8:1234::/124 dev wg0 - ip1 route add 2001:db8:7777::/124 dev wg0 + ipn 2 route add fe80::/128 dev wg0 + ipn 1 route add 192.168.4.0/28 dev wg0 + ipn 1 route add 192.168.73.0/27 dev wg0 + ipn 1 route add 2001:db8:1234::/124 dev wg0 + ipn 1 route add 2001:db8:7777::/124 dev wg0 } configure_peers -n1 wg set wg0 peer "$client_public" endpoint [::1]:2 -n2 wg set wg0 peer "$server_public" endpoint [::1]:1 -n2 ping6 -c 10 -f -W 1 fe80::%wg0 -n1 ping6 -c 10 -f -W 1 fe80::badc:0ffe:e0dd:f00d%wg0 +nn 1 wg set wg0 peer "$client_public" endpoint [::1]:2 +nn 2 wg set wg0 peer "$server_public" endpoint [::1]:1 +nn 2 ping6 -c 10 -f -W 1 fe80::%wg0 +nn 1 ping6 -c 10 -f -W 1 fe80::badc:0ffe:e0dd:f00d%wg0 + +pretty "" "clientsh.bash can be run with the following arguments:" +echo +echo wg-test-$$ $server_public +echo -n1 ./wg-dynamic-server wg0 +nn 1 ./wg-dynamic-server wg0 10 diff --git a/wg-dynamic-client.c b/wg-dynamic-client.c index f3e3274..e425a27 100644 --- a/wg-dynamic-client.c +++ b/wg-dynamic-client.c @@ -2,446 +2,288 @@ * * Copyright (C) 2019 WireGuard LLC. All Rights Reserved. */ +#define _POSIX_C_SOURCE 200112L -#include -#include +#include #include -#include -#include -#include +#include +#include +#include +#include +#include +#include #include -#include -#include -#include - #include "common.h" #include "dbg.h" +#include "ipm.h" #include "netlink.h" -struct wg_dynamic_lease { - struct wg_combined_ip ip4; - struct wg_combined_ip ip6; - uint32_t start; - uint32_t leasetime; - struct wg_dynamic_lease *next; -}; - static const char *progname; static const char *wg_interface; +static struct in6_addr well_known; +static struct in6_addr lladdr; + +static struct in_addr ipv4; +static struct in6_addr ipv6; +static bool ipv4_assigned = false, ipv6_assigned = false; + static wg_device *device = NULL; -static int our_fd = -1; -static struct in6_addr our_lladdr = { 0 }; -static struct wg_combined_ip our_gaddr4 = { 0 }; -static struct wg_combined_ip our_gaddr6 = { 0 }; -static struct wg_dynamic_lease our_lease = { 0 }; - -struct mnl_cb_data { - uint32_t ifindex; - struct in6_addr *lladdr; - struct wg_combined_ip *gaddr4; - struct wg_combined_ip *gaddr6; -}; +static int sockfd = -1; + +static volatile sig_atomic_t should_exit = 0; static void usage() { die("usage: %s \n", progname); } -int data_cb(const struct nlmsghdr *nlh, void *data) -{ - struct nlattr *tb[IFA_MAX + 1] = {}; - struct ifaddrmsg *ifa = mnl_nlmsg_get_payload(nlh); - struct mnl_cb_data *cb_data = (struct mnl_cb_data *)data; - unsigned char *addr; - - if (ifa->ifa_index != cb_data->ifindex) - return MNL_CB_OK; - - mnl_attr_parse(nlh, sizeof(*ifa), data_attr_cb, tb); - - if (!tb[IFA_ADDRESS]) - return MNL_CB_OK; - - addr = mnl_attr_get_payload(tb[IFA_ADDRESS]); - char out[INET6_ADDRSTRLEN]; - inet_ntop(ifa->ifa_family, addr, out, sizeof(out)); - debug("index=%d, family=%d, addr=%s\n", ifa->ifa_index, ifa->ifa_family, - out); - - if (ifa->ifa_scope == RT_SCOPE_LINK) { - if (ifa->ifa_prefixlen != 128) - return MNL_CB_OK; - memcpy(cb_data->lladdr, addr, 16); - } else if (ifa->ifa_scope == RT_SCOPE_UNIVERSE) { - switch (ifa->ifa_family) { - case AF_INET: - cb_data->gaddr4->family = ifa->ifa_family; - memcpy(&cb_data->gaddr4->ip4, addr, 4); - cb_data->gaddr4->cidr = ifa->ifa_prefixlen; - break; - case AF_INET6: - cb_data->gaddr6->family = ifa->ifa_family; - memcpy(&cb_data->gaddr6->ip6, addr, 16); - cb_data->gaddr6->cidr = ifa->ifa_prefixlen; - break; - default: - die("Unknown address family: %u\n", ifa->ifa_family); - } - } - - return MNL_CB_OK; -} - -static void iface_update(uint16_t cmd, uint16_t flags, uint32_t ifindex, - const struct wg_combined_ip *addr) +/* NOTE: do NOT call exit() in here */ +static void cleanup() { - struct mnl_socket *nl; - char buf[MNL_SOCKET_BUFFER_SIZE]; - struct nlmsghdr *nlh; - unsigned int seq, portid; - struct ifaddrmsg *ifaddr; /* linux/if_addr.h */ - int ret; - - nl = mnl_socket_open(NETLINK_ROUTE); - if (nl == NULL) - fatal("mnl_socket_open"); - - if (mnl_socket_bind(nl, 0, MNL_SOCKET_AUTOPID) < 0) - fatal("mnl_socket_bind"); - - portid = mnl_socket_get_portid(nl); - seq = time(NULL); - nlh = mnl_nlmsg_put_header(buf); - nlh->nlmsg_seq = seq; - nlh->nlmsg_type = cmd; - nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | flags; - ifaddr = mnl_nlmsg_put_extra_header(nlh, sizeof(struct ifaddrmsg)); - ifaddr->ifa_family = addr->family; - ifaddr->ifa_prefixlen = addr->cidr; - ifaddr->ifa_scope = RT_SCOPE_UNIVERSE; /* linux/rtnetlink.h */ - ifaddr->ifa_index = ifindex; - mnl_attr_put(nlh, IFA_LOCAL, addr->family == AF_INET ? 4 : 16, &addr); - - if (mnl_socket_sendto(nl, nlh, nlh->nlmsg_len) < 0) - fatal("mnl_socket_sendto"); - - do { - ret = mnl_socket_recvfrom(nl, buf, sizeof(buf)); - if (ret <= MNL_CB_STOP) - break; - ret = mnl_cb_run(buf, ret, seq, portid, NULL, NULL); - } while (ret > 0); - - if (ret == -1) - fatal("mnl_cb_run/mnl_socket_recvfrom"); + if (ipv4_assigned && ipm_deladdr_v4(device->ifindex, &ipv4)) + debug("Failed to cleanup ipv4 address"); + if (ipv6_assigned && ipm_deladdr_v6(device->ifindex, &ipv6)) + debug("Failed to cleanup ipv6 address"); - mnl_socket_close(nl); -} + if (sockfd >= 0) + close(sockfd); -static void iface_remove_addr(uint32_t ifindex, - const struct wg_combined_ip *addr) -{ - char ipstr[INET6_ADDRSTRLEN]; - debug("removing %s/%u from interface %u\n", - inet_ntop(addr->family, &addr, ipstr, sizeof ipstr), addr->cidr, - ifindex); - iface_update(RTM_DELADDR, 0, ifindex, addr); + ipm_free(); + wg_free_device(device); } -static void iface_add_addr(uint32_t ifindex, const struct wg_combined_ip *addr) +static void handler(int signum) { - char ipstr[INET6_ADDRSTRLEN]; - debug("adding %s/%u to interface %u\n", - inet_ntop(addr->family, &addr, ipstr, sizeof ipstr), addr->cidr, - ifindex); - iface_update(RTM_NEWADDR, NLM_F_REPLACE | NLM_F_CREATE, ifindex, addr); + UNUSED(signum); + should_exit = 1; } -static bool get_and_validate_local_addrs(uint32_t ifindex, - struct in6_addr *lladdr, - struct wg_combined_ip *gaddr4, - struct wg_combined_ip *gaddr6) +static void check_signal() { - struct mnl_cb_data cb_data = { - .ifindex = ifindex, - .lladdr = lladdr, - .gaddr4 = gaddr4, - .gaddr6 = gaddr6, - }; - - iface_get_all_addrs(AF_INET, data_cb, &cb_data); - iface_get_all_addrs(AF_INET6, data_cb, &cb_data); - - return !IN6_IS_ADDR_UNSPECIFIED(cb_data.lladdr); + if (should_exit) + exit(EXIT_FAILURE); } -static int try_connect(int *fd) +static int request_ip(struct wg_dynamic_request_ip *rip) { - struct timeval tval = { .tv_sec = 1, .tv_usec = 0 }; - struct sockaddr_in6 our_addr = { + unsigned char buf[RECV_BUFSIZE + MAX_LINESIZE]; + size_t msglen, remaining, off = 0; + struct sockaddr_in6 dstaddr = { .sin6_family = AF_INET6, - .sin6_addr = our_lladdr, + .sin6_addr = well_known, .sin6_port = htons(WG_DYNAMIC_PORT), .sin6_scope_id = device->ifindex, }; - struct sockaddr_in6 their_addr = { + struct sockaddr_in6 srcaddr = { .sin6_family = AF_INET6, + .sin6_addr = lladdr, .sin6_port = htons(WG_DYNAMIC_PORT), .sin6_scope_id = device->ifindex, }; + struct wg_dynamic_request req = { + .cmd = WGKEY_REQUEST_IP, + .version = 1, + .result = rip, + }; + struct timeval timeout = { .tv_sec = 30 }; + ssize_t ret; + int val = 1; - *fd = socket(AF_INET6, SOCK_STREAM, 0); - if (*fd < 0) + sockfd = socket(AF_INET6, SOCK_STREAM, 0); + if (sockfd < 0) fatal("Creating a socket failed"); - if (setsockopt(*fd, SOL_SOCKET, SO_RCVTIMEO, &tval, sizeof tval) == -1) - fatal("Setting socket option failed"); + if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof val)) + fatal("setsockopt(SO_REUSEADDR)"); + + if (setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, (char *)&timeout, + sizeof timeout)) + fatal("setsockopt(SO_RCVTIMEO)"); + + if (setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, (char *)&timeout, + sizeof timeout)) + fatal("setsockopt(SO_SNDTIMEO)"); - if (bind(*fd, (struct sockaddr *)&our_addr, sizeof(our_addr))) + if (bind(sockfd, (struct sockaddr *)&srcaddr, sizeof(srcaddr))) fatal("Binding socket failed"); - if (inet_pton(AF_INET6, WG_DYNAMIC_ADDR, &their_addr.sin6_addr) != 1) - fatal("inet_pton()"); + if (connect(sockfd, (struct sockaddr *)&dstaddr, sizeof(dstaddr))) + fatal("connect()"); + + if (ipv4_assigned) { + memcpy(&rip->ipv4, &ipv4, sizeof rip->ipv4); + rip->has_ipv4 = true; + } + if (ipv6_assigned) { + memcpy(&rip->ipv6, &ipv6, sizeof rip->ipv6); + rip->has_ipv6 = true; + } + + msglen = serialize_request_ip(true, (char *)buf, RECV_BUFSIZE, rip); + do { + ssize_t written = write(sockfd, buf + off, msglen - off); + if (written == -1) { + if (errno == EINTR) { + check_signal(); + continue; + } + + fatal("write()"); + } - if (connect(*fd, (struct sockaddr *)&their_addr, - sizeof(struct sockaddr_in6))) { - char out[INET6_ADDRSTRLEN]; + off += written; + } while (off < msglen); - if (!inet_ntop(their_addr.sin6_family, &their_addr.sin6_addr, - out, sizeof out)) - fatal("inet_ntop()"); - debug("Connecting to [%s]:%u failed: %s\n", out, - ntohs(their_addr.sin6_port), strerror(errno)); + memset(rip, 0, sizeof *rip); + + while ((ret = handle_request(sockfd, &req, buf, &remaining)) <= 0) { + if (ret == 0) { + check_signal(); + continue; + } + + if (close(sockfd)) + debug("Failed to close socket: %s\n", strerror(errno)); - if (close(*fd)) - debug("Closing socket failed: %s\n", strerror(errno)); - *fd = -1; return -1; } - return 0; -} + if (remaining > 0) + log_err("Warning: discarding %zu extra bytes sent by the server\n", + remaining); -static void request_ip(int fd, const struct wg_dynamic_lease *lease) -{ - unsigned char buf[MAX_RESPONSE_SIZE + 1]; - char addrstr[INET6_ADDRSTRLEN]; - size_t msglen; - - msglen = 0; - msglen += print_to_buf((char *)buf, sizeof buf, msglen, "%s=%d\n", - WG_DYNAMIC_KEY[WGKEY_REQUEST_IP], 1); - - if (lease && lease->ip4.ip4.s_addr) { - if (!inet_ntop(AF_INET, &lease->ip4.ip4, addrstr, - sizeof addrstr)) - fatal("inet_ntop()"); - msglen += print_to_buf((char *)buf, sizeof buf, msglen, - "ipv4=%s/32\n", addrstr); + if (rip->wg_errno) + return -1; + + if (!ipv4_assigned || memcmp(&ipv4, &rip->ipv4, sizeof ipv4)) { + if (ipv4_assigned && ipm_deladdr_v4(device->ifindex, &ipv4)) + fatal("ipm_deladdr_v4()"); + + memcpy(&ipv4, &rip->ipv4, sizeof ipv4); + if (ipm_newaddr_v4(device->ifindex, &ipv4)) + fatal("ipm_newaddr_v4()"); + ipv4_assigned = true; } - if (lease && !IN6_IS_ADDR_UNSPECIFIED(&lease->ip6.ip6)) { - if (!inet_ntop(AF_INET6, &lease->ip6.ip6, addrstr, - sizeof addrstr)) - fatal("inet_ntop()"); - msglen += print_to_buf((char *)buf, sizeof buf, msglen, - "ipv6=%s/128\n", addrstr); + + if (!ipv6_assigned || memcmp(&ipv6, &rip->ipv6, sizeof ipv6)) { + if (ipv6_assigned && ipm_deladdr_v6(device->ifindex, &ipv6)) + fatal("ipm_deladdr_v6()"); + + memcpy(&ipv6, &rip->ipv6, sizeof ipv6); + if (ipm_newaddr_v6(device->ifindex, &ipv6)) + fatal("ipm_newaddr_v6()"); + ipv6_assigned = true; } - /* nmsglen += print_to_buf((char *)buf, sizeof buf, msglen, - "leasetime=%u\n", fixme); */ - msglen += print_to_buf((char *)buf, sizeof buf, msglen, "\n"); + if (close(sockfd)) + debug("Failed to close socket: %s\n", strerror(errno)); - send_message(fd, buf, &msglen); + return 0; } -static uint32_t time_until_refresh(uint32_t now, struct wg_dynamic_lease *lease) +static void setup() { - uint32_t refresh_at; + struct sigaction sa = { .sa_handler = handler, .sa_flags = 0 }; + struct wg_combined_ip ip; + int ret; - if (lease->leasetime == 0) - return 0; - refresh_at = lease->start + (lease->leasetime * 8) / 10; + if (atexit(cleanup)) + die("Failed to set exit function\n"); - if (refresh_at < now) - return 0; - return refresh_at - now; -} + sigemptyset(&sa.sa_mask); + if (sigaction(SIGINT, &sa, NULL) == -1) + fatal("sigaction()"); -static int handle_received_lease(const struct wg_dynamic_request *req) -{ - uint32_t ret; - struct wg_dynamic_attr *attr; - struct wg_dynamic_lease *lease = &our_lease; - uint32_t now = current_time(); - uint32_t lease_start = 0; - uint32_t curleasetime = lease->start + lease->leasetime; - - attr = req->first; - while (attr) { - switch (attr->key) { - case WGKEY_IPV4: - memcpy(&lease->ip4, attr->value, - sizeof(struct wg_combined_ip)); - break; - case WGKEY_IPV6: - memcpy(&lease->ip6, attr->value, - sizeof(struct wg_combined_ip)); - break; - case WGKEY_LEASESTART: - memcpy(&lease_start, attr->value, sizeof(uint32_t)); - break; - case WGKEY_LEASETIME: - memcpy(&lease->leasetime, attr->value, - sizeof(uint32_t)); - break; - case WGKEY_ERRNO: - memcpy(&ret, attr->value, sizeof(uint32_t)); - if (ret) { - debug("Request IP failed with %ud from server\n", - ret); - return -ret; - } - break; - case WGKEY_ERRMSG: - /* TODO: do something with the error message */ - break; - default: - debug("Ignoring invalid attribute for request_ip: %d\n", - attr->key); - } - attr = attr->next; - } - - if (lease->leasetime == 0 || (lease->ip4.ip4.s_addr == 0 && - IN6_IS_ADDR_UNSPECIFIED(&lease->ip6.ip6))) - return -EINVAL; + if (wg_get_device(&device, wg_interface)) + fatal("Unable to access interface %s", wg_interface); - if (abs(now - lease_start) < 15) - lease->start = lease_start; - else - lease->start = now; + if (inet_pton(AF_INET6, WG_DYNAMIC_ADDR, &well_known) != 1) + fatal("inet_pton()"); - debug("Replacing lease %u -> %u\n", curleasetime, - lease->start + lease->leasetime); + ipm_init(); - return 0; -} + ret = ipm_getlladdr(device->ifindex, &ip); + if (ret == -1) + fatal("ipm_getlladdr()"); -static void cleanup() -{ - wg_free_device(device); - if (our_fd != -1 && close(our_fd)) - debug("Failed to close fd %d\n", our_fd); -} + if (ret == -2 || ip.family != AF_INET6) + die("%s needs to be assigned an IPv6 link local address\n", + wg_interface); -static bool handle_error(int fd, int ret) -{ - UNUSED(fd); - UNUSED(ret); + if (ret == -3) + die("Interface must not have multiple link-local addresses assigned\n"); - debug("Unable to parse response: %s\n", strerror(ret)); + if (ip.cidr != 128) + die("Link-local address must have a CIDR of 128\n"); - return true; + memcpy(&lladdr, &ip, 16); } -static void maybe_update_iface() +static void xnanosleep(time_t duration) { - if (memcmp(&our_gaddr4, &our_lease.ip4, sizeof our_gaddr4) || - our_gaddr4.cidr != our_lease.ip4.cidr) { - if (our_gaddr4.ip4.s_addr) - iface_remove_addr(device->ifindex, &our_gaddr4); - iface_add_addr(device->ifindex, &our_lease.ip4); - memcpy(&our_gaddr4, &our_lease.ip4, sizeof our_gaddr4); - } - if (memcmp(&our_gaddr6, &our_lease.ip6, sizeof our_gaddr6) || - our_gaddr6.cidr != our_lease.ip6.cidr) { - if (!IN6_IS_ADDR_UNSPECIFIED(&our_gaddr6.ip6)) - iface_remove_addr(device->ifindex, &our_gaddr6); - iface_add_addr(device->ifindex, &our_lease.ip6); - memcpy(&our_gaddr6, &our_lease.ip6, sizeof our_gaddr6); + struct timespec rem, timeout = { .tv_sec = duration }; + int ret; + + while ((ret = clock_nanosleep(CLOCK_BOOTTIME, 0, &timeout, &rem))) { + if (ret == EINTR) { + check_signal(); + memcpy(&timeout, &rem, sizeof timeout); + continue; + } + + die("clock_nanosleep(): %s\n", strerror(ret)); } } -static bool handle_response(int fd, struct wg_dynamic_request *req) +static void loop() { - UNUSED(fd); - -#if 0 - printf("Recieved response of type %s.\n", WG_DYNAMIC_KEY[req->cmd]); - struct wg_dynamic_attr *cur = req->first; - while (cur) { - printf(" with attr %s.\n", WG_DYNAMIC_KEY[cur->key]); - cur = cur->next; + struct wg_dynamic_request_ip rip = { 0 }; + struct timespec tsend, trecv; + time_t expires, timeout; + + if (clock_gettime(CLOCK_REALTIME, &tsend)) + fatal("clock_gettime(CLOCK_REALTIME)"); + + if (request_ip(&rip)) { + /* TODO: implement some sort of exponential backoff */ + debug("Server communication error, trying again in 30s\n"); + xnanosleep(30); + return; } -#endif - - switch (req->cmd) { - case WGKEY_REQUEST_IP: - if (handle_received_lease(req) == 0) - maybe_update_iface(); - break; - default: - debug("Unknown command: %d\n", req->cmd); - return true; + + if (clock_gettime(CLOCK_REALTIME, &trecv)) + fatal("clock_gettime(CLOCK_REALTIME)"); + + if (tsend.tv_sec < rip.start + 5 || rip.start > trecv.tv_sec + 5) + expires = tsend.tv_sec + rip.leasetime; + else + expires = MIN(rip.leasetime, trecv.tv_sec) + rip.leasetime; + + if (expires <= trecv.tv_sec) { + log_err("Warning: lease we tried to aquire already expired\n"); + return; } - return true; + /* TODO: implement random jitter */ + timeout = (expires - trecv.tv_sec); + timeout -= MIN(30, timeout * 0.5); + + debug("Sleeping for %zus\n", timeout); + xnanosleep(timeout); } -int main(int argc __attribute__((unused)), char *argv[] __attribute__((unused))) +int main(int argc, char *argv[]) { - int *fd = &our_fd; - struct wg_dynamic_request req = { 0 }; - progname = argv[0]; if (argc != 2) usage(); wg_interface = argv[1]; + setup(); - if (wg_get_device(&device, wg_interface)) - fatal("Unable to access interface %s", wg_interface); - - if (atexit(cleanup)) - die("Failed to set exit function\n"); - - if (!get_and_validate_local_addrs(device->ifindex, &our_lladdr, - &our_gaddr4, &our_gaddr6)) - die("%s needs to have an IPv6 link local address with prefixlen 128 assigned\n", - wg_interface); - // TODO: verify that we have a peer with an allowed-ips including fe80::/128 - - char lladr_str[INET6_ADDRSTRLEN]; - debug("%s: %s\n", wg_interface, - inet_ntop(AF_INET6, &our_lladdr, lladr_str, sizeof lladr_str)); - - /* If we have an address configured, let's assume it's from a - * lease in order to get renewal done. */ - if (our_gaddr4.ip4.s_addr || - !IN6_IS_ADDR_UNSPECIFIED(&our_gaddr6.ip6)) { - our_lease.start = current_time(); - our_lease.leasetime = 15; - memcpy(&our_lease.ip4, &our_gaddr4, - sizeof(struct wg_combined_ip)); - memcpy(&our_lease.ip6, &our_gaddr6, - sizeof(struct wg_combined_ip)); - } - - while (1) { - sleep(time_until_refresh(current_time(), &our_lease)); - - if (*fd == -1 && try_connect(fd)) { - sleep(1); - continue; - } - - request_ip(*fd, &our_lease); - - while (!handle_request(&req, handle_response, handle_error)) - ; - close_connection(&req); - } + while (1) + loop(); return 0; } diff --git a/wg-dynamic-server.c b/wg-dynamic-server.c index fca1dfc..5149cb5 100644 --- a/wg-dynamic-server.c +++ b/wg-dynamic-server.c @@ -9,7 +9,6 @@ #include #include #include -#include #include #include @@ -24,6 +23,7 @@ #include "common.h" #include "dbg.h" +#include "ipm.h" #include "khash.h" #include "lease.h" #include "netlink.h" @@ -33,7 +33,7 @@ static const char *wg_interface; static struct in6_addr well_known; static wg_device *device = NULL; -static struct wg_dynamic_request requests[MAX_CONNECTIONS] = { 0 }; +static uint32_t leasetime = WG_DYNAMIC_DEFAULT_LEASETIME; static int sockfd = -1; static int epollfd = -1; @@ -42,534 +42,492 @@ static struct mnl_socket *nlsock = NULL; KHASH_MAP_INIT_INT64(allowedht, wg_key *) khash_t(allowedht) * allowedips_ht; -struct mnl_cb_data { - uint32_t ifindex; - bool valid_ip_found; +struct wg_dynamic_connection { + struct wg_dynamic_request req; + int fd; + wg_key pubkey; + struct in6_addr lladdr; + unsigned char *outbuf; + size_t buflen; }; -static void usage() -{ - die("usage: %s \n", progname); -} - -static int data_cb(const struct nlmsghdr *nlh, void *data) -{ - struct nlattr *tb[IFA_MAX + 1] = {}; - struct ifaddrmsg *ifa = mnl_nlmsg_get_payload(nlh); - struct mnl_cb_data *cb_data = (struct mnl_cb_data *)data; - unsigned char *addr; - - if (ifa->ifa_index != cb_data->ifindex) - return MNL_CB_OK; - - if (ifa->ifa_scope != RT_SCOPE_LINK) - return MNL_CB_OK; - - mnl_attr_parse(nlh, sizeof(*ifa), data_attr_cb, tb); - - if (!tb[IFA_ADDRESS]) - return MNL_CB_OK; - - addr = mnl_attr_get_payload(tb[IFA_ADDRESS]); - char out[INET6_ADDRSTRLEN]; - inet_ntop(ifa->ifa_family, addr, out, sizeof(out)); - debug("index=%d, family=%d, addr=%s\n", ifa->ifa_index, ifa->ifa_family, - out); - - if (ifa->ifa_prefixlen != 64 || memcmp(addr, well_known.s6_addr, 16)) - return MNL_CB_OK; +static struct wg_dynamic_connection connections[MAX_CONNECTIONS] = { 0 }; - cb_data->valid_ip_found = true; - - return MNL_CB_OK; -} - -static bool validate_link_local_ip(uint32_t ifindex) +static void usage() { - struct mnl_cb_data cb_data = { - .ifindex = ifindex, - .valid_ip_found = false, - }; - - iface_get_all_addrs(AF_INET6, data_cb, &cb_data); - - return cb_data.valid_ip_found; + die("usage: %s \n", progname); } static bool valid_peer_found(wg_device *device) { - wg_peer *peer; - wg_key_b64_string key; - wg_allowedip *allowedip; - wg_for_each_peer (device, peer) { - wg_key_to_base64(key, peer->public_key); - debug("- peer %s\n", key); - debug(" allowedips:\n"); - - wg_for_each_allowedip (peer, allowedip) { - char out[INET6_ADDRSTRLEN]; - inet_ntop(allowedip->family, &allowedip->ip6, out, - sizeof(out)); - debug(" %s\n", out); - - if (is_link_local(allowedip->ip6.s6_addr) && - allowedip->cidr == 128) - return true; - } - } - - return false; + wg_peer *peer; + wg_key_b64_string key; + wg_allowedip *allowedip; + wg_for_each_peer (device, peer) { + wg_key_to_base64(key, peer->public_key); + debug("- peer %s\n", key); + debug(" allowedips:\n"); + + wg_for_each_allowedip (peer, allowedip) { + char out[INET6_ADDRSTRLEN]; + inet_ntop(allowedip->family, &allowedip->ip6, out, + sizeof(out)); + debug(" %s\n", out); + + if (is_link_local(allowedip->ip6.s6_addr) && + allowedip->cidr == 128) + return true; + } + } + + return false; } static void rebuild_allowedips_ht() { - wg_peer *peer; - wg_allowedip *allowedip; - khiter_t k; - uint64_t lh; - int ret; - - kh_clear(allowedht, allowedips_ht); - - wg_free_device(device); - if (wg_get_device(&device, wg_interface)) - fatal("Unable to access interface %s", wg_interface); - - wg_for_each_peer (device, peer) { - wg_for_each_allowedip (peer, allowedip) { - if (allowedip->family == AF_INET6 && - is_link_local(allowedip->ip6.s6_addr) && - allowedip->cidr == 128) { - memcpy(&lh, allowedip->ip6.s6_addr + 8, 8); - k = kh_put(allowedht, allowedips_ht, lh, &ret); - if (ret <= 0) - die("Failed to rebuild allowedips hashtable\n"); - - kh_value(allowedips_ht, k) = &peer->public_key; - } - } - } + wg_peer *peer; + wg_allowedip *allowedip; + khiter_t k; + uint64_t lh; + int ret; + + kh_clear(allowedht, allowedips_ht); + + wg_free_device(device); + if (wg_get_device(&device, wg_interface)) + fatal("Unable to access interface %s", wg_interface); + + wg_for_each_peer (device, peer) { + wg_for_each_allowedip (peer, allowedip) { + if (allowedip->family == AF_INET6 && + is_link_local(allowedip->ip6.s6_addr) && + allowedip->cidr == 128) { + memcpy(&lh, allowedip->ip6.s6_addr + 8, 8); + k = kh_put(allowedht, allowedips_ht, lh, &ret); + if (ret <= 0) + die("Failed to rebuild allowedips hashtable\n"); + + kh_value(allowedips_ht, k) = &peer->public_key; + } + } + } } static wg_key *addr_to_pubkey(struct sockaddr_storage *addr) { - khiter_t k; - uint64_t lh; - - if (addr->ss_family == AF_INET6) { - lh = *(uint64_t *)&((struct sockaddr_in6 *)addr) - ->sin6_addr.s6_addr[8]; - k = kh_get(allowedht, allowedips_ht, lh); - if (k != kh_end(allowedips_ht)) - return kh_val(allowedips_ht, k); - } - - return NULL; + khiter_t k; + uint64_t lh; + + if (addr->ss_family == AF_INET6) { + lh = *(uint64_t *)&((struct sockaddr_in6 *)addr) + ->sin6_addr.s6_addr[8]; + k = kh_get(allowedht, allowedips_ht, lh); + if (k != kh_end(allowedips_ht)) + return kh_val(allowedips_ht, k); + } + + return NULL; } -static int accept_connection(int sockfd, wg_key *dest) +static int accept_connection(wg_key *dest_pubkey, + struct in6_addr *dest_lladdr) { - int fd; - wg_key *pubkey; - struct sockaddr_storage addr; - socklen_t size = sizeof addr; + int fd; + wg_key *pubkey; + struct sockaddr_storage addr; + socklen_t size = sizeof addr; #ifdef __linux__ - fd = accept4(sockfd, (struct sockaddr *)&addr, &size, SOCK_NONBLOCK); - if (fd < 0) - return -errno; + fd = accept4(sockfd, (struct sockaddr *)&addr, &size, SOCK_NONBLOCK); + if (fd < 0) + return -errno; #else - fd = accept(sockfd, (struct sockaddr *)&addr, &size); - if (fd < 0) - return -errno; + fd = accept(sockfd, (struct sockaddr *)&addr, &size); + if (fd < 0) + return -errno; - int res = fcntl(fd, F_GETFL, 0); - if (res < 0 || fcntl(fd, F_SETFL, res | O_NONBLOCK) < 0) - fatal("Setting socket to nonblocking failed"); + int res = fcntl(fd, F_GETFL, 0); + if (res < 0 || fcntl(fd, F_SETFL, res | O_NONBLOCK) < 0) + fatal("Setting socket to nonblocking failed"); #endif - if (addr.ss_family != AF_INET6) { - debug("Rejecting client for not using an IPv6 address\n"); - return -EINVAL; - } - - if (((struct sockaddr_in6 *)&addr)->sin6_port != - htons(WG_DYNAMIC_PORT)) { - debug("Rejecting client for using port %u != %u\n", - htons(((struct sockaddr_in6 *)&addr)->sin6_port), - WG_DYNAMIC_PORT); - return -EINVAL; - } - - pubkey = addr_to_pubkey(&addr); - if (!pubkey) { - /* our copy of allowedips is outdated, refresh */ - rebuild_allowedips_ht(); - pubkey = addr_to_pubkey(&addr); - if (!pubkey) { - /* either we lost the race or something is very wrong */ - close(fd); - return -ENOENT; - } - } - memcpy(dest, pubkey, sizeof *dest); - - wg_key_b64_string key; - char out[INET6_ADDRSTRLEN]; - wg_key_to_base64(key, *pubkey); - inet_ntop(addr.ss_family, &((struct sockaddr_in6 *)&addr)->sin6_addr, - out, sizeof(out)); - debug("%s has pubkey: %s\n", out, key); - - return fd; -} - -static bool send_error(struct wg_dynamic_request *req, int error) -{ - char buf[MAX_RESPONSE_SIZE]; - size_t msglen = 0; - - print_to_buf(buf, sizeof buf, &msglen, "errno=%d\nerrmsg=%s\n\n", error, - WG_DYNAMIC_ERR[error]); - - return send_message(req, buf, msglen); + if (addr.ss_family != AF_INET6) { + debug("Rejecting client for not using an IPv6 address\n"); + return -EINVAL; + } + + if (((struct sockaddr_in6 *)&addr)->sin6_port != + htons(WG_DYNAMIC_PORT)) { + debug("Rejecting client for using port %u != %u\n", + htons(((struct sockaddr_in6 *)&addr)->sin6_port), + WG_DYNAMIC_PORT); + return -EINVAL; + } + + pubkey = addr_to_pubkey(&addr); + if (!pubkey) { + /* our copy of allowedips is outdated, refresh */ + rebuild_allowedips_ht(); + pubkey = addr_to_pubkey(&addr); + if (!pubkey) { + /* either we lost the race or something is very wrong */ + close(fd); + return -ENOENT; + } + } + memcpy(dest_pubkey, pubkey, sizeof *dest_pubkey); + + memcpy(dest_lladdr, &((struct sockaddr_in6 *)&addr)->sin6_addr, + sizeof *dest_lladdr); + + wg_key_b64_string key; + char out[INET6_ADDRSTRLEN]; + wg_key_to_base64(key, *pubkey); + inet_ntop(addr.ss_family, &((struct sockaddr_in6 *)&addr)->sin6_addr, + out, sizeof(out)); + debug("%s has pubkey: %s\n", out, key); + + return fd; } -static size_t serialize_lease(char *buf, size_t len, - const struct wg_dynamic_lease *lease) +static bool send_message(struct wg_dynamic_connection *con, + const unsigned char *buf, size_t len) { - char addrbuf[INET6_ADDRSTRLEN]; - size_t off = 0; - - if (lease->ipv4.s_addr) { - if (!inet_ntop(AF_INET, &lease->ipv4, addrbuf, sizeof addrbuf)) - fatal("inet_ntop()"); - - print_to_buf(buf, len, &off, "ipv4=%s/%d\n", addrbuf, 32); - } - - if (!IN6_IS_ADDR_UNSPECIFIED(&lease->ipv6)) { - if (!inet_ntop(AF_INET6, &lease->ipv6, addrbuf, sizeof addrbuf)) - fatal("inet_ntop()"); - - print_to_buf(buf, len, &off, "ipv6=%s/%d\n", addrbuf, 128); - } - - print_to_buf(buf, len, &off, "leasestart=%u\nleasetime=%u\nerrno=0\n\n", - lease->start_real, lease->leasetime); - - return off; + size_t offset = 0; + + while (1) { + ssize_t written = write(con->fd, buf + offset, len - offset); + if (written < 0) { + if (errno == EWOULDBLOCK || errno == EAGAIN) + break; + + if (errno == EINTR) + continue; + + debug("Writing to socket %d failed: %s\n", con->fd, + strerror(errno)); + return false; + } + + offset += written; + if (offset == len) + return true; + } + + debug("Socket %d blocking on write with %lu bytes left, postponing\n", + con->fd, len - offset); + + if (!con->outbuf) { + con->buflen = len - offset; + con->outbuf = malloc(con->buflen); + if (!con->outbuf) + fatal("malloc()"); + memcpy(con->outbuf, buf + offset, con->buflen); + } else { + con->buflen = len - offset; + memmove(con->outbuf, buf + offset, con->buflen); + } + + return true; } -static void add_allowed_ips(wg_key pubkey, struct in_addr *ipv4, - struct in6_addr *ipv6) +void close_connection(struct wg_dynamic_connection *con) { - wg_allowedip allowed_v4, allowed_v6; - wg_peer peer = { 0 }; - wg_device dev = { .first_peer = &peer }; - - strcpy(dev.name, wg_interface); - memcpy(peer.public_key, pubkey, sizeof peer.public_key); - wg_allowedip **cur = &peer.first_allowedip; - - if (ipv4) { - allowed_v4 = (wg_allowedip){ - .family = AF_INET, - .cidr = 32, - .ip4 = *ipv4, - }; - *cur = &allowed_v4; - cur = &allowed_v4.next_allowedip; - } + free_wg_dynamic_request(&con->req); - if (ipv6) { - allowed_v6 = (wg_allowedip){ - .family = AF_INET6, - .cidr = 128, - .ip6 = *ipv6, - }; - *cur = &allowed_v6; - } + if (close(con->fd)) + debug("Failed to close socket\n"); - if (wg_set_device(&dev)) - fatal("wg_set_device()"); + con->fd = -1; + memset(con->pubkey, 0, sizeof con->pubkey); + free(con->outbuf); + con->outbuf = NULL; + con->buflen = 0; } -static int response_request_ip(struct wg_dynamic_attr *cur, wg_key pubkey, - struct wg_dynamic_lease **lease) +static bool send_response(struct wg_dynamic_connection *con) { - struct in_addr *ipv4 = NULL; - struct in6_addr *ipv6 = NULL; - uint32_t leasetime = WG_DYNAMIC_LEASETIME; - - *lease = get_leases(pubkey); - - while (cur) { - switch (cur->key) { - case WGKEY_IPV4: - ipv4 = &((struct wg_combined_ip *)cur->value)->ip4; - break; - case WGKEY_IPV6: - ipv6 = &((struct wg_combined_ip *)cur->value)->ip6; - break; - case WGKEY_LEASETIME: - leasetime = *(uint32_t *)cur->value; - break; - default: - debug("Ignoring invalid attribute for request_ip: %d\n", - cur->key); - } - cur = cur->next; - } - - if (ipv4 && ipv6 && !ipv4->s_addr && IN6_IS_ADDR_UNSPECIFIED(ipv6)) - return E_INVALID_REQ; - - *lease = new_lease(pubkey, leasetime, ipv4, ipv6); - if (!*lease) - return E_IP_UNAVAIL; - - return E_NO_ERROR; + char buf[MAX_RESPONSE_SIZE]; + size_t msglen; + + switch (con->req.cmd) { + case WGKEY_REQUEST_IP:; + struct wg_dynamic_request_ip *rip = con->req.result; + struct in6_addr *lladdr = &con->lladdr; + struct in_addr *ip4 = rip->has_ipv4 ? &rip->ipv4 : NULL; + struct in6_addr *ip6 = rip->has_ipv6 ? &rip->ipv6 : NULL; + struct wg_dynamic_lease *lease; + struct wg_dynamic_request_ip ans = { 0 }; + + lease = set_lease(wg_interface, con->pubkey, leasetime, lladdr, ip4, ip6); + if (lease) { + memcpy(&ans.ipv4, &lease->ipv4, sizeof ans.ipv4); + memcpy(&ans.ipv6, &lease->ipv6, sizeof ans.ipv6); + ans.has_ipv4 = ans.has_ipv6 = true; + ans.start = lease->start_real; + ans.leasetime = lease->leasetime; + } else { + ans.wg_errno = E_IP_UNAVAIL; + } + + msglen = serialize_request_ip(false, buf, sizeof buf, &ans); + break; + default: + debug("Unknown command: %d\n", con->req.cmd); + BUG(); + } + + return send_message(con, (unsigned char *)buf, msglen); } -static bool send_response(struct wg_dynamic_request *req) +static void handle_client(struct wg_dynamic_connection *con) { - char buf[MAX_RESPONSE_SIZE]; - struct wg_dynamic_attr *cur = req->first; - struct wg_dynamic_lease *lease; - size_t msglen; - int ret; + unsigned char buf[RECV_BUFSIZE + MAX_LINESIZE]; + size_t rem = 0; + ssize_t ret; - switch (req->cmd) { - case WGKEY_REQUEST_IP: - ret = response_request_ip(cur, req->pubkey, &lease); - if (ret) - break; - - add_allowed_ips(req->pubkey, &lease->ipv4, &lease->ipv6); - msglen = serialize_lease(buf, sizeof buf, lease); - break; - default: - debug("Unknown command: %d\n", req->cmd); - BUG(); - } + while ((ret = handle_request(con->fd, &con->req, buf, &rem)) > 0) { + if (!send_response(con)) { + close_connection(con); + break; + } - if (ret) - return send_error(req, ret); + free_wg_dynamic_request(&con->req); + } - return send_message(req, buf, msglen); + if (ret < 0) + close_connection(con); } static void setup_sockets() { - int val = 1, res; - struct sockaddr_in6 addr = { - .sin6_family = AF_INET6, - .sin6_port = htons(WG_DYNAMIC_PORT), - .sin6_addr = well_known, - .sin6_scope_id = device->ifindex, - }; - - sockfd = socket(AF_INET6, SOCK_STREAM, 0); - if (sockfd < 0) - fatal("Creating a socket failed"); - - res = fcntl(sockfd, F_GETFL, 0); - if (res < 0 || fcntl(sockfd, F_SETFL, res | O_NONBLOCK) < 0) - fatal("Setting socket to nonblocking failed"); - - if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof val)) - fatal("Setting socket option failed"); - - if (bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1) - fatal("Binding socket failed"); - - if (listen(sockfd, SOMAXCONN) == -1) - fatal("Listening to socket failed"); - - /* netlink route socket */ - nlsock = mnl_socket_open(NETLINK_ROUTE); - if (!nlsock) - fatal("mnl_socket_open(NETLINK_ROUTE)"); - - res = fcntl(mnl_socket_get_fd(nlsock), F_GETFL, 0); - if (res < 0 || - fcntl(mnl_socket_get_fd(nlsock), F_SETFL, res | O_NONBLOCK) < 0) - fatal("Setting netlink socket to nonblocking failed"); - - if (mnl_socket_bind(nlsock, 0, MNL_SOCKET_AUTOPID) < 0) - fatal("mnl_socket_bind()"); - - val = RTNLGRP_IPV4_ROUTE; - if (mnl_socket_setsockopt(nlsock, NETLINK_ADD_MEMBERSHIP, &val, - sizeof val) < 0) - fatal("mnl_socket_setsockopt()"); - - val = RTNLGRP_IPV6_ROUTE; - if (mnl_socket_setsockopt(nlsock, NETLINK_ADD_MEMBERSHIP, &val, - sizeof val) < 0) - fatal("mnl_socket_setsockopt()"); + int val = 1, res; + struct sockaddr_in6 addr = { + .sin6_family = AF_INET6, + .sin6_port = htons(WG_DYNAMIC_PORT), + .sin6_addr = well_known, + .sin6_scope_id = device->ifindex, + }; + + sockfd = socket(AF_INET6, SOCK_STREAM, 0); + if (sockfd < 0) + fatal("Creating a socket failed"); + + res = fcntl(sockfd, F_GETFL, 0); + if (res < 0 || fcntl(sockfd, F_SETFL, res | O_NONBLOCK) < 0) + fatal("Setting socket to nonblocking failed"); + + if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof val)) + fatal("Setting socket option failed"); + + if (bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1) + fatal("Binding socket failed"); + + if (listen(sockfd, SOMAXCONN) == -1) + fatal("Listening to socket failed"); + + /* netlink route socket */ + nlsock = mnl_socket_open(NETLINK_ROUTE); + if (!nlsock) + fatal("mnl_socket_open(NETLINK_ROUTE)"); + + res = fcntl(mnl_socket_get_fd(nlsock), F_GETFL, 0); + if (res < 0 || + fcntl(mnl_socket_get_fd(nlsock), F_SETFL, res | O_NONBLOCK) < 0) + fatal("Setting netlink socket to nonblocking failed"); + + if (mnl_socket_bind(nlsock, 0, MNL_SOCKET_AUTOPID) < 0) + fatal("mnl_socket_bind()"); + + val = RTNLGRP_IPV4_ROUTE; + if (mnl_socket_setsockopt(nlsock, NETLINK_ADD_MEMBERSHIP, &val, + sizeof val) < 0) + fatal("mnl_socket_setsockopt()"); + + val = RTNLGRP_IPV6_ROUTE; + if (mnl_socket_setsockopt(nlsock, NETLINK_ADD_MEMBERSHIP, &val, + sizeof val) < 0) + fatal("mnl_socket_setsockopt()"); } static void cleanup() { - leases_free(); - kh_destroy(allowedht, allowedips_ht); - wg_free_device(device); + leases_free(); + kh_destroy(allowedht, allowedips_ht); + wg_free_device(device); - if (nlsock) - mnl_socket_close(nlsock); + if (nlsock) + mnl_socket_close(nlsock); - if (sockfd >= 0) - close(sockfd); + if (sockfd >= 0) + close(sockfd); - if (epollfd >= 0) - close(epollfd); + if (epollfd >= 0) + close(epollfd); - for (int i = 0; i < MAX_CONNECTIONS; ++i) { - if (requests[i].fd < 0) - continue; + for (int i = 0; i < MAX_CONNECTIONS; ++i) { + if (connections[i].fd < 0) + continue; - close_connection(&requests[i]); - } + close_connection(&connections[i]); + } } static void setup() { + struct wg_combined_ip ip; + int ret; + if (inet_pton(AF_INET6, WG_DYNAMIC_ADDR, &well_known) != 1) fatal("inet_pton()"); - allowedips_ht = kh_init(allowedht); + allowedips_ht = kh_init(allowedht); + + for (int i = 0; i < MAX_CONNECTIONS; ++i) + connections[i].fd = -1; - for (int i = 0; i < MAX_CONNECTIONS; ++i) - requests[i].fd = -1; + if (atexit(cleanup)) + die("Failed to set exit function\n"); - if (atexit(cleanup)) - die("Failed to set exit function\n"); + rebuild_allowedips_ht(); - rebuild_allowedips_ht(); + ipm_init(); + ret = ipm_getlladdr(device->ifindex, &ip); + if (ret == -1) + fatal("ipm_getlladdr()"); + if (ret == -2) + die("Interface must not have multiple link-local addresses assigned\n"); + ipm_free(); - if (!validate_link_local_ip(device->ifindex)) - // TODO: assign IP instead? + if (ret == -1 || ip.family != AF_INET6 || + memcmp(&ip.ip6, well_known.s6_addr, 16)) + /* TODO: assign IP instead? */ die("%s needs to have %s assigned\n", wg_interface, WG_DYNAMIC_ADDR); + if (ip.cidr != 64) + die("Link-local address must have a CIDR of 64\n"); + if (!valid_peer_found(device)) die("%s has no peers with link-local allowedips\n", wg_interface); - setup_sockets(); - leases_init("leases_file", nlsock); + setup_sockets(); + leases_init("leases_file", nlsock); } static int get_avail_request() { - for (int nfds = 0;; ++nfds) { - if (nfds >= MAX_CONNECTIONS) - return -1; + for (int nfds = 0;; ++nfds) { + if (nfds >= MAX_CONNECTIONS) + return -1; - if (requests[nfds].fd < 0) - return nfds; - } + if (connections[nfds].fd < 0) + return nfds; + } } -static void accept_incoming(int sockfd, int epollfd, - struct wg_dynamic_request *requests) +static void accept_incoming() { int n, fd; struct epoll_event ev; while ((n = get_avail_request()) >= 0) { - fd = accept_connection(sockfd, &requests[n].pubkey); + fd = accept_connection(&connections[n].pubkey, &connections[n].lladdr); if (fd < 0) { if (fd == -ENOENT) { debug("Failed to match IP to pubkey\n"); continue; - } else if (fd != -EAGAIN && fd != -EWOULDBLOCK) { - debug("Failed to accept connection: %s\n", - strerror(-fd)); - continue; + } else if (fd == -EAGAIN || fd == -EWOULDBLOCK) { + return; } - break; + debug("Failed to accept connection: %s\n", + strerror(-fd)); + continue; } ev.events = EPOLLIN | EPOLLET; - ev.data.ptr = &requests[n]; + ev.data.ptr = &connections[n]; if (epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev) == -1) fatal("epoll_ctl()"); - requests[n].fd = fd; + connections[n].fd = fd; } } static void handle_event(void *ptr, uint32_t events) { - struct wg_dynamic_request *req; - - if (ptr == &sockfd) { - accept_incoming(sockfd, epollfd, requests); - return; - } - - if (ptr == nlsock) { - leases_update_pools(nlsock); - return; - } - - req = (struct wg_dynamic_request *)ptr; - if (events & EPOLLIN) { - if (handle_request(req, send_response, send_error)) - close_connection(req); - } - - if (events & EPOLLOUT) { - if (send_message(req, req->buf, req->buflen)) - close_connection(req); - } + struct wg_dynamic_connection *con; + + if (ptr == &sockfd) { + accept_incoming(); + return; + } + + if (ptr == nlsock) { + leases_update_pools(nlsock); + return; + } + + con = (struct wg_dynamic_connection *)ptr; + if (events & EPOLLIN) { + handle_client(con); + } + + if (events & EPOLLOUT) { + if (!send_message(con, con->outbuf, con->buflen)) + close_connection(con); + } } static void poll_loop() { - struct epoll_event ev, events[MAX_CONNECTIONS]; - epollfd = epoll_create1(0); - if (epollfd == -1) - fatal("epoll_create1()"); - - ev.events = EPOLLIN | EPOLLET; - ev.data.ptr = &sockfd; - if (epoll_ctl(epollfd, EPOLL_CTL_ADD, sockfd, &ev)) - fatal("epoll_ctl()"); - - ev.events = EPOLLIN; - ev.data.ptr = nlsock; - if (epoll_ctl(epollfd, EPOLL_CTL_ADD, mnl_socket_get_fd(nlsock), &ev)) - fatal("epoll_ctl()"); - - while (1) { - time_t next = leases_refresh() * 1000; - int nfds = epoll_wait(epollfd, events, MAX_CONNECTIONS, next); - if (nfds == -1) { - if (errno == EINTR) - continue; - - fatal("epoll_wait()"); - } - - for (int i = 0; i < nfds; ++i) - handle_event(events[i].data.ptr, events[i].events); - } + struct epoll_event ev, events[MAX_CONNECTIONS]; + epollfd = epoll_create1(0); + if (epollfd == -1) + fatal("epoll_create1()"); + + ev.events = EPOLLIN | EPOLLET; + ev.data.ptr = &sockfd; + if (epoll_ctl(epollfd, EPOLL_CTL_ADD, sockfd, &ev)) + fatal("epoll_ctl()"); + + ev.events = EPOLLIN; + ev.data.ptr = nlsock; + if (epoll_ctl(epollfd, EPOLL_CTL_ADD, mnl_socket_get_fd(nlsock), &ev)) + fatal("epoll_ctl()"); + + while (1) { + time_t next = leases_refresh(wg_interface) * 1000; + int nfds = epoll_wait(epollfd, events, MAX_CONNECTIONS, next); + if (nfds == -1) { + if (errno == EINTR) + continue; + + fatal("epoll_wait()"); + } + + for (int i = 0; i < nfds; ++i) + handle_event(events[i].data.ptr, events[i].events); + } } int main(int argc, char *argv[]) { - progname = argv[0]; - if (argc != 2) - usage(); + char *endptr = NULL; + + progname = argv[0]; + if (argc != 3) + usage(); + + wg_interface = argv[1]; + leasetime = (uint32_t) strtoul(argv[2], &endptr, 10); + if (*endptr) + usage(); - wg_interface = argv[1]; - setup(); + setup(); - poll_loop(); + poll_loop(); - return 0; + return 0; } -- cgit v1.2.3-59-g8ed1b