Files
x/internal/net/addr_test.go
T
ginuerzh c06ba17916 fix(dialer): skip SO_BINDTODEVICE when interface is specified as IP address
When the user specifies an IP address as the interface (e.g.
interface=10.1.1.1), GOST resolved it to its hosting interface and
called SO_BINDTODEVICE on it — breaking outbound connectivity for
IP aliases on loopback or dummy interfaces (go-gost/gost#785).

The initial fix (caa82f2) only skipped SO_BINDTODEVICE for loopback
interfaces, missing non-loopback dummy interfaces like ip-aliases.

This change adds an isIP return value to ParseInterfaceAddr so callers
can distinguish 'user specified an IP' (intent: source IP binding for
policy routing — skip SO_BINDTODEVICE) from 'user specified an
interface name' (intent: force traffic through that NIC — keep
SO_BINDTODEVICE). The dialOnce method now only calls bindDevice when
bindToDevice is true AND the input was an interface name (!isIP).

The loopback-specific guard in bindDevice is reverted — superseded by
the broader isIP-based fix that covers all interface types.
2026-06-23 21:43:54 +08:00

268 lines
6.9 KiB
Go

package net
import (
"net"
"net/netip"
"testing"
)
func TestPortRange_Parse(t *testing.T) {
tests := []struct {
name string
input string
wantMin int
wantMax int
wantErr bool
}{
{"single port", "8080", 8080, 8080, false},
{"range", "8000-9000", 8000, 9000, false},
{"zero", "0", 0, 0, false},
{"max port", "65535", 65535, 65535, false},
{"negative", "-1", 0, 0, true},
{"too large", "65536", 0, 0, true},
{"three parts", "1-2-3", 0, 0, true},
{"invalid string", "abc", 0, 0, true},
{"invalid min", "abc-123", 0, 0, true},
{"invalid max", "123-abc", 0, 0, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var pr PortRange
err := pr.Parse(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr {
if pr.Min != tt.wantMin || pr.Max != tt.wantMax {
t.Errorf("Parse() got Min=%d Max=%d, want Min=%d Max=%d", pr.Min, pr.Max, tt.wantMin, tt.wantMax)
}
}
})
}
}
func TestPortRange_Contains(t *testing.T) {
pr := PortRange{Min: 8000, Max: 9000}
if !pr.Contains(8000) {
t.Error("should contain min")
}
if !pr.Contains(9000) {
t.Error("should contain max")
}
if !pr.Contains(8500) {
t.Error("should contain middle")
}
if pr.Contains(7999) {
t.Error("should not contain below min")
}
if pr.Contains(9001) {
t.Error("should not contain above max")
}
}
func TestIPRange_Parse(t *testing.T) {
tests := []struct {
name string
input string
wantMin string
wantMax string
wantErr bool
}{
{"single ipv4", "192.168.1.1", "192.168.1.1", "192.168.1.1", false},
{"ipv4 range", "192.168.1.1-192.168.1.254", "192.168.1.1", "192.168.1.254", false},
{"single ipv6", "::1", "::1", "::1", false},
{"ipv6 range", "::1-::2", "::1", "::2", false},
{"three parts", "1-2-3", "", "", true},
{"invalid single", "not-an-ip", "", "", true},
{"invalid min", "not-ip-192.168.1.2", "", "", true},
{"invalid max", "192.168.1.1-not-ip", "", "", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var r IPRange
err := r.Parse(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr {
wantMin := netip.MustParseAddr(tt.wantMin)
wantMax := netip.MustParseAddr(tt.wantMax)
if r.Min != wantMin || r.Max != wantMax {
t.Errorf("Parse() got Min=%v Max=%v, want Min=%v Max=%v", r.Min, r.Max, wantMin, wantMax)
}
}
})
}
}
func TestIPRange_Contains(t *testing.T) {
r := IPRange{
Min: netip.MustParseAddr("192.168.1.0"),
Max: netip.MustParseAddr("192.168.1.255"),
}
if !r.Contains(netip.MustParseAddr("192.168.1.0")) {
t.Error("should contain min")
}
if !r.Contains(netip.MustParseAddr("192.168.1.255")) {
t.Error("should contain max")
}
if !r.Contains(netip.MustParseAddr("192.168.1.100")) {
t.Error("should contain middle")
}
if r.Contains(netip.MustParseAddr("192.168.0.255")) {
t.Error("should not contain below min")
}
if r.Contains(netip.MustParseAddr("192.168.2.0")) {
t.Error("should not contain above max")
}
}
func TestAddrPortRange_Addrs(t *testing.T) {
tests := []struct {
name string
apr AddrPortRange
want []string
}{
{"with scheme", "http://192.168.1.1:8080", nil},
{"tls scheme", "tls://192.168.1.1:8080", nil},
{"invalid format", "not-a-valid-address", nil},
{"single port", "192.168.1.1:8080", []string{"192.168.1.1:8080"}},
{"port range", "192.168.1.1:8080-8082", []string{"192.168.1.1:8080", "192.168.1.1:8081", "192.168.1.1:8082"}},
{"ipv6 single", "[::1]:8080", []string{"[::1]:8080"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.apr.Addrs()
if len(got) != len(tt.want) {
t.Errorf("Addrs() = %v, want %v", got, tt.want)
return
}
for i := range got {
if got[i] != tt.want[i] {
t.Errorf("Addrs()[%d] = %s, want %s", i, got[i], tt.want[i])
}
}
})
}
}
func Test_ipToAddr(t *testing.T) {
ip := net.ParseIP("192.168.1.1")
tests := []struct {
network string
want net.Addr
}{
{"tcp", &net.TCPAddr{IP: ip, Port: 0}},
{"tcp4", &net.TCPAddr{IP: ip, Port: 0}},
{"tcp6", &net.TCPAddr{IP: ip, Port: 0}},
{"udp", &net.UDPAddr{IP: ip, Port: 0}},
{"udp4", &net.UDPAddr{IP: ip, Port: 0}},
{"udp6", &net.UDPAddr{IP: ip, Port: 0}},
{"ip", &net.IPAddr{IP: ip}},
{"other", &net.IPAddr{IP: ip}},
}
for _, tt := range tests {
t.Run(tt.network, func(t *testing.T) {
got := ipToAddr(ip, tt.network)
if got.Network() != tt.want.Network() || got.String() != tt.want.String() {
t.Errorf("ipToAddr() = %#v, want %#v", got, tt.want)
}
})
}
}
func Test_findInterfaceByIP(t *testing.T) {
// Test with localhost IP
ip := net.ParseIP("127.0.0.1")
name, err := findInterfaceByIP(ip)
if err != nil {
t.Fatal(err)
}
// 127.0.0.1 should be on the loopback interface
if name == "" {
t.Log("127.0.0.1 not found on any interface (may happen in containers)")
}
// Test with an IP that shouldn't be assigned
ip2 := net.ParseIP("203.0.113.1")
name2, err2 := findInterfaceByIP(ip2)
if err2 != nil {
t.Fatal(err2)
}
if name2 != "" {
t.Errorf("expected empty name for unassigned IP, got %s", name2)
}
}
func TestParseInterfaceAddr(t *testing.T) {
// Empty interface name
ifce, addrs, isIP, err := ParseInterfaceAddr("", "tcp")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if ifce != "" {
t.Errorf("expected empty ifce, got %s", ifce)
}
if isIP {
t.Errorf("expected isIP=false for empty string, got true")
}
if len(addrs) != 1 || addrs[0] != nil {
t.Errorf("expected [nil], got %v", addrs)
}
// Non-existent interface
_, _, _, err = ParseInterfaceAddr("nonexistent_interface_xyz", "tcp")
if err == nil {
t.Error("expected error for non-existent interface")
}
// IP string as interface name — isIP should be true
ifce, addrs, isIP, err = ParseInterfaceAddr("127.0.0.1", "tcp")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if !isIP {
t.Errorf("expected isIP=true for IP input, got false")
}
if ifce == "" {
t.Log("127.0.0.1 not found on any interface (may happen in containers)")
} else if len(addrs) != 1 {
t.Errorf("expected 1 addr, got %d", len(addrs))
}
// IP that doesn't exist on any interface — should still have isIP=true
ifce, addrs, isIP, err = ParseInterfaceAddr("203.0.113.1", "udp")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if !isIP {
t.Errorf("expected isIP=true for IP input, got false")
}
if ifce != "" {
t.Errorf("expected empty ifce for unassigned IP, got %s", ifce)
}
if len(addrs) != 1 {
t.Errorf("expected 1 addr, got %d", len(addrs))
}
// Test UDP network specific returns
ifce, addrs, isIP, err = ParseInterfaceAddr("127.0.0.1", "udp")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if !isIP {
t.Errorf("expected isIP=true for IP input, got false")
}
if ifce != "" {
if len(addrs) != 1 {
t.Errorf("expected 1 addr, got %d", len(addrs))
}
}
}