Files
drover-go/internal/sboxrun/config.go
T
root 168596bcb5
Build / test (push) Failing after 33s
Build / build-windows (push) Has been skipped
Release / release (push) Failing after 3m22s
sboxrun: domain+IP-CIDR rules + remove voice-quality test
Three follow-up fixes after the WinDivert→sing-box pivot:

1. Discord updater now routes through upstream. Previously only the
   process-name rule matched, but sing-box's TUN-side process
   detection on Windows mis-attributes the in-process Rust updater's
   TLS connection to e.g. steam.exe — the connection went direct and
   hit RKN block. Adding domain_suffix + ip_cidr rules for Cloudflare
   (162.159/16, 104.16/13, 172.64/13) and Fastly (199.232/16,
   151.101/16) catches updates.discord.com regardless of which PID
   the kernel claims sent it. Verified via curl through mihomo:
   updates.discord.com responds 400 in 393ms (i.e. TLS handshake
   succeeds, only the path is wrong — proves the routing reaches it).

2. DiscordSystemHelper.exe added to TargetProcs alongside Update.exe
   (modern Discord builds use it for elevated updates).

3. UDP voice quality test removed from the checker. The STUN-via-
   relay burst measured private mihomo BND.ADDR (192.168.1.132)
   which is unroutable from external clients, so the test reported
   100% loss every time despite voice actually working through
   sing-box's TUN+SOCKS5. The remaining 6 checks (TCP/greet/auth/
   connect/UDP/api) cover what's actionable; voice quality is
   verified empirically by joining a Discord call.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 23:21:50 +03:00

148 lines
4.1 KiB
Go

package sboxrun
import (
"encoding/json"
"fmt"
)
// Config captures the user-visible proxy settings + which processes
// to route through it. Everything else (TUN interface, log level,
// Clash API endpoint) is hard-coded sensible defaults.
type Config struct {
ProxyHost string // upstream SOCKS5 host
ProxyPort int // upstream SOCKS5 port
UseAuth bool
Login string
Password string
TargetProcs []string // exe names to route via upstream (e.g. ["Discord.exe"])
ClashAPIPort int // 0 → 9090 default
LogLevel string // "info" | "debug" | "warn" — empty → "info"
LogPath string // absolute path for sing-box log output (empty = sing-box stdout, lost when admin-detached)
}
// BuildSingBoxConfig generates the sing-box JSON config string. It's
// a minimal config: TUN inbound (with auto_route + WFP per-process
// rule), SOCKS5 outbound to upstream, direct outbound for everything
// else, and a route rule that sends TargetProcs through the SOCKS5.
//
// Clash API on 127.0.0.1:9090 (or ClashAPIPort) lets the GUI poll
// connection stats live.
func BuildSingBoxConfig(c Config) (string, error) {
if c.ProxyHost == "" || c.ProxyPort == 0 {
return "", fmt.Errorf("ProxyHost and ProxyPort are required")
}
if len(c.TargetProcs) == 0 {
return "", fmt.Errorf("at least one target process is required")
}
logLevel := c.LogLevel
if logLevel == "" {
logLevel = "info"
}
clashPort := c.ClashAPIPort
if clashPort == 0 {
clashPort = 9090
}
upstream := map[string]any{
"type": "socks",
"tag": "upstream",
"server": c.ProxyHost,
"server_port": c.ProxyPort,
"version": "5",
"udp_over_tcp": false,
}
if c.UseAuth {
upstream["username"] = c.Login
upstream["password"] = c.Password
}
logBlock := map[string]any{
"level": logLevel,
"timestamp": true,
}
if c.LogPath != "" {
logBlock["output"] = c.LogPath
}
cfg := map[string]any{
"log": logBlock,
"inbounds": []any{
map[string]any{
"type": "tun",
"tag": "tun-in",
"interface_name": "drover-tun",
"address": []string{"172.18.0.1/30"},
"auto_route": true,
"strict_route": false,
"stack": "system",
"sniff": true,
},
},
"outbounds": []any{
upstream,
map[string]any{"type": "direct", "tag": "direct"},
},
"route": map[string]any{
"auto_detect_interface": true,
"final": "direct",
"rules": []any{
// 1. Domain rule for sniffed SNI (works when sniffing
// catches the ClientHello before route decision).
map[string]any{
"domain_suffix": []string{
"discord.com",
"discord.gg",
"discord.media",
"discordapp.com",
"discordapp.net",
"discord.dev",
},
"outbound": "upstream",
},
// 2. IP-CIDR fallback — sing-box on Windows TUN
// sometimes misattributes the source process for
// Discord's in-process Rust updater (gets attributed
// to steam.exe or similar), so even with the right
// process_name list the updater's TLS connection to
// updates.discord.com (Fastly: 199.232.x.x) goes
// direct and gets RKN-blocked. Force the major
// Discord/Cloudflare/Fastly ranges through upstream
// regardless of which process the kernel claims sent
// them.
map[string]any{
"ip_cidr": []string{
// Fastly (updates.discord.com)
"151.101.0.0/16",
"199.232.0.0/16",
"185.199.108.0/22",
// Cloudflare (Discord gateway, CDN, media)
"162.158.0.0/15",
"162.159.0.0/16",
"104.16.0.0/13",
"104.24.0.0/14",
"172.64.0.0/13",
"131.0.72.0/22",
},
"outbound": "upstream",
},
// 3. Process-name rule — covers Discord traffic to
// non-Cloudflare destinations (RTC voice, etc).
map[string]any{
"process_name": c.TargetProcs,
"outbound": "upstream",
},
},
},
"experimental": map[string]any{
"clash_api": map[string]any{
"external_controller": fmt.Sprintf("127.0.0.1:%d", clashPort),
},
},
}
out, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
return "", err
}
return string(out), nil
}