net.luminis.quic.tls.QuicTransportParametersExtension Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of kwik Show documentation
Show all versions of kwik Show documentation
A QUIC implementation in Java
/*
* Copyright © 2019, 2020, 2021, 2022, 2023 Peter Doornbosch
*
* This file is part of Kwik, an implementation of the QUIC protocol in Java.
*
* Kwik is free software: you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your option)
* any later version.
*
* Kwik is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
* more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see .
*/
package net.luminis.quic.tls;
import net.luminis.quic.QuicConstants;
import net.luminis.quic.core.ProtocolError;
import net.luminis.quic.core.Role;
import net.luminis.quic.core.TransportParameters;
import net.luminis.quic.core.Version;
import net.luminis.quic.generic.InvalidIntegerEncodingException;
import net.luminis.quic.generic.VariableLengthInteger;
import net.luminis.quic.log.Logger;
import net.luminis.quic.util.Bytes;
import net.luminis.tls.alert.DecodeErrorException;
import net.luminis.tls.extension.Extension;
import net.luminis.tls.util.ByteUtils;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import static net.luminis.quic.QuicConstants.TransportParameterId.*;
import static net.luminis.quic.core.Role.Server;
/**
* Quic transport parameter TLS extension.
* see https://www.rfc-editor.org/rfc/rfc9001.html#name-quic-transport-parameters-e
*/
public class QuicTransportParametersExtension extends Extension {
private static final int MINIMUM_EXTENSION_LENGTH = 2;
public static final int CODEPOINT_IETFDRAFT = 0xffa5;
public static final int CODEPOINT_V1 = 0x39;
private final Version quicVersion;
private Role senderRole;
private byte[] data;
private TransportParameters params;
private Integer discardTransportParameterSize;
public static boolean isCodepoint(Version quicVersion, int extensionType) {
if (quicVersion.isV1V2()) {
return extensionType == CODEPOINT_V1;
}
else {
return extensionType == CODEPOINT_IETFDRAFT;
}
}
public QuicTransportParametersExtension() {
this(Version.getDefault());
}
public QuicTransportParametersExtension(Version quicVersion) {
this.quicVersion = quicVersion;
params = new TransportParameters();
}
/**
* Creates a Quic Transport Parameters Extension for use in a Client Hello.
* @param quicVersion
* @param senderRole
*/
public QuicTransportParametersExtension(Version quicVersion, TransportParameters params, Role senderRole) {
this.quicVersion = quicVersion;
this.params = params;
this.senderRole = senderRole;
}
@Override
public byte[] getBytes() {
if (data == null) {
serialize();
}
return data;
}
public void addDiscardTransportParameter(int parameterSize) {
// https://github.com/quicwg/base-drafts/wiki/Quantum-Readiness-test
discardTransportParameterSize = parameterSize;
}
protected void serialize() {
ByteBuffer buffer = ByteBuffer.allocate(1024 + (discardTransportParameterSize != null? discardTransportParameterSize: 0));
// https://tools.ietf.org/html/draft-ietf-quic-tls-32#section-8.2
// "quic_transport_parameters(0xffa5)"
buffer.putShort((short) (quicVersion.equals(Version.QUIC_version_1) || quicVersion.isV2()? CODEPOINT_V1: CODEPOINT_IETFDRAFT));
// Format is same as any TLS extension, so next are 2 bytes length
buffer.putShort((short) 0); // PlaceHolder, will be correctly set at the end of this method.
// https://tools.ietf.org/html/draft-ietf-quic-transport-32#section-18.2
// "Those transport parameters that are identified as integers use a variable-length integer encoding (...) and
// have a default value of 0 if the transport parameter is absent, unless otherwise stated."
if (senderRole == Server) {
// "The value of the Destination Connection ID field from the first Initial packet sent by the client (...)
// This transport parameter is only sent by a server."
addTransportParameter(buffer, original_destination_connection_id, params.getOriginalDestinationConnectionId());
}
// "The max idle timeout is a value in milliseconds that is encoded as an integer"
addTransportParameter(buffer, max_idle_timeout, params.getMaxIdleTimeout());
if (senderRole == Server && params.getStatelessResetToken() != null) {
// "A stateless reset token is used in verifying a stateless reset (...). This parameter is a sequence of 16
// bytes. This transport parameter MUST NOT be sent by a client, but MAY be sent by a server."
addTransportParameter(buffer, stateless_reset_token, params.getStatelessResetToken());
}
// "The maximum UDP payload size parameter is an integer value that limits the size of UDP payloads that the
// endpoint is willing to receive. UDP datagrams with payloads larger than this limit are not likely to be
// processed by the receiver."
addTransportParameter(buffer, max_udp_payload_size, params.getMaxUdpPayloadSize());
// "The initial maximum data parameter is an integer value that contains the initial value for the maximum
// amount of data that can be sent on the connection. This is equivalent to sending a MAX_DATA for the
// connection immediately after completing the handshake."
addTransportParameter(buffer, initial_max_data, params.getInitialMaxData());
// "This parameter is an integer value specifying the initial flow control limit for locally-initiated
// bidirectional streams. This limit applies to newly created bidirectional streams opened by the endpoint that
// sends the transport parameter."
addTransportParameter(buffer, initial_max_stream_data_bidi_local, params.getInitialMaxStreamDataBidiLocal());
// "This parameter is an integer value specifying the initial flow control limit for peer-initiated bidirectional
// streams. This limit applies to newly created bidirectional streams opened by the endpoint that receives
// the transport parameter."
addTransportParameter(buffer, initial_max_stream_data_bidi_remote, params.getInitialMaxStreamDataBidiRemote());
// "This parameter is an integer value specifying the initial flow control limit for unidirectional streams.
// This limit applies to newly created bidirectional streams opened by the endpoint that receives the transport
// parameter."
addTransportParameter(buffer, initial_max_stream_data_uni, params.getInitialMaxStreamDataUni());
// "The initial maximum bidirectional streams parameter is an integer value that contains the initial maximum
// number of bidirectional streams the peer may initiate. If this parameter is absent or zero, the peer cannot
// open bidirectional streams until a MAX_STREAMS frame is sent."
addTransportParameter(buffer, initial_max_streams_bidi, params.getInitialMaxStreamsBidi());
// "The initial maximum unidirectional streams parameter is an integer value that contains the initial maximum
// number of unidirectional streams the peer may initiate. If this parameter is absent or zero, the peer cannot
// open unidirectional streams until a MAX_STREAMS frame is sent."
addTransportParameter(buffer, initial_max_streams_uni, params.getInitialMaxStreamsUni());
// "The acknowledgement delay exponent is an integer value indicating an exponent used to decode the ACK Delay
// field in the ACK frame"
addTransportParameter(buffer, ack_delay_exponent, params.getAckDelayExponent());
// "The maximum acknowledgement delay is an integer value indicating the maximum amount of time in milliseconds
// by which the endpoint will delay sending acknowledgments."
addTransportParameter(buffer, max_ack_delay, params.getMaxAckDelay());
// "The disable active migration transport parameter is included if the endpoint does not support active
// connection migration (Section 9) on the address being used during the handshake. "
if (params.getDisableMigration()) {
addTransportParameter(buffer, disable_active_migration);
}
// Intentionally omitted (kwik server does not support preferred address)
// preferred_address
// "The maximum number of connection IDs from the peer that an endpoint is willing to store."
addTransportParameter(buffer, active_connection_id_limit, params.getActiveConnectionIdLimit());
// "The value that the endpoint included in the Source Connection ID field of the first Initial packet it
// sends for the connection"
addTransportParameter(buffer, initial_source_connection_id, params.getInitialSourceConnectionId());
if (senderRole == Server) {
// "The value that the the server included in the Source Connection ID field of a Retry packet"
// "This transport parameter is only sent by a server."
if (params.getRetrySourceConnectionId() != null) {
addTransportParameter(buffer, retry_source_connection_id, params.getRetrySourceConnectionId());
}
}
if (discardTransportParameterSize != null) {
// See https://github.com/quicwg/base-drafts/wiki/Quantum-Readiness-test
addTransportParameter(buffer, (short) 0x173e, new byte[discardTransportParameterSize]);
}
if (params.getVersionInformation() != null) {
TransportParameters.VersionInformation versions = params.getVersionInformation();
ByteBuffer data = ByteBuffer.allocate(4 + versions.getOtherVersions().size() * 4);
data.put(versions.getChosenVersion().getBytes());
versions.getOtherVersions().forEach(v -> data.put(v.getBytes()));
addTransportParameter(buffer, version_information, data.array());
}
int length = buffer.position();
buffer.limit(length);
int extensionsSize = length - 2 - 2; // 2 bytes for the length itself and 2 for the type
buffer.putShort(2, (short) extensionsSize);
data = new byte[length];
buffer.flip();
buffer.get(data);
}
public QuicTransportParametersExtension parse(ByteBuffer buffer, Role senderRole, Logger log) throws DecodeErrorException {
int extensionType = buffer.getShort() & 0xffff;
if (!isCodepoint(quicVersion, extensionType)) {
throw new RuntimeException(); // Must be programming error
}
int extensionLength = buffer.getShort();
int startPosition = buffer.position();
log.debug("Transport parameters: ");
while (buffer.position() - startPosition < extensionLength) {
try {
parseTransportParameter(buffer, senderRole, log);
} catch (InvalidIntegerEncodingException e) {
throw new DecodeErrorException("invalid integer encoding in transport parameter extension");
}
}
int realSize = buffer.position() - startPosition;
if (realSize != extensionLength) {
throw new DecodeErrorException("inconsistent size in transport parameter extension");
}
return this;
}
void parseTransportParameter(ByteBuffer buffer, Role senderRol, Logger log) throws DecodeErrorException, InvalidIntegerEncodingException {
long parameterId = VariableLengthInteger.parseLong(buffer);
int size = VariableLengthInteger.parse(buffer);
if (buffer.remaining() < size) {
throw new DecodeErrorException("Invalid transport parameter extension");
}
int startPosition = buffer.position();
if (parameterId == original_destination_connection_id.value) {
byte[] destinationCid = new byte[size];
buffer.get(destinationCid);
log.debug("- original destination connection id: ", destinationCid);
params.setOriginalDestinationConnectionId(destinationCid);
}
else if (parameterId == max_idle_timeout.value) {
long idleTimeout = VariableLengthInteger.parseLong(buffer);
log.debug("- max idle timeout: " + idleTimeout);
params.setMaxIdleTimeout(idleTimeout);
}
else if (parameterId == stateless_reset_token.value) {
byte[] resetToken = new byte[16];
buffer.get(resetToken);
log.debug("- stateless reset token: " + ByteUtils.bytesToHex(resetToken));
params.setStatelessResetToken(resetToken);
}
else if (parameterId == max_udp_payload_size.value) {
int maxPacketSize = VariableLengthInteger.parse(buffer);
log.debug("- max udp payload size: " + maxPacketSize);
params.setMaxUdpPayloadSize(maxPacketSize);
}
else if (parameterId == initial_max_data.value) {
long maxData = VariableLengthInteger.parseLong(buffer);
log.debug("- initial max data: " + maxData);
params.setInitialMaxData(maxData);
}
else if (parameterId == initial_max_stream_data_bidi_local.value) {
long maxStreamDataBidiLocal = VariableLengthInteger.parseLong(buffer);
log.debug("- initial max stream data bidi local: " + maxStreamDataBidiLocal);
params.setInitialMaxStreamDataBidiLocal(maxStreamDataBidiLocal);
}
else if (parameterId == initial_max_stream_data_bidi_remote.value) {
long maxStreamDataBidiRemote = VariableLengthInteger.parseLong(buffer);
log.debug("- initial max stream data bidi remote: " + maxStreamDataBidiRemote);
params.setInitialMaxStreamDataBidiRemote(maxStreamDataBidiRemote);
}
else if (parameterId == initial_max_stream_data_uni.value) {
long maxStreamDataUni = VariableLengthInteger.parseLong(buffer);
log.debug("- initial max stream data uni: " + maxStreamDataUni);
params.setInitialMaxStreamDataUni(maxStreamDataUni);
}
else if (parameterId == initial_max_streams_bidi.value) {
long maxBidiStreams = VariableLengthInteger.parseLong(buffer);
log.debug("- initial max bidi streams: " + maxBidiStreams);
params.setInitialMaxStreamsBidi(maxBidiStreams);
}
else if (parameterId == initial_max_streams_uni.value) {
long maxUniStreams = VariableLengthInteger.parseLong(buffer);
log.debug("- max uni streams: " + maxUniStreams);
params.setInitialMaxStreamsUni(maxUniStreams);
}
else if (parameterId == ack_delay_exponent.value) {
int ackDelayExponent = VariableLengthInteger.parse(buffer);
log.debug("- ack delay exponent: " + ackDelayExponent);
params.setAckDelayExponent(ackDelayExponent);
}
else if (parameterId == max_ack_delay.value) {
// https://tools.ietf.org/html/draft-ietf-quic-transport-30#section-18.2
// "The maximum acknowledgement delay is an integer value indicating the maximum amount of time in
// milliseconds by which the endpoint will delay sending acknowledgments. "
int maxAckDelay = VariableLengthInteger.parse(buffer);
log.debug("- max ack delay: " + maxAckDelay);
params.setMaxAckDelay(maxAckDelay);
}
else if (parameterId == disable_active_migration.value) {
log.debug("- disable migration");
params.setDisableMigration(true);
}
else if (parameterId == preferred_address.value) {
parsePreferredAddress(buffer, log);
}
else if (parameterId == active_connection_id_limit.value) {
long activeConnectionIdLimit = VariableLengthInteger.parseLong(buffer);
log.debug("- active connection id limit: " + activeConnectionIdLimit);
params.setActiveConnectionIdLimit((int) activeConnectionIdLimit);
}
else if (parameterId == initial_source_connection_id.value) {
byte[] initialSourceCid = new byte[size];
buffer.get(initialSourceCid);
log.debug("- initial source connection id: " + ByteUtils.bytesToHex(initialSourceCid));
params.setInitialSourceConnectionId(initialSourceCid);
}
else if (parameterId == retry_source_connection_id.value) {
byte[] retrySourceCid = new byte[size];
buffer.get(retrySourceCid);
log.debug("- retry source connection id: " + ByteUtils.bytesToHex(retrySourceCid));
params.setRetrySourceConnectionId(retrySourceCid);
}
else if (parameterId == version_information.value) {
// Óhttps://www.ietf.org/archive/id/draft-ietf-quic-version-negotiation-05.html#name-version-information
if (size % 4 != 0 || size < 4) {
throw new DecodeErrorException("invalid parameters size");
}
int chosenVersion = buffer.getInt();
List otherVersions = new ArrayList<>();
for (int i = 0; i < size/4 - 1; i++) {
int otherVersion = buffer.getInt();
otherVersions.add(Version.parse(otherVersion));
}
params.setVersionInformation(new TransportParameters.VersionInformation(Version.parse(chosenVersion), otherVersions));
}
else {
String extension = "";
if (parameterId == 0x0020) extension = "datagram";
if (parameterId == 0x0040) extension = "multi-path";
if (parameterId == 0x1057) extension = "loss-bits";
if (parameterId == 0x173e) extension = "discard";
if (parameterId == 0x2ab2) extension = "grease-quic-bit";
if (parameterId == 0x7157) extension = "timestamp"; // https://datatracker.ietf.org/doc/html/draft-huitema-quic-ts-02#section-5
if (parameterId == 0x7158) extension = "timestamp"; // https://datatracker.ietf.org/doc/html/draft-huitema-quic-ts-05#section-5
if (parameterId == 0x73db) extension = "version-negotiation"; // https://datatracker.ietf.org/doc/draft-ietf-quic-version-negotiation/02/
if (parameterId == 0xde1a) extension = "delayed-ack"; // https://datatracker.ietf.org/doc/html/draft-iyengar-quic-delayed-ack-01#section-3
if (parameterId == 0xff73db) extension = "version-information-4-13"; // https://datatracker.ietf.org/doc/draft-ietf-quic-version-negotiation/4/
if (parameterId == 0xff02de1aL) extension = "delayed-ack"; // https://datatracker.ietf.org/doc/html/draft-iyengar-quic-delayed-ack-02#section-3
String msg;
if (extension.isBlank()) {
msg = String.format("- unknown transport parameter 0x%04x, size %d", parameterId, size);
}
else {
msg = String.format("- unsupported transport parameter 0x%04x, size %d (%s)", parameterId, size, extension);
}
log.warn(msg);
buffer.get(new byte[size]);
}
int realSize = buffer.position() - startPosition;
if (realSize != size) {
throw new DecodeErrorException("inconsistent size in transport parameter");
}
}
private void parsePreferredAddress(ByteBuffer buffer, Logger log) {
try {
TransportParameters.PreferredAddress preferredAddress = new TransportParameters.PreferredAddress();
byte[] ip4 = new byte[4];
buffer.get(ip4);
if (!Bytes.allZero(ip4)) {
preferredAddress.setIp4(InetAddress.getByAddress(ip4));
}
preferredAddress.setIp4Port((buffer.get() << 8) | buffer.get());
byte[] ip6 = new byte[16];
buffer.get(ip6);
if (!Bytes.allZero(ip6)) {
preferredAddress.setIp6(InetAddress.getByAddress(ip6));
}
preferredAddress.setIp6Port((buffer.get() << 8) | buffer.get());
if (preferredAddress.getIp4() == null && preferredAddress.getIp6() == null) {
throw new ProtocolError("Preferred address: no valid IP address");
}
int connectionIdSize = buffer.get();
preferredAddress.setConnectionId(buffer, connectionIdSize);
preferredAddress.setStatelessResetToken(buffer, 16); //
params.setPreferredAddress(preferredAddress);
}
catch (UnknownHostException invalidIpAddressLength) {
// Impossible
throw new RuntimeException();
}
}
private void addTransportParameter(ByteBuffer buffer, QuicConstants.TransportParameterId id, long value) {
addTransportParameter(buffer, id.value, value);
}
private void addTransportParameter(ByteBuffer buffer, QuicConstants.TransportParameterId id) {
VariableLengthInteger.encode(id.value, buffer);
int valueLength = 0;
VariableLengthInteger.encode(valueLength, buffer);
}
private void addTransportParameter(ByteBuffer buffer, int id, long value) {
VariableLengthInteger.encode(id, buffer);
buffer.mark();
int encodedValueLength = VariableLengthInteger.encode(value, buffer);
buffer.reset();
VariableLengthInteger.encode(encodedValueLength, buffer);
VariableLengthInteger.encode(value, buffer);
}
protected void addTransportParameter(ByteBuffer buffer, QuicConstants.TransportParameterId id, byte[] value) {
addTransportParameter(buffer, id.value, value);
}
private void addTransportParameter(ByteBuffer buffer, int id, byte[] value) {
VariableLengthInteger.encode(id, buffer);
VariableLengthInteger.encode(value.length, buffer);
buffer.put(value);
}
public TransportParameters getTransportParameters() {
return params;
}
}