Skip to main content

rust_raknet/
nethernet.rs

1//! TCP signaling forwarding for Bedrock servers using NetherNet.
2//!
3//! NetherNet uses HTTP signaling to exchange WebRTC session descriptions, then
4//! sends gameplay data directly between the client and the Bedrock host. This
5//! proxy forwards only the TCP signaling connection to a NetherNet-capable
6//! backend; it does not implement WebRTC or relay gameplay traffic.
7
8use std::{io, net::SocketAddr};
9use tokio::{
10    io::copy_bidirectional,
11    net::{TcpListener, TcpStream},
12};
13
14/// Forwards NetherNet's TCP signaling connections to a NetherNet-capable server.
15///
16/// The client must still be able to reach the backend's advertised WebRTC ICE
17/// candidate directly for gameplay. This is useful when the signaling endpoint
18/// needs a different local address or port from the backend.
19///
20/// # Example
21/// ```no_run
22/// # async fn example() -> std::io::Result<()> {
23/// let proxy = rust_raknet::NetherNetProxy::bind(
24///     "127.0.0.1:19144".parse().unwrap(),
25///     "127.0.0.1:19142".parse().unwrap(),
26/// ).await?;
27/// proxy.run().await
28/// # }
29/// ```
30pub struct NetherNetProxy {
31    listener: TcpListener,
32    upstream: SocketAddr,
33}
34
35impl NetherNetProxy {
36    /// Binds a TCP signaling endpoint and forwards each connection to `upstream`.
37    pub async fn bind(listen: SocketAddr, upstream: SocketAddr) -> io::Result<Self> {
38        Ok(Self {
39            listener: TcpListener::bind(listen).await?,
40            upstream,
41        })
42    }
43
44    /// Returns the local TCP address used for NetherNet signaling.
45    pub fn local_addr(&self) -> io::Result<SocketAddr> {
46        self.listener.local_addr()
47    }
48
49    /// Accepts and forwards signaling connections until an I/O error occurs.
50    ///
51    /// Call this in a Tokio task if the application needs to stop it by
52    /// cancelling that task.
53    pub async fn run(&self) -> io::Result<()> {
54        loop {
55            let (client, peer) = self.listener.accept().await?;
56            let upstream = self.upstream;
57            tokio::spawn(async move {
58                if let Err(error) = forward(client, upstream).await {
59                    crate::raknet_log_error!(
60                        "NetherNet signaling relay for {peer} failed: {error}"
61                    );
62                }
63            });
64        }
65    }
66}
67
68async fn forward(mut client: TcpStream, upstream: SocketAddr) -> io::Result<()> {
69    let mut upstream_stream = TcpStream::connect(upstream).await?;
70    copy_bidirectional(&mut client, &mut upstream_stream).await?;
71    Ok(())
72}
73
74#[cfg(test)]
75mod tests {
76    use super::NetherNetProxy;
77    use std::time::Duration;
78    use tokio::{
79        io::{AsyncReadExt, AsyncWriteExt},
80        net::{TcpListener, TcpStream},
81        time::timeout,
82    };
83
84    #[tokio::test]
85    async fn closes_client_when_upstream_is_unavailable() {
86        let unavailable = TcpListener::bind("127.0.0.1:0").await.unwrap();
87        let upstream_addr = unavailable.local_addr().unwrap();
88        drop(unavailable);
89
90        let proxy = NetherNetProxy::bind("127.0.0.1:0".parse().unwrap(), upstream_addr)
91            .await
92            .unwrap();
93        let proxy_addr = proxy.local_addr().unwrap();
94        let proxy_task = tokio::spawn(async move { proxy.run().await });
95
96        let result = timeout(Duration::from_secs(3), async {
97            let mut client = TcpStream::connect(proxy_addr).await.unwrap();
98            client
99                .write_all(b"GET /v1/join HTTP/1.1\r\n\r\n")
100                .await
101                .unwrap();
102            let mut response = Vec::new();
103            client.read_to_end(&mut response).await.map(|_| response)
104        })
105        .await
106        .expect("client was not closed after the upstream connection failed");
107
108        assert!(result.is_err() || result.as_ref().is_ok_and(Vec::is_empty));
109        proxy_task.abort();
110        let _ = proxy_task.await;
111    }
112
113    #[tokio::test]
114    async fn forwards_signaling_bytes_in_both_directions() {
115        let upstream = TcpListener::bind("127.0.0.1:0").await.unwrap();
116        let upstream_addr = upstream.local_addr().unwrap();
117        let upstream_task = tokio::spawn(async move {
118            let (mut stream, _) = upstream.accept().await.unwrap();
119            let mut request = Vec::new();
120            let mut chunk = [0; 512];
121            loop {
122                let count = stream.read(&mut chunk).await.unwrap();
123                if count == 0 {
124                    break;
125                }
126                request.extend_from_slice(&chunk[..count]);
127                if request.windows(4).any(|window| window == b"\r\n\r\n") {
128                    break;
129                }
130            }
131            assert!(request.starts_with(b"GET /v1/join HTTP/1.1\r\n"));
132            stream
133                .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok")
134                .await
135                .unwrap();
136        });
137
138        let proxy = NetherNetProxy::bind("127.0.0.1:0".parse().unwrap(), upstream_addr)
139            .await
140            .unwrap();
141        let proxy_addr = proxy.local_addr().unwrap();
142        let proxy_task = tokio::spawn(async move { proxy.run().await });
143
144        let result = timeout(Duration::from_secs(3), async {
145            let mut client = TcpStream::connect(proxy_addr).await.unwrap();
146            client
147                .write_all(b"GET /v1/join HTTP/1.1\r\nHost: localhost\r\n\r\n")
148                .await
149                .unwrap();
150            client.shutdown().await.unwrap();
151            let mut response = Vec::new();
152            client.read_to_end(&mut response).await.unwrap();
153            response
154        })
155        .await
156        .expect("signaling forward timed out");
157
158        assert!(result.starts_with(b"HTTP/1.1 200 OK\r\n"));
159        assert!(result.ends_with(b"\r\nok"));
160        upstream_task.await.unwrap();
161        proxy_task.abort();
162        let _ = proxy_task.await;
163    }
164}