mirror of
https://github.com/apernet/hysteria.git
synced 2025-04-03 04:27:39 +03:00
315 lines
7.9 KiB
Go
315 lines
7.9 KiB
Go
package client
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"time"
|
|
|
|
coreErrs "github.com/apernet/hysteria/core/errors"
|
|
"github.com/apernet/hysteria/core/internal/congestion"
|
|
"github.com/apernet/hysteria/core/internal/protocol"
|
|
"github.com/apernet/hysteria/core/internal/utils"
|
|
|
|
"github.com/apernet/quic-go"
|
|
"github.com/apernet/quic-go/http3"
|
|
)
|
|
|
|
const (
|
|
closeErrCodeOK = 0x100 // HTTP3 ErrCodeNoError
|
|
closeErrCodeProtocolError = 0x101 // HTTP3 ErrCodeGeneralProtocolError
|
|
)
|
|
|
|
type Client interface {
|
|
TCP(addr string) (net.Conn, error)
|
|
UDP() (HyUDPConn, error)
|
|
Close() error
|
|
}
|
|
|
|
type HyUDPConn interface {
|
|
Receive() ([]byte, string, error)
|
|
Send([]byte, string) error
|
|
Close() error
|
|
}
|
|
|
|
type HandshakeInfo struct {
|
|
UDPEnabled bool
|
|
Tx uint64 // 0 if using BBR
|
|
}
|
|
|
|
func NewClient(config *Config) (Client, *HandshakeInfo, error) {
|
|
if err := config.verifyAndFill(); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
c := &clientImpl{
|
|
config: config,
|
|
}
|
|
info, err := c.connect()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return c, info, nil
|
|
}
|
|
|
|
type clientImpl struct {
|
|
config *Config
|
|
|
|
pktConn net.PacketConn
|
|
conn quic.Connection
|
|
|
|
udpSM *udpSessionManager
|
|
}
|
|
|
|
func (c *clientImpl) connect() (*HandshakeInfo, error) {
|
|
pktConn, err := c.config.ConnFactory.New(c.config.ServerAddr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Convert config to TLS config & QUIC config
|
|
tlsConfig := &tls.Config{
|
|
ServerName: c.config.TLSConfig.ServerName,
|
|
InsecureSkipVerify: c.config.TLSConfig.InsecureSkipVerify,
|
|
VerifyPeerCertificate: c.config.TLSConfig.VerifyPeerCertificate,
|
|
RootCAs: c.config.TLSConfig.RootCAs,
|
|
}
|
|
quicConfig := &quic.Config{
|
|
InitialStreamReceiveWindow: c.config.QUICConfig.InitialStreamReceiveWindow,
|
|
MaxStreamReceiveWindow: c.config.QUICConfig.MaxStreamReceiveWindow,
|
|
InitialConnectionReceiveWindow: c.config.QUICConfig.InitialConnectionReceiveWindow,
|
|
MaxConnectionReceiveWindow: c.config.QUICConfig.MaxConnectionReceiveWindow,
|
|
MaxIdleTimeout: c.config.QUICConfig.MaxIdleTimeout,
|
|
KeepAlivePeriod: c.config.QUICConfig.KeepAlivePeriod,
|
|
DisablePathMTUDiscovery: c.config.QUICConfig.DisablePathMTUDiscovery,
|
|
EnableDatagrams: true,
|
|
}
|
|
// Prepare RoundTripper
|
|
var conn quic.EarlyConnection
|
|
rt := &http3.RoundTripper{
|
|
TLSClientConfig: tlsConfig,
|
|
QUICConfig: quicConfig,
|
|
Dial: func(ctx context.Context, _ string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
|
|
qc, err := quic.DialEarly(ctx, pktConn, c.config.ServerAddr, tlsCfg, cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
conn = qc
|
|
return qc, nil
|
|
},
|
|
}
|
|
// Send auth HTTP request
|
|
req := &http.Request{
|
|
Method: http.MethodPost,
|
|
URL: &url.URL{
|
|
Scheme: "https",
|
|
Host: protocol.URLHost,
|
|
Path: protocol.URLPath,
|
|
},
|
|
Header: make(http.Header),
|
|
}
|
|
protocol.AuthRequestToHeader(req.Header, protocol.AuthRequest{
|
|
Auth: c.config.Auth,
|
|
Rx: c.config.BandwidthConfig.MaxRx,
|
|
})
|
|
resp, err := rt.RoundTrip(req)
|
|
if err != nil {
|
|
if conn != nil {
|
|
_ = conn.CloseWithError(closeErrCodeProtocolError, "")
|
|
}
|
|
_ = pktConn.Close()
|
|
return nil, coreErrs.ConnectError{Err: err}
|
|
}
|
|
if resp.StatusCode != protocol.StatusAuthOK {
|
|
_ = conn.CloseWithError(closeErrCodeProtocolError, "")
|
|
_ = pktConn.Close()
|
|
return nil, coreErrs.AuthError{StatusCode: resp.StatusCode}
|
|
}
|
|
// Auth OK
|
|
authResp := protocol.AuthResponseFromHeader(resp.Header)
|
|
var actualTx uint64
|
|
if authResp.RxAuto {
|
|
// Server asks client to use bandwidth detection,
|
|
// ignore local bandwidth config and use BBR
|
|
congestion.UseBBR(conn)
|
|
} else {
|
|
// actualTx = min(serverRx, clientTx)
|
|
actualTx = authResp.Rx
|
|
if actualTx == 0 || actualTx > c.config.BandwidthConfig.MaxTx {
|
|
// Server doesn't have a limit, or our clientTx is smaller than serverRx
|
|
actualTx = c.config.BandwidthConfig.MaxTx
|
|
}
|
|
if actualTx > 0 {
|
|
congestion.UseBrutal(conn, actualTx)
|
|
} else {
|
|
// We don't know our own bandwidth either, use BBR
|
|
congestion.UseBBR(conn)
|
|
}
|
|
}
|
|
_ = resp.Body.Close()
|
|
|
|
c.pktConn = pktConn
|
|
c.conn = conn
|
|
if authResp.UDPEnabled {
|
|
c.udpSM = newUDPSessionManager(&udpIOImpl{Conn: conn})
|
|
}
|
|
return &HandshakeInfo{
|
|
UDPEnabled: authResp.UDPEnabled,
|
|
Tx: actualTx,
|
|
}, nil
|
|
}
|
|
|
|
// openStream wraps the stream with QStream, which handles Close() properly
|
|
func (c *clientImpl) openStream() (quic.Stream, error) {
|
|
stream, err := c.conn.OpenStream()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &utils.QStream{Stream: stream}, nil
|
|
}
|
|
|
|
func (c *clientImpl) TCP(addr string) (net.Conn, error) {
|
|
stream, err := c.openStream()
|
|
if err != nil {
|
|
return nil, wrapIfConnectionClosed(err)
|
|
}
|
|
// Send request
|
|
err = protocol.WriteTCPRequest(stream, addr)
|
|
if err != nil {
|
|
_ = stream.Close()
|
|
return nil, wrapIfConnectionClosed(err)
|
|
}
|
|
if c.config.FastOpen {
|
|
// Don't wait for the response when fast open is enabled.
|
|
// Return the connection immediately, defer the response handling
|
|
// to the first Read() call.
|
|
return &tcpConn{
|
|
Orig: stream,
|
|
PseudoLocalAddr: c.conn.LocalAddr(),
|
|
PseudoRemoteAddr: c.conn.RemoteAddr(),
|
|
Established: false,
|
|
}, nil
|
|
}
|
|
// Read response
|
|
ok, msg, err := protocol.ReadTCPResponse(stream)
|
|
if err != nil {
|
|
_ = stream.Close()
|
|
return nil, wrapIfConnectionClosed(err)
|
|
}
|
|
if !ok {
|
|
_ = stream.Close()
|
|
return nil, coreErrs.DialError{Message: msg}
|
|
}
|
|
return &tcpConn{
|
|
Orig: stream,
|
|
PseudoLocalAddr: c.conn.LocalAddr(),
|
|
PseudoRemoteAddr: c.conn.RemoteAddr(),
|
|
Established: true,
|
|
}, nil
|
|
}
|
|
|
|
func (c *clientImpl) UDP() (HyUDPConn, error) {
|
|
if c.udpSM == nil {
|
|
return nil, coreErrs.DialError{Message: "UDP not enabled"}
|
|
}
|
|
return c.udpSM.NewUDP()
|
|
}
|
|
|
|
func (c *clientImpl) Close() error {
|
|
_ = c.conn.CloseWithError(closeErrCodeOK, "")
|
|
_ = c.pktConn.Close()
|
|
return nil
|
|
}
|
|
|
|
// wrapIfConnectionClosed checks if the error returned by quic-go
|
|
// indicates that the QUIC connection has been permanently closed,
|
|
// and if so, wraps the error with coreErrs.ClosedError.
|
|
// PITFALL: sometimes quic-go has "internal errors" that are not net.Error,
|
|
// but we still need to treat them as ClosedError.
|
|
func wrapIfConnectionClosed(err error) error {
|
|
netErr, ok := err.(net.Error)
|
|
if !ok || !netErr.Temporary() {
|
|
return coreErrs.ClosedError{Err: err}
|
|
} else {
|
|
return err
|
|
}
|
|
}
|
|
|
|
type tcpConn struct {
|
|
Orig quic.Stream
|
|
PseudoLocalAddr net.Addr
|
|
PseudoRemoteAddr net.Addr
|
|
Established bool
|
|
}
|
|
|
|
func (c *tcpConn) Read(b []byte) (n int, err error) {
|
|
if !c.Established {
|
|
// Read response
|
|
ok, msg, err := protocol.ReadTCPResponse(c.Orig)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if !ok {
|
|
return 0, coreErrs.DialError{Message: msg}
|
|
}
|
|
c.Established = true
|
|
}
|
|
return c.Orig.Read(b)
|
|
}
|
|
|
|
func (c *tcpConn) Write(b []byte) (n int, err error) {
|
|
return c.Orig.Write(b)
|
|
}
|
|
|
|
func (c *tcpConn) Close() error {
|
|
return c.Orig.Close()
|
|
}
|
|
|
|
func (c *tcpConn) LocalAddr() net.Addr {
|
|
return c.PseudoLocalAddr
|
|
}
|
|
|
|
func (c *tcpConn) RemoteAddr() net.Addr {
|
|
return c.PseudoRemoteAddr
|
|
}
|
|
|
|
func (c *tcpConn) SetDeadline(t time.Time) error {
|
|
return c.Orig.SetDeadline(t)
|
|
}
|
|
|
|
func (c *tcpConn) SetReadDeadline(t time.Time) error {
|
|
return c.Orig.SetReadDeadline(t)
|
|
}
|
|
|
|
func (c *tcpConn) SetWriteDeadline(t time.Time) error {
|
|
return c.Orig.SetWriteDeadline(t)
|
|
}
|
|
|
|
type udpIOImpl struct {
|
|
Conn quic.Connection
|
|
}
|
|
|
|
func (io *udpIOImpl) ReceiveMessage() (*protocol.UDPMessage, error) {
|
|
for {
|
|
msg, err := io.Conn.ReceiveDatagram(context.Background())
|
|
if err != nil {
|
|
// Connection error, this will stop the session manager
|
|
return nil, err
|
|
}
|
|
udpMsg, err := protocol.ParseUDPMessage(msg)
|
|
if err != nil {
|
|
// Invalid message, this is fine - just wait for the next
|
|
continue
|
|
}
|
|
return udpMsg, nil
|
|
}
|
|
}
|
|
|
|
func (io *udpIOImpl) SendMessage(buf []byte, msg *protocol.UDPMessage) error {
|
|
msgN := msg.Serialize(buf)
|
|
if msgN < 0 {
|
|
// Message larger than buffer, silent drop
|
|
return nil
|
|
}
|
|
return io.Conn.SendDatagram(buf[:msgN])
|
|
}
|