aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Makefile6
-rw-r--r--common.c464
-rw-r--r--common.h42
-rw-r--r--ipm.c216
-rw-r--r--ipm.h21
-rw-r--r--lease.c342
-rw-r--r--lease.h35
-rwxr-xr-xtests/clientsh.bash269
-rwxr-xr-xtests/netsh.sh89
-rw-r--r--wg-dynamic-client.c550
-rw-r--r--wg-dynamic-server.c798
11 files changed, 1645 insertions, 1187 deletions
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 <arpa/inet.h>
+#include <libmnl/libmnl.h>
+#include <linux/rtnetlink.h>
+#include <stdint.h>
+#include <time.h>
+
+#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 <stdint.h>
+
+#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 <stdlib.h>
#include <sys/socket.h>
#include <time.h>
+#include <string.h>
#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, &current->ipv4, ipv4_asc,
+ INET_ADDRSTRLEN);
+ debug("extending %s\n", ipv4_asc);
+#endif
+ new->ipv4 = current->ipv4;
+ ipv4_extended = true;
+ }
+ }
+ if (!IN6_IS_ADDR_UNSPECIFIED(&current->ipv6)) {
+ if (ipv6 && IN6_ARE_ADDR_EQUAL(ipv6, &current->ipv6)) {
+#if DEBUG
+ inet_ntop(AF_INET6, &current->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 <fcntl.h>
-#include <poll.h>
+#include <arpa/inet.h>
#include <signal.h>
-#include <stdio.h>
-#include <string.h>
-#include <stdlib.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <sys/socket.h>
+#include <sys/time.h>
+#include <sys/types.h>
+#include <time.h>
#include <unistd.h>
-#include <arpa/inet.h>
-#include <libmnl/libmnl.h>
-#include <linux/rtnetlink.h>
-
#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 <wg-interface>\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 <stdint.h>
#include <stdio.h>
#include <stdlib.h>
-#include <string.h>
#include <time.h>
#include <arpa/inet.h>
@@ -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 <wg-interface>\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 <wg-interface> <leasetime>\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;
}