refactor(errors): add scoped proxy failures
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
35db5f6840
commit
0a7c99264b
5 changed files with 133 additions and 52 deletions
21
core_impl.go
21
core_impl.go
|
|
@ -6,7 +6,6 @@ import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
"net"
|
"net"
|
||||||
|
|
@ -62,12 +61,12 @@ func splitterFactoryForMode(mode string) (func(*Config, *EventBus) Splitter, boo
|
||||||
func LoadConfig(path string) (*Config, Result) {
|
func LoadConfig(path string) (*Config, Result) {
|
||||||
data, err := os.ReadFile(path)
|
data, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, newErrorResult(err)
|
return nil, newErrorResult(NewScopedError("proxy.config", "read config failed", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
config := &Config{}
|
config := &Config{}
|
||||||
if err := json.Unmarshal(data, config); err != nil {
|
if err := json.Unmarshal(data, config); err != nil {
|
||||||
return nil, newErrorResult(err)
|
return nil, newErrorResult(NewScopedError("proxy.config", "parse config failed", err))
|
||||||
}
|
}
|
||||||
config.configPath = path
|
config.configPath = path
|
||||||
return config, config.Validate()
|
return config, config.Validate()
|
||||||
|
|
@ -77,31 +76,31 @@ func LoadConfig(path string) (*Config, Result) {
|
||||||
// if result := cfg.Validate(); !result.OK { return result }
|
// if result := cfg.Validate(); !result.OK { return result }
|
||||||
func (c *Config) Validate() Result {
|
func (c *Config) Validate() Result {
|
||||||
if c == nil {
|
if c == nil {
|
||||||
return newErrorResult(errors.New("config is nil"))
|
return newErrorResult(NewScopedError("proxy.config", "config is nil", nil))
|
||||||
}
|
}
|
||||||
if !isValidMode(c.Mode) {
|
if !isValidMode(c.Mode) {
|
||||||
return newErrorResult(errors.New("mode must be \"nicehash\" or \"simple\""))
|
return newErrorResult(NewScopedError("proxy.config", "mode must be \"nicehash\" or \"simple\"", nil))
|
||||||
}
|
}
|
||||||
if !isValidWorkersMode(c.Workers) {
|
if !isValidWorkersMode(c.Workers) {
|
||||||
return newErrorResult(errors.New("workers must be one of \"rig-id\", \"user\", \"password\", \"agent\", \"ip\", or \"false\""))
|
return newErrorResult(NewScopedError("proxy.config", "workers must be one of \"rig-id\", \"user\", \"password\", \"agent\", \"ip\", or \"false\"", nil))
|
||||||
}
|
}
|
||||||
if len(c.Bind) == 0 {
|
if len(c.Bind) == 0 {
|
||||||
return newErrorResult(errors.New("bind list is empty"))
|
return newErrorResult(NewScopedError("proxy.config", "bind list is empty", nil))
|
||||||
}
|
}
|
||||||
if len(c.Pools) == 0 {
|
if len(c.Pools) == 0 {
|
||||||
return newErrorResult(errors.New("pool list is empty"))
|
return newErrorResult(NewScopedError("proxy.config", "pool list is empty", nil))
|
||||||
}
|
}
|
||||||
enabledPools := 0
|
enabledPools := 0
|
||||||
for _, pool := range c.Pools {
|
for _, pool := range c.Pools {
|
||||||
if pool.Enabled && strings.TrimSpace(pool.URL) == "" {
|
if pool.Enabled && strings.TrimSpace(pool.URL) == "" {
|
||||||
return newErrorResult(errors.New("enabled pool url is empty"))
|
return newErrorResult(NewScopedError("proxy.config", "enabled pool url is empty", nil))
|
||||||
}
|
}
|
||||||
if pool.Enabled {
|
if pool.Enabled {
|
||||||
enabledPools++
|
enabledPools++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if enabledPools == 0 {
|
if enabledPools == 0 {
|
||||||
return newErrorResult(errors.New("pool list has no enabled entries"))
|
return newErrorResult(NewScopedError("proxy.config", "pool list has no enabled entries", nil))
|
||||||
}
|
}
|
||||||
return newSuccessResult()
|
return newSuccessResult()
|
||||||
}
|
}
|
||||||
|
|
@ -295,6 +294,7 @@ func (cd *CustomDiff) OnLogin(e Event) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// limiter := proxy.NewRateLimiter(proxy.RateLimit{MaxConnectionsPerMinute: 30, BanDurationSeconds: 300})
|
// limiter := proxy.NewRateLimiter(proxy.RateLimit{MaxConnectionsPerMinute: 30, BanDurationSeconds: 300})
|
||||||
|
//
|
||||||
// if limiter.Allow("203.0.113.42:3333") {
|
// if limiter.Allow("203.0.113.42:3333") {
|
||||||
// // first 30 connection attempts per minute are allowed
|
// // first 30 connection attempts per minute are allowed
|
||||||
// }
|
// }
|
||||||
|
|
@ -370,6 +370,7 @@ func (rl *RateLimiter) Tick() {
|
||||||
// watcher := proxy.NewConfigWatcher("config.json", func(cfg *proxy.Config) {
|
// watcher := proxy.NewConfigWatcher("config.json", func(cfg *proxy.Config) {
|
||||||
// p.Reload(cfg)
|
// p.Reload(cfg)
|
||||||
// })
|
// })
|
||||||
|
//
|
||||||
// watcher.Start() // polls once per second and reloads after the file mtime changes
|
// watcher.Start() // polls once per second and reloads after the file mtime changes
|
||||||
func NewConfigWatcher(configPath string, onChange func(*Config)) *ConfigWatcher {
|
func NewConfigWatcher(configPath string, onChange func(*Config)) *ConfigWatcher {
|
||||||
watcher := &ConfigWatcher{
|
watcher := &ConfigWatcher{
|
||||||
|
|
|
||||||
38
error.go
Normal file
38
error.go
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
package proxy
|
||||||
|
|
||||||
|
// ScopedError carries a stable error scope alongside a human-readable message.
|
||||||
|
//
|
||||||
|
// err := proxy.NewScopedError("proxy.config", "load failed", io.EOF)
|
||||||
|
type ScopedError struct {
|
||||||
|
Scope string
|
||||||
|
Message string
|
||||||
|
Cause error
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewScopedError creates an error that keeps a greppable scope token in the failure path.
|
||||||
|
//
|
||||||
|
// err := proxy.NewScopedError("proxy.server", "listen failed", cause)
|
||||||
|
func NewScopedError(scope, message string, cause error) error {
|
||||||
|
return &ScopedError{
|
||||||
|
Scope: scope,
|
||||||
|
Message: message,
|
||||||
|
Cause: cause,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ScopedError) Error() string {
|
||||||
|
if e == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if e.Cause == nil {
|
||||||
|
return e.Scope + ": " + e.Message
|
||||||
|
}
|
||||||
|
return e.Scope + ": " + e.Message + ": " + e.Cause.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ScopedError) Unwrap() error {
|
||||||
|
if e == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return e.Cause
|
||||||
|
}
|
||||||
43
error_test.go
Normal file
43
error_test.go
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
package proxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestError_NewScopedError_Good(t *testing.T) {
|
||||||
|
err := NewScopedError("proxy.config", "bind list is empty", nil)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected scoped error")
|
||||||
|
}
|
||||||
|
if got := err.Error(); got != "proxy.config: bind list is empty" {
|
||||||
|
t.Fatalf("unexpected scoped error string: %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestError_NewScopedError_Bad(t *testing.T) {
|
||||||
|
cause := errors.New("permission denied")
|
||||||
|
err := NewScopedError("proxy.config", "read config failed", cause)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected scoped error")
|
||||||
|
}
|
||||||
|
if !errors.Is(err, cause) {
|
||||||
|
t.Fatalf("expected errors.Is to unwrap the original cause")
|
||||||
|
}
|
||||||
|
if got := err.Error(); got != "proxy.config: read config failed: permission denied" {
|
||||||
|
t.Fatalf("unexpected wrapped error string: %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestError_NewScopedError_Ugly(t *testing.T) {
|
||||||
|
var scoped *ScopedError
|
||||||
|
|
||||||
|
if got := scoped.Error(); got != "" {
|
||||||
|
t.Fatalf("expected nil scoped error string to be empty, got %q", got)
|
||||||
|
}
|
||||||
|
if scoped.Unwrap() != nil {
|
||||||
|
t.Fatalf("expected nil scoped error to unwrap to nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
18
pool/impl.go
18
pool/impl.go
|
|
@ -6,7 +6,6 @@ import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
@ -53,15 +52,15 @@ func (c *StratumClient) IsActive() bool {
|
||||||
// result := client.Connect()
|
// result := client.Connect()
|
||||||
func (c *StratumClient) Connect() proxy.Result {
|
func (c *StratumClient) Connect() proxy.Result {
|
||||||
if c == nil {
|
if c == nil {
|
||||||
return proxy.Result{OK: false, Error: errors.New("client is nil")}
|
return proxy.Result{OK: false, Error: proxy.NewScopedError("proxy.pool.client", "client is nil", nil)}
|
||||||
}
|
}
|
||||||
addr := c.config.URL
|
addr := c.config.URL
|
||||||
if addr == "" {
|
if addr == "" {
|
||||||
return proxy.Result{OK: false, Error: errors.New("pool url is empty")}
|
return proxy.Result{OK: false, Error: proxy.NewScopedError("proxy.pool.client", "pool url is empty", nil)}
|
||||||
}
|
}
|
||||||
conn, err := net.Dial("tcp", addr)
|
conn, err := net.Dial("tcp", addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return proxy.Result{OK: false, Error: err}
|
return proxy.Result{OK: false, Error: proxy.NewScopedError("proxy.pool.client", "dial pool failed", err)}
|
||||||
}
|
}
|
||||||
if c.config.TLS {
|
if c.config.TLS {
|
||||||
host := addr
|
host := addr
|
||||||
|
|
@ -72,18 +71,18 @@ func (c *StratumClient) Connect() proxy.Result {
|
||||||
tlsConn := tls.Client(conn, tlsCfg)
|
tlsConn := tls.Client(conn, tlsCfg)
|
||||||
if err := tlsConn.Handshake(); err != nil {
|
if err := tlsConn.Handshake(); err != nil {
|
||||||
_ = conn.Close()
|
_ = conn.Close()
|
||||||
return proxy.Result{OK: false, Error: err}
|
return proxy.Result{OK: false, Error: proxy.NewScopedError("proxy.pool.tls", "handshake failed", err)}
|
||||||
}
|
}
|
||||||
if fp := strings.TrimSpace(strings.ToLower(c.config.TLSFingerprint)); fp != "" {
|
if fp := strings.TrimSpace(strings.ToLower(c.config.TLSFingerprint)); fp != "" {
|
||||||
cert := tlsConn.ConnectionState().PeerCertificates
|
cert := tlsConn.ConnectionState().PeerCertificates
|
||||||
if len(cert) == 0 {
|
if len(cert) == 0 {
|
||||||
_ = tlsConn.Close()
|
_ = tlsConn.Close()
|
||||||
return proxy.Result{OK: false, Error: errors.New("missing certificate")}
|
return proxy.Result{OK: false, Error: proxy.NewScopedError("proxy.pool.tls", "missing certificate", nil)}
|
||||||
}
|
}
|
||||||
sum := sha256.Sum256(cert[0].Raw)
|
sum := sha256.Sum256(cert[0].Raw)
|
||||||
if hex.EncodeToString(sum[:]) != fp {
|
if hex.EncodeToString(sum[:]) != fp {
|
||||||
_ = tlsConn.Close()
|
_ = tlsConn.Close()
|
||||||
return proxy.Result{OK: false, Error: errors.New("tls fingerprint mismatch")}
|
return proxy.Result{OK: false, Error: proxy.NewScopedError("proxy.pool.tls", "tls fingerprint mismatch", nil)}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.conn = tlsConn
|
c.conn = tlsConn
|
||||||
|
|
@ -211,16 +210,17 @@ func (c *StratumClient) writeJSON(payload any) error {
|
||||||
c.sendMu.Lock()
|
c.sendMu.Lock()
|
||||||
defer c.sendMu.Unlock()
|
defer c.sendMu.Unlock()
|
||||||
if c.conn == nil {
|
if c.conn == nil {
|
||||||
return errors.New("connection is nil")
|
return proxy.NewScopedError("proxy.pool.client", "connection is nil", nil)
|
||||||
}
|
}
|
||||||
data, err := json.Marshal(payload)
|
data, err := json.Marshal(payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return proxy.NewScopedError("proxy.pool.client", "marshal request failed", err)
|
||||||
}
|
}
|
||||||
data = append(data, '\n')
|
data = append(data, '\n')
|
||||||
_, err = c.conn.Write(data)
|
_, err = c.conn.Write(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.notifyDisconnect()
|
c.notifyDisconnect()
|
||||||
|
return proxy.NewScopedError("proxy.pool.client", "write request failed", err)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
@ -40,7 +39,7 @@ type MinerSnapshot struct {
|
||||||
// }
|
// }
|
||||||
func New(config *Config) (*Proxy, Result) {
|
func New(config *Config) (*Proxy, Result) {
|
||||||
if config == nil {
|
if config == nil {
|
||||||
return nil, newErrorResult(errors.New("config is nil"))
|
return nil, newErrorResult(NewScopedError("proxy", "config is nil", nil))
|
||||||
}
|
}
|
||||||
if result := config.Validate(); !result.OK {
|
if result := config.Validate(); !result.OK {
|
||||||
return nil, result
|
return nil, result
|
||||||
|
|
@ -490,11 +489,11 @@ func buildTLSConfig(cfg TLSConfig) (*tls.Config, Result) {
|
||||||
return nil, newSuccessResult()
|
return nil, newSuccessResult()
|
||||||
}
|
}
|
||||||
if cfg.CertFile == "" || cfg.KeyFile == "" {
|
if cfg.CertFile == "" || cfg.KeyFile == "" {
|
||||||
return nil, newErrorResult(errors.New("tls certificate or key path is empty"))
|
return nil, newErrorResult(NewScopedError("proxy.tls", "tls certificate or key path is empty", nil))
|
||||||
}
|
}
|
||||||
cert, err := tls.LoadX509KeyPair(cfg.CertFile, cfg.KeyFile)
|
cert, err := tls.LoadX509KeyPair(cfg.CertFile, cfg.KeyFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, newErrorResult(err)
|
return nil, newErrorResult(NewScopedError("proxy.tls", "load certificate failed", err))
|
||||||
}
|
}
|
||||||
tlsConfig := &tls.Config{Certificates: []tls.Certificate{cert}}
|
tlsConfig := &tls.Config{Certificates: []tls.Certificate{cert}}
|
||||||
applyTLSProtocols(tlsConfig, cfg.Protocols)
|
applyTLSProtocols(tlsConfig, cfg.Protocols)
|
||||||
|
|
@ -646,7 +645,7 @@ func (p *Proxy) startMonitoringServer() bool {
|
||||||
p.httpServer = &http.Server{Addr: addr, Handler: mux}
|
p.httpServer = &http.Server{Addr: addr, Handler: mux}
|
||||||
go func() {
|
go func() {
|
||||||
err := p.httpServer.Serve(listener)
|
err := p.httpServer.Serve(listener)
|
||||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
if err != nil && err != http.ErrServerClosed {
|
||||||
p.Stop()
|
p.Stop()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
@ -1769,17 +1768,17 @@ func (s *Server) Stop() {
|
||||||
|
|
||||||
func (s *Server) listen() Result {
|
func (s *Server) listen() Result {
|
||||||
if s == nil {
|
if s == nil {
|
||||||
return newErrorResult(errors.New("server is nil"))
|
return newErrorResult(NewScopedError("proxy.server", "server is nil", nil))
|
||||||
}
|
}
|
||||||
if s.listener != nil {
|
if s.listener != nil {
|
||||||
return newSuccessResult()
|
return newSuccessResult()
|
||||||
}
|
}
|
||||||
if s.addr.TLS && s.tlsCfg == nil {
|
if s.addr.TLS && s.tlsCfg == nil {
|
||||||
return newErrorResult(errors.New("tls listener requires a tls config"))
|
return newErrorResult(NewScopedError("proxy.server", "tls listener requires a tls config", nil))
|
||||||
}
|
}
|
||||||
ln, err := net.Listen("tcp", net.JoinHostPort(s.addr.Host, strconv.Itoa(int(s.addr.Port))))
|
ln, err := net.Listen("tcp", net.JoinHostPort(s.addr.Host, strconv.Itoa(int(s.addr.Port))))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return newErrorResult(err)
|
return newErrorResult(NewScopedError("proxy.server", "listen failed", err))
|
||||||
}
|
}
|
||||||
if s.tlsCfg != nil {
|
if s.tlsCfg != nil {
|
||||||
ln = tls.NewListener(ln, s.tlsCfg)
|
ln = tls.NewListener(ln, s.tlsCfg)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue