Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion include/seastar/net/socket_defs.hh
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
81 changes: 53 additions & 28 deletions src/net/inet_address.cc
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
#include <seastar/net/inet_address.hh>
#include <seastar/net/socket_defs.hh>
#include <seastar/net/dns.hh>

#include <charconv>
#include <seastar/net/ip.hh>
#include <seastar/core/reactor.hh>
#include <seastar/core/print.hh>
Expand Down Expand Up @@ -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;
}
// "<ipv6>%<zone>" (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<uint32_t>::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)
Expand Down Expand Up @@ -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);
}())
{}

Expand Down Expand Up @@ -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);
}
Expand Down
6 changes: 4 additions & 2 deletions src/net/posix-stack.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down
6 changes: 5 additions & 1 deletion src/net/socket_address.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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);
}

Expand Down
62 changes: 62 additions & 0 deletions tests/unit/network_interface_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
#include <seastar/core/thread.hh>
#include <seastar/util/log.hh>

#include <net/if.h>

using namespace seastar;

static logger niflog("network_interface_test");
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
Loading