mirror of
https://github.com/SagerNet/sing-box.git
synced 2025-06-08 11:44:13 +08:00
Add hysteria tcp server
This commit is contained in:
parent
f5bb4cf53f
commit
b992d942c4
@ -37,6 +37,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.TypeNaive:
|
case C.TypeNaive:
|
||||||
return NewNaive(ctx, router, logger, options.Tag, options.NaiveOptions)
|
return NewNaive(ctx, router, logger, options.Tag, options.NaiveOptions)
|
||||||
|
case C.TypeHysteria:
|
||||||
|
return NewHysteria(ctx, router, logger, options.Tag, options.HysteriaOptions)
|
||||||
default:
|
default:
|
||||||
return nil, E.New("unknown inbound type: ", options.Type)
|
return nil, E.New("unknown inbound type: ", options.Type)
|
||||||
}
|
}
|
||||||
|
256
inbound/hysteria.go
Normal file
256
inbound/hysteria.go
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
//go:build with_quic
|
||||||
|
|
||||||
|
package inbound
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
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"
|
||||||
|
dns "github.com/sagernet/sing-dns"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ adapter.Inbound = (*Hysteria)(nil)
|
||||||
|
|
||||||
|
type Hysteria struct {
|
||||||
|
ctx context.Context
|
||||||
|
router adapter.Router
|
||||||
|
logger log.ContextLogger
|
||||||
|
tag string
|
||||||
|
listenOptions option.ListenOptions
|
||||||
|
quicConfig *quic.Config
|
||||||
|
tlsConfig *TLSConfig
|
||||||
|
authKey []byte
|
||||||
|
xplusKey []byte
|
||||||
|
sendBPS uint64
|
||||||
|
recvBPS uint64
|
||||||
|
listener quic.Listener
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHysteria(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HysteriaInboundOptions) (*Hysteria, error) {
|
||||||
|
quicConfig := &quic.Config{
|
||||||
|
InitialStreamReceiveWindow: options.ReceiveWindowConn,
|
||||||
|
MaxStreamReceiveWindow: options.ReceiveWindowConn,
|
||||||
|
InitialConnectionReceiveWindow: options.ReceiveWindowClient,
|
||||||
|
MaxConnectionReceiveWindow: options.ReceiveWindowClient,
|
||||||
|
MaxIncomingStreams: int64(options.MaxConnClient),
|
||||||
|
KeepAlivePeriod: hysteria.KeepAlivePeriod,
|
||||||
|
DisablePathMTUDiscovery: options.DisableMTUDiscovery,
|
||||||
|
EnableDatagrams: true,
|
||||||
|
}
|
||||||
|
if options.ReceiveWindowConn == 0 {
|
||||||
|
quicConfig.InitialStreamReceiveWindow = hysteria.DefaultStreamReceiveWindow
|
||||||
|
quicConfig.MaxStreamReceiveWindow = hysteria.DefaultStreamReceiveWindow
|
||||||
|
}
|
||||||
|
if options.ReceiveWindowClient == 0 {
|
||||||
|
quicConfig.InitialConnectionReceiveWindow = hysteria.DefaultConnectionReceiveWindow
|
||||||
|
quicConfig.MaxConnectionReceiveWindow = hysteria.DefaultConnectionReceiveWindow
|
||||||
|
}
|
||||||
|
if quicConfig.MaxIncomingStreams == 0 {
|
||||||
|
quicConfig.MaxIncomingStreams = hysteria.DefaultMaxIncomingStreams
|
||||||
|
}
|
||||||
|
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) * hysteria.MbpsToBps
|
||||||
|
}
|
||||||
|
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) * hysteria.MbpsToBps
|
||||||
|
}
|
||||||
|
if up < hysteria.MinSpeedBPS {
|
||||||
|
return nil, E.New("invalid up speed")
|
||||||
|
}
|
||||||
|
if down < hysteria.MinSpeedBPS {
|
||||||
|
return nil, E.New("invalid down speed")
|
||||||
|
}
|
||||||
|
inbound := &Hysteria{
|
||||||
|
ctx: ctx,
|
||||||
|
router: router,
|
||||||
|
logger: logger,
|
||||||
|
tag: tag,
|
||||||
|
quicConfig: quicConfig,
|
||||||
|
listenOptions: options.ListenOptions,
|
||||||
|
authKey: auth,
|
||||||
|
xplusKey: xplus,
|
||||||
|
sendBPS: up,
|
||||||
|
recvBPS: down,
|
||||||
|
}
|
||||||
|
if options.TLS == nil || !options.TLS.Enabled {
|
||||||
|
return nil, ErrTLSRequired
|
||||||
|
}
|
||||||
|
if len(options.TLS.ALPN) == 0 {
|
||||||
|
options.TLS.ALPN = []string{hysteria.DefaultALPN}
|
||||||
|
}
|
||||||
|
tlsConfig, err := NewTLSConfig(logger, common.PtrValueOrDefault(options.TLS))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
inbound.tlsConfig = tlsConfig
|
||||||
|
return inbound, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hysteria) Type() string {
|
||||||
|
return C.TypeHysteria
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hysteria) Tag() string {
|
||||||
|
return h.tag
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hysteria) Start() error {
|
||||||
|
listenAddr := M.SocksaddrFrom(netip.Addr(h.listenOptions.Listen), h.listenOptions.ListenPort)
|
||||||
|
var packetConn net.PacketConn
|
||||||
|
var err error
|
||||||
|
packetConn, err = net.ListenUDP(M.NetworkFromNetAddr("udp", listenAddr.Addr), listenAddr.UDPAddr())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(h.xplusKey) > 0 {
|
||||||
|
packetConn = hysteria.NewXPlusPacketConn(packetConn, h.xplusKey)
|
||||||
|
}
|
||||||
|
err = h.tlsConfig.Start()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
listener, err := quic.Listen(packetConn, h.tlsConfig.Config(), h.quicConfig)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h.listener = listener
|
||||||
|
go h.acceptLoop()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hysteria) acceptLoop() {
|
||||||
|
for {
|
||||||
|
ctx := log.ContextWithNewID(h.ctx)
|
||||||
|
conn, err := h.listener.Accept(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
hErr := h.accept(ctx, conn)
|
||||||
|
if hErr != nil {
|
||||||
|
conn.CloseWithError(0, "")
|
||||||
|
NewError(h.logger, ctx, E.Cause(hErr, "process connection from ", conn.RemoteAddr()))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hysteria) accept(ctx context.Context, conn quic.Connection) error {
|
||||||
|
controlStream, err := conn.AcceptStream(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
clientHello, err := hysteria.ReadClientHello(controlStream)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !bytes.Equal(clientHello.Auth, h.authKey) {
|
||||||
|
err = hysteria.WriteServerHello(controlStream, hysteria.ServerHello{
|
||||||
|
Message: "wrong password",
|
||||||
|
})
|
||||||
|
return E.Errors(E.New("wrong password: ", string(clientHello.Auth)), err)
|
||||||
|
}
|
||||||
|
if clientHello.SendBPS == 0 || clientHello.RecvBPS == 0 {
|
||||||
|
return E.New("invalid rate from client")
|
||||||
|
}
|
||||||
|
serverSendBPS, serverRecvBPS := clientHello.RecvBPS, clientHello.SendBPS
|
||||||
|
if h.sendBPS > 0 && serverSendBPS > h.sendBPS {
|
||||||
|
serverSendBPS = h.sendBPS
|
||||||
|
}
|
||||||
|
if h.recvBPS > 0 && serverRecvBPS > h.recvBPS {
|
||||||
|
serverRecvBPS = h.recvBPS
|
||||||
|
}
|
||||||
|
err = hysteria.WriteServerHello(controlStream, hysteria.ServerHello{
|
||||||
|
OK: true,
|
||||||
|
SendBPS: serverSendBPS,
|
||||||
|
RecvBPS: serverRecvBPS,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// TODO: set congestion control
|
||||||
|
go h.udpRecvLoop(conn)
|
||||||
|
var stream quic.Stream
|
||||||
|
for {
|
||||||
|
stream, err = conn.AcceptStream(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
hErr := h.acceptStream(ctx, conn, stream)
|
||||||
|
if hErr != nil {
|
||||||
|
stream.Close()
|
||||||
|
NewError(h.logger, ctx, E.Cause(hErr, "process stream from ", conn.RemoteAddr()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hysteria) udpRecvLoop(conn quic.Connection) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hysteria) acceptStream(ctx context.Context, conn quic.Connection, stream quic.Stream) error {
|
||||||
|
request, err := hysteria.ReadClientRequest(stream)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if request.UDP {
|
||||||
|
err = hysteria.WriteServerResponse(stream, hysteria.ServerResponse{
|
||||||
|
Message: "unsupported",
|
||||||
|
}, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
stream.Close()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var metadata adapter.InboundContext
|
||||||
|
metadata.Inbound = h.tag
|
||||||
|
metadata.InboundType = C.TypeHysteria
|
||||||
|
metadata.SniffEnabled = h.listenOptions.SniffEnabled
|
||||||
|
metadata.SniffOverrideDestination = h.listenOptions.SniffOverrideDestination
|
||||||
|
metadata.DomainStrategy = dns.DomainStrategy(h.listenOptions.DomainStrategy)
|
||||||
|
metadata.Network = N.NetworkTCP
|
||||||
|
metadata.Source = M.SocksaddrFromNet(conn.RemoteAddr())
|
||||||
|
metadata.Destination = M.ParseSocksaddrHostPort(request.Host, request.Port)
|
||||||
|
return h.router.RouteConnection(ctx, hysteria.NewServerConn(stream, metadata.Destination), metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hysteria) Close() error {
|
||||||
|
return common.Close(
|
||||||
|
h.listener,
|
||||||
|
common.PtrOrNil(h.tlsConfig),
|
||||||
|
)
|
||||||
|
}
|
16
inbound/hysteria_stub.go
Normal file
16
inbound/hysteria_stub.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
//go:build !with_quic
|
||||||
|
|
||||||
|
package inbound
|
||||||
|
|
||||||
|
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.HysteriaInboundOptions) (adapter.Inbound, error) {
|
||||||
|
return nil, E.New(`QUIC is not included in this build, rebuild with -tags with_quic`)
|
||||||
|
}
|
@ -46,7 +46,7 @@ type Naive struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrNaiveTLSRequired = E.New("TLS required")
|
ErrTLSRequired = E.New("TLS required")
|
||||||
ErrNaiveMissingUsers = E.New("missing users")
|
ErrNaiveMissingUsers = E.New("missing users")
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -61,7 +61,7 @@ func NewNaive(ctx context.Context, router adapter.Router, logger log.ContextLogg
|
|||||||
authenticator: auth.NewAuthenticator(options.Users),
|
authenticator: auth.NewAuthenticator(options.Users),
|
||||||
}
|
}
|
||||||
if options.TLS == nil || !options.TLS.Enabled {
|
if options.TLS == nil || !options.TLS.Enabled {
|
||||||
return nil, ErrNaiveTLSRequired
|
return nil, ErrTLSRequired
|
||||||
}
|
}
|
||||||
if len(options.Users) == 0 {
|
if len(options.Users) == 0 {
|
||||||
return nil, ErrNaiveMissingUsers
|
return nil, ErrNaiveMissingUsers
|
||||||
|
@ -1,23 +1,38 @@
|
|||||||
package option
|
package option
|
||||||
|
|
||||||
|
type HysteriaInboundOptions struct {
|
||||||
|
ListenOptions
|
||||||
|
Up string `json:"up,omitempty"`
|
||||||
|
UpMbps int `json:"up_mbps,omitempty"`
|
||||||
|
Down string `json:"down,omitempty"`
|
||||||
|
DownMbps int `json:"down_mbps,omitempty"`
|
||||||
|
Obfs string `json:"obfs,omitempty"`
|
||||||
|
Auth []byte `json:"auth,omitempty"`
|
||||||
|
AuthString string `json:"auth_str,omitempty"`
|
||||||
|
ReceiveWindowConn uint64 `json:"recv_window_conn,omitempty"`
|
||||||
|
ReceiveWindowClient uint64 `json:"recv_window_client,omitempty"`
|
||||||
|
MaxConnClient int `json:"max_conn_client,omitempty"`
|
||||||
|
DisableMTUDiscovery bool `json:"disable_mtu_discovery,omitempty"`
|
||||||
|
TLS *InboundTLSOptions `json:"tls,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type HysteriaOutboundOptions struct {
|
type HysteriaOutboundOptions struct {
|
||||||
OutboundDialerOptions
|
OutboundDialerOptions
|
||||||
ServerOptions
|
ServerOptions
|
||||||
Protocol string `json:"protocol"`
|
Up string `json:"up,omitempty"`
|
||||||
Up string `json:"up"`
|
UpMbps int `json:"up_mbps,omitempty"`
|
||||||
UpMbps int `json:"up_mbps"`
|
Down string `json:"down,omitempty"`
|
||||||
Down string `json:"down"`
|
DownMbps int `json:"down_mbps,omitempty"`
|
||||||
DownMbps int `json:"down_mbps"`
|
Obfs string `json:"obfs,omitempty"`
|
||||||
Obfs string `json:"obfs"`
|
Auth []byte `json:"auth,omitempty"`
|
||||||
Auth []byte `json:"auth"`
|
AuthString string `json:"auth_str,omitempty"`
|
||||||
AuthString string `json:"auth_str"`
|
ALPN string `json:"alpn,omitempty"`
|
||||||
ALPN string `json:"alpn"`
|
ServerName string `json:"server_name,omitempty"`
|
||||||
ServerName string `json:"server_name"`
|
Insecure bool `json:"insecure,omitempty"`
|
||||||
Insecure bool `json:"insecure"`
|
CustomCA string `json:"ca,omitempty"`
|
||||||
CustomCA string `json:"ca"`
|
CustomCAStr string `json:"ca_str,omitempty"`
|
||||||
CustomCAStr string `json:"ca_str"`
|
ReceiveWindowConn uint64 `json:"recv_window_conn,omitempty"`
|
||||||
ReceiveWindowConn uint64 `json:"recv_window_conn"`
|
ReceiveWindow uint64 `json:"recv_window,omitempty"`
|
||||||
ReceiveWindow uint64 `json:"recv_window"`
|
DisableMTUDiscovery bool `json:"disable_mtu_discovery,omitempty"`
|
||||||
DisableMTUDiscovery bool `json:"disable_mtu_discovery"`
|
|
||||||
Network NetworkList `json:"network,omitempty"`
|
Network NetworkList `json:"network,omitempty"`
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ type _Inbound struct {
|
|||||||
VMessOptions VMessInboundOptions `json:"-"`
|
VMessOptions VMessInboundOptions `json:"-"`
|
||||||
TrojanOptions TrojanInboundOptions `json:"-"`
|
TrojanOptions TrojanInboundOptions `json:"-"`
|
||||||
NaiveOptions NaiveInboundOptions `json:"-"`
|
NaiveOptions NaiveInboundOptions `json:"-"`
|
||||||
|
HysteriaOptions HysteriaInboundOptions `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Inbound _Inbound
|
type Inbound _Inbound
|
||||||
@ -49,6 +50,8 @@ func (h Inbound) MarshalJSON() ([]byte, error) {
|
|||||||
v = h.TrojanOptions
|
v = h.TrojanOptions
|
||||||
case C.TypeNaive:
|
case C.TypeNaive:
|
||||||
v = h.NaiveOptions
|
v = h.NaiveOptions
|
||||||
|
case C.TypeHysteria:
|
||||||
|
v = h.HysteriaOptions
|
||||||
default:
|
default:
|
||||||
return nil, E.New("unknown inbound type: ", h.Type)
|
return nil, E.New("unknown inbound type: ", h.Type)
|
||||||
}
|
}
|
||||||
@ -84,6 +87,8 @@ func (h *Inbound) UnmarshalJSON(bytes []byte) error {
|
|||||||
v = &h.TrojanOptions
|
v = &h.TrojanOptions
|
||||||
case C.TypeNaive:
|
case C.TypeNaive:
|
||||||
v = &h.NaiveOptions
|
v = &h.NaiveOptions
|
||||||
|
case C.TypeHysteria:
|
||||||
|
v = &h.HysteriaOptions
|
||||||
default:
|
default:
|
||||||
return E.New("unknown inbound type: ", h.Type)
|
return E.New("unknown inbound type: ", h.Type)
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,6 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/common/dialer"
|
"github.com/sagernet/sing-box/common/dialer"
|
||||||
@ -26,16 +25,6 @@ import (
|
|||||||
"github.com/lucas-clemente/quic-go"
|
"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)
|
var _ adapter.Outbound = (*Hysteria)(nil)
|
||||||
|
|
||||||
type Hysteria struct {
|
type Hysteria struct {
|
||||||
@ -65,7 +54,7 @@ func NewHysteria(ctx context.Context, router adapter.Router, logger log.ContextL
|
|||||||
if options.ALPN != "" {
|
if options.ALPN != "" {
|
||||||
tlsConfig.NextProtos = []string{options.ALPN}
|
tlsConfig.NextProtos = []string{options.ALPN}
|
||||||
} else {
|
} else {
|
||||||
tlsConfig.NextProtos = []string{hyDefaultALPN}
|
tlsConfig.NextProtos = []string{hysteria.DefaultALPN}
|
||||||
}
|
}
|
||||||
var ca []byte
|
var ca []byte
|
||||||
var err error
|
var err error
|
||||||
@ -90,20 +79,20 @@ func NewHysteria(ctx context.Context, router adapter.Router, logger log.ContextL
|
|||||||
MaxStreamReceiveWindow: options.ReceiveWindowConn,
|
MaxStreamReceiveWindow: options.ReceiveWindowConn,
|
||||||
InitialConnectionReceiveWindow: options.ReceiveWindow,
|
InitialConnectionReceiveWindow: options.ReceiveWindow,
|
||||||
MaxConnectionReceiveWindow: options.ReceiveWindow,
|
MaxConnectionReceiveWindow: options.ReceiveWindow,
|
||||||
KeepAlivePeriod: hyKeepAlivePeriod,
|
KeepAlivePeriod: hysteria.KeepAlivePeriod,
|
||||||
DisablePathMTUDiscovery: options.DisableMTUDiscovery,
|
DisablePathMTUDiscovery: options.DisableMTUDiscovery,
|
||||||
EnableDatagrams: true,
|
EnableDatagrams: true,
|
||||||
}
|
}
|
||||||
if options.ReceiveWindowConn == 0 {
|
if options.ReceiveWindowConn == 0 {
|
||||||
quicConfig.InitialStreamReceiveWindow = hyDefaultStreamReceiveWindow
|
quicConfig.InitialStreamReceiveWindow = hysteria.DefaultStreamReceiveWindow
|
||||||
quicConfig.MaxStreamReceiveWindow = hyDefaultStreamReceiveWindow
|
quicConfig.MaxStreamReceiveWindow = hysteria.DefaultStreamReceiveWindow
|
||||||
}
|
}
|
||||||
if options.ReceiveWindow == 0 {
|
if options.ReceiveWindow == 0 {
|
||||||
quicConfig.InitialConnectionReceiveWindow = hyDefaultConnectionReceiveWindow
|
quicConfig.InitialConnectionReceiveWindow = hysteria.DefaultConnectionReceiveWindow
|
||||||
quicConfig.MaxConnectionReceiveWindow = hyDefaultConnectionReceiveWindow
|
quicConfig.MaxConnectionReceiveWindow = hysteria.DefaultConnectionReceiveWindow
|
||||||
}
|
}
|
||||||
if quicConfig.MaxIncomingStreams == 0 {
|
if quicConfig.MaxIncomingStreams == 0 {
|
||||||
quicConfig.MaxIncomingStreams = hyDefaultMaxIncomingStreams
|
quicConfig.MaxIncomingStreams = hysteria.DefaultMaxIncomingStreams
|
||||||
}
|
}
|
||||||
var auth []byte
|
var auth []byte
|
||||||
if len(options.Auth) > 0 {
|
if len(options.Auth) > 0 {
|
||||||
@ -122,7 +111,7 @@ func NewHysteria(ctx context.Context, router adapter.Router, logger log.ContextL
|
|||||||
return nil, E.New("invalid up speed format: ", options.Up)
|
return nil, E.New("invalid up speed format: ", options.Up)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
up = uint64(options.UpMbps) * hyMbpsToBps
|
up = uint64(options.UpMbps) * hysteria.MbpsToBps
|
||||||
}
|
}
|
||||||
if len(options.Down) > 0 {
|
if len(options.Down) > 0 {
|
||||||
down = hysteria.StringToBps(options.Down)
|
down = hysteria.StringToBps(options.Down)
|
||||||
@ -130,12 +119,12 @@ func NewHysteria(ctx context.Context, router adapter.Router, logger log.ContextL
|
|||||||
return nil, E.New("invalid down speed format: ", options.Down)
|
return nil, E.New("invalid down speed format: ", options.Down)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
down = uint64(options.DownMbps) * hyMbpsToBps
|
down = uint64(options.DownMbps) * hysteria.MbpsToBps
|
||||||
}
|
}
|
||||||
if up < hyMinSpeedBPS {
|
if up < hysteria.MinSpeedBPS {
|
||||||
return nil, E.New("invalid up speed")
|
return nil, E.New("invalid up speed")
|
||||||
}
|
}
|
||||||
if down < hyMinSpeedBPS {
|
if down < hysteria.MinSpeedBPS {
|
||||||
return nil, E.New("invalid down speed")
|
return nil, E.New("invalid down speed")
|
||||||
}
|
}
|
||||||
return &Hysteria{
|
return &Hysteria{
|
||||||
@ -214,7 +203,7 @@ func (h *Hysteria) offerNew(ctx context.Context) (quic.Connection, error) {
|
|||||||
Auth: h.authKey,
|
Auth: h.authKey,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "write hysteria client hello")
|
return nil, err
|
||||||
}
|
}
|
||||||
serverHello, err := hysteria.ReadServerHello(controlStream)
|
serverHello, err := hysteria.ReadServerHello(controlStream)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -8,6 +8,81 @@ import (
|
|||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestHysteriaSelf(t *testing.T) {
|
||||||
|
if !C.QUIC_AVAILABLE {
|
||||||
|
t.Skip("QUIC not included")
|
||||||
|
}
|
||||||
|
caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
|
||||||
|
startInstance(t, option.Options{
|
||||||
|
Log: &option.LogOptions{
|
||||||
|
Level: "trace",
|
||||||
|
},
|
||||||
|
Inbounds: []option.Inbound{
|
||||||
|
{
|
||||||
|
Type: C.TypeMixed,
|
||||||
|
Tag: "mixed-in",
|
||||||
|
MixedOptions: option.HTTPMixedInboundOptions{
|
||||||
|
ListenOptions: option.ListenOptions{
|
||||||
|
Listen: option.ListenAddress(netip.IPv4Unspecified()),
|
||||||
|
ListenPort: clientPort,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: C.TypeHysteria,
|
||||||
|
HysteriaOptions: option.HysteriaInboundOptions{
|
||||||
|
ListenOptions: option.ListenOptions{
|
||||||
|
Listen: option.ListenAddress(netip.IPv4Unspecified()),
|
||||||
|
ListenPort: serverPort,
|
||||||
|
},
|
||||||
|
UpMbps: 100,
|
||||||
|
DownMbps: 100,
|
||||||
|
AuthString: "password",
|
||||||
|
Obfs: "fuck me till the daylight",
|
||||||
|
TLS: &option.InboundTLSOptions{
|
||||||
|
Enabled: true,
|
||||||
|
ServerName: "example.org",
|
||||||
|
CertificatePath: certPem,
|
||||||
|
KeyPath: keyPem,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Outbounds: []option.Outbound{
|
||||||
|
{
|
||||||
|
Type: C.TypeDirect,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: C.TypeHysteria,
|
||||||
|
Tag: "hy-out",
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Route: &option.RouteOptions{
|
||||||
|
Rules: []option.Rule{
|
||||||
|
{
|
||||||
|
DefaultOptions: option.DefaultRule{
|
||||||
|
Inbound: []string{"mixed-in"},
|
||||||
|
Outbound: "hy-out",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
testTCP(t, clientPort, testPort)
|
||||||
|
}
|
||||||
|
|
||||||
func TestHysteriaOutbound(t *testing.T) {
|
func TestHysteriaOutbound(t *testing.T) {
|
||||||
if !C.QUIC_AVAILABLE {
|
if !C.QUIC_AVAILABLE {
|
||||||
t.Skip("QUIC not included")
|
t.Skip("QUIC not included")
|
||||||
|
@ -49,7 +49,7 @@ func (c *ClientConn) Write(b []byte) (n int, err error) {
|
|||||||
if !c.requestWritten {
|
if !c.requestWritten {
|
||||||
err = WriteClientRequest(c.Stream, ClientRequest{
|
err = WriteClientRequest(c.Stream, ClientRequest{
|
||||||
UDP: false,
|
UDP: false,
|
||||||
Host: c.destination.AddrString(),
|
Host: c.destination.Unwrap().AddrString(),
|
||||||
Port: c.destination.Port,
|
Port: c.destination.Port,
|
||||||
}, b)
|
}, b)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -73,6 +73,14 @@ func (c *ClientConn) Upstream() any {
|
|||||||
return c.Stream
|
return c.Stream
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *ClientConn) ReaderReplaceable() bool {
|
||||||
|
return c.responseRead
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClientConn) WriterReplaceable() bool {
|
||||||
|
return c.requestWritten
|
||||||
|
}
|
||||||
|
|
||||||
type ClientPacketConn struct {
|
type ClientPacketConn struct {
|
||||||
session quic.Connection
|
session quic.Connection
|
||||||
stream quic.Stream
|
stream quic.Stream
|
||||||
@ -130,7 +138,7 @@ func (c *ClientPacketConn) ReadPacketThreadSafe() (buffer *buf.Buffer, destinati
|
|||||||
func (c *ClientPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
|
func (c *ClientPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
|
||||||
return WriteUDPMessage(c.session, UDPMessage{
|
return WriteUDPMessage(c.session, UDPMessage{
|
||||||
SessionID: c.sessionId,
|
SessionID: c.sessionId,
|
||||||
Host: destination.AddrString(),
|
Host: destination.Unwrap().AddrString(),
|
||||||
Port: destination.Port,
|
Port: destination.Port,
|
||||||
FragCount: 1,
|
FragCount: 1,
|
||||||
Data: buffer.Bytes(),
|
Data: buffer.Bytes(),
|
||||||
@ -158,19 +166,19 @@ func (c *ClientPacketConn) SetWriteDeadline(t time.Time) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *ClientPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
|
func (c *ClientPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
|
||||||
panic("invalid")
|
return 0, nil, os.ErrInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ClientPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
|
func (c *ClientPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
|
||||||
panic("invalid")
|
return 0, os.ErrInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ClientPacketConn) Read(b []byte) (n int, err error) {
|
func (c *ClientPacketConn) Read(b []byte) (n int, err error) {
|
||||||
panic("invalid")
|
return 0, os.ErrInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ClientPacketConn) Write(b []byte) (n int, err error) {
|
func (c *ClientPacketConn) Write(b []byte) (n int, err error) {
|
||||||
panic("invalid")
|
return 0, os.ErrInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ClientPacketConn) Close() error {
|
func (c *ClientPacketConn) Close() error {
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"io"
|
"io"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
"github.com/sagernet/sing/common/buf"
|
"github.com/sagernet/sing/common/buf"
|
||||||
@ -13,6 +14,16 @@ import (
|
|||||||
"github.com/lucas-clemente/quic-go"
|
"github.com/lucas-clemente/quic-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
MbpsToBps = 125000
|
||||||
|
MinSpeedBPS = 16384
|
||||||
|
DefaultStreamReceiveWindow = 15728640 // 15 MB/s
|
||||||
|
DefaultConnectionReceiveWindow = 67108864 // 64 MB/s
|
||||||
|
DefaultMaxIncomingStreams = 1024
|
||||||
|
DefaultALPN = "hysteria"
|
||||||
|
KeepAlivePeriod = 10 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
const Version = 3
|
const Version = 3
|
||||||
|
|
||||||
type ClientHello struct {
|
type ClientHello struct {
|
||||||
@ -21,13 +32,6 @@ type ClientHello struct {
|
|||||||
Auth []byte
|
Auth []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServerHello struct {
|
|
||||||
OK bool
|
|
||||||
SendBPS uint64
|
|
||||||
RecvBPS uint64
|
|
||||||
Message string
|
|
||||||
}
|
|
||||||
|
|
||||||
func WriteClientHello(stream io.Writer, hello ClientHello) error {
|
func WriteClientHello(stream io.Writer, hello ClientHello) error {
|
||||||
var requestLen int
|
var requestLen int
|
||||||
requestLen += 1 // version
|
requestLen += 1 // version
|
||||||
@ -49,6 +53,44 @@ func WriteClientHello(stream io.Writer, hello ClientHello) error {
|
|||||||
return common.Error(stream.Write(request.Bytes()))
|
return common.Error(stream.Write(request.Bytes()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ReadClientHello(reader io.Reader) (*ClientHello, error) {
|
||||||
|
var version uint8
|
||||||
|
err := binary.Read(reader, binary.BigEndian, &version)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if version != Version {
|
||||||
|
return nil, E.New("unsupported client version: ", version)
|
||||||
|
}
|
||||||
|
var clientHello ClientHello
|
||||||
|
err = binary.Read(reader, binary.BigEndian, &clientHello.SendBPS)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = binary.Read(reader, binary.BigEndian, &clientHello.RecvBPS)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var authLen uint16
|
||||||
|
err = binary.Read(reader, binary.BigEndian, &authLen)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
clientHello.Auth = make([]byte, authLen)
|
||||||
|
_, err = io.ReadFull(reader, clientHello.Auth)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &clientHello, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServerHello struct {
|
||||||
|
OK bool
|
||||||
|
SendBPS uint64
|
||||||
|
RecvBPS uint64
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
func ReadServerHello(stream io.Reader) (*ServerHello, error) {
|
func ReadServerHello(stream io.Reader) (*ServerHello, error) {
|
||||||
var responseLen int
|
var responseLen int
|
||||||
responseLen += 1 // ok
|
responseLen += 1 // ok
|
||||||
@ -80,16 +122,59 @@ func ReadServerHello(stream io.Reader) (*ServerHello, error) {
|
|||||||
return &serverHello, nil
|
return &serverHello, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WriteServerHello(stream io.Writer, hello ServerHello) error {
|
||||||
|
var responseLen int
|
||||||
|
responseLen += 1 // ok
|
||||||
|
responseLen += 8 // sendBPS
|
||||||
|
responseLen += 8 // recvBPS
|
||||||
|
responseLen += 2 // message len
|
||||||
|
responseLen += len(hello.Message)
|
||||||
|
_response := buf.StackNewSize(responseLen)
|
||||||
|
defer common.KeepAlive(_response)
|
||||||
|
response := common.Dup(_response)
|
||||||
|
defer response.Release()
|
||||||
|
if hello.OK {
|
||||||
|
common.Must(response.WriteByte(1))
|
||||||
|
} else {
|
||||||
|
common.Must(response.WriteByte(0))
|
||||||
|
}
|
||||||
|
common.Must(
|
||||||
|
binary.Write(response, binary.BigEndian, hello.SendBPS),
|
||||||
|
binary.Write(response, binary.BigEndian, hello.RecvBPS),
|
||||||
|
binary.Write(response, binary.BigEndian, uint16(len(hello.Message))),
|
||||||
|
common.Error(response.WriteString(hello.Message)),
|
||||||
|
)
|
||||||
|
return common.Error(stream.Write(response.Bytes()))
|
||||||
|
}
|
||||||
|
|
||||||
type ClientRequest struct {
|
type ClientRequest struct {
|
||||||
UDP bool
|
UDP bool
|
||||||
Host string
|
Host string
|
||||||
Port uint16
|
Port uint16
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServerResponse struct {
|
func ReadClientRequest(stream io.Reader) (*ClientRequest, error) {
|
||||||
OK bool
|
var clientRequest ClientRequest
|
||||||
UDPSessionID uint32
|
err := binary.Read(stream, binary.BigEndian, &clientRequest.UDP)
|
||||||
Message string
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var hostLen uint16
|
||||||
|
err = binary.Read(stream, binary.BigEndian, &hostLen)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
host := make([]byte, hostLen)
|
||||||
|
_, err = io.ReadFull(stream, host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
clientRequest.Host = string(host)
|
||||||
|
err = binary.Read(stream, binary.BigEndian, &clientRequest.Port)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &clientRequest, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func WriteClientRequest(stream io.Writer, request ClientRequest, payload []byte) error {
|
func WriteClientRequest(stream io.Writer, request ClientRequest, payload []byte) error {
|
||||||
@ -117,6 +202,12 @@ func WriteClientRequest(stream io.Writer, request ClientRequest, payload []byte)
|
|||||||
return common.Error(stream.Write(buffer.Bytes()))
|
return common.Error(stream.Write(buffer.Bytes()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ServerResponse struct {
|
||||||
|
OK bool
|
||||||
|
UDPSessionID uint32
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
func ReadServerResponse(stream io.Reader) (*ServerResponse, error) {
|
func ReadServerResponse(stream io.Reader) (*ServerResponse, error) {
|
||||||
var responseLen int
|
var responseLen int
|
||||||
responseLen += 1 // ok
|
responseLen += 1 // ok
|
||||||
@ -146,6 +237,31 @@ func ReadServerResponse(stream io.Reader) (*ServerResponse, error) {
|
|||||||
return &serverResponse, nil
|
return &serverResponse, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WriteServerResponse(stream io.Writer, response ServerResponse, payload []byte) error {
|
||||||
|
var responseLen int
|
||||||
|
responseLen += 1 // ok
|
||||||
|
responseLen += 4 // udp session id
|
||||||
|
responseLen += 2 // message len
|
||||||
|
responseLen += len(response.Message)
|
||||||
|
responseLen += len(payload)
|
||||||
|
_buffer := buf.StackNewSize(responseLen)
|
||||||
|
defer common.KeepAlive(_buffer)
|
||||||
|
buffer := common.Dup(_buffer)
|
||||||
|
defer buffer.Release()
|
||||||
|
if response.OK {
|
||||||
|
common.Must(buffer.WriteByte(1))
|
||||||
|
} else {
|
||||||
|
common.Must(buffer.WriteByte(0))
|
||||||
|
}
|
||||||
|
common.Must(
|
||||||
|
binary.Write(buffer, binary.BigEndian, response.UDPSessionID),
|
||||||
|
binary.Write(buffer, binary.BigEndian, uint16(len(response.Message))),
|
||||||
|
common.Error(buffer.WriteString(response.Message)),
|
||||||
|
common.Error(buffer.Write(payload)),
|
||||||
|
)
|
||||||
|
return common.Error(stream.Write(buffer.Bytes()))
|
||||||
|
}
|
||||||
|
|
||||||
type UDPMessage struct {
|
type UDPMessage struct {
|
||||||
SessionID uint32
|
SessionID uint32
|
||||||
Host string
|
Host string
|
||||||
|
68
transport/hysteria/server.go
Normal file
68
transport/hysteria/server.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package hysteria
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
|
||||||
|
"github.com/lucas-clemente/quic-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ net.Conn = (*ServerConn)(nil)
|
||||||
|
_ N.HandshakeConn = (*ServerConn)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServerConn struct {
|
||||||
|
quic.Stream
|
||||||
|
destination M.Socksaddr
|
||||||
|
responseWritten bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewServerConn(stream quic.Stream, destination M.Socksaddr) *ServerConn {
|
||||||
|
return &ServerConn{
|
||||||
|
Stream: stream,
|
||||||
|
destination: destination,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ServerConn) LocalAddr() net.Addr {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ServerConn) RemoteAddr() net.Addr {
|
||||||
|
return c.destination.TCPAddr()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ServerConn) Write(b []byte) (n int, err error) {
|
||||||
|
if !c.responseWritten {
|
||||||
|
err = WriteServerResponse(c.Stream, ServerResponse{
|
||||||
|
OK: true,
|
||||||
|
}, b)
|
||||||
|
c.responseWritten = true
|
||||||
|
return len(b), nil
|
||||||
|
}
|
||||||
|
return c.Stream.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ServerConn) ReaderReplaceable() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ServerConn) WriterReplaceable() bool {
|
||||||
|
return c.responseWritten
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ServerConn) HandshakeFailure(err error) error {
|
||||||
|
if c.responseWritten {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return WriteServerResponse(c.Stream, ServerResponse{
|
||||||
|
Message: err.Error(),
|
||||||
|
}, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ServerConn) Upstream() any {
|
||||||
|
return c.Stream
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user