mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-23 22:27:38 -05:00
dfbc2e81e3
quic-go now vendors all of its dependencies, so we don't need to vendor them here. Created by running: gvt delete github.com/lucas-clemente/quic-go gvt delete github.com/bifurcation/mint gvt delete github.com/lucas-clemente/aes12 gvt delete github.com/lucas-clemente/fnv128a gvt delete github.com/lucas-clemente/quic-go-certificates gvt delete github.com/aead/chacha20 gvt delete github.com/hashicorp/golang-lru gvt fetch -tag v0.10.0-no-integrationtests github.com/lucas-clemente/quic-go
595 lines
17 KiB
Go
595 lines
17 KiB
Go
package quic
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/tls"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"sync"
|
|
|
|
"github.com/bifurcation/mint"
|
|
"github.com/lucas-clemente/quic-go/internal/handshake"
|
|
"github.com/lucas-clemente/quic-go/internal/protocol"
|
|
"github.com/lucas-clemente/quic-go/internal/utils"
|
|
"github.com/lucas-clemente/quic-go/internal/wire"
|
|
"github.com/lucas-clemente/quic-go/qerr"
|
|
)
|
|
|
|
type client struct {
|
|
mutex sync.Mutex
|
|
|
|
conn connection
|
|
// If the client is created with DialAddr, we create a packet conn.
|
|
// If it is started with Dial, we take a packet conn as a parameter.
|
|
createdPacketConn bool
|
|
|
|
hostname string
|
|
|
|
packetHandlers packetHandlerManager
|
|
|
|
token []byte
|
|
numRetries int
|
|
|
|
versionNegotiated bool // has the server accepted our version
|
|
receivedVersionNegotiationPacket bool
|
|
negotiatedVersions []protocol.VersionNumber // the list of versions from the version negotiation packet
|
|
|
|
tlsConf *tls.Config
|
|
mintConf *mint.Config
|
|
config *Config
|
|
|
|
srcConnID protocol.ConnectionID
|
|
destConnID protocol.ConnectionID
|
|
|
|
initialVersion protocol.VersionNumber
|
|
version protocol.VersionNumber
|
|
|
|
handshakeChan chan struct{}
|
|
closeCallback func(protocol.ConnectionID)
|
|
|
|
session quicSession
|
|
|
|
logger utils.Logger
|
|
}
|
|
|
|
var _ packetHandler = &client{}
|
|
|
|
var (
|
|
// make it possible to mock connection ID generation in the tests
|
|
generateConnectionID = protocol.GenerateConnectionID
|
|
generateConnectionIDForInitial = protocol.GenerateConnectionIDForInitial
|
|
errCloseSessionForNewVersion = errors.New("closing session in order to recreate it with a new version")
|
|
errCloseSessionForRetry = errors.New("closing session in response to a stateless retry")
|
|
)
|
|
|
|
// DialAddr establishes a new QUIC connection to a server.
|
|
// The hostname for SNI is taken from the given address.
|
|
func DialAddr(
|
|
addr string,
|
|
tlsConf *tls.Config,
|
|
config *Config,
|
|
) (Session, error) {
|
|
return DialAddrContext(context.Background(), addr, tlsConf, config)
|
|
}
|
|
|
|
// DialAddrContext establishes a new QUIC connection to a server using the provided context.
|
|
// The hostname for SNI is taken from the given address.
|
|
func DialAddrContext(
|
|
ctx context.Context,
|
|
addr string,
|
|
tlsConf *tls.Config,
|
|
config *Config,
|
|
) (Session, error) {
|
|
udpAddr, err := net.ResolveUDPAddr("udp", addr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4zero, Port: 0})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return dialContext(ctx, udpConn, udpAddr, addr, tlsConf, config, true)
|
|
}
|
|
|
|
// Dial establishes a new QUIC connection to a server using a net.PacketConn.
|
|
// The host parameter is used for SNI.
|
|
func Dial(
|
|
pconn net.PacketConn,
|
|
remoteAddr net.Addr,
|
|
host string,
|
|
tlsConf *tls.Config,
|
|
config *Config,
|
|
) (Session, error) {
|
|
return DialContext(context.Background(), pconn, remoteAddr, host, tlsConf, config)
|
|
}
|
|
|
|
// DialContext establishes a new QUIC connection to a server using a net.PacketConn using the provided context.
|
|
// The host parameter is used for SNI.
|
|
func DialContext(
|
|
ctx context.Context,
|
|
pconn net.PacketConn,
|
|
remoteAddr net.Addr,
|
|
host string,
|
|
tlsConf *tls.Config,
|
|
config *Config,
|
|
) (Session, error) {
|
|
return dialContext(ctx, pconn, remoteAddr, host, tlsConf, config, false)
|
|
}
|
|
|
|
func dialContext(
|
|
ctx context.Context,
|
|
pconn net.PacketConn,
|
|
remoteAddr net.Addr,
|
|
host string,
|
|
tlsConf *tls.Config,
|
|
config *Config,
|
|
createdPacketConn bool,
|
|
) (Session, error) {
|
|
config = populateClientConfig(config, createdPacketConn)
|
|
if !createdPacketConn {
|
|
for _, v := range config.Versions {
|
|
if v == protocol.Version44 {
|
|
return nil, errors.New("Cannot multiplex connections using gQUIC 44, see https://groups.google.com/a/chromium.org/forum/#!topic/proto-quic/pE9NlLLjizE. Please disable gQUIC 44 in the quic.Config, or use DialAddr")
|
|
}
|
|
}
|
|
}
|
|
packetHandlers, err := getMultiplexer().AddConn(pconn, config.ConnectionIDLength)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
c, err := newClient(pconn, remoteAddr, config, tlsConf, host, packetHandlers.Remove, createdPacketConn)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
c.packetHandlers = packetHandlers
|
|
if err := c.dial(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
return c.session, nil
|
|
}
|
|
|
|
func newClient(
|
|
pconn net.PacketConn,
|
|
remoteAddr net.Addr,
|
|
config *Config,
|
|
tlsConf *tls.Config,
|
|
host string,
|
|
closeCallback func(protocol.ConnectionID),
|
|
createdPacketConn bool,
|
|
) (*client, error) {
|
|
var hostname string
|
|
if tlsConf != nil {
|
|
hostname = tlsConf.ServerName
|
|
}
|
|
if hostname == "" {
|
|
var err error
|
|
hostname, _, err = net.SplitHostPort(host)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// check that all versions are actually supported
|
|
if config != nil {
|
|
for _, v := range config.Versions {
|
|
if !protocol.IsValidVersion(v) {
|
|
return nil, fmt.Errorf("%s is not a valid QUIC version", v)
|
|
}
|
|
}
|
|
}
|
|
onClose := func(protocol.ConnectionID) {}
|
|
if closeCallback != nil {
|
|
onClose = closeCallback
|
|
}
|
|
c := &client{
|
|
conn: &conn{pconn: pconn, currentAddr: remoteAddr},
|
|
createdPacketConn: createdPacketConn,
|
|
hostname: hostname,
|
|
tlsConf: tlsConf,
|
|
config: config,
|
|
version: config.Versions[0],
|
|
handshakeChan: make(chan struct{}),
|
|
closeCallback: onClose,
|
|
logger: utils.DefaultLogger.WithPrefix("client"),
|
|
}
|
|
return c, c.generateConnectionIDs()
|
|
}
|
|
|
|
// populateClientConfig populates fields in the quic.Config with their default values, if none are set
|
|
// it may be called with nil
|
|
func populateClientConfig(config *Config, createdPacketConn bool) *Config {
|
|
if config == nil {
|
|
config = &Config{}
|
|
}
|
|
versions := config.Versions
|
|
if len(versions) == 0 {
|
|
versions = protocol.SupportedVersions
|
|
}
|
|
|
|
handshakeTimeout := protocol.DefaultHandshakeTimeout
|
|
if config.HandshakeTimeout != 0 {
|
|
handshakeTimeout = config.HandshakeTimeout
|
|
}
|
|
idleTimeout := protocol.DefaultIdleTimeout
|
|
if config.IdleTimeout != 0 {
|
|
idleTimeout = config.IdleTimeout
|
|
}
|
|
|
|
maxReceiveStreamFlowControlWindow := config.MaxReceiveStreamFlowControlWindow
|
|
if maxReceiveStreamFlowControlWindow == 0 {
|
|
maxReceiveStreamFlowControlWindow = protocol.DefaultMaxReceiveStreamFlowControlWindowClient
|
|
}
|
|
maxReceiveConnectionFlowControlWindow := config.MaxReceiveConnectionFlowControlWindow
|
|
if maxReceiveConnectionFlowControlWindow == 0 {
|
|
maxReceiveConnectionFlowControlWindow = protocol.DefaultMaxReceiveConnectionFlowControlWindowClient
|
|
}
|
|
maxIncomingStreams := config.MaxIncomingStreams
|
|
if maxIncomingStreams == 0 {
|
|
maxIncomingStreams = protocol.DefaultMaxIncomingStreams
|
|
} else if maxIncomingStreams < 0 {
|
|
maxIncomingStreams = 0
|
|
}
|
|
maxIncomingUniStreams := config.MaxIncomingUniStreams
|
|
if maxIncomingUniStreams == 0 {
|
|
maxIncomingUniStreams = protocol.DefaultMaxIncomingUniStreams
|
|
} else if maxIncomingUniStreams < 0 {
|
|
maxIncomingUniStreams = 0
|
|
}
|
|
connIDLen := config.ConnectionIDLength
|
|
if connIDLen == 0 && !createdPacketConn {
|
|
connIDLen = protocol.DefaultConnectionIDLength
|
|
}
|
|
for _, v := range versions {
|
|
if v == protocol.Version44 {
|
|
connIDLen = 0
|
|
}
|
|
}
|
|
|
|
return &Config{
|
|
Versions: versions,
|
|
HandshakeTimeout: handshakeTimeout,
|
|
IdleTimeout: idleTimeout,
|
|
RequestConnectionIDOmission: config.RequestConnectionIDOmission,
|
|
ConnectionIDLength: connIDLen,
|
|
MaxReceiveStreamFlowControlWindow: maxReceiveStreamFlowControlWindow,
|
|
MaxReceiveConnectionFlowControlWindow: maxReceiveConnectionFlowControlWindow,
|
|
MaxIncomingStreams: maxIncomingStreams,
|
|
MaxIncomingUniStreams: maxIncomingUniStreams,
|
|
KeepAlive: config.KeepAlive,
|
|
}
|
|
}
|
|
|
|
func (c *client) generateConnectionIDs() error {
|
|
connIDLen := protocol.ConnectionIDLenGQUIC
|
|
if c.version.UsesTLS() {
|
|
connIDLen = c.config.ConnectionIDLength
|
|
}
|
|
srcConnID, err := generateConnectionID(connIDLen)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
destConnID := srcConnID
|
|
if c.version.UsesTLS() {
|
|
destConnID, err = generateConnectionIDForInitial()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
c.srcConnID = srcConnID
|
|
c.destConnID = destConnID
|
|
if c.version == protocol.Version44 {
|
|
c.srcConnID = nil
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *client) dial(ctx context.Context) error {
|
|
c.logger.Infof("Starting new connection to %s (%s -> %s), source connection ID %s, destination connection ID %s, version %s", c.hostname, c.conn.LocalAddr(), c.conn.RemoteAddr(), c.srcConnID, c.destConnID, c.version)
|
|
|
|
var err error
|
|
if c.version.UsesTLS() {
|
|
err = c.dialTLS(ctx)
|
|
} else {
|
|
err = c.dialGQUIC(ctx)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (c *client) dialGQUIC(ctx context.Context) error {
|
|
if err := c.createNewGQUICSession(); err != nil {
|
|
return err
|
|
}
|
|
err := c.establishSecureConnection(ctx)
|
|
if err == errCloseSessionForNewVersion {
|
|
return c.dial(ctx)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (c *client) dialTLS(ctx context.Context) error {
|
|
params := &handshake.TransportParameters{
|
|
StreamFlowControlWindow: protocol.ReceiveStreamFlowControlWindow,
|
|
ConnectionFlowControlWindow: protocol.ReceiveConnectionFlowControlWindow,
|
|
IdleTimeout: c.config.IdleTimeout,
|
|
OmitConnectionID: c.config.RequestConnectionIDOmission,
|
|
MaxBidiStreams: uint16(c.config.MaxIncomingStreams),
|
|
MaxUniStreams: uint16(c.config.MaxIncomingUniStreams),
|
|
DisableMigration: true,
|
|
}
|
|
extHandler := handshake.NewExtensionHandlerClient(params, c.initialVersion, c.config.Versions, c.version, c.logger)
|
|
mintConf, err := tlsToMintConfig(c.tlsConf, protocol.PerspectiveClient)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
mintConf.ExtensionHandler = extHandler
|
|
mintConf.ServerName = c.hostname
|
|
c.mintConf = mintConf
|
|
|
|
if err := c.createNewTLSSession(extHandler.GetPeerParams(), c.version); err != nil {
|
|
return err
|
|
}
|
|
err = c.establishSecureConnection(ctx)
|
|
if err == errCloseSessionForRetry || err == errCloseSessionForNewVersion {
|
|
return c.dial(ctx)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// establishSecureConnection runs the session, and tries to establish a secure connection
|
|
// It returns:
|
|
// - errCloseSessionForNewVersion when the server sends a version negotiation packet
|
|
// - handshake.ErrCloseSessionForRetry when the server performs a stateless retry (for IETF QUIC)
|
|
// - any other error that might occur
|
|
// - when the connection is secure (for gQUIC), or forward-secure (for IETF QUIC)
|
|
func (c *client) establishSecureConnection(ctx context.Context) error {
|
|
errorChan := make(chan error, 1)
|
|
|
|
go func() {
|
|
err := c.session.run() // returns as soon as the session is closed
|
|
if err != errCloseSessionForRetry && err != errCloseSessionForNewVersion && c.createdPacketConn {
|
|
c.conn.Close()
|
|
}
|
|
errorChan <- err
|
|
}()
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
// The session will send a PeerGoingAway error to the server.
|
|
c.session.Close()
|
|
return ctx.Err()
|
|
case err := <-errorChan:
|
|
return err
|
|
case <-c.handshakeChan:
|
|
// handshake successfully completed
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (c *client) handlePacket(p *receivedPacket) {
|
|
if err := c.handlePacketImpl(p); err != nil {
|
|
c.logger.Errorf("error handling packet: %s", err)
|
|
}
|
|
}
|
|
|
|
func (c *client) handlePacketImpl(p *receivedPacket) error {
|
|
c.mutex.Lock()
|
|
defer c.mutex.Unlock()
|
|
|
|
// handle Version Negotiation Packets
|
|
if p.header.IsVersionNegotiation {
|
|
err := c.handleVersionNegotiationPacket(p.header)
|
|
if err != nil {
|
|
c.session.destroy(err)
|
|
}
|
|
// version negotiation packets have no payload
|
|
return err
|
|
}
|
|
|
|
if !c.version.UsesIETFHeaderFormat() {
|
|
connID := p.header.DestConnectionID
|
|
// reject packets with truncated connection id if we didn't request truncation
|
|
if !c.config.RequestConnectionIDOmission && connID.Len() == 0 {
|
|
return errors.New("received packet with truncated connection ID, but didn't request truncation")
|
|
}
|
|
// reject packets with the wrong connection ID
|
|
if connID.Len() > 0 && !connID.Equal(c.srcConnID) {
|
|
return fmt.Errorf("received a packet with an unexpected connection ID (%s, expected %s)", connID, c.srcConnID)
|
|
}
|
|
if p.header.ResetFlag {
|
|
return c.handlePublicReset(p)
|
|
}
|
|
} else {
|
|
// reject packets with the wrong connection ID
|
|
if !p.header.DestConnectionID.Equal(c.srcConnID) {
|
|
return fmt.Errorf("received a packet with an unexpected connection ID (%s, expected %s)", p.header.DestConnectionID, c.srcConnID)
|
|
}
|
|
}
|
|
|
|
if p.header.IsLongHeader {
|
|
switch p.header.Type {
|
|
case protocol.PacketTypeRetry:
|
|
c.handleRetryPacket(p.header)
|
|
return nil
|
|
case protocol.PacketTypeHandshake, protocol.PacketType0RTT:
|
|
default:
|
|
return fmt.Errorf("Received unsupported packet type: %s", p.header.Type)
|
|
}
|
|
}
|
|
|
|
// this is the first packet we are receiving
|
|
// since it is not a Version Negotiation Packet, this means the server supports the suggested version
|
|
if !c.versionNegotiated {
|
|
c.versionNegotiated = true
|
|
}
|
|
|
|
c.session.handlePacket(p)
|
|
return nil
|
|
}
|
|
|
|
func (c *client) handlePublicReset(p *receivedPacket) error {
|
|
cr := c.conn.RemoteAddr()
|
|
// check if the remote address and the connection ID match
|
|
// otherwise this might be an attacker trying to inject a PUBLIC_RESET to kill the connection
|
|
if cr.Network() != p.remoteAddr.Network() || cr.String() != p.remoteAddr.String() || !p.header.DestConnectionID.Equal(c.srcConnID) {
|
|
return errors.New("Received a spoofed Public Reset")
|
|
}
|
|
pr, err := wire.ParsePublicReset(bytes.NewReader(p.data))
|
|
if err != nil {
|
|
return fmt.Errorf("Received a Public Reset. An error occurred parsing the packet: %s", err)
|
|
}
|
|
c.session.closeRemote(qerr.Error(qerr.PublicReset, fmt.Sprintf("Received a Public Reset for packet number %#x", pr.RejectedPacketNumber)))
|
|
c.logger.Infof("Received Public Reset, rejected packet number: %#x", pr.RejectedPacketNumber)
|
|
return nil
|
|
}
|
|
|
|
func (c *client) handleVersionNegotiationPacket(hdr *wire.Header) error {
|
|
// ignore delayed / duplicated version negotiation packets
|
|
if c.receivedVersionNegotiationPacket || c.versionNegotiated {
|
|
c.logger.Debugf("Received a delayed Version Negotiation Packet.")
|
|
return nil
|
|
}
|
|
|
|
for _, v := range hdr.SupportedVersions {
|
|
if v == c.version {
|
|
// the version negotiation packet contains the version that we offered
|
|
// this might be a packet sent by an attacker (or by a terribly broken server implementation)
|
|
// ignore it
|
|
return nil
|
|
}
|
|
}
|
|
|
|
c.logger.Infof("Received a Version Negotiation Packet. Supported Versions: %s", hdr.SupportedVersions)
|
|
newVersion, ok := protocol.ChooseSupportedVersion(c.config.Versions, hdr.SupportedVersions)
|
|
if !ok {
|
|
return qerr.InvalidVersion
|
|
}
|
|
c.receivedVersionNegotiationPacket = true
|
|
c.negotiatedVersions = hdr.SupportedVersions
|
|
|
|
// switch to negotiated version
|
|
c.initialVersion = c.version
|
|
c.version = newVersion
|
|
if err := c.generateConnectionIDs(); err != nil {
|
|
return err
|
|
}
|
|
|
|
c.logger.Infof("Switching to QUIC version %s. New connection ID: %s", newVersion, c.destConnID)
|
|
c.session.destroy(errCloseSessionForNewVersion)
|
|
return nil
|
|
}
|
|
|
|
func (c *client) handleRetryPacket(hdr *wire.Header) {
|
|
c.logger.Debugf("<- Received Retry")
|
|
hdr.Log(c.logger)
|
|
// A server that performs multiple retries must use a source connection ID of at least 8 bytes.
|
|
// Only a server that won't send additional Retries can use shorter connection IDs.
|
|
if hdr.OrigDestConnectionID.Len() < protocol.MinConnectionIDLenInitial {
|
|
c.logger.Debugf("Received a Retry with a too short Original Destination Connection ID: %d bytes, must have at least %d bytes.", hdr.OrigDestConnectionID.Len(), protocol.MinConnectionIDLenInitial)
|
|
return
|
|
}
|
|
if !hdr.OrigDestConnectionID.Equal(c.destConnID) {
|
|
c.logger.Debugf("Received spoofed Retry. Original Destination Connection ID: %s, expected: %s", hdr.OrigDestConnectionID, c.destConnID)
|
|
return
|
|
}
|
|
c.numRetries++
|
|
if c.numRetries > protocol.MaxRetries {
|
|
c.session.destroy(qerr.CryptoTooManyRejects)
|
|
return
|
|
}
|
|
c.destConnID = hdr.SrcConnectionID
|
|
c.token = hdr.Token
|
|
c.session.destroy(errCloseSessionForRetry)
|
|
}
|
|
|
|
func (c *client) createNewGQUICSession() error {
|
|
c.mutex.Lock()
|
|
defer c.mutex.Unlock()
|
|
runner := &runner{
|
|
onHandshakeCompleteImpl: func(_ Session) { close(c.handshakeChan) },
|
|
removeConnectionIDImpl: c.closeCallback,
|
|
}
|
|
sess, err := newClientSession(
|
|
c.conn,
|
|
runner,
|
|
c.hostname,
|
|
c.version,
|
|
c.destConnID,
|
|
c.srcConnID,
|
|
c.tlsConf,
|
|
c.config,
|
|
c.initialVersion,
|
|
c.negotiatedVersions,
|
|
c.logger,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.session = sess
|
|
c.packetHandlers.Add(c.srcConnID, c)
|
|
if c.config.RequestConnectionIDOmission {
|
|
c.packetHandlers.Add(protocol.ConnectionID{}, c)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *client) createNewTLSSession(
|
|
paramsChan <-chan handshake.TransportParameters,
|
|
version protocol.VersionNumber,
|
|
) error {
|
|
c.mutex.Lock()
|
|
defer c.mutex.Unlock()
|
|
runner := &runner{
|
|
onHandshakeCompleteImpl: func(_ Session) { close(c.handshakeChan) },
|
|
removeConnectionIDImpl: c.closeCallback,
|
|
}
|
|
sess, err := newTLSClientSession(
|
|
c.conn,
|
|
runner,
|
|
c.token,
|
|
c.destConnID,
|
|
c.srcConnID,
|
|
c.config,
|
|
c.mintConf,
|
|
paramsChan,
|
|
1,
|
|
c.logger,
|
|
c.version,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.session = sess
|
|
c.packetHandlers.Add(c.srcConnID, c)
|
|
return nil
|
|
}
|
|
|
|
func (c *client) Close() error {
|
|
c.mutex.Lock()
|
|
defer c.mutex.Unlock()
|
|
if c.session == nil {
|
|
return nil
|
|
}
|
|
return c.session.Close()
|
|
}
|
|
|
|
func (c *client) destroy(e error) {
|
|
c.mutex.Lock()
|
|
defer c.mutex.Unlock()
|
|
if c.session == nil {
|
|
return
|
|
}
|
|
c.session.destroy(e)
|
|
}
|
|
|
|
func (c *client) GetVersion() protocol.VersionNumber {
|
|
c.mutex.Lock()
|
|
v := c.version
|
|
c.mutex.Unlock()
|
|
return v
|
|
}
|
|
|
|
func (c *client) GetPerspective() protocol.Perspective {
|
|
return protocol.PerspectiveClient
|
|
}
|