summaryrefslogtreecommitdiff
path: root/src/shared/local-addresses.c
diff options
context:
space:
mode:
authorLennart Poettering <lennart@poettering.net>2021-03-26 18:06:26 +0100
committerLennart Poettering <lennart@poettering.net>2021-04-23 12:01:41 +0200
commit54e6f97bc9931679aa9b895546621b15e0f464a4 (patch)
tree2ebb6e370a084956daefa2f66ecb178fd7c7be54 /src/shared/local-addresses.c
parentce0cedacc4dbfc465797b039b3d3af0cd13b6bd5 (diff)
downloadsystemd-54e6f97bc9931679aa9b895546621b15e0f464a4.tar.gz
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.
Diffstat (limited to 'src/shared/local-addresses.c')
-rw-r--r--src/shared/local-addresses.c185
1 files changed, 183 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 <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);
}