1pub mod accept;
2#[cfg(feature = "extended")]
3pub mod advanced;
4mod auth;
5pub mod error;
6pub mod execute;
7pub mod listener;
8pub mod reply;
9
10use std::sync::Arc;
11use std::time::Duration;
12
13pub use accept::AcceptedSession;
14pub use accept::AuthReuseCache;
15pub use error::SessionOpenError;
16pub use execute::{build_chain_executor, FailureCategory, SessionReport};
17
18use eggress_routing::RouteService;
19
20pub trait SessionMetrics: Send + Sync {
28 fn record_session_start(&self);
29 fn record_session(&self, report: &SessionReport);
30 fn record_route_decision(&self, rule: &str, action: &str, outcome: &str);
31 fn record_upstream_open(&self, protocol: &str, outcome: &str);
32 fn record_upstream_failure(&self, protocol: &str, reason: &str);
33 fn record_auth_failure(&self);
34}
35
36pub struct NoopMetrics;
38
39impl SessionMetrics for NoopMetrics {
40 fn record_session_start(&self) {}
41 fn record_session(&self, _report: &SessionReport) {}
42 fn record_route_decision(&self, _rule: &str, _action: &str, _outcome: &str) {}
43 fn record_upstream_open(&self, _protocol: &str, _outcome: &str) {}
44 fn record_upstream_failure(&self, _protocol: &str, _reason: &str) {}
45 fn record_auth_failure(&self) {}
46}
47
48pub struct UdpAssociationHandle {
50 pub id: eggress_udp::assoc::UdpAssociationId,
51 pub relay_addr: std::net::SocketAddr,
52 pub cancel: tokio_util::sync::CancellationToken,
53}
54
55pub trait UdpService: Send + Sync {
57 fn create_association(
58 &self,
59 listener: &str,
60 client_tcp_peer: Option<std::net::SocketAddr>,
61 identity: eggress_core::ClientIdentity,
62 generation: u64,
63 ) -> std::pin::Pin<
64 Box<
65 dyn std::future::Future<
66 Output = Result<UdpAssociationHandle, eggress_udp::error::UdpError>,
67 > + Send
68 + 'static,
69 >,
70 >;
71 fn is_enabled(&self) -> bool;
72 fn active_count(
73 &self,
74 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = usize> + Send + 'static>>;
75}
76
77#[derive(Clone, Default)]
79pub struct ConnectionContext {
80 pub source: Option<std::net::SocketAddr>,
81 pub listener: String,
82 pub generation: u64,
83}
84
85#[derive(Clone)]
87pub struct ConnectionConfig {
88 pub routing: Arc<dyn RouteService>,
89 pub context: ConnectionContext,
90 pub handshake_timeout: Duration,
91 pub connect_timeout: Duration,
92 pub protocols: Arc<[eggress_core::ProtocolId]>,
93 pub authentication: accept::InboundAuthentication,
94 pub metrics: Option<Arc<dyn SessionMetrics>>,
95 pub udp: Option<Arc<dyn UdpService>>,
96 pub tls_client_config: Option<Arc<rustls::ClientConfig>>,
100 pub shadowsocks: Option<accept::InboundShadowsocksConfig>,
101 pub trojan: Option<accept::InboundTrojanConfig>,
103 pub fixed_target: Option<eggress_core::TargetAddr>,
104 pub local_bind: Option<String>,
105 #[cfg(feature = "ssh")]
107 pub ssh_sessions: Option<Arc<eggress_transport_ssh::SshSessionCache>>,
108 #[cfg(feature = "extended")]
110 pub shadowsocks_metrics: Option<Arc<eggress_protocol_shadowsocks::ShadowsocksMetrics>>,
111 #[cfg(not(feature = "extended"))]
112 pub shadowsocks_metrics: Option<()>,
113}
114
115pub async fn serve_connection(
121 client: eggress_core::BoxStream,
122 config: ConnectionConfig,
123) -> SessionReport {
124 let client: eggress_core::BoxStream = Box::new(tokio::io::BufReader::new(client));
128
129 if let Some(metrics) = &config.metrics {
130 metrics.record_session_start();
131 }
132
133 let accepted = tokio::time::timeout(
134 config.handshake_timeout,
135 accept::accept_with_fixed_target_for_peer(
136 client,
137 &config.protocols,
138 &config.authentication,
139 config.shadowsocks.as_ref(),
140 config.shadowsocks_metrics.as_ref(),
141 config.trojan.as_ref(),
142 config.fixed_target.as_ref(),
143 config.context.source.map(|peer| peer.ip()),
144 ),
145 )
146 .await;
147
148 let report = match accepted {
149 Ok(Ok(session)) => execute::execute(session, &config).await,
150 Ok(Err(accept::AcceptError::AuthenticationFailed)) => {
151 if let Some(metrics) = &config.metrics {
152 metrics.record_auth_failure();
153 }
154 SessionReport {
155 protocol: None,
156 target: None,
157 route: "unknown".to_string(),
158 bytes_upstream: 0,
159 bytes_downstream: 0,
160 outcome: execute::SessionOutcome::AuthenticationFailed,
161 failure: Some(execute::FailureCategory::Authentication),
162 rule_id: None,
163 upstream_group: None,
164 upstream_id: None,
165 selection_reason: None,
166 }
167 }
168 Ok(Err(_)) => SessionReport {
169 protocol: None,
170 target: None,
171 route: "unknown".to_string(),
172 bytes_upstream: 0,
173 bytes_downstream: 0,
174 outcome: execute::SessionOutcome::ClientProtocolError,
175 failure: Some(execute::FailureCategory::Protocol),
176 rule_id: None,
177 upstream_group: None,
178 upstream_id: None,
179 selection_reason: None,
180 },
181 Err(_) => SessionReport {
182 protocol: None,
183 target: None,
184 route: "unknown".to_string(),
185 bytes_upstream: 0,
186 bytes_downstream: 0,
187 outcome: execute::SessionOutcome::HandshakeTimedOut,
188 failure: Some(execute::FailureCategory::HandshakeTimeout),
189 rule_id: None,
190 upstream_group: None,
191 upstream_id: None,
192 selection_reason: None,
193 },
194 };
195
196 if let Some(metrics) = &config.metrics {
197 metrics.record_session(&report);
198 }
199
200 #[cfg(feature = "extended")]
201 if let Some(ss_metrics) = &config.shadowsocks_metrics {
202 if report.protocol.as_deref() == Some("shadowsocks") {
203 ss_metrics.record_tcp_session_closed();
204 ss_metrics.record_tcp_flow_close();
205 }
206 }
207
208 tracing::info!(
209 outcome = ?report.outcome,
210 failure = ?report.failure,
211 protocol = ?report.protocol,
212 target = ?report.target,
213 route = %report.route,
214 rule = ?report.rule_id,
215 upstream_group = ?report.upstream_group,
216 upstream = ?report.upstream_id,
217 selection_reason = ?report.selection_reason,
218 bytes_upstream = report.bytes_upstream,
219 bytes_downstream = report.bytes_downstream,
220 "connection completed",
221 );
222
223 report
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229 use eggress_routing::{RouteActionSpec, Router};
230 use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
231
232 fn all_protocols() -> Arc<[eggress_core::ProtocolId]> {
233 Arc::from([
234 eggress_core::ProtocolId::Http,
235 eggress_core::ProtocolId::Socks4,
236 eggress_core::ProtocolId::Socks5,
237 eggress_core::ProtocolId::Http2,
238 eggress_core::ProtocolId::WebSocket,
239 eggress_core::ProtocolId::Raw,
240 ])
241 }
242
243 fn direct_routing() -> Arc<dyn RouteService> {
244 Arc::new(Router::new(vec![], RouteActionSpec::Direct))
245 }
246
247 fn test_config(routing: Arc<dyn RouteService>) -> ConnectionConfig {
248 ConnectionConfig {
249 routing,
250 context: ConnectionContext::default(),
251 handshake_timeout: Duration::from_secs(5),
252 connect_timeout: Duration::from_secs(10),
253 protocols: all_protocols(),
254 authentication: accept::InboundAuthentication::None,
255 metrics: None,
256 udp: None,
257 tls_client_config: None,
258 shadowsocks: None,
259 shadowsocks_metrics: None,
260 trojan: None,
261 fixed_target: None,
262 local_bind: None,
263 }
264 }
265
266 #[tokio::test]
267 async fn test_serve_connection_socks5_direct() {
268 let (echo_addr, echo_jh) = eggress_testkit::start_echo_server().await;
269
270 let proxy_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
271 let proxy_addr = proxy_listener.local_addr().unwrap();
272
273 let proxy_jh = tokio::spawn(async move {
274 let (stream, _) = proxy_listener.accept().await.unwrap();
275 let boxed: eggress_core::BoxStream = Box::new(stream);
276 let config = test_config(direct_routing());
277 serve_connection(boxed, config).await
278 });
279
280 let mut stream = tokio::net::TcpStream::connect(proxy_addr).await.unwrap();
281 stream.write_all(&[0x05, 0x01, 0x00]).await.unwrap();
282 let mut response = [0u8; 2];
283 stream.read_exact(&mut response).await.unwrap();
284 assert_eq!(response, [0x05, 0x00]);
285
286 stream.write_all(&[0x05, 0x01, 0x00, 0x01]).await.unwrap();
287 match echo_addr.ip() {
288 std::net::IpAddr::V4(ip) => {
289 stream.write_all(&ip.octets()).await.unwrap();
290 }
291 std::net::IpAddr::V6(ip) => {
292 stream.write_all(&ip.octets()).await.unwrap();
293 }
294 }
295 stream
296 .write_all(&echo_addr.port().to_be_bytes())
297 .await
298 .unwrap();
299
300 let mut reply = [0u8; 10];
301 stream.read_exact(&mut reply).await.unwrap();
302 assert_eq!(reply[0], 0x05);
303 assert_eq!(reply[1], 0x00);
304
305 stream.write_all(b"hello").await.unwrap();
306 stream.shutdown().await.unwrap();
307
308 let mut buf = Vec::new();
309 stream.read_to_end(&mut buf).await.unwrap();
310 assert_eq!(&buf, b"hello");
311
312 let report = proxy_jh.await.unwrap();
313 assert!(matches!(report.outcome, execute::SessionOutcome::Completed));
314
315 echo_jh.abort();
316 }
317
318 #[tokio::test]
319 async fn test_serve_connection_http_connect_direct() {
320 let (echo_addr, echo_jh) = eggress_testkit::start_echo_server().await;
321
322 let proxy_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
323 let proxy_addr = proxy_listener.local_addr().unwrap();
324
325 let _proxy_jh = tokio::spawn(async move {
326 let (stream, _) = proxy_listener.accept().await.unwrap();
327 let boxed: eggress_core::BoxStream = Box::new(stream);
328 let config = test_config(direct_routing());
329 serve_connection(boxed, config).await
330 });
331
332 let mut stream = tokio::net::TcpStream::connect(proxy_addr).await.unwrap();
333 let connect_req = format!(
334 "CONNECT {}:{} HTTP/1.1\r\nHost: {}:{}\r\n\r\n",
335 echo_addr.ip(),
336 echo_addr.port(),
337 echo_addr.ip(),
338 echo_addr.port()
339 );
340 stream.write_all(connect_req.as_bytes()).await.unwrap();
341
342 let mut response = vec![0u8; 1024];
343 let n = stream.read(&mut response).await.unwrap();
344 let response_str = String::from_utf8_lossy(&response[..n]);
345 assert!(
346 response_str.contains("200"),
347 "expected 200, got: {response_str}"
348 );
349
350 let header_end = response_str.find("\r\n\r\n").unwrap() + 4;
351 let leftover = &response.as_slice()[header_end..n];
352
353 stream.write_all(b"hello proxy").await.unwrap();
354 stream.shutdown().await.unwrap();
355
356 let mut buf = Vec::new();
357 if !leftover.is_empty() {
358 buf.extend_from_slice(leftover);
359 }
360 stream.read_to_end(&mut buf).await.unwrap();
361 assert_eq!(&buf, b"hello proxy");
362
363 echo_jh.abort();
364 }
365
366 async fn start_echo_origin() -> (std::net::SocketAddr, tokio::task::JoinHandle<()>) {
367 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
368 let addr = listener.local_addr().unwrap();
369
370 let jh = tokio::spawn(async move {
371 loop {
372 let (mut stream, _) = match listener.accept().await {
373 Ok(s) => s,
374 Err(_) => break,
375 };
376 tokio::spawn(async move {
377 use tokio::io::AsyncReadExt;
378 use tokio::io::AsyncWriteExt;
379
380 let mut head = Vec::new();
381 let mut tmp = [0u8; 1];
382 loop {
383 if stream.read(&mut tmp).await.unwrap_or(0) == 0 {
384 return;
385 }
386 head.push(tmp[0]);
387 if head.len() >= 4 && &head[head.len() - 4..] == b"\r\n\r\n" {
388 break;
389 }
390 }
391
392 let head_str = String::from_utf8_lossy(&head);
393 let mut content_length: Option<u64> = None;
394 let mut is_chunked = false;
395 for line in head_str.lines() {
396 if let Some((name, value)) = line.split_once(':') {
397 if name.eq_ignore_ascii_case("Content-Length") {
398 content_length = match value.trim().parse() {
399 Ok(length) => Some(length),
400 Err(_) => return,
401 };
402 } else if name.eq_ignore_ascii_case("Transfer-Encoding")
403 && value.trim().eq_ignore_ascii_case("chunked")
404 {
405 is_chunked = true;
406 }
407 }
408 }
409
410 let body = match (content_length, is_chunked) {
411 (Some(len), _) => {
412 let mut body = vec![0u8; len as usize];
413 let mut off = 0;
414 while off < body.len() {
415 let n = stream.read(&mut body[off..]).await.unwrap_or(0);
416 if n == 0 {
417 break;
418 }
419 off += n;
420 }
421 body
422 }
423 (None, true) => {
424 let mut body = Vec::new();
425 loop {
426 let mut size_line = Vec::new();
427 loop {
428 let n = stream.read(&mut tmp).await.unwrap_or(0);
429 if n == 0 {
430 return;
431 }
432 size_line.push(tmp[0]);
433 if size_line.len() >= 2
434 && &size_line[size_line.len() - 2..] == b"\r\n"
435 {
436 break;
437 }
438 }
439 let size_str =
440 String::from_utf8_lossy(&size_line[..size_line.len() - 2]);
441 let chunk_size = match usize::from_str_radix(size_str.trim(), 16) {
442 Ok(size) => size,
443 Err(_) => return,
444 };
445 if chunk_size == 0 {
446 let mut trail = [0u8; 2];
447 let _ = stream.read_exact(&mut trail).await;
448 break;
449 }
450 let mut chunk = vec![0u8; chunk_size];
451 let mut off = 0;
452 while off < chunk.len() {
453 let n = stream.read(&mut chunk[off..]).await.unwrap_or(0);
454 if n == 0 {
455 return;
456 }
457 off += n;
458 }
459 body.extend_from_slice(&chunk);
460 let mut trail = [0u8; 2];
461 let _ = stream.read_exact(&mut trail).await;
462 }
463 body
464 }
465 _ => Vec::new(),
466 };
467
468 let response = format!(
469 "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
470 body.len()
471 );
472 let _ = stream.write_all(response.as_bytes()).await;
473 let _ = stream.write_all(&body).await;
474 let _ = stream.shutdown().await;
475 });
476 }
477 });
478
479 (addr, jh)
480 }
481
482 #[tokio::test]
483 async fn test_http_forward_post_content_length() {
484 let (origin_addr, origin_jh) = start_echo_origin().await;
485
486 let proxy_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
487 let proxy_addr = proxy_listener.local_addr().unwrap();
488
489 let proxy_jh = tokio::spawn(async move {
490 let (stream, _) = proxy_listener.accept().await.unwrap();
491 let boxed: eggress_core::BoxStream = Box::new(stream);
492 let config = test_config(direct_routing());
493 serve_connection(boxed, config).await
494 });
495
496 let mut stream = tokio::net::TcpStream::connect(proxy_addr).await.unwrap();
497 let body = b"hello world";
498 let request = format!(
499 "POST http://{}:{} HTTP/1.1\r\nHost: {}:{}\r\nContent-Length: {}\r\n\r\n",
500 origin_addr.ip(),
501 origin_addr.port(),
502 origin_addr.ip(),
503 origin_addr.port(),
504 body.len()
505 );
506 stream.write_all(request.as_bytes()).await.unwrap();
507 stream.write_all(body).await.unwrap();
508
509 let mut response = Vec::new();
510 stream.read_to_end(&mut response).await.unwrap();
511 let response_str = String::from_utf8_lossy(&response);
512 assert!(
513 response_str.ends_with("hello world"),
514 "body not echoed: {response_str}"
515 );
516
517 let report = proxy_jh.await.unwrap();
518 assert!(matches!(report.outcome, execute::SessionOutcome::Completed));
519
520 origin_jh.abort();
521 }
522
523 #[tokio::test]
524 async fn test_http_forward_post_chunked() {
525 let (origin_addr, origin_jh) = start_echo_origin().await;
526
527 let proxy_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
528 let proxy_addr = proxy_listener.local_addr().unwrap();
529
530 let proxy_jh = tokio::spawn(async move {
531 let (stream, _) = proxy_listener.accept().await.unwrap();
532 let boxed: eggress_core::BoxStream = Box::new(stream);
533 let config = test_config(direct_routing());
534 serve_connection(boxed, config).await
535 });
536
537 let mut stream = tokio::net::TcpStream::connect(proxy_addr).await.unwrap();
538 let body = b"chunked body";
539 let request = format!(
540 "POST http://{}:{} HTTP/1.1\r\nHost: {}:{}\r\nTransfer-Encoding: chunked\r\n\r\n",
541 origin_addr.ip(),
542 origin_addr.port(),
543 origin_addr.ip(),
544 origin_addr.port()
545 );
546 stream.write_all(request.as_bytes()).await.unwrap();
547 stream
548 .write_all(format!("{:x}\r\n", body.len()).as_bytes())
549 .await
550 .unwrap();
551 stream.write_all(body).await.unwrap();
552 stream.write_all(b"\r\n").await.unwrap();
553 stream.write_all(b"0\r\n\r\n").await.unwrap();
554
555 let mut response = Vec::new();
556 stream.read_to_end(&mut response).await.unwrap();
557 let response_str = String::from_utf8_lossy(&response);
558 assert!(
559 response_str.ends_with("chunked body"),
560 "body not echoed: {response_str}"
561 );
562
563 let report = proxy_jh.await.unwrap();
564 assert!(matches!(report.outcome, execute::SessionOutcome::Completed));
565
566 origin_jh.abort();
567 }
568
569 #[tokio::test]
570 async fn test_http_forward_get_no_body() {
571 let (origin_addr, origin_jh) = start_echo_origin().await;
572
573 let proxy_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
574 let proxy_addr = proxy_listener.local_addr().unwrap();
575
576 let proxy_jh = tokio::spawn(async move {
577 let (stream, _) = proxy_listener.accept().await.unwrap();
578 let boxed: eggress_core::BoxStream = Box::new(stream);
579 let config = test_config(direct_routing());
580 serve_connection(boxed, config).await
581 });
582
583 let mut stream = tokio::net::TcpStream::connect(proxy_addr).await.unwrap();
584 let request = format!(
585 "GET http://{}:{}/ HTTP/1.1\r\nHost: {}:{}\r\n\r\n",
586 origin_addr.ip(),
587 origin_addr.port(),
588 origin_addr.ip(),
589 origin_addr.port()
590 );
591 stream.write_all(request.as_bytes()).await.unwrap();
592
593 let mut response = Vec::new();
594 stream.read_to_end(&mut response).await.unwrap();
595 let response_str = String::from_utf8_lossy(&response);
596 assert!(
597 response_str.contains("200 OK"),
598 "expected 200, got: {response_str}"
599 );
600 let body_start = response_str.find("\r\n\r\n").unwrap() + 4;
601 let body = &response_str[body_start..];
602 assert!(body.is_empty(), "expected empty body for GET, got: {body}");
603
604 let report = proxy_jh.await.unwrap();
605 assert!(matches!(report.outcome, execute::SessionOutcome::Completed));
606
607 origin_jh.abort();
608 }
609
610 #[tokio::test(start_paused = true)]
611 async fn test_handshake_timeout_no_bytes() {
612 let (_client_stream, server_stream) = tokio::io::duplex(1024);
613 let boxed: eggress_core::BoxStream = Box::new(server_stream);
614 let config = test_config(direct_routing());
615
616 let task = tokio::spawn(serve_connection(boxed, config));
617
618 tokio::time::advance(Duration::from_secs(6)).await;
619
620 let report = task.await.unwrap();
621 assert!(matches!(
622 report.outcome,
623 execute::SessionOutcome::HandshakeTimedOut
624 ));
625 }
626
627 #[tokio::test(start_paused = true)]
628 async fn test_handshake_timeout_partial_http() {
629 let (mut client_stream, server_stream) = tokio::io::duplex(1024);
630 let boxed: eggress_core::BoxStream = Box::new(server_stream);
631 let config = test_config(direct_routing());
632
633 let task = tokio::spawn(serve_connection(boxed, config));
634
635 client_stream.write_all(b"CON").await.unwrap();
636 tokio::time::advance(Duration::from_secs(6)).await;
637
638 let report = task.await.unwrap();
639 assert!(matches!(
640 report.outcome,
641 execute::SessionOutcome::HandshakeTimedOut
642 ));
643 }
644
645 #[tokio::test(start_paused = true)]
646 async fn test_handshake_timeout_partial_socks5() {
647 let (mut client_stream, server_stream) = tokio::io::duplex(1024);
648 let boxed: eggress_core::BoxStream = Box::new(server_stream);
649 let config = test_config(direct_routing());
650
651 let task = tokio::spawn(serve_connection(boxed, config));
652
653 client_stream.write_all(&[0x05]).await.unwrap();
654 tokio::time::advance(Duration::from_secs(6)).await;
655
656 let report = task.await.unwrap();
657 assert!(matches!(
658 report.outcome,
659 execute::SessionOutcome::HandshakeTimedOut
660 ));
661 }
662
663 #[tokio::test]
664 async fn test_handshake_completes_before_timeout() {
665 let (echo_addr, echo_jh) = eggress_testkit::start_echo_server().await;
666
667 let proxy_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
668 let proxy_addr = proxy_listener.local_addr().unwrap();
669
670 let proxy_jh = tokio::spawn(async move {
671 let (stream, _) = proxy_listener.accept().await.unwrap();
672 let boxed: eggress_core::BoxStream = Box::new(stream);
673 let config = test_config(direct_routing());
674 serve_connection(boxed, config).await
675 });
676
677 let mut stream = tokio::net::TcpStream::connect(proxy_addr).await.unwrap();
678 stream.write_all(&[0x05, 0x01, 0x00]).await.unwrap();
679 let mut response = [0u8; 2];
680 stream.read_exact(&mut response).await.unwrap();
681 assert_eq!(response, [0x05, 0x00]);
682
683 stream.write_all(&[0x05, 0x01, 0x00, 0x01]).await.unwrap();
684 match echo_addr.ip() {
685 std::net::IpAddr::V4(ip) => {
686 stream.write_all(&ip.octets()).await.unwrap();
687 }
688 std::net::IpAddr::V6(ip) => {
689 stream.write_all(&ip.octets()).await.unwrap();
690 }
691 }
692 stream
693 .write_all(&echo_addr.port().to_be_bytes())
694 .await
695 .unwrap();
696
697 let mut reply = [0u8; 10];
698 stream.read_exact(&mut reply).await.unwrap();
699 assert_eq!(reply[0], 0x05);
700 assert_eq!(reply[1], 0x00);
701
702 stream.write_all(b"hello").await.unwrap();
703 stream.shutdown().await.unwrap();
704
705 let mut buf = Vec::new();
706 stream.read_to_end(&mut buf).await.unwrap();
707 assert_eq!(&buf, b"hello");
708
709 let report = proxy_jh.await.unwrap();
710 assert!(matches!(report.outcome, execute::SessionOutcome::Completed));
711
712 echo_jh.abort();
713 }
714
715 #[tokio::test]
716 async fn test_http_forward_get_reports_nonzero_bytes() {
717 let (origin_addr, origin_jh) = start_echo_origin().await;
718
719 let proxy_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
720 let proxy_addr = proxy_listener.local_addr().unwrap();
721
722 let proxy_jh = tokio::spawn(async move {
723 let (stream, _) = proxy_listener.accept().await.unwrap();
724 let boxed: eggress_core::BoxStream = Box::new(stream);
725 let config = test_config(direct_routing());
726 serve_connection(boxed, config).await
727 });
728
729 let mut stream = tokio::net::TcpStream::connect(proxy_addr).await.unwrap();
730 let request = format!(
731 "GET http://{}:{}/ HTTP/1.1\r\nHost: {}:{}\r\n\r\n",
732 origin_addr.ip(),
733 origin_addr.port(),
734 origin_addr.ip(),
735 origin_addr.port()
736 );
737 stream.write_all(request.as_bytes()).await.unwrap();
738
739 let mut response = Vec::new();
740 stream.read_to_end(&mut response).await.unwrap();
741
742 let report = proxy_jh.await.unwrap();
743 assert!(matches!(report.outcome, execute::SessionOutcome::Completed));
744 assert!(
745 report.bytes_upstream > 0,
746 "upstream bytes should be nonzero"
747 );
748 assert!(
749 report.bytes_downstream > 0,
750 "downstream bytes should be nonzero"
751 );
752
753 origin_jh.abort();
754 }
755
756 #[tokio::test]
757 async fn test_http_forward_post_reports_body_bytes() {
758 let (origin_addr, origin_jh) = start_echo_origin().await;
759
760 let proxy_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
761 let proxy_addr = proxy_listener.local_addr().unwrap();
762
763 let proxy_jh = tokio::spawn(async move {
764 let (stream, _) = proxy_listener.accept().await.unwrap();
765 let boxed: eggress_core::BoxStream = Box::new(stream);
766 let config = test_config(direct_routing());
767 serve_connection(boxed, config).await
768 });
769
770 let mut stream = tokio::net::TcpStream::connect(proxy_addr).await.unwrap();
771 let body = b"hello world";
772 let request = format!(
773 "POST http://{}:{} HTTP/1.1\r\nHost: {}:{}\r\nContent-Length: {}\r\n\r\n",
774 origin_addr.ip(),
775 origin_addr.port(),
776 origin_addr.ip(),
777 origin_addr.port(),
778 body.len()
779 );
780 stream.write_all(request.as_bytes()).await.unwrap();
781 stream.write_all(body).await.unwrap();
782
783 let mut response = Vec::new();
784 stream.read_to_end(&mut response).await.unwrap();
785
786 let report = proxy_jh.await.unwrap();
787 assert!(matches!(report.outcome, execute::SessionOutcome::Completed));
788 assert!(
789 report.bytes_upstream > body.len() as u64,
790 "upstream bytes ({}) should exceed body length ({})",
791 report.bytes_upstream,
792 body.len()
793 );
794 assert!(report.bytes_downstream > 0);
795
796 origin_jh.abort();
797 }
798
799 #[tokio::test]
800 async fn test_successful_session_has_no_failure() {
801 let (echo_addr, echo_jh) = eggress_testkit::start_echo_server().await;
802
803 let proxy_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
804 let proxy_addr = proxy_listener.local_addr().unwrap();
805
806 let proxy_jh = tokio::spawn(async move {
807 let (stream, _) = proxy_listener.accept().await.unwrap();
808 let boxed: eggress_core::BoxStream = Box::new(stream);
809 let config = test_config(direct_routing());
810 serve_connection(boxed, config).await
811 });
812
813 let mut stream = tokio::net::TcpStream::connect(proxy_addr).await.unwrap();
814 stream.write_all(&[0x05, 0x01, 0x00]).await.unwrap();
815 let mut response = [0u8; 2];
816 stream.read_exact(&mut response).await.unwrap();
817 assert_eq!(response, [0x05, 0x00]);
818
819 stream.write_all(&[0x05, 0x01, 0x00, 0x01]).await.unwrap();
820 match echo_addr.ip() {
821 std::net::IpAddr::V4(ip) => {
822 stream.write_all(&ip.octets()).await.unwrap();
823 }
824 std::net::IpAddr::V6(ip) => {
825 stream.write_all(&ip.octets()).await.unwrap();
826 }
827 }
828 stream
829 .write_all(&echo_addr.port().to_be_bytes())
830 .await
831 .unwrap();
832
833 let mut reply = [0u8; 10];
834 stream.read_exact(&mut reply).await.unwrap();
835 assert_eq!(reply[0], 0x05);
836 assert_eq!(reply[1], 0x00);
837
838 stream.write_all(b"hello").await.unwrap();
839 stream.shutdown().await.unwrap();
840
841 let mut buf = Vec::new();
842 stream.read_to_end(&mut buf).await.unwrap();
843 assert_eq!(&buf, b"hello");
844
845 let report = proxy_jh.await.unwrap();
846 assert!(matches!(report.outcome, execute::SessionOutcome::Completed));
847 assert_eq!(report.failure, None);
848
849 echo_jh.abort();
850 }
851
852 #[tokio::test(start_paused = true)]
853 async fn test_handshake_timeout_maps_to_failure_category() {
854 let (_client_stream, server_stream) = tokio::io::duplex(1024);
855 let boxed: eggress_core::BoxStream = Box::new(server_stream);
856 let config = test_config(direct_routing());
857
858 let task = tokio::spawn(serve_connection(boxed, config));
859
860 tokio::time::advance(Duration::from_secs(6)).await;
861
862 let report = task.await.unwrap();
863 assert!(matches!(
864 report.outcome,
865 execute::SessionOutcome::HandshakeTimedOut
866 ));
867 assert_eq!(
868 report.failure,
869 Some(execute::FailureCategory::HandshakeTimeout)
870 );
871 }
872
873 #[tokio::test]
874 async fn test_failure_category_from_session_open_error_dns() {
875 let error = SessionOpenError::Dns;
876 let category = execute::FailureCategory::from(&error);
877 assert_eq!(category, execute::FailureCategory::Dns);
878 }
879
880 #[tokio::test]
881 async fn test_failure_category_from_session_open_error_refused() {
882 let error = SessionOpenError::Refused;
883 let category = execute::FailureCategory::from(&error);
884 assert_eq!(category, execute::FailureCategory::ConnectionRefused);
885 }
886
887 #[tokio::test]
888 async fn test_failure_category_from_session_open_error_network_unreachable() {
889 let error = SessionOpenError::NetworkUnreachable;
890 let category = execute::FailureCategory::from(&error);
891 assert_eq!(category, execute::FailureCategory::NetworkUnreachable);
892 }
893
894 #[tokio::test]
895 async fn test_failure_category_from_session_open_error_host_unreachable() {
896 let error = SessionOpenError::HostUnreachable;
897 let category = execute::FailureCategory::from(&error);
898 assert_eq!(category, execute::FailureCategory::HostUnreachable);
899 }
900
901 #[tokio::test]
902 async fn test_failure_category_from_session_open_error_timeout() {
903 let error = SessionOpenError::Timeout;
904 let category = execute::FailureCategory::from(&error);
905 assert_eq!(category, execute::FailureCategory::RouteTimeout);
906 }
907
908 #[tokio::test]
909 async fn test_failure_category_from_session_open_error_upstream_auth() {
910 let error = SessionOpenError::UpstreamAuthentication;
911 let category = execute::FailureCategory::from(&error);
912 assert_eq!(category, execute::FailureCategory::UpstreamAuthentication);
913 }
914
915 #[tokio::test]
916 async fn test_failure_category_from_io_error_connection_refused() {
917 let error = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused");
918 let category = execute::FailureCategory::from_io_error(&error);
919 assert_eq!(category, execute::FailureCategory::ConnectionRefused);
920 }
921
922 #[tokio::test]
923 async fn test_failure_category_from_io_error_connection_reset() {
924 let error = std::io::Error::new(std::io::ErrorKind::ConnectionReset, "reset");
925 let category = execute::FailureCategory::from_io_error(&error);
926 assert_eq!(category, execute::FailureCategory::Relay);
927 }
928
929 #[tokio::test]
930 async fn test_failure_category_from_io_error_timeout() {
931 let error = std::io::Error::new(std::io::ErrorKind::TimedOut, "timeout");
932 let category = execute::FailureCategory::from_io_error(&error);
933 assert_eq!(category, execute::FailureCategory::Relay);
934 }
935
936 #[tokio::test]
937 async fn test_route_failure_maps_to_dns_category() {
938 let report = execute::SessionReport::open_failed(
939 SessionOpenError::Dns,
940 Some("socks5".to_string()),
941 Some("example.com:443".to_string()),
942 "direct".to_string(),
943 );
944 assert!(matches!(
945 report.outcome,
946 execute::SessionOutcome::RouteFailed
947 ));
948 assert_eq!(report.failure, Some(execute::FailureCategory::Dns));
949 }
950
951 #[tokio::test]
952 async fn test_route_failure_maps_to_connection_refused_category() {
953 let report = execute::SessionReport::open_failed(
954 SessionOpenError::Refused,
955 Some("http".to_string()),
956 Some("10.0.0.1:80".to_string()),
957 "chain(2)".to_string(),
958 );
959 assert!(matches!(
960 report.outcome,
961 execute::SessionOutcome::RouteFailed
962 ));
963 assert_eq!(
964 report.failure,
965 Some(execute::FailureCategory::ConnectionRefused)
966 );
967 }
968
969 #[tokio::test]
970 async fn test_completed_session_has_no_failure() {
971 let report = execute::SessionReport::completed(
972 Some("socks5".to_string()),
973 Some("example.com:443".to_string()),
974 "direct".to_string(),
975 1024,
976 2048,
977 );
978 assert!(matches!(report.outcome, execute::SessionOutcome::Completed));
979 assert_eq!(report.failure, None);
980 assert_eq!(report.bytes_upstream, 1024);
981 assert_eq!(report.bytes_downstream, 2048);
982 }
983
984 #[tokio::test]
985 async fn test_cancelled_session_has_cancelled_failure() {
986 let report = execute::SessionReport::cancelled(
987 Some("http".to_string()),
988 Some("example.com:80".to_string()),
989 "direct".to_string(),
990 );
991 assert!(matches!(report.outcome, execute::SessionOutcome::Cancelled));
992 assert_eq!(report.failure, Some(execute::FailureCategory::Cancelled));
993 }
994
995 #[tokio::test]
996 async fn test_authentication_failure_maps_to_failure_category() {
997 let auth = accept::InboundAuthentication::UsernamePassword {
998 username: "user".to_string(),
999 password: "secret".to_string(),
1000 };
1001
1002 let proxy_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1003 let proxy_addr = proxy_listener.local_addr().unwrap();
1004
1005 let proxy_jh = tokio::spawn(async move {
1006 let (stream, _) = proxy_listener.accept().await.unwrap();
1007 let boxed: eggress_core::BoxStream = Box::new(stream);
1008 let mut cfg = test_config(direct_routing());
1009 cfg.authentication = auth;
1010 let config = cfg;
1011 serve_connection(boxed, config).await
1012 });
1013
1014 let mut stream = tokio::net::TcpStream::connect(proxy_addr).await.unwrap();
1015 stream.write_all(&[0x05, 0x02, 0x00, 0x02]).await.unwrap();
1016 let mut response = [0u8; 2];
1017 stream.read_exact(&mut response).await.unwrap();
1018 assert_eq!(response, [0x05, 0x02]);
1019
1020 stream
1021 .write_all(&[0x01, 0x04, b'u', b's', b'e', b'r', 0x05])
1022 .await
1023 .unwrap();
1024 stream.write_all(b"wrong").await.unwrap();
1025 let mut auth_resp = [0u8; 2];
1026 stream.read_exact(&mut auth_resp).await.unwrap();
1027 assert_eq!(auth_resp, [0x01, 0x01]);
1028
1029 let report = proxy_jh.await.unwrap();
1030 assert!(matches!(
1031 report.outcome,
1032 execute::SessionOutcome::AuthenticationFailed
1033 ));
1034 assert_eq!(
1035 report.failure,
1036 Some(execute::FailureCategory::Authentication)
1037 );
1038 }
1039
1040 #[tokio::test]
1041 async fn test_reject_route_returns_403_for_http() {
1042 let rules = vec![eggress_routing::CompiledRule {
1043 id: eggress_routing::RuleId(std::sync::Arc::from("block")),
1044 matcher: eggress_routing::MatchExpr::Any,
1045 action: eggress_routing::RouteActionSpec::Reject(
1046 eggress_core::RejectReason::AccessDenied,
1047 ),
1048 }];
1049 let routing: Arc<dyn RouteService> =
1050 Arc::new(Router::new(rules, eggress_routing::RouteActionSpec::Direct));
1051
1052 let proxy_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1053 let proxy_addr = proxy_listener.local_addr().unwrap();
1054
1055 let _proxy_jh = tokio::spawn(async move {
1056 let (stream, _) = proxy_listener.accept().await.unwrap();
1057 let boxed: eggress_core::BoxStream = Box::new(stream);
1058 let config = test_config(routing);
1059 serve_connection(boxed, config).await
1060 });
1061
1062 let mut stream = tokio::net::TcpStream::connect(proxy_addr).await.unwrap();
1063 let request = "GET http://example.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n";
1064 stream.write_all(request.as_bytes()).await.unwrap();
1065
1066 let mut response = Vec::new();
1067 stream.read_to_end(&mut response).await.unwrap();
1068 let response_str = String::from_utf8_lossy(&response);
1069 assert!(
1070 response_str.contains("403"),
1071 "expected 403, got: {response_str}"
1072 );
1073 }
1074
1075 #[tokio::test]
1076 async fn test_source_cidr_matching_with_real_peer() {
1077 let (echo_addr, echo_jh) = eggress_testkit::start_echo_server().await;
1078
1079 let rules = vec![eggress_routing::CompiledRule {
1080 id: eggress_routing::RuleId(std::sync::Arc::from("allow-localhost")),
1081 matcher: eggress_routing::MatchExpr::SourceCidr("127.0.0.0/8".parse().unwrap()),
1082 action: eggress_routing::RouteActionSpec::Direct,
1083 }];
1084 let routing: Arc<dyn RouteService> =
1085 Arc::new(Router::new(rules, eggress_routing::RouteActionSpec::Direct));
1086
1087 let proxy_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1088 let proxy_addr = proxy_listener.local_addr().unwrap();
1089
1090 let proxy_jh = tokio::spawn(async move {
1091 let (stream, peer) = proxy_listener.accept().await.unwrap();
1092 let boxed: eggress_core::BoxStream = Box::new(stream);
1093 let mut cfg = test_config(routing.clone());
1094 cfg.context = ConnectionContext {
1095 source: Some(peer),
1096 listener: "test-listener".to_string(),
1097 generation: 0,
1098 };
1099 let config = cfg;
1100 serve_connection(boxed, config).await
1101 });
1102
1103 let mut stream = tokio::net::TcpStream::connect(proxy_addr).await.unwrap();
1104 stream.write_all(&[0x05, 0x01, 0x00]).await.unwrap();
1105 let mut response = [0u8; 2];
1106 stream.read_exact(&mut response).await.unwrap();
1107 assert_eq!(response, [0x05, 0x00]);
1108
1109 stream.write_all(&[0x05, 0x01, 0x00, 0x01]).await.unwrap();
1110 match echo_addr.ip() {
1111 std::net::IpAddr::V4(ip) => {
1112 stream.write_all(&ip.octets()).await.unwrap();
1113 }
1114 std::net::IpAddr::V6(ip) => {
1115 stream.write_all(&ip.octets()).await.unwrap();
1116 }
1117 }
1118 stream
1119 .write_all(&echo_addr.port().to_be_bytes())
1120 .await
1121 .unwrap();
1122
1123 let mut reply = [0u8; 10];
1124 stream.read_exact(&mut reply).await.unwrap();
1125 assert_eq!(reply[0], 0x05);
1126 assert_eq!(reply[1], 0x00);
1127
1128 stream.write_all(b"hello").await.unwrap();
1129 stream.shutdown().await.unwrap();
1130
1131 let mut buf = Vec::new();
1132 stream.read_to_end(&mut buf).await.unwrap();
1133 assert_eq!(&buf, b"hello");
1134
1135 let report = proxy_jh.await.unwrap();
1136 assert!(matches!(report.outcome, execute::SessionOutcome::Completed));
1137
1138 echo_jh.abort();
1139 }
1140
1141 #[tokio::test]
1142 async fn test_http_expectation_is_rejected_with_417_and_connection_close() {
1143 let proxy_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1144 let proxy_addr = proxy_listener.local_addr().unwrap();
1145
1146 let proxy_jh = tokio::spawn(async move {
1147 let (stream, _) = proxy_listener.accept().await.unwrap();
1148 let boxed: eggress_core::BoxStream = Box::new(stream);
1149 let config = test_config(direct_routing());
1150 serve_connection(boxed, config).await
1151 });
1152
1153 let mut stream = tokio::net::TcpStream::connect(proxy_addr).await.unwrap();
1154 stream
1155 .write_all(
1156 b"POST http://127.0.0.1:1/ HTTP/1.1\r\nHost: 127.0.0.1:1\r\nExpect: 100-continue\r\nContent-Length: 1048576\r\n\r\n",
1157 )
1158 .await
1159 .unwrap();
1160
1161 let mut response = Vec::new();
1162 tokio::time::timeout(Duration::from_secs(1), stream.read_to_end(&mut response))
1163 .await
1164 .expect("expectation rejection must be bounded")
1165 .unwrap();
1166 assert!(String::from_utf8_lossy(&response).starts_with("HTTP/1.1 417 Expectation Failed"));
1167
1168 let report = proxy_jh.await.unwrap();
1169 assert!(matches!(
1170 report.outcome,
1171 execute::SessionOutcome::ClientProtocolError
1172 ));
1173 }
1174
1175 #[tokio::test]
1176 async fn test_http_body_upload_is_not_limited_by_connect_timeout() {
1177 let origin_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1178 let origin_addr = origin_listener.local_addr().unwrap();
1179 let origin_jh = tokio::spawn(async move {
1180 let (stream, _) = origin_listener.accept().await.unwrap();
1181 let mut reader = BufReader::new(stream);
1182 let mut line = Vec::new();
1183 loop {
1184 line.clear();
1185 reader.read_until(b'\n', &mut line).await.unwrap();
1186 if line == b"\r\n" {
1187 break;
1188 }
1189 }
1190 let mut body = [0u8; 16];
1191 reader.read_exact(&mut body).await.unwrap();
1192 let mut stream = reader.into_inner();
1193 stream
1194 .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: close\r\n\r\n")
1195 .await
1196 .unwrap();
1197 });
1198
1199 let proxy_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1200 let proxy_addr = proxy_listener.local_addr().unwrap();
1201 let proxy_jh = tokio::spawn(async move {
1202 let (stream, _) = proxy_listener.accept().await.unwrap();
1203 let boxed: eggress_core::BoxStream = Box::new(stream);
1204 let mut config = test_config(direct_routing());
1205 config.connect_timeout = Duration::from_millis(50);
1206 serve_connection(boxed, config).await
1207 });
1208
1209 let mut stream = tokio::net::TcpStream::connect(proxy_addr).await.unwrap();
1210 let request = format!(
1211 "POST http://{}:{}/ HTTP/1.1\r\nHost: {}:{}\r\nContent-Length: 16\r\n\r\n",
1212 origin_addr.ip(),
1213 origin_addr.port(),
1214 origin_addr.ip(),
1215 origin_addr.port()
1216 );
1217 stream.write_all(request.as_bytes()).await.unwrap();
1218 tokio::time::sleep(Duration::from_millis(100)).await;
1219 stream.write_all(b"delayed-body-123").await.unwrap();
1220
1221 let mut response = Vec::new();
1222 tokio::time::timeout(Duration::from_secs(1), stream.read_to_end(&mut response))
1223 .await
1224 .expect("body upload must not inherit the connect timeout")
1225 .unwrap();
1226 let report = proxy_jh.await.unwrap();
1227 assert!(matches!(report.outcome, execute::SessionOutcome::Completed));
1228
1229 origin_jh.abort();
1230 }
1231}
1232
1233#[cfg(test)]
1237mod metrics_lifecycle_tests {
1238 use super::*;
1239 use eggress_routing::{RouteActionSpec, Router};
1240 use std::sync::atomic::{AtomicUsize, Ordering};
1241 use std::sync::Mutex;
1242 use tokio::io::{AsyncReadExt, AsyncWriteExt};
1243
1244 struct RecordingMetrics {
1246 starts: AtomicUsize,
1247 terminals: AtomicUsize,
1248 auth_failures: AtomicUsize,
1249 terminal_reports: Mutex<Vec<execute::SessionOutcome>>,
1250 }
1251
1252 impl RecordingMetrics {
1253 fn new() -> Self {
1254 Self {
1255 starts: AtomicUsize::new(0),
1256 terminals: AtomicUsize::new(0),
1257 auth_failures: AtomicUsize::new(0),
1258 terminal_reports: Mutex::new(Vec::new()),
1259 }
1260 }
1261 }
1262
1263 impl SessionMetrics for RecordingMetrics {
1264 fn record_session_start(&self) {
1265 self.starts.fetch_add(1, Ordering::SeqCst);
1266 }
1267 fn record_session(&self, report: &SessionReport) {
1268 self.terminals.fetch_add(1, Ordering::SeqCst);
1269 self.terminal_reports
1270 .lock()
1271 .unwrap()
1272 .push(std::mem::replace(
1273 &mut report.outcome.clone_outcome(),
1274 execute::SessionOutcome::Completed,
1275 ));
1276 }
1277 fn record_auth_failure(&self) {
1278 self.auth_failures.fetch_add(1, Ordering::SeqCst);
1279 }
1280 fn record_route_decision(&self, _: &str, _: &str, _: &str) {}
1281 fn record_upstream_open(&self, _: &str, _: &str) {}
1282 fn record_upstream_failure(&self, _: &str, _: &str) {}
1283 }
1284
1285 impl execute::SessionOutcome {
1286 fn clone_outcome(&self) -> Self {
1287 match self {
1288 Self::Completed => Self::Completed,
1289 Self::ClientProtocolError => Self::ClientProtocolError,
1290 Self::AuthenticationFailed => Self::AuthenticationFailed,
1291 Self::HandshakeTimedOut => Self::HandshakeTimedOut,
1292 Self::RouteFailed => Self::RouteFailed,
1293 Self::RelayFailed => Self::RelayFailed,
1294 Self::Cancelled => Self::Cancelled,
1295 }
1296 }
1297 }
1298
1299 fn test_direct_routing() -> Arc<dyn RouteService> {
1300 Arc::new(Router::new(vec![], RouteActionSpec::Direct))
1301 }
1302
1303 fn test_all_protocols() -> Arc<[eggress_core::ProtocolId]> {
1304 Arc::from([
1305 eggress_core::ProtocolId::Http,
1306 eggress_core::ProtocolId::Socks4,
1307 eggress_core::ProtocolId::Socks5,
1308 eggress_core::ProtocolId::Http2,
1309 eggress_core::ProtocolId::WebSocket,
1310 eggress_core::ProtocolId::Raw,
1311 ])
1312 }
1313
1314 fn metrics_config(metrics: Arc<RecordingMetrics>) -> ConnectionConfig {
1315 ConnectionConfig {
1316 routing: test_direct_routing(),
1317 context: ConnectionContext::default(),
1318 handshake_timeout: Duration::from_secs(5),
1319 connect_timeout: Duration::from_secs(10),
1320 protocols: test_all_protocols(),
1321 authentication: accept::InboundAuthentication::None,
1322 metrics: Some(metrics),
1323 udp: None,
1324 tls_client_config: None,
1325 shadowsocks: None,
1326 shadowsocks_metrics: None,
1327 trojan: None,
1328 fixed_target: None,
1329 local_bind: None,
1330 }
1331 }
1332
1333 #[tokio::test]
1334 async fn metrics_balanced_after_successful_session() {
1335 let (echo_addr, echo_jh) = eggress_testkit::start_echo_server().await;
1336 let metrics = Arc::new(RecordingMetrics::new());
1337 let proxy_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1338 let proxy_addr = proxy_listener.local_addr().unwrap();
1339 let m = metrics.clone();
1340 let proxy_jh = tokio::spawn(async move {
1341 let (stream, _) = proxy_listener.accept().await.unwrap();
1342 let boxed: eggress_core::BoxStream = Box::new(stream);
1343 let config = metrics_config(m);
1344 serve_connection(boxed, config).await
1345 });
1346
1347 let mut stream = tokio::net::TcpStream::connect(proxy_addr).await.unwrap();
1348 stream.write_all(&[0x05, 0x01, 0x00]).await.unwrap();
1349 let mut response = [0u8; 2];
1350 stream.read_exact(&mut response).await.unwrap();
1351 assert_eq!(response, [0x05, 0x00]);
1352
1353 stream.write_all(&[0x05, 0x01, 0x00, 0x01]).await.unwrap();
1354 match echo_addr.ip() {
1355 std::net::IpAddr::V4(ip) => stream.write_all(&ip.octets()).await.unwrap(),
1356 std::net::IpAddr::V6(ip) => stream.write_all(&ip.octets()).await.unwrap(),
1357 }
1358 stream
1359 .write_all(&echo_addr.port().to_be_bytes())
1360 .await
1361 .unwrap();
1362
1363 let mut reply = [0u8; 10];
1364 stream.read_exact(&mut reply).await.unwrap();
1365 stream.write_all(b"hello").await.unwrap();
1366 stream.shutdown().await.unwrap();
1367 let mut buf = Vec::new();
1368 stream.read_to_end(&mut buf).await.unwrap();
1369
1370 let report = proxy_jh.await.unwrap();
1371 assert!(matches!(report.outcome, execute::SessionOutcome::Completed));
1372
1373 let m = &*metrics;
1374 assert_eq!(m.starts.load(Ordering::SeqCst), 1, "expected 1 start");
1375 assert_eq!(m.terminals.load(Ordering::SeqCst), 1, "expected 1 terminal");
1376 assert_eq!(
1377 m.auth_failures.load(Ordering::SeqCst),
1378 0,
1379 "no auth failures"
1380 );
1381 echo_jh.abort();
1382 }
1383
1384 #[tokio::test]
1385 async fn metrics_balanced_after_auth_failure() {
1386 let auth = accept::InboundAuthentication::UsernamePassword {
1387 username: "user".to_string(),
1388 password: "secret".to_string(),
1389 };
1390 let metrics = Arc::new(RecordingMetrics::new());
1391 let proxy_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1392 let proxy_addr = proxy_listener.local_addr().unwrap();
1393 let m = metrics.clone();
1394 let proxy_jh = tokio::spawn(async move {
1395 let (stream, _) = proxy_listener.accept().await.unwrap();
1396 let boxed: eggress_core::BoxStream = Box::new(stream);
1397 let mut cfg = metrics_config(m);
1398 cfg.authentication = auth;
1399 serve_connection(boxed, cfg).await
1400 });
1401
1402 let mut stream = tokio::net::TcpStream::connect(proxy_addr).await.unwrap();
1403 stream.write_all(&[0x05, 0x02, 0x00, 0x02]).await.unwrap();
1404 let mut response = [0u8; 2];
1405 stream.read_exact(&mut response).await.unwrap();
1406 assert_eq!(response, [0x05, 0x02]);
1407
1408 stream
1409 .write_all(&[0x01, 0x04, b'u', b's', b'e', b'r', 0x05])
1410 .await
1411 .unwrap();
1412 stream.write_all(b"wrong").await.unwrap();
1413 let mut auth_resp = [0u8; 2];
1414 stream.read_exact(&mut auth_resp).await.unwrap();
1415
1416 let report = proxy_jh.await.unwrap();
1417 assert!(matches!(
1418 report.outcome,
1419 execute::SessionOutcome::AuthenticationFailed
1420 ));
1421
1422 let m = &*metrics;
1423 assert_eq!(m.starts.load(Ordering::SeqCst), 1);
1424 assert_eq!(m.terminals.load(Ordering::SeqCst), 1);
1425 assert_eq!(m.auth_failures.load(Ordering::SeqCst), 1);
1426 }
1427
1428 #[tokio::test]
1429 async fn metrics_balanced_after_protocol_error() {
1430 let metrics = Arc::new(RecordingMetrics::new());
1431 let proxy_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1432 let proxy_addr = proxy_listener.local_addr().unwrap();
1433 let m = metrics.clone();
1434 let proxy_jh = tokio::spawn(async move {
1435 let (stream, _) = proxy_listener.accept().await.unwrap();
1436 let boxed: eggress_core::BoxStream = Box::new(stream);
1437 serve_connection(boxed, metrics_config(m)).await
1438 });
1439
1440 let mut stream = tokio::net::TcpStream::connect(proxy_addr).await.unwrap();
1441 stream.write_all(b"garbage data").await.unwrap();
1442 stream.shutdown().await.unwrap();
1443
1444 let report = proxy_jh.await.unwrap();
1445 assert!(matches!(
1446 report.outcome,
1447 execute::SessionOutcome::ClientProtocolError
1448 ));
1449
1450 let m = &*metrics;
1451 assert_eq!(m.starts.load(Ordering::SeqCst), 1);
1452 assert_eq!(m.terminals.load(Ordering::SeqCst), 1);
1453 assert_eq!(m.auth_failures.load(Ordering::SeqCst), 0);
1454 }
1455
1456 #[tokio::test(start_paused = true)]
1457 async fn metrics_balanced_after_handshake_timeout() {
1458 let metrics = Arc::new(RecordingMetrics::new());
1459 let (_client_stream, server_stream) = tokio::io::duplex(1024);
1460 let boxed: eggress_core::BoxStream = Box::new(server_stream);
1461 let task = tokio::spawn(serve_connection(boxed, metrics_config(metrics.clone())));
1462 tokio::time::advance(Duration::from_secs(6)).await;
1463
1464 let report = task.await.unwrap();
1465 assert!(matches!(
1466 report.outcome,
1467 execute::SessionOutcome::HandshakeTimedOut
1468 ));
1469
1470 let m = &*metrics;
1471 assert_eq!(m.starts.load(Ordering::SeqCst), 1);
1472 assert_eq!(m.terminals.load(Ordering::SeqCst), 1);
1473 assert_eq!(m.auth_failures.load(Ordering::SeqCst), 0);
1474 }
1475
1476 #[tokio::test]
1477 async fn no_double_finalization_for_route_failure() {
1478 let rules = vec![eggress_routing::CompiledRule {
1479 id: eggress_routing::RuleId(std::sync::Arc::from("block")),
1480 matcher: eggress_routing::MatchExpr::Any,
1481 action: eggress_routing::RouteActionSpec::Reject(
1482 eggress_core::RejectReason::AccessDenied,
1483 ),
1484 }];
1485 let routing: Arc<dyn eggress_routing::RouteService> =
1486 Arc::new(Router::new(rules, eggress_routing::RouteActionSpec::Direct));
1487 let metrics = Arc::new(RecordingMetrics::new());
1488 let proxy_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1489 let proxy_addr = proxy_listener.local_addr().unwrap();
1490 let m = metrics.clone();
1491 let proxy_jh = tokio::spawn(async move {
1492 let (stream, _) = proxy_listener.accept().await.unwrap();
1493 let boxed: eggress_core::BoxStream = Box::new(stream);
1494 let mut cfg = metrics_config(m);
1495 cfg.routing = routing;
1496 serve_connection(boxed, cfg).await
1497 });
1498
1499 let mut stream = tokio::net::TcpStream::connect(proxy_addr).await.unwrap();
1500 let request = "GET http://example.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n";
1501 stream.write_all(request.as_bytes()).await.unwrap();
1502 let mut response = Vec::new();
1503 stream.read_to_end(&mut response).await.unwrap();
1504
1505 let report = proxy_jh.await.unwrap();
1506 assert!(matches!(
1507 report.outcome,
1508 execute::SessionOutcome::RouteFailed | execute::SessionOutcome::ClientProtocolError
1509 ));
1510
1511 let m = &*metrics;
1512 assert_eq!(m.starts.load(Ordering::SeqCst), 1);
1513 assert_eq!(m.terminals.load(Ordering::SeqCst), 1);
1514 }
1515}
1516
1517#[cfg(all(test, not(feature = "extended")))]
1523mod lean_negative_tests {
1524 use super::*;
1525 use eggress_core::ProtocolId;
1526 use tokio::io::{AsyncReadExt, AsyncWriteExt};
1527
1528 #[tokio::test]
1529 async fn lean_rejects_shadowsocks_accept() {
1530 use eggress_routing::{RouteActionSpec, Router};
1531
1532 let routing: Arc<dyn eggress_routing::RouteService> =
1533 Arc::new(Router::new(vec![], RouteActionSpec::Direct));
1534 let protocols: Arc<[ProtocolId]> = Arc::from([ProtocolId::Shadowsocks]);
1535 let (_client, server) = tokio::io::duplex(1024);
1536 let boxed: eggress_core::BoxStream = Box::new(server);
1537
1538 let config = ConnectionConfig {
1539 routing,
1540 context: ConnectionContext::default(),
1541 handshake_timeout: Duration::from_secs(2),
1542 connect_timeout: Duration::from_secs(5),
1543 protocols,
1544 authentication: accept::InboundAuthentication::None,
1545 metrics: None,
1546 udp: None,
1547 tls_client_config: None,
1548 shadowsocks: Some(accept::InboundShadowsocksConfig {
1549 method: "aes-256-gcm".to_string(),
1550 password: "test-password".to_string(),
1551 }),
1552 shadowsocks_metrics: None,
1553 trojan: None,
1554 fixed_target: None,
1555 local_bind: None,
1556 };
1557
1558 let report = serve_connection(boxed, config).await;
1559 assert!(
1562 matches!(
1563 report.outcome,
1564 execute::SessionOutcome::ClientProtocolError
1565 | execute::SessionOutcome::HandshakeTimedOut
1566 ),
1567 "shadowsocks should fail in lean build, got: {:?}",
1568 report.outcome
1569 );
1570 }
1571
1572 #[tokio::test]
1573 async fn lean_rejects_trojan_accept() {
1574 use eggress_routing::{RouteActionSpec, Router};
1575
1576 let routing: Arc<dyn eggress_routing::RouteService> =
1577 Arc::new(Router::new(vec![], RouteActionSpec::Direct));
1578 let protocols: Arc<[ProtocolId]> = Arc::from([ProtocolId::Trojan]);
1579 let (_client, server) = tokio::io::duplex(1024);
1580 let boxed: eggress_core::BoxStream = Box::new(server);
1581
1582 let config = ConnectionConfig {
1583 routing,
1584 context: ConnectionContext::default(),
1585 handshake_timeout: Duration::from_secs(2),
1586 connect_timeout: Duration::from_secs(5),
1587 protocols,
1588 authentication: accept::InboundAuthentication::None,
1589 metrics: None,
1590 udp: None,
1591 tls_client_config: None,
1592 shadowsocks: None,
1593 shadowsocks_metrics: None,
1594 trojan: Some(accept::InboundTrojanConfig {
1595 password: "test-password".to_string(),
1596 fallback: None,
1597 }),
1598 fixed_target: None,
1599 local_bind: None,
1600 };
1601
1602 let report = serve_connection(boxed, config).await;
1603 assert!(
1606 matches!(
1607 report.outcome,
1608 execute::SessionOutcome::ClientProtocolError
1609 | execute::SessionOutcome::HandshakeTimedOut
1610 ),
1611 "trojan should fail in lean build, got: {:?}",
1612 report.outcome
1613 );
1614 }
1615
1616 #[tokio::test]
1617 async fn lean_rejects_websocket_accept() {
1618 use eggress_routing::{RouteActionSpec, Router};
1619
1620 let routing: Arc<dyn eggress_routing::RouteService> =
1621 Arc::new(Router::new(vec![], RouteActionSpec::Direct));
1622 let protocols: Arc<[ProtocolId]> = Arc::from([ProtocolId::WebSocket]);
1623 let (mut client, server) = tokio::io::duplex(1024);
1624 let boxed: eggress_core::BoxStream = Box::new(server);
1625 let config = ConnectionConfig {
1626 routing,
1627 context: ConnectionContext::default(),
1628 handshake_timeout: Duration::from_secs(2),
1629 connect_timeout: Duration::from_secs(5),
1630 protocols,
1631 authentication: accept::InboundAuthentication::None,
1632 metrics: None,
1633 udp: None,
1634 tls_client_config: None,
1635 shadowsocks: None,
1636 shadowsocks_metrics: None,
1637 trojan: None,
1638 fixed_target: None,
1639 local_bind: None,
1640 };
1641 client.write_all(&[0xff]).await.unwrap();
1642
1643 let report = serve_connection(boxed, config).await;
1644 assert!(matches!(
1645 report.outcome,
1646 execute::SessionOutcome::ClientProtocolError
1647 ));
1648 }
1649
1650 #[tokio::test]
1651 async fn lean_serves_http_and_socks_normally() {
1652 let (echo_addr, echo_jh) = eggress_testkit::start_echo_server().await;
1653 let proxy_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1654 let proxy_addr = proxy_listener.local_addr().unwrap();
1655
1656 let proxy_jh = tokio::spawn(async move {
1657 let (stream, _) = proxy_listener.accept().await.unwrap();
1658 let boxed: eggress_core::BoxStream = Box::new(stream);
1659 let routing: Arc<dyn eggress_routing::RouteService> = Arc::new(
1660 eggress_routing::Router::new(vec![], eggress_routing::RouteActionSpec::Direct),
1661 );
1662 let config = ConnectionConfig {
1663 routing,
1664 context: ConnectionContext::default(),
1665 handshake_timeout: Duration::from_secs(5),
1666 connect_timeout: Duration::from_secs(10),
1667 protocols: Arc::from([ProtocolId::Http, ProtocolId::Socks4, ProtocolId::Socks5]),
1668 authentication: accept::InboundAuthentication::None,
1669 metrics: None,
1670 udp: None,
1671 tls_client_config: None,
1672 shadowsocks: None,
1673 shadowsocks_metrics: None,
1674 trojan: None,
1675 fixed_target: None,
1676 local_bind: None,
1677 };
1678 serve_connection(boxed, config).await
1679 });
1680
1681 let mut stream = tokio::net::TcpStream::connect(proxy_addr).await.unwrap();
1682 stream.write_all(&[0x05, 0x01, 0x00]).await.unwrap();
1683 let mut response = [0u8; 2];
1684 stream.read_exact(&mut response).await.unwrap();
1685 assert_eq!(response, [0x05, 0x00]);
1686
1687 stream.write_all(&[0x05, 0x01, 0x00, 0x01]).await.unwrap();
1688 match echo_addr.ip() {
1689 std::net::IpAddr::V4(ip) => {
1690 stream.write_all(&ip.octets()).await.unwrap();
1691 }
1692 std::net::IpAddr::V6(ip) => {
1693 stream.write_all(&ip.octets()).await.unwrap();
1694 }
1695 }
1696 stream
1697 .write_all(&echo_addr.port().to_be_bytes())
1698 .await
1699 .unwrap();
1700
1701 let mut reply = [0u8; 10];
1702 stream.read_exact(&mut reply).await.unwrap();
1703 assert_eq!(reply[0], 0x05);
1704 assert_eq!(reply[1], 0x00);
1705
1706 stream.write_all(b"hello").await.unwrap();
1707 stream.shutdown().await.unwrap();
1708
1709 let mut buf = Vec::new();
1710 stream.read_to_end(&mut buf).await.unwrap();
1711 assert_eq!(&buf, b"hello");
1712
1713 let report = proxy_jh.await.unwrap();
1714 assert!(matches!(report.outcome, execute::SessionOutcome::Completed));
1715
1716 echo_jh.abort();
1717 }
1718}