Skip to main content

termivar_proxy/
relay.rs

1//! Explicit fixed-upstream TCP relay.
2//!
3//! The relay accepts a TCP connection, opens the configured upstream socket,
4//! and copies bytes bidirectionally. It does not parse HTTP `CONNECT`, terminate
5//! TLS, generate certificates, or inspect application traffic.
6
7use std::net::SocketAddr;
8
9use tokio::{
10    io::copy_bidirectional,
11    net::{TcpListener, TcpStream},
12};
13
14/// A bound TCP listener that relays each connection to one explicit upstream.
15#[derive(Debug)]
16pub struct FixedUpstreamTcpRelay {
17    listener: TcpListener,
18    upstream_addr: SocketAddr,
19}
20
21impl FixedUpstreamTcpRelay {
22    /// Binds `listen_addr` and records the explicit fixed upstream.
23    pub async fn bind(listen_addr: SocketAddr, upstream_addr: SocketAddr) -> std::io::Result<Self> {
24        Ok(Self {
25            listener: TcpListener::bind(listen_addr).await?,
26            upstream_addr,
27        })
28    }
29
30    /// Returns the bound listener address.
31    pub fn local_addr(&self) -> std::io::Result<SocketAddr> {
32        self.listener.local_addr()
33    }
34
35    /// Returns the configured fixed upstream.
36    #[must_use]
37    pub fn upstream_addr(&self) -> SocketAddr {
38        self.upstream_addr
39    }
40
41    /// Accepts connections until the listener fails or the task is cancelled.
42    pub async fn run(self) -> std::io::Result<()> {
43        let listen_addr = self.listener.local_addr()?;
44        eprintln!(
45            "fixed-upstream TCP relay listening on {listen_addr}, upstream {}",
46            self.upstream_addr
47        );
48
49        loop {
50            let (downstream, _) = self.listener.accept().await?;
51            let upstream_addr = self.upstream_addr;
52            tokio::spawn(async move {
53                if let Err(error) = relay_connection(downstream, upstream_addr).await {
54                    eprintln!("TCP relay connection failed: {error}");
55                }
56            });
57        }
58    }
59}
60
61async fn relay_connection(
62    mut downstream: TcpStream,
63    upstream_addr: SocketAddr,
64) -> std::io::Result<()> {
65    let mut upstream = TcpStream::connect(upstream_addr).await?;
66    copy_bidirectional(&mut downstream, &mut upstream).await?;
67    Ok(())
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73    use tokio::io::{AsyncReadExt, AsyncWriteExt};
74
75    #[tokio::test]
76    async fn relays_bytes_only_to_the_explicit_loopback_upstream() {
77        let upstream_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
78        let upstream_addr = upstream_listener.local_addr().unwrap();
79        let relay = FixedUpstreamTcpRelay::bind("127.0.0.1:0".parse().unwrap(), upstream_addr)
80            .await
81            .unwrap();
82        assert_eq!(relay.upstream_addr(), upstream_addr);
83        let relay_addr = relay.local_addr().unwrap();
84        let relay_task = tokio::spawn(relay.run());
85
86        let upstream_task = tokio::spawn(async move {
87            let (mut connection, _) = upstream_listener.accept().await.unwrap();
88            let mut request = [0_u8; 4];
89            connection.read_exact(&mut request).await.unwrap();
90            assert_eq!(&request, b"ping");
91            connection.write_all(b"pong").await.unwrap();
92            connection.shutdown().await.unwrap();
93        });
94
95        let mut client = TcpStream::connect(relay_addr).await.unwrap();
96        client.write_all(b"ping").await.unwrap();
97        let mut response = [0_u8; 4];
98        client.read_exact(&mut response).await.unwrap();
99        assert_eq!(&response, b"pong");
100
101        upstream_task.await.unwrap();
102        relay_task.abort();
103    }
104}