aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--common.h5
-rw-r--r--docs/ip-request.md255
-rw-r--r--docs/protocol.md18
-rw-r--r--lease.c354
-rw-r--r--lease.h39
-rwxr-xr-xtests/clientsh.bash302
-rwxr-xr-xtests/netsh.sh89
-rwxr-xr-xtests/restore-leases.bash162
-rw-r--r--wg-dynamic-server.c110
9 files changed, 1160 insertions, 174 deletions
diff --git a/common.h b/common.h
index 04df3cf..917dfe8 100644
--- a/common.h
+++ b/common.h
@@ -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.
diff --git a/lease.c b/lease.c
index f0ec97c..2915736 100644
--- a/lease.c
+++ b/lease.c
@@ -14,6 +14,7 @@
#include <stdlib.h>
#include <sys/socket.h>
#include <time.h>
+#include <string.h>
#include "common.h"
#include "dbg.h"
@@ -90,10 +91,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, &current->ipv4, ip_asc,
+ sizeof(ip_asc));
+ debug("deleting from pool: %s\n", ip_asc);
+
+ if (ipp_del_v4(&ipns, &current->ipv4, 32))
+ die("ipp_del_v4()\n");
+ }
- if (wants_ipv4 && !ipns.total_ipv4)
- return NULL; /* no ipv4 addresses available */
+ if (!IN6_IS_ADDR_UNSPECIFIED(&current->ipv6) &&
+ IN6_IS_ADDR_UNSPECIFIED(&new->ipv6)) {
+ inet_ntop(AF_INET6, &current->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, &current->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
diff --git a/lease.h b/lease.h
index 3e1402d..383bac7 100644
--- a/lease.h
+++ b/lease.h
@@ -13,13 +13,15 @@
#include "common.h"
#include "netlink.h"
+#define WG_DYNAMIC_LEASE_CHUNKSIZE 256
+
struct wg_dynamic_lease {
time_t start_real;
time_t start_mono;
uint32_t leasetime; /* in seconds */
struct in_addr ipv4;
struct in6_addr ipv6;
- struct wg_dynamic_lease *next;
+ struct in6_addr lladdr;
};
/*
@@ -34,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();