From 16c964b3e143f6b2c3384d6f02e85e635984a4fc Mon Sep 17 00:00:00 2001 From: Haruue Date: Fri, 22 Nov 2024 13:47:44 +0900 Subject: [PATCH 1/2] feat(server): tcp fast open on direct outbounds --- app/cmd/server.go | 21 +-- app/cmd/server_test.go | 1 + app/cmd/server_test.yaml | 1 + app/go.mod | 4 +- app/go.sum | 8 +- extras/go.mod | 4 +- extras/go.sum | 8 +- extras/outbounds/fastopen.go | 230 +++++++++++++++++++++++++++ extras/outbounds/ob_direct.go | 113 +++++++++---- extras/outbounds/ob_direct_linux.go | 38 ++--- extras/outbounds/ob_direct_others.go | 7 +- 11 files changed, 363 insertions(+), 72 deletions(-) create mode 100644 extras/outbounds/fastopen.go diff --git a/app/cmd/server.go b/app/cmd/server.go index a4b8470940..1384dd8a10 100644 --- a/app/cmd/server.go +++ b/app/cmd/server.go @@ -204,6 +204,7 @@ type serverConfigOutboundDirect struct { BindIPv4 string `mapstructure:"bindIPv4"` BindIPv6 string `mapstructure:"bindIPv6"` BindDevice string `mapstructure:"bindDevice"` + FastOpen bool `mapstructure:"fastOpen"` } type serverConfigOutboundSOCKS5 struct { @@ -518,18 +519,18 @@ func (c *serverConfig) fillQUICConfig(hyConfig *server.Config) error { } func serverConfigOutboundDirectToOutbound(c serverConfigOutboundDirect) (outbounds.PluggableOutbound, error) { - var mode outbounds.DirectOutboundMode + opts := outbounds.DirectOutboundOptions{} switch strings.ToLower(c.Mode) { case "", "auto": - mode = outbounds.DirectOutboundModeAuto + opts.Mode = outbounds.DirectOutboundModeAuto case "64": - mode = outbounds.DirectOutboundMode64 + opts.Mode = outbounds.DirectOutboundMode64 case "46": - mode = outbounds.DirectOutboundMode46 + opts.Mode = outbounds.DirectOutboundMode46 case "6": - mode = outbounds.DirectOutboundMode6 + opts.Mode = outbounds.DirectOutboundMode6 case "4": - mode = outbounds.DirectOutboundMode4 + opts.Mode = outbounds.DirectOutboundMode4 default: return nil, configError{Field: "outbounds.direct.mode", Err: errors.New("unsupported mode")} } @@ -546,12 +547,14 @@ func serverConfigOutboundDirectToOutbound(c serverConfigOutboundDirect) (outboun if len(c.BindIPv6) > 0 && ip6 == nil { return nil, configError{Field: "outbounds.direct.bindIPv6", Err: errors.New("invalid IPv6 address")} } - return outbounds.NewDirectOutboundBindToIPs(mode, ip4, ip6) + opts.BindIP4 = ip4 + opts.BindIP6 = ip6 } if bindDevice { - return outbounds.NewDirectOutboundBindToDevice(mode, c.BindDevice) + opts.DeviceName = c.BindDevice } - return outbounds.NewDirectOutboundSimple(mode), nil + opts.FastOpen = c.FastOpen + return outbounds.NewDirectOutboundWithOptions(opts) } func serverConfigOutboundSOCKS5ToOutbound(c serverConfigOutboundSOCKS5) (outbounds.PluggableOutbound, error) { diff --git a/app/cmd/server_test.go b/app/cmd/server_test.go index f35edfbd81..bcf61c3526 100644 --- a/app/cmd/server_test.go +++ b/app/cmd/server_test.go @@ -138,6 +138,7 @@ func TestServerConfig(t *testing.T) { BindIPv4: "2.4.6.8", BindIPv6: "0:0:0:0:0:ffff:0204:0608", BindDevice: "eth233", + FastOpen: true, }, }, { diff --git a/app/cmd/server_test.yaml b/app/cmd/server_test.yaml index b7d1a3e7b9..dda6d984ed 100644 --- a/app/cmd/server_test.yaml +++ b/app/cmd/server_test.yaml @@ -108,6 +108,7 @@ outbounds: bindIPv4: 2.4.6.8 bindIPv6: 0:0:0:0:0:ffff:0204:0608 bindDevice: eth233 + fastOpen: true - name: badstuff type: socks5 socks5: diff --git a/app/go.mod b/app/go.mod index dab7e51776..2d36411f2c 100644 --- a/app/go.mod +++ b/app/go.mod @@ -25,7 +25,7 @@ require ( github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301 go.uber.org/zap v1.24.0 golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 - golang.org/x/sys v0.23.0 + golang.org/x/sys v0.25.0 ) require ( @@ -33,6 +33,8 @@ require ( github.com/apernet/quic-go v0.48.2-0.20241104191913-cb103fcecfe7 // indirect github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 // indirect github.com/cloudflare/circl v1.3.9 // indirect + github.com/database64128/netx-go v0.0.0-20240905055117-62795b8b054a // indirect + github.com/database64128/tfo-go/v2 v2.2.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect diff --git a/app/go.sum b/app/go.sum index dec519bcc1..1b30febb7f 100644 --- a/app/go.sum +++ b/app/go.sum @@ -63,6 +63,10 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/database64128/netx-go v0.0.0-20240905055117-62795b8b054a h1:t4SDi0pmNkryzKdM4QF3o5vqSP4GRjeZD/6j3nyxNP0= +github.com/database64128/netx-go v0.0.0-20240905055117-62795b8b054a/go.mod h1:7K2NQKbabB5mBl41vF6YayYl5g7YpDwc4dQ5iMpP3Lg= +github.com/database64128/tfo-go/v2 v2.2.2 h1:BxynF4qGF5ct3DpPLEG62uyJZ3LQhqaf0Ken+kyy7PM= +github.com/database64128/tfo-go/v2 v2.2.2/go.mod h1:2IW8jppdBwdVMjA08uEyMNnqiAHKUlqAA+J8NrsfktY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -463,8 +467,8 @@ golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= -golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= diff --git a/extras/go.mod b/extras/go.mod index 3da331af60..67ef38fecd 100644 --- a/extras/go.mod +++ b/extras/go.mod @@ -8,6 +8,7 @@ require ( github.com/apernet/hysteria/core/v2 v2.0.0-00010101000000-000000000000 github.com/apernet/quic-go v0.48.2-0.20241104191913-cb103fcecfe7 github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 + github.com/database64128/tfo-go/v2 v2.2.2 github.com/hashicorp/golang-lru/v2 v2.0.5 github.com/miekg/dns v1.1.59 github.com/refraction-networking/utls v1.6.6 @@ -21,6 +22,7 @@ require ( require ( github.com/andybalholm/brotli v1.1.0 // indirect github.com/cloudflare/circl v1.3.9 // indirect + github.com/database64128/netx-go v0.0.0-20240905055117-62795b8b054a // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect @@ -36,7 +38,7 @@ require ( golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect golang.org/x/mod v0.17.0 // indirect golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.23.0 // indirect + golang.org/x/sys v0.25.0 // indirect golang.org/x/text v0.17.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/extras/go.sum b/extras/go.sum index d07ba7cfce..98616ca728 100644 --- a/extras/go.sum +++ b/extras/go.sum @@ -10,6 +10,10 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/cloudflare/circl v1.3.9 h1:QFrlgFYf2Qpi8bSpVPK1HBvWpx16v/1TZivyo7pGuBE= github.com/cloudflare/circl v1.3.9/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/database64128/netx-go v0.0.0-20240905055117-62795b8b054a h1:t4SDi0pmNkryzKdM4QF3o5vqSP4GRjeZD/6j3nyxNP0= +github.com/database64128/netx-go v0.0.0-20240905055117-62795b8b054a/go.mod h1:7K2NQKbabB5mBl41vF6YayYl5g7YpDwc4dQ5iMpP3Lg= +github.com/database64128/tfo-go/v2 v2.2.2 h1:BxynF4qGF5ct3DpPLEG62uyJZ3LQhqaf0Ken+kyy7PM= +github.com/database64128/tfo-go/v2 v2.2.2/go.mod h1:2IW8jppdBwdVMjA08uEyMNnqiAHKUlqAA+J8NrsfktY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -92,8 +96,8 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= -golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= diff --git a/extras/outbounds/fastopen.go b/extras/outbounds/fastopen.go new file mode 100644 index 0000000000..a607500969 --- /dev/null +++ b/extras/outbounds/fastopen.go @@ -0,0 +1,230 @@ +package outbounds + +import ( + "net" + "sync" + "time" + + "github.com/database64128/tfo-go/v2" +) + +type fastOpenDialer struct { + dialer *tfo.Dialer +} + +func newFastOpenDialer(netDialer *net.Dialer) *fastOpenDialer { + return &fastOpenDialer{ + dialer: &tfo.Dialer{ + Dialer: *netDialer, + Fallback: true, + }, + } +} + +// Dial returns immediately without actually establishing a connection. +// The connection will be established by the first Write() call. +func (d *fastOpenDialer) Dial(network, address string) (net.Conn, error) { + return &fastOpenConn{ + dialer: d.dialer, + network: network, + address: address, + readyChan: make(chan struct{}), + }, nil +} + +type fastOpenConn struct { + dialer *tfo.Dialer + network string + address string + + conn net.Conn + connLock sync.RWMutex + readyChan chan struct{} + + // States before connection ready + deadline *time.Time + readDeadline *time.Time + writeDeadline *time.Time +} + +func (c *fastOpenConn) Read(b []byte) (n int, err error) { + c.connLock.RLock() + conn := c.conn + c.connLock.RUnlock() + + if conn != nil { + return conn.Read(b) + } + + // Wait until the connection is ready or closed + <-c.readyChan + + if c.conn == nil { + // This is equivalent to isClosedBeforeReady() == true + return 0, net.ErrClosed + } + + return c.conn.Read(b) +} + +func (c *fastOpenConn) Write(b []byte) (n int, err error) { + c.connLock.RLock() + conn := c.conn + c.connLock.RUnlock() + + if conn != nil { + return conn.Write(b) + } + + c.connLock.RLock() + closed := c.isClosedBeforeReady() + c.connLock.RUnlock() + + if closed { + return 0, net.ErrClosed + } + + c.connLock.Lock() + defer c.connLock.Unlock() + + if c.isClosedBeforeReady() { + // Closed by other goroutine + return 0, net.ErrClosed + } + + conn = c.conn + if conn != nil { + // Established by other goroutine + return conn.Write(b) + } + + conn, err = c.dialer.Dial(c.network, c.address, b) + if err != nil { + close(c.readyChan) + return 0, err + } + + // Apply pre-set states + if c.deadline != nil { + _ = conn.SetDeadline(*c.deadline) + } + if c.readDeadline != nil { + _ = conn.SetReadDeadline(*c.readDeadline) + } + if c.writeDeadline != nil { + _ = conn.SetWriteDeadline(*c.writeDeadline) + } + + c.conn = conn + close(c.readyChan) + return len(b), nil +} + +func (c *fastOpenConn) Close() error { + c.connLock.RLock() + defer c.connLock.RUnlock() + + if c.isClosedBeforeReady() { + return net.ErrClosed + } + + if c.conn != nil { + return c.conn.Close() + } + + close(c.readyChan) + return nil +} + +// isClosedBeforeReady returns true if the connection is closed before the real connection is established. +// This function should be called with connLock.RLock(). +func (c *fastOpenConn) isClosedBeforeReady() bool { + select { + case <-c.readyChan: + if c.conn == nil { + return true + } + default: + } + return false +} + +func (c *fastOpenConn) LocalAddr() net.Addr { + c.connLock.RLock() + defer c.connLock.RUnlock() + + if c.conn != nil { + return c.conn.LocalAddr() + } + + return nil +} + +func (c *fastOpenConn) RemoteAddr() net.Addr { + c.connLock.RLock() + conn := c.conn + c.connLock.RUnlock() + + if conn != nil { + return conn.RemoteAddr() + } + + addr, err := net.ResolveTCPAddr(c.network, c.address) + if err != nil { + return nil + } + return addr +} + +func (c *fastOpenConn) SetDeadline(t time.Time) error { + c.connLock.RLock() + defer c.connLock.RUnlock() + + c.deadline = &t + + if c.conn != nil { + return c.conn.SetDeadline(t) + } + + if c.isClosedBeforeReady() { + return net.ErrClosed + } + + return nil +} + +func (c *fastOpenConn) SetReadDeadline(t time.Time) error { + c.connLock.RLock() + defer c.connLock.RUnlock() + + c.readDeadline = &t + + if c.conn != nil { + return c.conn.SetReadDeadline(t) + } + + if c.isClosedBeforeReady() { + return net.ErrClosed + } + + return nil +} + +func (c *fastOpenConn) SetWriteDeadline(t time.Time) error { + c.connLock.RLock() + defer c.connLock.RUnlock() + + c.writeDeadline = &t + + if c.conn != nil { + return c.conn.SetWriteDeadline(t) + } + + if c.isClosedBeforeReady() { + return net.ErrClosed + } + + return nil +} + +var _ net.Conn = (*fastOpenConn)(nil) diff --git a/extras/outbounds/ob_direct.go b/extras/outbounds/ob_direct.go index b80ac00305..de7ddd2f6d 100644 --- a/extras/outbounds/ob_direct.go +++ b/extras/outbounds/ob_direct.go @@ -35,8 +35,8 @@ type directOutbound struct { Mode DirectOutboundMode // Dialer4 and Dialer6 are used for IPv4 and IPv6 TCP connections respectively. - Dialer4 *net.Dialer - Dialer6 *net.Dialer + DialFunc4 func(network, address string) (net.Conn, error) + DialFunc6 func(network, address string) (net.Conn, error) // DeviceName & BindIPs are for UDP connections. They don't use dialers, so we // need to bind them when creating the connection. @@ -45,6 +45,16 @@ type directOutbound struct { BindIP6 net.IP } +type DirectOutboundOptions struct { + Mode DirectOutboundMode + + DeviceName string + BindIP4 net.IP + BindIP6 net.IP + + FastOpen bool +} + type noAddressError struct { IPv4 bool IPv6 bool @@ -84,6 +94,57 @@ func (e resolveError) Unwrap() error { return e.Err } +func NewDirectOutboundWithOptions(opts DirectOutboundOptions) (PluggableOutbound, error) { + dialer4 := &net.Dialer{ + Timeout: defaultDialerTimeout, + } + if opts.BindIP4 != nil { + if opts.BindIP4.To4() == nil { + return nil, errors.New("BindIP4 must be an IPv4 address") + } + dialer4.LocalAddr = &net.TCPAddr{ + IP: opts.BindIP4, + } + } + dialer6 := &net.Dialer{ + Timeout: defaultDialerTimeout, + } + if opts.BindIP6 != nil { + if opts.BindIP6.To4() != nil { + return nil, errors.New("BindIP6 must be an IPv6 address") + } + dialer6.LocalAddr = &net.TCPAddr{ + IP: opts.BindIP6, + } + } + if opts.DeviceName != "" { + err := dialerBindToDevice(dialer4, opts.DeviceName) + if err != nil { + return nil, err + } + err = dialerBindToDevice(dialer6, opts.DeviceName) + if err != nil { + return nil, err + } + } + + dialFunc4 := dialer4.Dial + dialFunc6 := dialer6.Dial + if opts.FastOpen { + dialFunc4 = newFastOpenDialer(dialer4).Dial + dialFunc6 = newFastOpenDialer(dialer6).Dial + } + + return &directOutbound{ + Mode: opts.Mode, + DialFunc4: dialFunc4, + DialFunc6: dialFunc6, + DeviceName: opts.DeviceName, + BindIP4: opts.BindIP4, + BindIP6: opts.BindIP6, + }, nil +} + // NewDirectOutboundSimple creates a new directOutbound with the given mode, // without binding to a specific device. Works on all platforms. func NewDirectOutboundSimple(mode DirectOutboundMode) PluggableOutbound { @@ -91,9 +152,9 @@ func NewDirectOutboundSimple(mode DirectOutboundMode) PluggableOutbound { Timeout: defaultDialerTimeout, } return &directOutbound{ - Mode: mode, - Dialer4: d, - Dialer6: d, + Mode: mode, + DialFunc4: d.Dial, + DialFunc6: d.Dial, } } @@ -102,34 +163,20 @@ func NewDirectOutboundSimple(mode DirectOutboundMode) PluggableOutbound { // can be nil, in which case the directOutbound will not bind to a specific address // for that family. func NewDirectOutboundBindToIPs(mode DirectOutboundMode, bindIP4, bindIP6 net.IP) (PluggableOutbound, error) { - if bindIP4 != nil && bindIP4.To4() == nil { - return nil, errors.New("bindIP4 must be an IPv4 address") - } - if bindIP6 != nil && bindIP6.To4() != nil { - return nil, errors.New("bindIP6 must be an IPv6 address") - } - ob := &directOutbound{ - Mode: mode, - Dialer4: &net.Dialer{ - Timeout: defaultDialerTimeout, - }, - Dialer6: &net.Dialer{ - Timeout: defaultDialerTimeout, - }, + return NewDirectOutboundWithOptions(DirectOutboundOptions{ + Mode: mode, BindIP4: bindIP4, BindIP6: bindIP6, - } - if bindIP4 != nil { - ob.Dialer4.LocalAddr = &net.TCPAddr{ - IP: bindIP4, - } - } - if bindIP6 != nil { - ob.Dialer6.LocalAddr = &net.TCPAddr{ - IP: bindIP6, - } - } - return ob, nil + }) +} + +// NewDirectOutboundBindToDevice creates a new directOutbound with the given mode, +// and binds to the given device. Only works on Linux. +func NewDirectOutboundBindToDevice(mode DirectOutboundMode, deviceName string) (PluggableOutbound, error) { + return NewDirectOutboundWithOptions(DirectOutboundOptions{ + Mode: mode, + DeviceName: deviceName, + }) } // resolve is our built-in DNS resolver for handling the case when @@ -201,9 +248,9 @@ func (d *directOutbound) TCP(reqAddr *AddrEx) (net.Conn, error) { func (d *directOutbound) dialTCP(ip net.IP, port uint16) (net.Conn, error) { if ip.To4() != nil { - return d.Dialer4.Dial("tcp4", net.JoinHostPort(ip.String(), strconv.Itoa(int(port)))) + return d.DialFunc4("tcp4", net.JoinHostPort(ip.String(), strconv.Itoa(int(port)))) } else { - return d.Dialer6.Dial("tcp6", net.JoinHostPort(ip.String(), strconv.Itoa(int(port)))) + return d.DialFunc6("tcp6", net.JoinHostPort(ip.String(), strconv.Itoa(int(port)))) } } diff --git a/extras/outbounds/ob_direct_linux.go b/extras/outbounds/ob_direct_linux.go index 33b7d097be..5607e50bd0 100644 --- a/extras/outbounds/ob_direct_linux.go +++ b/extras/outbounds/ob_direct_linux.go @@ -6,31 +6,31 @@ import ( "syscall" ) -// NewDirectOutboundBindToDevice creates a new directOutbound with the given mode, -// and binds to the given device. Only works on Linux. -func NewDirectOutboundBindToDevice(mode DirectOutboundMode, deviceName string) (PluggableOutbound, error) { +func dialerBindToDevice(dialer *net.Dialer, deviceName string) error { if err := verifyDeviceName(deviceName); err != nil { - return nil, err + return err } - d := &net.Dialer{ - Timeout: defaultDialerTimeout, - Control: func(network, address string, c syscall.RawConn) error { - var errBind error - err := c.Control(func(fd uintptr) { - errBind = syscall.BindToDevice(int(fd), deviceName) - }) + + originControl := dialer.Control + dialer.Control = func(network, address string, c syscall.RawConn) error { + if originControl != nil { + // Chaining other control function + err := originControl(network, address, c) if err != nil { return err } - return errBind - }, + } + + var errBind error + err := c.Control(func(fd uintptr) { + errBind = syscall.BindToDevice(int(fd), deviceName) + }) + if err != nil { + return err + } + return errBind } - return &directOutbound{ - Mode: mode, - Dialer4: d, - Dialer6: d, - DeviceName: deviceName, - }, nil + return nil } func verifyDeviceName(deviceName string) error { diff --git a/extras/outbounds/ob_direct_others.go b/extras/outbounds/ob_direct_others.go index b416c3023f..eeedc84881 100644 --- a/extras/outbounds/ob_direct_others.go +++ b/extras/outbounds/ob_direct_others.go @@ -7,11 +7,8 @@ import ( "net" ) -// NewDirectOutboundBindToDevice creates a new directOutbound with the given mode, -// and binds to the given device. This doesn't work on non-Linux platforms, so this -// is just a stub function that always returns an error. -func NewDirectOutboundBindToDevice(mode DirectOutboundMode, deviceName string) (PluggableOutbound, error) { - return nil, errors.New("binding to device is not supported on this platform") +func dialerBindToDevice(dialer *net.Dialer, deviceName string) error { + return errors.New("binding to device is not supported on this platform") } func udpConnBindToDevice(conn *net.UDPConn, deviceName string) error { From d8c61c59d746a2c496c0a61751dc5ebf25da9beb Mon Sep 17 00:00:00 2001 From: Haruue Date: Sat, 23 Nov 2024 22:31:14 +0900 Subject: [PATCH 2/2] chore: disable fallback mode of tfo dialer tfo-go caches the "unsupported" status when fallback mode is enabled. In other words, if the hysteria server is started with net.ipv4.tcp_fastopen=0 and it fails once, the tfo will not be enabled until it is restarted, even if the user later sets sysctl net.ipv4.tcp_fastopen=3. --- extras/outbounds/fastopen.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/extras/outbounds/fastopen.go b/extras/outbounds/fastopen.go index a607500969..1d5d1eeea4 100644 --- a/extras/outbounds/fastopen.go +++ b/extras/outbounds/fastopen.go @@ -15,8 +15,7 @@ type fastOpenDialer struct { func newFastOpenDialer(netDialer *net.Dialer) *fastOpenDialer { return &fastOpenDialer{ dialer: &tfo.Dialer{ - Dialer: *netDialer, - Fallback: true, + Dialer: *netDialer, }, } }