use std::sync::Arc;
use crate::network::{ListenOpts, NetworkProvider, PeerAddr, TailscalePeerIdentity};
use super::{resolve_dial_addr, RawIncoming, RawListener, RawTransport, TransportError};
pub struct TcpTransport<N: NetworkProvider> {
network: Arc<N>,
}
impl<N: NetworkProvider + 'static> TcpTransport<N> {
pub fn new(network: Arc<N>) -> Self {
Self { network }
}
pub async fn listen_opts(
&self,
port: u16,
opts: ListenOpts,
) -> Result<RawListener, TransportError> {
tracing::debug!(port, tls = opts.tls, "tcp: starting listener");
let mut tcp_listener = self
.network
.listen_tcp_opts(port, opts)
.await
.map_err(|e| TransportError::ListenFailed(format!("tcp listen: {e}")))?;
let actual_port = tcp_listener.port;
let (tx, rx) = tokio::sync::mpsc::channel::<RawIncoming>(64);
tokio::spawn(async move {
loop {
match tcp_listener.incoming.recv().await {
Some(incoming) => {
let raw = RawIncoming {
stream: incoming.stream,
remote_addr: incoming.remote_addr,
remote_identity: parse_remote_identity(&incoming.remote_identity),
};
if tx.send(raw).await.is_err() {
tracing::debug!("tcp: listener channel closed");
break;
}
}
None => {
tracing::debug!("tcp: network listener channel closed");
break;
}
}
}
});
Ok(RawListener::new(rx, actual_port))
}
}
impl<N: NetworkProvider + 'static> RawTransport for TcpTransport<N> {
async fn open(
&self,
addr: &PeerAddr,
port: u16,
) -> Result<tokio::net::TcpStream, TransportError> {
let dial_addr = resolve_dial_addr(addr);
tracing::debug!(addr = %dial_addr, port, "tcp: dialing peer");
let stream = self
.network
.dial_tcp(&dial_addr, port)
.await
.map_err(|e| TransportError::ConnectFailed(format!("tcp dial: {e}")))?;
tracing::debug!(addr = %dial_addr, port, "tcp: connected");
Ok(stream)
}
async fn listen(&self, port: u16) -> Result<RawListener, TransportError> {
self.listen_opts(port, ListenOpts::default()).await
}
}
fn parse_remote_identity(raw: &str) -> Option<TailscalePeerIdentity> {
let trimmed = raw.trim();
if trimmed.is_empty() {
return None;
}
match serde_json::from_str::<TailscalePeerIdentity>(trimmed) {
Ok(identity) => Some(identity),
Err(e) => {
tracing::debug!(error = %e, "tcp: unparseable remote identity header; treating as anonymous");
None
}
}
}
#[cfg(test)]
mod unit_tests {
use super::*;
#[test]
fn parse_remote_identity_round_trips_whois_json() {
let json = r#"{"dnsName":"kitchen.tailnet.ts.net","loginName":"alice@example.com","displayName":"Alice","profilePicUrl":"https://p.example/a.png","nodeId":"nABC123.ts-node"}"#;
let identity = parse_remote_identity(json).expect("well-formed WhoIs JSON must parse");
assert_eq!(
identity,
TailscalePeerIdentity {
dns_name: Some("kitchen.tailnet.ts.net".to_string()),
login_name: Some("alice@example.com".to_string()),
display_name: Some("Alice".to_string()),
profile_pic_url: Some("https://p.example/a.png".to_string()),
node_id: Some("nABC123.ts-node".to_string()),
}
);
}
#[test]
fn parse_remote_identity_omits_absent_optional_fields() {
let identity = parse_remote_identity(r#"{"dnsName":"peer.ts.net"}"#)
.expect("partial WhoIs JSON must parse");
assert_eq!(identity.dns_name.as_deref(), Some("peer.ts.net"));
assert_eq!(identity.login_name, None);
assert_eq!(identity.node_id, None);
}
#[test]
fn parse_remote_identity_returns_none_for_malformed_json() {
assert_eq!(parse_remote_identity("peer.tailnet.ts.net"), None);
assert_eq!(parse_remote_identity("{not json"), None);
}
#[test]
fn parse_remote_identity_returns_none_for_empty_input() {
assert_eq!(parse_remote_identity(""), None);
assert_eq!(parse_remote_identity(" "), None);
}
}