feat: support ipv6 dual stack fallback for tuic

This commit is contained in:
wwqgtxx
2026-04-11 19:39:10 +08:00
parent 808c0d6251
commit fdafea36ce
5 changed files with 21 additions and 131 deletions
+13 -33
View File
@@ -12,6 +12,7 @@ import (
"github.com/metacubex/mihomo/component/ech"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/transport/tuic"
"github.com/metacubex/mihomo/transport/tuic/common"
"github.com/gofrs/uuid/v5"
"github.com/metacubex/quic-go"
@@ -25,8 +26,9 @@ type Tuic struct {
option *TuicOption
client *tuic.PoolClient
tlsConfig *tls.Config
echConfig *ech.Config
quicConfig *quic.Config
tlsConfig *tls.Config
echConfig *ech.Config
}
type TuicOption struct {
@@ -106,25 +108,13 @@ func (t *Tuic) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_
return newPacketConn(pc, t), nil
}
func (t *Tuic) dial(ctx context.Context) (transport *quic.Transport, addr net.Addr, err error) {
udpAddr, err := resolveUDPAddr(ctx, "udp", t.addr, t.prefer)
func (t *Tuic) dial(ctx context.Context) (quicConn *quic.Conn, err error) {
_, quicConn, err = common.DialQuic(ctx, t.addr, t.DialOptions(), t.dialer, t.tlsConfig, t.quicConfig, t.option.ReduceRtt)
if err != nil {
return nil, nil, err
return nil, err
}
err = t.echConfig.ClientHandle(ctx, t.tlsConfig)
if err != nil {
return nil, nil, err
}
addr = udpAddr
var pc net.PacketConn
pc, err = t.dialer.ListenPacket(ctx, "udp", "", udpAddr.AddrPort())
if err != nil {
return nil, nil, err
}
transport = &quic.Transport{Conn: pc}
transport.SetCreatedConn(true) // auto close conn
transport.SetSingleUse(true) // auto close transport
return
common.SetCongestionController(quicConn, t.option.CongestionController, t.option.CWND)
return quicConn, nil
}
// ProxyInfo implements C.ProxyAdapter
@@ -232,7 +222,6 @@ func NewTuic(option TuicOption) (*Tuic, error) {
tlsConfig.InsecureSkipVerify = true // tls: either ServerName or InsecureSkipVerify must be specified in the tls.Config
}
tlsClientConfig := tlsConfig
echConfig, err := option.ECHOpts.Parse()
if err != nil {
return nil, err
@@ -258,9 +247,10 @@ func NewTuic(option TuicOption) (*Tuic, error) {
rmark: option.RoutingMark,
prefer: option.IPVersion,
},
option: &option,
tlsConfig: tlsClientConfig,
echConfig: echConfig,
option: &option,
quicConfig: quicConfig,
tlsConfig: tlsConfig,
echConfig: echConfig,
}
t.dialer = option.NewDialer(t.DialOptions())
@@ -278,17 +268,12 @@ func NewTuic(option TuicOption) (*Tuic, error) {
if len(option.Token) > 0 {
tkn := tuic.GenTKN(option.Token)
clientOption := &tuic.ClientOptionV4{
TlsConfig: tlsClientConfig,
QuicConfig: quicConfig,
Token: tkn,
UdpRelayMode: udpRelayMode,
CongestionController: option.CongestionController,
ReduceRtt: option.ReduceRtt,
RequestTimeout: time.Duration(option.RequestTimeout) * time.Millisecond,
MaxUdpRelayPacketSize: option.MaxUdpRelayPacketSize,
FastOpen: option.FastOpen,
MaxOpenStreams: clientMaxOpenStreams,
CWND: option.CWND,
}
t.client = tuic.NewPoolClientV4(clientOption, t.dial)
@@ -298,16 +283,11 @@ func NewTuic(option TuicOption) (*Tuic, error) {
maxUdpRelayPacketSize = tuic.MaxFragSizeV5
}
clientOption := &tuic.ClientOptionV5{
TlsConfig: tlsClientConfig,
QuicConfig: quicConfig,
Uuid: uuid.FromStringOrNil(option.UUID),
Password: option.Password,
UdpRelayMode: udpRelayMode,
CongestionController: option.CongestionController,
ReduceRtt: option.ReduceRtt,
MaxUdpRelayPacketSize: maxUdpRelayPacketSize,
MaxOpenStreams: clientMaxOpenStreams,
CWND: option.CWND,
}
t.client = tuic.NewPoolClientV5(clientOption, t.dial)
+1 -1
View File
@@ -18,7 +18,7 @@ var (
TooManyOpenStreams = errors.New("tuic: too many open streams")
)
type DialFunc func(ctx context.Context) (transport *quic.Transport, addr net.Addr, err error)
type DialFunc func(ctx context.Context) (quicConn *quic.Conn, err error)
type Client interface {
DialContext(ctx context.Context, metadata *C.Metadata) (net.Conn, error)
+5 -61
View File
@@ -4,16 +4,11 @@ import (
"context"
"errors"
"net"
"runtime"
"sync"
"sync/atomic"
"time"
N "github.com/metacubex/mihomo/common/net"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/log"
"github.com/metacubex/quic-go"
list "github.com/bahlo/generic-list-go"
)
@@ -22,7 +17,7 @@ type PoolClient struct {
newClientOptionV4 *ClientOptionV4
newClientOptionV5 *ClientOptionV5
dialHelper *poolDialHelper
dialFn DialFunc
tcpClients list.List[Client]
tcpClientsMutex sync.Mutex
udpClients list.List[Client]
@@ -51,47 +46,6 @@ func (t *PoolClient) ListenPacket(ctx context.Context, metadata *C.Metadata) (ne
return N.NewRefPacketConn(pc, t), nil
}
// poolDialHelper is a helper for dialFn
// using a standalone struct to let finalizer working
type poolDialHelper struct {
dialFn DialFunc
dialResult atomic.Pointer[dialResult]
}
type dialResult struct {
transport *quic.Transport
addr net.Addr
}
func (t *poolDialHelper) dial(ctx context.Context) (transport *quic.Transport, addr net.Addr, err error) {
if dr := t.dialResult.Load(); dr != nil {
return dr.transport, dr.addr, nil
}
transport, addr, err = t.dialFn(ctx)
if err != nil {
return nil, nil, err
}
if _, ok := transport.Conn.(*net.UDPConn); ok { // only cache the system's UDPConn
transport.SetSingleUse(false) // don't close transport in each dial
dr := &dialResult{transport: transport, addr: addr}
t.dialResult.Store(dr)
}
return transport, addr, err
}
func (t *poolDialHelper) forceClose() {
if dr := t.dialResult.Swap(nil); dr != nil {
transport := dr.transport
if transport != nil {
_ = transport.Close()
}
}
}
func (t *PoolClient) newClient(udp bool) (client Client) {
clients := &t.tcpClients
clientsMutex := &t.tcpClientsMutex
@@ -103,11 +57,10 @@ func (t *PoolClient) newClient(udp bool) (client Client) {
clientsMutex.Lock()
defer clientsMutex.Unlock()
dialHelper := t.dialHelper
if t.newClientOptionV4 != nil {
client = NewClientV4(t.newClientOptionV4, udp, dialHelper.dial)
client = NewClientV4(t.newClientOptionV4, udp, t.dialFn)
} else {
client = NewClientV5(t.newClientOptionV5, udp, dialHelper.dial)
client = NewClientV5(t.newClientOptionV5, udp, t.dialFn)
}
client.SetLastVisited(time.Now())
@@ -168,27 +121,18 @@ func (t *PoolClient) getClient(udp bool) Client {
func NewPoolClientV4(clientOption *ClientOptionV4, dialFn DialFunc) *PoolClient {
p := &PoolClient{
dialHelper: &poolDialHelper{dialFn: dialFn},
dialFn: dialFn,
}
newClientOption := *clientOption
p.newClientOptionV4 = &newClientOption
runtime.SetFinalizer(p, closeClientPool)
log.Debugln("New TuicV4 PoolClient at %p", p)
return p
}
func NewPoolClientV5(clientOption *ClientOptionV5, dialFn DialFunc) *PoolClient {
p := &PoolClient{
dialHelper: &poolDialHelper{dialFn: dialFn},
dialFn: dialFn,
}
newClientOption := *clientOption
p.newClientOptionV5 = &newClientOption
runtime.SetFinalizer(p, closeClientPool)
log.Debugln("New TuicV5 PoolClient at %p", p)
return p
}
func closeClientPool(client *PoolClient) {
log.Debugln("Close Tuic PoolClient at %p", client)
client.dialHelper.forceClose()
}
+1 -18
View File
@@ -21,21 +21,15 @@ import (
"github.com/metacubex/quic-go"
"github.com/metacubex/randv2"
"github.com/metacubex/tls"
)
type ClientOption struct {
TlsConfig *tls.Config
QuicConfig *quic.Config
Token [32]byte
UdpRelayMode common.UdpRelayMode
CongestionController string
ReduceRtt bool
RequestTimeout time.Duration
MaxUdpRelayPacketSize int
FastOpen bool
MaxOpenStreams int64
CWND int
}
type clientImpl struct {
@@ -73,21 +67,10 @@ func (t *clientImpl) getQuicConn(ctx context.Context) (*quic.Conn, error) {
if t.quicConn != nil {
return t.quicConn, nil
}
transport, addr, err := t.dialFn(ctx)
quicConn, err := t.dialFn(ctx)
if err != nil {
return nil, err
}
var quicConn *quic.Conn
if t.ReduceRtt {
quicConn, err = transport.DialEarly(ctx, addr, t.TlsConfig, t.QuicConfig)
} else {
quicConn, err = transport.Dial(ctx, addr, t.TlsConfig, t.QuicConfig)
}
if err != nil {
return nil, err
}
common.SetCongestionController(quicConn, t.CongestionController, t.CWND)
go func() {
_ = t.sendAuthentication(quicConn)
+1 -18
View File
@@ -21,20 +21,14 @@ import (
"github.com/metacubex/quic-go"
"github.com/metacubex/randv2"
"github.com/metacubex/tls"
)
type ClientOption struct {
TlsConfig *tls.Config
QuicConfig *quic.Config
Uuid [16]byte
Password string
UdpRelayMode common.UdpRelayMode
CongestionController string
ReduceRtt bool
MaxUdpRelayPacketSize int
MaxOpenStreams int64
CWND int
}
type clientImpl struct {
@@ -72,21 +66,10 @@ func (t *clientImpl) getQuicConn(ctx context.Context) (*quic.Conn, error) {
if t.quicConn != nil {
return t.quicConn, nil
}
transport, addr, err := t.dialFn(ctx)
quicConn, err := t.dialFn(ctx)
if err != nil {
return nil, err
}
var quicConn *quic.Conn
if t.ReduceRtt {
quicConn, err = transport.DialEarly(ctx, addr, t.TlsConfig, t.QuicConfig)
} else {
quicConn, err = transport.Dial(ctx, addr, t.TlsConfig, t.QuicConfig)
}
if err != nil {
return nil, err
}
common.SetCongestionController(quicConn, t.CongestionController, t.CWND)
go func() {
_ = t.sendAuthentication(quicConn)