package outbound import ( "context" "crypto/sha256" "crypto/tls" "encoding/hex" "encoding/pem" "errors" "fmt" "net" "os" "runtime" "strconv" CN "github.com/Dreamacro/clash/common/net" "github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/proxydialer" tlsC "github.com/Dreamacro/clash/component/tls" C "github.com/Dreamacro/clash/constant" tuicCommon "github.com/Dreamacro/clash/transport/tuic/common" "github.com/metacubex/sing-quic/hysteria2" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) func init() { hysteria2.SetCongestionController = tuicCommon.SetCongestionController } type Hysteria2 struct { *Base option *Hysteria2Option client *hysteria2.Client dialer *hy2SingDialer } type Hysteria2Option struct { BasicOption Name string `proxy:"name"` Server string `proxy:"server"` Port int `proxy:"port"` Up string `proxy:"up,omitempty"` Down string `proxy:"down,omitempty"` Password string `proxy:"password,omitempty"` Obfs string `proxy:"obfs,omitempty"` ObfsPassword string `proxy:"obfs-password,omitempty"` SNI string `proxy:"sni,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` Fingerprint string `proxy:"fingerprint,omitempty"` ALPN []string `proxy:"alpn,omitempty"` CustomCA string `proxy:"ca,omitempty"` CustomCAString string `proxy:"ca-str,omitempty"` CWND int `proxy:"cwnd,omitempty"` } type hy2SingDialer struct { dialer dialer.Dialer proxyName string } var _ N.Dialer = (*hy2SingDialer)(nil) func (d *hy2SingDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { var cDialer C.Dialer = d.dialer if len(d.proxyName) > 0 { pd, err := proxydialer.NewByName(d.proxyName, d.dialer) if err != nil { return nil, err } cDialer = pd } return cDialer.DialContext(ctx, network, destination.String()) } func (d *hy2SingDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { var cDialer C.Dialer = d.dialer if len(d.proxyName) > 0 { pd, err := proxydialer.NewByName(d.proxyName, d.dialer) if err != nil { return nil, err } cDialer = pd } return cDialer.ListenPacket(ctx, "udp", "", destination.AddrPort()) } func (h *Hysteria2) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { options := h.Base.DialOptions(opts...) h.dialer.dialer = dialer.NewDialer(options...) c, err := h.client.DialConn(ctx, M.ParseSocksaddr(metadata.RemoteAddress())) if err != nil { return nil, err } return NewConn(CN.NewRefConn(c, h), h), nil } func (h *Hysteria2) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) { options := h.Base.DialOptions(opts...) h.dialer.dialer = dialer.NewDialer(options...) pc, err := h.client.ListenPacket(ctx) if err != nil { return nil, err } if pc == nil { return nil, errors.New("packetConn is nil") } return newPacketConn(CN.NewRefPacketConn(CN.NewThreadSafePacketConn(pc), h), h), nil } func closeHysteria2(h *Hysteria2) { if h.client != nil { _ = h.client.CloseWithError(errors.New("proxy removed")) } } func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) { addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) var salamanderPassword string if len(option.Obfs) > 0 { if option.ObfsPassword == "" { return nil, errors.New("missing obfs password") } switch option.Obfs { case hysteria2.ObfsTypeSalamander: salamanderPassword = option.ObfsPassword default: return nil, fmt.Errorf("unknown obfs type: %s", option.Obfs) } } serverName := option.Server if option.SNI != "" { serverName = option.SNI } tlsConfig := &tls.Config{ ServerName: serverName, InsecureSkipVerify: option.SkipCertVerify, MinVersion: tls.VersionTLS13, } var bs []byte var err error if len(option.CustomCA) > 0 { bs, err = os.ReadFile(option.CustomCA) if err != nil { return nil, fmt.Errorf("hysteria %s load ca error: %w", option.Name, 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 len(option.ALPN) > 0 { tlsConfig.NextProtos = option.ALPN } singDialer := &hy2SingDialer{dialer: dialer.NewDialer(), proxyName: option.DialerProxy} clientOptions := hysteria2.ClientOptions{ Context: context.TODO(), Dialer: singDialer, ServerAddress: M.ParseSocksaddrHostPort(option.Server, uint16(option.Port)), SendBPS: StringToBps(option.Up), ReceiveBPS: StringToBps(option.Down), SalamanderPassword: salamanderPassword, Password: option.Password, TLSConfig: tlsConfig, UDPDisabled: false, CWND: option.CWND, } client, err := hysteria2.NewClient(clientOptions) if err != nil { return nil, err } outbound := &Hysteria2{ Base: &Base{ name: option.Name, addr: addr, tp: C.Hysteria2, udp: true, iface: option.Interface, rmark: option.RoutingMark, prefer: C.NewDNSPrefer(option.IPVersion), }, option: &option, client: client, dialer: singDialer, } runtime.SetFinalizer(outbound, closeHysteria2) return outbound, nil }