feat: support ipv6 dual stack fallback for masque/trusttunnel/xhttp h3 mode

This commit is contained in:
wwqgtxx
2026-04-11 11:17:25 +08:00
parent 7b73775591
commit 1b2ea14596
7 changed files with 127 additions and 81 deletions
+2 -17
View File
@@ -257,26 +257,11 @@ func (w *Masque) run(ctx context.Context) error {
return err
}
} else {
var udpAddr *net.UDPAddr
udpAddr, err = resolveUDPAddr(ctx, "udp", w.addr, w.prefer)
var quicConn *quic.Conn
pc, quicConn, err = common.DialQuic(ctx, w.addr, w.DialOptions(), w.dialer, w.tlsConfig, w.quicConfig)
if err != nil {
return err
}
pc, err = w.dialer.ListenPacket(ctx, "udp", "", udpAddr.AddrPort())
if err != nil {
return err
}
transport := quic.Transport{Conn: pc}
transport.SetCreatedConn(true) // auto close conn
transport.SetSingleUse(true) // auto close transport
quicConn, err := transport.Dial(ctx, udpAddr, w.tlsConfig, w.quicConfig)
if err != nil {
_ = pc.Close()
return err
}
common.SetCongestionController(quicConn, w.option.CongestionController, w.option.CWND)
closer, ipConn, err = masque.ConnectTunnel(ctx, quicConn, w.uri)
+2 -9
View File
@@ -3,7 +3,6 @@ package outbound
import (
"context"
"net"
"net/netip"
"strconv"
N "github.com/metacubex/mihomo/common/net"
@@ -103,14 +102,8 @@ func NewTrustTunnel(option TrustTunnelOption) (*TrustTunnel, error) {
outbound.dialer = option.NewDialer(outbound.DialOptions())
tOption := trusttunnel.ClientOptions{
Dialer: outbound.dialer,
ResolvUDP: func(ctx context.Context, server string) (netip.AddrPort, error) {
udpAddr, err := resolveUDPAddr(ctx, "udp", server, option.IPVersion)
if err != nil {
return netip.AddrPort{}, err
}
return udpAddr.AddrPort(), nil
},
Dialer: outbound.dialer,
DialOptions: outbound.DialOptions,
Server: addr,
Username: option.UserName,
Password: option.Password,
+3 -32
View File
@@ -16,6 +16,7 @@ import (
tlsC "github.com/metacubex/mihomo/component/tls"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/transport/gun"
"github.com/metacubex/mihomo/transport/tuic/common"
"github.com/metacubex/mihomo/transport/vless"
"github.com/metacubex/mihomo/transport/vless/encryption"
"github.com/metacubex/mihomo/transport/vmess"
@@ -588,26 +589,11 @@ func NewVless(option VlessOption) (*Vless, error) {
return nil, err
}
udpAddr, err := resolveUDPAddr(ctx, "udp", v.addr, v.prefer)
if err != nil {
return nil, err
}
err = v.echConfig.ClientHandle(ctx, tlsConfig)
if err != nil {
return nil, err
}
packetConn, err := v.dialer.ListenPacket(ctx, "udp", "", udpAddr.AddrPort())
if err != nil {
return nil, err
}
transport := quic.Transport{Conn: packetConn}
transport.SetCreatedConn(true) // auto close conn
transport.SetSingleUse(true) // auto close transport
quicConn, err := transport.DialEarly(ctx, udpAddr, tlsConfig, cfg)
if err != nil {
_ = packetConn.Close()
return nil, err
}
_, quicConn, err := common.DialQuicEarly(ctx, v.addr, v.DialOptions(), v.dialer, tlsConfig, cfg)
return quicConn, nil
},
v.option.ALPN,
@@ -743,26 +729,11 @@ func NewVless(option VlessOption) (*Vless, error) {
return nil, err
}
udpAddr, err := resolveUDPAddr(ctx, "udp", downloadAddr, v.prefer)
if err != nil {
return nil, err
}
err = downloadEchConfig.ClientHandle(ctx, tlsConfig)
if err != nil {
return nil, err
}
packetConn, err := v.dialer.ListenPacket(ctx, "udp", "", udpAddr.AddrPort())
if err != nil {
return nil, err
}
transport := quic.Transport{Conn: packetConn}
transport.SetCreatedConn(true) // auto close conn
transport.SetSingleUse(true) // auto close transport
quicConn, err := transport.DialEarly(ctx, udpAddr, tlsConfig, cfg)
if err != nil {
_ = packetConn.Close()
return nil, err
}
_, quicConn, err := common.DialQuicEarly(ctx, downloadAddr, v.DialOptions(), v.dialer, tlsConfig, cfg)
return quicConn, nil
},
downloadALPN,
+14
View File
@@ -24,6 +24,12 @@ type NetDialer interface {
DialContext(ctx context.Context, network, address string) (net.Conn, error)
}
type NetDialerFunc func(ctx context.Context, network, address string) (net.Conn, error)
func (f NetDialerFunc) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
return f(ctx, network, address)
}
type option struct {
interfaceName string
fallbackBind bool
@@ -115,6 +121,14 @@ func WithOption(o option) Option {
}
}
func WithOptions(options ...Option) Option {
return func(opt *option) {
for _, o := range options {
o(opt)
}
}
}
func IsZeroOptions(opts []Option) bool {
return applyOptions(opts...) == option{}
}
+9 -9
View File
@@ -6,7 +6,6 @@ import (
"fmt"
"io"
"net"
"net/netip"
"net/url"
"sync"
"sync/atomic"
@@ -14,6 +13,7 @@ import (
"github.com/metacubex/mihomo/common/httputils"
"github.com/metacubex/mihomo/common/once"
"github.com/metacubex/mihomo/component/dialer"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/transport/vmess"
@@ -22,11 +22,11 @@ import (
"golang.org/x/exp/slices"
)
type ResolvUDPFunc func(ctx context.Context, server string) (netip.AddrPort, error)
type DialOptionsFunc func() []dialer.Option
type ClientOptions struct {
Dialer C.Dialer
ResolvUDP ResolvUDPFunc
DialOptions DialOptionsFunc // for quic
Server string
Username string
Password string
@@ -43,7 +43,7 @@ type ClientOptions struct {
type Client struct {
ctx context.Context
dialer C.Dialer
resolv ResolvUDPFunc
dialOptions DialOptionsFunc
server string
auth string
roundTripper http.RoundTripper
@@ -55,11 +55,11 @@ type Client struct {
func NewClient(ctx context.Context, options ClientOptions) (client *Client, err error) {
client = &Client{
ctx: ctx,
dialer: options.Dialer,
resolv: options.ResolvUDP,
server: options.Server,
auth: buildAuth(options.Username, options.Password),
ctx: ctx,
dialer: options.Dialer,
dialOptions: options.DialOptions,
server: options.Server,
auth: buildAuth(options.Username, options.Password),
}
if options.QUIC {
if len(options.TLSConfig.NextProtos) == 0 {
+2 -14
View File
@@ -30,26 +30,14 @@ func (c *Client) quicRoundTripper(tlsConfig *vmess.TLSConfig, congestionControlN
Allow0RTT: false,
},
Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (*quic.Conn, error) {
addrPort, err := c.resolv(ctx, c.server)
err := tlsConfig.ECH.ClientHandle(ctx, tlsCfg)
if err != nil {
return nil, err
}
err = tlsConfig.ECH.ClientHandle(ctx, tlsCfg)
_, quicConn, err := common.DialQuicEarly(ctx, addr, c.dialOptions(), c.dialer, tlsCfg, cfg)
if err != nil {
return nil, err
}
packetConn, err := c.dialer.ListenPacket(ctx, "udp", "", addrPort)
if err != nil {
return nil, err
}
transport := quic.Transport{Conn: packetConn}
transport.SetCreatedConn(true) // auto close conn
transport.SetSingleUse(true) // auto close transport
quicConn, err := transport.DialEarly(ctx, net.UDPAddrFromAddrPort(addrPort), tlsCfg, cfg)
if err != nil {
_ = packetConn.Close()
return nil, err
}
common.SetCongestionController(quicConn, congestionControlName, cwnd)
return quicConn, nil
},
+95
View File
@@ -0,0 +1,95 @@
package common
import (
"context"
"net"
"net/netip"
"time"
"github.com/metacubex/mihomo/component/dialer"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/quic-go"
"github.com/metacubex/tls"
)
// DialQuicEarly dials a new connection, attempting to use 0-RTT if possible.
func DialQuicEarly(ctx context.Context, address string, opts []dialer.Option, dialer C.Dialer, tlsConf *tls.Config, conf *quic.Config) (net.PacketConn, *quic.Conn, error) {
return dialQuic(ctx, address, opts, dialer, tlsConf, conf, true)
}
// DialQuic dials a new connection to a remote host (not using 0-RTT).
func DialQuic(ctx context.Context, address string, opts []dialer.Option, dialer C.Dialer, tlsConf *tls.Config, conf *quic.Config) (net.PacketConn, *quic.Conn, error) {
return dialQuic(ctx, address, opts, dialer, tlsConf, conf, false)
}
func dialQuic(ctx context.Context, address string, opts []dialer.Option, cDialer C.Dialer, tlsConf *tls.Config, conf *quic.Config, early bool) (net.PacketConn, *quic.Conn, error) {
d := dialer.NewDialer(
dialer.WithOptions(opts...),
dialer.WithNetDialer(dialer.NetDialerFunc(func(ctx context.Context, network, address string) (net.Conn, error) {
addrPort, err := netip.ParseAddrPort(address) // the dialer will resolve the domain to ip
if err != nil {
return nil, err
}
udpAddr := net.UDPAddrFromAddrPort(addrPort)
packetConn, err := cDialer.ListenPacket(ctx, "udp", "", udpAddr.AddrPort())
if err != nil {
return nil, err
}
transport := quic.Transport{Conn: packetConn}
transport.SetCreatedConn(true) // auto close conn
transport.SetSingleUse(true) // auto close transport
var quicConn *quic.Conn
if early {
quicConn, err = transport.DialEarly(ctx, udpAddr, tlsConf, conf)
} else {
quicConn, err = transport.Dial(ctx, udpAddr, tlsConf, conf)
}
if err != nil {
_ = packetConn.Close()
return nil, err
}
return quicNetConn{Conn: quicConn, pc: packetConn}, nil
})),
)
c, err := d.DialContext(ctx, "udp", address)
if err != nil {
return nil, nil, err
}
nc := c.(quicNetConn)
return nc.pc, nc.Conn, nil
}
type quicNetConn struct {
*quic.Conn
pc net.PacketConn
}
func (q quicNetConn) Close() error {
err := q.Conn.CloseWithError(0, "")
_ = q.pc.Close() // always close the packetConn
return err
}
func (q quicNetConn) Read(b []byte) (n int, err error) {
panic("should not call Read on quicNetConn")
}
func (q quicNetConn) Write(b []byte) (n int, err error) {
panic("should not call Write on quicNetConn")
}
func (q quicNetConn) SetDeadline(t time.Time) error {
panic("should not call SetDeadline on quicNetConn")
}
func (q quicNetConn) SetReadDeadline(t time.Time) error {
panic("should not call SetReadDeadline on quicNetConn")
}
func (q quicNetConn) SetWriteDeadline(t time.Time) error {
panic("should not call SetWriteDeadline on quicNetConn")
}
var _ net.Conn = quicNetConn{}