Skip to content

Commit 002fcbe

Browse files
Trent Houlistonclaude
andcommitted
Sync nuclear subtree from NUClear@e689ffdb
A peer whose data socket lands on the same ephemeral port as ours is no longer mistaken for our own announce coming back, which had made that one peer invisible while every other peer worked. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent e3bea85 commit 002fcbe

6 files changed

Lines changed: 122 additions & 2 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Node.js module for interacting with the [NUClear](https://github.com/Fastcode/NU
88

99
Version 2 uses the redesigned **NUClearNet** library from [NUClear PR #190](https://github.com/Fastcode/NUClear/pull/190) (wire protocol **0x03**). It is **not** compatible with 1.x clients or NUClear builds that still use the old `NUClearNetwork` stack (protocol 0x02). Upgrade Node clients and NUClear robots together.
1010

11-
The vendored NUClear tree is updated via `git subtree` from the `houliston/nuclearnet-v2` branch (currently [NUClear@2053a375](https://github.com/Fastcode/NUClear/commit/2053a375)).
11+
The vendored NUClear tree is updated via `git subtree` from the `houliston/nuclearnet-v2` branch (currently [NUClear@e689ffdb](https://github.com/Fastcode/NUClear/commit/e689ffdb)).
1212

1313
Peer join events may arrive slightly later than in 1.x because connection requires both multicast announce and a unicast CONNECT handshake.
1414

‎src/nuclear/src/nuclearnet/Discovery.cpp‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,27 @@ namespace network {
113113
return packet;
114114
}
115115

116+
bool Discovery::peek_announce_name(const uint8_t* data, std::size_t length, std::string& name) {
117+
// Minimum size: header(5) + name_length(2) + num_subscriptions(2) = 9
118+
if (length < sizeof(PacketHeader) + sizeof(uint16_t) + sizeof(uint16_t)) {
119+
return false;
120+
}
121+
122+
const uint8_t* ptr = data + sizeof(PacketHeader);
123+
124+
uint16_t name_len = 0;
125+
std::memcpy(&name_len, ptr, sizeof(uint16_t));
126+
ptr += sizeof(uint16_t);
127+
128+
if (length - sizeof(PacketHeader) - sizeof(uint16_t) < name_len + sizeof(uint16_t)) {
129+
return false;
130+
}
131+
132+
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
133+
name.assign(reinterpret_cast<const char*>(ptr), name_len);
134+
return !name.empty();
135+
}
136+
116137
Discovery::AnnounceResult Discovery::process_announce(const sock_t& source,
117138
const uint8_t* data,
118139
std::size_t length,

‎src/nuclear/src/nuclearnet/Discovery.hpp‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,17 @@ namespace network {
120120
static std::vector<uint8_t> build_announce_packet(const std::string& name,
121121
const std::vector<uint64_t>& subscriptions);
122122

123+
/**
124+
* Read the node name out of an announce packet without otherwise processing it.
125+
*
126+
* @param data Pointer to the announce packet
127+
* @param length Length of the packet in bytes
128+
* @param name Filled with the name if the packet is well formed
129+
*
130+
* @return true if a name could be read
131+
*/
132+
static bool peek_announce_name(const uint8_t* data, std::size_t length, std::string& name);
133+
123134
/**
124135
* Build a leave packet.
125136
*

‎src/nuclear/src/nuclearnet/NUClearNet.cpp‎

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,14 @@ namespace {
618618
return fds;
619619
}
620620

621+
in_port_t NUClearNet::own_data_port() const {
622+
switch (own_data_address.sock.sa_family) {
623+
case AF_INET: return own_data_address.ipv4.sin_port;
624+
case AF_INET6: return own_data_address.ipv6.sin6_port;
625+
default: return 0;
626+
}
627+
}
628+
621629
bool NUClearNet::is_own_data_endpoint(const sock_t& source) const {
622630
if (own_data_address.sock.sa_family == AF_UNSPEC) {
623631
return false;
@@ -634,6 +642,23 @@ namespace {
634642
return false;
635643
}
636644

645+
bool NUClearNet::is_own_announce(const sock_t& source, const uint8_t* data, std::size_t length) const {
646+
// The data socket usually binds INADDR_ANY, so getsockname gives us a port but no usable address to
647+
// compare against. The port alone is not enough: a remote peer's data socket can land on the same
648+
// ephemeral port as ours, and treating it as us would silently discard every announce it ever sends.
649+
// Require the name to match as well, so a port collision alone cannot hide a peer.
650+
if (!is_own_data_endpoint(source)) {
651+
return false;
652+
}
653+
654+
std::string name;
655+
if (!Discovery::peek_announce_name(data, length, name)) {
656+
return false;
657+
}
658+
659+
return name == node_name;
660+
}
661+
637662
void NUClearNet::announce() {
638663
if (!data_fd.valid()) {
639664
return;
@@ -711,7 +736,7 @@ namespace {
711736

712737
void NUClearNet::process_announce_packet(const sock_t& source, const uint8_t* data, std::size_t length) {
713738
// Ignore our own announces (multicast/broadcast loopback) without blocking other nodes on 127.0.0.1
714-
if (is_own_data_endpoint(source)) {
739+
if (is_own_announce(source, data, length)) {
715740
if (should_log(LogLevel::Debug)) {
716741
log(LogLevel::Debug, "net", "ignoring self announce from " + sock_str(source));
717742
}

‎src/nuclear/src/nuclearnet/NUClearNet.hpp‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,9 @@ namespace network {
193193
*/
194194
std::vector<fd_t> listen_fds() const;
195195

196+
/// The ephemeral port our data socket is bound to, in network byte order
197+
in_port_t own_data_port() const;
198+
196199
/// Process a single received packet (dispatches to per-type handlers)
197200
void process_packet(const sock_t& source, const uint8_t* data, std::size_t length);
198201

@@ -243,6 +246,9 @@ namespace network {
243246
/// Returns true if the UDP source is this node's data socket (same ephemeral port).
244247
bool is_own_data_endpoint(const sock_t& source) const;
245248

249+
/// Whether an announce packet is one of ours looped back, rather than a peer that shares our port
250+
bool is_own_announce(const sock_t& source, const uint8_t* data, std::size_t length) const;
251+
246252
// Configuration
247253
NetworkConfig config;
248254
std::string node_name;

‎src/nuclear/tests/tests/nuclearnet/ProcessPacket.cpp‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,3 +568,60 @@ SCENARIO("A peer that never acknowledges a reliable send is disconnected",
568568
REQUIRE(left);
569569
}
570570
}
571+
572+
SCENARIO("A peer sharing our ephemeral port is not mistaken for ourselves", "[nuclearnet][process_packet]") {
573+
if (!test_util::has_ipv4_multicast()) {
574+
SKIP("No multicast support");
575+
}
576+
577+
auto net = make_test_net("Us");
578+
579+
bool joined = false;
580+
net->set_join_callback([&](const NUClear::network::PeerInfo& p) { joined = (p.name == "Them"); });
581+
582+
// A remote peer whose data socket happens to have landed on the same ephemeral port as ours. Only the
583+
// port is available to compare against, because our own socket binds INADDR_ANY.
584+
sock_t peer{};
585+
peer.ipv4.sin_family = AF_INET;
586+
peer.ipv4.sin_addr.s_addr = htonl(0x0A000001);
587+
peer.ipv4.sin_port = net->own_data_port();
588+
589+
auto announce_pkt = build_announce("Them", {});
590+
net->process_packet(peer, announce_pkt.data(), announce_pkt.size());
591+
592+
// Complete the handshake so the join fires
593+
auto syn = build_connect(SYN);
594+
net->process_connect_packet(peer, syn.data(), syn.size());
595+
auto syn_ack = build_connect(SYN | CON_ACK);
596+
net->process_connect_packet(peer, syn_ack.data(), syn_ack.size());
597+
auto ack = build_connect(CON_ACK);
598+
net->process_connect_packet(peer, ack.data(), ack.size());
599+
600+
THEN("their announce is processed rather than discarded as our own") {
601+
REQUIRE(joined);
602+
}
603+
}
604+
605+
SCENARIO("Our own announce looped back is still ignored", "[nuclearnet][process_packet]") {
606+
if (!test_util::has_ipv4_multicast()) {
607+
SKIP("No multicast support");
608+
}
609+
610+
auto net = make_test_net("Us");
611+
612+
bool joined = false;
613+
net->set_join_callback([&](const NUClear::network::PeerInfo&) { joined = true; });
614+
615+
// Our own announce, coming back to us with our own name and port
616+
sock_t self{};
617+
self.ipv4.sin_family = AF_INET;
618+
self.ipv4.sin_addr.s_addr = htonl(0x0A000001);
619+
self.ipv4.sin_port = net->own_data_port();
620+
621+
auto announce_pkt = build_announce("Us", {});
622+
net->process_packet(self, announce_pkt.data(), announce_pkt.size());
623+
624+
THEN("it is ignored") {
625+
REQUIRE_FALSE(joined);
626+
}
627+
}

0 commit comments

Comments
 (0)