mirror of
https://github.com/caddyserver/caddy.git
synced 2025-12-08 06:09:53 +00:00
697 lines
25 KiB
Go
697 lines
25 KiB
Go
// Copyright 2025 Matthew Holt and The Caddy Authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package caddy
|
|
|
|
import (
|
|
"os"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestIsInterfaceName(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
expected bool
|
|
desc string
|
|
}{
|
|
// Valid interface names
|
|
{"eth0", true, "typical ethernet interface"},
|
|
{"wlan0", true, "wireless interface"},
|
|
{"tailscale0", true, "tailscale interface"},
|
|
{"enp0s3", true, "predictable network interface name"},
|
|
{"lo", true, "loopback interface"},
|
|
{"docker0", true, "docker bridge interface"},
|
|
{"br-901e40e4488d", true, "docker custom bridge interface"},
|
|
{"enx9cbf0d00631a", true, "USB ethernet adapter interface"},
|
|
{"veth1308dcd", true, "docker veth pair interface"},
|
|
|
|
// Invalid interface names (IP addresses)
|
|
{"192.168.1.1", false, "IPv4 address"},
|
|
{"127.0.0.1", false, "localhost IPv4"},
|
|
{"::1", false, "IPv6 localhost"},
|
|
{"2001:db8::1", false, "IPv6 address"},
|
|
{"fe80::", false, "IPv6 link-local address starting with letter"},
|
|
{"example.com", false, "hostname with dots"},
|
|
{"localhost", false, "hostname"},
|
|
{"my-host.local", false, "hostname with dashes and dots"},
|
|
{"3", false, "numeric file descriptor"},
|
|
{"10", false, "another numeric file descriptor"},
|
|
{"", false, "empty string"},
|
|
{"eth/0", false, "interface with forward slash"},
|
|
{"eth\\0", false, "interface with backslash"},
|
|
{"eth\n0", false, "interface with newline"},
|
|
{"eth\t0", false, "interface with tab"},
|
|
{"eth\x00", false, "interface with null character"},
|
|
|
|
// Invalid interface names (unregistered Caddy placeholders that won't be replaced)
|
|
{"{upstream}", false, "Caddy upstream placeholder (not registered in global replacer)"},
|
|
{"{http.request.host}", false, "Caddy HTTP placeholder (not registered in global replacer)"},
|
|
{"{vars.interface}", false, "Caddy variable placeholder (not registered in global replacer)"},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
result := isInterfaceName(test.input)
|
|
if result != test.expected {
|
|
t.Errorf("isInterfaceName(%q) = %v, expected %v (%s)",
|
|
test.input, result, test.expected, test.desc)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestIsInterfaceNameWindows(t *testing.T) {
|
|
if runtime.GOOS != "windows" {
|
|
t.Skip("Windows-specific test, skipping on non-Windows platform")
|
|
}
|
|
|
|
windowsTests := []struct {
|
|
input string
|
|
expected bool
|
|
desc string
|
|
}{
|
|
// Typical Windows interface names
|
|
{"Wi-Fi 2", true, "Windows Wi-Fi interface"},
|
|
{"vEthernet (WSL (Hyper-V firewall))", true, "Windows WSL virtual interface"},
|
|
{"Local Area Connection", true, "Windows LAN connection"},
|
|
{"Loopback Pseudo-Interface 1", true, "Windows loopback (should be detected as interface name)"},
|
|
{"Ethernet", true, "Windows Ethernet interface"},
|
|
{"OpenVPN Connect DCO Adapter", true, "Windows VPN adapter"},
|
|
|
|
// Should still reject invalid ones
|
|
{"192.168.1.1", false, "IP address should still fail"},
|
|
{"example.com", false, "hostname should still fail"},
|
|
{"", false, "empty string should still fail"},
|
|
}
|
|
|
|
for _, test := range windowsTests {
|
|
result := isInterfaceName(test.input)
|
|
if result != test.expected {
|
|
t.Errorf("isInterfaceName(%q) = %v, expected %v (%s)",
|
|
test.input, result, test.expected, test.desc)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestIsInterfaceNameUnix(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("Unix-specific test, skipping on Windows platform")
|
|
}
|
|
|
|
unixTests := []struct {
|
|
input string
|
|
expected bool
|
|
desc string
|
|
}{
|
|
// Should pass on Unix systems
|
|
{"eth0", true, "short ethernet interface"},
|
|
{"wlan0", true, "short wireless interface"},
|
|
{"br-901e40e4488d", true, "docker bridge (14 chars)"},
|
|
{"enx9cbf0d00631a", true, "USB ethernet (15 chars)"},
|
|
|
|
// Should fail on Unix systems - too long (would pass on Windows)
|
|
{"verylonginterfacename", false, "too long for Unix (22 chars)"},
|
|
{"Local Area Connection", false, "Windows-style name too long for Unix"},
|
|
}
|
|
|
|
for _, test := range unixTests {
|
|
result := isInterfaceName(test.input)
|
|
if result != test.expected {
|
|
t.Errorf("isInterfaceName(%q) = %v, expected %v (%s)",
|
|
test.input, result, test.expected, test.desc)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestIsInterfaceNameWithModes(t *testing.T) {
|
|
// Test isInterfaceName with interface:port:mode patterns
|
|
tests := []struct {
|
|
input string
|
|
expected bool
|
|
desc string
|
|
}{
|
|
// Valid interface:port:mode patterns
|
|
{"eth0:8080:ipv4", true, "ethernet interface with IPv4 mode"},
|
|
{"wlan0:443:ipv6", true, "wireless interface with IPv6 mode"},
|
|
{"docker0:9000:auto", true, "docker interface with auto mode"},
|
|
{"enp0s3:8080:ipv4", true, "predictable interface with mode"},
|
|
{"br-901e40e4488d:3000:ipv6", true, "docker bridge with IPv6 mode"},
|
|
{"veth1308dcd:8080:auto", true, "veth pair with auto mode"},
|
|
{"eth0:8080:all", true, "ethernet interface with all mode"},
|
|
{"eth0:8080:firstipv4", true, "ethernet interface with firstipv4 mode"},
|
|
{"wlan0:443:firstipv6", true, "wireless interface with firstipv6 mode"},
|
|
{"docker0:9000:firstipv4", true, "docker interface with firstipv4 mode"},
|
|
{"enp0s3:8080:firstipv6", true, "predictable interface with firstipv6 mode"},
|
|
|
|
// Invalid - wrong modes
|
|
{"eth0:8080:invalid", false, "interface with invalid mode"},
|
|
{"docker0:9000:tcp", false, "interface with non-binding mode"},
|
|
|
|
// Invalid - not interface names
|
|
{"192.168.1.1:80:ipv4", false, "IP address with mode"},
|
|
{"fe80:::8080:ipv6", false, "IPv6 address with mode"},
|
|
{"example.com:443:ipv6", false, "hostname with mode"},
|
|
{"localhost:8080:auto", false, "localhost with mode"},
|
|
|
|
// Edge cases
|
|
{"eth0:8080", false, "interface with port but no mode"},
|
|
{"eth0", true, "plain interface name should still work"},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
result := isInterfaceName(test.input)
|
|
if result != test.expected {
|
|
t.Errorf("isInterfaceName(%q) = %v, expected %v (%s)",
|
|
test.input, result, test.expected, test.desc)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSelectIPByMode(t *testing.T) {
|
|
// Test the IP selection logic with mock data
|
|
testCases := []struct {
|
|
mode InterfaceBindingMode
|
|
ipAddresses []string
|
|
expectedResults []string
|
|
expectError bool
|
|
desc string
|
|
}{
|
|
{
|
|
InterfaceBindingAuto,
|
|
[]string{"192.168.1.100", "fe80::1"},
|
|
[]string{"192.168.1.100"}, // Should prefer IPv4
|
|
false,
|
|
"auto mode prefers IPv4",
|
|
},
|
|
{
|
|
InterfaceBindingAuto,
|
|
[]string{"fe80::1", "2001:db8::1"},
|
|
[]string{"fe80::1"}, // Should fallback to IPv6
|
|
false,
|
|
"auto mode fallback to IPv6",
|
|
},
|
|
{
|
|
InterfaceBindingFirstIPv4,
|
|
[]string{"192.168.1.100", "10.0.0.1", "fe80::1"},
|
|
[]string{"192.168.1.100"}, // Should use first IPv4
|
|
false,
|
|
"firstipv4 mode uses first IPv4",
|
|
},
|
|
{
|
|
InterfaceBindingFirstIPv6,
|
|
[]string{"192.168.1.100", "fe80::1", "2001:db8::1"},
|
|
[]string{"fe80::1"}, // Should use first IPv6
|
|
false,
|
|
"firstipv6 mode uses first IPv6",
|
|
},
|
|
{
|
|
InterfaceBindingIPv4,
|
|
[]string{"192.168.1.100", "10.0.0.1", "fe80::1"},
|
|
[]string{"192.168.1.100", "10.0.0.1"}, // Should return all IPv4
|
|
false,
|
|
"ipv4 mode returns all IPv4 addresses",
|
|
},
|
|
{
|
|
InterfaceBindingIPv6,
|
|
[]string{"192.168.1.100", "fe80::1", "2001:db8::1"},
|
|
[]string{"fe80::1", "2001:db8::1"}, // Should return all IPv6
|
|
false,
|
|
"ipv6 mode returns all IPv6 addresses",
|
|
},
|
|
{
|
|
InterfaceBindingAll,
|
|
[]string{"192.168.1.100", "10.0.0.1", "fe80::1", "2001:db8::1"},
|
|
[]string{"192.168.1.100", "10.0.0.1", "fe80::1", "2001:db8::1"}, // Should return all IPs (IPv4 first, then IPv6)
|
|
false,
|
|
"all mode returns all IP addresses",
|
|
},
|
|
{
|
|
InterfaceBindingAll,
|
|
[]string{"192.168.1.100", "fe80::1"},
|
|
[]string{"192.168.1.100", "fe80::1"}, // Should return all IPs
|
|
false,
|
|
"all mode with mixed addresses",
|
|
},
|
|
{
|
|
InterfaceBindingAll,
|
|
[]string{"192.168.1.100"},
|
|
[]string{"192.168.1.100"}, // Single IPv4
|
|
false,
|
|
"all mode with single IPv4",
|
|
},
|
|
{
|
|
InterfaceBindingAll,
|
|
[]string{"fe80::1"},
|
|
[]string{"fe80::1"}, // Single IPv6
|
|
false,
|
|
"all mode with single IPv6",
|
|
},
|
|
{
|
|
InterfaceBindingIPv4,
|
|
[]string{"fe80::1"},
|
|
nil, // Should error - no IPv4
|
|
true,
|
|
"ipv4 mode with no IPv4 addresses should error",
|
|
},
|
|
{
|
|
InterfaceBindingIPv6,
|
|
[]string{"192.168.1.100"},
|
|
nil, // Should error - no IPv6
|
|
true,
|
|
"ipv6 mode with no IPv6 addresses should error",
|
|
},
|
|
{
|
|
InterfaceBindingAuto,
|
|
[]string{},
|
|
nil, // Should error - no addresses
|
|
true,
|
|
"auto mode with no addresses should error",
|
|
},
|
|
{
|
|
InterfaceBindingAll,
|
|
[]string{},
|
|
nil, // Should error - no addresses
|
|
true,
|
|
"all mode with no addresses should error",
|
|
},
|
|
{
|
|
InterfaceBindingAuto,
|
|
[]string{"invalid_ip", "also_invalid"},
|
|
nil, // Should error - no valid IPs
|
|
true,
|
|
"auto mode with invalid IP addresses should error",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
results, err := selectIPByMode(tc.ipAddresses, tc.mode)
|
|
|
|
if tc.expectError {
|
|
if err == nil {
|
|
t.Errorf("selectIPByMode(%v, %s) should have failed (%s)",
|
|
tc.ipAddresses, tc.mode, tc.desc)
|
|
}
|
|
continue
|
|
}
|
|
|
|
if err != nil {
|
|
t.Errorf("selectIPByMode(%v, %s) failed unexpectedly: %v (%s)",
|
|
tc.ipAddresses, tc.mode, err, tc.desc)
|
|
continue
|
|
}
|
|
|
|
if len(results) != len(tc.expectedResults) {
|
|
t.Errorf("selectIPByMode(%v, %s) returned %d results, expected %d (%s)",
|
|
tc.ipAddresses, tc.mode, len(results), len(tc.expectedResults), tc.desc)
|
|
continue
|
|
}
|
|
|
|
for i, result := range results {
|
|
if result != tc.expectedResults[i] {
|
|
t.Errorf("selectIPByMode(%v, %s) result[%d] = %s, expected %s (%s)",
|
|
tc.ipAddresses, tc.mode, i, result, tc.expectedResults[i], tc.desc)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestIsInterfaceNameWithPlaceholders(t *testing.T) {
|
|
// Set up environment variables for testing
|
|
os.Setenv("TEST_VALID_INTERFACE", "eth0")
|
|
os.Setenv("TEST_INVALID_INTERFACE", "192.168.1.1")
|
|
os.Setenv("INTERFACE_NUM", "1")
|
|
os.Setenv("PREFIX", "wlan")
|
|
defer func() {
|
|
os.Unsetenv("TEST_VALID_INTERFACE")
|
|
os.Unsetenv("TEST_INVALID_INTERFACE")
|
|
os.Unsetenv("INTERFACE_NUM")
|
|
os.Unsetenv("PREFIX")
|
|
}()
|
|
|
|
// Create temporary files for testing
|
|
validTempFile, err := os.CreateTemp("", "valid_interface_*.txt")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp file: %v", err)
|
|
}
|
|
defer os.Remove(validTempFile.Name())
|
|
validTempFile.WriteString("wlan0")
|
|
validTempFile.Close()
|
|
|
|
invalidTempFile, err := os.CreateTemp("", "invalid_interface_*.txt")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp file: %v", err)
|
|
}
|
|
defer os.Remove(invalidTempFile.Name())
|
|
invalidTempFile.WriteString("example.com") // Invalid interface (hostname)
|
|
invalidTempFile.Close()
|
|
|
|
emptyTempFile, err := os.CreateTemp("", "empty_interface_*.txt")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create empty temp file: %v", err)
|
|
}
|
|
defer os.Remove(emptyTempFile.Name())
|
|
emptyTempFile.Close() // Keep it empty
|
|
|
|
tests := []struct {
|
|
input string
|
|
expected bool
|
|
desc string
|
|
}{
|
|
// Valid placeholders resolving to valid interfaces
|
|
{"{env.TEST_VALID_INTERFACE}", true, "env placeholder resolving to valid interface"},
|
|
{"{file." + validTempFile.Name() + "}", true, "file placeholder resolving to valid interface"},
|
|
|
|
// Valid partial placeholders resolving to valid interfaces
|
|
{"eth{env.INTERFACE_NUM}", true, "partial env placeholder resolving to eth1"},
|
|
{"{env.PREFIX}0", true, "env placeholder with suffix resolving to wlan0"},
|
|
{"docker{env.INTERFACE_NUM}", true, "prefix with env placeholder resolving to docker1"},
|
|
|
|
// Valid placeholders resolving to invalid interfaces
|
|
{"{env.TEST_INVALID_INTERFACE}", false, "env placeholder resolving to IP address"},
|
|
{"{file." + invalidTempFile.Name() + "}", false, "file placeholder resolving to hostname"},
|
|
|
|
// Unregistered placeholders (not in global replacer, won't be replaced)
|
|
{"{http.request.host}", false, "HTTP placeholder (not in global replacer)"},
|
|
{"{vars.interface}", false, "vars placeholder (not in global replacer)"},
|
|
{"{upstream}", false, "upstream placeholder (not in global replacer)"},
|
|
|
|
// Mixed with unregistered placeholders (partial replacement will fail)
|
|
{"eth{env.INTERFACE_NUM}-{http.request.host}", false, "mixed env and HTTP (HTTP not replaced, contains {)"},
|
|
{"{env.PREFIX}-{vars.suffix}", false, "mixed env and vars (vars not replaced, contains {)"},
|
|
|
|
// Invalid placeholder resolution
|
|
{"{env.NONEXISTENT}", false, "nonexistent environment variable"},
|
|
{"{file." + emptyTempFile.Name() + "}", false, "empty file content"},
|
|
|
|
// Invalid placeholder syntax
|
|
{"{invalid}", false, "invalid placeholder without prefix"},
|
|
{"{env.}", false, "empty env placeholder"},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
result := isInterfaceName(test.input)
|
|
if result != test.expected {
|
|
t.Errorf("isInterfaceName(%q) = %v, expected %v (%s)",
|
|
test.input, result, test.expected, test.desc)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNetworkAddressIsInterfaceNetwork(t *testing.T) {
|
|
tests := []struct {
|
|
na NetworkAddress
|
|
expected bool
|
|
desc string
|
|
}{
|
|
{
|
|
NetworkAddress{Network: "tcp", Host: "eth0"},
|
|
true,
|
|
"TCP with interface name",
|
|
},
|
|
{
|
|
NetworkAddress{Network: "udp", Host: "wlan0"},
|
|
true,
|
|
"UDP with interface name",
|
|
},
|
|
{
|
|
NetworkAddress{Network: "tcp", Host: "192.168.1.1"},
|
|
false,
|
|
"TCP with IP address",
|
|
},
|
|
{
|
|
NetworkAddress{Network: "unix", Host: "eth0"},
|
|
false,
|
|
"Unix socket with interface-like name",
|
|
},
|
|
{
|
|
NetworkAddress{Network: "tcp", Host: "example.com"},
|
|
false,
|
|
"TCP with hostname",
|
|
},
|
|
// Test encoded interface names with binding modes
|
|
{
|
|
NetworkAddress{Network: "tcp", Host: "wlan0" + InterfaceDelimiter + "ipv4"},
|
|
true,
|
|
"TCP with encoded interface name and IPv4 mode",
|
|
},
|
|
{
|
|
NetworkAddress{Network: "tcp", Host: "eth0" + InterfaceDelimiter + "ipv6"},
|
|
true,
|
|
"TCP with encoded interface name and IPv6 mode",
|
|
},
|
|
{
|
|
NetworkAddress{Network: "udp", Host: "tailscale0" + InterfaceDelimiter + "ipv4"},
|
|
true,
|
|
"UDP with encoded interface name and mode",
|
|
},
|
|
{
|
|
NetworkAddress{Network: "tcp", Host: "example.com" + InterfaceDelimiter + "ipv4"},
|
|
false,
|
|
"TCP with hostname that has mode encoding (should be false)",
|
|
},
|
|
{
|
|
NetworkAddress{Network: "tcp", Host: "192.168.1.1" + InterfaceDelimiter + "auto"},
|
|
false,
|
|
"TCP with IP address that has mode encoding (should be false)",
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
result := test.na.IsInterfaceNetwork()
|
|
if result != test.expected {
|
|
t.Errorf("NetworkAddress{%s, %s}.IsInterfaceNetwork() = %v, expected %v (%s)",
|
|
test.na.Network, test.na.Host, result, test.expected, test.desc)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseInterfaceAddress(t *testing.T) {
|
|
tests := []struct {
|
|
network string
|
|
host string
|
|
port string
|
|
expectedHost string
|
|
expectErr bool
|
|
desc string
|
|
}{
|
|
// Valid cases - different interfaces and networks
|
|
{"tcp", "eth0", "80", "eth0" + InterfaceDelimiter + "auto", false, "valid interface with port"},
|
|
{"udp", "wlan0", "8080", "wlan0" + InterfaceDelimiter + "auto", false, "valid interface with different port"},
|
|
{"tcp", "eth0", "8000-8010", "eth0" + InterfaceDelimiter + "auto", false, "valid interface with port range"},
|
|
{"tcp", "wlan0", "443-444", "wlan0" + InterfaceDelimiter + "auto", false, "valid interface with small port range"},
|
|
{"tcp", "enp0s3", "9000-9100", "enp0s3" + InterfaceDelimiter + "auto", false, "valid interface with larger port range"},
|
|
{"tcp", "enp0s3", "9000", "enp0s3" + InterfaceDelimiter + "auto", false, "predictable interface name"},
|
|
{"tcp", "docker0", "3000", "docker0" + InterfaceDelimiter + "auto", false, "docker bridge interface"},
|
|
|
|
// Valid cases - different binding modes
|
|
{"tcp", "eth0", "443:ipv4", "eth0" + InterfaceDelimiter + "ipv4", false, "valid interface with IPv4 mode"},
|
|
{"tcp", "wlan0", "8080:ipv6", "wlan0" + InterfaceDelimiter + "ipv6", false, "valid interface with IPv6 mode"},
|
|
{"tcp", "enp0s3", "9000:auto", "enp0s3" + InterfaceDelimiter + "auto", false, "valid interface with explicit auto mode"},
|
|
{"tcp", "eth0", "443:all", "eth0" + InterfaceDelimiter + "all", false, "valid interface with all mode"},
|
|
{"tcp", "wlan0", "8080-8090:all", "wlan0" + InterfaceDelimiter + "all", false, "port range with all mode"},
|
|
{"tcp", "eth0", "443:firstipv4", "eth0" + InterfaceDelimiter + "firstipv4", false, "valid interface with firstipv4 mode"},
|
|
{"tcp", "wlan0", "8080:firstipv6", "wlan0" + InterfaceDelimiter + "firstipv6", false, "valid interface with firstipv6 mode"},
|
|
{"tcp", "docker0", "9000:firstipv4", "docker0" + InterfaceDelimiter + "firstipv4", false, "docker interface with firstipv4 mode"},
|
|
{"tcp", "enp0s3", "8080:firstipv6", "enp0s3" + InterfaceDelimiter + "firstipv6", false, "predictable interface with firstipv6 mode"},
|
|
|
|
// Valid cases - port ranges with binding modes
|
|
{"tcp", "eth0", "8080-8090:ipv4", "eth0" + InterfaceDelimiter + "ipv4", false, "port range with IPv4 mode"},
|
|
{"tcp", "wlan0", "443-445:ipv6", "wlan0" + InterfaceDelimiter + "ipv6", false, "port range with IPv6 mode"},
|
|
{"tcp", "docker0", "3000-3010:auto", "docker0" + InterfaceDelimiter + "auto", false, "port range with auto mode"},
|
|
{"tcp", "eth0", "8080-8090:firstipv4", "eth0" + InterfaceDelimiter + "firstipv4", false, "port range with firstipv4 mode"},
|
|
{"tcp", "wlan0", "443-445:firstipv6", "wlan0" + InterfaceDelimiter + "firstipv6", false, "port range with firstipv6 mode"},
|
|
|
|
// Error cases - invalid hosts
|
|
{"tcp", "192.168.1.1", "80", "", true, "IP address should fail"},
|
|
{"tcp", "example.com", "80", "", true, "hostname should fail"},
|
|
{"tcp", "localhost", "80", "", true, "localhost should fail"},
|
|
{"tcp", "", "80", "", true, "empty interface should fail"},
|
|
|
|
// Error cases - invalid ports
|
|
{"tcp", "eth0", "", "", true, "missing port should fail"},
|
|
{"tcp", "eth0", "invalid", "", true, "invalid port should fail"},
|
|
{"tcp", "eth0", "70000", "", true, "port too high should fail"},
|
|
{"tcp", "eth0", "8090-8080", "", true, "reversed port range should fail"},
|
|
{"tcp", "eth0", "8080-invalid", "", true, "invalid end port in range should fail"},
|
|
{"tcp", "eth0", "invalid-8090", "", true, "invalid start port in range should fail"},
|
|
{"tcp", "eth0", "8080-", "", true, "missing end port in range should fail"},
|
|
{"tcp", "eth0", "-8090", "", true, "missing start port in range should fail"},
|
|
|
|
// Error cases - invalid binding modes
|
|
{"tcp", "eth0", "443:invalid", "", true, "invalid mode should fail"},
|
|
{"tcp", "eth0", "443:tcp", "", true, "non-binding mode should fail"},
|
|
{"tcp", "eth0", "443:", "", true, "empty mode should fail"},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
na, err := parseInterfaceAddress(test.network, test.host, test.port)
|
|
|
|
if test.expectErr {
|
|
if err == nil {
|
|
t.Errorf("parseInterfaceAddress(%s, %s, %s) should have failed (%s)",
|
|
test.network, test.host, test.port, test.desc)
|
|
}
|
|
continue
|
|
}
|
|
|
|
if err != nil {
|
|
t.Errorf("parseInterfaceAddress(%s, %s, %s) failed: %v (%s)",
|
|
test.network, test.host, test.port, err, test.desc)
|
|
continue
|
|
}
|
|
|
|
if na.Network != test.network {
|
|
t.Errorf("parseInterfaceAddress(%s, %s, %s) network = %s, expected %s",
|
|
test.network, test.host, test.port, na.Network, test.network)
|
|
}
|
|
|
|
if na.Host != test.expectedHost {
|
|
t.Errorf("parseInterfaceAddress(%s, %s, %s) host = %s, expected %s",
|
|
test.network, test.host, test.port, na.Host, test.expectedHost)
|
|
}
|
|
|
|
// For valid cases, also verify the mode encoding/decoding
|
|
parts := strings.SplitN(na.Host, InterfaceDelimiter, 2)
|
|
if len(parts) != 2 {
|
|
t.Errorf("parseInterfaceAddress(%s, %s, %s) should encode mode in Host field",
|
|
test.network, test.host, test.port)
|
|
} else {
|
|
// Verify interface name is preserved
|
|
if parts[0] != test.host {
|
|
t.Errorf("parseInterfaceAddress(%s, %s, %s) interface = %s, expected %s",
|
|
test.network, test.host, test.port, parts[0], test.host)
|
|
}
|
|
// Verify mode is valid
|
|
mode := InterfaceBindingMode(parts[1])
|
|
if mode != InterfaceBindingAuto && mode != InterfaceBindingFirstIPv4 && mode != InterfaceBindingFirstIPv6 &&
|
|
mode != InterfaceBindingIPv4 && mode != InterfaceBindingIPv6 && mode != InterfaceBindingAll {
|
|
t.Errorf("parseInterfaceAddress(%s, %s, %s) invalid mode: %s",
|
|
test.network, test.host, test.port, mode)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTryParseInterfaceWithModeInHost(t *testing.T) {
|
|
tests := []struct {
|
|
host string
|
|
expectedInterface string
|
|
expectedPortWithMode string
|
|
expectedSuccess bool
|
|
desc string
|
|
}{
|
|
// Valid cases
|
|
{"eth0:8080:ipv4", "eth0", "8080:ipv4", true, "Ethernet interface with IPv4 mode"},
|
|
{"wlan0:443:ipv6", "wlan0", "443:ipv6", true, "Wireless interface with IPv6 mode"},
|
|
{"enp0s3:9000:auto", "enp0s3", "9000:auto", true, "Predictable network interface with auto mode"},
|
|
{"tailscale0:8090:ipv4", "tailscale0", "8090:ipv4", true, "Tailscale interface with IPv4 mode"},
|
|
{"docker0:3000:ipv6", "docker0", "3000:ipv6", true, "Docker bridge interface with IPv6 mode"},
|
|
{"wlan0:443:all", "wlan0", "443:all", true, "Wireless interface with all mode"},
|
|
{"eth0:8080:firstipv4", "eth0", "8080:firstipv4", true, "Ethernet interface with firstipv4 mode"},
|
|
{"wlan0:443:firstipv6", "wlan0", "443:firstipv6", true, "Wireless interface with firstipv6 mode"},
|
|
{"docker0:9000:firstipv4", "docker0", "9000:firstipv4", true, "Docker interface with firstipv4 mode"},
|
|
{"enp0s3:8080:firstipv6", "enp0s3", "8080:firstipv6", true, "Predictable interface with firstipv6 mode"},
|
|
|
|
// Invalid cases - not enough parts
|
|
{"eth0", "", "", false, "Interface name only"},
|
|
{"eth0:8080", "", "", false, "Interface with port but no mode"},
|
|
|
|
// Invalid cases - invalid mode
|
|
{"eth0:8080:invalid", "", "", false, "Invalid binding mode"},
|
|
{"enp0s3:8080:tcp", "", "", false, "Non-binding mode"},
|
|
|
|
// Invalid cases - invalid interface name
|
|
{"192.168.1.1:80:ipv4", "", "", false, "IP address instead of interface"},
|
|
{"example.com:443:ipv6", "", "", false, "Hostname instead of interface"},
|
|
{"localhost:8080:auto", "", "", false, "Localhost hostname"},
|
|
|
|
// Edge cases
|
|
{"", "", "", false, "Empty string"},
|
|
{"br-1234567890ab:8080:ipv4", "br-1234567890ab", "8080:ipv4", true, "Docker custom bridge interface"},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
result, success := tryParseInterfaceWithModeInHost(test.host)
|
|
|
|
if success != test.expectedSuccess {
|
|
t.Errorf("tryParseInterfaceWithModeInHost(%q) success = %v, expected %v (%s)",
|
|
test.host, success, test.expectedSuccess, test.desc)
|
|
continue
|
|
}
|
|
|
|
if !test.expectedSuccess {
|
|
continue // Skip checking values for cases that should fail
|
|
}
|
|
|
|
if result.interfaceName != test.expectedInterface {
|
|
t.Errorf("tryParseInterfaceWithModeInHost(%q) interfaceName = %q, expected %q (%s)",
|
|
test.host, result.interfaceName, test.expectedInterface, test.desc)
|
|
}
|
|
|
|
if result.portWithMode != test.expectedPortWithMode {
|
|
t.Errorf("tryParseInterfaceWithModeInHost(%q) portWithMode = %q, expected %q (%s)",
|
|
test.host, result.portWithMode, test.expectedPortWithMode, test.desc)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseNetworkAddressWithInterface(t *testing.T) {
|
|
tests := []struct {
|
|
addr string
|
|
expectErr bool
|
|
desc string
|
|
}{
|
|
{"eth0:80", false, "interface with port"},
|
|
{"tcp/wlan0:8080", false, "explicit TCP with interface"},
|
|
{"udp/eth0:53", false, "explicit UDP with interface"},
|
|
{"eth0", true, "interface without port should fail in default parsing"},
|
|
{"192.168.1.1:80", false, "regular IP address should still work"},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
na, err := ParseNetworkAddress(test.addr)
|
|
|
|
if test.expectErr {
|
|
if err == nil {
|
|
t.Errorf("ParseNetworkAddress(%s) should have failed (%s)", test.addr, test.desc)
|
|
}
|
|
continue
|
|
}
|
|
|
|
if err != nil {
|
|
t.Errorf("ParseNetworkAddress(%s) failed: %v (%s)", test.addr, err, test.desc)
|
|
continue
|
|
}
|
|
|
|
// For interface addresses, verify they are detected correctly
|
|
if isInterfaceName(na.Host) {
|
|
if !na.IsInterfaceNetwork() {
|
|
t.Errorf("ParseNetworkAddress(%s) should detect interface network (%s)", test.addr, test.desc)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Test interface without port with explicit default port (should work)
|
|
na, err := ParseNetworkAddressWithDefaults("eth0", "tcp", 8080)
|
|
if err != nil {
|
|
t.Errorf("ParseNetworkAddressWithDefaults(eth0, tcp, 8080) should succeed: %v", err)
|
|
} else {
|
|
if na.StartPort != 8080 || na.EndPort != 8080 {
|
|
t.Errorf("ParseNetworkAddressWithDefaults(eth0, tcp, 8080) should set port to 8080, got %d-%d", na.StartPort, na.EndPort)
|
|
}
|
|
if !na.IsInterfaceNetwork() {
|
|
t.Error("ParseNetworkAddressWithDefaults(eth0, tcp, 8080) should detect interface network")
|
|
}
|
|
}
|
|
}
|