init
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
# 默认使用 musl 目标,产出完全静态链接的单文件二进制(Linux 下免运行库运行)。
|
||||||
|
# 首次编译前需安装目标: rustup target add x86_64-unknown-linux-musl
|
||||||
|
[build]
|
||||||
|
target = "x86_64-unknown-linux-musl"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
Generated
+131
@@ -0,0 +1,131 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytes"
|
||||||
|
version = "1.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.189"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mini-socks"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mio"
|
||||||
|
version = "1.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"wasi",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project-lite"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.107"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.47"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "socket2"
|
||||||
|
version = "0.6.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "3.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio"
|
||||||
|
version = "1.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"libc",
|
||||||
|
"mio",
|
||||||
|
"pin-project-lite",
|
||||||
|
"socket2",
|
||||||
|
"tokio-macros",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-macros"
|
||||||
|
version = "2.7.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.24"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.11.1+wasi-snapshot-preview1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-link"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.61.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "mini-socks"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "Linux 下免运行库运行的轻量级 SOCKS5 服务器 (CONNECT / BIND / UDP, 多出口 IP)"
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "io-util"] }
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
opt-level = 3
|
||||||
|
lto = true
|
||||||
|
codegen-units = 1
|
||||||
|
strip = true
|
||||||
|
panic = "abort"
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 mini-socks contributors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
# mini-socks
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
**Linux 下免运行库运行的轻量级 SOCKS5 服务器。** 完全静态编译的单文件二进制,无任何运行时依赖,拷贝到任何 x86_64 Linux 上即可运行。
|
||||||
|
|
||||||
|
## 特性
|
||||||
|
|
||||||
|
- **SOCKS5 全特性**(RFC 1928):
|
||||||
|
- `CONNECT` —— TCP 代理
|
||||||
|
- `BIND` —— 反向连入(FTP 等场景)
|
||||||
|
- `UDP ASSOCIATE` —— UDP 中继
|
||||||
|
- **用户名/密码认证**(RFC 1929),未配置时免认证
|
||||||
|
- **多出口 IP**:客户端从哪个本机 IP 连入,出站流量就以该 IP 为源地址
|
||||||
|
- **systemd 服务**:一条命令安装/卸载,开机自启、掉线自动重启
|
||||||
|
- **零依赖**:musl 静态链接,无 glibc 版本问题,无 OpenSSL,无 Python
|
||||||
|
|
||||||
|
## 编译
|
||||||
|
|
||||||
|
需要 Rust 工具链(1.70+)与 musl 目标:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rustup target add x86_64-unknown-linux-musl
|
||||||
|
cargo build --release
|
||||||
|
```
|
||||||
|
|
||||||
|
产物为完全静态链接的二进制:
|
||||||
|
|
||||||
|
```
|
||||||
|
target/x86_64-unknown-linux-musl/release/mini-socks # 约 700 KB
|
||||||
|
```
|
||||||
|
|
||||||
|
> 项目已通过 `.cargo/config.toml` 默认启用 musl 目标。musl 目标仅在编译时需要,运行环境无任何要求。
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 免认证,监听 0.0.0.0:1080
|
||||||
|
./mini-socks -p 1080
|
||||||
|
|
||||||
|
# 启用用户名/密码认证
|
||||||
|
./mini-socks -p 1080 -a alice:s3cret
|
||||||
|
```
|
||||||
|
|
||||||
|
客户端连接示例:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl --socks5-hostname 127.0.0.1:1080 https://example.com
|
||||||
|
# 带认证
|
||||||
|
curl -U alice:s3cret --socks5-hostname 127.0.0.1:1080 https://example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
浏览器/软件代理设置:协议 `socks5`,地址 `<服务器 IP>`,端口 `1080`,账号密码可选。
|
||||||
|
|
||||||
|
## 命令行参数
|
||||||
|
|
||||||
|
```
|
||||||
|
mini-socks [-p <端口>] [-a <user:pass>] 前台运行
|
||||||
|
mini-socks [-p <端口>] [-a <user:pass>] -i 安装为 systemd 服务并启动(需 root)
|
||||||
|
mini-socks -u 停止并删除 systemd 服务(需 root)
|
||||||
|
mini-socks -h 显示版本、监听地址与帮助
|
||||||
|
```
|
||||||
|
|
||||||
|
| 参数 | 说明 |
|
||||||
|
| ---- | ---- |
|
||||||
|
| `-p <端口>` | 监听端口(1-65535),默认 `1080`,监听 `0.0.0.0:<端口>` |
|
||||||
|
| `-a <user:pass>` | 启用 SOCKS5 用户名/密码认证;缺省为免认证 |
|
||||||
|
| `-i` | 安装为 systemd 服务(`mini-socks.service`),立即启动并开机自启 |
|
||||||
|
| `-u` | 停止服务并删除 systemd 服务 |
|
||||||
|
| `-h` | 显示版本、监听地址与帮助 |
|
||||||
|
|
||||||
|
## systemd 服务
|
||||||
|
|
||||||
|
在服务器上以 root 执行,`-p` / `-a` 参数会被固化进服务:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo ./mini-socks -p 1080 -a alice:s3cret -i # 安装并立即启动
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl status mini-socks # 查看状态
|
||||||
|
journalctl -u mini-socks -f # 实时日志
|
||||||
|
systemctl restart mini-socks # 重启
|
||||||
|
sudo ./mini-socks -u # 停止并卸载服务
|
||||||
|
```
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- `-i` 直接以当前二进制的绝对路径作为 `ExecStart`,**不复制文件**;移动或删除二进制前请先 `-u` 卸载
|
||||||
|
- 重复执行 `-i` 为覆盖安装,以最后一次的参数为准并自动重启服务(例如改端口后重新 `-i` 即可生效)
|
||||||
|
- `-u` 只停止并删除服务,不删除二进制文件
|
||||||
|
- 服务崩溃后 3 秒自动拉起(`Restart=on-failure`),并设置开机自启
|
||||||
|
|
||||||
|
## 多出口 IP 模式
|
||||||
|
|
||||||
|
服务器上配置了多个 IP 时,mini-socks 按 **"从哪个 IP 连入,就从哪个 IP 出去"** 自动选择出口源地址:
|
||||||
|
|
||||||
|
```
|
||||||
|
用户 A ──socks5://10.0.0.2:1080──▶ ┌─────────────┐ ──源 IP 10.0.0.2──▶ 目标网站
|
||||||
|
│ mini-socks │
|
||||||
|
用户 B ──socks5://10.0.0.3:1080──▶ └─────────────┘ ──源 IP 10.0.0.3──▶ 目标网站
|
||||||
|
```
|
||||||
|
|
||||||
|
- 实现方式:TCP 出站绑定源地址(connect 前 bind),UDP 中继套接字同样绑定该地址
|
||||||
|
- 前提:这些 IP 已配置在本机网卡上(`ip addr` 可见)。云服务器请把多个公网/弹性 IP 绑定到同一块网卡
|
||||||
|
- 若入口 IP 不可用作源地址(如经过端口映射),自动回退系统默认路由并在日志中提示
|
||||||
|
|
||||||
|
多 IP 服务器上用户可见的效果:
|
||||||
|
|
||||||
|
- 访问 `socks5://10.0.0.2:1080` 时,目标网站看到的出口 IP 是 **10.0.0.2**
|
||||||
|
- 访问 `socks5://10.0.0.3:1080` 时,目标网站看到的出口 IP 是 **10.0.0.3**
|
||||||
|
|
||||||
|
## UDP 说明
|
||||||
|
|
||||||
|
- 遵循 RFC 1928 `UDP ASSOCIATE`:TCP 控制连接关闭即结束 UDP 会话
|
||||||
|
- 中继地址通告为客户端连入所用的 IP,保证可达
|
||||||
|
- 暂不支持分片(FRAG ≠ 0 的数据报丢弃),主流应用(游戏、语音、QUIC)均不使用分片
|
||||||
|
|
||||||
|
## 日志
|
||||||
|
|
||||||
|
日志输出到 stderr(systemd 下由 journald 收集),示例:
|
||||||
|
|
||||||
|
```
|
||||||
|
2026-09-06 15:23:25 [INFO] mini-socks v0.1.0 已启动,监听 0.0.0.0:1780,认证: 用户名/密码 (RFC 1929)
|
||||||
|
2026-09-06 15:23:25 [INFO] 认证通过: 用户 alice
|
||||||
|
2026-09-06 15:23:25 [INFO] CONNECT example.com:443 <- 10.0.0.2:51344,出口 10.0.0.2,转发 1024/20480 字节(上行/下行)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 防火墙
|
||||||
|
|
||||||
|
放行 SOCKS5 监听端口(TCP),如需 UDP 还需放行相应 UDP 流量:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# firewalld
|
||||||
|
firewall-cmd --permanent --add-port=1080/tcp && firewall-cmd --reload
|
||||||
|
# ufw
|
||||||
|
ufw allow 1080/tcp
|
||||||
|
```
|
||||||
|
|
||||||
|
> 公网部署强烈建议配合 `-a` 开启认证,或用防火墙限制来源 IP。
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>BIND 命令有什么用?</b></summary>
|
||||||
|
|
||||||
|
SOCKS5 的 `BIND` 用于让远端主动反向连入(典型如 FTP 主动模式数据连接)。日常 HTTP/网页代理不会用到,但 mini-socks 已完整实现,可配合需要的客户端使用。
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>多出口 IP 需要特殊配置吗?</b></summary>
|
||||||
|
|
||||||
|
不需要修改本程序参数。只要服务器网卡上已配置多个 IP,监听在 `0.0.0.0` 的 mini-socks 就会自动按连入 IP 选择出口。若某入口 IP 未配置在本机(如 NAT 映射场景),该连接会回退默认路由。
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>忘记服务参数想修改怎么办?</b></summary>
|
||||||
|
|
||||||
|
带新参数重新执行 `sudo ./mini-socks -p <端口> -a <user:pass> -i` 覆盖安装即可,服务会自动重启到新配置。
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
mini-socks/
|
||||||
|
├── src/
|
||||||
|
│ ├── main.rs # 命令行解析、帮助信息、日志
|
||||||
|
│ ├── socks.rs # SOCKS5 协议(CONNECT/BIND/UDP)、认证、多出口 IP
|
||||||
|
│ └── install.rs # systemd 服务安装/卸载
|
||||||
|
├── Cargo.toml
|
||||||
|
└── .cargo/config.toml
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[MIT](LICENSE)
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# 优化mini-socks:
|
||||||
|
- `-i` 安装到 /usr/bin/mini-socks
|
||||||
|
- `-u` 删除服务 与 /usr/bin
|
||||||
|
- `-a` 如果没有参数就随机生成 user:pass(12位), user只有小写字母6位字符, pass为12位随机数字与字母
|
||||||
|
- `-s` 提交一个代理到 supersite.host, 使用以下来提交代理: 以下API与TOKEN 都是硬编码
|
||||||
|
```bash
|
||||||
|
curl -X POST 'https://supersite.host/api/proxy/submit' \
|
||||||
|
-H 'Authorization: Bearer px_live_7cb412cec32c4906a1243f0f2ee906bf46dcbf6208373c40' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"protocol":"socks5","host":"proxy.example.com","port":8080,"username":"user","password":"secret","country_code":"US"}'
|
||||||
|
```
|
||||||
|
- 通过: https://api-ipv4.ip.sb/geoip (硬编码) 获取代理的IP地址与国家代码, 示例:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"country_code": "JP",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- 有什么问题与建议, 请发给我确认再编码.
|
||||||
|
|
||||||
|
|
||||||
|
`-s` 就是在安装服务成功后提交一次, 不写到systemd服务文件中.
|
||||||
|
|
||||||
|
|
||||||
|
安装完成后输出信息中加上: socks://user:pass@ip:port 这样的格式, 示例:
|
||||||
|
```bash
|
||||||
|
socks://user:pass@127.0.0.1:1080
|
||||||
|
```
|
||||||
|
|
||||||
|
要确认一下 -a 空参数, 在-i 安装服务时, 生成 -a user:pass 写到systemd服务文件中.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
让zcode搞的居于 Rust 不到800K的 socks5代理服务,
|
||||||
|
支持UDP, HTTP2、QUIC/HTTP/3、WebRTC.
|
||||||
|
```bash
|
||||||
|
wget -O mini-socks https://sh.adx9.com/mini-socks/mini-socks && chmod +x mini-socks && ./mini-socks -p 51080 -a -i -s
|
||||||
|
```
|
||||||
|
-p 端口
|
||||||
|
-a 需要认证空为随机生成, 可以自定义 `-a user:pass`
|
||||||
|
-i 安装服务
|
||||||
|
-s 把安装好的节点提交到supersite.host
|
||||||
|
删除 `mini-socks -u`
|
||||||
|
|
||||||
|
## 主要解决指纹浏览器
|
||||||
|
- 使用 TUN + socks5 解决 TCP + TLS 与 QUIC / HTTP/3指纹问题.
|
||||||
|
- HTTP代理无法透传 TCP + TLS等指纹.
|
||||||
|
- 已经测试 Wix 注册的reCaptcha已经过不了的, 已经死掉的IP现在又可以注册了.
|
||||||
|
- 避免幸存者偏差, 还要跟踪测试.
|
||||||
|
- 当前应用在GOOGLE gemini搜索的MCP
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"gemini-search": {
|
||||||
|
"type": "http",
|
||||||
|
"url": "https://gsm.adx9.com/mcp",
|
||||||
|
"headers": {
|
||||||
|
"Authorization": "Bearer sk-gsm-c24dcf4850a39793eaa54d68b8dbb7c1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- 在执行安装前判断是否已经安装, 端口是否被占用, 如果不输入 -p 就是随机端口30000~59999 之间随机一个端口.
|
||||||
|
- 保证只能安装一个服务, 占用一个端口.
|
||||||
|
- 修复安装完成后的显示问题 `socks://user:pass@127.0.0.1:1080` 改为 `socks5://[user]:[pass]@[ip]:[port]`
|
||||||
+453
@@ -0,0 +1,453 @@
|
|||||||
|
//! systemd 服务安装(-i)与卸载(-u)。
|
||||||
|
//!
|
||||||
|
//! 安装时复制二进制到 `/usr/bin/mini-socks`,并把 `-p` / `-a` 固化进 unit。
|
||||||
|
//! 全机只允许一个实例:已安装则拒绝再装;未指定 `-p` 时在 30000~59999 选空闲端口。
|
||||||
|
|
||||||
|
use std::fs;
|
||||||
|
use std::io::Read;
|
||||||
|
use std::net::TcpListener;
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::process::{Command, exit};
|
||||||
|
|
||||||
|
use crate::{Auth, VERSION, log_info, log_warn};
|
||||||
|
|
||||||
|
const UNIT_NAME: &str = "mini-socks";
|
||||||
|
const UNIT_PATH: &str = "/etc/systemd/system/mini-socks.service";
|
||||||
|
const BIN_PATH: &str = "/usr/bin/mini-socks";
|
||||||
|
|
||||||
|
/// 安装时未指定 `-p` 的随机端口区间(含端点)。
|
||||||
|
const RANDOM_PORT_MIN: u16 = 30000;
|
||||||
|
const RANDOM_PORT_MAX: u16 = 59999;
|
||||||
|
|
||||||
|
// -s 提交接口(按需求硬编码)
|
||||||
|
const GEOIP_URL: &str = "https://api-ipv4.ip.sb/geoip";
|
||||||
|
const SUBMIT_URL: &str = "https://supersite.host/api/proxy/submit";
|
||||||
|
const SUBMIT_TOKEN: &str = "px_live_7cb412cec32c4906a1243f0f2ee906bf46dcbf6208373c40";
|
||||||
|
|
||||||
|
/// systemd ExecStart 的参数引用:用双引号包裹并转义,防止空格/引号破坏解析。
|
||||||
|
fn quote(s: &str) -> String {
|
||||||
|
let mut q = String::with_capacity(s.len() + 2);
|
||||||
|
q.push('"');
|
||||||
|
for c in s.chars() {
|
||||||
|
if c == '"' || c == '\\' {
|
||||||
|
q.push('\\');
|
||||||
|
}
|
||||||
|
q.push(c);
|
||||||
|
}
|
||||||
|
q.push('"');
|
||||||
|
q
|
||||||
|
}
|
||||||
|
|
||||||
|
fn require_systemd() {
|
||||||
|
if !std::path::Path::new("/run/systemd/system").exists() {
|
||||||
|
eprintln!("mini-socks: 未检测到 systemd(/run/systemd/system 不存在),无法操作服务。");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn systemctl(args: &[&str]) -> Result<(), String> {
|
||||||
|
match Command::new("systemctl").args(args).output() {
|
||||||
|
Ok(o) if o.status.success() => Ok(()),
|
||||||
|
Ok(o) => {
|
||||||
|
let msg = String::from_utf8_lossy(&o.stderr).trim().to_string();
|
||||||
|
Err(if msg.is_empty() { format!("systemctl {} 失败", args.join(" ")) } else { msg })
|
||||||
|
}
|
||||||
|
Err(e) => Err(format!("无法执行 systemctl({e}),请确认系统已安装 systemd")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn die(msg: String) -> ! {
|
||||||
|
eprintln!("mini-socks: {msg}");
|
||||||
|
exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 探测端口是否可绑定(TCP 0.0.0.0)。
|
||||||
|
pub fn port_available(port: u16) -> bool {
|
||||||
|
TcpListener::bind(("0.0.0.0", port)).is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 在 30000~59999 内随机选一个当前空闲的端口。
|
||||||
|
pub fn pick_random_port() -> Result<u16, String> {
|
||||||
|
let span = (RANDOM_PORT_MAX - RANDOM_PORT_MIN + 1) as u32;
|
||||||
|
let mut f = fs::File::open("/dev/urandom").map_err(|e| format!("读取 /dev/urandom 失败: {e}"))?;
|
||||||
|
let mut buf = [0u8; 4];
|
||||||
|
for _ in 0..128 {
|
||||||
|
f.read_exact(&mut buf).map_err(|e| format!("读取 /dev/urandom 失败: {e}"))?;
|
||||||
|
let n = u32::from_le_bytes(buf);
|
||||||
|
let port = RANDOM_PORT_MIN + (n % span) as u16;
|
||||||
|
if port_available(port) {
|
||||||
|
return Ok(port);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(format!(
|
||||||
|
"在 {RANDOM_PORT_MIN}~{RANDOM_PORT_MAX} 内找不到空闲端口,请用 -p 指定或释放端口后重试"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn already_installed() -> bool {
|
||||||
|
Path::new(UNIT_PATH).exists()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// -i:检查未安装 → 选定端口 → 复制二进制 → 写 unit → enable/restart。
|
||||||
|
/// 全机只允许一个服务;已安装须先 `-u`。
|
||||||
|
pub fn install(port: Option<u16>, auth: Option<&Auth>, submit: bool) {
|
||||||
|
require_systemd();
|
||||||
|
|
||||||
|
if already_installed() {
|
||||||
|
die(format!(
|
||||||
|
"已安装 {UNIT_NAME}.service({UNIT_PATH} 存在),全机仅允许一个实例。\n\
|
||||||
|
请先执行 `sudo mini-socks -u` 卸载后再安装。"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let port = match port {
|
||||||
|
Some(p) => {
|
||||||
|
if !port_available(p) {
|
||||||
|
die(format!("端口 {p} 已被占用,请换一个端口或先释放后再装。"));
|
||||||
|
}
|
||||||
|
p
|
||||||
|
}
|
||||||
|
None => match pick_random_port() {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(e) => die(e),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = copy_self_to(Path::new(BIN_PATH)) {
|
||||||
|
if e.kind() == std::io::ErrorKind::PermissionDenied {
|
||||||
|
die(format!("复制二进制到 {BIN_PATH} 权限不足,请使用 root/sudo 运行。"));
|
||||||
|
}
|
||||||
|
die(format!("复制二进制到 {BIN_PATH} 失败: {e}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let unit = build_unit(port, auth);
|
||||||
|
|
||||||
|
if let Err(e) = fs::write(UNIT_PATH, unit) {
|
||||||
|
if e.kind() == std::io::ErrorKind::PermissionDenied {
|
||||||
|
die(format!("写入 {UNIT_PATH} 权限不足,请使用 root/sudo 运行。"));
|
||||||
|
}
|
||||||
|
die(format!("写入 {UNIT_PATH} 失败: {e}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = systemctl(&["daemon-reload"]) {
|
||||||
|
die(e);
|
||||||
|
}
|
||||||
|
if let Err(e) = systemctl(&["enable", UNIT_NAME]) {
|
||||||
|
die(e);
|
||||||
|
}
|
||||||
|
if let Err(e) = systemctl(&["restart", UNIT_NAME]) {
|
||||||
|
die(format!("{e}\n(可运行 `journalctl -u {UNIT_NAME} -e` 查看日志,常见原因:端口被占用)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 展示用公网 IP(失败则回退 127.0.0.1);有 -s 时复用,避免再打一次 geoip
|
||||||
|
let geo = lookup_geoip();
|
||||||
|
let display_ip = geo.as_ref().map(|(ip, _)| ip.as_str()).unwrap_or("127.0.0.1");
|
||||||
|
|
||||||
|
let auth_desc = match auth {
|
||||||
|
Some(a) => format!("用户名/密码(用户 {})", a.user),
|
||||||
|
None => "无(免认证)".to_string(),
|
||||||
|
};
|
||||||
|
println!(
|
||||||
|
"服务安装完成并已启动:\n \
|
||||||
|
服务: {UNIT_NAME}.service\n \
|
||||||
|
程序: {BIN_PATH}\n \
|
||||||
|
监听: 0.0.0.0:{port}\n \
|
||||||
|
认证: {auth_desc}\n \
|
||||||
|
连接: {}\n\n\
|
||||||
|
常用命令:\n \
|
||||||
|
systemctl status mini-socks 查看运行状态\n \
|
||||||
|
journalctl -u mini-socks -f 实时查看日志\n \
|
||||||
|
systemctl restart mini-socks 重启服务\n \
|
||||||
|
sudo mini-socks -u 停止并卸载服务",
|
||||||
|
crate::socks_uri(auth, display_ip, port),
|
||||||
|
);
|
||||||
|
|
||||||
|
if submit {
|
||||||
|
let a = match auth {
|
||||||
|
Some(a) => a,
|
||||||
|
None => die("内部错误:-s 提交要求已设置认证。".into()),
|
||||||
|
};
|
||||||
|
match geo {
|
||||||
|
Ok((ip, cc)) => submit_proxy_with(port, a, &ip, &cc),
|
||||||
|
Err(e) => log_warn!("提交代理信息失败: {e}(服务不受影响)"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// -u:停止 → 禁用 → 删除 unit 文件 → daemon-reload → 删除 /usr/bin 下的二进制。
|
||||||
|
pub fn uninstall() {
|
||||||
|
require_systemd();
|
||||||
|
|
||||||
|
let _ = Command::new("systemctl").args(["stop", UNIT_NAME]).output();
|
||||||
|
let _ = Command::new("systemctl").args(["disable", UNIT_NAME]).output();
|
||||||
|
|
||||||
|
match fs::remove_file(UNIT_PATH) {
|
||||||
|
Ok(()) => {}
|
||||||
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
|
||||||
|
println!("mini-socks: 服务未安装({UNIT_PATH} 不存在),无需卸载。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
|
||||||
|
die(format!("删除 {UNIT_PATH} 权限不足,请使用 root/sudo 运行。"));
|
||||||
|
}
|
||||||
|
Err(e) => die(format!("删除 {UNIT_PATH} 失败: {e}")),
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = Command::new("systemctl").args(["daemon-reload"]).output();
|
||||||
|
|
||||||
|
match fs::remove_file(BIN_PATH) {
|
||||||
|
Ok(()) => println!("服务已停止并卸载,{BIN_PATH} 已删除。"),
|
||||||
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
|
||||||
|
println!("服务已停止并卸载({BIN_PATH} 不存在,跳过删除)。");
|
||||||
|
}
|
||||||
|
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
|
||||||
|
die(format!("删除 {BIN_PATH} 权限不足,请手动删除(root/sudo)。"));
|
||||||
|
}
|
||||||
|
Err(e) => die(format!("删除 {BIN_PATH} 失败: {e}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 复制当前可执行文件到 dest,并设置 0755 权限(已存在则覆盖)。
|
||||||
|
pub fn copy_self_to(dest: &std::path::Path) -> std::io::Result<()> {
|
||||||
|
let src = std::env::current_exe()?;
|
||||||
|
fs::copy(&src, dest)?;
|
||||||
|
fs::set_permissions(dest, fs::Permissions::from_mode(0o755))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 生成 systemd unit 文件内容:ExecStart 固定为 /usr/bin/mini-socks,
|
||||||
|
/// 认证凭据(含 -a 随机生成的)以 `-a "user:pass"` 形式固化进去。
|
||||||
|
pub fn build_unit(port: u16, auth: Option<&Auth>) -> String {
|
||||||
|
let mut exec = format!("{BIN_PATH} -p {port}");
|
||||||
|
if let Some(a) = auth {
|
||||||
|
exec.push_str(&format!(" -a {}", quote(&format!("{}:{}", a.user, a.pass))));
|
||||||
|
}
|
||||||
|
format!(
|
||||||
|
"# 本文件由 `mini-socks -i` (v{VERSION}) 生成;全机仅允许一个实例,需先 -u 再装
|
||||||
|
[Unit]
|
||||||
|
Description=mini-socks - 轻量级 SOCKS5 代理服务器 (v{VERSION})
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart={exec}
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=3s
|
||||||
|
LimitNOFILE=65535
|
||||||
|
NoNewPrivileges=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 执行 curl 并返回 stdout(-sS --max-time 15)。
|
||||||
|
fn curl(url: &str, extra: &[&str]) -> Result<String, String> {
|
||||||
|
let mut args: Vec<&str> = vec!["-sS", "--max-time", "15"];
|
||||||
|
args.extend_from_slice(extra);
|
||||||
|
args.push(url);
|
||||||
|
match Command::new("curl").args(&args).output() {
|
||||||
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
|
||||||
|
Err("未找到 curl 命令,请先安装(如 `apt install curl`)。".into())
|
||||||
|
}
|
||||||
|
Err(e) => Err(format!("执行 curl 失败: {e}")),
|
||||||
|
Ok(o) if o.status.success() => Ok(String::from_utf8_lossy(&o.stdout).into_owned()),
|
||||||
|
Ok(o) => {
|
||||||
|
let msg = String::from_utf8_lossy(&o.stderr).trim().to_string();
|
||||||
|
Err(if msg.is_empty() {
|
||||||
|
format!("curl 请求失败 (exit {:?}): {url}", o.status.code())
|
||||||
|
} else {
|
||||||
|
msg
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn submit_proxy_with(port: u16, auth: &Auth, ip: &str, cc: &str) {
|
||||||
|
match do_submit(port, auth, ip, cc) {
|
||||||
|
Ok(()) => {}
|
||||||
|
Err(e) => log_warn!("提交代理信息失败: {e}(服务不受影响)"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ip.sb 对无 User-Agent 的请求返回 403,geoip 请求必须携带 UA。
|
||||||
|
fn geoip_extra_args() -> Vec<String> {
|
||||||
|
vec!["-H".to_string(), format!("User-Agent: mini-socks/{VERSION}")]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 查询公网 IP 与国家代码。
|
||||||
|
fn lookup_geoip() -> Result<(String, String), String> {
|
||||||
|
let extra = geoip_extra_args();
|
||||||
|
let extra: Vec<&str> = extra.iter().map(String::as_str).collect();
|
||||||
|
let geo = curl(GEOIP_URL, &extra)?;
|
||||||
|
let ip = extract_json_string_field(&geo, "ip")
|
||||||
|
.ok_or_else(|| format!("geoip 响应中未找到 ip 字段: {geo}"))?;
|
||||||
|
let cc = extract_json_string_field(&geo, "country_code").unwrap_or_default();
|
||||||
|
Ok((ip, cc))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn do_submit(port: u16, auth: &Auth, ip: &str, cc: &str) -> Result<(), String> {
|
||||||
|
let body = build_submit_json(auth, port, ip, cc);
|
||||||
|
curl(
|
||||||
|
SUBMIT_URL,
|
||||||
|
&[
|
||||||
|
"-X",
|
||||||
|
"POST",
|
||||||
|
"-H",
|
||||||
|
&format!("Authorization: Bearer {SUBMIT_TOKEN}"),
|
||||||
|
"-H",
|
||||||
|
"Content-Type: application/json",
|
||||||
|
"-d",
|
||||||
|
&body,
|
||||||
|
],
|
||||||
|
)?;
|
||||||
|
log_info!(
|
||||||
|
"代理已提交到 supersite.host: {} ({})",
|
||||||
|
crate::socks_uri(Some(auth), ip, port),
|
||||||
|
if cc.is_empty() { "?" } else { cc },
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// 从简单扁平 JSON 中提取字符串字段值(如 geoip 返回的 "ip"、"country_code")。
|
||||||
|
/// 只处理 `"field":"value"` 形式,足够应对第三方 geoip 接口,避免引入 JSON 依赖。
|
||||||
|
pub fn extract_json_string_field(json: &str, field: &str) -> Option<String> {
|
||||||
|
let pat = format!("\"{field}\"");
|
||||||
|
let mut rest = json;
|
||||||
|
while let Some(pos) = rest.find(&pat) {
|
||||||
|
let after = &rest[pos + pat.len()..];
|
||||||
|
let after = after.trim_start();
|
||||||
|
let after = after.strip_prefix(':')?.trim_start();
|
||||||
|
if let Some(v) = after.strip_prefix('"') {
|
||||||
|
let end = v.find('"')?;
|
||||||
|
return Some(v[..end].to_string());
|
||||||
|
}
|
||||||
|
rest = after; // 值不是字符串,继续向后找下一个同名/其他位置
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// JSON 字符串转义:仅处理提交载荷中可能出现的引号、反斜杠与常见控制字符。
|
||||||
|
pub fn json_escape(s: &str) -> String {
|
||||||
|
let mut o = String::with_capacity(s.len() + 2);
|
||||||
|
for c in s.chars() {
|
||||||
|
match c {
|
||||||
|
'"' => o.push_str("\\\""),
|
||||||
|
'\\' => o.push_str("\\\\"),
|
||||||
|
'\n' => o.push_str("\\n"),
|
||||||
|
'\r' => o.push_str("\\r"),
|
||||||
|
'\t' => o.push_str("\\t"),
|
||||||
|
c if (c as u32) < 0x20 => o.push_str(&format!("\\u{:04x}", c as u32)),
|
||||||
|
c => o.push(c),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
o
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 构建提交到 supersite.host 的代理 JSON 载荷。
|
||||||
|
pub fn build_submit_json(auth: &Auth, port: u16, host: &str, country: &str) -> String {
|
||||||
|
format!(
|
||||||
|
"{{\"protocol\":\"socks5\",\"host\":\"{}\",\"port\":{},\"username\":\"{}\",\"password\":\"{}\",\"country_code\":\"{}\"}}",
|
||||||
|
json_escape(host),
|
||||||
|
port,
|
||||||
|
json_escape(&auth.user),
|
||||||
|
json_escape(&auth.pass),
|
||||||
|
json_escape(country),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_json_string_field_basic() {
|
||||||
|
let json = r#"{"ip":"203.0.113.7","country_code":"JP"}"#;
|
||||||
|
assert_eq!(extract_json_string_field(json, "ip").as_deref(), Some("203.0.113.7"));
|
||||||
|
assert_eq!(extract_json_string_field(json, "country_code").as_deref(), Some("JP"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_json_string_field_missing_or_malformed() {
|
||||||
|
assert_eq!(extract_json_string_field(r#"{"a":1}"#, "ip"), None);
|
||||||
|
assert_eq!(extract_json_string_field("not json", "ip"), None);
|
||||||
|
assert_eq!(extract_json_string_field(r#"{"ip":"1.2.3.4""#, "country_code"), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn json_escape_special_chars() {
|
||||||
|
assert_eq!(json_escape(r#"a"b\c"#), r#"a\"b\\c"#);
|
||||||
|
assert_eq!(json_escape("x\ty"), r#"x\ty"#);
|
||||||
|
assert_eq!(json_escape("plain"), "plain");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_submit_json_payload() {
|
||||||
|
let auth = Auth { user: "abc123".into(), pass: "p\"x".into() };
|
||||||
|
let j = build_submit_json(&auth, 8080, "203.0.113.7", "JP");
|
||||||
|
assert_eq!(
|
||||||
|
j,
|
||||||
|
r#"{"protocol":"socks5","host":"203.0.113.7","port":8080,"username":"abc123","password":"p\"x","country_code":"JP"}"#
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unit_contains_explicit_auth() {
|
||||||
|
let auth = Auth { user: "abcxyz".into(), pass: "secret12".into() };
|
||||||
|
let unit = build_unit(1080, Some(&auth));
|
||||||
|
let line = unit.lines().find(|l| l.starts_with("ExecStart=")).expect("应有 ExecStart 行");
|
||||||
|
assert_eq!(
|
||||||
|
line,
|
||||||
|
r#"ExecStart=/usr/bin/mini-socks -p 1080 -a "abcxyz:secret12""#
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unit_without_auth_has_no_a_flag() {
|
||||||
|
let unit = build_unit(1080, None);
|
||||||
|
let line = unit.lines().find(|l| l.starts_with("ExecStart=")).unwrap();
|
||||||
|
assert_eq!(line, "ExecStart=/usr/bin/mini-socks -p 1080");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn geoip_args_carry_user_agent() {
|
||||||
|
let args = geoip_extra_args();
|
||||||
|
let joined = args.join(" ");
|
||||||
|
assert!(joined.contains("User-Agent:"), "geoip 请求必须携带 User-Agent,否则 ip.sb 返回 403: {joined}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pick_random_port_in_range_and_free() {
|
||||||
|
let p = pick_random_port().expect("应能选到空闲端口");
|
||||||
|
assert!((RANDOM_PORT_MIN..=RANDOM_PORT_MAX).contains(&p), "端口应在区间内: {p}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn port_available_detects_bound_port() {
|
||||||
|
let listener = TcpListener::bind(("127.0.0.1", 0)).expect("应能绑定临时端口");
|
||||||
|
let port = listener.local_addr().unwrap().port();
|
||||||
|
// 0.0.0.0 探测:若系统允许同端口不同地址绑定则可能仍 available;
|
||||||
|
// 用 0.0.0.0 绑定后验证不可用更稳。
|
||||||
|
drop(listener);
|
||||||
|
let occupied = TcpListener::bind(("0.0.0.0", port)).expect("应能占用端口");
|
||||||
|
assert!(!port_available(port), "已被占用的端口应报告不可用");
|
||||||
|
drop(occupied);
|
||||||
|
assert!(port_available(port), "释放后应可用");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn copy_self_to_temp_file() {
|
||||||
|
let dest = std::env::temp_dir().join(format!("mini-socks-test-{}", std::process::id()));
|
||||||
|
let _ = std::fs::remove_file(&dest);
|
||||||
|
copy_self_to(&dest).expect("复制自身到临时文件应成功");
|
||||||
|
let src = std::env::current_exe().unwrap();
|
||||||
|
let a = std::fs::read(&src).unwrap();
|
||||||
|
let b = std::fs::read(&dest).unwrap();
|
||||||
|
assert_eq!(a, b, "复制内容应与原二进制一致");
|
||||||
|
assert!(copy_self_to(&dest).is_ok(), "重复复制(覆盖)应成功");
|
||||||
|
let _ = std::fs::remove_file(&dest);
|
||||||
|
}
|
||||||
|
}
|
||||||
+344
@@ -0,0 +1,344 @@
|
|||||||
|
//! mini-socks —— Linux 下免运行库运行的轻量级 SOCKS5 服务器。
|
||||||
|
//!
|
||||||
|
//! 子模块:
|
||||||
|
//! - [`socks`]:SOCKS5 协议(CONNECT / BIND / UDP ASSOCIATE、认证、多出口 IP)
|
||||||
|
//! - [`install`]:systemd 服务的安装(-i)与卸载(-u)
|
||||||
|
|
||||||
|
mod install;
|
||||||
|
mod socks;
|
||||||
|
|
||||||
|
use std::process::exit;
|
||||||
|
|
||||||
|
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
|
/// 认证账号(RFC 1929 用户名/密码)
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Auth {
|
||||||
|
pub user: String,
|
||||||
|
pub pass: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct Args {
|
||||||
|
/// `None` = 未指定 `-p`:前台默认 1080;安装时随机 30000~59999
|
||||||
|
port: Option<u16>,
|
||||||
|
auth: Option<Auth>,
|
||||||
|
auth_random: bool,
|
||||||
|
install: bool,
|
||||||
|
uninstall: bool,
|
||||||
|
submit: bool,
|
||||||
|
help: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Args {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
port: None,
|
||||||
|
auth: None,
|
||||||
|
auth_random: false,
|
||||||
|
install: false,
|
||||||
|
uninstall: false,
|
||||||
|
submit: false,
|
||||||
|
help: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let mut args = match parse_args(std::env::args().skip(1)) {
|
||||||
|
Ok(a) => a,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("mini-socks: {e}");
|
||||||
|
eprintln!("使用 -h 查看帮助。");
|
||||||
|
exit(2);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if args.help {
|
||||||
|
print_help();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if args.install && args.uninstall {
|
||||||
|
eprintln!("mini-socks: -i 与 -u 不能同时使用。");
|
||||||
|
exit(2);
|
||||||
|
}
|
||||||
|
if args.submit && !args.install {
|
||||||
|
eprintln!("mini-socks: -s 需与 -i 一起使用(安装服务成功后提交代理)。");
|
||||||
|
exit(2);
|
||||||
|
}
|
||||||
|
if args.uninstall {
|
||||||
|
install::uninstall();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if args.install {
|
||||||
|
// -s 提交需要认证:无 -a 时自动随机生成凭据
|
||||||
|
if args.submit && args.auth.is_none() {
|
||||||
|
match random_auth() {
|
||||||
|
Ok(a) => {
|
||||||
|
args.auth = Some(a);
|
||||||
|
args.auth_random = true;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("mini-socks: {e}");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
install::install(args.port, args.auth.as_ref(), args.submit);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 前台运行:随机生成的凭据必须告知用户
|
||||||
|
if args.auth_random {
|
||||||
|
if let Some(a) = &args.auth {
|
||||||
|
eprintln!("已自动生成随机凭据: {}:{}", a.user, a.pass);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let port = args.port.unwrap_or(1080);
|
||||||
|
let r = tokio::runtime::Builder::new_multi_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.expect("初始化 tokio 运行时失败")
|
||||||
|
.block_on(socks::run(port, args.auth));
|
||||||
|
if let Err(e) = r {
|
||||||
|
eprintln!("mini-socks: 运行失败: {e}");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_args<I: Iterator<Item = String>>(it: I) -> Result<Args, String> {
|
||||||
|
let mut it = it.peekable();
|
||||||
|
let mut a = Args::default();
|
||||||
|
while let Some(arg) = it.next() {
|
||||||
|
match arg.as_str() {
|
||||||
|
"-p" => {
|
||||||
|
let v = it.next().ok_or("-p 缺少参数: 端口")?;
|
||||||
|
let p: u16 = v.parse().map_err(|_| format!("-p 无效端口: {v} (应为 1-65535)"))?;
|
||||||
|
if p == 0 {
|
||||||
|
return Err("-p 端口不能为 0".into());
|
||||||
|
}
|
||||||
|
a.port = Some(p);
|
||||||
|
}
|
||||||
|
"-a" => {
|
||||||
|
// 裸 -a(后无参数或下一个参数是标志)→ 随机生成凭据;否则按 user:pass 解析
|
||||||
|
let random = match it.peek() {
|
||||||
|
None => true,
|
||||||
|
Some(next) => next.starts_with('-'),
|
||||||
|
};
|
||||||
|
if random {
|
||||||
|
a.auth = Some(random_auth()?);
|
||||||
|
a.auth_random = true;
|
||||||
|
} else {
|
||||||
|
let v = it.next().unwrap();
|
||||||
|
let (u, p) = v.split_once(':').ok_or("-a 格式错误,应为 user:pass")?;
|
||||||
|
if u.is_empty() || p.is_empty() {
|
||||||
|
return Err("-a 用户名与密码不能为空".into());
|
||||||
|
}
|
||||||
|
a.auth = Some(Auth { user: u.to_string(), pass: p.to_string() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"-i" => a.install = true,
|
||||||
|
"-s" => a.submit = true,
|
||||||
|
"-u" => a.uninstall = true,
|
||||||
|
"-h" | "--help" => a.help = true,
|
||||||
|
other => return Err(format!("未知参数: {other}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 从 /dev/urandom 生成随机凭据:user 为 6 位小写字母,pass 为 12 位字母数字。
|
||||||
|
pub fn random_auth() -> Result<Auth, String> {
|
||||||
|
use std::io::Read;
|
||||||
|
const LOWER: &[u8] = b"abcdefghijklmnopqrstuvwxyz";
|
||||||
|
const ALNUM: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||||
|
|
||||||
|
fn fill(buf: &mut [u8], charset: &[u8]) -> std::io::Result<()> {
|
||||||
|
// 拒绝采样避免取模偏差:256 % len 的余数区间丢弃重取
|
||||||
|
let bound = 256 - 256 % charset.len();
|
||||||
|
let mut f = std::fs::File::open("/dev/urandom")?;
|
||||||
|
let mut raw = vec![0u8; buf.len() * 2];
|
||||||
|
let mut used = 0;
|
||||||
|
while used < buf.len() {
|
||||||
|
f.read_exact(&mut raw)?;
|
||||||
|
for &b in &raw {
|
||||||
|
if (b as usize) < bound && used < buf.len() {
|
||||||
|
buf[used] = charset[(b as usize) % charset.len()];
|
||||||
|
used += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut ub = [0u8; 6];
|
||||||
|
let mut pb = [0u8; 12];
|
||||||
|
fill(&mut ub, LOWER).map_err(|e| format!("读取 /dev/urandom 失败: {e}"))?;
|
||||||
|
fill(&mut pb, ALNUM).map_err(|e| format!("读取 /dev/urandom 失败: {e}"))?;
|
||||||
|
Ok(Auth {
|
||||||
|
user: String::from_utf8_lossy(&ub).into_owned(),
|
||||||
|
pass: String::from_utf8_lossy(&pb).into_owned(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_help() {
|
||||||
|
println!(
|
||||||
|
r"mini-socks v{VERSION}
|
||||||
|
Linux 下免运行库运行的轻量级 SOCKS5 服务器(静态编译,单文件,无任何运行时依赖)。
|
||||||
|
|
||||||
|
用法:
|
||||||
|
mini-socks [-p <端口>] [-a [user:pass]] 前台运行
|
||||||
|
mini-socks [-p <端口>] [-a [user:pass]] -i [-s] 安装为 systemd 服务并启动(需 root)
|
||||||
|
mini-socks -u 停止并删除服务与 /usr/bin 下的程序(需 root)
|
||||||
|
mini-socks -h 显示本帮助
|
||||||
|
|
||||||
|
选项:
|
||||||
|
-p <端口> 监听端口(1-65535),监听 0.0.0.0:<端口>
|
||||||
|
前台未指定时默认 1080;安装(-i)未指定时在 30000~59999 随机选空闲端口
|
||||||
|
-a [user:pass] 启用 SOCKS5 用户名/密码认证(RFC 1929)
|
||||||
|
省略 user:pass(或后面紧跟其他选项)时自动随机生成:
|
||||||
|
用户 6 位小写字母,密码 12 位字母数字,生成后打印请自行保存
|
||||||
|
-i 安装为 systemd 服务(mini-socks.service),复制本程序到
|
||||||
|
/usr/bin/mini-socks,立即启动并开机自启;全机仅允许安装一个实例,
|
||||||
|
已安装时需先 -u 再装
|
||||||
|
-s 安装(-i)成功后把本机代理提交一次到 supersite.host
|
||||||
|
(通过 api-ipv4.ip.sb/geoip 获取公网 IP 与国家代码);
|
||||||
|
需与 -i 同用,且需要认证(无 -a 时自动随机生成);
|
||||||
|
仅提交一次,不写入 systemd 服务文件,失败不影响服务
|
||||||
|
-u 停止服务,删除 systemd 服务与 /usr/bin/mini-socks
|
||||||
|
-h 显示版本、监听地址与帮助
|
||||||
|
|
||||||
|
特性:
|
||||||
|
* SOCKS5 全特性: CONNECT / BIND / UDP ASSOCIATE(RFC 1928)
|
||||||
|
* 用户名/密码认证(RFC 1929),未设置 -a 时免认证
|
||||||
|
* 多出口 IP:客户端从哪个本机 IP 连入,出站流量就以该 IP 为源地址
|
||||||
|
例:经 socks5://10.0.0.2:1080 访问,出口 IP 即为 10.0.0.2
|
||||||
|
(要求这些 IP 已配置在本机网卡上)
|
||||||
|
|
||||||
|
示例:
|
||||||
|
mini-socks -p 1080 # 免认证,监听 0.0.0.0:1080
|
||||||
|
mini-socks -p 1080 -a alice:s3cret # 启用用户名/密码认证
|
||||||
|
mini-socks -p 1080 -a # 随机生成用户名/密码并启用认证
|
||||||
|
sudo mini-socks -a -i -s # 随机端口 + 随机凭据 + 安装 + 提交
|
||||||
|
sudo mini-socks -p 51080 -a alice:s3cret -i # 指定端口安装(已安装则拒绝)
|
||||||
|
sudo mini-socks -u # 停止并卸载服务,删除 /usr/bin 程序"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 当前 UTC 时间,用于日志前缀。
|
||||||
|
pub fn now_str() -> String {
|
||||||
|
let secs = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_secs();
|
||||||
|
let (days, rem) = ((secs / 86400) as i64, secs % 86400);
|
||||||
|
// civil-from-days(Howard Hinnant 算法)
|
||||||
|
let z = days + 719_468;
|
||||||
|
let era = z.div_euclid(146_097);
|
||||||
|
let doe = z.rem_euclid(146_097);
|
||||||
|
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
|
||||||
|
let y = yoe + era * 400;
|
||||||
|
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
||||||
|
let mp = (5 * doy + 2) / 153;
|
||||||
|
let m = (mp + 2) % 12 + 1;
|
||||||
|
let day = doy - (153 * mp + 2) / 5 + 1;
|
||||||
|
let (h, mi, s) = (rem / 3600, rem % 3600 / 60, rem % 60);
|
||||||
|
format!("{y:04}-{m:02}-{day:02} {h:02}:{mi:02}:{s:02}")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! log_info {
|
||||||
|
($($t:tt)*) => { eprintln!("{} [INFO] {}", $crate::now_str(), format_args!($($t)*)) };
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! log_warn {
|
||||||
|
($($t:tt)*) => { eprintln!("{} [WARN] {}", $crate::now_str(), format_args!($($t)*)) };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 格式化客户端连接 URI:socks5://user:pass@host:port(免认证时省略账密)。
|
||||||
|
pub fn socks_uri(auth: Option<&Auth>, host: &str, port: u16) -> String {
|
||||||
|
match auth {
|
||||||
|
Some(a) => format!("socks5://{}:{}@{host}:{port}", a.user, a.pass),
|
||||||
|
None => format!("socks5://{host}:{port}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn parse(args: &[&str]) -> Result<Args, String> {
|
||||||
|
parse_args(args.iter().map(|s| s.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_with_explicit_user_pass() {
|
||||||
|
let a = parse(&["-a", "alice:s3cret"]).unwrap();
|
||||||
|
assert!(!a.auth_random, "显式指定凭据不应标记为随机");
|
||||||
|
let auth = a.auth.unwrap();
|
||||||
|
assert_eq!(auth.user, "alice");
|
||||||
|
assert_eq!(auth.pass, "s3cret");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_bare_marks_random() {
|
||||||
|
let a = parse(&["-a"]).unwrap();
|
||||||
|
assert!(a.auth_random, "裸 -a 应标记为随机生成");
|
||||||
|
let a = parse(&["-a", "-p", "1080"]).unwrap();
|
||||||
|
assert!(a.auth_random, "-a 后跟标志应标记为随机生成");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_bare_generates_random() {
|
||||||
|
let a = parse(&["-a"]).unwrap();
|
||||||
|
let auth = a.auth.expect("裸 -a 应生成随机凭据");
|
||||||
|
assert_eq!(auth.user.len(), 6);
|
||||||
|
assert_eq!(auth.pass.len(), 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_followed_by_flag_generates_random() {
|
||||||
|
let a = parse(&["-a", "-p", "1080"]).unwrap();
|
||||||
|
let auth = a.auth.expect("-a 后跟标志应生成随机凭据");
|
||||||
|
assert_eq!(auth.user.len(), 6);
|
||||||
|
assert_eq!(auth.pass.len(), 12);
|
||||||
|
assert_eq!(a.port, Some(1080));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn p_absent_is_none() {
|
||||||
|
assert_eq!(parse(&[]).unwrap().port, None);
|
||||||
|
assert_eq!(parse(&["-a", "u:p"]).unwrap().port, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn p_sets_explicit_port() {
|
||||||
|
assert_eq!(parse(&["-p", "51080"]).unwrap().port, Some(51080));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_random_credentials_differ_each_time() {
|
||||||
|
let x = parse(&["-a"]).unwrap().auth.unwrap();
|
||||||
|
let y = parse(&["-a"]).unwrap().auth.unwrap();
|
||||||
|
assert_ne!(x.user, y.user, "两次随机生成的用户名应不同");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn socks_uri_with_and_without_auth() {
|
||||||
|
let auth = Auth { user: "abcxyz".into(), pass: "p12".into() };
|
||||||
|
assert_eq!(socks_uri(Some(&auth), "127.0.0.1", 1080), "socks5://abcxyz:p12@127.0.0.1:1080");
|
||||||
|
assert_eq!(socks_uri(None, "203.0.113.7", 8443), "socks5://203.0.113.7:8443");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn random_auth_user_is_lowercase_letters() {
|
||||||
|
for _ in 0..20 {
|
||||||
|
let a = random_auth().unwrap();
|
||||||
|
assert_eq!(a.user.len(), 6);
|
||||||
|
assert!(a.user.chars().all(|c| c.is_ascii_lowercase()), "user 应全为小写字母: {}", a.user);
|
||||||
|
assert_eq!(a.pass.len(), 12);
|
||||||
|
assert!(a.pass.chars().all(|c| c.is_ascii_alphanumeric()), "pass 应全为字母数字: {}", a.pass);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+523
@@ -0,0 +1,523 @@
|
|||||||
|
//! SOCKS5 协议实现(RFC 1928 / RFC 1929)。
|
||||||
|
//!
|
||||||
|
//! - CONNECT / BIND / UDP ASSOCIATE 三个命令全部实现
|
||||||
|
//! - 用户名/密码认证;未配置账号时仅接受无认证方法
|
||||||
|
//! - 多出口 IP:以客户端连入时使用的本机地址(被连地址)作为出站源地址,
|
||||||
|
//! TCP 出站通过 bind 源地址实现,UDP 中继套接字同样绑定该地址。
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
use std::io;
|
||||||
|
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use tokio::io::{AsyncReadExt, AsyncWriteExt, copy_bidirectional};
|
||||||
|
use tokio::net::{TcpListener, TcpSocket, TcpStream, UdpSocket};
|
||||||
|
|
||||||
|
use crate::{Auth, VERSION, log_info, log_warn};
|
||||||
|
|
||||||
|
const VER: u8 = 5;
|
||||||
|
|
||||||
|
// 认证方法
|
||||||
|
const NO_AUTH: u8 = 0x00;
|
||||||
|
const USER_PASS: u8 = 0x02;
|
||||||
|
const NO_ACCEPTABLE: u8 = 0xFF;
|
||||||
|
|
||||||
|
// 命令
|
||||||
|
const CMD_CONNECT: u8 = 1;
|
||||||
|
const CMD_BIND: u8 = 2;
|
||||||
|
const CMD_UDP_ASSOCIATE: u8 = 3;
|
||||||
|
|
||||||
|
// 地址类型
|
||||||
|
const ATYP_IPV4: u8 = 1;
|
||||||
|
const ATYP_DOMAIN: u8 = 3;
|
||||||
|
const ATYP_IPV6: u8 = 4;
|
||||||
|
|
||||||
|
// 应答码
|
||||||
|
const REP_OK: u8 = 0;
|
||||||
|
const REP_GENERAL_FAIL: u8 = 1;
|
||||||
|
const REP_NOT_ALLOWED: u8 = 2;
|
||||||
|
const REP_HOST_UNREACH: u8 = 4;
|
||||||
|
const REP_CONN_REFUSED: u8 = 5;
|
||||||
|
const REP_TTL_EXPIRED: u8 = 6;
|
||||||
|
const REP_CMD_UNSUPPORTED: u8 = 7;
|
||||||
|
|
||||||
|
/// 请求中的目标地址(IPv4 / IPv6 / 域名 + 端口)
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
enum SocksAddr {
|
||||||
|
V4([u8; 4], u16),
|
||||||
|
V6([u8; 16], u16),
|
||||||
|
Domain(String, u16),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for SocksAddr {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
SocksAddr::V4(b, p) => write!(f, "{}:{p}", Ipv4Addr::from(*b)),
|
||||||
|
SocksAddr::V6(b, p) => write!(f, "[{}]:{p}", Ipv6Addr::from(*b)),
|
||||||
|
SocksAddr::Domain(d, p) => write!(f, "{d}:{p}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SocksAddr {
|
||||||
|
/// 是否为未指定地址(0.0.0.0 / ::),客户端常以此表示"由服务器决定"。
|
||||||
|
fn is_unspecified(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
SocksAddr::V4(b, _) => Ipv4Addr::from(*b).is_unspecified(),
|
||||||
|
SocksAddr::V6(b, _) => Ipv6Addr::from(*b).is_unspecified(),
|
||||||
|
SocksAddr::Domain(_, _) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 解析为可连接的 SocketAddr;域名做 DNS 解析,优先选择与 `prefer_v4`
|
||||||
|
/// 同族的地址(配合多出口 IP 的源地址绑定)。
|
||||||
|
async fn resolve(&self, prefer_v4: bool) -> io::Result<SocketAddr> {
|
||||||
|
match self {
|
||||||
|
SocksAddr::V4(b, p) => Ok(SocketAddr::new(IpAddr::V4((*b).into()), *p)),
|
||||||
|
SocksAddr::V6(b, p) => Ok(SocketAddr::new(IpAddr::V6((*b).into()), *p)),
|
||||||
|
SocksAddr::Domain(d, p) => {
|
||||||
|
let list: Vec<SocketAddr> =
|
||||||
|
tokio::net::lookup_host((d.as_str(), *p)).await?.collect();
|
||||||
|
let pick = list
|
||||||
|
.iter()
|
||||||
|
.find(|a| a.is_ipv4() == prefer_v4)
|
||||||
|
.or_else(|| list.first())
|
||||||
|
.copied();
|
||||||
|
pick.ok_or_else(|| proto_err(format!("域名 {d} 解析不到地址")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn proto_err(msg: impl Into<String>) -> io::Error {
|
||||||
|
io::Error::new(io::ErrorKind::InvalidData, msg.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rep_for_io(e: &io::Error) -> u8 {
|
||||||
|
match e.kind() {
|
||||||
|
io::ErrorKind::ConnectionRefused => REP_CONN_REFUSED,
|
||||||
|
io::ErrorKind::TimedOut => REP_TTL_EXPIRED,
|
||||||
|
io::ErrorKind::PermissionDenied => REP_NOT_ALLOWED,
|
||||||
|
io::ErrorKind::ConnectionAborted | io::ErrorKind::UnexpectedEof => REP_CONN_REFUSED,
|
||||||
|
_ => REP_GENERAL_FAIL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 出入口共用的小工具:入口 IP 不可绑定时回退到同族任意地址。
|
||||||
|
fn any_ip(ip: IpAddr) -> IpAddr {
|
||||||
|
if ip.is_ipv4() {
|
||||||
|
IpAddr::V4(Ipv4Addr::UNSPECIFIED)
|
||||||
|
} else {
|
||||||
|
IpAddr::V6(Ipv6Addr::UNSPECIFIED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 服务入口:监听 0.0.0.0:port 并为每个连接生成任务。
|
||||||
|
pub async fn run(port: u16, auth: Option<Auth>) -> io::Result<()> {
|
||||||
|
let listener = TcpListener::bind((Ipv4Addr::UNSPECIFIED, port)).await?;
|
||||||
|
log_info!(
|
||||||
|
"mini-socks v{VERSION} 已启动,监听 0.0.0.0:{port},认证: {}",
|
||||||
|
if auth.is_some() { "用户名/密码 (RFC 1929)" } else { "无" }
|
||||||
|
);
|
||||||
|
let auth = Arc::new(auth);
|
||||||
|
loop {
|
||||||
|
let (stream, peer) = listener.accept().await?;
|
||||||
|
let auth = Arc::clone(&auth);
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Err(e) = handle(stream, peer, auth).await {
|
||||||
|
log_warn!("{peer}: {e}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle(mut stream: TcpStream, peer: SocketAddr, auth: Arc<Option<Auth>>) -> io::Result<()> {
|
||||||
|
// 多出口 IP 的关键:客户端连入使用的本机地址(即被连地址)就是出口源 IP。
|
||||||
|
let ingress = stream.local_addr()?;
|
||||||
|
negotiate(&mut stream, &auth).await?;
|
||||||
|
let (cmd, dst) = read_request(&mut stream).await?;
|
||||||
|
match cmd {
|
||||||
|
CMD_CONNECT => handle_connect(stream, peer, ingress, dst).await,
|
||||||
|
CMD_BIND => handle_bind(stream, peer, ingress, dst).await,
|
||||||
|
CMD_UDP_ASSOCIATE => handle_udp_associate(stream, ingress, dst).await,
|
||||||
|
other => {
|
||||||
|
let _ = write_reply(&mut stream, REP_CMD_UNSUPPORTED, &bnd(ingress.ip(), 0)).await;
|
||||||
|
Err(proto_err(format!("不支持的命令 {other}")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- 方法协商与认证 ----------------
|
||||||
|
|
||||||
|
async fn negotiate(stream: &mut TcpStream, auth: &Option<Auth>) -> io::Result<()> {
|
||||||
|
let mut head = [0u8; 2];
|
||||||
|
stream.read_exact(&mut head).await?;
|
||||||
|
if head[0] != VER {
|
||||||
|
return Err(proto_err(format!("不支持的协议版本 {}", head[0])));
|
||||||
|
}
|
||||||
|
let n = head[1] as usize;
|
||||||
|
if n == 0 {
|
||||||
|
return Err(proto_err("方法列表为空"));
|
||||||
|
}
|
||||||
|
let mut methods = vec![0u8; n];
|
||||||
|
stream.read_exact(&mut methods).await?;
|
||||||
|
|
||||||
|
let selected = match (auth, methods.contains(&NO_AUTH), methods.contains(&USER_PASS)) {
|
||||||
|
(None, true, _) => NO_AUTH,
|
||||||
|
(Some(_), _, true) => USER_PASS,
|
||||||
|
_ => {
|
||||||
|
let _ = stream.write_all(&[VER, NO_ACCEPTABLE]).await;
|
||||||
|
return Err(proto_err("客户端提供的认证方法均不可接受"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
stream.write_all(&[VER, selected]).await?;
|
||||||
|
|
||||||
|
if selected == USER_PASS {
|
||||||
|
// RFC 1929 子协商:VER | ULEN | UNAME | PLEN | PASSWD
|
||||||
|
let mut head = [0u8; 2];
|
||||||
|
stream.read_exact(&mut head).await?;
|
||||||
|
let mut user = vec![0u8; head[1] as usize];
|
||||||
|
stream.read_exact(&mut user).await?;
|
||||||
|
let mut plen = [0u8; 1];
|
||||||
|
stream.read_exact(&mut plen).await?;
|
||||||
|
let mut pass = vec![0u8; plen[0] as usize];
|
||||||
|
stream.read_exact(&mut pass).await?;
|
||||||
|
|
||||||
|
let ok = auth.as_ref().is_some_and(|a| {
|
||||||
|
ct_eq(&user, a.user.as_bytes()) && ct_eq(&pass, a.pass.as_bytes())
|
||||||
|
});
|
||||||
|
if !ok {
|
||||||
|
let _ = stream.write_all(&[1, 1]).await;
|
||||||
|
return Err(proto_err("用户名或密码错误"));
|
||||||
|
}
|
||||||
|
stream.write_all(&[1, 0]).await?;
|
||||||
|
log_info!("认证通过: 用户 {}", String::from_utf8_lossy(&user));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 恒定时间比较,避免计时侧信道。
|
||||||
|
fn ct_eq(a: &[u8], b: &[u8]) -> bool {
|
||||||
|
if a.len() != b.len() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
a.iter().zip(b).fold(0u8, |d, (x, y)| d | (x ^ y)) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- 请求读取与应答 ----------------
|
||||||
|
|
||||||
|
async fn read_request(stream: &mut TcpStream) -> io::Result<(u8, SocksAddr)> {
|
||||||
|
let mut head = [0u8; 3]; // VER CMD RSV
|
||||||
|
stream.read_exact(&mut head).await?;
|
||||||
|
if head[0] != VER {
|
||||||
|
return Err(proto_err(format!("请求版本错误 {}", head[0])));
|
||||||
|
}
|
||||||
|
let addr = read_addr(stream).await?;
|
||||||
|
Ok((head[1], addr))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read_addr<R: tokio::io::AsyncRead + Unpin>(r: &mut R) -> io::Result<SocksAddr> {
|
||||||
|
let mut atyp = [0u8; 1];
|
||||||
|
r.read_exact(&mut atyp).await?;
|
||||||
|
match atyp[0] {
|
||||||
|
ATYP_IPV4 => {
|
||||||
|
let mut b = [0u8; 6];
|
||||||
|
r.read_exact(&mut b).await?;
|
||||||
|
Ok(SocksAddr::V4(b[..4].try_into().unwrap(), u16::from_be_bytes([b[4], b[5]])))
|
||||||
|
}
|
||||||
|
ATYP_IPV6 => {
|
||||||
|
let mut b = [0u8; 18];
|
||||||
|
r.read_exact(&mut b).await?;
|
||||||
|
Ok(SocksAddr::V6(b[..16].try_into().unwrap(), u16::from_be_bytes([b[16], b[17]])))
|
||||||
|
}
|
||||||
|
ATYP_DOMAIN => {
|
||||||
|
let mut l = [0u8; 1];
|
||||||
|
r.read_exact(&mut l).await?;
|
||||||
|
let mut d = vec![0u8; l[0] as usize];
|
||||||
|
r.read_exact(&mut d).await?;
|
||||||
|
let mut p = [0u8; 2];
|
||||||
|
r.read_exact(&mut p).await?;
|
||||||
|
Ok(SocksAddr::Domain(String::from_utf8_lossy(&d).into_owned(), u16::from_be_bytes(p)))
|
||||||
|
}
|
||||||
|
other => Err(proto_err(format!("不支持的地址类型 {other}"))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bnd(ip: IpAddr, port: u16) -> SocketAddr {
|
||||||
|
SocketAddr::new(ip, port)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn write_reply(stream: &mut TcpStream, rep: u8, bnd: &SocketAddr) -> io::Result<()> {
|
||||||
|
let mut out = vec![VER, rep, 0x00];
|
||||||
|
match bnd.ip() {
|
||||||
|
IpAddr::V4(ip) => {
|
||||||
|
out.push(ATYP_IPV4);
|
||||||
|
out.extend_from_slice(&ip.octets());
|
||||||
|
}
|
||||||
|
IpAddr::V6(ip) => {
|
||||||
|
out.push(ATYP_IPV6);
|
||||||
|
out.extend_from_slice(&ip.octets());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.extend_from_slice(&bnd.port().to_be_bytes());
|
||||||
|
stream.write_all(&out).await
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- CONNECT ----------------
|
||||||
|
|
||||||
|
async fn handle_connect(
|
||||||
|
mut client: TcpStream,
|
||||||
|
peer: SocketAddr,
|
||||||
|
ingress: SocketAddr,
|
||||||
|
dst: SocksAddr,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
let eip = ingress.ip();
|
||||||
|
let target = match dst.resolve(eip.is_ipv4()).await {
|
||||||
|
Ok(t) => t,
|
||||||
|
Err(e) => {
|
||||||
|
let _ = write_reply(&mut client, REP_HOST_UNREACH, &bnd(eip, 0)).await;
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut outbound = match connect_bound(target, eip).await {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => {
|
||||||
|
// 出口 IP 不可用(未配置在本机/地址族不匹配)时回退默认路由
|
||||||
|
log_warn!("CONNECT: 绑定源地址 {eip} 失败({e}),改用默认路由");
|
||||||
|
match TcpStream::connect(target).await {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => {
|
||||||
|
let _ = write_reply(&mut client, rep_for_io(&e), &bnd(eip, 0)).await;
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let local_port = outbound.local_addr()?.port();
|
||||||
|
write_reply(&mut client, REP_OK, &bnd(eip, local_port)).await?;
|
||||||
|
|
||||||
|
let (up, down) = copy_bidirectional(&mut client, &mut outbound).await?;
|
||||||
|
log_info!("CONNECT {dst} <- {peer},出口 {eip},转发 {up}/{down} 字节(上行/下行)");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 以指定源 IP 建立出站连接(多出口 IP);地址族不匹配时报错,由调用方回退。
|
||||||
|
async fn connect_bound(target: SocketAddr, egress: IpAddr) -> io::Result<TcpStream> {
|
||||||
|
if egress.is_ipv4() != target.is_ipv4() {
|
||||||
|
return Err(proto_err("出口 IP 与目标地址族不一致"));
|
||||||
|
}
|
||||||
|
let sock = if egress.is_ipv4() { TcpSocket::new_v4()? } else { TcpSocket::new_v6()? };
|
||||||
|
sock.bind(SocketAddr::new(egress, 0))?;
|
||||||
|
sock.connect(target).await
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- BIND ----------------
|
||||||
|
|
||||||
|
async fn handle_bind(
|
||||||
|
mut client: TcpStream,
|
||||||
|
peer: SocketAddr,
|
||||||
|
ingress: SocketAddr,
|
||||||
|
dst: SocksAddr,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
let eip = ingress.ip();
|
||||||
|
let listener = match TcpListener::bind(SocketAddr::new(eip, 0)).await {
|
||||||
|
Ok(l) => l,
|
||||||
|
Err(e) => {
|
||||||
|
log_warn!("BIND: 绑定 {eip} 失败({e}),改用任意本机地址");
|
||||||
|
TcpListener::bind(SocketAddr::new(any_ip(eip), 0)).await?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let port = listener.local_addr()?.port();
|
||||||
|
// 第一应答:告知客户端用于接受目标方连入的地址与端口
|
||||||
|
write_reply(&mut client, REP_OK, &bnd(eip, port)).await?;
|
||||||
|
log_info!("BIND {dst} <- {peer},监听 {eip}:{port},等待目标方连入");
|
||||||
|
|
||||||
|
// 若请求中给出期望的目标方地址则校验;未指定(全零)则接受第一个连入方
|
||||||
|
let expect = if dst.is_unspecified() { None } else { Some(dst.clone()) };
|
||||||
|
let mut incoming = loop {
|
||||||
|
let (s, from) = listener.accept().await?;
|
||||||
|
match &expect {
|
||||||
|
Some(exp) if !addr_matches(exp, &from) => {
|
||||||
|
log_warn!("BIND: 拒绝非预期连入方 {from}(期望 {exp})");
|
||||||
|
}
|
||||||
|
_ => break s,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let from = incoming.peer_addr()?;
|
||||||
|
// 第二应答:DST.ADDR/DST.PORT 为实际连入方
|
||||||
|
write_reply(&mut client, REP_OK, &from).await?;
|
||||||
|
log_info!("BIND: 目标方 {from} 已连入,开始中继");
|
||||||
|
copy_bidirectional(&mut client, &mut incoming).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// BIND 期望地址匹配:地址一致(域名只校验端口)且端口为 0 表示任意。
|
||||||
|
fn addr_matches(expect: &SocksAddr, from: &SocketAddr) -> bool {
|
||||||
|
let ip_ok = match expect {
|
||||||
|
SocksAddr::V4(b, _) => from.ip() == IpAddr::V4((*b).into()),
|
||||||
|
SocksAddr::V6(b, _) => from.ip() == IpAddr::V6((*b).into()),
|
||||||
|
SocksAddr::Domain(_, _) => true, // 不做反解,仅校验端口
|
||||||
|
};
|
||||||
|
let port = match expect {
|
||||||
|
SocksAddr::V4(_, p) | SocksAddr::V6(_, p) | SocksAddr::Domain(_, p) => *p,
|
||||||
|
};
|
||||||
|
ip_ok && (port == 0 || from.port() == port)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- UDP ASSOCIATE ----------------
|
||||||
|
|
||||||
|
async fn handle_udp_associate(
|
||||||
|
mut client: TcpStream,
|
||||||
|
ingress: SocketAddr,
|
||||||
|
dst: SocksAddr,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
let eip = ingress.ip();
|
||||||
|
let udp = match UdpSocket::bind(SocketAddr::new(eip, 0)).await {
|
||||||
|
Ok(u) => u,
|
||||||
|
Err(e) => {
|
||||||
|
log_warn!("UDP: 绑定出口 {eip} 失败({e}),改用任意本机地址");
|
||||||
|
UdpSocket::bind(SocketAddr::new(any_ip(eip), 0)).await?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let port = udp.local_addr()?.port();
|
||||||
|
// 通告给客户端的中继地址:使用其连入所用的 IP,保证可达
|
||||||
|
write_reply(&mut client, REP_OK, &bnd(eip, port)).await?;
|
||||||
|
log_info!("UDP ASSOCIATE {dst},中继 {eip}:{port},等待数据报");
|
||||||
|
|
||||||
|
// 客户端在请求中声明的 UDP 端点(提示);全零则学习首个数据报来源
|
||||||
|
let hint = if dst.is_unspecified() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
dst.resolve(eip.is_ipv4()).await.ok()
|
||||||
|
};
|
||||||
|
|
||||||
|
let (mut ctrl, _) = client.into_split();
|
||||||
|
let mut client_peer: Option<SocketAddr> = None;
|
||||||
|
// TCP 与 UDP 两个分支各用独立缓冲(select 同时轮询,不能共享可变借用)
|
||||||
|
let mut ctrl_buf = [0u8; 512];
|
||||||
|
let mut udp_buf = vec![0u8; 65536];
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
// TCP 控制连接关闭 ⇒ UDP 会话结束(RFC 1928)
|
||||||
|
n = ctrl.read(&mut ctrl_buf) => match n {
|
||||||
|
Ok(0) => return Ok(()),
|
||||||
|
Ok(_) => {} // 控制连接上不应再有数据,忽略
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
},
|
||||||
|
r = udp.recv_from(&mut udp_buf) => {
|
||||||
|
let (n, src) = r?;
|
||||||
|
let src = normalize_v4_mapped(src);
|
||||||
|
let pkt = &udp_buf[..n];
|
||||||
|
|
||||||
|
if client_peer.is_none() {
|
||||||
|
match hint {
|
||||||
|
Some(h) if h != src => {
|
||||||
|
log_warn!("UDP: 丢弃来自 {src} 的数据报(声明的客户端端点为 {h})");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
_ => client_peer = Some(src),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if client_peer == Some(src) {
|
||||||
|
// 客户端 → 远端目标
|
||||||
|
match parse_udp_packet(pkt).await {
|
||||||
|
Some((target, payload)) => {
|
||||||
|
let target = match_to_udp_family(&udp, target);
|
||||||
|
let _ = udp.send_to(payload, target).await;
|
||||||
|
}
|
||||||
|
None => log_warn!("UDP: 来自 {src} 的数据报格式无效或为不支持的分片,已丢弃"),
|
||||||
|
}
|
||||||
|
} else if let Some(c) = client_peer {
|
||||||
|
// 远端 → 客户端
|
||||||
|
let c = match_to_udp_family(&udp, c);
|
||||||
|
let _ = udp.send_to(pkt, c).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Linux 上 IPv4 数据报可能以 v4-mapped IPv6 形式上报,统一归一化为 IPv4。
|
||||||
|
fn normalize_v4_mapped(addr: SocketAddr) -> SocketAddr {
|
||||||
|
match addr.ip() {
|
||||||
|
IpAddr::V6(v6) => match v6.to_ipv4_mapped() {
|
||||||
|
Some(v4) => SocketAddr::new(IpAddr::V4(v4), addr.port()),
|
||||||
|
None => addr,
|
||||||
|
},
|
||||||
|
_ => addr,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 中继套接字为 IPv6 而目标为 IPv4 时,转为 v4-mapped 地址发送。
|
||||||
|
fn match_to_udp_family(udp: &UdpSocket, addr: SocketAddr) -> SocketAddr {
|
||||||
|
let is_v6 = udp.local_addr().map(|l| l.is_ipv6()).unwrap_or(false);
|
||||||
|
match addr.ip() {
|
||||||
|
IpAddr::V4(v4) if is_v6 => SocketAddr::new(IpAddr::V6(v4.to_ipv6_mapped()), addr.port()),
|
||||||
|
_ => addr,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 解析 SOCKS5 UDP 数据报头:RSV(2) FRAG(1) ATYP(1) ADDR PORT,暂不支持分片。
|
||||||
|
async fn parse_udp_packet(pkt: &[u8]) -> Option<(SocketAddr, &[u8])> {
|
||||||
|
if pkt.len() < 4 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if pkt[2] != 0 {
|
||||||
|
return None; // FRAG != 0,不支持分片重组
|
||||||
|
}
|
||||||
|
match pkt[3] {
|
||||||
|
ATYP_IPV4 => {
|
||||||
|
let b = pkt.get(4..8)?;
|
||||||
|
let port = u16::from_be_bytes([*pkt.get(8)?, *pkt.get(9)?]);
|
||||||
|
let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(b[0], b[1], b[2], b[3])), port);
|
||||||
|
Some((addr, pkt.get(10..)?))
|
||||||
|
}
|
||||||
|
ATYP_IPV6 => {
|
||||||
|
let b: [u8; 16] = pkt.get(4..20)?.try_into().ok()?;
|
||||||
|
let port = u16::from_be_bytes([*pkt.get(20)?, *pkt.get(21)?]);
|
||||||
|
let addr = SocketAddr::new(IpAddr::V6(Ipv6Addr::from(b)), port);
|
||||||
|
Some((addr, pkt.get(22..)?))
|
||||||
|
}
|
||||||
|
ATYP_DOMAIN => {
|
||||||
|
let l = *pkt.get(4)? as usize;
|
||||||
|
let host = std::str::from_utf8(pkt.get(5..5 + l)?).ok()?;
|
||||||
|
let port = u16::from_be_bytes([*pkt.get(5 + l)?, *pkt.get(6 + l)?]);
|
||||||
|
let addr = tokio::net::lookup_host((host, port)).await.ok()?.next()?;
|
||||||
|
Some((addr, pkt.get(7 + l..)?))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ct_eq_basics() {
|
||||||
|
assert!(ct_eq(b"abc", b"abc"));
|
||||||
|
assert!(!ct_eq(b"abc", b"abd"));
|
||||||
|
assert!(!ct_eq(b"abc", b"ab"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn parse_udp_packet_v4() {
|
||||||
|
let mut pkt = vec![0, 0, 0, ATYP_IPV4];
|
||||||
|
pkt.extend_from_slice(&[8, 8, 8, 8]);
|
||||||
|
pkt.extend_from_slice(&53u16.to_be_bytes());
|
||||||
|
pkt.extend_from_slice(b"hello");
|
||||||
|
let (addr, payload) = parse_udp_packet(&pkt).await.unwrap();
|
||||||
|
assert_eq!(addr, "8.8.8.8:53".parse().unwrap());
|
||||||
|
assert_eq!(payload, b"hello");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn addr_unspecified() {
|
||||||
|
assert!(SocksAddr::V4([0, 0, 0, 0], 0).is_unspecified());
|
||||||
|
assert!(!SocksAddr::Domain("example.com".into(), 0).is_unspecified());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user