1
0
mirror of https://github.com/facebookincubator/mvfst.git synced 2025-11-24 04:01:07 +03:00
Files
mvfst/quic/client/handshake/ClientHandshake.cpp
Joseph Beshay b45c82b884 ACK_EXTENDED frame support
Summary:
This introduces a new frame type for acks (ACK_EXTENDED) that can carry optional fields depending on the features supported by the peer. The currently supported features set will include ECN count fields, and Receive Timstamp fields. This enables a quic connection to report both ECN counts and receive timestamps, which is not possible otherwise because they use different frame types.

Support for the extended ack as well as the set of features that can be included in it is negotiated through a new transport parameter (extended_ack_supported = 0xff0a004). Its value indicates which features are supported by the local transport. The value is an integer which is evaluated against the following bitmasks:
```
  ECN_COUNTS = 0x01,
  RECEIVE_TIMESTAMPS = 0x02,
```

This diff introduces the transport parameter and negotiates the supported features between the peers of the connection. The parameter is cached in the psk cache so the client can remember the server config. It is also encoded inside the 0-rtt ticket so the server can reject it if its local config has changed.

The following diffs add reading and writing the frame itself.

The ACK_EXTENDED frame itself will have the following format
```
ACK_EXTENDED Frame {
  Type (i) = 0xB1
  // Fields of the existing ACK (type=0x02) frame:
  Largest Acknowledged (i),
  ACK Delay (i),
  ACK Range Count (i),
  First ACK Range (i),
  ACK Range (..) ...,
  Extended Ack Features (i),
  // Optional ECN counts (if bit 0 is set in Features)
  [ECN Counts (..)],
  // Optional Receive Timestamps (if bit 1 is set in Features)
  [Receive Timestamps (..)]
}

// Fields from the existing ACK_ECN frame
ECN Counts {
  ECT0 Count (i),
  ECT1 Count (i),
  ECN-CE Count (i),
}

// Fields from the existing ACK_RECEIVE_TIMESTAMPS frame
Receive Timestamps {
  Timestamp Range Count (i),
  Timestamp Ranges (..) ...,
}

Timestamp Range {
  Gap (i),
  Timestamp Delta Count (i),
  Timestamp Delta (i) ...,
}
```

Reviewed By: sharmafb

Differential Revision: D68931151

fbshipit-source-id: 44c8c83d2f434abca97c4e85f0fa7502736cddc1
2025-02-24 12:32:50 -08:00

268 lines
8.8 KiB
C++

/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include <quic/client/handshake/ClientHandshake.h>
#include <quic/client/handshake/CachedServerTransportParameters.h>
#include <quic/client/handshake/ClientTransportParametersExtension.h>
#include <quic/client/state/ClientStateMachine.h>
#include <quic/state/QuicStreamFunctions.h>
namespace quic {
ClientHandshake::ClientHandshake(QuicClientConnectionState* conn)
: conn_(conn) {}
void ClientHandshake::connect(
Optional<std::string> hostname,
std::shared_ptr<ClientTransportParametersExtension> transportParams) {
transportParams_ = std::move(transportParams);
Optional<CachedServerTransportParameters> cachedServerTransportParams =
connectImpl(std::move(hostname));
throwOnError();
if (conn_->zeroRttWriteCipher) {
if (conn_->qLogger) {
conn_->qLogger->addTransportStateUpdate(kZeroRttAttempted);
}
// If zero rtt write cipher is derived, it means the cached psk was valid
DCHECK(cachedServerTransportParams);
cacheServerInitialParams(
*conn_,
cachedServerTransportParams->initialMaxData,
cachedServerTransportParams->initialMaxStreamDataBidiLocal,
cachedServerTransportParams->initialMaxStreamDataBidiRemote,
cachedServerTransportParams->initialMaxStreamDataUni,
cachedServerTransportParams->initialMaxStreamsBidi,
cachedServerTransportParams->initialMaxStreamsUni,
cachedServerTransportParams->knobFrameSupport,
cachedServerTransportParams->ackReceiveTimestampsEnabled,
cachedServerTransportParams->maxReceiveTimestampsPerAck,
cachedServerTransportParams->receiveTimestampsExponent,
cachedServerTransportParams->reliableStreamResetSupport,
cachedServerTransportParams->extendedAckFeatures);
updateTransportParamsFromCachedEarlyParams(
*conn_, *cachedServerTransportParams);
}
}
void ClientHandshake::doHandshake(
std::unique_ptr<folly::IOBuf> data,
EncryptionLevel encryptionLevel) {
if (!data) {
return;
}
// TODO: deal with clear text alert messages. It's possible that a MITM who
// mucks with the finished messages could cause the decryption to be invalid
// on the server, which would result in a cleartext close or a cleartext
// alert. We currently switch to 1-rtt ciphers immediately for reads and
// throw away the cleartext cipher for reads, this would result in us
// dropping the alert and timing out instead.
if (phase_ == Phase::Initial) {
// This could be an HRR or a cleartext alert.
handshakeInitiated();
}
// First add it to the right read buffer.
switch (encryptionLevel) {
case EncryptionLevel::Initial:
initialReadBuf_.append(std::move(data));
break;
case EncryptionLevel::Handshake:
handshakeReadBuf_.append(std::move(data));
break;
case EncryptionLevel::EarlyData:
case EncryptionLevel::AppData:
appDataReadBuf_.append(std::move(data));
break;
default:
LOG(FATAL) << "Unhandled EncryptionLevel";
}
// Get the current buffer type the transport is accepting.
waitForData_ = false;
while (!waitForData_) {
switch (getReadRecordLayerEncryptionLevel()) {
case EncryptionLevel::Initial:
processSocketData(initialReadBuf_);
break;
case EncryptionLevel::Handshake:
processSocketData(handshakeReadBuf_);
break;
case EncryptionLevel::EarlyData:
case EncryptionLevel::AppData:
processSocketData(appDataReadBuf_);
break;
default:
LOG(FATAL) << "Unhandled EncryptionLevel";
}
throwOnError();
}
}
void ClientHandshake::handshakeConfirmed() {
phase_ = Phase::Established;
}
ClientHandshake::Phase ClientHandshake::getPhase() const {
return phase_;
}
const Optional<ServerTransportParameters>&
ClientHandshake::getServerTransportParams() {
return transportParams_->getServerTransportParams();
}
Optional<bool> ClientHandshake::getZeroRttRejected() {
return zeroRttRejected_;
}
Optional<bool> ClientHandshake::getCanResendZeroRtt() const {
return canResendZeroRtt_;
}
void ClientHandshake::computeCiphers(CipherKind kind, folly::ByteRange secret) {
std::unique_ptr<Aead> aead = buildAead(kind, secret);
std::unique_ptr<PacketNumberCipher> packetNumberCipher =
buildHeaderCipher(secret);
switch (kind) {
case CipherKind::HandshakeWrite:
conn_->handshakeWriteCipher = std::move(aead);
conn_->handshakeWriteHeaderCipher = std::move(packetNumberCipher);
break;
case CipherKind::HandshakeRead:
conn_->readCodec->setHandshakeReadCipher(std::move(aead));
conn_->readCodec->setHandshakeHeaderCipher(std::move(packetNumberCipher));
break;
case CipherKind::OneRttWrite:
writeTrafficSecret_ = folly::IOBuf::copyBuffer(secret);
conn_->oneRttWriteCipher = std::move(aead);
conn_->oneRttWriteHeaderCipher = std::move(packetNumberCipher);
break;
case CipherKind::OneRttRead:
readTrafficSecret_ = folly::IOBuf::copyBuffer(secret);
conn_->readCodec->setOneRttReadCipher(std::move(aead));
conn_->readCodec->setOneRttHeaderCipher(std::move(packetNumberCipher));
conn_->readCodec->setNextOneRttReadCipher(getNextOneRttReadCipher());
break;
case CipherKind::ZeroRttWrite:
getClientConn()->zeroRttWriteCipher = std::move(aead);
getClientConn()->zeroRttWriteHeaderCipher = std::move(packetNumberCipher);
break;
default:
// Report error?
break;
}
}
std::unique_ptr<Aead> ClientHandshake::getNextOneRttWriteCipher() {
throwOnError();
CHECK(writeTrafficSecret_);
LOG_IF(WARNING, trafficSecretSync_ > 1 || trafficSecretSync_ < -1)
<< "Client read and write secrets are out of sync";
writeTrafficSecret_ = getNextTrafficSecret(writeTrafficSecret_->coalesce());
trafficSecretSync_--;
auto cipher =
buildAead(CipherKind::OneRttWrite, writeTrafficSecret_->coalesce());
return cipher;
}
std::unique_ptr<Aead> ClientHandshake::getNextOneRttReadCipher() {
throwOnError();
CHECK(readTrafficSecret_);
LOG_IF(WARNING, trafficSecretSync_ > 1 || trafficSecretSync_ < -1)
<< "Client read and write secrets are out of sync";
readTrafficSecret_ = getNextTrafficSecret(readTrafficSecret_->coalesce());
trafficSecretSync_++;
auto cipher =
buildAead(CipherKind::OneRttRead, readTrafficSecret_->coalesce());
return cipher;
}
void ClientHandshake::raiseError(folly::exception_wrapper error) {
error_ = std::move(error);
}
void ClientHandshake::throwOnError() {
if (error_) {
error_.throw_exception();
}
}
void ClientHandshake::waitForData() {
waitForData_ = true;
}
void ClientHandshake::writeDataToStream(
EncryptionLevel encryptionLevel,
Buf data) {
if (encryptionLevel == EncryptionLevel::AppData) {
// Don't write 1-rtt handshake data on the client.
return;
}
auto cryptoStream = getCryptoStream(*conn_->cryptoState, encryptionLevel);
writeDataToQuicStream(*cryptoStream, std::move(data));
}
void ClientHandshake::handshakeInitiated() {
CHECK(phase_ == Phase::Initial);
phase_ = Phase::Handshake;
}
void ClientHandshake::computeZeroRttCipher() {
VLOG(10) << "Computing Client zero rtt keys";
earlyDataAttempted_ = true;
}
void ClientHandshake::computeOneRttCipher(bool earlyDataAccepted) {
// The 1-rtt handshake should have succeeded if we know that the early
// write failed. We currently treat the data as lost.
// TODO: we need to deal with HRR based rejection as well, however we don't
// have an API right now.
if (earlyDataAttempted_ && !earlyDataAccepted) {
zeroRttRejected_ = true;
// If the early parameters don't match. The transport needs to update the
// parameters or terminate the connection to force the client to retry.
canResendZeroRtt_ = matchEarlyParameters();
} else if (earlyDataAttempted_ && earlyDataAccepted) {
zeroRttRejected_ = false;
}
// After a successful handshake we should send packets with the type of
// ClientCleartext. We assume that by the time we get the data for the QUIC
// stream, the server would have also acked all the client initial packets.
CHECK(phase_ == Phase::Handshake);
phase_ = Phase::OneRttKeysDerived;
}
QuicClientConnectionState* ClientHandshake::getClientConn() {
return conn_;
}
const QuicClientConnectionState* ClientHandshake::getClientConn() const {
return conn_;
}
const std::shared_ptr<ClientTransportParametersExtension>&
ClientHandshake::getClientTransportParameters() const {
return transportParams_;
}
void ClientHandshake::setZeroRttRejectedForTest(bool rejected) {
zeroRttRejected_ = rejected;
}
void ClientHandshake::setCanResendZeroRttForTest(bool canResendZeroRtt) {
canResendZeroRtt_ = canResendZeroRtt;
}
} // namespace quic