Add validation structure, fixup missing/inoperational WebUI junk.

This commit is contained in:
eyedeekay
2025-10-19 11:20:54 -04:00
parent 374030f564
commit 76a0e20d60
16 changed files with 1023 additions and 79 deletions

2
go.mod
View File

@@ -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

View File

@@ -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
}

View File

@@ -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)
}
})
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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)
}