From 7f445968585face415fa8f4294f8a97cfe4df627 Mon Sep 17 00:00:00 2001 From: Virgil Date: Sat, 4 Apr 2026 18:19:09 +0000 Subject: [PATCH] fix(proxy): validate config and reload pools Co-Authored-By: Virgil --- config_test.go | 41 +++++++++++++++++++++++++++++++++ core_impl.go | 28 +++++++++++++++++++---- pool/impl.go | 12 +++++++++- pool/impl_test.go | 27 ++++++++++++++++++++++ reload_test.go | 58 +++++++++++++++++++++++++++++++++++++++++++++++ state_impl.go | 15 ++++++++---- 6 files changed, 172 insertions(+), 9 deletions(-) create mode 100644 config_test.go create mode 100644 pool/impl_test.go create mode 100644 reload_test.go diff --git a/config_test.go b/config_test.go new file mode 100644 index 0000000..e5f31c4 --- /dev/null +++ b/config_test.go @@ -0,0 +1,41 @@ +package proxy + +import "testing" + +func TestConfig_Validate_Good(t *testing.T) { + cfg := &Config{ + Mode: "nicehash", + Workers: WorkersByRigID, + Bind: []BindAddr{{Host: "0.0.0.0", Port: 3333}}, + Pools: []PoolConfig{{URL: "pool.example:3333", Enabled: true}}, + } + + if result := cfg.Validate(); !result.OK { + t.Fatalf("expected valid config, got error: %v", result.Error) + } +} + +func TestConfig_Validate_Bad(t *testing.T) { + cfg := &Config{ + Workers: WorkersByRigID, + Bind: []BindAddr{{Host: "0.0.0.0", Port: 3333}}, + Pools: []PoolConfig{{URL: "pool.example:3333", Enabled: true}}, + } + + if result := cfg.Validate(); result.OK { + t.Fatalf("expected missing mode to fail validation") + } +} + +func TestConfig_Validate_Ugly(t *testing.T) { + cfg := &Config{ + Mode: "nicehash", + Workers: WorkersMode("unknown"), + Bind: []BindAddr{{Host: "0.0.0.0", Port: 3333}}, + Pools: []PoolConfig{{URL: "", Enabled: true}}, + } + + if result := cfg.Validate(); result.OK { + t.Fatalf("expected invalid workers and empty pool url to fail validation") + } +} diff --git a/core_impl.go b/core_impl.go index dbacba7..2a86054 100644 --- a/core_impl.go +++ b/core_impl.go @@ -59,10 +59,6 @@ func LoadConfig(path string) (*Config, Result) { if err := json.Unmarshal(data, cfg); err != nil { return nil, errorResult(err) } - - if cfg.Mode == "" { - cfg.Mode = "nicehash" - } return cfg, cfg.Validate() } @@ -71,6 +67,12 @@ func (c *Config) Validate() Result { if c == nil { return errorResult(errors.New("config is nil")) } + if !isValidMode(c.Mode) { + return errorResult(errors.New("mode must be \"nicehash\" or \"simple\"")) + } + if !isValidWorkersMode(c.Workers) { + return errorResult(errors.New("workers must be one of \"rig-id\", \"user\", \"password\", \"agent\", \"ip\", or \"false\"")) + } if len(c.Bind) == 0 { return errorResult(errors.New("bind list is empty")) } @@ -85,6 +87,24 @@ func (c *Config) Validate() Result { return successResult() } +func isValidMode(mode string) bool { + switch strings.ToLower(strings.TrimSpace(mode)) { + case "nicehash", "simple": + return true + default: + return false + } +} + +func isValidWorkersMode(mode WorkersMode) bool { + switch mode { + case WorkersByRigID, WorkersByUser, WorkersByPass, WorkersByAgent, WorkersByIP, WorkersDisabled: + return true + default: + return false + } +} + // NewEventBus creates an empty synchronous event dispatcher. func NewEventBus() *EventBus { return &EventBus{listeners: make(map[EventType][]EventHandler)} diff --git a/pool/impl.go b/pool/impl.go index 9c3f5cd..02cbe9c 100644 --- a/pool/impl.go +++ b/pool/impl.go @@ -338,7 +338,7 @@ func (s *FailoverStrategy) Connect() { } func (s *FailoverStrategy) connectLocked(start int) { - enabled := enabledPools(s.pools) + enabled := enabledPools(s.currentPools()) if len(enabled) == 0 { return } @@ -368,6 +368,16 @@ func (s *FailoverStrategy) connectLocked(start int) { } } +func (s *FailoverStrategy) currentPools() []proxy.PoolConfig { + if s == nil { + return nil + } + if s.cfg != nil && len(s.cfg.Pools) > 0 { + return s.cfg.Pools + } + return s.pools +} + // Submit sends the share through the active client. func (s *FailoverStrategy) Submit(jobID, nonce, result, algo string) int64 { if s == nil || s.client == nil { diff --git a/pool/impl_test.go b/pool/impl_test.go new file mode 100644 index 0000000..9abbb66 --- /dev/null +++ b/pool/impl_test.go @@ -0,0 +1,27 @@ +package pool + +import ( + "testing" + + "dappco.re/go/proxy" +) + +func TestFailoverStrategy_CurrentPools_Good(t *testing.T) { + cfg := &proxy.Config{ + Mode: "nicehash", + Workers: proxy.WorkersByRigID, + Bind: []proxy.BindAddr{{Host: "127.0.0.1", Port: 3333}}, + Pools: []proxy.PoolConfig{{URL: "pool-a.example:3333", Enabled: true}}, + } + strategy := NewFailoverStrategy(cfg.Pools, nil, cfg) + + if got := len(strategy.currentPools()); got != 1 { + t.Fatalf("expected 1 pool, got %d", got) + } + + cfg.Pools = []proxy.PoolConfig{{URL: "pool-b.example:4444", Enabled: true}} + + if got := strategy.currentPools(); len(got) != 1 || got[0].URL != "pool-b.example:4444" { + t.Fatalf("expected current pools to follow config reload, got %+v", got) + } +} diff --git a/reload_test.go b/reload_test.go new file mode 100644 index 0000000..4a8cf43 --- /dev/null +++ b/reload_test.go @@ -0,0 +1,58 @@ +package proxy + +import "testing" + +func TestProxy_Reload_Good(t *testing.T) { + original := &Config{ + Mode: "nicehash", + Workers: WorkersByRigID, + Bind: []BindAddr{{Host: "127.0.0.1", Port: 3333}}, + Pools: []PoolConfig{{URL: "pool-a.example:3333", Enabled: true}}, + } + p := &Proxy{ + config: original, + customDiff: NewCustomDiff(1), + rateLimit: NewRateLimiter(RateLimit{}), + } + + updated := &Config{ + Mode: "simple", + Workers: WorkersByUser, + Bind: []BindAddr{{Host: "0.0.0.0", Port: 4444}}, + Pools: []PoolConfig{{URL: "pool-b.example:4444", Enabled: true}}, + CustomDiff: 50000, + AccessPassword: "secret", + CustomDiffStats: true, + AlgoExtension: true, + AccessLogFile: "/tmp/access.log", + ReuseTimeout: 30, + Retries: 5, + RetryPause: 2, + Watch: true, + RateLimit: RateLimit{MaxConnectionsPerMinute: 10, BanDurationSeconds: 60}, + } + + p.Reload(updated) + + if p.config != original { + t.Fatalf("expected reload to preserve the existing config pointer") + } + if got := p.config.Bind[0]; got.Host != "127.0.0.1" || got.Port != 3333 { + t.Fatalf("expected bind addresses to remain unchanged, got %+v", got) + } + if p.config.Mode != "nicehash" { + t.Fatalf("expected mode to remain unchanged, got %q", p.config.Mode) + } + if p.config.Workers != WorkersByRigID { + t.Fatalf("expected workers mode to remain unchanged, got %q", p.config.Workers) + } + if got := p.config.Pools[0].URL; got != "pool-b.example:4444" { + t.Fatalf("expected pools to reload, got %q", got) + } + if got := p.customDiff.globalDiff; got != 50000 { + t.Fatalf("expected custom diff to reload, got %d", got) + } + if !p.rateLimit.IsActive() { + t.Fatalf("expected rate limiter to be replaced with active configuration") + } +} diff --git a/state_impl.go b/state_impl.go index 693cab9..7393db2 100644 --- a/state_impl.go +++ b/state_impl.go @@ -35,9 +35,6 @@ func New(cfg *Config) (*Proxy, Result) { if cfg == nil { return nil, errorResult(errors.New("config is nil")) } - if cfg.Mode == "" { - cfg.Mode = "nicehash" - } if result := cfg.Validate(); !result.OK { return nil, result } @@ -227,7 +224,17 @@ func (p *Proxy) Reload(cfg *Config) { if p == nil || cfg == nil { return } - p.config = cfg + if p.config == nil { + p.config = cfg + } else { + preservedBind := append([]BindAddr(nil), p.config.Bind...) + preservedMode := p.config.Mode + preservedWorkers := p.config.Workers + *p.config = *cfg + p.config.Bind = preservedBind + p.config.Mode = preservedMode + p.config.Workers = preservedWorkers + } if p.customDiff != nil { p.customDiff.globalDiff = cfg.CustomDiff }