mirror of
https://github.com/torlando-tech/pyxis.git
synced 2026-08-14 06:39:54 +00:00
fix(autointerface): explicit mld6_joingroup + LOOP + RX/TX diagnostics
Three changes, motivated by debugging "Sideband + pyxis on the same WiFi don't hear each other's announces": 1. Always call \`mld6_joingroup_netif()\` in addition to \`setsockopt IPV6_JOIN_GROUP\`. On ESP-IDF lwIP, the setsockopt path returns success but doesn't reliably push the multicast hash into the WiFi MAC filter — incoming multicast frames get silently dropped at L2. Calling the netif's mld6 API directly programs the chip filter. Joining twice on the netif is refcount-safe. 2. Set IPV6_MULTICAST_LOOP=1 so pyxis receives its own multicast echoes. ESP-IDF lwIP defaults this off, which makes upstream's "carrier lost / multicast echo timeout" warning fire even on a functioning network. With LOOP=1, the initial-echo path actually works on isolated test setups too. Logged as DEBUG if the platform doesn't support the option. 3. Add a periodic \`AutoInterface: stats announce_tx=N tx_fail=N disc_rx=N disc_self=N data_rx=N peers=N\` heartbeat (every 10s). Without this it's hard to tell whether pyxis isn't sending, isn't receiving, or is sending+receiving but rejecting the tokens. Discovery-RX from non-self addresses with bad tokens now also logs once with the hex prefix so token-mismatch cases are visible (group_id drift, scope-suffix encoding mismatches). Added _initial_echo_received update on first self-echo so the firewall warning at startup_grace fires correctly. After this, pyxis's own multicast loopback works (disc_self=N matches announce_tx=N within 10s). Cross-LAN multicast against rnsd / Sideband still doesn't make it through, which is an ESP32 WiFi multicast TX limitation — pyxis's frames aren't reaching the AP. Not a fix here; the diagnostics make the boundary visible. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
848c7df200
commit
2504fefa66
@@ -256,6 +256,24 @@ void AutoInterface::loop() {
|
||||
// Process incoming data packets
|
||||
process_data();
|
||||
|
||||
// Periodic stats heartbeat — visibility into TX/RX during peer
|
||||
// discovery debugging. Without this it's hard to tell whether
|
||||
// pyxis is sending at all, sending and getting blocked at the
|
||||
// AP, or sending fine but rejecting the responses.
|
||||
if (now - _last_stats_log >= 10.0) {
|
||||
_last_stats_log = now;
|
||||
char buf[160];
|
||||
snprintf(buf, sizeof(buf),
|
||||
"AutoInterface: stats announce_tx=%lu tx_fail=%lu disc_rx=%lu disc_self=%lu data_rx=%lu peers=%u",
|
||||
(unsigned long)_stat_announce_sent,
|
||||
(unsigned long)_stat_announce_send_fail,
|
||||
(unsigned long)_stat_discovery_rx,
|
||||
(unsigned long)_stat_discovery_rx_self,
|
||||
(unsigned long)_stat_data_rx,
|
||||
(unsigned)_peers.size());
|
||||
INFO(buf);
|
||||
}
|
||||
|
||||
// Check multicast echo timeout
|
||||
check_echo_timeout();
|
||||
|
||||
@@ -714,27 +732,37 @@ bool AutoInterface::setup_discovery_socket() {
|
||||
_if_index = netif_get_index(nif);
|
||||
INFO("AutoInterface: Using interface index " + std::to_string(_if_index) + " for multicast");
|
||||
|
||||
// Join the IPv6 multicast group using standard socket API
|
||||
// This properly links the socket to receive multicast packets
|
||||
// Join the IPv6 multicast group via standard socket API. On
|
||||
// ESP-IDF this returns success but in practice doesn't always
|
||||
// push the multicast hash into the WiFi MAC filter — incoming
|
||||
// multicast frames get silently dropped at L2. Symptoms: pyxis
|
||||
// sends fine, but its discovery socket never sees its own
|
||||
// echo or any peer's announces. Workaround: ALSO call lwIP's
|
||||
// mld6_joingroup_netif() directly, which goes through the
|
||||
// netif's igmp_mac_filter and updates the chip filter.
|
||||
struct ipv6_mreq mreq;
|
||||
memcpy(&mreq.ipv6mr_multiaddr, &_multicast_address, sizeof(_multicast_address));
|
||||
mreq.ipv6mr_interface = _if_index;
|
||||
|
||||
if (setsockopt(_discovery_socket, IPPROTO_IPV6, IPV6_JOIN_GROUP, &mreq, sizeof(mreq)) < 0) {
|
||||
WARNING("AutoInterface: Failed to join multicast group via setsockopt (errno=" +
|
||||
std::to_string(errno) + "), trying mld6 API");
|
||||
// Fallback to lwIP mld6 API
|
||||
ip6_addr_t mcast_addr;
|
||||
memcpy(&mcast_addr.addr, &_multicast_address, sizeof(_multicast_address));
|
||||
err_t err = mld6_joingroup_netif(nif, &mcast_addr);
|
||||
if (err == ERR_OK) {
|
||||
INFO("AutoInterface: Joined IPv6 multicast group via mld6 API: " + _multicast_address_str);
|
||||
} else {
|
||||
WARNING("AutoInterface: mld6_joingroup failed (err=" + std::to_string(err) +
|
||||
") - discovery may not work");
|
||||
}
|
||||
} else {
|
||||
bool joined_setsockopt = (setsockopt(_discovery_socket, IPPROTO_IPV6,
|
||||
IPV6_JOIN_GROUP, &mreq, sizeof(mreq)) == 0);
|
||||
if (joined_setsockopt) {
|
||||
INFO("AutoInterface: Joined IPv6 multicast group via setsockopt: " + _multicast_address_str);
|
||||
} else {
|
||||
WARNING("AutoInterface: setsockopt IPV6_JOIN_GROUP failed (errno=" +
|
||||
std::to_string(errno) + ")");
|
||||
}
|
||||
|
||||
// Always call mld6_joingroup_netif() too — on ESP-IDF, this is
|
||||
// what actually programs the WiFi multicast filter. Joining
|
||||
// twice on the netif is idempotent (refcounted internally).
|
||||
ip6_addr_t mcast_addr;
|
||||
memcpy(&mcast_addr.addr, &_multicast_address, sizeof(_multicast_address));
|
||||
err_t err = mld6_joingroup_netif(nif, &mcast_addr);
|
||||
if (err == ERR_OK) {
|
||||
INFO("AutoInterface: mld6_joingroup_netif OK on " + std::string(nif->name, 2));
|
||||
} else {
|
||||
WARNING("AutoInterface: mld6_joingroup_netif failed (err=" + std::to_string(err) + ")");
|
||||
}
|
||||
|
||||
// Set multicast interface for outgoing packets (critical for multicast to reach other hosts!)
|
||||
@@ -744,6 +772,16 @@ bool AutoInterface::setup_discovery_socket() {
|
||||
} else {
|
||||
DEBUG("AutoInterface: Set IPV6_MULTICAST_IF to interface " + std::to_string(_if_index));
|
||||
}
|
||||
|
||||
// Enable multicast loopback so we receive our own echoes — the
|
||||
// upstream "carrier" check expects to see them and goes
|
||||
// "carrier lost" otherwise. ESP-IDF lwIP defaults this off.
|
||||
int loop = 1;
|
||||
if (setsockopt(_discovery_socket, IPPROTO_IPV6, IPV6_MULTICAST_LOOP,
|
||||
&loop, sizeof(loop)) < 0) {
|
||||
DEBUG("AutoInterface: IPV6_MULTICAST_LOOP not supported (errno=" +
|
||||
std::to_string(errno) + ") — own-echo timeout will fire on isolated networks");
|
||||
}
|
||||
} else {
|
||||
WARNING("AutoInterface: Could not find station netif for multicast join");
|
||||
}
|
||||
@@ -873,8 +911,10 @@ void AutoInterface::send_announce() {
|
||||
ssize_t sent = sendto(_discovery_socket, _discovery_token.data(), _discovery_token.size(), 0,
|
||||
(struct sockaddr*)&dest_addr, sizeof(dest_addr));
|
||||
if (sent > 0) {
|
||||
_stat_announce_sent++;
|
||||
DEBUG("AutoInterface: Sent discovery announce (" + std::to_string(sent) + " bytes) to " + _multicast_address_str);
|
||||
} else {
|
||||
_stat_announce_send_fail++;
|
||||
WARNING("AutoInterface: Failed to send discovery announce (errno=" + std::to_string(errno) + ")");
|
||||
}
|
||||
}
|
||||
@@ -902,9 +942,20 @@ void AutoInterface::process_discovery() {
|
||||
// Hot path - no logging to avoid heap allocation on every packet
|
||||
|
||||
while (len > 0) {
|
||||
_stat_discovery_rx++;
|
||||
// Convert source address to COMPRESSED string format (match Python)
|
||||
std::string src_str = ipv6_to_compressed_string((const uint8_t*)&src_addr.sin6_addr);
|
||||
|
||||
// Self-echo bookkeeping (sender == us → multicast loopback works)
|
||||
if (src_str == _link_local_address_str) {
|
||||
_stat_discovery_rx_self++;
|
||||
_last_multicast_echo = RNS::Utilities::OS::time();
|
||||
if (!_initial_echo_received) {
|
||||
_initial_echo_received = true;
|
||||
INFO("AutoInterface: Initial multicast self-echo received");
|
||||
}
|
||||
}
|
||||
|
||||
// Verify the peering hash (full TOKEN_SIZE = 32 bytes)
|
||||
Bytes combined;
|
||||
combined.append((const uint8_t*)_group_id.c_str(), _group_id.length());
|
||||
@@ -912,10 +963,18 @@ void AutoInterface::process_discovery() {
|
||||
Bytes expected_hash = Identity::full_hash(combined);
|
||||
|
||||
// Compare received token with expected (full TOKEN_SIZE = 32 bytes)
|
||||
if (len >= (ssize_t)TOKEN_SIZE && memcmp(recv_buffer, expected_hash.data(), TOKEN_SIZE) == 0) {
|
||||
bool token_match = (len >= (ssize_t)TOKEN_SIZE
|
||||
&& memcmp(recv_buffer, expected_hash.data(), TOKEN_SIZE) == 0);
|
||||
if (token_match) {
|
||||
// Valid peer - use IPv6Address (IPAddress is IPv4-only!)
|
||||
IPv6Address remoteIP((const uint8_t*)&src_addr.sin6_addr);
|
||||
add_or_refresh_peer(remoteIP, RNS::Utilities::OS::time());
|
||||
} else if (src_str != _link_local_address_str) {
|
||||
// Peer's hash mismatched ours — log once per source so the
|
||||
// user can see "we got a packet but couldn't validate" cases
|
||||
// (group_id mismatch / scope-suffix mismatch / wrong size).
|
||||
WARNINGF("AutoInterface: Discovery RX from %s len=%d but peering hash mismatched (token check failed)",
|
||||
src_str.c_str(), (int)len);
|
||||
}
|
||||
|
||||
// Try to receive more
|
||||
@@ -937,6 +996,7 @@ void AutoInterface::process_data() {
|
||||
(struct sockaddr*)&src_addr, &src_len);
|
||||
|
||||
while (len > 0) {
|
||||
_stat_data_rx++;
|
||||
_buffer.clear();
|
||||
_buffer.append(recv_buffer, len);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user