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

// 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
}