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` positioned at the
68    /// start of the connection (all sniffed bytes are preserved in the buffer
69    /// and will be replayed to the protocol handler on first read).
70    pub async fn dispatch(
71        &self,
72        stream: BoxStream,
73    ) -> Result<(ProtocolId, ReplayStream), DispatchError> {
74        let mut replay = ReplayStream::with_max_buffer(stream, self.max_sniff);
75
76        let mut read_buf = vec![0u8; 4096];
77        let mut total_read: usize = 0;
78
79        let result = tokio::time::timeout(self.handshake_timeout, async {
80            loop {
81                // Check buffer overflow
82                if total_read >= self.max_sniff {
83                    return Err(DispatchError::BufferOverflow(self.max_sniff));
84                }
85
86                // Read more data from the stream
87                let to_read = (self.max_sniff - total_read).min(read_buf.len());
88                let n = replay
89                    .read(&mut read_buf[..to_read])
90                    .await
91                    .map_err(|e| DispatchError::Io(std::io::Error::new(e.kind(), e.to_string())))?;
92
93                if n == 0 {
94                    // Stream closed before we could determine the protocol.
95                    break;
96                }
97
98                total_read += n;
99                let prefix = &replay.buffer()[..total_read];
100
101                // Try each detector in order
102                let mut need_more_min = None;
103                for detector in &self.detectors {
104                    match detector.detect(prefix) {
105                        DetectResult::Match { confidence: _ } => {
106                            replay.finish_sniff();
107                            return Ok((detector.id(), replay));
108                        }
109                        DetectResult::NeedMore { minimum } => {
110                            if need_more_min.is_none_or(|m| minimum < m) {
111                                need_more_min = Some(minimum);
112                            }
113                        }
114                        DetectResult::NoMatch => {}
115                    }
116                }
117
118                // If any detector needs more and we haven't hit the buffer
119                // limit, continue reading.
120                if need_more_min.is_some() {
121                    if total_read < self.max_sniff {
122                        continue;
123                    }
124                    // Buffer full but some detector still needs more data.
125                    return Err(DispatchError::BufferOverflow(self.max_sniff));
126                }
127
128                // No detector needs more data and none matched.
129                break;
130            }
131
132            // No protocol matched.
133            Err(DispatchError::NoMatch)
134        });
135
136        match result.await {
137            Ok(result) => result,
138            Err(_elapsed) => Err(DispatchError::Timeout),
139        }
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146    use crate::detect::PrefixDetector;
147    use tokio::io::{AsyncReadExt, AsyncWriteExt};
148
149    fn make_dispatcher(handshake_timeout: Duration) -> ProtocolDispatcher {
150        let detectors: Vec<Box<dyn ProtocolDetector>> = vec![
151            Box::new(PrefixDetector::new(ProtocolId::Http, b"GET ".to_vec())),
152            Box::new(PrefixDetector::new(ProtocolId::Socks5, b"\x05".to_vec())),
153            Box::new(PrefixDetector::new(ProtocolId::Http, b"SSH-".to_vec())),
154        ];
155        ProtocolDispatcher::with_defaults(detectors, handshake_timeout)
156    }
157
158    #[tokio::test]
159    async fn test_dispatch_http() {
160        let dispatcher = make_dispatcher(Duration::from_secs(5));
161        let (mut tx, rx) = tokio::io::duplex(1024);
162
163        tx.write_all(b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
164            .await
165            .unwrap();
166
167        let (proto, mut replay) = dispatcher.dispatch(Box::new(rx)).await.unwrap();
168        assert_eq!(proto, ProtocolId::Http);
169
170        // The replay stream should contain the full sniffed data
171        assert_eq!(
172            replay.buffer(),
173            b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"
174        );
175
176        // Reads after sniff should come from the underlying stream
177        tx.write_all(b"more data").await.unwrap();
178        tx.shutdown().await.unwrap();
179
180        let mut buf = [0u8; 1024];
181        let n = replay.read(&mut buf).await.unwrap();
182        assert_eq!(&buf[..n], b"more data");
183    }
184
185    #[tokio::test]
186    async fn test_dispatch_socks5() {
187        let dispatcher = make_dispatcher(Duration::from_secs(5));
188        let (mut tx, rx) = tokio::io::duplex(1024);
189
190        tx.write_all(b"\x05\x01\x00").await.unwrap();
191
192        let (proto, _) = dispatcher.dispatch(Box::new(rx)).await.unwrap();
193        assert_eq!(proto, ProtocolId::Socks5);
194    }
195
196    #[tokio::test]
197    async fn test_dispatch_ssh() {
198        let dispatcher = make_dispatcher(Duration::from_secs(5));
199        let (mut tx, rx) = tokio::io::duplex(1024);
200
201        tx.write_all(b"SSH-2.0-OpenSSH_8.9\r\n").await.unwrap();
202
203        let (proto, _) = dispatcher.dispatch(Box::new(rx)).await.unwrap();
204        assert_eq!(proto, ProtocolId::Http);
205    }
206
207    #[tokio::test]
208    async fn test_dispatch_no_match() {
209        let dispatcher = make_dispatcher(Duration::from_secs(5));
210        let (tx, rx) = tokio::io::duplex(1024);
211
212        let jh = tokio::spawn(async move {
213            let mut stream = tx;
214            stream.write_all(b"\xFF\xFE\xFD").await.unwrap();
215            stream.shutdown().await.unwrap();
216        });
217
218        let result = dispatcher.dispatch(Box::new(rx)).await;
219        assert!(result.is_err());
220        match result.unwrap_err() {
221            DispatchError::NoMatch => {}
222            e => panic!("expected NoMatch, got {:?}", e),
223        }
224        jh.await.unwrap();
225    }
226
227    #[tokio::test]
228    async fn test_dispatch_timeout() {
229        let dispatcher = make_dispatcher(Duration::from_millis(50));
230        let (_tx, rx) = tokio::io::duplex(1024);
231
232        // Never send data — should time out
233        let result = dispatcher.dispatch(Box::new(rx)).await;
234        assert!(matches!(result, Err(DispatchError::Timeout)));
235    }
236
237    #[tokio::test]
238    async fn test_dispatch_buffer_overflow() {
239        let detectors: Vec<Box<dyn ProtocolDetector>> = vec![Box::new(PrefixDetector::new(
240            ProtocolId::Http,
241            b"NEVER_MATCH_ANYTHING_HERE_FOREVER".to_vec(),
242        ))];
243        let dispatcher = ProtocolDispatcher::new(
244            detectors,
245            16, // very small buffer
246            Duration::from_secs(5),
247        );
248
249        let (tx, rx) = tokio::io::duplex(1024);
250
251        let jh = tokio::spawn(async move {
252            let mut stream = tx;
253            stream.write_all(b"AAAA_BBBB_CCCC_DDDD_EEEE").await.unwrap();
254            stream.shutdown().await.unwrap();
255        });
256
257        let result = dispatcher.dispatch(Box::new(rx)).await;
258        assert!(matches!(result, Err(DispatchError::BufferOverflow(16))));
259        jh.await.unwrap();
260    }
261
262    #[tokio::test]
263    async fn test_dispatch_ordered_detection() {
264        // Both detectors would match \x05, but HTTP is listed first
265        let detectors: Vec<Box<dyn ProtocolDetector>> = vec![
266            Box::new(PrefixDetector::new(ProtocolId::Http, b"\x05".to_vec())),
267            Box::new(PrefixDetector::new(ProtocolId::Socks5, b"\x05".to_vec())),
268        ];
269        let dispatcher = ProtocolDispatcher::with_defaults(detectors, Duration::from_secs(5));
270
271        let (mut tx, rx) = tokio::io::duplex(1024);
272        tx.write_all(b"\x05").await.unwrap();
273
274        let (proto, _) = dispatcher.dispatch(Box::new(rx)).await.unwrap();
275        // First detector wins
276        assert_eq!(proto, ProtocolId::Http);
277    }
278
279    #[tokio::test]
280    async fn test_dispatch_fragmented_detection() {
281        let dispatcher = make_dispatcher(Duration::from_secs(5));
282        let (tx, rx) = tokio::io::duplex(1024);
283
284        let jh = tokio::spawn(async move {
285            let mut stream = tx;
286            stream.write_all(b"GE").await.unwrap();
287            tokio::time::sleep(std::time::Duration::from_millis(10)).await;
288            stream.write_all(b"T /").await.unwrap();
289            stream.shutdown().await.unwrap();
290        });
291
292        let (proto, _) = dispatcher.dispatch(Box::new(rx)).await.unwrap();
293        assert_eq!(proto, ProtocolId::Http);
294
295        jh.await.unwrap();
296    }
297
298    #[tokio::test]
299    async fn test_dispatch_unknown_input_closes() {
300        let dispatcher = make_dispatcher(Duration::from_secs(5));
301        let (tx, rx) = tokio::io::duplex(1024);
302
303        // Send nothing then close
304        drop(tx);
305
306        let result = dispatcher.dispatch(Box::new(rx)).await;
307        // Should get NoMatch since stream closed before any detector matched
308        assert!(matches!(result, Err(DispatchError::NoMatch)));
309    }
310
311    #[tokio::test]
312    async fn test_dispatch_stream_closed_mid_detection() {
313        let dispatcher = make_dispatcher(Duration::from_secs(5));
314        let (mut tx, rx) = tokio::io::duplex(1024);
315
316        // Send a partial prefix then close
317        tx.write_all(b"GE").await.unwrap();
318        drop(tx);
319
320        let result = dispatcher.dispatch(Box::new(rx)).await;
321        assert!(matches!(result, Err(DispatchError::NoMatch)));
322    }
323
324    #[tokio::test]
325    async fn test_dispatch_protocol_ids() {
326        let dispatcher = make_dispatcher(Duration::from_secs(5));
327        let ids = dispatcher.protocol_ids();
328        assert_eq!(
329            ids,
330            vec![ProtocolId::Http, ProtocolId::Socks5, ProtocolId::Http]
331        );
332    }
333}