Refine WireGuard client routing and authentication UI
This commit is contained in:
1 parent
eabb3e4472
commit
41fb9d5631
33 files changed
+1709
-469
No files matched your search
+204
-4
@@ -4,6 +4,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
@@ -15,6 +16,7 @@ import (
|
||||
"net/netip"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
@@ -22,6 +24,7 @@ import (
|
||||
|
||||
"golang.zx2c4.com/wireguard/conn"
|
||||
"golang.zx2c4.com/wireguard/device"
|
||||
"golang.zx2c4.com/wireguard/tun"
|
||||
"golang.zx2c4.com/wireguard/tun/netstack"
|
||||
)
|
||||
|
||||
@@ -45,20 +48,80 @@ type result struct {
|
||||
Connected bool `json:"connected"`
|
||||
Detail string `json:"detail"`
|
||||
Target string `json:"target,omitempty"`
|
||||
LatencyMS *int64 `json:"latency_ms,omitempty"`
|
||||
}
|
||||
|
||||
// Only these counters may cross the Go/Rust process boundary. IpcGet also
|
||||
// contains the device private key, so never serialize its raw response.
|
||||
type tunnelStatus struct {
|
||||
Status string `json:"status"`
|
||||
HandshakeUnix uint64 `json:"handshake_unix"`
|
||||
RXBytes uint64 `json:"rx_bytes"`
|
||||
TXBytes uint64 `json:"tx_bytes"`
|
||||
}
|
||||
|
||||
func sanitizedStatus(raw string) (tunnelStatus, error) {
|
||||
status := tunnelStatus{Status: "device_status"}
|
||||
seenPeer := false
|
||||
for _, line := range strings.Split(raw, "\n") {
|
||||
key, value, ok := strings.Cut(line, "=")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if key == "public_key" {
|
||||
if seenPeer {
|
||||
return tunnelStatus{}, errors.New("设备返回多个 Peer")
|
||||
}
|
||||
seenPeer = true
|
||||
}
|
||||
if !seenPeer {
|
||||
continue
|
||||
}
|
||||
var field *uint64
|
||||
switch key {
|
||||
case "last_handshake_time_sec":
|
||||
field = &status.HandshakeUnix
|
||||
case "rx_bytes":
|
||||
field = &status.RXBytes
|
||||
case "tx_bytes":
|
||||
field = &status.TXBytes
|
||||
default:
|
||||
continue
|
||||
}
|
||||
parsed, err := strconv.ParseUint(value, 10, 64)
|
||||
if err != nil {
|
||||
return tunnelStatus{}, errors.New("设备状态计数器无效")
|
||||
}
|
||||
*field = parsed
|
||||
}
|
||||
if !seenPeer {
|
||||
return tunnelStatus{}, errors.New("设备没有可用的 Peer")
|
||||
}
|
||||
return status, nil
|
||||
}
|
||||
|
||||
func readRecord(path string) (*record, error) {
|
||||
info, err := os.Lstat(path)
|
||||
// Open the same inode that we check: Lstat followed by ReadFile would let
|
||||
// another process replace the path with a symlink between those calls.
|
||||
file, err := openRecord(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("无法读取本机设备配置: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
info, err := file.Stat()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("无法检查本机设备配置: %w", err)
|
||||
}
|
||||
if !info.Mode().IsRegular() || info.Mode().Perm()&0077 != 0 {
|
||||
return nil, errors.New("本机设备配置权限必须为 0600")
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
data, err := io.ReadAll(io.LimitReader(file, 64*1024+1))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("无法读取本机设备配置: %w", err)
|
||||
}
|
||||
if len(data) > 64*1024 {
|
||||
return nil, errors.New("本机设备配置超过大小限制")
|
||||
}
|
||||
var r record
|
||||
if err := json.Unmarshal(data, &r); err != nil {
|
||||
return nil, errors.New("本机设备配置格式无效")
|
||||
@@ -227,6 +290,9 @@ func serveSOCKS(r *record, address string) error {
|
||||
}
|
||||
defer listener.Close()
|
||||
parentPID := os.Getppid()
|
||||
if parentPID <= 1 {
|
||||
return errors.New("代理控制进程已退出")
|
||||
}
|
||||
go func() {
|
||||
ticker := time.NewTicker(2 * time.Second)
|
||||
defer ticker.Stop()
|
||||
@@ -261,6 +327,121 @@ func serveSOCKS(r *record, address string) error {
|
||||
}
|
||||
}
|
||||
|
||||
// serveTUN creates a native interface. The parent owns IP/routes/DNS and must
|
||||
// not report a usable connection until those OS changes have succeeded.
|
||||
func serveTUN(r *record) error {
|
||||
if runtime.GOOS != "darwin" {
|
||||
return errors.New("原生 TUN 目前仅实现 macOS 数据面,系统网络配置尚未接入")
|
||||
}
|
||||
// Capture before initialization: a parent dying while the device starts
|
||||
// must not be mistaken for a new, healthy parent (typically launchd).
|
||||
parentPID := os.Getppid()
|
||||
if parentPID <= 1 {
|
||||
return errors.New("隧道控制进程已退出")
|
||||
}
|
||||
endpoint, port, err := resolveEndpoint(r.Config.Endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resolved := *r
|
||||
resolved.Config.Endpoint = net.JoinHostPort(endpoint.String(), port)
|
||||
settings, _, err := uapi(&resolved, netip.Addr{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mtu := int(r.Config.MTU)
|
||||
if mtu == 0 {
|
||||
mtu = 1420
|
||||
}
|
||||
if mtu < 576 || mtu > 9000 {
|
||||
return errors.New("MTU 无效")
|
||||
}
|
||||
// Upstream's macOS CreateTUN optionally writes the assigned interface name
|
||||
// to WG_TUN_NAME_FILE. Our JSON handshake already returns the name; ignore
|
||||
// inherited environment so a launcher's variable cannot trigger a file write.
|
||||
_ = os.Unsetenv("WG_TUN_NAME_FILE")
|
||||
interfaceDevice, err := tun.CreateTUN("utun", mtu)
|
||||
if err != nil {
|
||||
return fmt.Errorf("无法创建本机网络接口: %w", err)
|
||||
}
|
||||
name, err := interfaceDevice.Name()
|
||||
if err != nil {
|
||||
_ = interfaceDevice.Close()
|
||||
return fmt.Errorf("无法获取本机网络接口: %w", err)
|
||||
}
|
||||
plan, err := planNativeRoutes(r, name, endpoint)
|
||||
if err != nil {
|
||||
_ = interfaceDevice.Close()
|
||||
return err
|
||||
}
|
||||
if plan.EndpointBypass {
|
||||
exit, err := physicalExitFor(endpoint)
|
||||
if err != nil {
|
||||
_ = interfaceDevice.Close()
|
||||
return err
|
||||
}
|
||||
plan.PhysicalExit = &exit
|
||||
}
|
||||
dev := device.NewDevice(interfaceDevice, conn.NewDefaultBind(), device.NewLogger(device.LogLevelSilent, ""))
|
||||
defer dev.Close()
|
||||
if err := dev.IpcSet(settings); err != nil {
|
||||
return errors.New("WireGuard 连接参数无效")
|
||||
}
|
||||
if err := dev.Up(); err != nil {
|
||||
return fmt.Errorf("Go 隧道启动失败: %w", err)
|
||||
}
|
||||
// Never include configuration, tokens, or keys in the readiness protocol.
|
||||
if err := json.NewEncoder(os.Stdout).Encode(struct {
|
||||
Status string `json:"status"`
|
||||
Plan networkPlan `json:"plan"`
|
||||
}{Status: "interface_created", Plan: plan}); err != nil {
|
||||
return fmt.Errorf("无法向控制面报告本机接口: %w", err)
|
||||
}
|
||||
stop := make(chan os.Signal, 1)
|
||||
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
|
||||
defer signal.Stop(stop)
|
||||
commands := make(chan string)
|
||||
go func() {
|
||||
defer close(commands)
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
for scanner.Scan() {
|
||||
commands <- scanner.Text()
|
||||
}
|
||||
}()
|
||||
ticker := time.NewTicker(2 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-stop:
|
||||
return nil
|
||||
case command, ok := <-commands:
|
||||
if !ok || command == "stop" {
|
||||
return nil
|
||||
}
|
||||
if command != "status" {
|
||||
return errors.New("未知隧道控制指令")
|
||||
}
|
||||
raw, err := dev.IpcGet()
|
||||
if err != nil {
|
||||
return errors.New("无法读取 WireGuard 状态")
|
||||
}
|
||||
status, err := sanitizedStatus(raw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := json.NewEncoder(os.Stdout).Encode(status); err != nil {
|
||||
return errors.New("无法报告 WireGuard 状态")
|
||||
}
|
||||
case <-dev.Wait():
|
||||
return errors.New("WireGuard 接口意外关闭")
|
||||
case <-ticker.C:
|
||||
if os.Getppid() != parentPID || parentPID <= 1 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleSOCKS(client net.Conn, r *record, stack *netstack.Net) {
|
||||
defer client.Close()
|
||||
_ = client.SetDeadline(time.Now().Add(15 * time.Second))
|
||||
@@ -375,13 +556,21 @@ func main() {
|
||||
fmt.Println("opsx-tunnel-go " + version)
|
||||
return
|
||||
}
|
||||
if len(os.Args) != 4 || (os.Args[1] != "probe" && os.Args[1] != "proxy") {
|
||||
fmt.Fprintln(os.Stderr, "usage: opsx-tunnel-go <probe|proxy> <local-record> <IP:port|loopback:port>")
|
||||
if (len(os.Args) != 4 || (os.Args[1] != "probe" && os.Args[1] != "proxy")) &&
|
||||
(len(os.Args) != 3 || os.Args[1] != "tun") {
|
||||
fmt.Fprintln(os.Stderr, "usage: opsx-tunnel-go <probe|proxy> <local-record> <IP:port|loopback:port> | tun <local-record>")
|
||||
os.Exit(2)
|
||||
}
|
||||
// stdout only contains a non-secret, machine-readable result. Never echo the
|
||||
// configuration, keys, token, or arbitrary upstream error strings.
|
||||
r, err := readRecord(os.Args[2])
|
||||
if err == nil && os.Args[1] == "tun" {
|
||||
if err = serveTUN(r); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err == nil && os.Args[1] == "proxy" {
|
||||
err = serveSOCKS(r, os.Args[3])
|
||||
if err != nil {
|
||||
@@ -390,12 +579,23 @@ func main() {
|
||||
}
|
||||
return
|
||||
}
|
||||
var latency *int64
|
||||
if err == nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second)
|
||||
defer cancel()
|
||||
started := time.Now()
|
||||
err = probe(ctx, r, os.Args[3])
|
||||
if err == nil {
|
||||
elapsed := time.Since(started).Milliseconds()
|
||||
if elapsed < 1 {
|
||||
elapsed = 1
|
||||
}
|
||||
// Assignment occurs below, after the result is constructed.
|
||||
latency = &elapsed
|
||||
}
|
||||
}
|
||||
res := result{Connected: err == nil, Target: os.Args[3]}
|
||||
res.LatencyMS = latency
|
||||
if err == nil {
|
||||
res.Detail = "WireGuard 出口 TCP 已连通"
|
||||
} else {
|
||||
|
||||
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"net/netip"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -64,6 +65,93 @@ func TestRejectSymlinkedKeyFile(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRejectOversizedRecord(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "record.json")
|
||||
if err := os.WriteFile(path, []byte(strings.Repeat("x", 64*1024+1)), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := readRecord(path); err == nil || !strings.Contains(err.Error(), "大小限制") {
|
||||
t.Fatalf("oversized record accepted: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatusReturnsOnlyNonSecretCounters(t *testing.T) {
|
||||
raw := "private_key=secret\npublic_key=peer\npreshared_key=secret\nendpoint=192.0.2.1:51820\n" +
|
||||
"last_handshake_time_sec=123\nrx_bytes=456\ntx_bytes=789\nallowed_ip=0.0.0.0/0\n"
|
||||
status, err := sanitizedStatus(raw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if status.Status != "device_status" || status.HandshakeUnix != 123 || status.RXBytes != 456 || status.TXBytes != 789 {
|
||||
t.Fatalf("unexpected status: %+v", status)
|
||||
}
|
||||
data, err := json.Marshal(status)
|
||||
if err != nil || strings.Contains(string(data), "secret") || strings.Contains(string(data), "192.0.2.1") {
|
||||
t.Fatalf("status leaked configuration: %s, %v", data, err)
|
||||
}
|
||||
for _, malformed := range []string{"private_key=secret", "public_key=peer\nrx_bytes=bad", "public_key=one\npublic_key=two"} {
|
||||
if _, err := sanitizedStatus(malformed); err == nil {
|
||||
t.Fatalf("malformed device status accepted: %q", malformed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNativeRoutePlanGuardsPhysicalEndpoint(t *testing.T) {
|
||||
r := testRecord()
|
||||
ip, port, err := resolveEndpoint(r.Config.Endpoint)
|
||||
if err != nil || ip.String() != "121.4.111.208" || port != "51820" {
|
||||
t.Fatalf("endpoint resolution failed: %s, %s, %v", ip, port, err)
|
||||
}
|
||||
r.Config.AllowedIPs = "0.0.0.0/0"
|
||||
plan, err := planNativeRoutes(r, "utun12", ip)
|
||||
if err != nil || !plan.EndpointBypass || len(plan.Routes) != 2 ||
|
||||
plan.Routes[0].String() != "0.0.0.0/1" || plan.Routes[1].String() != "128.0.0.0/1" {
|
||||
t.Fatalf("full route missing endpoint bypass: %+v, %v", plan, err)
|
||||
}
|
||||
r.Config.AllowedIPs = "10.88.0.0/24, 10.88.0.0/24"
|
||||
plan, err = planNativeRoutes(r, "utun2", netip.MustParseAddr("121.4.111.208"))
|
||||
if err != nil || plan.EndpointBypass || len(plan.Routes) != 1 || plan.Routes[0].String() != "10.88.0.0/24" {
|
||||
t.Fatalf("split route not normalized: %+v, %v", plan, err)
|
||||
}
|
||||
r.Config.AllowedIPs = "::/0, 0.0.0.0/0"
|
||||
if _, err := planNativeRoutes(r, "utun2", netip.MustParseAddr("121.4.111.208")); err == nil {
|
||||
t.Fatal("unsupported IPv6 route accepted")
|
||||
}
|
||||
for _, name := range []string{"utun", "utun1;route", "eth0", "utun-1"} {
|
||||
if _, err := planNativeRoutes(r, name, netip.MustParseAddr("121.4.111.208")); err == nil {
|
||||
t.Fatalf("unsafe interface accepted: %q", name)
|
||||
}
|
||||
}
|
||||
for _, endpoint := range []string{"127.0.0.1:51820", "[::1]:51820", "missing-port"} {
|
||||
if _, _, err := resolveEndpoint(endpoint); err == nil {
|
||||
t.Fatalf("invalid endpoint accepted: %q", endpoint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPhysicalRouteParsingRejectsVPNAndInjection(t *testing.T) {
|
||||
output := " route to: 121.4.111.208\n gateway: 192.168.80.1\n interface: en0\n"
|
||||
exit, err := parsePhysicalRoute(output)
|
||||
if err != nil || exit.Interface != "en0" || exit.Gateway == nil || exit.Gateway.String() != "192.168.80.1" {
|
||||
t.Fatalf("physical gateway rejected: %+v, %v", exit, err)
|
||||
}
|
||||
direct, err := parsePhysicalRoute("interface: en4\n")
|
||||
if err != nil || direct.Gateway != nil {
|
||||
t.Fatalf("direct physical link rejected: %+v, %v", direct, err)
|
||||
}
|
||||
for _, raw := range []string{
|
||||
"interface: utun2\ngateway: 10.0.0.1\n",
|
||||
"interface: en0;touch /tmp/injected\n",
|
||||
"interface: lo0\n",
|
||||
"gateway: 192.168.80.1\n",
|
||||
"interface: en0\ngateway: invalid\n",
|
||||
} {
|
||||
if _, err := parsePhysicalRoute(raw); err == nil {
|
||||
t.Fatalf("unsafe route accepted: %q", raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyCannotBindPublicInterface(t *testing.T) {
|
||||
for _, address := range []string{"0.0.0.0:19090", "192.0.2.1:19090", "127.0.0.1:0", "garbage"} {
|
||||
if err := serveSOCKS(testRecord(), address); err == nil {
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func resolveEndpoint(raw string) (netip.Addr, string, error) {
|
||||
host, port, err := net.SplitHostPort(raw)
|
||||
if err != nil || host == "" || port == "" {
|
||||
return netip.Addr{}, "", errors.New("VPN 公网端点格式无效")
|
||||
}
|
||||
if ip, err := netip.ParseAddr(host); err == nil {
|
||||
if !ip.Is4() || !ip.IsGlobalUnicast() {
|
||||
return netip.Addr{}, "", errors.New("VPN 公网端点需要有效 IPv4 地址")
|
||||
}
|
||||
return ip, port, nil
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
addresses, err := net.DefaultResolver.LookupNetIP(ctx, "ip4", host)
|
||||
if err != nil {
|
||||
return netip.Addr{}, "", errors.New("无法在连接前解析 VPN 公网端点")
|
||||
}
|
||||
for _, address := range addresses {
|
||||
if address.Is4() && address.IsGlobalUnicast() {
|
||||
return address, port, nil
|
||||
}
|
||||
}
|
||||
return netip.Addr{}, "", errors.New("VPN 公网端点没有可用 IPv4 地址")
|
||||
}
|
||||
|
||||
// networkPlan contains only public addressing information. The privileged
|
||||
// network adapter still needs to discover the physical gateway and persist a
|
||||
// rollback journal before applying any of these routes.
|
||||
type networkPlan struct {
|
||||
Interface string `json:"interface"`
|
||||
ClientAddress netip.Prefix `json:"client_address"`
|
||||
Routes []netip.Prefix `json:"routes"`
|
||||
Endpoint netip.Addr `json:"endpoint"`
|
||||
EndpointBypass bool `json:"endpoint_bypass"`
|
||||
PhysicalExit *physicalRoute `json:"physical_exit,omitempty"`
|
||||
}
|
||||
|
||||
type physicalRoute struct {
|
||||
Interface string `json:"interface"`
|
||||
Gateway *netip.Addr `json:"gateway,omitempty"`
|
||||
}
|
||||
|
||||
// macOS route -n get output is only an observation, never an executable
|
||||
// command. Reject another VPN's utun as the physical endpoint exit.
|
||||
func parsePhysicalRoute(output string) (physicalRoute, error) {
|
||||
var exit physicalRoute
|
||||
for _, line := range strings.Split(output, "\n") {
|
||||
key, value, ok := strings.Cut(strings.TrimSpace(line), ":")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
value = strings.TrimSpace(value)
|
||||
switch key {
|
||||
case "interface":
|
||||
exit.Interface = value
|
||||
case "gateway":
|
||||
gateway, err := netip.ParseAddr(value)
|
||||
if err != nil || !gateway.Is4() || gateway.IsUnspecified() || gateway.IsLoopback() {
|
||||
return physicalRoute{}, errors.New("物理网关地址无效")
|
||||
}
|
||||
exit.Gateway = &gateway
|
||||
}
|
||||
}
|
||||
if len(exit.Interface) < 2 || len(exit.Interface) > 16 || strings.HasPrefix(exit.Interface, "utun") || exit.Interface == "lo0" {
|
||||
return physicalRoute{}, errors.New("VPN 网关当前未经过物理网络出口")
|
||||
}
|
||||
for _, char := range exit.Interface {
|
||||
if !((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9')) {
|
||||
return physicalRoute{}, errors.New("物理出口接口名称无效")
|
||||
}
|
||||
}
|
||||
return exit, nil
|
||||
}
|
||||
|
||||
func planNativeRoutes(r *record, interfaceName string, endpoint netip.Addr) (networkPlan, error) {
|
||||
if !strings.HasPrefix(interfaceName, "utun") || len(interfaceName) < 5 {
|
||||
return networkPlan{}, errors.New("本机接口名称无效")
|
||||
}
|
||||
for _, digit := range interfaceName[4:] {
|
||||
if digit < '0' || digit > '9' {
|
||||
return networkPlan{}, errors.New("本机接口名称无效")
|
||||
}
|
||||
}
|
||||
address, err := netip.ParsePrefix(r.Config.ClientAddress)
|
||||
if err != nil || !address.Addr().Is4() || address.Addr().IsUnspecified() {
|
||||
return networkPlan{}, errors.New("本机 IPv4 地址无效")
|
||||
}
|
||||
if !endpoint.IsValid() || !endpoint.Is4() || !endpoint.IsGlobalUnicast() {
|
||||
return networkPlan{}, errors.New("VPN 公网端点必须解析为可达的 IPv4 地址")
|
||||
}
|
||||
plan := networkPlan{Interface: interfaceName, ClientAddress: address, Endpoint: endpoint, Routes: []netip.Prefix{}}
|
||||
seen := make(map[netip.Prefix]bool)
|
||||
for _, part := range strings.Split(r.Config.AllowedIPs, ",") {
|
||||
prefix, err := netip.ParsePrefix(strings.TrimSpace(part))
|
||||
if err != nil {
|
||||
return networkPlan{}, errors.New("WireGuard 授权路由无效")
|
||||
}
|
||||
if !prefix.Addr().Is4() {
|
||||
return networkPlan{}, errors.New("IPv6 系统路由尚未实现,请在专线中仅授权 IPv4 路由")
|
||||
}
|
||||
prefix = prefix.Masked()
|
||||
if prefix.Bits() == 0 {
|
||||
// These /1 routes override the physical default while retaining it
|
||||
// for explicit endpoint /32 bypass. The helper must install the bypass.
|
||||
for _, half := range []string{"0.0.0.0/1", "128.0.0.0/1"} {
|
||||
p := netip.MustParsePrefix(half)
|
||||
if !seen[p] {
|
||||
plan.Routes = append(plan.Routes, p)
|
||||
seen[p] = true
|
||||
}
|
||||
}
|
||||
plan.EndpointBypass = true
|
||||
continue
|
||||
}
|
||||
if !seen[prefix] {
|
||||
plan.Routes = append(plan.Routes, prefix)
|
||||
seen[prefix] = true
|
||||
}
|
||||
if prefix.Contains(endpoint) {
|
||||
plan.EndpointBypass = true
|
||||
}
|
||||
}
|
||||
if plan.EndpointBypass {
|
||||
// A physical /32 must be installed before any matching VPN route.
|
||||
// Otherwise a full-tunnel VPN can attempt to reach itself through TUN.
|
||||
if endpoint == address.Addr() {
|
||||
return networkPlan{}, errors.New("VPN 端点与本机地址冲突")
|
||||
}
|
||||
}
|
||||
return plan, nil
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
//go:build !windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func openRecord(path string) (*os.File, error) {
|
||||
fd, err := unix.Open(path, unix.O_RDONLY|unix.O_CLOEXEC|unix.O_NOFOLLOW, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return os.NewFile(uintptr(fd), path), nil
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
//go:build windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func openRecord(path string) (*os.File, error) {
|
||||
name, err := windows.UTF16PtrFromString(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
handle, err := windows.CreateFile(name, windows.GENERIC_READ, windows.FILE_SHARE_READ,
|
||||
nil, windows.OPEN_EXISTING, windows.FILE_FLAG_OPEN_REPARSE_POINT, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var data windows.ByHandleFileInformation
|
||||
if err := windows.GetFileInformationByHandle(handle, &data); err != nil {
|
||||
windows.CloseHandle(handle)
|
||||
return nil, err
|
||||
}
|
||||
if data.FileAttributes&windows.FILE_ATTRIBUTE_REPARSE_POINT != 0 {
|
||||
windows.CloseHandle(handle)
|
||||
return nil, syscall.EINVAL
|
||||
}
|
||||
return os.NewFile(uintptr(handle), path), nil
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//go:build darwin
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/netip"
|
||||
"os/exec"
|
||||
"time"
|
||||
)
|
||||
|
||||
func physicalExitFor(endpoint netip.Addr) (physicalRoute, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
output, err := exec.CommandContext(ctx, "/sbin/route", "-n", "get", endpoint.String()).Output()
|
||||
if err != nil {
|
||||
return physicalRoute{}, errors.New("无法读取 VPN 网关的当前物理出口")
|
||||
}
|
||||
return parsePhysicalRoute(string(output))
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
//go:build !darwin
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/netip"
|
||||
)
|
||||
|
||||
func physicalExitFor(_ netip.Addr) (physicalRoute, error) {
|
||||
return physicalRoute{}, errors.New("当前平台尚不支持系统隧道网络配置")
|
||||
}
|
||||
Reference in new issue
Block a user