diff options
-rw-r--r-- | Makefile | 8 | ||||
-rw-r--r-- | common.c | 503 | ||||
-rw-r--r-- | common.h | 49 | ||||
-rw-r--r-- | docs/ip-request.md | 255 | ||||
-rw-r--r-- | docs/protocol.md | 18 | ||||
-rw-r--r-- | ipm.c | 216 | ||||
-rw-r--r-- | ipm.h | 21 | ||||
-rw-r--r-- | khash.h | 28 | ||||
-rw-r--r-- | lease.c | 479 | ||||
-rw-r--r-- | lease.h | 22 | ||||
-rw-r--r-- | radix-trie.c | 476 | ||||
-rw-r--r-- | radix-trie.h | 41 | ||||
-rw-r--r-- | random.c | 27 | ||||
-rw-r--r-- | random.h | 4 | ||||
-rw-r--r-- | siphash.c | 204 | ||||
-rw-r--r-- | siphash.h | 85 | ||||
-rwxr-xr-x | tests/clientsh.bash | 302 | ||||
-rwxr-xr-x | tests/netsh.sh | 373 | ||||
-rw-r--r-- | wg-dynamic-client.c | 573 | ||||
-rw-r--r-- | wg-dynamic-server.c | 346 |
20 files changed, 2055 insertions, 1975 deletions
@@ -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 siphash.o ifneq ($(V),1) clean: @@ -56,7 +56,7 @@ clean: $(RM) wg-dynamic-client wg-dynamic-server *.o *.d endif -install: wg +install: all @install -v -d "$(DESTDIR)$(BINDIR)" && install -v -m 0755 wg-dynamic-server "$(DESTDIR)$(BINDIR)/wg-dynamic-server" @install -v -d "$(DESTDIR)$(BINDIR)" && install -v -m 0755 wg-dynamic-server "$(DESTDIR)$(BINDIR)/wg-dynamic-client" @install -v -d "$(DESTDIR)$(MANDIR)/man8" && install -v -m 0644 man/wg-dynamic-server.8 "$(DESTDIR)$(MANDIR)/man8/wg-dynamic-server.8" @@ -20,311 +20,336 @@ #include "common.h" #include "dbg.h" -static bool parse_ip_cidr(struct wg_combined_ip *ip, char *value) +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) { - uintmax_t res; - char *endptr; - char *sep; + struct wg_combined_ip *ip = &kv.ip; + struct wg_dynamic_request_ip *r = (struct wg_dynamic_request_ip *)*dest; - if (value[0] == '\0') { - memset(ip, 0, ip->family == AF_INET ? 4 : 16); - ip->cidr = 0; - return true; + 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_IP: + if (ip->family == AF_INET) { + memcpy(&r->ipv4, &ip->ip4, sizeof r->ipv4); + r->cidrv4 = ip->cidr; + r->has_ipv4 = true; + } else { + 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) +{ + char *sep; sep = strchr(value, '/'); - if (!sep) - return false; + if (sep) { + char *endptr; + uintmax_t res = strtoumax(sep + 1, &endptr, 10); + if (res > UINT8_MAX || *endptr != '\0' || sep + 1 == endptr) + return false; + + if ((ip->family == AF_INET && res > 32) || + (ip->family == AF_INET6 && res > 128)) + return false; + + *sep = '\0'; + ip->cidr = (uint8_t)res; + } else { + ip->cidr = ip->family == AF_INET ? 32 : 128; + } - *sep = '\0'; if (inet_pton(ip->family, value, ip) != 1) return false; - res = strtoumax(sep + 1, &endptr, 10); - if (res > UINT8_MAX || *endptr != '\0' || sep + 1 == endptr) - return false; - - // TODO: validate cidr range depending on ip->family - ip->cidr = (uint8_t)res; - 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; + case WGKEY_IP: + ip = &kv->ip; + ip->family = strchr(str, ':') ? AF_INET6 : AF_INET; + 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; + *key = parse_key((char *)buf); + if (*key == WGKEY_UNKNOWN) + return line_len; - *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; - - 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; + 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->cmd == WGKEY_UNKNOWN) { + ret = parse_line(buf, len, req, &req->cmd, &kv); + if (ret <= 0) + return ret; - memmove(buf + req->last->len, buf, len); - memcpy(buf, req->last->value, req->last->len); - free(req->last); + req->version = kv.u32; + if (req->cmd >= WGKEY_ENDCMD || req->cmd <= WGKEY_EOMSG || + req->version != 1) + return -EPROTONOSUPPORT; - if (req->first == req->last) { - req->first = NULL; - req->last = NULL; - } else { - attr = req->first; - while (attr->next != req->last) - attr = attr->next; + len -= ret; + offset += ret; - attr->next = NULL; - req->last = attr; - } + 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; + 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)) +int 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; + size_t leftover; - while (1) { - bytes = read(req->fd, buf, RECV_BUFSIZE); - if (bytes < 0) { - if (errno == EWOULDBLOCK || errno == EAGAIN) - break; + BUG_ON((*remaining > 0 && req->buf) || (req->buf && !req->len)); - // TODO: handle EINTR + do { + leftover = req->len; + if (*remaining > 0) + bytes = *remaining; + else + bytes = read(fd, buf + leftover, RECV_BUFSIZE); - debug("Reading from socket %d failed: %s\n", req->fd, + if (bytes < 0) { + if (errno == EWOULDBLOCK || errno == EAGAIN || + errno == EINTR) + return 0; + + 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); - } + if (req->buf) { + memcpy(buf, req->buf, leftover); + free(req->buf); + req->buf = NULL; + req->len = 0; + } - return false; + processed = parse_request(req, buf, bytes + leftover); + if (processed < 0) + return processed; /* Parsing error */ + if (!processed) + *remaining = 0; + } while (processed == 0); + + *remaining = (bytes + leftover) - processed; + memmove(buf, buf + processed, *remaining); + + 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()"); + + print_to_buf(buf, len, &off, "ip=%s/32\n", addrbuf); } - debug("Socket %d blocking on write with %lu bytes left, postponing\n", - req->fd, len - offset); + if (rip->has_ipv6) { + if (!inet_ntop(AF_INET6, &rip->ipv6, addrbuf, sizeof addrbuf)) + fatal("inet_ntop()"); - if (!req->buf) { - req->buflen = len - offset; - req->buf = malloc(req->buflen); - if (!req->buf) - fatal("malloc()"); - - 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, "ip=%s/128\n", addrbuf); } - return false; + if (rip->has_ipv4 || rip->has_ipv6) + print_to_buf(buf, len, &off, "leasestart=%u\nleasetime=%u\n", + rip->start, rip->leasetime); + + if (!send) + print_to_buf(buf, len, &off, "errno=%u\n", rip->wg_errno); + + if (rip->wg_errno) + print_to_buf(buf, len, &off, "errmsg=%s\n", + WG_DYNAMIC_ERR[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 +370,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; -} @@ -15,27 +15,21 @@ #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_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, "") \ /* CMD END */ \ - E(WGKEY_INCOMPLETE, "") \ - E(WGKEY_IPV4, "ipv4") \ - E(WGKEY_IPV6, "ipv6") \ + E(WGKEY_IP, "ip") \ E(WGKEY_LEASESTART, "leasestart") \ E(WGKEY_LEASETIME, "leasetime") \ E(WGKEY_ERRNO, "errno") \ @@ -52,7 +46,8 @@ static const char *const WG_DYNAMIC_KEY[] = { ITEMS }; #define ITEMS \ E(E_NO_ERROR, "Success") /* must be the first entry */ \ E(E_INVALID_REQ, "Invalid request") \ - E(E_IP_UNAVAIL, "Chosen IP unavailable") + E(E_UNSUPP_PROTO, "Unsupported protocol") \ + E(E_IP_UNAVAIL, "Chosen IP(s) unavailable") #define E(x, y) x, enum wg_dynamic_err { ITEMS }; @@ -62,22 +57,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; - struct in6_addr lladdr; 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 { @@ -91,15 +85,12 @@ struct wg_combined_ip { #define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0])) +int 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/docs/ip-request.md b/docs/ip-request.md deleted file mode 100644 index 1852bae..0000000 --- a/docs/ip-request.md +++ /dev/null @@ -1,255 +0,0 @@ - -DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT -DRAFT DRAFT -DRAFT DRAFT -DRAFT 2019-09-27 DRAFT -DRAFT DRAFT -DRAFT DRAFT -DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT - -# Dynamic IP address allocation - -This document describes version 1 of the wg-dynamic `request_ip` -command. - -See protocol.md for a general description of the protocol. - - -## Server - -The wg-dynamic server is a daemon responsible for handing out IP -addresses in form of leases to wg-dynamic clients requesting them. - -Unless the client asks for a specific address, one is picked uniformly -random from a pool of free addresses and is handed out to wg-dynamic -clients upon request. - -A pool of available addresses, both IPv4 and IPv6, is being maintained -by the server. The pool is populated with the list of addresses being -routed over the wg interface and updated as leases are being granted -and expire. The first and last IPv4 addresses of an address block with -a prefix length less than 31 bits are not handed out. - -A request for a specific IP address is granted if the address is -present in the pool and the server plans on keeping it in the pool for -the lifetime of the lease. - -A request not including an IP address should result in a lease for one -IPv4 address and one IPv6 address. The server is allowed to impose -rate limiting on the churn of addresses granted to a given client. - -A given client is at any given time granted at most one IPv4 address -and at most one IPv6 address. - -A lease is valid for a configured amount of time, by default 3600 -seconds. - - -## Client - -The wg-dynamic client is a daemon responsible for requesting IP -address leases from a wg-dynamic server, for a given wg -interface. Requests for leases are sent over the clients wg interface -to a server on a well known IPv6 link local address and well known low -TCP port. The source port used MUST be the same as the well known -server port. - -At any given time, a client has one or zero leases. A lease has one or -zero IPv4 addresses and one or zero IPv6 addresses. - -A client starts refreshing its lease 300 seconds (plus/minus a random -jitter, 0s-30s) before the lease runs out. It keeps refreshing its -lease until it has one with a lifetime longer than 300 seconds. - -### Security - -A client MUST send requests from an IPv6 link local address with -netmask 128 and with the scope of the wg interface. It MUST use the -same low TCP port as being used by the server. This way wg-dynamic -requests are guaranteed to be sent only to and received only from the -configured wg-dynamic server endpoint and only sent by a process with -permissions to bind to a low port. - -TODO: no routing information is being accepted by clients, eg only -configuring /32 and /128 addresses - -## Protocol - -A client requests IP address leasees by sending a request\_ip -command. The request\_ip command has the following optional attribute: - -- ipv4 (optional) - - Omitting this attribute indicates that the client is fine with any - IPv4 address and gets an address by random, given that the IPv4 pool - is not exhausted. - - Sending this attribute with an IPv4 address in CIDR notation as its - value means that the client would like to have this particular - address granted. - - Sending this attribute without a value ("ipv4=") means that the - client does not want an IPv4 address and that any IPv4 address being - allocated for it should be released. - -- ipv6 (optional) - - Omitting this attribute indicates that the client is fine with any - IPv6 address and gets an address by random, given that the IPv6 pool - is not exhausted. - - Sending this attribute with an IPv6 address in CIDR notation as its - value means that the client would like to have this particular - address granted. - - Sending this attribute without a value ("ipv6=") means that the - client does not want an IPv6 address and that any IPv6 address being - allocated for it should be released. - - -The server response to a request\_ip command contains an errno -attribute and, if the request was succesful, up to three attributes -containing the result: - -- ipv4 - - IPv4 address for the client to use. A server MUST offer IPv4 - addresses with a prefix length of 32 bits and a client MUST refuse an - address with a shorter prefix. - -- ipv6 - - IPv6 address for the client to use. A server MUST offer IPv6 - addresses with a prefix length of 128 bits and a client MUST refuse - an address with a shorter prefix. - -- leasestart - - The start time of the lease, in seconds since the epoch. - - A client receiving a response with a leasestart that deviates from - current time of the client by more than 15 seconds MUST use current - time instead of leasestart. - -- leasetime - - The number of seconds that this lease is valid, starting from - leasestart. - - MUST be the current time of the server. - -- errno - - Errno is 0 for a successful operation. - -A point in time is expressed as seconds since the epoch (1970-01-01 -00:00 UTC). When this document talks about current time, the current -time of the computers clock in UTC is what's being refered to. - -If the request fails, errno is != 0 and an errmsg attribute is the -only other attribute in the response: - -- errno - - A positive integer indicating what made the request fail. The - following error codes are defined: - - - 1: Undefined internal error - -- errmsg - - A text suitable for human consumption, describing what made the - request fail. - -### Examples - -#### Example 1 - -Client asking for any IPv4 address and any IPv6 address, receiving -both. This results in two leases being recorded by the client, one per -address. - -client -> server request - - request_ip=1 - -server -> client response - - request_ip=1 - ipv4=192.168.47.11/32 - ipv6=fd00::4711/128 - leasestart=1569234893 - leasetime=1800 - errno=0 - -#### Example 2 - -Client asking for a specific IPv4 address and a specific IPv6 address, -receiving both. - -client -> server request - - request_ip=1 - ipv4=192.168.47.11/32 - ipv6=fd00::4711/128 - -server -> client response - - request_ip=1 - ipv4=192.168.47.11/32 - ipv6=fd00::4711/128 - leasestart=1569236593 - leasetime=1800 - errno=0 - -#### Example 3 - -Client asking for a specific IPv4 address and a specific IPv6 address, -receiving none because both the IPv4 and IPv6 pools are empty. - -client -> server request - - request_ip=1 - ipv4=192.168.47.11/32 - ipv6=fd00::4711/128 - -server -> client response - - request_ip=1 - errno=0 - -#### Example 4 - -Client asking for any IPv4 address and a specific IPv6 address, -receiving no IPv4 address because that pool is empty and another IPv6 -because the requested address is not free. - -client -> server request - - request_ip=1 - ipv6=fd00::4711/128 - -server -> client response - - request_ip=1 - ipv6=fd00::42/128 - leasestart=1569273827 - leasetime=1800 - errno=0 - -#### Example 5 - -Client asking for any IPv4 address and no IPv6 address, receiving an -IPv4 address and no IPv6 address. If the client did have an IPv6 -address allocated at the time of the request it's now been released. - -client -> server request - - request_ip=1 - ipv6= - -server -> client response - - request_ip=1 - ipv4=192.168.47.22/32 - errno=0 diff --git a/docs/protocol.md b/docs/protocol.md deleted file mode 100644 index 4ff9bd9..0000000 --- a/docs/protocol.md +++ /dev/null @@ -1,18 +0,0 @@ -# wg-dynamic protocol - -This document describes version 1 of the wg-dynamic protocol. - -The wg-dynamic protocol runs over a reliable and ordered data stream -between a client and a server. Addressing is done on an upper layer, -typically IP. - -Clients send a request message to a server and the server responds -with a response message. - -Messages are ASCII text with key=value pairs separated by newline -characters. A message ends with two consecutive newline characters. - -The first key=value pair is treated as a command with the key being -the command and the value being the protocol version. The key=value -pairs following the first pair are command attributes. The command in -a response matches the command of the request it's a response to. @@ -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; +} @@ -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 @@ -129,6 +129,9 @@ int main() { #include <string.h> #include <limits.h> +#include "random.h" +#include "siphash.h" + /* compiler specific configuration */ #if UINT_MAX == 0xffffffffu @@ -197,6 +200,7 @@ static const double __ac_HASH_UPPER = 0.77; khint32_t *flags; \ khkey_t *keys; \ khval_t *vals; \ + siphash_key_t siphash_key; \ } kh_##name##_t; #define __KHASH_PROTOTYPES(name, khkey_t, khval_t) \ @@ -210,7 +214,10 @@ static const double __ac_HASH_UPPER = 0.77; #define __KHASH_IMPL(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \ SCOPE kh_##name##_t *kh_init_##name(void) { \ - return (kh_##name##_t*)kcalloc(1, sizeof(kh_##name##_t)); \ + kh_##name##_t *s = kcalloc(1, sizeof(kh_##name##_t)); \ + if (!get_random_bytes((uint8_t *)&s->siphash_key, 16)) \ + return NULL; \ + return s; \ } \ SCOPE void kh_destroy_##name(kh_##name##_t *h) \ { \ @@ -629,20 +636,13 @@ typedef const unsigned char *khwgkey_t; #define kh_wgkey_hash_equal(a, b) (memcmp(a, b, 32) == 0) -static kh_inline khint_t __fnv_1a_32_hash(const unsigned char *wgkey) -{ - khint_t hash = 0x811c9dc5UL; - for (int i = 0; i < 32; ++i) { - hash ^= wgkey[i]; - hash *= 16777619UL; - } - return hash; -} +#define __SIPHASH(key) ((khint32_t) siphash(key, 32, &h->siphash_key)) +#define __SIPHASH_u64(key) ((khint32_t) siphash_1u64(key, &h->siphash_key)) -#define KHASH_SET_INIT_WGKEY(name) \ - KHASH_INIT(name, khwgkey_t, char, 0, __fnv_1a_32_hash, kh_wgkey_hash_equal) +#define KHASH_MAP_INIT_SECURE_INT64(name, khval_t) \ + KHASH_INIT(name, khint64_t, khval_t, 1, __SIPHASH_u64, kh_int64_hash_equal) -#define KHASH_MAP_INIT_WGKEY(name, khval_t) \ - KHASH_INIT(name, khwgkey_t, khval_t, 1, __fnv_1a_32_hash, kh_wgkey_hash_equal) +#define KHASH_MAP_INIT_SECURE_WGKEY(name, khval_t) \ + KHASH_INIT(name, khwgkey_t, khval_t, 1, __SIPHASH, kh_wgkey_hash_equal) #endif /* __AC_KHASH_H */ @@ -12,9 +12,9 @@ #include <stdint.h> #include <stdio.h> #include <stdlib.h> +#include <string.h> #include <sys/socket.h> #include <time.h> -#include <string.h> #include "common.h" #include "dbg.h" @@ -26,11 +26,13 @@ #define TIME_T_MAX (((time_t)1 << (sizeof(time_t) * CHAR_BIT - 2)) - 1) * 2 + 1 -static struct ip_pool pool; +static const char *devname = NULL; +static int ifindex = 0; +static struct ipns ipns; static time_t gexpires = TIME_T_MAX; static bool synchronized; -KHASH_MAP_INIT_WGKEY(leaseht, struct wg_dynamic_lease *) +KHASH_MAP_INIT_SECURE_WGKEY(leaseht, struct wg_dynamic_lease *) khash_t(leaseht) *leases_ht = NULL; static time_t get_monotonic_time() @@ -52,16 +54,23 @@ static time_t get_monotonic_time() return monotime.tv_sec; } -void leases_init(char *fname, struct mnl_socket *nlsock) +void leases_init(const char *device_name, int interface_index, char *fname, + struct mnl_socket *nlsock) { struct nlmsghdr *nlh; struct rtmsg *rtm; char buf[MNL_NLMSG_HDRLEN + MNL_ALIGN(sizeof *rtm)]; unsigned int seq; + devname = device_name; + ifindex = interface_index; + synchronized = false; leases_ht = kh_init(leaseht); - ipp_init(&pool); + if (!leases_ht) + fatal("kh_init()"); + + ipp_init(&ipns); nlh = mnl_nlmsg_put_header(buf); nlh->nlmsg_type = RTM_GETROUTE; @@ -88,187 +97,210 @@ void leases_free() } kh_destroy(leaseht, leases_ht); - ipp_free(&pool); + ipp_free(&ipns); } -static struct wg_dynamic_lease *new_lease(const struct wg_dynamic_lease *lease, - const struct in_addr *ipv4, - const struct in6_addr *ipv6) +struct allowedips_update { + wg_key peer_pubkey; + struct wg_dynamic_lease *lease; +}; + +static char *updates_to_str(const struct allowedips_update *u) { - struct wg_dynamic_lease *newlease; + static char buf[4096]; + wg_key_b64_string pubkey_asc; + char ll[INET6_ADDRSTRLEN] = { 0 }, v4[INET_ADDRSTRLEN] = { 0 }, + v6[INET6_ADDRSTRLEN] = { 0 }; - newlease = calloc(1, sizeof(*newlease)); - if (!newlease) - fatal("calloc()"); + if (!u) + return "(null)"; - if (!lease_is_valid(lease)) - return newlease; + wg_key_to_base64(pubkey_asc, u->peer_pubkey); + inet_ntop(AF_INET, &u->lease->ipv4, v4, sizeof v4); + inet_ntop(AF_INET6, &u->lease->ipv6, v6, sizeof v6); + inet_ntop(AF_INET6, &u->lease->lladdr, ll, sizeof ll); + snprintf(buf, sizeof buf, "(%p) [%s] %s [%s]", u->lease, ll, v4, v6); - if (ipv4 && ipv4->s_addr && ipv4->s_addr == lease->ipv4.s_addr) { - char ip_asc[INET_ADDRSTRLEN]; - inet_ntop(AF_INET, ipv4, ip_asc, sizeof(ip_asc)); - debug("extending %s\n", ip_asc); + return buf; +} - newlease->ipv4 = lease->ipv4; - } +static void update_allowed_ips_bulk(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; + + BUG_ON(nupdates > WG_DYNAMIC_LEASE_CHUNKSIZE); + for (int i = 0; i < nupdates; i++) { + debug("setting allowedips for %s\n", + updates_to_str(&updates[i])); + + peers[i].flags |= WGPEER_REPLACE_ALLOWEDIPS; + memcpy(peers[i].public_key, updates[i].peer_pubkey, + sizeof(wg_key)); + wg_allowedip **aipp = &peers[i].first_allowedip; - if (ipv6 && !IN6_IS_ADDR_UNSPECIFIED(ipv6) && - IN6_ARE_ADDR_EQUAL(ipv6, &lease->ipv6)) { - char ip_asc[INET6_ADDRSTRLEN]; - inet_ntop(AF_INET6, ipv6, ip_asc, sizeof(ip_asc)); - debug("extending %s\n", ip_asc); + if (!IN6_IS_ADDR_UNSPECIFIED(&updates[i].lease->lladdr)) { + allowedips[i * 3 + 0] = (wg_allowedip){ + .family = AF_INET6, + .cidr = 128, + .ip6 = updates[i].lease->lladdr, + }; + *aipp = &allowedips[i * 3 + 0]; + aipp = &allowedips[i * 3 + 0].next_allowedip; + } + if (updates[i].lease->ipv4.s_addr) { + allowedips[i * 3 + 1] = (wg_allowedip){ + .family = AF_INET, + .cidr = 32, + .ip4 = updates[i].lease->ipv4, + }; + *aipp = &allowedips[i * 3 + 1]; + aipp = &allowedips[i * 3 + 1].next_allowedip; + } + if (!IN6_IS_ADDR_UNSPECIFIED(&updates[i].lease->ipv6)) { + allowedips[i * 3 + 2] = (wg_allowedip){ + .family = AF_INET6, + .cidr = 128, + .ip6 = updates[i].lease->ipv6, + }; + *aipp = &allowedips[i * 3 + 2]; + } - newlease->ipv6 = lease->ipv6; + *pp = &peers[i]; + pp = &peers[i].next_peer; } - return newlease; + strncpy(dev.name, devname, sizeof(dev.name) - 1); + if (wg_set_device(&dev)) + fatal("wg_set_device()"); +} + +/* Updates allowedips for peer_pubkey, adding what's in lease + * (including lladdr), removing all others. + */ +static void update_allowed_ips(wg_key peer_pubkey, + struct wg_dynamic_lease *lease) +{ + struct allowedips_update update; + + memcpy(update.peer_pubkey, peer_pubkey, sizeof(wg_key)); + update.lease = lease; + + update_allowed_ips_bulk(&update, 1); } -struct wg_dynamic_lease *set_lease(const char *devname, wg_key pubkey, - uint32_t leasetime, +struct wg_dynamic_lease *set_lease(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 *current, *new; - uint64_t index_l; - uint32_t index, index_h; + bool delete_ipv4 = !ipv4 || (ipv4 && !ipv4->s_addr); + bool delete_ipv6 = !ipv6 || (ipv6 && IN6_IS_ADDR_UNSPECIFIED(ipv6)); + struct wg_dynamic_lease *lease; struct timespec tp; khiter_t k; - int ret; - bool wants_ipv4 = !ipv4 || ipv4->s_addr; - bool wants_ipv6 = !ipv6 || !IN6_IS_ADDR_UNSPECIFIED(ipv6); - wg_key_b64_string pubkey_asc; - wg_key_to_base64(pubkey_asc, pubkey); - - current = get_leases(pubkey); - new = new_lease(current, ipv4, ipv6); - - if (current) { - char ip_asc[INET6_ADDRSTRLEN]; + int kh_ret; - if (current->ipv4.s_addr && !new->ipv4.s_addr) { - inet_ntop(AF_INET, ¤t->ipv4, ip_asc, - sizeof(ip_asc)); - debug("deleting from pool: %s\n", ip_asc); - - if (ipp_del_v4(&pool, ¤t->ipv4, 32)) - die("ipp_del_v4()\n"); - } + lease = get_leases(pubkey); + if (!lease) { + lease = calloc(1, sizeof(*lease)); + lease->lladdr = *lladdr; + } - if (!IN6_IS_ADDR_UNSPECIFIED(¤t->ipv6) && - IN6_IS_ADDR_UNSPECIFIED(&new->ipv6)) { - inet_ntop(AF_INET6, ¤t->ipv6, ip_asc, - sizeof(ip_asc)); - debug("deleting from pool: %s\n", ip_asc); + if (delete_ipv4 && lease->ipv4.s_addr) { + if (ipp_del_v4(&ipns, &lease->ipv4, 32)) + die("ipp_del_v4()\n"); + memset(&lease->ipv4, 0, sizeof(lease->ipv4)); + } - if (ipp_del_v6(&pool, ¤t->ipv6, 128)) - die("ipp_del_v6()\n"); - } + if (delete_ipv6 && !IN6_IS_ADDR_UNSPECIFIED(&lease->ipv6)) { + if (ipp_del_v6(&ipns, &lease->ipv6, 128)) + die("ipp_del_v6()\n"); + memset(&lease->ipv6, 0, sizeof(lease->ipv6)); } - if (wants_ipv4 && !new->ipv4.s_addr) { - if (!pool.total_ipv4) { + if (ipv4 && !ipv4->s_addr) { + if (!ipns.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, &new->ipv4, index); + memset(&lease->ipv4, 0, sizeof(lease->ipv4)); } else { - char ip_asc[INET_ADDRSTRLEN]; - inet_ntop(AF_INET, ipv4, ip_asc, sizeof(ip_asc)); - debug("wants %s: ", ip_asc); - - if (!ipp_add_v4(&pool, ipv4, 32)) { - debug("allocated\n"); - - new->ipv4 = *ipv4; + uint32_t index = random_bounded(ipns.total_ipv4); + debug("new_lease(v4): %u of %ju\n", index, + ipns.total_ipv4); + ipp_addnth_v4(&ipns, &lease->ipv4, index); + } + } else if (ipv4) { + if (!memcmp(&lease->ipv4, ipv4, sizeof(*ipv4))) { + debug("extending(v4)\n"); + } else { + if (!ipp_add_v4(&ipns, ipv4, 32)) { + lease->ipv4 = *ipv4; } else { - debug("not free\n"); + memset(&lease->ipv4, 0, sizeof(lease->ipv4)); } } } - if (wants_ipv6 && IN6_IS_ADDR_UNSPECIFIED(&new->ipv6)) { - if (!pool.totalh_ipv6 && !pool.totall_ipv6) { + if (ipv6 && IN6_IS_ADDR_UNSPECIFIED(ipv6)) { + if (!ipns.totalh_ipv6 && !ipns.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); + memset(&lease->ipv6, 0, sizeof(lease->ipv6)); + } else { + uint64_t index_l; + uint32_t index_h; + if (ipns.totalh_ipv6 > 0) { + index_l = random_u64(); + index_h = random_bounded(ipns.totalh_ipv6); } else { - index_l = random_bounded(pool.totall_ipv6 - 1); + index_l = random_bounded(ipns.totall_ipv6); index_h = 0; } debug("new_lease(v6): %u:%ju of %u:%ju\n", index_h, - index_l, pool.totalh_ipv6, pool.totall_ipv6); - ipp_addnth_v6(&pool, &new->ipv6, index_l, index_h); + index_l, ipns.totalh_ipv6, ipns.totall_ipv6); + ipp_addnth_v6(&ipns, &lease->ipv6, index_l, index_h); + } + } else if (ipv6) { + if (!memcmp(&lease->ipv6, ipv6, sizeof(*ipv6))) { + debug("extending(v6)\n"); } else { - char ip_asc[INET6_ADDRSTRLEN]; - inet_ntop(AF_INET6, ipv6, ip_asc, sizeof(ip_asc)); - debug("wants %s: ", ip_asc); - - if (!ipp_add_v6(&pool, ipv6, 128)) { - debug("allocated\n"); - - new->ipv6 = *ipv6; + if (!ipp_add_v6(&ipns, ipv6, 128)) { + lease->ipv6 = *ipv6; } else { - debug("not free\n"); + memset(&lease->ipv6, 0, sizeof(lease->ipv6)); } } } - new->lladdr = *lladdr; - update_allowed_ips(devname, pubkey, new); - - if (!new->ipv4.s_addr && IN6_IS_ADDR_UNSPECIFIED(&new->ipv6)) { - khiter_t k = kh_get(leaseht, leases_ht, pubkey); - if (k != kh_end(leases_ht)) { - BUG_ON(!current); - BUG_ON(kh_value(leases_ht, k) != current); - debug("freeing lease: %s\n", lease_to_str(current)); - free(current); - free((char *)kh_key(leases_ht, k)); - kh_del(leaseht, leases_ht, k); - } - - free(new); - return NULL; - } + update_allowed_ips(pubkey, lease); if (clock_gettime(CLOCK_REALTIME, &tp)) fatal("clock_gettime(CLOCK_REALTIME)"); - new->start_real = tp.tv_sec; - new->start_mono = get_monotonic_time(); - new->leasetime = leasetime; + lease->start_real = tp.tv_sec; + lease->start_mono = get_monotonic_time(); + lease->leasetime = leasetime; wg_key *pubcopy = malloc(sizeof(wg_key)); if (!pubcopy) fatal("malloc()"); memcpy(pubcopy, pubkey, sizeof(wg_key)); - k = kh_put(leaseht, leases_ht, *pubcopy, &ret); - if (ret < 0) { - fatal("kh_put()"); - } else if (ret == 0) { - BUG_ON(!current); - BUG_ON(kh_value(leases_ht, k) != current); - debug("freeing lease (replace): %s\n", lease_to_str(current)); - free(current); - } - kh_value(leases_ht, k) = new; + k = kh_put(leaseht, leases_ht, *pubcopy, &kh_ret); + + if (kh_ret < 0) + die("kh_put(): %d\n", kh_ret); - debug("new lease: %s\n", lease_to_str(new)); + kh_value(leases_ht, k) = lease; - if (new->start_mono + new->leasetime < gexpires) - gexpires = new->start_mono + new->leasetime; + if (lease->start_mono + lease->leasetime < gexpires) + gexpires = lease->start_mono + lease->leasetime; /* TODO: add record to file */ - return new; + return lease; } struct wg_dynamic_lease *get_leases(wg_key pubkey) @@ -281,105 +313,7 @@ struct wg_dynamic_lease *get_leases(wg_key pubkey) return kh_val(leases_ht, k); } -struct allowedips_update { - wg_key peer_pubkey; - struct in6_addr lladdr; - struct in_addr ipv4; - struct in6_addr ipv6; -}; - -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; -} - -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) -{ - 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(const char *devname) +int leases_refresh() { time_t cur_time = get_monotonic_time(); struct allowedips_update updates[WG_DYNAMIC_LEASE_CHUNKSIZE] = { 0 }; @@ -397,15 +331,19 @@ int leases_refresh(const char *devname) 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 (lease->ipv4.s_addr) { + ipp_del_v4(&ipns, &lease->ipv4, 32); + memset(&lease->ipv4, 0, sizeof(lease->ipv4)); + } - if (!IN6_IS_ADDR_UNSPECIFIED(&lease->ipv6)) - ipp_del_v6(&pool, &lease->ipv6, 128); + if (!IN6_IS_ADDR_UNSPECIFIED(&lease->ipv6)) { + ipp_del_v6(&ipns, &lease->ipv6, 128); + memset(&lease->ipv6, 0, sizeof(lease->ipv6)); + } memcpy(updates[i].peer_pubkey, kh_key(leases_ht, k), sizeof(wg_key)); - updates[i].lladdr = lease->lladdr; + updates[i].lease = lease; wg_key_b64_string pubkey_asc; wg_key_to_base64(pubkey_asc, updates[i].peer_pubkey); @@ -413,12 +351,12 @@ int leases_refresh(const char *devname) ++i; if (i == WG_DYNAMIC_LEASE_CHUNKSIZE) { - update_allowed_ips_bulk(devname, updates, i); - i = 0; + update_allowed_ips_bulk(updates, i); + while (i) + free(updates[--i].lease); memset(updates, 0, sizeof updates); } - free(lease); free((char *)kh_key(leases_ht, k)); kh_del(leaseht, leases_ht, k); } else { @@ -427,8 +365,11 @@ int leases_refresh(const char *devname) } } - if (i) - update_allowed_ips_bulk(devname, updates, i); + if (i) { + update_allowed_ips_bulk(updates, i); + while (i) + free(updates[--i].lease); + } return MIN(INT_MAX / 1000, gexpires - cur_time); } @@ -440,7 +381,7 @@ static int data_ipv4_attr_cb(const struct nlattr *attr, void *data) switch (type) { case RTA_DST: - case RTA_GATEWAY: + case RTA_OIF: if (mnl_attr_validate(attr, MNL_TYPE_U32) < 0) { log_err("mnl_attr_validate: %s\n", strerror(errno)); return MNL_CB_ERROR; @@ -460,9 +401,14 @@ static int data_ipv6_attr_cb(const struct nlattr *attr, void *data) switch (type) { case RTA_DST: - case RTA_GATEWAY: if (mnl_attr_validate2(attr, MNL_TYPE_BINARY, sizeof(struct in6_addr)) < 0) { + log_err("mnl_attr_validate2: %s\n", strerror(errno)); + return MNL_CB_ERROR; + } + break; + case RTA_OIF: + if (mnl_attr_validate(attr, MNL_TYPE_U32) < 0) { log_err("mnl_attr_validate: %s\n", strerror(errno)); return MNL_CB_ERROR; } @@ -478,13 +424,22 @@ static int process_nlpacket_cb(const struct nlmsghdr *nlh, void *data) { struct nlattr *tb[RTA_MAX + 1] = {}; struct rtmsg *rm = mnl_nlmsg_get_payload(nlh); - UNUSED(data); + uint32_t ifindex; + + BUG_ON(!data); + ifindex = *((int *)data); if (rm->rtm_family == AF_INET) mnl_attr_parse(nlh, sizeof(*rm), data_ipv4_attr_cb, tb); else if (rm->rtm_family == AF_INET6) mnl_attr_parse(nlh, sizeof(*rm), data_ipv6_attr_cb, tb); + if (!tb[RTA_OIF] || mnl_attr_get_u32(tb[RTA_OIF]) != ifindex) { + debug("ignoring interface %u (want %u)\n", + tb[RTA_OIF] ? mnl_attr_get_u32(tb[RTA_OIF]) : 0, ifindex); + return MNL_CB_OK; + } + if (tb[RTA_GATEWAY]) return MNL_CB_OK; @@ -500,18 +455,20 @@ static int process_nlpacket_cb(const struct nlmsghdr *nlh, void *data) if (nlh->nlmsg_type == RTM_NEWROUTE) { if (rm->rtm_family == AF_INET) { - if (ipp_addpool_v4(&pool, addr, rm->rtm_dst_len)) + if (ipp_addpool_v4(&ipns, addr, rm->rtm_dst_len)) die("ipp_addpool_v4()\n"); } else if (rm->rtm_family == AF_INET6) { - if (ipp_addpool_v6(&pool, addr, rm->rtm_dst_len)) + if (ipp_addpool_v6(&ipns, addr, rm->rtm_dst_len)) die("ipp_addpool_v6()\n"); } } else if (nlh->nlmsg_type == RTM_DELROUTE) { if (rm->rtm_family == AF_INET) { - if (ipp_removepool_v4(&pool, addr) && synchronized) + if (ipp_removepool_v4(&ipns, addr, rm->rtm_dst_len) && + synchronized) die("ipp_removepool_v4()\n"); } else if (rm->rtm_family == AF_INET6) { - if (ipp_removepool_v6(&pool, addr) && synchronized) + if (ipp_removepool_v6(&ipns, addr, rm->rtm_dst_len) && + synchronized) die("ipp_removepool_v6()\n"); } } @@ -525,39 +482,11 @@ void leases_update_pools(struct mnl_socket *nlsock) char buf[MNL_SOCKET_BUFFER_SIZE]; while ((ret = mnl_socket_recvfrom(nlsock, buf, sizeof buf)) > 0) { - if (mnl_cb_run(buf, ret, 0, 0, process_nlpacket_cb, NULL) == -1) + if (mnl_cb_run(buf, ret, 0, 0, process_nlpacket_cb, + (void *)&ifindex) == -1) fatal("mnl_cb_run()"); } 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 @@ -28,7 +28,8 @@ struct wg_dynamic_lease { * Initializes internal state, retrieves routes from nlsock and reads leases * from fname. */ -void leases_init(char *fname, struct mnl_socket *nlsock); +void leases_init(const char *device_name, int interface_index, char *fname, + struct mnl_socket *nlsock); /* * Frees everything, closes file. @@ -41,8 +42,7 @@ void leases_free(); * taken. Frees currently held lease, if any. Updates allowedips for * the peer. */ -struct wg_dynamic_lease *set_lease(const char *devname, wg_key pubkey, - uint32_t leasetime, +struct wg_dynamic_lease *set_lease(wg_key pubkey, uint32_t leasetime, const struct in6_addr *lladdr, const struct in_addr *ipv4, const struct in6_addr *ipv6); @@ -55,25 +55,11 @@ struct wg_dynamic_lease *get_leases(wg_key pubkey); /* 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(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); +int leases_refresh(); /* * 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); - -char *lease_to_str(const struct wg_dynamic_lease *l); - #endif diff --git a/radix-trie.c b/radix-trie.c index 25bdd75..678f3be 100644 --- a/radix-trie.c +++ b/radix-trie.c @@ -20,19 +20,23 @@ #define __aligned(x) __attribute__((aligned(x))) #endif +enum radix_node_flags { + RNODE_IS_LEAF = 1U << 0, + RNODE_IS_POOLNODE = 1U << 1, + RNODE_IS_SHADOWED = 1U << 2, +}; + struct radix_node { struct radix_node *bit[2]; uint64_t left; uint64_t right; uint8_t bits[16]; - uint8_t cidr, bit_at_a, bit_at_b; - bool is_leaf; + uint8_t cidr, bit_at_a, bit_at_b, flags; }; struct radix_pool { struct radix_node *node; struct radix_pool *next; - bool shadowed; }; static unsigned int fls64(uint64_t x) @@ -92,7 +96,7 @@ static struct radix_node *new_node(const uint8_t *key, uint8_t cidr, node->bit_at_a ^= (bits / 8U - 1U) % 8U; #endif node->bit_at_b = 7U - (cidr % 8U); - node->is_leaf = false; + node->flags = 0; if (bits - cidr > 0 && bits - cidr - 1 < 64) node->left = node->right = 1ULL << (bits - cidr - 1); else @@ -117,26 +121,6 @@ static bool prefix_matches(const struct radix_node *node, const uint8_t *key, #define CHOOSE_NODE(parent, key) \ (parent)->bit[(key[(parent)->bit_at_a] >> (parent)->bit_at_b) & 1] -static bool node_placement(struct radix_node *trie, const uint8_t *key, - uint8_t cidr, uint8_t bits, - struct radix_node **rnode) -{ - struct radix_node *node = trie, *parent = NULL; - bool exact = false; - - while (node && node->cidr <= cidr && prefix_matches(node, key, bits)) { - parent = node; - if (parent->cidr == cidr) { - exact = true; - break; - } - - node = CHOOSE_NODE(parent, key); - } - *rnode = parent; - return exact; -} - static uint64_t subnet_diff(uint8_t *ip1, uint8_t *ip2, uint8_t bits) { if (bits == 32) @@ -145,6 +129,14 @@ static uint64_t subnet_diff(uint8_t *ip1, uint8_t *ip2, uint8_t bits) return *(const uint64_t *)&ip1[8] - *(const uint64_t *)&ip2[8]; } +static uint64_t taken_ips(struct radix_node *node, uint8_t bits) +{ + if ((bits - node->cidr) >= 64) + return 0; + + return (1ULL << (bits - node->cidr)) - (node->left + node->right); +} + static void add_nth(struct radix_node *start, uint8_t bits, uint64_t n, uint8_t *dest) { @@ -201,7 +193,7 @@ static void add_nth(struct radix_node *start, uint8_t bits, uint64_t n, } newnode = new_node(ip, cidr, bits); - newnode->is_leaf = true; + newnode->flags |= RNODE_IS_LEAF; swap_endian(dest, (const uint8_t *)ip, bits); if (!target) { @@ -214,43 +206,64 @@ static void add_nth(struct radix_node *start, uint8_t bits, uint64_t n, CHOOSE_NODE(between, newnode->bits) = newnode; CHOOSE_NODE(parent, between->bits) = between; - between->left -= - (1ULL << (bits - between->bit[0]->cidr)) - - (between->bit[0]->left + between->bit[0]->right); - between->right -= - (1ULL << (bits - between->bit[1]->cidr)) - - (between->bit[1]->left + between->bit[1]->right); + between->left -= taken_ips(between->bit[0], bits); + between->right -= taken_ips(between->bit[1], bits); } } -static int add(struct radix_node **trie, uint8_t bits, const uint8_t *key, - uint8_t cidr, bool is_leaf) +static struct radix_node *add(struct radix_node **trie, uint8_t bits, + const uint8_t *key, uint8_t cidr, uint8_t type) { - struct radix_node *node, *newnode, *down, *parent; + struct radix_node *node = NULL, *newnode, *down, *parent, *tmp = *trie; + bool exact = false, in_pool = false; - if (cidr > bits) - return -EINVAL; + if (cidr > bits) { + errno = EINVAL; + return NULL; + } if (!*trie) { + if (type & RNODE_IS_LEAF) { + errno = ENOENT; + return NULL; + } + *trie = new_node(key, cidr, bits); - (*trie)->is_leaf = is_leaf; - return 0; + (*trie)->flags = type; + return *trie; } - if (node_placement(*trie, key, cidr, bits, &node)) { - /* exact match, so use the existing node */ - if (node->is_leaf) - return 1; + while (tmp && tmp->cidr <= cidr && prefix_matches(tmp, key, bits)) { + node = tmp; + if (tmp->flags & RNODE_IS_POOLNODE) + in_pool = true; - node->is_leaf = is_leaf; - return 0; + if (node->cidr == cidr) { + exact = true; + break; + } + + tmp = CHOOSE_NODE(node, key); } - if (node && node->is_leaf) - return 1; + if (!in_pool && (type & RNODE_IS_LEAF)) { + errno = ENOENT; + return NULL; + } + + if (exact) { + /* exact match, so use the existing node */ + if (node->flags & type) { + errno = EEXIST; + return NULL; + } + + node->flags = type; + return node; + } newnode = new_node(key, cidr, bits); - newnode->is_leaf = is_leaf; + newnode->flags = type; if (!node) { down = *trie; @@ -259,7 +272,7 @@ static int add(struct radix_node **trie, uint8_t bits, const uint8_t *key, if (!down) { CHOOSE_NODE(node, key) = newnode; - return 0; + return newnode; } } cidr = MIN(cidr, common_bits(down, key, bits)); @@ -276,13 +289,19 @@ static int add(struct radix_node **trie, uint8_t bits, const uint8_t *key, CHOOSE_NODE(node, down->bits) = down; CHOOSE_NODE(node, newnode->bits) = newnode; + + if (CHOOSE_NODE(node, down->bits) == node->bit[0]) + node->left -= taken_ips(down, bits); + else + node->right -= taken_ips(down, bits); + if (!parent) *trie = node; else CHOOSE_NODE(parent, node->bits) = node; } - return 0; + return newnode; } static void radix_free_nodes(struct radix_node *node) @@ -322,15 +341,15 @@ static int insert_v4(struct radix_node **root, const struct in_addr *ip, { /* Aligned so it can be passed to fls */ uint8_t key[4] __aligned(__alignof(uint32_t)); - int ret; swap_endian(key, (const uint8_t *)ip, 32); - ret = add(root, 32, key, cidr, true); - if (!ret) + if (add(root, 32, key, cidr, RNODE_IS_LEAF)) { decrement_radix(*root, 32, (uint8_t *)key); + return 0; + } - return ret; + return -1; } static int insert_v6(struct radix_node **root, const struct in6_addr *ip, @@ -338,99 +357,128 @@ static int insert_v6(struct radix_node **root, const struct in6_addr *ip, { /* Aligned so it can be passed to fls64 */ uint8_t key[16] __aligned(__alignof(uint64_t)); - int ret; swap_endian(key, (const uint8_t *)ip, 128); - ret = add(root, 128, key, cidr, true); - if (!ret) + if (add(root, 128, key, cidr, RNODE_IS_LEAF)) { decrement_radix(*root, 128, (uint8_t *)key); + return 0; + } - return ret; + return -1; } -static int remove_node(struct radix_node *trie, const uint8_t *key, +static int remove_node(struct radix_node **trie, const uint8_t *key, uint8_t bits) { - struct radix_node **node = &trie, **target = NULL; + struct radix_node **node = trie, **target = NULL; + uint64_t *pnodes[127]; + int i = 0; while (*node && prefix_matches(*node, key, bits)) { - if ((*node)->is_leaf) { + if ((*node)->flags & RNODE_IS_LEAF) { target = node; break; } if (CHOOSE_NODE(*node, key) == (*node)->bit[0]) - ++((*node)->left); + pnodes[i++] = &((*node)->left); else - ++((*node)->right); + pnodes[i++] = &((*node)->right); + BUG_ON(i >= 127); node = &CHOOSE_NODE(*node, key); } if (!target) return 1; /* key not found in trie */ + for (int j = 0; j < i; ++j) + ++(*(pnodes[j])); + + free(*node); *target = NULL; - radix_free_nodes(*node); return 0; } -static void totalip_inc(struct ip_pool *ipp, uint8_t bits, uint8_t val) +static void totalip_inc(struct ipns *ns, uint8_t bits, uint8_t val) { if (bits == 32) { - BUG_ON(val >= 32); - ipp->total_ipv4 += 1ULL << val; + BUG_ON(val > 32); + ns->total_ipv4 += 1ULL << val; } else if (bits == 128) { - uint64_t tmp = ipp->totall_ipv6; + uint64_t tmp = ns->totall_ipv6; BUG_ON(val > 64); - ipp->totall_ipv6 += (val == 64) ? 0 : 1ULL << val; - if (ipp->totall_ipv6 <= tmp) - ++ipp->totalh_ipv6; + ns->totall_ipv6 += (val == 64) ? 0 : 1ULL << val; + if (ns->totall_ipv6 <= tmp) + ++ns->totalh_ipv6; } } -static void totalip_dec(struct ip_pool *ipp, uint8_t bits, uint8_t val) +static void shadow_nodes(struct radix_node *node) { - if (bits == 32) { - BUG_ON(val >= 32); - ipp->total_ipv4 -= 1ULL << val; - } else if (bits == 128) { - uint64_t tmp = ipp->totall_ipv6; - BUG_ON(val > 64); - ipp->totall_ipv6 -= (val == 64) ? 0 : 1ULL << val; - if (ipp->totall_ipv6 >= tmp) - --ipp->totalh_ipv6; + if (!node) + return; + + if (node->flags & RNODE_IS_POOLNODE) { + BUG_ON(node->flags & RNODE_IS_SHADOWED); + node->flags |= RNODE_IS_SHADOWED; + return; } + + if (node->flags & RNODE_IS_LEAF) + return; + + shadow_nodes(node->bit[0]); + shadow_nodes(node->bit[1]); } -static int ipp_addpool(struct ip_pool *ipp, struct radix_pool **pool, +static int ipp_addpool(struct ipns *ns, struct radix_pool **pool, struct radix_node **root, uint8_t bits, const uint8_t *key, uint8_t cidr) { + struct radix_node **node = root, *newnode; struct radix_pool *newpool; - struct radix_node *node; - bool shadowed = false; - - while (*pool) { - node = (*pool)->node; - - if (common_bits(node, key, bits) >= MIN(cidr, node->cidr)) { - if (cidr > node->cidr) { - shadowed = true; - } else if (cidr < node->cidr && !(*pool)->shadowed) { - (*pool)->shadowed = true; - totalip_dec(ipp, bits, bits - cidr); - } else { - return -1; - } + bool shadow = false, good_match = false; + uint8_t flags; + + while (*node && (*node)->cidr <= cidr && + prefix_matches(*node, key, bits)) { + if ((*node)->cidr == cidr) { + good_match = true; + break; } - pool = &(*pool)->next; + if ((*node)->flags & RNODE_IS_POOLNODE) + shadow = true; + + node = &CHOOSE_NODE(*node, key); } - BUG_ON(add(root, bits, key, cidr, false)); + flags = RNODE_IS_POOLNODE | (shadow ? RNODE_IS_SHADOWED : 0); + + if (good_match) { + if ((*node)->flags & RNODE_IS_POOLNODE) + return -1; /* already exists */ + + BUG_ON((*node)->flags & RNODE_IS_SHADOWED); + (*node)->flags |= flags; + + newnode = *node; + } else { + newnode = add(node, bits, key, cidr, flags); + if (newnode->bit[0]) + newnode->left -= taken_ips(newnode->bit[0], bits); + + if (newnode->bit[1]) + newnode->right -= taken_ips(newnode->bit[1], bits); + } + + if (!shadow) { + shadow_nodes(newnode->bit[0]); + shadow_nodes(newnode->bit[1]); + } if (bits == 32) { /* TODO: insert network address (0) and broadcast address (255) @@ -438,23 +486,103 @@ static int ipp_addpool(struct ip_pool *ipp, struct radix_pool **pool, /* TODO: special case /31 ?, see RFC 3021 */ } - if (!shadowed) - totalip_inc(ipp, bits, bits - cidr); + if (!shadow) + totalip_inc(ns, bits, bits - cidr); newpool = malloc(sizeof *newpool); if (!newpool) fatal("malloc()"); - node = *root; - while (node->cidr != cidr) { - node = CHOOSE_NODE(node, key); + newpool->node = newnode; + newpool->next = *pool; + *pool = newpool; + + return 0; +} + +static int orphan_nodes(struct radix_node *node, uint64_t *val) +{ + uint64_t v1 = 0, v2 = 0; + + if (!node) + return 0; - BUG_ON(!node || !prefix_matches(node, key, bits)); + if (node->flags & RNODE_IS_POOLNODE) { + BUG_ON(!(node->flags & RNODE_IS_SHADOWED)); + node->flags &= ~RNODE_IS_SHADOWED; + return 0; } - newpool->node = node; - newpool->shadowed = shadowed; - newpool->next = NULL; - *pool = newpool; + + if (node->flags & RNODE_IS_LEAF) { + BUG_ON(node->bit[0] || node->bit[1]); + *val = 1; + free(node); + return 1; + } + + if (orphan_nodes(node->bit[0], &v1)) + node->bit[0] = NULL; + + if (orphan_nodes(node->bit[1], &v2)) + node->bit[1] = NULL; + + node->left += v1; + node->right += v2; + *val = v1 + v2; + + if (node->bit[0] || node->bit[1]) + return 0; /* still need this node */ + + free(node); + return 1; +} + +static int ipp_removepool(struct ipns *ns, uint8_t bits, const uint8_t *key, + uint8_t cidr) +{ + struct radix_pool **current, *next; + struct radix_node *node; + + for (current = &ns->ip4_pools; *current; current = &(*current)->next) { + struct radix_node *node = (*current)->node; + if (node->cidr == cidr && common_bits(node, key, bits) >= cidr) + break; + } + + if (!*current) + return -1; + + node = (*current)->node; + + if (node->flags & RNODE_IS_SHADOWED) { + node->flags &= ~RNODE_IS_SHADOWED; + } else { + struct radix_node *n = ns->ip4_root; + uint64_t v1 = 0, v2 = 0; + + if (orphan_nodes(node->bit[0], &v1)) + node->bit[0] = NULL; + + if (orphan_nodes(node->bit[1], &v2)) + node->bit[1] = NULL; + + node->left += v1; + node->right += v2; + + while (n && n->cidr < cidr && prefix_matches(n, key, bits)) { + if (n->bit[0] == CHOOSE_NODE(n, key)) + n->left += v1 + v2; + else + n->right += v1 + v2; + + n = CHOOSE_NODE(n, key); + } + } + + node->flags &= ~RNODE_IS_POOLNODE; + next = (*current)->next; + free(*current); + *current = next; return 0; } @@ -498,101 +626,103 @@ static void debug_print_trie(struct radix_node *root, uint8_t bits) node_to_str(root->bit[0], child1, bits); node_to_str(root->bit[1], child2, bits); - debug("%s (%zu, %zu) -> %s, %s\n", parent, root->left, root->right, - child1, child2); + debug("%s (%zu, %zu, %c%c%c) -> %s, %s\n", parent, root->left, + root->right, root->flags & RNODE_IS_LEAF ? 'l' : '-', + root->flags & RNODE_IS_POOLNODE ? 'p' : '-', + root->flags & RNODE_IS_SHADOWED ? 's' : '-', child1, child2); debug_print_trie(root->bit[0], bits); debug_print_trie(root->bit[1], bits); } -void debug_print_trie_v4(struct ip_pool *pool) +void debug_print_trie_v4(struct ipns *ns) { - debug_print_trie(pool->ip4_root, 32); + debug_print_trie(ns->ip4_root, 32); } -void debug_print_trie_v6(struct ip_pool *pool) +void debug_print_trie_v6(struct ipns *ns) { - debug_print_trie(pool->ip6_root, 128); + debug_print_trie(ns->ip6_root, 128); } #endif -void ipp_init(struct ip_pool *pool) +void ipp_init(struct ipns *ns) { - pool->ip4_root = pool->ip6_root = NULL; - pool->ip4_pool = pool->ip6_pool = NULL; - pool->totall_ipv6 = pool->totalh_ipv6 = pool->total_ipv4 = 0; + ns->ip4_root = ns->ip6_root = NULL; + ns->ip4_pools = ns->ip6_pools = NULL; + ns->totall_ipv6 = ns->totalh_ipv6 = ns->total_ipv4 = 0; } -void ipp_free(struct ip_pool *pool) +void ipp_free(struct ipns *ns) { struct radix_pool *next; - radix_free_nodes(pool->ip4_root); - radix_free_nodes(pool->ip6_root); + radix_free_nodes(ns->ip4_root); + radix_free_nodes(ns->ip6_root); - for (struct radix_pool *cur = pool->ip4_pool; cur; cur = next) { + for (struct radix_pool *cur = ns->ip4_pools; cur; cur = next) { next = cur->next; free(cur); } - for (struct radix_pool *cur = pool->ip6_pool; cur; cur = next) { + for (struct radix_pool *cur = ns->ip6_pools; cur; cur = next) { next = cur->next; free(cur); } } -int ipp_add_v4(struct ip_pool *pool, const struct in_addr *ip, uint8_t cidr) +int ipp_add_v4(struct ipns *ns, const struct in_addr *ip, uint8_t cidr) { - int ret = insert_v4(&pool->ip4_root, ip, cidr); + int ret = insert_v4(&ns->ip4_root, ip, cidr); if (!ret) - --pool->total_ipv4; + --ns->total_ipv4; return ret; } -int ipp_add_v6(struct ip_pool *pool, const struct in6_addr *ip, uint8_t cidr) +int ipp_add_v6(struct ipns *ns, const struct in6_addr *ip, uint8_t cidr) { - int ret = insert_v6(&pool->ip6_root, ip, cidr); + int ret = insert_v6(&ns->ip6_root, ip, cidr); if (!ret) { - if (pool->totall_ipv6 == 0) - --pool->totalh_ipv6; + if (ns->totall_ipv6 == 0) + --ns->totalh_ipv6; - --pool->totall_ipv6; + --ns->totall_ipv6; } return ret; } -int ipp_del_v4(struct ip_pool *pool, const struct in_addr *ip, uint8_t cidr) +int ipp_del_v4(struct ipns *ns, const struct in_addr *ip, uint8_t cidr) { uint8_t key[4] __aligned(__alignof(uint32_t)); int ret; swap_endian(key, (const uint8_t *)ip, 32); - ret = remove_node(pool->ip4_root, key, cidr); + ret = remove_node(&ns->ip4_root, key, cidr); if (!ret) - ++pool->total_ipv4; + ++ns->total_ipv4; return ret; } -int ipp_del_v6(struct ip_pool *pool, const struct in6_addr *ip, uint8_t cidr) +int ipp_del_v6(struct ipns *ns, const struct in6_addr *ip, uint8_t cidr) { uint8_t key[16] __aligned(__alignof(uint64_t)); int ret; swap_endian(key, (const uint8_t *)ip, 128); - ret = remove_node(pool->ip6_root, key, cidr); + ret = remove_node(&ns->ip6_root, key, cidr); if (!ret) { - ++pool->totall_ipv6; - if (pool->totall_ipv6 == 0) - ++pool->totalh_ipv6; + ++ns->totall_ipv6; + if (ns->totall_ipv6 == 0) + ++ns->totalh_ipv6; } return ret; } -int ipp_addpool_v4(struct ip_pool *ipp, const struct in_addr *ip, uint8_t cidr) +int ipp_addpool_v4(struct ipns *ns, const struct in_addr *ip, uint8_t cidr) { uint8_t key[4] __aligned(__alignof(uint32_t)); @@ -600,10 +730,10 @@ int ipp_addpool_v4(struct ip_pool *ipp, const struct in_addr *ip, uint8_t cidr) return -1; swap_endian(key, (const uint8_t *)ip, 32); - return ipp_addpool(ipp, &ipp->ip4_pool, &ipp->ip4_root, 32, key, cidr); + return ipp_addpool(ns, &ns->ip4_pools, &ns->ip4_root, 32, key, cidr); } -int ipp_addpool_v6(struct ip_pool *ipp, const struct in6_addr *ip, uint8_t cidr) +int ipp_addpool_v6(struct ipns *ns, const struct in6_addr *ip, uint8_t cidr) { uint8_t key[16] __aligned(__alignof(uint64_t)); @@ -611,27 +741,37 @@ int ipp_addpool_v6(struct ip_pool *ipp, const struct in6_addr *ip, uint8_t cidr) return -1; swap_endian(key, (const uint8_t *)ip, 128); - return ipp_addpool(ipp, &ipp->ip6_pool, &ipp->ip6_root, 128, key, cidr); + return ipp_addpool(ns, &ns->ip6_pools, &ns->ip6_root, 128, key, cidr); } -/* TODO: implement */ -int ipp_removepool_v4(struct ip_pool *pool, const struct in_addr *ip) +int ipp_removepool_v4(struct ipns *ns, const struct in_addr *ip, uint8_t cidr) { - return 0; + uint8_t key[4] __aligned(__alignof(uint32_t)); + + if (cidr <= 0 || cidr >= 32) + return -1; + + swap_endian(key, (const uint8_t *)ip, 32); + return ipp_removepool(ns, 32, key, cidr); } -/* TODO: implement */ -int ipp_removepool_v6(struct ip_pool *pool, const struct in6_addr *ip) +int ipp_removepool_v6(struct ipns *ns, const struct in6_addr *ip, uint8_t cidr) { - return 0; + uint8_t key[16] __aligned(__alignof(uint64_t)); + + if (cidr < 64 || cidr >= 128) + return -1; + + swap_endian(key, (const uint8_t *)ip, 128); + return ipp_removepool(ns, 128, key, cidr); } -void ipp_addnth_v4(struct ip_pool *pool, struct in_addr *dest, uint32_t index) +void ipp_addnth_v4(struct ipns *ns, struct in_addr *dest, uint32_t index) { - struct radix_pool *current = pool->ip4_pool; + struct radix_pool *current; - for (current = pool->ip4_pool; current; current = current->next) { - if (current->shadowed) + for (current = ns->ip4_pools; current; current = current->next) { + if (current->node->flags & RNODE_IS_SHADOWED) continue; if (index < current->node->left + current->node->right) @@ -643,24 +783,24 @@ void ipp_addnth_v4(struct ip_pool *pool, struct in_addr *dest, uint32_t index) BUG_ON(!current); add_nth(current->node, 32, index, (uint8_t *)&dest->s_addr); - --pool->total_ipv4; + --ns->total_ipv4; } -void ipp_addnth_v6(struct ip_pool *pool, struct in6_addr *dest, - uint32_t index_low, uint64_t index_high) +void ipp_addnth_v6(struct ipns *ns, struct in6_addr *dest, uint32_t index_low, + uint64_t index_high) { - struct radix_pool *current = pool->ip6_pool; + struct radix_pool *current; uint64_t tmp; - while (current) { - if (current->shadowed || - (current->node->left == 0 && current->node->right == 0)) { - current = current->next; + for (current = ns->ip6_pools; current; current = current->next) { + if (current->node->flags & RNODE_IS_SHADOWED || + (current->node->left == 0 && current->node->right == 0)) continue; - } - if (index_high == 0 && - index_low < (current->node->left + current->node->right)) + /* left + right may overflow, so we substract 1 which is safe + * since we ensured it's > 0 except when the total is 2^64 */ + if (index_high == 0 && index_low <= (current->node->left + + current->node->right - 1)) break; tmp = index_low - (current->node->left + current->node->right); @@ -669,15 +809,13 @@ void ipp_addnth_v6(struct ip_pool *pool, struct in6_addr *dest, --index_high; } index_low = tmp; - - current = current->next; } BUG_ON(!current || index_high); add_nth(current->node, 128, index_low, (uint8_t *)&dest->s6_addr); - if (pool->totall_ipv6 == 0) - --pool->totalh_ipv6; + if (ns->totall_ipv6 == 0) + --ns->totalh_ipv6; - --pool->totall_ipv6; + --ns->totall_ipv6; } diff --git a/radix-trie.h b/radix-trie.h index a72ee50..ffaf7ad 100644 --- a/radix-trie.h +++ b/radix-trie.h @@ -10,37 +10,38 @@ #include <stdbool.h> #include <stdint.h> -struct ip_pool { - uint64_t totall_ipv6; - uint32_t totalh_ipv6, total_ipv4; +struct ipns { + /* Total amount of available addresses over all pools */ + uint64_t totall_ipv6, total_ipv4; + uint32_t totalh_ipv6; + struct radix_node *ip4_root, *ip6_root; - struct radix_pool *ip4_pool, *ip6_pool; + struct radix_pool *ip4_pools, *ip6_pools; }; -void ipp_init(struct ip_pool *pool); -void ipp_free(struct ip_pool *pool); +void ipp_init(struct ipns *ns); +void ipp_free(struct ipns *ns); -int ipp_add_v4(struct ip_pool *pool, const struct in_addr *ip, uint8_t cidr); -int ipp_add_v6(struct ip_pool *pool, const struct in6_addr *ip, uint8_t cidr); +int ipp_add_v4(struct ipns *ns, const struct in_addr *ip, uint8_t cidr); +int ipp_add_v6(struct ipns *ns, const struct in6_addr *ip, uint8_t cidr); -int ipp_del_v4(struct ip_pool *pool, const struct in_addr *ip, uint8_t cidr); -int ipp_del_v6(struct ip_pool *pool, const struct in6_addr *ip, uint8_t cidr); +int ipp_del_v4(struct ipns *ns, const struct in_addr *ip, uint8_t cidr); +int ipp_del_v6(struct ipns *ns, const struct in6_addr *ip, uint8_t cidr); -void ipp_addnth_v4(struct ip_pool *pool, struct in_addr *dest, uint32_t index); -void ipp_addnth_v6(struct ip_pool *pool, struct in6_addr *dest, - uint32_t index_low, uint64_t index_high); +void ipp_addnth_v4(struct ipns *ns, struct in_addr *dest, uint32_t index); +void ipp_addnth_v6(struct ipns *ns, struct in6_addr *dest, uint32_t index_low, + uint64_t index_high); -int ipp_addpool_v4(struct ip_pool *ipp, const struct in_addr *ip, uint8_t cidr); -int ipp_addpool_v6(struct ip_pool *ipp, const struct in6_addr *ip, - uint8_t cidr); +int ipp_addpool_v4(struct ipns *ns, const struct in_addr *ip, uint8_t cidr); +int ipp_addpool_v6(struct ipns *ns, const struct in6_addr *ip, uint8_t cidr); -int ipp_removepool_v4(struct ip_pool *pool, const struct in_addr *ip); -int ipp_removepool_v6(struct ip_pool *pool, const struct in6_addr *ip); +int ipp_removepool_v4(struct ipns *ns, const struct in_addr *ip, uint8_t cidr); +int ipp_removepool_v6(struct ipns *ns, const struct in6_addr *ip, uint8_t cidr); #ifdef DEBUG void node_to_str(struct radix_node *node, char *buf, uint8_t bits); -void debug_print_trie_v4(struct ip_pool *pool); -void debug_print_trie_v6(struct ip_pool *pool); +void debug_print_trie_v4(struct ipns *ns); +void debug_print_trie_v6(struct ipns *ns); #endif #endif @@ -27,7 +27,7 @@ #endif #endif -static inline bool __attribute__((__warn_unused_result__)) +bool __attribute__((__warn_unused_result__)) get_random_bytes(uint8_t *out, size_t len) { ssize_t ret = 0; @@ -68,24 +68,27 @@ get_random_bytes(uint8_t *out, size_t len) return i == len; } -uint64_t random_bounded(uint64_t bound) +uint64_t random_u64() { uint64_t ret; + if (!get_random_bytes((uint8_t *)&ret, sizeof(ret))) + fatal("get_random_bytes()"); - if (bound == 0) - return 0; + return ret; +} - if (bound == 1) { - if (!get_random_bytes((uint8_t *)&ret, sizeof(ret))) - fatal("get_random_bytes()"); - return (ret > 0x7FFFFFFFFFFFFFFF) ? 1 : 0; - } +/* Returns a random number [0, bound) (exclusive) */ +uint64_t random_bounded(uint64_t bound) +{ + uint64_t ret, max_mod_bound; + + if (bound < 2) + return 0; - const uint64_t max_mod_bound = (1 + ~bound) % bound; + max_mod_bound = (1 + ~bound) % bound; do { - if (!get_random_bytes((uint8_t *)&ret, sizeof(ret))) - fatal("get_random_bytes()"); + ret = random_u64(); } while (ret < max_mod_bound); return ret % bound; @@ -6,8 +6,12 @@ #ifndef __RANDOM_H__ #define __RANDOM_H__ +#include <stdbool.h> +#include <stddef.h> #include <stdint.h> +bool get_random_bytes(uint8_t *out, size_t len); +uint64_t random_u64(); uint64_t random_bounded(uint64_t bound); #endif diff --git a/siphash.c b/siphash.c new file mode 100644 index 0000000..63cc2c6 --- /dev/null +++ b/siphash.c @@ -0,0 +1,204 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2016-2019 WireGuard LLC. All Rights Reserved. + * + * SipHash: a fast short-input PRF + * https://131002.net/siphash/ + */ + +#include "siphash.h" + +static inline uint64_t rol64(uint64_t word, unsigned int shift) +{ + return (word << shift) | (word >> (64 - shift)); +} + +#define SIPROUND \ + do { \ + v0 += v1; \ + v1 = rol64(v1, 13); \ + v1 ^= v0; \ + v0 = rol64(v0, 32); \ + v2 += v3; \ + v3 = rol64(v3, 16); \ + v3 ^= v2; \ + v0 += v3; \ + v3 = rol64(v3, 21); \ + v3 ^= v0; \ + v2 += v1; \ + v1 = rol64(v1, 17); \ + v1 ^= v2; \ + v2 = rol64(v2, 32); \ + } while (0) + +#define PREAMBLE(len) \ + uint64_t v0 = 0x736f6d6570736575ULL; \ + uint64_t v1 = 0x646f72616e646f6dULL; \ + uint64_t v2 = 0x6c7967656e657261ULL; \ + uint64_t v3 = 0x7465646279746573ULL; \ + uint64_t b = ((uint64_t)(len)) << 56; \ + v3 ^= key->key[1]; \ + v2 ^= key->key[0]; \ + v1 ^= key->key[1]; \ + v0 ^= key->key[0]; + +#define POSTAMBLE \ + v3 ^= b; \ + SIPROUND; \ + SIPROUND; \ + v0 ^= b; \ + v2 ^= 0xff; \ + SIPROUND; \ + SIPROUND; \ + SIPROUND; \ + SIPROUND; \ + return (v0 ^ v1) ^ (v2 ^ v3); + +uint64_t __siphash_aligned(const void *data, size_t len, + const siphash_key_t *key) +{ + const uint8_t *end = data + len - (len % sizeof(uint64_t)); + const uint8_t left = len & (sizeof(uint64_t) - 1); + uint64_t m; + PREAMBLE(len) + for (; data != end; data += sizeof(uint64_t)) { + m = le64toh(*((uint64_t *)data)); + v3 ^= m; + SIPROUND; + SIPROUND; + v0 ^= m; + } + switch (left) { + case 7: + b |= ((uint64_t)end[6]) << 48; /* fall through */ + case 6: + b |= ((uint64_t)end[5]) << 40; /* fall through */ + case 5: + b |= ((uint64_t)end[4]) << 32; /* fall through */ + case 4: + b |= le32toh(*((uint32_t *)data)); + break; + case 3: + b |= ((uint64_t)end[2]) << 16; /* fall through */ + case 2: + b |= le16toh(*((uint16_t *)data)); + break; + case 1: + b |= end[0]; + } + POSTAMBLE +} + +/** + * siphash_1u64 - compute 64-bit siphash PRF value of a u64 + * @first: first u64 + * @key: the siphash key + */ +uint64_t siphash_1u64(const uint64_t first, const siphash_key_t *key) +{ + PREAMBLE(8) + v3 ^= first; + SIPROUND; + SIPROUND; + v0 ^= first; + POSTAMBLE +} + +/** + * siphash_2u64 - compute 64-bit siphash PRF value of 2 u64 + * @first: first u64 + * @second: second u64 + * @key: the siphash key + */ +uint64_t siphash_2u64(const uint64_t first, const uint64_t second, + const siphash_key_t *key) +{ + PREAMBLE(16) + v3 ^= first; + SIPROUND; + SIPROUND; + v0 ^= first; + v3 ^= second; + SIPROUND; + SIPROUND; + v0 ^= second; + POSTAMBLE +} + +/** + * siphash_3u64 - compute 64-bit siphash PRF value of 3 u64 + * @first: first u64 + * @second: second u64 + * @third: third u64 + * @key: the siphash key + */ +uint64_t siphash_3u64(const uint64_t first, const uint64_t second, + const uint64_t third, const siphash_key_t *key) +{ + PREAMBLE(24) + v3 ^= first; + SIPROUND; + SIPROUND; + v0 ^= first; + v3 ^= second; + SIPROUND; + SIPROUND; + v0 ^= second; + v3 ^= third; + SIPROUND; + SIPROUND; + v0 ^= third; + POSTAMBLE +} + +/** + * siphash_4u64 - compute 64-bit siphash PRF value of 4 u64 + * @first: first u64 + * @second: second u64 + * @third: third u64 + * @forth: forth u64 + * @key: the siphash key + */ +uint64_t siphash_4u64(const uint64_t first, const uint64_t second, + const uint64_t third, const uint64_t forth, + const siphash_key_t *key) +{ + PREAMBLE(32) + v3 ^= first; + SIPROUND; + SIPROUND; + v0 ^= first; + v3 ^= second; + SIPROUND; + SIPROUND; + v0 ^= second; + v3 ^= third; + SIPROUND; + SIPROUND; + v0 ^= third; + v3 ^= forth; + SIPROUND; + SIPROUND; + v0 ^= forth; + POSTAMBLE +} + +uint64_t siphash_1u32(const uint32_t first, const siphash_key_t *key) +{ + PREAMBLE(4) + b |= first; + POSTAMBLE +} + +uint64_t siphash_3u32(const uint32_t first, const uint32_t second, + const uint32_t third, const siphash_key_t *key) +{ + uint64_t combined = (uint64_t)second << 32 | first; + PREAMBLE(12) + v3 ^= combined; + SIPROUND; + SIPROUND; + v0 ^= combined; + b |= third; + POSTAMBLE +} diff --git a/siphash.h b/siphash.h new file mode 100644 index 0000000..8fefb31 --- /dev/null +++ b/siphash.h @@ -0,0 +1,85 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2016-2019 WireGuard LLC. All Rights Reserved. + * + * SipHash: a fast short-input PRF + * https://131002.net/siphash/ + */ + +#ifndef _LINUX_SIPHASH_H +#define _LINUX_SIPHASH_H + +#ifndef _DEFAULT_SOURCE +#define _DEFAULT_SOURCE +#endif +#include <endian.h> +#include <stdbool.h> +#include <stdint.h> +#include <stddef.h> + +typedef struct { + uint64_t key[2]; +} siphash_key_t; + +static inline bool siphash_key_is_zero(const siphash_key_t *key) +{ + return !(key->key[0] | key->key[1]); +} + +uint64_t __siphash_aligned(const void *data, size_t len, + const siphash_key_t *key); + +uint64_t siphash_1u64(const uint64_t a, const siphash_key_t *key); +uint64_t siphash_2u64(const uint64_t a, const uint64_t b, + const siphash_key_t *key); +uint64_t siphash_3u64(const uint64_t a, const uint64_t b, const uint64_t c, + const siphash_key_t *key); +uint64_t siphash_4u64(const uint64_t a, const uint64_t b, const uint64_t c, + const uint64_t d, const siphash_key_t *key); +uint64_t siphash_1u32(const uint32_t a, const siphash_key_t *key); +uint64_t siphash_3u32(const uint32_t a, const uint32_t b, const uint32_t c, + const siphash_key_t *key); + +static inline uint64_t siphash_2u32(const uint32_t a, const uint32_t b, + const siphash_key_t *key) +{ + return siphash_1u64((uint64_t)b << 32 | a, key); +} +static inline uint64_t siphash_4u32(const uint32_t a, const uint32_t b, + const uint32_t c, const uint32_t d, + const siphash_key_t *key) +{ + return siphash_2u64((uint64_t)b << 32 | a, (uint64_t)d << 32 | c, key); +} + +static inline uint64_t ___siphash_aligned(const uint64_t *data, size_t len, + const siphash_key_t *key) +{ + if (__builtin_constant_p(len) && len == 4) + return siphash_1u32(le32toh(*((const uint32_t *)data)), key); + if (__builtin_constant_p(len) && len == 8) + return siphash_1u64(le64toh(data[0]), key); + if (__builtin_constant_p(len) && len == 16) + return siphash_2u64(le64toh(data[0]), le64toh(data[1]), key); + if (__builtin_constant_p(len) && len == 24) + return siphash_3u64(le64toh(data[0]), le64toh(data[1]), + le64toh(data[2]), key); + if (__builtin_constant_p(len) && len == 32) + return siphash_4u64(le64toh(data[0]), le64toh(data[1]), + le64toh(data[2]), le64toh(data[3]), key); + return __siphash_aligned(data, len, key); +} + +/** + * siphash - compute 64-bit siphash PRF value + * @data: buffer to hash + * @size: size of @data + * @key: the siphash key + */ +static inline uint64_t siphash(const void *data, size_t len, + const siphash_key_t *key) +{ + return ___siphash_aligned(data, len, key); +} + +#endif /* _LINUX_SIPHASH_H */ diff --git a/tests/clientsh.bash b/tests/clientsh.bash deleted file mode 100755 index 3f297c5..0000000 --- a/tests/clientsh.bash +++ /dev/null @@ -1,302 +0,0 @@ -#! /bin/bash - -# BUG: When running this a second time targeting the same -# wg-dynamic-server, first ncat is hanging on receiving a response. -# tcpdump shows that the response is indeed sent by the server. - -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 -} - -run_k_fill() { - local nclients=$1; shift - local k=$1; shift - local i - - for i in $(seq $nclients); do - req_check $(( 9 + $i )) - done -} - -test_many() { - local func=$1; shift - local NCLIENTS=$1; shift - local k=$1; shift - local n - - for n in $(seq 10 $(( 9+$NCLIENTS ))); do setup_client_peer $n; done - - 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 - $func $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]} != $C4_FIRST_V4" - [[ -z "${IPV6[5]}" ]] || fail "ipv6 not empty: ${IPV6[5]}" - - pretty "" "SUCCESS\n" -} - -test_case_3() { - # Two clients -- 6 and 7. - for i in 6 7; do setup_client_peer $i; done - - pretty 6 "Any v4, any v6" - req_check 6 - [[ ${ERRNO[6]} = 0 ]] || fail "errno: ${ERRNO[6]}" - local C6_FIRST_V4=${IPV4[6]} - local C6_FIRST_V6=${IPV6[6]} - - pretty 6 "Drop v4, extend v6" - req_check 6 "-" $C6_FIRST_V6 - [[ ${ERRNO[6]} = 0 ]] || fail "errno: ${ERRNO[6]} != 0" - [[ ${IPV4[6]} = 0.0.0.0/32 ]] || fail "ipv4: ${IPV4[6]} != 0.0.0.0/32" - [[ ${IPV6[6]} = $C6_FIRST_V6 ]] || fail "ipv6: ${IPV6[6]} != $C6_FIRST_V6" - - pretty "" "SUCCESS\n" -} - -test_case_1 -test_case_2 -test_case_3 - -N_RANDOM=20 -K_RANDOM=4 -[ $# -gt 0 ] && { N_RANDOM=$1; shift; } -[ $# -gt 0 ] && { K_RANDOM=$1; shift; } - -test_many run_k_at_random $N_RANDOM $K_RANDOM -#test_many run_k_fill $N_RANDOM $K_RANDOM diff --git a/tests/netsh.sh b/tests/netsh.sh index f9e9c1e..dd30c2f 100755 --- a/tests/netsh.sh +++ b/tests/netsh.sh @@ -6,84 +6,351 @@ set -e exec 3>&1 - -[ $# -ge 1 ] && ( n_clients="$1"; shift; ) - +netnsprefix="wg-test-$$" export WG_HIDE_KEYS=never -netnsn() { echo wg-test-$$-$1; } +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() { 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" +nn() { + [[ "$1" != "-q" ]] && pretty $1 "$*" || shift + local netns=$(netnsn $1); shift + maybe_exec ip netns exec $netns "$@"; +} +ipn() { + [[ "$1" != "-q" ]] && pretty $1 "ip $*" || shift + local netns=$(netnsn $1); shift + ip -n $netns "$@"; +} 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 +### Server +ip netns del $(netnsn 0) 2>/dev/null || true pp ip netns add $(netnsn 0) +ip netns del $(netnsn 1) 2>/dev/null || true pp ip netns add $(netnsn 1) -pp ip netns add $(netnsn 2) ipn 0 link set up dev lo +ns="0 1" 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() { - ipn 1 addr add fe80::/64 dev wg0 - ipn 2 addr add fe80::badc:0ffe:e0dd:f00d/128 dev 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 - - nn 2 wg set wg0 \ - private-key <(echo $client_private) \ - listen-port 2 \ - peer $server_public \ - allowed-ips 0.0.0.0/0,::/0 - - ipn 1 link set up dev wg0 - ipn 2 link set up 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 + +ipn 1 addr add fe80::/64 dev wg0 +nn 1 wg set wg0 \ + private-key <(echo $server_private) \ + listen-port 1 +ipn 1 link set up dev wg0 + +# Add prefixes to pool. +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 + +# Start server. +nn 1 ./wg-dynamic-server --leasetime 10 wg0 & +sleep 1 # FIXME: synchronise with server output instead? + +### Clients +declare -a IPV4 +declare -a IPV6 +declare -a LEASESTART +declare -a LEASETIME +declare -a ERRNO + +setup_client_peer() { + local n=$1; shift + ns+=" $n" + + ip netns del $(netnsn $n) 2>/dev/null || true + 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 > /dev/null + nn 1 ping6 -c 10 -f -W 1 fe80::badc:0ffe:e0dd:$n%wg0 > /dev/null +} + +# check_alowedips(PUBKEY, IPV4, IPV6) verifies that IPV4 and IPV6 +# are allowed on server. Empty argument means there must be no address +# for the given family, except the lladdr for v6. +check_alowedips() { + local pubkey="$1"; shift + local ipv4="$1"; shift + local ipv6="$1"; shift + + local v4_found= + local v6_found= + while read -r _pubkey _ips; do + [[ "$_pubkey" = "$pubkey" ]] || continue + for _ip in $_ips; do + case $_ip in + fe80:badc:ffe:e0dd:*) + continue ;; + *:*) + [[ -n "$v6_found" ]] && { echo "bad allowedip: $_ip"; return 1; } + [[ "$ipv6" = "$_ip" ]] && v6_found=$_ip + continue ;; + *) + [[ -n "$v4_found" ]] && { echo "bad allowedip: $_ip"; return 1; } + [[ "$ipv4" = "$_ip" ]] && v4_found=$_ip + continue ;; + esac + done + done < <(nn -q 1 wg show wg0 allowed-ips) + + [[ "$ipv4" != "$v4_found" ]] && { echo "missing allowedip: $ipv4"; return 1; } + [[ "$ipv6" != "$v6_found" ]] && { echo "missing allowedip: $ipv6"; return 1; } + + return 0 +} + +send_cmd() { + local n=$1; shift + local REQ="$1"; shift + + # It would have been nice to use /dev/tcp/fe80::%w0/970 instead of + # ncat, but we need to use a specific source port. + while read -r line && [[ -n $line ]] ; do + key="${line%%=*}" + value="${line#*=}" + case "$key" in + ip) + if [[ "$value" =~ : ]]; then + IPV6[$n]="$value" + else + IPV4[$n]="$value" + fi + continue ;; + leasestart) LEASESTART[$n]="$value"; continue ;; + leasetime) LEASETIME[$n]="$value"; continue ;; + errno) ERRNO[$n]="$value"; continue ;; + esac + done < <(printf $REQ | nn -q $n ncat -p 970 fe80::%wg0 970 2>/dev/null) +} + +# req(N, IPV4, IPV6) sends a request for client N, asking for IPV4 and +# IPv6. An empty string or "-" results in a request for releasing an +# address rather than allocating one. NOTE: Asking for 0.0.0.0 or ::0 +# means asking for any address in the pool. +req() { + local n=$1; shift + local ipv4_req= + [ $# -gt 0 ] && [ -n "$1" ] && { ipv4_req="ip=$1\n"; shift; } + [ "$ipv4_req" = "ip=-\n" ] && ipv4_req= + local ipv6_req= + [ $# -gt 0 ] && [ -n "$1" ] && { ipv6_req="ip=$1\n"; shift; } + [ "$ipv6_req" = "ip=-\n" ] && ipv6_req= + + 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 "$pubkey" "${IPV4[$n]}" "${IPV6[$n]}" +} + +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]} != 2" + + pretty 3 "Request addresses not in the pool" + req 3 "1.1.1.1" "fd00::1" + [[ ${ERRNO[3]} = 3 ]] || fail "errno: ${ERRNO[3]} != 3" + [[ -z ${IPV4[3]} ]] || fail "ipv4 not empty: ${IPV4[3]}" + [[ -z ${IPV6[3]} ]] || fail "ipv6 not empty: ${IPV6[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 0.0.0.0 :: + [[ ${ERRNO[4]} = 0 ]] || fail "errno: ${ERRNO[4]}" + local C4_FIRST_V4=${IPV4[4]} + [[ -z $C4_FIRST_V4 ]] && fail "no ipv4" + local C4_FIRST_V6=${IPV6[4]} + [[ -z $C4_FIRST_V6 ]] && fail "no ipv6" + + 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[4]} != $C4_FIRST_V4" + [[ ${IPV6[4]} = $C4_FIRST_V6 ]] || fail "${IPV6[4]} != $C4_FIRST_V6" + + 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[4]} != $C4_FIRST_V4" + [[ -z ${IPV6[4]} ]] || fail "ipv6 not empty: ${IPV6[4]}" + + pretty 5 "Requesting the v4 of client 4 and no v6 => errno=3 and no addrs" + req 5 $C4_FIRST_V4 - + [[ ${ERRNO[5]} = 3 ]] || fail "errno: ${ERRNO[5]} != 3" + [[ -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[5]} != $C4_FIRST_V4" + [[ -z ${IPV6[5]} ]] || fail "ipv6 not empty: ${IPV6[5]}" + + pretty "" "SUCCESS\n" +} + +test_case_3() { + # Two clients -- 6 and 7. + for i in 6 7; do setup_client_peer $i; done + + pretty 6 "Any v4, any v6" + req_check 6 0.0.0.0 :: + [[ ${ERRNO[6]} = 0 ]] || fail "errno: ${ERRNO[6]}" + local C6_FIRST_V4=${IPV4[6]} + [[ -z $C6_FIRST_V4 ]] && fail "no ipv4" + local C6_FIRST_V6=${IPV6[6]} + [[ -z $C6_FIRST_V6 ]] && fail "no ipv6" + + pretty 6 "Drop v4, extend v6" + req_check 6 - $C6_FIRST_V6 + [[ ${ERRNO[6]} = 0 ]] || fail "errno: ${ERRNO[6]}" + [[ -z ${IPV4[6]} ]] || fail "ipv4 not empty: ${IPV4[6]}" + [[ ${IPV6[6]} = $C6_FIRST_V6 ]] || fail "${IPV6[6]} != $C6_FIRST_V6" + + pretty "" "SUCCESS\n" +} + +# run_k_at_random(NCLIENTS, K) is invoked by test_forever() and runs +# req_check() for a random number of clients (the up to K first +# ones). The check performed is one of nine possible combinations of +# requesting a specific address (ipv4 and ipv6), requesting any type +# of address and releasing one or both addresses allocated. +run_k_at_random() { + local nclients=$1; shift + local k=$1; shift + local n=10 # First client. + local i + + if [[ $nclients -gt $k ]]; then + n=$(( $n + $RANDOM % ($n - $k) )) + fi + + for i in $(seq $n $(( $n + $k - 1 ))); do + case $(( $RANDOM % 9 )) in + 0) req_check $i 0.0.0.0 ::0 ; continue ;; # any v4, any v6 + 1) req_check $i 0.0.0.0 - ; continue ;; # any v4, drop v6 + 2) req_check $i - ::0 ; continue ;; # drop v4, any v6 + 3) req_check $i - - ; continue ;; # drop v4, drop v6 + + 4) req_check $i ${IPV4[$i]} ::0 ; continue ;; # extend v4, any v6 + 5) req_check $i ${IPV4[$i]} - ; continue ;; # extend v4, drop v6 + 6) req_check $i 0.0.0.0 ${IPV6[$i]} ; continue ;; # any v4, extend v6 + 7) req_check $i - ${IPV6[$i]} ; continue ;; # drop v4, extend v6 + + 8) req_check $i ${IPV4[$i]} ${IPV6[$i]} ; continue ;; # extend v4, extend v6 + esac + done +} + +# run_k_fill(NCLIENTS, _ignored) is invoked by test_forever() and runs +# req_check() for NCLIENTS, asking for any IPv4 and any IPv6 address. +run_k_fill() { + local nclients=$1; shift + shift + local i + + for i in $(seq $nclients); do + req_check $(( 10 + $i - 1 )) 0.0.0.0 ::0 + done +} + +# test_forever(NCLIENTS, FUNC, ARG) sets up NCLIENTS clients, numbered +# from 10 and upwards, runs one req_check() per client and enters an +# infinite loop invoking FUNC with NCLIENTS and ARGS. +test_forever() { + local nclients=$1; shift + local func=$1; shift + local arg=$1; shift + local i + + for i in $(seq 10 $(( 10 + $nclients - 1 ))); do + setup_client_peer $i + done + + for i in $(seq 10 $(( 10 + $nclients - 1 ))); do + req_check $i + done + + while sleep 1; do + if [ $(( $RANDOM % 100 )) -lt 50 ]; then + $func $nclients $arg + fi + done } -configure_peers -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 +### Tests. -pretty "" "clientsh.bash can be run with the following arguments:" -echo -echo wg-test-$$ $server_public -echo +# Ordinary test cases. +test_case_1 +test_case_2 +test_case_3 -nn 1 ./wg-dynamic-server --leasetime 10 wg0 +# Long running test cases, forever actually. +[ $# -gt 0 ] && { NCLIENTS=$1; shift; } || NCLIENTS=20 +[ $# -gt 0 ] && { ARGS=$1; shift; } || ARGS=4 +#test_forever $NCLIENTS run_k_at_random $ARGS +#test_forever $NCLIENTS run_k_fill $ARGS diff --git a/wg-dynamic-client.c b/wg-dynamic-client.c index f3e3274..7839210 100644 --- a/wg-dynamic-client.c +++ b/wg-dynamic-client.c @@ -2,446 +2,311 @@ * * 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\n"); + if (ipv6_assigned && ipm_deladdr_v6(device->ifindex, &ipv6)) + debug("Failed to cleanup ipv6 address\n"); - 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 = 0, 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 (connect(*fd, (struct sockaddr *)&their_addr, - sizeof(struct sockaddr_in6))) { - char out[INET6_ADDRSTRLEN]; + rip->has_ipv4 = rip->has_ipv6 = true; + if (ipv4_assigned) + memcpy(&rip->ipv4, &ipv4, sizeof rip->ipv4); - 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)); + if (ipv6_assigned) + memcpy(&rip->ipv6, &ipv6, sizeof rip->ipv6); - if (close(*fd)) - debug("Closing socket failed: %s\n", strerror(errno)); - *fd = -1; + 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()"); + } + + off += written; + } while (off < msglen); + + 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)); + + log_err("Server communication error.\n"); return -1; } - return 0; -} + if (remaining > 0) + log_err("Warning: discarding %zu extra bytes sent by the server\n", + remaining); + + if (rip->wg_errno && !rip->has_ipv4 && !rip->has_ipv6) { + if (rip->errmsg) { + log_err("Server refused request: %s\n", rip->errmsg); + return -1; + } else if (rip->wg_errno <= ARRAY_SIZE(WG_DYNAMIC_ERR) - 1) { + log_err("Server refused request: %s\n", + WG_DYNAMIC_ERR[rip->wg_errno]); + } else { + log_err("Server refused request: unknown error code %u\n", + rip->wg_errno); + } -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); + free(rip->errmsg); /* TODO: this could be done cleaner */ + return -1; + } + free(rip->errmsg); + + if (!ipv4_assigned || memcmp(&ipv4, &rip->ipv4, sizeof ipv4)) { + if (ipv4_assigned && ipm_deladdr_v4(device->ifindex, &ipv4)) + fatal("ipm_deladdr_v4()"); + + if (rip->has_ipv4) { + memcpy(&ipv4, &rip->ipv4, sizeof ipv4); + if (ipm_newaddr_v4(device->ifindex, &ipv4)) + fatal("ipm_newaddr_v4()"); + ipv4_assigned = true; + } else { + memset(&ipv4, 0, sizeof ipv4); + ipv4_assigned = false; + } } - 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()"); + + if (rip->has_ipv6) { + memcpy(&ipv6, &rip->ipv6, sizeof ipv6); + if (ipm_newaddr_v6(device->ifindex, &ipv6)) + fatal("ipm_newaddr_v6()"); + ipv6_assigned = true; + } else { + memset(&ipv6, 0, sizeof ipv6); + ipv6_assigned = false; + } } - /* 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 */ + log_err("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 27f4054..566559b 100644 --- a/wg-dynamic-server.c +++ b/wg-dynamic-server.c @@ -6,6 +6,7 @@ #define _GNU_SOURCE #define _POSIX_C_SOURCE 200112L +#include <getopt.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> @@ -23,6 +24,7 @@ #include "common.h" #include "dbg.h" +#include "ipm.h" #include "khash.h" #include "lease.h" #include "netlink.h" @@ -32,68 +34,31 @@ static const char *wg_interface = NULL; 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 uint32_t leasetime = 3600; static int sockfd = -1; static int epollfd = -1; static struct mnl_socket *nlsock = NULL; -KHASH_MAP_INIT_INT64(allowedht, wg_key *) +KHASH_MAP_INIT_SECURE_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 [--leasetime <leasetime>] <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); +static struct wg_dynamic_connection connections[MAX_CONNECTIONS] = { 0 }; - 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; - - 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; + fprintf(stderr, "usage: %s [--leasetime <leasetime>] <wg-interface>\n", + progname); + exit(EXIT_FAILURE); } static bool valid_peer_found(wg_device *device) @@ -167,8 +132,7 @@ static wg_key *addr_to_pubkey(struct sockaddr_storage *addr) return NULL; } -static int accept_connection(int sockfd, wg_key *dest_pubkey, - struct in6_addr *dest_lladdr) +static int accept_connection(wg_key *dest_pubkey, struct in6_addr *dest_lladdr) { int fd; wg_key *pubkey; @@ -227,101 +191,131 @@ static int accept_connection(int sockfd, wg_key *dest_pubkey, return fd; } -static bool send_error(struct wg_dynamic_request *req, int error) +static bool send_message(struct wg_dynamic_connection *con, + const unsigned char *buf, size_t len) { - 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]); + size_t offset = 0; - return send_message(req, buf, msglen); -} + while (1) { + ssize_t written = write(con->fd, buf + offset, len - offset); + if (written < 0) { + if (errno == EWOULDBLOCK || errno == EAGAIN) + break; -static size_t serialize_lease(char *buf, size_t len, - const struct wg_dynamic_lease *lease) -{ - char addrbuf[INET6_ADDRSTRLEN]; - size_t off = 0; + if (errno == EINTR) + continue; - if (lease->ipv4.s_addr) { - if (!inet_ntop(AF_INET, &lease->ipv4, addrbuf, sizeof addrbuf)) - fatal("inet_ntop()"); + debug("Writing to socket %d failed: %s\n", con->fd, + strerror(errno)); + return false; + } - print_to_buf(buf, len, &off, "ipv4=%s/%d\n", addrbuf, 32); + offset += written; + if (offset == len) + return true; } - if (!IN6_IS_ADDR_UNSPECIFIED(&lease->ipv6)) { - if (!inet_ntop(AF_INET6, &lease->ipv6, addrbuf, sizeof addrbuf)) - fatal("inet_ntop()"); + debug("Socket %d blocking on write with %lu bytes left, postponing\n", + con->fd, len - offset); - print_to_buf(buf, len, &off, "ipv6=%s/%d\n", addrbuf, 128); - } + if (!con->outbuf) { + con->buflen = len - offset; + con->outbuf = malloc(con->buflen); + if (!con->outbuf) + fatal("malloc()"); - print_to_buf(buf, len, &off, "leasestart=%u\nleasetime=%u\nerrno=0\n\n", - lease->start_real, lease->leasetime); + memcpy(con->outbuf, buf + offset, con->buflen); + } else { + con->buflen = len - offset; + memmove(con->outbuf, buf + offset, con->buflen); + } - return off; + return true; } -static int response_request_ip(struct wg_dynamic_attr *cur, wg_key pubkey, - const struct in6_addr *lladdr, - struct wg_dynamic_lease **lease) +void close_connection(struct wg_dynamic_connection *con) { - struct in_addr *ipv4 = NULL; - struct in6_addr *ipv6 = NULL; - - 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; - } + free_wg_dynamic_request(&con->req); - *lease = set_lease(wg_interface, pubkey, leasetime, lladdr, ipv4, ipv6); - if (!*lease) - return E_IP_UNAVAIL; + if (close(con->fd)) + debug("Failed to close socket\n"); - return E_NO_ERROR; + con->fd = -1; + memset(con->pubkey, 0, sizeof con->pubkey); + free(con->outbuf); + con->outbuf = NULL; + con->buflen = 0; } -static bool send_response(struct wg_dynamic_request *req) +static bool send_response(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; - switch (req->cmd) { - case WGKEY_REQUEST_IP: - ret = response_request_ip(cur, req->pubkey, &req->lladdr, - &lease); - if (ret) - break; + switch (con->req.cmd) { + case WGKEY_REQUEST_IP:; + struct wg_dynamic_request_ip *rip = con->req.result; + 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(con->pubkey, leasetime, &con->lladdr, ip4, + ip6); + + if (lease->ipv4.s_addr) { + ans.has_ipv4 = true; + memcpy(&ans.ipv4, &lease->ipv4, sizeof ans.ipv4); + } + if (!IN6_IS_ADDR_UNSPECIFIED(&lease->ipv6)) { + ans.has_ipv6 = true; + memcpy(&ans.ipv6, &lease->ipv6, sizeof ans.ipv6); + } - msglen = serialize_lease(buf, sizeof buf, lease); + if ((!ans.has_ipv4 && rip->has_ipv4) || + (!ans.has_ipv6 && rip->has_ipv6)) + ans.wg_errno = E_IP_UNAVAIL; + + ans.start = lease->start_real; + ans.leasetime = lease->leasetime; + + msglen = serialize_request_ip(false, buf, sizeof buf, &ans); break; default: - debug("Unknown command: %d\n", req->cmd); + debug("Unknown command: %d\n", con->req.cmd); BUG(); } - if (ret) - return send_error(req, ret); + return send_message(con, (unsigned char *)buf, msglen); +} + +static void handle_client(struct wg_dynamic_connection *con) +{ + unsigned char buf[RECV_BUFSIZE + MAX_LINESIZE]; + size_t rem = 0; + ssize_t ret; + + while ((ret = handle_request(con->fd, &con->req, buf, &rem)) > 0) { + if (!send_response(con)) { + close_connection(con); + break; + } + + free_wg_dynamic_request(&con->req); + } - return send_message(req, buf, msglen); + if (ret < 0) { + size_t len = 0; + uint32_t err = E_INVALID_REQ; + if (-ret == EPROTONOSUPPORT) + err = E_UNSUPP_PROTO; + + print_to_buf((char *)buf, sizeof buf, &len, + "errno=%u\nerrmsg=%s\n\n", err, + WG_DYNAMIC_ERR[err]); + send_message(con, buf, len); + close_connection(con); + } } static void setup_sockets() @@ -391,14 +385,14 @@ static void cleanup() close(epollfd); for (int i = 0; i < MAX_CONNECTIONS; ++i) { - if (requests[i].fd < 0) + if (connections[i].fd < 0) continue; - close_connection(&requests[i]); + close_connection(&connections[i]); } } -static void init_leaess_from_peers() +static void init_leases_from_peers() { wg_peer *peer; @@ -420,38 +414,54 @@ static void init_leaess_from_peers() if (!ipv4 && !ipv6) continue; - set_lease(wg_interface, peer->public_key, leasetime, lladdr, - ipv4, ipv6); + set_lease(peer->public_key, leasetime, lladdr, ipv4, ipv6); } } 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); + if (!allowedips_ht) + fatal("kh_init()"); for (int i = 0; i < MAX_CONNECTIONS; ++i) - requests[i].fd = -1; + connections[i].fd = -1; if (atexit(cleanup)) die("Failed to set exit function\n"); rebuild_allowedips_ht(); - if (!validate_link_local_ip(device->ifindex)) - // TODO: assign IP instead? + 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 (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(NULL, nlsock); - init_leaess_from_peers(); + leases_init(wg_interface, device->ifindex, NULL, nlsock); + init_leases_from_peers(); } static int get_avail_request() @@ -460,48 +470,47 @@ static int get_avail_request() if (nfds >= MAX_CONNECTIONS) return -1; - if (requests[nfds].fd < 0) + 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, - &requests[n].lladdr); + 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; + struct wg_dynamic_connection *con; if (ptr == &sockfd) { - accept_incoming(sockfd, epollfd, requests); + accept_incoming(); return; } @@ -510,15 +519,14 @@ static void handle_event(void *ptr, uint32_t events) return; } - req = (struct wg_dynamic_request *)ptr; + con = (struct wg_dynamic_connection *)ptr; if (events & EPOLLIN) { - if (handle_request(req, send_response, send_error)) - close_connection(req); + handle_client(con); } if (events & EPOLLOUT) { - if (send_message(req, req->buf, req->buflen)) - close_connection(req); + if (!send_message(con, con->outbuf, con->buflen)) + close_connection(con); } } @@ -540,7 +548,7 @@ static void poll_loop() fatal("epoll_ctl()"); while (1) { - time_t next = leases_refresh(wg_interface) * 1000; + time_t next = leases_refresh() * 1000; int nfds = epoll_wait(epollfd, events, MAX_CONNECTIONS, next); if (nfds == -1) { if (errno == EINTR) @@ -556,27 +564,37 @@ static void poll_loop() int main(int argc, char *argv[]) { - char *endptr = NULL; - progname = argv[0]; - ++argv; - --argc; - while (argc > 0) { - if (!strcmp(argv[0], "--leasetime") && argc >= 2) { - leasetime = (uint32_t) strtoul(argv[1], &endptr, 10); + while (1) { + int ret, index; + char *endptr = NULL; + const struct option options[] = { + { "leasetime", required_argument, NULL, 0 }, + { 0, 0, 0, 0 } + }; + + ret = getopt_long(argc, argv, "", options, &index); + if (ret == -1) + break; + + switch (ret) { + case 0: + if (index != 0) + usage(); + leasetime = (uint32_t)strtoul(optarg, &endptr, 10); if (*endptr) usage(); - argv += 2; - argc -= 2; - } else { - wg_interface = argv[0]; - argv += 1; - argc -= 1; break; + default: + usage(); } } - if (!wg_interface || argc > 0) + + if (optind < argc) + wg_interface = argv[optind++]; + + if (!wg_interface || optind < argc) usage(); setup(); |