diff options
-rw-r--r-- | common.c | 171 | ||||
-rw-r--r-- | common.h | 33 | ||||
-rw-r--r-- | dbg.h | 1 | ||||
-rwxr-xr-x | tests/netsh.sh | 12 | ||||
-rw-r--r-- | wg-dynamic-client.c | 556 | ||||
-rw-r--r-- | wg-dynamic-server.c | 349 |
6 files changed, 1002 insertions, 120 deletions
@@ -9,10 +9,14 @@ #include <stdbool.h> #include <stdint.h> #include <stdlib.h> +#include <stdarg.h> #include <string.h> #include <unistd.h> +#include <sys/param.h> #include <arpa/inet.h> +#include <libmnl/libmnl.h> +#include <linux/rtnetlink.h> #include "common.h" #include "dbg.h" @@ -47,6 +51,9 @@ static struct wg_dynamic_attr *parse_value(enum wg_dynamic_key key, char *value) uintmax_t uresult; union { uint32_t leasetime; + uint32_t leasestart; + uint32_t err; + char errmsg[72]; struct wg_combined_ip ip; } data = { 0 }; @@ -65,6 +72,14 @@ static struct wg_dynamic_attr *parse_value(enum wg_dynamic_key key, char *value) return NULL; break; + case WGKEY_LEASESTART: + len = sizeof data.leasestart; + uresult = strtoumax(value, &endptr, 10); + if (uresult > UINT32_MAX || *endptr != '\0') + return NULL; + + data.leasestart = (uint32_t)uresult; + break; case WGKEY_LEASETIME: len = sizeof data.leasetime; uresult = strtoumax(value, &endptr, 10); @@ -73,6 +88,19 @@ static struct wg_dynamic_attr *parse_value(enum wg_dynamic_key key, char *value) data.leasetime = (uint32_t)uresult; break; + case WGKEY_ERRNO: + len = sizeof data.err; + uresult = strtoumax(value, &endptr, 10); + if (uresult > UINT32_MAX || *endptr != '\0') + return NULL; + + data.err = (uint32_t)uresult; + break; + case WGKEY_ERRMSG: + strncpy(data.errmsg, value, sizeof data.errmsg); + len = MIN(sizeof data.errmsg, + strlen(value) + 1); /* Copying the NUL byte too. */ + break; default: debug("Invalid key %d, aborting\n", key); abort(); @@ -192,6 +220,9 @@ void free_wg_dynamic_request(struct wg_dynamic_request *req) req->cmd = WGKEY_UNKNOWN; req->version = 0; + free(req->buf); + req->buf = NULL; + req->buflen = 0; req->first = NULL; req->last = NULL; } @@ -249,8 +280,8 @@ static int parse_request(struct wg_dynamic_request *req, unsigned char *buf, } bool handle_request(int fd, struct wg_dynamic_request *req, - void (*success)(int, struct wg_dynamic_request *req), - void (*error)(int, int)) + bool (*success)(int, struct wg_dynamic_request *), + bool (*error)(int, int)) { ssize_t bytes; int ret; @@ -264,23 +295,141 @@ bool handle_request(int fd, struct wg_dynamic_request *req, // TODO: handle EINTR - debug("Reading from socket failed: %s\n", + debug("Reading from socket %d failed: %s\n", fd, strerror(errno)); return true; } else if (bytes == 0) { - debug("Client disconnected unexpectedly\n"); + debug("Peer disconnected unexpectedly\n"); return true; } ret = parse_request(req, buf, bytes); - if (ret < 0) { - error(fd, -ret); - return true; - } else if (ret == 0) { - success(fd, req); - return true; - } + if (ret < 0) + return error(fd, -ret); + else if (ret == 0) + return success(fd, req); } return false; } + +size_t send_message(int fd, unsigned char *buf, size_t *len) +{ + ssize_t bytes; + size_t offset = 0; + + while (*len) { + bytes = write(fd, buf + offset, *len); + if (bytes < 0) { + if (errno == EWOULDBLOCK || errno == EAGAIN) + break; + + // TODO: handle EINTR + + debug("Writing to socket %d failed: %s\n", fd, + strerror(errno)); + *len = 0; + return 0; + } + + *len -= bytes; + offset += bytes; + } + + return offset; +} + +void send_later(struct wg_dynamic_request *req, unsigned char *const buf, + size_t msglen) +{ + unsigned char *newbuf = malloc(msglen); + if (!newbuf) + fatal("Failed malloc()"); + memcpy(newbuf, buf, msglen); + + free(req->buf); + req->buf = newbuf; + req->buflen = msglen; +} + +size_t print_to_buf(char *buf, size_t bufsize, size_t offset, char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + int n = vsnprintf(buf + offset, bufsize - offset, fmt, ap); + va_end(ap); + if (n < 0) + fatal("Failed snprintf"); + if (n + offset >= bufsize) + fatal("Outbuffer too small"); + return 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(int *fd, struct wg_dynamic_request *req) +{ + if (close(*fd)) + debug("Failed to close socket\n"); + + *fd = -1; + free_wg_dynamic_request(req); +} + +bool is_link_local(unsigned char *addr) +{ + /* TODO: check if the remaining 48 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); +} @@ -19,9 +19,14 @@ #define RECV_BUFSIZE 8192 +#define MAX_RESPONSE_SIZE 8192 + static const char WG_DYNAMIC_ADDR[] = "fe80::"; static const uint16_t WG_DYNAMIC_PORT = 1337; +#define WG_DYNAMIC_LEASETIME 10 /* NOTE: 10s is good for testing purposes */ + +/* TODO: Move client specific items to its own CLIENT_ITEMS */ #define ITEMS \ E(WGKEY_UNKNOWN, "") /* must be the first entry */ \ /* CMD START */ \ @@ -31,7 +36,10 @@ static const uint16_t WG_DYNAMIC_PORT = 1337; E(WGKEY_INCOMPLETE, "") \ E(WGKEY_IPV4, "ipv4") \ E(WGKEY_IPV6, "ipv6") \ - E(WGKEY_LEASETIME, "leasetime") + E(WGKEY_LEASESTART, "leasestart") \ + E(WGKEY_LEASETIME, "leasetime") \ + E(WGKEY_ERRNO, "errno") \ + E(WGKEY_ERRMSG, "errmsg") #define E(x, y) x, enum wg_dynamic_key { ITEMS }; @@ -52,6 +60,8 @@ struct wg_dynamic_request { enum wg_dynamic_key cmd; uint32_t version; wg_key pubkey; + unsigned char *buf; + size_t buflen; struct wg_dynamic_attr *first, *last; }; @@ -64,11 +74,26 @@ struct wg_combined_ip { uint8_t cidr; }; +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; +}; + #define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0])) void free_wg_dynamic_request(struct wg_dynamic_request *req); bool handle_request(int fd, struct wg_dynamic_request *req, - void (*success)(int, struct wg_dynamic_request *req), - void (*error)(int, int)); - + bool (*success)(int, struct wg_dynamic_request *), + bool (*error)(int, int)); +size_t send_message(int fd, unsigned char *buf, size_t *len); +void send_later(struct wg_dynamic_request *req, unsigned char *const buf, + size_t msglen); +size_t print_to_buf(char *buf, size_t bufsize, size_t len, char *fmt, ...); +uint32_t current_time(); +void close_connection(int *fd, 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); #endif @@ -29,6 +29,7 @@ extern int DBG_LVL; #define SUFFIX(S, M, ...) M S, __VA_ARGS__ #define log_err(...) fprintf(stderr, PREFIX(__VA_ARGS__)) +/* FIXME: This looks backwards -- DBG_LVL=2 gives info but not warn. */ #define log_warn(...) do { if (DBG_LVL > 2) log_err(__VA_ARGS__); } while (0) #define log_info(...) do { if (DBG_LVL > 1) log_err(__VA_ARGS__); } while (0) #define die(...) \ diff --git a/tests/netsh.sh b/tests/netsh.sh index 27979c5..df97a24 100755 --- a/tests/netsh.sh +++ b/tests/netsh.sh @@ -55,7 +55,7 @@ client_public=$(wg pubkey <<< $client_private) configure_peers() { ip1 addr add fe80::/64 dev wg0 - ip2 addr add fe80::badc:0ffe:e0dd:f00d/64 dev wg0 + ip2 addr add fe80::badc:0ffe:e0dd:f00d/128 dev wg0 n1 wg set wg0 \ private-key <(echo $server_private) \ @@ -69,6 +69,8 @@ configure_peers() { peer $server_public \ allowed-ips 0.0.0.0/0,::/0 + ip2 route add fe80::/128 dev wg0 + ip1 link set up dev wg0 ip2 link set up dev wg0 } @@ -79,4 +81,10 @@ n2 wg set wg0 peer "$server_public" endpoint [::1]:1 n2 ping6 -c 10 -f -W 1 fe80::%wg0 n1 ping6 -c 10 -f -W 1 fe80::badc:0ffe:e0dd:f00d%wg0 -n1 ./wg-dynamic-server wg0 +pp echo "PID: $$" + +if [ -z "$NETSH_GDB" ]; then + n1 ./wg-dynamic-server wg0 +else # Try NETSH_GDB="-ex run" tests./netsh.sh + n1 gdb $NETSH_GDB --args ./wg-dynamic-server wg0 +fi diff --git a/wg-dynamic-client.c b/wg-dynamic-client.c index 8554269..560c8d7 100644 --- a/wg-dynamic-client.c +++ b/wg-dynamic-client.c @@ -3,38 +3,554 @@ * Copyright (C) 2019 WireGuard LLC. All Rights Reserved. */ -#include "netlink.h" #include <stdio.h> +#include <unistd.h> +#include <fcntl.h> #include <string.h> #include <stdlib.h> +#include <signal.h> +#include <poll.h> + +#include <arpa/inet.h> +#include <libmnl/libmnl.h> +#include <linux/rtnetlink.h> + +#include "common.h" +#include "dbg.h" +#include "netlink.h" + +#define LEASE_CHECK_INTERVAL 1000 /* 1s is convenient for testing */ +#define LOW_PORT_START 214 + +int DBG_LVL = 3; + +static const char *progname; +static const char *wg_interface; +static wg_device *device = NULL; +static struct pollfd pollfds[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 void usage() +{ + die("usage: %s <wg-interface>\n", progname); +} + +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; +} + +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->ip, addr, 4); + cb_data->gaddr4->cidr = ifa->ifa_prefixlen; + break; + case AF_INET6: + cb_data->gaddr6->family = ifa->ifa_family; + memcpy(&cb_data->gaddr6->ip, 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) +{ + 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->ip); + + 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"); + + mnl_socket_close(nl); +} + +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->ip, ipstr, sizeof ipstr), + addr->cidr, ifindex); + iface_update(RTM_DELADDR, 0, ifindex, addr); +} + +static void iface_add_addr(uint32_t ifindex, const struct wg_combined_ip *addr) +{ + char ipstr[INET6_ADDRSTRLEN]; + debug("adding %s/%u to interface %u\n", + inet_ntop(addr->family, &addr->ip, ipstr, sizeof ipstr), + addr->cidr, ifindex); + iface_update(RTM_NEWADDR, NLM_F_REPLACE | NLM_F_CREATE, ifindex, addr); +} + +static bool get_and_validate_local_addrs(uint32_t ifindex, + struct in6_addr *lladdr, + struct wg_combined_ip *gaddr4, + struct wg_combined_ip *gaddr6) +{ + 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 0 +static void dump_leases() +{ + char ip4str[INET6_ADDRSTRLEN], ip6str[INET6_ADDRSTRLEN]; + struct wg_dynamic_lease *l = &our_lease; + + if (l->start == 0) { + debug("lease NONE\n"); + return; + } + + debug("lease %u %u %s/%u %s/%u\n", l->start + l->leasetime, + l->start + l->leasetime - current_time(), + inet_ntop(AF_INET, &l->ip4.ip.ip4, ip4str, INET6_ADDRSTRLEN), + l->ip4.cidr, + inet_ntop(AF_INET6, &l->ip6.ip.ip6, ip6str, INET6_ADDRSTRLEN), + l->ip6.cidr); +} +#endif + +static int do_connect(int *fd) +{ + int res; + struct sockaddr_in6 our_addr = { + .sin6_family = AF_INET6, + .sin6_addr = our_lladdr, + .sin6_scope_id = device->ifindex, + }; + struct sockaddr_in6 their_addr = { + .sin6_family = AF_INET6, + .sin6_port = htons(WG_DYNAMIC_PORT), + .sin6_scope_id = device->ifindex, + }; + + *fd = socket(AF_INET6, SOCK_STREAM, 0); + if (*fd < 0) + fatal("Creating a socket failed"); + + for (int port = LOW_PORT_START;; port++) { + our_addr.sin6_port = htons(port); + if (!bind(*fd, (struct sockaddr *)&our_addr, sizeof(our_addr))) + break; + if (errno != EADDRINUSE) + fatal("Binding socket failed"); + if (port >= 1024) + die("No low ports available"); + } + + if (!inet_pton(AF_INET6, WG_DYNAMIC_ADDR, &their_addr.sin6_addr)) + fatal("inet_pton()"); + if (connect(*fd, (struct sockaddr *)&their_addr, + sizeof(struct sockaddr_in6))) { + char out[INET6_ADDRSTRLEN]; + 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 (close(*fd)) + debug("Closing socket failed: %s\n", strerror(errno)); + *fd = -1; + return -1; + } + + res = fcntl(*fd, F_GETFL, 0); + if (res < 0 || fcntl(*fd, F_SETFL, res | O_NONBLOCK) < 0) + fatal("Setting socket to nonblocking failed"); + + return 0; +} + +static size_t connect_and_send(unsigned char *buf, size_t *len) +{ + size_t ret; + if (pollfds[0].fd < 0) + if (do_connect(&pollfds[0].fd)) + return 0; + ret = send_message(pollfds[0].fd, buf, len); + return ret; +} + +static bool request_ip(struct wg_dynamic_request *req, + const struct wg_dynamic_lease *lease) +{ + unsigned char buf[MAX_RESPONSE_SIZE + 1]; + char addrstr[INET6_ADDRSTRLEN]; + size_t msglen; + size_t written; + + 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.ip.ip4.s_addr) { + if (!inet_ntop(AF_INET, &lease->ip4.ip.ip4, addrstr, + sizeof addrstr)) + fatal("inet_ntop()"); + msglen += print_to_buf((char *)buf, sizeof buf, msglen, + "ipv4=%s/32\n", addrstr); + } + if (lease && !IN6_IS_ADDR_UNSPECIFIED(&lease->ip6.ip.ip6)) { + if (!inet_ntop(AF_INET6, &lease->ip6.ip.ip6, addrstr, + sizeof addrstr)) + fatal("inet_ntop()"); + msglen += print_to_buf((char *)buf, sizeof buf, msglen, + "ipv6=%s/128\n", addrstr); + } + /* nmsglen += print_to_buf((char *)buf, sizeof buf, msglen, + "leasetime=%u\n", fixme); */ + + msglen += print_to_buf((char *)buf, sizeof buf, msglen, "\n"); + + written = connect_and_send(buf, &msglen); + if (msglen == 0) + return true; + + debug("Socket %d blocking with %lu bytes to write, postponing\n", + pollfds[0].fd, msglen); + send_later(req, buf + written, msglen); + return false; +} + +static int maybe_refresh_lease(uint32_t now, struct wg_dynamic_lease *lease, + struct wg_dynamic_request *req) +{ + if (now > lease->start + (lease->leasetime * 2) / 3) { + debug("Refreshing lease expiring on %u\n", + lease->start + lease->leasetime); + request_ip(req, lease); + return 0; + } + + return 1; +} + +static bool lease_is_valid(uint32_t now, struct wg_dynamic_lease *lease) +{ + return now < lease->start + lease->leasetime; +} + +static void maybe_remove_lease(uint32_t now, struct wg_dynamic_lease *lease) +{ + if (!lease_is_valid(now, lease)) + memset(lease, 0, sizeof *lease); +} + +static void check_leases(struct wg_dynamic_request *req) +{ + uint32_t now = current_time(); + + if (!lease_is_valid(now, &our_lease)) + request_ip(req, NULL); + else { + maybe_remove_lease(now, &our_lease); + maybe_refresh_lease(now, &our_lease, req); + } +} + +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.ip.ip4.s_addr == 0 && + IN6_IS_ADDR_UNSPECIFIED(&lease->ip6.ip.ip6))) + return -EINVAL; + + if (abs(now - lease_start) < 15) + lease->start = lease_start; + else + lease->start = now; + + debug("Replacing lease %u -> %u\n", curleasetime, + lease->start + lease->leasetime); + + return 0; +} + +static void cleanup() +{ + wg_free_device(device); + if (pollfds[0].fd >= 0) + if (close(pollfds[0].fd)) + debug("Failed to close fd"); +} + +static bool handle_error(int fd, int ret) +{ + UNUSED(fd); + UNUSED(ret); + + debug("Unable to parse response: %s\n", strerror(ret)); + + return true; +} + +static void maybe_update_iface() +{ + if (memcmp(&our_gaddr4.ip, &our_lease.ip4.ip, sizeof our_gaddr4.ip) || + our_gaddr4.cidr != our_lease.ip4.cidr) { + if (our_gaddr4.ip.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.ip, &our_lease.ip6.ip, sizeof our_gaddr6.ip) || + our_gaddr6.cidr != our_lease.ip6.cidr) { + if (!IN6_IS_ADDR_UNSPECIFIED(&our_gaddr6.ip.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); + } +} + +static bool handle_response(int fd, struct wg_dynamic_request *req) +{ + 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; + } +#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; + } + + return true; +} int main(int argc __attribute__((unused)), char *argv[] __attribute__((unused))) { - char *device_names, *device_name; - size_t len; + struct wg_dynamic_request req = { 0 }; + uint32_t now = current_time(); + + progname = argv[0]; + if (argc != 2) + usage(); + + wg_interface = argv[1]; + + 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 (signal(SIGPIPE, SIG_IGN) == SIG_ERR) + fatal("Unable to ignore SIGPIPE"); + + 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 matching the requirements (fe80::/128)? - device_names = wg_list_device_names(); - if (!device_names) { - perror("Unable to get device names"); - return 1; + char lladr_str[INET6_ADDRSTRLEN]; + debug("%s: %s\n", wg_interface, + inet_ntop(AF_INET6, &our_lladdr, lladr_str, sizeof lladr_str)); + + if (our_gaddr4.ip.ip4.s_addr || + !IN6_IS_ADDR_UNSPECIFIED(&our_gaddr6.ip.ip6)) { + our_lease.start = now; + 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)); } - wg_for_each_device_name(device_names, device_name, len) { - wg_device *device; - wg_peer *peer; - wg_key_b64_string key; - if (wg_get_device(&device, device_name) < 0) { - perror("Unable to get device"); + /* TODO: use a blocking socket instead of the unnecessary + * complexity of nonblocking */ + + pollfds[0] = (struct pollfd){.fd = -1, .events = POLLIN }; + while (1) { + int nevents = poll(pollfds, 1, LEASE_CHECK_INTERVAL); + + if (nevents == -1) + fatal("poll()"); + + if (nevents == 0) { + /* FIXME: if there's any risk for this path to + * be starving, maybe do this regardless of + * socket readiness? */ + check_leases(&req); continue; } - wg_key_to_base64(key, device->public_key); - printf("%s has public key %s\n", device_name, key); - wg_for_each_peer(device, peer) { - wg_key_to_base64(key, peer->public_key); - printf(" - peer %s\n", key); + + if (pollfds[0].revents & POLLOUT) { + pollfds[0].revents &= ~POLLOUT; + debug("sending, trying again with %lu bytes\n", + req.buflen); + send_message(pollfds[0].fd, req.buf, &req.buflen); + if (!req.buflen) + close_connection(&pollfds[0].fd, &req); + } + + if (pollfds[0].revents & POLLIN) { + pollfds[0].revents &= ~POLLIN; + if (handle_request(pollfds[0].fd, &req, handle_response, + handle_error)) + close_connection(&pollfds[0].fd, &req); + else if (req.buf) + pollfds[0].events |= POLLOUT; } - wg_free_device(device); } - free(device_names); + return 0; } diff --git a/wg-dynamic-server.c b/wg-dynamic-server.c index b63c72d..fc8098c 100644 --- a/wg-dynamic-server.c +++ b/wg-dynamic-server.c @@ -9,6 +9,7 @@ #include <stdint.h> #include <stdio.h> #include <stdlib.h> +#include <unistd.h> #include <string.h> #include <time.h> @@ -17,7 +18,7 @@ #include <poll.h> #include <sys/socket.h> #include <sys/types.h> - +#include <netdb.h> #include <libmnl/libmnl.h> #include <linux/rtnetlink.h> @@ -29,6 +30,7 @@ static const char *progname; static const char *wg_interface; static struct in6_addr well_known; +/* static in_addr_t inaddr_any = INADDR_ANY; */ static wg_device *device = NULL; static struct radix_trie allowedips_trie; @@ -36,6 +38,7 @@ static struct pollfd pollfds[MAX_CONNECTIONS + 1]; struct mnl_cb_data { uint32_t ifindex; + struct in6_addr addr; bool valid_ip_found; }; @@ -44,12 +47,6 @@ static void usage() die("usage: %s <wg-interface>\n", progname); } -static bool is_link_local(unsigned char *addr) -{ - /* TODO: check if the remaining 48 bits are 0 */ - return addr[0] == 0xFE && addr[1] == 0x80; -} - static int data_attr_cb(const struct nlattr *attr, void *data) { const struct nlattr **tb = data; @@ -105,56 +102,26 @@ static int data_cb(const struct nlmsghdr *nlh, void *data) static bool validate_link_local_ip(uint32_t ifindex) { - 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; struct mnl_cb_data cb_data = { - .ifindex = ifindex, - .valid_ip_found = false, + .ifindex = ifindex, .valid_ip_found = false, }; - 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 = AF_INET6; - - if (mnl_socket_sendto(nl, nlh, nlh->nlmsg_len) < 0) - fatal("mnl_socket_sendto"); - - ret = mnl_socket_recvfrom(nl, buf, sizeof(buf)); - while (ret > 0) { - ret = mnl_cb_run(buf, ret, seq, portid, data_cb, &cb_data); - if (ret <= MNL_CB_STOP) - break; - ret = mnl_socket_recvfrom(nl, buf, sizeof(buf)); - } - if (ret == -1) - fatal("mnl_cb_run/mnl_socket_recvfrom"); - - mnl_socket_close(nl); + iface_get_all_addrs(AF_INET6, data_cb, &cb_data); return cb_data.valid_ip_found; } +static int get_avail_pollfds() +{ + for (int nfds = 1;; ++nfds) { + if (nfds >= MAX_CONNECTIONS + 1) + return -1; + + if (pollfds[nfds].fd < 0) + return nfds; + } +} + static bool valid_peer_found(wg_device *device) { wg_peer *peer; @@ -180,17 +147,6 @@ static bool valid_peer_found(wg_device *device) return false; } -static int get_avail_pollfds() -{ - for (int nfds = 1;; ++nfds) { - if (nfds >= MAX_CONNECTIONS + 1) - return -1; - - if (pollfds[nfds].fd < 0) - return nfds; - } -} - static void rebuild_allowedips_trie() { int ret; @@ -242,6 +198,7 @@ static int accept_connection(int sockfd, wg_key *dest) wg_key *pubkey; struct sockaddr_storage addr; socklen_t size = sizeof addr; + #ifdef __linux__ fd = accept4(sockfd, (struct sockaddr *)&addr, &size, SOCK_NONBLOCK); if (fd < 0) @@ -255,6 +212,27 @@ static int accept_connection(int sockfd, wg_key *dest) if (res < 0 || fcntl(fd, F_SETFL, res | O_NONBLOCK) < 0) fatal("Setting socket to nonblocking failed"); #endif + + char addrstring[INET6_ADDRSTRLEN]; + char serv[10]; + if (getnameinfo((struct sockaddr *)&addr, sizeof addr, addrstring, + sizeof addrstring, serv, sizeof serv, + NI_NUMERICHOST | NI_NUMERICSERV)) + fatal("getnameinfo"); + + if (addr.ss_family != AF_INET6) { + debug("%s: rejecting client for not using an IPv6 address\n", + addrstring); + return -EINVAL; + } + unsigned long port = strtoul(serv, NULL, 10); + if (port >= 1024) { + debug("%s: rejecting client for not using a low port: %lu\n", + addrstring, port); + return -EINVAL; + } + //debug("Client connection from [%s]:%lu\n", addrstring, port); + pubkey = addr_to_pubkey(&addr); if (!pubkey) { /* our copy of allowedips is outdated, refresh */ @@ -266,14 +244,11 @@ static int accept_connection(int sockfd, wg_key *dest) return -ENOENT; } } - memcpy(dest, pubkey, sizeof *pubkey); + memcpy(dest, pubkey, sizeof *dest); wg_key_b64_string key; - char out[INET6_ADDRSTRLEN]; wg_key_to_base64(key, *pubkey); - inet_ntop(addr.ss_family, &((struct sockaddr_in6 *)&addr)->sin6_addr, - out, sizeof(out)); - debug("%s has pubkey: %s\n", out, key); + debug("%s has pubkey: %s\n", addrstring, key); return fd; } @@ -295,28 +270,225 @@ static void accept_incoming(int sockfd, struct wg_dynamic_request *reqs) } } -static void close_connection(int *fd, struct wg_dynamic_request *req) +static int allocate_from_pool(struct wg_dynamic_request *const req, + struct wg_dynamic_lease *lease) +{ + struct wg_dynamic_attr *attr; + struct wg_combined_ip default_v4 = {.family = AF_INET, .cidr = 32 }, + default_v6 = {.family = AF_INET6, .cidr = 128 }; + + /* NOTE: handing out whatever the client asks for */ + /* TODO: choose an ip address from pool of available + * addresses, together with an appropriate lease time */ + /* NOTE: the pool is to be drawn from what routes are pointing + * to the wg interface, and kept up to date as the routing + * table changes */ + + if (!inet_pton(AF_INET, "192.168.47.11", &default_v4.ip.ip4)) + fatal("inet_pton()"); + memcpy(&lease->ip4, &default_v4, sizeof(struct wg_combined_ip)); + + if (!inet_pton(AF_INET6, "fd00::4711", &default_v6.ip.ip6)) + fatal("inet_pton()"); + memcpy(&lease->ip6, &default_v6, sizeof(struct wg_combined_ip)); + + lease->start = current_time(); + lease->leasetime = WG_DYNAMIC_LEASETIME; + + attr = req->first; + while (attr) { + switch (attr->key) { + case WGKEY_IPV4: + break; /* FIXME */ + memcpy(&lease->ip4, attr->value, + sizeof(struct wg_combined_ip)); + break; + case WGKEY_IPV6: + break; /* FIXME */ + memcpy(&lease->ip6, attr->value, + sizeof(struct wg_combined_ip)); + break; + case WGKEY_LEASETIME: + memcpy(&lease->leasetime, attr->value, + sizeof(uint32_t)); + break; + default: + debug("Ignoring invalid attribute for request_ip: %d\n", + attr->key); + } + + attr = attr->next; + } + + return 0; +} + +static bool send_error(int fd, int ret) +{ + UNUSED(fd); + debug("Error: %s\n", strerror(ret)); + return true; +} + +static bool serialise_lease(char *buf, size_t bufsize, size_t *offset, + const struct wg_dynamic_lease *lease) +{ + char addrbuf[INET6_ADDRSTRLEN]; + bool ret = false; + + if (lease->ip4.family) { /* FIXME: memcmp(&lease->ip4, &inaddr_any, 4) instead? */ + if (!inet_ntop(AF_INET, &lease->ip4.ip.ip4, addrbuf, + sizeof addrbuf)) + fatal("inet_ntop()"); + *offset += print_to_buf(buf, bufsize, *offset, "ipv4=%s/%d\n", + addrbuf, lease->ip4.cidr); + ret = true; + } + if (lease->ip6.family) { /* FIXME: !IN6_IS_ADDR_UNSPECIFIED(&lease->ip6) instead? */ + if (!inet_ntop(AF_INET6, &lease->ip6.ip.ip6, addrbuf, + sizeof addrbuf)) + fatal("inet_ntop()"); + *offset += print_to_buf(buf, bufsize, *offset, "ipv6=%s/%d\n", + addrbuf, lease->ip6.cidr); + ret = true; + } + + if (ret) { + *offset += print_to_buf(buf, bufsize, *offset, + "leasestart=%u\n", lease->start); + *offset += print_to_buf(buf, bufsize, *offset, "leasetime=%u\n", + lease->leasetime); + } + + return ret; +} + +static struct wg_peer *current_peer(struct wg_dynamic_request *req) +{ + struct wg_peer *peer; + + wg_for_each_peer (device, peer) { + if (!memcmp(peer->public_key, req->pubkey, sizeof(wg_key))) + return peer; + } + + return NULL; +} + +static void insert_allowed_ip(struct wg_peer *peer, + const struct wg_combined_ip *ip) { - if (close(*fd)) - debug("Failed to close socket"); + struct wg_allowedip *newip; + + newip = calloc(1, sizeof(struct wg_allowedip)); + if (!newip) + fatal("calloc()"); - *fd = -1; - free_wg_dynamic_request(req); + newip->family = ip->family; + switch (newip->family) { + case AF_INET: + memcpy(&newip->ip4, &ip->ip.ip4, sizeof(struct in_addr)); + break; + case AF_INET6: + memcpy(&newip->ip6, &ip->ip.ip6, sizeof(struct in6_addr)); + break; + } + newip->cidr = ip->cidr; + + if (!peer->first_allowedip) + peer->first_allowedip = newip; + else + peer->last_allowedip->next_allowedip = newip; + peer->last_allowedip = newip; } -static void send_response(int fd, struct wg_dynamic_request *req) +static int add_allowed_ips(struct wg_peer *peer, + const struct wg_dynamic_lease *lease) { + if (lease->ip4.ip.ip4.s_addr) + insert_allowed_ip(peer, &lease->ip4); + if (!IN6_IS_ADDR_UNSPECIFIED(&lease->ip6.ip.ip6)) + insert_allowed_ip(peer, &lease->ip6); + + return wg_set_device(device); +} + +static bool send_response(int fd, struct wg_dynamic_request *req) +{ + int ret; + char *errmsg = "OK"; + unsigned char buf[MAX_RESPONSE_SIZE + 1]; + size_t msglen; + size_t written; + struct wg_dynamic_lease lease = { 0 }; + struct wg_peer *peer; + +#if 0 printf("Recieved request 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; } -} +#endif -static void send_error(int fd, int ret) -{ - debug("Error: %s\n", strerror(ret)); + peer = current_peer(req); + if (!peer) + die("Unable to find peer\n"); + + ret = 0; + msglen = 0; + switch (req->cmd) { + case WGKEY_REQUEST_IP: + msglen = print_to_buf((char *)buf, sizeof buf, 0, "%s=%d\n", + WG_DYNAMIC_KEY[req->cmd], 1); + ret = allocate_from_pool(req, &lease); + if (ret) { + debug("IP address allocation failing with %d\n", ret); + ret = 0x01; + errmsg = "Out of IP addresses"; + break; + } + + ret = add_allowed_ips(peer, &lease); + if (ret) { + debug("Unable to add address(es) to peer: %s\n", + strerror(-ret)); + ret = 0x2; + errmsg = "Internal error"; + break; + } + + if (!serialise_lease((char *)buf, sizeof buf, &msglen, + &lease)) { + die("Nothing to hand out, despite succeeding allocate_from_pool()\n"); + ret = 0x3; + errmsg = "Internal error"; + break; + } + + break; + + default: + debug("Unknown command: %d\n", req->cmd); + return true; + } + + msglen += print_to_buf((char *)buf, sizeof buf, msglen, "errno=%d\n", ret); + if (ret) + msglen += print_to_buf((char *)buf, sizeof buf, msglen, "errmsg=%s\n", errmsg); + if (msglen == sizeof buf) + fatal("Outbuffer too small"); + buf[msglen++] = '\n'; + + written = send_message(fd, buf, &msglen); + if (msglen == 0) + return true; + + debug("Socket %d blocking on write with %lu bytes left, postponing\n", + fd, msglen); + send_later(req, buf + written, msglen); + return false; } static void setup_socket(int *fd) @@ -367,13 +539,11 @@ int main(int argc, char *argv[]) int *sockfd = &pollfds[0].fd; progname = argv[0]; - inet_pton(AF_INET6, WG_DYNAMIC_ADDR, &well_known); + if (!inet_pton(AF_INET6, WG_DYNAMIC_ADDR, &well_known)) + fatal("inet_pton()"); for (int i = 0; i < MAX_CONNECTIONS + 1; ++i) { - pollfds[i] = (struct pollfd){ - .fd = -1, - .events = POLLIN, - }; + pollfds[i] = (struct pollfd){.fd = -1, .events = POLLIN }; } if (argc != 2) @@ -408,13 +578,26 @@ int main(int argc, char *argv[]) } for (int i = 1; i < MAX_CONNECTIONS + 1; ++i) { - if (!(pollfds[i].revents & POLLIN)) + if (!(pollfds[i].revents & POLLOUT)) + continue; + + pollfds[i].revents &= ~POLLOUT; + send_message(pollfds[i].fd, reqs[i - 1].buf, + &reqs[i - 1].buflen); + if (!reqs[i - 1].buflen) + close_connection(&pollfds[i].fd, &reqs[i - 1]); + } + + for (int i = 1; i < MAX_CONNECTIONS + 1; ++i) { + if (pollfds[i].fd < 0 || !pollfds[i].revents & POLLIN) continue; - pollfds[i].revents = 0; + pollfds[i].revents &= ~POLLIN; if (handle_request(pollfds[i].fd, &reqs[i - 1], send_response, send_error)) close_connection(&pollfds[i].fd, &reqs[i - 1]); + else if (reqs[i - 1].buf) + pollfds[i].events |= POLLOUT; } } |