Release 3.10.0+1: sidebar UI (Home / Profiles / Settings / Logs / About), remembered account with 50 GB monthly renew + auto-renew, Windows VPN mode (wintun + tun2socks).
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
// Package vpnmode implements "Режим VPN": a wintun TUN device that captures
|
||||
// ALL system traffic (apps/games included) and feeds it into the local SOCKS
|
||||
// proxy of the active protocol engine via the embedded tun2socks stack.
|
||||
//
|
||||
// Windows-only for now. On other platforms Supported() returns false and the
|
||||
// UI hides the switch (no fake toggle).
|
||||
package vpnmode
|
||||
|
||||
import "io"
|
||||
|
||||
// Config describes one VPN-mode session.
|
||||
type Config struct {
|
||||
// SOCKSAddr is the host:port of the running local SOCKS5 proxy.
|
||||
SOCKSAddr string
|
||||
// ExcludeHosts are VPN server endpoints (host names or IPs) that must
|
||||
// bypass the TUN default route, otherwise the tunnel would loop.
|
||||
ExcludeHosts []string
|
||||
// DNSServers go through the tunnel (default 1.1.1.1 + 8.8.8.8).
|
||||
DNSServers []string
|
||||
// Stderr receives diagnostic messages (may be nil).
|
||||
Stderr io.Writer
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
//go:build !windows
|
||||
|
||||
package vpnmode
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Supported: VPN mode ships Windows-first; the UI hides the switch elsewhere.
|
||||
func Supported() bool { return false }
|
||||
|
||||
// IsElevated is meaningless off Windows.
|
||||
func IsElevated() bool { return false }
|
||||
|
||||
// WintunAvailable is Windows-only.
|
||||
func WintunAvailable() bool { return false }
|
||||
|
||||
// Session is a stub off Windows.
|
||||
type Session struct{}
|
||||
|
||||
// Start always fails off Windows.
|
||||
func Start(cfg Config) (*Session, error) {
|
||||
return nil, fmt.Errorf("режим VPN пока доступен только на Windows")
|
||||
}
|
||||
|
||||
// Close is a no-op stub.
|
||||
func (s *Session) Close() error { return nil }
|
||||
@@ -0,0 +1,281 @@
|
||||
//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("Запустите Navis от имени администратора для режима VPN")
|
||||
}
|
||||
if !WintunAvailable() {
|
||||
return nil, fmt.Errorf("не найден wintun.dll — скачайте с wintun.net и положите рядом с Navis.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
|
||||
}
|
||||
Reference in New Issue
Block a user