| /* SPDX-License-Identifier: LGPL-2.1-or-later */ |
| /*** |
| Copyright © 2015-2017 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved. |
| ***/ |
| |
| #include <sys/ioctl.h> |
| #include <net/if.h> |
| #include <netinet/in.h> |
| #include <linux/if_arp.h> |
| #include <linux/ipv6_route.h> |
| |
| #include "sd-resolve.h" |
| |
| #include "alloc-util.h" |
| #include "dns-domain.h" |
| #include "event-util.h" |
| #include "fd-util.h" |
| #include "fileio.h" |
| #include "hexdecoct.h" |
| #include "memory-util.h" |
| #include "netlink-util.h" |
| #include "networkd-manager.h" |
| #include "networkd-route-util.h" |
| #include "networkd-route.h" |
| #include "networkd-util.h" |
| #include "parse-helpers.h" |
| #include "parse-util.h" |
| #include "random-util.h" |
| #include "resolve-private.h" |
| #include "string-util.h" |
| #include "strv.h" |
| #include "wireguard.h" |
| |
| static void wireguard_resolve_endpoints(NetDev *netdev); |
| static int peer_resolve_endpoint(WireguardPeer *peer); |
| |
| static WireguardPeer* wireguard_peer_free(WireguardPeer *peer) { |
| WireguardIPmask *mask; |
| |
| if (!peer) |
| return NULL; |
| |
| if (peer->wireguard) { |
| LIST_REMOVE(peers, peer->wireguard->peers, peer); |
| |
| if (peer->section) |
| hashmap_remove(peer->wireguard->peers_by_section, peer->section); |
| } |
| |
| config_section_free(peer->section); |
| |
| while ((mask = peer->ipmasks)) { |
| LIST_REMOVE(ipmasks, peer->ipmasks, mask); |
| free(mask); |
| } |
| |
| free(peer->endpoint_host); |
| free(peer->endpoint_port); |
| free(peer->preshared_key_file); |
| explicit_bzero_safe(peer->preshared_key, WG_KEY_LEN); |
| |
| sd_event_source_disable_unref(peer->resolve_retry_event_source); |
| sd_resolve_query_unref(peer->resolve_query); |
| |
| return mfree(peer); |
| } |
| |
| DEFINE_SECTION_CLEANUP_FUNCTIONS(WireguardPeer, wireguard_peer_free); |
| |
| static int wireguard_peer_new_static(Wireguard *w, const char *filename, unsigned section_line, WireguardPeer **ret) { |
| _cleanup_(config_section_freep) ConfigSection *n = NULL; |
| _cleanup_(wireguard_peer_freep) WireguardPeer *peer = NULL; |
| int r; |
| |
| assert(w); |
| assert(ret); |
| assert(filename); |
| assert(section_line > 0); |
| |
| r = config_section_new(filename, section_line, &n); |
| if (r < 0) |
| return r; |
| |
| peer = hashmap_get(w->peers_by_section, n); |
| if (peer) { |
| *ret = TAKE_PTR(peer); |
| return 0; |
| } |
| |
| peer = new(WireguardPeer, 1); |
| if (!peer) |
| return -ENOMEM; |
| |
| *peer = (WireguardPeer) { |
| .flags = WGPEER_F_REPLACE_ALLOWEDIPS, |
| .wireguard = w, |
| .section = TAKE_PTR(n), |
| }; |
| |
| LIST_PREPEND(peers, w->peers, peer); |
| |
| r = hashmap_ensure_put(&w->peers_by_section, &config_section_hash_ops, peer->section, peer); |
| if (r < 0) |
| return r; |
| |
| *ret = TAKE_PTR(peer); |
| return 0; |
| } |
| |
| static int wireguard_set_ipmask_one(NetDev *netdev, sd_netlink_message *message, const WireguardIPmask *mask, uint16_t index) { |
| int r; |
| |
| assert(message); |
| assert(mask); |
| assert(index > 0); |
| |
| /* This returns 1 on success, 0 on recoverable error, and negative errno on failure. */ |
| |
| r = sd_netlink_message_open_array(message, index); |
| if (r < 0) |
| return 0; |
| |
| r = sd_netlink_message_append_u16(message, WGALLOWEDIP_A_FAMILY, mask->family); |
| if (r < 0) |
| goto cancel; |
| |
| r = netlink_message_append_in_addr_union(message, WGALLOWEDIP_A_IPADDR, mask->family, &mask->ip); |
| if (r < 0) |
| goto cancel; |
| |
| r = sd_netlink_message_append_u8(message, WGALLOWEDIP_A_CIDR_MASK, mask->cidr); |
| if (r < 0) |
| goto cancel; |
| |
| r = sd_netlink_message_close_container(message); |
| if (r < 0) |
| return log_netdev_error_errno(netdev, r, "Could not add wireguard allowed ip: %m"); |
| |
| return 1; |
| |
| cancel: |
| r = sd_netlink_message_cancel_array(message); |
| if (r < 0) |
| return log_netdev_error_errno(netdev, r, "Could not cancel wireguard allowed ip message attribute: %m"); |
| |
| return 0; |
| } |
| |
| static int wireguard_set_peer_one(NetDev *netdev, sd_netlink_message *message, const WireguardPeer *peer, uint16_t index, WireguardIPmask **mask_start) { |
| WireguardIPmask *start, *last = NULL; |
| uint16_t j = 0; |
| int r; |
| |
| assert(message); |
| assert(peer); |
| assert(index > 0); |
| assert(mask_start); |
| |
| /* This returns 1 on success, 0 on recoverable error, and negative errno on failure. */ |
| |
| start = *mask_start ?: peer->ipmasks; |
| |
| r = sd_netlink_message_open_array(message, index); |
| if (r < 0) |
| return 0; |
| |
| r = sd_netlink_message_append_data(message, WGPEER_A_PUBLIC_KEY, &peer->public_key, sizeof(peer->public_key)); |
| if (r < 0) |
| goto cancel; |
| |
| if (!*mask_start) { |
| r = sd_netlink_message_append_data(message, WGPEER_A_PRESHARED_KEY, &peer->preshared_key, WG_KEY_LEN); |
| if (r < 0) |
| goto cancel; |
| |
| r = sd_netlink_message_append_u32(message, WGPEER_A_FLAGS, peer->flags); |
| if (r < 0) |
| goto cancel; |
| |
| r = sd_netlink_message_append_u16(message, WGPEER_A_PERSISTENT_KEEPALIVE_INTERVAL, peer->persistent_keepalive_interval); |
| if (r < 0) |
| goto cancel; |
| |
| if (IN_SET(peer->endpoint.sa.sa_family, AF_INET, AF_INET6)) { |
| r = netlink_message_append_sockaddr_union(message, WGPEER_A_ENDPOINT, &peer->endpoint); |
| if (r < 0) |
| goto cancel; |
| } |
| } |
| |
| r = sd_netlink_message_open_container(message, WGPEER_A_ALLOWEDIPS); |
| if (r < 0) |
| goto cancel; |
| |
| LIST_FOREACH(ipmasks, mask, start) { |
| r = wireguard_set_ipmask_one(netdev, message, mask, ++j); |
| if (r < 0) |
| return r; |
| if (r == 0) { |
| last = mask; |
| break; |
| } |
| } |
| |
| r = sd_netlink_message_close_container(message); |
| if (r < 0) |
| return log_netdev_error_errno(netdev, r, "Could not add wireguard allowed ip: %m"); |
| |
| r = sd_netlink_message_close_container(message); |
| if (r < 0) |
| return log_netdev_error_errno(netdev, r, "Could not add wireguard peer: %m"); |
| |
| *mask_start = last; /* Start next cycle from this mask. */ |
| return !last; |
| |
| cancel: |
| r = sd_netlink_message_cancel_array(message); |
| if (r < 0) |
| return log_netdev_error_errno(netdev, r, "Could not cancel wireguard peers: %m"); |
| |
| return 0; |
| } |
| |
| static int wireguard_set_interface(NetDev *netdev) { |
| _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *message = NULL; |
| WireguardIPmask *mask_start = NULL; |
| WireguardPeer *peer_start; |
| bool sent_once = false; |
| uint32_t serial; |
| Wireguard *w; |
| int r; |
| |
| assert(netdev); |
| w = WIREGUARD(netdev); |
| assert(w); |
| |
| for (peer_start = w->peers; peer_start || !sent_once; ) { |
| uint16_t i = 0; |
| |
| message = sd_netlink_message_unref(message); |
| |
| r = sd_genl_message_new(netdev->manager->genl, WG_GENL_NAME, WG_CMD_SET_DEVICE, &message); |
| if (r < 0) |
| return log_netdev_error_errno(netdev, r, "Failed to allocate generic netlink message: %m"); |
| |
| r = sd_netlink_message_append_string(message, WGDEVICE_A_IFNAME, netdev->ifname); |
| if (r < 0) |
| return log_netdev_error_errno(netdev, r, "Could not append wireguard interface name: %m"); |
| |
| if (peer_start == w->peers) { |
| r = sd_netlink_message_append_data(message, WGDEVICE_A_PRIVATE_KEY, &w->private_key, WG_KEY_LEN); |
| if (r < 0) |
| return log_netdev_error_errno(netdev, r, "Could not append wireguard private key: %m"); |
| |
| r = sd_netlink_message_append_u16(message, WGDEVICE_A_LISTEN_PORT, w->port); |
| if (r < 0) |
| return log_netdev_error_errno(netdev, r, "Could not append wireguard port: %m"); |
| |
| r = sd_netlink_message_append_u32(message, WGDEVICE_A_FWMARK, w->fwmark); |
| if (r < 0) |
| return log_netdev_error_errno(netdev, r, "Could not append wireguard fwmark: %m"); |
| |
| r = sd_netlink_message_append_u32(message, WGDEVICE_A_FLAGS, w->flags); |
| if (r < 0) |
| return log_netdev_error_errno(netdev, r, "Could not append wireguard flags: %m"); |
| } |
| |
| r = sd_netlink_message_open_container(message, WGDEVICE_A_PEERS); |
| if (r < 0) |
| return log_netdev_error_errno(netdev, r, "Could not append wireguard peer attributes: %m"); |
| |
| WireguardPeer *peer_last = NULL; |
| LIST_FOREACH(peers, peer, peer_start) { |
| r = wireguard_set_peer_one(netdev, message, peer, ++i, &mask_start); |
| if (r < 0) |
| return r; |
| if (r == 0) { |
| peer_last = peer; |
| break; |
| } |
| } |
| peer_start = peer_last; /* Start next cycle from this peer. */ |
| |
| r = sd_netlink_message_close_container(message); |
| if (r < 0) |
| return log_netdev_error_errno(netdev, r, "Could not close wireguard container: %m"); |
| |
| r = sd_netlink_send(netdev->manager->genl, message, &serial); |
| if (r < 0) |
| return log_netdev_error_errno(netdev, r, "Could not set wireguard device: %m"); |
| |
| sent_once = true; |
| } |
| |
| return 0; |
| } |
| |
| static int on_resolve_retry(sd_event_source *s, usec_t usec, void *userdata) { |
| WireguardPeer *peer = ASSERT_PTR(userdata); |
| NetDev *netdev; |
| |
| assert(peer->wireguard); |
| |
| netdev = NETDEV(peer->wireguard); |
| |
| if (!netdev_is_managed(netdev)) |
| return 0; |
| |
| peer->resolve_query = sd_resolve_query_unref(peer->resolve_query); |
| |
| (void) peer_resolve_endpoint(peer); |
| return 0; |
| } |
| |
| static usec_t peer_next_resolve_usec(WireguardPeer *peer) { |
| usec_t usec; |
| |
| /* Given the number of retries this function will return an exponential increasing amount of |
| * milliseconds to wait starting at 200ms and capped at 25 seconds. */ |
| |
| assert(peer); |
| |
| usec = (2 << MIN(peer->n_retries, 7U)) * 100 * USEC_PER_MSEC; |
| |
| return random_u64_range(usec / 10) + usec * 9 / 10; |
| } |
| |
| static int wireguard_peer_resolve_handler( |
| sd_resolve_query *q, |
| int ret, |
| const struct addrinfo *ai, |
| void *userdata) { |
| |
| WireguardPeer *peer = ASSERT_PTR(userdata); |
| NetDev *netdev; |
| int r; |
| |
| assert(peer->wireguard); |
| |
| netdev = NETDEV(peer->wireguard); |
| |
| if (!netdev_is_managed(netdev)) |
| return 0; |
| |
| if (ret != 0) { |
| log_netdev_warning(netdev, "Failed to resolve host '%s:%s', ignoring: %s", |
| peer->endpoint_host, peer->endpoint_port, gai_strerror(ret)); |
| peer->n_retries++; |
| |
| } else { |
| bool found = false; |
| for (; ai; ai = ai->ai_next) { |
| if (!IN_SET(ai->ai_family, AF_INET, AF_INET6)) |
| continue; |
| |
| if (ai->ai_addrlen != (ai->ai_family == AF_INET ? sizeof(struct sockaddr_in) : sizeof(struct sockaddr_in6))) |
| continue; |
| |
| memcpy(&peer->endpoint, ai->ai_addr, ai->ai_addrlen); |
| (void) wireguard_set_interface(netdev); |
| peer->n_retries = 0; |
| found = true; |
| break; |
| } |
| |
| if (!found) { |
| log_netdev_warning(netdev, "Neither IPv4 nor IPv6 address found for peer endpoint %s:%s, ignoring the endpoint.", |
| peer->endpoint_host, peer->endpoint_port); |
| peer->n_retries++; |
| } |
| } |
| |
| if (peer->n_retries > 0) { |
| r = event_reset_time_relative(netdev->manager->event, |
| &peer->resolve_retry_event_source, |
| CLOCK_BOOTTIME, |
| peer_next_resolve_usec(peer), 0, |
| on_resolve_retry, peer, 0, "wireguard-resolve-retry", true); |
| if (r < 0) |
| log_netdev_warning_errno(netdev, r, "Could not arm resolve retry handler for endpoint %s:%s, ignoring: %m", |
| peer->endpoint_host, peer->endpoint_port); |
| } |
| |
| wireguard_resolve_endpoints(netdev); |
| return 0; |
| } |
| |
| static int peer_resolve_endpoint(WireguardPeer *peer) { |
| static const struct addrinfo hints = { |
| .ai_family = AF_UNSPEC, |
| .ai_socktype = SOCK_DGRAM, |
| .ai_protocol = IPPROTO_UDP |
| }; |
| NetDev *netdev; |
| int r; |
| |
| assert(peer); |
| assert(peer->wireguard); |
| |
| netdev = NETDEV(peer->wireguard); |
| |
| if (!peer->endpoint_host || !peer->endpoint_port) |
| /* Not necessary to resolve the endpoint. */ |
| return 0; |
| |
| if (sd_event_source_get_enabled(peer->resolve_retry_event_source, NULL) > 0) |
| /* Timer event source is enabled. The endpoint will be resolved later. */ |
| return 0; |
| |
| if (peer->resolve_query) |
| /* Being resolved, or already resolved. */ |
| return 0; |
| |
| r = sd_resolve_getaddrinfo(netdev->manager->resolve, |
| &peer->resolve_query, |
| peer->endpoint_host, |
| peer->endpoint_port, |
| &hints, |
| wireguard_peer_resolve_handler, |
| peer); |
| if (r < 0) |
| return log_netdev_full_errno(netdev, r == -ENOBUFS ? LOG_DEBUG : LOG_WARNING, r, |
| "Failed to create endpoint resolver for %s:%s, ignoring: %m", |
| peer->endpoint_host, peer->endpoint_port); |
| |
| return 0; |
| } |
| |
| static void wireguard_resolve_endpoints(NetDev *netdev) { |
| Wireguard *w; |
| |
| assert(netdev); |
| w = WIREGUARD(netdev); |
| assert(w); |
| |
| LIST_FOREACH(peers, peer, w->peers) |
| if (peer_resolve_endpoint(peer) == -ENOBUFS) |
| /* Too many requests. Let's resolve remaining endpoints later. */ |
| break; |
| } |
| |
| static int netdev_wireguard_post_create(NetDev *netdev, Link *link) { |
| assert(netdev); |
| assert(WIREGUARD(netdev)); |
| |
| (void) wireguard_set_interface(netdev); |
| wireguard_resolve_endpoints(netdev); |
| return 0; |
| } |
| |
| int config_parse_wireguard_listen_port( |
| const char *unit, |
| const char *filename, |
| unsigned line, |
| const char *section, |
| unsigned section_line, |
| const char *lvalue, |
| int ltype, |
| const char *rvalue, |
| void *data, |
| void *userdata) { |
| |
| uint16_t *s = ASSERT_PTR(data); |
| int r; |
| |
| assert(rvalue); |
| |
| if (isempty(rvalue) || streq(rvalue, "auto")) { |
| *s = 0; |
| return 0; |
| } |
| |
| r = parse_ip_port(rvalue, s); |
| if (r < 0) { |
| log_syntax(unit, LOG_WARNING, filename, line, r, |
| "Invalid port specification, ignoring assignment: %s", rvalue); |
| return 0; |
| } |
| |
| return 0; |
| } |
| |
| static int wireguard_decode_key_and_warn( |
| const char *rvalue, |
| uint8_t ret[static WG_KEY_LEN], |
| const char *unit, |
| const char *filename, |
| unsigned line, |
| const char *lvalue) { |
| |
| _cleanup_(erase_and_freep) void *key = NULL; |
| size_t len; |
| int r; |
| |
| assert(rvalue); |
| assert(ret); |
| assert(filename); |
| assert(lvalue); |
| |
| if (isempty(rvalue)) { |
| memzero(ret, WG_KEY_LEN); |
| return 0; |
| } |
| |
| if (!streq(lvalue, "PublicKey")) |
| (void) warn_file_is_world_accessible(filename, NULL, unit, line); |
| |
| r = unbase64mem_full(rvalue, strlen(rvalue), true, &key, &len); |
| if (r == -ENOMEM) |
| return log_oom(); |
| if (r < 0) { |
| log_syntax(unit, LOG_WARNING, filename, line, r, |
| "Failed to decode wireguard key provided by %s=, ignoring assignment: %m", lvalue); |
| return 0; |
| } |
| if (len != WG_KEY_LEN) { |
| log_syntax(unit, LOG_WARNING, filename, line, 0, |
| "Wireguard key provided by %s= has invalid length (%zu bytes), ignoring assignment.", |
| lvalue, len); |
| return 0; |
| } |
| |
| memcpy(ret, key, WG_KEY_LEN); |
| return 0; |
| } |
| |
| int config_parse_wireguard_private_key( |
| const char *unit, |
| const char *filename, |
| unsigned line, |
| const char *section, |
| unsigned section_line, |
| const char *lvalue, |
| int ltype, |
| const char *rvalue, |
| void *data, |
| void *userdata) { |
| |
| Wireguard *w; |
| |
| assert(data); |
| w = WIREGUARD(data); |
| assert(w); |
| |
| return wireguard_decode_key_and_warn(rvalue, w->private_key, unit, filename, line, lvalue); |
| } |
| |
| int config_parse_wireguard_private_key_file( |
| const char *unit, |
| const char *filename, |
| unsigned line, |
| const char *section, |
| unsigned section_line, |
| const char *lvalue, |
| int ltype, |
| const char *rvalue, |
| void *data, |
| void *userdata) { |
| |
| _cleanup_free_ char *path = NULL; |
| Wireguard *w; |
| |
| assert(data); |
| w = WIREGUARD(data); |
| assert(w); |
| |
| if (isempty(rvalue)) { |
| w->private_key_file = mfree(w->private_key_file); |
| return 0; |
| } |
| |
| path = strdup(rvalue); |
| if (!path) |
| return log_oom(); |
| |
| if (path_simplify_and_warn(path, PATH_CHECK_ABSOLUTE, unit, filename, line, lvalue) < 0) |
| return 0; |
| |
| return free_and_replace(w->private_key_file, path); |
| } |
| |
| int config_parse_wireguard_peer_key( |
| const char *unit, |
| const char *filename, |
| unsigned line, |
| const char *section, |
| unsigned section_line, |
| const char *lvalue, |
| int ltype, |
| const char *rvalue, |
| void *data, |
| void *userdata) { |
| |
| _cleanup_(wireguard_peer_free_or_set_invalidp) WireguardPeer *peer = NULL; |
| Wireguard *w; |
| int r; |
| |
| assert(data); |
| w = WIREGUARD(data); |
| assert(w); |
| |
| r = wireguard_peer_new_static(w, filename, section_line, &peer); |
| if (r < 0) |
| return log_oom(); |
| |
| r = wireguard_decode_key_and_warn(rvalue, |
| streq(lvalue, "PublicKey") ? peer->public_key : peer->preshared_key, |
| unit, filename, line, lvalue); |
| if (r < 0) |
| return r; |
| |
| TAKE_PTR(peer); |
| return 0; |
| } |
| |
| int config_parse_wireguard_preshared_key_file( |
| const char *unit, |
| const char *filename, |
| unsigned line, |
| const char *section, |
| unsigned section_line, |
| const char *lvalue, |
| int ltype, |
| const char *rvalue, |
| void *data, |
| void *userdata) { |
| |
| _cleanup_(wireguard_peer_free_or_set_invalidp) WireguardPeer *peer = NULL; |
| _cleanup_free_ char *path = NULL; |
| Wireguard *w; |
| int r; |
| |
| assert(data); |
| w = WIREGUARD(data); |
| assert(w); |
| |
| r = wireguard_peer_new_static(w, filename, section_line, &peer); |
| if (r < 0) |
| return log_oom(); |
| |
| if (isempty(rvalue)) { |
| peer->preshared_key_file = mfree(peer->preshared_key_file); |
| TAKE_PTR(peer); |
| return 0; |
| } |
| |
| path = strdup(rvalue); |
| if (!path) |
| return log_oom(); |
| |
| if (path_simplify_and_warn(path, PATH_CHECK_ABSOLUTE, unit, filename, line, lvalue) < 0) |
| return 0; |
| |
| free_and_replace(peer->preshared_key_file, path); |
| TAKE_PTR(peer); |
| return 0; |
| } |
| |
| int config_parse_wireguard_allowed_ips( |
| const char *unit, |
| const char *filename, |
| unsigned line, |
| const char *section, |
| unsigned section_line, |
| const char *lvalue, |
| int ltype, |
| const char *rvalue, |
| void *data, |
| void *userdata) { |
| |
| _cleanup_(wireguard_peer_free_or_set_invalidp) WireguardPeer *peer = NULL; |
| union in_addr_union addr; |
| unsigned char prefixlen; |
| int r, family; |
| Wireguard *w; |
| WireguardIPmask *ipmask; |
| |
| assert(rvalue); |
| assert(data); |
| |
| w = WIREGUARD(data); |
| assert(w); |
| |
| r = wireguard_peer_new_static(w, filename, section_line, &peer); |
| if (r < 0) |
| return log_oom(); |
| |
| for (const char *p = rvalue;;) { |
| _cleanup_free_ char *word = NULL; |
| union in_addr_union masked; |
| |
| r = extract_first_word(&p, &word, "," WHITESPACE, 0); |
| if (r == 0) |
| break; |
| if (r == -ENOMEM) |
| return log_oom(); |
| if (r < 0) { |
| log_syntax(unit, LOG_WARNING, filename, line, r, |
| "Failed to split allowed ips \"%s\" option: %m", rvalue); |
| break; |
| } |
| |
| r = in_addr_prefix_from_string_auto(word, &family, &addr, &prefixlen); |
| if (r < 0) { |
| log_syntax(unit, LOG_WARNING, filename, line, r, |
| "Network address is invalid, ignoring assignment: %s", word); |
| continue; |
| } |
| |
| masked = addr; |
| assert_se(in_addr_mask(family, &masked, prefixlen) >= 0); |
| if (!in_addr_equal(family, &masked, &addr)) |
| log_syntax(unit, LOG_WARNING, filename, line, 0, |
| "Specified address '%s' is not properly masked, assuming '%s'.", |
| word, |
| IN_ADDR_PREFIX_TO_STRING(family, &masked, prefixlen)); |
| |
| ipmask = new(WireguardIPmask, 1); |
| if (!ipmask) |
| return log_oom(); |
| |
| *ipmask = (WireguardIPmask) { |
| .family = family, |
| .ip = masked, |
| .cidr = prefixlen, |
| }; |
| |
| LIST_PREPEND(ipmasks, peer->ipmasks, ipmask); |
| } |
| |
| TAKE_PTR(peer); |
| return 0; |
| } |
| |
| int config_parse_wireguard_endpoint( |
| const char *unit, |
| const char *filename, |
| unsigned line, |
| const char *section, |
| unsigned section_line, |
| const char *lvalue, |
| int ltype, |
| const char *rvalue, |
| void *data, |
| void *userdata) { |
| |
| _cleanup_(wireguard_peer_free_or_set_invalidp) WireguardPeer *peer = NULL; |
| _cleanup_free_ char *host = NULL; |
| union in_addr_union addr; |
| const char *p; |
| uint16_t port; |
| Wireguard *w; |
| int family, r; |
| |
| assert(filename); |
| assert(rvalue); |
| assert(userdata); |
| |
| w = WIREGUARD(userdata); |
| assert(w); |
| |
| r = wireguard_peer_new_static(w, filename, section_line, &peer); |
| if (r < 0) |
| return log_oom(); |
| |
| r = in_addr_port_ifindex_name_from_string_auto(rvalue, &family, &addr, &port, NULL, NULL); |
| if (r >= 0) { |
| if (family == AF_INET) |
| peer->endpoint.in = (struct sockaddr_in) { |
| .sin_family = AF_INET, |
| .sin_addr = addr.in, |
| .sin_port = htobe16(port), |
| }; |
| else if (family == AF_INET6) |
| peer->endpoint.in6 = (struct sockaddr_in6) { |
| .sin6_family = AF_INET6, |
| .sin6_addr = addr.in6, |
| .sin6_port = htobe16(port), |
| }; |
| else |
| assert_not_reached(); |
| |
| peer->endpoint_host = mfree(peer->endpoint_host); |
| peer->endpoint_port = mfree(peer->endpoint_port); |
| |
| TAKE_PTR(peer); |
| return 0; |
| } |
| |
| p = strrchr(rvalue, ':'); |
| if (!p) { |
| log_syntax(unit, LOG_WARNING, filename, line, 0, |
| "Unable to find port of endpoint, ignoring assignment: %s", |
| rvalue); |
| return 0; |
| } |
| |
| host = strndup(rvalue, p - rvalue); |
| if (!host) |
| return log_oom(); |
| |
| if (!dns_name_is_valid(host)) { |
| log_syntax(unit, LOG_WARNING, filename, line, 0, |
| "Invalid domain name of endpoint, ignoring assignment: %s", |
| rvalue); |
| return 0; |
| } |
| |
| p++; |
| r = parse_ip_port(p, &port); |
| if (r < 0) { |
| log_syntax(unit, LOG_WARNING, filename, line, r, |
| "Invalid port of endpoint, ignoring assignment: %s", |
| rvalue); |
| return 0; |
| } |
| |
| peer->endpoint = (union sockaddr_union) {}; |
| |
| free_and_replace(peer->endpoint_host, host); |
| |
| r = free_and_strdup(&peer->endpoint_port, p); |
| if (r < 0) |
| return log_oom(); |
| |
| TAKE_PTR(peer); /* The peer may already have been in the hash map, that is fine too. */ |
| return 0; |
| } |
| |
| int config_parse_wireguard_keepalive( |
| const char *unit, |
| const char *filename, |
| unsigned line, |
| const char *section, |
| unsigned section_line, |
| const char *lvalue, |
| int ltype, |
| const char *rvalue, |
| void *data, |
| void *userdata) { |
| |
| _cleanup_(wireguard_peer_free_or_set_invalidp) WireguardPeer *peer = NULL; |
| uint16_t keepalive = 0; |
| Wireguard *w; |
| int r; |
| |
| assert(rvalue); |
| assert(data); |
| |
| w = WIREGUARD(data); |
| assert(w); |
| |
| r = wireguard_peer_new_static(w, filename, section_line, &peer); |
| if (r < 0) |
| return log_oom(); |
| |
| if (streq(rvalue, "off")) |
| keepalive = 0; |
| else { |
| r = safe_atou16(rvalue, &keepalive); |
| if (r < 0) { |
| log_syntax(unit, LOG_WARNING, filename, line, r, |
| "Failed to parse \"%s\" as keepalive interval (range 0–65535), ignoring assignment: %m", |
| rvalue); |
| return 0; |
| } |
| } |
| |
| peer->persistent_keepalive_interval = keepalive; |
| |
| TAKE_PTR(peer); |
| return 0; |
| } |
| |
| int config_parse_wireguard_route_table( |
| const char *unit, |
| const char *filename, |
| unsigned line, |
| const char *section, |
| unsigned section_line, |
| const char *lvalue, |
| int ltype, |
| const char *rvalue, |
| void *data, |
| void *userdata) { |
| |
| NetDev *netdev = ASSERT_PTR(userdata); |
| uint32_t *table = ASSERT_PTR(data); |
| int r; |
| |
| assert(filename); |
| assert(lvalue); |
| assert(rvalue); |
| |
| if (isempty(rvalue) || parse_boolean(rvalue) == 0) { |
| *table = 0; /* Disabled. */ |
| return 0; |
| } |
| |
| r = manager_get_route_table_from_string(netdev->manager, rvalue, table); |
| if (r < 0) { |
| log_syntax(unit, LOG_WARNING, filename, line, r, |
| "Failed to parse %s=, ignoring assignment: %s", |
| lvalue, rvalue); |
| return 0; |
| } |
| |
| return 0; |
| } |
| |
| int config_parse_wireguard_peer_route_table( |
| const char *unit, |
| const char *filename, |
| unsigned line, |
| const char *section, |
| unsigned section_line, |
| const char *lvalue, |
| int ltype, |
| const char *rvalue, |
| void *data, |
| void *userdata) { |
| |
| _cleanup_(wireguard_peer_free_or_set_invalidp) WireguardPeer *peer = NULL; |
| NetDev *netdev = ASSERT_PTR(userdata); |
| Wireguard *w; |
| int r; |
| |
| assert(filename); |
| assert(lvalue); |
| assert(rvalue); |
| assert(netdev->manager); |
| |
| w = WIREGUARD(netdev); |
| assert(w); |
| |
| r = wireguard_peer_new_static(w, filename, section_line, &peer); |
| if (r < 0) |
| return log_oom(); |
| |
| if (isempty(rvalue)) { |
| peer->route_table_set = false; /* Use the table specified in [WireGuard] section. */ |
| TAKE_PTR(peer); |
| return 0; |
| } |
| |
| if (parse_boolean(rvalue) == 0) { |
| peer->route_table = 0; /* Disabled. */ |
| peer->route_table_set = true; |
| TAKE_PTR(peer); |
| return 0; |
| } |
| |
| r = manager_get_route_table_from_string(netdev->manager, rvalue, &peer->route_table); |
| if (r < 0) { |
| log_syntax(unit, LOG_WARNING, filename, line, r, |
| "Failed to parse %s=, ignoring assignment: %s", |
| lvalue, rvalue); |
| return 0; |
| } |
| |
| peer->route_table_set = true; |
| TAKE_PTR(peer); |
| return 0; |
| } |
| |
| int config_parse_wireguard_route_priority( |
| const char *unit, |
| const char *filename, |
| unsigned line, |
| const char *section, |
| unsigned section_line, |
| const char *lvalue, |
| int ltype, |
| const char *rvalue, |
| void *data, |
| void *userdata) { |
| |
| uint32_t *priority = ASSERT_PTR(data); |
| int r; |
| |
| assert(filename); |
| assert(lvalue); |
| assert(rvalue); |
| |
| if (isempty(rvalue)) { |
| *priority = 0; |
| return 0; |
| } |
| |
| r = safe_atou32(rvalue, priority); |
| if (r < 0) { |
| log_syntax(unit, LOG_WARNING, filename, line, r, |
| "Could not parse route priority \"%s\", ignoring assignment: %m", rvalue); |
| return 0; |
| } |
| |
| return 0; |
| } |
| |
| int config_parse_wireguard_peer_route_priority( |
| const char *unit, |
| const char *filename, |
| unsigned line, |
| const char *section, |
| unsigned section_line, |
| const char *lvalue, |
| int ltype, |
| const char *rvalue, |
| void *data, |
| void *userdata) { |
| |
| _cleanup_(wireguard_peer_free_or_set_invalidp) WireguardPeer *peer = NULL; |
| Wireguard *w; |
| int r; |
| |
| assert(filename); |
| assert(lvalue); |
| assert(rvalue); |
| assert(userdata); |
| |
| w = WIREGUARD(userdata); |
| assert(w); |
| |
| r = wireguard_peer_new_static(w, filename, section_line, &peer); |
| if (r < 0) |
| return log_oom(); |
| |
| if (isempty(rvalue)) { |
| peer->route_priority_set = false; /* Use the priority specified in [WireGuard] section. */ |
| TAKE_PTR(peer); |
| return 0; |
| } |
| |
| r = safe_atou32(rvalue, &peer->route_priority); |
| if (r < 0) { |
| log_syntax(unit, LOG_WARNING, filename, line, r, |
| "Could not parse route priority \"%s\", ignoring assignment: %m", rvalue); |
| return 0; |
| } |
| |
| peer->route_priority_set = true; |
| TAKE_PTR(peer); |
| return 0; |
| } |
| |
| static void wireguard_init(NetDev *netdev) { |
| Wireguard *w; |
| |
| assert(netdev); |
| w = WIREGUARD(netdev); |
| assert(w); |
| |
| w->flags = WGDEVICE_F_REPLACE_PEERS; |
| } |
| |
| static void wireguard_done(NetDev *netdev) { |
| Wireguard *w; |
| |
| assert(netdev); |
| w = WIREGUARD(netdev); |
| assert(w); |
| |
| explicit_bzero_safe(w->private_key, WG_KEY_LEN); |
| free(w->private_key_file); |
| |
| hashmap_free_with_destructor(w->peers_by_section, wireguard_peer_free); |
| |
| set_free(w->routes); |
| } |
| |
| static int wireguard_read_key_file(const char *filename, uint8_t dest[static WG_KEY_LEN]) { |
| _cleanup_(erase_and_freep) char *key = NULL; |
| size_t key_len; |
| int r; |
| |
| if (!filename) |
| return 0; |
| |
| assert(dest); |
| |
| r = read_full_file_full( |
| AT_FDCWD, filename, UINT64_MAX, SIZE_MAX, |
| READ_FULL_FILE_SECURE | READ_FULL_FILE_UNBASE64 | READ_FULL_FILE_WARN_WORLD_READABLE | READ_FULL_FILE_CONNECT_SOCKET, |
| NULL, &key, &key_len); |
| if (r < 0) |
| return r; |
| |
| if (key_len != WG_KEY_LEN) |
| return -EINVAL; |
| |
| memcpy(dest, key, WG_KEY_LEN); |
| return 0; |
| } |
| |
| static int wireguard_peer_verify(WireguardPeer *peer) { |
| NetDev *netdev = NETDEV(peer->wireguard); |
| int r; |
| |
| if (section_is_invalid(peer->section)) |
| return -EINVAL; |
| |
| if (eqzero(peer->public_key)) |
| return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL), |
| "%s: WireGuardPeer section without PublicKey= configured. " |
| "Ignoring [WireGuardPeer] section from line %u.", |
| peer->section->filename, peer->section->line); |
| |
| r = wireguard_read_key_file(peer->preshared_key_file, peer->preshared_key); |
| if (r < 0) |
| return log_netdev_error_errno(netdev, r, |
| "%s: Failed to read preshared key from '%s'. " |
| "Ignoring [WireGuardPeer] section from line %u.", |
| peer->section->filename, peer->preshared_key_file, |
| peer->section->line); |
| |
| return 0; |
| } |
| |
| static int wireguard_verify(NetDev *netdev, const char *filename) { |
| Wireguard *w; |
| int r; |
| |
| assert(netdev); |
| w = WIREGUARD(netdev); |
| assert(w); |
| |
| r = wireguard_read_key_file(w->private_key_file, w->private_key); |
| if (r < 0) |
| return log_netdev_error_errno(netdev, r, |
| "Failed to read private key from %s. Ignoring network device.", |
| w->private_key_file); |
| |
| if (eqzero(w->private_key)) |
| return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL), |
| "%s: Missing PrivateKey= or PrivateKeyFile=, " |
| "Ignoring network device.", filename); |
| |
| LIST_FOREACH(peers, peer, w->peers) { |
| if (wireguard_peer_verify(peer) < 0) { |
| wireguard_peer_free(peer); |
| continue; |
| } |
| |
| if ((peer->route_table_set ? peer->route_table : w->route_table) == 0) |
| continue; |
| |
| LIST_FOREACH(ipmasks, ipmask, peer->ipmasks) { |
| _cleanup_(route_freep) Route *route = NULL; |
| |
| r = route_new(&route); |
| if (r < 0) |
| return log_oom(); |
| |
| route->family = ipmask->family; |
| route->dst = ipmask->ip; |
| route->dst_prefixlen = ipmask->cidr; |
| route->scope = RT_SCOPE_UNIVERSE; |
| route->protocol = RTPROT_STATIC; |
| route->table = peer->route_table_set ? peer->route_table : w->route_table; |
| route->priority = peer->route_priority_set ? peer->route_priority : w->route_priority; |
| if (route->priority == 0 && route->family == AF_INET6) |
| route->priority = IP6_RT_PRIO_USER; |
| route->source = NETWORK_CONFIG_SOURCE_STATIC; |
| |
| r = set_ensure_consume(&w->routes, &route_hash_ops, TAKE_PTR(route)); |
| if (r < 0) |
| return log_oom(); |
| } |
| } |
| |
| return 0; |
| } |
| |
| const NetDevVTable wireguard_vtable = { |
| .object_size = sizeof(Wireguard), |
| .sections = NETDEV_COMMON_SECTIONS "WireGuard\0WireGuardPeer\0", |
| .post_create = netdev_wireguard_post_create, |
| .init = wireguard_init, |
| .done = wireguard_done, |
| .create_type = NETDEV_CREATE_INDEPENDENT, |
| .config_verify = wireguard_verify, |
| .iftype = ARPHRD_NONE, |
| }; |