Kaynağa Gözat

Refactor multi networks strategy

世界 11 ay önce
ebeveyn
işleme
8de6d7e1df

+ 2 - 0
adapter/inbound.go

@@ -69,6 +69,8 @@ type InboundContext struct {
 	UDPDisableDomainUnmapping bool
 	UDPConnect                bool
 	NetworkStrategy           C.NetworkStrategy
+	NetworkType               []C.InterfaceType
+	FallbackNetworkType       []C.InterfaceType
 	FallbackDelay             time.Duration
 
 	DNSServer string

+ 7 - 5
adapter/network.go

@@ -28,10 +28,12 @@ type NetworkManager interface {
 }
 
 type NetworkOptions struct {
-	DefaultNetworkStrategy C.NetworkStrategy
-	DefaultFallbackDelay   time.Duration
-	DefaultInterface       string
-	DefaultMark            uint32
+	NetworkStrategy     C.NetworkStrategy
+	NetworkType         []C.InterfaceType
+	FallbackNetworkType []C.InterfaceType
+	FallbackDelay       time.Duration
+	BindInterface       string
+	RoutingMark         uint32
 }
 
 type InterfaceUpdateListener interface {
@@ -45,7 +47,7 @@ type WIFIState struct {
 
 type NetworkInterface struct {
 	control.Interface
-	Type        string
+	Type        C.InterfaceType
 	DNSServers  []string
 	Expensive   bool
 	Constrained bool

+ 3 - 3
adapter/outbound/default.go

@@ -26,7 +26,7 @@ func NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata a
 	var err error
 	if len(metadata.DestinationAddresses) > 0 {
 		if parallelDialer, isParallelDialer := this.(dialer.ParallelInterfaceDialer); isParallelDialer {
-			outConn, err = dialer.DialSerialNetwork(ctx, parallelDialer, N.NetworkTCP, metadata.Destination, metadata.DestinationAddresses, metadata.NetworkStrategy, metadata.FallbackDelay)
+			outConn, err = dialer.DialSerialNetwork(ctx, parallelDialer, N.NetworkTCP, metadata.Destination, metadata.DestinationAddresses, metadata.NetworkStrategy, metadata.NetworkType, metadata.FallbackNetworkType, metadata.FallbackDelay)
 		} else {
 			outConn, err = N.DialSerial(ctx, this, N.NetworkTCP, metadata.Destination, metadata.DestinationAddresses)
 		}
@@ -56,7 +56,7 @@ func NewPacketConnection(ctx context.Context, this N.Dialer, conn N.PacketConn,
 	if metadata.UDPConnect {
 		if len(metadata.DestinationAddresses) > 0 {
 			if parallelDialer, isParallelDialer := this.(dialer.ParallelInterfaceDialer); isParallelDialer {
-				outConn, err = dialer.DialSerialNetwork(ctx, parallelDialer, N.NetworkUDP, metadata.Destination, metadata.DestinationAddresses, metadata.NetworkStrategy, metadata.FallbackDelay)
+				outConn, err = dialer.DialSerialNetwork(ctx, parallelDialer, N.NetworkUDP, metadata.Destination, metadata.DestinationAddresses, metadata.NetworkStrategy, metadata.NetworkType, metadata.FallbackNetworkType, metadata.FallbackDelay)
 			} else {
 				outConn, err = N.DialSerial(ctx, this, N.NetworkUDP, metadata.Destination, metadata.DestinationAddresses)
 			}
@@ -74,7 +74,7 @@ func NewPacketConnection(ctx context.Context, this N.Dialer, conn N.PacketConn,
 	} else {
 		if len(metadata.DestinationAddresses) > 0 {
 			if parallelDialer, isParallelDialer := this.(dialer.ParallelInterfaceDialer); isParallelDialer {
-				outPacketConn, destinationAddress, err = dialer.ListenSerialNetworkPacket(ctx, parallelDialer, metadata.Destination, metadata.DestinationAddresses, metadata.NetworkStrategy, metadata.FallbackDelay)
+				outPacketConn, destinationAddress, err = dialer.ListenSerialNetworkPacket(ctx, parallelDialer, metadata.Destination, metadata.DestinationAddresses, metadata.NetworkStrategy, metadata.NetworkType, metadata.FallbackNetworkType, metadata.FallbackDelay)
 			} else {
 				outPacketConn, destinationAddress, err = N.ListenSerial(ctx, this, metadata.Destination, metadata.DestinationAddresses)
 			}

+ 40 - 23
common/dialer/default.go

@@ -10,6 +10,7 @@ import (
 	"github.com/sagernet/sing-box/common/conntrack"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/atomic"
 	"github.com/sagernet/sing/common/control"
 	E "github.com/sagernet/sing/common/exceptions"
@@ -33,6 +34,8 @@ type DefaultDialer struct {
 	isWireGuardListener  bool
 	networkManager       adapter.NetworkManager
 	networkStrategy      C.NetworkStrategy
+	networkType          []C.InterfaceType
+	fallbackNetworkType  []C.InterfaceType
 	networkFallbackDelay time.Duration
 	networkLastFallback  atomic.TypedValue[time.Time]
 }
@@ -43,6 +46,8 @@ func NewDefault(networkManager adapter.NetworkManager, options option.DialerOpti
 		listener             net.ListenConfig
 		interfaceFinder      control.InterfaceFinder
 		networkStrategy      C.NetworkStrategy
+		networkType          []C.InterfaceType
+		fallbackNetworkType  []C.InterfaceType
 		networkFallbackDelay time.Duration
 	)
 	if networkManager != nil {
@@ -56,8 +61,8 @@ func NewDefault(networkManager adapter.NetworkManager, options option.DialerOpti
 		listener.Control = control.Append(listener.Control, bindFunc)
 	}
 	if options.RoutingMark > 0 {
-		dialer.Control = control.Append(dialer.Control, control.RoutingMark(options.RoutingMark))
-		listener.Control = control.Append(listener.Control, control.RoutingMark(options.RoutingMark))
+		dialer.Control = control.Append(dialer.Control, control.RoutingMark(uint32(options.RoutingMark)))
+		listener.Control = control.Append(listener.Control, control.RoutingMark(uint32(options.RoutingMark)))
 	}
 	if networkManager != nil {
 		autoRedirectOutputMark := networkManager.AutoRedirectOutputMark()
@@ -74,6 +79,8 @@ func NewDefault(networkManager adapter.NetworkManager, options option.DialerOpti
 			return nil, E.New("`network_strategy` is conflict with `bind_interface`, `inet4_bind_address` and `inet6_bind_address`")
 		}
 		networkStrategy = C.NetworkStrategy(options.NetworkStrategy)
+		networkType = common.Map(options.NetworkType, option.InterfaceType.Build)
+		fallbackNetworkType = common.Map(options.FallbackNetworkType, option.InterfaceType.Build)
 		networkFallbackDelay = time.Duration(options.NetworkFallbackDelay)
 		if networkManager == nil || !networkManager.AutoDetectInterface() {
 			return nil, E.New("`route.auto_detect_interface` is require by `network_strategy`")
@@ -81,23 +88,31 @@ func NewDefault(networkManager adapter.NetworkManager, options option.DialerOpti
 	}
 	if networkManager != nil && options.BindInterface == "" && options.Inet4BindAddress == nil && options.Inet6BindAddress == nil {
 		defaultOptions := networkManager.DefaultOptions()
-		if defaultOptions.DefaultInterface != "" {
-			bindFunc := control.BindToInterface(networkManager.InterfaceFinder(), defaultOptions.DefaultInterface, -1)
-			dialer.Control = control.Append(dialer.Control, bindFunc)
-			listener.Control = control.Append(listener.Control, bindFunc)
-		} else if networkManager.AutoDetectInterface() {
-			if defaultOptions.DefaultNetworkStrategy != C.NetworkStrategyDefault && C.NetworkStrategy(options.NetworkStrategy) == C.NetworkStrategyDefault {
-				networkStrategy = defaultOptions.DefaultNetworkStrategy
-				networkFallbackDelay = defaultOptions.DefaultFallbackDelay
-				bindFunc := networkManager.ProtectFunc()
-				dialer.Control = control.Append(dialer.Control, bindFunc)
-				listener.Control = control.Append(listener.Control, bindFunc)
-			} else {
-				bindFunc := networkManager.AutoDetectInterfaceFunc()
+		if options.BindInterface == "" {
+			if defaultOptions.BindInterface != "" {
+				bindFunc := control.BindToInterface(networkManager.InterfaceFinder(), defaultOptions.BindInterface, -1)
 				dialer.Control = control.Append(dialer.Control, bindFunc)
 				listener.Control = control.Append(listener.Control, bindFunc)
+			} else if networkManager.AutoDetectInterface() {
+				if defaultOptions.NetworkStrategy != C.NetworkStrategyDefault && C.NetworkStrategy(options.NetworkStrategy) == C.NetworkStrategyDefault {
+					networkStrategy = defaultOptions.NetworkStrategy
+					networkType = defaultOptions.NetworkType
+					fallbackNetworkType = defaultOptions.FallbackNetworkType
+					networkFallbackDelay = defaultOptions.FallbackDelay
+					bindFunc := networkManager.ProtectFunc()
+					dialer.Control = control.Append(dialer.Control, bindFunc)
+					listener.Control = control.Append(listener.Control, bindFunc)
+				} else {
+					bindFunc := networkManager.AutoDetectInterfaceFunc()
+					dialer.Control = control.Append(dialer.Control, bindFunc)
+					listener.Control = control.Append(listener.Control, bindFunc)
+				}
 			}
 		}
+		if options.RoutingMark == 0 && defaultOptions.RoutingMark != 0 {
+			dialer.Control = control.Append(dialer.Control, control.RoutingMark(defaultOptions.RoutingMark))
+			listener.Control = control.Append(listener.Control, control.RoutingMark(defaultOptions.RoutingMark))
+		}
 	}
 	if options.ReuseAddr {
 		listener.Control = control.Append(listener.Control, control.ReuseAddr())
@@ -179,6 +194,8 @@ func NewDefault(networkManager adapter.NetworkManager, options option.DialerOpti
 		isWireGuardListener:  options.IsWireGuardListener,
 		networkManager:       networkManager,
 		networkStrategy:      networkStrategy,
+		networkType:          networkType,
+		fallbackNetworkType:  fallbackNetworkType,
 		networkFallbackDelay: networkFallbackDelay,
 	}, nil
 }
@@ -202,11 +219,11 @@ func (d *DefaultDialer) DialContext(ctx context.Context, network string, address
 			return trackConn(DialSlowContext(&d.dialer6, ctx, network, address))
 		}
 	} else {
-		return d.DialParallelInterface(ctx, network, address, d.networkStrategy, d.networkFallbackDelay)
+		return d.DialParallelInterface(ctx, network, address, d.networkStrategy, d.networkType, d.fallbackNetworkType, d.networkFallbackDelay)
 	}
 }
 
-func (d *DefaultDialer) DialParallelInterface(ctx context.Context, network string, address M.Socksaddr, strategy C.NetworkStrategy, fallbackDelay time.Duration) (net.Conn, error) {
+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 == C.NetworkStrategyDefault {
 		return d.DialContext(ctx, network, address)
 	}
@@ -226,9 +243,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, fallbackDelay)
+		conn, isPrimary, err = d.dialParallelInterface(ctx, dialer, network, address.String(), strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
 	} else {
-		conn, isPrimary, err = d.dialParallelInterfaceFastFallback(ctx, dialer, network, address.String(), strategy, fallbackDelay, d.networkLastFallback.Store)
+		conn, isPrimary, err = d.dialParallelInterfaceFastFallback(ctx, dialer, network, address.String(), strategy, interfaceType, fallbackInterfaceType, fallbackDelay, d.networkLastFallback.Store)
 	}
 	if err != nil {
 		return nil, err
@@ -249,11 +266,11 @@ func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksadd
 			return trackPacketConn(d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr4))
 		}
 	} else {
-		return d.ListenSerialInterfacePacket(ctx, destination, d.networkStrategy, d.networkFallbackDelay)
+		return d.ListenSerialInterfacePacket(ctx, destination, d.networkStrategy, d.networkType, d.fallbackNetworkType, d.networkFallbackDelay)
 	}
 }
 
-func (d *DefaultDialer) ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy C.NetworkStrategy, fallbackDelay time.Duration) (net.PacketConn, error) {
+func (d *DefaultDialer) ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error) {
 	if strategy == C.NetworkStrategyDefault {
 		return d.ListenPacket(ctx, destination)
 	}
@@ -264,11 +281,11 @@ func (d *DefaultDialer) ListenSerialInterfacePacket(ctx context.Context, destina
 	if destination.IsIPv4() && !destination.Addr.IsUnspecified() {
 		network += "4"
 	}
-	return trackPacketConn(d.listenSerialInterfacePacket(ctx, d.udpListener, network, "", strategy, fallbackDelay))
+	return trackPacketConn(d.listenSerialInterfacePacket(ctx, d.udpListener, network, "", strategy, interfaceType, fallbackInterfaceType, fallbackDelay))
 }
 
 func (d *DefaultDialer) ListenPacketCompat(network, address string) (net.PacketConn, error) {
-	return d.listenSerialInterfacePacket(context.Background(), d.udpListener, network, address, d.networkStrategy, d.networkFallbackDelay)
+	return d.listenSerialInterfacePacket(context.Background(), d.udpListener, network, address, d.networkStrategy, d.networkType, d.fallbackNetworkType, d.networkFallbackDelay)
 }
 
 func trackConn(conn net.Conn, err error) (net.Conn, error) {

+ 36 - 54
common/dialer/default_parallel_interface.go

@@ -7,14 +7,14 @@ import (
 
 	"github.com/sagernet/sing-box/adapter"
 	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/control"
 	E "github.com/sagernet/sing/common/exceptions"
-	F "github.com/sagernet/sing/common/format"
 	N "github.com/sagernet/sing/common/network"
 )
 
-func (d *DefaultDialer) dialParallelInterface(ctx context.Context, dialer net.Dialer, network string, addr string, strategy C.NetworkStrategy, fallbackDelay time.Duration) (net.Conn, bool, error) {
-	primaryInterfaces, fallbackInterfaces := selectInterfaces(d.networkManager, strategy)
+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) {
+	primaryInterfaces, fallbackInterfaces := selectInterfaces(d.networkManager, strategy, interfaceType, fallbackInterfaceType)
 	if len(primaryInterfaces)+len(fallbackInterfaces) == 0 {
 		return nil, false, E.New("no available network interface")
 	}
@@ -84,8 +84,8 @@ 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, fallbackDelay time.Duration, resetFastFallback func(time.Time)) (net.Conn, bool, error) {
-	primaryInterfaces, fallbackInterfaces := selectInterfaces(d.networkManager, strategy)
+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) {
+	primaryInterfaces, fallbackInterfaces := selectInterfaces(d.networkManager, strategy, interfaceType, fallbackInterfaceType)
 	if len(primaryInterfaces)+len(fallbackInterfaces) == 0 {
 		return nil, false, E.New("no available network interface")
 	}
@@ -144,8 +144,8 @@ func (d *DefaultDialer) dialParallelInterfaceFastFallback(ctx context.Context, d
 	}
 }
 
-func (d *DefaultDialer) listenSerialInterfacePacket(ctx context.Context, listener net.ListenConfig, network string, addr string, strategy C.NetworkStrategy, fallbackDelay time.Duration) (net.PacketConn, error) {
-	primaryInterfaces, fallbackInterfaces := selectInterfaces(d.networkManager, strategy)
+func (d *DefaultDialer) listenSerialInterfacePacket(ctx context.Context, listener net.ListenConfig, network string, addr string, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error) {
+	primaryInterfaces, fallbackInterfaces := selectInterfaces(d.networkManager, strategy, interfaceType, fallbackInterfaceType)
 	if len(primaryInterfaces)+len(fallbackInterfaces) == 0 {
 		return nil, E.New("no available network interface")
 	}
@@ -174,12 +174,12 @@ func (d *DefaultDialer) listenSerialInterfacePacket(ctx context.Context, listene
 	return nil, E.Errors(errors...)
 }
 
-func selectInterfaces(networkManager adapter.NetworkManager, strategy C.NetworkStrategy) (primaryInterfaces []adapter.NetworkInterface, fallbackInterfaces []adapter.NetworkInterface) {
+func selectInterfaces(networkManager adapter.NetworkManager, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType) (primaryInterfaces []adapter.NetworkInterface, fallbackInterfaces []adapter.NetworkInterface) {
 	interfaces := networkManager.NetworkInterfaces()
 	switch strategy {
-	case C.NetworkStrategyFallback:
-		defaultIf := networkManager.InterfaceMonitor().DefaultInterface()
-		if defaultIf != nil {
+	case C.NetworkStrategyDefault:
+		if len(interfaceType) == 0 {
+			defaultIf := networkManager.InterfaceMonitor().DefaultInterface()
 			for _, iif := range interfaces {
 				if iif.Index == defaultIf.Index {
 					primaryInterfaces = append(primaryInterfaces, iif)
@@ -188,54 +188,36 @@ func selectInterfaces(networkManager adapter.NetworkManager, strategy C.NetworkS
 				}
 			}
 		} else {
-			primaryInterfaces = interfaces
+			primaryInterfaces = common.Filter(interfaces, func(iif adapter.NetworkInterface) bool {
+				return common.Contains(interfaceType, iif.Type)
+			})
 		}
 	case C.NetworkStrategyHybrid:
-		primaryInterfaces = interfaces
-	case C.NetworkStrategyWIFI:
-		for _, iif := range interfaces {
-			if iif.Type == C.InterfaceTypeWIFI {
-				primaryInterfaces = append(primaryInterfaces, iif)
-			} else {
-				fallbackInterfaces = append(fallbackInterfaces, iif)
-			}
-		}
-	case C.NetworkStrategyCellular:
-		for _, iif := range interfaces {
-			if iif.Type == C.InterfaceTypeCellular {
-				primaryInterfaces = append(primaryInterfaces, iif)
-			} else {
-				fallbackInterfaces = append(fallbackInterfaces, iif)
-			}
-		}
-	case C.NetworkStrategyEthernet:
-		for _, iif := range interfaces {
-			if iif.Type == C.InterfaceTypeEthernet {
-				primaryInterfaces = append(primaryInterfaces, iif)
-			} else {
-				fallbackInterfaces = append(fallbackInterfaces, iif)
-			}
-		}
-	case C.NetworkStrategyWIFIOnly:
-		for _, iif := range interfaces {
-			if iif.Type == C.InterfaceTypeWIFI {
-				primaryInterfaces = append(primaryInterfaces, iif)
-			}
-		}
-	case C.NetworkStrategyCellularOnly:
-		for _, iif := range interfaces {
-			if iif.Type == C.InterfaceTypeCellular {
-				primaryInterfaces = append(primaryInterfaces, iif)
-			}
+		if len(interfaceType) == 0 {
+			primaryInterfaces = interfaces
+		} else {
+			primaryInterfaces = common.Filter(interfaces, func(iif adapter.NetworkInterface) bool {
+				return common.Contains(interfaceType, iif.Type)
+			})
 		}
-	case C.NetworkStrategyEthernetOnly:
-		for _, iif := range interfaces {
-			if iif.Type == C.InterfaceTypeEthernet {
-				primaryInterfaces = append(primaryInterfaces, iif)
+	case C.NetworkStrategyFallback:
+		if len(interfaceType) == 0 {
+			defaultIf := networkManager.InterfaceMonitor().DefaultInterface()
+			for _, iif := range interfaces {
+				if iif.Index == defaultIf.Index {
+					primaryInterfaces = append(primaryInterfaces, iif)
+				} else {
+					fallbackInterfaces = append(fallbackInterfaces, iif)
+				}
 			}
+		} else {
+			primaryInterfaces = common.Filter(interfaces, func(iif adapter.NetworkInterface) bool {
+				return common.Contains(interfaceType, iif.Type)
+			})
 		}
-	default:
-		panic(F.ToString("unknown network strategy: ", strategy))
+		fallbackInterfaces = common.Filter(interfaces, func(iif adapter.NetworkInterface) bool {
+			return common.Contains(fallbackInterfaceType, iif.Type)
+		})
 	}
 	return primaryInterfaces, fallbackInterfaces
 }

+ 9 - 9
common/dialer/default_parallel_network.go

@@ -13,13 +13,13 @@ import (
 	N "github.com/sagernet/sing/common/network"
 )
 
-func DialSerialNetwork(ctx context.Context, dialer ParallelInterfaceDialer, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy C.NetworkStrategy, fallbackDelay time.Duration) (net.Conn, error) {
+func DialSerialNetwork(ctx context.Context, dialer ParallelInterfaceDialer, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) {
 	if parallelDialer, isParallel := dialer.(ParallelNetworkDialer); isParallel {
-		return parallelDialer.DialParallelNetwork(ctx, network, destination, destinationAddresses, strategy, fallbackDelay)
+		return parallelDialer.DialParallelNetwork(ctx, network, destination, destinationAddresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
 	}
 	var errors []error
 	for _, address := range destinationAddresses {
-		conn, err := dialer.DialParallelInterface(ctx, network, M.SocksaddrFrom(address, destination.Port), strategy, fallbackDelay)
+		conn, err := dialer.DialParallelInterface(ctx, network, M.SocksaddrFrom(address, destination.Port), strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
 		if err == nil {
 			return conn, nil
 		}
@@ -28,7 +28,7 @@ func DialSerialNetwork(ctx context.Context, dialer ParallelInterfaceDialer, netw
 	return nil, E.Errors(errors...)
 }
 
-func DialParallelNetwork(ctx context.Context, dialer ParallelInterfaceDialer, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, preferIPv6 bool, strategy C.NetworkStrategy, fallbackDelay time.Duration) (net.Conn, error) {
+func DialParallelNetwork(ctx context.Context, dialer ParallelInterfaceDialer, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, preferIPv6 bool, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) {
 	if fallbackDelay == 0 {
 		fallbackDelay = N.DefaultFallbackDelay
 	}
@@ -43,7 +43,7 @@ func DialParallelNetwork(ctx context.Context, dialer ParallelInterfaceDialer, ne
 		return address.Is6() && !address.Is4In6()
 	})
 	if len(addresses4) == 0 || len(addresses6) == 0 {
-		return DialSerialNetwork(ctx, dialer, network, destination, destinationAddresses, strategy, fallbackDelay)
+		return DialSerialNetwork(ctx, dialer, network, destination, destinationAddresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
 	}
 	var primaries, fallbacks []netip.Addr
 	if preferIPv6 {
@@ -65,7 +65,7 @@ func DialParallelNetwork(ctx context.Context, dialer ParallelInterfaceDialer, ne
 		if !primary {
 			ras = fallbacks
 		}
-		c, err := DialSerialNetwork(ctx, dialer, network, destination, ras, strategy, fallbackDelay)
+		c, err := DialSerialNetwork(ctx, dialer, network, destination, ras, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
 		select {
 		case results <- dialResult{Conn: c, error: err, primary: primary, done: true}:
 		case <-returned:
@@ -106,13 +106,13 @@ func DialParallelNetwork(ctx context.Context, dialer ParallelInterfaceDialer, ne
 	}
 }
 
-func ListenSerialNetworkPacket(ctx context.Context, dialer ParallelInterfaceDialer, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy C.NetworkStrategy, fallbackDelay time.Duration) (net.PacketConn, netip.Addr, error) {
+func ListenSerialNetworkPacket(ctx context.Context, dialer ParallelInterfaceDialer, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, netip.Addr, error) {
 	if parallelDialer, isParallel := dialer.(ParallelNetworkDialer); isParallel {
-		return parallelDialer.ListenSerialNetworkPacket(ctx, destination, destinationAddresses, strategy, fallbackDelay)
+		return parallelDialer.ListenSerialNetworkPacket(ctx, destination, destinationAddresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
 	}
 	var errors []error
 	for _, address := range destinationAddresses {
-		conn, err := dialer.ListenSerialInterfacePacket(ctx, M.SocksaddrFrom(address, destination.Port), strategy, fallbackDelay)
+		conn, err := dialer.ListenSerialInterfacePacket(ctx, M.SocksaddrFrom(address, destination.Port), strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
 		if err == nil {
 			return conn, address, nil
 		}

+ 4 - 4
common/dialer/dialer.go

@@ -77,11 +77,11 @@ func NewDirect(ctx context.Context, options option.DialerOptions) (ParallelInter
 
 type ParallelInterfaceDialer interface {
 	N.Dialer
-	DialParallelInterface(ctx context.Context, network string, destination M.Socksaddr, strategy C.NetworkStrategy, fallbackDelay time.Duration) (net.Conn, error)
-	ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy C.NetworkStrategy, fallbackDelay time.Duration) (net.PacketConn, error)
+	DialParallelInterface(ctx context.Context, network string, destination M.Socksaddr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error)
+	ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error)
 }
 
 type ParallelNetworkDialer interface {
-	DialParallelNetwork(ctx context.Context, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy C.NetworkStrategy, fallbackDelay time.Duration) (net.Conn, error)
-	ListenSerialNetworkPacket(ctx context.Context, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy C.NetworkStrategy, fallbackDelay time.Duration) (net.PacketConn, netip.Addr, error)
+	DialParallelNetwork(ctx context.Context, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error)
+	ListenSerialNetworkPacket(ctx context.Context, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, netip.Addr, error)
 }

+ 5 - 5
common/dialer/resolve.go

@@ -106,7 +106,7 @@ func (d *resolveDialer) ListenPacket(ctx context.Context, destination M.Socksadd
 	return bufio.NewNATPacketConn(bufio.NewPacketConn(conn), M.SocksaddrFrom(destinationAddress, destination.Port), destination), nil
 }
 
-func (d *resolveParallelNetworkDialer) DialParallelInterface(ctx context.Context, network string, destination M.Socksaddr, strategy C.NetworkStrategy, fallbackDelay time.Duration) (net.Conn, error) {
+func (d *resolveParallelNetworkDialer) DialParallelInterface(ctx context.Context, network string, destination M.Socksaddr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) {
 	if !destination.IsFqdn() {
 		return d.dialer.DialContext(ctx, network, destination)
 	}
@@ -128,13 +128,13 @@ func (d *resolveParallelNetworkDialer) DialParallelInterface(ctx context.Context
 		fallbackDelay = d.fallbackDelay
 	}
 	if d.parallel {
-		return DialParallelNetwork(ctx, d.dialer, network, destination, addresses, d.strategy == dns.DomainStrategyPreferIPv6, strategy, fallbackDelay)
+		return DialParallelNetwork(ctx, d.dialer, network, destination, addresses, d.strategy == dns.DomainStrategyPreferIPv6, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
 	} else {
-		return DialSerialNetwork(ctx, d.dialer, network, destination, addresses, strategy, fallbackDelay)
+		return DialSerialNetwork(ctx, d.dialer, network, destination, addresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
 	}
 }
 
-func (d *resolveParallelNetworkDialer) ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy C.NetworkStrategy, fallbackDelay time.Duration) (net.PacketConn, error) {
+func (d *resolveParallelNetworkDialer) ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error) {
 	if !destination.IsFqdn() {
 		return d.dialer.ListenPacket(ctx, destination)
 	}
@@ -152,7 +152,7 @@ func (d *resolveParallelNetworkDialer) ListenSerialInterfacePacket(ctx context.C
 	if err != nil {
 		return nil, err
 	}
-	conn, destinationAddress, err := ListenSerialNetworkPacket(ctx, d.dialer, destination, addresses, strategy, fallbackDelay)
+	conn, destinationAddress, err := ListenSerialNetworkPacket(ctx, d.dialer, destination, addresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
 	if err != nil {
 		return nil, err
 	}

+ 14 - 2
common/srs/binary.go

@@ -226,7 +226,7 @@ func readDefaultRule(reader varbin.Reader, recover bool) (rule option.DefaultHea
 			}
 			rule.AdGuardDomainMatcher = matcher
 		case ruleItemNetworkType:
-			rule.NetworkType, err = readRuleItemString(reader)
+			rule.NetworkType, err = readRuleItemUint8[option.InterfaceType](reader)
 		case ruleItemNetworkIsExpensive:
 			rule.NetworkIsExpensive = true
 		case ruleItemNetworkIsConstrained:
@@ -349,7 +349,7 @@ func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, gen
 		if generateVersion < C.RuleSetVersion3 {
 			return E.New("network_type rule item is only supported in version 3 or later")
 		}
-		err = writeRuleItemString(writer, ruleItemNetworkType, rule.NetworkType)
+		err = writeRuleItemUint8(writer, ruleItemNetworkType, rule.NetworkType)
 		if err != nil {
 			return err
 		}
@@ -414,6 +414,18 @@ func writeRuleItemString(writer varbin.Writer, itemType uint8, value []string) e
 	return varbin.Write(writer, binary.BigEndian, value)
 }
 
+func readRuleItemUint8[E ~uint8](reader varbin.Reader) ([]E, error) {
+	return varbin.ReadValue[[]E](reader, binary.BigEndian)
+}
+
+func writeRuleItemUint8[E ~uint8](writer varbin.Writer, itemType uint8, value []E) error {
+	err := writer.WriteByte(itemType)
+	if err != nil {
+		return err
+	}
+	return varbin.Write(writer, binary.BigEndian, value)
+}
+
 func readRuleItemUint16(reader varbin.Reader) ([]uint16, error) {
 	return varbin.ReadValue[[]uint16](reader, binary.BigEndian)
 }

+ 31 - 23
constant/network.go

@@ -5,44 +5,52 @@ import (
 	F "github.com/sagernet/sing/common/format"
 )
 
+type InterfaceType uint8
+
 const (
-	InterfaceTypeWIFI     = "wifi"
-	InterfaceTypeCellular = "cellular"
-	InterfaceTypeEthernet = "ethernet"
-	InterfaceTypeOther    = "other"
+	InterfaceTypeWIFI InterfaceType = iota
+	InterfaceTypeCellular
+	InterfaceTypeEthernet
+	InterfaceTypeOther
+)
+
+var (
+	interfaceTypeToString = map[InterfaceType]string{
+		InterfaceTypeWIFI:     "wifi",
+		InterfaceTypeCellular: "cellular",
+		InterfaceTypeEthernet: "ethernet",
+		InterfaceTypeOther:    "other",
+	}
+	StringToInterfaceType = common.ReverseMap(interfaceTypeToString)
 )
 
-type NetworkStrategy int
+func (t InterfaceType) String() string {
+	name, loaded := interfaceTypeToString[t]
+	if !loaded {
+		return F.ToString(int(t))
+	}
+	return name
+}
+
+type NetworkStrategy uint8
 
 const (
 	NetworkStrategyDefault NetworkStrategy = iota
 	NetworkStrategyFallback
 	NetworkStrategyHybrid
-	NetworkStrategyWIFI
-	NetworkStrategyCellular
-	NetworkStrategyEthernet
-	NetworkStrategyWIFIOnly
-	NetworkStrategyCellularOnly
-	NetworkStrategyEthernetOnly
 )
 
 var (
-	NetworkStrategyToString = map[NetworkStrategy]string{
-		NetworkStrategyDefault:      "default",
-		NetworkStrategyFallback:     "fallback",
-		NetworkStrategyHybrid:       "hybrid",
-		NetworkStrategyWIFI:         "wifi",
-		NetworkStrategyCellular:     "cellular",
-		NetworkStrategyEthernet:     "ethernet",
-		NetworkStrategyWIFIOnly:     "wifi_only",
-		NetworkStrategyCellularOnly: "cellular_only",
-		NetworkStrategyEthernetOnly: "ethernet_only",
+	networkStrategyToString = map[NetworkStrategy]string{
+		NetworkStrategyDefault:  "default",
+		NetworkStrategyFallback: "fallback",
+		NetworkStrategyHybrid:   "hybrid",
 	}
-	StringToNetworkStrategy = common.ReverseMap(NetworkStrategyToString)
+	StringToNetworkStrategy = common.ReverseMap(networkStrategyToString)
 )
 
 func (s NetworkStrategy) String() string {
-	name, loaded := NetworkStrategyToString[s]
+	name, loaded := networkStrategyToString[s]
 	if !loaded {
 		return F.ToString(int(s))
 	}

+ 23 - 14
docs/configuration/route/index.md

@@ -7,6 +7,8 @@ icon: material/new-box
 !!! quote "Changes in sing-box 1.11.0"
 
     :material-plus: [default_network_strategy](#default_network_strategy)  
+    :material-plus: [default_network_type](#default_network_type)  
+    :material-plus: [default_fallback_network_type](#default_fallback_network_type)  
     :material-alert: [default_fallback_delay](#default_fallback_delay)
 
 !!! quote "Changes in sing-box 1.8.0"
@@ -30,17 +32,18 @@ icon: material/new-box
     "default_interface": "",
     "default_mark": 0,
     "default_network_strategy": "",
+    "default_network_type": [],
+    "default_fallback_network_type": [],
     "default_fallback_delay": ""
   }
 }
 ```
 
-### Fields
+!!! note ""
+
+    You can ignore the JSON Array [] tag when the content is only one item
 
-| Key       | Format                |
-|-----------|-----------------------|
-| `geoip`   | [GeoIP](./geoip/)     |
-| `geosite` | [Geosite](./geosite/) |
+### Fields
 
 #### rules
 
@@ -96,11 +99,9 @@ Takes no effect if `outbound.routing_mark` is set.
 
 #### default_network_strategy
 
-!!! quote ""
-
-    Only supported in graphical clients on Android and iOS with `auto_detect_interface` enabled.
+!!! question "Since sing-box 1.11.0"
 
-Strategy for selecting network interfaces.
+See [Dial Fields](/configuration/shared/dial/#network_strategy) for details.
 
 Takes no effect if `outbound.bind_interface`, `outbound.inet4_bind_address` or `outbound.inet6_bind_address` is set.
 
@@ -108,12 +109,20 @@ Can be overrides by `outbound.network_strategy`.
 
 Conflicts with `default_interface`.
 
-See [Dial Fields](/configuration/shared/dial/#network_strategy) for available values.
+#### default_network_type
 
-#### default_fallback_delay
+!!! question "Since sing-box 1.11.0"
 
-!!! quote ""
+See [Dial Fields](/configuration/shared/dial/#network_type) for details.
+
+#### default_fallback_network_type
+
+!!! question "Since sing-box 1.11.0"
+
+See [Dial Fields](/configuration/shared/dial/#fallback_network_type) for details.
+
+#### default_fallback_delay
 
-    Only supported in graphical clients on Android and iOS with `auto_detect_interface` enabled and `network_strategy` set.
+!!! question "Since sing-box 1.11.0"
 
-See [Dial Fields](/configuration/shared/dial/#fallback_delay) for details.
+See [Dial Fields](/configuration/shared/dial/#fallback_delay) for details.

+ 30 - 9
docs/configuration/route/index.zh.md

@@ -1,5 +1,16 @@
+---
+icon: material/new-box
+---
+
 # 路由
 
+!!! quote "sing-box 1.11.0 中的更改"
+
+    :material-plus: [network_strategy](#network_strategy)  
+    :material-plus: [default_network_type](#default_network_type)  
+    :material-plus: [default_fallback_network_type](#default_fallback_network_type)  
+    :material-alert: [default_fallback_delay](#default_fallback_delay)
+
 !!! quote "sing-box 1.8.0 中的更改"
 
     :material-plus: [rule_set](#rule_set)  
@@ -26,6 +37,10 @@
 }
 ```
 
+!!! note ""
+
+    当内容只有一项时,可以忽略 JSON 数组 [] 标签
+
 ### 字段
 
 | 键         | 格式                    |
@@ -87,11 +102,9 @@
 
 #### network_strategy
 
-!!! quote ""
-
-    仅在 Android 与 Apple 平台图形客户端中支持,并且需要 `auto_detect_interface`。
+!!! question "自 sing-box 1.11.0 起"
 
-选择网络接口的策略
+详情参阅 [拨号字段](/configuration/shared/dial/#network_strategy)。
 
 当 `outbound.bind_interface`, `outbound.inet4_bind_address` 或 `outbound.inet6_bind_address` 已设置时不生效。
 
@@ -99,12 +112,20 @@
 
 与 `default_interface` 冲突。
 
-可用值请参阅 [拨号字段](/configuration/shared/dial/#network_strategy)。
+#### default_network_type
 
-#### fallback_delay
+!!! question "自 sing-box 1.11.0 起"
 
-!!! quote ""
+详情参阅 [拨号字段](/configuration/shared/dial/#default_network_type)。
+
+#### default_fallback_network_type
+
+!!! question "自 sing-box 1.11.0 起"
+
+详情参阅 [拨号字段](/configuration/shared/dial/#default_fallback_network_type)。
+
+#### default_fallback_delay
 
-    仅在 Android 与 Apple 平台图形客户端中支持,并且需要 `auto_detect_interface` 且 `network_strategy` 已设置。
+!!! question "自 sing-box 1.11.0 起"
 
-详情请参阅 [拨号字段](/configuration/shared/dial/#fallback_delay)。
+详情参阅 [拨号字段](/configuration/shared/dial/#fallback_delay)。

+ 13 - 9
docs/configuration/route/rule_action.md

@@ -11,12 +11,18 @@ icon: material/new-box
   "action": "route", // default
   "outbound": "",
   "network_strategy": "",
+  "network_type": [],
+  "fallback_network_type": [],
   "fallback_delay": "",
   "udp_disable_domain_unmapping": false,
   "udp_connect": false
 }
 ```
 
+!!! note ""
+
+    You can ignore the JSON Array [] tag when the content is only one item
+
 `route` inherits the classic rule behavior of routing connection to the specified outbound.
 
 #### outbound
@@ -27,22 +33,20 @@ Tag of target outbound.
 
 #### network_strategy
 
-!!! quote ""
-
-    Only supported in graphical clients on Android and iOS with `auto_detect_interface` enabled.
-
-Strategy for selecting network interfaces.
+See [Dial Fields](/configuration/shared/dial/#network_strategy) for details.
 
 Only take effect if outbound is direct without `outbound.bind_interface`,
 `outbound.inet4_bind_address` and `outbound.inet6_bind_address` set.
 
-See [Dial Fields](/configuration/shared/dial/#network_strategy) for available values.
+#### network_type
 
-#### fallback_delay
+See [Dial Fields](/configuration/shared/dial/#network_type) for details.
+
+#### fallback_network_type
 
-!!! quote ""
+See [Dial Fields](/configuration/shared/dial/#fallback_network_type) for details.
 
-    Only supported in graphical clients on Android and iOS with `auto_detect_interface` enabled and `network_strategy` set.
+#### fallback_delay
 
 See [Dial Fields](/configuration/shared/dial/#fallback_delay) for details.
 

+ 13 - 9
docs/configuration/route/rule_action.zh.md

@@ -12,6 +12,8 @@ icon: material/new-box
   "outbound": "",
   "network_strategy": "",
   "fallback_delay": "",
+  "network_type": [],
+  "fallback_network_type": [],
   "udp_disable_domain_unmapping": false,
   "udp_connect": false
 }
@@ -27,22 +29,20 @@ icon: material/new-box
 
 #### network_strategy
 
-!!! quote ""
-
-    仅在 Android 与 Apple 平台图形客户端中支持,并且需要 `auto_detect_interface`。
-
-选择网络接口的策略。
+详情参阅 [拨号字段](/configuration/shared/dial/#network_strategy)。
 
 仅当出站为 `direct` 且 `outbound.bind_interface`, `outbound.inet4_bind_address`
 且 `outbound.inet6_bind_address` 未设置时生效。
 
-可用值参阅 [拨号字段](/configuration/shared/dial/#network_strategy)。
+#### network_type
 
-#### fallback_delay
+详情参阅 [拨号字段](/configuration/shared/dial/#network_type)。
 
-!!! quote ""
+#### fallback_network_type
 
-    仅在 Android 与 Apple 平台图形客户端中支持,并且需要 `auto_detect_interface` 且 `network_strategy` 已设置。
+详情参阅 [拨号字段](/configuration/shared/dial/#fallback_network_type)。
+
+#### fallback_delay
 
 详情参阅 [拨号字段](/configuration/shared/dial/#fallback_delay)。
 
@@ -68,6 +68,10 @@ icon: material/new-box
 }
 ```
 
+!!! note ""
+
+    当内容只有一项时,可以忽略 JSON 数组 [] 标签
+
 `route-options` 为路由设置选项。
 
 ### reject

+ 49 - 14
docs/configuration/shared/dial.md

@@ -5,7 +5,9 @@ icon: material/new-box
 !!! quote "Changes in sing-box 1.11.0"
 
     :material-plus: [network_strategy](#network_strategy)  
-    :material-alert: [fallback_delay](#fallback_delay)
+    :material-alert: [fallback_delay](#fallback_delay)  
+    :material-alert: [network_type](#network_type)  
+    :material-alert: [fallback_network_type](#fallback_network_type)
 
 ### Structure
 
@@ -23,10 +25,16 @@ icon: material/new-box
   "udp_fragment": false,
   "domain_strategy": "prefer_ipv6",
   "network_strategy": "default",
+  "network_type": [],
+  "fallback_network_type": [],
   "fallback_delay": "300ms"
 }
 ```
 
+!!! note ""
+
+    You can ignore the JSON Array [] tag when the content is only one item
+
 ### Fields
 
 #### detour
@@ -101,30 +109,57 @@ If set, the requested domain name will be resolved to IP before connect.
 
 !!! quote ""
 
-    Only supported in graphical clients on Android and iOS with `auto_detect_interface` enabled.
+    Only supported in graphical clients on Android and Apple platforms with `auto_detect_interface` enabled.
 
 Strategy for selecting network interfaces.
 
 Available values:
 
-- `default` (default): Connect to the default interface.
-- `fallback`: Try all other interfaces when timeout.
-- `hybrid`: Connect to all interfaces concurrently and choose the fastest one.
-- `wifi`:  Prioritize WIFI, but try all other interfaces when unavailable or timeout.
-- `cellular`: Prioritize Cellular, but try all other interfaces when unavailable or timeout.
-- `ethernet`: Prioritize Ethernet, but try all other interfaces when unavailable or timeout.
-- `wifi_only`: Connect to WIFI only.
-- `cellular_only`: Connect to Cellular only.
-- `ethernet_only`: Connect to Ethernet only.
+- `default` (default): Connect to default network or networks specified in `network_type` sequentially.
+- `hybrid`: Connect to all networks or networks specified in `network_type` concurrently.
+- `fallback`: Connect to default network or preferred networks specified in `network_type` concurrently, and try fallback networks when unavailable or timeout.
 
-For fallback strategies, when preferred interfaces fails or times out,
-it will enter a 15s fast fallback state (upgraded to `hybrid`),
-and exit immediately if recovers.
+For fallback, when preferred interfaces fails or times out,
+it will enter a 15s fast fallback state (Connect to all preferred and fallback networks concurrently),
+and exit immediately if preferred networks recover.
 
 Conflicts with `bind_interface`, `inet4_bind_address` and `inet6_bind_address`.
 
+#### network_type
+
+!!! question "Since sing-box 1.11.0"
+
+!!! quote ""
+
+    Only supported in graphical clients on Android and Apple platforms with `auto_detect_interface` enabled.
+
+Network types to use when using `default` or `hybrid` network strategy or
+preferred network types to use when using `fallback` network strategy.
+
+Available values: `wifi`, `cellular`, `ethernet`, `other`.
+
+Device's default network is used by default.
+
+#### fallback_network_type
+
+!!! question "Since sing-box 1.11.0"
+
+!!! quote ""
+
+    Only supported in graphical clients on Android and Apple platforms with `auto_detect_interface` enabled.
+
+Fallback network types when preferred networks are unavailable or timeout when using `fallback` network strategy.
+
+All other networks expect preferred are used by default.
+
 #### fallback_delay
 
+!!! question "Since sing-box 1.11.0"
+
+!!! quote ""
+
+    Only supported in graphical clients on Android and Apple platforms with `auto_detect_interface` enabled.
+
 The length of time to wait before spawning a RFC 6555 Fast Fallback connection.
 
 For `domain_strategy`, is the amount of time to wait for connection to succeed before assuming

+ 42 - 12
docs/configuration/shared/dial.zh.md

@@ -5,7 +5,9 @@ icon: material/new-box
 !!! quote "sing-box 1.11.0 中的更改"
 
     :material-plus: [network_strategy](#network_strategy)  
-    :material-alert: [fallback_delay](#fallback_delay)
+    :material-alert: [fallback_delay](#fallback_delay)  
+    :material-alert: [network_type](#network_type)  
+    :material-alert: [fallback_network_type](#fallback_network_type)
 
 ### 结构
 
@@ -23,10 +25,16 @@ icon: material/new-box
   "udp_fragment": false,
   "domain_strategy": "prefer_ipv6",
   "network_strategy": "",
+  "network_type": [],
+  "fallback_network_type": [],
   "fallback_delay": "300ms"
 }
 ```
 
+!!! note ""
+
+    当内容只有一项时,可以忽略 JSON 数组 [] 标签
+
 ### 字段
 
 #### detour
@@ -99,26 +107,48 @@ icon: material/new-box
 
 !!! quote ""
 
-    仅在 Android 与 iOS 平台图形客户端中支持。
+    仅在 Android 与 iOS 平台图形客户端中支持,并且需要 `route.auto_detect_interface`
 
 用于选择网络接口的策略。
 
 可用值:
 
-- `default` (默认): 连接到默认接口,
-- `fallback`: 如果超时,尝试所有剩余接口。
-- `hybrid`: 同时尝试所有接口,选择最快的一个。
-- `wifi`:  优先使用 WIFI,但在不可用或超时时尝试所有其他接口。
-- `cellular`: 优先使用蜂窝数据,但在不可用或超时时尝试所有其他接口。
-- `ethernet`: 优先使用以太网,但在不可用或超时时尝试所有其他接口。
-- `wifi_only`: 仅连接到 WIFI。
-- `cellular_only`: 仅连接到蜂窝数据。
-- `ethernet_only`: 仅连接到以太网。
+- `default`(默认值):按顺序连接默认网络或 `network_type` 中指定的网络。
+- `hybrid`:同时连接所有网络或 `network_type` 中指定的网络。
+- `fallback`:同时连接默认网络或 `network_type` 中指定的首选网络,当不可用或超时时尝试回退网络。
 
-对于回退策略, 当优先使用的接口发生故障或超时时, 将进入 15 秒的快速回退状态(升级为 `hybrid`), 且恢复后立即退出。
+对于回退模式,当首选接口失败或超时时,
+将进入15秒的快速回退状态(同时连接所有首选和回退网络),
+如果首选网络恢复,则立即退出。
 
 与 `bind_interface`, `bind_inet4_address` 和 `bind_inet6_address` 冲突。
 
+#### network_type
+
+!!! question "自 sing-box 1.11.0 起"
+
+!!! quote ""
+
+    仅在 Android 与 iOS 平台图形客户端中支持,并且需要 `route.auto_detect_interface`。
+
+当使用 `default` 或 `hybrid` 网络策略时要使用的网络类型,或当使用 `fallback` 网络策略时要使用的首选网络类型。
+
+可用值:`wifi`, `cellular`, `ethernet`, `other`。
+
+默认使用设备默认网络。
+
+#### fallback_network_type
+
+!!! question "自 sing-box 1.11.0 起"
+
+!!! quote ""
+
+    仅在 Android 与 iOS 平台图形客户端中支持,并且需要 `route.auto_detect_interface`。
+
+当使用 `fallback` 网络策略时,在首选网络不可用或超时的情况下要使用的回退网络类型。
+
+默认使用除首选网络外的所有其他网络。
+
 #### fallback_delay
 
 在生成 RFC 6555 快速回退连接之前等待的时间长度。

+ 5 - 5
experimental/libbox/platform.go

@@ -34,10 +34,10 @@ type InterfaceUpdateListener interface {
 }
 
 const (
-	InterfaceTypeWIFI     = C.InterfaceTypeWIFI
-	InterfaceTypeCellular = C.InterfaceTypeCellular
-	InterfaceTypeEthernet = C.InterfaceTypeEthernet
-	InterfaceTypeOther    = C.InterfaceTypeOther
+	InterfaceTypeWIFI     = int32(C.InterfaceTypeWIFI)
+	InterfaceTypeCellular = int32(C.InterfaceTypeCellular)
+	InterfaceTypeEthernet = int32(C.InterfaceTypeEthernet)
+	InterfaceTypeOther    = int32(C.InterfaceTypeOther)
 )
 
 type NetworkInterface struct {
@@ -47,7 +47,7 @@ type NetworkInterface struct {
 	Addresses StringIterator
 	Flags     int32
 
-	Type      string
+	Type      int32
 	DNSServer StringIterator
 	Metered   bool
 }

+ 1 - 1
experimental/libbox/service.go

@@ -202,7 +202,7 @@ func (w *platformInterfaceWrapper) Interfaces() ([]adapter.NetworkInterface, err
 				Addresses: common.Map(iteratorToArray[string](netInterface.Addresses), netip.MustParsePrefix),
 				Flags:     linkFlags(uint32(netInterface.Flags)),
 			},
-			Type:        netInterface.Type,
+			Type:        C.InterfaceType(netInterface.Type),
 			DNSServers:  iteratorToArray[string](netInterface.DNSServer),
 			Expensive:   netInterface.Metered || isDefault && w.isExpensive,
 			Constrained: isDefault && w.isConstrained,

+ 19 - 17
option/outbound.go

@@ -65,23 +65,25 @@ type DialerOptionsWrapper interface {
 }
 
 type DialerOptions struct {
-	Detour               string             `json:"detour,omitempty"`
-	BindInterface        string             `json:"bind_interface,omitempty"`
-	Inet4BindAddress     *badoption.Addr    `json:"inet4_bind_address,omitempty"`
-	Inet6BindAddress     *badoption.Addr    `json:"inet6_bind_address,omitempty"`
-	ProtectPath          string             `json:"protect_path,omitempty"`
-	RoutingMark          uint32             `json:"routing_mark,omitempty"`
-	ReuseAddr            bool               `json:"reuse_addr,omitempty"`
-	ConnectTimeout       badoption.Duration `json:"connect_timeout,omitempty"`
-	TCPFastOpen          bool               `json:"tcp_fast_open,omitempty"`
-	TCPMultiPath         bool               `json:"tcp_multi_path,omitempty"`
-	UDPFragment          *bool              `json:"udp_fragment,omitempty"`
-	UDPFragmentDefault   bool               `json:"-"`
-	DomainStrategy       DomainStrategy     `json:"domain_strategy,omitempty"`
-	NetworkStrategy      NetworkStrategy    `json:"network_strategy,omitempty"`
-	FallbackDelay        badoption.Duration `json:"fallback_delay,omitempty"`
-	NetworkFallbackDelay badoption.Duration `json:"network_fallback_delay,omitempty"`
-	IsWireGuardListener  bool               `json:"-"`
+	Detour               string                            `json:"detour,omitempty"`
+	BindInterface        string                            `json:"bind_interface,omitempty"`
+	Inet4BindAddress     *badoption.Addr                   `json:"inet4_bind_address,omitempty"`
+	Inet6BindAddress     *badoption.Addr                   `json:"inet6_bind_address,omitempty"`
+	ProtectPath          string                            `json:"protect_path,omitempty"`
+	RoutingMark          FwMark                            `json:"routing_mark,omitempty"`
+	ReuseAddr            bool                              `json:"reuse_addr,omitempty"`
+	ConnectTimeout       badoption.Duration                `json:"connect_timeout,omitempty"`
+	TCPFastOpen          bool                              `json:"tcp_fast_open,omitempty"`
+	TCPMultiPath         bool                              `json:"tcp_multi_path,omitempty"`
+	UDPFragment          *bool                             `json:"udp_fragment,omitempty"`
+	UDPFragmentDefault   bool                              `json:"-"`
+	DomainStrategy       DomainStrategy                    `json:"domain_strategy,omitempty"`
+	NetworkStrategy      NetworkStrategy                   `json:"network_strategy,omitempty"`
+	NetworkType          badoption.Listable[InterfaceType] `json:"network_type,omitempty"`
+	FallbackNetworkType  badoption.Listable[InterfaceType] `json:"fallback_network_type,omitempty"`
+	FallbackDelay        badoption.Duration                `json:"fallback_delay,omitempty"`
+	NetworkFallbackDelay badoption.Duration                `json:"network_fallback_delay,omitempty"`
+	IsWireGuardListener  bool                              `json:"-"`
 }
 
 func (o *DialerOptions) TakeDialerOptions() DialerOptions {

+ 14 - 12
option/route.go

@@ -3,18 +3,20 @@ package option
 import "github.com/sagernet/sing/common/json/badoption"
 
 type RouteOptions struct {
-	GeoIP                  *GeoIPOptions      `json:"geoip,omitempty"`
-	Geosite                *GeositeOptions    `json:"geosite,omitempty"`
-	Rules                  []Rule             `json:"rules,omitempty"`
-	RuleSet                []RuleSet          `json:"rule_set,omitempty"`
-	Final                  string             `json:"final,omitempty"`
-	FindProcess            bool               `json:"find_process,omitempty"`
-	AutoDetectInterface    bool               `json:"auto_detect_interface,omitempty"`
-	OverrideAndroidVPN     bool               `json:"override_android_vpn,omitempty"`
-	DefaultInterface       string             `json:"default_interface,omitempty"`
-	DefaultMark            uint32             `json:"default_mark,omitempty"`
-	DefaultNetworkStrategy NetworkStrategy    `json:"default_network_strategy,omitempty"`
-	DefaultFallbackDelay   badoption.Duration `json:"default_fallback_delay,omitempty"`
+	GeoIP                      *GeoIPOptions                     `json:"geoip,omitempty"`
+	Geosite                    *GeositeOptions                   `json:"geosite,omitempty"`
+	Rules                      []Rule                            `json:"rules,omitempty"`
+	RuleSet                    []RuleSet                         `json:"rule_set,omitempty"`
+	Final                      string                            `json:"final,omitempty"`
+	FindProcess                bool                              `json:"find_process,omitempty"`
+	AutoDetectInterface        bool                              `json:"auto_detect_interface,omitempty"`
+	OverrideAndroidVPN         bool                              `json:"override_android_vpn,omitempty"`
+	DefaultInterface           string                            `json:"default_interface,omitempty"`
+	DefaultMark                FwMark                            `json:"default_mark,omitempty"`
+	DefaultNetworkStrategy     NetworkStrategy                   `json:"default_network_strategy,omitempty"`
+	DefaultNetworkType         badoption.Listable[InterfaceType] `json:"default_network_type,omitempty"`
+	DefaultFallbackNetworkType badoption.Listable[InterfaceType] `json:"default_fallback_network_type,omitempty"`
+	DefaultFallbackDelay       badoption.Duration                `json:"default_fallback_delay,omitempty"`
 }
 
 type GeoIPOptions struct {

+ 36 - 36
option/rule.go

@@ -67,42 +67,42 @@ func (r Rule) IsValid() bool {
 }
 
 type RawDefaultRule struct {
-	Inbound                  badoption.Listable[string] `json:"inbound,omitempty"`
-	IPVersion                int                        `json:"ip_version,omitempty"`
-	Network                  badoption.Listable[string] `json:"network,omitempty"`
-	AuthUser                 badoption.Listable[string] `json:"auth_user,omitempty"`
-	Protocol                 badoption.Listable[string] `json:"protocol,omitempty"`
-	Client                   badoption.Listable[string] `json:"client,omitempty"`
-	Domain                   badoption.Listable[string] `json:"domain,omitempty"`
-	DomainSuffix             badoption.Listable[string] `json:"domain_suffix,omitempty"`
-	DomainKeyword            badoption.Listable[string] `json:"domain_keyword,omitempty"`
-	DomainRegex              badoption.Listable[string] `json:"domain_regex,omitempty"`
-	Geosite                  badoption.Listable[string] `json:"geosite,omitempty"`
-	SourceGeoIP              badoption.Listable[string] `json:"source_geoip,omitempty"`
-	GeoIP                    badoption.Listable[string] `json:"geoip,omitempty"`
-	SourceIPCIDR             badoption.Listable[string] `json:"source_ip_cidr,omitempty"`
-	SourceIPIsPrivate        bool                       `json:"source_ip_is_private,omitempty"`
-	IPCIDR                   badoption.Listable[string] `json:"ip_cidr,omitempty"`
-	IPIsPrivate              bool                       `json:"ip_is_private,omitempty"`
-	SourcePort               badoption.Listable[uint16] `json:"source_port,omitempty"`
-	SourcePortRange          badoption.Listable[string] `json:"source_port_range,omitempty"`
-	Port                     badoption.Listable[uint16] `json:"port,omitempty"`
-	PortRange                badoption.Listable[string] `json:"port_range,omitempty"`
-	ProcessName              badoption.Listable[string] `json:"process_name,omitempty"`
-	ProcessPath              badoption.Listable[string] `json:"process_path,omitempty"`
-	ProcessPathRegex         badoption.Listable[string] `json:"process_path_regex,omitempty"`
-	PackageName              badoption.Listable[string] `json:"package_name,omitempty"`
-	User                     badoption.Listable[string] `json:"user,omitempty"`
-	UserID                   badoption.Listable[int32]  `json:"user_id,omitempty"`
-	ClashMode                string                     `json:"clash_mode,omitempty"`
-	NetworkType              badoption.Listable[string] `json:"network_type,omitempty"`
-	NetworkIsExpensive       bool                       `json:"network_is_expensive,omitempty"`
-	NetworkIsConstrained     bool                       `json:"network_is_constrained,omitempty"`
-	WIFISSID                 badoption.Listable[string] `json:"wifi_ssid,omitempty"`
-	WIFIBSSID                badoption.Listable[string] `json:"wifi_bssid,omitempty"`
-	RuleSet                  badoption.Listable[string] `json:"rule_set,omitempty"`
-	RuleSetIPCIDRMatchSource bool                       `json:"rule_set_ip_cidr_match_source,omitempty"`
-	Invert                   bool                       `json:"invert,omitempty"`
+	Inbound                  badoption.Listable[string]        `json:"inbound,omitempty"`
+	IPVersion                int                               `json:"ip_version,omitempty"`
+	Network                  badoption.Listable[string]        `json:"network,omitempty"`
+	AuthUser                 badoption.Listable[string]        `json:"auth_user,omitempty"`
+	Protocol                 badoption.Listable[string]        `json:"protocol,omitempty"`
+	Client                   badoption.Listable[string]        `json:"client,omitempty"`
+	Domain                   badoption.Listable[string]        `json:"domain,omitempty"`
+	DomainSuffix             badoption.Listable[string]        `json:"domain_suffix,omitempty"`
+	DomainKeyword            badoption.Listable[string]        `json:"domain_keyword,omitempty"`
+	DomainRegex              badoption.Listable[string]        `json:"domain_regex,omitempty"`
+	Geosite                  badoption.Listable[string]        `json:"geosite,omitempty"`
+	SourceGeoIP              badoption.Listable[string]        `json:"source_geoip,omitempty"`
+	GeoIP                    badoption.Listable[string]        `json:"geoip,omitempty"`
+	SourceIPCIDR             badoption.Listable[string]        `json:"source_ip_cidr,omitempty"`
+	SourceIPIsPrivate        bool                              `json:"source_ip_is_private,omitempty"`
+	IPCIDR                   badoption.Listable[string]        `json:"ip_cidr,omitempty"`
+	IPIsPrivate              bool                              `json:"ip_is_private,omitempty"`
+	SourcePort               badoption.Listable[uint16]        `json:"source_port,omitempty"`
+	SourcePortRange          badoption.Listable[string]        `json:"source_port_range,omitempty"`
+	Port                     badoption.Listable[uint16]        `json:"port,omitempty"`
+	PortRange                badoption.Listable[string]        `json:"port_range,omitempty"`
+	ProcessName              badoption.Listable[string]        `json:"process_name,omitempty"`
+	ProcessPath              badoption.Listable[string]        `json:"process_path,omitempty"`
+	ProcessPathRegex         badoption.Listable[string]        `json:"process_path_regex,omitempty"`
+	PackageName              badoption.Listable[string]        `json:"package_name,omitempty"`
+	User                     badoption.Listable[string]        `json:"user,omitempty"`
+	UserID                   badoption.Listable[int32]         `json:"user_id,omitempty"`
+	ClashMode                string                            `json:"clash_mode,omitempty"`
+	NetworkType              badoption.Listable[InterfaceType] `json:"network_type,omitempty"`
+	NetworkIsExpensive       bool                              `json:"network_is_expensive,omitempty"`
+	NetworkIsConstrained     bool                              `json:"network_is_constrained,omitempty"`
+	WIFISSID                 badoption.Listable[string]        `json:"wifi_ssid,omitempty"`
+	WIFIBSSID                badoption.Listable[string]        `json:"wifi_bssid,omitempty"`
+	RuleSet                  badoption.Listable[string]        `json:"rule_set,omitempty"`
+	RuleSetIPCIDRMatchSource bool                              `json:"rule_set_ip_cidr_match_source,omitempty"`
+	Invert                   bool                              `json:"invert,omitempty"`
 
 	// Deprecated: renamed to rule_set_ip_cidr_match_source
 	Deprecated_RulesetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"`

+ 38 - 38
option/rule_dns.go

@@ -68,44 +68,44 @@ func (r DNSRule) IsValid() bool {
 }
 
 type RawDefaultDNSRule struct {
-	Inbound                  badoption.Listable[string]       `json:"inbound,omitempty"`
-	IPVersion                int                              `json:"ip_version,omitempty"`
-	QueryType                badoption.Listable[DNSQueryType] `json:"query_type,omitempty"`
-	Network                  badoption.Listable[string]       `json:"network,omitempty"`
-	AuthUser                 badoption.Listable[string]       `json:"auth_user,omitempty"`
-	Protocol                 badoption.Listable[string]       `json:"protocol,omitempty"`
-	Domain                   badoption.Listable[string]       `json:"domain,omitempty"`
-	DomainSuffix             badoption.Listable[string]       `json:"domain_suffix,omitempty"`
-	DomainKeyword            badoption.Listable[string]       `json:"domain_keyword,omitempty"`
-	DomainRegex              badoption.Listable[string]       `json:"domain_regex,omitempty"`
-	Geosite                  badoption.Listable[string]       `json:"geosite,omitempty"`
-	SourceGeoIP              badoption.Listable[string]       `json:"source_geoip,omitempty"`
-	GeoIP                    badoption.Listable[string]       `json:"geoip,omitempty"`
-	IPCIDR                   badoption.Listable[string]       `json:"ip_cidr,omitempty"`
-	IPIsPrivate              bool                             `json:"ip_is_private,omitempty"`
-	SourceIPCIDR             badoption.Listable[string]       `json:"source_ip_cidr,omitempty"`
-	SourceIPIsPrivate        bool                             `json:"source_ip_is_private,omitempty"`
-	SourcePort               badoption.Listable[uint16]       `json:"source_port,omitempty"`
-	SourcePortRange          badoption.Listable[string]       `json:"source_port_range,omitempty"`
-	Port                     badoption.Listable[uint16]       `json:"port,omitempty"`
-	PortRange                badoption.Listable[string]       `json:"port_range,omitempty"`
-	ProcessName              badoption.Listable[string]       `json:"process_name,omitempty"`
-	ProcessPath              badoption.Listable[string]       `json:"process_path,omitempty"`
-	ProcessPathRegex         badoption.Listable[string]       `json:"process_path_regex,omitempty"`
-	PackageName              badoption.Listable[string]       `json:"package_name,omitempty"`
-	User                     badoption.Listable[string]       `json:"user,omitempty"`
-	UserID                   badoption.Listable[int32]        `json:"user_id,omitempty"`
-	Outbound                 badoption.Listable[string]       `json:"outbound,omitempty"`
-	ClashMode                string                           `json:"clash_mode,omitempty"`
-	NetworkType              badoption.Listable[string]       `json:"network_type,omitempty"`
-	NetworkIsExpensive       bool                             `json:"network_is_expensive,omitempty"`
-	NetworkIsConstrained     bool                             `json:"network_is_constrained,omitempty"`
-	WIFISSID                 badoption.Listable[string]       `json:"wifi_ssid,omitempty"`
-	WIFIBSSID                badoption.Listable[string]       `json:"wifi_bssid,omitempty"`
-	RuleSet                  badoption.Listable[string]       `json:"rule_set,omitempty"`
-	RuleSetIPCIDRMatchSource bool                             `json:"rule_set_ip_cidr_match_source,omitempty"`
-	RuleSetIPCIDRAcceptEmpty bool                             `json:"rule_set_ip_cidr_accept_empty,omitempty"`
-	Invert                   bool                             `json:"invert,omitempty"`
+	Inbound                  badoption.Listable[string]        `json:"inbound,omitempty"`
+	IPVersion                int                               `json:"ip_version,omitempty"`
+	QueryType                badoption.Listable[DNSQueryType]  `json:"query_type,omitempty"`
+	Network                  badoption.Listable[string]        `json:"network,omitempty"`
+	AuthUser                 badoption.Listable[string]        `json:"auth_user,omitempty"`
+	Protocol                 badoption.Listable[string]        `json:"protocol,omitempty"`
+	Domain                   badoption.Listable[string]        `json:"domain,omitempty"`
+	DomainSuffix             badoption.Listable[string]        `json:"domain_suffix,omitempty"`
+	DomainKeyword            badoption.Listable[string]        `json:"domain_keyword,omitempty"`
+	DomainRegex              badoption.Listable[string]        `json:"domain_regex,omitempty"`
+	Geosite                  badoption.Listable[string]        `json:"geosite,omitempty"`
+	SourceGeoIP              badoption.Listable[string]        `json:"source_geoip,omitempty"`
+	GeoIP                    badoption.Listable[string]        `json:"geoip,omitempty"`
+	IPCIDR                   badoption.Listable[string]        `json:"ip_cidr,omitempty"`
+	IPIsPrivate              bool                              `json:"ip_is_private,omitempty"`
+	SourceIPCIDR             badoption.Listable[string]        `json:"source_ip_cidr,omitempty"`
+	SourceIPIsPrivate        bool                              `json:"source_ip_is_private,omitempty"`
+	SourcePort               badoption.Listable[uint16]        `json:"source_port,omitempty"`
+	SourcePortRange          badoption.Listable[string]        `json:"source_port_range,omitempty"`
+	Port                     badoption.Listable[uint16]        `json:"port,omitempty"`
+	PortRange                badoption.Listable[string]        `json:"port_range,omitempty"`
+	ProcessName              badoption.Listable[string]        `json:"process_name,omitempty"`
+	ProcessPath              badoption.Listable[string]        `json:"process_path,omitempty"`
+	ProcessPathRegex         badoption.Listable[string]        `json:"process_path_regex,omitempty"`
+	PackageName              badoption.Listable[string]        `json:"package_name,omitempty"`
+	User                     badoption.Listable[string]        `json:"user,omitempty"`
+	UserID                   badoption.Listable[int32]         `json:"user_id,omitempty"`
+	Outbound                 badoption.Listable[string]        `json:"outbound,omitempty"`
+	ClashMode                string                            `json:"clash_mode,omitempty"`
+	NetworkType              badoption.Listable[InterfaceType] `json:"network_type,omitempty"`
+	NetworkIsExpensive       bool                              `json:"network_is_expensive,omitempty"`
+	NetworkIsConstrained     bool                              `json:"network_is_constrained,omitempty"`
+	WIFISSID                 badoption.Listable[string]        `json:"wifi_ssid,omitempty"`
+	WIFIBSSID                badoption.Listable[string]        `json:"wifi_bssid,omitempty"`
+	RuleSet                  badoption.Listable[string]        `json:"rule_set,omitempty"`
+	RuleSetIPCIDRMatchSource bool                              `json:"rule_set_ip_cidr_match_source,omitempty"`
+	RuleSetIPCIDRAcceptEmpty bool                              `json:"rule_set_ip_cidr_accept_empty,omitempty"`
+	Invert                   bool                              `json:"invert,omitempty"`
 
 	// Deprecated: renamed to rule_set_ip_cidr_match_source
 	Deprecated_RulesetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"`

+ 22 - 22
option/rule_set.go

@@ -146,28 +146,28 @@ func (r HeadlessRule) IsValid() bool {
 }
 
 type DefaultHeadlessRule struct {
-	QueryType            badoption.Listable[DNSQueryType] `json:"query_type,omitempty"`
-	Network              badoption.Listable[string]       `json:"network,omitempty"`
-	Domain               badoption.Listable[string]       `json:"domain,omitempty"`
-	DomainSuffix         badoption.Listable[string]       `json:"domain_suffix,omitempty"`
-	DomainKeyword        badoption.Listable[string]       `json:"domain_keyword,omitempty"`
-	DomainRegex          badoption.Listable[string]       `json:"domain_regex,omitempty"`
-	SourceIPCIDR         badoption.Listable[string]       `json:"source_ip_cidr,omitempty"`
-	IPCIDR               badoption.Listable[string]       `json:"ip_cidr,omitempty"`
-	SourcePort           badoption.Listable[uint16]       `json:"source_port,omitempty"`
-	SourcePortRange      badoption.Listable[string]       `json:"source_port_range,omitempty"`
-	Port                 badoption.Listable[uint16]       `json:"port,omitempty"`
-	PortRange            badoption.Listable[string]       `json:"port_range,omitempty"`
-	ProcessName          badoption.Listable[string]       `json:"process_name,omitempty"`
-	ProcessPath          badoption.Listable[string]       `json:"process_path,omitempty"`
-	ProcessPathRegex     badoption.Listable[string]       `json:"process_path_regex,omitempty"`
-	PackageName          badoption.Listable[string]       `json:"package_name,omitempty"`
-	NetworkType          badoption.Listable[string]       `json:"network_type,omitempty"`
-	NetworkIsExpensive   bool                             `json:"network_is_expensive,omitempty"`
-	NetworkIsConstrained bool                             `json:"network_is_constrained,omitempty"`
-	WIFISSID             badoption.Listable[string]       `json:"wifi_ssid,omitempty"`
-	WIFIBSSID            badoption.Listable[string]       `json:"wifi_bssid,omitempty"`
-	Invert               bool                             `json:"invert,omitempty"`
+	QueryType            badoption.Listable[DNSQueryType]  `json:"query_type,omitempty"`
+	Network              badoption.Listable[string]        `json:"network,omitempty"`
+	Domain               badoption.Listable[string]        `json:"domain,omitempty"`
+	DomainSuffix         badoption.Listable[string]        `json:"domain_suffix,omitempty"`
+	DomainKeyword        badoption.Listable[string]        `json:"domain_keyword,omitempty"`
+	DomainRegex          badoption.Listable[string]        `json:"domain_regex,omitempty"`
+	SourceIPCIDR         badoption.Listable[string]        `json:"source_ip_cidr,omitempty"`
+	IPCIDR               badoption.Listable[string]        `json:"ip_cidr,omitempty"`
+	SourcePort           badoption.Listable[uint16]        `json:"source_port,omitempty"`
+	SourcePortRange      badoption.Listable[string]        `json:"source_port_range,omitempty"`
+	Port                 badoption.Listable[uint16]        `json:"port,omitempty"`
+	PortRange            badoption.Listable[string]        `json:"port_range,omitempty"`
+	ProcessName          badoption.Listable[string]        `json:"process_name,omitempty"`
+	ProcessPath          badoption.Listable[string]        `json:"process_path,omitempty"`
+	ProcessPathRegex     badoption.Listable[string]        `json:"process_path_regex,omitempty"`
+	PackageName          badoption.Listable[string]        `json:"package_name,omitempty"`
+	NetworkType          badoption.Listable[InterfaceType] `json:"network_type,omitempty"`
+	NetworkIsExpensive   bool                              `json:"network_is_expensive,omitempty"`
+	NetworkIsConstrained bool                              `json:"network_is_constrained,omitempty"`
+	WIFISSID             badoption.Listable[string]        `json:"wifi_ssid,omitempty"`
+	WIFIBSSID            badoption.Listable[string]        `json:"wifi_bssid,omitempty"`
+	Invert               bool                              `json:"invert,omitempty"`
 
 	DomainMatcher *domain.Matcher `json:"-"`
 	SourceIPSet   *netipx.IPSet   `json:"-"`

+ 24 - 0
option/types.go

@@ -171,3 +171,27 @@ func (n *NetworkStrategy) UnmarshalJSON(content []byte) error {
 	*n = NetworkStrategy(strategy)
 	return nil
 }
+
+type InterfaceType C.InterfaceType
+
+func (t InterfaceType) Build() C.InterfaceType {
+	return C.InterfaceType(t)
+}
+
+func (t InterfaceType) MarshalJSON() ([]byte, error) {
+	return json.Marshal(C.InterfaceType(t).String())
+}
+
+func (t *InterfaceType) UnmarshalJSON(content []byte) error {
+	var value string
+	err := json.Unmarshal(content, &value)
+	if err != nil {
+		return err
+	}
+	interfaceType, loaded := C.StringToInterfaceType[value]
+	if !loaded {
+		return E.New("unknown interface type: ", value)
+	}
+	*t = InterfaceType(interfaceType)
+	return nil
+}

+ 10 - 6
protocol/direct/outbound.go

@@ -12,7 +12,7 @@ import (
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
-	dns "github.com/sagernet/sing-dns"
+	"github.com/sagernet/sing-dns"
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/bufio"
 	E "github.com/sagernet/sing/common/exceptions"
@@ -37,6 +37,8 @@ type Outbound struct {
 	domainStrategy       dns.DomainStrategy
 	fallbackDelay        time.Duration
 	networkStrategy      C.NetworkStrategy
+	networkType          []C.InterfaceType
+	fallbackNetworkType  []C.InterfaceType
 	networkFallbackDelay time.Duration
 	overrideOption       int
 	overrideDestination  M.Socksaddr
@@ -55,6 +57,8 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
 		domainStrategy:       dns.DomainStrategy(options.DomainStrategy),
 		fallbackDelay:        time.Duration(options.FallbackDelay),
 		networkStrategy:      C.NetworkStrategy(options.NetworkStrategy),
+		networkType:          common.Map(options.NetworkType, option.InterfaceType.Build),
+		fallbackNetworkType:  common.Map(options.FallbackNetworkType, option.InterfaceType.Build),
 		networkFallbackDelay: time.Duration(options.NetworkFallbackDelay),
 		dialer:               outboundDialer,
 		// loopBack:       newLoopBackDetector(router),
@@ -171,10 +175,10 @@ func (h *Outbound) DialParallel(ctx context.Context, network string, destination
 			return nil, E.New("no IPv6 address available for ", destination)
 		}
 	}
-	return dialer.DialParallelNetwork(ctx, h.dialer, network, destination, destinationAddresses, domainStrategy == dns.DomainStrategyPreferIPv6, h.networkStrategy, h.fallbackDelay)
+	return dialer.DialParallelNetwork(ctx, h.dialer, network, destination, destinationAddresses, domainStrategy == dns.DomainStrategyPreferIPv6, h.networkStrategy, h.networkType, h.fallbackNetworkType, h.fallbackDelay)
 }
 
-func (h *Outbound) DialParallelNetwork(ctx context.Context, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, networkStrategy C.NetworkStrategy, fallbackDelay time.Duration) (net.Conn, error) {
+func (h *Outbound) DialParallelNetwork(ctx context.Context, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, networkStrategy C.NetworkStrategy, networkType []C.InterfaceType, fallbackNetworkType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) {
 	ctx, metadata := adapter.ExtendContext(ctx)
 	metadata.Outbound = h.Tag()
 	metadata.Destination = destination
@@ -210,10 +214,10 @@ func (h *Outbound) DialParallelNetwork(ctx context.Context, network string, dest
 			return nil, E.New("no IPv6 address available for ", destination)
 		}
 	}
-	return dialer.DialParallelNetwork(ctx, h.dialer, network, destination, destinationAddresses, domainStrategy == dns.DomainStrategyPreferIPv6, networkStrategy, fallbackDelay)
+	return dialer.DialParallelNetwork(ctx, h.dialer, network, destination, destinationAddresses, domainStrategy == dns.DomainStrategyPreferIPv6, networkStrategy, networkType, fallbackNetworkType, fallbackDelay)
 }
 
-func (h *Outbound) ListenSerialNetworkPacket(ctx context.Context, destination M.Socksaddr, destinationAddresses []netip.Addr, networkStrategy C.NetworkStrategy, fallbackDelay time.Duration) (net.PacketConn, netip.Addr, error) {
+func (h *Outbound) ListenSerialNetworkPacket(ctx context.Context, destination M.Socksaddr, destinationAddresses []netip.Addr, networkStrategy C.NetworkStrategy, networkType []C.InterfaceType, fallbackNetworkType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, netip.Addr, error) {
 	ctx, metadata := adapter.ExtendContext(ctx)
 	metadata.Outbound = h.Tag()
 	metadata.Destination = destination
@@ -232,7 +236,7 @@ func (h *Outbound) ListenSerialNetworkPacket(ctx context.Context, destination M.
 	} else {
 		h.logger.InfoContext(ctx, "outbound packet connection to ", destination)
 	}
-	conn, newDestination, err := dialer.ListenSerialNetworkPacket(ctx, h.dialer, destination, destinationAddresses, networkStrategy, fallbackDelay)
+	conn, newDestination, err := dialer.ListenSerialNetworkPacket(ctx, h.dialer, destination, destinationAddresses, networkStrategy, networkType, fallbackNetworkType, fallbackDelay)
 	if err != nil {
 		return nil, netip.Addr{}, err
 	}

+ 7 - 5
route/network.go

@@ -59,10 +59,12 @@ func NewNetworkManager(ctx context.Context, logger logger.ContextLogger, routeOp
 		interfaceFinder:     control.NewDefaultInterfaceFinder(),
 		autoDetectInterface: routeOptions.AutoDetectInterface,
 		defaultOptions: adapter.NetworkOptions{
-			DefaultInterface:       routeOptions.DefaultInterface,
-			DefaultMark:            routeOptions.DefaultMark,
-			DefaultNetworkStrategy: C.NetworkStrategy(routeOptions.DefaultNetworkStrategy),
-			DefaultFallbackDelay:   time.Duration(routeOptions.DefaultFallbackDelay),
+			BindInterface:       routeOptions.DefaultInterface,
+			RoutingMark:         uint32(routeOptions.DefaultMark),
+			NetworkStrategy:     C.NetworkStrategy(routeOptions.DefaultNetworkStrategy),
+			NetworkType:         common.Map(routeOptions.DefaultNetworkType, option.InterfaceType.Build),
+			FallbackNetworkType: common.Map(routeOptions.DefaultFallbackNetworkType, option.InterfaceType.Build),
+			FallbackDelay:       time.Duration(routeOptions.DefaultFallbackDelay),
 		},
 		pauseManager:      service.FromContext[pause.Manager](ctx),
 		platformInterface: service.FromContext[platform.Interface](ctx),
@@ -382,7 +384,7 @@ func (r *NetworkManager) notifyInterfaceUpdate(defaultInterface *control.Interfa
 		networkInterface := common.Find(r.networkInterfaces.Load(), func(it adapter.NetworkInterface) bool {
 			return it.Interface.Index == defaultInterface.Index
 		})
-		if networkInterface.Type == "" {
+		if networkInterface.Name == "" {
 			// race
 			return
 		}

+ 2 - 0
route/rule/rule_action.go

@@ -140,6 +140,8 @@ func (r *RuleActionRoute) String() string {
 
 type RuleActionRouteOptions struct {
 	NetworkStrategy           C.NetworkStrategy
+	NetworkType               []C.InterfaceType
+	FallbackNetworkType       []C.InterfaceType
 	FallbackDelay             time.Duration
 	UDPDisableDomainUnmapping bool
 	UDPConnect                bool

+ 2 - 1
route/rule/rule_default.go

@@ -8,6 +8,7 @@ import (
 	"github.com/sagernet/sing-box/experimental/deprecated"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing/common"
 	E "github.com/sagernet/sing/common/exceptions"
 	"github.com/sagernet/sing/service"
 )
@@ -224,7 +225,7 @@ func NewDefaultRule(ctx context.Context, logger log.ContextLogger, options optio
 		rule.allItems = append(rule.allItems, item)
 	}
 	if len(options.NetworkType) > 0 {
-		item := NewNetworkTypeItem(networkManager, options.NetworkType)
+		item := NewNetworkTypeItem(networkManager, common.Map(options.NetworkType, option.InterfaceType.Build))
 		rule.items = append(rule.items, item)
 		rule.allItems = append(rule.allItems, item)
 	}

+ 1 - 1
route/rule/rule_dns.go

@@ -221,7 +221,7 @@ func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options op
 		rule.allItems = append(rule.allItems, item)
 	}
 	if len(options.NetworkType) > 0 {
-		item := NewNetworkTypeItem(networkManager, options.NetworkType)
+		item := NewNetworkTypeItem(networkManager, common.Map(options.NetworkType, option.InterfaceType.Build))
 		rule.items = append(rule.items, item)
 		rule.allItems = append(rule.allItems, item)
 	}

+ 2 - 1
route/rule/rule_headless.go

@@ -6,6 +6,7 @@ import (
 	"github.com/sagernet/sing-box/adapter"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing/common"
 	E "github.com/sagernet/sing/common/exceptions"
 	"github.com/sagernet/sing/service"
 )
@@ -142,7 +143,7 @@ func NewDefaultHeadlessRule(ctx context.Context, options option.DefaultHeadlessR
 	}
 	if networkManager != nil {
 		if len(options.NetworkType) > 0 {
-			item := NewNetworkTypeItem(networkManager, options.NetworkType)
+			item := NewNetworkTypeItem(networkManager, common.Map(options.NetworkType, option.InterfaceType.Build))
 			rule.items = append(rule.items, item)
 			rule.allItems = append(rule.allItems, item)
 		}

+ 3 - 2
route/rule/rule_item_network_type.go

@@ -4,6 +4,7 @@ import (
 	"strings"
 
 	"github.com/sagernet/sing-box/adapter"
+	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing/common"
 	F "github.com/sagernet/sing/common/format"
 )
@@ -12,10 +13,10 @@ var _ RuleItem = (*NetworkTypeItem)(nil)
 
 type NetworkTypeItem struct {
 	networkManager adapter.NetworkManager
-	networkType    []string
+	networkType    []C.InterfaceType
 }
 
-func NewNetworkTypeItem(networkManager adapter.NetworkManager, networkType []string) *NetworkTypeItem {
+func NewNetworkTypeItem(networkManager adapter.NetworkManager, networkType []C.InterfaceType) *NetworkTypeItem {
 	return &NetworkTypeItem{
 		networkManager: networkManager,
 		networkType:    networkType,