mirror of
https://github.com/go-i2p/go-i2ptunnel.git
synced 2025-12-20 15:15:52 -05:00
Add validation structure, fixup missing/inoperational WebUI junk.
This commit is contained in:
2
go.mod
2
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
|
||||
|
||||
267
lib/core/validate/validate.go
Normal file
267
lib/core/validate/validate.go
Normal 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
|
||||
}
|
||||
598
lib/core/validate/validate_test.go
Normal file
598
lib/core/validate/validate_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user