From c5b596c293fdceddc47e4ba6870d058fcbcedc9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=8B=E9=87=8E=E3=81=8B=E3=81=88=E3=81=A7?= Date: Tue, 27 Aug 2024 09:49:51 +0800 Subject: [PATCH] feat: add MPTCP support (#601) --- cmd/run.go | 4 ++-- common/utils.go | 5 +++-- component/outbound/dialer/connectivity_check.go | 10 ++++++---- config/config.go | 1 + config/desc.go | 1 + control/control_plane.go | 11 +++++++---- control/dns_control.go | 5 +++-- control/tcp.go | 2 +- control/udp.go | 2 +- example.dae | 4 ++++ go.mod | 7 +++---- go.sum | 15 +++------------ 12 files changed, 35 insertions(+), 32 deletions(-) diff --git a/cmd/run.go b/cmd/run.go index 7e808c6a94..f7dde16e62 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -331,7 +331,7 @@ func newControlPlane(log *logrus.Logger, bpf interface{}, dnsCache map[string]*c Transport: &http.Transport{ DialContext: func(ctx context.Context, network, addr string) (c net.Conn, err error) { cd := netproxy.ContextDialerConverter{Dialer: direct.SymmetricDirect} - conn, err := cd.DialContext(ctx, common.MagicNetwork("tcp", conf.Global.SoMarkFromDae), addr) + conn, err := cd.DialContext(ctx, common.MagicNetwork("tcp", conf.Global.SoMarkFromDae, conf.Global.Mptcp), addr) if err != nil { return nil, err } @@ -373,7 +373,7 @@ func newControlPlane(log *logrus.Logger, bpf interface{}, dnsCache map[string]*c Transport: &http.Transport{ DialContext: func(ctx context.Context, network, addr string) (c net.Conn, err error) { cd := netproxy.ContextDialerConverter{Dialer: direct.SymmetricDirect} - conn, err := cd.DialContext(ctx, common.MagicNetwork("tcp", conf.Global.SoMarkFromDae), addr) + conn, err := cd.DialContext(ctx, common.MagicNetwork("tcp", conf.Global.SoMarkFromDae, conf.Global.Mptcp), addr) if err != nil { return nil, err } diff --git a/common/utils.go b/common/utils.go index 350195b675..0d9ce794f2 100644 --- a/common/utils.go +++ b/common/utils.go @@ -469,13 +469,14 @@ nextLink: return Deduplicate(defaultIfs), nil } -func MagicNetwork(network string, mark uint32) string { - if mark == 0 { +func MagicNetwork(network string, mark uint32, mptcp bool) string { + if mark == 0 && !mptcp { return network } else { return netproxy.MagicNetwork{ Network: network, Mark: mark, + Mptcp: mptcp, }.Encode() } } diff --git a/component/outbound/dialer/connectivity_check.go b/component/outbound/dialer/connectivity_check.go index 3cb8f1e0bb..9e478a6666 100644 --- a/component/outbound/dialer/connectivity_check.go +++ b/component/outbound/dialer/connectivity_check.go @@ -282,8 +282,10 @@ func (d *Dialer) ActivateCheck() { func (d *Dialer) aliveBackground() { cycle := d.CheckInterval var tcpSomark uint32 + var mptcp bool if network, err := netproxy.ParseMagicNetwork(d.TcpCheckOptionRaw.ResolverNetwork); err == nil { tcpSomark = network.Mark + mptcp = network.Mptcp } tcp4CheckOpt := &CheckOption{ networkType: &NetworkType{ @@ -304,7 +306,7 @@ func (d *Dialer) aliveBackground() { }).Debugln("Skip check due to no DNS record.") return false, nil } - return d.HttpCheck(ctx, opt.Url, opt.Ip4, opt.Method, tcpSomark) + return d.HttpCheck(ctx, opt.Url, opt.Ip4, opt.Method, tcpSomark, mptcp) }, } tcp6CheckOpt := &CheckOption{ @@ -326,7 +328,7 @@ func (d *Dialer) aliveBackground() { }).Debugln("Skip check due to no DNS record.") return false, nil } - return d.HttpCheck(ctx, opt.Url, opt.Ip6, opt.Method, tcpSomark) + return d.HttpCheck(ctx, opt.Url, opt.Ip6, opt.Method, tcpSomark, mptcp) }, } tcpNetwork := netproxy.MagicNetwork{ @@ -580,7 +582,7 @@ func (d *Dialer) Check(opts *CheckOption) (ok bool, err error) { return ok, err } -func (d *Dialer) HttpCheck(ctx context.Context, u *netutils.URL, ip netip.Addr, method string, soMark uint32) (ok bool, err error) { +func (d *Dialer) HttpCheck(ctx context.Context, u *netutils.URL, ip netip.Addr, method string, soMark uint32, mptcp bool) (ok bool, err error) { // HTTP(S) check. if method == "" { method = http.MethodGet @@ -590,7 +592,7 @@ func (d *Dialer) HttpCheck(ctx context.Context, u *netutils.URL, ip netip.Addr, Transport: &http.Transport{ DialContext: func(ctx context.Context, network, addr string) (c net.Conn, err error) { // Force to dial "ip". - conn, err := cd.DialContext(ctx, common.MagicNetwork("tcp", soMark), net.JoinHostPort(ip.String(), u.Port())) + conn, err := cd.DialContext(ctx, common.MagicNetwork("tcp", soMark, mptcp), net.JoinHostPort(ip.String(), u.Port())) if err != nil { return nil, err } diff --git a/config/config.go b/config/config.go index 0c21515173..873278d701 100644 --- a/config/config.go +++ b/config/config.go @@ -42,6 +42,7 @@ type Global struct { TlsImplementation string `mapstructure:"tls_implementation" default:"tls"` UtlsImitate string `mapstructure:"utls_imitate" default:"chrome_auto"` PprofPort uint16 `mapstructure:"pprof_port" default:"0"` + Mptcp bool `mapstructure:"mptcp" default:"false"` } type Utls struct { diff --git a/config/desc.go b/config/desc.go index fad148a4e7..2440527cbc 100644 --- a/config/desc.go +++ b/config/desc.go @@ -57,6 +57,7 @@ var GlobalDesc = Desc{ "sniffing_timeout": "Timeout to waiting for first data sending for sniffing. It is always 0 if dial_mode is ip. Set it higher is useful in high latency LAN network.", "tls_implementation": "TLS implementation. \"tls\" is to use Go's crypto/tls. \"utls\" is to use uTLS, which can imitate browser's Client Hello.", "utls_imitate": "The Client Hello ID for uTLS to imitate. This takes effect only if tls_implementation is utls. See more: https://github.com/daeuniverse/dae/blob/331fa23c16/component/outbound/transport/tls/utls.go#L17", + "mptcp": "Enable Multipath TCP. If is true, dae will try to use MPTCP to connect all nodes, but it will only take effects when the node supports MPTCP. It can use for load balance and failover to multiple interfaces and IPs.", } var DnsDesc = Desc{ diff --git a/control/control_plane.go b/control/control_plane.go index 0e640f0904..5cc190853c 100644 --- a/control/control_plane.go +++ b/control/control_plane.go @@ -75,6 +75,7 @@ type ControlPlane struct { sniffingTimeout time.Duration tproxyPortProtect bool soMarkFromDae uint32 + mptcp bool } func NewControlPlane( @@ -261,8 +262,8 @@ func NewControlPlane( TlsImplementation: global.TlsImplementation, UtlsImitate: global.UtlsImitate}, Log: log, - TcpCheckOptionRaw: dialer.TcpCheckOptionRaw{Raw: global.TcpCheckUrl, Log: log, ResolverNetwork: common.MagicNetwork("udp", global.SoMarkFromDae), Method: global.TcpCheckHttpMethod}, - CheckDnsOptionRaw: dialer.CheckDnsOptionRaw{Raw: global.UdpCheckDns, ResolverNetwork: common.MagicNetwork("udp", global.SoMarkFromDae), Somark: global.SoMarkFromDae}, + TcpCheckOptionRaw: dialer.TcpCheckOptionRaw{Raw: global.TcpCheckUrl, Log: log, ResolverNetwork: common.MagicNetwork("udp", global.SoMarkFromDae, global.Mptcp), Method: global.TcpCheckHttpMethod}, + CheckDnsOptionRaw: dialer.CheckDnsOptionRaw{Raw: global.UdpCheckDns, ResolverNetwork: common.MagicNetwork("udp", global.SoMarkFromDae, global.Mptcp), Somark: global.SoMarkFromDae}, CheckInterval: global.CheckInterval, CheckTolerance: global.CheckTolerance, CheckDnsTcp: true, @@ -395,6 +396,7 @@ func NewControlPlane( sniffingTimeout: sniffingTimeout, tproxyPortProtect: global.TproxyPortProtect, soMarkFromDae: global.SoMarkFromDae, + mptcp: global.Mptcp, } defer func() { if err != nil { @@ -407,7 +409,7 @@ func NewControlPlane( Logger: log, LocationFinder: locationFinder, UpstreamReadyCallback: plane.dnsUpstreamReadyCallback, - UpstreamResolverNetwork: common.MagicNetwork("udp", global.SoMarkFromDae), + UpstreamResolverNetwork: common.MagicNetwork("udp", global.SoMarkFromDae, global.Mptcp), }) if err != nil { return nil, err @@ -620,7 +622,7 @@ func (c *ControlPlane) ChooseDialTarget(outbound consts.OutboundIndex, dst netip // TODO: use DNS controller and re-route by control plane. systemDns, err := netutils.SystemDns() if err == nil { - if ip46, err := netutils.ResolveIp46(ctx, direct.SymmetricDirect, systemDns, domain, common.MagicNetwork("udp", c.soMarkFromDae), true); err == nil && (ip46.Ip4.IsValid() || ip46.Ip6.IsValid()) { + if ip46, err := netutils.ResolveIp46(ctx, direct.SymmetricDirect, systemDns, domain, common.MagicNetwork("udp", c.soMarkFromDae, c.mptcp), true); err == nil && (ip46.Ip4.IsValid() || ip46.Ip6.IsValid()) { // Has A/AAAA records. It is a real domain. dialMode = consts.DialMode_Domain // Add it to real-domain set. @@ -938,6 +940,7 @@ func (c *ControlPlane) chooseBestDnsDialer( bestOutbound: bestOutbound, bestTarget: bestTarget, mark: dialMark, + mptcp: c.mptcp, }, nil } diff --git a/control/dns_control.go b/control/dns_control.go index 6ff705452c..8b41c309d2 100644 --- a/control/dns_control.go +++ b/control/dns_control.go @@ -344,6 +344,7 @@ type dialArgument struct { bestOutbound *outbound.DialerGroup bestTarget netip.AddrPort mark uint32 + mptcp bool } func (c *DnsController) Handle_(dnsMessage *dnsmessage.Msg, req *udpRequest) (err error) { @@ -570,7 +571,7 @@ func (c *DnsController) dialSend(invokingDepth int, req *udpRequest, data []byte // TODO: connection pool. conn, err = bestContextDialer.DialContext( ctxDial, - common.MagicNetwork("udp", dialArgument.mark), + common.MagicNetwork("udp", dialArgument.mark, dialArgument.mptcp), dialArgument.bestTarget.String(), ) if err != nil { @@ -633,7 +634,7 @@ func (c *DnsController) dialSend(invokingDepth int, req *udpRequest, data []byte case consts.L4ProtoStr_TCP: // We can block here because we are in a coroutine. - conn, err = bestContextDialer.DialContext(ctxDial, common.MagicNetwork("tcp", dialArgument.mark), dialArgument.bestTarget.String()) + conn, err = bestContextDialer.DialContext(ctxDial, common.MagicNetwork("tcp", dialArgument.mark, dialArgument.mptcp), dialArgument.bestTarget.String()) if err != nil { return fmt.Errorf("failed to dial proxy to tcp: %w", err) } diff --git a/control/tcp.go b/control/tcp.go index dd99cd052f..9796a8e4bd 100644 --- a/control/tcp.go +++ b/control/tcp.go @@ -165,7 +165,7 @@ func (c *ControlPlane) RouteDialTcp(p *RouteDialParam) (conn netproxy.Conn, err cd := netproxy.ContextDialerConverter{ Dialer: d, } - return cd.DialContext(ctx, common.MagicNetwork("tcp", routingResult.Mark), dialTarget) + return cd.DialContext(ctx, common.MagicNetwork("tcp", routingResult.Mark, c.mptcp), dialTarget) } type WriteCloser interface { diff --git a/control/udp.go b/control/udp.go index 638ce93a3d..5f3b459685 100644 --- a/control/udp.go +++ b/control/udp.go @@ -250,7 +250,7 @@ getNew: Target: dialTarget, Dialer: dialerForNew, Outbound: outbound, - Network: common.MagicNetwork("udp", routingResult.Mark), + Network: common.MagicNetwork("udp", routingResult.Mark, c.mptcp), SniffedDomain: domain, }, nil }, diff --git a/example.dae b/example.dae index e2ceeb12af..e1814e2b86 100644 --- a/example.dae +++ b/example.dae @@ -96,6 +96,10 @@ global { # The Client Hello ID for uTLS to imitate. This takes effect only if tls_implementation is utls. # See more: https://github.com/daeuniverse/dae/blob/331fa23c16/component/outbound/transport/tls/utls.go#L17 utls_imitate: chrome_auto + + # Multipath TCP (MPTCP) support. If is true, dae will try to use MPTCP to connect all nodes, but it will only take + # effects when the node supports MPTCP. It can use for load balance and failover to multiple interfaces and IPs. + mptcp: false } # Subscriptions defined here will be resolved as nodes and merged as a part of the global node pool. diff --git a/go.mod b/go.mod index 7e393173d9..405e731c05 100644 --- a/go.mod +++ b/go.mod @@ -8,13 +8,15 @@ require ( github.com/bits-and-blooms/bloom/v3 v3.5.0 github.com/cilium/ebpf v0.12.3 github.com/daeuniverse/dae-config-dist/go/dae_config v0.0.0-20230604120805-1c27619b592d - github.com/daeuniverse/outbound v0.0.0-20240628165628-7c0c217530ea + github.com/daeuniverse/outbound v0.0.0-20240807173909-1bac5b52e542 github.com/fsnotify/fsnotify v1.7.0 github.com/json-iterator/go v1.1.12 + github.com/mholt/archiver/v3 v3.5.1 github.com/miekg/dns v1.1.55 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 github.com/okzk/sdnotify v0.0.0-20180710141335-d9becc38acbd github.com/safchain/ethtool v0.3.0 + github.com/shirou/gopsutil/v4 v4.24.5 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.7.0 github.com/v2rayA/ahocorasick-domain v0.0.0-20231231085011-99ceb8ef3208 @@ -43,14 +45,11 @@ require ( github.com/gorilla/websocket v1.5.0 // indirect github.com/klauspost/compress v1.17.4 // indirect github.com/klauspost/pgzip v1.2.5 // indirect - github.com/mholt/archiver/v3 v3.5.1 // indirect github.com/nwaples/rardecode v1.1.0 // indirect github.com/onsi/ginkgo/v2 v2.11.0 // indirect github.com/pierrec/lz4/v4 v4.1.2 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/quic-go/qpack v0.4.0 // indirect - github.com/shirou/gopsutil/v4 v4.24.5 // indirect - github.com/stretchr/testify v1.9.0 // indirect github.com/ulikunitz/xz v0.5.9 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect diff --git a/go.sum b/go.sum index d35af8738d..db7bec1642 100644 --- a/go.sum +++ b/go.sum @@ -23,8 +23,8 @@ github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBS github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/daeuniverse/dae-config-dist/go/dae_config v0.0.0-20230604120805-1c27619b592d h1:hnC39MjR7xt5kZjrKlef7DXKFDkiX8MIcDXYC/6Jf9Q= github.com/daeuniverse/dae-config-dist/go/dae_config v0.0.0-20230604120805-1c27619b592d/go.mod h1:VGWGgv7pCP5WGyHGUyb9+nq/gW0yBm+i/GfCNATOJ1M= -github.com/daeuniverse/outbound v0.0.0-20240628165628-7c0c217530ea h1:mQwAcoKHR/AVsajoEpP/NSYL8nBTuP+kw7l2+xWM4xE= -github.com/daeuniverse/outbound v0.0.0-20240628165628-7c0c217530ea/go.mod h1:z0vJ5ZlLErX8WTruVeOuGr+1KOhSFcaPzEhZMAYfPdA= +github.com/daeuniverse/outbound v0.0.0-20240807173909-1bac5b52e542 h1:hL3E0XKvBvVmjNJrRDsI7SnmZmiery12f6/7b+kBILw= +github.com/daeuniverse/outbound v0.0.0-20240807173909-1bac5b52e542/go.mod h1:z0vJ5ZlLErX8WTruVeOuGr+1KOhSFcaPzEhZMAYfPdA= github.com/daeuniverse/quic-go v0.0.0-20240413031024-943f218e0810 h1:YtEYouFaNrg9sV9vf3UabvKShKn6sD0QaCdOxCwaF3g= github.com/daeuniverse/quic-go v0.0.0-20240413031024-943f218e0810/go.mod h1:61o2uZUGLrlv1i+oO2rx9sVX0vbf8cHzdSHt7h6lMnM= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -77,9 +77,8 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20230705174524-200ffdc848b8 h1:n6vlPhxsA+BW/XsS5+uqi7GyzaLa5MH7qlSLBZtRdiA= github.com/google/pprof v0.0.0-20230705174524-200ffdc848b8/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA= @@ -166,17 +165,11 @@ github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRM github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/twmb/murmur3 v1.1.6 h1:mqrRot1BRxm+Yct+vavLMou2/iJt0tNVTTC0QoIjaZg= @@ -244,8 +237,6 @@ golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=