Browse Source

Add auto_route and auto_detect_interface for linux

世界 3 years ago
parent
commit
638f8a52d1

+ 3 - 0
adapter/router.go

@@ -23,6 +23,9 @@ type Router interface {
 	Exchange(ctx context.Context, message *dnsmessage.Message) (*dnsmessage.Message, error)
 	Lookup(ctx context.Context, domain string, strategy C.DomainStrategy) ([]netip.Addr, error)
 	LookupDefault(ctx context.Context, domain string) ([]netip.Addr, error)
+	AutoDetectInterface() bool
+	DefaultInterfaceName() string
+	DefaultInterfaceIndex() int
 }
 
 type Rule interface {

+ 23 - 0
common/dialer/auto_linux.go

@@ -0,0 +1,23 @@
+package dialer
+
+import (
+	"syscall"
+
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing/common/control"
+	E "github.com/sagernet/sing/common/exceptions"
+)
+
+func BindToInterface(router adapter.Router) control.Func {
+	return func(network, address string, conn syscall.RawConn) error {
+		interfaceName := router.DefaultInterfaceName()
+		if interfaceName == "" {
+			return nil
+		}
+		var innerErr error
+		err := conn.Control(func(fd uintptr) {
+			innerErr = syscall.BindToDevice(int(fd), interfaceName)
+		})
+		return E.Errors(innerErr, err)
+	}
+}

+ 12 - 0
common/dialer/auto_other.go

@@ -0,0 +1,12 @@
+//go:build !linux
+
+package dialer
+
+import (
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing/common/control"
+)
+
+func BindToInterface(router adapter.Router) control.Func {
+	return nil
+}

+ 5 - 1
common/dialer/default.go

@@ -5,6 +5,7 @@ import (
 	"net"
 	"time"
 
+	"github.com/sagernet/sing-box/adapter"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing/common/control"
@@ -18,12 +19,15 @@ type DefaultDialer struct {
 	net.ListenConfig
 }
 
-func NewDefault(options option.DialerOptions) *DefaultDialer {
+func NewDefault(router adapter.Router, options option.DialerOptions) *DefaultDialer {
 	var dialer net.Dialer
 	var listener net.ListenConfig
 	if options.BindInterface != "" {
 		dialer.Control = control.Append(dialer.Control, control.BindToInterface(options.BindInterface))
 		listener.Control = control.Append(listener.Control, control.BindToInterface(options.BindInterface))
+	} else if router.AutoDetectInterface() {
+		dialer.Control = BindToInterface(router)
+		listener.Control = BindToInterface(router)
 	}
 	if options.RoutingMark != 0 {
 		dialer.Control = control.Append(dialer.Control, control.RoutingMark(options.RoutingMark))

+ 1 - 1
common/dialer/dialer.go

@@ -12,7 +12,7 @@ import (
 
 func New(router adapter.Router, options option.DialerOptions) N.Dialer {
 	if options.Detour == "" {
-		return NewDefault(options)
+		return NewDefault(router, options)
 	} else {
 		return NewDetour(router, options.Detour)
 	}

+ 9 - 0
common/iffmonitor/monitor.go

@@ -0,0 +1,9 @@
+package iffmonitor
+
+import "github.com/sagernet/sing-box/adapter"
+
+type InterfaceMonitor interface {
+	adapter.Service
+	DefaultInterfaceName() string
+	DefaultInterfaceIndex() int
+}

+ 100 - 0
common/iffmonitor/monitor_linux.go

@@ -0,0 +1,100 @@
+package iffmonitor
+
+import (
+	"os"
+
+	"github.com/sagernet/sing-box/log"
+	E "github.com/sagernet/sing/common/exceptions"
+
+	"github.com/vishvananda/netlink"
+)
+
+var _ InterfaceMonitor = (*monitor)(nil)
+
+type monitor struct {
+	logger                log.Logger
+	defaultInterfaceName  string
+	defaultInterfaceIndex int
+	update                chan netlink.RouteUpdate
+	close                 chan struct{}
+}
+
+func New(logger log.Logger) (InterfaceMonitor, error) {
+	return &monitor{
+		logger: logger,
+		update: make(chan netlink.RouteUpdate, 2),
+		close:  make(chan struct{}),
+	}, nil
+}
+
+func (m *monitor) Start() error {
+	err := netlink.RouteSubscribe(m.update, m.close)
+	if err != nil {
+		return err
+	}
+	err = m.checkUpdate()
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func (m *monitor) loopUpdate() {
+	for {
+		select {
+		case <-m.close:
+			return
+		case <-m.update:
+			err := m.checkUpdate()
+			if err != nil {
+				m.logger.Error(E.Cause(err, "check default interface"))
+			}
+		}
+	}
+}
+
+func (m *monitor) checkUpdate() error {
+	routes, err := netlink.RouteList(nil, netlink.FAMILY_V4)
+	if err != nil {
+		return err
+	}
+	for _, route := range routes {
+		if route.Dst != nil {
+			continue
+		}
+		var link netlink.Link
+		link, err = netlink.LinkByIndex(route.LinkIndex)
+		if err != nil {
+			return err
+		}
+
+		if link.Type() == "tuntap" {
+			continue
+		}
+
+		m.defaultInterfaceName = link.Attrs().Name
+		m.defaultInterfaceIndex = link.Attrs().Index
+
+		m.logger.Info("updated default interface ", m.defaultInterfaceName, ", index ", m.defaultInterfaceIndex)
+		return nil
+	}
+	return E.New("no route to internet")
+}
+
+func (m *monitor) Close() error {
+	select {
+	case <-m.close:
+		return os.ErrClosed
+	default:
+	}
+	close(m.close)
+	return nil
+}
+
+func (m *monitor) DefaultInterfaceName() string {
+	return m.defaultInterfaceName
+}
+
+func (m *monitor) DefaultInterfaceIndex() int {
+	return m.defaultInterfaceIndex
+}

+ 13 - 0
common/iffmonitor/monitor_other.go

@@ -0,0 +1,13 @@
+//go:build !linux
+
+package iffmonitor
+
+import (
+	"os"
+
+	"github.com/sagernet/sing-box/log"
+)
+
+func New(logger log.Logger) (InterfaceMonitor, error) {
+	return nil, os.ErrInvalid
+}

+ 63 - 1
common/tun/tun_linux.go

@@ -1,6 +1,7 @@
 package tun
 
 import (
+	"net"
 	"net/netip"
 
 	"github.com/vishvananda/netlink"
@@ -15,7 +16,7 @@ func Open(name string) (uintptr, error) {
 	return uintptr(tunFd), nil
 }
 
-func Configure(name string, inet4Address netip.Prefix, inet6Address netip.Prefix, mtu uint32) error {
+func Configure(name string, inet4Address netip.Prefix, inet6Address netip.Prefix, mtu uint32, autoRoute bool) error {
 	tunLink, err := netlink.LinkByName(name)
 	if err != nil {
 		return err
@@ -47,5 +48,66 @@ func Configure(name string, inet4Address netip.Prefix, inet6Address netip.Prefix
 		return err
 	}
 
+	if autoRoute {
+		if inet4Address.IsValid() {
+			err = netlink.RouteAdd(&netlink.Route{
+				Dst: &net.IPNet{
+					IP:   net.IPv4zero,
+					Mask: net.CIDRMask(0, 32),
+				},
+				LinkIndex: tunLink.Attrs().Index,
+			})
+			if err != nil {
+				return err
+			}
+		}
+		if inet6Address.IsValid() {
+			err = netlink.RouteAdd(&netlink.Route{
+				Dst: &net.IPNet{
+					IP:   net.IPv6zero,
+					Mask: net.CIDRMask(0, 128),
+				},
+				LinkIndex: tunLink.Attrs().Index,
+			})
+			if err != nil {
+				return err
+			}
+		}
+	}
+
+	return nil
+}
+
+func UnConfigure(name string, inet4Address netip.Prefix, inet6Address netip.Prefix, autoRoute bool) error {
+	if autoRoute {
+		tunLink, err := netlink.LinkByName(name)
+		if err != nil {
+			return err
+		}
+		if inet4Address.IsValid() {
+			err = netlink.RouteDel(&netlink.Route{
+				Dst: &net.IPNet{
+					IP:   net.IPv4zero,
+					Mask: net.CIDRMask(0, 32),
+				},
+				LinkIndex: tunLink.Attrs().Index,
+			})
+			if err != nil {
+				return err
+			}
+		}
+		if inet6Address.IsValid() {
+			err = netlink.RouteDel(&netlink.Route{
+				Dst: &net.IPNet{
+					IP:   net.IPv6zero,
+					Mask: net.CIDRMask(0, 128),
+				},
+				LinkIndex: tunLink.Attrs().Index,
+			})
+			if err != nil {
+				return err
+			}
+		}
+	}
 	return nil
 }

+ 9 - 0
common/tun/tun_other.go

@@ -3,9 +3,18 @@
 package tun
 
 import (
+	"net/netip"
 	"os"
 )
 
 func Open(name string) (uintptr, error) {
 	return 0, os.ErrInvalid
 }
+
+func Configure(name string, inet4Address netip.Prefix, inet6Address netip.Prefix, mtu uint32, autoRoute bool) error {
+	return os.ErrInvalid
+}
+
+func UnConfigure(name string, inet4Address netip.Prefix, inet6Address netip.Prefix, autoRoute bool) error {
+	return os.ErrInvalid
+}

+ 1 - 1
inbound/default.go

@@ -236,7 +236,7 @@ func (a *myInboundAdapter) NewError(ctx context.Context, err error) {
 
 func NewError(logger log.Logger, ctx context.Context, err error) {
 	common.Close(err)
-	if E.IsClosed(err) {
+	if E.IsClosed(err) || E.IsCanceled(err) {
 		logger.WithContext(ctx).Debug("connection closed")
 		return
 	}

+ 15 - 3
inbound/tun.go

@@ -32,8 +32,9 @@ type Tun struct {
 	logger  log.Logger
 	options option.TunInboundOptions
 
-	tunFd uintptr
-	tun   *tun.GVisorTun
+	tunName string
+	tunFd   uintptr
+	tun     *tun.GVisorTun
 }
 
 func NewTun(ctx context.Context, router adapter.Router, logger log.Logger, tag string, options option.TunInboundOptions) (*Tun, error) {
@@ -70,10 +71,11 @@ func (t *Tun) Start() error {
 	if err != nil {
 		return E.Cause(err, "create tun interface")
 	}
-	err = tun.Configure(tunName, netip.Prefix(t.options.Inet4Address), netip.Prefix(t.options.Inet6Address), mtu)
+	err = tun.Configure(tunName, netip.Prefix(t.options.Inet4Address), netip.Prefix(t.options.Inet6Address), mtu, t.options.AutoRoute)
 	if err != nil {
 		return E.Cause(err, "configure tun interface")
 	}
+	t.tunName = tunName
 	t.tunFd = tunFd
 	t.tun = tun.NewGVisor(t.ctx, tunFd, mtu, t)
 	err = t.tun.Start()
@@ -85,6 +87,10 @@ func (t *Tun) Start() error {
 }
 
 func (t *Tun) Close() error {
+	err := tun.UnConfigure(t.tunName, netip.Prefix(t.options.Inet4Address), netip.Prefix(t.options.Inet6Address), t.options.AutoRoute)
+	if err != nil {
+		return err
+	}
 	return E.Errors(
 		t.tun.Close(),
 		os.NewFile(t.tunFd, "tun").Close(),
@@ -99,6 +105,9 @@ func (t *Tun) NewConnection(ctx context.Context, conn net.Conn, upstreamMetadata
 	metadata.Network = C.NetworkTCP
 	metadata.Source = upstreamMetadata.Source
 	metadata.Destination = upstreamMetadata.Destination
+	metadata.SniffEnabled = t.options.SniffEnabled
+	metadata.SniffOverrideDestination = t.options.SniffOverrideDestination
+	metadata.DomainStrategy = C.DomainStrategy(t.options.DomainStrategy)
 	return t.router.RouteConnection(ctx, conn, metadata)
 }
 
@@ -110,6 +119,9 @@ func (t *Tun) NewPacketConnection(ctx context.Context, conn N.PacketConn, upstre
 	metadata.Network = C.NetworkUDP
 	metadata.Source = upstreamMetadata.Source
 	metadata.Destination = upstreamMetadata.Destination
+	metadata.SniffEnabled = t.options.SniffEnabled
+	metadata.SniffOverrideDestination = t.options.SniffOverrideDestination
+	metadata.DomainStrategy = C.DomainStrategy(t.options.DomainStrategy)
 	return t.router.RoutePacketConnection(ctx, conn, metadata)
 }
 

+ 11 - 5
option/inbound.go

@@ -83,16 +83,20 @@ func (h *Inbound) UnmarshalJSON(bytes []byte) error {
 	return nil
 }
 
-type ListenOptions struct {
-	Listen                   ListenAddress  `json:"listen"`
-	ListenPort               uint16         `json:"listen_port"`
-	TCPFastOpen              bool           `json:"tcp_fast_open,omitempty"`
-	UDPTimeout               int64          `json:"udp_timeout,omitempty"`
+type InboundOptions struct {
 	SniffEnabled             bool           `json:"sniff,omitempty"`
 	SniffOverrideDestination bool           `json:"sniff_override_destination,omitempty"`
 	DomainStrategy           DomainStrategy `json:"domain_strategy,omitempty"`
 }
 
+type ListenOptions struct {
+	Listen      ListenAddress `json:"listen"`
+	ListenPort  uint16        `json:"listen_port"`
+	TCPFastOpen bool          `json:"tcp_fast_open,omitempty"`
+	UDPTimeout  int64         `json:"udp_timeout,omitempty"`
+	InboundOptions
+}
+
 type SimpleInboundOptions struct {
 	ListenOptions
 	Users []auth.User `json:"users,omitempty"`
@@ -144,4 +148,6 @@ type TunInboundOptions struct {
 	MTU           uint32       `json:"mtu,omitempty"`
 	Inet4Address  ListenPrefix `json:"inet4_address"`
 	Inet6Address  ListenPrefix `json:"inet6_address"`
+	AutoRoute     bool         `json:"auto_route"`
+	InboundOptions
 }

+ 5 - 4
option/route.go

@@ -9,10 +9,11 @@ import (
 )
 
 type RouteOptions struct {
-	GeoIP   *GeoIPOptions   `json:"geoip,omitempty"`
-	Geosite *GeositeOptions `json:"geosite,omitempty"`
-	Rules   []Rule          `json:"rules,omitempty"`
-	Final   string          `json:"final,omitempty"`
+	GeoIP               *GeoIPOptions   `json:"geoip,omitempty"`
+	Geosite             *GeositeOptions `json:"geosite,omitempty"`
+	Rules               []Rule          `json:"rules,omitempty"`
+	Final               string          `json:"final,omitempty"`
+	AutoDetectInterface bool            `json:"auto_detect_interface,omitempty"`
 }
 
 func (o RouteOptions) Equals(other RouteOptions) bool {

+ 7 - 1
outbound/direct.go

@@ -9,6 +9,7 @@ import (
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing/common/bufio"
 	M "github.com/sagernet/sing/common/metadata"
 	N "github.com/sagernet/sing/common/network"
 )
@@ -77,7 +78,12 @@ func (h *Direct) ListenPacket(ctx context.Context, destination M.Socksaddr) (net
 }
 
 func (h *Direct) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
-	return NewConnection(ctx, h, conn, metadata)
+	ctx = adapter.WithContext(ctx, &metadata)
+	outConn, err := h.DialContext(ctx, C.NetworkTCP, metadata.Destination)
+	if err != nil {
+		return err
+	}
+	return bufio.CopyConn(ctx, conn, outConn)
 }
 
 func (h *Direct) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {

+ 41 - 3
route/router.go

@@ -16,6 +16,7 @@ import (
 	"github.com/sagernet/sing-box/common/dialer"
 	"github.com/sagernet/sing-box/common/geoip"
 	"github.com/sagernet/sing-box/common/geosite"
+	"github.com/sagernet/sing-box/common/iffmonitor"
 	"github.com/sagernet/sing-box/common/sniff"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/dns"
@@ -62,6 +63,9 @@ type Router struct {
 	defaultTransport adapter.DNSTransport
 	transports       []adapter.DNSTransport
 	transportMap     map[string]adapter.DNSTransport
+
+	autoDetectInterface bool
+	interfaceMonitor    iffmonitor.InterfaceMonitor
 }
 
 func NewRouter(ctx context.Context, logger log.Logger, options option.RouteOptions, dnsOptions option.DNSOptions) (*Router, error) {
@@ -80,6 +84,7 @@ func NewRouter(ctx context.Context, logger log.Logger, options option.RouteOptio
 		defaultDetour:         options.Final,
 		dnsClient:             dns.NewClient(dnsOptions.DNSClientOptions),
 		defaultDomainStrategy: C.DomainStrategy(dnsOptions.Strategy),
+		autoDetectInterface:   options.AutoDetectInterface,
 	}
 	for i, ruleOptions := range options.Rules {
 		routeRule, err := NewRule(router, logger, ruleOptions)
@@ -181,6 +186,14 @@ func NewRouter(ctx context.Context, logger log.Logger, options option.RouteOptio
 	router.defaultTransport = defaultTransport
 	router.transports = transports
 	router.transportMap = transportMap
+
+	if options.AutoDetectInterface {
+		monitor, err := iffmonitor.New(router.logger)
+		if err != nil {
+			return nil, E.Cause(err, "create default interface monitor")
+		}
+		router.interfaceMonitor = monitor
+	}
 	return router, nil
 }
 
@@ -303,6 +316,12 @@ func (r *Router) Start() error {
 		r.geositeCache = nil
 		r.geositeReader = nil
 	}
+	if r.interfaceMonitor != nil {
+		err := r.interfaceMonitor.Start()
+		if err != nil {
+			return err
+		}
+	}
 	return nil
 }
 
@@ -321,6 +340,7 @@ func (r *Router) Close() error {
 	}
 	return common.Close(
 		common.PtrOrNil(r.geoIPReader),
+		r.interfaceMonitor,
 	)
 }
 
@@ -399,7 +419,7 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad
 }
 
 func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
-	if metadata.SniffEnabled {
+	if metadata.SniffEnabled && metadata.Destination.Port == 443 {
 		_buffer := buf.StackNewPacket()
 		defer common.KeepAlive(_buffer)
 		buffer := common.Dup(_buffer)
@@ -417,9 +437,9 @@ func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, m
 				metadata.Destination.Fqdn = metadata.Domain
 			}
 			if metadata.Domain != "" {
-				r.logger.WithContext(ctx).Info("sniffed protocol: ", metadata.Protocol, ", domain: ", metadata.Domain)
+				r.logger.WithContext(ctx).Info("sniffed packet protocol: ", metadata.Protocol, ", domain: ", metadata.Domain)
 			} else {
-				r.logger.WithContext(ctx).Info("sniffed protocol: ", metadata.Protocol)
+				r.logger.WithContext(ctx).Info("sniffed packet protocol: ", metadata.Protocol)
 			}
 		}
 		conn = bufio.NewCachedPacketConn(conn, buffer, originDestination)
@@ -485,6 +505,24 @@ func (r *Router) matchDNS(ctx context.Context) adapter.DNSTransport {
 	return r.defaultTransport
 }
 
+func (r *Router) AutoDetectInterface() bool {
+	return r.autoDetectInterface
+}
+
+func (r *Router) DefaultInterfaceName() string {
+	if r.interfaceMonitor == nil {
+		return ""
+	}
+	return r.interfaceMonitor.DefaultInterfaceName()
+}
+
+func (r *Router) DefaultInterfaceIndex() int {
+	if r.interfaceMonitor == nil {
+		return 0
+	}
+	return r.interfaceMonitor.DefaultInterfaceIndex()
+}
+
 func hasGeoRule(rules []option.Rule, cond func(rule option.DefaultRule) bool) bool {
 	for _, rule := range rules {
 		switch rule.Type {