Skip to content

Commit 6fcdaa9

Browse files
committed
fix: harden socks5 connections
1 parent 3dd20d0 commit 6fcdaa9

12 files changed

Lines changed: 324 additions & 21 deletions

File tree

CHANGELOG.en.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22

33
All notable changes to `ii` are documented here. The default Chinese version is [CHANGELOG.md](CHANGELOG.md).
44

5+
## 0.3.10 - 2026-08-11
6+
7+
### Added
8+
9+
- Added `ii socks5` options `--max`, `--handshake-timeout`, and `--idle-timeout`. Defaults cap active connections at 100, require SOCKS5 handshakes within 10 seconds, and close CONNECT tunnels after 5 minutes without traffic, preventing public half-open and permanently idle connections from exhausting service resources.
10+
11+
### Changed
12+
13+
- `ii test` now reports target access as successful only after it reads target response body data through the proxy; header-only empty responses now fail explicitly.
14+
515
## 0.3.9 - 2026-08-11
616

717
### Added

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22

33
本文件记录 `ii` 的重要变更。默认中文版本在这里,英文版本见 [CHANGELOG.en.md](CHANGELOG.en.md)
44

5+
## 0.3.10 - 2026-08-11
6+
7+
### 新增
8+
9+
- `ii socks5` 新增 `--max``--handshake-timeout``--idle-timeout`,默认限制 100 个活动连接、10 秒 SOCKS5 握手和 5 分钟无流量的 CONNECT 隧道,防止公网半开和永久空闲连接耗尽服务资源。
10+
11+
### 变更
12+
13+
- `ii test` 仅在经代理读取到目标响应体数据后才将目标访问判为通过;仅有响应头的空响应现在会明确失败。
14+
515
## 0.3.9 - 2026-08-11
616

717
### 新增

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ unused-async = "warn"
2424

2525
[package]
2626
name = "ii"
27-
version = "0.3.9"
27+
version = "0.3.10"
2828
edition.workspace = true
2929
rust-version.workspace = true
3030
license.workspace = true

README.en.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ ii socks5
303303
ii socks5 --port 1080 --username alice --password secret
304304
```
305305

306-
`ii socks5` is a standalone ordinary network proxy. It listens on a random `0.0.0.0` port by default and prints the actual address. It supports SOCKS5 `CONNECT`, `UDP ASSOCIATE`, `BIND`, IPv4, IPv6, and domain targets. It does not use Iroh, tickets, or relays. Supplying `--username` and `--password` enables SOCKS5 username/password authentication; both options are required together.
306+
`ii socks5` is a standalone ordinary network proxy. It listens on a random `0.0.0.0` port by default and prints the actual address. It supports SOCKS5 `CONNECT`, `UDP ASSOCIATE`, `BIND`, IPv4, IPv6, and domain targets. It does not use Iroh, tickets, or relays. Supplying `--username` and `--password` enables SOCKS5 username/password authentication; both options are required together. To prevent public half-open connections from exhausting resources, it defaults to 100 active connections, a 10-second handshake deadline, and a 5-minute idle `CONNECT` tunnel timeout; use `--max`, `--handshake-timeout`, and `--idle-timeout` to adjust them.
307307

308308
### Self-hosted Relay
309309

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ ii socks5
303303
ii socks5 --port 1080 --username alice --password secret
304304
```
305305

306-
`ii socks5` 是独立的普通网络代理,默认监听 `0.0.0.0` 的随机端口并打印实际地址。支持 SOCKS5 `CONNECT``UDP ASSOCIATE``BIND`、IPv4、IPv6 和域名目标;不经过 Iroh、ticket 或 relay。提供 `--username``--password` 时启用 SOCKS5 用户名密码认证,两个参数必须成对出现。
306+
`ii socks5` 是独立的普通网络代理,默认监听 `0.0.0.0` 的随机端口并打印实际地址。支持 SOCKS5 `CONNECT``UDP ASSOCIATE``BIND`、IPv4、IPv6 和域名目标;不经过 Iroh、ticket 或 relay。提供 `--username``--password` 时启用 SOCKS5 用户名密码认证,两个参数必须成对出现。为避免公网半开连接耗尽资源,默认最多 100 个活动连接,10 秒内必须完成握手,`CONNECT` 隧道连续 5 分钟无数据会关闭;可用 `--max``--handshake-timeout``--idle-timeout` 调整。
307307

308308
### 自建 Relay
309309

ii.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ ii watch <目录> [--interval <duration>] [--stabilize <duration>] [发送选项
1515
ii queue <path...> [--after <duration>|--every <duration>] [发送选项]
1616
ii web [<目录>] [--port <port>] [--bind <ip>] [--token [<value>]] [--upload] [--path <目录>] [--once]
1717
ii dav [<目录>] [--port <port>] [--bind <ip>] [--token [<value>]] [--read-only] [--username <username> --password <password>] [--tls [--domain <name>] [--cert <path> --key <path>]]
18-
ii socks5 [--port <port>] [--bind <ip>] [--username <user> --password <pass>]
18+
ii socks5 [--port <port>] [--bind <ip>] [--username <user> --password <pass>] [--max <n>] [--handshake-timeout <duration>] [--idle-timeout <duration>]
1919
ii http [<目录>] [--port <port>] [--bind <ip>] [--token [<value>]]
2020
ii paste [<text>] [--port <port>] [--bind <ip>] [--token [<value>]] [--ttl <duration>]
2121
ii drop [<目录>] [--port <port>] [--bind <ip>] [--token [<value>]]
@@ -374,6 +374,8 @@ ii socks5 --bind 192.168.1.20 --username alice --password secret
374374

375375
支持 SOCKS5 `CONNECT``UDP ASSOCIATE``BIND`,以及 IPv4、IPv6 和域名目标。域名由代理端解析。没有认证参数时使用 SOCKS5 无认证方式;`--username <user>``--password <pass>` 必须成对提供,提供后只接受 RFC 1929 用户名密码认证。
376376

377+
默认最多保留 100 个活动连接,客户端必须在 10 秒内完成 SOCKS5 握手,`CONNECT` 隧道连续 5 分钟没有任意方向的数据即关闭。`--max <n>``--handshake-timeout <duration>``--idle-timeout <duration>` 可覆盖这些默认值;时长接受 `500ms``2s``5m``1h`
378+
377379
## 代理、转发与网络工具
378380

379381
```powershell

src/cli/help.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Usage:
88
ii queue <path...> [--after <duration>|--every <duration>] [options]
99
ii web [directory] [--port <port>] [--bind <ip>] [--token [value]] [--upload] [--path <dir>] [--once]
1010
ii dav [directory] [--port <port>] [--bind <ip>] [--token [value]] [--read-only] [--username <username> --password <password>] [--tls [--domain <name>] [--cert <path> --key <path>]]
11-
ii socks5 [--port <port>] [--bind <ip>] [--username <user> --password <pass>]
11+
ii socks5 [--port <port>] [--bind <ip>] [--username <user> --password <pass>] [--max <n>] [--handshake-timeout <duration>] [--idle-timeout <duration>]
1212
ii http [directory] [--port <port>] [--bind <ip>] [--token [value]]
1313
ii paste [text] [--port <port>] [--bind <ip>] [--token [value]] [--ttl <duration>]
1414
ii drop [directory] [--port <port>] [--bind <ip>] [--token [value]]
@@ -190,13 +190,16 @@ Options:
190190

191191
pub(crate) const SOCKS5_HELP: &str = "\
192192
Usage:
193-
ii socks5 [--port <port>] [--bind <ip>] [--username <user> --password <pass>]
193+
ii socks5 [--port <port>] [--bind <ip>] [--username <user> --password <pass>] [--max <n>] [--handshake-timeout <duration>] [--idle-timeout <duration>]
194194
195195
Options:
196196
--port <port> Listener port; defaults to a random free port
197197
--bind <ip> Listener IPv4 or IPv6 address; defaults to 0.0.0.0
198198
--username <user> Enable SOCKS5 username/password authentication
199199
--password <pass> SOCKS5 password; requires --username
200+
--max <n> Maximum active connections; defaults to 100
201+
--handshake-timeout <d> SOCKS5 handshake deadline; defaults to 10s
202+
--idle-timeout <d> Close a CONNECT tunnel with no traffic; defaults to 5m
200203
";
201204

202205
pub(crate) const HTTP_HELP: &str = "\

src/cli/socks5.rs

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,31 @@
11
use super::*;
2+
use std::time::Duration;
3+
4+
const DEFAULT_MAX_CONNECTIONS: usize = 100;
5+
const DEFAULT_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(10);
6+
const DEFAULT_IDLE_TIMEOUT: Duration = Duration::from_secs(5 * 60);
27

38
pub(super) fn parse(args: Vec<String>) -> Result<Socks5Args, ParseAction> {
4-
let mut out = Socks5Args::default();
9+
let mut out = Socks5Args {
10+
max_connections: DEFAULT_MAX_CONNECTIONS,
11+
handshake_timeout: DEFAULT_HANDSHAKE_TIMEOUT,
12+
idle_timeout: DEFAULT_IDLE_TIMEOUT,
13+
..Default::default()
14+
};
515
let mut iter = ArgsIter::new(args);
616
while let Some(arg) = iter.next() {
717
match split_long_value(&arg) {
818
Some(("port", value)) => out.port = Some(parse_port("--port", value)?),
919
Some(("bind", value)) => out.bind = Some(parse_bind("--bind", value)?),
1020
Some(("username", value)) => out.username = Some(value.to_string()),
1121
Some(("password", value)) => out.password = Some(value.to_string()),
22+
Some(("max", value)) => out.max_connections = parse_max("--max", value)?,
23+
Some(("handshake-timeout", value)) => {
24+
out.handshake_timeout = parse_duration("--handshake-timeout", value)?
25+
}
26+
Some(("idle-timeout", value)) => {
27+
out.idle_timeout = parse_duration("--idle-timeout", value)?
28+
}
1229
Some((flag, _)) => {
1330
return Err(ParseAction::error(format!("unknown option `--{flag}`")));
1431
}
@@ -18,6 +35,15 @@ pub(super) fn parse(args: Vec<String>) -> Result<Socks5Args, ParseAction> {
1835
"--bind" => out.bind = Some(parse_bind("--bind", &iter.value("--bind")?)?),
1936
"--username" => out.username = Some(iter.value("--username")?),
2037
"--password" => out.password = Some(iter.value("--password")?),
38+
"--max" => out.max_connections = parse_max("--max", &iter.value("--max")?)?,
39+
"--handshake-timeout" => {
40+
out.handshake_timeout =
41+
parse_duration("--handshake-timeout", &iter.value("--handshake-timeout")?)?
42+
}
43+
"--idle-timeout" => {
44+
out.idle_timeout =
45+
parse_duration("--idle-timeout", &iter.value("--idle-timeout")?)?
46+
}
2147
_ if arg.starts_with('-') => {
2248
return Err(ParseAction::error(format!("unknown option `{arg}`")));
2349
}
@@ -48,3 +74,49 @@ pub(super) fn parse(args: Vec<String>) -> Result<Socks5Args, ParseAction> {
4874
}
4975
Ok(out)
5076
}
77+
78+
fn parse_max(flag: &str, value: &str) -> Result<usize, ParseAction> {
79+
value
80+
.parse()
81+
.ok()
82+
.filter(|value| *value > 0)
83+
.ok_or_else(|| ParseAction::error(format!("{flag} expects a positive integer")))
84+
}
85+
86+
#[cfg(test)]
87+
mod tests {
88+
use super::*;
89+
90+
#[test]
91+
fn defaults_connection_governance() {
92+
let args = parse(vec![]).unwrap();
93+
assert_eq!(args.max_connections, DEFAULT_MAX_CONNECTIONS);
94+
assert_eq!(args.handshake_timeout, DEFAULT_HANDSHAKE_TIMEOUT);
95+
assert_eq!(args.idle_timeout, DEFAULT_IDLE_TIMEOUT);
96+
}
97+
98+
#[test]
99+
fn accepts_connection_governance_options() {
100+
let args = parse(
101+
["--max=4", "--handshake-timeout=2s", "--idle-timeout=30s"]
102+
.into_iter()
103+
.map(str::to_string)
104+
.collect(),
105+
)
106+
.unwrap();
107+
assert_eq!(args.max_connections, 4);
108+
assert_eq!(args.handshake_timeout, Duration::from_secs(2));
109+
assert_eq!(args.idle_timeout, Duration::from_secs(30));
110+
}
111+
112+
#[test]
113+
fn rejects_invalid_connection_governance_options() {
114+
for args in [
115+
vec!["--max=0"],
116+
vec!["--handshake-timeout=0s"],
117+
vec!["--idle-timeout=invalid"],
118+
] {
119+
assert!(parse(args.into_iter().map(str::to_string).collect()).is_err());
120+
}
121+
}
122+
}

src/command.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,9 @@ pub struct Socks5Args {
177177
pub bind: Option<IpAddr>,
178178
pub username: Option<String>,
179179
pub password: Option<String>,
180+
pub max_connections: usize,
181+
pub handshake_timeout: std::time::Duration,
182+
pub idle_timeout: std::time::Duration,
180183
}
181184

182185
#[derive(Debug, Clone, Default)]

0 commit comments

Comments
 (0)