use std::io::{BufRead, BufReader, Write};
use std::process::{Command, Stdio};
use std::sync::mpsc::{self, Receiver, Sender};
use std::time::Duration;
use rns_net::{AnnouncedIdentity, DestHash, PacketHash};
use rns_net::{Callbacks, InterfaceConfig, NodeConfig, RnsNode, TcpClientConfig, MODE_FULL};
const KNOWN_DESTINATIONS_TTL: Duration = Duration::from_secs(48 * 60 * 60);
struct TestCallbacks {
announce_tx: Sender<(DestHash, u8)>,
}
impl Callbacks for TestCallbacks {
fn on_announce(&mut self, announced: AnnouncedIdentity) {
let _ = self.announce_tx.send((announced.dest_hash, announced.hops));
}
fn on_path_updated(&mut self, _: DestHash, _: u8) {}
fn on_local_delivery(&mut self, _: DestHash, _: Vec<u8>, _: PacketHash) {}
}
fn rns_available() -> bool {
Command::new("python3")
.args(["-c", "import RNS; print('ok')"])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
#[test]
fn python_announce_received() {
if !rns_available() {
eprintln!("Skipping: Python RNS not available");
return;
}
let python_script = r#"
import sys, os, time, signal, json, tempfile, socket
# Find a free port
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('127.0.0.1', 0))
port = sock.getsockname()[1]
sock.close()
# Write config
config_dir = tempfile.mkdtemp()
config_path = os.path.join(config_dir, "config")
with open(config_path, "w") as f:
f.write(f"""[reticulum]
enable_transport = false
share_instance = yes
[interfaces]
[[TCP Server Interface]]
type = TCPServerInterface
interface_enabled = true
listen_ip = 127.0.0.1
listen_port = {port}
""")
import RNS
reticulum = RNS.Reticulum(configdir=config_dir)
identity = RNS.Identity()
destination = RNS.Destination(identity, RNS.Destination.IN, RNS.Destination.SINGLE, "interop", "test")
dest_hash = destination.hash.hex()
print(json.dumps({"port": port, "dest_hash": dest_hash}), flush=True)
# Wait for Rust node to connect before announcing
sys.stdin.readline()
time.sleep(0.5)
destination.announce()
# Wait for signal
signal.signal(signal.SIGTERM, lambda *a: sys.exit(0))
try:
while True:
time.sleep(1)
except (KeyboardInterrupt, SystemExit):
pass
"#;
let mut child = Command::new("python3")
.args(["-c", python_script])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("Failed to start Python");
let stdout = child.stdout.take().unwrap();
let mut reader = BufReader::new(stdout);
let mut line = String::new();
reader
.read_line(&mut line)
.expect("Failed to read from Python");
let info: serde_json::Value = serde_json::from_str(line.trim()).expect("Failed to parse JSON");
let port = info["port"].as_u64().unwrap() as u16;
let expected_dest_hash_hex = info["dest_hash"].as_str().unwrap().to_string();
eprintln!(
"Python server on port {}, dest_hash={}",
port, expected_dest_hash_hex
);
let (announce_tx, announce_rx): (Sender<(DestHash, u8)>, Receiver<(DestHash, u8)>) =
mpsc::channel();
let node = RnsNode::start(
NodeConfig {
panic_on_interface_error: false,
transport_enabled: false,
identity: None,
interfaces: vec![InterfaceConfig {
name: String::new(),
type_name: "TCPClientInterface".to_string(),
config_data: Box::new(TcpClientConfig {
name: "interop-tcp".into(),
target_host: "127.0.0.1".into(),
target_port: port,
reconnect_wait: Duration::from_millis(500),
max_reconnect_tries: Some(3),
connect_timeout: Duration::from_secs(5),
..Default::default()
}),
mode: MODE_FULL,
ingress_control: rns_core::transport::types::IngressControlConfig::enabled(),
ifac: None,
discovery: None,
}],
share_instance: false,
instance_name: "default".into(),
shared_instance_port: 37428,
rpc_port: 0,
cache_dir: None,
management: Default::default(),
probe_port: None,
probe_addrs: vec![],
probe_protocol: rns_core::holepunch::ProbeProtocol::Rnsp,
device: None,
hooks: Vec::new(),
discover_interfaces: false,
discovery_required_value: None,
respond_to_probes: false,
prefer_shorter_path: false,
max_paths_per_destination: 1,
packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
max_path_destinations: usize::MAX,
max_tunnel_destinations_total: usize::MAX,
known_destinations_ttl: KNOWN_DESTINATIONS_TTL,
known_destinations_max_entries: 8192,
announce_table_ttl: std::time::Duration::from_secs(
rns_core::constants::ANNOUNCE_TABLE_TTL as u64,
),
announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
driver_event_queue_capacity: rns_net::event::DEFAULT_EVENT_QUEUE_CAPACITY,
interface_writer_queue_capacity:
rns_net::interface::DEFAULT_ASYNC_WRITER_QUEUE_CAPACITY,
announce_sig_cache_enabled: true,
announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
announce_sig_cache_ttl: std::time::Duration::from_secs(
rns_core::constants::ANNOUNCE_SIG_CACHE_TTL as u64,
),
registry: None,
#[cfg(feature = "rns-hooks")]
provider_bridge: None,
},
Box::new(TestCallbacks { announce_tx }),
)
.expect("Failed to start Rust node");
std::thread::sleep(Duration::from_secs(2));
if let Some(ref mut stdin) = child.stdin {
let _ = writeln!(stdin, "go");
}
let result = announce_rx.recv_timeout(Duration::from_secs(10));
node.shutdown();
let _ = child.kill();
let _ = child.wait();
match result {
Ok((dest_hash, hops)) => {
let received_hex: String = dest_hash.0.iter().map(|b| format!("{:02x}", b)).collect();
eprintln!("Received announce: dest={} hops={}", received_hex, hops);
assert_eq!(received_hex, expected_dest_hash_hex);
}
Err(_) => {
panic!("Timed out waiting for announce from Python");
}
}
}