diff --git a/src/shared/local-addresses.c b/src/shared/local-addresses.c index 10550fca20..4e811ba2bf 100644 --- a/src/shared/local-addresses.c +++ b/src/shared/local-addresses.c @@ -44,8 +44,8 @@ bool has_local_address(const struct local_address *addresses, size_t n_addresses assert(addresses || n_addresses == 0); assert(needle); - for (size_t i = 0; i < n_addresses; i++) - if (address_compare(addresses + i, needle) == 0) + FOREACH_ARRAY(i, addresses, n_addresses) + if (address_compare(i, needle) == 0) return true; return false; @@ -81,7 +81,8 @@ static int add_local_address_full( uint32_t priority, uint32_t weight, int family, - const union in_addr_union *address) { + const union in_addr_union *address, + const union in_addr_union *prefsrc) { assert(list); assert(n_list); @@ -99,6 +100,7 @@ static int add_local_address_full( .weight = weight, .family = family, .address = *address, + .prefsrc = prefsrc ? *prefsrc : IN_ADDR_NULL, }; return 1; @@ -112,7 +114,10 @@ int add_local_address( int family, const union in_addr_union *address) { - return add_local_address_full(list, n_list, ifindex, scope, 0, 0, family, address); + return add_local_address_full( + list, n_list, ifindex, + scope, /* priority = */ 0, /* weight = */ 0, + family, address, /* prefsrc = */ NULL); } int local_addresses( @@ -235,9 +240,14 @@ static int add_local_gateway( uint32_t priority, uint32_t weight, int family, - const union in_addr_union *address) { + const union in_addr_union *address, + const union in_addr_union *prefsrc) { - return add_local_address_full(list, n_list, ifindex, 0, priority, weight, family, address); + return add_local_address_full( + list, n_list, + ifindex, + /* scope = */ 0, priority, weight, + family, address, prefsrc); } static int parse_nexthop_one( @@ -246,6 +256,7 @@ static int parse_nexthop_one( bool allow_via, int family, uint32_t priority, + const union in_addr_union *prefsrc, const struct rtnexthop *rtnh) { bool has_gw = false; @@ -268,7 +279,7 @@ static int parse_nexthop_one( union in_addr_union a; memcpy(&a, RTA_DATA(attr), FAMILY_ADDRESS_SIZE(family)); - r = add_local_gateway(list, n_list, rtnh->rtnh_ifindex, priority, rtnh->rtnh_hops, family, &a); + r = add_local_gateway(list, n_list, rtnh->rtnh_ifindex, priority, rtnh->rtnh_hops, family, &a, prefsrc); if (r < 0) return r; @@ -294,7 +305,8 @@ static int parse_nexthop_one( return -EBADMSG; /* gateway address should be always IPv6. */ r = add_local_gateway(list, n_list, rtnh->rtnh_ifindex, priority, rtnh->rtnh_hops, via->family, - &(union in_addr_union) { .in6 = via->address.in6 }); + &(union in_addr_union) { .in6 = via->address.in6 }, + /* prefsrc = */ NULL); if (r < 0) return r; @@ -311,6 +323,7 @@ static int parse_nexthops( bool allow_via, int family, uint32_t priority, + const union in_addr_union *prefsrc, const struct rtnexthop *rtnh, size_t size) { @@ -334,7 +347,7 @@ static int parse_nexthops( if (ifindex > 0 && rtnh->rtnh_ifindex != ifindex) goto next_nexthop; - r = parse_nexthop_one(list, n_list, allow_via, family, priority, rtnh); + r = parse_nexthop_one(list, n_list, allow_via, family, priority, prefsrc, rtnh); if (r < 0) return r; @@ -393,6 +406,7 @@ int local_gateways( return r; for (sd_netlink_message *m = reply; m; m = sd_netlink_message_next(m)) { + union in_addr_union prefsrc = IN_ADDR_NULL; uint16_t type; unsigned char dst_len, src_len, table; uint32_t ifi = 0, priority = 0; @@ -439,6 +453,10 @@ int local_gateways( if (af != AF_UNSPEC && af != family) continue; + r = netlink_message_read_in_addr_union(m, RTA_PREFSRC, family, &prefsrc); + if (r < 0 && r != -ENODATA) + return r; + r = sd_netlink_message_read_u32(m, RTA_OIF, &ifi); if (r < 0 && r != -ENODATA) return r; @@ -453,7 +471,7 @@ int local_gateways( if (r < 0 && r != -ENODATA) return r; if (r >= 0) { - r = add_local_gateway(&list, &n_list, ifi, priority, 0, family, &gateway); + r = add_local_gateway(&list, &n_list, ifi, priority, 0, family, &gateway, &prefsrc); if (r < 0) return r; @@ -474,8 +492,10 @@ int local_gateways( if (via.family != AF_INET6) return -EBADMSG; + /* Ignore prefsrc, and let's take the source address by socket command, if necessary. */ r = add_local_gateway(&list, &n_list, ifi, priority, 0, via.family, - &(union in_addr_union) { .in6 = via.address.in6 }); + &(union in_addr_union) { .in6 = via.address.in6 }, + /* prefsrc = */ NULL); if (r < 0) return r; } @@ -490,7 +510,7 @@ int local_gateways( if (r < 0 && r != -ENODATA) return r; if (r >= 0) { - r = parse_nexthops(&list, &n_list, ifindex, allow_via, family, priority, rta_multipath, rta_len); + r = parse_nexthops(&list, &n_list, ifindex, allow_via, family, priority, &prefsrc, rta_multipath, rta_len); if (r < 0) return r; } @@ -512,7 +532,51 @@ static int add_local_outbound( int family, const union in_addr_union *address) { - return add_local_address_full(list, n_list, ifindex, 0, 0, 0, family, address); + return add_local_address_full( + list, n_list, ifindex, + /* scope = */ 0, /* priority = */ 0, /* weight = */ 0, + family, address, /* prefsrc = */ NULL); +} + +static int add_local_outbound_by_prefsrc( + struct local_address **list, + size_t *n_list, + const struct local_address *gateway, + const struct local_address *addresses, + size_t n_addresses) { + + int r; + + assert(list); + assert(n_list); + assert(gateway); + + if (!in_addr_is_set(gateway->family, &gateway->prefsrc)) + return 0; + + /* If the gateway has prefsrc, then let's honor the field. But, check if the address is assigned to + * the same interface, like we do with SO_BINDTOINDEX. */ + + bool found = false; + FOREACH_ARRAY(a, addresses, n_addresses) { + if (a->ifindex != gateway->ifindex) + continue; + if (a->family != gateway->family) + continue; + if (in_addr_equal(a->family, &a->address, &gateway->prefsrc) <= 0) + continue; + + found = true; + break; + } + if (!found) + return -EHOSTUNREACH; + + r = add_local_outbound(list, n_list, gateway->ifindex, gateway->family, &gateway->prefsrc); + if (r < 0) + return r; + + return 1; } int local_outbounds( @@ -521,9 +585,9 @@ int local_outbounds( int af, struct local_address **ret) { - _cleanup_free_ struct local_address *list = NULL, *gateways = NULL; + _cleanup_free_ struct local_address *list = NULL, *gateways = NULL, *addresses = NULL; size_t n_list = 0; - int r, n_gateways; + int r, n_gateways, n_addresses; /* 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 @@ -544,21 +608,31 @@ int local_outbounds( return 0; } - for (int i = 0; i < n_gateways; i++) { + n_addresses = local_addresses(context, ifindex, af, &addresses); + if (n_addresses < 0) + return n_addresses; + + FOREACH_ARRAY(i, gateways, n_gateways) { _cleanup_close_ int fd = -EBADF; union sockaddr_union sa; socklen_t salen; - fd = socket(gateways[i].family, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0); + r = add_local_outbound_by_prefsrc(&list, &n_list, i, addresses, n_addresses); + if (r > 0 || r == -EHOSTUNREACH) + continue; + if (r < 0) + return r; + + fd = socket(i->family, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0); if (fd < 0) return -errno; - switch (gateways[i].family) { + switch (i->family) { case AF_INET: sa.in = (struct sockaddr_in) { .sin_family = AF_INET, - .sin_addr = gateways[i].address.in, + .sin_addr = i->address.in, .sin_port = htobe16(53), /* doesn't really matter which port we pick — * we just care about the routing decision */ }; @@ -568,9 +642,9 @@ int local_outbounds( case AF_INET6: sa.in6 = (struct sockaddr_in6) { .sin6_family = AF_INET6, - .sin6_addr = gateways[i].address.in6, + .sin6_addr = i->address.in6, .sin6_port = htobe16(53), - .sin6_scope_id = gateways[i].ifindex, + .sin6_scope_id = i->ifindex, }; break; @@ -584,18 +658,18 @@ int local_outbounds( * 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); + r = socket_set_unicast_if(fd, i->family, i->ifindex); if (r < 0) - log_debug_errno(r, "Failed to set unicast interface index %i, ignoring: %m", gateways[i].ifindex); + log_debug_errno(r, "Failed to set unicast interface index %i, ignoring: %m", 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); + r = socket_bind_to_ifindex(fd, i->ifindex); if (r < 0) - log_debug_errno(r, "Failed to bind socket to interface %i, ignoring: %m", gateways[i].ifindex); + log_debug_errno(r, "Failed to bind socket to interface %i, ignoring: %m", 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 @@ -612,16 +686,16 @@ int local_outbounds( salen = SOCKADDR_LEN(sa); if (getsockname(fd, &sa.sa, &salen) < 0) return -errno; - assert(sa.sa.sa_family == gateways[i].family); + assert(sa.sa.sa_family == i->family); assert(salen == SOCKADDR_LEN(sa)); - switch (gateways[i].family) { + switch (i->family) { case AF_INET: if (in4_addr_is_null(&sa.in.sin_addr)) /* Auto-binding didn't work. :-( */ continue; - r = add_local_outbound(&list, &n_list, gateways[i].ifindex, gateways[i].family, + r = add_local_outbound(&list, &n_list, i->ifindex, i->family, &(union in_addr_union) { .in = sa.in.sin_addr }); if (r < 0) return r; @@ -631,7 +705,7 @@ int local_outbounds( if (in6_addr_is_null(&sa.in6.sin6_addr)) continue; - r = add_local_outbound(&list, &n_list, gateways[i].ifindex, gateways[i].family, + r = add_local_outbound(&list, &n_list, i->ifindex, i->family, &(union in_addr_union) { .in6 = sa.in6.sin6_addr }); if (r < 0) return r; diff --git a/src/shared/local-addresses.h b/src/shared/local-addresses.h index 42abca7838..9e8374fa6f 100644 --- a/src/shared/local-addresses.h +++ b/src/shared/local-addresses.h @@ -12,6 +12,7 @@ struct local_address { uint32_t weight; int family; union in_addr_union address; + union in_addr_union prefsrc; }; bool has_local_address(const struct local_address *addresses, size_t n_addresses, const struct local_address *needle); diff --git a/src/test/test-local-addresses.c b/src/test/test-local-addresses.c index f418f3712d..d4a2a6a097 100644 --- a/src/test/test-local-addresses.c +++ b/src/test/test-local-addresses.c @@ -153,12 +153,13 @@ static void check_local_gateways(sd_netlink *rtnl, int ifindex, int request_ifin }) == IN_SET(family, AF_UNSPEC, AF_INET6)); } -static void check_local_outbounds(sd_netlink *rtnl, int ifindex, int request_ifindex, int family) { +static void check_local_outbounds(sd_netlink *rtnl, int ifindex, int request_ifindex, int family, const char *ipv6_expected) { _cleanup_free_ struct local_address *a = NULL; union in_addr_union u; int n; - log_debug("/* Local Outbounds (ifindex:%i, %s) */", request_ifindex, family == AF_UNSPEC ? "AF_UNSPEC" : af_to_name(family)); + log_debug("/* Local Outbounds (ifindex:%i, %s, expected_ipv6_address=%s) */", + request_ifindex, family == AF_UNSPEC ? "AF_UNSPEC" : af_to_name(family), ipv6_expected); n = local_outbounds(rtnl, request_ifindex, family, &a); assert_se(n >= 0); @@ -180,7 +181,7 @@ static void check_local_outbounds(sd_netlink *rtnl, int ifindex, int request_ifi .address = u, }) == (family == AF_UNSPEC && support_rta_via)); - assert_se(in_addr_from_string(AF_INET6, "2001:db8:1:123::123", &u) >= 0); + assert_se(in_addr_from_string(AF_INET6, ipv6_expected, &u) >= 0); assert_se(has_local_address(a, n, &(struct local_address) { .ifindex = ifindex, @@ -312,12 +313,86 @@ TEST(local_addresses_with_dummy) { check_local_gateways(rtnl, ifindex, ifindex, AF_UNSPEC); check_local_gateways(rtnl, ifindex, ifindex, AF_INET); check_local_gateways(rtnl, ifindex, ifindex, AF_INET6); - check_local_outbounds(rtnl, ifindex, 0, AF_UNSPEC); - check_local_outbounds(rtnl, ifindex, 0, AF_INET); - check_local_outbounds(rtnl, ifindex, 0, AF_INET6); - check_local_outbounds(rtnl, ifindex, ifindex, AF_UNSPEC); - check_local_outbounds(rtnl, ifindex, ifindex, AF_INET); - check_local_outbounds(rtnl, ifindex, ifindex, AF_INET6); + check_local_outbounds(rtnl, ifindex, 0, AF_UNSPEC, "2001:db8:1:123::123"); + check_local_outbounds(rtnl, ifindex, 0, AF_INET, "2001:db8:1:123::123"); + check_local_outbounds(rtnl, ifindex, 0, AF_INET6, "2001:db8:1:123::123"); + check_local_outbounds(rtnl, ifindex, ifindex, AF_UNSPEC, "2001:db8:1:123::123"); + check_local_outbounds(rtnl, ifindex, ifindex, AF_INET, "2001:db8:1:123::123"); + check_local_outbounds(rtnl, ifindex, ifindex, AF_INET6, "2001:db8:1:123::123"); + + /* Add one more IPv6 address. */ + assert_se(sd_rtnl_message_new_addr_update(rtnl, &message, ifindex, AF_INET6) >= 0); + assert_se(sd_rtnl_message_addr_set_scope(message, RT_SCOPE_UNIVERSE) >= 0); + assert_se(sd_rtnl_message_addr_set_prefixlen(message, 64) >= 0); + assert_se(in_addr_from_string(AF_INET6, "2001:db8:1:123::124", &u) >= 0); + assert_se(sd_netlink_message_append_in6_addr(message, IFA_LOCAL, &u.in6) >= 0); + assert_se(sd_netlink_message_append_u32(message, IFA_FLAGS, IFA_F_NODAD) >= 0); + assert_se(sd_netlink_call(rtnl, message, 0, NULL) >= 0); + message = sd_netlink_message_unref(message); + + /* Replace the previous IPv6 default gateway with one with preferred source address. */ + assert_se(sd_rtnl_message_new_route(rtnl, &message, RTM_DELROUTE, AF_INET6, RTPROT_STATIC) >= 0); + assert_se(sd_rtnl_message_route_set_type(message, RTN_UNICAST) >= 0); + assert_se(sd_netlink_message_append_u32(message, RTA_PRIORITY, 1234) >= 0); + assert_se(sd_netlink_message_append_u32(message, RTA_TABLE, RT_TABLE_MAIN) >= 0); + assert_se(in_addr_from_string(AF_INET6, "2001:db8:1:123::1", &u) >= 0); + assert_se(sd_netlink_message_append_in6_addr(message, RTA_GATEWAY, &u.in6) >= 0); + assert_se(sd_netlink_message_append_u32(message, RTA_OIF, ifindex) >= 0); + assert_se(sd_netlink_call(rtnl, message, 0, NULL) >= 0); + message = sd_netlink_message_unref(message); + + assert_se(sd_rtnl_message_new_route(rtnl, &message, RTM_NEWROUTE, AF_INET6, RTPROT_STATIC) >= 0); + assert_se(sd_rtnl_message_route_set_type(message, RTN_UNICAST) >= 0); + assert_se(sd_netlink_message_append_u32(message, RTA_PRIORITY, 1234) >= 0); + assert_se(sd_netlink_message_append_u32(message, RTA_TABLE, RT_TABLE_MAIN) >= 0); + assert_se(in_addr_from_string(AF_INET6, "2001:db8:1:123::1", &u) >= 0); + assert_se(sd_netlink_message_append_in6_addr(message, RTA_GATEWAY, &u.in6) >= 0); + assert_se(sd_netlink_message_append_u32(message, RTA_OIF, ifindex) >= 0); + assert_se(in_addr_from_string(AF_INET6, "2001:db8:1:123::123", &u) >= 0); + assert_se(sd_netlink_message_append_in6_addr(message, RTA_PREFSRC, &u.in6) >= 0); + assert_se(sd_netlink_call(rtnl, message, 0, NULL) >= 0); + message = sd_netlink_message_unref(message); + + /* Check again. */ + check_local_outbounds(rtnl, ifindex, 0, AF_UNSPEC, "2001:db8:1:123::123"); + check_local_outbounds(rtnl, ifindex, 0, AF_INET, "2001:db8:1:123::123"); + check_local_outbounds(rtnl, ifindex, 0, AF_INET6, "2001:db8:1:123::123"); + check_local_outbounds(rtnl, ifindex, ifindex, AF_UNSPEC, "2001:db8:1:123::123"); + check_local_outbounds(rtnl, ifindex, ifindex, AF_INET, "2001:db8:1:123::123"); + check_local_outbounds(rtnl, ifindex, ifindex, AF_INET6, "2001:db8:1:123::123"); + + /* Replace the preferred source address. */ + assert_se(sd_rtnl_message_new_route(rtnl, &message, RTM_DELROUTE, AF_INET6, RTPROT_STATIC) >= 0); + assert_se(sd_rtnl_message_route_set_type(message, RTN_UNICAST) >= 0); + assert_se(sd_netlink_message_append_u32(message, RTA_PRIORITY, 1234) >= 0); + assert_se(sd_netlink_message_append_u32(message, RTA_TABLE, RT_TABLE_MAIN) >= 0); + assert_se(in_addr_from_string(AF_INET6, "2001:db8:1:123::1", &u) >= 0); + assert_se(sd_netlink_message_append_in6_addr(message, RTA_GATEWAY, &u.in6) >= 0); + assert_se(sd_netlink_message_append_u32(message, RTA_OIF, ifindex) >= 0); + assert_se(in_addr_from_string(AF_INET6, "2001:db8:1:123::123", &u) >= 0); + assert_se(sd_netlink_message_append_in6_addr(message, RTA_PREFSRC, &u.in6) >= 0); + assert_se(sd_netlink_call(rtnl, message, 0, NULL) >= 0); + message = sd_netlink_message_unref(message); + + assert_se(sd_rtnl_message_new_route(rtnl, &message, RTM_NEWROUTE, AF_INET6, RTPROT_STATIC) >= 0); + assert_se(sd_rtnl_message_route_set_type(message, RTN_UNICAST) >= 0); + assert_se(sd_netlink_message_append_u32(message, RTA_PRIORITY, 1234) >= 0); + assert_se(sd_netlink_message_append_u32(message, RTA_TABLE, RT_TABLE_MAIN) >= 0); + assert_se(in_addr_from_string(AF_INET6, "2001:db8:1:123::1", &u) >= 0); + assert_se(sd_netlink_message_append_in6_addr(message, RTA_GATEWAY, &u.in6) >= 0); + assert_se(sd_netlink_message_append_u32(message, RTA_OIF, ifindex) >= 0); + assert_se(in_addr_from_string(AF_INET6, "2001:db8:1:123::124", &u) >= 0); + assert_se(sd_netlink_message_append_in6_addr(message, RTA_PREFSRC, &u.in6) >= 0); + assert_se(sd_netlink_call(rtnl, message, 0, NULL) >= 0); + message = sd_netlink_message_unref(message); + + /* Check again. */ + check_local_outbounds(rtnl, ifindex, 0, AF_UNSPEC, "2001:db8:1:123::124"); + check_local_outbounds(rtnl, ifindex, 0, AF_INET, "2001:db8:1:123::124"); + check_local_outbounds(rtnl, ifindex, 0, AF_INET6, "2001:db8:1:123::124"); + check_local_outbounds(rtnl, ifindex, ifindex, AF_UNSPEC, "2001:db8:1:123::124"); + check_local_outbounds(rtnl, ifindex, ifindex, AF_INET, "2001:db8:1:123::124"); + check_local_outbounds(rtnl, ifindex, ifindex, AF_INET6, "2001:db8:1:123::124"); /* Cleanup */ assert_se(sd_rtnl_message_new_link(rtnl, &message, RTM_DELLINK, ifindex) >= 0);