diff --git a/README-CN.md b/README-CN.md index d5f9aa6..5053198 100644 --- a/README-CN.md +++ b/README-CN.md @@ -68,6 +68,7 @@ - ✅ 支持多级跳板机 (Jump Host),轻松穿透复杂网络 - ✅ 支持跨平台运行(Windows / Linux / macOS) - ✅ 支持作为系统 HTTP 代理(可选扩展) +- ✅ 支持 TUN 模式: 支持所有基于 TCP 协议的应用代理 - ✅ 自定义路由规则: 支持通过自定义的规则文件进行流量分流 - ✅ 命令行自动补全: 支持 Bash, Zsh, Fish, PowerShell @@ -157,18 +158,25 @@ gotun --socks5 :1080 user@example.com | 参数 | 简写 | 说明 | 默认值 | |------|------|------|--------| -| `--listen` | `-l` | 本地HTTP代理监听地址 | `:8080` | -| `--port` | `-p` | SSH服务器端口 | `22` | -| `--pass` | | SSH密码 (不安全, 建议使用交互式认证) | | +| `--http` | | 本地 HTTP 代理监听地址 (别名 `--listen`) | `:8080` | +| `--listen` | `-l` | [已废弃] 同 `--http` | `:8080` | +| `--socks5` | | SOCKS5 代理监听地址 | `:1080` | +| `--port` | `-p` | SSH 服务器端口 | `22` | +| `--pass` | | SSH 密码 (不安全, 建议使用交互式认证) | | | `--identity_file` | `-i` | 用于认证的私钥文件路径 | | | `--jump` | `-J` | 跳板机列表,用逗号分隔 (格式: user@host:port) | | -| `--target` | | 可选的目标网络覆盖 | | -| `--socks5` | | SOCKS5 代理监听地址 | `:1080` | +| `--http-upstream` | | 强制将所有 HTTP 请求转发到此上游 (格式: host:port) | | +| `--target` | | [已废弃] 同 `--http-upstream` | | | `--timeout` | | 连接超时时间 | `10s` | | `--verbose` | `-v` | 启用详细日志 | `false` | | `--log` | | 日志文件路径 | 输出到标准输出 | | `--sys-proxy` | | 自动设置/恢复系统代理 | `true` | | `--rules` | | 代理规则配置文件路径 | | +| `--tun` | | 启用 TUN 模式 (VPN 模式) | `false` | +| `--tun-global` | `-g` | 启用全局 TUN 模式 (转发所有流量) | `false` | +| `--tun-ip` | | TUN 设备 CIDR 地址 | `10.0.0.1/24` | +| `--tun-route` | | 添加静态路由到 TUN (CIDR格式, 可多次使用) | | +| `--tun-nat` | | NAT 映射规则 (格式: LocalCIDR:RemoteCIDR) | | ### 使用场景 @@ -316,30 +324,75 @@ gotun --rules ./rules.yaml user@your_ssh_server.com 现在,当您访问 `internal.company.com` 时,流量会直接发送;而访问 `google.com` 时,流量则会通过 SSH 隧道代理。 -### 故障排除 +### TUN 模式 (高级) + +gotun 可以在本地创建一个虚拟网卡,将所有(或指定)TCP 流量拦截并通过 SSH 隧道透明传输。这使得不支持代理设置的软件也能通过 SSH 隧道访问远程资源。 + +#### 为什么使用 TUN 模式? + +- **全应用代理**: 完美支持 **RDP 远程桌面**、**数据库连接** (MySQL/PostgreSQL)、**Redis** 等基于 TCP 的应用层协议。 +- **无需配置**: 启用全局模式后,所有 TCP 流量自动走代理,无需在软件中逐个配置代理。 +- **网络映射**: 可以将远程内网的整个网段映射到本地,解决本地与远程网段冲突的问题。 + +> **⚠️ 注意**: 当前版本的 TUN 模式仅支持 **TCP 协议**。不支持 UDP 流量和 ICMP 协议(因此无法使用 `ping` 命令测试连通性,请使用 `telnet` 或 `nc -vz` 测试 TCP 端口)。 + +#### 核心参数 + +| 参数 | 简写 | 说明 | +|------|------|------| +| `--tun` | | 显式启用 TUN 模式 (通常配合路由参数自动启用,可省略) | +| `--tun-global` | `-g` | **全局模式**:接管本机所有网络流量 (自动处理网关防止 SSH 断连) | +| `--tun-route` | | **指定网段代理**:仅将指定网段路由到 TUN (支持 CIDR,可多次使用) | +| `--tun-nat` | | **NAT 网段映射**:将本地网段映射到远程网段 (格式 `LocalCIDR:RemoteCIDR`) | +| `--tun-ip` | | 指定 TUN 设备的内部 IP (默认 `10.0.0.1/24`) | + +#### 使用示例 + +**1. 全局模式** + +将本机所有流量通过远程服务器转发。 -#### 连接问题 +> **⚠️ 警告**: 启动虚拟网卡可能会与 Clash、ZeroTier 等同样操作网卡或路由表的软件产生冲突。请谨慎使用全局 TUN 模式,建议优先使用指定网段代理模式。 ```bash -# 启用详细日志进行调试 -gotun -v user@example.com +# -g 自动启用 TUN 模式 +sudo gotun -g user@server.com +``` + +**2. 指定网段代理** + +仅将指定网段的流量放入隧道,其他流量直连。例如,只有访问 `10.0.0.0/24` 网段的流量才通过 SSH 隧道: -# 指定日志文件 -gotun -v --log ./gotun.log user@example.com +```bash +# 访问 10.0.0.x 的流量走 SSH,其他走本地 +sudo gotun --tun-route 10.0.0.0/24 user@server.com ``` -#### 权限问题 +**3. NAT 网段映射** -在某些系统上设置系统代理需要管理员权限: +解决网段冲突问题。例如:你需要访问的远程目标网段为 `192.168.0.0/24`,但你本地也有物理网卡或其他网络环境使用了 `192.168.0.0/24`。为了避免冲突,可以将远程的 `192.168.0.0/24` 映射到本地的一个无冲突网段(如 `10.0.0.0/24`)。 ```bash -# macOS/Linux -sudo gotun user@example.com +# 访问本地 10.0.0.1 -> 自动 NAT 到远程 192.168.0.1 +sudo gotun --tun-nat 10.0.0.0/24:192.168.0.0/24 user@server.com +``` + +> **注意**: +> - **权限**: TUN 模式需要 `sudo` (macOS/Linux) 或管理员权限 (Windows)。 +> - **Windows 用户**: 首次运行时会自动释放 `wintun.dll`,无需手动安装驱动。 -# Windows (以管理员身份运行 PowerShell/CMD) -.\gotun.exe user@example.com +**4. RDP 远程桌面连接示例** + +场景:你需要远程桌面连接到位于 `192.168.2.0/24` 网段的 Windows 机器(IP: `192.168.2.1`),但该网段无法直接访问。你有一台位于同一网段的 Linux 服务器(IP: `192.168.2.2`)开启了 SSH 服务。 + +```bash +# 将 192.168.2.0/24 网段的流量通过 SSH 隧道转发 +sudo gotun --tun-route 192.168.2.0/24 user@192.168.2.2 ``` +启动后,你就可以直接打开 Windows 远程桌面客户端,输入 `192.168.2.1` 进行连接,就像你在同一个局域网内一样。 + + --- @@ -357,7 +410,7 @@ sudo gotun user@example.com - [x] **自定义路由规则**: 支持自定义的规则文件进行流量分流 - [x] **命令行自动补全**: 基于 Cobra 的智能提示 - [x] **SOCKS5 代理支持**: 更广泛的协议支持 -- [ ] **RDP网关**:支持RDP远程桌面网关 +- [x] **TUN 模式**: L3 级 VPN 支持 (全局/规则/NAT) - [ ] **托盘 GUI 界面**: 图形化用户界面 - [ ] **配置文件导出/导入**: 配置管理功能 - [ ] **连接池优化**: 提升性能和稳定性 diff --git a/README.md b/README.md index 3ea7827..6077663 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ Your machine SSH (tcp/22) Bastion I - Supports single and multi-hop SSH jump hosts - Cross-platform: Windows, Linux, macOS - Can be used as a system HTTP proxy (optional) +- TUN Mode: Supports standard TCP-based application proxying - Rule-based traffic splitting via configuration file - Shell completion support for Bash, Zsh, Fish, PowerShell - Structured logging and verbose mode for debugging @@ -164,22 +165,22 @@ If system proxy support is enabled, some platforms can be configured automatical ## Command-line options -| Flag | Short | Description | Default | -|-------------------|-------|---------------------------------------------------|--------------| -| `--listen` | `-l` | Local HTTP proxy bind address | `:8080` | -| `--port` | `-p` | SSH server port | `22` | -| `--pass` | | SSH password (not recommended; use interactively) | | -| `--identity_file` | `-i` | Private key file path | | -| `--jump` | `-J` | Comma-separated jump hosts (`user@host:port`) | | -| `--target` | | Optional target network scope/coverage | | -| `--socks5` | | SOCKS5 proxy bind address | `:1080` | -| `--timeout` | | SSH connection timeout | `10s` | -| `--verbose` | `-v` | Enable verbose logging | `false` | -| `--log` | | Log file path | stdout | -| `--sys-proxy` | | Enable automatic system proxy configuration | `true` | -| `--rules` | | Path to routing rules configuration file | | - -Run `gotun --help` for the full list of options. +| Flag | Short | Description | Default | +|------|-------|-------------|---------| +| `--http` | | Local HTTP proxy listen address (alias for `--listen`) | `:8080` | +| `--listen` | `-l` | [Deprecated] Same as `--http` | `:8080` | +| `--socks5` | | SOCKS5 proxy listen address | `:1080` | +| `--port` | `-p` | SSH server port | `22` | +| `--pass` | | SSH password (insecure, interactive preferred) | | +| `--identity_file` | `-i` | Private key file path | | +| `--jump` | `-J` | Comma-separated jump hosts (`user@host:port`) | | +| `--http-upstream` | | Force forward all HTTP requests to this upstream (`host:port`) | | +| `--target` | | [Deprecated] Same as `--http-upstream` | | +| `--timeout` | | Connection timeout | `10s` | +| `--verbose` | `-v` | Enable verbose logging | `false` | +| `--log` | | Log file path | stdout | +| `--sys-proxy` | | Auto-configure system proxy | `true` | +| `--rules` | | Path to routing rules config file | | --- @@ -312,7 +313,7 @@ Platform notes: --- -## Rule-based routing (advanced) +## Rule-based routing (Advanced) `gotun` can read a Clash-style YAML rules file to decide which traffic is sent via the SSH proxy and which goes directly. @@ -353,7 +354,75 @@ gotun --rules ./rules.yaml user@your_ssh_server.com Requests will be matched from top to bottom; the first matching rule applies. --- +## TUN Mode (Advanced) +gotun creates a local virtual network interface that intercepts specific (or all) TCP traffic and transparently tunnels it via SSH. This allows applications that don't support proxy settings to access remote resources through the SSH tunnel. + +### Why use TUN Mode? + +- **Full Application Proxy**: Perfectly supports **RDP (Remote Desktop)**, **Database connections** (MySQL/PostgreSQL), **Redis**, and other TCP-based application protocols. +- **Zero Config**: In Global Mode, all TCP traffic is routed automatically without per-app configuration. +- **Network Mapping**: Map a remote internal subnet to your local machine, solving IP conflict issues between local and remote networks. + +> **⚠️ Note**: Current TUN Mode only supports **TCP protocol**. UDP traffic and ICMP (ping) are not supported (use `telnet` or `nc -vz` to test connectivity). + +### Core Parameters + +| Flag | Short | Description | +|------|-------|-------------| +| `--tun` | | Explicitly enable TUN mode (auto-enabled by other TUN flags, optional) | +| `--tun-global` | `-g` | **Global Mode**: Routes ALL network traffic (auto-handles gateway to prevent SSH drop) | +| `--tun-route` | | **Split Tunneling**: Route specific CIDRs to TUN (can be repeated) | +| `--tun-nat` | | **NAT Mapping**: Map local subnet to remote subnet (`LocalCIDR:RemoteCIDR`) | +| `--tun-ip` | | Internal IP for the TUN interface (default `10.0.0.1/24`) | + +### Usage Examples + +**1. Global Mode** + +Route all local traffic through the remote server. + +> **⚠️ Warning**: Global TUN mode might conflict with other software that modifies routing tables (e.g., Clash, ZeroTier). Use with caution or prefer Split Tunneling. + +```bash +# -g automatically enables TUN mode +sudo gotun -g user@server.com +``` + +**2. Split Tunneling** + +Route only specific subnets through the tunnel. For example, only traffic to `10.0.0.0/24` goes via SSH: + +```bash +# Traffic to 10.0.0.x goes via SSH, everything else is direct +sudo gotun --tun-route 10.0.0.0/24 user@server.com +``` + +**3. NAT Mapping** + +Solve subnet conflicts. For example, remote target is `192.168.0.0/24`, but your local network also uses this range. Map it to a conflict-free local range (e.g., `10.0.0.0/24`). + +```bash +# Access Local 10.0.0.1 -> Auto-NAT -> Remote 192.168.0.1 +sudo gotun --tun-nat 10.0.0.0/24:192.168.0.0/24 user@server.com +``` + +> **Note**: +> - **Privileges**: TUN mode requires `sudo` (macOS/Linux) or Admin (Windows). +> - **Windows**: `wintun.dll` is auto-extracted on first run; no manual driver installation needed. + +**4. RDP Remote Desktop Example** + +Scenario: You need to RDP into a Windows machine at `192.168.2.1` (behind the SSH server), but you can't reach that IP directly. The SSH server (`192.168.2.2`) can reach it. + +```bash +# Route traffic for 192.168.2.0/24 through the SSH tunnel +sudo gotun --tun-route 192.168.2.0/24 user@192.168.2.2 +``` + +Once started, open your Remote Desktop Client and connect to `192.168.2.1` directly. It will feel like you are on the same LAN. + +--- ## Troubleshooting ### Connection issues @@ -408,10 +477,10 @@ Implemented: - [x] Rule-based routing - [x] Shell completion for common shells - [x] SOCKS5 proxy support +- [x] TUN Mode: L3 VPN support (Global/Split/NAT) Planned: -- [ ] RDP gateway support - [ ] Tray/GUI frontend - [ ] Export/import of configuration profiles - [ ] Connection pooling and performance tuning diff --git a/cmd/gotun/cli/root.go b/cmd/gotun/cli/root.go index b63dc7e..d272075 100644 --- a/cmd/gotun/cli/root.go +++ b/cmd/gotun/cli/root.go @@ -2,8 +2,10 @@ package cli import ( "fmt" + "net" "os" "os/signal" + "runtime" "strings" "syscall" "time" @@ -13,12 +15,14 @@ import ( "github.com/Sesame2/gotun/internal/proxy" "github.com/Sesame2/gotun/internal/router" "github.com/Sesame2/gotun/internal/sysproxy" + "github.com/Sesame2/gotun/internal/tun" "github.com/spf13/cobra" ) var ( - Version = "dev" - cfg = config.NewConfig() + Version = "dev" + cfg = config.NewConfig() + aliasFlags []string ) // rootCmd 代表不带任何子命令时的基础命令 @@ -30,6 +34,28 @@ var rootCmd = &cobra.Command{ 它可以帮助您安全地访问内网资源或将远程主机作为网络出口。`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + // 自动开启 TUN 模式: 如果指定了 Global, Route 或 NAT + if cfg.TunGlobal || len(cfg.TunRoute) > 0 || len(aliasFlags) > 0 { + cfg.TunMode = true + } + + // 检查是否启用 TUN 模式且非 root 用户 (Windows 除外) + if cfg.TunMode && runtime.GOOS != "windows" && os.Geteuid() != 0 { + fmt.Println("TUN 模式需要 root 权限,尝试使用 sudo 重新启动...") + + exe, err := os.Executable() + if err != nil { + return fmt.Errorf("无法获取当前可执行文件路径: %w", err) + } + + // 构建 sudo 命令 + sudoArgs := []string{"sudo", exe} + sudoArgs = append(sudoArgs, os.Args[1:]...) + + // 执行 sudo + syscall.Exec("/usr/bin/sudo", sudoArgs, os.Environ()) + return nil + } // 从参数填充SSH用户和服务器 user, host, err := parseSSHTarget(args[0]) @@ -44,6 +70,55 @@ var rootCmd = &cobra.Command{ cfg.SSHServer = fmt.Sprintf("%s:%s", cfg.SSHServer, cfg.SSHPort) } + // 解析 alias 参数到 Config + for _, alias := range aliasFlags { + parts := strings.Split(alias, ":") + if len(parts) != 2 { + return fmt.Errorf("无效的别名格式: %s, 应为 Src:Dst (例如 10.0.0.1:192.168.1.1 或 10.0.0.0/24:192.168.1.0/24)", alias) + } + + // Helper: parse IP or CIDR to *net.IPNet + parseNet := func(s string) (*net.IPNet, error) { + // 尝试解析为 CIDR + _, ipNet, err := net.ParseCIDR(s) + if err == nil { + return ipNet, nil + } + // 尝试解析为单 IP + ip := net.ParseIP(s) + if ip == nil { + return nil, fmt.Errorf("无效的 IP 或网段: %s", s) + } + // 转换为 /32 (IPv4) + ip = ip.To4() + if ip == nil { + return nil, fmt.Errorf("不支持 IPv6: %s", s) + } + return &net.IPNet{IP: ip, Mask: net.CIDRMask(32, 32)}, nil + } + + srcNet, err := parseNet(parts[0]) + if err != nil { + return err + } + dstNet, err := parseNet(parts[1]) + if err != nil { + return err + } + + // 校验掩码大小是否一致 + srcSize, _ := srcNet.Mask.Size() + dstSize, _ := dstNet.Mask.Size() + if srcSize != dstSize { + return fmt.Errorf("源网段和目标网段掩码长度不一致: %s (%d) != %s (%d)", parts[0], srcSize, parts[1], dstSize) + } + + cfg.SubnetAliases = append(cfg.SubnetAliases, config.SubnetAlias{ + Src: srcNet, + Dst: dstNet, + }) + } + // 验证配置 if err := cfg.Validate(); err != nil { return fmt.Errorf("配置错误: %w", err) @@ -97,6 +172,15 @@ var rootCmd = &cobra.Command{ proxyMgr = sysproxy.NewManager(log, cfg.ListenAddr, cfg.SocksAddr) } + // 5. 初始化 TUN 模式 + var tunService *tun.TunService + if cfg.TunMode { + tunService, err = tun.NewTunService(cfg, log, sshClient) + if err != nil { + return fmt.Errorf("TUN服务初始化失败: %w", err) + } + } + sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) @@ -112,6 +196,15 @@ var rootCmd = &cobra.Command{ } }() + if tunService != nil { + go func() { + if err := tunService.Start(); err != nil { + log.Errorf("TUN服务启动失败: %v", err) + sigChan <- syscall.SIGTERM + } + }() + } + if socksProxy != nil { go func() { if err := socksProxy.Start(); err != nil { @@ -126,6 +219,9 @@ var rootCmd = &cobra.Command{ if cfg.SocksAddr != "" { fmt.Println("SOCKS5 Proxy:", "socks5://"+cfg.SocksAddr) } + if cfg.TunMode { + fmt.Printf("TUN Mode: Enabled (CIDR: %s)\n", cfg.TunCIDR) + } if len(cfg.JumpHosts) > 0 { fmt.Printf("跳板机链: %s -> %s\n", fmt.Sprintf("%v", cfg.JumpHosts), cfg.SSHServer) @@ -160,25 +256,46 @@ var rootCmd = &cobra.Command{ } } + if tunService != nil { + if err := tunService.Close(); err != nil { + log.Errorf("关闭TUN服务失败: %v", err) + } + } + return nil }, } // init函数在main函数之前执行,定义原来所有的flag func init() { - // 使用 PersistentFlags,这样未来如果添加子命令,它们也能继承这些flag - rootCmd.PersistentFlags().StringVarP(&cfg.ListenAddr, "listen", "l", ":8080", "本地HTTP代理监听地址") + // --- Group 1: SSH Connection --- rootCmd.PersistentFlags().StringVarP(&cfg.SSHPort, "port", "p", "22", "SSH服务器端口") rootCmd.PersistentFlags().StringVar(&cfg.SSHPassword, "pass", "", "SSH密码 (不安全, 建议使用交互式认证)") rootCmd.PersistentFlags().StringVarP(&cfg.SSHKeyFile, "identity_file", "i", "", "用于认证的私钥文件路径") rootCmd.PersistentFlags().StringSliceVarP(&cfg.JumpHosts, "jump", "J", []string{}, "跳板机列表,用逗号分隔 (格式: user@host:port)") - rootCmd.PersistentFlags().StringVar(&cfg.SSHTargetDial, "target", "", "可选的目标网络覆盖") rootCmd.PersistentFlags().DurationVar(&cfg.Timeout, "timeout", 10*time.Second, "连接超时时间") + + // --- Group 2: Proxy Services --- + rootCmd.PersistentFlags().StringVarP(&cfg.ListenAddr, "listen", "l", ":8080", "本地HTTP代理监听地址 [已废弃,推荐使用 --http]") + rootCmd.PersistentFlags().StringVar(&cfg.ListenAddr, "http", ":8080", "本地HTTP代理监听地址 (别名: --listen)") + rootCmd.PersistentFlags().StringVar(&cfg.SocksAddr, "socks5", "", "SOCKS5 代理监听地址 (例如 :1080)") + rootCmd.PersistentFlags().BoolVar(&cfg.SystemProxy, "sys-proxy", true, "自动设置/恢复系统代理") + rootCmd.PersistentFlags().StringVar(&cfg.HTTPUpstream, "http-upstream", "", "强制将所有HTTP请求转发到此上游 (格式: host:port)") + // 兼容旧参数 target (隐藏) + rootCmd.PersistentFlags().StringVar(&cfg.HTTPUpstream, "target", "", "DEPRECATED: use --http-upstream") + rootCmd.PersistentFlags().MarkHidden("target") + + // --- Group 3: TUN Mode --- + rootCmd.PersistentFlags().BoolVar(&cfg.TunMode, "tun", false, "启用 TUN 模式 (VPN 模式)") + rootCmd.PersistentFlags().BoolVarP(&cfg.TunGlobal, "tun-global", "g", false, "启用全局 TUN 模式 (转发所有流量)") + rootCmd.PersistentFlags().StringVar(&cfg.TunCIDR, "tun-ip", "10.0.0.1/24", "TUN 设备 CIDR 地址") + rootCmd.PersistentFlags().StringSliceVar(&cfg.TunRoute, "tun-route", []string{}, "添加静态路由到 TUN (CIDR格式, 可多次使用)") + rootCmd.PersistentFlags().StringSliceVar(&aliasFlags, "tun-nat", []string{}, "NAT 映射规则 (格式: SrcCIDR:DstCIDR)") + + // --- Group 4: General --- rootCmd.PersistentFlags().BoolVarP(&cfg.Verbose, "verbose", "v", false, "启用详细日志") rootCmd.PersistentFlags().StringVar(&cfg.LogFile, "log", "", "日志文件路径") - rootCmd.PersistentFlags().BoolVar(&cfg.SystemProxy, "sys-proxy", true, "自动设置/恢复系统代理") rootCmd.PersistentFlags().StringVar(&cfg.RuleFile, "rules", "", "代理规则配置文件路径") - rootCmd.PersistentFlags().StringVar(&cfg.SocksAddr, "socks5", ":1080", "SOCKS5 代理监听地址") } func Execute(version string) { diff --git a/go.mod b/go.mod index 279d48c..22fcc6a 100644 --- a/go.mod +++ b/go.mod @@ -6,11 +6,17 @@ require ( github.com/spf13/cobra v1.10.1 golang.org/x/crypto v0.42.0 golang.org/x/term v0.35.0 + golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb gopkg.in/yaml.v3 v3.0.1 + gvisor.dev/gvisor v0.0.0-20251220000015-517913d17844 ) require ( + github.com/google/btree v1.1.2 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/spf13/pflag v1.0.10 // indirect + golang.org/x/net v0.44.0 // indirect golang.org/x/sys v0.36.0 // indirect + golang.org/x/time v0.12.0 // indirect + golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect ) diff --git a/go.sum b/go.sum index 59fea3d..f8c0d7a 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,6 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= +github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -9,11 +11,21 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= +golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= +golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= +golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A= +golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gvisor.dev/gvisor v0.0.0-20251220000015-517913d17844 h1:7SkRScnij3eBOP12JnH9KnIfAcpPFMWy9KxNHjOXGTM= +gvisor.dev/gvisor v0.0.0-20251220000015-517913d17844/go.mod h1:W1ZgZ/Dh85TgSZWH67l2jKVpDE5bjIaut7rjwwOiHzQ= diff --git a/internal/assets/dll/wintun_amd64.dll b/internal/assets/dll/wintun_amd64.dll new file mode 100644 index 0000000..aee04e7 Binary files /dev/null and b/internal/assets/dll/wintun_amd64.dll differ diff --git a/internal/assets/dll/wintun_arm64.dll b/internal/assets/dll/wintun_arm64.dll new file mode 100644 index 0000000..dc4e4ae Binary files /dev/null and b/internal/assets/dll/wintun_arm64.dll differ diff --git a/internal/assets/wintun_stub.go b/internal/assets/wintun_stub.go new file mode 100644 index 0000000..bb49e9d --- /dev/null +++ b/internal/assets/wintun_stub.go @@ -0,0 +1,8 @@ +//go:build !windows + +package assets + +// SetupWintun 非 Windows 平台无需操作 +func SetupWintun() error { + return nil +} diff --git a/internal/assets/wintun_windows.go b/internal/assets/wintun_windows.go new file mode 100644 index 0000000..8bc200c --- /dev/null +++ b/internal/assets/wintun_windows.go @@ -0,0 +1,64 @@ +//go:build windows + +package assets + +import ( + "embed" + "fmt" + "os" + "path/filepath" + "runtime" + "sync" +) + +//go:embed dll/*.dll +var wintunFS embed.FS + +var setupOnce sync.Once + +// SetupWintun 提取并释放 wintun.dll 到当前目录 +func SetupWintun() error { + var err error + setupOnce.Do(func() { + // 1. 确定目标 DLL 文件名 (原始文件名) + var dllName string + switch runtime.GOARCH { + case "amd64": + dllName = "wintun_amd64.dll" + case "arm64": + dllName = "wintun_arm64.dll" + default: + // 不支持架构,直接返回 (让 wireguard-go 自行决定是否报错) + return + } + + // 2. 确定目标释放路径 (当前可执行文件目录) + exePath, e := os.Executable() + if e != nil { + err = fmt.Errorf("无法获取可执行文件路径: %v", e) + return + } + exeDir := filepath.Dir(exePath) + targetPath := filepath.Join(exeDir, "wintun.dll") // 统一重命名为 wintun.dll + + // 3. 检查文件是否已存在 (如果存在则跳过,避免覆盖) + if _, e := os.Stat(targetPath); e == nil { + return + } + + // 4. 从 embed FS 读取 + srcPath := "dll/" + dllName + data, e := wintunFS.ReadFile(srcPath) + if e != nil { + err = fmt.Errorf("读取嵌入资源失败 %s: %v", srcPath, e) + return + } + + // 5. 写入磁盘 + if e := os.WriteFile(targetPath, data, 0755); e != nil { + err = fmt.Errorf("释放 wintun.dll 失败: %v", e) + return + } + }) + return err +} diff --git a/internal/config/config.go b/internal/config/config.go index 248ea38..97d6b61 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -3,10 +3,17 @@ package config import ( "errors" "fmt" + "net" "strings" "time" ) +// SubnetAlias 定义网段映射规则 +type SubnetAlias struct { + Src *net.IPNet + Dst *net.IPNet +} + // Config 存储应用配置 type Config struct { ListenAddr string @@ -14,10 +21,15 @@ type Config struct { SSHUser string SSHPassword string SSHKeyFile string - SSHTargetDial string - SSHPort string // 添加SSH端口配置 - SocksAddr string // SOCKS5 监听地址 - JumpHosts []string // 跳板机列表 + HTTPUpstream string // 强制 HTTP 上游 (原 SSHTargetDial) + SSHPort string // 添加SSH端口配置 + SocksAddr string // SOCKS5 监听地址 + TunMode bool // 是否启用 TUN 模式 + TunCIDR string // TUN 设备 CIDR (e.g. 10.0.0.1/24) + TunRoute []string // 需要路由到 TUN 的网段 + TunGlobal bool // 是否开启全局模式 + SubnetAliases []SubnetAlias // 网段/IP映射规则 (NAT) + JumpHosts []string // 跳板机列表 Timeout time.Duration Verbose bool LogFile string @@ -39,6 +51,11 @@ func NewConfig() *Config { SystemProxy: true, RuleFile: "", SocksAddr: "", + TunMode: false, + TunCIDR: "10.0.0.1/24", + TunRoute: []string{}, + TunGlobal: false, + SubnetAliases: []SubnetAlias{}, } } diff --git a/internal/proxy/http.go b/internal/proxy/http.go index f7ee342..9b23ad5 100644 --- a/internal/proxy/http.go +++ b/internal/proxy/http.go @@ -86,9 +86,10 @@ func (p *HTTPOverSSH) handlePlainHTTP(w http.ResponseWriter, req *http.Request) p.logger.Infof("来自 %s 的请求: %s %s", req.RemoteAddr, req.Method, req.URL.String()) targetAddr := req.URL.Host - if p.cfg.SSHTargetDial != "" { - p.logger.Debugf("使用指定的目标地址覆盖: %s", p.cfg.SSHTargetDial) - targetAddr = p.cfg.SSHTargetDial + // 检查是否强制了上游转发 + if p.cfg.HTTPUpstream != "" { + targetAddr = p.cfg.HTTPUpstream + p.logger.Infof("Upstream forced to %s", targetAddr) } if !strings.Contains(targetAddr, ":") { diff --git a/internal/tun/tun.go b/internal/tun/tun.go new file mode 100644 index 0000000..c252234 --- /dev/null +++ b/internal/tun/tun.go @@ -0,0 +1,735 @@ +package tun + +import ( + "encoding/binary" + "fmt" + "io" + "net" + "os/exec" + "runtime" + "strings" + "sync" + + "github.com/Sesame2/gotun/internal/assets" + "github.com/Sesame2/gotun/internal/config" + "github.com/Sesame2/gotun/internal/logger" + "github.com/Sesame2/gotun/internal/proxy" + + "golang.zx2c4.com/wireguard/tun" + + "gvisor.dev/gvisor/pkg/buffer" + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet" + "gvisor.dev/gvisor/pkg/tcpip/header" + "gvisor.dev/gvisor/pkg/tcpip/link/channel" + "gvisor.dev/gvisor/pkg/tcpip/network/ipv4" + "gvisor.dev/gvisor/pkg/tcpip/stack" + "gvisor.dev/gvisor/pkg/tcpip/transport/tcp" + "gvisor.dev/gvisor/pkg/tcpip/transport/udp" + "gvisor.dev/gvisor/pkg/waiter" +) + +// TunService 管理 TUN 设备和用户态协议栈 +type TunService struct { + cfg *config.Config + logger *logger.Logger + ssh *proxy.SSHClient + dev tun.Device + stack *stack.Stack + endpoint *channel.Endpoint + tunIP string + tunMask string + peerIP string + routes []string + global bool + + ifIndex int // [新增] 用于存储 Wintun 网卡的接口索引 + + closeOnce sync.Once +} + +// NewTunService 创建 TUN 服务 +func NewTunService(cfg *config.Config, log *logger.Logger, sshClient *proxy.SSHClient) (*TunService, error) { + // 解析 CIDR + ip, ipNet, err := net.ParseCIDR(cfg.TunCIDR) + if err != nil { + return nil, fmt.Errorf("无效的 TUN CIDR: %s (%v)", cfg.TunCIDR, err) + } + tunIP := ip.To4() + if tunIP == nil { + return nil, fmt.Errorf("只支持 IPv4 TUN 地址: %s", cfg.TunCIDR) + } + + // 计算 Mask + mask := net.IP(ipNet.Mask).String() + + // 计算 Peer IP (简单起见,IP+1) + peerIP := make(net.IP, len(tunIP)) + copy(peerIP, tunIP) + peerIP[3]++ // +1 + + return &TunService{ + cfg: cfg, + logger: log, + ssh: sshClient, + tunIP: tunIP.String(), + tunMask: mask, // 内部仍使用 mask 字符串 + peerIP: peerIP.String(), + routes: cfg.TunRoute, + global: cfg.TunGlobal, + }, nil +} + +// Start 启动 TUN 设备和协议栈 +func (t *TunService) Start() error { + // 0. (Windows Only) 释放 Wintun DLL + if err := assets.SetupWintun(); err != nil { + return fmt.Errorf("准备 Wintun 驱动失败: %v", err) + } + + // 1. 创建 TUN 设备 (使用 wireguard-go) + // 在 Windows 上,这将使用 Wintun (L3) + // 在 macOS 上,必须使用 utun[0-9]* 格式,通常传 "utun" 会自动分配 + devName := "gotun" + if runtime.GOOS == "darwin" { + devName = "utun" + } + + dev, err := tun.CreateTUN(devName, 1500) + if err != nil { + return fmt.Errorf("创建 TUN 设备失败: %v", err) + } + t.dev = dev + + realName, err := dev.Name() + if err == nil { + t.logger.Infof("[TUN] 设备已创建: %s", realName) + } else { + realName = "gotun" + } + + // 获取网卡索引 (Windows 特有) + if runtime.GOOS == "windows" { + iface, err := net.InterfaceByName(realName) + if err == nil { + t.ifIndex = iface.Index + } else { + // 如果 CreateTUN 返回的名字和系统里的不一致,尝试模糊匹配 + t.logger.Warnf("按名称 %s 查找接口失败,尝试遍历查找...", realName) + ifaces, _ := net.Interfaces() + for _, i := range ifaces { + // Wintun 驱动显示的适配器描述通常包含 WireGuard 或 Tun + // 但 InterfaceByName 通常匹配的是 Connection Name (如 'gotun') + if i.Name == realName { + t.ifIndex = i.Index + break + } + } + } + + if t.ifIndex > 0 { + t.logger.Infof("[TUN] 获取到网卡索引 (IF): %d", t.ifIndex) + } else { + t.logger.Warn("[TUN] 警告: 未能获取网卡索引,路由配置可能会失败") + } + } + + // 2. 配置 TUN 网卡 IP (需调用系统命令) + if err := t.setupTunIP(realName); err != nil { + dev.Close() + return fmt.Errorf("配置 TUN IP 失败: %v", err) + } + + // 检测路由冲突 + t.checkRouteConflicts() + + // 2.5 配置路由 + if t.global { + if err := t.setupGlobalRoutes(realName); err != nil { + t.logger.Warnf("[TUN] 配置全局路由失败: %v", err) + } + } else if len(t.routes) > 0 { + if err := t.setupRoutes(realName); err != nil { + t.logger.Warnf("[TUN] 配置路由部分失败: %v", err) + } + } + + // 配置别名路由 (Subnet/IP Mapping) + for _, sas := range t.cfg.SubnetAliases { + cidr := sas.Src.String() + t.logger.Infof("[TUN] 添加别名路由: %s -> TUN", cidr) + if err := t.addRoute(cidr, t.tunIP, realName); err != nil { + t.logger.Warnf("[TUN] 添加别名路由失败 %s: %v", cidr, err) + } + } + + // 3. 初始化 gVisor 用户态协议栈 + t.initNetstack() + + // 4. 启动数据泵 + go t.pumpTunToStack() + go t.pumpStackToTun() + + t.logger.Infof("[TUN] 模式启动成功! IP: %s Peer: %s", t.tunIP, t.peerIP) + + return nil +} + +// Close 关闭服务 +func (t *TunService) Close() error { + t.closeOnce.Do(func() { + if t.dev != nil { + t.dev.Close() + } + if t.stack != nil { + t.stack.Close() + } + }) + return nil +} + +// initNetstack 初始化 gVisor 协议栈 +func (t *TunService) initNetstack() { + s := stack.New(stack.Options{ + NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol}, + TransportProtocols: []stack.TransportProtocolFactory{tcp.NewProtocol, udp.NewProtocol}, + }) + + e := channel.New(256, 1500, "") + t.endpoint = e + + if err := s.CreateNIC(1, e); err != nil { + t.logger.Fatalf("[TUN] 创建 NIC 失败: %v", err) + } + + parsedIP := net.ParseIP(t.tunIP) + addr := tcpip.AddrFromSlice(parsedIP.To4()) + protocolAddr := tcpip.ProtocolAddress{ + Protocol: ipv4.ProtocolNumber, + AddressWithPrefix: tcpip.AddressWithPrefix{ + Address: addr, + PrefixLen: 24, // 对应 255.255.255.0 + }, + } + if err := s.AddProtocolAddress(1, protocolAddr, stack.AddressProperties{}); err != nil { + t.logger.Fatalf("[TUN] 添加协议地址失败: %v", err) + } + + if err := s.SetPromiscuousMode(1, true); err != nil { + t.logger.Fatalf("设置混杂模式失败: %v", err) + } + if err := s.SetSpoofing(1, true); err != nil { + t.logger.Fatalf("设置 Spoofing 失败: %v", err) + } + + s.SetRouteTable([]tcpip.Route{ + { + Destination: header.IPv4EmptySubnet, + NIC: 1, + }, + }) + + // TCP Handler + tcpHandler := tcp.NewForwarder(s, 0, 10, func(r *tcp.ForwarderRequest) { + id := r.ID() + destIP := id.LocalAddress.String() + destPort := id.LocalPort + + // --- 地址重写逻辑 (NAT) --- + targetHost := destIP + parsedDestIP := net.ParseIP(destIP) + + if parsedDestIP != nil { + parsedDestIP = parsedDestIP.To4() // Ensure IPv4 + if parsedDestIP != nil { + for _, rule := range t.cfg.SubnetAliases { + if rule.Src.Contains(parsedDestIP) { + // 计算偏移量: destIP - rule.Src.IP + offset := ipSub(parsedDestIP, rule.Src.IP) + // 计算新目标: rule.Dst.IP + offset + realTargetIP := ipAdd(rule.Dst.IP, offset) + + targetHost = realTargetIP.String() + t.logger.Infof("[TUN] 命中 NAT 规则: %s -> %s (Offset: %d)", destIP, targetHost, offset) + break + } + } + } + } + + targetAddr := fmt.Sprintf("%s:%d", targetHost, destPort) + // ------------------------ + + t.logger.Infof("[TUN] 收到 TCP 连接请求 -> %s (原始目标: %s:%d)", targetAddr, destIP, destPort) + var wq waiter.Queue + ep, err := r.CreateEndpoint(&wq) + if err != nil { + t.logger.Errorf("创建 TCP Endpoint 失败: %v", err) + r.Complete(true) + return + } + r.Complete(false) + localConn := gonet.NewTCPConn(&wq, ep) + go t.handleTCPForward(localConn, targetAddr) + }) + s.SetTransportProtocolHandler(tcp.ProtocolNumber, tcpHandler.HandlePacket) + + // UDP Handler (DNS) + udpHandler := udp.NewForwarder(s, func(r *udp.ForwarderRequest) bool { + id := r.ID() + if id.LocalPort != 53 { + return false + } + + var wq waiter.Queue + ep, err := r.CreateEndpoint(&wq) + if err != nil { + t.logger.Errorf("[TUN] 创建 UDP Endpoint 失败: %v", err) + return true + } + + localConn := gonet.NewUDPConn(&wq, ep) + go t.handleUDPForward(localConn, id.LocalAddress.String(), id.LocalPort) + return true + }) + s.SetTransportProtocolHandler(udp.ProtocolNumber, udpHandler.HandlePacket) + + t.stack = s +} + +// handleUDPForward (DNS) +func (t *TunService) handleUDPForward(conn *gonet.UDPConn, targetIP string, targetPort uint16) { + defer conn.Close() + buf := make([]byte, 2048) + n, _, err := conn.ReadFrom(buf) + if err != nil { + return + } + dnsQuery := buf[:n] + + tcpQuery := make([]byte, 2+len(dnsQuery)) + binary.BigEndian.PutUint16(tcpQuery[0:2], uint16(len(dnsQuery))) + copy(tcpQuery[2:], dnsQuery) + + targetAddr := fmt.Sprintf("%s:%d", targetIP, targetPort) + remoteConn, err := t.ssh.Dial("tcp", targetAddr) + if err != nil { + t.logger.Warnf("[TUN] 连接远程 DNS 失败 %s: %v", targetAddr, err) + return + } + defer remoteConn.Close() + + if _, err := remoteConn.Write(tcpQuery); err != nil { + return + } + lenBuf := make([]byte, 2) + if _, err := io.ReadFull(remoteConn, lenBuf); err != nil { + return + } + respLen := binary.BigEndian.Uint16(lenBuf) + respBuf := make([]byte, respLen) + if _, err := io.ReadFull(remoteConn, respBuf); err != nil { + return + } + conn.Write(respBuf) +} + +// handleTCPForward (Traffic) +func (t *TunService) handleTCPForward(localConn net.Conn, targetAddr string) { + defer localConn.Close() + remoteConn, err := t.ssh.Dial("tcp", targetAddr) + if err != nil { + t.logger.Warnf("[TUN] 连接目标失败 %s: %v", targetAddr, err) + return + } + defer remoteConn.Close() + t.logger.Infof("[TUN] 隧道建立: %s <-> %s", localConn.RemoteAddr(), targetAddr) + var wg sync.WaitGroup + wg.Add(2) + go func() { + defer wg.Done() + io.Copy(remoteConn, localConn) + if c, ok := remoteConn.(interface{ CloseWrite() error }); ok { + c.CloseWrite() + } + }() + go func() { + defer wg.Done() + io.Copy(localConn, remoteConn) + if c, ok := localConn.(*net.TCPConn); ok { + c.CloseWrite() + } + }() + wg.Wait() +} + +// pumpTunToStack 将 TUN 设备读取的数据写入 gVisor Stack +func (t *TunService) pumpTunToStack() { + // WireGuard tun Read 使用 Batch API + const batchSize = 1 + bufs := make([][]byte, batchSize) + for i := 0; i < batchSize; i++ { + bufs[i] = make([]byte, 1600) + } + sizes := make([]int, batchSize) + + // offset 在 Windows (Wintun) 上通常是 0 + // 在 Unix (macOS/Linux) 上,WireGuard 实现通常需要 4 字节 offset 用于处理 PI Header + offset := 0 + if runtime.GOOS == "darwin" || runtime.GOOS == "linux" { + offset = 4 + } + + for { + n, err := t.dev.Read(bufs, sizes, offset) + if err != nil { + if strings.Contains(err.Error(), "file already closed") || strings.Contains(err.Error(), "closed network connection") { + return + } + t.logger.Errorf("[TUN] 读取设备失败: %v", err) + return + } + + for i := 0; i < n; i++ { + size := sizes[i] + data := bufs[i][offset : offset+size] + + packetBuf := stack.NewPacketBuffer(stack.PacketBufferOptions{ + Payload: buffer.MakeWithData(data), + }) + t.endpoint.InjectInbound(header.IPv4ProtocolNumber, packetBuf) + } + } +} + +// pumpStackToTun 将 gVisor Stack 的输出写入 TUN 设备 +func (t *TunService) pumpStackToTun() { + offset := 0 + if runtime.GOOS == "darwin" || runtime.GOOS == "linux" { + offset = 4 + } + + for { + pkt := t.endpoint.Read() + if pkt == nil { + continue + } + views := pkt.ToView().ToSlice() + pkt.DecRef() + + // WireGuard Write 也是 batch 接口 + // 我们需要为 offset 预留空间 + buf := make([]byte, offset+len(views)) + copy(buf[offset:], views) + + _, err := t.dev.Write([][]byte{buf}, offset) + if err != nil { + if strings.Contains(err.Error(), "file already closed") || strings.Contains(err.Error(), "closed network connection") { + return + } + t.logger.Errorf("[TUN] 写入设备失败: %v", err) + return + } + } +} + +// setupTunIP 配置网卡 IP +func (t *TunService) setupTunIP(devName string) error { + t.logger.Infof("[TUN] 正在配置 %s IP: %s (Peer: %s)", devName, t.tunIP, t.peerIP) + + var cmd *exec.Cmd + switch runtime.GOOS { + case "darwin": + cmd = exec.Command("ifconfig", devName, t.tunIP, t.peerIP, "up") + case "linux": + err := exec.Command("ip", "addr", "add", fmt.Sprintf("%s/24", t.tunIP), "dev", devName).Run() + if err != nil { + return err + } + cmd = exec.Command("ip", "link", "set", devName, "up") + case "windows": + // Windows Wintun 配置 + cmd = exec.Command("netsh", "interface", "ip", "set", "address", + fmt.Sprintf("name=%s", devName), + "source=static", + fmt.Sprintf("addr=%s", t.tunIP), + fmt.Sprintf("mask=%s", t.tunMask), + ) + default: + return fmt.Errorf("不支持的操作系统") + } + + if cmd != nil { + output, err := cmd.CombinedOutput() + if err != nil { + outputStr := string(output) + // Windows 下如果 IP 已存在,netsh 可能报错 "对象已存在" 或 "Object already exists" + if runtime.GOOS == "windows" { + if strings.Contains(outputStr, "Object already exists") || strings.Contains(outputStr, "对象已存在") { + t.logger.Warnf("[TUN] Windows IP 配置提示: %s (视为成功)", strings.TrimSpace(outputStr)) + return nil + } + + // 双重检查:尝试检查是否实际上已经配置成功 + checkCmd := exec.Command("netsh", "interface", "ip", "show", "address", fmt.Sprintf("name=%s", devName)) + checkOut, checkErr := checkCmd.CombinedOutput() + if checkErr == nil && strings.Contains(string(checkOut), t.tunIP) { + t.logger.Warnf("[TUN] 配置 IP 命令返回错误,但检测到 IP 已存在,忽略错误: %v", err) + return nil + } + } + return fmt.Errorf("执行命令失败: %s, %v", outputStr, err) + } + } + return nil +} + +// setupRoutes 配置路由 +func (t *TunService) setupRoutes(devName string) error { + t.logger.Infof("[TUN] 正在配置路由: %v", t.routes) + for _, cidr := range t.routes { + if err := t.addRoute(cidr, t.tunIP, devName); err != nil { + t.logger.Errorf("[TUN] 添加路由失败 %s: %v", cidr, err) + } + } + return nil +} + +// setupGlobalRoutes 配置全局路由 +func (t *TunService) setupGlobalRoutes(devName string) error { + t.logger.Info("[TUN] 正在配置全局路由...") + gateway, err := t.getDefaultGateway() + if err != nil { + return fmt.Errorf("无法获取默认网关: %v", err) + } + t.logger.Infof("[TUN] 检测到默认网关: %s", gateway) + + sshHost := t.cfg.SSHServer + if host, _, err := net.SplitHostPort(sshHost); err == nil { + sshHost = host + } + + sshIPs, err := net.LookupIP(sshHost) + if err != nil { + return fmt.Errorf("无法解析 SSH 服务器 IP: %v", err) + } + if len(sshIPs) == 0 { + return fmt.Errorf("SSH 服务器 IP 解析为空") + } + targetSSH_IP := sshIPs[0].String() + t.logger.Infof("[TUN] 为 SSH 服务器 %s (%s) 添加绕过路由 via %s", sshHost, targetSSH_IP, gateway) + + if err := t.addRoute(targetSSH_IP, gateway, ""); err != nil { + return fmt.Errorf("添加 SSH 绕过路由失败: %v", err) + } + + t.logger.Info("[TUN] 添加全局覆盖路由 (0.0.0.0/1, 128.0.0.0/1)...") + if err := t.addRoute("0.0.0.0/1", t.tunIP, devName); err != nil { + return fmt.Errorf("添加 0.0.0.0/1 路由失败: %v", err) + } + if err := t.addRoute("128.0.0.0/1", t.tunIP, devName); err != nil { + return fmt.Errorf("添加 128.0.0.0/1 路由失败: %v", err) + } + return nil +} + +// addRoute 添加路由 +func (t *TunService) addRoute(target, gateway, devName string) error { + var cmd *exec.Cmd + + // Windows 解析 CIDR + var destIP, mask string + if runtime.GOOS == "windows" { + ip, network, err := net.ParseCIDR(target) + if err == nil { + destIP = ip.String() + mask = net.IP(network.Mask).String() + } else { + destIP = target + mask = "255.255.255.255" + } + } + + switch runtime.GOOS { + case "darwin": + if (gateway == t.tunIP || gateway == t.peerIP) && devName != "" { + cmd = exec.Command("route", "add", target, "-interface", devName) + } else { + cmd = exec.Command("route", "add", target, gateway) + } + case "linux": + args := []string{"route", "add", target, "via", gateway} + cmd = exec.Command("ip", args...) + case "windows": + // Windows: Wintun 是 L3 + // 1. 先删 (忽略错误) + exec.Command("route", "delete", destIP).Run() + + // 2. 准备添加命令 + isTunRoute := false + routeGw := gateway + + // 如果网关是 "0.0.0.0" 或 tunIP 或 peerIP,说明是要进 TUN + if gateway == "0.0.0.0" || gateway == t.tunIP || gateway == t.peerIP { + isTunRoute = true + routeGw = "0.0.0.0" // Wintun 标准网关 + } + + // 强制 METRIC 1 以提高优先级 + args := []string{"add", destIP, "mask", mask, routeGw, "METRIC", "1"} + + // 【关键修复】如果是 TUN 路由,必须指定 IF 索引 + if isTunRoute && t.ifIndex > 0 { + args = append(args, "IF", fmt.Sprintf("%d", t.ifIndex)) + } + + cmd = exec.Command("route", args...) + default: + return fmt.Errorf("不支持的操作系统") + } + + t.logger.Infof("[TUN] 执行路由命令: %s", cmd.String()) + if output, err := cmd.CombinedOutput(); err != nil { + outStr := string(output) + // 处理 "路由添加失败: 对象已存在" + if strings.Contains(outStr, "File exists") || strings.Contains(outStr, "exist") || strings.Contains(outStr, "已存在") { + t.logger.Warnf("[TUN] 路由已存在,忽略错误: %s", outStr) + return nil + } + return fmt.Errorf("cmd: %s, output: %s, err: %v", cmd.String(), outStr, err) + } + return nil +} + +// getDefaultGateway (同上) +func (t *TunService) getDefaultGateway() (string, error) { + switch runtime.GOOS { + case "darwin": + out, err := exec.Command("route", "-n", "get", "default").Output() + if err != nil { + return "", err + } + lines := strings.Split(string(out), "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "gateway:") { + parts := strings.Fields(line) + if len(parts) >= 2 { + return parts[1], nil + } + } + } + case "linux": + out, err := exec.Command("ip", "route", "show", "default").Output() + if err != nil { + return "", err + } + parts := strings.Fields(string(out)) + if len(parts) >= 3 && parts[0] == "default" && parts[1] == "via" { + return parts[2], nil + } + case "windows": + out, err := exec.Command("route", "print", "0.0.0.0").Output() + if err != nil { + return "", err + } + lines := strings.Split(string(out), "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "0.0.0.0") { + fields := strings.Fields(line) + if len(fields) >= 3 { + return fields[2], nil + } + } + } + } + return "", fmt.Errorf("未找到默认网关") +} + +// checkRouteConflicts 检查请求的路由是否与本机物理网卡冲突 +func (t *TunService) checkRouteConflicts() { + ifaces, err := net.Interfaces() + if err != nil { + t.logger.Warnf("[TUN] 无法获取本机网卡信息,跳过冲突检测: %v", err) + return + } + + sshHost := t.cfg.SSHServer + if host, _, err := net.SplitHostPort(sshHost); err == nil { + sshHost = host + } + sshIPs, _ := net.LookupIP(sshHost) + + // check conflict with t.routes & SubnetAliases + checkConflict := func(targetCIDR string, targetName string) { + _, network, err := net.ParseCIDR(targetCIDR) + if err != nil { + return + } + + // 1. 检查 SSH Server 死循环 + for _, sshIP := range sshIPs { + sshIPV4 := sshIP.To4() + if sshIPV4 != nil && network.Contains(sshIPV4) { + t.logger.Fatalf("[TUN] ❌ 致命错误: SSH 服务器 IP %s 包含在路由网段 %s 中!这将导致死循环 (SSH 流量被 TUN 拦截)。请调整路由或别名设置。", sshIPV4, targetCIDR) + } + } + + // 2. 检查本机网卡冲突 + for _, iface := range ifaces { + // 跳过 Loopback 和 Down 的接口 + if iface.Flags&net.FlagLoopback != 0 || iface.Flags&net.FlagUp == 0 { + continue + } + addrs, _ := iface.Addrs() + for _, addr := range addrs { + var ip net.IP + switch v := addr.(type) { + case *net.IPNet: + ip = v.IP + case *net.IPAddr: + ip = v.IP + } + ip = ip.To4() + if ip == nil || ip.IsLoopback() { + continue + } + + if network.Contains(ip) { + t.logger.Warnf("[TUN] ⚠️ 路由冲突警告: 请求的路由 %s 包含了本机网卡 %s 的 IP %s。这可能导致流量优先走物理网卡而跳过 TUN,导致代理不生效!", targetCIDR, iface.Name, ip.String()) + } + } + } + } + + for _, route := range t.routes { + checkConflict(route, "User Route") + } + for _, alias := range t.cfg.SubnetAliases { + checkConflict(alias.Src.String(), "Alias Route") + } +} + +// Helper functions for IP arithmetic +func ipToUint32(ip net.IP) uint32 { + if len(ip) == 16 { + return binary.BigEndian.Uint32(ip[12:16]) + } + return binary.BigEndian.Uint32(ip) +} + +func uint32ToIP(n uint32) net.IP { + ip := make(net.IP, 4) + binary.BigEndian.PutUint32(ip, n) + return ip +} + +func ipAdd(ip net.IP, offset uint32) net.IP { + val := ipToUint32(ip) + return uint32ToIP(val + offset) +} + +func ipSub(a, b net.IP) uint32 { + return ipToUint32(a) - ipToUint32(b) +}