diff --git a/adapter/outbound/base.go b/adapter/outbound/base.go index 24de7d94..d51655d8 100644 --- a/adapter/outbound/base.go +++ b/adapter/outbound/base.go @@ -140,10 +140,15 @@ func (b *Base) DialOptions(opts ...dialer.Option) []dialer.Option { default: } + if b.tfo { + opts = append(opts, dialer.WithTFO(true)) + } + return opts } type BasicOption struct { + TFO bool `proxy:"tfo,omitempty" group:"tfo,omitempty"` Interface string `proxy:"interface-name,omitempty" group:"interface-name,omitempty"` RoutingMark int `proxy:"routing-mark,omitempty" group:"routing-mark,omitempty"` IPVersion string `proxy:"ip-version,omitempty" group:"ip-version,omitempty"` diff --git a/adapter/outbound/http.go b/adapter/outbound/http.go index 720dc3e1..6a668ebb 100644 --- a/adapter/outbound/http.go +++ b/adapter/outbound/http.go @@ -170,6 +170,7 @@ func NewHttp(option HttpOption) (*Http, error) { name: option.Name, addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)), tp: C.Http, + tfo: option.TFO, iface: option.Interface, rmark: option.RoutingMark, prefer: C.NewDNSPrefer(option.IPVersion), diff --git a/adapter/outbound/shadowsocks.go b/adapter/outbound/shadowsocks.go index 2ac1f234..6e6a8d0a 100644 --- a/adapter/outbound/shadowsocks.go +++ b/adapter/outbound/shadowsocks.go @@ -252,6 +252,7 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { addr: addr, tp: C.Shadowsocks, udp: option.UDP, + tfo: option.TFO, iface: option.Interface, rmark: option.RoutingMark, prefer: C.NewDNSPrefer(option.IPVersion), diff --git a/adapter/outbound/shadowsocksr.go b/adapter/outbound/shadowsocksr.go index e84de879..135e7132 100644 --- a/adapter/outbound/shadowsocksr.go +++ b/adapter/outbound/shadowsocksr.go @@ -163,6 +163,7 @@ func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) { addr: addr, tp: C.ShadowsocksR, udp: option.UDP, + tfo: option.TFO, iface: option.Interface, rmark: option.RoutingMark, prefer: C.NewDNSPrefer(option.IPVersion), diff --git a/adapter/outbound/snell.go b/adapter/outbound/snell.go index 1331b526..d6f1efee 100644 --- a/adapter/outbound/snell.go +++ b/adapter/outbound/snell.go @@ -167,6 +167,7 @@ func NewSnell(option SnellOption) (*Snell, error) { addr: addr, tp: C.Snell, udp: option.UDP, + tfo: option.TFO, iface: option.Interface, rmark: option.RoutingMark, prefer: C.NewDNSPrefer(option.IPVersion), diff --git a/adapter/outbound/socks5.go b/adapter/outbound/socks5.go index d40a6bff..cdb89cc2 100644 --- a/adapter/outbound/socks5.go +++ b/adapter/outbound/socks5.go @@ -182,6 +182,7 @@ func NewSocks5(option Socks5Option) (*Socks5, error) { addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)), tp: C.Socks5, udp: option.UDP, + tfo: option.TFO, iface: option.Interface, rmark: option.RoutingMark, prefer: C.NewDNSPrefer(option.IPVersion), diff --git a/adapter/outbound/trojan.go b/adapter/outbound/trojan.go index 2a8cfe47..beedd614 100644 --- a/adapter/outbound/trojan.go +++ b/adapter/outbound/trojan.go @@ -250,6 +250,7 @@ func NewTrojan(option TrojanOption) (*Trojan, error) { addr: addr, tp: C.Trojan, udp: option.UDP, + tfo: option.TFO, iface: option.Interface, rmark: option.RoutingMark, prefer: C.NewDNSPrefer(option.IPVersion), diff --git a/adapter/outbound/vless.go b/adapter/outbound/vless.go index e46e245d..eef05687 100644 --- a/adapter/outbound/vless.go +++ b/adapter/outbound/vless.go @@ -510,6 +510,7 @@ func NewVless(option VlessOption) (*Vless, error) { tp: C.Vless, udp: option.UDP, xudp: option.XUDP, + tfo: option.TFO, iface: option.Interface, rmark: option.RoutingMark, prefer: C.NewDNSPrefer(option.IPVersion), diff --git a/adapter/outbound/vmess.go b/adapter/outbound/vmess.go index e8220767..2ae72069 100644 --- a/adapter/outbound/vmess.go +++ b/adapter/outbound/vmess.go @@ -387,6 +387,7 @@ func NewVmess(option VmessOption) (*Vmess, error) { tp: C.Vmess, udp: option.UDP, xudp: option.XUDP, + tfo: option.TFO, iface: option.Interface, rmark: option.RoutingMark, prefer: C.NewDNSPrefer(option.IPVersion), diff --git a/component/dialer/dialer.go b/component/dialer/dialer.go index 39e47bde..9ac9d719 100644 --- a/component/dialer/dialer.go +++ b/component/dialer/dialer.go @@ -118,7 +118,11 @@ func dialContext(ctx context.Context, network string, destination netip.Addr, po return nil, ErrorDisableIPv6 } - return dialer.DialContext(ctx, network, net.JoinHostPort(destination.String(), port)) + address := net.JoinHostPort(destination.String(), port) + if opt.tfo { + return dialTFO(ctx, *dialer, network, address) + } + return dialer.DialContext(ctx, network, address) } func singleDialContext(ctx context.Context, network string, address string, opt *option) (net.Conn, error) { diff --git a/component/dialer/options.go b/component/dialer/options.go index 27adc845..1c4e7bfc 100644 --- a/component/dialer/options.go +++ b/component/dialer/options.go @@ -18,6 +18,7 @@ type option struct { routingMark int network int prefer int + tfo bool resolver resolver.Resolver } @@ -69,6 +70,12 @@ func WithOnlySingleStack(isIPv4 bool) Option { } } +func WithTFO(tfo bool) Option { + return func(opt *option) { + opt.tfo = tfo + } +} + func WithOption(o option) Option { return func(opt *option) { *opt = o diff --git a/component/dialer/tfo.go b/component/dialer/tfo.go new file mode 100644 index 00000000..2db2e91e --- /dev/null +++ b/component/dialer/tfo.go @@ -0,0 +1,119 @@ +package dialer + +import ( + "context" + "github.com/sagernet/tfo-go" + "io" + "net" + "time" +) + +type tfoConn struct { + net.Conn + closed bool + dialed chan bool + cancel context.CancelFunc + ctx context.Context + dialFn func(ctx context.Context, earlyData []byte) (net.Conn, error) +} + +func (c *tfoConn) Dial(earlyData []byte) (err error) { + c.Conn, err = c.dialFn(c.ctx, earlyData) + if err != nil { + return + } + c.dialed <- true + return err +} + +func (c *tfoConn) Read(b []byte) (n int, err error) { + if c.closed { + return 0, io.ErrClosedPipe + } + if c.Conn == nil { + select { + case <-c.ctx.Done(): + return 0, io.ErrUnexpectedEOF + case <-c.dialed: + } + } + return c.Conn.Read(b) +} + +func (c *tfoConn) Write(b []byte) (n int, err error) { + if c.closed { + return 0, io.ErrClosedPipe + } + if c.Conn == nil { + if err := c.Dial(b); err != nil { + return 0, err + } + return len(b), nil + } + + return c.Conn.Write(b) +} + +func (c *tfoConn) Close() error { + c.closed = true + c.cancel() + if c.Conn == nil { + return nil + } + return c.Conn.Close() +} + +func (c *tfoConn) LocalAddr() net.Addr { + if c.Conn == nil { + return nil + } + return c.Conn.LocalAddr() +} + +func (c *tfoConn) RemoteAddr() net.Addr { + if c.Conn == nil { + return nil + } + return c.Conn.RemoteAddr() +} + +func (c *tfoConn) SetDeadline(t time.Time) error { + if err := c.SetReadDeadline(t); err != nil { + return err + } + return c.SetWriteDeadline(t) +} + +func (c *tfoConn) SetReadDeadline(t time.Time) error { + if c.Conn == nil { + return nil + } + return c.Conn.SetReadDeadline(t) +} + +func (c *tfoConn) SetWriteDeadline(t time.Time) error { + if c.Conn == nil { + return nil + } + return c.Conn.SetWriteDeadline(t) +} + +func (c *tfoConn) Upstream() any { + if c.Conn == nil { // ensure return a nil interface not an interface with nil value + return nil + } + return c.Conn +} + +func dialTFO(ctx context.Context, netDialer net.Dialer, network, address string) (net.Conn, error) { + ctx, cancel := context.WithCancel(ctx) + dialer := tfo.Dialer{Dialer: netDialer, DisableTFO: false} + return &tfoConn{ + dialed: make(chan bool, 1), + cancel: cancel, + ctx: ctx, + dialFn: func(ctx context.Context, earlyData []byte) (net.Conn, error) { + return dialer.DialContext(ctx, network, address, earlyData) + }, + }, nil +}