From 758708747aa4667c098a68f809d2b8e5ef7f093f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 9 Nov 2026 13:31:48 +0000 Subject: [PATCH] Fix dialing IPv6 link-local addresses without zone --- common/dialer/default.go | 27 ++++++++++- common/dialer/default_parallel_interface.go | 52 ++++++++++----------- 2 files changed, 51 insertions(+), 28 deletions(-) diff --git a/common/dialer/default.go b/common/dialer/default.go index 0c093868c..5a878e201 100644 --- a/common/dialer/default.go +++ b/common/dialer/default.go @@ -6,6 +6,7 @@ import ( "net" "net/netip" "slices" + "strconv" "syscall" "time" @@ -39,6 +40,7 @@ type DefaultDialer struct { udpAddr4 string udpAddr6 string netns string + bindInterface string autoDetectBindFunc control.Func connectionManager adapter.ConnectionManager networkManager adapter.NetworkManager @@ -62,6 +64,7 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial dialer net.Dialer listener net.ListenConfig interfaceFinder control.InterfaceFinder + bindInterface string networkStrategy *C.NetworkStrategy defaultNetworkStrategy bool networkType []C.InterfaceType @@ -86,6 +89,7 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial } dialer.Control = control.Append(dialer.Control, bindFunc) listener.Control = control.Append(listener.Control, bindFunc) + bindInterface = options.BindInterface } if options.RoutingMark > 0 { if !C.IsLinux { @@ -107,6 +111,7 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial bindFunc := control.BindToInterface(networkManager.InterfaceFinder(), defaultOptions.BindInterface, -1) dialer.Control = control.Append(dialer.Control, bindFunc) listener.Control = control.Append(listener.Control, bindFunc) + bindInterface = defaultOptions.BindInterface } else if networkManager.AutoDetectInterface() && !disableDefaultBind && options.NetNs == "" { if platformInterface != nil && platformInterface.UsePlatformNetworkInterfaces() { networkStrategy = (*C.NetworkStrategy)(options.NetworkStrategy) @@ -242,6 +247,7 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial udpAddr4: udpAddr4, udpAddr6: udpAddr6, netns: options.NetNs, + bindInterface: bindInterface, autoDetectBindFunc: autoDetectBindFunc, connectionManager: connectionManager, networkManager: networkManager, @@ -279,6 +285,19 @@ func (d *DefaultDialer) DialContext(ctx context.Context, network string, address return nil, E.New("domain not resolved") } if d.networkStrategy == nil { + if d.netns == "" && address.Addr.Zone() == "" && isLinkScoped(address.Addr) { + if d.bindInterface != "" { + address.Addr = address.Addr.WithZone(d.bindInterface) + } else if d.autoDetectBindFunc != nil || d.defaultNetworkStrategy { + iif, err := d.networkManager.InterfaceFinder().ByAddr(address.Addr) + if err != nil { + iif = d.networkManager.InterfaceMonitor().DefaultInterface() + } + if iif != nil { + address.Addr = address.Addr.WithZone(strconv.Itoa(iif.Index)) + } + } + } conn, err := listener.ListenNetworkNamespace[net.Conn](ctx, d.netns, func() (net.Conn, error) { switch N.NetworkName(network) { case N.NetworkUDP: @@ -300,6 +319,10 @@ func (d *DefaultDialer) DialContext(ctx context.Context, network string, address } } +func isLinkScoped(addr netip.Addr) bool { + return addr.Is6() && (addr.IsLinkLocalUnicast() || addr.IsLinkLocalMulticast() || addr.IsInterfaceLocalMulticast()) +} + func (d *DefaultDialer) DialParallelInterface(ctx context.Context, network string, address M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) { if strategy == nil { strategy = d.networkStrategy @@ -329,9 +352,9 @@ func (d *DefaultDialer) DialParallelInterface(ctx context.Context, network strin err error ) if !fastFallback { - conn, isPrimary, err = d.dialParallelInterface(ctx, dialer, network, address.String(), *strategy, interfaceType, fallbackInterfaceType, fallbackDelay) + conn, isPrimary, err = d.dialParallelInterface(ctx, dialer, network, address, *strategy, interfaceType, fallbackInterfaceType, fallbackDelay) } else { - conn, isPrimary, err = d.dialParallelInterfaceFastFallback(ctx, dialer, network, address.String(), *strategy, interfaceType, fallbackInterfaceType, fallbackDelay, d.networkLastFallback.Store) + conn, isPrimary, err = d.dialParallelInterfaceFastFallback(ctx, dialer, network, address, *strategy, interfaceType, fallbackInterfaceType, fallbackDelay, d.networkLastFallback.Store) } if err != nil { // bind interface failed on legacy xiaomi systems diff --git a/common/dialer/default_parallel_interface.go b/common/dialer/default_parallel_interface.go index 14bde1a33..4443bd9b8 100644 --- a/common/dialer/default_parallel_interface.go +++ b/common/dialer/default_parallel_interface.go @@ -3,6 +3,7 @@ package dialer import ( "context" "net" + "strconv" "syscall" "time" @@ -11,10 +12,11 @@ import ( "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) -func (d *DefaultDialer) dialParallelInterface(ctx context.Context, dialer net.Dialer, network string, addr string, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, bool, error) { +func (d *DefaultDialer) dialParallelInterface(ctx context.Context, dialer net.Dialer, network string, destination M.Socksaddr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, bool, error) { primaryInterfaces, fallbackInterfaces := selectInterfaces(d.networkManager, strategy, interfaceType, fallbackInterfaceType) if len(primaryInterfaces)+len(fallbackInterfaces) == 0 { return nil, false, E.New("no available network interface") @@ -31,13 +33,9 @@ func (d *DefaultDialer) dialParallelInterface(ctx context.Context, dialer net.Di } else { iif = fallbackInterfaces[0] } - perNetDialer := dialer - if defaultInterface == nil || iif.Index != defaultInterface.Index { - perNetDialer.Control = control.Append(perNetDialer.Control, control.BindToInterface(nil, iif.Name, iif.Index)) - } - conn, err := perNetDialer.DialContext(ctx, network, addr) + conn, err := dialInterface(ctx, dialer, network, destination, iif, defaultInterface) if err != nil { - return nil, false, E.Cause(err, "dial ", iif.Name, " (", iif.Index, ")") + return nil, false, err } return conn, primary, nil } @@ -53,14 +51,10 @@ func (d *DefaultDialer) dialParallelInterface(ctx context.Context, dialer net.Di } results := make(chan dialResult) // unbuffered startRacer := func(ctx context.Context, primary bool, iif adapter.NetworkInterface) { - perNetDialer := dialer - if defaultInterface == nil || iif.Index != defaultInterface.Index { - perNetDialer.Control = control.Append(perNetDialer.Control, control.BindToInterface(nil, iif.Name, iif.Index)) - } - conn, err := perNetDialer.DialContext(ctx, network, addr) + conn, err := dialInterface(ctx, dialer, network, destination, iif, defaultInterface) if err != nil { select { - case results <- dialResult{error: E.Cause(err, "dial ", iif.Name, " (", iif.Index, ")"), primary: primary}: + case results <- dialResult{error: err, primary: primary}: case <-returned: } } else { @@ -109,7 +103,7 @@ func (d *DefaultDialer) dialParallelInterface(ctx context.Context, dialer net.Di } } -func (d *DefaultDialer) dialParallelInterfaceFastFallback(ctx context.Context, dialer net.Dialer, network string, addr string, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration, resetFastFallback func(time.Time)) (net.Conn, bool, error) { +func (d *DefaultDialer) dialParallelInterfaceFastFallback(ctx context.Context, dialer net.Dialer, network string, destination M.Socksaddr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration, resetFastFallback func(time.Time)) (net.Conn, bool, error) { primaryInterfaces, fallbackInterfaces := selectInterfaces(d.networkManager, strategy, interfaceType, fallbackInterfaceType) if len(primaryInterfaces)+len(fallbackInterfaces) == 0 { return nil, false, E.New("no available network interface") @@ -126,13 +120,9 @@ func (d *DefaultDialer) dialParallelInterfaceFastFallback(ctx context.Context, d } else { iif = fallbackInterfaces[0] } - perNetDialer := dialer - if defaultInterface == nil || iif.Index != defaultInterface.Index { - perNetDialer.Control = control.Append(perNetDialer.Control, control.BindToInterface(nil, iif.Name, iif.Index)) - } - conn, err := perNetDialer.DialContext(ctx, network, addr) + conn, err := dialInterface(ctx, dialer, network, destination, iif, defaultInterface) if err != nil { - return nil, false, E.Cause(err, "dial ", iif.Name, " (", iif.Index, ")") + return nil, false, err } return conn, primary, nil } @@ -149,14 +139,10 @@ func (d *DefaultDialer) dialParallelInterfaceFastFallback(ctx context.Context, d startAt := time.Now() results := make(chan dialResult) // unbuffered startRacer := func(ctx context.Context, primary bool, iif adapter.NetworkInterface) { - perNetDialer := dialer - if defaultInterface == nil || iif.Index != defaultInterface.Index { - perNetDialer.Control = control.Append(perNetDialer.Control, control.BindToInterface(nil, iif.Name, iif.Index)) - } - conn, err := perNetDialer.DialContext(ctx, network, addr) + conn, err := dialInterface(ctx, dialer, network, destination, iif, defaultInterface) if err != nil { select { - case results <- dialResult{error: E.Cause(err, "dial ", iif.Name, " (", iif.Index, ")"), primary: primary}: + case results <- dialResult{error: err, primary: primary}: case <-returned: } } else { @@ -191,6 +177,20 @@ func (d *DefaultDialer) dialParallelInterfaceFastFallback(ctx context.Context, d return nil, false, E.Errors(errors...) } +func dialInterface(ctx context.Context, dialer net.Dialer, network string, destination M.Socksaddr, iif adapter.NetworkInterface, defaultInterface *control.Interface) (net.Conn, error) { + if defaultInterface == nil || iif.Index != defaultInterface.Index { + dialer.Control = control.Append(dialer.Control, control.BindToInterface(nil, iif.Name, iif.Index)) + } + if destination.Addr.Zone() == "" && isLinkScoped(destination.Addr) { + destination.Addr = destination.Addr.WithZone(strconv.Itoa(iif.Index)) + } + conn, err := dialer.DialContext(ctx, network, destination.String()) + if err != nil { + return nil, E.Cause(err, "dial ", iif.Name, " (", iif.Index, ")") + } + return conn, nil +} + func (d *DefaultDialer) serialInterfaceControl(strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType) control.Func { return func(network, address string, conn syscall.RawConn) error { primaryInterfaces, fallbackInterfaces := selectInterfaces(d.networkManager, strategy, interfaceType, fallbackInterfaceType)