You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
256 lines
8.9 KiB
256 lines
8.9 KiB
// Package warp provides the Warp (MASQUE/Cloudflare) mode runner for the vk-turn-proxy client.
|
|
// It is activated by the -warp flag in the main binary and reuses existing vk-turn flags
|
|
// for VK TURN relay integration (-vk-link, -listen, etc.).
|
|
package warp
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"net/netip"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/cacggghp/vk-turn-proxy/client/warp/api"
|
|
"github.com/cacggghp/vk-turn-proxy/client/warp/config"
|
|
"github.com/cacggghp/vk-turn-proxy/client/warp/internal"
|
|
"github.com/cacggghp/vk-turn-proxy/client/warp/proxy"
|
|
"golang.zx2c4.com/wireguard/tun/netstack"
|
|
)
|
|
|
|
// RunnerConfig holds all parameters needed to start the Warp mode.
|
|
type RunnerConfig struct {
|
|
// ConfigPath is the path to the Warp config.json.
|
|
// If empty and the file is not found at the default path, registration is triggered.
|
|
ConfigPath string
|
|
// ProxyAddr is the address for the mixed SOCKS5/HTTP proxy (e.g. "127.0.0.1:4080").
|
|
ProxyAddr string
|
|
// GetRelayConn is an optional function that provides a pre-allocated TURN relay connection.
|
|
// If nil, Warp connects directly to Cloudflare's MASQUE endpoint.
|
|
GetRelayConn api.GetRelayConnFunc
|
|
// ConnectPort is the port for the MASQUE QUIC connection (default 443).
|
|
ConnectPort int
|
|
// UseIPv6 selects the IPv6 endpoint instead of IPv4 for the MASQUE connection.
|
|
UseIPv6 bool
|
|
// KeepalivePeriod is the QUIC keepalive interval.
|
|
KeepalivePeriod time.Duration
|
|
// InitialPacketSize is the initial QUIC packet size.
|
|
InitialPacketSize uint16
|
|
// ReconnectDelay is the delay between tunnel reconnect attempts.
|
|
ReconnectDelay time.Duration
|
|
// MTU is the MTU for the virtual TUN device.
|
|
MTU int
|
|
// LocalDNS skips tunnel DNS and uses the system resolver.
|
|
// Useful when 162.159.36.1 is unreachable over the TURN relay.
|
|
LocalDNS bool
|
|
// Debug enables verbose logging in the warp/api package.
|
|
Debug bool
|
|
}
|
|
|
|
// DefaultRunnerConfig returns a RunnerConfig with sensible defaults.
|
|
func DefaultRunnerConfig() RunnerConfig {
|
|
return RunnerConfig{
|
|
ConfigPath: "config.json",
|
|
ProxyAddr: "127.0.0.1:4080",
|
|
ConnectPort: 443,
|
|
UseIPv6: false,
|
|
KeepalivePeriod: 30 * time.Second,
|
|
InitialPacketSize: 1242,
|
|
ReconnectDelay: 1 * time.Second,
|
|
MTU: 1200, // Lowered to avoid fragmentation over TURN relay
|
|
}
|
|
}
|
|
|
|
// Run starts the Warp-in-VK-TURN mode.
|
|
// It handles config loading/registration, then starts the MASQUE tunnel and mixed proxy.
|
|
func Run(ctx context.Context, cfg RunnerConfig) error {
|
|
// 1. Resolve config path to absolute
|
|
cfgPath, err := resolveConfigPath(cfg.ConfigPath)
|
|
if err != nil {
|
|
return fmt.Errorf("warp: resolve config path: %w", err)
|
|
}
|
|
|
|
// 2. Try to load config
|
|
if err := config.LoadConfig(cfgPath); err != nil {
|
|
if cfg.ConfigPath != "" && cfg.ConfigPath != "config.json" {
|
|
// User explicitly specified a config path — error out
|
|
return fmt.Errorf("warp: config file not found at %s: %w", cfgPath, err)
|
|
}
|
|
// Default path not found — start interactive registration
|
|
log.Printf("[Warp] Config not found at %s. Starting registration...", cfgPath)
|
|
if err := runInteractiveRegistration(cfgPath); err != nil {
|
|
return fmt.Errorf("warp: registration failed: %w", err)
|
|
}
|
|
}
|
|
|
|
// 3. Prepare TLS keys from config
|
|
privKey, err := config.AppConfig.GetEcPrivateKey()
|
|
if err != nil {
|
|
return fmt.Errorf("warp: get private key: %w", err)
|
|
}
|
|
peerPubKey, err := config.AppConfig.GetEcEndpointPublicKey()
|
|
if err != nil {
|
|
return fmt.Errorf("warp: get peer public key: %w", err)
|
|
}
|
|
cert, err := internal.GenerateCert(privKey, &privKey.PublicKey)
|
|
if err != nil {
|
|
return fmt.Errorf("warp: generate cert: %w", err)
|
|
}
|
|
|
|
tlsConfig, err := api.PrepareTlsConfig(privKey, peerPubKey, cert, internal.ConnectSNI)
|
|
if err != nil {
|
|
return fmt.Errorf("warp: prepare TLS config: %w", err)
|
|
}
|
|
|
|
// 4. Determine MASQUE endpoint
|
|
connectPort := cfg.ConnectPort
|
|
if connectPort <= 0 {
|
|
connectPort = 443
|
|
}
|
|
var endpoint *net.UDPAddr
|
|
if cfg.UseIPv6 {
|
|
addr := net.JoinHostPort(config.AppConfig.EndpointV6, fmt.Sprint(connectPort))
|
|
endpoint, err = net.ResolveUDPAddr("udp", addr)
|
|
if err != nil {
|
|
return fmt.Errorf("warp: resolve IPv6 endpoint: %w", err)
|
|
}
|
|
} else {
|
|
addr := net.JoinHostPort(config.AppConfig.EndpointV4, fmt.Sprint(connectPort))
|
|
endpoint, err = net.ResolveUDPAddr("udp", addr)
|
|
if err != nil {
|
|
return fmt.Errorf("warp: resolve IPv4 endpoint: %w", err)
|
|
}
|
|
if ip4 := endpoint.IP.To4(); ip4 != nil {
|
|
endpoint.IP = ip4
|
|
}
|
|
}
|
|
|
|
// DNS addresses: Cloudflare WARP intentionally blocks/drops most regular UDP port 53 traffic
|
|
// over MASQUE tunnels to public servers (like 9.9.9.9 or 1.1.1.1) to enforce their DoH proxy.
|
|
// You MUST use their internal designated DNS forwarder: 162.159.36.1
|
|
dnsAddrs := []netip.Addr{
|
|
netip.MustParseAddr("162.159.36.1"),
|
|
netip.MustParseAddr("1.1.1.1"),
|
|
netip.MustParseAddr("1.0.0.1"),
|
|
}
|
|
var localAddresses []netip.Addr
|
|
parseInternalIP := func(s string) (netip.Addr, error) {
|
|
// Strip mask if present (e.g. 172.16.0.2/32)
|
|
if i := strings.Index(s, "/"); i != -1 {
|
|
s = s[:i]
|
|
}
|
|
return netip.ParseAddr(s)
|
|
}
|
|
if v4, err := parseInternalIP(config.AppConfig.IPv4); err == nil {
|
|
localAddresses = append(localAddresses, v4)
|
|
}
|
|
if v6, err := parseInternalIP(config.AppConfig.IPv6); err == nil {
|
|
localAddresses = append(localAddresses, v6)
|
|
}
|
|
|
|
api.Verbose = cfg.Debug
|
|
tunDev, tunNet, err := netstack.CreateNetTUN(localAddresses, dnsAddrs, cfg.MTU)
|
|
if err != nil {
|
|
return fmt.Errorf("warp: create virtual TUN: %w", err)
|
|
}
|
|
defer tunDev.Close()
|
|
|
|
// 6. Init mixed proxy so we can pass its SetReady callback
|
|
mp := proxy.NewMixedProxy(cfg.ProxyAddr, tunNet, dnsAddrs, cfg.LocalDNS)
|
|
|
|
// 7. Start tunnel maintenance in background
|
|
log.Printf("[Warp] Starting MASQUE tunnel to %s (via TURN: %v)", endpoint, cfg.GetRelayConn != nil)
|
|
go api.MaintainTunnel(
|
|
ctx,
|
|
tlsConfig,
|
|
cfg.KeepalivePeriod,
|
|
cfg.InitialPacketSize,
|
|
endpoint,
|
|
api.NewNetstackAdapter(tunDev),
|
|
cfg.MTU,
|
|
cfg.ReconnectDelay,
|
|
cfg.GetRelayConn,
|
|
mp.SetReady,
|
|
)
|
|
|
|
// 8. Start mixed proxy listener (blocks until cancelled)
|
|
// Both SOCKS5 and HTTP resolve DNS through the MASQUE tunnel via TunnelDNSResolver,
|
|
// then dial tunNet with the resolved IP — matching the working httpproxy.go pattern.
|
|
return mp.ListenAndServe(ctx)
|
|
}
|
|
|
|
// resolveConfigPath returns the absolute path for the config file.
|
|
// If the path is relative, it is resolved relative to the executable's directory.
|
|
func resolveConfigPath(cfgPath string) (string, error) {
|
|
if filepath.IsAbs(cfgPath) {
|
|
return cfgPath, nil
|
|
}
|
|
// Try CWD first
|
|
if _, err := os.Stat(cfgPath); err == nil {
|
|
abs, err := filepath.Abs(cfgPath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return abs, nil
|
|
}
|
|
// Fall back to executable directory (useful on Android/embedded)
|
|
exePath, err := os.Executable()
|
|
if err != nil {
|
|
return cfgPath, nil //nolint:nilerr — best effort
|
|
}
|
|
return filepath.Join(filepath.Dir(exePath), cfgPath), nil
|
|
}
|
|
|
|
// runInteractiveRegistration runs the interactive Cloudflare WARP registration flow.
|
|
// It asks the user to accept TOS and choose a device name, then saves the config.
|
|
func runInteractiveRegistration(cfgPath string) error {
|
|
log.Printf("[Warp] === Cloudflare WARP Registration ===")
|
|
|
|
// Register (will prompt for TOS internally inside api.Register)
|
|
accountData, err := api.Register(internal.DefaultModel, internal.DefaultLocale, "", false /* acceptTos — prompt inside */)
|
|
if err != nil {
|
|
return fmt.Errorf("register: %w", err)
|
|
}
|
|
|
|
fmt.Print("[Warp] Enter device name (leave empty for default): ")
|
|
var deviceName string
|
|
_, _ = fmt.Scanln(&deviceName)
|
|
|
|
privKey, pubKey, err := internal.GenerateEcKeyPair()
|
|
if err != nil {
|
|
return fmt.Errorf("generate key pair: %w", err)
|
|
}
|
|
|
|
log.Printf("[Warp] Enrolling device key...")
|
|
updatedAccountData, apiErr, err := api.EnrollKey(accountData, pubKey, deviceName)
|
|
if err != nil {
|
|
if apiErr != nil {
|
|
return fmt.Errorf("enroll key: %v (API errors: %s)", err, apiErr.ErrorsAsString("; "))
|
|
}
|
|
return fmt.Errorf("enroll key: %w", err)
|
|
}
|
|
|
|
log.Printf("[Warp] Registration successful. Saving config to %s...", cfgPath)
|
|
config.AppConfig = config.Config{
|
|
PrivateKey: base64.StdEncoding.EncodeToString(privKey),
|
|
EndpointV4: updatedAccountData.Config.Peers[0].Endpoint.V4[:len(updatedAccountData.Config.Peers[0].Endpoint.V4)-2],
|
|
EndpointV6: updatedAccountData.Config.Peers[0].Endpoint.V6[1 : len(updatedAccountData.Config.Peers[0].Endpoint.V6)-3],
|
|
EndpointPubKey: updatedAccountData.Config.Peers[0].PublicKey,
|
|
License: updatedAccountData.Account.License,
|
|
ID: updatedAccountData.ID,
|
|
AccessToken: accountData.Token,
|
|
IPv4: updatedAccountData.Config.Interface.Addresses.V4,
|
|
IPv6: updatedAccountData.Config.Interface.Addresses.V6,
|
|
}
|
|
|
|
if err := config.AppConfig.SaveConfig(cfgPath); err != nil {
|
|
return fmt.Errorf("save config: %w", err)
|
|
}
|
|
config.ConfigLoaded = true
|
|
log.Printf("[Warp] Config saved successfully.")
|
|
return nil
|
|
}
|
|
|