Skip to main content

eggress_server/
reply.rs

1use tokio::io::AsyncWriteExt;
2
3use crate::accept::{PendingTunnel, ReplyContext, TunnelProtocol};
4use crate::error::SessionOpenError;
5use eggress_protocol_socks::socks5::server::SocksAddr;
6
7/// Send a tunnel success reply after route is established.
8pub async fn send_tunnel_success(
9    pending: &mut PendingTunnel,
10    _bound_addr: Option<std::net::SocketAddr>,
11) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
12    match (&pending.protocol, &pending.reply_context) {
13        (TunnelProtocol::HttpConnect, ReplyContext::Http) => {
14            pending
15                .client
16                .write_all(b"HTTP/1.1 200 Connection Established\r\n\r\n")
17                .await?;
18        }
19        (TunnelProtocol::Http2, ReplyContext::Http2)
20        | (TunnelProtocol::Http3, ReplyContext::Http3)
21        | (TunnelProtocol::WebSocket, ReplyContext::WebSocket) => {}
22        (TunnelProtocol::Socks4, ReplyContext::Socks4) => {
23            eggress_protocol_socks::socks4::server::write_socks4_reply(
24                &mut pending.client,
25                eggress_protocol_socks::socks4::server::Socks4Status::Granted,
26                "0.0.0.0:0".parse().unwrap(),
27            )
28            .await?;
29        }
30        (TunnelProtocol::Socks5, ReplyContext::Socks5) => {
31            let bind_addr = SocksAddr::IPv4([0, 0, 0, 0], 0);
32            eggress_protocol_socks::socks5::server::send_connect_reply(
33                &mut pending.client,
34                0x00,
35                &bind_addr,
36            )
37            .await?;
38        }
39        (TunnelProtocol::Shadowsocks, ReplyContext::Shadowsocks) => {
40            // Shadowsocks has no success reply - the server starts relaying immediately
41        }
42        (TunnelProtocol::Trojan, ReplyContext::Trojan) => {
43            // Trojan has no success reply - the server starts relaying immediately
44        }
45        (TunnelProtocol::Raw, ReplyContext::Raw) => {}
46        _ => {
47            return Err("mismatched protocol and reply context".into());
48        }
49    }
50    Ok(())
51}
52
53/// Send a tunnel failure reply when route opening fails.
54pub async fn send_tunnel_failure(
55    pending: &mut PendingTunnel,
56    error: &SessionOpenError,
57) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
58    match (&pending.protocol, &pending.reply_context) {
59        (TunnelProtocol::HttpConnect, ReplyContext::Http) => {
60            let status = http_failure_status(error);
61            pending.client.write_all(status).await?;
62        }
63        (TunnelProtocol::Http2, ReplyContext::Http2)
64        | (TunnelProtocol::Http3, ReplyContext::Http3)
65        | (TunnelProtocol::WebSocket, ReplyContext::WebSocket) => {
66            pending.client.shutdown().await.ok();
67        }
68        (TunnelProtocol::Socks4, ReplyContext::Socks4) => {
69            eggress_protocol_socks::socks4::server::write_socks4_reply(
70                &mut pending.client,
71                eggress_protocol_socks::socks4::server::Socks4Status::Failed,
72                "0.0.0.0:0".parse().unwrap(),
73            )
74            .await?;
75        }
76        (TunnelProtocol::Socks5, ReplyContext::Socks5) => {
77            let rep = socks5_failure_rep(error);
78            let bind_addr = SocksAddr::IPv4([0, 0, 0, 0], 0);
79            eggress_protocol_socks::socks5::server::send_connect_reply(
80                &mut pending.client,
81                rep,
82                &bind_addr,
83            )
84            .await?;
85        }
86        (TunnelProtocol::Shadowsocks, ReplyContext::Shadowsocks) => {
87            // Shadowsocks has no failure reply - just close the connection
88            pending.client.shutdown().await.ok();
89        }
90        (TunnelProtocol::Trojan, ReplyContext::Trojan) => {
91            // Trojan has no failure reply - just close the connection
92            pending.client.shutdown().await.ok();
93        }
94        (TunnelProtocol::Raw, ReplyContext::Raw) => {
95            pending.client.shutdown().await.ok();
96        }
97        _ => {
98            return Err("mismatched protocol and reply context".into());
99        }
100    }
101    Ok(())
102}
103
104/// Send an HTTP forward-proxy failure reply.
105pub async fn send_http_forward_failure(
106    client: &mut eggress_core::BoxStream,
107    error: &SessionOpenError,
108) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
109    let status = http_failure_status(error);
110    client.write_all(status).await?;
111    Ok(())
112}
113
114/// Reject request expectations because the forwarder does not implement the
115/// 100-continue negotiation or a concurrent request/response pump.
116pub async fn send_http_expectation_failed(
117    client: &mut eggress_core::BoxStream,
118) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
119    client
120        .write_all(b"HTTP/1.1 417 Expectation Failed\r\nConnection: close\r\n\r\n")
121        .await?;
122    client.shutdown().await?;
123    Ok(())
124}
125
126/// Reject an upstream protocol upgrade rather than accidentally turning the
127/// forward-proxy session into an unframed tunnel.
128pub async fn send_http_upgrade_unsupported(
129    client: &mut eggress_core::BoxStream,
130) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
131    client
132        .write_all(b"HTTP/1.1 501 Not Implemented\r\nConnection: close\r\n\r\n")
133        .await?;
134    client.shutdown().await?;
135    Ok(())
136}
137
138fn http_failure_status(error: &SessionOpenError) -> &'static [u8] {
139    match error {
140        SessionOpenError::Timeout => b"HTTP/1.1 504 Gateway Timeout\r\nConnection: close\r\n\r\n",
141        SessionOpenError::PolicyDenied => b"HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n",
142        _ => b"HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n",
143    }
144}
145
146fn socks5_failure_rep(error: &SessionOpenError) -> u8 {
147    match error {
148        SessionOpenError::Timeout => 0x06,
149        SessionOpenError::PolicyDenied => 0x02,
150        SessionOpenError::NetworkUnreachable => 0x03,
151        SessionOpenError::HostUnreachable | SessionOpenError::Dns => 0x04,
152        SessionOpenError::Refused => 0x05,
153        _ => 0x01,
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160    use crate::accept::{PendingTunnel, ReplyContext, TunnelProtocol};
161    use eggress_core::{TargetAddr, TargetHost};
162    use tokio::io::AsyncReadExt;
163
164    fn make_pending(
165        protocol: TunnelProtocol,
166        reply_context: ReplyContext,
167    ) -> (PendingTunnel, tokio::io::DuplexStream) {
168        let (client_stream, server_stream) = tokio::io::duplex(1024);
169        let pending = PendingTunnel {
170            target: TargetAddr {
171                host: TargetHost::Domain("example.com".into()),
172                port: 443,
173            },
174            client: Box::new(client_stream),
175            protocol,
176            reply_context,
177            identity: eggress_core::ClientIdentity::Anonymous,
178        };
179        (pending, server_stream)
180    }
181
182    #[tokio::test]
183    async fn test_send_tunnel_success_http() {
184        let (mut pending, mut server) =
185            make_pending(TunnelProtocol::HttpConnect, ReplyContext::Http);
186        send_tunnel_success(&mut pending, None).await.unwrap();
187
188        let mut response = vec![0u8; 1024];
189        let n = server.read(&mut response).await.unwrap();
190        let s = String::from_utf8_lossy(&response[..n]);
191        assert!(s.contains("200"));
192    }
193
194    #[tokio::test]
195    async fn test_send_tunnel_success_socks4() {
196        let (mut pending, mut server) = make_pending(TunnelProtocol::Socks4, ReplyContext::Socks4);
197        send_tunnel_success(&mut pending, None).await.unwrap();
198
199        let mut response = [0u8; 8];
200        server.read_exact(&mut response).await.unwrap();
201        assert_eq!(response[0], 0x00);
202        assert_eq!(response[1], 90); // granted
203    }
204
205    #[tokio::test]
206    async fn test_send_tunnel_success_socks5() {
207        let (mut pending, mut server) = make_pending(TunnelProtocol::Socks5, ReplyContext::Socks5);
208        send_tunnel_success(&mut pending, None).await.unwrap();
209
210        let mut response = [0u8; 10];
211        server.read_exact(&mut response).await.unwrap();
212        assert_eq!(response[0], 0x05);
213        assert_eq!(response[1], 0x00); // success
214    }
215
216    #[tokio::test]
217    async fn test_send_tunnel_failure_http_timeout() {
218        let (mut pending, mut server) =
219            make_pending(TunnelProtocol::HttpConnect, ReplyContext::Http);
220        send_tunnel_failure(&mut pending, &SessionOpenError::Timeout)
221            .await
222            .unwrap();
223
224        let mut response = vec![0u8; 1024];
225        let n = server.read(&mut response).await.unwrap();
226        let s = String::from_utf8_lossy(&response[..n]);
227        assert!(s.contains("504"));
228    }
229
230    #[tokio::test]
231    async fn test_send_tunnel_failure_socks5_refused() {
232        let (mut pending, mut server) = make_pending(TunnelProtocol::Socks5, ReplyContext::Socks5);
233        send_tunnel_failure(&mut pending, &SessionOpenError::Refused)
234            .await
235            .unwrap();
236
237        let mut response = [0u8; 10];
238        server.read_exact(&mut response).await.unwrap();
239        assert_eq!(response[0], 0x05);
240        assert_eq!(response[1], 0x05); // connection refused
241    }
242
243    #[tokio::test]
244    async fn test_send_http_forward_failure() {
245        let (client_stream, mut server) = tokio::io::duplex(1024);
246        let mut client: eggress_core::BoxStream = Box::new(client_stream);
247        send_http_forward_failure(&mut client, &SessionOpenError::Refused)
248            .await
249            .unwrap();
250
251        let mut response = vec![0u8; 1024];
252        let n = server.read(&mut response).await.unwrap();
253        let s = String::from_utf8_lossy(&response[..n]);
254        assert!(s.contains("502"));
255    }
256}