219 lines
5.7 KiB
Go
219 lines
5.7 KiB
Go
|
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"`
|
||
|
}
|
||
|
|
||
|
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,
|
||
|
}
|
||
|
|
||
|
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
|
||
|
}
|