mirror of
https://github.com/MetaCubeX/mihomo.git
synced 2024-12-25 19:55:38 +08:00
1181fd4560
only work with meta tuic server or sing-box 1.4.0-beta.6
344 lines
11 KiB
Go
344 lines
11 KiB
Go
package outbound
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"crypto/tls"
|
|
"encoding/hex"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"net"
|
|
"os"
|
|
"strconv"
|
|
"time"
|
|
|
|
"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 {
|
|
*Base
|
|
option *TuicOption
|
|
client *tuic.PoolClient
|
|
}
|
|
|
|
type TuicOption struct {
|
|
BasicOption
|
|
Name string `proxy:"name"`
|
|
Server string `proxy:"server"`
|
|
Port int `proxy:"port"`
|
|
Token string `proxy:"token,omitempty"`
|
|
UUID string `proxy:"uuid,omitempty"`
|
|
Password string `proxy:"password,omitempty"`
|
|
Ip string `proxy:"ip,omitempty"`
|
|
HeartbeatInterval int `proxy:"heartbeat-interval,omitempty"`
|
|
ALPN []string `proxy:"alpn,omitempty"`
|
|
ReduceRtt bool `proxy:"reduce-rtt,omitempty"`
|
|
RequestTimeout int `proxy:"request-timeout,omitempty"`
|
|
UdpRelayMode string `proxy:"udp-relay-mode,omitempty"`
|
|
CongestionController string `proxy:"congestion-controller,omitempty"`
|
|
DisableSni bool `proxy:"disable-sni,omitempty"`
|
|
MaxUdpRelayPacketSize int `proxy:"max-udp-relay-packet-size,omitempty"`
|
|
|
|
FastOpen bool `proxy:"fast-open,omitempty"`
|
|
MaxOpenStreams int `proxy:"max-open-streams,omitempty"`
|
|
CWND int `proxy:"cwnd,omitempty"`
|
|
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
|
Fingerprint string `proxy:"fingerprint,omitempty"`
|
|
CustomCA string `proxy:"ca,omitempty"`
|
|
CustomCAString string `proxy:"ca-str,omitempty"`
|
|
ReceiveWindowConn int `proxy:"recv-window-conn,omitempty"`
|
|
ReceiveWindow int `proxy:"recv-window,omitempty"`
|
|
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
|
|
func (t *Tuic) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
|
|
return t.DialContextWithDialer(ctx, dialer.NewDialer(t.Base.DialOptions(opts...)...), metadata)
|
|
}
|
|
|
|
// DialContextWithDialer implements C.ProxyAdapter
|
|
func (t *Tuic) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (C.Conn, error) {
|
|
conn, err := t.client.DialContextWithDialer(ctx, metadata, dialer, t.dialWithDialer)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return NewConn(conn, t), err
|
|
}
|
|
|
|
// ListenPacketContext implements C.ProxyAdapter
|
|
func (t *Tuic) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
|
|
return t.ListenPacketWithDialer(ctx, dialer.NewDialer(t.Base.DialOptions(opts...)...), metadata)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
return newPacketConn(pc, t), nil
|
|
}
|
|
|
|
// SupportWithDialer implements C.ProxyAdapter
|
|
func (t *Tuic) SupportWithDialer() C.NetWork {
|
|
return C.ALLNet
|
|
}
|
|
|
|
func (t *Tuic) dialWithDialer(ctx context.Context, dialer C.Dialer) (transport *quic.Transport, addr net.Addr, err error) {
|
|
if len(t.option.DialerProxy) > 0 {
|
|
dialer, err = proxydialer.NewByName(t.option.DialerProxy, dialer)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
}
|
|
udpAddr, err := resolveUDPAddrWithPrefer(ctx, "udp", t.addr, t.prefer)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
addr = udpAddr
|
|
var pc net.PacketConn
|
|
pc, err = 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
|
|
}
|
|
|
|
func NewTuic(option TuicOption) (*Tuic, error) {
|
|
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
|
serverName := option.Server
|
|
tlsConfig := &tls.Config{
|
|
ServerName: serverName,
|
|
InsecureSkipVerify: option.SkipCertVerify,
|
|
MinVersion: tls.VersionTLS13,
|
|
}
|
|
if option.SNI != "" {
|
|
tlsConfig.ServerName = option.SNI
|
|
}
|
|
|
|
var bs []byte
|
|
var err error
|
|
if len(option.CustomCA) > 0 {
|
|
bs, err = os.ReadFile(option.CustomCA)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("tuic %s load ca error: %w", addr, err)
|
|
}
|
|
} else if option.CustomCAString != "" {
|
|
bs = []byte(option.CustomCAString)
|
|
}
|
|
|
|
if len(bs) > 0 {
|
|
block, _ := pem.Decode(bs)
|
|
if block == nil {
|
|
return nil, fmt.Errorf("CA cert is not PEM")
|
|
}
|
|
|
|
fpBytes := sha256.Sum256(block.Bytes)
|
|
if len(option.Fingerprint) == 0 {
|
|
option.Fingerprint = hex.EncodeToString(fpBytes[:])
|
|
}
|
|
}
|
|
|
|
if len(option.Fingerprint) != 0 {
|
|
var err error
|
|
tlsConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
tlsConfig = tlsC.GetGlobalTLSConfig(tlsConfig)
|
|
}
|
|
|
|
if option.ALPN != nil { // structure's Decode will ensure value not nil when input has value even it was set an empty array
|
|
tlsConfig.NextProtos = option.ALPN
|
|
} else {
|
|
tlsConfig.NextProtos = []string{"h3"}
|
|
}
|
|
|
|
if option.RequestTimeout == 0 {
|
|
option.RequestTimeout = 8000
|
|
}
|
|
|
|
if option.HeartbeatInterval <= 0 {
|
|
option.HeartbeatInterval = 10000
|
|
}
|
|
|
|
udpRelayMode := tuic.QUIC
|
|
if option.UdpRelayMode != "quic" {
|
|
udpRelayMode = tuic.NATIVE
|
|
}
|
|
|
|
if option.MaxUdpRelayPacketSize == 0 {
|
|
option.MaxUdpRelayPacketSize = 1252
|
|
}
|
|
|
|
if option.MaxOpenStreams == 0 {
|
|
option.MaxOpenStreams = 100
|
|
}
|
|
|
|
if option.CWND == 0 {
|
|
option.CWND = 32
|
|
}
|
|
|
|
packetOverHead := tuic.PacketOverHeadV4
|
|
if len(option.Token) == 0 {
|
|
packetOverHead = tuic.PacketOverHeadV5
|
|
}
|
|
|
|
if option.MaxDatagramFrameSize == 0 {
|
|
option.MaxDatagramFrameSize = option.MaxUdpRelayPacketSize + packetOverHead
|
|
}
|
|
|
|
if option.MaxDatagramFrameSize > 1400 {
|
|
option.MaxDatagramFrameSize = 1400
|
|
}
|
|
option.MaxUdpRelayPacketSize = option.MaxDatagramFrameSize - packetOverHead
|
|
|
|
// ensure server's incoming stream can handle correctly, increase to 1.1x
|
|
quicMaxOpenStreams := int64(option.MaxOpenStreams)
|
|
quicMaxOpenStreams = quicMaxOpenStreams + int64(math.Ceil(float64(quicMaxOpenStreams)/10.0))
|
|
quicConfig := &quic.Config{
|
|
InitialStreamReceiveWindow: uint64(option.ReceiveWindowConn),
|
|
MaxStreamReceiveWindow: uint64(option.ReceiveWindowConn),
|
|
InitialConnectionReceiveWindow: uint64(option.ReceiveWindow),
|
|
MaxConnectionReceiveWindow: uint64(option.ReceiveWindow),
|
|
MaxIncomingStreams: quicMaxOpenStreams,
|
|
MaxIncomingUniStreams: quicMaxOpenStreams,
|
|
KeepAlivePeriod: time.Duration(option.HeartbeatInterval) * time.Millisecond,
|
|
DisablePathMTUDiscovery: option.DisableMTUDiscovery,
|
|
MaxDatagramFrameSize: int64(option.MaxDatagramFrameSize),
|
|
EnableDatagrams: true,
|
|
}
|
|
if option.ReceiveWindowConn == 0 {
|
|
quicConfig.InitialStreamReceiveWindow = tuic.DefaultStreamReceiveWindow / 10
|
|
quicConfig.MaxStreamReceiveWindow = tuic.DefaultStreamReceiveWindow
|
|
}
|
|
if option.ReceiveWindow == 0 {
|
|
quicConfig.InitialConnectionReceiveWindow = tuic.DefaultConnectionReceiveWindow / 10
|
|
quicConfig.MaxConnectionReceiveWindow = tuic.DefaultConnectionReceiveWindow
|
|
}
|
|
|
|
if len(option.Ip) > 0 {
|
|
addr = net.JoinHostPort(option.Ip, strconv.Itoa(option.Port))
|
|
}
|
|
if option.DisableSni {
|
|
tlsConfig.ServerName = ""
|
|
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,
|
|
addr: addr,
|
|
tp: C.Tuic,
|
|
udp: true,
|
|
tfo: option.FastOpen,
|
|
iface: option.Interface,
|
|
rmark: option.RoutingMark,
|
|
prefer: C.NewDNSPrefer(option.IPVersion),
|
|
},
|
|
option: &option,
|
|
}
|
|
|
|
clientMaxOpenStreams := int64(option.MaxOpenStreams)
|
|
|
|
// to avoid tuic's "too many open streams", decrease to 0.9x
|
|
if clientMaxOpenStreams == 100 {
|
|
clientMaxOpenStreams = clientMaxOpenStreams - int64(math.Ceil(float64(clientMaxOpenStreams)/10.0))
|
|
}
|
|
|
|
if clientMaxOpenStreams < 1 {
|
|
clientMaxOpenStreams = 1
|
|
}
|
|
|
|
if len(option.Token) > 0 {
|
|
tkn := tuic.GenTKN(option.Token)
|
|
clientOption := &tuic.ClientOptionV4{
|
|
TlsConfig: tlsConfig,
|
|
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)
|
|
} else {
|
|
clientOption := &tuic.ClientOptionV5{
|
|
TlsConfig: tlsConfig,
|
|
QuicConfig: quicConfig,
|
|
Uuid: uuid.FromStringOrNil(option.UUID),
|
|
Password: option.Password,
|
|
UdpRelayMode: udpRelayMode,
|
|
CongestionController: option.CongestionController,
|
|
ReduceRtt: option.ReduceRtt,
|
|
MaxUdpRelayPacketSize: option.MaxUdpRelayPacketSize,
|
|
MaxOpenStreams: clientMaxOpenStreams,
|
|
CWND: option.CWND,
|
|
}
|
|
|
|
t.client = tuic.NewPoolClientV5(clientOption)
|
|
}
|
|
|
|
return t, nil
|
|
}
|