Skip to main content

eggress_core/
dispatch.rs

1use std::time::Duration;
2
3use tokio::io::AsyncReadExt;
4
5use crate::detect::{DetectResult, ProtocolDetector};
6use crate::replay::ReplayStream;
7use crate::{BoxStream, ProtocolId};
8
9const DEFAULT_MAX_SNIFF: usize = 8 * 1024;
10
11/// Errors that can occur during protocol dispatch.
12#[derive(Debug, thiserror::Error)]
13pub enum DispatchError {
14    #[error("handshake timeout")]
15    Timeout,
16    #[error("sniff buffer full ({0} bytes) with no protocol match")]
17    BufferOverflow(usize),
18    #[error("no protocol matched the connection")]
19    NoMatch,
20    #[error("IO error: {0}")]
21    Io(#[from] std::io::Error),
22}
23
24/// Dispatches a connection to the appropriate protocol handler based on
25/// sniffed initial bytes.
26pub struct ProtocolDispatcher {
27    detectors: Vec<Box<dyn ProtocolDetector>>,
28    max_sniff: usize,
29    handshake_timeout: Duration,
30}
31
32impl ProtocolDispatcher {
33    /// Creates a new `ProtocolDispatcher`.
34    ///
35    /// # Arguments
36    /// * `detectors` - Ordered list of protocol detectors. The first match wins.
37    /// * `max_sniff` - Maximum number of bytes to buffer for protocol detection.
38    /// * `handshake_timeout` - Maximum time to spend on protocol detection.
39    pub fn new(
40        detectors: Vec<Box<dyn ProtocolDetector>>,
41        max_sniff: usize,
42        handshake_timeout: Duration,
43    ) -> Self {
44        Self {
45            detectors,
46            max_sniff,
47            handshake_timeout,
48        }
49    }
50
51    /// Creates a new `ProtocolDispatcher` with default sniff buffer size.
52    pub fn with_defaults(
53        detectors: Vec<Box<dyn ProtocolDetector>>,
54        handshake_timeout: Duration,
55    ) -> Self {
56        Self::new(detectors, DEFAULT_MAX_SNIFF, handshake_timeout)
57    }
58
59    /// Returns the list of registered protocol IDs.
60    pub fn protocol_ids(&self) -> Vec<ProtocolId> {
61        self.detectors.iter().map(|d| d.id()).collect()
62    }
63
64    /// Dispatches a connection by sniffing its initial bytes and matching
65    /// against registered protocol detectors.
66    ///
67    /// Returns the matched protocol ID and a `ReplayStream`. The sniffed
68    /// prefix bytes are consumed by the detection reads performed here, so
69    /// the returned stream is positioned immediately *after* the sniffed
70    /// prefix — they are NOT replayed on subsequent reads. The sniffed
71    /// bytes remain available for inspection via [`ReplayStream::buffer`]
72    /// and [`ReplayStream::buffered_remaining`].
73    pub async fn dispatch(
74        &self,
75        stream: BoxStream,
76    ) -> Result<(ProtocolId, ReplayStream), DispatchError> {
77        let mut replay = ReplayStream::with_max_buffer(stream, self.max_sniff);
78
79        let mut read_buf = [0u8; 4096];
80        let mut total_read: usize = 0;
81
82        let result = tokio::time::timeout(self.handshake_timeout, async {
83            loop {
84                // Check buffer overflow
85                if total_read >= self.max_sniff {
86                    return Err(DispatchError::BufferOverflow(self.max_sniff));
87                }
88
89                // Read more data from the stream
90                let to_read = (self.max_sniff - total_read).min(read_buf.len());
91                let n = replay
92                    .read(&mut read_buf[..to_read])
93                    .await
94                    .map_err(DispatchError::Io)?;
95
96                if n == 0 {
97                    // Stream closed before we could determine the protocol.
98                    break;
99                }
100
101                total_read += n;
102                let prefix = &replay.buffer()[..total_read];
103
104                // Try each detector in order
105                let mut need_more_min = None;
106                for detector in &self.detectors {
107                    match detector.detect(prefix) {
108                        DetectResult::Match { confidence: _ } => {
109                            replay.finish_sniff();
110                            return Ok((detector.id(), replay));
111                        }
112                        DetectResult::NeedMore { minimum } => {
113                            if need_more_min.is_none_or(|m| minimum < m) {
114                                need_more_min = Some(minimum);
115                            }
116                        }
117                        DetectResult::NoMatch => {}
118                    }
119                }
120
121                // If any detector needs more and we haven't hit the buffer
122                // limit, continue reading.
123                if need_more_min.is_some() {
124                    if total_read < self.max_sniff {
125                        continue;
126                    }
127                    // Buffer full but some detector still needs more data.
128                    return Err(DispatchError::BufferOverflow(self.max_sniff));
129                }
130
131                // No detector needs more data and none matched.
132                break;
133            }
134
135            // No protocol matched.
136            Err(DispatchError::NoMatch)
137        });
138
139        match result.await {
140            Ok(result) => result,
141            Err(_elapsed) => Err(DispatchError::Timeout),
142        }
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149    use crate::detect::PrefixDetector;
150    use tokio::io::{AsyncReadExt, AsyncWriteExt};
151
152    fn make_dispatcher(handshake_timeout: Duration) -> ProtocolDispatcher {
153        let detectors: Vec<Box<dyn ProtocolDetector>> = vec![
154            Box::new(PrefixDetector::new(ProtocolId::Http, b"GET ".to_vec())),
155            Box::new(PrefixDetector::new(ProtocolId::Socks5, b"\x05".to_vec())),
156            Box::new(PrefixDetector::new(ProtocolId::Http, b"CUSTOM-".to_vec())),
157        ];
158        ProtocolDispatcher::with_defaults(detectors, handshake_timeout)
159    }
160
161    #[tokio::test]
162    async fn test_dispatch_http() {
163        let dispatcher = make_dispatcher(Duration::from_secs(5));
164        let (mut tx, rx) = tokio::io::duplex(1024);
165
166        tx.write_all(b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
167            .await
168            .unwrap();
169
170        let (proto, mut replay) = dispatcher.dispatch(Box::new(rx)).await.unwrap();
171        assert_eq!(proto, ProtocolId::Http);
172
173        // The replay stream should contain the full sniffed data
174        assert_eq!(
175            replay.buffer(),
176            b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"
177        );
178
179        // Reads after sniff should come from the underlying stream
180        tx.write_all(b"more data").await.unwrap();
181        tx.shutdown().await.unwrap();
182
183        let mut buf = [0u8; 1024];
184        let n = replay.read(&mut buf).await.unwrap();
185        assert_eq!(&buf[..n], b"more data");
186    }
187
188    #[tokio::test]
189    async fn test_dispatch_socks5() {
190        let dispatcher = make_dispatcher(Duration::from_secs(5));
191        let (mut tx, rx) = tokio::io::duplex(1024);
192
193        tx.write_all(b"\x05\x01\x00").await.unwrap();
194
195        let (proto, _) = dispatcher.dispatch(Box::new(rx)).await.unwrap();
196        assert_eq!(proto, ProtocolId::Socks5);
197    }
198
199    #[tokio::test]
200    async fn test_dispatch_custom_prefix_as_http() {
201        let dispatcher = make_dispatcher(Duration::from_secs(5));
202        let (mut tx, rx) = tokio::io::duplex(1024);
203
204        tx.write_all(b"CUSTOM-payload").await.unwrap();
205
206        let (proto, _) = dispatcher.dispatch(Box::new(rx)).await.unwrap();
207        assert_eq!(proto, ProtocolId::Http);
208    }
209
210    #[tokio::test]
211    async fn test_dispatch_no_match() {
212        let dispatcher = make_dispatcher(Duration::from_secs(5));
213        let (tx, rx) = tokio::io::duplex(1024);
214
215        let jh = tokio::spawn(async move {
216            let mut stream = tx;
217            stream.write_all(b"\xFF\xFE\xFD").await.unwrap();
218            stream.shutdown().await.unwrap();
219        });
220
221        let result = dispatcher.dispatch(Box::new(rx)).await;
222        assert!(result.is_err());
223        match result.unwrap_err() {
224            DispatchError::NoMatch => {}
225            e => panic!("expected NoMatch, got {:?}", e),
226        }
227        jh.await.unwrap();
228    }
229
230    #[tokio::test]
231    async fn test_dispatch_timeout() {
232        let dispatcher = make_dispatcher(Duration::from_millis(50));
233        let (_tx, rx) = tokio::io::duplex(1024);
234
235        // Never send data — should time out
236        let result = dispatcher.dispatch(Box::new(rx)).await;
237        assert!(matches!(result, Err(DispatchError::Timeout)));
238    }
239
240    #[tokio::test]
241    async fn test_dispatch_buffer_overflow() {
242        let detectors: Vec<Box<dyn ProtocolDetector>> = vec![Box::new(PrefixDetector::new(
243            ProtocolId::Http,
244            b"NEVER_MATCH_ANYTHING_HERE_FOREVER".to_vec(),
245        ))];
246        let dispatcher = ProtocolDispatcher::new(
247            detectors,
248            16, // very small buffer
249            Duration::from_secs(5),
250        );
251
252        let (tx, rx) = tokio::io::duplex(1024);
253
254        let jh = tokio::spawn(async move {
255            let mut stream = tx;
256            stream.write_all(b"AAAA_BBBB_CCCC_DDDD_EEEE").await.unwrap();
257            stream.shutdown().await.unwrap();
258        });
259
260        let result = dispatcher.dispatch(Box::new(rx)).await;
261        assert!(matches!(result, Err(DispatchError::BufferOverflow(16))));
262        jh.await.unwrap();
263    }
264
265    #[tokio::test]
266    async fn test_dispatch_ordered_detection() {
267        // Both detectors would match \x05, but HTTP is listed first
268        let detectors: Vec<Box<dyn ProtocolDetector>> = vec![
269            Box::new(PrefixDetector::new(ProtocolId::Http, b"\x05".to_vec())),
270            Box::new(PrefixDetector::new(ProtocolId::Socks5, b"\x05".to_vec())),
271        ];
272        let dispatcher = ProtocolDispatcher::with_defaults(detectors, Duration::from_secs(5));
273
274        let (mut tx, rx) = tokio::io::duplex(1024);
275        tx.write_all(b"\x05").await.unwrap();
276
277        let (proto, _) = dispatcher.dispatch(Box::new(rx)).await.unwrap();
278        // First detector wins
279        assert_eq!(proto, ProtocolId::Http);
280    }
281
282    #[tokio::test]
283    async fn test_dispatch_fragmented_detection() {
284        let dispatcher = make_dispatcher(Duration::from_secs(5));
285        let (tx, rx) = tokio::io::duplex(1024);
286
287        let jh = tokio::spawn(async move {
288            let mut stream = tx;
289            stream.write_all(b"GE").await.unwrap();
290            tokio::time::sleep(std::time::Duration::from_millis(10)).await;
291            stream.write_all(b"T /").await.unwrap();
292            stream.shutdown().await.unwrap();
293        });
294
295        let (proto, _) = dispatcher.dispatch(Box::new(rx)).await.unwrap();
296        assert_eq!(proto, ProtocolId::Http);
297
298        jh.await.unwrap();
299    }
300
301    #[tokio::test]
302    async fn test_dispatch_unknown_input_closes() {
303        let dispatcher = make_dispatcher(Duration::from_secs(5));
304        let (tx, rx) = tokio::io::duplex(1024);
305
306        // Send nothing then close
307        drop(tx);
308
309        let result = dispatcher.dispatch(Box::new(rx)).await;
310        // Should get NoMatch since stream closed before any detector matched
311        assert!(matches!(result, Err(DispatchError::NoMatch)));
312    }
313
314    #[tokio::test]
315    async fn test_dispatch_stream_closed_mid_detection() {
316        let dispatcher = make_dispatcher(Duration::from_secs(5));
317        let (mut tx, rx) = tokio::io::duplex(1024);
318
319        // Send a partial prefix then close
320        tx.write_all(b"GE").await.unwrap();
321        drop(tx);
322
323        let result = dispatcher.dispatch(Box::new(rx)).await;
324        assert!(matches!(result, Err(DispatchError::NoMatch)));
325    }
326
327    #[tokio::test]
328    async fn test_dispatch_protocol_ids() {
329        let dispatcher = make_dispatcher(Duration::from_secs(5));
330        let ids = dispatcher.protocol_ids();
331        assert_eq!(
332            ids,
333            vec![ProtocolId::Http, ProtocolId::Socks5, ProtocolId::Http]
334        );
335    }
336}