321 lines
9.2 KiB
Go
321 lines
9.2 KiB
Go
package outbound
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"crypto/tls"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
"os"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/metacubex/quic-go"
|
|
"github.com/metacubex/quic-go/congestion"
|
|
M "github.com/sagernet/sing/common/metadata"
|
|
|
|
"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"
|
|
"github.com/Dreamacro/clash/log"
|
|
hyCongestion "github.com/Dreamacro/clash/transport/hysteria/congestion"
|
|
"github.com/Dreamacro/clash/transport/hysteria/core"
|
|
"github.com/Dreamacro/clash/transport/hysteria/obfs"
|
|
"github.com/Dreamacro/clash/transport/hysteria/pmtud_fix"
|
|
"github.com/Dreamacro/clash/transport/hysteria/transport"
|
|
"github.com/Dreamacro/clash/transport/hysteria/utils"
|
|
)
|
|
|
|
const (
|
|
mbpsToBps = 125000
|
|
|
|
DefaultStreamReceiveWindow = 15728640 // 15 MB/s
|
|
DefaultConnectionReceiveWindow = 67108864 // 64 MB/s
|
|
|
|
DefaultALPN = "hysteria"
|
|
DefaultProtocol = "udp"
|
|
DefaultHopInterval = 10
|
|
)
|
|
|
|
type Hysteria struct {
|
|
*Base
|
|
|
|
option *HysteriaOption
|
|
client *core.Client
|
|
}
|
|
|
|
func (h *Hysteria) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
|
|
tcpConn, err := h.client.DialTCP(metadata.RemoteAddress(), h.genHdc(ctx, opts...))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return NewConn(tcpConn, h), nil
|
|
}
|
|
|
|
func (h *Hysteria) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
|
|
udpConn, err := h.client.DialUDP(h.genHdc(ctx, opts...))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return newPacketConn(&hyPacketConn{udpConn}, h), nil
|
|
}
|
|
|
|
func (h *Hysteria) genHdc(ctx context.Context, opts ...dialer.Option) utils.PacketDialer {
|
|
return &hyDialerWithContext{
|
|
ctx: context.Background(),
|
|
hyDialer: func(network string) (net.PacketConn, error) {
|
|
var err error
|
|
var cDialer C.Dialer = dialer.NewDialer(h.Base.DialOptions(opts...)...)
|
|
if len(h.option.DialerProxy) > 0 {
|
|
cDialer, err = proxydialer.NewByName(h.option.DialerProxy, cDialer)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
rAddrPort, _ := netip.ParseAddrPort(h.Addr())
|
|
return cDialer.ListenPacket(ctx, network, "", rAddrPort)
|
|
},
|
|
remoteAddr: func(addr string) (net.Addr, error) {
|
|
return resolveUDPAddrWithPrefer(ctx, "udp", addr, h.prefer)
|
|
},
|
|
}
|
|
}
|
|
|
|
type HysteriaOption struct {
|
|
BasicOption
|
|
Name string `proxy:"name"`
|
|
Server string `proxy:"server"`
|
|
Port int `proxy:"port,omitempty"`
|
|
Ports string `proxy:"ports,omitempty"`
|
|
Protocol string `proxy:"protocol,omitempty"`
|
|
ObfsProtocol string `proxy:"obfs-protocol,omitempty"` // compatible with Stash
|
|
Up string `proxy:"up"`
|
|
UpSpeed int `proxy:"up-speed,omitempty"` // compatible with Stash
|
|
Down string `proxy:"down"`
|
|
DownSpeed int `proxy:"down-speed,omitempty"` // compatible with Stash
|
|
Auth string `proxy:"auth,omitempty"`
|
|
AuthString string `proxy:"auth-str,omitempty"`
|
|
Obfs string `proxy:"obfs,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"`
|
|
ReceiveWindowConn int `proxy:"recv-window-conn,omitempty"`
|
|
ReceiveWindow int `proxy:"recv-window,omitempty"`
|
|
DisableMTUDiscovery bool `proxy:"disable-mtu-discovery,omitempty"`
|
|
FastOpen bool `proxy:"fast-open,omitempty"`
|
|
HopInterval int `proxy:"hop-interval,omitempty"`
|
|
}
|
|
|
|
func (c *HysteriaOption) Speed() (uint64, uint64, error) {
|
|
var up, down uint64
|
|
up = StringToBps(c.Up)
|
|
if up == 0 {
|
|
return 0, 0, fmt.Errorf("invaild upload speed: %s", c.Up)
|
|
}
|
|
|
|
down = StringToBps(c.Down)
|
|
if down == 0 {
|
|
return 0, 0, fmt.Errorf("invaild download speed: %s", c.Down)
|
|
}
|
|
|
|
return up, down, nil
|
|
}
|
|
|
|
func NewHysteria(option HysteriaOption) (*Hysteria, error) {
|
|
clientTransport := &transport.ClientTransport{
|
|
Dialer: &net.Dialer{
|
|
Timeout: 8 * time.Second,
|
|
},
|
|
}
|
|
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
|
ports := option.Ports
|
|
|
|
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", 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 len(option.ALPN) > 0 {
|
|
tlsConfig.NextProtos = option.ALPN
|
|
} else {
|
|
tlsConfig.NextProtos = []string{DefaultALPN}
|
|
}
|
|
quicConfig := &quic.Config{
|
|
InitialStreamReceiveWindow: uint64(option.ReceiveWindowConn),
|
|
MaxStreamReceiveWindow: uint64(option.ReceiveWindowConn),
|
|
InitialConnectionReceiveWindow: uint64(option.ReceiveWindow),
|
|
MaxConnectionReceiveWindow: uint64(option.ReceiveWindow),
|
|
KeepAlivePeriod: 10 * time.Second,
|
|
DisablePathMTUDiscovery: option.DisableMTUDiscovery,
|
|
EnableDatagrams: true,
|
|
}
|
|
if option.ObfsProtocol != "" {
|
|
option.Protocol = option.ObfsProtocol
|
|
}
|
|
if option.Protocol == "" {
|
|
option.Protocol = DefaultProtocol
|
|
}
|
|
if option.HopInterval == 0 {
|
|
option.HopInterval = DefaultHopInterval
|
|
}
|
|
hopInterval := time.Duration(int64(option.HopInterval)) * time.Second
|
|
if option.ReceiveWindow == 0 {
|
|
quicConfig.InitialStreamReceiveWindow = DefaultStreamReceiveWindow / 10
|
|
quicConfig.MaxStreamReceiveWindow = DefaultStreamReceiveWindow
|
|
}
|
|
if option.ReceiveWindow == 0 {
|
|
quicConfig.InitialConnectionReceiveWindow = DefaultConnectionReceiveWindow / 10
|
|
quicConfig.MaxConnectionReceiveWindow = DefaultConnectionReceiveWindow
|
|
}
|
|
if !quicConfig.DisablePathMTUDiscovery && pmtud_fix.DisablePathMTUDiscovery {
|
|
log.Infoln("hysteria: Path MTU Discovery is not yet supported on this platform")
|
|
}
|
|
|
|
var auth = []byte(option.AuthString)
|
|
if option.Auth != "" {
|
|
auth, err = base64.StdEncoding.DecodeString(option.Auth)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
var obfuscator obfs.Obfuscator
|
|
if len(option.Obfs) > 0 {
|
|
obfuscator = obfs.NewXPlusObfuscator([]byte(option.Obfs))
|
|
}
|
|
|
|
up, down, err := option.Speed()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if option.UpSpeed != 0 {
|
|
up = uint64(option.UpSpeed * mbpsToBps)
|
|
}
|
|
if option.DownSpeed != 0 {
|
|
down = uint64(option.DownSpeed * mbpsToBps)
|
|
}
|
|
client, err := core.NewClient(
|
|
addr, ports, option.Protocol, auth, tlsConfig, quicConfig, clientTransport, up, down, func(refBPS uint64) congestion.CongestionControl {
|
|
return hyCongestion.NewBrutalSender(congestion.ByteCount(refBPS))
|
|
}, obfuscator, hopInterval, option.FastOpen,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("hysteria %s create error: %w", addr, err)
|
|
}
|
|
return &Hysteria{
|
|
Base: &Base{
|
|
name: option.Name,
|
|
addr: addr,
|
|
tp: C.Hysteria,
|
|
udp: true,
|
|
tfo: option.FastOpen,
|
|
iface: option.Interface,
|
|
rmark: option.RoutingMark,
|
|
prefer: C.NewDNSPrefer(option.IPVersion),
|
|
},
|
|
option: &option,
|
|
client: client,
|
|
}, nil
|
|
}
|
|
|
|
type hyPacketConn struct {
|
|
core.UDPConn
|
|
}
|
|
|
|
func (c *hyPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
|
|
b, addrStr, err := c.UDPConn.ReadFrom()
|
|
if err != nil {
|
|
return
|
|
}
|
|
n = copy(p, b)
|
|
addr = M.ParseSocksaddr(addrStr).UDPAddr()
|
|
return
|
|
}
|
|
|
|
func (c *hyPacketConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) {
|
|
b, addrStr, err := c.UDPConn.ReadFrom()
|
|
if err != nil {
|
|
return
|
|
}
|
|
data = b
|
|
addr = M.ParseSocksaddr(addrStr).UDPAddr()
|
|
return
|
|
}
|
|
|
|
func (c *hyPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
|
|
err = c.UDPConn.WriteTo(p, M.SocksaddrFromNet(addr).String())
|
|
if err != nil {
|
|
return
|
|
}
|
|
n = len(p)
|
|
return
|
|
}
|
|
|
|
type hyDialerWithContext struct {
|
|
hyDialer func(network string) (net.PacketConn, error)
|
|
ctx context.Context
|
|
remoteAddr func(host string) (net.Addr, error)
|
|
}
|
|
|
|
func (h *hyDialerWithContext) ListenPacket(rAddr net.Addr) (net.PacketConn, error) {
|
|
network := "udp"
|
|
if addrPort, err := netip.ParseAddrPort(rAddr.String()); err == nil {
|
|
network = dialer.ParseNetwork(network, addrPort.Addr())
|
|
}
|
|
return h.hyDialer(network)
|
|
}
|
|
|
|
func (h *hyDialerWithContext) Context() context.Context {
|
|
return h.ctx
|
|
}
|
|
|
|
func (h *hyDialerWithContext) RemoteAddr(host string) (net.Addr, error) {
|
|
return h.remoteAddr(host)
|
|
}
|