Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/tw-control/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,6 @@ tw-api = { workspace = true }
tw-types = { workspace = true }
# 网卡那条断言要对着本机真实的清单比,不写死某个平台的名字
tw-config = { workspace = true }
# 测试挑端口:从远程控制端口那一段里随机挑(见 tests/common)
rand = { workspace = true }
tokio = { workspace = true, features = ["rt", "macros"] }
50 changes: 50 additions & 0 deletions crates/tw-control/tests/common/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//! 几个测试文件共用的小工具。

use std::net::SocketAddr;

/// 一个空着的端口号:**系统不会自己把它分出去**,也不会再给第二个测试。
///
/// 号要在谁都没绑它之前就定下来时用它:写进配置或请求(远程控制端口 —— 配置
/// 不收 0,见 `config.remote_port_zero`;`PUT /listen` 要保存的端口),或者要它
/// 一直没人听(连不上的代理)。只要网关起来就行的地方用不着它:
/// `tw_gateway::serve` 自己绑 0,把真的地址交回来。
///
/// **不能绑 0 拿号再放掉。**放掉的号回到系统的临时端口段(Linux 默认
/// 32768–60999,macOS 和 Windows 49152–65535),并行的测试绑 0、发起连接都从
/// 那一段取号:要绑它的,core 绑之前号就被拿走,报一个和代码无关的
/// `gw.listen.port_taken`;要它没人听的,别的测试在上面起了服务,「连不上」就
/// 连上了。这里从那一段下面挑,和 core 挑远程控制端口同一段。
///
/// **号用 UDP 占着,直到进程退出。**UDP 和 TCP 是两套端口:占着 UDP 的这个号,
/// TCP 的照样绑得上;而别的用例、同时在跑的别的测试进程(两个工作目录各跑一遍)
/// 来拿同一个号时,系统说它已经被占了。只在进程里记账的话,后一种管不到。
pub fn spare_port() -> u16 {
use std::sync::{Mutex, PoisonError};

static HELD: Mutex<Vec<std::net::UdpSocket>> = Mutex::new(Vec::new());
for _ in 0..1000 {
let port = rand::random_range(tw_config::REMOTE_PORT_RANGE);
let Ok(hold) = std::net::UdpSocket::bind(SocketAddr::from(([127, 0, 0, 1], port))) else {
continue;
};
if free(port, [127, 0, 0, 1]) && free(port, [0, 0, 0, 0]) {
HELD.lock()
.unwrap_or_else(PoisonError::into_inner)
.push(hold);
return port;
}
}
panic!("no spare port left in {:?}", tw_config::REMOTE_PORT_RANGE);
}

/// 这个端口的 TCP 此刻绑不绑得上。别的程序正占着的(开发机上可能真有一个 core
/// 在这一段里听)、系统留作他用的(Windows 会成段地保留端口)算绑不上;别的错误
/// 说明这台机器出了别的问题,直接报出来。
fn free(port: u16, ip: [u8; 4]) -> bool {
use std::io::ErrorKind;
match std::net::TcpListener::bind(SocketAddr::from((ip, port))) {
Ok(_) => true,
Err(e) if matches!(e.kind(), ErrorKind::AddrInUse | ErrorKind::PermissionDenied) => false,
Err(e) => panic!("binding port {port} to try it failed: {e}"),
}
}
37 changes: 16 additions & 21 deletions crates/tw-control/tests/listen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ use axum::http::{Request, StatusCode};
use tower::ServiceExt;
use tw_control::{ConfigManager, ControlState};

mod common;
use common::spare_port;

struct Bed {
dir: tempfile::TempDir,
app: axum::Router,
Expand Down Expand Up @@ -78,14 +81,6 @@ async fn call(
)
}

fn free_port() -> u16 {
std::net::TcpListener::bind("127.0.0.1:0")
.unwrap()
.local_addr()
.unwrap()
.port()
}

fn yaml(port: u16) -> String {
format!(
"version: 1
Expand All @@ -103,8 +98,8 @@ listen:

#[tokio::test]
async fn saving_writes_what_was_chosen_and_leaves_no_trace_of_defaults() {
let b = bed(&yaml(free_port()));
let port = free_port();
let b = bed(&yaml(spare_port()));
let port = spare_port();
let (st, v) = call(
&b.app,
"PUT",
Expand Down Expand Up @@ -143,7 +138,7 @@ async fn saving_writes_what_was_chosen_and_leaves_no_trace_of_defaults() {
async fn a_port_in_use_is_refused_and_nothing_is_written() {
// **这个接口存在的理由。**写进去之后才发现绑不上,网关守着旧地址而配置
// 文件说着新地址 —— 两边从那一刻起各说各的
let start = free_port();
let start = spare_port();
let b = bed(&yaml(start));
let squatter = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let taken = squatter.local_addr().unwrap().port();
Expand All @@ -167,13 +162,13 @@ async fn a_port_in_use_is_refused_and_nothing_is_written() {

#[tokio::test]
async fn an_interface_that_is_not_there_is_refused_by_name() {
let b = bed(&yaml(free_port()));
let b = bed(&yaml(spare_port()));
let before = b.file();
let (st, v) = call(
&b.app,
"PUT",
"/listen",
serde_json::json!({ "bind": "en97", "port": free_port(), "allow_from": [] }),
serde_json::json!({ "bind": "en97", "port": spare_port(), "allow_from": [] }),
)
.await;
assert_eq!(st, StatusCode::CONFLICT, "{v}");
Expand All @@ -200,7 +195,7 @@ async fn an_interface_that_is_not_there_is_refused_by_name() {
&b.app,
"PUT",
"/listen",
serde_json::json!({ "bind": "192.168.1.5\twifi", "port": free_port(), "allow_from": [] }),
serde_json::json!({ "bind": "192.168.1.5\twifi", "port": spare_port(), "allow_from": [] }),
)
.await;
assert_eq!(st, StatusCode::BAD_REQUEST, "{v}");
Expand All @@ -221,7 +216,7 @@ async fn an_interface_that_is_not_there_is_refused_by_name() {
async fn the_status_follows_the_listener_after_a_save() {
// 界面左下角的地址以前是启动时记的一次:改了端口,网关已经在新端口上
// 服务,状态里还写着旧的
let p1 = free_port();
let p1 = spare_port();
let b = bed(&yaml(p1));
let gw = b.gw.clone();
let want = tw_config::try_parse(&b.file())
Expand All @@ -236,7 +231,7 @@ async fn the_status_follows_the_listener_after_a_save() {
let (_, s) = call(&b.app, "GET", "/status", serde_json::Value::Null).await;
assert_eq!(s["gateway_addr"], format!("127.0.0.1:{p1}"));

let p2 = free_port();
let p2 = spare_port();
let (st, v) = call(
&b.app,
"PUT",
Expand All @@ -260,12 +255,12 @@ async fn the_status_follows_the_listener_after_a_save() {

#[tokio::test]
async fn a_stale_version_is_refused_like_any_other_edit() {
let b = bed(&yaml(free_port()));
let b = bed(&yaml(spare_port()));
let (st, _) = call(
&b.app,
"PUT",
"/listen",
serde_json::json!({ "bind": "loopback", "port": free_port(), "base_version": "nope", "allow_from": [] }),
serde_json::json!({ "bind": "loopback", "port": spare_port(), "base_version": "nope", "allow_from": [] }),
)
.await;
assert_eq!(st, StatusCode::CONFLICT);
Expand Down Expand Up @@ -296,8 +291,8 @@ listen:
async fn an_empty_allow_list_is_written_down_and_the_default_one_is_not() {
// 不写 = 默认名单,所以空的必须写成 `[]`;和默认名单一样的不写,
// 配置文件不因为存了一次就多出几行
let b = bed(&yaml(free_port()));
let port = free_port();
let b = bed(&yaml(spare_port()));
let port = spare_port();
let save = |allow: serde_json::Value| serde_json::json!({ "bind": "all", "port": port, "allow_from": allow });
let (st, v) = call(&b.app, "PUT", "/listen", save(serde_json::json!([]))).await;
assert_eq!(st, StatusCode::OK, "{v}");
Expand All @@ -319,7 +314,7 @@ async fn an_empty_allow_list_is_written_down_and_the_default_one_is_not() {
#[tokio::test]
async fn interfaces_come_one_per_name() {
// 配置里按名字存:同一张网卡列两行,选第二行等于选第一行
let b = bed(&yaml(free_port()));
let b = bed(&yaml(spare_port()));
let (st, v) = call(&b.app, "GET", "/interfaces", serde_json::Value::Null).await;
assert_eq!(st, StatusCode::OK, "{v}");
let names: Vec<&str> = v
Expand Down
16 changes: 8 additions & 8 deletions crates/tw-control/tests/live_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ use axum::http::{Request, StatusCode};
use tower::ServiceExt;
use tw_control::{ConfigManager, ControlState};

mod common;
use common::spare_port;

fn control(
d: &tempfile::TempDir,
yaml: &str,
Expand Down Expand Up @@ -52,11 +55,9 @@ async fn get(app: &axum::Router, path: &str) -> (StatusCode, serde_json::Value)

/// 把网关真的跑起来:凭据和代理的状态只有真实转发才会碰到。
async fn serve(gw: tw_gateway::AppState) -> SocketAddr {
let addr = {
let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
l.local_addr().unwrap()
};
tokio::spawn(async move { tw_gateway::serve(gw, addr).await.unwrap() });
let addr = tw_gateway::serve(gw, ([127, 0, 0, 1], 0).into())
.await
.unwrap();
tokio::time::sleep(Duration::from_millis(60)).await;
addr
}
Expand All @@ -76,10 +77,9 @@ async fn ask(gw: SocketAddr) -> u16 {
.as_u16()
}

/// 一个确定没人在听的地址。
/// 一个确定没人在听的地址(见 [`spare_port`])。
fn dead_addr() -> SocketAddr {
let l = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
l.local_addr().unwrap()
SocketAddr::from(([127, 0, 0, 1], spare_port()))
}

// ---------------------------------------------------------------- 还在跑的请求
Expand Down
27 changes: 11 additions & 16 deletions crates/tw-control/tests/remote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ use tw_api::control::{Address, ControlKey};
use tw_control::{ConfigManager, ControlState};
use tw_link::LinkError;

mod common;
use common::spare_port;

const KEY: &str = "c0ffee00c0ffee00c0ffee00c0ffee00c0ffee00c0ffee00c0ffee00c0ffee00";

fn yaml(port: u16, enabled: bool, allow: &str) -> String {
Expand All @@ -26,14 +29,6 @@ fn yaml_at(bind: &str, port: u16, enabled: bool, allow: &str) -> String {
)
}

fn free_port() -> u16 {
std::net::TcpListener::bind("127.0.0.1:0")
.unwrap()
.local_addr()
.unwrap()
.port()
}

struct Bed {
dir: tempfile::TempDir,
state: ControlState,
Expand Down Expand Up @@ -176,7 +171,7 @@ async fn send(
/// 放行的来源握得上手,状态里说得出开着、听在哪。
#[tokio::test]
async fn an_allowed_source_gets_in_and_the_status_says_where_it_listens() {
let port = free_port();
let port = spare_port();
let b = bed(yaml(port, true, "[127.0.0.1]")).await;
let l = b.until(|l| l.addr.is_some()).await;
assert_eq!(l.addr.unwrap().port(), port);
Expand All @@ -196,7 +191,7 @@ async fn an_allowed_source_gets_in_and_the_status_says_where_it_listens() {
/// 不是「钥匙不对」。
#[tokio::test]
async fn a_source_outside_allow_from_is_closed_without_a_word() {
let port = free_port();
let port = spare_port();
let b = bed(yaml(port, true, "[10.0.0.0/8]")).await;
let l = b.until(|l| l.addr.is_some()).await;
let mut raw = tokio::net::TcpStream::connect(l.addr.unwrap())
Expand Down Expand Up @@ -229,7 +224,7 @@ async fn a_source_outside_allow_from_is_closed_without_a_word() {
/// 同一个来源一分钟里握手失败五次,之后连对的钥匙也直接关掉。
#[tokio::test]
async fn five_wrong_keys_from_one_source_bench_it() {
let port = free_port();
let port = spare_port();
let b = bed(yaml(port, true, "[127.0.0.1]")).await;
let addr = b.until(|l| l.addr.is_some()).await.addr.unwrap();
let wrong = "1".repeat(64);
Expand Down Expand Up @@ -259,7 +254,7 @@ async fn five_wrong_keys_from_one_source_bench_it() {
/// 同样的事从本机的通道做得了。
#[tokio::test]
async fn a_remote_connection_cannot_stop_the_core_take_diagnostics_or_move_its_own_door() {
let port = free_port();
let port = spare_port();
let b = bed(yaml(port, true, "[127.0.0.1]")).await;
let addr = b.until(|l| l.addr.is_some()).await.addr.unwrap();
let (mut r, _) = open_tcp(addr, KEY).await.unwrap();
Expand Down Expand Up @@ -350,7 +345,7 @@ async fn a_remote_connection_cannot_stop_the_core_take_diagnostics_or_move_its_o
/// 旧的并说为什么,本机的通道照常。
#[tokio::test]
async fn it_follows_the_configuration_live() {
let p1 = free_port();
let p1 = spare_port();
let b = bed(yaml(p1, true, "[127.0.0.1]")).await;
let a1 = b.until(|l| l.addr.is_some()).await.addr.unwrap();
let (mut s, conn) = open_tcp(a1, KEY).await.unwrap();
Expand All @@ -374,7 +369,7 @@ async fn it_follows_the_configuration_live() {
));

// 换个端口打开
let p2 = free_port();
let p2 = spare_port();
b.rewrite(&yaml(p2, true, "[127.0.0.1]")).await;
let a2 = b
.until(|l| l.addr.is_some_and(|a| a.port() == p2))
Expand Down Expand Up @@ -421,7 +416,7 @@ async fn a_port_that_cannot_be_bound_at_start_does_not_take_the_local_channel_do
/// 却仍放行它的,连接照常。
#[tokio::test]
async fn narrowing_allow_from_closes_the_connections_it_no_longer_lets_in() {
let port = free_port();
let port = spare_port();
let b = bed(yaml(port, true, "[127.0.0.1]")).await;
let addr = b.until(|l| l.addr.is_some()).await.addr.unwrap();
let (mut s, conn) = open_tcp(addr, KEY).await.unwrap();
Expand Down Expand Up @@ -458,7 +453,7 @@ async fn narrowing_allow_from_closes_the_connections_it_no_longer_lets_in() {
/// 旧的自己。要能当场换过去,不报错。
#[tokio::test]
async fn the_same_port_moves_between_all_and_a_specific_address_live() {
let port = free_port();
let port = spare_port();
let b = bed(yaml_at("all", port, true, "[127.0.0.1]")).await;
let l = b.until(|l| l.addr.is_some()).await;
assert!(l.addr.unwrap().ip().is_unspecified(), "{l:?}");
Expand Down
19 changes: 15 additions & 4 deletions crates/tw-gateway/src/l1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -888,14 +888,25 @@ mod tests {
/// 并行测试里别的用例就可能绑到它 —— 这个文件里有七处在绑随机端口,
/// 其中几处还起了真的 SOCKS5 服务器。于是「死端口」活了过来,
/// 测试看到的不是 TCP 拒绝而是一次握手。Windows 上撞得尤其勤。
/// 放掉之前先连一次确认它是死的也不够:确认完了照样会被绑去。
///
/// 所以拿到之后先自己连一次确认它是死的,活着就换一个。
/// 所以从系统不会自己分出去的那一段里挑(和 core 挑远程控制端口同一段),
/// 号用 UDP 占着直到进程退出。UDP 和 TCP 是两套端口,占着 UDP 的号不妨碍
/// TCP 上没人听;而别的测试挑号时也先占 UDP 的同一个号(见集成测试的
/// `spare_port`),占不到就换,不会拿它去绑。
async fn dead_port() -> SocketAddr {
use std::sync::{Mutex, PoisonError};
static HELD: Mutex<Vec<std::net::UdpSocket>> = Mutex::new(Vec::new());
for _ in 0..64 {
let l = TcpListener::bind("127.0.0.1:0").await.unwrap();
let a = l.local_addr().unwrap();
drop(l);
let port = rand::random_range(tw_config::REMOTE_PORT_RANGE);
let a = SocketAddr::from(([127, 0, 0, 1], port));
let Ok(hold) = std::net::UdpSocket::bind(a) else {
continue;
};
if tokio::net::TcpStream::connect(a).await.is_err() {
HELD.lock()
.unwrap_or_else(PoisonError::into_inner)
.push(hold);
return a;
}
}
Expand Down
19 changes: 19 additions & 0 deletions crates/tw-gateway/src/listen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,25 @@ fn snapshot(bound: &[Bound], error: Option<Msg>) -> Listening {
}
}

/// 起服务,**不跟配置走**,绑上了就交回真的地址。见 [`crate::server::serve`]。
pub(crate) async fn serve_detached(
state: AppState,
want: SocketAddr,
) -> std::io::Result<SocketAddr> {
let listener = TcpListener::bind(want)
.await
.map_err(|e| std::io::Error::new(e.kind(), format!("{}", bind_failure(want, &e))))?;
let bound = start(&state, want, listener)?;
let actual = bound.actual;
state.set_listening(snapshot(std::slice::from_ref(&bound), None));
// 丢掉 `Bound` 就停止接新连接:把它留在一个不会结束的任务里
tokio::spawn(async move {
let _bound = bound;
std::future::pending::<()>().await
});
Ok(actual)
}

/// 起服务。`follow` 为真时**跟着配置里的监听地址走**(「温」那一级热重载)。
///
/// 命令行给了 `--port` 时 `follow` 为假:那是一个显式的覆盖,不该被配置
Expand Down
Loading
Loading