c20398642d
CI / check-and-test (pull_request) Successful in 11s
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
588 lines
15 KiB
Go
588 lines
15 KiB
Go
package config
|
|
|
|
import (
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestLoadConfig(t *testing.T) {
|
|
// Create a temporary config file
|
|
tempDir := t.TempDir()
|
|
configFile := filepath.Join(tempDir, "test-config.yaml")
|
|
|
|
configContent := `
|
|
instance_id: test-instance
|
|
listen_address: 127.0.0.1:8080
|
|
remote_address: ""
|
|
ports:
|
|
- tcp://localhost:22
|
|
- tcp://localhost:80
|
|
encryption_key: a0e3dd20a761b118ca234160dd8b87230a001e332a97c9cfe3b8b9c99efaae03
|
|
keep_alive: true
|
|
read_timeout: 30s
|
|
write_timeout: 30s
|
|
dns_server:
|
|
enabled: true
|
|
listen_port: 5353
|
|
backup_server: 8.8.8.8:53
|
|
custom_records:
|
|
- name: test.local
|
|
type: A
|
|
value: 192.168.1.100
|
|
ttl: 300
|
|
`
|
|
|
|
err := os.WriteFile(configFile, []byte(configContent), 0644)
|
|
if err != nil {
|
|
t.Fatalf("Failed to write test config: %v", err)
|
|
}
|
|
|
|
// Test loading config
|
|
config, err := LoadConfig(configFile)
|
|
if err != nil {
|
|
t.Fatalf("Failed to load config: %v", err)
|
|
}
|
|
|
|
// Verify config values
|
|
if config.InstanceID != "test-instance" {
|
|
t.Errorf("Expected InstanceID 'test-instance', got '%s'", config.InstanceID)
|
|
}
|
|
|
|
if config.ListenAddress != "127.0.0.1:8080" {
|
|
t.Errorf("Expected ListenAddress '127.0.0.1:8080', got '%s'", config.ListenAddress)
|
|
}
|
|
|
|
if config.RemoteAddress != "" {
|
|
t.Errorf("Expected empty RemoteAddress, got '%s'", config.RemoteAddress)
|
|
}
|
|
|
|
if len(config.Ports) != 2 {
|
|
t.Errorf("Expected 2 ports, got %d", len(config.Ports))
|
|
}
|
|
|
|
if config.Ports[0].LocalPort != 22 || config.Ports[0].RemotePort != 22 || config.Ports[0].Protocol != "tcp" || config.Ports[0].TargetHost != "localhost" {
|
|
t.Error("First port rule is incorrect")
|
|
}
|
|
|
|
if config.EncryptionKey != "a0e3dd20a761b118ca234160dd8b87230a001e332a97c9cfe3b8b9c99efaae03" {
|
|
t.Errorf("Expected EncryptionKey 'a0e3dd20a761b118ca234160dd8b87230a001e332a97c9cfe3b8b9c99efaae03', got '%s'", config.EncryptionKey)
|
|
}
|
|
|
|
if !config.KeepAlive {
|
|
t.Error("Expected KeepAlive to be true")
|
|
}
|
|
|
|
if config.ReadTimeout != 30*time.Second {
|
|
t.Errorf("Expected ReadTimeout 30s, got %v", config.ReadTimeout)
|
|
}
|
|
|
|
if config.WriteTimeout != 30*time.Second {
|
|
t.Errorf("Expected WriteTimeout 30s, got %v", config.WriteTimeout)
|
|
}
|
|
|
|
if !config.DNSServer.Enabled {
|
|
t.Error("Expected DNS server to be enabled")
|
|
}
|
|
|
|
if config.DNSServer.ListenPort != 5353 {
|
|
t.Errorf("Expected DNS listen port 5353, got %d", config.DNSServer.ListenPort)
|
|
}
|
|
|
|
if len(config.DNSServer.CustomRecords) != 1 {
|
|
t.Errorf("Expected 1 custom DNS record, got %d", len(config.DNSServer.CustomRecords))
|
|
}
|
|
}
|
|
|
|
func TestLoadConfigDefaults(t *testing.T) {
|
|
// Create a minimal config file
|
|
tempDir := t.TempDir()
|
|
configFile := filepath.Join(tempDir, "minimal-config.yaml")
|
|
|
|
configContent := `
|
|
instance_id: test-instance
|
|
listen_address: 127.0.0.1:8080
|
|
ports:
|
|
- tcp://localhost:22
|
|
encryption_key: a0e3dd20a761b118ca234160dd8b87230a001e332a97c9cfe3b8b9c99efaae03
|
|
`
|
|
|
|
err := os.WriteFile(configFile, []byte(configContent), 0644)
|
|
if err != nil {
|
|
t.Fatalf("Failed to write test config: %v", err)
|
|
}
|
|
|
|
config, err := LoadConfig(configFile)
|
|
if err != nil {
|
|
t.Fatalf("Failed to load config: %v", err)
|
|
}
|
|
|
|
// Check default values
|
|
if config.ReadTimeout != 30*time.Second {
|
|
t.Errorf("Expected default ReadTimeout 30s, got %v", config.ReadTimeout)
|
|
}
|
|
|
|
if config.WriteTimeout != 30*time.Second {
|
|
t.Errorf("Expected default WriteTimeout 30s, got %v", config.WriteTimeout)
|
|
}
|
|
|
|
if config.DNSServer.ListenPort != 5353 {
|
|
t.Errorf("Expected default DNS listen port 5353, got %d", config.DNSServer.ListenPort)
|
|
}
|
|
|
|
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) {
|
|
tests := []struct {
|
|
name string
|
|
listenAddress string
|
|
remoteAddress string
|
|
expectedMode string
|
|
expectedError bool
|
|
}{
|
|
{
|
|
name: "Server mode",
|
|
listenAddress: "127.0.0.1:8080",
|
|
remoteAddress: "",
|
|
expectedMode: "server",
|
|
expectedError: false,
|
|
},
|
|
{
|
|
name: "Client mode",
|
|
listenAddress: "",
|
|
remoteAddress: "127.0.0.1:8080",
|
|
expectedMode: "client",
|
|
expectedError: false,
|
|
},
|
|
{
|
|
name: "Both addresses set - error",
|
|
listenAddress: "127.0.0.1:8080",
|
|
remoteAddress: "127.0.0.1:8080",
|
|
expectedMode: "",
|
|
expectedError: true,
|
|
},
|
|
{
|
|
name: "Neither address set - error",
|
|
listenAddress: "",
|
|
remoteAddress: "",
|
|
expectedMode: "",
|
|
expectedError: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
config := &Config{
|
|
ListenAddress: tt.listenAddress,
|
|
RemoteAddress: tt.remoteAddress,
|
|
}
|
|
|
|
mode, err := DetectMode(config)
|
|
|
|
if tt.expectedError {
|
|
if err == nil {
|
|
t.Error("Expected error but got none")
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Errorf("Unexpected error: %v", err)
|
|
}
|
|
if mode != tt.expectedMode {
|
|
t.Errorf("Expected mode '%s', got '%s'", tt.expectedMode, mode)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLoadConfigFileNotFound(t *testing.T) {
|
|
_, err := LoadConfig("nonexistent-config.yaml")
|
|
if err == nil {
|
|
t.Error("Expected error for nonexistent config file")
|
|
}
|
|
}
|
|
|
|
func TestLoadConfigInvalidYAML(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
configFile := filepath.Join(tempDir, "invalid-config.yaml")
|
|
|
|
// Write invalid YAML
|
|
err := os.WriteFile(configFile, []byte("invalid: yaml: content: ["), 0644)
|
|
if err != nil {
|
|
t.Fatalf("Failed to write test config: %v", err)
|
|
}
|
|
|
|
_, err = LoadConfig(configFile)
|
|
if err == nil {
|
|
t.Error("Expected error for invalid YAML")
|
|
}
|
|
}
|
|
|
|
func TestPortRuleURLFormat(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
config string
|
|
expected PortRule
|
|
}{
|
|
{
|
|
name: "Server format with localhost",
|
|
config: `
|
|
instance_id: test
|
|
listen_address: :8080
|
|
ports:
|
|
- tcp://localhost:80
|
|
encryption_key: a0e3dd20a761b118ca234160dd8b87230a001e332a97c9cfe3b8b9c99efaae03
|
|
`,
|
|
expected: PortRule{
|
|
LocalPort: 80,
|
|
RemotePort: 80,
|
|
Protocol: "tcp",
|
|
TargetHost: "localhost",
|
|
},
|
|
},
|
|
{
|
|
name: "Server format with remote host",
|
|
config: `
|
|
instance_id: test
|
|
listen_address: :8080
|
|
ports:
|
|
- tcp://server-a:22
|
|
encryption_key: a0e3dd20a761b118ca234160dd8b87230a001e332a97c9cfe3b8b9c99efaae03
|
|
`,
|
|
expected: PortRule{
|
|
LocalPort: 22,
|
|
RemotePort: 22,
|
|
Protocol: "tcp",
|
|
TargetHost: "server-a",
|
|
},
|
|
},
|
|
{
|
|
name: "Client format",
|
|
config: `
|
|
instance_id: test
|
|
remote_address: localhost:8080
|
|
ports:
|
|
- tcp://80:8080
|
|
encryption_key: a0e3dd20a761b118ca234160dd8b87230a001e332a97c9cfe3b8b9c99efaae03
|
|
`,
|
|
expected: PortRule{
|
|
LocalPort: 8080,
|
|
RemotePort: 80,
|
|
Protocol: "tcp",
|
|
TargetHost: "",
|
|
},
|
|
},
|
|
{
|
|
name: "UDP format",
|
|
config: `
|
|
instance_id: test
|
|
listen_address: :8080
|
|
ports:
|
|
- udp://dns-server:53
|
|
encryption_key: a0e3dd20a761b118ca234160dd8b87230a001e332a97c9cfe3b8b9c99efaae03
|
|
`,
|
|
expected: PortRule{
|
|
LocalPort: 53,
|
|
RemotePort: 53,
|
|
Protocol: "udp",
|
|
TargetHost: "dns-server",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
configFile := filepath.Join(tempDir, "test-config.yaml")
|
|
|
|
err := os.WriteFile(configFile, []byte(tt.config), 0644)
|
|
if err != nil {
|
|
t.Fatalf("Failed to write test config: %v", err)
|
|
}
|
|
|
|
config, err := LoadConfig(configFile)
|
|
if err != nil {
|
|
t.Fatalf("Failed to load config: %v", err)
|
|
}
|
|
|
|
if len(config.Ports) != 1 {
|
|
t.Fatalf("Expected 1 port, got %d", len(config.Ports))
|
|
}
|
|
|
|
port := config.Ports[0]
|
|
if port.LocalPort != tt.expected.LocalPort {
|
|
t.Errorf("Expected LocalPort %d, got %d", tt.expected.LocalPort, port.LocalPort)
|
|
}
|
|
if port.RemotePort != tt.expected.RemotePort {
|
|
t.Errorf("Expected RemotePort %d, got %d", tt.expected.RemotePort, port.RemotePort)
|
|
}
|
|
if port.Protocol != tt.expected.Protocol {
|
|
t.Errorf("Expected Protocol %s, got %s", tt.expected.Protocol, port.Protocol)
|
|
}
|
|
if port.TargetHost != tt.expected.TargetHost {
|
|
t.Errorf("Expected TargetHost %s, got %s", tt.expected.TargetHost, port.TargetHost)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPortRuleOldFormatFails(t *testing.T) {
|
|
// Test that old object format now fails
|
|
tempDir := t.TempDir()
|
|
configFile := filepath.Join(tempDir, "old-format-config.yaml")
|
|
|
|
configContent := `
|
|
instance_id: test
|
|
listen_address: :8080
|
|
ports:
|
|
- local_port: 22
|
|
remote_port: 22
|
|
protocol: tcp
|
|
encryption_key: test-key
|
|
`
|
|
|
|
err := os.WriteFile(configFile, []byte(configContent), 0644)
|
|
if err != nil {
|
|
t.Fatalf("Failed to write test config: %v", err)
|
|
}
|
|
|
|
_, err = LoadConfig(configFile)
|
|
if err == nil {
|
|
t.Error("Expected error for old format, but got none")
|
|
}
|
|
|
|
// Check that the error message mentions the expected format
|
|
if !strings.Contains(err.Error(), "protocol://") {
|
|
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)
|
|
}
|
|
}
|