diff --git a/go.mod b/go.mod index 420d888..56bd6a1 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/go-i2p/i2pkeys v0.33.92 github.com/go-i2p/onramp v0.33.93-0.20251016200402-d3ac8f5353c5 github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301 + gopkg.in/yaml.v2 v2.4.0 ) require ( @@ -38,7 +39,6 @@ require ( golang.org/x/sys v0.34.0 // indirect golang.org/x/text v0.27.0 // indirect golang.org/x/time v0.9.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect ) replace github.com/go-i2p/go-i2ptunnel-config => ../go-i2ptunnel-config diff --git a/lib/core/validate/validate.go b/lib/core/validate/validate.go new file mode 100644 index 0000000..5279b32 --- /dev/null +++ b/lib/core/validate/validate.go @@ -0,0 +1,267 @@ +package validate + +import ( + "fmt" + "net" + "strconv" + "strings" + + "github.com/go-i2p/i2pkeys" +) + +// ValidationError represents a configuration validation error with actionable context. +type ValidationError struct { + Field string // Field name that failed validation + Value string // The invalid value + Message string // User-friendly error message + Hint string // Suggestion for fixing the error +} + +func (e *ValidationError) Error() string { + if e.Hint != "" { + return fmt.Sprintf("%s: %s (hint: %s)", e.Field, e.Message, e.Hint) + } + return fmt.Sprintf("%s: %s", e.Field, e.Message) +} + +// Port validates a port number is in the valid range (1-65535). +// For non-root deployments, ports below 1024 trigger a warning in the hint. +// +// Why: Privileged ports (<1024) require root/capabilities on Unix systems. +// Production deployments should use unprivileged ports for security. +func Port(port int) error { + if port < 1 || port > 65535 { + return &ValidationError{ + Field: "port", + Value: strconv.Itoa(port), + Message: "port must be between 1 and 65535", + Hint: "choose a port in the valid range (1024-65535 recommended for non-root)", + } + } + // Warn about privileged ports but don't fail validation + if port < 1024 { + return &ValidationError{ + Field: "port", + Value: strconv.Itoa(port), + Message: "port is in privileged range (<1024)", + Hint: "requires root privileges or capabilities; consider using port >=1024", + } + } + return nil +} + +// PortString validates a port number provided as a string. +// Returns the parsed port value and any validation error. +func PortString(portStr string) (int, error) { + port, err := strconv.Atoi(portStr) + if err != nil { + return 0, &ValidationError{ + Field: "port", + Value: portStr, + Message: "invalid port format", + Hint: "port must be a numeric value between 1 and 65535", + } + } + if err := Port(port); err != nil { + return 0, err + } + return port, nil +} + +// I2PAddress validates an I2P destination address format. +// Accepts base32.i2p, base64, or full I2P addresses. +// +// Why: Invalid addresses cause runtime connection failures. Fail fast at config time. +// Design: Uses i2pkeys.Lookup which handles multiple formats and performs validation. +func I2PAddress(addr string) error { + if addr == "" { + return &ValidationError{ + Field: "target", + Value: addr, + Message: "I2P address cannot be empty", + Hint: "provide a valid base32.i2p or base64 I2P address", + } + } + + // Use i2pkeys library to validate address format + // This handles base32, base64, and full destination formats + _, err := i2pkeys.Lookup(addr) + if err != nil { + return &ValidationError{ + Field: "target", + Value: addr, + Message: "invalid I2P address format", + Hint: "provide a valid base32.i2p address (e.g., example.b32.i2p) or base64 destination", + } + } + return nil +} + +// NetworkAddress validates a network address in host:port format. +// Used for local service targets (e.g., localhost:8080, 127.0.0.1:3000). +// +// Why: Server tunnels forward to local services. Invalid addresses cause runtime failures. +// Design: Uses net.ResolveTCPAddr for validation, ensuring both hostname and port are valid. +func NetworkAddress(addr string) error { + if addr == "" { + return &ValidationError{ + Field: "target", + Value: addr, + Message: "network address cannot be empty", + Hint: "provide a valid host:port address (e.g., localhost:8080, 127.0.0.1:3000)", + } + } + + // Validate format using standard library + tcpAddr, err := net.ResolveTCPAddr("tcp", addr) + if err != nil { + return &ValidationError{ + Field: "target", + Value: addr, + Message: "invalid network address format", + Hint: "provide address as host:port (e.g., localhost:8080, 192.168.1.1:9000)", + } + } + + // Validate the port component + if err := Port(tcpAddr.Port); err != nil { + // Wrap the port error with network address context + if portErr, ok := err.(*ValidationError); ok { + return &ValidationError{ + Field: "target", + Value: addr, + Message: portErr.Message, + Hint: portErr.Hint, + } + } + return err + } + + return nil +} + +// Interface validates a network interface address for binding. +// Accepts IP addresses or empty string for all interfaces. +// +// Why: Invalid interface addresses cause bind failures at startup. +// Design: Uses net.ParseIP for validation. Empty string means bind to all interfaces (0.0.0.0). +func Interface(iface string) error { + // Empty string means bind to all interfaces (standard behavior) + if iface == "" { + return nil + } + + // Validate IP address format + ip := net.ParseIP(iface) + if ip == nil { + return &ValidationError{ + Field: "interface", + Value: iface, + Message: "invalid interface address", + Hint: "provide a valid IP address (e.g., 127.0.0.1, ::1) or leave empty for all interfaces", + } + } + + return nil +} + +// RequiredString validates that a required string field is not empty. +// Used for critical configuration fields like tunnel name and type. +func RequiredString(field, value string) error { + value = strings.TrimSpace(value) + if value == "" { + return &ValidationError{ + Field: field, + Value: value, + Message: fmt.Sprintf("%s is required", field), + Hint: fmt.Sprintf("provide a non-empty value for %s", field), + } + } + return nil +} + +// TunnelType validates that a tunnel type matches one of the supported types. +// Supported types: tcpclient, tcpserver, httpclient, httpserver, ircclient, ircserver, +// udpclient, udpserver, socksclient. +func TunnelType(tunnelType string) error { + validTypes := map[string]bool{ + "tcpclient": true, + "tcpserver": true, + "httpclient": true, + "httpserver": true, + "ircclient": true, + "ircserver": true, + "udpclient": true, + "udpserver": true, + "socksclient": true, + } + + if !validTypes[tunnelType] { + return &ValidationError{ + Field: "type", + Value: tunnelType, + Message: "unsupported tunnel type", + Hint: "supported types: tcpclient, tcpserver, httpclient, httpserver, ircclient, ircserver, udpclient, udpserver, socksclient", + } + } + return nil +} + +// MaxConnections validates the maximum connections setting for server tunnels. +// Zero means unlimited. Negative values are rejected. +// +// Why: Limits resource usage and prevents DoS. Validation prevents configuration mistakes. +func MaxConnections(maxConns int) error { + if maxConns < 0 { + return &ValidationError{ + Field: "maxconns", + Value: strconv.Itoa(maxConns), + Message: "maxconns cannot be negative", + Hint: "use 0 for unlimited connections or a positive number to set a limit", + } + } + // Warn about very high connection limits + if maxConns > 10000 { + return &ValidationError{ + Field: "maxconns", + Value: strconv.Itoa(maxConns), + Message: "maxconns is very high (>10000)", + Hint: "high connection limits may cause resource exhaustion; consider a lower value", + } + } + return nil +} + +// RateLimit validates the rate limit setting for server tunnels. +// Zero means no rate limiting. Negative values are rejected. +// +// Why: Rate limiting protects against abuse. Validation prevents configuration errors. +func RateLimit(rateLimit float64) error { + if rateLimit < 0 { + return &ValidationError{ + Field: "ratelimit", + Value: strconv.FormatFloat(rateLimit, 'f', -1, 64), + Message: "ratelimit cannot be negative", + Hint: "use 0 for no rate limiting or a positive number for requests/second", + } + } + return nil +} + +// RateLimitString validates a rate limit provided as a string. +// Returns the parsed rate limit value and any validation error. +func RateLimitString(rateLimitStr string) (float64, error) { + rateLimit, err := strconv.ParseFloat(rateLimitStr, 64) + if err != nil { + return 0, &ValidationError{ + Field: "ratelimit", + Value: rateLimitStr, + Message: "invalid ratelimit format", + Hint: "ratelimit must be a numeric value (e.g., 10.0, 100.5)", + } + } + if err := RateLimit(rateLimit); err != nil { + return 0, err + } + return rateLimit, nil +} diff --git a/lib/core/validate/validate_test.go b/lib/core/validate/validate_test.go new file mode 100644 index 0000000..3948e98 --- /dev/null +++ b/lib/core/validate/validate_test.go @@ -0,0 +1,598 @@ +package validate + +import ( + "strings" + "testing" +) + +// TestPort verifies port validation with various valid and invalid values +func TestPort(t *testing.T) { + tests := []struct { + name string + port int + wantError bool + wantHint string + }{ + { + name: "valid unprivileged port", + port: 8080, + wantError: false, + }, + { + name: "valid high port", + port: 65535, + wantError: false, + }, + { + name: "valid low unprivileged port", + port: 1024, + wantError: false, + }, + { + name: "privileged port with warning", + port: 80, + wantError: true, + wantHint: "requires root privileges", + }, + { + name: "port zero", + port: 0, + wantError: true, + }, + { + name: "negative port", + port: -1, + wantError: true, + }, + { + name: "port too high", + port: 65536, + wantError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := Port(tt.port) + if tt.wantError && err == nil { + t.Errorf("Port(%d) expected error but got none", tt.port) + } + if !tt.wantError && err != nil { + t.Errorf("Port(%d) unexpected error: %v", tt.port, err) + } + if tt.wantHint != "" && err != nil { + if !strings.Contains(err.Error(), tt.wantHint) { + t.Errorf("Port(%d) error missing hint '%s': %v", tt.port, tt.wantHint, err) + } + } + }) + } +} + +// TestPortString verifies port string validation and parsing +func TestPortString(t *testing.T) { + tests := []struct { + name string + portStr string + wantPort int + wantError bool + }{ + { + name: "valid port string", + portStr: "8080", + wantPort: 8080, + wantError: false, + }, + { + name: "invalid non-numeric", + portStr: "abc", + wantError: true, + }, + { + name: "invalid float", + portStr: "80.5", + wantError: true, + }, + { + name: "invalid empty", + portStr: "", + wantError: true, + }, + { + name: "valid but privileged", + portStr: "443", + wantPort: 443, + wantError: true, // Warning for privileged port + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + port, err := PortString(tt.portStr) + if tt.wantError && err == nil { + t.Errorf("PortString(%q) expected error but got none", tt.portStr) + } + if !tt.wantError && err != nil { + t.Errorf("PortString(%q) unexpected error: %v", tt.portStr, err) + } + if !tt.wantError && port != tt.wantPort { + t.Errorf("PortString(%q) = %d, want %d", tt.portStr, port, tt.wantPort) + } + }) + } +} + +// TestI2PAddress verifies I2P address validation +func TestI2PAddress(t *testing.T) { + tests := []struct { + name string + addr string + wantError bool + }{ + { + name: "empty address", + addr: "", + wantError: true, + }, + { + name: "invalid format", + addr: "not-an-address", + wantError: true, + }, + { + name: "invalid base32", + addr: "invalid!!!.b32.i2p", + wantError: true, + }, + // Note: Valid addresses require actual I2P keys or lookups + // The i2pkeys.Lookup function validates format and may do DNS lookups + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := I2PAddress(tt.addr) + if tt.wantError && err == nil { + t.Errorf("I2PAddress(%q) expected error but got none", tt.addr) + } + if !tt.wantError && err != nil { + t.Errorf("I2PAddress(%q) unexpected error: %v", tt.addr, err) + } + }) + } +} + +// TestNetworkAddress verifies network address validation +func TestNetworkAddress(t *testing.T) { + tests := []struct { + name string + addr string + wantError bool + }{ + { + name: "valid localhost", + addr: "localhost:8080", + wantError: false, + }, + { + name: "valid IPv4", + addr: "127.0.0.1:3000", + wantError: false, + }, + { + name: "valid IPv6", + addr: "[::1]:8080", + wantError: false, + }, + { + name: "empty address", + addr: "", + wantError: true, + }, + { + name: "missing port", + addr: "localhost", + wantError: true, + }, + { + name: "invalid port", + addr: "localhost:99999", + wantError: true, + }, + { + name: "invalid format", + addr: "not-valid", + wantError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := NetworkAddress(tt.addr) + if tt.wantError && err == nil { + t.Errorf("NetworkAddress(%q) expected error but got none", tt.addr) + } + if !tt.wantError && err != nil { + t.Errorf("NetworkAddress(%q) unexpected error: %v", tt.addr, err) + } + }) + } +} + +// TestInterface verifies network interface validation +func TestInterface(t *testing.T) { + tests := []struct { + name string + iface string + wantError bool + }{ + { + name: "empty (all interfaces)", + iface: "", + wantError: false, + }, + { + name: "localhost IPv4", + iface: "127.0.0.1", + wantError: false, + }, + { + name: "localhost IPv6", + iface: "::1", + wantError: false, + }, + { + name: "any IPv4", + iface: "0.0.0.0", + wantError: false, + }, + { + name: "any IPv6", + iface: "::", + wantError: false, + }, + { + name: "invalid format", + iface: "not-an-ip", + wantError: true, + }, + { + name: "invalid IPv4", + iface: "999.999.999.999", + wantError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := Interface(tt.iface) + if tt.wantError && err == nil { + t.Errorf("Interface(%q) expected error but got none", tt.iface) + } + if !tt.wantError && err != nil { + t.Errorf("Interface(%q) unexpected error: %v", tt.iface, err) + } + }) + } +} + +// TestRequiredString verifies required field validation +func TestRequiredString(t *testing.T) { + tests := []struct { + name string + field string + value string + wantError bool + }{ + { + name: "valid value", + field: "name", + value: "my-tunnel", + wantError: false, + }, + { + name: "empty string", + field: "name", + value: "", + wantError: true, + }, + { + name: "whitespace only", + field: "name", + value: " ", + wantError: true, + }, + { + name: "tabs only", + field: "type", + value: "\t\t", + wantError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := RequiredString(tt.field, tt.value) + if tt.wantError && err == nil { + t.Errorf("RequiredString(%q, %q) expected error but got none", tt.field, tt.value) + } + if !tt.wantError && err != nil { + t.Errorf("RequiredString(%q, %q) unexpected error: %v", tt.field, tt.value, err) + } + if err != nil { + // Verify error contains field name + if !strings.Contains(err.Error(), tt.field) { + t.Errorf("RequiredString error missing field name '%s': %v", tt.field, err) + } + } + }) + } +} + +// TestTunnelType verifies tunnel type validation +func TestTunnelType(t *testing.T) { + tests := []struct { + name string + tunnelType string + wantError bool + }{ + { + name: "tcpclient", + tunnelType: "tcpclient", + wantError: false, + }, + { + name: "tcpserver", + tunnelType: "tcpserver", + wantError: false, + }, + { + name: "httpclient", + tunnelType: "httpclient", + wantError: false, + }, + { + name: "httpserver", + tunnelType: "httpserver", + wantError: false, + }, + { + name: "ircclient", + tunnelType: "ircclient", + wantError: false, + }, + { + name: "ircserver", + tunnelType: "ircserver", + wantError: false, + }, + { + name: "udpclient", + tunnelType: "udpclient", + wantError: false, + }, + { + name: "udpserver", + tunnelType: "udpserver", + wantError: false, + }, + { + name: "socksclient", + tunnelType: "socksclient", + wantError: false, + }, + { + name: "invalid type", + tunnelType: "invalidtype", + wantError: true, + }, + { + name: "empty type", + tunnelType: "", + wantError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := TunnelType(tt.tunnelType) + if tt.wantError && err == nil { + t.Errorf("TunnelType(%q) expected error but got none", tt.tunnelType) + } + if !tt.wantError && err != nil { + t.Errorf("TunnelType(%q) unexpected error: %v", tt.tunnelType, err) + } + }) + } +} + +// TestMaxConnections verifies max connections validation +func TestMaxConnections(t *testing.T) { + tests := []struct { + name string + maxConns int + wantError bool + wantHint string + }{ + { + name: "unlimited (zero)", + maxConns: 0, + wantError: false, + }, + { + name: "reasonable limit", + maxConns: 100, + wantError: false, + }, + { + name: "high limit", + maxConns: 1000, + wantError: false, + }, + { + name: "very high limit with warning", + maxConns: 15000, + wantError: true, + wantHint: "resource exhaustion", + }, + { + name: "negative value", + maxConns: -1, + wantError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := MaxConnections(tt.maxConns) + if tt.wantError && err == nil { + t.Errorf("MaxConnections(%d) expected error but got none", tt.maxConns) + } + if !tt.wantError && err != nil { + t.Errorf("MaxConnections(%d) unexpected error: %v", tt.maxConns, err) + } + if tt.wantHint != "" && err != nil { + if !strings.Contains(err.Error(), tt.wantHint) { + t.Errorf("MaxConnections(%d) error missing hint '%s': %v", tt.maxConns, tt.wantHint, err) + } + } + }) + } +} + +// TestRateLimit verifies rate limit validation +func TestRateLimit(t *testing.T) { + tests := []struct { + name string + rateLimit float64 + wantError bool + }{ + { + name: "no limit (zero)", + rateLimit: 0, + wantError: false, + }, + { + name: "reasonable limit", + rateLimit: 10.0, + wantError: false, + }, + { + name: "high limit", + rateLimit: 1000.5, + wantError: false, + }, + { + name: "fractional limit", + rateLimit: 0.5, + wantError: false, + }, + { + name: "negative value", + rateLimit: -1.0, + wantError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := RateLimit(tt.rateLimit) + if tt.wantError && err == nil { + t.Errorf("RateLimit(%f) expected error but got none", tt.rateLimit) + } + if !tt.wantError && err != nil { + t.Errorf("RateLimit(%f) unexpected error: %v", tt.rateLimit, err) + } + }) + } +} + +// TestRateLimitString verifies rate limit string validation and parsing +func TestRateLimitString(t *testing.T) { + tests := []struct { + name string + rateLimitStr string + wantRateLimit float64 + wantError bool + }{ + { + name: "valid rate limit", + rateLimitStr: "10.5", + wantRateLimit: 10.5, + wantError: false, + }, + { + name: "zero limit", + rateLimitStr: "0", + wantRateLimit: 0, + wantError: false, + }, + { + name: "invalid non-numeric", + rateLimitStr: "abc", + wantError: true, + }, + { + name: "invalid empty", + rateLimitStr: "", + wantError: true, + }, + { + name: "negative value", + rateLimitStr: "-5.0", + wantError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rateLimit, err := RateLimitString(tt.rateLimitStr) + if tt.wantError && err == nil { + t.Errorf("RateLimitString(%q) expected error but got none", tt.rateLimitStr) + } + if !tt.wantError && err != nil { + t.Errorf("RateLimitString(%q) unexpected error: %v", tt.rateLimitStr, err) + } + if !tt.wantError && rateLimit != tt.wantRateLimit { + t.Errorf("RateLimitString(%q) = %f, want %f", tt.rateLimitStr, rateLimit, tt.wantRateLimit) + } + }) + } +} + +// TestValidationError verifies ValidationError formatting +func TestValidationError(t *testing.T) { + tests := []struct { + name string + err *ValidationError + wantError string + }{ + { + name: "error with hint", + err: &ValidationError{ + Field: "port", + Value: "abc", + Message: "invalid format", + Hint: "use numeric value", + }, + wantError: "port: invalid format (hint: use numeric value)", + }, + { + name: "error without hint", + err: &ValidationError{ + Field: "name", + Value: "", + Message: "cannot be empty", + }, + wantError: "name: cannot be empty", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.err.Error() + if got != tt.wantError { + t.Errorf("ValidationError.Error() = %q, want %q", got, tt.wantError) + } + }) + } +} diff --git a/lib/http/client/httpclient.go b/lib/http/client/httpclient.go index 9c4d8dc..e02762c 100644 --- a/lib/http/client/httpclient.go +++ b/lib/http/client/httpclient.go @@ -34,6 +34,7 @@ import ( httpinspector "github.com/go-i2p/go-connfilter/http" i2pconv "github.com/go-i2p/go-i2ptunnel-config/lib" i2ptunnel "github.com/go-i2p/go-i2ptunnel/lib/core" + "github.com/go-i2p/go-i2ptunnel/lib/core/validate" "github.com/go-i2p/onramp" "github.com/elazarl/goproxy" @@ -176,19 +177,25 @@ func (h *HTTPClient) Options() map[string]string { // Set the tunnel's options func (h *HTTPClient) SetOptions(opts map[string]string) error { - // Apply configuration options from the map + // Apply configuration options from the map with validation if name, ok := opts["name"]; ok { + if err := validate.RequiredString("name", name); err != nil { + return err + } h.TunnelConfig.Name = name } if iface, ok := opts["interface"]; ok { + if err := validate.Interface(iface); err != nil { + return err + } h.TunnelConfig.Interface = iface } if portStr, ok := opts["port"]; ok { - if port, err := strconv.Atoi(portStr); err == nil { - h.TunnelConfig.Port = port - } else { - return fmt.Errorf("invalid port value: %s", portStr) + port, err := validate.PortString(portStr) + if err != nil { + return err } + h.TunnelConfig.Port = port } return nil } diff --git a/lib/http/server/httpserver.go b/lib/http/server/httpserver.go index e4d5701..94ae4f6 100644 --- a/lib/http/server/httpserver.go +++ b/lib/http/server/httpserver.go @@ -34,6 +34,7 @@ import ( "github.com/go-i2p/go-forward/stream" i2pconv "github.com/go-i2p/go-i2ptunnel-config/lib" i2ptunnel "github.com/go-i2p/go-i2ptunnel/lib/core" + "github.com/go-i2p/go-i2ptunnel/lib/core/validate" limitedlistener "github.com/go-i2p/go-limit" "github.com/go-i2p/onramp" ) @@ -171,33 +172,42 @@ func (h *HTTPServer) Options() map[string]string { // Set the tunnel's options func (h *HTTPServer) SetOptions(opts map[string]string) error { - // Apply configuration options from the map + // Apply configuration options from the map with validation if name, ok := opts["name"]; ok { + if err := validate.RequiredString("name", name); err != nil { + return err + } h.TunnelConfig.Name = name } if iface, ok := opts["interface"]; ok { + if err := validate.Interface(iface); err != nil { + return err + } h.TunnelConfig.Interface = iface } if portStr, ok := opts["port"]; ok { - if port, err := strconv.Atoi(portStr); err == nil { - h.TunnelConfig.Port = port - } else { - return fmt.Errorf("invalid port value: %s", portStr) + port, err := validate.PortString(portStr) + if err != nil { + return err } + h.TunnelConfig.Port = port } if maxconnsStr, ok := opts["maxconns"]; ok { - if maxconns, err := strconv.Atoi(maxconnsStr); err == nil { - h.LimitedConfig.MaxConns = maxconns - } else { + maxconns, err := strconv.Atoi(maxconnsStr) + if err != nil { return fmt.Errorf("invalid maxconns value: %s", maxconnsStr) } + if err := validate.MaxConnections(maxconns); err != nil { + return err + } + h.LimitedConfig.MaxConns = maxconns } if ratelimitStr, ok := opts["ratelimit"]; ok { - if ratelimit, err := strconv.ParseFloat(ratelimitStr, 64); err == nil { - h.LimitedConfig.RateLimit = ratelimit - } else { - return fmt.Errorf("invalid ratelimit value: %s", ratelimitStr) + ratelimit, err := validate.RateLimitString(ratelimitStr) + if err != nil { + return err } + h.LimitedConfig.RateLimit = ratelimit } return nil } diff --git a/lib/irc/client/client.go b/lib/irc/client/client.go index 3400551..e609676 100644 --- a/lib/irc/client/client.go +++ b/lib/irc/client/client.go @@ -24,6 +24,7 @@ import ( "github.com/go-i2p/go-forward/stream" i2pconv "github.com/go-i2p/go-i2ptunnel-config/lib" i2ptunnel "github.com/go-i2p/go-i2ptunnel/lib/core" + "github.com/go-i2p/go-i2ptunnel/lib/core/validate" "github.com/go-i2p/i2pkeys" "github.com/go-i2p/onramp" ) @@ -157,21 +158,30 @@ func (i *IRCClient) Options() map[string]string { // Set the tunnel's options func (i *IRCClient) SetOptions(opts map[string]string) error { - // Apply configuration options from the map + // Apply configuration options from the map with validation if name, ok := opts["name"]; ok { + if err := validate.RequiredString("name", name); err != nil { + return err + } i.TunnelConfig.Name = name } if iface, ok := opts["interface"]; ok { + if err := validate.Interface(iface); err != nil { + return err + } i.TunnelConfig.Interface = iface } if portStr, ok := opts["port"]; ok { - if port, err := strconv.Atoi(portStr); err == nil { - i.TunnelConfig.Port = port - } else { - return fmt.Errorf("invalid port value: %s", portStr) + port, err := validate.PortString(portStr) + if err != nil { + return err } + i.TunnelConfig.Port = port } if target, ok := opts["target"]; ok { + if err := validate.I2PAddress(target); err != nil { + return err + } addr, err := i2pkeys.Lookup(target) if err != nil { return fmt.Errorf("invalid target address: %w", err) diff --git a/lib/irc/server/sanitize.go b/lib/irc/server/sanitize.go index 964940e..051c579 100644 --- a/lib/irc/server/sanitize.go +++ b/lib/irc/server/sanitize.go @@ -9,10 +9,8 @@ import ( ircinspector "github.com/go-i2p/go-connfilter/irc" ) -var ( - // Pattern to match user@host format - userHostPattern = regexp.MustCompile(`@[^\s]+`) -) +// Pattern to match user@host format +var userHostPattern = regexp.MustCompile(`@[^\s]+`) // ApplyIRCServerFilters wraps a listener with IRC server-side filtering // Blocks administrative commands and masks host information diff --git a/lib/irc/server/server.go b/lib/irc/server/server.go index 758230d..7a98bd3 100644 --- a/lib/irc/server/server.go +++ b/lib/irc/server/server.go @@ -24,6 +24,7 @@ import ( "github.com/go-i2p/go-forward/stream" i2pconv "github.com/go-i2p/go-i2ptunnel-config/lib" i2ptunnel "github.com/go-i2p/go-i2ptunnel/lib/core" + "github.com/go-i2p/go-i2ptunnel/lib/core/validate" limitedlistener "github.com/go-i2p/go-limit" "github.com/go-i2p/onramp" ) @@ -161,33 +162,42 @@ func (i *IRCServer) Options() map[string]string { // Set the tunnel's options func (i *IRCServer) SetOptions(opts map[string]string) error { - // Apply configuration options from the map + // Apply configuration options from the map with validation if name, ok := opts["name"]; ok { + if err := validate.RequiredString("name", name); err != nil { + return err + } i.TunnelConfig.Name = name } if iface, ok := opts["interface"]; ok { + if err := validate.Interface(iface); err != nil { + return err + } i.TunnelConfig.Interface = iface } if portStr, ok := opts["port"]; ok { - if port, err := strconv.Atoi(portStr); err == nil { - i.TunnelConfig.Port = port - } else { - return fmt.Errorf("invalid port value: %s", portStr) + port, err := validate.PortString(portStr) + if err != nil { + return err } + i.TunnelConfig.Port = port } if maxconnsStr, ok := opts["maxconns"]; ok { - if maxconns, err := strconv.Atoi(maxconnsStr); err == nil { - i.LimitedConfig.MaxConns = maxconns - } else { + maxconns, err := strconv.Atoi(maxconnsStr) + if err != nil { return fmt.Errorf("invalid maxconns value: %s", maxconnsStr) } + if err := validate.MaxConnections(maxconns); err != nil { + return err + } + i.LimitedConfig.MaxConns = maxconns } if ratelimitStr, ok := opts["ratelimit"]; ok { - if ratelimit, err := strconv.ParseFloat(ratelimitStr, 64); err == nil { - i.LimitedConfig.RateLimit = ratelimit - } else { - return fmt.Errorf("invalid ratelimit value: %s", ratelimitStr) + ratelimit, err := validate.RateLimitString(ratelimitStr) + if err != nil { + return err } + i.LimitedConfig.RateLimit = ratelimit } return nil } diff --git a/lib/socks/client/socks.go b/lib/socks/client/socks.go index b522329..7f7a650 100644 --- a/lib/socks/client/socks.go +++ b/lib/socks/client/socks.go @@ -49,6 +49,7 @@ import ( i2pconv "github.com/go-i2p/go-i2ptunnel-config/lib" i2ptunnel "github.com/go-i2p/go-i2ptunnel/lib/core" + "github.com/go-i2p/go-i2ptunnel/lib/core/validate" "github.com/go-i2p/onramp" "github.com/txthinking/socks5" ) @@ -188,19 +189,25 @@ func (s *SOCKS) Options() map[string]string { // Set the tunnel's options func (s *SOCKS) SetOptions(opts map[string]string) error { - // Apply configuration options from the map + // Apply configuration options from the map with validation if name, ok := opts["name"]; ok { + if err := validate.RequiredString("name", name); err != nil { + return err + } s.TunnelConfig.Name = name } if iface, ok := opts["interface"]; ok { + if err := validate.Interface(iface); err != nil { + return err + } s.TunnelConfig.Interface = iface } if portStr, ok := opts["port"]; ok { - if port, err := strconv.Atoi(portStr); err == nil { - s.TunnelConfig.Port = port - } else { - return fmt.Errorf("invalid port value: %s", portStr) + port, err := validate.PortString(portStr) + if err != nil { + return err } + s.TunnelConfig.Port = port } return nil } diff --git a/lib/tcp/client/loadconfig_test.go b/lib/tcp/client/loadconfig_test.go index 92d0c75..06533e3 100644 --- a/lib/tcp/client/loadconfig_test.go +++ b/lib/tcp/client/loadconfig_test.go @@ -24,7 +24,7 @@ func TestLoadConfig_Success(t *testing.T) { port: 9999 target: ukeu3k5oycgaauneqgtnvselmt4yemvoilkln7jpvamvfx7dnkdq.b32.i2p ` - if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil { + if err := os.WriteFile(configPath, []byte(configContent), 0o644); err != nil { t.Fatalf("Failed to create config file: %v", err) } @@ -75,7 +75,7 @@ func TestLoadConfig_RunningTunnel(t *testing.T) { port: 9999 target: ukeu3k5oycgaauneqgtnvselmt4yemvoilkln7jpvamvfx7dnkdq.b32.i2p ` - if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil { + if err := os.WriteFile(configPath, []byte(configContent), 0o644); err != nil { t.Fatalf("Failed to create config file: %v", err) } @@ -143,7 +143,7 @@ func TestLoadConfig_WrongType(t *testing.T) { port: 9999 target: localhost:80 ` - if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil { + if err := os.WriteFile(configPath, []byte(configContent), 0o644); err != nil { t.Fatalf("Failed to create config file: %v", err) } @@ -187,7 +187,7 @@ func TestLoadConfig_InvalidTarget(t *testing.T) { port: 9999 target: invalid-address-not-base32 ` - if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil { + if err := os.WriteFile(configPath, []byte(configContent), 0o644); err != nil { t.Fatalf("Failed to create config file: %v", err) } @@ -226,7 +226,7 @@ interface=127.0.0.1 listenPort=7777 targetDestination=ukeu3k5oycgaauneqgtnvselmt4yemvoilkln7jpvamvfx7dnkdq.b32.i2p ` - if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil { + if err := os.WriteFile(configPath, []byte(configContent), 0o644); err != nil { t.Fatalf("Failed to create config file: %v", err) } diff --git a/lib/tcp/client/tcpclient.go b/lib/tcp/client/tcpclient.go index e80d90f..3b69053 100644 --- a/lib/tcp/client/tcpclient.go +++ b/lib/tcp/client/tcpclient.go @@ -31,6 +31,7 @@ import ( "github.com/go-i2p/go-forward/stream" i2pconv "github.com/go-i2p/go-i2ptunnel-config/lib" i2ptunnel "github.com/go-i2p/go-i2ptunnel/lib/core" + "github.com/go-i2p/go-i2ptunnel/lib/core/validate" "github.com/go-i2p/i2pkeys" "github.com/go-i2p/onramp" ) @@ -160,21 +161,30 @@ func (t *TCPClient) Options() map[string]string { // Set the tunnel's options func (t *TCPClient) SetOptions(opts map[string]string) error { - // Apply configuration options from the map + // Apply configuration options from the map with validation if name, ok := opts["name"]; ok { + if err := validate.RequiredString("name", name); err != nil { + return err + } t.TunnelConfig.Name = name } if iface, ok := opts["interface"]; ok { + if err := validate.Interface(iface); err != nil { + return err + } t.TunnelConfig.Interface = iface } if portStr, ok := opts["port"]; ok { - if port, err := strconv.Atoi(portStr); err == nil { - t.TunnelConfig.Port = port - } else { - return fmt.Errorf("invalid port value: %s", portStr) + port, err := validate.PortString(portStr) + if err != nil { + return err } + t.TunnelConfig.Port = port } if target, ok := opts["target"]; ok { + if err := validate.I2PAddress(target); err != nil { + return err + } addr, err := i2pkeys.Lookup(target) if err != nil { return fmt.Errorf("invalid target address: %w", err) diff --git a/lib/tcp/server/tcpserver.go b/lib/tcp/server/tcpserver.go index 253e78e..8410204 100644 --- a/lib/tcp/server/tcpserver.go +++ b/lib/tcp/server/tcpserver.go @@ -24,6 +24,7 @@ import ( "github.com/go-i2p/go-forward/stream" i2pconv "github.com/go-i2p/go-i2ptunnel-config/lib" i2ptunnel "github.com/go-i2p/go-i2ptunnel/lib/core" + "github.com/go-i2p/go-i2ptunnel/lib/core/validate" limitedlistener "github.com/go-i2p/go-limit" "github.com/go-i2p/onramp" ) @@ -161,33 +162,42 @@ func (t *TCPServer) Options() map[string]string { // Set the tunnel's options func (t *TCPServer) SetOptions(opts map[string]string) error { - // Apply configuration options from the map + // Apply configuration options from the map with validation if name, ok := opts["name"]; ok { + if err := validate.RequiredString("name", name); err != nil { + return err + } t.TunnelConfig.Name = name } if iface, ok := opts["interface"]; ok { + if err := validate.Interface(iface); err != nil { + return err + } t.TunnelConfig.Interface = iface } if portStr, ok := opts["port"]; ok { - if port, err := strconv.Atoi(portStr); err == nil { - t.TunnelConfig.Port = port - } else { - return fmt.Errorf("invalid port value: %s", portStr) + port, err := validate.PortString(portStr) + if err != nil { + return err } + t.TunnelConfig.Port = port } if maxconnsStr, ok := opts["maxconns"]; ok { - if maxconns, err := strconv.Atoi(maxconnsStr); err == nil { - t.LimitedConfig.MaxConns = maxconns - } else { + maxconns, err := strconv.Atoi(maxconnsStr) + if err != nil { return fmt.Errorf("invalid maxconns value: %s", maxconnsStr) } + if err := validate.MaxConnections(maxconns); err != nil { + return err + } + t.LimitedConfig.MaxConns = maxconns } if ratelimitStr, ok := opts["ratelimit"]; ok { - if ratelimit, err := strconv.ParseFloat(ratelimitStr, 64); err == nil { - t.LimitedConfig.RateLimit = ratelimit - } else { - return fmt.Errorf("invalid ratelimit value: %s", ratelimitStr) + ratelimit, err := validate.RateLimitString(ratelimitStr) + if err != nil { + return err } + t.LimitedConfig.RateLimit = ratelimit } return nil } diff --git a/lib/udp/client/udpclient.go b/lib/udp/client/udpclient.go index db565b6..4a4b041 100644 --- a/lib/udp/client/udpclient.go +++ b/lib/udp/client/udpclient.go @@ -33,6 +33,7 @@ import ( "github.com/go-i2p/go-forward/packet" i2pconv "github.com/go-i2p/go-i2ptunnel-config/lib" i2ptunnel "github.com/go-i2p/go-i2ptunnel/lib/core" + "github.com/go-i2p/go-i2ptunnel/lib/core/validate" "github.com/go-i2p/go-sam-go/datagram" "github.com/go-i2p/i2pkeys" "github.com/go-i2p/onramp" @@ -161,21 +162,30 @@ func (u *UDPClient) Options() map[string]string { // Set the tunnel's options func (u *UDPClient) SetOptions(opts map[string]string) error { - // Apply configuration options from the map + // Apply configuration options from the map with validation if name, ok := opts["name"]; ok { + if err := validate.RequiredString("name", name); err != nil { + return err + } u.TunnelConfig.Name = name } if iface, ok := opts["interface"]; ok { + if err := validate.Interface(iface); err != nil { + return err + } u.TunnelConfig.Interface = iface } if portStr, ok := opts["port"]; ok { - if port, err := strconv.Atoi(portStr); err == nil { - u.TunnelConfig.Port = port - } else { - return fmt.Errorf("invalid port value: %s", portStr) + port, err := validate.PortString(portStr) + if err != nil { + return err } + u.TunnelConfig.Port = port } if target, ok := opts["target"]; ok { + if err := validate.I2PAddress(target); err != nil { + return err + } addr, err := i2pkeys.Lookup(target) if err != nil { return fmt.Errorf("invalid target address: %w", err) diff --git a/lib/udp/server/udpserver.go b/lib/udp/server/udpserver.go index 0b06296..d9bdbaa 100644 --- a/lib/udp/server/udpserver.go +++ b/lib/udp/server/udpserver.go @@ -33,6 +33,7 @@ import ( "github.com/go-i2p/go-forward/packet" i2pconv "github.com/go-i2p/go-i2ptunnel-config/lib" i2ptunnel "github.com/go-i2p/go-i2ptunnel/lib/core" + "github.com/go-i2p/go-i2ptunnel/lib/core/validate" "github.com/go-i2p/onramp" // github.com/go-i2p/go-forward/packet ) @@ -160,19 +161,25 @@ func (u *UDPServer) Options() map[string]string { // Set the tunnel's options func (u *UDPServer) SetOptions(opts map[string]string) error { - // Apply configuration options from the map + // Apply configuration options from the map with validation if name, ok := opts["name"]; ok { + if err := validate.RequiredString("name", name); err != nil { + return err + } u.TunnelConfig.Name = name } if iface, ok := opts["interface"]; ok { + if err := validate.Interface(iface); err != nil { + return err + } u.TunnelConfig.Interface = iface } if portStr, ok := opts["port"]; ok { - if port, err := strconv.Atoi(portStr); err == nil { - u.TunnelConfig.Port = port - } else { - return fmt.Errorf("invalid port value: %s", portStr) + port, err := validate.PortString(portStr) + if err != nil { + return err } + u.TunnelConfig.Port = port } return nil } diff --git a/webui/controller/config_test.go b/webui/controller/config_test.go index 3fb8c94..dc54e1c 100644 --- a/webui/controller/config_test.go +++ b/webui/controller/config_test.go @@ -31,7 +31,7 @@ func createTestConfig(t *testing.T, name, tunnelType, target string, port int) s configContent += "\n" - if err := os.WriteFile(configFile, []byte(configContent), 0644); err != nil { + if err := os.WriteFile(configFile, []byte(configContent), 0o644); err != nil { t.Fatalf("Failed to write config file: %v", err) } diff --git a/webui/controller/i2ptunnelconfig.go b/webui/controller/i2ptunnelconfig.go index 77e2319..5d55f83 100644 --- a/webui/controller/i2ptunnelconfig.go +++ b/webui/controller/i2ptunnelconfig.go @@ -159,13 +159,13 @@ func (c *Config) saveConfig() error { // Ensure directory exists dir := filepath.Dir(c.configPath) - if err := os.MkdirAll(dir, 0755); err != nil { + if err := os.MkdirAll(dir, 0o755); err != nil { return fmt.Errorf("failed to create config directory: %w", err) } // Write to file atomically using temp file + rename tempPath := c.configPath + ".tmp" - if err := os.WriteFile(tempPath, data, 0644); err != nil { + if err := os.WriteFile(tempPath, data, 0o644); err != nil { return fmt.Errorf("failed to write temp config: %w", err) }