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
|
}, 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 {
|
type hyPacketConn struct {
|
||||||
core.UDPConn
|
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"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/component/resolver"
|
"github.com/Dreamacro/clash/component/resolver"
|
||||||
|
@ -120,3 +122,39 @@ func safeConnClose(c net.Conn, err error) {
|
||||||
_ = c.Close()
|
_ = 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
|
break
|
||||||
}
|
}
|
||||||
proxy, err = outbound.NewHysteria(*hyOption)
|
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":
|
case "wireguard":
|
||||||
wgOption := &outbound.WireGuardOption{}
|
wgOption := &outbound.WireGuardOption{}
|
||||||
err = decoder.Decode(mapping, wgOption)
|
err = decoder.Decode(mapping, wgOption)
|
||||||
|
|
|
@ -36,6 +36,7 @@ const (
|
||||||
Vless
|
Vless
|
||||||
Trojan
|
Trojan
|
||||||
Hysteria
|
Hysteria
|
||||||
|
Hysteria2
|
||||||
WireGuard
|
WireGuard
|
||||||
Tuic
|
Tuic
|
||||||
)
|
)
|
||||||
|
@ -200,6 +201,8 @@ func (at AdapterType) String() string {
|
||||||
return "Trojan"
|
return "Trojan"
|
||||||
case Hysteria:
|
case Hysteria:
|
||||||
return "Hysteria"
|
return "Hysteria"
|
||||||
|
case Hysteria2:
|
||||||
|
return "Hysteria2"
|
||||||
case WireGuard:
|
case WireGuard:
|
||||||
return "WireGuard"
|
return "WireGuard"
|
||||||
case Tuic:
|
case Tuic:
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -20,6 +20,7 @@ require (
|
||||||
github.com/mdlayher/netlink v1.7.2
|
github.com/mdlayher/netlink v1.7.2
|
||||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759
|
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759
|
||||||
github.com/metacubex/quic-go v0.38.1-0.20230909013832-033f6a2115cf
|
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-shadowsocks v0.2.5
|
||||||
github.com/metacubex/sing-shadowsocks2 v0.1.4
|
github.com/metacubex/sing-shadowsocks2 v0.1.4
|
||||||
github.com/metacubex/sing-tun v0.1.12
|
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/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 h1:652uMd78eKMU7/sVkW8qqAdZkJaiDoUflfCs5LHvb0Q=
|
||||||
github.com/metacubex/sing v0.0.0-20230921005553-6eacdd2c7a24/go.mod h1:GQ673iPfUnkbK/dIPkfd1Xh1MjOGo36gkl/mkiHY7Jg=
|
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 h1:O2RRSHlKGEpAVG/OHJQxyHqDy8uvvdCW/oW2TDBOIhc=
|
||||||
github.com/metacubex/sing-shadowsocks v0.2.5/go.mod h1:Xz2uW9BEYGEoA8B4XEpoxt7ERHClFCwsMAvWaruoyMo=
|
github.com/metacubex/sing-shadowsocks v0.2.5/go.mod h1:Xz2uW9BEYGEoA8B4XEpoxt7ERHClFCwsMAvWaruoyMo=
|
||||||
github.com/metacubex/sing-shadowsocks2 v0.1.4 h1:OOCf8lgsVcpTOJUeaFAMzyKVebaQOBnKirDdUdBoKIE=
|
github.com/metacubex/sing-shadowsocks2 v0.1.4 h1:OOCf8lgsVcpTOJUeaFAMzyKVebaQOBnKirDdUdBoKIE=
|
||||||
|
|
Loading…
Reference in a new issue