Add tcp-tls transport and tunnel protocol disguised as HTTPS
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).
This commit is contained in:
1 parent
db9015b26c
commit
fac3bd7bb9
20 files changed
+2195
-36
No files matched your search
@@ -23,6 +23,9 @@ use tsunagi::iroh_types::EndpointAddr;
|
||||
use tsunagi::overlay::{MemoryTunFactory, TunFactory};
|
||||
use tsunagi::state::Ipv4Range;
|
||||
use tsunagi::{Agent, NetworkId};
|
||||
use tsunagi_tcp_tls::{
|
||||
TCP_TLS_PROTOCOL, TCP_TLS_VERSION, TcpTlsCodec, TcpTlsConfig, TcpTlsPlugin, TcpTlsTransport,
|
||||
};
|
||||
use tsunagi_wg_quic::{WireguardConfig, WireguardPlugin};
|
||||
|
||||
/// A small agent for private mesh networks.
|
||||
@@ -1164,13 +1167,21 @@ struct ProtocolSpec {
|
||||
}
|
||||
|
||||
/// Every protocol this build has.
|
||||
const PROTOCOLS: &[ProtocolSpec] = &[ProtocolSpec {
|
||||
name: tsunagi_wg_quic::WIREGUARD_PROTOCOL,
|
||||
version: tsunagi_wg_quic::ANNOUNCEMENT_VERSION,
|
||||
summary: "WireGuard's cryptography carried in iroh's QUIC datagrams, so it \
|
||||
crosses NAT and survives where plain WireGuard is blocked",
|
||||
options: WireguardPlugin::OPTIONS,
|
||||
}];
|
||||
const PROTOCOLS: &[ProtocolSpec] = &[
|
||||
ProtocolSpec {
|
||||
name: tsunagi_wg_quic::WIREGUARD_PROTOCOL,
|
||||
version: tsunagi_wg_quic::ANNOUNCEMENT_VERSION,
|
||||
summary: "WireGuard's cryptography carried in iroh's QUIC datagrams, so it \
|
||||
crosses NAT and survives where plain WireGuard is blocked",
|
||||
options: WireguardPlugin::OPTIONS,
|
||||
},
|
||||
ProtocolSpec {
|
||||
name: TCP_TLS_PROTOCOL,
|
||||
version: TCP_TLS_VERSION,
|
||||
summary: "Noise E2EE carried over TCP TLS 1.3 (port 443) disguised as HTTPS traffic",
|
||||
options: TcpTlsCodec::OPTIONS,
|
||||
},
|
||||
];
|
||||
|
||||
/// One `-o` setting, and the protocol it was aimed at.
|
||||
struct Setting {
|
||||
@@ -3375,7 +3386,7 @@ async fn up(args: UpArgs) -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
let discovery: Arc<dyn NetworkDiscovery> =
|
||||
Arc::new(CompositeDiscovery::new([
|
||||
Arc::new(StaticBootstrap::new(bootstrap)) as Arc<dyn NetworkDiscovery>,
|
||||
Arc::new(StaticBootstrap::new(bootstrap.clone())) as Arc<dyn NetworkDiscovery>,
|
||||
]));
|
||||
|
||||
let mut config = AgentConfig::new(paths.clone())
|
||||
@@ -3436,6 +3447,7 @@ async fn up(args: UpArgs) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// The interface belongs to the agent, so it is configured once whatever
|
||||
// was selected to carry traffic over it.
|
||||
let mut wireguard = None;
|
||||
let mut tcp_tls_transport = None;
|
||||
if !wanted.is_empty() {
|
||||
let tun_factory: Arc<dyn TunFactory> = if args.no_tun {
|
||||
Arc::new(MemoryTunFactory::new())
|
||||
@@ -3459,6 +3471,32 @@ async fn up(args: UpArgs) -> Result<(), Box<dyn std::error::Error>> {
|
||||
config = config.with_plugin(plugin.clone() as Arc<dyn IpPlugin>);
|
||||
wireguard = Some(plugin);
|
||||
}
|
||||
TCP_TLS_PROTOCOL => {
|
||||
let tls_cfg = TcpTlsConfig::new();
|
||||
let tls_cfg = TcpTlsCodec::configure(tls_cfg, &options)?;
|
||||
let identity = tsunagi::storage::StateStore::open(paths.state_db())?
|
||||
.load_or_create_device_identity()?;
|
||||
let transport = Arc::new(
|
||||
TcpTlsTransport::bind(&identity, tls_cfg.port)
|
||||
.await
|
||||
.map_err(|err| err.to_string())?,
|
||||
);
|
||||
for ep_addr in &bootstrap {
|
||||
for ip in ep_addr.ip_addrs() {
|
||||
transport.set_peer_addr(ep_addr.id, *ip);
|
||||
}
|
||||
}
|
||||
let codec = TcpTlsCodec::new(identity.endpoint_id(), Arc::clone(&transport));
|
||||
let plugin = Arc::new(TcpTlsPlugin::new(codec));
|
||||
config = config
|
||||
.with_plugin(plugin as Arc<dyn IpPlugin>)
|
||||
.with_custom_transport(
|
||||
TCP_TLS_PROTOCOL,
|
||||
Arc::clone(&transport)
|
||||
as Arc<dyn tsunagi::dataplane::transport::PacketTransport>,
|
||||
);
|
||||
tcp_tls_transport = Some(transport);
|
||||
}
|
||||
other => return Err(format!("`{other}` is listed but not built in").into()),
|
||||
}
|
||||
}
|
||||
@@ -3495,6 +3533,22 @@ async fn up(args: UpArgs) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// is never dropped without being closed.
|
||||
let mut events = agent.subscribe();
|
||||
|
||||
if let Some(transport) = tcp_tls_transport {
|
||||
let agent_inbound = agent.clone();
|
||||
tokio::spawn(async move {
|
||||
transport
|
||||
.accept_loop(move |inbound| {
|
||||
let agent = agent_inbound.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = agent.install_inbound_link(inbound).await {
|
||||
tracing::debug!(%err, "failed to install inbound tcp-tls link");
|
||||
}
|
||||
});
|
||||
})
|
||||
.await;
|
||||
});
|
||||
}
|
||||
|
||||
println!("tsunagi is up");
|
||||
println!(" endpoint id {}", agent.endpoint_id());
|
||||
println!(" hostname {}", agent.hostname());
|
||||
|
||||
Reference in new issue
Block a user