diff --git a/adapter/outbound/base.go b/adapter/outbound/base.go index 15eedd1ae1..e9119415ec 100644 --- a/adapter/outbound/base.go +++ b/adapter/outbound/base.go @@ -16,6 +16,7 @@ type Base struct { iface string tp C.AdapterType udp bool + rmark int } // Name implements C.ProxyAdapter @@ -66,19 +67,25 @@ func (b *Base) DialOptions(opts ...dialer.Option) []dialer.Option { opts = append(opts, dialer.WithInterface(b.iface)) } + if b.rmark != 0 { + opts = append(opts, dialer.WithRoutingMark(b.rmark)) + } + return opts } type BasicOption struct { - Interface string `proxy:"interface-name,omitempty" group:"interface-name,omitempty"` + Interface string `proxy:"interface-name,omitempty" group:"interface-name,omitempty"` + RoutingMark int `proxy:"routing-mark,omitempty" group:"routing-mark,omitempty"` } type BaseOption struct { - Name string - Addr string - Type C.AdapterType - UDP bool - Interface string + Name string + Addr string + Type C.AdapterType + UDP bool + Interface string + RoutingMark int } func NewBase(opt BaseOption) *Base { @@ -88,6 +95,7 @@ func NewBase(opt BaseOption) *Base { tp: opt.Type, udp: opt.UDP, iface: opt.Interface, + rmark: opt.RoutingMark, } } diff --git a/adapter/outbound/socks5.go b/adapter/outbound/socks5.go index 874da9f145..d81c76140e 100644 --- a/adapter/outbound/socks5.go +++ b/adapter/outbound/socks5.go @@ -24,7 +24,7 @@ type Socks5 struct { } type Socks5Option struct { - *BaseOption + BasicOption Name string `proxy:"name"` Server string `proxy:"server"` Port int `proxy:"port"` diff --git a/adapter/outboundgroup/fallback.go b/adapter/outboundgroup/fallback.go index 2f9f0887fe..b08af487fa 100644 --- a/adapter/outboundgroup/fallback.go +++ b/adapter/outboundgroup/fallback.go @@ -94,9 +94,10 @@ func (f *Fallback) findAliveProxy(touch bool) C.Proxy { func NewFallback(option *GroupCommonOption, providers []provider.ProxyProvider) *Fallback { return &Fallback{ Base: outbound.NewBase(outbound.BaseOption{ - Name: option.Name, - Type: C.Fallback, - Interface: option.Interface, + Name: option.Name, + Type: C.Fallback, + Interface: option.Interface, + RoutingMark: option.RoutingMark, }), single: singledo.NewSingle(defaultGetProxiesDuration), providers: providers, diff --git a/adapter/outboundgroup/loadbalance.go b/adapter/outboundgroup/loadbalance.go index df1c2ba49b..26c8052ad5 100644 --- a/adapter/outboundgroup/loadbalance.go +++ b/adapter/outboundgroup/loadbalance.go @@ -171,9 +171,10 @@ func NewLoadBalance(option *GroupCommonOption, providers []provider.ProxyProvide } return &LoadBalance{ Base: outbound.NewBase(outbound.BaseOption{ - Name: option.Name, - Type: C.LoadBalance, - Interface: option.Interface, + Name: option.Name, + Type: C.LoadBalance, + Interface: option.Interface, + RoutingMark: option.RoutingMark, }), single: singledo.NewSingle(defaultGetProxiesDuration), providers: providers, diff --git a/adapter/outboundgroup/relay.go b/adapter/outboundgroup/relay.go index 4ec4ffacfd..393a69bb92 100644 --- a/adapter/outboundgroup/relay.go +++ b/adapter/outboundgroup/relay.go @@ -103,9 +103,10 @@ func (r *Relay) proxies(metadata *C.Metadata, touch bool) []C.Proxy { func NewRelay(option *GroupCommonOption, providers []provider.ProxyProvider) *Relay { return &Relay{ Base: outbound.NewBase(outbound.BaseOption{ - Name: option.Name, - Type: C.Relay, - Interface: option.Interface, + Name: option.Name, + Type: C.Relay, + Interface: option.Interface, + RoutingMark: option.RoutingMark, }), single: singledo.NewSingle(defaultGetProxiesDuration), providers: providers, diff --git a/adapter/outboundgroup/selector.go b/adapter/outboundgroup/selector.go index 4ec67cd1d9..47e4b87d08 100644 --- a/adapter/outboundgroup/selector.go +++ b/adapter/outboundgroup/selector.go @@ -101,9 +101,10 @@ func NewSelector(option *GroupCommonOption, providers []provider.ProxyProvider) selected := providers[0].Proxies()[0].Name() return &Selector{ Base: outbound.NewBase(outbound.BaseOption{ - Name: option.Name, - Type: C.Selector, - Interface: option.Interface, + Name: option.Name, + Type: C.Selector, + Interface: option.Interface, + RoutingMark: option.RoutingMark, }), single: singledo.NewSingle(defaultGetProxiesDuration), providers: providers, diff --git a/adapter/outboundgroup/urltest.go b/adapter/outboundgroup/urltest.go index 6866db0df9..47144c0468 100644 --- a/adapter/outboundgroup/urltest.go +++ b/adapter/outboundgroup/urltest.go @@ -137,9 +137,10 @@ func parseURLTestOption(config map[string]interface{}) []urlTestOption { func NewURLTest(option *GroupCommonOption, providers []provider.ProxyProvider, options ...urlTestOption) *URLTest { urlTest := &URLTest{ Base: outbound.NewBase(outbound.BaseOption{ - Name: option.Name, - Type: C.URLTest, - Interface: option.Interface, + Name: option.Name, + Type: C.URLTest, + Interface: option.Interface, + RoutingMark: option.RoutingMark, }), single: singledo.NewSingle(defaultGetProxiesDuration), fastSingle: singledo.NewSingle(time.Second * 10), diff --git a/component/dialer/dialer.go b/component/dialer/dialer.go index 87b62dd65a..c84fcaa463 100644 --- a/component/dialer/dialer.go +++ b/component/dialer/dialer.go @@ -59,6 +59,9 @@ func ListenPacket(ctx context.Context, network, address string, options ...Optio if cfg.addrReuse { addrReuseToListenConfig(lc) } + if cfg.routingMark != 0 { + bindMarkToListenConfig(cfg.routingMark, lc, network, address) + } return lc.ListenPacket(ctx, network, address) } @@ -82,6 +85,9 @@ func dialContext(ctx context.Context, network string, destination net.IP, port s return nil, err } } + if opt.routingMark != 0 { + bindMarkToDialer(opt.routingMark, dialer, network, destination) + } return dialer.DialContext(ctx, network, net.JoinHostPort(destination.String(), port)) } diff --git a/component/dialer/mark_linux.go b/component/dialer/mark_linux.go new file mode 100644 index 0000000000..79a2185e40 --- /dev/null +++ b/component/dialer/mark_linux.go @@ -0,0 +1,44 @@ +//go:build linux +// +build linux + +package dialer + +import ( + "net" + "syscall" +) + +func bindMarkToDialer(mark int, dialer *net.Dialer, _ string, _ net.IP) { + dialer.Control = bindMarkToControl(mark, dialer.Control) +} + +func bindMarkToListenConfig(mark int, lc *net.ListenConfig, _, address string) { + lc.Control = bindMarkToControl(mark, lc.Control) +} + +func bindMarkToControl(mark int, chain controlFn) controlFn { + return func(network, address string, c syscall.RawConn) (err error) { + defer func() { + if err == nil && chain != nil { + err = chain(network, address, c) + } + }() + + ipStr, _, err := net.SplitHostPort(address) + if err == nil { + ip := net.ParseIP(ipStr) + if ip != nil && !ip.IsGlobalUnicast() { + return + } + } + + return c.Control(func(fd uintptr) { + switch network { + case "tcp4", "udp4": + syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, mark) + case "tcp6", "udp6": + syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, mark) + } + }) + } +} diff --git a/component/dialer/mark_nonlinux.go b/component/dialer/mark_nonlinux.go new file mode 100644 index 0000000000..5d9befb1a0 --- /dev/null +++ b/component/dialer/mark_nonlinux.go @@ -0,0 +1,27 @@ +//go:build !linux +// +build !linux + +package dialer + +import ( + "net" + "sync" + + "github.com/Dreamacro/clash/log" +) + +var printMarkWarnOnce sync.Once + +func printMarkWarn() { + printMarkWarnOnce.Do(func() { + log.Warnln("Routing mark on socket is not supported on current platform") + }) +} + +func bindMarkToDialer(mark int, dialer *net.Dialer, _ string, _ net.IP) { + printMarkWarn() +} + +func bindMarkToListenConfig(mark int, lc *net.ListenConfig, _, address string) { + printMarkWarn() +} diff --git a/component/dialer/options.go b/component/dialer/options.go index 96d9eb751f..b3cca81051 100644 --- a/component/dialer/options.go +++ b/component/dialer/options.go @@ -10,6 +10,7 @@ var ( type option struct { interfaceName string addrReuse bool + routingMark int } type Option func(opt *option) @@ -25,3 +26,9 @@ func WithAddrReuse(reuse bool) Option { opt.addrReuse = reuse } } + +func WithRoutingMark(mark int) Option { + return func(opt *option) { + opt.routingMark = mark + } +}