From 54e6f97bc9931679aa9b895546621b15e0f464a4 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 26 Mar 2021 18:06:26 +0100 Subject: [PATCH 1/4] local-addresses: add helper for determining local "outbound" IP addresses This adds a small helper, similar in style to local_addresses() and local_gateways() that determines the local "outbound" addresses. What's an "outbound" address supposed to be? The local IP addresses that are the most likely used for outbound communication. It's determined by using connect() towards the default gws on an UDP socket, and then reading the address of the socket this caused it to be bound to. This is not the "public" or "external" IP address of the local system, and is not supposed to be. It's just the local IP addresses that are likely the ones going to be used by the local IP stack for communication with other hosts. --- src/shared/local-addresses.c | 185 +++++++++++++++++++++++++++++++- src/shared/local-addresses.h | 2 + src/test/test-local-addresses.c | 7 ++ 3 files changed, 192 insertions(+), 2 deletions(-) diff --git a/src/shared/local-addresses.c b/src/shared/local-addresses.c index 1de890f142..c97687cd05 100644 --- a/src/shared/local-addresses.c +++ b/src/shared/local-addresses.c @@ -1,8 +1,11 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include + #include "sd-netlink.h" #include "alloc-util.h" +#include "fd-util.h" #include "local-addresses.h" #include "macro.h" #include "netlink-util.h" @@ -33,7 +36,34 @@ static int address_compare(const struct local_address *a, const struct local_add return memcmp(&a->address, &b->address, FAMILY_ADDRESS_SIZE(a->family)); } -int local_addresses(sd_netlink *context, int ifindex, int af, struct local_address **ret) { +static void suppress_duplicates(struct local_address *list, size_t *n_list) { + size_t old_size, new_size; + + /* Removes duplicate entries, assumes the list of addresses is already sorted. Updates in-place. */ + + if (*n_list < 2) /* list with less than two entries can't have duplicates */ + return; + + old_size = *n_list; + new_size = 1; + + for (size_t i = 1; i < old_size; i++) { + + if (address_compare(list + i, list + new_size - 1) == 0) + continue; + + list[new_size++] = list[i]; + } + + *n_list = new_size; +} + +int local_addresses( + sd_netlink *context, + int ifindex, + int af, + struct local_address **ret) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL, *reply = NULL; _cleanup_(sd_netlink_unrefp) sd_netlink *rtnl = NULL; _cleanup_free_ struct local_address *list = NULL; @@ -135,6 +165,7 @@ int local_addresses(sd_netlink *context, int ifindex, int af, struct local_addre if (ret) { typesafe_qsort(list, n_list, address_compare); + suppress_duplicates(list, &n_list); *ret = TAKE_PTR(list); } @@ -171,7 +202,12 @@ static int add_local_gateway( return 0; } -int local_gateways(sd_netlink *context, int ifindex, int af, struct local_address **ret) { +int local_gateways( + sd_netlink *context, + int ifindex, + int af, + struct local_address **ret) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL, *reply = NULL; _cleanup_(sd_netlink_unrefp) sd_netlink *rtnl = NULL; _cleanup_free_ struct local_address *list = NULL; @@ -308,6 +344,151 @@ int local_gateways(sd_netlink *context, int ifindex, int af, struct local_addres if (ret) { typesafe_qsort(list, n_list, address_compare); + suppress_duplicates(list, &n_list); + *ret = TAKE_PTR(list); + } + + return (int) n_list; +} + +int local_outbounds( + sd_netlink *context, + int ifindex, + int af, + struct local_address **ret) { + + _cleanup_free_ struct local_address *list = NULL, *gateways = NULL; + size_t n_list = 0, n_allocated = 0; + int r, n_gateways; + + /* Determines our default outbound addresses, i.e. the "primary" local addresses we use to talk to IP + * addresses behind the default routes. This is still an address of the local host (i.e. this doesn't + * resolve NAT or so), but it's the set of addresses the local IP stack most likely uses to talk to + * other hosts. + * + * This works by connect()ing a SOCK_DGRAM socket to the local gateways, and then reading the IP + * address off the socket that was chosen for the routing decision. */ + + n_gateways = local_gateways(context, ifindex, af, &gateways); + if (n_gateways < 0) + return n_gateways; + if (n_gateways == 0) { + /* No gateways? Then we have no outbound addresses either. */ + if (ret) + *ret = NULL; + + return 0; + } + + for (int i = 0; i < n_gateways; i++) { + _cleanup_close_ int fd = -1; + union sockaddr_union sa; + socklen_t salen; + + fd = socket(gateways[i].family, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0); + if (fd < 0) + return -errno; + + switch (gateways[i].family) { + + case AF_INET: + sa.in = (struct sockaddr_in) { + .sin_family = AF_INET, + .sin_addr = gateways[i].address.in, + .sin_port = htobe16(53), /* doesn't really matter which port we pick — we just care about the routing decision */ + }; + + break; + + case AF_INET6: + sa.in6 = (struct sockaddr_in6) { + .sin6_family = AF_INET6, + .sin6_addr = gateways[i].address.in6, + .sin6_port = htobe16(53), + .sin6_scope_id = gateways[i].ifindex, + }; + + break; + + default: + assert_not_reached("Unexpected protocol"); + } + + /* So ideally we'd just use IP_UNICAST_IF here to pass the ifindex info to the kernel before + * connect()ing, sot that it influences the routing decision. However, on current kernels + * IP_UNICAST_IF doesn't actually influence the routing decision for UDP — which I think + * should probably just be considered a bug. Once that bug is fixed this is the best API to + * use, since it is the most lightweight. */ + r = socket_set_unicast_if(fd, gateways[i].family, gateways[i].ifindex); + if (r < 0) + log_debug_errno(r, "Failed to set unicast interface index %i, ignoring: %m", gateways[i].ifindex); + + /* We'll also use SO_BINDTOINDEX. This requires CAP_NET_RAW on old kernels, hence there's a + * good chance this fails. Since 5.7 this restriction was dropped and the first + * SO_BINDTOINDEX on a socket may be done without privileges. This one has the benefit of + * really influencing the routing decision, i.e. this one definitely works for us — as long + * as we have the privileges for it.*/ + r = socket_bind_to_ifindex(fd, gateways[i].ifindex); + if (r < 0) + log_debug_errno(r, "Failed to bind socket to interface %i, ignoring: %m", gateways[i].ifindex); + + /* Let's now connect() to the UDP socket, forcing the kernel to make a routing decision and + * auto-bind the socket. We ignore failures on this, since that failure might happen for a + * multitude of reasons (policy/firewall issues, who knows?) and some of them might be + * *after* the routing decision and the auto-binding already took place. If so we can still + * make use of the binding and return it. Hence, let's not unnecessarily fail early here: we + * can still easily detect if the auto-binding worked or not, by comparing the bound IP + * address with zero — which we do below. */ + if (connect(fd, &sa.sa, SOCKADDR_LEN(sa)) < 0) + log_debug_errno(errno, "Failed to connect SOCK_DGRAM socket to gateway, ignoring: %m"); + + /* Let's now read the socket address of the socket. A routing decision should have been + * made. Let's verify that and use the data. */ + salen = SOCKADDR_LEN(sa); + if (getsockname(fd, &sa.sa, &salen) < 0) + return -errno; + assert(sa.sa.sa_family == gateways[i].family); + assert(salen == SOCKADDR_LEN(sa)); + + switch (gateways[i].family) { + + case AF_INET: + if (in4_addr_is_null(&sa.in.sin_addr)) /* Auto-binding didn't work. :-( */ + continue; + + if (!GREEDY_REALLOC(list, n_allocated, n_list+1)) + return -ENOMEM; + + list[n_list++] = (struct local_address) { + .family = gateways[i].family, + .ifindex = gateways[i].ifindex, + .address.in = sa.in.sin_addr, + }; + + break; + + case AF_INET6: + if (in6_addr_is_null(&sa.in6.sin6_addr)) + continue; + + if (!GREEDY_REALLOC(list, n_allocated, n_list+1)) + return -ENOMEM; + + list[n_list++] = (struct local_address) { + .family = gateways[i].family, + .ifindex = gateways[i].ifindex, + .address.in6 = sa.in6.sin6_addr, + }; + break; + + default: + assert_not_reached("Unexpected protocol"); + } + } + + if (ret) { + typesafe_qsort(list, n_list, address_compare); + suppress_duplicates(list, &n_list); *ret = TAKE_PTR(list); } diff --git a/src/shared/local-addresses.h b/src/shared/local-addresses.h index c633995dc9..38a17d233e 100644 --- a/src/shared/local-addresses.h +++ b/src/shared/local-addresses.h @@ -15,3 +15,5 @@ struct local_address { int local_addresses(sd_netlink *rtnl, int ifindex, int af, struct local_address **ret); int local_gateways(sd_netlink *rtnl, int ifindex, int af, struct local_address **ret); + +int local_outbounds(sd_netlink *rtnl, int ifindex, int af, struct local_address **ret); diff --git a/src/test/test-local-addresses.c b/src/test/test-local-addresses.c index 7eeddd28f4..7b75132e18 100644 --- a/src/test/test-local-addresses.c +++ b/src/test/test-local-addresses.c @@ -40,5 +40,12 @@ int main(int argc, char *argv[]) { print_local_addresses(a, (unsigned) n); free(a); + n = local_outbounds(NULL, 0, AF_UNSPEC, &a); + assert_se(n >= 0); + + printf("Local Outbounds:\n"); + print_local_addresses(a, (unsigned) n); + free(a); + return 0; } From a1fdbcbe3d0ab58ac274f5a6a0669752d5b4238b Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 26 Mar 2021 18:11:41 +0100 Subject: [PATCH 2/4] nss-myhostname: expose the "outbound" IP addresses under the synthetic "_outbound" hostname I found myself often looking for a quick way to determine "the local IP address", and then being lost in the "ip addr" output to find for the right one to use. This is supposed to help a bit with that. Let's introduce a new special hostname "_outbound" with semantics similar to "_gateway" that resolves to addresses that are the closest I could come up with that maps to "the" local IP address. --- src/basic/hostname-util.h | 5 +++++ src/nss-myhostname/nss-myhostname.c | 19 +++++++++++++++++-- src/test/test-nss-hosts.c | 3 +-- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/basic/hostname-util.h b/src/basic/hostname-util.h index 576ca083e0..c3fc6752cb 100644 --- a/src/basic/hostname-util.h +++ b/src/basic/hostname-util.h @@ -28,3 +28,8 @@ static inline bool is_gateway_hostname(const char *hostname) { /* This tries to identify the valid syntaxes for the our synthetic "gateway" host. */ return STRCASE_IN_SET(hostname, "_gateway", "_gateway."); } + +static inline bool is_outbound_hostname(const char *hostname) { + /* This tries to identify the valid syntaxes for the our synthetic "outbound" host. */ + return STRCASE_IN_SET(hostname, "_outbound", "_outbound."); +} diff --git a/src/nss-myhostname/nss-myhostname.c b/src/nss-myhostname/nss-myhostname.c index 6e37966da0..3536c5fc83 100644 --- a/src/nss-myhostname/nss-myhostname.c +++ b/src/nss-myhostname/nss-myhostname.c @@ -54,8 +54,7 @@ enum nss_status _nss_myhostname_gethostbyname4_r( assert(h_errnop); if (is_localhost(name)) { - /* We respond to 'localhost', so that /etc/hosts - * is optional */ + /* We respond to 'localhost', so that /etc/hosts is optional */ canonical = "localhost"; local_address_ipv4 = htobe32(INADDR_LOOPBACK); @@ -68,6 +67,14 @@ enum nss_status _nss_myhostname_gethostbyname4_r( canonical = "_gateway"; + } else if (is_outbound_hostname(name)) { + + n_addresses = local_outbounds(NULL, 0, AF_UNSPEC, &addresses); + if (n_addresses <= 0) + goto not_found; + + canonical = "_outbound"; + } else { hn = gethostname_malloc(); if (!hn) { @@ -343,6 +350,14 @@ enum nss_status _nss_myhostname_gethostbyname3_r( canonical = "_gateway"; + } else if (is_outbound_hostname(name)) { + + n_addresses = local_outbounds(NULL, 0, af, &addresses); + if (n_addresses <= 0) + goto not_found; + + canonical = "_outbound"; + } else { hn = gethostname_malloc(); if (!hn) { diff --git a/src/test/test-nss-hosts.c b/src/test/test-nss-hosts.c index e9bc6ecce6..42ed53adc3 100644 --- a/src/test/test-nss-hosts.c +++ b/src/test/test-nss-hosts.c @@ -455,8 +455,7 @@ static int parse_argv(int argc, char **argv, } else { _cleanup_free_ char *hostname; assert_se(hostname = gethostname_malloc()); - - assert_se(names = strv_new("localhost", "_gateway", "foo_no_such_host", hostname)); + assert_se(names = strv_new("localhost", "_gateway", "_outbound", "foo_no_such_host", hostname)); n = make_addresses(&addrs); assert_se(n >= 0); From ee18f107d3d64b39e4e361e54924ca165f25d4e8 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 26 Mar 2021 18:31:33 +0100 Subject: [PATCH 3/4] resolved: synthesize _outbound magic hostname here too --- src/resolve/resolved-dns-scope.c | 5 +++-- src/resolve/resolved-dns-synthesize.c | 30 ++++++++++++++++++++++----- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/resolve/resolved-dns-scope.c b/src/resolve/resolved-dns-scope.c index e155df0efa..178482727c 100644 --- a/src/resolve/resolved-dns-scope.c +++ b/src/resolve/resolved-dns-scope.c @@ -630,8 +630,8 @@ DnsScopeMatch dns_scope_good_domain( if (dns_name_endswith(domain, "invalid") > 0) return DNS_SCOPE_NO; - /* Never go to network for the _gateway domain, it's something special, synthesized locally. */ - if (is_gateway_hostname(domain)) + /* Never go to network for the _gateway or _outbound domain — they're something special, synthesized locally. */ + if (is_gateway_hostname(domain) || is_outbound_hostname(domain)) return DNS_SCOPE_NO; switch (s->protocol) { @@ -739,6 +739,7 @@ DnsScopeMatch dns_scope_good_domain( if ((dns_name_is_single_label(domain) && /* only resolve single label names via LLMNR */ !is_gateway_hostname(domain) && /* don't resolve "_gateway" with LLMNR, let local synthesizing logic handle that */ + !is_outbound_hostname(domain) && /* similar for "_outbound" */ dns_name_equal(domain, "local") == 0 && /* don't resolve "local" with LLMNR, it's the top-level domain of mDNS after all, see above */ manager_is_own_hostname(s->manager, domain) <= 0)) /* never resolve the local hostname via LLMNR */ return DNS_SCOPE_YES_BASE + 1; /* Return +1, as we consider ourselves authoritative diff --git a/src/resolve/resolved-dns-synthesize.c b/src/resolve/resolved-dns-synthesize.c index d4a4be71b0..ef1423f441 100644 --- a/src/resolve/resolved-dns-synthesize.c +++ b/src/resolve/resolved-dns-synthesize.c @@ -311,27 +311,33 @@ static int synthesize_system_hostname_ptr(Manager *m, int af, const union in_add return added; } -static int synthesize_gateway_rr(Manager *m, const DnsResourceKey *key, int ifindex, DnsAnswer **answer) { +static int synthesize_gateway_rr( + Manager *m, + const DnsResourceKey *key, + int ifindex, + int (*lookup)(sd_netlink *context, int ifindex, int af, struct local_address **ret), /* either local_gateways() or local_outbound() */ + DnsAnswer **answer) { _cleanup_free_ struct local_address *addresses = NULL; int n = 0, af, r; assert(m); assert(key); + assert(lookup); assert(answer); af = dns_type_to_af(key->type); if (af >= 0) { - n = local_gateways(m->rtnl, ifindex, af, &addresses); + n = lookup(m->rtnl, ifindex, af, &addresses); if (n < 0) /* < 0 means: error */ return n; if (n == 0) { /* == 0 means we have no gateway */ /* See if there's a gateway on the other protocol */ if (af == AF_INET) - n = local_gateways(m->rtnl, ifindex, AF_INET6, NULL); + n = lookup(m->rtnl, ifindex, AF_INET6, NULL); else { assert(af == AF_INET6); - n = local_gateways(m->rtnl, ifindex, AF_INET, NULL); + n = lookup(m->rtnl, ifindex, AF_INET, NULL); } if (n <= 0) /* error (if < 0) or really no gateway at all (if == 0) */ return n; @@ -402,7 +408,7 @@ int dns_synthesize_answer( } else if (is_gateway_hostname(name)) { - r = synthesize_gateway_rr(m, key, ifindex, &answer); + r = synthesize_gateway_rr(m, key, ifindex, local_gateways, &answer); if (r < 0) return log_error_errno(r, "Failed to synthesize gateway RRs: %m"); if (r == 0) { /* if we have no gateway return NXDOMAIN */ @@ -410,6 +416,16 @@ int dns_synthesize_answer( continue; } + } else if (is_outbound_hostname(name)) { + + r = synthesize_gateway_rr(m, key, ifindex, local_outbounds, &answer); + if (r < 0) + return log_error_errno(r, "Failed to synthesize outbound RRs: %m"); + if (r == 0) { /* if we have no gateway return NXDOMAIN */ + nxdomain = true; + continue; + } + } else if ((dns_name_endswith(name, "127.in-addr.arpa") > 0 && dns_name_equal(name, "2.0.0.127.in-addr.arpa") == 0) || dns_name_equal(name, "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa") > 0) { @@ -431,6 +447,10 @@ int dns_synthesize_answer( if (v == 0 && w == 0) /* This IP address is neither a local one nor a gateway */ continue; + /* Note that we never synthesize reverse PTR for _outbound, since those are local + * addresses and thus mapped to the local hostname anyway, hence they already have a + * mapping. */ + } else continue; From 2f166bb79b026ca3dfa1117b1a01aecbabcaccf3 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 26 Mar 2021 18:40:52 +0100 Subject: [PATCH 4/4] man: document _outbound --- man/nss-myhostname.xml | 7 +++++++ man/resolvectl.xml | 10 +++++----- man/systemd-resolved.service.xml | 7 +++++++ 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/man/nss-myhostname.xml b/man/nss-myhostname.xml index 8d5b549c3b..8a603fb9ce 100644 --- a/man/nss-myhostname.xml +++ b/man/nss-myhostname.xml @@ -51,6 +51,13 @@ ordered by their metric. This assigns a stable hostname to the current gateway, useful for referencing it independently of the current network configuration state. + + The hostname _outbound is resolved to the local IPv4 and IPv6 + addresses that are most likely used for communication with other hosts. This is determined by + requesting a routing decision to the configured default gateways from the kernel and then using the + local IP addresses selected by this decision. This hostname is only available if there is at least one + local default gateway configured. This assigns a stable hostname to the local outbound IP addresses, + useful for referencing them independently of the current network configuration state. Various software relies on an always-resolvable local diff --git a/man/resolvectl.xml b/man/resolvectl.xml index 6dd4674456..bd1a636d60 100644 --- a/man/resolvectl.xml +++ b/man/resolvectl.xml @@ -299,11 +299,11 @@ Takes a boolean parameter; used in conjunction with query. If true (the default), select domains are resolved on the local system, among them - localhost and _gateway or entries from - /etc/hosts. If false these domains are not resolved locally, and either fail (in - case of localhost or _gateway and suchlike) or go to the - network via regular DNS/mDNS/LLMNR lookups (in case of /etc/hosts - entries). + localhost, _gateway and _outbound, or + entries from /etc/hosts. If false these domains are not resolved locally, and + either fail (in case of localhost, _gateway or + _outbound and suchlike) or go to the network via regular DNS/mDNS/LLMNR lookups + (in case of /etc/hosts entries). diff --git a/man/systemd-resolved.service.xml b/man/systemd-resolved.service.xml index ed6214cd62..9c7c482549 100644 --- a/man/systemd-resolved.service.xml +++ b/man/systemd-resolved.service.xml @@ -104,6 +104,13 @@ gateway addresses, ordered by their metric. This assigns a stable hostname to the current gateway, useful for referencing it independently of the current network configuration state. + The hostname _outbound is resolved to the local IPv4 and IPv6 + addresses that are most likely used for communication with other hosts. This is determined by + requesting a routing decision to the configured default gateways from the kernel and then using the + local IP addresses selected by this decision. This hostname is only available if there is at least one + local default gateway configured. This assigns a stable hostname to the local outbound IP addresses, + useful for referencing them independently of the current network configuration state. + The mappings defined in /etc/hosts are resolved to their configured addresses and back, but they will not affect lookups for non-address types (like MX). Support for /etc/hosts may be disabled with ReadEtcHosts=no,