Files
pyxis/lib/auto_interface/AutoInterface.h
T
torlando-tech 2504fefa66 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>
2026-05-09 13:29:33 -04:00

184 lines
6.6 KiB
C++

#pragma once
#include "Interface.h"
#include "Identity.h"
#include "Bytes.h"
#include "Type.h"
#include "AutoInterfacePeer.h"
#ifdef ARDUINO
#include <WiFi.h>
#include <WiFiUdp.h>
#include <IPv6Address.h>
#include <lwip/ip6_addr.h>
#include <lwip/netdb.h>
#include <lwip/sockets.h>
#else
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#endif
#include <vector>
#include <deque>
#include <string>
#include <cstdint>
// AutoInterface - automatic peer discovery via IPv6 multicast
// Matches Python RNS AutoInterface behavior for interoperability
class AutoInterface : public RNS::InterfaceImpl {
public:
// Protocol constants (match Python RNS)
static const uint16_t DEFAULT_DISCOVERY_PORT = 29716;
static const uint16_t DEFAULT_DATA_PORT = 42671;
static constexpr const char* DEFAULT_GROUP_ID = "reticulum";
static constexpr double PEERING_TIMEOUT = 22.0; // seconds (matches Python RNS)
static constexpr double ANNOUNCE_INTERVAL = 1.6; // seconds (matches Python RNS)
static constexpr double MCAST_ECHO_TIMEOUT = 6.5; // seconds (matches Python RNS)
static constexpr double REVERSE_PEERING_INTERVAL = ANNOUNCE_INTERVAL * 3.25; // ~5.2 seconds
static constexpr double PEER_JOB_INTERVAL = 4.0; // seconds (matches Python RNS)
static const size_t DEQUE_SIZE = 48; // packet dedup window
static constexpr double DEQUE_TTL = 0.75; // seconds
static const uint32_t BITRATE_GUESS = 10 * 1000 * 1000;
static const uint16_t HW_MTU = 1196;
// Discovery token is full_hash(group_id + link_local_address) = 32 bytes
// Python RNS sends and expects the full 32-byte hash (HASHLENGTH//8 = 256//8 = 32)
static const size_t TOKEN_SIZE = 32;
public:
AutoInterface(const char* name = "AutoInterface");
virtual ~AutoInterface();
// Configuration (call before start())
void set_group_id(const std::string& group_id) { _group_id = group_id; }
void set_discovery_port(uint16_t port) { _discovery_port = port; }
void set_data_port(uint16_t port) { _data_port = port; }
void set_interface_name(const std::string& ifname) { _ifname = ifname; }
// InterfaceImpl overrides
virtual bool start() override;
virtual void stop() override;
virtual void loop() override;
virtual inline std::string toString() const override {
return "AutoInterface[" + _name + "/" + _group_id + "]";
}
// Getters for testing
const RNS::Bytes& get_discovery_token() const { return _discovery_token; }
const RNS::Bytes& get_multicast_address() const { return _multicast_address_bytes; }
size_t peer_count() const { return _peers.size(); }
// Carrier state tracking (matches Python RNS)
bool carrier_changed() {
bool changed = _carrier_changed;
_carrier_changed = false; // Clear flag on read
return changed;
}
void clear_carrier_changed() { _carrier_changed = false; }
bool is_timed_out() const { return _timed_out; }
protected:
virtual void send_outgoing(const RNS::Bytes& data) override;
private:
// Discovery and addressing
void calculate_multicast_address();
void calculate_discovery_token();
bool get_link_local_address();
// Socket operations
bool setup_discovery_socket();
bool setup_unicast_discovery_socket();
bool setup_data_socket();
bool join_multicast_group();
// Main loop operations
void send_announce();
void process_discovery();
void process_unicast_discovery();
void send_reverse_peering();
void reverse_announce(AutoInterfacePeer& peer);
void process_data();
void check_echo_timeout();
void check_link_local_address();
// Peer management
#ifdef ARDUINO
void add_or_refresh_peer(const IPv6Address& addr, double timestamp);
#else
void add_or_refresh_peer(const struct in6_addr& addr, double timestamp);
#endif
void expire_stale_peers();
// Deduplication
bool is_duplicate(const RNS::Bytes& packet);
void add_to_deque(const RNS::Bytes& packet);
void expire_deque_entries();
// Configuration
std::string _group_id = DEFAULT_GROUP_ID;
uint16_t _discovery_port = DEFAULT_DISCOVERY_PORT;
uint16_t _unicast_discovery_port = DEFAULT_DISCOVERY_PORT + 1; // 29717
uint16_t _data_port = DEFAULT_DATA_PORT;
std::string _ifname; // Network interface name (e.g., "eth0", "wlan0")
// Computed values
RNS::Bytes _discovery_token; // 16 bytes
RNS::Bytes _multicast_address_bytes; // 16 bytes (IPv6)
struct in6_addr _multicast_address;
struct in6_addr _link_local_address;
std::string _link_local_address_str;
std::string _multicast_address_str; // For logging
bool _data_socket_ok = false; // Data socket initialized successfully
#ifdef ARDUINO
IPv6Address _link_local_ip; // ESP32: link-local as IPv6Address
IPv6Address _multicast_ip; // ESP32: multicast as IPv6Address
#endif
// Sockets
#ifdef ARDUINO
int _discovery_socket = -1; // Raw socket for IPv6 multicast discovery
int _unicast_discovery_socket = -1; // Raw socket for unicast discovery (reverse peering)
int _data_socket = -1; // Raw socket for IPv6 unicast data (WiFiUDP doesn't support IPv6)
unsigned int _if_index = 0; // Interface index for scope_id
#else
int _discovery_socket = -1;
int _unicast_discovery_socket = -1; // Socket for unicast discovery (reverse peering)
int _data_socket = -1;
unsigned int _if_index = 0; // Interface index for multicast
#endif
// Peers and state
std::vector<AutoInterfacePeer> _peers;
double _last_announce = 0;
double _last_peer_job = 0; // Timestamp of last peer job check
// Echo tracking (matches Python RNS multicast_echoes / initial_echoes)
double _last_multicast_echo = 0.0; // Timestamp of last own echo received
bool _initial_echo_received = false; // True once first echo received
bool _timed_out = false; // Current timeout state
bool _carrier_changed = false; // Flag for Transport layer notification
bool _firewall_warning_logged = false; // Track firewall warning (log once)
// Deduplication: pairs of (packet_hash, timestamp)
struct DequeEntry {
RNS::Bytes hash;
double timestamp;
};
std::deque<DequeEntry> _packet_deque;
// Receive buffer
RNS::Bytes _buffer;
// Diagnostic counters (printed periodically as INFO)
uint32_t _stat_announce_sent = 0;
uint32_t _stat_announce_send_fail = 0;
uint32_t _stat_discovery_rx = 0;
uint32_t _stat_discovery_rx_self = 0;
uint32_t _stat_data_rx = 0;
double _last_stats_log = 0;
};