mirror of
https://github.com/SagerNet/sing-box.git
synced 2025-06-08 19:54:12 +08:00
Add hysteria tcp client
This commit is contained in:
parent
3dfa99efe1
commit
17f10e0d3a
@ -15,6 +15,7 @@ const (
|
|||||||
TypeTrojan = "trojan"
|
TypeTrojan = "trojan"
|
||||||
TypeNaive = "naive"
|
TypeNaive = "naive"
|
||||||
TypeWireGuard = "wireguard"
|
TypeWireGuard = "wireguard"
|
||||||
|
TypeHysteria = "hysteria"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
23
option/hysteria.go
Normal file
23
option/hysteria.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package option
|
||||||
|
|
||||||
|
type HysteriaOutboundOptions struct {
|
||||||
|
OutboundDialerOptions
|
||||||
|
ServerOptions
|
||||||
|
Protocol string `json:"protocol"`
|
||||||
|
Up string `json:"up"`
|
||||||
|
UpMbps int `json:"up_mbps"`
|
||||||
|
Down string `json:"down"`
|
||||||
|
DownMbps int `json:"down_mbps"`
|
||||||
|
Obfs string `json:"obfs"`
|
||||||
|
Auth []byte `json:"auth"`
|
||||||
|
AuthString string `json:"auth_str"`
|
||||||
|
ALPN string `json:"alpn"`
|
||||||
|
ServerName string `json:"server_name"`
|
||||||
|
Insecure bool `json:"insecure"`
|
||||||
|
CustomCA string `json:"ca"`
|
||||||
|
CustomCAStr string `json:"ca_str"`
|
||||||
|
ReceiveWindowConn uint64 `json:"recv_window_conn"`
|
||||||
|
ReceiveWindow uint64 `json:"recv_window"`
|
||||||
|
DisableMTUDiscovery bool `json:"disable_mtu_discovery"`
|
||||||
|
Network NetworkList `json:"network,omitempty"`
|
||||||
|
}
|
@ -17,6 +17,7 @@ type _Outbound struct {
|
|||||||
VMessOptions VMessOutboundOptions `json:"-"`
|
VMessOptions VMessOutboundOptions `json:"-"`
|
||||||
TrojanOptions TrojanOutboundOptions `json:"-"`
|
TrojanOptions TrojanOutboundOptions `json:"-"`
|
||||||
WireGuardOptions WireGuardOutboundOptions `json:"-"`
|
WireGuardOptions WireGuardOutboundOptions `json:"-"`
|
||||||
|
HysteriaOutbound HysteriaOutboundOptions `json:"-"`
|
||||||
SelectorOptions SelectorOutboundOptions `json:"-"`
|
SelectorOptions SelectorOutboundOptions `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,6 +42,8 @@ func (h Outbound) MarshalJSON() ([]byte, error) {
|
|||||||
v = h.TrojanOptions
|
v = h.TrojanOptions
|
||||||
case C.TypeWireGuard:
|
case C.TypeWireGuard:
|
||||||
v = h.WireGuardOptions
|
v = h.WireGuardOptions
|
||||||
|
case C.TypeHysteria:
|
||||||
|
v = h.HysteriaOutbound
|
||||||
case C.TypeSelector:
|
case C.TypeSelector:
|
||||||
v = h.SelectorOptions
|
v = h.SelectorOptions
|
||||||
default:
|
default:
|
||||||
@ -72,6 +75,8 @@ func (h *Outbound) UnmarshalJSON(bytes []byte) error {
|
|||||||
v = &h.TrojanOptions
|
v = &h.TrojanOptions
|
||||||
case C.TypeWireGuard:
|
case C.TypeWireGuard:
|
||||||
v = &h.WireGuardOptions
|
v = &h.WireGuardOptions
|
||||||
|
case C.TypeHysteria:
|
||||||
|
v = &h.HysteriaOutbound
|
||||||
case C.TypeSelector:
|
case C.TypeSelector:
|
||||||
v = &h.SelectorOptions
|
v = &h.SelectorOptions
|
||||||
default:
|
default:
|
||||||
|
@ -33,6 +33,8 @@ func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, o
|
|||||||
return NewTrojan(ctx, router, logger, options.Tag, options.TrojanOptions)
|
return NewTrojan(ctx, router, logger, options.Tag, options.TrojanOptions)
|
||||||
case C.TypeWireGuard:
|
case C.TypeWireGuard:
|
||||||
return NewWireGuard(ctx, router, logger, options.Tag, options.WireGuardOptions)
|
return NewWireGuard(ctx, router, logger, options.Tag, options.WireGuardOptions)
|
||||||
|
case C.TypeHysteria:
|
||||||
|
return NewHysteria(ctx, router, logger, options.Tag, options.HysteriaOutbound)
|
||||||
case C.TypeSelector:
|
case C.TypeSelector:
|
||||||
return NewSelector(router, logger, options.Tag, options.SelectorOptions)
|
return NewSelector(router, logger, options.Tag, options.SelectorOptions)
|
||||||
default:
|
default:
|
||||||
|
253
outbound/hysteria.go
Normal file
253
outbound/hysteria.go
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
//go:build with_quic
|
||||||
|
|
||||||
|
package outbound
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/common/dialer"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing-box/transport/hysteria"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
"github.com/sagernet/sing/common/bufio"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
|
||||||
|
"github.com/lucas-clemente/quic-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
hyMbpsToBps = 125000
|
||||||
|
hyMinSpeedBPS = 16384
|
||||||
|
hyDefaultStreamReceiveWindow = 15728640 // 15 MB/s
|
||||||
|
hyDefaultConnectionReceiveWindow = 67108864 // 64 MB/s
|
||||||
|
hyDefaultMaxIncomingStreams = 1024
|
||||||
|
hyDefaultALPN = "hysteria"
|
||||||
|
hyKeepAlivePeriod = 10 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ adapter.Outbound = (*Hysteria)(nil)
|
||||||
|
|
||||||
|
type Hysteria struct {
|
||||||
|
myOutboundAdapter
|
||||||
|
ctx context.Context
|
||||||
|
dialer N.Dialer
|
||||||
|
serverAddr M.Socksaddr
|
||||||
|
tlsConfig *tls.Config
|
||||||
|
quicConfig *quic.Config
|
||||||
|
authKey []byte
|
||||||
|
xplusKey []byte
|
||||||
|
sendBPS uint64
|
||||||
|
recvBPS uint64
|
||||||
|
connAccess sync.Mutex
|
||||||
|
conn quic.Connection
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHysteria(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HysteriaOutboundOptions) (*Hysteria, error) {
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
ServerName: options.ServerName,
|
||||||
|
InsecureSkipVerify: options.Insecure,
|
||||||
|
MinVersion: tls.VersionTLS13,
|
||||||
|
}
|
||||||
|
if options.ALPN != "" {
|
||||||
|
tlsConfig.NextProtos = []string{options.ALPN}
|
||||||
|
} else {
|
||||||
|
tlsConfig.NextProtos = []string{hyDefaultALPN}
|
||||||
|
}
|
||||||
|
var ca []byte
|
||||||
|
var err error
|
||||||
|
if options.CustomCA != "" {
|
||||||
|
ca, err = os.ReadFile(options.CustomCA)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if options.CustomCAStr != "" {
|
||||||
|
ca = []byte(options.CustomCAStr)
|
||||||
|
}
|
||||||
|
if len(ca) > 0 {
|
||||||
|
cp := x509.NewCertPool()
|
||||||
|
if !cp.AppendCertsFromPEM(ca) {
|
||||||
|
return nil, E.New("parse ca failed")
|
||||||
|
}
|
||||||
|
tlsConfig.RootCAs = cp
|
||||||
|
}
|
||||||
|
quicConfig := &quic.Config{
|
||||||
|
InitialStreamReceiveWindow: options.ReceiveWindowConn,
|
||||||
|
MaxStreamReceiveWindow: options.ReceiveWindowConn,
|
||||||
|
InitialConnectionReceiveWindow: options.ReceiveWindow,
|
||||||
|
MaxConnectionReceiveWindow: options.ReceiveWindow,
|
||||||
|
KeepAlivePeriod: hyKeepAlivePeriod,
|
||||||
|
DisablePathMTUDiscovery: options.DisableMTUDiscovery,
|
||||||
|
EnableDatagrams: true,
|
||||||
|
}
|
||||||
|
if options.ReceiveWindowConn == 0 {
|
||||||
|
quicConfig.InitialStreamReceiveWindow = hyDefaultStreamReceiveWindow
|
||||||
|
quicConfig.MaxStreamReceiveWindow = hyDefaultStreamReceiveWindow
|
||||||
|
}
|
||||||
|
if options.ReceiveWindow == 0 {
|
||||||
|
quicConfig.InitialConnectionReceiveWindow = hyDefaultConnectionReceiveWindow
|
||||||
|
quicConfig.MaxConnectionReceiveWindow = hyDefaultConnectionReceiveWindow
|
||||||
|
}
|
||||||
|
if quicConfig.MaxIncomingStreams == 0 {
|
||||||
|
quicConfig.MaxIncomingStreams = hyDefaultMaxIncomingStreams
|
||||||
|
}
|
||||||
|
var auth []byte
|
||||||
|
if len(options.Auth) > 0 {
|
||||||
|
auth = options.Auth
|
||||||
|
} else {
|
||||||
|
auth = []byte(options.AuthString)
|
||||||
|
}
|
||||||
|
var xplus []byte
|
||||||
|
if options.Obfs != "" {
|
||||||
|
xplus = []byte(options.Obfs)
|
||||||
|
}
|
||||||
|
var up, down uint64
|
||||||
|
if len(options.Up) > 0 {
|
||||||
|
up = hysteria.StringToBps(options.Up)
|
||||||
|
if up == 0 {
|
||||||
|
return nil, E.New("invalid up speed format: ", options.Up)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
up = uint64(options.UpMbps) * hyMbpsToBps
|
||||||
|
}
|
||||||
|
if len(options.Down) > 0 {
|
||||||
|
down = hysteria.StringToBps(options.Down)
|
||||||
|
if down == 0 {
|
||||||
|
return nil, E.New("invalid down speed format: ", options.Down)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
down = uint64(options.DownMbps) * hyMbpsToBps
|
||||||
|
}
|
||||||
|
if up < hyMinSpeedBPS {
|
||||||
|
return nil, E.New("invalid up speed")
|
||||||
|
}
|
||||||
|
if down < hyMinSpeedBPS {
|
||||||
|
return nil, E.New("invalid down speed")
|
||||||
|
}
|
||||||
|
return &Hysteria{
|
||||||
|
myOutboundAdapter: myOutboundAdapter{
|
||||||
|
protocol: C.TypeHysteria,
|
||||||
|
network: options.Network.Build(),
|
||||||
|
router: router,
|
||||||
|
logger: logger,
|
||||||
|
tag: tag,
|
||||||
|
},
|
||||||
|
ctx: ctx,
|
||||||
|
dialer: dialer.NewOutbound(router, options.OutboundDialerOptions),
|
||||||
|
serverAddr: options.ServerOptions.Build(),
|
||||||
|
tlsConfig: tlsConfig,
|
||||||
|
quicConfig: quicConfig,
|
||||||
|
authKey: auth,
|
||||||
|
xplusKey: xplus,
|
||||||
|
sendBPS: up,
|
||||||
|
recvBPS: down,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hysteria) offer(ctx context.Context) (quic.Connection, error) {
|
||||||
|
conn := h.conn
|
||||||
|
if conn != nil && !common.Done(conn.Context()) {
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
h.connAccess.Lock()
|
||||||
|
defer h.connAccess.Unlock()
|
||||||
|
conn = h.conn
|
||||||
|
if conn != nil && !common.Done(conn.Context()) {
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
conn, err := h.offerNew(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
h.conn = conn
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hysteria) offerNew(ctx context.Context) (quic.Connection, error) {
|
||||||
|
udpConn, err := h.dialer.DialContext(h.ctx, "udp", h.serverAddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var packetConn net.PacketConn
|
||||||
|
packetConn = bufio.NewUnbindPacketConn(udpConn)
|
||||||
|
if h.xplusKey != nil {
|
||||||
|
packetConn = hysteria.NewXPlusPacketConn(packetConn, h.xplusKey)
|
||||||
|
}
|
||||||
|
packetConn = &hysteria.WrapPacketConn{PacketConn: packetConn}
|
||||||
|
quicConn, err := quic.Dial(packetConn, udpConn.RemoteAddr(), h.serverAddr.AddrString(), h.tlsConfig, h.quicConfig)
|
||||||
|
if err != nil {
|
||||||
|
packetConn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
controlStream, err := quicConn.OpenStreamSync(ctx)
|
||||||
|
if err != nil {
|
||||||
|
packetConn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = hysteria.WriteClientHello(controlStream, hysteria.ClientHello{
|
||||||
|
SendBPS: h.sendBPS,
|
||||||
|
RecvBPS: h.recvBPS,
|
||||||
|
Auth: h.authKey,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "write hysteria client hello")
|
||||||
|
}
|
||||||
|
serverHello, err := hysteria.ReadServerHello(controlStream)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !serverHello.OK {
|
||||||
|
return nil, E.New("remote error: ", serverHello.Message)
|
||||||
|
}
|
||||||
|
// TODO: set congestion control
|
||||||
|
return quicConn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hysteria) Close() error {
|
||||||
|
h.connAccess.Lock()
|
||||||
|
defer h.connAccess.Unlock()
|
||||||
|
if h.conn != nil {
|
||||||
|
h.conn.CloseWithError(0, "")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hysteria) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||||
|
conn, err := h.offer(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
stream, err := conn.OpenStream()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch N.NetworkName(network) {
|
||||||
|
case N.NetworkTCP:
|
||||||
|
return hysteria.NewClientConn(stream, destination), nil
|
||||||
|
default:
|
||||||
|
return nil, E.New("unsupported network: ", network)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hysteria) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||||
|
return nil, os.ErrInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hysteria) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
|
||||||
|
return NewConnection(ctx, h, conn, metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hysteria) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
|
||||||
|
return NewPacketConnection(ctx, h, conn, metadata)
|
||||||
|
}
|
16
outbound/hysteria_stub.go
Normal file
16
outbound/hysteria_stub.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
//go:build !with_quic
|
||||||
|
|
||||||
|
package outbound
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewHysteria(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HysteriaOutboundOptions) (adapter.Outbound, error) {
|
||||||
|
return nil, E.New(`QUIC is not included in this build, rebuild with -tags with_quic`)
|
||||||
|
}
|
@ -35,14 +35,6 @@ func startInstance(t *testing.T, options option.Options) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func testTCP(t *testing.T, clientPort uint16, testPort uint16) {
|
|
||||||
dialer := socks.NewClient(N.SystemDialer, M.ParseSocksaddrHostPort("127.0.0.1", clientPort), socks.Version5, "", "")
|
|
||||||
dialTCP := func() (net.Conn, error) {
|
|
||||||
return dialer.DialContext(context.Background(), "tcp", M.ParseSocksaddrHostPort("127.0.0.1", testPort))
|
|
||||||
}
|
|
||||||
require.NoError(t, testPingPongWithConn(t, testPort, dialTCP))
|
|
||||||
}
|
|
||||||
|
|
||||||
func testSuit(t *testing.T, clientPort uint16, testPort uint16) {
|
func testSuit(t *testing.T, clientPort uint16, testPort uint16) {
|
||||||
dialer := socks.NewClient(N.SystemDialer, M.ParseSocksaddrHostPort("127.0.0.1", clientPort), socks.Version5, "", "")
|
dialer := socks.NewClient(N.SystemDialer, M.ParseSocksaddrHostPort("127.0.0.1", clientPort), socks.Version5, "", "")
|
||||||
dialTCP := func() (net.Conn, error) {
|
dialTCP := func() (net.Conn, error) {
|
||||||
@ -51,36 +43,23 @@ func testSuit(t *testing.T, clientPort uint16, testPort uint16) {
|
|||||||
dialUDP := func() (net.PacketConn, error) {
|
dialUDP := func() (net.PacketConn, error) {
|
||||||
return dialer.ListenPacket(context.Background(), M.ParseSocksaddrHostPort("127.0.0.1", testPort))
|
return dialer.ListenPacket(context.Background(), M.ParseSocksaddrHostPort("127.0.0.1", testPort))
|
||||||
}
|
}
|
||||||
/*t.Run("tcp", func(t *testing.T) {
|
// require.NoError(t, testPingPongWithConn(t, testPort, dialTCP))
|
||||||
t.Parallel()
|
// require.NoError(t, testPingPongWithPacketConn(t, testPort, dialUDP))
|
||||||
var err error
|
|
||||||
for retry := 0; retry < 3; retry++ {
|
|
||||||
err = testLargeDataWithConn(t, testPort, dialTCP)
|
|
||||||
if err == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
require.NoError(t, err)
|
|
||||||
})
|
|
||||||
t.Run("udp", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
var err error
|
|
||||||
for retry := 0; retry < 3; retry++ {
|
|
||||||
err = testLargeDataWithPacketConn(t, testPort, dialUDP)
|
|
||||||
if err == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
require.NoError(t, err)
|
|
||||||
})*/
|
|
||||||
//require.NoError(t, testPingPongWithConn(t, testPort, dialTCP))
|
|
||||||
//require.NoError(t, testPingPongWithPacketConn(t, testPort, dialUDP))
|
|
||||||
require.NoError(t, testLargeDataWithConn(t, testPort, dialTCP))
|
require.NoError(t, testLargeDataWithConn(t, testPort, dialTCP))
|
||||||
require.NoError(t, testLargeDataWithPacketConn(t, testPort, dialUDP))
|
require.NoError(t, testLargeDataWithPacketConn(t, testPort, dialUDP))
|
||||||
|
|
||||||
// require.NoError(t, testPacketConnTimeout(t, dialUDP))
|
// require.NoError(t, testPacketConnTimeout(t, dialUDP))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testTCP(t *testing.T, clientPort uint16, testPort uint16) {
|
||||||
|
dialer := socks.NewClient(N.SystemDialer, M.ParseSocksaddrHostPort("127.0.0.1", clientPort), socks.Version5, "", "")
|
||||||
|
dialTCP := func() (net.Conn, error) {
|
||||||
|
return dialer.DialContext(context.Background(), "tcp", M.ParseSocksaddrHostPort("127.0.0.1", testPort))
|
||||||
|
}
|
||||||
|
require.NoError(t, testPingPongWithConn(t, testPort, dialTCP))
|
||||||
|
require.NoError(t, testLargeDataWithConn(t, testPort, dialTCP))
|
||||||
|
}
|
||||||
|
|
||||||
func testSuitWg(t *testing.T, clientPort uint16, testPort uint16) {
|
func testSuitWg(t *testing.T, clientPort uint16, testPort uint16) {
|
||||||
dialer := socks.NewClient(N.SystemDialer, M.ParseSocksaddrHostPort("127.0.0.1", clientPort), socks.Version5, "", "")
|
dialer := socks.NewClient(N.SystemDialer, M.ParseSocksaddrHostPort("127.0.0.1", clientPort), socks.Version5, "", "")
|
||||||
dialTCP := func() (net.Conn, error) {
|
dialTCP := func() (net.Conn, error) {
|
||||||
|
@ -32,6 +32,7 @@ const (
|
|||||||
ImageTrojan = "trojangfw/trojan:latest"
|
ImageTrojan = "trojangfw/trojan:latest"
|
||||||
ImageNaive = "pocat/naiveproxy:client"
|
ImageNaive = "pocat/naiveproxy:client"
|
||||||
ImageBoringTun = "ghcr.io/ntkme/boringtun:edge"
|
ImageBoringTun = "ghcr.io/ntkme/boringtun:edge"
|
||||||
|
ImageHysteria = "tobyxdd/hysteria:latest"
|
||||||
)
|
)
|
||||||
|
|
||||||
var allImages = []string{
|
var allImages = []string{
|
||||||
@ -41,6 +42,7 @@ var allImages = []string{
|
|||||||
ImageTrojan,
|
ImageTrojan,
|
||||||
ImageNaive,
|
ImageNaive,
|
||||||
ImageBoringTun,
|
ImageBoringTun,
|
||||||
|
ImageHysteria,
|
||||||
}
|
}
|
||||||
|
|
||||||
var localIP = netip.MustParseAddr("127.0.0.1")
|
var localIP = netip.MustParseAddr("127.0.0.1")
|
||||||
|
9
test/config/hysteria.json
Normal file
9
test/config/hysteria.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"listen": ":10000",
|
||||||
|
"cert": "/etc/hysteria/cert.pem",
|
||||||
|
"key": "/etc/hysteria/key.pem",
|
||||||
|
"auth_str": "password",
|
||||||
|
"obfs": "fuck me till the daylight",
|
||||||
|
"up_mbps": 100,
|
||||||
|
"down_mbps": 100
|
||||||
|
}
|
60
test/hysteria_test.go
Normal file
60
test/hysteria_test.go
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/netip"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHysteriaOutbound(t *testing.T) {
|
||||||
|
if !C.QUIC_AVAILABLE {
|
||||||
|
t.Skip("QUIC not included")
|
||||||
|
}
|
||||||
|
caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
|
||||||
|
startDockerContainer(t, DockerOptions{
|
||||||
|
Image: ImageHysteria,
|
||||||
|
Ports: []uint16{serverPort, testPort},
|
||||||
|
Cmd: []string{"-c", "/etc/hysteria/config.json", "server"},
|
||||||
|
Bind: map[string]string{
|
||||||
|
"hysteria.json": "/etc/hysteria/config.json",
|
||||||
|
certPem: "/etc/hysteria/cert.pem",
|
||||||
|
keyPem: "/etc/hysteria/key.pem",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
startInstance(t, option.Options{
|
||||||
|
Log: &option.LogOptions{
|
||||||
|
Level: "trace",
|
||||||
|
},
|
||||||
|
Inbounds: []option.Inbound{
|
||||||
|
{
|
||||||
|
Type: C.TypeMixed,
|
||||||
|
MixedOptions: option.HTTPMixedInboundOptions{
|
||||||
|
ListenOptions: option.ListenOptions{
|
||||||
|
Listen: option.ListenAddress(netip.IPv4Unspecified()),
|
||||||
|
ListenPort: clientPort,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Outbounds: []option.Outbound{
|
||||||
|
{
|
||||||
|
Type: C.TypeHysteria,
|
||||||
|
HysteriaOutbound: option.HysteriaOutboundOptions{
|
||||||
|
ServerOptions: option.ServerOptions{
|
||||||
|
Server: "127.0.0.1",
|
||||||
|
ServerPort: serverPort,
|
||||||
|
},
|
||||||
|
UpMbps: 100,
|
||||||
|
DownMbps: 100,
|
||||||
|
AuthString: "password",
|
||||||
|
Obfs: "fuck me till the daylight",
|
||||||
|
CustomCA: caPem,
|
||||||
|
ServerName: "example.org",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
testTCP(t, clientPort, testPort)
|
||||||
|
}
|
69
transport/hysteria/client.go
Normal file
69
transport/hysteria/client.go
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
package hysteria
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
|
||||||
|
"github.com/lucas-clemente/quic-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ net.Conn = (*ClientConn)(nil)
|
||||||
|
|
||||||
|
type ClientConn struct {
|
||||||
|
quic.Stream
|
||||||
|
destination M.Socksaddr
|
||||||
|
requestWritten bool
|
||||||
|
responseRead bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClientConn(stream quic.Stream, destination M.Socksaddr) *ClientConn {
|
||||||
|
return &ClientConn{
|
||||||
|
Stream: stream,
|
||||||
|
destination: destination,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClientConn) Read(b []byte) (n int, err error) {
|
||||||
|
if !c.responseRead {
|
||||||
|
var response *ServerResponse
|
||||||
|
response, err = ReadServerResponse(c.Stream)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !response.OK {
|
||||||
|
return 0, E.New("remote error: " + response.Message)
|
||||||
|
}
|
||||||
|
c.responseRead = true
|
||||||
|
}
|
||||||
|
return c.Stream.Read(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClientConn) Write(b []byte) (n int, err error) {
|
||||||
|
if !c.requestWritten {
|
||||||
|
err = WriteClientRequest(c.Stream, ClientRequest{
|
||||||
|
UDP: false,
|
||||||
|
Host: c.destination.AddrString(),
|
||||||
|
Port: c.destination.Port,
|
||||||
|
}, b)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.requestWritten = true
|
||||||
|
return len(b), nil
|
||||||
|
}
|
||||||
|
return c.Stream.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClientConn) LocalAddr() net.Addr {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClientConn) RemoteAddr() net.Addr {
|
||||||
|
return c.destination.TCPAddr()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClientConn) Upstream() any {
|
||||||
|
return c.Stream
|
||||||
|
}
|
142
transport/hysteria/protocol.go
Normal file
142
transport/hysteria/protocol.go
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
package hysteria
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
"github.com/sagernet/sing/common/buf"
|
||||||
|
)
|
||||||
|
|
||||||
|
const Version = 3
|
||||||
|
|
||||||
|
type ClientHello struct {
|
||||||
|
SendBPS uint64
|
||||||
|
RecvBPS uint64
|
||||||
|
Auth []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServerHello struct {
|
||||||
|
OK bool
|
||||||
|
SendBPS uint64
|
||||||
|
RecvBPS uint64
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClientRequest struct {
|
||||||
|
UDP bool
|
||||||
|
Host string
|
||||||
|
Port uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServerResponse struct {
|
||||||
|
OK bool
|
||||||
|
UDPSessionID uint32
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteClientHello(stream io.Writer, hello ClientHello) error {
|
||||||
|
var requestLen int
|
||||||
|
requestLen += 1 // version
|
||||||
|
requestLen += 8 // sendBPS
|
||||||
|
requestLen += 8 // recvBPS
|
||||||
|
requestLen += 2 // auth len
|
||||||
|
requestLen += len(hello.Auth)
|
||||||
|
_request := buf.StackNewSize(requestLen)
|
||||||
|
defer common.KeepAlive(_request)
|
||||||
|
request := common.Dup(_request)
|
||||||
|
defer request.Release()
|
||||||
|
common.Must(
|
||||||
|
request.WriteByte(Version),
|
||||||
|
binary.Write(request, binary.BigEndian, hello.SendBPS),
|
||||||
|
binary.Write(request, binary.BigEndian, hello.RecvBPS),
|
||||||
|
binary.Write(request, binary.BigEndian, uint16(len(hello.Auth))),
|
||||||
|
common.Error(request.Write(hello.Auth)),
|
||||||
|
)
|
||||||
|
return common.Error(stream.Write(request.Bytes()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadServerHello(stream io.Reader) (*ServerHello, error) {
|
||||||
|
var responseLen int
|
||||||
|
responseLen += 1 // ok
|
||||||
|
responseLen += 8 // sendBPS
|
||||||
|
responseLen += 8 // recvBPS
|
||||||
|
responseLen += 2 // message len
|
||||||
|
_response := buf.StackNewSize(responseLen)
|
||||||
|
defer common.KeepAlive(_response)
|
||||||
|
response := common.Dup(_response)
|
||||||
|
defer response.Release()
|
||||||
|
_, err := response.ReadFullFrom(stream, responseLen)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var serverHello ServerHello
|
||||||
|
serverHello.OK = response.Byte(0) == 1
|
||||||
|
serverHello.SendBPS = binary.BigEndian.Uint64(response.Range(1, 9))
|
||||||
|
serverHello.RecvBPS = binary.BigEndian.Uint64(response.Range(9, 17))
|
||||||
|
messageLen := binary.BigEndian.Uint16(response.Range(17, 19))
|
||||||
|
if messageLen == 0 {
|
||||||
|
return &serverHello, nil
|
||||||
|
}
|
||||||
|
message := make([]byte, messageLen)
|
||||||
|
_, err = io.ReadFull(stream, message)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
serverHello.Message = string(message)
|
||||||
|
return &serverHello, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteClientRequest(stream io.Writer, request ClientRequest, payload []byte) error {
|
||||||
|
var requestLen int
|
||||||
|
requestLen += 1 // udp
|
||||||
|
requestLen += 2 // host len
|
||||||
|
requestLen += len(request.Host)
|
||||||
|
requestLen += 2 // port
|
||||||
|
requestLen += len(payload)
|
||||||
|
_buffer := buf.StackNewSize(requestLen)
|
||||||
|
defer common.KeepAlive(_buffer)
|
||||||
|
buffer := common.Dup(_buffer)
|
||||||
|
defer buffer.Release()
|
||||||
|
if request.UDP {
|
||||||
|
common.Must(buffer.WriteByte(1))
|
||||||
|
} else {
|
||||||
|
common.Must(buffer.WriteByte(0))
|
||||||
|
}
|
||||||
|
common.Must(
|
||||||
|
binary.Write(buffer, binary.BigEndian, uint16(len(request.Host))),
|
||||||
|
common.Error(buffer.WriteString(request.Host)),
|
||||||
|
binary.Write(buffer, binary.BigEndian, request.Port),
|
||||||
|
common.Error(buffer.Write(payload)),
|
||||||
|
)
|
||||||
|
return common.Error(stream.Write(buffer.Bytes()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadServerResponse(stream io.Reader) (*ServerResponse, error) {
|
||||||
|
var responseLen int
|
||||||
|
responseLen += 1 // ok
|
||||||
|
responseLen += 4 // udp session id
|
||||||
|
responseLen += 2 // message len
|
||||||
|
_response := buf.StackNewSize(responseLen)
|
||||||
|
defer common.KeepAlive(_response)
|
||||||
|
response := common.Dup(_response)
|
||||||
|
defer response.Release()
|
||||||
|
_, err := response.ReadFullFrom(stream, responseLen)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var serverResponse ServerResponse
|
||||||
|
serverResponse.OK = response.Byte(0) == 1
|
||||||
|
serverResponse.UDPSessionID = binary.BigEndian.Uint32(response.Range(1, 5))
|
||||||
|
messageLen := binary.BigEndian.Uint16(response.Range(5, 7))
|
||||||
|
if messageLen == 0 {
|
||||||
|
return &serverResponse, nil
|
||||||
|
}
|
||||||
|
message := make([]byte, messageLen)
|
||||||
|
_, err = io.ReadFull(stream, message)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
serverResponse.Message = string(message)
|
||||||
|
return &serverResponse, nil
|
||||||
|
}
|
36
transport/hysteria/speed.go
Normal file
36
transport/hysteria/speed.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package hysteria
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func StringToBps(s string) uint64 {
|
||||||
|
if s == "" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
m := regexp.MustCompile(`^(\d+)\s*([KMGT]?)([Bb])ps$`).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
|
||||||
|
}
|
29
transport/hysteria/wrap.go
Normal file
29
transport/hysteria/wrap.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package hysteria
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WrapPacketConn struct {
|
||||||
|
net.PacketConn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *WrapPacketConn) SetReadBuffer(bytes int) error {
|
||||||
|
return common.MustCast[*net.UDPConn](c.PacketConn).SetReadBuffer(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *WrapPacketConn) SetWriteBuffer(bytes int) error {
|
||||||
|
return common.MustCast[*net.UDPConn](c.PacketConn).SetWriteBuffer(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *WrapPacketConn) SyscallConn() (syscall.RawConn, error) {
|
||||||
|
return common.MustCast[*net.UDPConn](c.PacketConn).SyscallConn()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *WrapPacketConn) File() (f *os.File, err error) {
|
||||||
|
return common.MustCast[*net.UDPConn](c.PacketConn).File()
|
||||||
|
}
|
119
transport/hysteria/xplus.go
Normal file
119
transport/hysteria/xplus.go
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
package hysteria
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"math/rand"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
"github.com/sagernet/sing/common/buf"
|
||||||
|
"github.com/sagernet/sing/common/bufio"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
)
|
||||||
|
|
||||||
|
const xplusSaltLen = 16
|
||||||
|
|
||||||
|
var errInalidPacket = E.New("invalid packet")
|
||||||
|
|
||||||
|
func NewXPlusPacketConn(conn net.PacketConn, key []byte) net.PacketConn {
|
||||||
|
vectorisedWriter, isVectorised := bufio.CreateVectorisedPacketWriter(conn)
|
||||||
|
if isVectorised {
|
||||||
|
return &VectorisedXPlusConn{
|
||||||
|
XPlusPacketConn: XPlusPacketConn{
|
||||||
|
PacketConn: conn,
|
||||||
|
key: key,
|
||||||
|
rand: rand.New(rand.NewSource(time.Now().UnixNano())),
|
||||||
|
},
|
||||||
|
writer: vectorisedWriter,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return &XPlusPacketConn{
|
||||||
|
PacketConn: conn,
|
||||||
|
key: key,
|
||||||
|
rand: rand.New(rand.NewSource(time.Now().UnixNano())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type XPlusPacketConn struct {
|
||||||
|
net.PacketConn
|
||||||
|
key []byte
|
||||||
|
randAccess sync.Mutex
|
||||||
|
rand *rand.Rand
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *XPlusPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
|
||||||
|
n, addr, err = c.PacketConn.ReadFrom(p)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
} else if n < xplusSaltLen {
|
||||||
|
return 0, nil, errInalidPacket
|
||||||
|
}
|
||||||
|
key := sha256.Sum256(append(c.key, p[:xplusSaltLen]...))
|
||||||
|
for i := range p[xplusSaltLen:] {
|
||||||
|
p[i] = p[xplusSaltLen+i] ^ key[i%sha256.Size]
|
||||||
|
}
|
||||||
|
n -= xplusSaltLen
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *XPlusPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
|
||||||
|
// can't use unsafe buffer on WriteTo
|
||||||
|
buffer := buf.NewSize(len(p) + xplusSaltLen)
|
||||||
|
defer buffer.Release()
|
||||||
|
salt := buffer.Extend(xplusSaltLen)
|
||||||
|
c.randAccess.Lock()
|
||||||
|
_, _ = c.rand.Read(salt)
|
||||||
|
c.randAccess.Unlock()
|
||||||
|
key := sha256.Sum256(append(c.key, salt...))
|
||||||
|
for i := range p {
|
||||||
|
common.Must(buffer.WriteByte(p[i] ^ key[i%sha256.Size]))
|
||||||
|
}
|
||||||
|
return c.PacketConn.WriteTo(buffer.Bytes(), addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *XPlusPacketConn) Upstream() any {
|
||||||
|
return c.PacketConn
|
||||||
|
}
|
||||||
|
|
||||||
|
type VectorisedXPlusConn struct {
|
||||||
|
XPlusPacketConn
|
||||||
|
writer N.VectorisedPacketWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *VectorisedXPlusConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
|
||||||
|
header := buf.NewSize(xplusSaltLen)
|
||||||
|
defer header.Release()
|
||||||
|
salt := header.Extend(xplusSaltLen)
|
||||||
|
c.randAccess.Lock()
|
||||||
|
_, _ = c.rand.Read(salt)
|
||||||
|
c.randAccess.Unlock()
|
||||||
|
key := sha256.Sum256(append(c.key, salt...))
|
||||||
|
for i := range p {
|
||||||
|
p[i] ^= key[i%sha256.Size]
|
||||||
|
}
|
||||||
|
return bufio.WriteVectorisedPacket(c.writer, [][]byte{header.Bytes(), p}, M.SocksaddrFromNet(addr))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *VectorisedXPlusConn) WriteVectorisedPacket(buffers []*buf.Buffer, destination M.Socksaddr) error {
|
||||||
|
header := buf.NewSize(xplusSaltLen)
|
||||||
|
salt := header.Extend(xplusSaltLen)
|
||||||
|
c.randAccess.Lock()
|
||||||
|
_, _ = c.rand.Read(salt)
|
||||||
|
c.randAccess.Unlock()
|
||||||
|
key := sha256.Sum256(append(c.key, salt...))
|
||||||
|
var index int
|
||||||
|
for _, buffer := range buffers {
|
||||||
|
data := buffer.Bytes()
|
||||||
|
for i := range data {
|
||||||
|
data[i] ^= key[index%sha256.Size]
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buffers = append([]*buf.Buffer{header}, buffers...)
|
||||||
|
return c.writer.WriteVectorisedPacket(buffers, destination)
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user