Implement the `tcp-tls` protocol crate and generic tunnel codec abstractions alongside `wg-quic` to circumvent deep packet inspection (DPI/TSPU), UDP blocking, and QUIC throttling. Traffic uses standard TCP on port 443 with TLS 1.3 framing, looking identical to regular HTTPS traffic to network observers. Architectural highlights: - TunnelCodec & GenericTunnelPlugin: Provide an extensible plugin abstraction in `crates/tsunagi/src/dataplane/tunnel.rs` that encapsulates background task lifecycles, per-peer channels, and sink dispatch, so new transport plugins only define cryptographic transformations and handshake payloads. - MultiPacketTransport & Protocol Priority Selection: Support multiple simultaneous transports in `AgentConfig` and dynamically dispatch `open()` calls by protocol identifier. In `ensure_links`, choose the highest-priority mutually supported protocol per peer to avoid duplicate links. - TLS 1.3 & Certificate Pinning: Generate self-signed Ed25519 X.509 certificates derived from the persistent `DeviceIdentity` secret. Clients pin peer certificates using `PinnedEndpointVerifier` against `iroh::EndpointId`. - End-to-end Encryption: `PeerCipher` uses X25519 static-static ECDH, HKDF-SHA256, and ChaCha20-Poly1305 with incrementing 64-bit nonces. Relayed traffic across intermediate nodes remains confidential and tamper-proof. - Privilege handling: Default to TCP port 443 with fallback to 8443 on `PermissionDenied`, providing an actionable message recommending: `sudo setcap cap_net_admin,cap_net_bind_service+p <path>`. Validation on Linux: - `cargo fmt --all -- --check` passed. - `cargo clippy --locked --workspace --all-targets -- -D warnings` passed. - `cargo test --locked --workspace --all-targets` passed (all workspace unit, integration, CLI, and new `tsunagi-tcp-tls` tests passed).
74 lines
2.5 KiB
Rust
74 lines
2.5 KiB
Rust
#![allow(missing_docs)]
|
|
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
|
|
|
use tsunagi_tcp_tls::crypto::PeerCipher;
|
|
use x25519_dalek::{PublicKey, StaticSecret};
|
|
|
|
#[test]
|
|
fn test_peer_cipher_bidirectional_roundtrip() {
|
|
let peer_a_id = iroh::SecretKey::generate().public();
|
|
let peer_b_id = iroh::SecretKey::generate().public();
|
|
|
|
let secret_a = StaticSecret::from([10u8; 32]);
|
|
let public_a = PublicKey::from(&secret_a);
|
|
|
|
let secret_b = StaticSecret::from([20u8; 32]);
|
|
let public_b = PublicKey::from(&secret_b);
|
|
|
|
let cipher_a = PeerCipher::new(peer_a_id, peer_b_id, &secret_a, &public_b);
|
|
let cipher_b = PeerCipher::new(peer_b_id, peer_a_id, &secret_b, &public_a);
|
|
|
|
// A -> B
|
|
let msg_from_a = b"hello from node A to node B over Noise E2EE";
|
|
let encrypted_by_a = cipher_a
|
|
.encrypt(msg_from_a)
|
|
.expect("encryption should succeed");
|
|
let decrypted_by_b = cipher_b
|
|
.decrypt(&encrypted_by_a)
|
|
.expect("decryption should succeed");
|
|
assert_eq!(&decrypted_by_b[..], msg_from_a);
|
|
|
|
// B -> A
|
|
let msg_from_b = b"hello back from node B to node A";
|
|
let encrypted_by_b = cipher_b
|
|
.encrypt(msg_from_b)
|
|
.expect("encryption should succeed");
|
|
let decrypted_by_a = cipher_a
|
|
.decrypt(&encrypted_by_b)
|
|
.expect("decryption should succeed");
|
|
assert_eq!(&decrypted_by_a[..], msg_from_b);
|
|
|
|
// Sequence nonces increment
|
|
let msg2 = b"packet 2";
|
|
let enc2 = cipher_a.encrypt(msg2).expect("encryption should succeed");
|
|
assert_ne!(encrypted_by_a[..8], enc2[..8], "nonces should differ");
|
|
let dec2 = cipher_b.decrypt(&enc2).expect("decryption should succeed");
|
|
assert_eq!(&dec2[..], msg2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_tampered_payload_rejected() {
|
|
let peer_a_id = iroh::SecretKey::generate().public();
|
|
let peer_b_id = iroh::SecretKey::generate().public();
|
|
|
|
let secret_a = StaticSecret::from([10u8; 32]);
|
|
let public_a = PublicKey::from(&secret_a);
|
|
|
|
let secret_b = StaticSecret::from([20u8; 32]);
|
|
let public_b = PublicKey::from(&secret_b);
|
|
|
|
let cipher_a = PeerCipher::new(peer_a_id, peer_b_id, &secret_a, &public_b);
|
|
let cipher_b = PeerCipher::new(peer_b_id, peer_a_id, &secret_b, &public_a);
|
|
|
|
let encrypted = cipher_a.encrypt(b"secret payload").expect("encrypt");
|
|
// Corrupt one byte of ciphertext
|
|
let len = encrypted.len();
|
|
let mut modified = encrypted.to_vec();
|
|
modified[len - 1] ^= 0xff;
|
|
|
|
assert!(
|
|
cipher_b.decrypt(&modified).is_err(),
|
|
"corrupted ciphertext must fail AEAD verification"
|
|
);
|
|
}
|