porta-rs 0.1.1

Zero-trust CGNAT bypass via WireGuard tunnel
use anyhow::{Context, Result};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
use std::net::SocketAddr;
use std::sync::Arc;
use parking_lot::RwLock;

use crate::config::PortMapping;

/// TCP connection statistics
#[derive(Debug, Default, Clone)]
pub struct TcpStats {
    pub connections_active: u64,
    pub connections_total: u64,
    pub bytes_sent: u64,
    pub bytes_received: u64,
}

/// TCP forwarder
pub struct TcpForwarder {
    stats: Arc<RwLock<TcpStats>>,
}

impl TcpForwarder {
    pub fn new() -> Self {
        Self {
            stats: Arc::new(RwLock::new(TcpStats::default())),
        }
    }

    /// Start forwarding for a port mapping
    pub async fn start_forwarding(&self, mapping: &PortMapping) -> Result<()> {
        let listener = TcpListener::bind(format!("0.0.0.0:{}", mapping.remote_port))
            .await
            .context(format!("Failed to bind TCP port {}", mapping.remote_port))?;

        tracing::info!(
            "TCP forwarder listening on :{} -> localhost:{}",
            mapping.remote_port,
            mapping.local_port
        );

        let stats = self.stats.clone();
        let local_port = mapping.local_port;

        tokio::spawn(async move {
            loop {
                match listener.accept().await {
                    Ok((stream, addr)) => {
                        let stats = stats.clone();
                        tokio::spawn(async move {
                            if let Err(e) = Self::handle_connection(stream, addr, local_port, stats).await {
                                tracing::error!("TCP connection error: {}", e);
                            }
                        });
                    }
                    Err(e) => {
                        tracing::error!("Failed to accept TCP connection: {}", e);
                    }
                }
            }
        });

        Ok(())
    }

    /// Handle a single TCP connection
    async fn handle_connection(
        client_stream: TcpStream,
        client_addr: SocketAddr,
        local_port: u16,
        stats: Arc<RwLock<TcpStats>>,
    ) -> Result<()> {
        // Connect to local service
        let local_addr = format!("127.0.0.1:{}", local_port);
        let local_stream = TcpStream::connect(&local_addr)
            .await
            .context(format!("Failed to connect to {}", local_addr))?;

        tracing::debug!("TCP connection: {} -> :{} -> {}", client_addr, local_port, local_addr);

        // Update stats
        {
            let mut s = stats.write();
            s.connections_active += 1;
            s.connections_total += 1;
        }

        // Bidirectional copy
        let (mut client_read, mut client_write) = client_stream.into_split();
        let (mut local_read, mut local_write) = local_stream.into_split();

        let stats_send = stats.clone();
        let stats_recv = stats.clone();

        let send_task = tokio::spawn(async move {
            let mut buf = vec![0u8; 8192];
            let mut total = 0u64;
            loop {
                match client_read.read(&mut buf).await {
                    Ok(0) => break,
                    Ok(n) => {
                        if local_write.write_all(&buf[..n]).await.is_err() {
                            break;
                        }
                        total += n as u64;
                    }
                    Err(_) => break,
                }
            }
            let mut s = stats_send.write();
            s.bytes_sent += total;
            s.connections_active = s.connections_active.saturating_sub(1);
        });

        let recv_task = tokio::spawn(async move {
            let mut buf = vec![0u8; 8192];
            let mut total = 0u64;
            loop {
                match local_read.read(&mut buf).await {
                    Ok(0) => break,
                    Ok(n) => {
                        if client_write.write_all(&buf[..n]).await.is_err() {
                            break;
                        }
                        total += n as u64;
                    }
                    Err(_) => break,
                }
            }
            let mut s = stats_recv.write();
            s.bytes_received += total;
        });

        tokio::select! {
            _ = send_task => {},
            _ = recv_task => {},
        }

        Ok(())
    }

    /// Get current statistics
    pub fn stats(&self) -> TcpStats {
        self.stats.read().clone()
    }
}

impl Default for TcpForwarder {
    fn default() -> Self {
        Self::new()
    }
}