|
|
package client |
|
|
|
|
|
import ( |
|
|
"context" |
|
|
"crypto/tls" |
|
|
"net" |
|
|
"net/http" |
|
|
"net/url" |
|
|
"time" |
|
|
|
|
|
coreErrs "github.com/apernet/hysteria/core/v2/errors" |
|
|
"github.com/apernet/hysteria/core/v2/internal/congestion" |
|
|
"github.com/apernet/hysteria/core/v2/internal/protocol" |
|
|
"github.com/apernet/hysteria/core/v2/internal/utils" |
|
|
|
|
|
"github.com/apernet/quic-go" |
|
|
"github.com/apernet/quic-go/http3" |
|
|
) |
|
|
|
|
|
const ( |
|
|
closeErrCodeOK = 0x100 |
|
|
closeErrCodeProtocolError = 0x101 |
|
|
) |
|
|
|
|
|
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 |
|
|
} |
|
|
|
|
|
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 |
|
|
} |
|
|
|
|
|
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, |
|
|
} |
|
|
|
|
|
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 |
|
|
}, |
|
|
} |
|
|
|
|
|
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} |
|
|
} |
|
|
|
|
|
authResp := protocol.AuthResponseFromHeader(resp.Header) |
|
|
var actualTx uint64 |
|
|
if authResp.RxAuto { |
|
|
|
|
|
|
|
|
congestion.UseBBR(conn) |
|
|
} else { |
|
|
|
|
|
actualTx = authResp.Rx |
|
|
if actualTx == 0 || actualTx > c.config.BandwidthConfig.MaxTx { |
|
|
|
|
|
actualTx = c.config.BandwidthConfig.MaxTx |
|
|
} |
|
|
if actualTx > 0 { |
|
|
congestion.UseBrutal(conn, actualTx) |
|
|
} else { |
|
|
|
|
|
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 |
|
|
} |
|
|
|
|
|
|
|
|
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) |
|
|
} |
|
|
|
|
|
err = protocol.WriteTCPRequest(stream, addr) |
|
|
if err != nil { |
|
|
_ = stream.Close() |
|
|
return nil, wrapIfConnectionClosed(err) |
|
|
} |
|
|
if c.config.FastOpen { |
|
|
|
|
|
|
|
|
|
|
|
return &tcpConn{ |
|
|
Orig: stream, |
|
|
PseudoLocalAddr: c.conn.LocalAddr(), |
|
|
PseudoRemoteAddr: c.conn.RemoteAddr(), |
|
|
Established: false, |
|
|
}, nil |
|
|
} |
|
|
|
|
|
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 |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 { |
|
|
|
|
|
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 { |
|
|
|
|
|
return nil, err |
|
|
} |
|
|
udpMsg, err := protocol.ParseUDPMessage(msg) |
|
|
if err != nil { |
|
|
|
|
|
continue |
|
|
} |
|
|
return udpMsg, nil |
|
|
} |
|
|
} |
|
|
|
|
|
func (io *udpIOImpl) SendMessage(buf []byte, msg *protocol.UDPMessage) error { |
|
|
msgN := msg.Serialize(buf) |
|
|
if msgN < 0 { |
|
|
|
|
|
return nil |
|
|
} |
|
|
return io.Conn.SendDatagram(buf[:msgN]) |
|
|
} |
|
|
|