use std::time::Duration;
use tokio::io::AsyncReadExt;
use crate::detect::{DetectResult, ProtocolDetector};
use crate::replay::ReplayStream;
use crate::{BoxStream, ProtocolId};
const DEFAULT_MAX_SNIFF: usize = 8 * 1024;
#[derive(Debug, thiserror::Error)]
pub enum DispatchError {
#[error("handshake timeout")]
Timeout,
#[error("sniff buffer full ({0} bytes) with no protocol match")]
BufferOverflow(usize),
#[error("no protocol matched the connection")]
NoMatch,
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
}
pub struct ProtocolDispatcher {
detectors: Vec<Box<dyn ProtocolDetector>>,
max_sniff: usize,
handshake_timeout: Duration,
}
impl ProtocolDispatcher {
pub fn new(
detectors: Vec<Box<dyn ProtocolDetector>>,
max_sniff: usize,
handshake_timeout: Duration,
) -> Self {
Self {
detectors,
max_sniff,
handshake_timeout,
}
}
pub fn with_defaults(
detectors: Vec<Box<dyn ProtocolDetector>>,
handshake_timeout: Duration,
) -> Self {
Self::new(detectors, DEFAULT_MAX_SNIFF, handshake_timeout)
}
pub fn protocol_ids(&self) -> Vec<ProtocolId> {
self.detectors.iter().map(|d| d.id()).collect()
}
pub async fn dispatch(
&self,
stream: BoxStream,
) -> Result<(ProtocolId, ReplayStream), DispatchError> {
let mut replay = ReplayStream::with_max_buffer(stream, self.max_sniff);
let mut read_buf = [0u8; 4096];
let mut total_read: usize = 0;
let result = tokio::time::timeout(self.handshake_timeout, async {
loop {
if total_read >= self.max_sniff {
return Err(DispatchError::BufferOverflow(self.max_sniff));
}
let to_read = (self.max_sniff - total_read).min(read_buf.len());
let n = replay
.read(&mut read_buf[..to_read])
.await
.map_err(DispatchError::Io)?;
if n == 0 {
break;
}
total_read += n;
let prefix = &replay.buffer()[..total_read];
let mut need_more_min = None;
for detector in &self.detectors {
match detector.detect(prefix) {
DetectResult::Match { confidence: _ } => {
replay.finish_sniff();
return Ok((detector.id(), replay));
}
DetectResult::NeedMore { minimum } => {
if need_more_min.is_none_or(|m| minimum < m) {
need_more_min = Some(minimum);
}
}
DetectResult::NoMatch => {}
}
}
if need_more_min.is_some() {
if total_read < self.max_sniff {
continue;
}
return Err(DispatchError::BufferOverflow(self.max_sniff));
}
break;
}
Err(DispatchError::NoMatch)
});
match result.await {
Ok(result) => result,
Err(_elapsed) => Err(DispatchError::Timeout),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::detect::PrefixDetector;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
fn make_dispatcher(handshake_timeout: Duration) -> ProtocolDispatcher {
let detectors: Vec<Box<dyn ProtocolDetector>> = vec![
Box::new(PrefixDetector::new(ProtocolId::Http, b"GET ".to_vec())),
Box::new(PrefixDetector::new(ProtocolId::Socks5, b"\x05".to_vec())),
Box::new(PrefixDetector::new(ProtocolId::Http, b"CUSTOM-".to_vec())),
];
ProtocolDispatcher::with_defaults(detectors, handshake_timeout)
}
#[tokio::test]
async fn test_dispatch_http() {
let dispatcher = make_dispatcher(Duration::from_secs(5));
let (mut tx, rx) = tokio::io::duplex(1024);
tx.write_all(b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
.await
.unwrap();
let (proto, mut replay) = dispatcher.dispatch(Box::new(rx)).await.unwrap();
assert_eq!(proto, ProtocolId::Http);
assert_eq!(
replay.buffer(),
b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"
);
tx.write_all(b"more data").await.unwrap();
tx.shutdown().await.unwrap();
let mut buf = [0u8; 1024];
let n = replay.read(&mut buf).await.unwrap();
assert_eq!(&buf[..n], b"more data");
}
#[tokio::test]
async fn test_dispatch_socks5() {
let dispatcher = make_dispatcher(Duration::from_secs(5));
let (mut tx, rx) = tokio::io::duplex(1024);
tx.write_all(b"\x05\x01\x00").await.unwrap();
let (proto, _) = dispatcher.dispatch(Box::new(rx)).await.unwrap();
assert_eq!(proto, ProtocolId::Socks5);
}
#[tokio::test]
async fn test_dispatch_custom_prefix_as_http() {
let dispatcher = make_dispatcher(Duration::from_secs(5));
let (mut tx, rx) = tokio::io::duplex(1024);
tx.write_all(b"CUSTOM-payload").await.unwrap();
let (proto, _) = dispatcher.dispatch(Box::new(rx)).await.unwrap();
assert_eq!(proto, ProtocolId::Http);
}
#[tokio::test]
async fn test_dispatch_no_match() {
let dispatcher = make_dispatcher(Duration::from_secs(5));
let (tx, rx) = tokio::io::duplex(1024);
let jh = tokio::spawn(async move {
let mut stream = tx;
stream.write_all(b"\xFF\xFE\xFD").await.unwrap();
stream.shutdown().await.unwrap();
});
let result = dispatcher.dispatch(Box::new(rx)).await;
assert!(result.is_err());
match result.unwrap_err() {
DispatchError::NoMatch => {}
e => panic!("expected NoMatch, got {:?}", e),
}
jh.await.unwrap();
}
#[tokio::test]
async fn test_dispatch_timeout() {
let dispatcher = make_dispatcher(Duration::from_millis(50));
let (_tx, rx) = tokio::io::duplex(1024);
let result = dispatcher.dispatch(Box::new(rx)).await;
assert!(matches!(result, Err(DispatchError::Timeout)));
}
#[tokio::test]
async fn test_dispatch_buffer_overflow() {
let detectors: Vec<Box<dyn ProtocolDetector>> = vec![Box::new(PrefixDetector::new(
ProtocolId::Http,
b"NEVER_MATCH_ANYTHING_HERE_FOREVER".to_vec(),
))];
let dispatcher = ProtocolDispatcher::new(
detectors,
16, Duration::from_secs(5),
);
let (tx, rx) = tokio::io::duplex(1024);
let jh = tokio::spawn(async move {
let mut stream = tx;
stream.write_all(b"AAAA_BBBB_CCCC_DDDD_EEEE").await.unwrap();
stream.shutdown().await.unwrap();
});
let result = dispatcher.dispatch(Box::new(rx)).await;
assert!(matches!(result, Err(DispatchError::BufferOverflow(16))));
jh.await.unwrap();
}
#[tokio::test]
async fn test_dispatch_ordered_detection() {
let detectors: Vec<Box<dyn ProtocolDetector>> = vec![
Box::new(PrefixDetector::new(ProtocolId::Http, b"\x05".to_vec())),
Box::new(PrefixDetector::new(ProtocolId::Socks5, b"\x05".to_vec())),
];
let dispatcher = ProtocolDispatcher::with_defaults(detectors, Duration::from_secs(5));
let (mut tx, rx) = tokio::io::duplex(1024);
tx.write_all(b"\x05").await.unwrap();
let (proto, _) = dispatcher.dispatch(Box::new(rx)).await.unwrap();
assert_eq!(proto, ProtocolId::Http);
}
#[tokio::test]
async fn test_dispatch_fragmented_detection() {
let dispatcher = make_dispatcher(Duration::from_secs(5));
let (tx, rx) = tokio::io::duplex(1024);
let jh = tokio::spawn(async move {
let mut stream = tx;
stream.write_all(b"GE").await.unwrap();
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
stream.write_all(b"T /").await.unwrap();
stream.shutdown().await.unwrap();
});
let (proto, _) = dispatcher.dispatch(Box::new(rx)).await.unwrap();
assert_eq!(proto, ProtocolId::Http);
jh.await.unwrap();
}
#[tokio::test]
async fn test_dispatch_unknown_input_closes() {
let dispatcher = make_dispatcher(Duration::from_secs(5));
let (tx, rx) = tokio::io::duplex(1024);
drop(tx);
let result = dispatcher.dispatch(Box::new(rx)).await;
assert!(matches!(result, Err(DispatchError::NoMatch)));
}
#[tokio::test]
async fn test_dispatch_stream_closed_mid_detection() {
let dispatcher = make_dispatcher(Duration::from_secs(5));
let (mut tx, rx) = tokio::io::duplex(1024);
tx.write_all(b"GE").await.unwrap();
drop(tx);
let result = dispatcher.dispatch(Box::new(rx)).await;
assert!(matches!(result, Err(DispatchError::NoMatch)));
}
#[tokio::test]
async fn test_dispatch_protocol_ids() {
let dispatcher = make_dispatcher(Duration::from_secs(5));
let ids = dispatcher.protocol_ids();
assert_eq!(
ids,
vec![ProtocolId::Http, ProtocolId::Socks5, ProtocolId::Http]
);
}
}