From c20398642d39f6af651c74c05fbb3842f463a731 Mon Sep 17 00:00:00 2001 From: s1d3sw1ped_bot <12+s1d3sw1ped_bot@git.s1d3sw1ped.com> Date: Tue, 1 Sep 2026 04:10:14 +0000 Subject: [PATCH] Default local listeners to 127.0.0.1 Client TCP/UDP and the built-in DNS server bind loopback unless bind_address or a per-rule host (tcp://22:0.0.0.0:2222) is set. README and generated server examples no longer document :9000/:8080 as if they were localhost-only. Closes #3 --- README.md | 70 +++++++----- internal/client/client.go | 18 ++- pkg/config/config.go | 117 ++++++++++++++++++-- pkg/config/config_test.go | 224 ++++++++++++++++++++++++++++++++++++++ pkg/dns/dns.go | 9 +- 5 files changed, 393 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 97313fb..1e22a34 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Flow: User → Client:8080,2222 → Encrypted Tunnel (Port 9000) → Server:80,2 **Server Configuration** (`server.yaml`): ```yaml instance_id: teleport-server-01 -listen_address: :9000 +listen_address: 127.0.0.1:9000 # loopback; use 0.0.0.0:9000 to publish remote_address: "" ports: - "tcp://192.168.1.100:80" @@ -50,6 +50,7 @@ write_timeout: 30s instance_id: teleport-client-01 listen_address: "" remote_address: server.example.com:9000 +bind_address: 127.0.0.1 ports: - "tcp://80:8080" - "tcp://22:2222" @@ -59,9 +60,11 @@ read_timeout: 30s write_timeout: 30s ``` -**Usage**: -- User connects to `localhost:8080` → Traffic flows through encrypted tunnel on port 9000 → Server forwards to remote web server at `192.168.1.100:80` -- User connects to `localhost:2222` → Traffic flows through same encrypted tunnel on port 9000 → Server forwards to remote SSH server at `192.168.1.200:22` +**Usage**: +- User connects to `127.0.0.1:8080` (client local listener, loopback by default) → Traffic flows through encrypted tunnel on `127.0.0.1:9000` → Server forwards to remote web server at `192.168.1.100:80` +- User connects to `127.0.0.1:2222` → Traffic flows through the same encrypted tunnel → Server forwards to remote SSH server at `192.168.1.200:22` + +Local client ports bind `127.0.0.1` unless you set `bind_address: 0.0.0.0` or a per-rule host such as `"tcp://22:0.0.0.0:2222"`. Server `listen_address: 127.0.0.1:9000` is loopback-only; `0.0.0.0:9000` (or `:9000`) publishes the tunnel on all interfaces and must be explicit. Both services share the same encrypted tunnel connection! @@ -116,7 +119,7 @@ Generate example configuration files: ```yaml instance_id: teleport-server-01 -listen_address: :8080 +listen_address: 127.0.0.1:8080 # use 0.0.0.0:8080 to accept remote clients remote_address: "" ports: - "tcp://localhost:80" @@ -143,6 +146,7 @@ dns_server: instance_id: teleport-client-01 listen_address: "" remote_address: localhost:8080 +bind_address: 127.0.0.1 ports: - "tcp://80:8080" encryption_key: your-secure-encryption-key-change-this-to-something-random @@ -169,16 +173,19 @@ dns_server: ## Configuration Fields - `instance_id`: Unique identifier for this teleport instance -- `listen_address`: Address to listen on (server mode) - format: `host:port` +- `listen_address`: Address the **server** binds for the encrypted tunnel (`host:port`). Examples use `127.0.0.1:9000`. Use `0.0.0.0:9000` or `:9000` only when you intend to publish the server on all interfaces. - `remote_address`: Address of remote teleport server (client mode) - format: `host:port` +- `bind_address`: Host for **client** local TCP/UDP listeners (default: `127.0.0.1`). Set to `0.0.0.0` to accept connections from other machines. Publishing on all interfaces is never implicit. - `ports`: Array of port forwarding rules in URL-style format - **Server format**: `protocol://target:targetport` - forwards to remote target - - **Client format**: `protocol://targetport:localport` - forwards to teleport server's targetport, listens on localport + - **Client format**: `protocol://targetport:localport` - forwards to teleport server's targetport, listens on `bind_address:localport` (default `127.0.0.1`) + - **Client with bind**: `protocol://targetport:bindhost:localport` - per-rule listen host, e.g. `"tcp://22:0.0.0.0:2222"` - Examples: - - `"tcp://localhost:80"` (server) - listen on port 80, forward to localhost:80 - - `"tcp://server-a:22"` (server) - listen on port 22, forward to server-a:22 - - `"tcp://80:8080"` (client) - listen on port 8080, forward to teleport server's port 80 - - `"udp://53:5353"` (client) - listen on port 5353, forward to teleport server's port 53 + - `"tcp://localhost:80"` (server) - forward to localhost:80 + - `"tcp://server-a:22"` (server) - forward to server-a:22 + - `"tcp://80:8080"` (client) - listen on `127.0.0.1:8080`, forward to teleport server's port 80 + - `"tcp://22:0.0.0.0:2222"` (client) - listen on all interfaces port 2222 + - `"udp://53:5353"` (client) - listen on `127.0.0.1:5353`, forward to teleport server's port 53 - `encryption_key`: Shared secret key for encryption (must be the same on both sides) - `keep_alive`: Enable TCP keep-alive - `read_timeout`: Read timeout duration @@ -192,6 +199,7 @@ dns_server: - `dns_server`: DNS server configuration - `enabled`: Enable built-in DNS server - `listen_port`: Port for DNS server to listen on + - `bind_address`: DNS listen host (default: `127.0.0.1`). Set to `0.0.0.0` to serve DNS on all interfaces; the default is not an open recursive forwarder on every interface. - `backup_server`: Backup DNS server for fallback (e.g., "8.8.8.8:53") - `custom_records`: Array of custom DNS records - `name`: Domain name (e.g., "example.com") @@ -278,14 +286,15 @@ Teleport uses URL-style port mapping format for cleaner configuration: ```yaml ports: - - "tcp://80:8080" # Listen on port 8080, forward to teleport server's port 80 - - "udp://53:5353" # Listen on port 5353, forward to teleport server's port 53 - - "tcp://22:2222" # Listen on port 2222, forward to teleport server's port 22 + - "tcp://80:8080" # Listen on 127.0.0.1:8080, forward to teleport server's port 80 + - "udp://53:5353" # Listen on 127.0.0.1:5353, forward to teleport server's port 53 + - "tcp://22:0.0.0.0:2222" # Explicit all-interfaces bind ``` -**Format**: -- **Server**: `"protocol://target:targetport"` - listen on targetport, forward to target:targetport -- **Client**: `"protocol://targetport:localport"` - listen on localport, forward to teleport server's targetport +**Format**: +- **Server**: `"protocol://target:targetport"` - forward to target:targetport +- **Client**: `"protocol://targetport:localport"` - listen on `127.0.0.1:localport` (or `bind_address`), forward to teleport server's targetport +- **Client with bind**: `"protocol://targetport:bindhost:localport"` - listen on bindhost:localport **Examples:** ```yaml @@ -295,11 +304,12 @@ ports: - "tcp://server-a:22" # Listen on port 22, forward to server-a:22 - "udp://dns-server:53" # Listen on port 53, forward to dns-server:53 -# Client configurations +# Client configurations ports: - - "tcp://80:8080" # Listen on port 8080, forward to teleport server's port 80 - - "tcp://22:2222" # Listen on port 2222, forward to teleport server's port 22 - - "udp://53:5353" # Listen on port 5353, forward to teleport server's port 53 + - "tcp://80:8080" # Listen on 127.0.0.1:8080, forward to teleport server's port 80 + - "tcp://22:2222" # Listen on 127.0.0.1:2222, forward to teleport server's port 22 + - "tcp://22:0.0.0.0:2222" # Listen on all interfaces (explicit) + - "udp://53:5353" # Listen on 127.0.0.1:5353, forward to teleport server's port 53 ``` **Example: Remote SSH Access** @@ -308,10 +318,10 @@ To access SSH on Server A through teleport server on Server B: **Server B configuration:** ```yaml instance_id: teleport-server-b -listen_address: :8080 +listen_address: 127.0.0.1:8080 remote_address: "" ports: - - "tcp://server-a:22" # Listen on port 22, forward to server-a:22 + - "tcp://server-a:22" # Forward to server-a:22 encryption_key: your-shared-key ``` @@ -320,8 +330,9 @@ encryption_key: your-shared-key instance_id: teleport-client-c listen_address: "" remote_address: server-b:8080 +bind_address: 127.0.0.1 ports: - - "tcp://22:2222" # Listen on local port 2222, forward to teleport server's port 22 + - "tcp://22:2222" # Listen on 127.0.0.1:2222, forward to teleport server's port 22 encryption_key: your-shared-key ``` @@ -345,7 +356,7 @@ The program validates your configuration and will show clear error messages if: **Valid Server Configuration:** ```yaml -listen_address: :8080 +listen_address: 127.0.0.1:8080 remote_address: "" ports: - "tcp://localhost:22" @@ -355,6 +366,7 @@ ports: ```yaml listen_address: "" remote_address: server:8080 +bind_address: 127.0.0.1 ports: - "tcp://22:2222" ``` @@ -369,6 +381,7 @@ ports: - Encryption keys are validated for entropy and strength - Logging includes automatic sanitization of sensitive data - Rate limiting prevents abuse and DoS attacks +- Client local listeners and the built-in DNS server bind `127.0.0.1` by default so LAN/internet hosts cannot use your tunnel unless you set `bind_address` / a per-rule host to `0.0.0.0` ## Example Use Cases @@ -394,7 +407,7 @@ ports: - Generate: `./teleport -generate-config -config teleport-client.yaml` - Edit the configuration file with local ports and remote server address - Run: `./teleport -config teleport-client.yaml` - - Connect to `localhost:local_port` to access the remote service + - Connect to `127.0.0.1:local_port` (loopback by default) to access the remote service ## Multi-Client Support @@ -428,6 +441,7 @@ The built-in DNS server provides: dns_server: enabled: true listen_port: 5353 + bind_address: 127.0.0.1 backup_server: 8.8.8.8:53 custom_records: - name: api.local @@ -510,8 +524,8 @@ Teleport includes sophisticated logging with: - The encryption key must be identical on both server and client - Use `./teleport --generate-key` to create a secure random encryption key -- The server listens on the specified `listen_address` for incoming connections -- The client connects to the remote server and forwards local connections +- The server listens on the specified `listen_address` for incoming tunnel connections (examples bind loopback; `0.0.0.0` must be explicit) +- The client connects to the remote server and forwards local connections from `bind_address` (default `127.0.0.1`) - All port forwarding is bidirectional - Port format uses URL-style conventions: `"protocol://target:port"` - Configuration files use YAML format for better readability diff --git a/internal/client/client.go b/internal/client/client.go index c60faa1..a3d89b4 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -93,9 +93,11 @@ func (tc *TeleportClient) startPortForwarding(rule config.PortRule) { // startTCPForwarding starts TCP port forwarding func (tc *TeleportClient) startTCPForwarding(rule config.PortRule) { - listener, err := net.Listen("tcp", fmt.Sprintf(":%d", rule.LocalPort)) + listenAddr := tc.config.ClientListenAddr(rule) + listener, err := net.Listen("tcp", listenAddr) if err != nil { logger.WithFields(map[string]interface{}{ + "addr": listenAddr, "port": rule.LocalPort, "error": err, }).Error("Failed to start TCP listener") @@ -104,6 +106,7 @@ func (tc *TeleportClient) startTCPForwarding(rule config.PortRule) { defer listener.Close() logger.WithFields(map[string]interface{}{ + "listen_addr": listenAddr, "local_port": rule.LocalPort, "remote_addr": tc.config.RemoteAddress, "remote_port": rule.RemotePort, @@ -254,9 +257,11 @@ func (tc *TeleportClient) returnConnection(conn net.Conn) { // startUDPForwarding starts UDP port forwarding func (tc *TeleportClient) startUDPForwarding(rule config.PortRule) { - addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf(":%d", rule.LocalPort)) + listenAddr := tc.config.ClientListenAddr(rule) + addr, err := net.ResolveUDPAddr("udp", listenAddr) if err != nil { logger.WithFields(map[string]interface{}{ + "addr": listenAddr, "port": rule.LocalPort, "error": err, }).Error("Failed to resolve UDP address") @@ -266,6 +271,7 @@ func (tc *TeleportClient) startUDPForwarding(rule config.PortRule) { conn, err := net.ListenUDP("udp", addr) if err != nil { logger.WithFields(map[string]interface{}{ + "addr": listenAddr, "port": rule.LocalPort, "error": err, }).Error("Failed to start UDP listener") @@ -277,6 +283,7 @@ func (tc *TeleportClient) startUDPForwarding(rule config.PortRule) { tc.udpMutex.Unlock() logger.WithFields(map[string]interface{}{ + "listen_addr": listenAddr, "local_port": rule.LocalPort, "remote_addr": tc.config.RemoteAddress, "remote_port": rule.RemotePort, @@ -641,7 +648,12 @@ func (tc *TeleportClient) handleTCPConnection(clientConn net.Conn, rule config.P defer tc.returnConnection(serverConn) // Send port forward request to server - request := types.PortForwardRequest(rule) + request := types.PortForwardRequest{ + LocalPort: rule.LocalPort, + RemotePort: rule.RemotePort, + Protocol: rule.Protocol, + TargetHost: rule.TargetHost, + } if err := tc.sendRequestToConnection(serverConn, request); err != nil { logger.WithField("error", err).Error("Failed to send port forward request") diff --git a/pkg/config/config.go b/pkg/config/config.go index 24155c7..0d819b9 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -4,6 +4,7 @@ import ( "crypto/rand" "encoding/hex" "fmt" + "net" "os" "strconv" "strings" @@ -19,6 +20,7 @@ type Config struct { InstanceID string `yaml:"instance_id"` ListenAddress string `yaml:"listen_address"` RemoteAddress string `yaml:"remote_address"` + BindAddress string `yaml:"bind_address"` // local TCP/UDP/DNS bind host; default 127.0.0.1 Ports []PortRule `yaml:"ports"` EncryptionKey string `yaml:"encryption_key"` KeepAlive bool `yaml:"keep_alive"` @@ -31,10 +33,43 @@ type Config struct { // PortRule defines a port forwarding rule type PortRule struct { - LocalPort int `yaml:"local_port"` - RemotePort int `yaml:"remote_port"` - Protocol string `yaml:"protocol"` // "tcp" or "udp" - TargetHost string `yaml:"target_host,omitempty"` // Target host for server-side forwarding (defaults to localhost) + LocalPort int `yaml:"local_port"` + RemotePort int `yaml:"remote_port"` + Protocol string `yaml:"protocol"` // "tcp" or "udp" + TargetHost string `yaml:"target_host,omitempty"` // Target host for server-side forwarding (defaults to localhost) + BindAddress string `yaml:"-"` // Client local listen host; empty means use Config.BindAddress +} + +// DefaultBindAddress is the loopback host used when bind_address is omitted. +// Binding 0.0.0.0 (all interfaces) requires an explicit override. +const DefaultBindAddress = "127.0.0.1" + +// NormalizeBindAddress returns host, or 127.0.0.1 when host is empty. +func NormalizeBindAddress(host string) string { + if host == "" { + return DefaultBindAddress + } + return host +} + +// LocalListenAddr returns host:port for a local listener. Empty host becomes 127.0.0.1. +func LocalListenAddr(host string, port int) string { + return net.JoinHostPort(NormalizeBindAddress(host), strconv.Itoa(port)) +} + +// ClientListenAddr is the address the client binds for a port rule. +// Per-rule BindAddress wins over Config.BindAddress; both default to 127.0.0.1. +func (c *Config) ClientListenAddr(rule PortRule) string { + host := rule.BindAddress + if host == "" && c != nil { + host = c.BindAddress + } + return LocalListenAddr(host, rule.LocalPort) +} + +// ListenAddr is the address the built-in DNS server binds. Default 127.0.0.1. +func (d DNSServerConfig) ListenAddr() string { + return LocalListenAddr(d.BindAddress, d.ListenPort) } // UnmarshalYAML implements custom YAML unmarshaling for PortRule @@ -133,21 +168,70 @@ func (p *PortRule) UnmarshalYAML(value *yaml.Node) error { p.TargetHost = "" // Client doesn't specify target host return nil } + } else if len(addressParts) >= 3 { + // Client format with bind host: protocol://targetport:bindhost:localport + // e.g. tcp://22:127.0.0.1:2222 or tcp://22:0.0.0.0:2222 + firstColon := strings.Index(addressPart, ":") + lastColon := strings.LastIndex(addressPart, ":") + if firstColon < 0 || lastColon <= firstColon { + return fmt.Errorf("invalid address format: %s (expected 'targetport:bindhost:localport')", addressPart) + } + targetPortStr := addressPart[:firstColon] + bindHost := addressPart[firstColon+1 : lastColon] + localPortStr := addressPart[lastColon+1:] + + bindHost = strings.TrimPrefix(bindHost, "[") + bindHost = strings.TrimSuffix(bindHost, "]") + if bindHost == "" { + return fmt.Errorf("bind host is required in format 'targetport:bindhost:localport'") + } + if len(bindHost) > 253 { + return fmt.Errorf("bind host too long") + } + for _, c := range bindHost { + if c < 32 || c > 126 { + return fmt.Errorf("invalid character in bind host") + } + } + + targetPort, err := strconv.Atoi(targetPortStr) + if err != nil { + return fmt.Errorf("invalid target port: %s", targetPortStr) + } + localPort, err := strconv.Atoi(localPortStr) + if err != nil { + return fmt.Errorf("invalid local port: %s", localPortStr) + } + if targetPort < 1 || targetPort > 65535 { + return fmt.Errorf("invalid target port: %d (must be 1-65535)", targetPort) + } + if localPort < 1 || localPort > 65535 { + return fmt.Errorf("invalid local port: %d (must be 1-65535)", localPort) + } + + p.LocalPort = localPort + p.RemotePort = targetPort + p.Protocol = protocol + p.TargetHost = "" + p.BindAddress = bindHost + return nil } else { - return fmt.Errorf("invalid address format: %s (expected 'target:port' for server or 'targetport:localport' for client)", addressPart) + return fmt.Errorf("invalid address format: %s (expected 'target:port' for server, 'targetport:localport' for client, or 'targetport:bindhost:localport' for client with bind)", addressPart) } } // MarshalYAML implements custom YAML marshaling for PortRule func (p PortRule) MarshalYAML() (interface{}, error) { - // Use new format: protocol://target:targetport (server) or protocol://targetport:localport (client) + // Server: protocol://target:targetport + // Client: protocol://targetport:localport + // Client with explicit bind: protocol://targetport:bindhost:localport if p.TargetHost == "" { - // Client format: protocol://targetport:localport + if p.BindAddress != "" { + return fmt.Sprintf("%s://%d:%s:%d", p.Protocol, p.RemotePort, p.BindAddress, p.LocalPort), nil + } return fmt.Sprintf("%s://%d:%d", p.Protocol, p.RemotePort, p.LocalPort), nil - } else { - // Server format: protocol://target:targetport - return fmt.Sprintf("%s://%s:%d", p.Protocol, p.TargetHost, p.RemotePort), nil } + return fmt.Sprintf("%s://%s:%d", p.Protocol, p.TargetHost, p.RemotePort), nil } // RateLimitConfig defines rate limiting configuration @@ -162,6 +246,7 @@ type RateLimitConfig struct { type DNSServerConfig struct { Enabled bool `yaml:"enabled"` ListenPort int `yaml:"listen_port"` + BindAddress string `yaml:"bind_address"` BackupServer string `yaml:"backup_server"` CustomRecords []DNSRecord `yaml:"custom_records"` } @@ -217,9 +302,15 @@ func LoadConfig(filename string) (*Config, error) { if config.RateLimit.WindowSize == 0 { config.RateLimit.WindowSize = 1 * time.Second } + if config.BindAddress == "" { + config.BindAddress = DefaultBindAddress + } if config.DNSServer.ListenPort == 0 { config.DNSServer.ListenPort = 5353 } + if config.DNSServer.BindAddress == "" { + config.DNSServer.BindAddress = DefaultBindAddress + } if config.DNSServer.BackupServer == "" { config.DNSServer.BackupServer = "8.8.8.8:53" } @@ -457,8 +548,9 @@ func GenerateExampleConfig(filename string) error { // Generate server configuration config = Config{ InstanceID: "teleport-server-01", - ListenAddress: ":8080", + ListenAddress: "127.0.0.1:8080", RemoteAddress: "", + BindAddress: DefaultBindAddress, Ports: []PortRule{ {LocalPort: 80, RemotePort: 80, Protocol: "tcp", TargetHost: "localhost"}, }, @@ -475,6 +567,7 @@ func GenerateExampleConfig(filename string) error { }, DNSServer: DNSServerConfig{ ListenPort: 5353, + BindAddress: DefaultBindAddress, BackupServer: "8.8.8.8:53", CustomRecords: []DNSRecord{}, }, @@ -485,6 +578,7 @@ func GenerateExampleConfig(filename string) error { InstanceID: "teleport-client-01", ListenAddress: "", RemoteAddress: "localhost:8080", + BindAddress: DefaultBindAddress, Ports: []PortRule{ {LocalPort: 8080, RemotePort: 80, Protocol: "tcp", TargetHost: ""}, }, @@ -501,6 +595,7 @@ func GenerateExampleConfig(filename string) error { }, DNSServer: DNSServerConfig{ ListenPort: 5353, + BindAddress: DefaultBindAddress, BackupServer: "8.8.8.8:53", CustomRecords: []DNSRecord{ {Name: "app.local", Type: "A", Value: "127.0.0.1", TTL: 300}, diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 573d4ce..0f4d6f5 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -1,6 +1,7 @@ package config import ( + "net" "os" "path/filepath" "strings" @@ -135,6 +136,14 @@ encryption_key: a0e3dd20a761b118ca234160dd8b87230a001e332a97c9cfe3b8b9c99efaae03 if config.DNSServer.BackupServer != "8.8.8.8:53" { t.Errorf("Expected default backup server '8.8.8.8:53', got '%s'", config.DNSServer.BackupServer) } + + if config.BindAddress != DefaultBindAddress { + t.Errorf("Expected default BindAddress %q, got %q", DefaultBindAddress, config.BindAddress) + } + + if config.DNSServer.BindAddress != DefaultBindAddress { + t.Errorf("Expected default DNS BindAddress %q, got %q", DefaultBindAddress, config.DNSServer.BindAddress) + } } func TestDetectMode(t *testing.T) { @@ -361,3 +370,218 @@ encryption_key: test-key t.Errorf("Expected error message to mention 'protocol://', got: %v", err) } } + +func TestDefaultLocalListenAddrIsLoopback(t *testing.T) { + if got := LocalListenAddr("", 9000); got != "127.0.0.1:9000" { + t.Fatalf("empty host: got %q want 127.0.0.1:9000", got) + } + if got := LocalListenAddr("127.0.0.1", 2222); got != "127.0.0.1:2222" { + t.Fatalf("loopback host: got %q", got) + } + + cfg := &Config{} + tcpRule := PortRule{LocalPort: 8080, RemotePort: 80, Protocol: "tcp"} + udpRule := PortRule{LocalPort: 5353, RemotePort: 53, Protocol: "udp"} + if got := cfg.ClientListenAddr(tcpRule); got != "127.0.0.1:8080" { + t.Fatalf("default TCP listen: got %q want 127.0.0.1:8080", got) + } + if got := cfg.ClientListenAddr(udpRule); got != "127.0.0.1:5353" { + t.Fatalf("default UDP listen: got %q want 127.0.0.1:5353", got) + } + + dnsCfg := DNSServerConfig{ListenPort: 5353} + if got := dnsCfg.ListenAddr(); got != "127.0.0.1:5353" { + t.Fatalf("default DNS listen: got %q want 127.0.0.1:5353", got) + } +} + +func TestExplicitBindAddressOverride(t *testing.T) { + if got := LocalListenAddr("0.0.0.0", 9000); got != "0.0.0.0:9000" { + t.Fatalf("0.0.0.0 override: got %q", got) + } + + cfg := &Config{BindAddress: "0.0.0.0"} + rule := PortRule{LocalPort: 2222, RemotePort: 22, Protocol: "tcp"} + if got := cfg.ClientListenAddr(rule); got != "0.0.0.0:2222" { + t.Fatalf("global bind_address 0.0.0.0: got %q", got) + } + + rule.BindAddress = "10.0.0.5" + if got := cfg.ClientListenAddr(rule); got != "10.0.0.5:2222" { + t.Fatalf("per-rule host should win: got %q", got) + } + + dnsCfg := DNSServerConfig{ListenPort: 5353, BindAddress: "0.0.0.0"} + if got := dnsCfg.ListenAddr(); got != "0.0.0.0:5353" { + t.Fatalf("DNS bind_address 0.0.0.0: got %q", got) + } +} + +func TestLocalListenAddrBinds(t *testing.T) { + ln, err := net.Listen("tcp", LocalListenAddr("", 0)) + if err != nil { + t.Fatalf("listen default: %v", err) + } + defer ln.Close() + ip := ln.Addr().(*net.TCPAddr).IP + if !ip.IsLoopback() { + t.Fatalf("default TCP bind is not loopback: %v", ip) + } + + udpAddr, err := net.ResolveUDPAddr("udp", LocalListenAddr("", 0)) + if err != nil { + t.Fatalf("resolve default udp: %v", err) + } + uc, err := net.ListenUDP("udp", udpAddr) + if err != nil { + t.Fatalf("listen default udp: %v", err) + } + defer uc.Close() + if !uc.LocalAddr().(*net.UDPAddr).IP.IsLoopback() { + t.Fatalf("default UDP bind is not loopback: %v", uc.LocalAddr()) + } + + all, err := net.Listen("tcp", LocalListenAddr("0.0.0.0", 0)) + if err != nil { + t.Fatalf("listen 0.0.0.0: %v", err) + } + defer all.Close() + if !all.Addr().(*net.TCPAddr).IP.IsUnspecified() { + t.Fatalf("explicit 0.0.0.0 bind is not unspecified: %v", all.Addr()) + } +} + +func TestPortRuleBindHostFormat(t *testing.T) { + tests := []struct { + name string + config string + expected PortRule + }{ + { + name: "client with loopback bind", + config: ` +instance_id: test +remote_address: localhost:8080 +ports: + - tcp://22:127.0.0.1:2222 +encryption_key: a0e3dd20a761b118ca234160dd8b87230a001e332a97c9cfe3b8b9c99efaae03 +`, + expected: PortRule{LocalPort: 2222, RemotePort: 22, Protocol: "tcp", BindAddress: "127.0.0.1"}, + }, + { + name: "client with all-interfaces bind", + config: ` +instance_id: test +remote_address: localhost:8080 +ports: + - tcp://22:0.0.0.0:2222 +encryption_key: a0e3dd20a761b118ca234160dd8b87230a001e332a97c9cfe3b8b9c99efaae03 +`, + expected: PortRule{LocalPort: 2222, RemotePort: 22, Protocol: "tcp", BindAddress: "0.0.0.0"}, + }, + { + name: "client two-part still defaults bind via config", + config: ` +instance_id: test +remote_address: localhost:8080 +ports: + - tcp://80:8080 +encryption_key: a0e3dd20a761b118ca234160dd8b87230a001e332a97c9cfe3b8b9c99efaae03 +`, + expected: PortRule{LocalPort: 8080, RemotePort: 80, Protocol: "tcp", BindAddress: ""}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tempDir := t.TempDir() + configFile := filepath.Join(tempDir, "test-config.yaml") + if err := os.WriteFile(configFile, []byte(tt.config), 0644); err != nil { + t.Fatalf("write config: %v", err) + } + cfg, err := LoadConfig(configFile) + if err != nil { + t.Fatalf("load: %v", err) + } + if len(cfg.Ports) != 1 { + t.Fatalf("expected 1 port, got %d", len(cfg.Ports)) + } + port := cfg.Ports[0] + if port.LocalPort != tt.expected.LocalPort || port.RemotePort != tt.expected.RemotePort || + port.Protocol != tt.expected.Protocol || port.BindAddress != tt.expected.BindAddress { + t.Errorf("got %+v want %+v", port, tt.expected) + } + }) + } +} + +func TestLoadConfigGlobalBindAddress(t *testing.T) { + tempDir := t.TempDir() + configFile := filepath.Join(tempDir, "cfg.yaml") + content := ` +instance_id: test +remote_address: localhost:8080 +bind_address: 0.0.0.0 +ports: + - tcp://80:8080 +encryption_key: a0e3dd20a761b118ca234160dd8b87230a001e332a97c9cfe3b8b9c99efaae03 +dns_server: + enabled: true + listen_port: 5353 + bind_address: 0.0.0.0 + backup_server: 8.8.8.8:53 +` + if err := os.WriteFile(configFile, []byte(content), 0644); err != nil { + t.Fatal(err) + } + cfg, err := LoadConfig(configFile) + if err != nil { + t.Fatalf("load: %v", err) + } + if cfg.BindAddress != "0.0.0.0" { + t.Fatalf("BindAddress got %q", cfg.BindAddress) + } + if cfg.DNSServer.BindAddress != "0.0.0.0" { + t.Fatalf("DNS BindAddress got %q", cfg.DNSServer.BindAddress) + } + if got := cfg.ClientListenAddr(cfg.Ports[0]); got != "0.0.0.0:8080" { + t.Fatalf("client listen got %q", got) + } + if got := cfg.DNSServer.ListenAddr(); got != "0.0.0.0:5353" { + t.Fatalf("dns listen got %q", got) + } +} + +func TestGenerateExampleConfigUsesLoopback(t *testing.T) { + tempDir := t.TempDir() + serverFile := filepath.Join(tempDir, "server.yaml") + if err := GenerateExampleConfig(serverFile); err != nil { + t.Fatal(err) + } + data, err := os.ReadFile(serverFile) + if err != nil { + t.Fatal(err) + } + body := string(data) + if strings.Contains(body, "listen_address: :8080") || strings.Contains(body, "listen_address: :9000") { + t.Fatalf("generated server config still documents all-interfaces listen: %s", body) + } + if !strings.Contains(body, "127.0.0.1:8080") { + t.Fatalf("generated server config missing loopback listen_address: %s", body) + } + + clientFile := filepath.Join(tempDir, "client.yaml") + if err := GenerateExampleConfig(clientFile); err != nil { + t.Fatal(err) + } + cfg, err := LoadConfig(clientFile) + if err != nil { + t.Fatalf("reload generated client: %v", err) + } + if cfg.BindAddress != DefaultBindAddress { + t.Fatalf("generated client bind_address %q", cfg.BindAddress) + } + if got := cfg.ClientListenAddr(cfg.Ports[0]); !strings.HasPrefix(got, "127.0.0.1:") { + t.Fatalf("generated client local listen %q", got) + } +} diff --git a/pkg/dns/dns.go b/pkg/dns/dns.go index 05aa88f..2dd93b5 100644 --- a/pkg/dns/dns.go +++ b/pkg/dns/dns.go @@ -1,7 +1,6 @@ package dns import ( - "fmt" "net" "strings" "time" @@ -18,9 +17,10 @@ func StartDNSServer(cfg *config.Config) { return } + listenAddr := cfg.DNSServer.ListenAddr() // Create DNS server server := &dns.Server{ - Addr: fmt.Sprintf(":%d", cfg.DNSServer.ListenPort), + Addr: listenAddr, Net: "udp", } @@ -29,7 +29,10 @@ func StartDNSServer(cfg *config.Config) { handleDNSQuery(w, r, cfg) }) - logger.WithField("port", cfg.DNSServer.ListenPort).Info("DNS server started") + logger.WithFields(map[string]interface{}{ + "addr": listenAddr, + "port": cfg.DNSServer.ListenPort, + }).Info("DNS server started") // Start server if err := server.ListenAndServe(); err != nil {