From c06ba17916e4c0d64dc94e44c2d48f928a338cfa Mon Sep 17 00:00:00 2001 From: ginuerzh Date: Tue, 23 Jun 2026 21:43:54 +0800 Subject: [PATCH] fix(dialer): skip SO_BINDTODEVICE when interface is specified as IP address MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- internal/net/addr.go | 8 +++++++- internal/net/addr_test.go | 26 +++++++++++++++++++------- internal/net/dialer/dialer.go | 9 +++++---- internal/net/dialer/dialer_linux.go | 8 -------- 4 files changed, 31 insertions(+), 20 deletions(-) diff --git a/internal/net/addr.go b/internal/net/addr.go index d55e15f6..79675939 100644 --- a/internal/net/addr.go +++ b/internal/net/addr.go @@ -14,7 +14,11 @@ import ( // addresses assigned to that interface. The network parameter determines the address // type (TCPAddr for "tcp"/"tcp4"/"tcp6", UDPAddr for "udp"/"udp4"/"udp6", IPAddr // otherwise). -func ParseInterfaceAddr(ifceName, network string) (ifce string, addr []net.Addr, err error) { +// +// isIP reports whether ifceName was parsed as an IP address (true) rather than an +// interface name (false). When isIP is true, callers should skip SO_BINDTODEVICE — +// the user's intent is source IP binding for policy routing, not device binding. +func ParseInterfaceAddr(ifceName, network string) (ifce string, addr []net.Addr, isIP bool, err error) { if ifceName == "" { addr = append(addr, nil) return @@ -43,6 +47,8 @@ func ParseInterfaceAddr(ifceName, network string) (ifce string, addr []net.Addr, } } } else { + // ifceName is an IP address — skip SO_BINDTODEVICE, use LocalAddr only + isIP = true ifce, err = findInterfaceByIP(ip) if err != nil { return diff --git a/internal/net/addr_test.go b/internal/net/addr_test.go index b40d2176..fa6e3de4 100644 --- a/internal/net/addr_test.go +++ b/internal/net/addr_test.go @@ -202,39 +202,48 @@ func Test_findInterfaceByIP(t *testing.T) { func TestParseInterfaceAddr(t *testing.T) { // Empty interface name - ifce, addrs, err := ParseInterfaceAddr("", "tcp") + 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") + _, _, _, err = ParseInterfaceAddr("nonexistent_interface_xyz", "tcp") if err == nil { t.Error("expected error for non-existent interface") } - // IP string as interface name - ifce, addrs, err = ParseInterfaceAddr("127.0.0.1", "tcp") + // 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 - ifce, addrs, err = ParseInterfaceAddr("203.0.113.1", "udp") + // 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) } @@ -243,10 +252,13 @@ func TestParseInterfaceAddr(t *testing.T) { } // Test UDP network specific returns - ifce, addrs, err = ParseInterfaceAddr("127.0.0.1", "udp") + 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)) diff --git a/internal/net/dialer/dialer.go b/internal/net/dialer/dialer.go index 14eab31c..7008892b 100644 --- a/internal/net/dialer/dialer.go +++ b/internal/net/dialer/dialer.go @@ -76,13 +76,14 @@ func (d *Dialer) Dial(ctx context.Context, network, addr string) (conn net.Conn, ifce = strings.TrimSuffix(ifce, "!") var ifceName string var ifAddrs []net.Addr - ifceName, ifAddrs, err = xnet.ParseInterfaceAddr(ifce, network) + var isIP bool + ifceName, ifAddrs, isIP, err = xnet.ParseInterfaceAddr(ifce, network) if err != nil && strict { return } for _, ifAddr := range ifAddrs { - conn, err = d.dialOnce(ctx, network, addr, ifceName, ifAddr, log) + conn, err = d.dialOnce(ctx, network, addr, ifceName, ifAddr, !isIP, log) if err == nil { return } @@ -100,7 +101,7 @@ func (d *Dialer) Dial(ctx context.Context, network, addr string) (conn net.Conn, return } -func (d *Dialer) dialOnce(ctx context.Context, network, addr, ifceName string, ifAddr net.Addr, log logger.Logger) (net.Conn, error) { +func (d *Dialer) dialOnce(ctx context.Context, network, addr, ifceName string, ifAddr net.Addr, bindToDevice bool, log logger.Logger) (net.Conn, error) { if ifceName != "" { log.Debugf("dial %s/%s via interface %s@%s", addr, network, ifceName, ifAddr) } @@ -148,7 +149,7 @@ func (d *Dialer) dialOnce(ctx context.Context, network, addr, ifceName string, i LocalAddr: ifAddr, Control: func(network, address string, c syscall.RawConn) error { return c.Control(func(fd uintptr) { - if ifceName != "" { + if ifceName != "" && bindToDevice { if err := bindDevice(network, address, fd, ifceName); err != nil { log.Warnf("%s/%s bind device: %v", address, network, err) } diff --git a/internal/net/dialer/dialer_linux.go b/internal/net/dialer/dialer_linux.go index 29ce583b..bf5f863e 100644 --- a/internal/net/dialer/dialer_linux.go +++ b/internal/net/dialer/dialer_linux.go @@ -11,14 +11,6 @@ func bindDevice(network, address string, fd uintptr, ifceName string) error { return nil } - // Skip SO_BINDTODEVICE for loopback interface — it prevents - // outbound traffic from reaching non-local destinations. - // Source IP binding via LocalAddr is sufficient for policy routing - // when using IP aliases on loopback (e.g. ip addr add 10.1.1.1/32 dev lo). - if ifce, err := net.InterfaceByName(ifceName); err == nil && ifce.Flags&net.FlagLoopback != 0 { - return nil - } - host, _, _ := net.SplitHostPort(address) if ip := net.ParseIP(host); ip != nil && !ip.IsGlobalUnicast() { return nil