From ea7bec0fd41141d04c7dedaa2e48d9c90f047570 Mon Sep 17 00:00:00 2001 From: eyedeekay Date: Sat, 18 Oct 2025 22:35:38 -0400 Subject: [PATCH] Implement configuration loading for HTTP, IRC, TCP, and UDP tunnels using go-i2ptunnel-config --- go.mod | 4 +-- lib/http/client/httpclient.go | 43 +++++++++++++++++++++++++-- lib/http/server/httpserver.go | 49 +++++++++++++++++++++++++++++-- lib/irc/client/client.go | 46 +++++++++++++++++++++++++++-- lib/irc/server/server.go | 46 +++++++++++++++++++++++++++-- lib/socks/client/socks.go | 43 +++++++++++++++++++++++++-- lib/tcp/client/tcpclient.go | 55 +++++++++++++++++++++++++++++++++-- lib/tcp/server/tcpserver.go | 49 +++++++++++++++++++++++++++++-- lib/udp/client/udpclient.go | 46 +++++++++++++++++++++++++++-- lib/udp/server/udpserver.go | 46 +++++++++++++++++++++++++++-- 10 files changed, 398 insertions(+), 29 deletions(-) diff --git a/go.mod b/go.mod index 91d19ad..420d888 100644 --- a/go.mod +++ b/go.mod @@ -8,8 +8,9 @@ require ( github.com/elazarl/goproxy v1.7.0 github.com/go-i2p/go-connfilter v0.0.0-20250205023438-0f2b889a80f6 github.com/go-i2p/go-forward v0.0.0-20250202052226-ee8a43dcb664 - github.com/go-i2p/go-i2ptunnel-config v0.0.0-20250209030407-ba90db65df97 + github.com/go-i2p/go-i2ptunnel-config v0.0.0-20251019021515-10ef90f2473b github.com/go-i2p/go-limit v0.0.0-20250203203118-210616857c15 + github.com/go-i2p/go-sam-go v0.0.0-20251016194809-b6ead96fdc39 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 @@ -20,7 +21,6 @@ require ( github.com/cretz/bine v0.2.0 // indirect github.com/go-i2p/common v0.0.0-20250819203334-e5459df35789 // indirect github.com/go-i2p/crypto v0.0.0-20250822224541-85015740db11 // indirect - github.com/go-i2p/go-sam-go v0.0.0-20251016194809-b6ead96fdc39 // indirect github.com/go-i2p/logger v0.0.0-20241123010126-3050657e5d0c // indirect github.com/magiconair/properties v1.8.9 // indirect github.com/oklog/ulid/v2 v2.1.1 // indirect diff --git a/lib/http/client/httpclient.go b/lib/http/client/httpclient.go index 5d259c2..9c4d8dc 100644 --- a/lib/http/client/httpclient.go +++ b/lib/http/client/httpclient.go @@ -27,6 +27,7 @@ import ( "fmt" "net" "net/http" + "os" "strconv" "sync" @@ -192,8 +193,44 @@ func (h *HTTPClient) SetOptions(opts map[string]string) error { return nil } -// Load the tunnel config from file +// LoadConfig loads tunnel configuration from a file and updates the tunnel settings. +// The tunnel must be stopped before calling LoadConfig to prevent inconsistent state. +// Supported formats: .properties, .ini, .yaml/.yml +// +// Why: HTTP proxies need dynamic configuration updates for production deployments. +// Design: Uses go-i2ptunnel-config library for parsing. Preserves SAM connection and I2P keys. func (h *HTTPClient) LoadConfig(path string) error { - // For now, return an error indicating this method needs configuration file support - return fmt.Errorf("LoadConfig not yet implemented: would load configuration from %s", path) + // Prevent config changes while tunnel is running to avoid race conditions + if h.I2PTunnelStatus == i2ptunnel.I2PTunnelStatusRunning || + h.I2PTunnelStatus == i2ptunnel.I2PTunnelStatusStarting { + return fmt.Errorf("cannot load config while tunnel is %s - stop tunnel first", h.I2PTunnelStatus) + } + + // Parse config file using the converter library + conv := i2pconv.Converter{} + format, err := conv.DetectFormat(path) + if err != nil { + return fmt.Errorf("failed to detect config format: %w", err) + } + + bytes, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("failed to read config file: %w", err) + } + + newConfig, err := conv.ParseInput(bytes, format) + if err != nil { + return fmt.Errorf("failed to parse config: %w", err) + } + + // Type safety: ensure loaded config matches expected tunnel type + if newConfig.Type != "httpclient" { + return fmt.Errorf("config file contains %s tunnel, expected httpclient", newConfig.Type) + } + + // Update mutable configuration fields + // The Garlic connection (SAM) is preserved to maintain tunnel identity and keys + h.TunnelConfig = *newConfig + + return nil } diff --git a/lib/http/server/httpserver.go b/lib/http/server/httpserver.go index a8bf267..e4d5701 100644 --- a/lib/http/server/httpserver.go +++ b/lib/http/server/httpserver.go @@ -26,6 +26,7 @@ import ( "context" "fmt" "net" + "os" "strconv" httpinspector "github.com/go-i2p/go-connfilter/http" @@ -201,8 +202,50 @@ func (h *HTTPServer) SetOptions(opts map[string]string) error { return nil } -// Load the tunnel config from file +// LoadConfig loads tunnel configuration from a file and updates the tunnel settings. +// The tunnel must be stopped before calling LoadConfig to prevent inconsistent state. +// Supported formats: .properties, .ini, .yaml/.yml +// +// Why: HTTP reverse proxies need dynamic configuration updates for production deployments. +// Design: Uses go-i2ptunnel-config library for parsing. Preserves SAM connection and I2P keys. func (h *HTTPServer) LoadConfig(path string) error { - // For now, return an error indicating this method needs configuration file support - return fmt.Errorf("LoadConfig not yet implemented: would load configuration from %s", path) + // Prevent config changes while tunnel is running to avoid race conditions + if h.I2PTunnelStatus == i2ptunnel.I2PTunnelStatusRunning || + h.I2PTunnelStatus == i2ptunnel.I2PTunnelStatusStarting { + return fmt.Errorf("cannot load config while tunnel is %s - stop tunnel first", h.I2PTunnelStatus) + } + + // Parse config file using the converter library + conv := i2pconv.Converter{} + format, err := conv.DetectFormat(path) + if err != nil { + return fmt.Errorf("failed to detect config format: %w", err) + } + + bytes, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("failed to read config file: %w", err) + } + + newConfig, err := conv.ParseInput(bytes, format) + if err != nil { + return fmt.Errorf("failed to parse config: %w", err) + } + + // Type safety: ensure loaded config matches expected tunnel type + if newConfig.Type != "httpserver" { + return fmt.Errorf("config file contains %s tunnel, expected httpserver", newConfig.Type) + } + + // Validate target address (local service) before applying changes + targetAddr, err := net.ResolveTCPAddr("tcp", newConfig.Target) + if err != nil { + return fmt.Errorf("invalid target address in config: %w", err) + } + + // Update mutable configuration fields + h.TunnelConfig = *newConfig + h.Addr = targetAddr + + return nil } diff --git a/lib/irc/client/client.go b/lib/irc/client/client.go index 7b9bef9..3400551 100644 --- a/lib/irc/client/client.go +++ b/lib/irc/client/client.go @@ -16,6 +16,7 @@ import ( "context" "fmt" "net" + "os" "strconv" ircinspector "github.com/go-i2p/go-connfilter/irc" @@ -180,8 +181,47 @@ func (i *IRCClient) SetOptions(opts map[string]string) error { return nil } -// Load the tunnel config from file +// LoadConfig loads tunnel configuration from a file and updates the tunnel settings. +// The tunnel must be stopped before calling LoadConfig to prevent inconsistent state. +// Supported formats: .properties, .ini, .yaml/.yml func (i *IRCClient) LoadConfig(path string) error { - // For now, return an error indicating this method needs configuration file support - return fmt.Errorf("LoadConfig not yet implemented: would load configuration from %s", path) + // Prevent config changes while tunnel is running to avoid race conditions + if i.I2PTunnelStatus == i2ptunnel.I2PTunnelStatusRunning || + i.I2PTunnelStatus == i2ptunnel.I2PTunnelStatusStarting { + return fmt.Errorf("cannot load config while tunnel is %s - stop tunnel first", i.I2PTunnelStatus) + } + + // Parse config file using the converter library + conv := i2pconv.Converter{} + format, err := conv.DetectFormat(path) + if err != nil { + return fmt.Errorf("failed to detect config format: %w", err) + } + + bytes, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("failed to read config file: %w", err) + } + + newConfig, err := conv.ParseInput(bytes, format) + if err != nil { + return fmt.Errorf("failed to parse config: %w", err) + } + + // Type safety: ensure loaded config matches expected tunnel type + if newConfig.Type != "ircclient" { + return fmt.Errorf("config file contains %s tunnel, expected ircclient", newConfig.Type) + } + + // Validate target address before applying changes + addr, err := i2pkeys.Lookup(newConfig.Target) + if err != nil { + return fmt.Errorf("invalid target address in config: %w", err) + } + + // Update mutable configuration fields + i.TunnelConfig = *newConfig + i.I2PAddr = addr + + return nil } diff --git a/lib/irc/server/server.go b/lib/irc/server/server.go index 0e6b660..758230d 100644 --- a/lib/irc/server/server.go +++ b/lib/irc/server/server.go @@ -16,6 +16,7 @@ import ( "context" "fmt" "net" + "os" "strconv" ircinspector "github.com/go-i2p/go-connfilter/irc" @@ -191,8 +192,47 @@ func (i *IRCServer) SetOptions(opts map[string]string) error { return nil } -// Load the tunnel config from file +// LoadConfig loads tunnel configuration from a file and updates the tunnel settings. +// The tunnel must be stopped before calling LoadConfig to prevent inconsistent state. +// Supported formats: .properties, .ini, .yaml/.yml func (i *IRCServer) LoadConfig(path string) error { - // For now, return an error indicating this method needs configuration file support - return fmt.Errorf("LoadConfig not yet implemented: would load configuration from %s", path) + // Prevent config changes while tunnel is running to avoid race conditions + if i.I2PTunnelStatus == i2ptunnel.I2PTunnelStatusRunning || + i.I2PTunnelStatus == i2ptunnel.I2PTunnelStatusStarting { + return fmt.Errorf("cannot load config while tunnel is %s - stop tunnel first", i.I2PTunnelStatus) + } + + // Parse config file using the converter library + conv := i2pconv.Converter{} + format, err := conv.DetectFormat(path) + if err != nil { + return fmt.Errorf("failed to detect config format: %w", err) + } + + bytes, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("failed to read config file: %w", err) + } + + newConfig, err := conv.ParseInput(bytes, format) + if err != nil { + return fmt.Errorf("failed to parse config: %w", err) + } + + // Type safety: ensure loaded config matches expected tunnel type + if newConfig.Type != "ircserver" { + return fmt.Errorf("config file contains %s tunnel, expected ircserver", newConfig.Type) + } + + // Validate target address (local service) before applying changes + targetAddr, err := net.ResolveTCPAddr("tcp", newConfig.Target) + if err != nil { + return fmt.Errorf("invalid target address in config: %w", err) + } + + // Update mutable configuration fields + i.TunnelConfig = *newConfig + i.Addr = targetAddr + + return nil } diff --git a/lib/socks/client/socks.go b/lib/socks/client/socks.go index 024353b..b522329 100644 --- a/lib/socks/client/socks.go +++ b/lib/socks/client/socks.go @@ -43,6 +43,7 @@ import ( "context" "fmt" "net" + "os" "strconv" "sync" @@ -204,8 +205,44 @@ func (s *SOCKS) SetOptions(opts map[string]string) error { return nil } -// Load the tunnel config from file +// LoadConfig loads tunnel configuration from a file and updates the tunnel settings. +// The tunnel must be stopped before calling LoadConfig to prevent inconsistent state. +// Supported formats: .properties, .ini, .yaml/.yml +// +// Why: SOCKS proxies need dynamic configuration for production deployments. +// Design: Uses go-i2ptunnel-config library for parsing. Preserves SAM connection and I2P keys. func (s *SOCKS) LoadConfig(path string) error { - // For now, return an error indicating this method needs configuration file support - return fmt.Errorf("LoadConfig not yet implemented: would load configuration from %s", path) + // Prevent config changes while tunnel is running to avoid race conditions + if s.I2PTunnelStatus == i2ptunnel.I2PTunnelStatusRunning || + s.I2PTunnelStatus == i2ptunnel.I2PTunnelStatusStarting { + return fmt.Errorf("cannot load config while tunnel is %s - stop tunnel first", s.I2PTunnelStatus) + } + + // Parse config file using the converter library + conv := i2pconv.Converter{} + format, err := conv.DetectFormat(path) + if err != nil { + return fmt.Errorf("failed to detect config format: %w", err) + } + + bytes, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("failed to read config file: %w", err) + } + + newConfig, err := conv.ParseInput(bytes, format) + if err != nil { + return fmt.Errorf("failed to parse config: %w", err) + } + + // Type safety: ensure loaded config matches expected tunnel type + if newConfig.Type != "socks" { + return fmt.Errorf("config file contains %s tunnel, expected socks", newConfig.Type) + } + + // Update mutable configuration fields + // The Garlic connection (SAM) is preserved to maintain tunnel identity and keys + s.TunnelConfig = *newConfig + + return nil } diff --git a/lib/tcp/client/tcpclient.go b/lib/tcp/client/tcpclient.go index 5be619c..e80d90f 100644 --- a/lib/tcp/client/tcpclient.go +++ b/lib/tcp/client/tcpclient.go @@ -24,6 +24,7 @@ import ( "context" "fmt" "net" + "os" "strconv" "github.com/go-i2p/go-forward/config" @@ -183,8 +184,56 @@ func (t *TCPClient) SetOptions(opts map[string]string) error { return nil } -// Load the tunnel config from file +// LoadConfig loads tunnel configuration from a file and updates the tunnel settings. +// The tunnel must be stopped before calling LoadConfig to prevent inconsistent state. +// Supported formats: .properties, .ini, .yaml/.yml +// +// Why: Production deployments need to reload configuration without recreating tunnel objects. +// This enables configuration management tools and web UIs to persist changes. +// +// Design: Uses the go-i2ptunnel-config library to parse config files in multiple formats, +// then updates only the mutable fields. SAM connection is preserved to maintain tunnel identity. +// The Garlic (I2P connection) is NOT reloaded - it maintains the existing keys and SAM session. func (t *TCPClient) LoadConfig(path string) error { - // For now, return an error indicating this method needs configuration file support - return fmt.Errorf("LoadConfig not yet implemented: would load configuration from %s", path) + // Prevent config changes while tunnel is running to avoid race conditions + if t.I2PTunnelStatus == i2ptunnel.I2PTunnelStatusRunning || + t.I2PTunnelStatus == i2ptunnel.I2PTunnelStatusStarting { + return fmt.Errorf("cannot load config while tunnel is %s - stop tunnel first", t.I2PTunnelStatus) + } + + // Parse config file using the converter library + // This handles format detection and validation for .properties, .ini, .yaml + conv := i2pconv.Converter{} + format, err := conv.DetectFormat(path) + if err != nil { + return fmt.Errorf("failed to detect config format: %w", err) + } + + bytes, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("failed to read config file: %w", err) + } + + newConfig, err := conv.ParseInput(bytes, format) + if err != nil { + return fmt.Errorf("failed to parse config: %w", err) + } + + // Type safety: ensure loaded config matches expected tunnel type + if newConfig.Type != "tcpclient" { + return fmt.Errorf("config file contains %s tunnel, expected tcpclient", newConfig.Type) + } + + // Validate target address before applying changes + addr, err := i2pkeys.Lookup(newConfig.Target) + if err != nil { + return fmt.Errorf("invalid target address in config: %w", err) + } + + // Update mutable configuration fields + // The Garlic connection (SAM) is preserved to maintain tunnel identity and keys + t.TunnelConfig = *newConfig + t.I2PAddr = addr + + return nil } diff --git a/lib/tcp/server/tcpserver.go b/lib/tcp/server/tcpserver.go index 656cb08..253e78e 100644 --- a/lib/tcp/server/tcpserver.go +++ b/lib/tcp/server/tcpserver.go @@ -17,6 +17,7 @@ import ( "context" "fmt" "net" + "os" "strconv" "github.com/go-i2p/go-forward/config" @@ -191,8 +192,50 @@ func (t *TCPServer) SetOptions(opts map[string]string) error { return nil } -// Load the tunnel config from file +// LoadConfig loads tunnel configuration from a file and updates the tunnel settings. +// The tunnel must be stopped before calling LoadConfig to prevent inconsistent state. +// Supported formats: .properties, .ini, .yaml/.yml +// +// Why: Production deployments need to reload configuration without recreating tunnel objects. +// Design: Uses go-i2ptunnel-config library for parsing. Preserves SAM connection and I2P keys. func (t *TCPServer) LoadConfig(path string) error { - // For now, return an error indicating this method needs configuration file support - return fmt.Errorf("LoadConfig not yet implemented: would load configuration from %s", path) + // Prevent config changes while tunnel is running to avoid race conditions + if t.I2PTunnelStatus == i2ptunnel.I2PTunnelStatusRunning || + t.I2PTunnelStatus == i2ptunnel.I2PTunnelStatusStarting { + return fmt.Errorf("cannot load config while tunnel is %s - stop tunnel first", t.I2PTunnelStatus) + } + + // Parse config file using the converter library + conv := i2pconv.Converter{} + format, err := conv.DetectFormat(path) + if err != nil { + return fmt.Errorf("failed to detect config format: %w", err) + } + + bytes, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("failed to read config file: %w", err) + } + + newConfig, err := conv.ParseInput(bytes, format) + if err != nil { + return fmt.Errorf("failed to parse config: %w", err) + } + + // Type safety: ensure loaded config matches expected tunnel type + if newConfig.Type != "tcpserver" { + return fmt.Errorf("config file contains %s tunnel, expected tcpserver", newConfig.Type) + } + + // Validate target address (local service) before applying changes + targetAddr, err := net.ResolveTCPAddr("tcp", newConfig.Target) + if err != nil { + return fmt.Errorf("invalid target address in config: %w", err) + } + + // Update mutable configuration fields + t.TunnelConfig = *newConfig + t.Addr = targetAddr + + return nil } diff --git a/lib/udp/client/udpclient.go b/lib/udp/client/udpclient.go index e6bfdba..db565b6 100644 --- a/lib/udp/client/udpclient.go +++ b/lib/udp/client/udpclient.go @@ -26,6 +26,7 @@ import ( "context" "fmt" "net" + "os" "strconv" "github.com/go-i2p/go-forward/config" @@ -184,8 +185,47 @@ func (u *UDPClient) SetOptions(opts map[string]string) error { return nil } -// Load the tunnel config from file +// LoadConfig loads tunnel configuration from a file and updates the tunnel settings. +// The tunnel must be stopped before calling LoadConfig to prevent inconsistent state. +// Supported formats: .properties, .ini, .yaml/.yml func (u *UDPClient) LoadConfig(path string) error { - // For now, return an error indicating this method needs configuration file support - return fmt.Errorf("LoadConfig not yet implemented: would load configuration from %s", path) + // Prevent config changes while tunnel is running to avoid race conditions + if u.I2PTunnelStatus == i2ptunnel.I2PTunnelStatusRunning || + u.I2PTunnelStatus == i2ptunnel.I2PTunnelStatusStarting { + return fmt.Errorf("cannot load config while tunnel is %s - stop tunnel first", u.I2PTunnelStatus) + } + + // Parse config file using the converter library + conv := i2pconv.Converter{} + format, err := conv.DetectFormat(path) + if err != nil { + return fmt.Errorf("failed to detect config format: %w", err) + } + + bytes, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("failed to read config file: %w", err) + } + + newConfig, err := conv.ParseInput(bytes, format) + if err != nil { + return fmt.Errorf("failed to parse config: %w", err) + } + + // Type safety: ensure loaded config matches expected tunnel type + if newConfig.Type != "udpclient" { + return fmt.Errorf("config file contains %s tunnel, expected udpclient", newConfig.Type) + } + + // Validate target address before applying changes + addr, err := i2pkeys.Lookup(newConfig.Target) + if err != nil { + return fmt.Errorf("invalid target address in config: %w", err) + } + + // Update mutable configuration fields + u.TunnelConfig = *newConfig + u.I2PAddr = addr + + return nil } diff --git a/lib/udp/server/udpserver.go b/lib/udp/server/udpserver.go index 4bb40d7..0b06296 100644 --- a/lib/udp/server/udpserver.go +++ b/lib/udp/server/udpserver.go @@ -26,6 +26,7 @@ import ( "context" "fmt" "net" + "os" "strconv" "github.com/go-i2p/go-forward/config" @@ -176,8 +177,47 @@ func (u *UDPServer) SetOptions(opts map[string]string) error { return nil } -// Load the tunnel config from file +// LoadConfig loads tunnel configuration from a file and updates the tunnel settings. +// The tunnel must be stopped before calling LoadConfig to prevent inconsistent state. +// Supported formats: .properties, .ini, .yaml/.yml func (u *UDPServer) LoadConfig(path string) error { - // For now, return an error indicating this method needs configuration file support - return fmt.Errorf("LoadConfig not yet implemented: would load configuration from %s", path) + // Prevent config changes while tunnel is running to avoid race conditions + if u.I2PTunnelStatus == i2ptunnel.I2PTunnelStatusRunning || + u.I2PTunnelStatus == i2ptunnel.I2PTunnelStatusStarting { + return fmt.Errorf("cannot load config while tunnel is %s - stop tunnel first", u.I2PTunnelStatus) + } + + // Parse config file using the converter library + conv := i2pconv.Converter{} + format, err := conv.DetectFormat(path) + if err != nil { + return fmt.Errorf("failed to detect config format: %w", err) + } + + bytes, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("failed to read config file: %w", err) + } + + newConfig, err := conv.ParseInput(bytes, format) + if err != nil { + return fmt.Errorf("failed to parse config: %w", err) + } + + // Type safety: ensure loaded config matches expected tunnel type + if newConfig.Type != "udpserver" { + return fmt.Errorf("config file contains %s tunnel, expected udpserver", newConfig.Type) + } + + // Validate target address (local service) before applying changes + targetAddr, err := net.ResolveUDPAddr("udp", newConfig.Target) + if err != nil { + return fmt.Errorf("invalid target address in config: %w", err) + } + + // Update mutable configuration fields + u.TunnelConfig = *newConfig + u.Addr = targetAddr + + return nil }