From 4fbff9a4454ac6bd71aebc0df6d8442a467201db Mon Sep 17 00:00:00 2001 From: Linus Nordberg Date: Mon, 8 Apr 2019 22:05:59 +0200 Subject: Implement client sending ip-request and configuring interface --- tests/netsh.sh | 4 +- wg-dynamic-client.c | 529 ++++++++++++++++++++++++++++++++++++++++++++++++++-- wg-dynamic-server.c | 12 +- 3 files changed, 521 insertions(+), 24 deletions(-) diff --git a/tests/netsh.sh b/tests/netsh.sh index 27979c5..3d98550 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) \ @@ -71,6 +71,8 @@ configure_peers() { ip1 link set up dev wg0 ip2 link set up dev wg0 + + ip2 route add fe80::/128 dev wg0 } configure_peers diff --git a/wg-dynamic-client.c b/wg-dynamic-client.c index 8554269..f966747 100644 --- a/wg-dynamic-client.c +++ b/wg-dynamic-client.c @@ -3,38 +3,527 @@ * Copyright (C) 2019 WireGuard LLC. All Rights Reserved. */ -#include "netlink.h" +#include +#include +#include #include #include #include +#include + +#include +#include +#include + +#include "common.h" +#include "dbg.h" +#include "netlink.h" + +#define LEASE_CHECK_INTERVAL 1000 /* 1s is convenient for testing */ + +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 \n", progname); +} + +int data_cb(const struct nlmsghdr *nlh, void *data) +{ + struct nlattr *tb[IFA_MAX + 1] = {}; + struct ifaddrmsg *ifa = mnl_nlmsg_get_payload(nlh); + struct mnl_cb_data *cb_data = (struct mnl_cb_data *)data; + unsigned char *addr; + + if (ifa->ifa_index != cb_data->ifindex) + return MNL_CB_OK; + + mnl_attr_parse(nlh, sizeof(*ifa), data_attr_cb, tb); + + if (!tb[IFA_ADDRESS]) + return MNL_CB_OK; + + addr = mnl_attr_get_payload(tb[IFA_ADDRESS]); + char out[INET6_ADDRSTRLEN]; + inet_ntop(ifa->ifa_family, addr, out, sizeof(out)); + debug("index=%d, family=%d, addr=%s\n", ifa->ifa_index, ifa->ifa_family, + out); + + if (ifa->ifa_scope == RT_SCOPE_LINK) { + if (ifa->ifa_prefixlen != 128) + return MNL_CB_OK; + memcpy(cb_data->lladdr, addr, 16); + } else if (ifa->ifa_scope == RT_SCOPE_UNIVERSE) { + switch (ifa->ifa_family) { + case AF_INET: + cb_data->gaddr4->family = ifa->ifa_family; + memcpy(&cb_data->gaddr4->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_port = htons(WG_DYNAMIC_PORT), + .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"); + + if (bind(*fd, (struct sockaddr *)&our_addr, sizeof(our_addr))) + fatal("Binding socket failed"); + + if (inet_pton(AF_INET6, WG_DYNAMIC_ADDR, &their_addr.sin6_addr) != 1) + 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 with an allowed-ips including 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) { + size_t off; + 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); + off = send_message(pollfds[0].fd, req.buf, &req.buflen); + if (req.buflen) + memmove(req.buf, req.buf + off, req.buflen); + else + 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 77eb9a4..34fe9bc 100644 --- a/wg-dynamic-server.c +++ b/wg-dynamic-server.c @@ -543,13 +543,19 @@ int main(int argc, char *argv[]) } for (int i = 1; i < MAX_CONNECTIONS + 1; ++i) { + size_t off; + 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) + + off = send_message(pollfds[i].fd, reqs[i - 1].buf, + &reqs[i - 1].buflen); + if (reqs[i - 1].buflen) + memmove(reqs[i - 1].buf, reqs[i - 1].buf + off, + reqs[i - 1].buflen); + else close_connection(&pollfds[i].fd, &reqs[i - 1]); } -- cgit v1.2.3-59-g8ed1b