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#[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
24pub struct ProtocolDispatcher {
27 detectors: Vec<Box<dyn ProtocolDetector>>,
28 max_sniff: usize,
29 handshake_timeout: Duration,
30}
31
32impl ProtocolDispatcher {
33 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 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 pub fn protocol_ids(&self) -> Vec<ProtocolId> {
61 self.detectors.iter().map(|d| d.id()).collect()
62 }
63
64 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 if total_read >= self.max_sniff {
83 return Err(DispatchError::BufferOverflow(self.max_sniff));
84 }
85
86 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 break;
96 }
97
98 total_read += n;
99 let prefix = &replay.buffer()[..total_read];
100
101 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 need_more_min.is_some() {
121 if total_read < self.max_sniff {
122 continue;
123 }
124 return Err(DispatchError::BufferOverflow(self.max_sniff));
126 }
127
128 break;
130 }
131
132 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 assert_eq!(
172 replay.buffer(),
173 b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"
174 );
175
176 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 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, 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 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 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 drop(tx);
305
306 let result = dispatcher.dispatch(Box::new(rx)).await;
307 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 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}