Keep dist/navis-release/ path and ship Navis.exe as a same-hash alias of EvilFox.exe so 3.x clients can still update from the Windows branch feed.
282 lines
8.4 KiB
Go
282 lines
8.4 KiB
Go
//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
|
||
}
|