Fix gVisor ICMP interception: proxy ping through WireGuard tunnel
Fixes: admin/mini-sing#1 gVisor TUN stack intercepts ICMP echo requests and replies locally with fake <1ms latency. Real pings to Tailscale peers never reach the WireGuard tunnel. Fix: PrepareConnection now checks ICMP destinations against route rules. If the matching outbound implements ICMPPinger (e.g. Tailscale), returns an icmpProxy DirectRouteDestination that: 1. Sends the ICMP echo through tailnet's netstack via DialPing("ping4") 2. Reads the real reply from the WireGuard tunnel 3. Rebuilds a raw IP+ICMP reply packet 4. Writes it back to the TUN via DirectRouteContext Changes: - adapter/outbound.go: Add ICMPPinger interface - adapter/router.go: Add FindOutbound to ConnectionRouterEx - route/router.go: Expose FindOutbound method - protocol/tailscale/outbound.go: Implement ICMPPinger via tailnet.DialPing - protocol/tun/inbound.go: ICMP proxy in PrepareConnection + icmpProxy type - tailnet/outbound.go: Add DialPing method (wraps tnet.DialContext("ping4"))
This commit is contained in:
338
FEATURE_SURVEY.md
Normal file
338
FEATURE_SURVEY.md
Normal file
@@ -0,0 +1,338 @@
|
||||
# 代理/VPN 客户端功能全面调研报告
|
||||
|
||||
> 调研日期: 2026-04-02
|
||||
> 调研对象: sing-box, Clash (原版), mihomo (Clash.Meta), Hiddify, FlClash, Stash, mini-sing
|
||||
|
||||
---
|
||||
|
||||
## 一、功能矩阵对照表
|
||||
|
||||
### A. 协议支持
|
||||
|
||||
#### 入站协议 (Inbound)
|
||||
|
||||
| 协议 | sing-box | Clash | mihomo | Hiddify | FlClash | Stash | mini-sing |
|
||||
|------|----------|-------|--------|---------|---------|-------|-----------|
|
||||
| Mixed (SOCKS+HTTP) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| SOCKS (独立) | ✅ | ✅ | ✅ | - | - | ✅ | - |
|
||||
| HTTP (独立) | ✅ | ✅ | ✅ | - | - | ✅ | - |
|
||||
| TUN | ✅ | ✅(Premium) | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Redirect | ✅ | ✅ | ✅ | - | - | - | - |
|
||||
| TProxy | ✅ | ✅ | ✅ | - | - | - | - |
|
||||
| Shadowsocks | ✅ | - | - | - | - | - | ✅ |
|
||||
| Trojan | ✅ | - | - | - | - | - | ✅ |
|
||||
| VMess | ✅ | - | - | - | - | - | - |
|
||||
| VLESS | ✅ | - | - | - | - | - | - |
|
||||
| Hysteria/Hysteria2 | ✅ | - | - | - | - | - | - |
|
||||
| TUIC | ✅ | - | - | - | - | - | - |
|
||||
| Naive | ✅ | - | - | - | - | - | - |
|
||||
| ShadowTLS | ✅ | - | - | - | - | - | - |
|
||||
| AnyTLS | ✅ | - | - | - | - | - | - |
|
||||
| Direct | ✅ | - | ✅ | - | - | - | - |
|
||||
|
||||
#### 出站协议 (Outbound)
|
||||
|
||||
| 协议 | sing-box | Clash | mihomo | Hiddify | FlClash | Stash | mini-sing |
|
||||
|------|----------|-------|--------|---------|---------|-------|-----------|
|
||||
| Shadowsocks | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| ShadowsocksR | - | ✅ | ✅ | - | ✅ | ✅ | - |
|
||||
| VMess | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| VLESS | ✅ | - | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Trojan | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Hysteria | ✅ | - | ✅ | ✅ | ✅ | ✅ | - |
|
||||
| Hysteria2 | ✅ | - | ✅ | ✅ | ✅ | ✅ | - |
|
||||
| TUIC | ✅ | - | ✅ | ✅ | ✅ | ✅ | - |
|
||||
| WireGuard | ✅ | - | ✅ | ✅ | ✅ | ✅ | - |
|
||||
| SSH | ✅ | - | ✅ | ✅ | - | ✅ | - |
|
||||
| Tor | ✅ | - | - | - | - | - | - |
|
||||
| Snell | - | ✅ | ✅ | - | ✅ | ✅ | - |
|
||||
| ShadowTLS | ✅ | - | ✅ | ✅ | ✅ | ✅ | - |
|
||||
| AnyTLS | ✅ | - | ✅ | - | - | - | - |
|
||||
| NaiveProxy | ✅ | - | - | ✅ | - | - | - |
|
||||
| SOCKS | ✅ | ✅ | ✅ | - | ✅ | ✅ | - |
|
||||
| HTTP | ✅ | ✅ | ✅ | - | ✅ | ✅ | - |
|
||||
| Direct | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Block/Reject | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| DNS | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | - |
|
||||
| Tailscale | - | - | - | - | - | ✅ | ✅ |
|
||||
| Selector | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| URLTest | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Fallback | - | ✅ | ✅ | - | ✅ | ✅ | - |
|
||||
| Load-Balance | - | ✅ | ✅ | - | ✅ | ✅ | - |
|
||||
| Relay/Chain | - | ✅ | ✅(via dialer-proxy) | - | ✅ | ✅ | - |
|
||||
|
||||
**用户最常需要的协议排序**: Shadowsocks > VLESS (Reality) > Trojan > VMess > Hysteria2 > WireGuard > TUIC
|
||||
|
||||
---
|
||||
|
||||
### B. 传输层 (Transport)
|
||||
|
||||
| 传输层 | sing-box | Clash | mihomo | Hiddify | FlClash | Stash | mini-sing |
|
||||
|--------|----------|-------|--------|---------|---------|-------|-----------|
|
||||
| TCP (原始) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| TLS | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| WebSocket | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | - |
|
||||
| gRPC | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | - |
|
||||
| HTTP/2 (H2) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | - |
|
||||
| HTTP Upgrade | ✅ | - | ✅ | ✅ | ✅ | ✅ | - |
|
||||
| QUIC | ✅ | - | ✅ | ✅ | ✅ | ✅ | - |
|
||||
| Reality | ✅ | - | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| uTLS 指纹 | ✅ | - | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Multiplex (smux/yamux/h2mux) | ✅ | - | ✅ | ✅ | ✅ | - | - |
|
||||
| TCP Brutal | ✅ | - | ✅ | ✅ | ✅ | - | - |
|
||||
| SplitHTTP/XHTTP | ✅ | - | ✅ | ✅ | - | - | - |
|
||||
| ECH | ✅ | - | ✅ | ✅ | - | - | - |
|
||||
|
||||
---
|
||||
|
||||
### C. 路由规则 (Routing)
|
||||
|
||||
| 规则类型 | sing-box | Clash | mihomo | Hiddify | FlClash | Stash | mini-sing |
|
||||
|----------|----------|-------|--------|---------|---------|-------|-----------|
|
||||
| domain | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| domain_suffix | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| domain_keyword | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| domain_regex | ✅ | - | ✅ | ✅ | ✅ | ✅ | - |
|
||||
| domain_wildcard | - | - | ✅ | - | ✅ | ✅ | - |
|
||||
| ip_cidr | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| source_ip_cidr | ✅ | ✅ | ✅ | - | ✅ | - | - |
|
||||
| ip_is_private | ✅ | - | - | - | - | - | ✅ |
|
||||
| GeoIP | ✅(已弃用) | ✅ | ✅ | ✅ | ✅ | ✅ | - |
|
||||
| GeoSite | ✅(已弃用) | - | ✅ | ✅ | ✅ | ✅ | - |
|
||||
| IP-ASN | ✅ | - | ✅ | - | ✅ | ✅ | - |
|
||||
| port / port_range | ✅ | ✅ | ✅ | - | ✅ | ✅ | ✅ |
|
||||
| source_port | ✅ | ✅ | ✅ | - | ✅ | - | - |
|
||||
| process_name | ✅ | ✅ | ✅ | - | ✅ | ✅ | - |
|
||||
| process_path | ✅ | ✅ | ✅ | - | ✅ | ✅ | - |
|
||||
| package_name (Android) | ✅ | - | ✅ | - | ✅ | - | - |
|
||||
| network (tcp/udp) | ✅ | - | ✅ | - | ✅ | ✅ | ✅ |
|
||||
| protocol (sniffed) | ✅ | - | - | - | - | ✅ | ✅ |
|
||||
| inbound | ✅ | - | ✅ | - | ✅ | - | ✅ |
|
||||
| rule-set (远程规则集) | ✅ | ✅(Premium) | ✅ | - | ✅ | ✅ | - |
|
||||
| AND/OR/NOT 逻辑组合 | ✅ | - | ✅ | - | ✅ | ✅ | - |
|
||||
| SCRIPT | - | ✅(Premium) | - | - | - | ✅ | - |
|
||||
| wifi_ssid / wifi_bssid | ✅ | - | - | - | - | - | - |
|
||||
| clash_mode | ✅ | ✅ | ✅ | - | ✅ | - | - |
|
||||
| user-agent | - | - | - | - | - | ✅ | - |
|
||||
| url-regex | - | - | - | - | - | ✅ | - |
|
||||
| SUB-RULE | - | - | ✅ | - | ✅ | - | - |
|
||||
| Protocol Sniff | ✅ | - | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Sniff Override Dest | ✅(已弃用) | - | ✅ | ✅ | ✅ | ✅ | ✅(内建) |
|
||||
|
||||
---
|
||||
|
||||
### D. DNS
|
||||
|
||||
| 功能 | sing-box | Clash | mihomo | Hiddify | FlClash | Stash | mini-sing |
|
||||
|------|----------|-------|--------|---------|---------|-------|-----------|
|
||||
| UDP DNS | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| TCP DNS | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | - |
|
||||
| DoT (DNS over TLS) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| DoH (DNS over HTTPS) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | - |
|
||||
| DoQ (DNS over QUIC) | ✅ | - | ✅ | ✅ | ✅ | ✅ | - |
|
||||
| DoH3 (HTTP/3) | ✅ | - | ✅ | ✅ | ✅ | - | - |
|
||||
| DHCP DNS | ✅ | - | - | - | - | - | - |
|
||||
| FakeIP | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | - |
|
||||
| DNS 规则/分流 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| DNS 劫持 | ✅ | - | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| DNS 缓存 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| ECS (Client Subnet) | ✅ | - | ✅ | - | ✅ | - | - |
|
||||
| Hosts 映射 | ✅ | ✅ | ✅ | - | ✅ | ✅ | - |
|
||||
| 反向缓存 (IP→域名) | ✅ | - | - | - | - | - | ✅ |
|
||||
| 域名策略 (prefer_ipv4等) | ✅ | - | ✅ | - | ✅ | - | ✅(prefer_ipv4) |
|
||||
| 污染过滤 | ✅ | ✅ | ✅ | - | ✅ | - | ✅ |
|
||||
| Prefetch | ✅ | - | - | - | - | - | ✅ |
|
||||
| Serve-Stale | ✅ | - | - | - | - | - | ✅ |
|
||||
| Tailscale DNS | ✅ | - | - | - | - | ✅ | ✅ |
|
||||
|
||||
---
|
||||
|
||||
### E. TUN/VPN
|
||||
|
||||
| 功能 | sing-box | Clash | mihomo | Hiddify | FlClash | Stash | mini-sing |
|
||||
|------|----------|-------|--------|---------|---------|-------|-----------|
|
||||
| Auto-Route | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Strict-Route | ✅ | - | ✅ | - | ✅ | - | ✅ |
|
||||
| gVisor Stack | ✅ | - | ✅ | ✅ | ✅ | - | ✅ |
|
||||
| System Stack | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Mixed Stack | ✅ | - | ✅ | ✅ | ✅ | - | - |
|
||||
| 分应用代理 (Android) | ✅ | ✅ | ✅ | ✅ | ✅ | - | ✅(via VpnService) |
|
||||
| Exclude Routes | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| MTU 设置 | ✅ | ✅ | ✅ | - | ✅ | - | ✅ |
|
||||
| File Descriptor 传递 | ✅ | - | - | - | - | ✅ | ✅ |
|
||||
| 网络变化监听 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅(非 Android) |
|
||||
| 健康检查+自动回滚 | - | - | - | - | - | - | ✅ |
|
||||
|
||||
---
|
||||
|
||||
### F. UI/UX 功能
|
||||
|
||||
| 功能 | sing-box | Clash | mihomo | Hiddify | FlClash | Stash | mini-sing |
|
||||
|------|----------|-------|--------|---------|---------|-------|-----------|
|
||||
| Clash API (RESTful) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 部分(/status, /tailscale) |
|
||||
| Dashboard (Yacd等) | ✅(兼容) | ✅ | ✅(MetaCubeXD) | 内建 | 内建 | 内建 | - |
|
||||
| 连接日志 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | - |
|
||||
| 流量统计 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | - |
|
||||
| 订阅/配置管理 | ✅(远程配置) | ✅ | ✅(Provider) | ✅ | ✅ | ✅ | - |
|
||||
| 配置格式 | JSON | YAML | YAML | sing-box JSON | YAML | YAML | JSON |
|
||||
| URL 导入 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | - |
|
||||
| QR 扫码导入 | ✅(客户端) | - | - | ✅ | - | - | - |
|
||||
| GUI 客户端 | ✅(官方) | ✅(CfW等) | ✅(多款) | ✅ | ✅ | ✅ | - (CLI only) |
|
||||
| SIGHUP 热重载 | ✅ | - | - | - | - | - | - |
|
||||
| 配置编辑器 | ✅(客户端) | - | - | ✅ | - | ✅ | - |
|
||||
| WebDAV 同步 | - | - | - | - | ✅ | ✅ | - |
|
||||
|
||||
---
|
||||
|
||||
### G. 高级功能
|
||||
|
||||
| 功能 | sing-box | Clash | mihomo | Hiddify | FlClash | Stash | mini-sing |
|
||||
|------|----------|-------|--------|---------|---------|-------|-----------|
|
||||
| GeoIP 数据库 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | - |
|
||||
| GeoSite 数据库 | ✅ | - | ✅ | ✅ | ✅ | ✅ | - |
|
||||
| Rule-Set (远程规则集) | ✅ | ✅(Premium) | ✅ | - | ✅ | ✅ | - |
|
||||
| Proxy Provider | - | ✅ | ✅ | ✅ | ✅ | ✅ | - |
|
||||
| Proxy Chain | ✅(detour) | ✅(relay) | ✅(dialer-proxy) | - | ✅ | ✅ | ✅(Trojan detour) |
|
||||
| MITM (中间人) | - | - | - | - | - | ✅ | - |
|
||||
| HTTP Rewrite | - | - | - | - | - | ✅ | - |
|
||||
| Script/JavaScript | - | ✅(Premium) | - | - | - | ✅ | - |
|
||||
| SSID Policy Group | - | - | - | - | - | ✅ | - |
|
||||
| On-Demand (按需连接) | - | - | - | - | - | ✅ | - |
|
||||
| 带宽统计 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | - |
|
||||
| V2Ray API (统计) | ✅ | - | - | - | - | - | - |
|
||||
|
||||
---
|
||||
|
||||
### H. 平台支持
|
||||
|
||||
| 平台 | sing-box | Clash | mihomo | Hiddify | FlClash | Stash | mini-sing |
|
||||
|------|----------|-------|--------|---------|---------|-------|-----------|
|
||||
| Android | ✅ | ✅ | ✅ | ✅ | ✅ | - | ✅ |
|
||||
| iOS | ✅ | - | - | ✅ | - | ✅ | ✅(c-archive) |
|
||||
| macOS | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Windows | ✅ | ✅ | ✅ | ✅ | ✅ | - | ✅ |
|
||||
| Linux | ✅ | ✅ | ✅ | ✅ | ✅ | - | ✅ |
|
||||
| tvOS | - | - | - | - | - | ✅ | - |
|
||||
| GUI | ✅(官方) | ✅(第三方) | ✅(第三方) | ✅ | ✅ | ✅ | - |
|
||||
| CLI | ✅ | ✅ | ✅ | - | - | - | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## 二、mini-sing 现状总结
|
||||
|
||||
### 已实现
|
||||
- **入站**: mixed (SOCKS5+HTTP), TUN (gVisor/system stack), Shadowsocks, Trojan
|
||||
- **出站**: Shadowsocks, Trojan, VMess, VLESS, Direct, Block, Selector, URLTest, Tailscale
|
||||
- **传输层**: TCP, TLS (标准 + uTLS + Reality)
|
||||
- **路由**: domain, domain_suffix, domain_keyword, ip_cidr, ip_is_private, port, network, protocol, inbound
|
||||
- **DNS**: UDP, DoT, 分流路由, 缓存, 反向缓存, 污染过滤, Prefetch, Serve-Stale, Tailscale DNS
|
||||
- **TUN**: auto-route, strict-route, gVisor/system stack, MTU, file descriptor, 健康检查自动回滚, 出站路由排除
|
||||
- **平台**: Android (JNI c-shared), iOS (c-archive), macOS, Linux, Windows
|
||||
- **API**: 基础 HTTP API (/status, /tailscale/peers)
|
||||
- **协议嗅探**: TLS SNI, HTTP Host
|
||||
|
||||
### 代码规模
|
||||
~6,500 行 Go 代码,极度精简。
|
||||
|
||||
---
|
||||
|
||||
## 三、优先级排序: mini-sing 下一步应该添加什么
|
||||
|
||||
### P0: 基础可用性 (缺少会导致用户抱怨)
|
||||
|
||||
| # | 功能 | 理由 |
|
||||
|---|------|------|
|
||||
| 1 | **Hysteria2 出站** | 当前最热门的翻墙协议之一,基于 QUIC 的高吞吐量协议。大量节点提供商已全面转向 Hysteria2。缺少此协议会导致用户无法连接大量服务器。 |
|
||||
| 2 | **WebSocket 传输层** | 最广泛使用的传输层协议,CDN 中转必备。VMess+WS、VLESS+WS、Trojan+WS 是最常见的服务器配置。当前 mini-sing 只支持纯 TCP+TLS,无法连接任何 WS 节点。 |
|
||||
| 3 | **订阅链接导入 (Subscription)** | 用户从机场获取的几乎都是订阅链接 (clash/sing-box/base64 格式)。没有订阅导入,用户必须手动编写 JSON 配置,这对 99% 的用户是不可接受的。 |
|
||||
| 4 | **完整 Clash API** | /connections, /proxies, /proxies/{name}, /proxies/{name}/delay, /traffic, /logs 等端点。这是所有 Dashboard 和客户端 UI 依赖的接口,没有就无法接入任何现有 Dashboard。 |
|
||||
| 5 | **GeoIP/GeoSite 数据库支持** | 中国用户的核心需求: CN 域名/IP 直连。当前只有硬编码的关键词列表,无法覆盖完整的 CN 域名和 IP 地址。rule-set 或内置 GeoIP/GeoSite 至少需要支持一种。 |
|
||||
| 6 | **DNS 出站 (dns outbound)** | 将 DNS 查询路由到 DNS resolver 而不是走代理。TUN 模式下的标准配置需要 `"protocol": "dns"` 规则指向 dns-out。当前缺失此出站类型。 |
|
||||
|
||||
### P1: 竞争力功能 (重要,影响用户是否选择 mini-sing)
|
||||
|
||||
| # | 功能 | 理由 |
|
||||
|---|------|------|
|
||||
| 7 | **gRPC 传输层** | 仅次于 WebSocket 的第二常用传输层,部分机场只提供 gRPC 配置。 |
|
||||
| 8 | **HTTP Upgrade 传输层** | sing-box 推荐的 WS 替代方案,性能更好,CDN 兼容性好。 |
|
||||
| 9 | **FakeIP** | 现代代理客户端的标配 DNS 解析方式,减少 DNS 泄漏和延迟。所有主流竞品都支持。 |
|
||||
| 10 | **Rule-Set (远程规则集)** | 用户不想维护几万行规则。支持从 URL 加载 GeoIP/GeoSite 规则集是现代客户端的标准功能。 |
|
||||
| 11 | **Fallback 代理组** | 当首选代理不可用时自动切换到备选。Selector 和 URLTest 之外的第三种必备组类型。 |
|
||||
| 12 | **TUIC 出站** | 基于 QUIC 的低延迟协议,虽然用户量不如 Hysteria2 但仍有不少机场提供。 |
|
||||
| 13 | **WireGuard 出站** | Cloudflare WARP 用户必备。很多用户用 WARP 作为落地出口。 |
|
||||
| 14 | **process_name / process_path 规则** | macOS/Windows/Linux 上按进程分流是非常实用的功能。例如: 浏览器走代理、终端直连。 |
|
||||
| 15 | **domain_regex 规则** | 正则匹配域名,处理复杂域名规则时必备。 |
|
||||
| 16 | **DoH (DNS over HTTPS)** | 比 DoT 更通用的加密 DNS 协议,几乎所有公共 DNS 都支持 DoH。 |
|
||||
| 17 | **连接日志和流量统计** | 通过 Clash API 暴露实时连接信息和流量数据,是诊断网络问题和监控使用的基础功能。 |
|
||||
|
||||
### P2: 差异化功能 (锦上添花,竞争优势)
|
||||
|
||||
| # | 功能 | 理由 |
|
||||
|---|------|------|
|
||||
| 18 | **Multiplex (sing-mux: smux/yamux/h2mux)** | 多路复用减少连接数,配合 TCP Brutal 可以暴力提速。进阶用户看重。 |
|
||||
| 19 | **ShadowTLS 出站** | 伪装为正常 TLS 握手的协议,配合 Shadowsocks 使用,反审查能力强。 |
|
||||
| 20 | **Load-Balance 代理组** | 负载均衡,分散流量到多个节点。多出口用户需要。 |
|
||||
| 21 | **Proxy Provider (远程代理列表)** | 从 URL 动态加载代理节点列表,机场订阅的标准方式。与订阅导入互补。 |
|
||||
| 22 | **AND/OR/NOT 逻辑规则** | 组合多个条件的高级规则,减少规则数量。 |
|
||||
| 23 | **ECS (EDNS Client Subnet)** | DNS 查询携带客户端子网信息,获得更精准的 CDN 解析结果。 |
|
||||
| 24 | **Hosts 映射** | 本地域名映射,调试和特殊需求。 |
|
||||
| 25 | **ECH (Encrypted Client Hello)** | TLS 的 SNI 加密,最新的隐私保护技术。 |
|
||||
| 26 | **source_port / source_ip_cidr 规则** | 按源地址/端口分流,局域网代理场景下有用。 |
|
||||
| 27 | **package_name 规则 (Android)** | Android 按包名分流,比全局代理更精细。 |
|
||||
| 28 | **SIGHUP 热重载** | 改配置不需要重启进程,运维友好。 |
|
||||
|
||||
### P3: 低优先级 / 小众需求
|
||||
|
||||
| # | 功能 | 理由 |
|
||||
|---|------|------|
|
||||
| 29 | **ShadowsocksR 出站** | 过时协议,新机场很少提供,但仍有少量存量用户。 |
|
||||
| 30 | **Snell 出站** | Surge 生态专属协议,用户群极小。 |
|
||||
| 31 | **SSH 出站** | 可以通过 SSH 隧道代理,小众但偶尔有用。 |
|
||||
| 32 | **Tor 出站** | 匿名需求,极小众。 |
|
||||
| 33 | **NaiveProxy** | 反审查协议,用户群较小。 |
|
||||
| 34 | **Redirect / TProxy 入站** | Linux 透明代理,服务器/路由器场景。 |
|
||||
| 35 | **MITM / HTTP Rewrite** | 中间人解析和 HTTP 重写,Stash 独有,需求面窄。 |
|
||||
| 36 | **Script/JavaScript 规则** | 脚本规则,灵活但维护成本高,用户少。 |
|
||||
| 37 | **SSID Policy / On-Demand** | iOS 独有功能,根据 WiFi SSID 切换策略。 |
|
||||
| 38 | **tvOS 支持** | Apple TV 用户,极小众。 |
|
||||
| 39 | **IP-ASN 规则** | 按 ASN 编号匹配,高级用户用。 |
|
||||
| 40 | **WebDAV 同步** | 多设备配置同步,FlClash/Stash 有但不是核心需求。 |
|
||||
|
||||
---
|
||||
|
||||
## 四、建议实施路径
|
||||
|
||||
### 第一阶段: 让用户能用起来 (P0)
|
||||
```
|
||||
订阅导入 → WebSocket 传输 → Clash API → GeoIP/GeoSite → Hysteria2 → DNS 出站
|
||||
```
|
||||
订阅导入是最关键的,因为没有它用户根本无法方便地配置代理。WebSocket 紧随其后,因为它是使用最广泛的传输层。
|
||||
|
||||
### 第二阶段: 追齐主流 (P1)
|
||||
```
|
||||
FakeIP → Rule-Set → gRPC/HTTPUpgrade → TUIC/WireGuard → 连接日志 → process_name → DoH → Fallback → domain_regex
|
||||
```
|
||||
|
||||
### 第三阶段: 建立差异化优势 (P2)
|
||||
```
|
||||
Multiplex → ShadowTLS → Load-Balance → Proxy Provider → 逻辑规则 → ECH
|
||||
```
|
||||
|
||||
### mini-sing 的独特优势 (已有,应持续强化)
|
||||
1. **极致精简** — 6,500 行 vs sing-box 的 ~100,000 行,启动更快,内存更少
|
||||
2. **Tailscale 原生集成** — 竞品中只有 Stash 有,且不如 mini-sing 深度
|
||||
3. **健康检查自动回滚** — 独创功能,TUN 模式下网络异常自动恢复
|
||||
4. **DNS 反向缓存** — IP 到域名的映射,提升路由精度
|
||||
5. **Prefetch + Serve-Stale** — DNS 缓存高级策略,减少延迟
|
||||
|
||||
---
|
||||
|
||||
## 五、数据来源
|
||||
|
||||
- [sing-box 官方文档](https://sing-box.sagernet.org/)
|
||||
- [mihomo 官方文档](https://wiki.metacubex.one/en/)
|
||||
- [Clash Wiki](https://en.clash.wiki/)
|
||||
- [Stash Wiki](https://stash.wiki/en)
|
||||
- [Hiddify GitHub](https://github.com/hiddify/hiddify-app)
|
||||
- [FlClash 官网](https://flclash.cc/en/)
|
||||
- mini-sing 源码 (`/Users/mira/sing/mini-sing/`)
|
||||
@@ -2,6 +2,7 @@ package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
@@ -38,6 +39,13 @@ type OutboundOptionsRegistry interface {
|
||||
CreateOptions(outboundType string) (any, bool)
|
||||
}
|
||||
|
||||
// ICMPPinger is optionally implemented by outbounds that can proxy ICMP echo.
|
||||
// Used by TUN inbound to forward pings through tunnels (e.g., Tailscale WireGuard).
|
||||
type ICMPPinger interface {
|
||||
// DialPing opens an ICMP connection ("ping4"/"ping6") through the tunnel.
|
||||
DialPing(ctx context.Context, network, address string) (net.Conn, error)
|
||||
}
|
||||
|
||||
type OutboundManager interface {
|
||||
Lifecycle
|
||||
Outbounds() []Outbound
|
||||
|
||||
@@ -22,6 +22,8 @@ type ConnectionRouterEx interface {
|
||||
ConnectionRouter
|
||||
RouteConnectionEx(ctx context.Context, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||
RoutePacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||
// FindOutbound returns the outbound that would handle this metadata without routing.
|
||||
FindOutbound(metadata *InboundContext) Outbound
|
||||
}
|
||||
|
||||
type Rule interface {
|
||||
|
||||
@@ -136,10 +136,12 @@ func (r *Resolver) Exchange(query []byte) ([]byte, error) {
|
||||
|
||||
domain := strings.TrimSuffix(msg.Question[0].Name, ".")
|
||||
|
||||
// Tailscale domains (.ts.net only; .local is mDNS reserved, not Tailscale-specific)
|
||||
// Tailscale MagicDNS — supports .ts.net FQDN and short hostnames (e.g. "worker").
|
||||
// LookupTailscale expands short names using search domains from the control plane.
|
||||
if r.tailscale != nil {
|
||||
lower := strings.ToLower(domain)
|
||||
if strings.HasSuffix(lower, ".ts.net") {
|
||||
// Try Tailscale lookup for .ts.net domains and short names (no dots = likely a hostname)
|
||||
if strings.HasSuffix(lower, ".ts.net") || !strings.Contains(domain, ".") {
|
||||
if addrs, ok := r.tailscale.LookupTailscale(domain); ok && len(addrs) > 0 {
|
||||
if r.logger != nil {
|
||||
r.logger.Debug("dns tailscale: ", domain, " -> ", addrs)
|
||||
|
||||
217
dns/resolver_test.go
Normal file
217
dns/resolver_test.go
Normal file
@@ -0,0 +1,217 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// mockTailscale implements TailscaleLookup for testing
|
||||
type mockTailscale struct {
|
||||
peers map[string][]netip.Addr
|
||||
}
|
||||
|
||||
func (m *mockTailscale) LookupTailscale(domain string) ([]netip.Addr, bool) {
|
||||
// Simulate real LookupTailscale behavior: try exact match, then expand with search domains
|
||||
searchDomains := []string{"tail879fb9.ts.net"}
|
||||
|
||||
candidates := []string{domain, domain + "."}
|
||||
for _, sd := range searchDomains {
|
||||
candidates = append(candidates, domain+"."+sd, domain+"."+sd+".")
|
||||
}
|
||||
|
||||
for _, c := range candidates {
|
||||
if addrs, ok := m.peers[c]; ok {
|
||||
return addrs, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func newMockTailscale() *mockTailscale {
|
||||
return &mockTailscale{
|
||||
peers: map[string][]netip.Addr{
|
||||
"worker.tail879fb9.ts.net.": {
|
||||
netip.MustParseAddr("100.96.174.121"),
|
||||
netip.MustParseAddr("fd7a:115c:a1e0::5034:ae79"),
|
||||
},
|
||||
"m1pro.tail879fb9.ts.net.": {
|
||||
netip.MustParseAddr("100.123.124.119"),
|
||||
},
|
||||
"station.tail879fb9.ts.net.": {
|
||||
netip.MustParseAddr("100.67.38.87"),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildQuery(name string, qtype uint16) []byte {
|
||||
msg := new(dns.Msg)
|
||||
msg.SetQuestion(dns.Fqdn(name), qtype)
|
||||
msg.RecursionDesired = true
|
||||
packed, _ := msg.Pack()
|
||||
return packed
|
||||
}
|
||||
|
||||
func parseResponse(t *testing.T, raw []byte) *dns.Msg {
|
||||
t.Helper()
|
||||
msg := new(dns.Msg)
|
||||
if err := msg.Unpack(raw); err != nil {
|
||||
t.Fatalf("failed to unpack response: %v", err)
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
func TestExchange_TailscaleFQDN(t *testing.T) {
|
||||
r := &Resolver{tailscale: newMockTailscale()}
|
||||
|
||||
resp, err := r.Exchange(buildQuery("worker.tail879fb9.ts.net", dns.TypeA))
|
||||
if err != nil {
|
||||
t.Fatalf("Exchange failed: %v", err)
|
||||
}
|
||||
|
||||
msg := parseResponse(t, resp)
|
||||
if len(msg.Answer) == 0 {
|
||||
t.Fatal("expected at least one answer")
|
||||
}
|
||||
|
||||
a, ok := msg.Answer[0].(*dns.A)
|
||||
if !ok {
|
||||
t.Fatalf("expected A record, got %T", msg.Answer[0])
|
||||
}
|
||||
if a.A.String() != "100.96.174.121" {
|
||||
t.Fatalf("expected 100.96.174.121, got %s", a.A.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestExchange_TailscaleShortName(t *testing.T) {
|
||||
r := &Resolver{tailscale: newMockTailscale()}
|
||||
|
||||
// "worker" (no dots) should be resolved via LookupTailscale with search domain expansion
|
||||
resp, err := r.Exchange(buildQuery("worker", dns.TypeA))
|
||||
if err != nil {
|
||||
t.Fatalf("Exchange failed: %v", err)
|
||||
}
|
||||
|
||||
msg := parseResponse(t, resp)
|
||||
if len(msg.Answer) == 0 {
|
||||
t.Fatal("short name 'worker' should resolve to tailscale peer")
|
||||
}
|
||||
|
||||
a, ok := msg.Answer[0].(*dns.A)
|
||||
if !ok {
|
||||
t.Fatalf("expected A record, got %T", msg.Answer[0])
|
||||
}
|
||||
if a.A.String() != "100.96.174.121" {
|
||||
t.Fatalf("expected 100.96.174.121, got %s", a.A.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestExchange_TailscaleShortName_Multiple(t *testing.T) {
|
||||
r := &Resolver{tailscale: newMockTailscale()}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
wantIP string
|
||||
}{
|
||||
{"worker", "100.96.174.121"},
|
||||
{"m1pro", "100.123.124.119"},
|
||||
{"station", "100.67.38.87"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
resp, err := r.Exchange(buildQuery(tt.name, dns.TypeA))
|
||||
if err != nil {
|
||||
t.Fatalf("Exchange(%q) failed: %v", tt.name, err)
|
||||
}
|
||||
msg := parseResponse(t, resp)
|
||||
if len(msg.Answer) == 0 {
|
||||
t.Fatalf("Exchange(%q): no answers", tt.name)
|
||||
}
|
||||
a := msg.Answer[0].(*dns.A)
|
||||
if a.A.String() != tt.wantIP {
|
||||
t.Fatalf("Exchange(%q) = %s, want %s", tt.name, a.A.String(), tt.wantIP)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExchange_TailscaleShortName_NotFound(t *testing.T) {
|
||||
r := &Resolver{tailscale: newMockTailscale()}
|
||||
|
||||
// "nonexistent" doesn't match any peer — should fall through
|
||||
// Since we have no upstream DNS server configured, this will error
|
||||
_, err := r.Exchange(buildQuery("nonexistent", dns.TypeA))
|
||||
if err == nil {
|
||||
t.Fatal("expected error for nonexistent short name with no upstream DNS")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExchange_RegularDomain_SkipsTailscale(t *testing.T) {
|
||||
r := &Resolver{tailscale: newMockTailscale()}
|
||||
|
||||
// "google.com" has dots — should NOT attempt tailscale lookup, falls to upstream
|
||||
_, err := r.Exchange(buildQuery("google.com", dns.TypeA))
|
||||
if err == nil {
|
||||
t.Fatal("expected error for regular domain with no upstream DNS")
|
||||
}
|
||||
// The error should be "resolver not initialized", not a tailscale error
|
||||
if err.Error() != "dns: resolver not initialized" {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExchange_TailscaleFQDN_WithTrailingDot(t *testing.T) {
|
||||
r := &Resolver{tailscale: newMockTailscale()}
|
||||
|
||||
// FQDN with trailing dot (as DNS wire format sends it)
|
||||
resp, err := r.Exchange(buildQuery("m1pro.tail879fb9.ts.net.", dns.TypeA))
|
||||
if err != nil {
|
||||
t.Fatalf("Exchange failed: %v", err)
|
||||
}
|
||||
|
||||
msg := parseResponse(t, resp)
|
||||
if len(msg.Answer) == 0 {
|
||||
t.Fatal("FQDN with trailing dot should resolve")
|
||||
}
|
||||
a := msg.Answer[0].(*dns.A)
|
||||
if a.A.String() != "100.123.124.119" {
|
||||
t.Fatalf("got %s, want 100.123.124.119", a.A.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestExchange_TailscaleAAAA(t *testing.T) {
|
||||
r := &Resolver{tailscale: newMockTailscale()}
|
||||
|
||||
resp, err := r.Exchange(buildQuery("worker.tail879fb9.ts.net", dns.TypeAAAA))
|
||||
if err != nil {
|
||||
t.Fatalf("Exchange failed: %v", err)
|
||||
}
|
||||
|
||||
msg := parseResponse(t, resp)
|
||||
// Should have both A and AAAA since buildDNSResponse returns all addrs
|
||||
hasAAAA := false
|
||||
for _, rr := range msg.Answer {
|
||||
if aaaa, ok := rr.(*dns.AAAA); ok {
|
||||
hasAAAA = true
|
||||
if aaaa.AAAA.String() != "fd7a:115c:a1e0::5034:ae79" {
|
||||
t.Fatalf("got %s, want fd7a:115c:a1e0::5034:ae79", aaaa.AAAA.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
if !hasAAAA {
|
||||
t.Fatal("expected AAAA record for worker")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExchange_NoTailscale(t *testing.T) {
|
||||
// No tailscale configured — short names should fall through
|
||||
r := &Resolver{}
|
||||
|
||||
_, err := r.Exchange(buildQuery("worker", dns.TypeA))
|
||||
if err == nil {
|
||||
t.Fatal("expected error with no tailscale and no upstream")
|
||||
}
|
||||
}
|
||||
@@ -198,6 +198,14 @@ func (t *Outbound) ServerAddr() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// DialPing implements adapter.ICMPPinger — opens an ICMP connection through WireGuard.
|
||||
func (t *Outbound) DialPing(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
if !t.started.Load() || t.tn == nil {
|
||||
return nil, fmt.Errorf("tailscale not started")
|
||||
}
|
||||
return t.tn.DialPing(ctx, network, address)
|
||||
}
|
||||
|
||||
// PeerInfo represents a tailnet device.
|
||||
type PeerInfo struct {
|
||||
Hostname string `json:"hostname"`
|
||||
|
||||
@@ -233,8 +233,29 @@ func (t *Inbound) Close() error {
|
||||
|
||||
// === tun.Handler interface ===
|
||||
|
||||
func (t *Inbound) PrepareConnection(network string, source M.Socksaddr, destination M.Socksaddr, _ tun.DirectRouteContext, _ time.Duration) (tun.DirectRouteDestination, error) {
|
||||
// Allow all connections through the stack (no direct routing bypass)
|
||||
func (t *Inbound) PrepareConnection(network string, source M.Socksaddr, destination M.Socksaddr, routeContext tun.DirectRouteContext, _ time.Duration) (tun.DirectRouteDestination, error) {
|
||||
// For ICMP, check if the destination routes to an outbound that supports ping.
|
||||
// Without this, gVisor replies to ICMP echo locally with fake <1ms latency.
|
||||
if network == N.NetworkICMP && destination.IsIP() && routeContext != nil {
|
||||
outboundManager, ok := adapter.OutboundManagerFromContext(t.ctx)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
// Find matching outbound via route rules
|
||||
metadata := adapter.InboundContext{
|
||||
Inbound: t.Tag(),
|
||||
InboundType: C.TypeTun,
|
||||
Source: source,
|
||||
Destination: destination,
|
||||
}
|
||||
if outbound := t.router.FindOutbound(&metadata); outbound != nil {
|
||||
_ = outboundManager // used for discovery
|
||||
if pinger, ok := outbound.(adapter.ICMPPinger); ok {
|
||||
t.logger.Debug("ICMP proxy: ", destination.Addr, " → ", outbound.Tag())
|
||||
return newICMPProxy(t.ctx, pinger, destination.Addr, routeContext, t.logger), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -303,6 +324,117 @@ func (t *Inbound) handleDNSPacket(ctx context.Context, conn N.PacketConn, source
|
||||
}
|
||||
}
|
||||
|
||||
// === ICMP proxy for tunneled outbounds ===
|
||||
|
||||
// icmpProxy forwards ICMP echo requests through an outbound that supports ping
|
||||
// (e.g., Tailscale WireGuard tunnel) and writes the real reply back to the TUN.
|
||||
type icmpProxy struct {
|
||||
ctx context.Context
|
||||
pinger adapter.ICMPPinger
|
||||
destination netip.Addr
|
||||
routeContext tun.DirectRouteContext
|
||||
logger logger.ContextLogger
|
||||
closed bool
|
||||
}
|
||||
|
||||
func newICMPProxy(ctx context.Context, pinger adapter.ICMPPinger, dest netip.Addr, rc tun.DirectRouteContext, l logger.ContextLogger) *icmpProxy {
|
||||
return &icmpProxy{ctx: ctx, pinger: pinger, destination: dest, routeContext: rc, logger: l}
|
||||
}
|
||||
|
||||
func (p *icmpProxy) WritePacket(packet *buf.Buffer) error {
|
||||
// packet is a raw IP+ICMP packet from the TUN. We extract the ICMP echo
|
||||
// request, send it through the tunnel, receive the real reply, rebuild a
|
||||
// raw IP+ICMP reply packet, and write it back to the TUN.
|
||||
data := make([]byte, packet.Len())
|
||||
copy(data, packet.Bytes())
|
||||
|
||||
if len(data) < 20 {
|
||||
return fmt.Errorf("icmp proxy: packet too short")
|
||||
}
|
||||
ipHdrLen := int(data[0]&0x0f) << 2
|
||||
if len(data) < ipHdrLen+8 {
|
||||
return fmt.Errorf("icmp proxy: ICMP payload too short")
|
||||
}
|
||||
|
||||
origSrcIP := make([]byte, 4)
|
||||
copy(origSrcIP, data[12:16]) // save original source for reply
|
||||
|
||||
icmpPayload := data[ipHdrLen:]
|
||||
|
||||
go func() {
|
||||
network := "ping4"
|
||||
if p.destination.Is6() {
|
||||
network = "ping6"
|
||||
}
|
||||
|
||||
conn, err := p.pinger.DialPing(p.ctx, network, p.destination.String())
|
||||
if err != nil {
|
||||
p.logger.Debug("icmp proxy dial: ", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
conn.SetDeadline(time.Now().Add(10 * time.Second))
|
||||
|
||||
if _, err := conn.Write(icmpPayload); err != nil {
|
||||
p.logger.Debug("icmp proxy write: ", err)
|
||||
return
|
||||
}
|
||||
|
||||
reply := make([]byte, 1500)
|
||||
n, err := conn.Read(reply)
|
||||
if err != nil {
|
||||
p.logger.Debug("icmp proxy read: ", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Rebuild IP+ICMP reply: reuse original IP header, swap src↔dst, replace payload
|
||||
replyPkt := make([]byte, ipHdrLen+n)
|
||||
copy(replyPkt, data[:ipHdrLen]) // copy original IP header
|
||||
copy(replyPkt[ipHdrLen:], reply[:n]) // ICMP reply payload
|
||||
|
||||
// Swap source and destination IP
|
||||
srcIP := p.destination.As4()
|
||||
copy(replyPkt[12:16], srcIP[:]) // src = peer (destination of original)
|
||||
copy(replyPkt[16:20], origSrcIP) // dst = original source (us)
|
||||
|
||||
// Update total length
|
||||
totalLen := uint16(ipHdrLen + n)
|
||||
replyPkt[2] = byte(totalLen >> 8)
|
||||
replyPkt[3] = byte(totalLen)
|
||||
|
||||
// Protocol = ICMP (should already be 1, but ensure)
|
||||
replyPkt[9] = 1
|
||||
|
||||
// Recalculate IP header checksum
|
||||
replyPkt[10] = 0
|
||||
replyPkt[11] = 0
|
||||
var csum uint32
|
||||
for i := 0; i < ipHdrLen; i += 2 {
|
||||
csum += uint32(replyPkt[i])<<8 | uint32(replyPkt[i+1])
|
||||
}
|
||||
for csum > 0xffff {
|
||||
csum = (csum & 0xffff) + (csum >> 16)
|
||||
}
|
||||
replyPkt[10] = byte(^csum >> 8)
|
||||
replyPkt[11] = byte(^csum)
|
||||
|
||||
if err := p.routeContext.WritePacket(replyPkt); err != nil {
|
||||
p.logger.Debug("icmp proxy writeback: ", err)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *icmpProxy) Close() error {
|
||||
p.closed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *icmpProxy) IsClosed() bool {
|
||||
return p.closed
|
||||
}
|
||||
|
||||
// === DNS hijack via iptables ===
|
||||
|
||||
func (t *Inbound) setupDNSHijack() {
|
||||
|
||||
@@ -115,6 +115,12 @@ func (r *Router) RoutePacketConnectionEx(ctx context.Context, conn N.PacketConn,
|
||||
r.connection.NewPacketConnection(ctx, selectedOutbound, conn, metadata, onClose)
|
||||
}
|
||||
|
||||
// FindOutbound returns the outbound that would handle this metadata, without
|
||||
// actually routing anything. Used by TUN inbound for ICMP proxy decisions.
|
||||
func (r *Router) FindOutbound(metadata *adapter.InboundContext) adapter.Outbound {
|
||||
return r.matchAndSelect(metadata)
|
||||
}
|
||||
|
||||
func (r *Router) matchAndSelect(metadata *adapter.InboundContext) adapter.Outbound {
|
||||
// DNS reverse lookup: if destination is IP and we have a cached domain, populate it
|
||||
if r.dnsCache != nil && metadata.Destination.Fqdn == "" && metadata.Destination.IsIP() {
|
||||
|
||||
Reference in New Issue
Block a user