diff --git a/socks/applyfileover_test.go b/socks/applyfileover_test.go
new file mode 100644
index 0000000..c3b0833
--- /dev/null
+++ b/socks/applyfileover_test.go
@@ -0,0 +1,126 @@
+package main
+
+import (
+ "testing"
+ "time"
+)
+
+// TestApplyFileOverAllFields walks every field branch of applyFileOver by
+// overlaying a file with every field set onto an empty base, then verifying
+// each landed. This is the branch-coverage companion to the partial-merge
+// semantics test.
+func TestApplyFileOverAllFields(t *testing.T) {
+ overlay := &reliabilityFile{
+ UdpTeardownSignal: boolPtr(true),
+ QuicRebindOnExitLoss: boolPtr(true),
+ TcpCollapseMaxHoldMs: int64Ptr(1500),
+ SendStallTimeoutMs: int64Ptr(3000),
+ ClusterAffinityFallback: boolPtr(true),
+ ServerNameAffinityBridge: boolPtr(true),
+ SequenceIdleTimeoutMs: int64Ptr(120000),
+ TcpSequenceIdleTimeoutMs: int64Ptr(600000),
+ BlackholeReceiveTimeoutMs: int64Ptr(20000),
+ MaxFlowsPerExit: intPtr(16),
+ AffinityStickyPastCap: boolPtr(true),
+ QuarantineGroupFollow: boolPtr(true),
+ GroupFollowWindowMs: int64Ptr(45000),
+ DialFailureRerace: boolPtr(true),
+ UplinkStalenessGateMs: int64Ptr(5000),
+ SoftVerdictDemote: boolPtr(true),
+ RemovalBudgetCount: intPtr(2),
+ RemovalBudgetWindowMs: int64Ptr(30000),
+ StandingReserve: boolPtr(true),
+ EffectiveTierSelection: boolPtr(true),
+ MinBlackholeDestinations: intPtr(2),
+ BlackholeLoadCorroboration: intPtr(8),
+ ProviderProbe: boolPtr(true),
+ ProbeTimeoutMs: int64Ptr(4000),
+ ProbeSampleHostCount: intPtr(4),
+ ProbeSilenceWarnStreak: intPtr(2),
+ EvaluationPoolMultiple: intPtr(2),
+ FormationPollTimeoutMs: int64Ptr(200),
+ BusyProbe: boolPtr(true),
+ BusyProbeBudgetMs: int64Ptr(1500),
+ SchedulerPauseToleranceMs: int64Ptr(2000),
+ SchedulerPauseRecoveryTimeoutMs: int64Ptr(5000),
+ BlackholeConnectComparativeTimeoutMs: int64Ptr(10000),
+ HeartbeatIntervalMs: int64Ptr(60000),
+ }
+
+ dst := &reliabilityFile{}
+ if err := applyFileOver(dst, overlay); err != nil {
+ t.Fatal(err)
+ }
+
+ s, err := fileToSettings(dst)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ checks := []struct {
+ name string
+ got any
+ want any
+ }{
+ {"UdpTeardownSignal", s.UdpTeardownSignal, true},
+ {"QuicRebindOnExitLoss", s.QuicRebindOnExitLoss, true},
+ {"TcpCollapseMaxHold", s.TcpCollapseMaxHold, 1500 * time.Millisecond},
+ {"SendStallTimeout", s.SendStallTimeout, 3 * time.Second},
+ {"ClusterAffinityFallback", s.ClusterAffinityFallback, true},
+ {"ServerNameAffinityBridge", s.ServerNameAffinityBridge, true},
+ {"SequenceIdleTimeout", s.SequenceIdleTimeout, 120 * time.Second},
+ {"TcpSequenceIdleTimeout", s.TcpSequenceIdleTimeout, 600 * time.Second},
+ {"BlackholeReceiveTimeout", s.BlackholeReceiveTimeout, 20 * time.Second},
+ {"MaxFlowsPerExit", s.MaxFlowsPerExit, 16},
+ {"AffinityStickyPastCap", s.AffinityStickyPastCap, true},
+ {"QuarantineGroupFollow", s.QuarantineGroupFollow, true},
+ {"GroupFollowWindow", s.GroupFollowWindow, 45 * time.Second},
+ {"DialFailureRerace", s.DialFailureRerace, true},
+ {"UplinkStalenessGate", s.UplinkStalenessGate, 5 * time.Second},
+ {"SoftVerdictDemote", s.SoftVerdictDemote, true},
+ {"RemovalBudgetCount", s.RemovalBudgetCount, 2},
+ {"RemovalBudgetWindow", s.RemovalBudgetWindow, 30 * time.Second},
+ {"StandingReserve", s.StandingReserve, true},
+ {"EffectiveTierSelection", s.EffectiveTierSelection, true},
+ {"MinBlackholeDestinations", s.MinBlackholeDestinations, 2},
+ {"BlackholeLoadCorroboration", s.BlackholeLoadCorroboration, 8},
+ {"ProviderProbe", s.ProviderProbe, true},
+ {"ProbeTimeout", s.ProbeTimeout, 4 * time.Second},
+ {"ProbeSampleHostCount", s.ProbeSampleHostCount, 4},
+ {"ProbeSilenceWarnStreak", s.ProbeSilenceWarnStreak, 2},
+ {"EvaluationPoolMultiple", s.EvaluationPoolMultiple, 2},
+ {"FormationPollTimeout", s.FormationPollTimeout, 200 * time.Millisecond},
+ {"BusyProbe", s.BusyProbe, true},
+ {"BusyProbeBudget", s.BusyProbeBudget, 1500 * time.Millisecond},
+ {"SchedulerPauseTolerance", s.SchedulerPauseTolerance, 2 * time.Second},
+ {"SchedulerPauseRecoveryTimeout", s.SchedulerPauseRecoveryTimeout, 5 * time.Second},
+ {"BlackholeConnectComparativeTimeout", s.BlackholeConnectComparativeTimeout, 10 * time.Second},
+ {"HeartbeatInterval", s.HeartbeatInterval, 60 * time.Second},
+ }
+ for _, c := range checks {
+ if c.got != c.want {
+ t.Errorf("%s = %v, want %v", c.name, c.got, c.want)
+ }
+ }
+}
+
+// TestLoadReliabilitySettingsFile covers the thin file-loading wrapper and
+// its error paths.
+func TestLoadReliabilitySettingsFile(t *testing.T) {
+ path := writeRelFile(t, `{"maxFlowsPerExit": 32}`)
+ s, err := loadReliabilitySettingsFile(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if s.MaxFlowsPerExit != 32 {
+ t.Errorf("MaxFlowsPerExit = %d, want 32", s.MaxFlowsPerExit)
+ }
+
+ if _, err := loadReliabilitySettingsFile("/nonexistent.json"); err == nil {
+ t.Error("expected error for missing file")
+ }
+ bad := writeRelFile(t, `{bad`)
+ if _, err := loadReliabilitySettingsFile(bad); err == nil {
+ t.Error("expected error for invalid json")
+ }
+}
diff --git a/socks/control_panel.html b/socks/control_panel.html
new file mode 100644
index 0000000..9ee3f93
--- /dev/null
+++ b/socks/control_panel.html
@@ -0,0 +1,429 @@
+
+
+
+
+
+
+socksproxy dev panel
+
+
+
+
+

+
+
socksproxy dev panel
+
connecting...
+
+
--
+
+
+Measurements
+loading...
+
+Actions
+
+
+
+
+
+
+
+
+
+Exits
+loading...
+
+
+
+
+
+
diff --git a/socks/control_server.go b/socks/control_server.go
new file mode 100644
index 0000000..617ec2c
--- /dev/null
+++ b/socks/control_server.go
@@ -0,0 +1,337 @@
+package main
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "net"
+ "net/http"
+ "net/url"
+ "sync"
+ "time"
+
+ _ "embed"
+
+ "github.com/urnetwork/connect"
+)
+
+//go:embed control_panel.html
+var controlPanelHTML []byte
+
+//go:embed ur_logo.svg
+var urLogoSVG []byte
+
+// mcControl is the slice of the multi client the control plane uses. The real
+// *connect.RemoteUserNatMultiClient satisfies it; tests substitute a fake.
+type mcControl interface {
+ ReliabilitySettings() *connect.ReliabilitySettings
+ SetReliabilitySettings(settings *connect.ReliabilitySettings)
+ ReliabilityMetrics() *connect.ReliabilityMetricsSnapshot
+ ResetReliabilityMetrics()
+ Shuffle()
+ ProbeAllExits() int
+ Exits() []*connect.ExitInfo
+ DropExit(clientId connect.Id) bool
+ StallExit(clientId connect.Id, stalled bool) bool
+ MigrateExit(clientId connect.Id) int
+ PacketStats() *connect.PacketStats
+}
+
+// controlServer exposes the live connection to a local http client so the
+// dev knobs can be changed mid-run without a reconnect, mirroring the android
+// developer screen. Bind it to loopback -- it is an unauthenticated control
+// plane. Even on loopback, mutating requests are rejected unless they carry
+// no Origin header or a loopback Origin, so a web page open in a browser
+// cannot drive the control plane (same class as the webpack-dev-server
+// drive-by-localhost CVEs).
+type controlServer struct {
+ server *http.Server
+ mc mcControl
+ mu sync.Mutex // serializes the read-merge-write of settings
+ started time.Time // when the control server came up; session elapsed
+ cancel context.CancelFunc // optional: quit action cancels the session ctx
+}
+
+// SetCancel wires the session cancel function so the "quit" action can stop
+// the process gracefully (same path as Ctrl+C).
+func (c *controlServer) SetCancel(cancel context.CancelFunc) {
+ c.cancel = cancel
+}
+
+// newControlServer builds the local http control plane bound to addr. It does
+// not bind until serve is called; addr must be a loopback address (enforced
+// by the caller in main.go run).
+func newControlServer(addr string, mc mcControl) (*controlServer, error) {
+ c := &controlServer{mc: mc, started: time.Now()}
+ mux := http.NewServeMux()
+ mux.HandleFunc("/", c.handleRoot)
+ mux.HandleFunc("/favicon.svg", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "image/svg+xml")
+ w.Write(urLogoSVG)
+ })
+ mux.HandleFunc("/settings", c.handleSettings)
+ mux.HandleFunc("/actions", c.handleActions)
+ mux.HandleFunc("/metrics", c.handleMetrics)
+ mux.HandleFunc("/exits", c.handleExits)
+ mux.HandleFunc("/stats", c.handleStats)
+ c.server = &http.Server{
+ Addr: addr,
+ Handler: mux,
+ ReadHeaderTimeout: 5 * time.Second,
+ ReadTimeout: 10 * time.Second,
+ WriteTimeout: 10 * time.Second,
+ IdleTimeout: 60 * time.Second,
+ }
+ return c, nil
+}
+
+func (c *controlServer) serve(ctx context.Context) {
+ go func() {
+ <-ctx.Done()
+ shutdownCtx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+ c.server.Shutdown(shutdownCtx)
+ }()
+ if err := c.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
+ fmt.Printf("control server error: %v\n", err)
+ }
+}
+
+func writeJSON(w http.ResponseWriter, status int, v any) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(status)
+ json.NewEncoder(w).Encode(v)
+}
+
+// handleRoot serves the embedded dev panel (the GUI mirror of the android
+// developer screen). Everything else on / is a 404; the api routes are
+// registered explicitly.
+func (c *controlServer) handleRoot(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path != "/" {
+ http.NotFound(w, r)
+ return
+ }
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ w.Write(controlPanelHTML)
+}
+
+// loopbackOrigin reports whether the request's Origin header (if any) is a
+// loopback origin. A request with no Origin header (curl, local scripts) is
+// allowed; a browser cross-origin request always sends Origin, and only
+// loopback origins may drive the unauthenticated control plane.
+func loopbackOrigin(r *http.Request) bool {
+ origin := r.Header.Get("Origin")
+ if origin == "" {
+ return true
+ }
+ u, err := url.Parse(origin)
+ if err != nil || u.Hostname() == "" {
+ return false
+ }
+ host := u.Hostname()
+ ip := net.ParseIP(host)
+ if host != "localhost" && (ip == nil || !ip.IsLoopback()) {
+ return false
+ }
+ return true
+}
+
+// GET /settings -> current ReliabilitySettings as ms/json
+// PUT /settings -> partial reliabilityFile; only named fields change
+func (c *controlServer) handleSettings(w http.ResponseWriter, r *http.Request) {
+ switch r.Method {
+ case http.MethodGet:
+ c.mu.Lock()
+ s := c.mc.ReliabilitySettings()
+ c.mu.Unlock()
+ writeJSON(w, http.StatusOK, settingsToFile(s))
+ case http.MethodPut, http.MethodPost:
+ if !loopbackOrigin(r) {
+ writeJSON(w, http.StatusForbidden, map[string]string{"error": "non-loopback origin rejected"})
+ return
+ }
+ var f reliabilityFile
+ r.Body = http.MaxBytesReader(w, r.Body, 64<<10) // 64 KiB is plenty for a settings overlay
+ if err := json.NewDecoder(r.Body).Decode(&f); err != nil {
+ writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
+ return
+ }
+ // start from current so unset fields are preserved
+ c.mu.Lock()
+ merged, err := mergeReliabilitySettings(c.mc.ReliabilitySettings(), f)
+ if err != nil {
+ c.mu.Unlock()
+ writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
+ return
+ }
+ c.mc.SetReliabilitySettings(merged)
+ out := settingsToFile(c.mc.ReliabilitySettings())
+ c.mu.Unlock()
+ writeJSON(w, http.StatusOK, out)
+ default:
+ w.WriteHeader(http.StatusMethodNotAllowed)
+ }
+}
+
+// POST /actions {"action":"drop|stall|migrate|shuffle|probe-all|reset-metrics","clientId":"...","stalled":true}
+func (c *controlServer) handleActions(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ w.WriteHeader(http.StatusMethodNotAllowed)
+ return
+ }
+ if !loopbackOrigin(r) {
+ writeJSON(w, http.StatusForbidden, map[string]string{"error": "non-loopback origin rejected"})
+ return
+ }
+ var req struct {
+ Action string `json:"action"`
+ ClientId string `json:"clientId"`
+ Stalled bool `json:"stalled"`
+ }
+ r.Body = http.MaxBytesReader(w, r.Body, 64<<10)
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
+ return
+ }
+ switch req.Action {
+ case "quit":
+ if c.cancel != nil {
+ c.cancel()
+ writeJSON(w, http.StatusOK, map[string]any{"ok": true, "quitting": true})
+ } else {
+ writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "no cancel wired"})
+ }
+ case "reset-metrics":
+ c.mc.ResetReliabilityMetrics()
+ writeJSON(w, http.StatusOK, map[string]any{"ok": true})
+ case "shuffle":
+ c.mc.Shuffle()
+ writeJSON(w, http.StatusOK, map[string]any{"ok": true})
+ case "probe-all":
+ n := c.mc.ProbeAllExits()
+ writeJSON(w, http.StatusOK, map[string]any{"scheduled": n})
+ case "drop", "stall", "migrate":
+ if req.ClientId == "" {
+ writeJSON(w, http.StatusBadRequest, map[string]string{"error": "clientId required"})
+ return
+ }
+ id, err := connect.ParseId(req.ClientId)
+ if err != nil {
+ writeJSON(w, http.StatusBadRequest, map[string]string{"error": fmt.Sprintf("bad clientId: %v", err)})
+ return
+ }
+ switch req.Action {
+ case "drop":
+ ok := c.mc.DropExit(id)
+ writeJSON(w, http.StatusOK, map[string]any{"ok": ok})
+ case "stall":
+ ok := c.mc.StallExit(id, req.Stalled)
+ writeJSON(w, http.StatusOK, map[string]any{"ok": ok})
+ case "migrate":
+ n := c.mc.MigrateExit(id)
+ writeJSON(w, http.StatusOK, map[string]any{"moved": n})
+ }
+ default:
+ writeJSON(w, http.StatusBadRequest, map[string]string{"error": "unknown action"})
+ }
+}
+
+// GET /metrics -> reliability snapshot
+func (c *controlServer) handleMetrics(w http.ResponseWriter, r *http.Request) {
+ m := c.mc.ReliabilityMetrics()
+ writeJSON(w, http.StatusOK, m)
+}
+
+// GET /exits -> current exit table
+func (c *controlServer) handleExits(w http.ResponseWriter, r *http.Request) {
+ writeJSON(w, http.StatusOK, c.mc.Exits())
+}
+
+// statsResponse is the session-level readout the panel's header row shows:
+// elapsed time and bytes/packets up and down since the control server
+// started. Egress = traffic sent toward the network (up), Ingress = traffic
+// received back (down).
+type statsResponse struct {
+ ElapsedSeconds int64 `json:"elapsedSeconds"`
+ BytesUp int64 `json:"bytesUp"`
+ BytesDown int64 `json:"bytesDown"`
+ PacketsUp int64 `json:"packetsUp"`
+ PacketsDown int64 `json:"packetsDown"`
+}
+
+// GET /stats -> session elapsed + traffic counters
+func (c *controlServer) handleStats(w http.ResponseWriter, r *http.Request) {
+ ps := c.mc.PacketStats()
+ elapsed := time.Since(c.started) / time.Second
+ writeJSON(w, http.StatusOK, &statsResponse{
+ ElapsedSeconds: int64(elapsed),
+ BytesUp: int64(ps.RemoteEgressByteCount),
+ BytesDown: int64(ps.RemoteIngressByteCount),
+ PacketsUp: int64(ps.RemoteEgressPacketCount),
+ PacketsDown: int64(ps.RemoteIngressPacketCount),
+ })
+}
+
+// settingsToFile renders current settings as the json-friendly shape.
+func settingsToFile(s *connect.ReliabilitySettings) *reliabilityFile {
+ if s == nil {
+ return &reliabilityFile{}
+ }
+ ms := func(d time.Duration) *int64 {
+ v := int64(d / time.Millisecond)
+ return &v
+ }
+ b := func(v bool) *bool { return &v }
+ i := func(v int) *int { return &v }
+ return &reliabilityFile{
+ UdpTeardownSignal: b(s.UdpTeardownSignal),
+ QuicRebindOnExitLoss: b(s.QuicRebindOnExitLoss),
+ TcpCollapseMaxHoldMs: ms(s.TcpCollapseMaxHold),
+ SendStallTimeoutMs: ms(s.SendStallTimeout),
+ ClusterAffinityFallback: b(s.ClusterAffinityFallback),
+ ServerNameAffinityBridge: b(s.ServerNameAffinityBridge),
+ SequenceIdleTimeoutMs: ms(s.SequenceIdleTimeout),
+ TcpSequenceIdleTimeoutMs: ms(s.TcpSequenceIdleTimeout),
+ BlackholeReceiveTimeoutMs: ms(s.BlackholeReceiveTimeout),
+ MaxFlowsPerExit: i(s.MaxFlowsPerExit),
+ AffinityStickyPastCap: b(s.AffinityStickyPastCap),
+ QuarantineGroupFollow: b(s.QuarantineGroupFollow),
+ GroupFollowWindowMs: ms(s.GroupFollowWindow),
+ DialFailureRerace: b(s.DialFailureRerace),
+ UplinkStalenessGateMs: ms(s.UplinkStalenessGate),
+ SoftVerdictDemote: b(s.SoftVerdictDemote),
+ RemovalBudgetCount: i(s.RemovalBudgetCount),
+ RemovalBudgetWindowMs: ms(s.RemovalBudgetWindow),
+ StandingReserve: b(s.StandingReserve),
+ EffectiveTierSelection: b(s.EffectiveTierSelection),
+ MinBlackholeDestinations: i(s.MinBlackholeDestinations),
+ BlackholeLoadCorroboration: i(s.BlackholeLoadCorroboration),
+ ProviderProbe: b(s.ProviderProbe),
+ ProbeTimeoutMs: ms(s.ProbeTimeout),
+ ProbeSampleHostCount: i(s.ProbeSampleHostCount),
+ ProbeSilenceWarnStreak: i(s.ProbeSilenceWarnStreak),
+ EvaluationPoolMultiple: i(s.EvaluationPoolMultiple),
+ FormationPollTimeoutMs: ms(s.FormationPollTimeout),
+ BusyProbe: b(s.BusyProbe),
+ BusyProbeBudgetMs: ms(s.BusyProbeBudget),
+ SchedulerPauseToleranceMs: ms(s.SchedulerPauseTolerance),
+ SchedulerPauseRecoveryTimeoutMs: ms(s.SchedulerPauseRecoveryTimeout),
+ BlackholeConnectComparativeTimeoutMs: ms(s.BlackholeConnectComparativeTimeout),
+ HeartbeatIntervalMs: ms(s.HeartbeatInterval),
+ }
+}
+
+// mergeReliabilitySettings applies a partial file over the current settings.
+func mergeReliabilitySettings(cur *connect.ReliabilitySettings, f reliabilityFile) (*connect.ReliabilitySettings, error) {
+ if cur == nil {
+ cur = &connect.ReliabilitySettings{}
+ }
+ // reuse the loader by round-tripping through the file it would produce
+ merged := settingsToFile(cur)
+ // overwrite named fields (json decode already validated types)
+ if err := applyFileOver(merged, &f); err != nil {
+ return nil, err
+ }
+ // build back into a struct
+ return fileToSettings(merged)
+}
diff --git a/socks/coverage_test.go b/socks/coverage_test.go
new file mode 100644
index 0000000..47aff26
--- /dev/null
+++ b/socks/coverage_test.go
@@ -0,0 +1,415 @@
+package main
+
+import (
+ "context"
+ "encoding/json"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/golang-jwt/jwt/v5"
+ "github.com/urnetwork/connect"
+)
+
+// --- pure helpers in main.go ---
+
+func TestFmtDurationMillis(t *testing.T) {
+ cases := []struct {
+ ms int64
+ want string
+ }{
+ {0, "0ms"},
+ {500, "500ms"},
+ {1000, "1.0s"},
+ {1500, "1.5s"},
+ {65000, "65.0s"},
+ {-1, "-"},
+ }
+ for _, c := range cases {
+ if got := fmtDurationMillis(c.ms); got != c.want {
+ t.Errorf("fmtDurationMillis(%d) = %q, want %q", c.ms, got, c.want)
+ }
+ }
+}
+
+func TestExitLabel(t *testing.T) {
+ id, err := connect.ParseId("019fde39-e690-add0-ac88-e354a0d76d6a")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if got := exitLabel(id); got != "a0d76d6a" {
+ t.Errorf("exitLabel = %q, want last-8 chars", got)
+ }
+ // a short id must be returned whole (the len<=8 branch). connect.ParseId
+ // only accepts full-length ids, so test the string logic directly.
+ if got := exitLabelString("abcd"); got != "abcd" {
+ t.Errorf("exitLabelString(short) = %q, want whole id", got)
+ }
+ if got := exitLabelString("abcdefgh"); got != "abcdefgh" {
+ t.Errorf("exitLabelString(8) = %q, want whole id", got)
+ }
+}
+
+func TestFilterMapSlice(t *testing.T) {
+ nums := []int{1, 2, 3, 4}
+ evens := filter(nums, func(v int) bool { return v%2 == 0 })
+ if len(evens) != 2 || evens[0] != 2 || evens[1] != 4 {
+ t.Errorf("filter evens = %v, want [2 4]", evens)
+ }
+ doubled := mapSlice(nums, func(v int) int { return v * 2 })
+ if len(doubled) != 4 || doubled[3] != 8 {
+ t.Errorf("mapSlice doubled = %v, want [2 4 6 8]", doubled)
+ }
+}
+
+// parseByJwtClientId extracts the client_id claim from an (unverified) JWT.
+func TestParseByJwtClientId(t *testing.T) {
+ cid := "019fde39-e690-add0-ac88-e354a0d76d6a"
+ tok := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
+ "client_id": cid,
+ })
+ signed, err := tok.SignedString([]byte("test-secret"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ got, err := parseByJwtClientId(signed)
+ if err != nil {
+ t.Fatalf("parseByJwtClientId: %v", err)
+ }
+ if got.String() != cid {
+ t.Errorf("client id = %s, want %s", got.String(), cid)
+ }
+
+ // missing client_id claim -> error
+ bad := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{"sub": "x"})
+ badSigned, err := bad.SignedString([]byte("test-secret"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := parseByJwtClientId(badSigned); err == nil {
+ t.Error("expected error for JWT without client_id claim")
+ }
+}
+
+// --- getProviderSpec using the real sample fixture ---
+
+func loadSampleLocations(t *testing.T) *FindLocationsResult {
+ t.Helper()
+ data, err := os.ReadFile("findLocations.sample.json")
+ if err != nil {
+ t.Skipf("sample fixture not present: %v", err)
+ }
+ var res FindLocationsResult
+ if err := json.Unmarshal(data, &res); err != nil {
+ t.Fatalf("unmarshal fixture: %v", err)
+ }
+ return &res
+}
+
+func TestGetProviderSpecByCountry(t *testing.T) {
+ locs := loadSampleLocations(t)
+ specs, err := getProviderSpec(locs, "", "United States", "", "")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(specs) != 1 || specs[0].LocationId == nil {
+ t.Fatalf("country spec = %+v, want 1 spec with LocationId", specs)
+ }
+}
+
+func TestGetProviderSpecByCityCaseInsensitive(t *testing.T) {
+ locs := loadSampleLocations(t)
+ specs, err := getProviderSpec(locs, "los angeles", "", "", "")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(specs) != 1 || specs[0].LocationId == nil {
+ t.Fatalf("city spec = %+v, want 1 spec", specs)
+ }
+}
+
+func TestGetProviderSpecByProviderID(t *testing.T) {
+ locs := loadSampleLocations(t)
+ cid := "019fde39-e690-add0-ac88-e354a0d76d6a"
+ specs, err := getProviderSpec(locs, "", "", "", cid)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(specs) != 1 || specs[0].ClientId == nil || specs[0].ClientId.String() != cid {
+ t.Fatalf("provider-id spec = %+v, want client %s", specs, cid)
+ }
+}
+
+func TestGetProviderSpecBadProviderID(t *testing.T) {
+ locs := loadSampleLocations(t)
+ if _, err := getProviderSpec(locs, "", "", "", "not-an-id"); err == nil {
+ t.Fatal("expected error for malformed provider id")
+ }
+}
+
+func TestGetProviderSpecNoMatch(t *testing.T) {
+ locs := loadSampleLocations(t)
+ if _, err := getProviderSpec(locs, "", "Atlantis", "", ""); err == nil {
+ t.Fatal("expected error for unmatched country")
+ }
+}
+
+// --- loginWithAuthCode against a stubbed /auth/code-login ---
+
+func TestLoginWithAuthCodeSuccess(t *testing.T) {
+ expectedJwt := "eyJhbGciOiJIUzI1NiJ9.eyJjbGllbnRfaWQiOiIwMTlmZGUzOS1lNjkwLWFkZDAtYWM4OC1lMzU0YTBkNzZkNmEifQ.fake"
+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path != "/auth/code-login" {
+ t.Errorf("path = %s, want /auth/code-login", r.URL.Path)
+ }
+ var body map[string]any
+ json.NewDecoder(r.Body).Decode(&body)
+ if body["auth_code"] != "the-code" {
+ t.Errorf("auth_code = %v, want the-code", body["auth_code"])
+ }
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]any{"by_jwt": expectedJwt})
+ }))
+ defer srv.Close()
+
+ got, err := loginWithAuthCode(context.Background(), srv.URL, "the-code")
+ if err != nil {
+ t.Fatalf("loginWithAuthCode: %v", err)
+ }
+ if got != expectedJwt {
+ t.Errorf("jwt = %q, want %q", got, expectedJwt)
+ }
+}
+
+func TestLoginWithAuthCodeError(t *testing.T) {
+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]any{
+ "error": map[string]any{"message": "invalid auth code"},
+ })
+ }))
+ defer srv.Close()
+
+ if _, err := loginWithAuthCode(context.Background(), srv.URL, "bad"); err == nil {
+ t.Fatal("expected error for rejected auth code")
+ } else if !strings.Contains(err.Error(), "invalid auth code") {
+ t.Errorf("error = %v, want it to surface the backend message", err)
+ }
+}
+
+// --- control server error branches ---
+
+func TestControlSettingsBadJSON(t *testing.T) {
+ mc := &fakeMC{}
+ c, err := newControlServer("127.0.0.1:0", mc)
+ if err != nil {
+ t.Fatal(err)
+ }
+ srv := httptest.NewServer(c.server.Handler)
+ defer srv.Close()
+
+ req, _ := http.NewRequest("PUT", srv.URL+"/settings", strings.NewReader("{not json"))
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if resp.StatusCode != http.StatusBadRequest {
+ t.Errorf("PUT bad json status = %d, want 400", resp.StatusCode)
+ }
+ resp.Body.Close()
+}
+
+func TestControlActionsStallMigrate(t *testing.T) {
+ mc := &fakeMC{}
+ c, err := newControlServer("127.0.0.1:0", mc)
+ if err != nil {
+ t.Fatal(err)
+ }
+ srv := httptest.NewServer(c.server.Handler)
+ defer srv.Close()
+
+ cid := "019fde39-e690-add0-ac88-e354a0d76d6a"
+
+ resp, err := http.Post(srv.URL+"/actions", "application/json",
+ strings.NewReader(`{"action":"stall","clientId":"`+cid+`","stalled":true}`))
+ if err != nil {
+ t.Fatal(err)
+ }
+ resp.Body.Close()
+ if len(mc.stalled) != 1 || mc.stalled[0] != cid {
+ t.Errorf("stalled = %v, want [%s]", mc.stalled, cid)
+ }
+
+ resp, err = http.Post(srv.URL+"/actions", "application/json",
+ strings.NewReader(`{"action":"migrate","clientId":"`+cid+`"}`))
+ if err != nil {
+ t.Fatal(err)
+ }
+ resp.Body.Close()
+ if len(mc.migrated) != 1 || mc.migrated[0] != cid {
+ t.Errorf("migrated = %v, want [%s]", mc.migrated, cid)
+ }
+
+ // malformed client id -> 400
+ resp, err = http.Post(srv.URL+"/actions", "application/json",
+ strings.NewReader(`{"action":"drop","clientId":"nope"}`))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if resp.StatusCode != http.StatusBadRequest {
+ t.Errorf("drop bad clientId status = %d, want 400", resp.StatusCode)
+ }
+ resp.Body.Close()
+}
+
+// TestControlQuitAction verifies the quit action invokes the wired cancel.
+func TestControlQuitAction(t *testing.T) {
+ mc := &fakeMC{}
+ c, err := newControlServer("127.0.0.1:0", mc)
+ if err != nil {
+ t.Fatal(err)
+ }
+ cancelled := false
+ c.SetCancel(func() { cancelled = true })
+ srv := httptest.NewServer(c.server.Handler)
+ defer srv.Close()
+
+ resp, err := http.Post(srv.URL+"/actions", "application/json", strings.NewReader(`{"action":"quit"}`))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if resp.StatusCode != http.StatusOK {
+ t.Fatalf("POST quit status = %d, want 200", resp.StatusCode)
+ }
+ resp.Body.Close()
+ if !cancelled {
+ t.Error("quit action did not invoke the session cancel")
+ }
+}
+
+// --- /stats session readout ---
+
+func TestControlStatsEndpoint(t *testing.T) {
+ mc := &fakeMC{}
+ c, err := newControlServer("127.0.0.1:0", mc)
+ if err != nil {
+ t.Fatal(err)
+ }
+ srv := httptest.NewServer(c.server.Handler)
+ defer srv.Close()
+
+ resp, err := http.Get(srv.URL + "/stats")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != http.StatusOK {
+ t.Fatalf("GET /stats status = %d, want 200", resp.StatusCode)
+ }
+ var st statsResponse
+ if err := json.NewDecoder(resp.Body).Decode(&st); err != nil {
+ t.Fatal(err)
+ }
+ if st.BytesUp != 1024 || st.BytesDown != 2048 {
+ t.Errorf("stats bytes = %d up / %d down, want 1024/2048", st.BytesUp, st.BytesDown)
+ }
+ if st.PacketsUp != 10 || st.PacketsDown != 20 {
+ t.Errorf("stats packets = %d up / %d down, want 10/20", st.PacketsUp, st.PacketsDown)
+ }
+ if st.ElapsedSeconds < 0 {
+ t.Errorf("elapsedSeconds = %d, want >= 0", st.ElapsedSeconds)
+ }
+}
+
+// --- serve() shutdown on context cancel ---
+
+func TestControlServerServeShutdown(t *testing.T) {
+ mc := &fakeMC{}
+ // port 0 = ephemeral; we only verify the goroutine exits on ctx cancel
+ c, err := newControlServer("127.0.0.1:0", mc)
+ if err != nil {
+ t.Fatal(err)
+ }
+ ctx, cancel := context.WithCancel(context.Background())
+ done := make(chan struct{})
+ go func() {
+ c.serve(ctx)
+ close(done)
+ }()
+ time.Sleep(50 * time.Millisecond)
+ cancel()
+ select {
+ case <-done:
+ // clean exit
+ case <-time.After(3 * time.Second):
+ t.Fatal("serve did not return within 3s of ctx cancel")
+ }
+}
+
+// --- printReliabilityMetrics output format ---
+
+func TestPrintReliabilityMetrics(t *testing.T) {
+ mc := &fakeMC{
+ settings: &connect.ReliabilitySettings{},
+ metrics: &connect.ReliabilityMetricsSnapshot{
+ FlowsOpened: 12,
+ DialFailuresIntercepted: 3,
+ FlowsReraced: 2,
+ ExitLossEvents: 1,
+ FlowsLostToExit: 4,
+ MaxFlowsLostInOneEvent: 4,
+ MeanFlowsLostPerExitLoss: 4.0,
+ RecoveryCount: 1,
+ RecoveryMeanNanos: 500 * 1e6,
+ RecoveryMaxNanos: 1500 * 1e6,
+ ProbesSent: 10,
+ ProbesAnswered: 8,
+ ProvidersQualified: 3,
+ VerdictsHeldUplinkStale: 2,
+ VerdictsHeldTransportDown: 1,
+ },
+ exits: []*connect.ExitInfo{
+ {ClientId: mustParseId(t, "019fde39-e690-add0-ac88-e354a0d76d6a"), FlowCount: 5, Tier: 1, EffectiveTier: 1, Proven: true},
+ },
+ }
+
+ // capture stdout
+ old := os.Stdout
+ r, w, _ := os.Pipe()
+ os.Stdout = w
+ printReliabilityMetrics(mc)
+ w.Close()
+ os.Stdout = old
+ buf, _ := io.ReadAll(r)
+ r.Close()
+ out := string(buf)
+
+ for _, want := range []string{
+ "reliability metrics",
+ "flows_opened=12",
+ "dial_failures_intercepted=3",
+ "exit_loss_events=1",
+ "mean_flows_lost_per_exit_loss=4.00",
+ "recovery_mean=500ms",
+ "probes_sent=10",
+ "verdicts_held_uplink_stale=2",
+ "exits:",
+ } {
+ if !strings.Contains(out, want) {
+ t.Errorf("metrics output missing %q; got:\n%s", want, out)
+ }
+ }
+}
+
+func mustParseId(t *testing.T, s string) connect.Id {
+ t.Helper()
+ id, err := connect.ParseId(s)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return id
+}
diff --git a/socks/docs/control-panel.png b/socks/docs/control-panel.png
new file mode 100644
index 0000000..f2cac21
Binary files /dev/null and b/socks/docs/control-panel.png differ
diff --git a/socks/main.go b/socks/main.go
index 513dba7..90992d3 100644
--- a/socks/main.go
+++ b/socks/main.go
@@ -3,13 +3,14 @@ package main
import (
"context"
"errors"
+ "flag"
"fmt"
"net"
// "net/netip"
- "flag"
"os"
"os/signal"
"slices"
+ "strconv"
"strings"
"syscall"
"time"
@@ -40,15 +41,22 @@ func initGlog() {
func main() {
cfg := struct {
- addr string
- apiURL string
- platformURL string
- userAuth string
- password string
- providerID string
- city string
- country string
- region string
+ addr string
+ network string
+ apiURL string
+ platformURL string
+ userAuth string
+ password string
+ authCode string
+ jwt string
+ providerID string
+ city string
+ country string
+ region string
+ metricsEveryMs int
+ resetMetrics bool
+ reliability string
+ control string
}{}
usage := `socksproxy - dev socks5 proxy over urnetwork.
@@ -57,14 +65,21 @@ Usage:
Options:
--addr= socks5 server address (env ADDR, default 127.0.0.1:9999)
- --api-url= api url (env API_URL, default https://api.bringyour.com)
- --platform-url= platform url (env PLATFORM_URL, default wss://connect.bringyour.com)
- --user-auth= user auth, required (env USER_AUTH)
- --password= password, required (env PASSWORD)
+ --network= preset endpoints: main or beta (env NETWORK, default main)
+ --api-url= api url (env API_URL, default from --network)
+ --platform-url= platform url (env PLATFORM_URL, default from --network)
+ --user-auth= user auth (env USER_AUTH)
+ --password= password (env PASSWORD)
+ --auth-code= auth code login (env AUTH_CODE) — preferred for beta/test networks
+ --jwt= existing network jwt (env JWT), skips login entirely
--provider-id= provider id (env PROVIDER_ID)
--city= city (env CITY)
--country= country (env COUNTRY)
--region= region (env REGION)
+ --metrics-every= print reliability metrics + exit table every N ms (env METRICS_EVERY, 0 = off)
+ --reset-metrics zero the reliability counters at startup (env RESET_METRICS=1)
+ --reliability= json file of reliability settings applied at startup (env RELIABILITY)
+ --control= local control http server (env CONTROL, e.g. 127.0.0.1:9998, empty = off)
-h --help show this help.`
opts, err := docopt.ParseArgs(usage, os.Args[1:], Version)
@@ -90,16 +105,51 @@ Options:
return def
}
cfg.addr = pick("--addr", "ADDR", "127.0.0.1:9999")
- cfg.apiURL = pick("--api-url", "API_URL", "https://api.bringyour.com")
- cfg.platformURL = pick("--platform-url", "PLATFORM_URL", "wss://connect.bringyour.com")
+ cfg.network = pick("--network", "NETWORK", "main")
+ switch cfg.network {
+ case "main":
+ cfg.apiURL = pick("--api-url", "API_URL", "https://api.bringyour.com")
+ cfg.platformURL = pick("--platform-url", "PLATFORM_URL", "wss://connect.bringyour.com")
+ case "beta":
+ cfg.apiURL = pick("--api-url", "API_URL", "https://api.beta-test.net")
+ cfg.platformURL = pick("--platform-url", "PLATFORM_URL", "wss://connect.beta-test.net")
+ default:
+ fmt.Fprintf(os.Stderr, "unknown --network %q: expected main or beta\n", cfg.network)
+ os.Exit(1)
+ }
+ // explicit --api-url / --platform-url still override the preset, and the
+ // env vars follow -- pick() above already gave flags priority over env
cfg.userAuth = pick("--user-auth", "USER_AUTH", "")
cfg.password = pick("--password", "PASSWORD", "")
+ cfg.authCode = pick("--auth-code", "AUTH_CODE", "")
+ cfg.jwt = pick("--jwt", "JWT", "")
cfg.providerID = pick("--provider-id", "PROVIDER_ID", "")
cfg.city = pick("--city", "CITY", "")
cfg.country = pick("--country", "COUNTRY", "")
cfg.region = pick("--region", "REGION", "")
- if cfg.userAuth == "" || cfg.password == "" {
- fmt.Fprintln(os.Stderr, "--user-auth and --password are required (or set USER_AUTH / PASSWORD)")
+
+ if v := optStr("--metrics-every"); v != "" {
+ ms, err := strconv.Atoi(v)
+ if err != nil || ms <= 0 {
+ fmt.Fprintln(os.Stderr, "--metrics-every must be a positive millisecond value")
+ os.Exit(1)
+ }
+ cfg.metricsEveryMs = ms
+ } else if v := os.Getenv("METRICS_EVERY"); v != "" {
+ ms, err := strconv.Atoi(v)
+ if err != nil || ms <= 0 {
+ fmt.Fprintln(os.Stderr, "--metrics-every must be a positive millisecond value")
+ os.Exit(1)
+ }
+ cfg.metricsEveryMs = ms
+ }
+ resetMetrics, _ := opts.Bool("--reset-metrics")
+ cfg.resetMetrics = resetMetrics || os.Getenv("RESET_METRICS") == "1"
+ cfg.reliability = pick("--reliability", "RELIABILITY", "")
+ cfg.control = pick("--control", "CONTROL", "")
+
+ if cfg.jwt == "" && cfg.authCode == "" && (cfg.userAuth == "" || cfg.password == "") {
+ fmt.Fprintln(os.Stderr, "provide --jwt, --auth-code, or --user-auth + --password (or set JWT / AUTH_CODE / USER_AUTH + PASSWORD)")
os.Exit(1)
}
@@ -108,7 +158,16 @@ Options:
run := func() error {
- jwt, err := login(ctx, cfg.apiURL, cfg.userAuth, cfg.password)
+ var jwt string
+ var err error
+ switch {
+ case cfg.jwt != "":
+ jwt = cfg.jwt
+ case cfg.authCode != "":
+ jwt, err = loginWithAuthCode(ctx, cfg.apiURL, cfg.authCode)
+ default:
+ jwt, err = login(ctx, cfg.apiURL, cfg.userAuth, cfg.password)
+ }
if err != nil {
return fmt.Errorf("login failed: %w", err)
}
@@ -191,6 +250,65 @@ Options:
protocol.ProvideMode_Network,
)
+ if cfg.reliability != "" {
+ // merge over the current (default) settings so unspecified fields
+ // keep their defaults instead of being zeroed
+ f, err := parseReliabilityFile(cfg.reliability)
+ if err != nil {
+ return fmt.Errorf("reliability settings: %w", err)
+ }
+ merged := settingsToFile(mc.ReliabilitySettings())
+ if err := applyFileOver(merged, f); err != nil {
+ return fmt.Errorf("reliability settings: %w", err)
+ }
+ settings, err := fileToSettings(merged)
+ if err != nil {
+ return fmt.Errorf("reliability settings: %w", err)
+ }
+ mc.SetReliabilitySettings(settings)
+ fmt.Printf("reliability settings loaded from %s\n", cfg.reliability)
+ }
+
+ if cfg.control != "" {
+ // the control server is unauthenticated and can change settings,
+ // drop exits, and migrate flows -- refuse non-loopback binds
+ ok, err := controlAddrIsLoopback(cfg.control)
+ if err != nil {
+ return fmt.Errorf("control address: %w", err)
+ }
+ if !ok {
+ return fmt.Errorf("control address %q must be loopback; the control server is unauthenticated", cfg.control)
+ }
+ server, err := newControlServer(cfg.control, mc)
+ if err != nil {
+ return fmt.Errorf("control server: %w", err)
+ }
+ server.SetCancel(stop)
+ go server.serve(ctx)
+ fmt.Printf("control server listening on %s\n", cfg.control)
+ }
+
+ if cfg.resetMetrics {
+ mc.ResetReliabilityMetrics()
+ fmt.Println("reliability metrics reset")
+ }
+
+ if cfg.metricsEveryMs > 0 {
+ go func() {
+ ticker := time.NewTicker(time.Duration(cfg.metricsEveryMs) * time.Millisecond)
+ defer ticker.Stop()
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ case <-ticker.C:
+ printReliabilityMetrics(mc)
+ }
+ }
+ }()
+ fmt.Printf("reliability metrics every %dms\n", cfg.metricsEveryMs)
+ }
+
source := connect.SourceId(clientID)
go func() {
@@ -237,6 +355,104 @@ Options:
}
}
+// printReliabilityMetrics writes the reliability counters and the current
+// exit table to stdout. It is the console equivalent of the android
+// developer screen's measurements block: flows opened, dial failures
+// intercepted/reraced, blast radius (mean + worst exit loss), recovery
+// mean/max/pending, probes sent/answered/qualified, busy probes, verdicts
+// held, removals deferred, rebinds, and one row per exit (warning /
+// quarantine flags, flow count, dial failures, tier vs effective tier,
+// proven status). Called on the --metrics-every ticker.
+func printReliabilityMetrics(mc mcControl) {
+ m := mc.ReliabilityMetrics()
+ if m == nil {
+ return
+ }
+ fmt.Printf("\n--- reliability metrics %s ---\n", time.Now().Format("15:04:05"))
+ fmt.Printf("flows_opened=%d dial_failures_intercepted=%d flows_reraced=%d\n", m.FlowsOpened, m.DialFailuresIntercepted, m.FlowsReraced)
+ fmt.Printf("exit_loss_events=%d flows_lost_to_exit=%d max_flows_lost_one_event=%d mean_flows_lost_per_exit_loss=%.2f\n",
+ m.ExitLossEvents, m.FlowsLostToExit, m.MaxFlowsLostInOneEvent, m.MeanFlowsLostPerExitLoss)
+ fmt.Printf("recovery_count=%d recovery_missed=%d recovery_mean=%s recovery_max=%s recovery_pending=%d\n",
+ m.RecoveryCount, m.RecoveryMissed, fmtDurationMillis(m.RecoveryMeanNanos/1e6), fmtDurationMillis(m.RecoveryMaxNanos/1e6), m.RecoveryPending)
+ fmt.Printf("probes_sent=%d probes_answered=%d providers_qualified=%d busy_probes_sent=%d busy_probes_acquitted=%d scheduler_pauses=%d\n",
+ m.ProbesSent, m.ProbesAnswered, m.ProvidersQualified, m.BusyProbesSent, m.BusyProbesAcquitted, m.SchedulerPausesDetected)
+ fmt.Printf("verdicts_held_uplink_stale=%d verdicts_held_transport_down=%d removals_deferred=%d\n",
+ m.VerdictsHeldUplinkStale, m.VerdictsHeldTransportDown, m.RemovalsDeferred)
+ fmt.Printf("flows_rebound=%d rebinds_accepted=%d rebinds_redialed=%d groups_followed=%d groups_scattered=%d\n",
+ m.FlowsRebound, m.RebindsAccepted, m.RebindsRedialed, m.GroupsFollowed, m.GroupsScattered)
+
+ exits := mc.Exits()
+ if len(exits) == 0 {
+ fmt.Println("exits: none")
+ return
+ }
+ fmt.Println("exits:")
+ for _, e := range exits {
+ flags := ""
+ if e.Warning {
+ flags += "W"
+ }
+ if e.Quarantined {
+ flags += "Q"
+ }
+ if e.Done {
+ flags += "D"
+ }
+ if flags == "" {
+ flags = "-"
+ }
+ fmt.Printf(" %s %s flows=%d dial_failures=%d tier=%d effective_tier=%d proven=%v\n",
+ flags, exitLabel(e.ClientId), e.FlowCount, e.DialFailureCount, e.Tier, e.EffectiveTier, e.Proven)
+ }
+}
+
+// fmtDurationMillis renders a millisecond duration compactly: raw ms below a
+// second, one-decimal seconds above. Negative values (an unset clock) render
+// as "-" so "never measured" reads differently from "0ms".
+func fmtDurationMillis(ms int64) string {
+ if ms < 0 {
+ return "-"
+ }
+ if ms < 1000 {
+ return fmt.Sprintf("%dms", ms)
+ }
+ return fmt.Sprintf("%.1fs", float64(ms)/1000.0)
+}
+
+// exitLabel renders a short suffix of the client id so rows are
+// distinguishable. Client ids are ULIDs -- the leading characters encode
+// creation time, so channels opened milliseconds apart look identical; the
+// random component is in the tail.
+func exitLabel(id connect.Id) string {
+ return exitLabelString(id.String())
+}
+
+func exitLabelString(s string) string {
+ if len(s) <= 8 {
+ return s
+ }
+ return s[len(s)-8:]
+}
+
+// controlAddrIsLoopback reports whether addr is a loopback bind address
+// (localhost, 127.0.0.1, or ::1) with a port. The control server is
+// unauthenticated, so anything else must be refused.
+func controlAddrIsLoopback(addr string) (bool, error) {
+ host, _, err := net.SplitHostPort(addr)
+ if err != nil {
+ return false, err
+ }
+ ip := net.ParseIP(host)
+ if host != "localhost" && (ip == nil || !ip.IsLoopback()) {
+ return false, nil
+ }
+ return true, nil
+}
+
+// getProviderSpec resolves the provider-locations result to a ProviderSpec
+// by, in order: exact provider id, city, country, region. The matching is
+// case-insensitive. An unmatched location yields a help listing of every
+// country/region/city available.
func getProviderSpec(
locations *FindLocationsResult,
city string,
@@ -400,6 +616,29 @@ func login(ctx context.Context, apiURL, userAuth, password string) (string, erro
}
+// loginWithAuthCode redeems an auth code via POST /auth/code-login and
+// returns the network by_jwt. This is the auth path for beta/test networks
+// that have no password accounts -- the same flow the provider CLI uses.
+func loginWithAuthCode(ctx context.Context, apiURL, authCode string) (string, error) {
+ api := connect.NewBringYourApi(
+ ctx,
+ connect.NewClientStrategyWithDefaults(ctx),
+ apiURL,
+ )
+
+ res, err := api.AuthCodeLoginSync(&connect.AuthCodeLoginArgs{
+ AuthCode: authCode,
+ })
+ if err != nil {
+ return "", err
+ }
+ if res.Error != nil {
+ return "", errors.New(res.Error.Message)
+ }
+
+ return res.ByJwt, nil
+}
+
func getProviderLocations(ctx context.Context, apiURL string, jwt string) (*FindLocationsResult, error) {
strategy := connect.NewClientStrategyWithDefaults(ctx)
@@ -467,6 +706,8 @@ func authNetworkClient(ctx context.Context, apiURL, jwt string, req *connect.Aut
return res.ByClientJwt, nil
}
+// parseByJwtClientId extracts the client_id claim from a by_jwt token without
+// verifying its signature (the network already authenticated the token).
func parseByJwtClientId(byJwt string) (connect.Id, error) {
claims := gojwt.MapClaims{}
gojwt.NewParser().ParseUnverified(byJwt, claims)
diff --git a/socks/main_test.go b/socks/main_test.go
index 7332337..c219674 100644
--- a/socks/main_test.go
+++ b/socks/main_test.go
@@ -22,7 +22,7 @@ import (
// exercise the real, current text/literals rather than a copy that could
// drift from it.
-// usageFromSource extracts the literal `usage := \`...\`` doc string from
+// usageFromSource extracts the literal `usage := \`...\` doc string from
// main.go.
func usageFromSource(t *testing.T) string {
t.Helper()
@@ -134,4 +134,4 @@ func TestDevSocksProxyHasNoValidUserOverride(t *testing.T) {
"ConnectDialWithRequest; the dev \"any credentials accepted\" override should "+
"have been removed in favor of the library's nil-ValidUser no-auth default:\n%s", between)
}
-}
\ No newline at end of file
+}
diff --git a/socks/reliability_file.go b/socks/reliability_file.go
new file mode 100644
index 0000000..f24ebee
--- /dev/null
+++ b/socks/reliability_file.go
@@ -0,0 +1,302 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "time"
+
+ "github.com/urnetwork/connect"
+)
+
+// reliabilityFile mirrors connect.ReliabilitySettings with json-friendly
+// fields. Durations are milliseconds (same convention as the android dev
+// screen's presets); a nil field is untouched (keeps the current/default
+// value), while an explicit zero value is applied.
+type reliabilityFile struct {
+ UdpTeardownSignal *bool `json:"udpTeardownSignal"`
+ QuicRebindOnExitLoss *bool `json:"quicRebindOnExitLoss"`
+ TcpCollapseMaxHoldMs *int64 `json:"tcpCollapseMaxHoldMs"`
+ SendStallTimeoutMs *int64 `json:"sendStallTimeoutMs"`
+ ClusterAffinityFallback *bool `json:"clusterAffinityFallback"`
+ ServerNameAffinityBridge *bool `json:"serverNameAffinityBridge"`
+ SequenceIdleTimeoutMs *int64 `json:"sequenceIdleTimeoutMs"`
+ TcpSequenceIdleTimeoutMs *int64 `json:"tcpSequenceIdleTimeoutMs"`
+ BlackholeReceiveTimeoutMs *int64 `json:"blackholeReceiveTimeoutMs"`
+ MaxFlowsPerExit *int `json:"maxFlowsPerExit"`
+ AffinityStickyPastCap *bool `json:"affinityStickyPastCap"`
+ QuarantineGroupFollow *bool `json:"quarantineGroupFollow"`
+ GroupFollowWindowMs *int64 `json:"groupFollowWindowMs"`
+ DialFailureRerace *bool `json:"dialFailureRerace"`
+ UplinkStalenessGateMs *int64 `json:"uplinkStalenessGateMs"`
+ SoftVerdictDemote *bool `json:"softVerdictDemote"`
+ RemovalBudgetCount *int `json:"removalBudgetCount"`
+ RemovalBudgetWindowMs *int64 `json:"removalBudgetWindowMs"`
+ StandingReserve *bool `json:"standingReserve"`
+ EffectiveTierSelection *bool `json:"effectiveTierSelection"`
+ MinBlackholeDestinations *int `json:"minBlackholeDestinations"`
+ BlackholeLoadCorroboration *int `json:"blackholeLoadCorroboration"`
+ ProviderProbe *bool `json:"providerProbe"`
+ ProbeTimeoutMs *int64 `json:"probeTimeoutMs"`
+ ProbeSampleHostCount *int `json:"probeSampleHostCount"`
+ ProbeSilenceWarnStreak *int `json:"probeSilenceWarnStreak"`
+ EvaluationPoolMultiple *int `json:"evaluationPoolMultiple"`
+ FormationPollTimeoutMs *int64 `json:"formationPollTimeoutMs"`
+ BusyProbe *bool `json:"busyProbe"`
+ BusyProbeBudgetMs *int64 `json:"busyProbeBudgetMs"`
+ SchedulerPauseToleranceMs *int64 `json:"schedulerPauseToleranceMs"`
+ SchedulerPauseRecoveryTimeoutMs *int64 `json:"schedulerPauseRecoveryTimeoutMs"`
+ BlackholeConnectComparativeTimeoutMs *int64 `json:"blackholeConnectComparativeTimeoutMs"`
+ HeartbeatIntervalMs *int64 `json:"heartbeatIntervalMs"`
+}
+
+// parseReliabilityFile reads and unmarshals a reliability settings file into
+// its json shape. Missing fields stay nil (untouched); an explicit zero value
+// is applied. See reliabilityFile.
+func parseReliabilityFile(path string) (*reliabilityFile, error) {
+ data, err := os.ReadFile(path)
+ if err != nil {
+ return nil, err
+ }
+ var f reliabilityFile
+ if err := json.Unmarshal(data, &f); err != nil {
+ return nil, fmt.Errorf("parse %s: %w", path, err)
+ }
+ return &f, nil
+}
+
+// loadReliabilitySettingsFile reads a settings file and converts it to a
+// connect.ReliabilitySettings. Fields absent from the file are left at their
+// zero value here; callers that need to preserve engine defaults must merge
+// over the current settings first (see applyFileOver + settingsToFile).
+func loadReliabilitySettingsFile(path string) (*connect.ReliabilitySettings, error) {
+ f, err := parseReliabilityFile(path)
+ if err != nil {
+ return nil, err
+ }
+ return fileToSettings(f)
+}
+
+// fileToSettings converts the json shape to a connect.ReliabilitySettings.
+// Durations are milliseconds in the file, time.Duration in the struct; nil
+// fields map to zero values in the struct.
+func fileToSettings(f *reliabilityFile) (*connect.ReliabilitySettings, error) {
+ if f == nil {
+ return &connect.ReliabilitySettings{}, nil
+ }
+ settings := &connect.ReliabilitySettings{}
+ if f.UdpTeardownSignal != nil {
+ settings.UdpTeardownSignal = *f.UdpTeardownSignal
+ }
+ if f.QuicRebindOnExitLoss != nil {
+ settings.QuicRebindOnExitLoss = *f.QuicRebindOnExitLoss
+ }
+ if f.TcpCollapseMaxHoldMs != nil {
+ settings.TcpCollapseMaxHold = time.Duration(*f.TcpCollapseMaxHoldMs) * time.Millisecond
+ }
+ if f.SendStallTimeoutMs != nil {
+ settings.SendStallTimeout = time.Duration(*f.SendStallTimeoutMs) * time.Millisecond
+ }
+ if f.ClusterAffinityFallback != nil {
+ settings.ClusterAffinityFallback = *f.ClusterAffinityFallback
+ }
+ if f.ServerNameAffinityBridge != nil {
+ settings.ServerNameAffinityBridge = *f.ServerNameAffinityBridge
+ }
+ if f.SequenceIdleTimeoutMs != nil {
+ settings.SequenceIdleTimeout = time.Duration(*f.SequenceIdleTimeoutMs) * time.Millisecond
+ }
+ if f.TcpSequenceIdleTimeoutMs != nil {
+ settings.TcpSequenceIdleTimeout = time.Duration(*f.TcpSequenceIdleTimeoutMs) * time.Millisecond
+ }
+ if f.BlackholeReceiveTimeoutMs != nil {
+ settings.BlackholeReceiveTimeout = time.Duration(*f.BlackholeReceiveTimeoutMs) * time.Millisecond
+ }
+ if f.MaxFlowsPerExit != nil {
+ settings.MaxFlowsPerExit = *f.MaxFlowsPerExit
+ }
+ if f.AffinityStickyPastCap != nil {
+ settings.AffinityStickyPastCap = *f.AffinityStickyPastCap
+ }
+ if f.QuarantineGroupFollow != nil {
+ settings.QuarantineGroupFollow = *f.QuarantineGroupFollow
+ }
+ if f.GroupFollowWindowMs != nil {
+ settings.GroupFollowWindow = time.Duration(*f.GroupFollowWindowMs) * time.Millisecond
+ }
+ if f.DialFailureRerace != nil {
+ settings.DialFailureRerace = *f.DialFailureRerace
+ }
+ if f.UplinkStalenessGateMs != nil {
+ settings.UplinkStalenessGate = time.Duration(*f.UplinkStalenessGateMs) * time.Millisecond
+ }
+ if f.SoftVerdictDemote != nil {
+ settings.SoftVerdictDemote = *f.SoftVerdictDemote
+ }
+ if f.RemovalBudgetCount != nil {
+ settings.RemovalBudgetCount = *f.RemovalBudgetCount
+ }
+ if f.RemovalBudgetWindowMs != nil {
+ settings.RemovalBudgetWindow = time.Duration(*f.RemovalBudgetWindowMs) * time.Millisecond
+ }
+ if f.StandingReserve != nil {
+ settings.StandingReserve = *f.StandingReserve
+ }
+ if f.EffectiveTierSelection != nil {
+ settings.EffectiveTierSelection = *f.EffectiveTierSelection
+ }
+ if f.MinBlackholeDestinations != nil {
+ settings.MinBlackholeDestinations = *f.MinBlackholeDestinations
+ }
+ if f.BlackholeLoadCorroboration != nil {
+ settings.BlackholeLoadCorroboration = *f.BlackholeLoadCorroboration
+ }
+ if f.ProviderProbe != nil {
+ settings.ProviderProbe = *f.ProviderProbe
+ }
+ if f.ProbeTimeoutMs != nil {
+ settings.ProbeTimeout = time.Duration(*f.ProbeTimeoutMs) * time.Millisecond
+ }
+ if f.ProbeSampleHostCount != nil {
+ settings.ProbeSampleHostCount = *f.ProbeSampleHostCount
+ }
+ if f.ProbeSilenceWarnStreak != nil {
+ settings.ProbeSilenceWarnStreak = *f.ProbeSilenceWarnStreak
+ }
+ if f.EvaluationPoolMultiple != nil {
+ settings.EvaluationPoolMultiple = *f.EvaluationPoolMultiple
+ }
+ if f.FormationPollTimeoutMs != nil {
+ settings.FormationPollTimeout = time.Duration(*f.FormationPollTimeoutMs) * time.Millisecond
+ }
+ if f.BusyProbe != nil {
+ settings.BusyProbe = *f.BusyProbe
+ }
+ if f.BusyProbeBudgetMs != nil {
+ settings.BusyProbeBudget = time.Duration(*f.BusyProbeBudgetMs) * time.Millisecond
+ }
+ if f.SchedulerPauseToleranceMs != nil {
+ settings.SchedulerPauseTolerance = time.Duration(*f.SchedulerPauseToleranceMs) * time.Millisecond
+ }
+ if f.SchedulerPauseRecoveryTimeoutMs != nil {
+ settings.SchedulerPauseRecoveryTimeout = time.Duration(*f.SchedulerPauseRecoveryTimeoutMs) * time.Millisecond
+ }
+ if f.BlackholeConnectComparativeTimeoutMs != nil {
+ settings.BlackholeConnectComparativeTimeout = time.Duration(*f.BlackholeConnectComparativeTimeoutMs) * time.Millisecond
+ }
+ if f.HeartbeatIntervalMs != nil {
+ settings.HeartbeatInterval = time.Duration(*f.HeartbeatIntervalMs) * time.Millisecond
+ }
+
+ return settings, nil
+}
+
+// applyFileOver copies every non-nil field of src onto dst (both json shapes).
+func applyFileOver(dst, src *reliabilityFile) error {
+ if src == nil {
+ return nil
+ }
+ if src.UdpTeardownSignal != nil {
+ dst.UdpTeardownSignal = src.UdpTeardownSignal
+ }
+ if src.QuicRebindOnExitLoss != nil {
+ dst.QuicRebindOnExitLoss = src.QuicRebindOnExitLoss
+ }
+ if src.TcpCollapseMaxHoldMs != nil {
+ dst.TcpCollapseMaxHoldMs = src.TcpCollapseMaxHoldMs
+ }
+ if src.SendStallTimeoutMs != nil {
+ dst.SendStallTimeoutMs = src.SendStallTimeoutMs
+ }
+ if src.ClusterAffinityFallback != nil {
+ dst.ClusterAffinityFallback = src.ClusterAffinityFallback
+ }
+ if src.ServerNameAffinityBridge != nil {
+ dst.ServerNameAffinityBridge = src.ServerNameAffinityBridge
+ }
+ if src.SequenceIdleTimeoutMs != nil {
+ dst.SequenceIdleTimeoutMs = src.SequenceIdleTimeoutMs
+ }
+ if src.TcpSequenceIdleTimeoutMs != nil {
+ dst.TcpSequenceIdleTimeoutMs = src.TcpSequenceIdleTimeoutMs
+ }
+ if src.BlackholeReceiveTimeoutMs != nil {
+ dst.BlackholeReceiveTimeoutMs = src.BlackholeReceiveTimeoutMs
+ }
+ if src.MaxFlowsPerExit != nil {
+ dst.MaxFlowsPerExit = src.MaxFlowsPerExit
+ }
+ if src.AffinityStickyPastCap != nil {
+ dst.AffinityStickyPastCap = src.AffinityStickyPastCap
+ }
+ if src.QuarantineGroupFollow != nil {
+ dst.QuarantineGroupFollow = src.QuarantineGroupFollow
+ }
+ if src.GroupFollowWindowMs != nil {
+ dst.GroupFollowWindowMs = src.GroupFollowWindowMs
+ }
+ if src.DialFailureRerace != nil {
+ dst.DialFailureRerace = src.DialFailureRerace
+ }
+ if src.UplinkStalenessGateMs != nil {
+ dst.UplinkStalenessGateMs = src.UplinkStalenessGateMs
+ }
+ if src.SoftVerdictDemote != nil {
+ dst.SoftVerdictDemote = src.SoftVerdictDemote
+ }
+ if src.RemovalBudgetCount != nil {
+ dst.RemovalBudgetCount = src.RemovalBudgetCount
+ }
+ if src.RemovalBudgetWindowMs != nil {
+ dst.RemovalBudgetWindowMs = src.RemovalBudgetWindowMs
+ }
+ if src.StandingReserve != nil {
+ dst.StandingReserve = src.StandingReserve
+ }
+ if src.EffectiveTierSelection != nil {
+ dst.EffectiveTierSelection = src.EffectiveTierSelection
+ }
+ if src.MinBlackholeDestinations != nil {
+ dst.MinBlackholeDestinations = src.MinBlackholeDestinations
+ }
+ if src.BlackholeLoadCorroboration != nil {
+ dst.BlackholeLoadCorroboration = src.BlackholeLoadCorroboration
+ }
+ if src.ProviderProbe != nil {
+ dst.ProviderProbe = src.ProviderProbe
+ }
+ if src.ProbeTimeoutMs != nil {
+ dst.ProbeTimeoutMs = src.ProbeTimeoutMs
+ }
+ if src.ProbeSampleHostCount != nil {
+ dst.ProbeSampleHostCount = src.ProbeSampleHostCount
+ }
+ if src.ProbeSilenceWarnStreak != nil {
+ dst.ProbeSilenceWarnStreak = src.ProbeSilenceWarnStreak
+ }
+ if src.EvaluationPoolMultiple != nil {
+ dst.EvaluationPoolMultiple = src.EvaluationPoolMultiple
+ }
+ if src.FormationPollTimeoutMs != nil {
+ dst.FormationPollTimeoutMs = src.FormationPollTimeoutMs
+ }
+ if src.BusyProbe != nil {
+ dst.BusyProbe = src.BusyProbe
+ }
+ if src.BusyProbeBudgetMs != nil {
+ dst.BusyProbeBudgetMs = src.BusyProbeBudgetMs
+ }
+ if src.SchedulerPauseToleranceMs != nil {
+ dst.SchedulerPauseToleranceMs = src.SchedulerPauseToleranceMs
+ }
+ if src.SchedulerPauseRecoveryTimeoutMs != nil {
+ dst.SchedulerPauseRecoveryTimeoutMs = src.SchedulerPauseRecoveryTimeoutMs
+ }
+ if src.BlackholeConnectComparativeTimeoutMs != nil {
+ dst.BlackholeConnectComparativeTimeoutMs = src.BlackholeConnectComparativeTimeoutMs
+ }
+ if src.HeartbeatIntervalMs != nil {
+ dst.HeartbeatIntervalMs = src.HeartbeatIntervalMs
+ }
+ return nil
+}
diff --git a/socks/reliability_test.go b/socks/reliability_test.go
new file mode 100644
index 0000000..7d74fc0
--- /dev/null
+++ b/socks/reliability_test.go
@@ -0,0 +1,505 @@
+package main
+
+import (
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/urnetwork/connect"
+)
+
+// --- reliability file ---
+
+func writeRelFile(t *testing.T, content string) string {
+ t.Helper()
+ path := filepath.Join(t.TempDir(), "reliability.json")
+ if err := os.WriteFile(path, []byte(content), 0644); err != nil {
+ t.Fatal(err)
+ }
+ return path
+}
+
+func TestParseReliabilityFilePartial(t *testing.T) {
+ path := writeRelFile(t, `{
+ "maxFlowsPerExit": 32,
+ "sendStallTimeoutMs": 5000,
+ "busyProbe": true
+ }`)
+ f, err := parseReliabilityFile(path)
+ if err != nil {
+ t.Fatalf("parse: %v", err)
+ }
+ if f.MaxFlowsPerExit == nil || *f.MaxFlowsPerExit != 32 {
+ t.Errorf("MaxFlowsPerExit = %v, want 32", f.MaxFlowsPerExit)
+ }
+ if f.SendStallTimeoutMs == nil || *f.SendStallTimeoutMs != 5000 {
+ t.Errorf("SendStallTimeoutMs = %v, want 5000", f.SendStallTimeoutMs)
+ }
+ if f.BusyProbe == nil || !*f.BusyProbe {
+ t.Errorf("BusyProbe = %v, want true", f.BusyProbe)
+ }
+ // unspecified fields stay nil (untouched), not zero
+ if f.UdpTeardownSignal != nil {
+ t.Errorf("UdpTeardownSignal = %v, want nil (untouched)", f.UdpTeardownSignal)
+ }
+ if f.BlackholeReceiveTimeoutMs != nil {
+ t.Errorf("BlackholeReceiveTimeoutMs = %v, want nil (untouched)", f.BlackholeReceiveTimeoutMs)
+ }
+}
+
+func TestParseReliabilityFileBadJSON(t *testing.T) {
+ path := writeRelFile(t, `{not json`)
+ if _, err := parseReliabilityFile(path); err == nil {
+ t.Fatal("expected error for invalid json")
+ }
+}
+
+func TestParseReliabilityFileMissing(t *testing.T) {
+ if _, err := parseReliabilityFile("/nonexistent/reliability.json"); err == nil {
+ t.Fatal("expected error for missing file")
+ }
+}
+
+func TestFileToSettingsExplicitZeroApplied(t *testing.T) {
+ // an explicit 0 in the file must be applied (0 = off / pre-fix behavior),
+ // distinct from nil (untouched)
+ f := &reliabilityFile{
+ MaxFlowsPerExit: intPtr(0),
+ SendStallTimeoutMs: int64Ptr(0),
+ BusyProbe: boolPtr(false),
+ BlackholeReceiveTimeoutMs: int64Ptr(0),
+ }
+ s, err := fileToSettings(f)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if s.MaxFlowsPerExit != 0 {
+ t.Errorf("MaxFlowsPerExit = %d, want 0 (explicit zero applied)", s.MaxFlowsPerExit)
+ }
+ if s.SendStallTimeout != 0 {
+ t.Errorf("SendStallTimeout = %v, want 0", s.SendStallTimeout)
+ }
+ if s.BusyProbe {
+ t.Error("BusyProbe = true, want false")
+ }
+ if s.BlackholeReceiveTimeout != 0 {
+ t.Errorf("BlackholeReceiveTimeout = %v, want 0", s.BlackholeReceiveTimeout)
+ }
+}
+
+func TestApplyFileOverPreservesBase(t *testing.T) {
+ base := &connect.ReliabilitySettings{
+ UdpTeardownSignal: true,
+ SendStallTimeout: 3000 * time.Millisecond,
+ MaxFlowsPerExit: 16,
+ BlackholeReceiveTimeout: 20 * time.Second,
+ }
+ overlay := &reliabilityFile{MaxFlowsPerExit: intPtr(64)}
+
+ merged := settingsToFile(base)
+ if err := applyFileOver(merged, overlay); err != nil {
+ t.Fatal(err)
+ }
+ s, err := fileToSettings(merged)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if s.MaxFlowsPerExit != 64 {
+ t.Errorf("MaxFlowsPerExit = %d, want 64", s.MaxFlowsPerExit)
+ }
+ if !s.UdpTeardownSignal {
+ t.Error("UdpTeardownSignal lost, want true (preserved from base)")
+ }
+ if s.SendStallTimeout != 3*time.Second {
+ t.Errorf("SendStallTimeout = %v, want 3s (preserved)", s.SendStallTimeout)
+ }
+ if s.BlackholeReceiveTimeout != 20*time.Second {
+ t.Errorf("BlackholeReceiveTimeout = %v, want 20s (preserved)", s.BlackholeReceiveTimeout)
+ }
+}
+
+func TestSettingsToFileRoundTrip(t *testing.T) {
+ s := &connect.ReliabilitySettings{
+ UdpTeardownSignal: true,
+ QuicRebindOnExitLoss: true,
+ TcpCollapseMaxHold: 1500 * time.Millisecond,
+ SendStallTimeout: 3 * time.Second,
+ ClusterAffinityFallback: true,
+ ServerNameAffinityBridge: true,
+ SequenceIdleTimeout: 120 * time.Second,
+ TcpSequenceIdleTimeout: 600 * time.Second,
+ BlackholeReceiveTimeout: 20 * time.Second,
+ MaxFlowsPerExit: 16,
+ AffinityStickyPastCap: true,
+ QuarantineGroupFollow: true,
+ GroupFollowWindow: 45 * time.Second,
+ DialFailureRerace: true,
+ UplinkStalenessGate: 5 * time.Second,
+ SoftVerdictDemote: true,
+ RemovalBudgetCount: 2,
+ RemovalBudgetWindow: 30 * time.Second,
+ StandingReserve: true,
+ EffectiveTierSelection: true,
+ MinBlackholeDestinations: 2,
+ BlackholeLoadCorroboration: 8,
+ ProviderProbe: true,
+ ProbeTimeout: 4 * time.Second,
+ ProbeSampleHostCount: 0,
+ ProbeSilenceWarnStreak: 2,
+ EvaluationPoolMultiple: 2,
+ FormationPollTimeout: 200 * time.Millisecond,
+ BusyProbe: true,
+ BusyProbeBudget: 0,
+ SchedulerPauseTolerance: 2 * time.Second,
+ SchedulerPauseRecoveryTimeout: 5 * time.Second,
+ BlackholeConnectComparativeTimeout: 10 * time.Second,
+ HeartbeatInterval: 60 * time.Second,
+ }
+
+ back, err := fileToSettings(settingsToFile(s))
+ if err != nil {
+ t.Fatal(err)
+ }
+ // spot-check the ones with interesting conversions
+ if back.MaxFlowsPerExit != 16 {
+ t.Errorf("MaxFlowsPerExit = %d, want 16", back.MaxFlowsPerExit)
+ }
+ if back.SendStallTimeout != 3*time.Second {
+ t.Errorf("SendStallTimeout = %v, want 3s", back.SendStallTimeout)
+ }
+ if back.ProbeSampleHostCount != 0 {
+ t.Errorf("ProbeSampleHostCount = %d, want 0 (explicit zero survives)", back.ProbeSampleHostCount)
+ }
+ if !back.BusyProbe {
+ t.Error("BusyProbe lost")
+ }
+ if back.BusyProbeBudget != 0 {
+ t.Errorf("BusyProbeBudget = %v, want 0", back.BusyProbeBudget)
+ }
+ if back.HeartbeatInterval != 60*time.Second {
+ t.Errorf("HeartbeatInterval = %v, want 60s", back.HeartbeatInterval)
+ }
+}
+
+// --- control server loopback guard ---
+
+func TestControlAddrLoopbackGuard(t *testing.T) {
+ ok, err := controlAddrIsLoopback("127.0.0.1:9998")
+ if err != nil || !ok {
+ t.Errorf("127.0.0.1:9998 should be allowed (ok=%v err=%v)", ok, err)
+ }
+ ok, err = controlAddrIsLoopback("localhost:9998")
+ if err != nil || !ok {
+ t.Errorf("localhost:9998 should be allowed (ok=%v err=%v)", ok, err)
+ }
+ ok, err = controlAddrIsLoopback("[::1]:9998")
+ if err != nil || !ok {
+ t.Errorf("[::1]:9998 should be allowed (ok=%v err=%v)", ok, err)
+ }
+ ok, err = controlAddrIsLoopback("0.0.0.0:9998")
+ if err != nil || ok {
+ t.Errorf("0.0.0.0:9998 must be rejected (ok=%v err=%v)", ok, err)
+ }
+ ok, err = controlAddrIsLoopback("192.168.1.5:9998")
+ if err != nil || ok {
+ t.Errorf("192.168.1.5:9998 must be rejected (ok=%v err=%v)", ok, err)
+ }
+ if _, err := controlAddrIsLoopback("not-an-addr"); err == nil {
+ t.Error("expected SplitHostPort error for malformed address")
+ }
+}
+
+// --- control server endpoints (with a real multi client is impossible here,
+// so use the real server with a stub mc via a minimal fake) ---
+
+// fakeMC implements just enough of the mc surface for the control handlers.
+type fakeMC struct {
+ settings *connect.ReliabilitySettings
+ metrics *connect.ReliabilityMetricsSnapshot
+ exits []*connect.ExitInfo
+ dropped []string
+ stalled []string
+ migrated []string
+ shuffled bool
+ probed int
+ resets int
+}
+
+func (f *fakeMC) ReliabilitySettings() *connect.ReliabilitySettings {
+ if f.settings == nil {
+ return &connect.ReliabilitySettings{}
+ }
+ return f.settings
+}
+func (f *fakeMC) SetReliabilitySettings(s *connect.ReliabilitySettings) { f.settings = s }
+func (f *fakeMC) ReliabilityMetrics() *connect.ReliabilityMetricsSnapshot {
+ if f.metrics == nil {
+ return &connect.ReliabilityMetricsSnapshot{}
+ }
+ return f.metrics
+}
+func (f *fakeMC) ResetReliabilityMetrics() { f.resets++ }
+func (f *fakeMC) Shuffle() { f.shuffled = true }
+func (f *fakeMC) ProbeAllExits() int { f.probed++; return 3 }
+func (f *fakeMC) Exits() []*connect.ExitInfo { return f.exits }
+func (f *fakeMC) DropExit(id connect.Id) bool {
+ f.dropped = append(f.dropped, id.String())
+ return true
+}
+func (f *fakeMC) StallExit(id connect.Id, stalled bool) bool {
+ f.stalled = append(f.stalled, id.String())
+ return true
+}
+func (f *fakeMC) MigrateExit(id connect.Id) int {
+ f.migrated = append(f.migrated, id.String())
+ return 5
+}
+func (f *fakeMC) PacketStats() *connect.PacketStats {
+ return &connect.PacketStats{
+ RemoteEgressByteCount: 1024,
+ RemoteIngressByteCount: 2048,
+ RemoteEgressPacketCount: 10,
+ RemoteIngressPacketCount: 20,
+ }
+}
+
+func TestControlEndpoints(t *testing.T) {
+ mc := &fakeMC{}
+ c, err := newControlServer("127.0.0.1:0", mc)
+ if err != nil {
+ t.Fatal(err)
+ }
+ srv := httptest.NewServer(c.server.Handler)
+ defer srv.Close()
+
+ // GET /settings returns json with current values
+ resp, err := http.Get(srv.URL + "/settings")
+ if err != nil {
+ t.Fatal(err)
+ }
+ var got map[string]any
+ if err := json.NewDecoder(resp.Body).Decode(&got); err != nil {
+ t.Fatal(err)
+ }
+ resp.Body.Close()
+ if got["maxFlowsPerExit"] != nil && got["maxFlowsPerExit"].(float64) != 0 {
+ t.Errorf("unexpected maxFlowsPerExit: %v", got["maxFlowsPerExit"])
+ }
+
+ // PUT /settings partial merge: set maxFlowsPerExit, keep the rest
+ req, _ := http.NewRequest("PUT", srv.URL+"/settings", strings.NewReader(`{"maxFlowsPerExit": 64}`))
+ req.Header.Set("Content-Type", "application/json")
+ resp, err = http.DefaultClient.Do(req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if resp.StatusCode != http.StatusOK {
+ t.Fatalf("PUT /settings status = %d, want 200", resp.StatusCode)
+ }
+ resp.Body.Close()
+ if mc.settings == nil || mc.settings.MaxFlowsPerExit != 64 {
+ t.Fatalf("settings after PUT = %+v, want MaxFlowsPerExit=64", mc.settings)
+ }
+
+ // POST /actions reset-metrics
+ resp, err = http.Post(srv.URL+"/actions", "application/json", strings.NewReader(`{"action":"reset-metrics"}`))
+ if err != nil {
+ t.Fatal(err)
+ }
+ resp.Body.Close()
+ if mc.resets != 1 {
+ t.Errorf("resets = %d, want 1", mc.resets)
+ }
+
+ // POST /actions shuffle
+ resp, _ = http.Post(srv.URL+"/actions", "application/json", strings.NewReader(`{"action":"shuffle"}`))
+ resp.Body.Close()
+ if !mc.shuffled {
+ t.Error("shuffle not called")
+ }
+
+ // POST /actions probe-all
+ resp, _ = http.Post(srv.URL+"/actions", "application/json", strings.NewReader(`{"action":"probe-all"}`))
+ resp.Body.Close()
+ if mc.probed != 1 {
+ t.Errorf("probed = %d, want 1", mc.probed)
+ }
+
+ // POST /actions drop with a valid client id
+ cid := "019fde39-e690-add0-ac88-e354a0d76d6a"
+ resp, _ = http.Post(srv.URL+"/actions", "application/json", strings.NewReader(`{"action":"drop","clientId":"`+cid+`"}`))
+ resp.Body.Close()
+ if len(mc.dropped) != 1 || mc.dropped[0] != cid {
+ t.Errorf("dropped = %v, want [%s]", mc.dropped, cid)
+ }
+
+ // POST /actions drop with missing clientId -> 400
+ resp, _ = http.Post(srv.URL+"/actions", "application/json", strings.NewReader(`{"action":"drop"}`))
+ if resp.StatusCode != http.StatusBadRequest {
+ t.Errorf("drop w/o clientId status = %d, want 400", resp.StatusCode)
+ }
+ resp.Body.Close()
+
+ // POST /actions unknown action -> 400
+ resp, _ = http.Post(srv.URL+"/actions", "application/json", strings.NewReader(`{"action":"bogus"}`))
+ if resp.StatusCode != http.StatusBadRequest {
+ t.Errorf("bogus action status = %d, want 400", resp.StatusCode)
+ }
+ resp.Body.Close()
+
+ // GET /metrics returns the snapshot
+ resp, err = http.Get(srv.URL + "/metrics")
+ if err != nil {
+ t.Fatal(err)
+ }
+ var m connect.ReliabilityMetricsSnapshot
+ if err := json.NewDecoder(resp.Body).Decode(&m); err != nil {
+ t.Fatal(err)
+ }
+ resp.Body.Close()
+
+ // GET /exits returns the table
+ resp, err = http.Get(srv.URL + "/exits")
+ if err != nil {
+ t.Fatal(err)
+ }
+ var xs []*connect.ExitInfo
+ if err := json.NewDecoder(resp.Body).Decode(&xs); err != nil {
+ t.Fatal(err)
+ }
+ resp.Body.Close()
+}
+
+func TestControlServerRejectsBadMethod(t *testing.T) {
+ mc := &fakeMC{}
+ c, err := newControlServer("127.0.0.1:0", mc)
+ if err != nil {
+ t.Fatal(err)
+ }
+ srv := httptest.NewServer(c.server.Handler)
+ defer srv.Close()
+
+ req, _ := http.NewRequest("DELETE", srv.URL+"/settings", nil)
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if resp.StatusCode != http.StatusMethodNotAllowed {
+ t.Errorf("DELETE /settings status = %d, want 405", resp.StatusCode)
+ }
+ resp.Body.Close()
+}
+
+// TestControlRejectsNonLoopbackOrigin pins the CSRF guard: a browser
+// cross-origin request (which always sends Origin) must be refused on the
+// mutating endpoints, while requests without Origin (curl) and with a
+// loopback Origin pass.
+func TestControlRejectsNonLoopbackOrigin(t *testing.T) {
+ mc := &fakeMC{}
+ c, err := newControlServer("127.0.0.1:0", mc)
+ if err != nil {
+ t.Fatal(err)
+ }
+ srv := httptest.NewServer(c.server.Handler)
+ defer srv.Close()
+
+ // no Origin -> allowed (curl)
+ req, _ := http.NewRequest("PUT", srv.URL+"/settings", strings.NewReader(`{"maxFlowsPerExit": 16}`))
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if resp.StatusCode != http.StatusOK {
+ t.Errorf("PUT without Origin = %d, want 200", resp.StatusCode)
+ }
+ resp.Body.Close()
+
+ // loopback Origin -> allowed
+ req, _ = http.NewRequest("POST", srv.URL+"/actions", strings.NewReader(`{"action":"shuffle"}`))
+ req.Header.Set("Origin", "http://127.0.0.1:8080")
+ resp, err = http.DefaultClient.Do(req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if resp.StatusCode != http.StatusOK {
+ t.Errorf("POST with loopback Origin = %d, want 200", resp.StatusCode)
+ }
+ resp.Body.Close()
+
+ // evil cross-origin -> 403 on both mutating endpoints
+ for _, tc := range []struct {
+ method, path, body string
+ }{
+ {"PUT", "/settings", `{"maxFlowsPerExit": 32}`},
+ {"POST", "/actions", `{"action":"drop","clientId":"019fde39-e690-add0-ac88-e354a0d76d6a"}`},
+ } {
+ req, _ := http.NewRequest(tc.method, srv.URL+tc.path, strings.NewReader(tc.body))
+ req.Header.Set("Origin", "https://evil.example")
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if resp.StatusCode != http.StatusForbidden {
+ t.Errorf("%s with evil Origin = %d, want 403", tc.method, resp.StatusCode)
+ }
+ resp.Body.Close()
+ }
+
+ // the evil-origin drop must not have executed
+ if len(mc.dropped) != 0 {
+ t.Errorf("drop executed despite non-loopback origin: %v", mc.dropped)
+ }
+}
+
+// TestControlConcurrentSettingsPut exercises the lost-update race fix: two
+// PUTs that set different fields concurrently must both survive.
+func TestControlConcurrentSettingsPut(t *testing.T) {
+ mc := &fakeMC{}
+ c, err := newControlServer("127.0.0.1:0", mc)
+ if err != nil {
+ t.Fatal(err)
+ }
+ srv := httptest.NewServer(c.server.Handler)
+ defer srv.Close()
+
+ done := make(chan struct{}, 2)
+ go func() {
+ defer func() { done <- struct{}{} }()
+ req, _ := http.NewRequest("PUT", srv.URL+"/settings", strings.NewReader(`{"maxFlowsPerExit": 64}`))
+ resp, err := http.DefaultClient.Do(req)
+ if err == nil {
+ resp.Body.Close()
+ }
+ }()
+ go func() {
+ defer func() { done <- struct{}{} }()
+ req, _ := http.NewRequest("PUT", srv.URL+"/settings", strings.NewReader(`{"sendStallTimeoutMs": 5000}`))
+ resp, err := http.DefaultClient.Do(req)
+ if err == nil {
+ resp.Body.Close()
+ }
+ }()
+ <-done
+ <-done
+
+ if mc.settings == nil {
+ t.Fatal("no settings applied")
+ }
+ if mc.settings.MaxFlowsPerExit != 64 {
+ t.Errorf("MaxFlowsPerExit = %d, want 64 (lost update)", mc.settings.MaxFlowsPerExit)
+ }
+ if mc.settings.SendStallTimeout != 5*time.Second {
+ t.Errorf("SendStallTimeout = %v, want 5s (lost update)", mc.settings.SendStallTimeout)
+ }
+}
+
+// --- helpers ---
+
+func intPtr(v int) *int { return &v }
+func int64Ptr(v int64) *int64 { return &v }
+func boolPtr(v bool) *bool { return &v }
diff --git a/socks/ur_logo.svg b/socks/ur_logo.svg
new file mode 100644
index 0000000..921e5c2
--- /dev/null
+++ b/socks/ur_logo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file