/* SPDX-License-Identifier: MIT * * Copyright (C) 2015-2019 WireGuard LLC. All Rights Reserved. */ #define _GNU_SOURCE #define _POSIX_C_SOURCE 200112L #include #include #include #include #include #include #include #include #include #include #include #include #include #include "common.h" #include "dbg.h" #include "ipm.h" #include "khash.h" #include "lease.h" #include "netlink.h" static const char *progname; static const char *wg_interface; static struct in6_addr well_known; static wg_device *device = NULL; static int sockfd = -1; static int epollfd = -1; static struct mnl_socket *nlsock = NULL; KHASH_MAP_INIT_INT64(allowedht, wg_key *) khash_t(allowedht) * allowedips_ht; struct wg_dynamic_connection { struct wg_dynamic_request req; int fd; wg_key pubkey; unsigned char *outbuf; size_t buflen; }; static struct wg_dynamic_connection connections[MAX_CONNECTIONS] = { 0 }; static void usage() { die("usage: %s \n", progname); } static bool valid_peer_found(wg_device *device) { wg_peer *peer; wg_key_b64_string key; wg_allowedip *allowedip; wg_for_each_peer (device, peer) { wg_key_to_base64(key, peer->public_key); debug("- peer %s\n", key); debug(" allowedips:\n"); wg_for_each_allowedip (peer, allowedip) { char out[INET6_ADDRSTRLEN]; inet_ntop(allowedip->family, &allowedip->ip6, out, sizeof(out)); debug(" %s\n", out); if (is_link_local(allowedip->ip6.s6_addr) && allowedip->cidr == 128) return true; } } return false; } static void rebuild_allowedips_ht() { wg_peer *peer; wg_allowedip *allowedip; khiter_t k; uint64_t lh; int ret; kh_clear(allowedht, allowedips_ht); wg_free_device(device); if (wg_get_device(&device, wg_interface)) fatal("Unable to access interface %s", wg_interface); wg_for_each_peer (device, peer) { wg_for_each_allowedip (peer, allowedip) { if (allowedip->family == AF_INET6 && is_link_local(allowedip->ip6.s6_addr) && allowedip->cidr == 128) { memcpy(&lh, allowedip->ip6.s6_addr + 8, 8); k = kh_put(allowedht, allowedips_ht, lh, &ret); if (ret <= 0) die("Failed to rebuild allowedips hashtable\n"); kh_value(allowedips_ht, k) = &peer->public_key; } } } } static wg_key *addr_to_pubkey(struct sockaddr_storage *addr) { khiter_t k; uint64_t lh; if (addr->ss_family == AF_INET6) { lh = *(uint64_t *)&((struct sockaddr_in6 *)addr) ->sin6_addr.s6_addr[8]; k = kh_get(allowedht, allowedips_ht, lh); if (k != kh_end(allowedips_ht)) return kh_val(allowedips_ht, k); } return NULL; } static int accept_connection(wg_key *dest) { int fd; wg_key *pubkey; struct sockaddr_storage addr; socklen_t size = sizeof addr; #ifdef __linux__ fd = accept4(sockfd, (struct sockaddr *)&addr, &size, SOCK_NONBLOCK); if (fd < 0) return -errno; #else fd = accept(sockfd, (struct sockaddr *)&addr, &size); if (fd < 0) return -errno; int res = fcntl(fd, F_GETFL, 0); if (res < 0 || fcntl(fd, F_SETFL, res | O_NONBLOCK) < 0) fatal("Setting socket to nonblocking failed"); #endif if (addr.ss_family != AF_INET6) { debug("Rejecting client for not using an IPv6 address\n"); return -EINVAL; } if (((struct sockaddr_in6 *)&addr)->sin6_port != htons(WG_DYNAMIC_PORT)) { debug("Rejecting client for using port %u != %u\n", htons(((struct sockaddr_in6 *)&addr)->sin6_port), WG_DYNAMIC_PORT); return -EINVAL; } pubkey = addr_to_pubkey(&addr); if (!pubkey) { /* our copy of allowedips is outdated, refresh */ rebuild_allowedips_ht(); pubkey = addr_to_pubkey(&addr); if (!pubkey) { /* either we lost the race or something is very wrong */ close(fd); return -ENOENT; } } memcpy(dest, pubkey, sizeof *dest); wg_key_b64_string key; char out[INET6_ADDRSTRLEN]; wg_key_to_base64(key, *pubkey); inet_ntop(addr.ss_family, &((struct sockaddr_in6 *)&addr)->sin6_addr, out, sizeof(out)); debug("%s has pubkey: %s\n", out, key); return fd; } static bool send_message(struct wg_dynamic_connection *con, const unsigned char *buf, size_t len) { size_t offset = 0; while (1) { ssize_t written = write(con->fd, buf + offset, len - offset); if (written < 0) { if (errno == EWOULDBLOCK || errno == EAGAIN) break; if (errno == EINTR) continue; debug("Writing to socket %d failed: %s\n", con->fd, strerror(errno)); return false; } offset += written; if (offset == len) return true; } debug("Socket %d blocking on write with %lu bytes left, postponing\n", con->fd, len - offset); if (!con->outbuf) { con->buflen = len - offset; con->outbuf = malloc(con->buflen); if (!con->outbuf) fatal("malloc()"); memcpy(con->outbuf, buf + offset, con->buflen); } else { con->buflen = len - offset; memmove(con->outbuf, buf + offset, con->buflen); } return true; } void close_connection(struct wg_dynamic_connection *con) { free_wg_dynamic_request(&con->req); if (close(con->fd)) debug("Failed to close socket\n"); con->fd = -1; memset(con->pubkey, 0, sizeof con->pubkey); free(con->outbuf); con->outbuf = NULL; 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]; size_t msglen; switch (con->req.cmd) { case WGKEY_REQUEST_IP:; struct wg_dynamic_request_ip *rip = con->req.result; struct in_addr *ip4 = rip->has_ipv4 ? &rip->ipv4 : NULL; struct in6_addr *ip6 = rip->has_ipv6 ? &rip->ipv6 : NULL; struct wg_dynamic_lease *lease; struct wg_dynamic_request_ip ans = { 0 }; lease = new_lease(con->pubkey, WG_DYNAMIC_LEASETIME, 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; } msglen = serialize_request_ip(false, buf, sizeof buf, &ans); break; default: debug("Unknown command: %d\n", con->req.cmd); BUG(); } return send_message(con, (unsigned char *)buf, msglen); } static void handle_client(struct wg_dynamic_connection *con) { unsigned char buf[RECV_BUFSIZE + MAX_LINESIZE]; size_t rem = 0; ssize_t ret; while ((ret = handle_request(con->fd, &con->req, buf, &rem)) > 0) { if (!send_response(con)) { close_connection(con); break; } free_wg_dynamic_request(&con->req); } if (ret < 0) close_connection(con); } static void setup_sockets() { int val = 1, res; struct sockaddr_in6 addr = { .sin6_family = AF_INET6, .sin6_port = htons(WG_DYNAMIC_PORT), .sin6_addr = well_known, .sin6_scope_id = device->ifindex, }; sockfd = socket(AF_INET6, SOCK_STREAM, 0); if (sockfd < 0) fatal("Creating a socket failed"); res = fcntl(sockfd, F_GETFL, 0); if (res < 0 || fcntl(sockfd, F_SETFL, res | O_NONBLOCK) < 0) fatal("Setting socket to nonblocking failed"); if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof val)) fatal("Setting socket option failed"); if (bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1) fatal("Binding socket failed"); if (listen(sockfd, SOMAXCONN) == -1) fatal("Listening to socket failed"); /* netlink route socket */ nlsock = mnl_socket_open(NETLINK_ROUTE); if (!nlsock) fatal("mnl_socket_open(NETLINK_ROUTE)"); res = fcntl(mnl_socket_get_fd(nlsock), F_GETFL, 0); if (res < 0 || fcntl(mnl_socket_get_fd(nlsock), F_SETFL, res | O_NONBLOCK) < 0) fatal("Setting netlink socket to nonblocking failed"); if (mnl_socket_bind(nlsock, 0, MNL_SOCKET_AUTOPID) < 0) fatal("mnl_socket_bind()"); val = RTNLGRP_IPV4_ROUTE; if (mnl_socket_setsockopt(nlsock, NETLINK_ADD_MEMBERSHIP, &val, sizeof val) < 0) fatal("mnl_socket_setsockopt()"); val = RTNLGRP_IPV6_ROUTE; if (mnl_socket_setsockopt(nlsock, NETLINK_ADD_MEMBERSHIP, &val, sizeof val) < 0) fatal("mnl_socket_setsockopt()"); } static void cleanup() { leases_free(); kh_destroy(allowedht, allowedips_ht); wg_free_device(device); if (nlsock) mnl_socket_close(nlsock); if (sockfd >= 0) close(sockfd); if (epollfd >= 0) close(epollfd); for (int i = 0; i < MAX_CONNECTIONS; ++i) { if (connections[i].fd < 0) continue; close_connection(&connections[i]); } } static void setup() { struct wg_combined_ip ip; int ret; if (inet_pton(AF_INET6, WG_DYNAMIC_ADDR, &well_known) != 1) fatal("inet_pton()"); allowedips_ht = kh_init(allowedht); for (int i = 0; i < MAX_CONNECTIONS; ++i) connections[i].fd = -1; if (atexit(cleanup)) die("Failed to set exit function\n"); rebuild_allowedips_ht(); ipm_init(); ret = ipm_getlladdr(device->ifindex, &ip); if (ret == -1) fatal("ipm_getlladdr()"); if (ret == -2) die("Interface must not have multiple link-local addresses assigned\n"); ipm_free(); if (ret == -1 || ip.family != AF_INET6 || memcmp(&ip.ip6, well_known.s6_addr, 16)) /* TODO: assign IP instead? */ die("%s needs to have %s assigned\n", wg_interface, WG_DYNAMIC_ADDR); if (ip.cidr != 64) die("Link-local address must have a CIDR of 64\n"); if (!valid_peer_found(device)) die("%s has no peers with link-local allowedips\n", wg_interface); setup_sockets(); leases_init("leases_file", nlsock); } static int get_avail_request() { for (int nfds = 0;; ++nfds) { if (nfds >= MAX_CONNECTIONS) return -1; if (connections[nfds].fd < 0) return nfds; } } static void accept_incoming() { int n, fd; struct epoll_event ev; while ((n = get_avail_request()) >= 0) { fd = accept_connection(&connections[n].pubkey); if (fd < 0) { if (fd == -ENOENT) { debug("Failed to match IP to pubkey\n"); continue; } else if (fd == -EAGAIN || fd == -EWOULDBLOCK) { return; } debug("Failed to accept connection: %s\n", strerror(-fd)); continue; } ev.events = EPOLLIN | EPOLLET; ev.data.ptr = &connections[n]; if (epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev) == -1) fatal("epoll_ctl()"); connections[n].fd = fd; } } static void handle_event(void *ptr, uint32_t events) { struct wg_dynamic_connection *con; if (ptr == &sockfd) { accept_incoming(); return; } if (ptr == nlsock) { leases_update_pools(nlsock); return; } con = (struct wg_dynamic_connection *)ptr; if (events & EPOLLIN) { handle_client(con); } if (events & EPOLLOUT) { if (!send_message(con, con->outbuf, con->buflen)) close_connection(con); } } static void poll_loop() { struct epoll_event ev, events[MAX_CONNECTIONS]; epollfd = epoll_create1(0); if (epollfd == -1) fatal("epoll_create1()"); ev.events = EPOLLIN | EPOLLET; ev.data.ptr = &sockfd; if (epoll_ctl(epollfd, EPOLL_CTL_ADD, sockfd, &ev)) fatal("epoll_ctl()"); ev.events = EPOLLIN; ev.data.ptr = nlsock; if (epoll_ctl(epollfd, EPOLL_CTL_ADD, mnl_socket_get_fd(nlsock), &ev)) fatal("epoll_ctl()"); while (1) { time_t next = leases_refresh() * 1000; int nfds = epoll_wait(epollfd, events, MAX_CONNECTIONS, next); if (nfds == -1) { if (errno == EINTR) continue; fatal("epoll_wait()"); } for (int i = 0; i < nfds; ++i) handle_event(events[i].data.ptr, events[i].events); } } int main(int argc, char *argv[]) { progname = argv[0]; if (argc != 2) usage(); wg_interface = argv[1]; setup(); poll_loop(); return 0; }