diff options
-rw-r--r-- | common.h | 5 | ||||
-rw-r--r-- | docs/ip-request.md | 255 | ||||
-rw-r--r-- | docs/protocol.md | 18 | ||||
-rw-r--r-- | lease.c | 354 | ||||
-rw-r--r-- | lease.h | 39 | ||||
-rwxr-xr-x | tests/clientsh.bash | 302 | ||||
-rwxr-xr-x | tests/netsh.sh | 89 | ||||
-rwxr-xr-x | tests/restore-leases.bash | 162 | ||||
-rw-r--r-- | wg-dynamic-server.c | 110 |
9 files changed, 1160 insertions, 174 deletions
@@ -21,7 +21,7 @@ static const char WG_DYNAMIC_ADDR[] = "fe80::"; static const uint16_t WG_DYNAMIC_PORT = 970; /* ASCII sum of "wireguard" */ -#define WG_DYNAMIC_LEASETIME 10 /* NOTE: 10s is good for testing purposes */ +#define WG_DYNAMIC_DEFAULT_LEASETIME 3600 #define ITEMS \ E(WGKEY_UNKNOWN, "") /* must be the first entry */ \ @@ -62,6 +62,9 @@ static const char *const WG_DYNAMIC_ERR[] = { ITEMS }; 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 len; /* <= MAX_LINESIZE */ void *result; diff --git a/docs/ip-request.md b/docs/ip-request.md new file mode 100644 index 0000000..1852bae --- /dev/null +++ b/docs/ip-request.md @@ -0,0 +1,255 @@ + +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 new file mode 100644 index 0000000..4ff9bd9 --- /dev/null +++ b/docs/protocol.md @@ -0,0 +1,18 @@ +# 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. @@ -14,6 +14,7 @@ #include <stdlib.h> #include <sys/socket.h> #include <time.h> +#include <string.h> #include "common.h" #include "dbg.h" @@ -90,10 +91,46 @@ void leases_free() ipp_free(&ipns); } -struct wg_dynamic_lease *new_lease(wg_key pubkey, uint32_t leasetime, - struct in_addr *ipv4, struct in6_addr *ipv6) +static struct wg_dynamic_lease *new_lease(const struct wg_dynamic_lease *lease, + const struct in_addr *ipv4, + const struct in6_addr *ipv6) { - struct wg_dynamic_lease *lease, *parent; + struct wg_dynamic_lease *newlease; + + newlease = calloc(1, sizeof(*newlease)); + if (!newlease) + fatal("calloc()"); + + if (!lease_is_valid(lease)) + return newlease; + + 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); + + newlease->ipv4 = lease->ipv4; + } + + 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); + + newlease->ipv6 = lease->ipv6; + } + + return newlease; +} + +struct wg_dynamic_lease *set_lease(const char *devname, wg_key pubkey, + uint32_t leasetime, + const struct in6_addr *lladdr, + const struct in_addr *ipv4, + const struct in6_addr *ipv6) +{ + struct wg_dynamic_lease *current, *new; uint64_t index_l; uint32_t index, index_h; struct timespec tp; @@ -101,37 +138,66 @@ struct wg_dynamic_lease *new_lease(wg_key pubkey, uint32_t leasetime, int ret; bool wants_ipv4 = !ipv4 || ipv4->s_addr; bool wants_ipv6 = !ipv6 || !IN6_IS_ADDR_UNSPECIFIED(ipv6); + wg_key_b64_string pubkey_asc; + wg_key_to_base64(pubkey_asc, pubkey); - lease = malloc(sizeof *lease); - if (!lease) - fatal("malloc()"); + current = get_leases(pubkey); + new = new_lease(current, ipv4, ipv6); + + if (current) { + char ip_asc[INET6_ADDRSTRLEN]; + + 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(&ipns, ¤t->ipv4, 32)) + die("ipp_del_v4()\n"); + } - if (wants_ipv4 && !ipns.total_ipv4) - return NULL; /* no ipv4 addresses available */ + 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 (wants_ipv6 && !ipns.totalh_ipv6 && !ipns.totall_ipv6) - return NULL; /* no ipv6 addresses available */ + if (ipp_del_v6(&ipns, ¤t->ipv6, 128)) + die("ipp_del_v6()\n"); + } + } - if (wants_ipv4) { - if (!ipv4) { - index = random_bounded(ipns.total_ipv4); + if (wants_ipv4 && !new->ipv4.s_addr) { + if (!ipns.total_ipv4) { + debug("IPv4 pool empty\n"); + } else if (!ipv4) { + index = random_bounded(ipns.total_ipv4 - 1); debug("new_lease(v4): %u of %ju\n", index, ipns.total_ipv4); - ipp_addnth_v4(&ipns, &lease->ipv4, index); + ipp_addnth_v4(&ipns, &new->ipv4, index); } else { - if (ipp_add_v4(&ipns, ipv4, 32)) - return NULL; + char ip_asc[INET_ADDRSTRLEN]; + inet_ntop(AF_INET, ipv4, ip_asc, sizeof(ip_asc)); + debug("wants %s: ", ip_asc); + + if (!ipp_add_v4(&ipns, ipv4, 32)) { + debug("allocated\n"); - memcpy(&lease->ipv4, ipv4, sizeof *ipv4); + new->ipv4 = *ipv4; + } else { + debug("not free\n"); + } } } - if (wants_ipv6) { - if (!ipv6) { + if (wants_ipv6 && IN6_IS_ADDR_UNSPECIFIED(&new->ipv6)) { + if (!ipns.totalh_ipv6 && !ipns.totall_ipv6) { + debug("IPv6 pool empty\n"); + } else if (!ipv6) { if (ipns.totalh_ipv6 > 0) { - index_l = random_u64(); - index_h = random_bounded(ipns.totalh_ipv6); + index_l = random_bounded(UINT64_MAX); + index_h = random_bounded(ipns.totalh_ipv6 - 1); } else { index_l = random_bounded(ipns.totall_ipv6); index_h = 0; @@ -139,26 +205,45 @@ struct wg_dynamic_lease *new_lease(wg_key pubkey, uint32_t leasetime, debug("new_lease(v6): %u:%ju of %u:%ju\n", index_h, index_l, ipns.totalh_ipv6, ipns.totall_ipv6); - ipp_addnth_v6(&ipns, &lease->ipv6, index_l, index_h); + ipp_addnth_v6(&ipns, &new->ipv6, index_l, index_h); } else { - if (ipp_add_v6(&ipns, ipv6, 128)) { - if (!ipv4 || ipv4->s_addr) - ipp_del_v4(&ipns, ipv4, 32); + char ip_asc[INET6_ADDRSTRLEN]; + inet_ntop(AF_INET6, ipv6, ip_asc, sizeof(ip_asc)); + debug("wants %s: ", ip_asc); - return NULL; + if (!ipp_add_v6(&ipns, ipv6, 128)) { + debug("allocated\n"); + + new->ipv6 = *ipv6; + } else { + debug("not free\n"); } + } + } - memcpy(&lease->ipv6, ipv6, sizeof *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; } if (clock_gettime(CLOCK_REALTIME, &tp)) fatal("clock_gettime(CLOCK_REALTIME)"); - - lease->start_real = tp.tv_sec; - lease->start_mono = get_monotonic_time(); - lease->leasetime = leasetime; - lease->next = NULL; + new->start_real = tp.tv_sec; + new->start_mono = get_monotonic_time(); + new->leasetime = leasetime; wg_key *pubcopy = malloc(sizeof(wg_key)); if (!pubcopy) @@ -169,21 +254,21 @@ struct wg_dynamic_lease *new_lease(wg_key pubkey, uint32_t leasetime, if (ret < 0) { fatal("kh_put()"); } else if (ret == 0) { - parent = kh_value(leases_ht, k); - while (parent->next) - parent = parent->next; - - parent->next = lease; - } else { - kh_value(leases_ht, k) = lease; + BUG_ON(!current); + BUG_ON(kh_value(leases_ht, k) != current); + debug("freeing lease (replace): %s\n", lease_to_str(current)); + free(current); } + kh_value(leases_ht, k) = new; - if (lease->start_mono + lease->leasetime < gexpires) - gexpires = lease->start_mono + lease->leasetime; + debug("new lease: %s\n", lease_to_str(new)); + + if (new->start_mono + new->leasetime < gexpires) + gexpires = new->start_mono + new->leasetime; /* TODO: add record to file */ - return lease; + return new; } struct wg_dynamic_lease *get_leases(wg_key pubkey) @@ -196,55 +281,155 @@ struct wg_dynamic_lease *get_leases(wg_key pubkey) return kh_val(leases_ht, k); } -bool extend_lease(struct wg_dynamic_lease *lease, uint32_t leasetime) +struct allowedips_update { + wg_key peer_pubkey; + struct in6_addr lladdr; + struct in_addr ipv4; + struct in6_addr ipv6; +}; + +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) { - UNUSED(lease); - UNUSED(leasetime); - return false; + struct allowedips_update update; + + memcpy(update.peer_pubkey, peer_pubkey, sizeof(wg_key)); + update.lladdr = lease->lladdr; + update.ipv4 = lease->ipv4; + update.ipv6 = lease->ipv6; + + update_allowed_ips_bulk(devname, &update, 1); } -int leases_refresh() +int leases_refresh(const char *devname) { time_t cur_time = get_monotonic_time(); + struct allowedips_update updates[WG_DYNAMIC_LEASE_CHUNKSIZE] = { 0 }; if (cur_time < gexpires) return MIN(INT_MAX / 1000, gexpires - cur_time); gexpires = TIME_T_MAX; + int i = 0; for (khint_t k = kh_begin(leases_ht); k != kh_end(leases_ht); ++k) { if (!kh_exist(leases_ht, k)) continue; - - struct wg_dynamic_lease **pp = &kh_val(leases_ht, k), *tmp; - while (*pp) { - struct in_addr *ipv4 = &(*pp)->ipv4; - struct in6_addr *ipv6 = &(*pp)->ipv6; - time_t expires = (*pp)->start_mono + (*pp)->leasetime; - if (cur_time >= expires) { - if (ipv4->s_addr) - ipp_del_v4(&ipns, ipv4, 32); - - if (!IN6_IS_ADDR_UNSPECIFIED(ipv6)) - ipp_del_v6(&ipns, ipv6, 128); - - tmp = *pp; - *pp = (*pp)->next; - free(tmp); - } else { - if (expires < gexpires) - gexpires = expires; - - pp = &(*pp)->next; + struct wg_dynamic_lease *lease = kh_val(leases_ht, k); + BUG_ON(!lease); + time_t expires = lease->start_mono + lease->leasetime; + if (cur_time >= expires) { + if (lease->ipv4.s_addr) + ipp_del_v4(&ipns, &lease->ipv4, 32); + + if (!IN6_IS_ADDR_UNSPECIFIED(&lease->ipv6)) + ipp_del_v6(&ipns, &lease->ipv6, 128); + + memcpy(updates[i].peer_pubkey, kh_key(leases_ht, k), + sizeof(wg_key)); + updates[i].lladdr = lease->lladdr; + + wg_key_b64_string pubkey_asc; + wg_key_to_base64(pubkey_asc, updates[i].peer_pubkey); + debug("Peer losing its lease: %s\n", pubkey_asc); + + ++i; + if (i == WG_DYNAMIC_LEASE_CHUNKSIZE) { + update_allowed_ips_bulk(devname, updates, i); + i = 0; + memset(updates, 0, sizeof updates); } - } - if (!kh_val(leases_ht, k)) { + free(lease); free((char *)kh_key(leases_ht, k)); kh_del(leaseht, leases_ht, k); + } else { + if (expires < gexpires) + gexpires = expires; } } + if (i) + update_allowed_ips_bulk(devname, updates, i); + return MIN(INT_MAX / 1000, gexpires - cur_time); } @@ -347,3 +532,32 @@ void leases_update_pools(struct mnl_socket *nlsock) if (ret == -1 && errno != EAGAIN && errno != EWOULDBLOCK) fatal("mnl_socket_recvfrom()"); } + +bool lease_is_valid(const struct wg_dynamic_lease *lease) +{ + if (!lease) + return false; + + if (get_monotonic_time() >= lease->start_mono + lease->leasetime) + return false; + + return true; +} + +#ifdef DEBUG +char *lease_to_str(const struct wg_dynamic_lease *l) +{ + static char buf[4096]; + char ll[INET6_ADDRSTRLEN], v4[INET_ADDRSTRLEN], v6[INET6_ADDRSTRLEN]; + + if (!l) + return "(null)"; + + inet_ntop(AF_INET6, &l->lladdr, ll, sizeof ll); + inet_ntop(AF_INET, &l->ipv4, v4, sizeof v4); + inet_ntop(AF_INET6, &l->ipv6, v6, sizeof v6); + snprintf(buf, sizeof buf, "(%p) [%s] %s [%s]", l, ll, v4, v6); + + return buf; +} +#endif @@ -13,13 +13,15 @@ #include "common.h" #include "netlink.h" +#define WG_DYNAMIC_LEASE_CHUNKSIZE 256 + struct wg_dynamic_lease { time_t start_real; time_t start_mono; uint32_t leasetime; /* in seconds */ struct in_addr ipv4; struct in6_addr ipv6; - struct wg_dynamic_lease *next; + struct in6_addr lladdr; }; /* @@ -34,31 +36,44 @@ void leases_init(char *fname, struct mnl_socket *nlsock); void leases_free(); /* - * Creates a new lease and returns a pointer to it, or NULL if either we ran out - * of assignable IPs or the requested IP is already taken. + * Creates a new lease and returns a pointer to it, or NULL if either + * we ran out of assignable IPs or the requested IP's are already + * taken. Frees currently held lease, if any. Updates allowedips for + * the peer. */ -struct wg_dynamic_lease *new_lease(wg_key pubkey, uint32_t leasetime, - struct in_addr *ipv4, struct in6_addr *ipv6); +struct wg_dynamic_lease *set_lease(const char *devname, wg_key pubkey, + uint32_t leasetime, + const struct in6_addr *lladdr, + const struct in_addr *ipv4, + const struct in6_addr *ipv6); /* * Returns all leases belonging to pubkey, or NULL if there are none. */ struct wg_dynamic_lease *get_leases(wg_key pubkey); -/* - * Extend the lease to be leasetime seconds long again. Returns true on error, - * or false otherwise. - */ -bool extend_lease(struct wg_dynamic_lease *lease, uint32_t leasetime); - /* Refreshes all leases, meaning expired ones will be removed. Returns the * amount of seconds until the next lease will expire, or at most INT_MAX/1000. */ -int leases_refresh(); +int leases_refresh(const char *devname); + +/* + * Updates allowedips for peer_pubkey on devname, adding what's in + * lease (including lladdr), removing all others. + */ +void update_allowed_ips(const char *devname, wg_key peer_pubkey, + const struct wg_dynamic_lease *lease); /* * Updates all pools with information from the mnl socket nlsock. */ void leases_update_pools(struct mnl_socket *nlsock); +/* + * Return true if lease is !NULL and has not expired. + */ +bool lease_is_valid(const struct wg_dynamic_lease *lease); + +char *lease_to_str(const struct wg_dynamic_lease *l); + #endif diff --git a/tests/clientsh.bash b/tests/clientsh.bash new file mode 100755 index 0000000..3f297c5 --- /dev/null +++ b/tests/clientsh.bash @@ -0,0 +1,302 @@ +#! /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 0376c14..f9e9c1e 100755 --- a/tests/netsh.sh +++ b/tests/netsh.sh @@ -6,83 +6,84 @@ set -e exec 3>&1 + +[ $# -ge 1 ] && ( n_clients="$1"; shift; ) + export WG_HIDE_KEYS=never -netns0="wg-test-$$-0" -netns1="wg-test-$$-1" -netns2="wg-test-$$-2" +netnsn() { echo wg-test-$$-$1; } pretty() { echo -e "\x1b[32m\x1b[1m[+] ${1:+NS$1: }${2}\x1b[0m" >&3; } pp() { pretty "" "$*"; "$@"; } maybe_exec() { if [[ $BASHPID -eq $$ ]]; then "$@"; else exec "$@"; fi; } -n0() { pretty 0 "$*"; maybe_exec ip netns exec $netns0 "$@"; } -n1() { pretty 1 "$*"; maybe_exec ip netns exec $netns1 "$@"; } -n2() { pretty 2 "$*"; maybe_exec ip netns exec $netns2 "$@"; } -ip0() { pretty 0 "ip $*"; ip -n $netns0 "$@"; } -ip1() { pretty 1 "ip $*"; ip -n $netns1 "$@"; } -ip2() { pretty 2 "ip $*"; ip -n $netns2 "$@"; } +nn() { local netns=$(netnsn $1) n=$1; shift; pretty $n "$*"; maybe_exec ip netns exec $netns "$@"; } +ipn() { local netns=$(netnsn $1) n=$1; shift; pretty $n "ip $*"; ip -n $netns "$@"; } + +ns="0 1 2" cleanup() { set +e exec 2>/dev/null - ip0 link del dev wg0 - ip1 link del dev wg0 - ip2 link del dev wg0 - local to_kill="$(ip netns pids $netns0) $(ip netns pids $netns1) $(ip netns pids $netns2)" + + for n in $ns; do ipn $n link del dev wg0; done + + local to_kill="$(for n in $ns; do $(ip netns pids $(netnsn $n)); done)" [[ -n $to_kill ]] && kill $to_kill - pp ip netns del $netns0 - pp ip netns del $netns1 - pp ip netns del $netns2 + + for n in $ns; do pp ip netns del $(netnsn $n); done + exit } trap cleanup EXIT -ip netns del $netns0 2>/dev/null || true -ip netns del $netns1 2>/dev/null || true -ip netns del $netns2 2>/dev/null || true -pp ip netns add $netns0 -pp ip netns add $netns1 -pp ip netns add $netns2 -ip0 link set up dev lo - -ip0 link add dev wg0 type wireguard -ip0 link set wg0 netns $netns1 -ip0 link add dev wg0 type wireguard -ip0 link set wg0 netns $netns2 +pp ip netns add $(netnsn 0) +pp ip netns add $(netnsn 1) +pp ip netns add $(netnsn 2) +ipn 0 link set up dev lo + +ipn 0 link add dev wg0 type wireguard +ipn 0 link set wg0 netns $(netnsn 1) +ipn 0 link add dev wg0 type wireguard +ipn 0 link set wg0 netns $(netnsn 2) server_private=$(wg genkey) server_public=$(wg pubkey <<< $server_private) client_private=$(wg genkey) client_public=$(wg pubkey <<< $client_private) configure_peers() { - ip1 addr add fe80::/64 dev wg0 - ip2 addr add fe80::badc:0ffe:e0dd:f00d/128 dev wg0 + ipn 1 addr add fe80::/64 dev wg0 + ipn 2 addr add fe80::badc:0ffe:e0dd:f00d/128 dev wg0 - n1 wg set wg0 \ + nn 1 wg set wg0 \ private-key <(echo $server_private) \ listen-port 1 \ peer $client_public \ allowed-ips fe80::badc:0ffe:e0dd:f00d/128 - n2 wg set wg0 \ + nn 2 wg set wg0 \ private-key <(echo $client_private) \ listen-port 2 \ peer $server_public \ allowed-ips 0.0.0.0/0,::/0 - ip1 link set up dev wg0 - ip2 link set up dev wg0 + ipn 1 link set up dev wg0 + ipn 2 link set up dev wg0 - ip2 route add fe80::/128 dev wg0 - ip1 route add 192.168.4.0/28 dev wg0 - ip1 route add 192.168.73.0/27 dev wg0 - ip1 route add 2001:db8:1234::/124 dev wg0 - ip1 route add 2001:db8:7777::/124 dev wg0 + ipn 2 route add fe80::/128 dev wg0 + ipn 1 route add 192.168.4.0/28 dev wg0 + ipn 1 route add 192.168.73.0/27 dev wg0 + ipn 1 route add 2001:db8:1234::/124 dev wg0 + ipn 1 route add 2001:db8:7777::/124 dev wg0 } configure_peers -n1 wg set wg0 peer "$client_public" endpoint [::1]:2 -n2 wg set wg0 peer "$server_public" endpoint [::1]:1 -n2 ping6 -c 10 -f -W 1 fe80::%wg0 -n1 ping6 -c 10 -f -W 1 fe80::badc:0ffe:e0dd:f00d%wg0 +nn 1 wg set wg0 peer "$client_public" endpoint [::1]:2 +nn 2 wg set wg0 peer "$server_public" endpoint [::1]:1 +nn 2 ping6 -c 10 -f -W 1 fe80::%wg0 +nn 1 ping6 -c 10 -f -W 1 fe80::badc:0ffe:e0dd:f00d%wg0 + +pretty "" "clientsh.bash can be run with the following arguments:" +echo +echo wg-test-$$ $server_public +echo -n1 ./wg-dynamic-server wg0 +nn 1 ./wg-dynamic-server --leasetime 10 wg0 diff --git a/tests/restore-leases.bash b/tests/restore-leases.bash new file mode 100755 index 0000000..43e4737 --- /dev/null +++ b/tests/restore-leases.bash @@ -0,0 +1,162 @@ +set -e +exec 3>&1 + +export WG_HIDE_KEYS=never +netnsn() { echo wg-test-$$-$1; } +pretty() { echo -e "\x1b[32m\x1b[1m[+] ${1:+NS$1: }${2}\x1b[0m" >&3; } +pp() { pretty "" "$*"; "$@"; } +maybe_exec() { if [[ $BASHPID -eq $$ ]]; then "$@"; else exec "$@"; fi; } +nn() { local netns=$(netnsn $1) n=$1; shift; pretty $n "$*"; maybe_exec ip netns exec $netns "$@"; } +ipn() { local netns=$(netnsn $1) n=$1; shift; pretty $n "ip $*"; ip -n $netns "$@"; } + +ns="0 1 2" + +cleanup() { + set +e + exec 2>/dev/null + + 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 + +pp ip netns add $(netnsn 0) +pp ip netns add $(netnsn 1) +pp ip netns add $(netnsn 2) +ipn 0 link set up dev lo + +ipn 0 link add dev wg0 type wireguard +ipn 0 link set wg0 netns $(netnsn 1) +ipn 0 link add dev wg0 type wireguard +ipn 0 link set wg0 netns $(netnsn 2) +server_private=$(wg genkey) +server_public=$(wg pubkey <<< $server_private) +client_private=$(wg genkey) +client_public=$(wg pubkey <<< $client_private) + +configure_peers() { + 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 +} +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 + +pretty "" "clientsh.bash can be run with the following arguments:" +echo +echo wg-test-$$ $server_public +echo + +## Start server in the background with 30s lease time +exec 4< <(nn 1 ./wg-dynamic-server --leasetime 30 wg0) +server_pid=$! +pretty "" "server_pid: $server_pid" + +## Get a lease +send_cmd() { + local n=$1; shift + local REQ="$1"; shift + + eval $( + printf $REQ | nn $n ncat -v -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 + ) +} +check_alowedips() { + local pubkey="$1"; shift + local ip="$1"; shift + + [[ -z "$ip" ]] && return 0 + + nn 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 "" "Missing $ip in allowedips" + return 1 +} +declare -a IPV4 +declare -a IPV6 +declare -a LEASESTART +declare -a LEASETIME +declare -a ERRNO + +send_cmd 2 "request_ip=1\n\n" +check_alowedips "$client_public" "${IPV4[2]}" +check_alowedips "$client_public" "${IPV6[2]}" + +## Restart server with 10s leasetime +nn 1 kill $server_pid +sleep 1 +exec 4< <(nn 1 ./wg-dynamic-server --leasetime 3 wg0) || { pretty "" $?; false; } +server_pid=$! +pretty "" "server_pid 2: $server_pid" + +## Verify that the lease has been restored +check_alowedips "$client_public" "${IPV4[2]}" +check_alowedips "$client_public" "${IPV6[2]}" + +## Wait for the lease to expire +pp sleep 4 + +## Restart server +nn 1 kill $server_pid +sleep 1 +exec 4< <(nn 1 ./wg-dynamic-server wg0) + +## Verify that the lease has not reappeared +notlladdr='192.168.*/32|2001:.*/128' +nn 1 wg show wg0 allowed-ips | + while read -r _pubkey _ips; do + [[ "$_pubkey" = "$client_public" ]] || continue + for _ip in $_ips; do + [[ "$_ip" =~ $notlladdr ]] && { pretty "" "FAIL: $_ip"; false; } + done + done && true + +pretty "" "SUCCESS\n" diff --git a/wg-dynamic-server.c b/wg-dynamic-server.c index 0f16710..feb9656 100644 --- a/wg-dynamic-server.c +++ b/wg-dynamic-server.c @@ -9,7 +9,6 @@ #include <stdint.h> #include <stdio.h> #include <stdlib.h> -#include <string.h> #include <time.h> #include <arpa/inet.h> @@ -30,10 +29,11 @@ #include "netlink.h" static const char *progname; -static const char *wg_interface; +static const char *wg_interface = NULL; static struct in6_addr well_known; static wg_device *device = NULL; +static uint32_t leasetime = WG_DYNAMIC_DEFAULT_LEASETIME; static int sockfd = -1; static int epollfd = -1; @@ -46,6 +46,7 @@ struct wg_dynamic_connection { struct wg_dynamic_request req; int fd; wg_key pubkey; + struct in6_addr lladdr; unsigned char *outbuf; size_t buflen; }; @@ -54,7 +55,7 @@ static struct wg_dynamic_connection connections[MAX_CONNECTIONS] = { 0 }; static void usage() { - die("usage: %s <wg-interface>\n", progname); + die("usage: %s [--leasetime <leasetime>] <wg-interface>\n", progname); } static bool valid_peer_found(wg_device *device) @@ -128,7 +129,8 @@ static wg_key *addr_to_pubkey(struct sockaddr_storage *addr) return NULL; } -static int accept_connection(wg_key *dest) +static int accept_connection(wg_key *dest_pubkey, + struct in6_addr *dest_lladdr) { int fd; wg_key *pubkey; @@ -172,7 +174,10 @@ static int accept_connection(wg_key *dest) return -ENOENT; } } - memcpy(dest, pubkey, sizeof *dest); + memcpy(dest_pubkey, pubkey, sizeof *dest_pubkey); + + memcpy(dest_lladdr, &((struct sockaddr_in6 *)&addr)->sin6_addr, + sizeof *dest_lladdr); wg_key_b64_string key; char out[INET6_ADDRSTRLEN]; @@ -216,7 +221,6 @@ static bool send_message(struct wg_dynamic_connection *con, con->outbuf = malloc(con->buflen); if (!con->outbuf) fatal("malloc()"); - memcpy(con->outbuf, buf + offset, con->buflen); } else { con->buflen = len - offset; @@ -240,40 +244,6 @@ void close_connection(struct wg_dynamic_connection *con) con->buflen = 0; } -static void add_allowed_ips(wg_key pubkey, struct in_addr *ipv4, - struct in6_addr *ipv6) -{ - wg_allowedip allowed_v4, allowed_v6; - wg_peer peer = { 0 }; - wg_device dev = { .first_peer = &peer }; - - strcpy(dev.name, wg_interface); - memcpy(peer.public_key, pubkey, sizeof peer.public_key); - wg_allowedip **cur = &peer.first_allowedip; - - if (ipv4) { - allowed_v4 = (wg_allowedip){ - .family = AF_INET, - .cidr = 32, - .ip4 = *ipv4, - }; - *cur = &allowed_v4; - cur = &allowed_v4.next_allowedip; - } - - if (ipv6) { - allowed_v6 = (wg_allowedip){ - .family = AF_INET6, - .cidr = 128, - .ip6 = *ipv6, - }; - *cur = &allowed_v6; - } - - if (wg_set_device(&dev)) - fatal("wg_set_device()"); -} - static bool send_response(struct wg_dynamic_connection *con) { char buf[MAX_RESPONSE_SIZE]; @@ -282,19 +252,19 @@ static bool send_response(struct wg_dynamic_connection *con) switch (con->req.cmd) { case WGKEY_REQUEST_IP:; struct wg_dynamic_request_ip *rip = con->req.result; + struct in6_addr *lladdr = &con->lladdr; struct in_addr *ip4 = rip->has_ipv4 ? &rip->ipv4 : NULL; struct in6_addr *ip6 = rip->has_ipv6 ? &rip->ipv6 : NULL; struct wg_dynamic_lease *lease; struct wg_dynamic_request_ip ans = { 0 }; - lease = new_lease(con->pubkey, WG_DYNAMIC_LEASETIME, ip4, ip6); + lease = set_lease(wg_interface, con->pubkey, leasetime, lladdr, ip4, ip6); if (lease) { memcpy(&ans.ipv4, &lease->ipv4, sizeof ans.ipv4); memcpy(&ans.ipv6, &lease->ipv6, sizeof ans.ipv6); ans.has_ipv4 = ans.has_ipv6 = true; ans.start = lease->start_real; ans.leasetime = lease->leasetime; - add_allowed_ips(con->pubkey, &ans.ipv4, &ans.ipv6); } else { ans.wg_errno = E_IP_UNAVAIL; } @@ -402,6 +372,33 @@ static void cleanup() } } +static void init_leaess_from_peers() +{ + wg_peer *peer; + + wg_for_each_peer (device, peer) { + wg_allowedip *allowedip; + struct in6_addr *lladdr = NULL; + struct in_addr *ipv4 = NULL; + struct in6_addr *ipv6 = NULL; + wg_for_each_allowedip (peer, allowedip) { + if (allowedip->family == AF_INET6 && + IN6_IS_ADDR_LINKLOCAL(&allowedip->ip6)) + lladdr = &allowedip->ip6; + else if (allowedip->family == AF_INET && !ipv4) + ipv4 = &allowedip->ip4; + else if (allowedip->family == AF_INET6 && !ipv6) + ipv6 = &allowedip->ip6; + } + + if (!ipv4 && !ipv6) + continue; + + set_lease(wg_interface, peer->public_key, leasetime, lladdr, + ipv4, ipv6); + } +} + static void setup() { struct wg_combined_ip ip; @@ -442,7 +439,8 @@ static void setup() wg_interface); setup_sockets(); - leases_init("leases_file", nlsock); + leases_init(NULL, nlsock); + init_leaess_from_peers(); } static int get_avail_request() @@ -462,7 +460,7 @@ static void accept_incoming() struct epoll_event ev; while ((n = get_avail_request()) >= 0) { - fd = accept_connection(&connections[n].pubkey); + fd = accept_connection(&connections[n].pubkey, &connections[n].lladdr); if (fd < 0) { if (fd == -ENOENT) { debug("Failed to match IP to pubkey\n"); @@ -528,7 +526,7 @@ static void poll_loop() fatal("epoll_ctl()"); while (1) { - time_t next = leases_refresh() * 1000; + time_t next = leases_refresh(wg_interface) * 1000; int nfds = epoll_wait(epollfd, events, MAX_CONNECTIONS, next); if (nfds == -1) { if (errno == EINTR) @@ -544,11 +542,29 @@ static void poll_loop() int main(int argc, char *argv[]) { + char *endptr = NULL; + progname = argv[0]; - if (argc != 2) + ++argv; + --argc; + + while (argc > 0) { + if (!strcmp(argv[0], "--leasetime") && argc >= 2) { + leasetime = (uint32_t) strtoul(argv[1], &endptr, 10); + if (*endptr) + usage(); + argv += 2; + argc -= 2; + } else { + wg_interface = argv[0]; + argv += 1; + argc -= 1; + break; + } + } + if (!wg_interface || argc > 0) usage(); - wg_interface = argv[1]; setup(); poll_loop(); |