feat: support Hysteria2
This commit is contained in:
parent
24fd577767
commit
9b8e2d9343
7 changed files with 269 additions and 36 deletions
|
@ -268,42 +268,6 @@ func NewHysteria(option HysteriaOption) (*Hysteria, error) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
func stringToBps(s string) uint64 {
|
||||
if s == "" {
|
||||
return 0
|
||||
}
|
||||
|
||||
// when have not unit, use Mbps
|
||||
if v, err := strconv.Atoi(s); err == nil {
|
||||
return stringToBps(fmt.Sprintf("%d Mbps", v))
|
||||
}
|
||||
|
||||
m := rateStringRegexp.FindStringSubmatch(s)
|
||||
if m == nil {
|
||||
return 0
|
||||
}
|
||||
var n uint64
|
||||
switch m[2] {
|
||||
case "K":
|
||||
n = 1 << 10
|
||||
case "M":
|
||||
n = 1 << 20
|
||||
case "G":
|
||||
n = 1 << 30
|
||||
case "T":
|
||||
n = 1 << 40
|
||||
default:
|
||||
n = 1
|
||||
}
|
||||
v, _ := strconv.ParseUint(m[1], 10, 64)
|
||||
n = v * n
|
||||
if m[3] == "b" {
|
||||
// Bits, need to convert to bytes
|
||||
n = n >> 3
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
type hyPacketConn struct {
|
||||
core.UDPConn
|
||||
}
|
||||
|
|
218
adapter/outbound/hysteria2.go
Normal file
218
adapter/outbound/hysteria2.go
Normal file
|
@ -0,0 +1,218 @@
|
|||
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
|
||||
}
|
|
@ -4,8 +4,10 @@ import (
|
|||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/Dreamacro/clash/component/resolver"
|
||||
|
@ -120,3 +122,39 @@ func safeConnClose(c net.Conn, err error) {
|
|||
_ = c.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func stringToBps(s string) uint64 {
|
||||
if s == "" {
|
||||
return 0
|
||||
}
|
||||
|
||||
// when have not unit, use Mbps
|
||||
if v, err := strconv.Atoi(s); err == nil {
|
||||
return stringToBps(fmt.Sprintf("%d Mbps", v))
|
||||
}
|
||||
|
||||
m := rateStringRegexp.FindStringSubmatch(s)
|
||||
if m == nil {
|
||||
return 0
|
||||
}
|
||||
var n uint64
|
||||
switch m[2] {
|
||||
case "K":
|
||||
n = 1 << 10
|
||||
case "M":
|
||||
n = 1 << 20
|
||||
case "G":
|
||||
n = 1 << 30
|
||||
case "T":
|
||||
n = 1 << 40
|
||||
default:
|
||||
n = 1
|
||||
}
|
||||
v, _ := strconv.ParseUint(m[1], 10, 64)
|
||||
n = v * n
|
||||
if m[3] == "b" {
|
||||
// Bits, need to convert to bytes
|
||||
n = n >> 3
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
|
|
@ -92,6 +92,13 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
|
|||
break
|
||||
}
|
||||
proxy, err = outbound.NewHysteria(*hyOption)
|
||||
case "hysteria2":
|
||||
hyOption := &outbound.Hysteria2Option{}
|
||||
err = decoder.Decode(mapping, hyOption)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
proxy, err = outbound.NewHysteria2(*hyOption)
|
||||
case "wireguard":
|
||||
wgOption := &outbound.WireGuardOption{}
|
||||
err = decoder.Decode(mapping, wgOption)
|
||||
|
|
|
@ -36,6 +36,7 @@ const (
|
|||
Vless
|
||||
Trojan
|
||||
Hysteria
|
||||
Hysteria2
|
||||
WireGuard
|
||||
Tuic
|
||||
)
|
||||
|
@ -200,6 +201,8 @@ func (at AdapterType) String() string {
|
|||
return "Trojan"
|
||||
case Hysteria:
|
||||
return "Hysteria"
|
||||
case Hysteria2:
|
||||
return "Hysteria2"
|
||||
case WireGuard:
|
||||
return "WireGuard"
|
||||
case Tuic:
|
||||
|
|
1
go.mod
1
go.mod
|
@ -20,6 +20,7 @@ require (
|
|||
github.com/mdlayher/netlink v1.7.2
|
||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759
|
||||
github.com/metacubex/quic-go v0.38.1-0.20230909013832-033f6a2115cf
|
||||
github.com/metacubex/sing-quic v0.0.0-20230921015854-1ed89eed54d5
|
||||
github.com/metacubex/sing-shadowsocks v0.2.5
|
||||
github.com/metacubex/sing-shadowsocks2 v0.1.4
|
||||
github.com/metacubex/sing-tun v0.1.12
|
||||
|
|
2
go.sum
2
go.sum
|
@ -99,6 +99,8 @@ github.com/metacubex/quic-go v0.38.1-0.20230909013832-033f6a2115cf h1:hflzPbb2M+
|
|||
github.com/metacubex/quic-go v0.38.1-0.20230909013832-033f6a2115cf/go.mod h1:7RCcKJJk1DMeNQQNnYKS+7FqftqPfG031oP8jrYRMw8=
|
||||
github.com/metacubex/sing v0.0.0-20230921005553-6eacdd2c7a24 h1:652uMd78eKMU7/sVkW8qqAdZkJaiDoUflfCs5LHvb0Q=
|
||||
github.com/metacubex/sing v0.0.0-20230921005553-6eacdd2c7a24/go.mod h1:GQ673iPfUnkbK/dIPkfd1Xh1MjOGo36gkl/mkiHY7Jg=
|
||||
github.com/metacubex/sing-quic v0.0.0-20230921015854-1ed89eed54d5 h1:XhWilr6vJoXy4n/sP2datek28FTF3s3rWHhezdFFD8s=
|
||||
github.com/metacubex/sing-quic v0.0.0-20230921015854-1ed89eed54d5/go.mod h1:oGpQmqe5tj3sPdPWCNLbBoUSwqd+Z6SqVO7TlMNVnH4=
|
||||
github.com/metacubex/sing-shadowsocks v0.2.5 h1:O2RRSHlKGEpAVG/OHJQxyHqDy8uvvdCW/oW2TDBOIhc=
|
||||
github.com/metacubex/sing-shadowsocks v0.2.5/go.mod h1:Xz2uW9BEYGEoA8B4XEpoxt7ERHClFCwsMAvWaruoyMo=
|
||||
github.com/metacubex/sing-shadowsocks2 v0.1.4 h1:OOCf8lgsVcpTOJUeaFAMzyKVebaQOBnKirDdUdBoKIE=
|
||||
|
|
Loading…
Reference in a new issue