1
0
mirror of https://github.com/systemd/systemd.git synced 2025-03-31 14:50:15 +03:00

Merge pull request #19134 from poettering/outbound-special-hostname

introduce a new synthetic hostname "_outbound" that maps to "the" local IP address
This commit is contained in:
Zbigniew Jędrzejewski-Szmek 2021-05-07 17:15:22 +02:00 committed by GitHub
commit d0f14a6cf4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 262 additions and 18 deletions

View File

@ -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.</para></listitem>
<listitem><para>The hostname <literal>_outbound</literal> 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.</para></listitem>
</itemizedlist>
<para>Various software relies on an always-resolvable local

View File

@ -299,11 +299,11 @@
<listitem><para>Takes a boolean parameter; used in conjunction with <command>query</command>. If true
(the default), select domains are resolved on the local system, among them
<literal>localhost</literal> and <literal>_gateway</literal> or entries from
<filename>/etc/hosts</filename>. If false these domains are not resolved locally, and either fail (in
case of <literal>localhost</literal> or <literal>_gateway</literal> and suchlike) or go to the
network via regular DNS/mDNS/LLMNR lookups (in case of <filename>/etc/hosts</filename>
entries).</para></listitem>
<literal>localhost</literal>, <literal>_gateway</literal> and <literal>_outbound</literal>, or
entries from <filename>/etc/hosts</filename>. If false these domains are not resolved locally, and
either fail (in case of <literal>localhost</literal>, <literal>_gateway</literal> or
<literal>_outbound</literal> and suchlike) or go to the network via regular DNS/mDNS/LLMNR lookups
(in case of <filename>/etc/hosts</filename> entries).</para></listitem>
</varlistentry>
<varlistentry>

View File

@ -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.</para></listitem>
<listitem><para>The hostname <literal>_outbound</literal> 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.</para></listitem>
<listitem><para>The mappings defined in <filename>/etc/hosts</filename> are resolved to their
configured addresses and back, but they will not affect lookups for non-address types (like MX).
Support for <filename>/etc/hosts</filename> may be disabled with <varname>ReadEtcHosts=no</varname>,

View File

@ -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.");
}

View File

@ -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) {

View File

@ -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

View File

@ -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;

View File

@ -1,8 +1,11 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <net/if_arp.h>
#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);
}

View File

@ -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);

View File

@ -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;
}

View File

@ -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);