use std::sync::Arc;
use std::time::Duration;
use dig_ip::LocalStack;
use dig_nat::config::LocalIdentity;
use dig_nat::dialer::{HappyEyeballsConfig, MtlsDialer};
use dig_nat::identity::peer_id_from_leaf_cert_der;
use dig_nat::method::{MethodOutcome, TraversalKind};
use dig_nat::mux::{
AvailabilityAnswer, AvailabilityItem, AvailabilityRequest, AvailabilityResponse,
};
use dig_nat::peer::PeerTarget;
use dig_nat::strategy::Dialer;
use dig_nat::{PeerId, PeerSession};
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
use rustls::ServerConfig;
use tokio::io::AsyncWriteExt;
use tokio::net::TcpListener;
use tokio_rustls::TlsAcceptor;
fn gen() -> (Vec<u8>, Vec<u8>, PeerId) {
let c = rcgen::generate_simple_self_signed(vec!["peer.dig".into()]).unwrap();
let cert = c.cert.der().to_vec();
let key = c.key_pair.serialize_der();
let id = peer_id_from_leaf_cert_der(&cert).unwrap();
(cert, key, id)
}
async fn spawn_mtls_server(server_cert: Vec<u8>, server_key: Vec<u8>) -> std::net::SocketAddr {
let cfg = ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(
vec![CertificateDer::from(server_cert)],
PrivateKeyDer::try_from(server_key).unwrap(),
)
.unwrap();
let acceptor = TlsAcceptor::from(Arc::new(cfg));
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
if let Ok((tcp, _)) = listener.accept().await {
if let Ok(tls) = acceptor.accept(tcp).await {
let mut session = PeerSession::server(tls);
while let Some(mut s) = session.accept_stream().await {
tokio::spawn(async move {
if let Ok(req) = AvailabilityRequest::decode(&mut s).await {
let resp = AvailabilityResponse {
items: req
.items
.iter()
.map(|_| AvailabilityAnswer {
available: true,
roots: None,
total_length: Some(123),
chunk_count: Some(1),
complete: Some(true),
})
.collect(),
};
let _ = s.write_all(&resp.encode()).await;
let _ = s.shutdown().await;
}
});
}
}
}
});
addr
}
fn local_identity() -> LocalIdentity {
let (cert, key, _) = gen();
LocalIdentity::from_der(cert, key).unwrap()
}
#[tokio::test]
async fn dial_success_verifies_identity_and_muxes() {
let (scert, skey, server_id) = gen();
let addr = spawn_mtls_server(scert, skey).await;
let dialer = MtlsDialer::new(local_identity());
let peer = PeerTarget::with_addr(server_id, addr, "DIG_MAINNET");
let outcome = MethodOutcome::single(TraversalKind::Direct, addr);
let mut conn = dialer.dial(&peer, &outcome).await.expect("dial succeeds");
assert_eq!(
conn.peer_id, server_id,
"verified identity == server cert id"
);
assert_eq!(conn.method, TraversalKind::Direct);
let resp = conn
.query_availability(vec![AvailabilityItem {
store_id: "aa".repeat(32),
root: None,
retrieval_key: None,
}])
.await
.expect("availability over mTLS");
assert_eq!(resp.items.len(), 1);
assert!(resp.items[0].available);
assert_eq!(resp.items[0].total_length, Some(123));
}
#[tokio::test]
async fn dial_rejects_wrong_peer_id() {
let (scert, skey, _server_id) = gen();
let addr = spawn_mtls_server(scert, skey).await;
let dialer = MtlsDialer::new(local_identity());
let wrong = PeerId::from_bytes([0x7fu8; 32]);
let peer = PeerTarget::with_addr(wrong, addr, "DIG_MAINNET");
let outcome = MethodOutcome::single(TraversalKind::Direct, addr);
let err = dialer.dial(&peer, &outcome).await.unwrap_err();
assert_eq!(err.kind, TraversalKind::Direct);
assert!(
err.reason.contains("mtls handshake") || err.reason.contains("peer_id"),
"handshake rejected on identity mismatch, got: {}",
err.reason
);
}
#[tokio::test]
async fn dial_falls_back_from_unreachable_ipv6_to_ipv4() {
let (scert, skey, server_id) = gen();
let addr = spawn_mtls_server(scert, skey).await;
let dialer = MtlsDialer::new(local_identity())
.with_happy_eyeballs(HappyEyeballsConfig {
per_attempt_timeout: Duration::from_secs(2),
stagger: Duration::from_millis(30),
})
.with_local_stack(LocalStack::from_flags(true, true));
let unreachable_v6: std::net::SocketAddr = "[2001:db8::1]:9".parse().unwrap();
let peer = PeerTarget::with_addrs(server_id, vec![unreachable_v6, addr], "DIG_MAINNET");
let outcome = MethodOutcome::candidates(TraversalKind::Direct, peer.direct_addrs().to_vec());
let conn = dialer
.dial(&peer, &outcome)
.await
.expect("falls back to the reachable IPv4 candidate");
assert_eq!(
conn.peer_id, server_id,
"mTLS identity verified over IPv4 fallback"
);
assert_eq!(
conn.remote_addr, addr,
"connected over the IPv4 loopback address"
);
assert!(conn.remote_addr.is_ipv4());
}
#[tokio::test]
async fn dial_v4_only_host_to_v6_only_peer_is_clean_no_common_family() {
let dialer =
MtlsDialer::new(local_identity()).with_local_stack(LocalStack::from_flags(false, true));
let v6_only: std::net::SocketAddr = "[2001:db8::1]:9".parse().unwrap();
let peer = PeerTarget::with_addr(PeerId::from_bytes([1u8; 32]), v6_only, "DIG_MAINNET");
let outcome = MethodOutcome::single(TraversalKind::Direct, v6_only);
let err = dialer.dial(&peer, &outcome).await.unwrap_err();
assert_eq!(err.kind, TraversalKind::Direct);
assert!(
err.reason.contains("no common address family"),
"reports a clean no-common-family error, got: {}",
err.reason
);
assert!(!err.reason.contains("tcp connect"));
}
#[tokio::test]
async fn dial_tcp_refused_is_clean_error() {
let dialer = MtlsDialer::new(local_identity());
let peer = PeerTarget::with_addr(
PeerId::from_bytes([1u8; 32]),
"127.0.0.1:9".parse().unwrap(),
"DIG_MAINNET",
);
let outcome = MethodOutcome::single(TraversalKind::Direct, "127.0.0.1:9".parse().unwrap());
let err = dialer.dial(&peer, &outcome).await.unwrap_err();
assert_eq!(err.kind, TraversalKind::Direct);
assert!(err.reason.contains("tcp connect"));
}