//go:build windows package vpnmode import ( "encoding/binary" "fmt" "io" "net" "os" "os/exec" "path/filepath" "strconv" "strings" "sync" "syscall" "time" "unsafe" "golang.org/x/sys/windows" "gvisor.dev/gvisor/pkg/tcpip/stack" t2core "github.com/xjasonlyu/tun2socks/v2/core" "github.com/xjasonlyu/tun2socks/v2/core/device" t2tun "github.com/xjasonlyu/tun2socks/v2/core/device/tun" t2log "github.com/xjasonlyu/tun2socks/v2/log" "github.com/xjasonlyu/tun2socks/v2/proxy/socks5" "github.com/xjasonlyu/tun2socks/v2/tunnel" ) const ( tunName = "NavisTun" tunAddr = "10.255.99.2" tunMask = "255.255.255.0" // On-link gateway inside the TUN subnet; wintun swallows everything anyway. tunGateway = "10.255.99.1" ) // Supported reports that VPN mode is available on this OS. func Supported() bool { return true } // IsElevated reports whether the process runs with administrator rights. func IsElevated() bool { return windows.GetCurrentProcessToken().IsElevated() } // WintunAvailable checks that wintun.dll can be found (exe dir or System32). func WintunAvailable() bool { if exe, err := os.Executable(); err == nil { if st, err := os.Stat(filepath.Join(filepath.Dir(exe), "wintun.dll")); err == nil && !st.IsDir() { return true } } if sysDir, err := windows.GetSystemDirectory(); err == nil { if st, err := os.Stat(filepath.Join(sysDir, "wintun.dll")); err == nil && !st.IsDir() { return true } } return false } // Session is a running VPN-mode capture (TUN + tun2socks + routes). type Session struct { mu sync.Mutex dev device.Device stack *stack.Stack excluded []exclusionRoute // host routes added via route.exe, removed on Close stderr io.Writer closed bool setupDone bool } type exclusionRoute struct { ip string gateway string ifIndex uint32 } // Start opens the wintun adapter, wires it to the local SOCKS proxy and takes // over the default route (0.0.0.0/1 + 128.0.0.0/1). Requires admin+wintun.dll. func Start(cfg Config) (*Session, error) { if cfg.Stderr == nil { cfg.Stderr = os.Stderr } if strings.TrimSpace(cfg.SOCKSAddr) == "" { return nil, fmt.Errorf("режим VPN: нет локального SOCKS-прокси у текущего протокола") } if !IsElevated() { return nil, fmt.Errorf("Запустите EvilFox от имени администратора для режима VPN") } if !WintunAvailable() { return nil, fmt.Errorf("не найден wintun.dll — скачайте с wintun.net и положите рядом с EvilFox.exe") } if len(cfg.DNSServers) == 0 { cfg.DNSServers = []string{"1.1.1.1", "8.8.8.8"} } // Resolve exclusion routes BEFORE the default route changes, otherwise // DNS + GetBestRoute would already point into the (not yet working) TUN. var excls []exclusionRoute for _, h := range cfg.ExcludeHosts { h = strings.TrimSpace(h) if h == "" { continue } ips, err := net.LookupIP(h) if err != nil { return nil, fmt.Errorf("режим VPN: не удалось определить IP сервера %s: %w", h, err) } for _, ip := range ips { ip4 := ip.To4() if ip4 == nil || ip4.IsLoopback() || ip4.IsPrivate() { continue } gw, ifIdx, err := bestRoute(ip4) if err != nil { fmt.Fprintf(cfg.Stderr, "vpnmode: GetBestRoute %s: %v\n", ip4, err) continue } excls = append(excls, exclusionRoute{ip: ip4.String(), gateway: gw.String(), ifIndex: ifIdx}) } } // Keep tun2socks quiet unless something is wrong. if lvl, err := t2log.ParseLevel("warning"); err == nil { t2log.SetLogger(t2log.Must(t2log.NewLeveled(lvl))) } px, err := socks5.New(cfg.SOCKSAddr, "", "") if err != nil { return nil, fmt.Errorf("режим VPN: socks5 %s: %w", cfg.SOCKSAddr, err) } tunnel.T().SetProxy(px) dev, err := t2tun.Open(tunName, 0) if err != nil { return nil, fmt.Errorf("режим VPN: не удалось создать TUN-адаптер (wintun): %w", err) } st, err := t2core.CreateStack(&t2core.Config{ LinkEndpoint: dev, TransportHandler: tunnel.T(), }) if err != nil { dev.Close() return nil, fmt.Errorf("режим VPN: сетевой стек: %w", err) } s := &Session{dev: dev, stack: st, stderr: cfg.Stderr} if err := s.setupNetwork(cfg, excls); err != nil { _ = s.Close() return nil, err } return s, nil } func (s *Session) setupNetwork(cfg Config, excls []exclusionRoute) error { // The adapter needs a moment to register with the IP stack. var lastErr error for i := 0; i < 20; i++ { if _, err := net.InterfaceByName(tunName); err == nil { lastErr = nil break } else { lastErr = err } time.Sleep(150 * time.Millisecond) } if lastErr != nil { return fmt.Errorf("режим VPN: адаптер %s не появился: %w", tunName, lastErr) } if out, err := runHidden("netsh", "interface", "ipv4", "set", "address", "name="+tunName, "source=static", "addr="+tunAddr, "mask="+tunMask); err != nil { return fmt.Errorf("режим VPN: настройка адреса TUN: %v (%s)", err, out) } // DNS goes through the tunnel (queries hit the default route → TUN). if out, err := runHidden("netsh", "interface", "ipv4", "set", "dnsservers", "name="+tunName, "source=static", "address="+cfg.DNSServers[0], "register=none", "validate=no"); err != nil { fmt.Fprintf(s.stderr, "vpnmode: set dns: %v (%s)\n", err, out) } for i, d := range cfg.DNSServers[1:] { if out, err := runHidden("netsh", "interface", "ipv4", "add", "dnsservers", "name="+tunName, "address="+d, "index="+strconv.Itoa(i+2), "validate=no"); err != nil { fmt.Fprintf(s.stderr, "vpnmode: add dns: %v (%s)\n", err, out) } } // Exclusions first — the server endpoint must keep using the real uplink. for _, e := range excls { if e.gateway == "0.0.0.0" || e.gateway == "" { continue } if out, err := runHidden("route", "add", e.ip, "mask", "255.255.255.255", e.gateway, "metric", "1", "if", strconv.FormatUint(uint64(e.ifIndex), 10)); err != nil { return fmt.Errorf("режим VPN: маршрут-исключение для %s: %v (%s)", e.ip, err, out) } s.excluded = append(s.excluded, e) } // Two half-default routes beat 0.0.0.0/0 without touching the original. for _, prefix := range []string{"0.0.0.0/1", "128.0.0.0/1"} { if out, err := runHidden("netsh", "interface", "ipv4", "add", "route", prefix, tunName, tunGateway, "metric=1", "store=active"); err != nil { return fmt.Errorf("режим VPN: маршрут %s: %v (%s)", prefix, err, out) } } s.setupDone = true fmt.Fprintf(s.stderr, "vpnmode: TUN %s активен, весь трафик через %s (исключений: %d)\n", tunName, cfg.SOCKSAddr, len(s.excluded)) return nil } // Close removes exclusion routes and destroys the TUN adapter (its own routes // disappear together with the adapter). func (s *Session) Close() error { s.mu.Lock() defer s.mu.Unlock() if s.closed { return nil } s.closed = true for _, e := range s.excluded { if out, err := runHidden("route", "delete", e.ip, "mask", "255.255.255.255", e.gateway); err != nil { fmt.Fprintf(s.stderr, "vpnmode: route delete %s: %v (%s)\n", e.ip, err, out) } } s.excluded = nil if s.dev != nil { s.dev.Close() s.dev = nil } if s.stack != nil { s.stack.Close() s.stack = nil } return nil } func runHidden(name string, args ...string) (string, error) { cmd := exec.Command(name, args...) cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true, CreationFlags: 0x08000000} // CREATE_NO_WINDOW out, err := cmd.CombinedOutput() return strings.TrimSpace(string(out)), err } var ( modiphlpapi = windows.NewLazySystemDLL("iphlpapi.dll") procGetBestRoute = modiphlpapi.NewProc("GetBestRoute") ) // mibIPForwardRow mirrors MIB_IPFORWARDROW (iphlpapi). type mibIPForwardRow struct { Dest uint32 Mask uint32 Policy uint32 NextHop uint32 IfIndex uint32 Type uint32 Proto uint32 Age uint32 NextHopAS uint32 Metric1 uint32 Metric2 uint32 Metric3 uint32 Metric4 uint32 Metric5 uint32 } // bestRoute asks Windows for the current gateway + interface toward ip // (IPv4, network byte order DWORDs). func bestRoute(ip4 net.IP) (net.IP, uint32, error) { dest := binary.LittleEndian.Uint32(ip4.To4()) var row mibIPForwardRow r, _, _ := procGetBestRoute.Call(uintptr(dest), 0, uintptr(unsafe.Pointer(&row))) if r != 0 { return nil, 0, fmt.Errorf("GetBestRoute: код %d", r) } gw := make(net.IP, 4) binary.LittleEndian.PutUint32(gw, row.NextHop) return gw, row.IfIndex, nil }