diff --git a/adapter/outbound/tuic.go b/adapter/outbound/tuic.go index c10a853a..4d826912 100644 --- a/adapter/outbound/tuic.go +++ b/adapter/outbound/tuic.go @@ -6,6 +6,7 @@ import ( "crypto/tls" "encoding/hex" "encoding/pem" + "errors" "fmt" "math" "net" @@ -15,12 +16,15 @@ import ( "github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/proxydialer" + "github.com/Dreamacro/clash/component/resolver" tlsC "github.com/Dreamacro/clash/component/tls" C "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/transport/tuic" "github.com/gofrs/uuid/v5" "github.com/metacubex/quic-go" + M "github.com/sagernet/sing/common/metadata" + "github.com/sagernet/sing/common/uot" ) type Tuic struct { @@ -59,6 +63,9 @@ type TuicOption struct { DisableMTUDiscovery bool `proxy:"disable-mtu-discovery,omitempty"` MaxDatagramFrameSize int `proxy:"max-datagram-frame-size,omitempty"` SNI string `proxy:"sni,omitempty"` + + UDPOverStream bool `proxy:"udp-over-stream,omitempty"` + UDPOverStreamVersion int `proxy:"udp-over-stream-version,omitempty"` } // DialContext implements C.ProxyAdapter @@ -82,6 +89,32 @@ func (t *Tuic) ListenPacketContext(ctx context.Context, metadata *C.Metadata, op // ListenPacketWithDialer implements C.ProxyAdapter func (t *Tuic) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) { + if t.option.UDPOverStream { + uotDestination := uot.RequestDestination(uint8(t.option.UDPOverStreamVersion)) + uotMetadata := *metadata + uotMetadata.Host = uotDestination.Fqdn + uotMetadata.DstPort = uotDestination.Port + c, err := t.DialContextWithDialer(ctx, dialer, &uotMetadata) + if err != nil { + return nil, err + } + + // tuic uos use stream-oriented udp with a special address, so we need a net.UDPAddr + if !metadata.Resolved() { + ip, err := resolver.ResolveIP(ctx, metadata.Host) + if err != nil { + return nil, errors.New("can't resolve ip") + } + metadata.DstIP = ip + } + + destination := M.SocksaddrFromNet(metadata.UDPAddr()) + if t.option.UDPOverStreamVersion == uot.LegacyVersion { + return newPacketConn(uot.NewConn(c, uot.Request{Destination: destination}), t), nil + } else { + return newPacketConn(uot.NewLazyConn(c, uot.Request{Destination: destination}), t), nil + } + } pc, err := t.client.ListenPacketWithDialer(ctx, metadata, dialer, t.dialWithDialer) if err != nil { return nil, err @@ -239,6 +272,14 @@ func NewTuic(option TuicOption) (*Tuic, error) { tlsConfig.InsecureSkipVerify = true // tls: either ServerName or InsecureSkipVerify must be specified in the tls.Config } + switch option.UDPOverStreamVersion { + case uot.Version, uot.LegacyVersion: + case 0: + option.UDPOverStreamVersion = uot.LegacyVersion + default: + return nil, fmt.Errorf("tuic %s unknown udp over stream protocol version: %d", addr, option.UDPOverStreamVersion) + } + t := &Tuic{ Base: &Base{ name: option.Name, diff --git a/docs/config.yaml b/docs/config.yaml index 6ae3910e..62cd0c58 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -681,6 +681,11 @@ proxies: # socks5 # skip-cert-verify: true # max-open-streams: 20 # default 100, too many open streams may hurt performance # sni: example.com + # + # meta和sing-box私有扩展,将ss-uot用于udp中继,开启此选项后udp-relay-mode将失效 + # 警告,与原版tuic不兼容!!! + # udp-over-stream: false + # udp-over-stream-version: 1 # ShadowsocksR # The supported ciphers (encryption methods): all stream ciphers in ss diff --git a/listener/sing/sing.go b/listener/sing/sing.go index f59fd613..d5731bbf 100644 --- a/listener/sing/sing.go +++ b/listener/sing/sing.go @@ -72,7 +72,27 @@ func UpstreamMetadata(metadata M.Metadata) M.Metadata { } } -func (h *ListenerHandler) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { +func ConvertMetadata(metadata *C.Metadata) M.Metadata { + return M.Metadata{ + Protocol: metadata.Type.String(), + Source: M.SocksaddrFrom(metadata.SrcIP, metadata.SrcPort), + Destination: M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort), + } +} + +func (h *ListenerHandler) IsSpecialFqdn(fqdn string) bool { + switch fqdn { + case mux.Destination.Fqdn: + case vmess.MuxDestination.Fqdn: + case uot.MagicAddress: + case uot.LegacyMagicAddress: + default: + return false + } + return true +} + +func (h *ListenerHandler) ParseSpecialFqdn(ctx context.Context, conn net.Conn, metadata M.Metadata) error { switch metadata.Destination.Fqdn { case mux.Destination.Fqdn: return mux.HandleConnection(ctx, h, log.SingLogger, conn, UpstreamMetadata(metadata)) @@ -89,6 +109,13 @@ func (h *ListenerHandler) NewConnection(ctx context.Context, conn net.Conn, meta metadata.Destination = M.Socksaddr{Addr: netip.IPv4Unspecified()} return h.NewPacketConnection(ctx, uot.NewConn(conn, uot.Request{}), metadata) } + return errors.New("not special fqdn") +} + +func (h *ListenerHandler) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { + if h.IsSpecialFqdn(metadata.Destination.Fqdn) { + return h.ParseSpecialFqdn(ctx, conn, metadata) + } target := socks5.ParseAddr(metadata.Destination.String()) wg := &sync.WaitGroup{} defer wg.Wait() // this goroutine must exit after conn.Close() diff --git a/listener/tuic/server.go b/listener/tuic/server.go index 76996b27..125c53e1 100644 --- a/listener/tuic/server.go +++ b/listener/tuic/server.go @@ -1,6 +1,7 @@ package tuic import ( + "context" "crypto/tls" "net" "strings" @@ -11,6 +12,7 @@ import ( "github.com/Dreamacro/clash/common/sockopt" C "github.com/Dreamacro/clash/constant" LC "github.com/Dreamacro/clash/listener/config" + "github.com/Dreamacro/clash/listener/sing" "github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/transport/socks5" "github.com/Dreamacro/clash/transport/tuic" @@ -36,6 +38,12 @@ func New(config LC.TuicServer, tcpIn chan<- C.ConnContext, udpIn chan<- C.Packet inbound.WithSpecialRules(""), } } + h := &sing.ListenerHandler{ + TcpIn: tcpIn, + UdpIn: udpIn, + Type: C.TUIC, + Additions: additions, + } cert, err := CN.ParseCert(config.Certificate, config.PrivateKey) if err != nil { return nil, err @@ -86,7 +94,19 @@ func New(config LC.TuicServer, tcpIn chan<- C.ConnContext, udpIn chan<- C.Packet newAdditions = slices.Clone(additions) newAdditions = append(newAdditions, _additions...) } - tcpIn <- inbound.NewSocket(addr, conn, C.TUIC, newAdditions...) + connCtx := inbound.NewSocket(addr, conn, C.TUIC, newAdditions...) + metadata := sing.ConvertMetadata(connCtx.Metadata()) + if h.IsSpecialFqdn(metadata.Destination.Fqdn) { + go func() { // ParseSpecialFqdn will block, so open a new goroutine + _ = h.ParseSpecialFqdn( + sing.WithAdditions(context.Background(), newAdditions...), + conn, + metadata, + ) + }() + return nil + } + tcpIn <- connCtx return nil } handleUdpFn := func(addr socks5.Addr, packet C.UDPPacket, _additions ...inbound.Addition) error {