diff --git a/listeners.go b/listeners.go index 62783fac3..47b33c479 100644 --- a/listeners.go +++ b/listeners.go @@ -1049,6 +1049,8 @@ func isInterfaceName(s string) bool { // Check if the last part is a valid binding mode lastPart := parts[2] if lastPart == string(InterfaceBindingAuto) || + lastPart == string(InterfaceBindingFirstIPv4) || + lastPart == string(InterfaceBindingFirstIPv6) || lastPart == string(InterfaceBindingIPv4) || lastPart == string(InterfaceBindingIPv6) || lastPart == string(InterfaceBindingAll) { @@ -1113,6 +1115,8 @@ func tryParseInterfaceWithModeInHost(host string) (interfaceWithMode, bool) { // Check if the last part is a valid binding mode if parts[2] != string(InterfaceBindingAuto) && + parts[2] != string(InterfaceBindingFirstIPv4) && + parts[2] != string(InterfaceBindingFirstIPv6) && parts[2] != string(InterfaceBindingIPv4) && parts[2] != string(InterfaceBindingIPv6) && parts[2] != string(InterfaceBindingAll) { @@ -1162,6 +1166,10 @@ func parseInterfaceAddress(network, host, port string) (NetworkAddress, error) { switch modeStr { case "auto": mode = InterfaceBindingAuto + case "firstipv4": + mode = InterfaceBindingFirstIPv4 + case "firstipv6": + mode = InterfaceBindingFirstIPv6 case "ipv4": mode = InterfaceBindingIPv4 case "ipv6": @@ -1169,7 +1177,7 @@ func parseInterfaceAddress(network, host, port string) (NetworkAddress, error) { case "all": mode = InterfaceBindingAll default: - return NetworkAddress{}, fmt.Errorf("unknown interface binding mode: %s (supported: auto, ipv4, ipv6, all)", modeStr) + return NetworkAddress{}, fmt.Errorf("unknown interface binding mode: %s (supported: auto, firstipv4, firstipv6, ipv4, ipv6, all)", modeStr) } } } else { diff --git a/listeners_interface_test.go b/listeners_interface_test.go index cd795a25f..6dcf85f81 100644 --- a/listeners_interface_test.go +++ b/listeners_interface_test.go @@ -149,6 +149,10 @@ func TestIsInterfaceNameWithModes(t *testing.T) { {"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"}, @@ -496,11 +500,17 @@ func TestParseInterfaceAddress(t *testing.T) { {"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"}, @@ -588,6 +598,10 @@ func TestTryParseInterfaceWithModeInHost(t *testing.T) { {"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"},