diff --git a/include/seastar/net/socket_defs.hh b/include/seastar/net/socket_defs.hh index f54cb486d27..28928c34fd7 100644 --- a/include/seastar/net/socket_defs.hh +++ b/include/seastar/net/socket_defs.hh @@ -139,7 +139,8 @@ struct ipv6_addr { ipv6_addr(const ipv6_bytes&, uint16_t port = 0) noexcept; ipv6_addr(uint16_t port = 0) noexcept; - // throws if not a valid ipv6 addr + // "addr" or "[addr]:port". A zoned literal (fe80::1%eth0) has no + // representation here: use net::inet_address. ipv6_addr(const std::string&); ipv6_addr(const std::string&, uint16_t port); ipv6_addr(const net::inet_address&, uint16_t = 0) noexcept; diff --git a/src/net/inet_address.cc b/src/net/inet_address.cc index f8e9282a995..8aadc7e9726 100644 --- a/src/net/inet_address.cc +++ b/src/net/inet_address.cc @@ -29,6 +29,8 @@ #include #include #include + +#include #include #include #include @@ -70,32 +72,36 @@ seastar::net::inet_address::parse_numerical(const sstring& addr) { in._in_family = family::INET; return in; } + if (::inet_pton(AF_INET6, addr.c_str(), &in._in6)) { + in._in_family = family::INET6; + return in; + } + // "%" (RFC 4007). An interface name that matches nothing makes + // the literal invalid; a numeric zone is kept as given, so a literal written + // for another host survives. auto i = addr.find_last_of('%'); - if (i != sstring::npos) { - auto ext = addr.substr(i + 1); - auto src = addr.substr(0, i); - auto res = parse_numerical(src); - - if (res) { - uint32_t index = std::numeric_limits::max(); - try { - index = std::stoul(ext); - } catch (...) { - } - for (auto& nwif : engine().net().network_interfaces()) { - if (nwif.index() == index || nwif.name() == ext || nwif.display_name() == ext) { - res->_scope = nwif.index(); - break; - } - } + if (i == sstring::npos) { + return {}; + } + auto ext = addr.substr(i + 1); + auto res = parse_numerical(addr.substr(0, i)); + if (!res || !res->is_ipv6() || ext.empty()) { + return {}; + } + uint32_t index = invalid_scope; + auto [end, ec] = std::from_chars(ext.data(), ext.data() + ext.size(), index); + bool numeric = ec == std::errc() && end == ext.data() + ext.size(); + for (auto& nwif : engine().net().network_interfaces()) { + if ((numeric && nwif.index() == index) || nwif.name() == ext || nwif.display_name() == ext) { + res->_scope = nwif.index(); return *res; } } - if (::inet_pton(AF_INET6, addr.c_str(), &in._in6)) { - in._in_family = family::INET6; - return in; + if (!numeric) { + return {}; } - return {}; + res->_scope = index; + return *res; } seastar::net::inet_address::inet_address(const sstring& addr) @@ -296,11 +302,25 @@ seastar::ipv6_addr::ipv6_addr(const ::in6_addr& in6, uint16_t p) noexcept seastar::ipv6_addr::ipv6_addr(const std::string& s) : ipv6_addr([&] { - auto lc = s.find_last_of(']'); - auto cp = s.find_first_of(':', lc); - auto port = cp != std::string::npos ? std::stoul(s.substr(cp + 1)) : 0; - auto ss = lc != std::string::npos ? s.substr(1, lc - 1) : s; - return ipv6_addr(net::ipv6_address(ss).bytes(), uint16_t(port)); + // "::1:9092" is a valid address, so only a bracketed host can carry + // a port. + if (s.empty() || s.front() != '[') { + return ipv6_addr(net::ipv6_address(s).bytes(), 0); + } + auto rb = s.find(']'); + if (rb == std::string::npos) { + throw std::invalid_argument(fmt::format("Unterminated bracket in IPv6 address {}", s)); + } + uint16_t port = 0; + if (rb + 1 < s.size()) { + auto* first = s.data() + rb + 1; + auto* last = s.data() + s.size(); + auto [end, ec] = *first == ':' ? std::from_chars(first + 1, last, port) : std::from_chars_result{first, std::errc::invalid_argument}; + if (ec != std::errc() || end != last) { + throw std::invalid_argument(fmt::format("Invalid port in IPv6 address {}", s)); + } + } + return ipv6_addr(net::ipv6_address(s.substr(1, rb - 1)).bytes(), port); }()) {} @@ -329,13 +349,18 @@ seastar::net::inet_address seastar::socket_address::addr() const noexcept { switch (family()) { case AF_INET: return net::inet_address(as_posix_sockaddr_in().sin_addr); - case AF_INET6: - return net::inet_address(as_posix_sockaddr_in6().sin6_addr, as_posix_sockaddr_in6().sin6_scope_id); + case AF_INET6: { + auto& in6 = as_posix_sockaddr_in6(); + return net::inet_address(in6.sin6_addr, in6.sin6_scope_id ? in6.sin6_scope_id : net::inet_address::invalid_scope); + } default: return net::inet_address(); } } +// The port is read through the sockaddr_in view for every family. +static_assert(offsetof(::sockaddr_in, sin_port) == offsetof(::sockaddr_in6, sin6_port)); + ::in_port_t seastar::socket_address::port() const noexcept { return net::ntoh(u.in.sin_port); } diff --git a/src/net/posix-stack.cc b/src/net/posix-stack.cc index 533dad3935e..bb7302bf109 100644 --- a/src/net/posix-stack.cc +++ b/src/net/posix-stack.cc @@ -358,9 +358,11 @@ class posix_proxied_connected_socket_impl final : public posix_connected_socket_ } }; +// A link-local destination without a zone cannot be routed; pick the zone of +// the interface whose route covers it. static void resolve_outgoing_address(socket_address& a) { if (a.family() != AF_INET6 - || a.as_posix_sockaddr_in6().sin6_scope_id != inet_address::invalid_scope + || a.as_posix_sockaddr_in6().sin6_scope_id != 0 || !IN6_IS_ADDR_LINKLOCAL(&a.as_posix_sockaddr_in6().sin6_addr) ) { return; @@ -434,7 +436,7 @@ static void resolve_outgoing_address(socket_address& a) { dest_str[39] = '\0'; struct in6_addr addr; - if (inet_pton(AF_INET6, dest_str, &addr) < 0) { + if (inet_pton(AF_INET6, dest_str, &addr) != 1) { /* not an Ipv6 address */ continue; } diff --git a/src/net/socket_address.cc b/src/net/socket_address.cc index 88628b0f574..c50b68cf3e0 100644 --- a/src/net/socket_address.cc +++ b/src/net/socket_address.cc @@ -75,7 +75,9 @@ socket_address::socket_address(const ipv6_addr& addr, uint32_t scope) noexcept u.in6.sin6_family = AF_INET6; u.in6.sin6_port = htons(addr.port); u.in6.sin6_flowinfo = 0; - u.in6.sin6_scope_id = scope; + // The kernel's "no zone" is 0; inet_address::invalid_scope is only a + // marker for the in-memory type. + u.in6.sin6_scope_id = scope == net::inet_address::invalid_scope ? 0 : scope; std::copy(addr.ip.begin(), addr.ip.end(), u.in6.sin6_addr.s6_addr); } @@ -151,6 +153,8 @@ bool socket_address::operator==(const socket_address& a) const noexcept { auto& in1 = as_posix_sockaddr_in6(); auto& in2 = a.as_posix_sockaddr_in6(); + // sin6_scope_id is deliberately not compared (std::hash agrees), so a + // literal parsed with %zone equals the same address without one. return IN6_ARE_ADDR_EQUAL(&in1.sin6_addr, &in2.sin6_addr); } diff --git a/tests/unit/network_interface_test.cc b/tests/unit/network_interface_test.cc index 86f354c149a..2ab63387d0a 100644 --- a/tests/unit/network_interface_test.cc +++ b/tests/unit/network_interface_test.cc @@ -28,6 +28,8 @@ #include #include +#include + using namespace seastar; static logger niflog("network_interface_test"); @@ -136,6 +138,62 @@ SEASTAR_TEST_CASE(test_ipv4_mapped_addresses) { return make_ready_future(); } +// Zone index 0 is the kernel's "no zone": it must not surface as a scope or +// print as %0, and the in-memory "no scope" marker must not reach the kernel. +SEASTAR_TEST_CASE(test_ipv6_scope_round_trip) { + ::sockaddr_in6 in6{}; + in6.sin6_family = AF_INET6; + in6.sin6_port = net::hton(uint16_t(9092)); + BOOST_REQUIRE_EQUAL(::inet_pton(AF_INET6, "2001:db8::1", &in6.sin6_addr), 1); + const socket_address from_kernel(in6); + BOOST_REQUIRE_EQUAL(from_kernel.addr().scope(), net::inet_address::invalid_scope); + BOOST_REQUIRE_EQUAL(fmt::to_string(from_kernel), "[2001:db8::1]:9092"); + BOOST_REQUIRE_EQUAL(fmt::to_string(from_kernel.addr()), "2001:db8::1"); + + const socket_address unscoped(ipv6_addr("2001:db8::1", 9092)); + BOOST_REQUIRE_EQUAL(unscoped.as_posix_sockaddr_in6().sin6_scope_id, 0u); + BOOST_REQUIRE_EQUAL(socket_address(net::inet_address("2001:db8::1"), 9092).as_posix_sockaddr_in6().sin6_scope_id, 0u); + BOOST_REQUIRE_EQUAL(unscoped, from_kernel); + + // a real zone survives in both directions + if (auto lo = if_nametoindex("lo")) { + const socket_address scoped(ipv6_addr("fe80::1", 1), lo); + BOOST_REQUIRE_EQUAL(scoped.as_posix_sockaddr_in6().sin6_scope_id, lo); + BOOST_REQUIRE_EQUAL(scoped.addr().scope(), lo); + BOOST_REQUIRE_EQUAL(fmt::to_string(scoped), fmt::format("[fe80::1%{}]:1", lo)); + BOOST_REQUIRE_EQUAL(socket_address(net::inet_address(fmt::format("fe80::1%{}", lo)), 1), scoped); + } + return make_ready_future(); +} + +// RFC 4007 ยง11: a numeric zone is an interface index and is kept as given. +SEASTAR_TEST_CASE(test_inet_address_numeric_zone) { + const net::inet_address foreign("fe80::1%4000000"); + BOOST_REQUIRE_EQUAL(foreign.scope(), 4000000u); + BOOST_REQUIRE_EQUAL(fmt::to_string(foreign), "fe80::1%4000000"); + BOOST_REQUIRE(!net::inet_address::parse_numerical("fe80::1%no-such-interface")); + if (auto lo = if_nametoindex("lo")) { + BOOST_REQUIRE_EQUAL(net::inet_address("fe80::1%lo").scope(), lo); + BOOST_REQUIRE_EQUAL(net::inet_address(fmt::format("fe80::1%{}", lo)).scope(), lo); + } + return make_ready_future(); +} + +SEASTAR_TEST_CASE(test_ipv6_addr_string_forms) { + BOOST_REQUIRE_EQUAL(ipv6_addr("::1").port, 0); + BOOST_REQUIRE_EQUAL(ipv6_addr("[::1]").port, 0); + BOOST_REQUIRE_EQUAL(ipv6_addr("[::1]:9092").port, 9092); + BOOST_REQUIRE_EQUAL(socket_address(ipv6_addr("[::1]:9092")), socket_address(ipv6_addr("::1", 9092))); + // an unbracketed trailing group is part of the address, not a port + BOOST_REQUIRE_EQUAL(socket_address(ipv6_addr("::1:9092")), socket_address(ipv6_addr("0:0:0:0:0:0:1:9092", 0))); + + for (auto bad : {"[::1]:70000", "[::1]:-1", "[::1]:x", "[::1]9092", "[::1]:", "[::1"}) { + BOOST_CHECK_THROW(ipv6_addr{bad}, std::invalid_argument); + } + BOOST_CHECK_THROW(ipv6_addr("[fe80::1%lo]:9092"), std::runtime_error); + return make_ready_future(); +} + SEASTAR_TEST_CASE(test_inet_address_format) { const std::string tests[] = { // IPv4 addresses @@ -183,6 +241,10 @@ SEASTAR_TEST_CASE(test_inet_address_parse_invalid) { "fe80:2030:12345", "fe80:2030:12345%%", ":", + // zones belong to IPv6 link-local addresses, and must name something + "127.0.0.1%lo", + "fe80::1%", + "fe80::1%no-such-interface", }; for (auto s : tests) {