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:
Navis
2026-08-01 18:34:25 +03:00
co-authored by Cursor
parent 77bd7da861
commit 01fac376ba
25 changed files with 1803 additions and 602 deletions
+22
View File
@@ -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
}
+25
View File
@@ -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 }
+281
View File
@@ -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
}