use std::io::Write;
use std::sync::atomic::Ordering;
use std::time::Duration;
use tempfile::NamedTempFile;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
fn write_config(content: &str) -> NamedTempFile {
let mut f = NamedTempFile::new().unwrap();
f.write_all(content.as_bytes()).unwrap();
f.flush().unwrap();
f
}
async fn wait_ready(state: &eggress_runtime::RuntimeState) {
for _ in 0..100 {
if state.readiness.load(Ordering::Relaxed) {
return;
}
tokio::time::sleep(Duration::from_millis(50)).await;
}
panic!("timeout waiting for readiness");
}
fn get_admin_addr(state: &eggress_runtime::RuntimeState) -> String {
state
.admin_local_addr
.lock()
.unwrap()
.expect("admin should have bound")
.to_string()
}
fn get_listener_addr(state: &eggress_runtime::RuntimeState) -> std::net::SocketAddr {
let addrs = state.listener_addrs.lock().unwrap();
addrs[0].unwrap()
}
async fn start_tcp_echo() -> std::net::SocketAddr {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
loop {
let (mut stream, _) = match listener.accept().await {
Ok(s) => s,
Err(_) => break,
};
tokio::spawn(async move {
let mut buf = [0u8; 4096];
loop {
match stream.read(&mut buf).await {
Ok(0) => break,
Ok(n) => {
if stream.write_all(&buf[..n]).await.is_err() {
break;
}
}
Err(_) => break,
}
}
});
}
});
addr
}
async fn start_socks5_tcp_upstream() -> std::net::SocketAddr {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
loop {
let (mut stream, _) = match listener.accept().await {
Ok(s) => s,
Err(_) => break,
};
tokio::spawn(async move {
let mut header = [0u8; 2];
if stream.read_exact(&mut header).await.is_err() {
return;
}
let nmethods = header[1] as usize;
let mut methods = vec![0u8; nmethods];
let _ = stream.read_exact(&mut methods).await;
let _ = stream.write_all(&[0x05, 0x00]).await;
let mut req = [0u8; 4];
if stream.read_exact(&mut req).await.is_err() {
return;
}
let cmd = req[1];
let atyp = req[3];
if cmd != 0x01 {
let _ = stream
.write_all(&[0x05, 0x07, 0x00, 0x01, 0, 0, 0, 0, 0, 0])
.await;
return;
}
let target_addr = match atyp {
0x01 => {
let mut addr = [0u8; 4];
if stream.read_exact(&mut addr).await.is_err() {
return;
}
let port = stream.read_u16().await.unwrap_or(0);
format!("{}.{}.{}.{}:{}", addr[0], addr[1], addr[2], addr[3], port)
}
0x03 => {
let len = stream.read_u8().await.unwrap_or(0) as usize;
let mut domain = vec![0u8; len];
if stream.read_exact(&mut domain).await.is_err() {
return;
}
let port = stream.read_u16().await.unwrap_or(0);
let domain = String::from_utf8_lossy(&domain);
format!("{}:{}", domain, port)
}
_ => return,
};
let target = match tokio::net::TcpStream::connect(&target_addr).await {
Ok(t) => t,
Err(_) => {
let _ = stream
.write_all(&[0x05, 0x01, 0x00, 0x01, 0, 0, 0, 0, 0, 0])
.await;
return;
}
};
let _ = stream
.write_all(&[0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0])
.await;
let (mut cr, mut cw) = stream.into_split();
let (mut tr, mut tw) = target.into_split();
let c2t = tokio::spawn(async move {
let _ = tokio::io::copy(&mut cr, &mut tw).await;
let _ = tw.shutdown().await;
});
let t2c = tokio::spawn(async move {
let _ = tokio::io::copy(&mut tr, &mut cw).await;
let _ = cw.shutdown().await;
});
let _ = tokio::join!(c2t, t2c);
});
}
});
addr
}
fn metric_value(body: &str, name: &str) -> Option<f64> {
let candidates = [name.to_string(), format!("{name}_total")];
let mut total = 0.0;
let mut found = false;
for line in body.lines() {
if line.starts_with('#') {
continue;
}
if !candidates.iter().any(|n| matches_name(line, n)) {
continue;
}
if let Some(v) = line_value(line) {
total += v;
found = true;
}
}
found.then_some(total)
}
fn metric_value_with_labels(body: &str, name: &str, labels: &[(&str, &str)]) -> Option<f64> {
let candidates = [name.to_string(), format!("{name}_total")];
for line in body.lines() {
if line.starts_with('#') {
continue;
}
let Some(cand) = candidates.iter().find(|n| matches_name(line, n)) else {
continue;
};
let prefix = format!("{cand}{{");
let Some(rest) = line.strip_prefix(&prefix) else {
continue;
};
let Some(brace) = rest.find('}') else {
continue;
};
let label_section = &rest[..brace];
let value_section = &rest[brace + 1..];
let all_present = labels.iter().all(|(k, v)| {
let needle = format!("{k}=\"{v}\"");
label_section.contains(&needle)
});
if !all_present {
continue;
}
if let Ok(v) = value_section.split_whitespace().last()?.parse::<f64>() {
return Some(v);
}
}
None
}
fn matches_name(line: &str, name: &str) -> bool {
if let Some(rest) = line.strip_prefix(name) {
rest.starts_with(' ') || rest.starts_with('{')
} else {
false
}
}
fn line_value(line: &str) -> Option<f64> {
line.split_whitespace().last()?.parse::<f64>().ok()
}
async fn http_get(addr: &str, path: &str) -> (u16, String) {
let mut stream = tokio::net::TcpStream::connect(addr).await.unwrap();
let request = format!("GET {path} HTTP/1.1\r\nHost: {addr}\r\nConnection: close\r\n\r\n");
tokio::io::AsyncWriteExt::write_all(&mut stream, request.as_bytes())
.await
.unwrap();
tokio::io::AsyncWriteExt::flush(&mut stream).await.unwrap();
let mut response = Vec::new();
loop {
let mut buf = [0u8; 4096];
match tokio::io::AsyncReadExt::read(&mut stream, &mut buf).await {
Ok(0) => break,
Ok(n) => response.extend_from_slice(&buf[..n]),
Err(_) => break,
}
}
let response = String::from_utf8_lossy(&response);
let status_line = response.lines().next().unwrap_or("");
let status = status_line
.split_whitespace()
.nth(1)
.and_then(|s| s.parse::<u16>().ok())
.unwrap_or(0);
let body = response.split("\r\n\r\n").nth(1).unwrap_or("").to_string();
(status, body)
}
async fn socks5_connect(stream: &mut tokio::net::TcpStream, target: std::net::SocketAddr) -> bool {
let ip_octets = match target.ip() {
std::net::IpAddr::V4(ip) => ip.octets(),
_ => panic!("expected IPv4"),
};
stream.write_all(&[0x05, 0x01, 0x00]).await.unwrap();
let mut resp = [0u8; 2];
stream.read_exact(&mut resp).await.unwrap();
if resp != [0x05, 0x00] {
return false;
}
stream.write_all(&[0x05, 0x01, 0x00, 0x01]).await.unwrap();
stream.write_all(&ip_octets).await.unwrap();
stream
.write_all(&target.port().to_be_bytes())
.await
.unwrap();
let mut reply = [0u8; 10];
stream.read_exact(&mut reply).await.unwrap();
reply[1] == 0x00
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn round_robin_distribution_across_sessions() {
let echo_addr = start_tcp_echo().await;
let upstream1 = start_socks5_tcp_upstream().await;
let upstream2 = start_socks5_tcp_upstream().await;
let config = format!(
r#"
version = 1
[[listeners]]
name = "socks-in"
bind = "127.0.0.1:0"
protocols = ["socks5"]
[[upstreams]]
id = "up-1"
uri = "socks5://127.0.0.1:{port1}"
[[upstreams]]
id = "up-2"
uri = "socks5://127.0.0.1:{port2}"
[[upstream_groups]]
id = "rr-grp"
scheduler = "round-robin"
members = ["up-1", "up-2"]
fallback = "reject"
[[rules]]
id = "route-all"
upstream_group = "rr-grp"
[admin]
bind = "127.0.0.1:0"
enabled = true
"#,
port1 = upstream1.port(),
port2 = upstream2.port(),
);
let f = write_config(&config);
let path = f.path().to_str().unwrap();
let mut sup = eggress_runtime::ServiceSupervisor::start(path).unwrap();
let state = sup.state().clone();
let token = sup.shutdown_token();
let jh = tokio::task::spawn_blocking(move || sup.run());
wait_ready(&state).await;
let listener_addr = get_listener_addr(&state);
let admin_addr = get_admin_addr(&state);
for _ in 0..4 {
let mut stream = tokio::net::TcpStream::connect(listener_addr)
.await
.expect("connect");
let ok = socks5_connect(&mut stream, echo_addr).await;
assert!(ok, "SOCKS5 CONNECT should succeed");
stream.write_all(b"test").await.unwrap();
let mut buf = [0u8; 4096];
let _ = tokio::time::timeout(Duration::from_secs(2), stream.read(&mut buf)).await;
drop(stream);
tokio::time::sleep(Duration::from_millis(50)).await;
}
tokio::time::sleep(Duration::from_millis(200)).await;
let (status, body) = http_get(&admin_addr, "/metrics").await;
assert_eq!(status, 200);
let connections_total = metric_value(&body, "eggress_connections_total")
.expect("eggress_connections_total should be present");
assert!(
connections_total >= 4.0,
"should have at least 4 connections, got {connections_total}"
);
token.cancel();
jh.await.ok();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn least_connections_chooses_lower_active_count() {
let echo_addr = start_tcp_echo().await;
let upstream = start_socks5_tcp_upstream().await;
let config = format!(
r#"
version = 1
[[listeners]]
name = "socks-in"
bind = "127.0.0.1:0"
protocols = ["socks5"]
[[upstreams]]
id = "lc-up"
uri = "socks5://127.0.0.1:{port}"
[[upstream_groups]]
id = "lc-grp"
scheduler = "least-connections"
members = ["lc-up"]
fallback = "reject"
[[rules]]
id = "route-all"
upstream_group = "lc-grp"
[admin]
bind = "127.0.0.1:0"
enabled = true
"#,
port = upstream.port(),
);
let f = write_config(&config);
let path = f.path().to_str().unwrap();
let mut sup = eggress_runtime::ServiceSupervisor::start(path).unwrap();
let state = sup.state().clone();
let token = sup.shutdown_token();
let jh = tokio::task::spawn_blocking(move || sup.run());
wait_ready(&state).await;
let listener_addr = get_listener_addr(&state);
let admin_addr = get_admin_addr(&state);
let mut handles = Vec::new();
for _ in 0..3 {
let addr = listener_addr;
let echo = echo_addr;
handles.push(tokio::spawn(async move {
let mut stream = tokio::net::TcpStream::connect(addr).await.expect("connect");
let ok = socks5_connect(&mut stream, echo).await;
assert!(ok, "SOCKS5 CONNECT should succeed");
stream.write_all(b"hello").await.unwrap();
let mut buf = [0u8; 4096];
let _ = tokio::time::timeout(Duration::from_secs(2), stream.read(&mut buf)).await;
drop(stream);
}));
}
for h in handles {
h.await.unwrap();
}
tokio::time::sleep(Duration::from_millis(200)).await;
let (status, body) = http_get(&admin_addr, "/metrics").await;
assert_eq!(status, 200);
let connections_total = metric_value(&body, "eggress_connections_total")
.expect("eggress_connections_total should be present");
assert!(
connections_total >= 3.0,
"should have at least 3 connections, got {connections_total}"
);
token.cancel();
jh.await.ok();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn direct_fallback_only_when_configured() {
let echo_addr = start_tcp_echo().await;
let config = r#"
version = 1
[[listeners]]
name = "socks-in"
bind = "127.0.0.1:0"
protocols = ["socks5"]
[[rules]]
id = "route-all"
any = true
direct = true
[admin]
bind = "127.0.0.1:0"
enabled = true
"#;
let f = write_config(config);
let path = f.path().to_str().unwrap();
let mut sup = eggress_runtime::ServiceSupervisor::start(path).unwrap();
let state = sup.state().clone();
let token = sup.shutdown_token();
let jh = tokio::task::spawn_blocking(move || sup.run());
wait_ready(&state).await;
let listener_addr = get_listener_addr(&state);
let admin_addr = get_admin_addr(&state);
let mut stream = tokio::net::TcpStream::connect(listener_addr)
.await
.expect("connect");
let ok = tokio::time::timeout(
Duration::from_secs(5),
socks5_connect(&mut stream, echo_addr),
)
.await
.expect("SOCKS5 connect timed out");
assert!(ok, "CONNECT should succeed via direct route");
stream.write_all(b"fallback-test").await.unwrap();
let mut buf = [0u8; 4096];
let n = tokio::time::timeout(Duration::from_secs(2), async {
stream.read(&mut buf).await
})
.await
.expect("timeout")
.expect("read");
assert_eq!(&buf[..n], b"fallback-test");
drop(stream);
tokio::time::sleep(Duration::from_millis(200)).await;
let (status, body) = http_get(&admin_addr, "/metrics").await;
assert_eq!(status, 200);
let route_direct = metric_value_with_labels(
&body,
"eggress_route_decisions_total",
&[("action", "direct")],
);
assert!(
route_direct.is_some_and(|v| v >= 1.0),
"direct route decisions should be >= 1, got {route_direct:?}"
);
token.cancel();
jh.await.ok();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn reject_when_all_upstreams_fail_and_fallback_reject() {
let config = r#"
version = 1
[[listeners]]
name = "socks-in"
bind = "127.0.0.1:0"
protocols = ["socks5"]
[[upstreams]]
id = "bad-up"
uri = "socks5://127.0.0.1:1"
[[upstream_groups]]
id = "rej-grp"
scheduler = "first-available"
members = ["bad-up"]
fallback = "reject"
[[rules]]
id = "route-all"
upstream_group = "rej-grp"
[admin]
bind = "127.0.0.1:0"
enabled = true
"#;
let f = write_config(config);
let path = f.path().to_str().unwrap();
let mut sup = eggress_runtime::ServiceSupervisor::start(path).unwrap();
let state = sup.state().clone();
let token = sup.shutdown_token();
let jh = tokio::task::spawn_blocking(move || sup.run());
wait_ready(&state).await;
let listener_addr = get_listener_addr(&state);
let mut stream = tokio::net::TcpStream::connect(listener_addr)
.await
.expect("connect");
stream.write_all(&[0x05, 0x01, 0x00]).await.unwrap();
let mut resp = [0u8; 2];
stream.read_exact(&mut resp).await.unwrap();
assert_eq!(resp, [0x05, 0x00]);
stream
.write_all(&[0x05, 0x01, 0x00, 0x01, 127, 0, 0, 1])
.await
.unwrap();
stream.write_all(&9u16.to_be_bytes()).await.unwrap();
let mut reply = [0u8; 10];
let mut offset = 0;
while offset < reply.len() {
match stream.read(&mut reply[offset..]).await {
Ok(0) => break,
Ok(n) => offset += n,
Err(_) => break,
}
}
if offset >= 2 {
assert_ne!(reply[1], 0x00, "CONNECT should fail with reject fallback");
}
drop(stream);
token.cancel();
jh.await.ok();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn failed_upstream_releases_pending_lease() {
let config = r#"
version = 1
[[listeners]]
name = "socks-in"
bind = "127.0.0.1:0"
protocols = ["socks5"]
[[upstreams]]
id = "bad-up"
uri = "socks5://127.0.0.1:1"
[[upstream_groups]]
id = "fail-grp"
scheduler = "first-available"
members = ["bad-up"]
fallback = "reject"
[[rules]]
id = "route-all"
upstream_group = "fail-grp"
[admin]
bind = "127.0.0.1:0"
enabled = true
"#;
let f = write_config(config);
let path = f.path().to_str().unwrap();
let mut sup = eggress_runtime::ServiceSupervisor::start(path).unwrap();
let state = sup.state().clone();
let token = sup.shutdown_token();
let jh = tokio::task::spawn_blocking(move || sup.run());
wait_ready(&state).await;
let listener_addr = get_listener_addr(&state);
let admin_addr = get_admin_addr(&state);
let mut stream = tokio::net::TcpStream::connect(listener_addr)
.await
.expect("connect");
stream.write_all(&[0x05, 0x01, 0x00]).await.unwrap();
let mut resp = [0u8; 2];
stream.read_exact(&mut resp).await.unwrap();
stream
.write_all(&[0x05, 0x01, 0x00, 0x01, 127, 0, 0, 1])
.await
.unwrap();
stream.write_all(&9u16.to_be_bytes()).await.unwrap();
let mut reply = [0u8; 10];
let mut offset = 0;
while offset < reply.len() {
match stream.read(&mut reply[offset..]).await {
Ok(0) => break,
Ok(n) => offset += n,
Err(_) => break,
}
}
drop(stream);
tokio::time::sleep(Duration::from_millis(300)).await;
let (status, body) = http_get(&admin_addr, "/metrics").await;
assert_eq!(status, 200);
let upstream_failure = metric_value(&body, "eggress_upstream_open_failures_total");
assert!(
upstream_failure.is_some_and(|v| v >= 1.0),
"upstream failure counter should be >= 1, got {upstream_failure:?}"
);
token.cancel();
jh.await.ok();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn health_unavailable_upstream_skipped() {
let echo_addr = start_tcp_echo().await;
let upstream = start_socks5_tcp_upstream().await;
let config = format!(
r#"
version = 1
[[listeners]]
name = "socks-in"
bind = "127.0.0.1:0"
protocols = ["socks5"]
[[upstreams]]
id = "healthy-up"
uri = "socks5://127.0.0.1:{port}"
[[upstream_groups]]
id = "health-grp"
scheduler = "first-available"
members = ["healthy-up"]
fallback = "reject"
[[rules]]
id = "route-all"
upstream_group = "health-grp"
[admin]
bind = "127.0.0.1:0"
enabled = true
"#,
port = upstream.port(),
);
let f = write_config(&config);
let path = f.path().to_str().unwrap();
let mut sup = eggress_runtime::ServiceSupervisor::start(path).unwrap();
let state = sup.state().clone();
let token = sup.shutdown_token();
let jh = tokio::task::spawn_blocking(move || sup.run());
wait_ready(&state).await;
let listener_addr = get_listener_addr(&state);
let admin_addr = get_admin_addr(&state);
let mut stream = tokio::net::TcpStream::connect(listener_addr)
.await
.expect("connect");
let ok = socks5_connect(&mut stream, echo_addr).await;
assert!(ok, "CONNECT through healthy upstream should succeed");
stream.write_all(b"health-test").await.unwrap();
let mut buf = [0u8; 4096];
let n = tokio::time::timeout(Duration::from_secs(2), async {
stream.read(&mut buf).await
})
.await
.expect("timeout")
.expect("read");
assert_eq!(&buf[..n], b"health-test");
drop(stream);
tokio::time::sleep(Duration::from_millis(200)).await;
let (status, body) = http_get(&admin_addr, "/metrics").await;
assert_eq!(status, 200);
let upstream_open = metric_value_with_labels(
&body,
"eggress_upstream_open_total",
&[("outcome", "success")],
);
assert!(
upstream_open.is_some_and(|v| v >= 1.0),
"upstream open success should be >= 1, got {upstream_open:?}"
);
token.cancel();
jh.await.ok();
}