]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
local-addresses: add helper for determining local "outbound" IP addresses
authorLennart Poettering <lennart@poettering.net>
Fri, 26 Mar 2021 17:06:26 +0000 (18:06 +0100)
committerLennart Poettering <lennart@poettering.net>
Fri, 23 Apr 2021 10:01:41 +0000 (12:01 +0200)
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
src/shared/local-addresses.h
src/test/test-local-addresses.c

index 1de890f1421fe5416bcd2135e8ab8ebbee3d5ed4..c97687cd054b0cc4b69f4a004f8e501e40ad793a 100644 (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);
         }
 
index c633995dc9a62b7b7c0f5113c9dbed5f7a1e2b6a..38a17d233e61eb7d5847d510e781b5944e32b9ae 100644 (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);
index 7eeddd28f44e418b8ffe24d8fe8abbaa86ff7828..7b75132e1826febc9b5874993362c45ebc5e2a9d 100644 (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;
 }