/* * 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; import net.luminis.quic.cid.ConnectionIdInfo; import net.luminis.quic.cid.ConnectionIdManager; import net.luminis.quic.frame.*; import net.luminis.quic.log.Logger; import net.luminis.quic.log.NullLogger; import net.luminis.quic.packet.*; import net.luminis.quic.send.SenderImpl; import net.luminis.quic.stream.EarlyDataStream; import net.luminis.quic.stream.FlowControl; import net.luminis.quic.stream.StreamManager; import net.luminis.quic.tls.QuicTransportParametersExtension; import net.luminis.tls.*; import net.luminis.tls.extension.ApplicationLayerProtocolNegotiationExtension; import net.luminis.tls.extension.EarlyDataExtension; import net.luminis.tls.extension.Extension; import net.luminis.tls.handshake.*; import javax.net.ssl.X509TrustManager; import java.io.IOException; import java.net.*; import java.nio.ByteBuffer; import java.nio.file.Path; import java.security.PrivateKey; import java.security.cert.X509Certificate; import java.time.Duration; import java.time.Instant; import java.util.*; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.stream.Collectors; import static net.luminis.quic.EarlyDataStatus.*; import static net.luminis.quic.EncryptionLevel.*; import static net.luminis.quic.QuicConstants.TransportErrorCode.*; import static net.luminis.tls.util.ByteUtils.bytesToHex; /** * Creates and maintains a QUIC connection with a QUIC server. */ public class QuicClientConnectionImpl extends QuicConnectionImpl implements QuicClientConnection, PacketProcessor, TlsStatusEventHandler, FrameProcessor { private final String host; private final int port; private final QuicSessionTicket sessionTicket; private final TlsClientEngine tlsEngine; private final DatagramSocketFactory socketFactory; private final DatagramSocket socket; private final InetAddress serverAddress; private final SenderImpl sender; private final Receiver receiver; private final StreamManager streamManager; private final X509Certificate clientCertificate; private final PrivateKey clientCertificateKey; private final ConnectionIdManager connectionIdManager; private final Version originalVersion; private final Version preferredVersion; private volatile byte[] token; private final CountDownLatch handshakeFinishedCondition = new CountDownLatch(1); private volatile TransportParameters peerTransportParams; private KeepAliveActor keepAliveActor; private String applicationProtocol; private final List newSessionTickets = Collections.synchronizedList(new ArrayList<>()); private boolean ignoreVersionNegotiation; private volatile EarlyDataStatus earlyDataStatus = None; private final List cipherSuites; private final GlobalAckGenerator ackGenerator; private Integer clientHelloEnlargement; private volatile Thread receiverThread; private volatile String handshakeError; private QuicClientConnectionImpl(String host, int port, QuicSessionTicket sessionTicket, Version originalVersion, Version preferredVersion, Logger log, String proxyHost, Path secretsFile, Integer initialRtt, Integer cidLength, List cipherSuites, X509Certificate clientCertificate, PrivateKey clientCertificateKey, DatagramSocketFactory socketFactory ) throws UnknownHostException, SocketException { super(originalVersion, Role.Client, secretsFile, log); log.info("Creating connection with " + host + ":" + port + " with " + originalVersion); this.originalVersion = originalVersion; this.preferredVersion = preferredVersion; this.host = host; this.port = port; serverAddress = InetAddress.getByName(proxyHost != null? proxyHost: host); this.sessionTicket = sessionTicket; this.cipherSuites = cipherSuites; this.clientCertificate = clientCertificate; this.clientCertificateKey = clientCertificateKey; this.socketFactory = socketFactory; socket = socketFactory.createDatagramSocket(); idleTimer = new IdleTimer(this, log); sender = new SenderImpl(quicVersion, getMaxPacketSize(), socket, new InetSocketAddress(serverAddress, port), this, initialRtt, log); sender.enableAllLevels(); idleTimer.setPtoSupplier(sender::getPto); ackGenerator = sender.getGlobalAckGenerator(); receiver = new Receiver(socket, log, this::abortConnection); streamManager = new StreamManager(this, Role.Client, log, 10, 10); BiConsumer closeWithErrorFunction = (error, reason) -> { immediateCloseWithError(EncryptionLevel.App, error, reason); }; connectionIdManager = new ConnectionIdManager(cidLength, 2, sender, closeWithErrorFunction, log); connectionState = Status.Created; tlsEngine = new TlsClientEngine(new ClientMessageSender() { @Override public void send(ClientHello clientHello) { CryptoStream cryptoStream = getCryptoStream(Initial); cryptoStream.write(clientHello, true); connectionState = Status.Handshaking; connectionSecrets.setClientRandom(clientHello.getClientRandom()); log.sentPacketInfo(cryptoStream.toStringSent()); } @Override public void send(FinishedMessage finished) { CryptoStream cryptoStream = getCryptoStream(Handshake); cryptoStream.write(finished, true); log.sentPacketInfo(cryptoStream.toStringSent()); } @Override public void send(CertificateMessage certificateMessage) throws IOException { CryptoStream cryptoStream = getCryptoStream(Handshake); cryptoStream.write(certificateMessage, true); log.sentPacketInfo(cryptoStream.toStringSent()); } @Override public void send(CertificateVerifyMessage certificateVerifyMessage) { CryptoStream cryptoStream = getCryptoStream(Handshake); cryptoStream.write(certificateVerifyMessage, true); log.sentPacketInfo(cryptoStream.toStringSent()); } }, this); } /** * Set up the connection with the server. */ @Override public void connect(int connectionTimeout, String alpn) throws IOException { connect(connectionTimeout, alpn, null, null); } @Override public void connect(int connectionTimeout, String alpn, TransportParameters transportParameters) throws IOException { connect(connectionTimeout, alpn, transportParameters, null); } /** * Set up the connection with the server, enabling use of 0-RTT data. * The early data is sent on a bidirectional stream and the output stream is closed immediately after sending the data * if closeOutput is set in the StreamEarlyData. * If this connection object is not in the initial state, an IllegalStateException will be thrown, so * the connect method can only be successfully called once. Use the isConnected method to check whether * it can be connected. * * @param connectionTimeout the connection timeout in milliseconds * @param applicationProtocol the ALPN of the protocol that will be used on top of the QUIC connection * @param transportParameters the transport parameters to use for the connection * @param earlyData early data to send (RTT-0), each element of the list will lead to a bidirectional stream * @return list of streams that was created for the early data; the size of the list will be equal * to the size of the list of the earlyData parameter, but may contain nulls if a stream * could not be created due to reaching the max initial streams limit. * @throws IOException */ @Override public synchronized List connect(int connectionTimeout, String applicationProtocol, TransportParameters transportParameters, List earlyData) throws IOException { if (applicationProtocol.trim().isEmpty()) { throw new IllegalArgumentException("ALPN cannot be empty"); } if (connectionState != Status.Created) { throw new IllegalStateException("Cannot connect a connection that is in state " + connectionState); } if (earlyData != null && !earlyData.isEmpty() && sessionTicket == null) { throw new IllegalStateException("Cannot send early data without session ticket"); } this.applicationProtocol = applicationProtocol; if (transportParameters != null) { this.transportParams = transportParameters; connectionIdManager.setMaxPeerConnectionIds(transportParams.getActiveConnectionIdLimit()); } this.transportParams.setInitialSourceConnectionId(connectionIdManager.getInitialConnectionId()); if (earlyData == null) { earlyData = Collections.emptyList(); } log.info(String.format("Original destination connection id: %s (scid: %s)", bytesToHex(connectionIdManager.getOriginalDestinationConnectionId()), bytesToHex(connectionIdManager.getInitialConnectionId()))); generateInitialKeys(); receiver.start(); sender.start(connectionSecrets); startReceiverLoop(); startHandshake(applicationProtocol, !earlyData.isEmpty()); List earlyDataStreams = sendEarlyData(earlyData); try { boolean handshakeFinished = handshakeFinishedCondition.await(connectionTimeout, TimeUnit.MILLISECONDS); if (!handshakeFinished) { abortHandshake(); throw new ConnectException("Connection timed out after " + connectionTimeout + " ms"); } else if (connectionState != Status.Connected) { abortHandshake(); throw new ConnectException("Handshake error: " + (handshakeError != null? handshakeError: "")); } } catch (InterruptedException e) { abortHandshake(); throw new RuntimeException(); // Should not happen. } if (!earlyData.isEmpty()) { if (earlyDataStatus != Accepted) { log.info("Server did not accept early data; retransmitting all data."); } for (QuicStream stream: earlyDataStreams) { if (stream != null) { ((EarlyDataStream) stream).writeRemaining(earlyDataStatus == Accepted); } } } return earlyDataStreams; } private List sendEarlyData(List streamEarlyDataList) throws IOException { if (!streamEarlyDataList.isEmpty()) { TransportParameters rememberedTransportParameters = new TransportParameters(); sessionTicket.copyTo(rememberedTransportParameters); setZeroRttTransportParameters(rememberedTransportParameters); // https://tools.ietf.org/html/draft-ietf-quic-tls-27#section-4.5 // "the amount of data which the client can send in 0-RTT is controlled by the "initial_max_data" // transport parameter supplied by the server" long earlyDataSizeLeft = sessionTicket.getInitialMaxData(); List earlyDataStreams = new ArrayList<>(); for (StreamEarlyData streamEarlyData: streamEarlyDataList) { EarlyDataStream earlyDataStream = streamManager.createEarlyDataStream(true); if (earlyDataStream != null) { earlyDataStream.writeEarlyData(streamEarlyData.data, streamEarlyData.closeOutput, earlyDataSizeLeft); earlyDataSizeLeft = Long.max(0, earlyDataSizeLeft - streamEarlyData.data.length); } else { log.info("Creating early data stream failed, max bidi streams = " + rememberedTransportParameters.getInitialMaxStreamsBidi()); } earlyDataStreams.add(earlyDataStream); } earlyDataStatus = Requested; return earlyDataStreams; } else { return Collections.emptyList(); } } private void abortHandshake() { connectionState = Status.Failed; sender.stop(); terminate(); } @Override public void keepAlive(int seconds) { if (connectionState != Status.Connected) { throw new IllegalStateException("keep alive can only be set when connected"); } if (idleTimer.isEnabled()) { keepAliveActor = new KeepAliveActor(quicVersion, seconds, (int) idleTimer.getIdleTimeout(), sender); } } public void ping() { if (connectionState == Status.Connected) { sender.send(new PingFrame(quicVersion.getVersion()), App); sender.flush(); } else { throw new IllegalStateException("not connected"); } } private void startReceiverLoop() { receiverThread = new Thread(this::receiveAndProcessPackets, "receiver-loop"); receiverThread.setDaemon(true); receiverThread.start(); } private void receiveAndProcessPackets() { Thread currentThread = Thread.currentThread(); int receivedPacketCounter = 0; try { while (! currentThread.isInterrupted()) { RawPacket rawPacket = receiver.get(15); if (rawPacket != null) { Duration processDelay = Duration.between(rawPacket.getTimeReceived(), Instant.now()); log.raw("Start processing packet " + ++receivedPacketCounter + " (" + rawPacket.getLength() + " bytes)", rawPacket.getData(), 0, rawPacket.getLength()); log.debug("Processing delay for packet #" + receivedPacketCounter + ": " + processDelay.toMillis() + " ms"); parseAndProcessPackets(receivedPacketCounter, rawPacket.getTimeReceived(), rawPacket.getData(), null); sender.datagramProcessed(receiver.hasMore()); } } } catch (InterruptedException e) { log.debug("Terminating receiver loop because of interrupt"); } catch (Exception error) { log.debug("Terminating receiver loop because of error", error); abortConnection(error); } } private void generateInitialKeys() { connectionSecrets.computeInitialKeys(connectionIdManager.getCurrentPeerConnectionId()); } private void startHandshake(String applicationProtocol, boolean withEarlyData) { tlsEngine.setServerName(host); tlsEngine.addSupportedCiphers(cipherSuites); if (clientCertificate != null && clientCertificateKey != null) { tlsEngine.setClientCertificateCallback(authorities -> { if (! authorities.contains(clientCertificate.getIssuerX500Principal())) { log.warn("Client certificate is not signed by one of the requested authorities: " + authorities); } return new CertificateWithPrivateKey(clientCertificate, clientCertificateKey); }); } if (preferredVersion != null && !preferredVersion.equals(originalVersion)) { transportParams.setVersionInformation(new TransportParameters.VersionInformation(originalVersion, List.of(preferredVersion, originalVersion))); } else if (quicVersion.getVersion().isV2()) { transportParams.setVersionInformation(new TransportParameters.VersionInformation(Version.QUIC_version_2, List.of(Version.QUIC_version_2, Version.QUIC_version_1))); } QuicTransportParametersExtension tpExtension = new QuicTransportParametersExtension(quicVersion.getVersion(), transportParams, Role.Client); if (clientHelloEnlargement != null) { tpExtension.addDiscardTransportParameter(clientHelloEnlargement); } tlsEngine.add(tpExtension); tlsEngine.add(new ApplicationLayerProtocolNegotiationExtension(applicationProtocol)); if (withEarlyData) { tlsEngine.add(new EarlyDataExtension()); } if (sessionTicket != null) { tlsEngine.setNewSessionTicket(sessionTicket); } try { tlsEngine.startHandshake(); } catch (IOException e) { // Will not happen, as our ClientMessageSender implementation will not throw. } } @Override public void earlySecretsKnown() { if (sessionTicket != null) { TlsConstants.CipherSuite cipher = sessionTicket.getCipher(); connectionSecrets.computeEarlySecrets(tlsEngine, cipher, quicVersion.getVersion()); } } @Override public void handshakeSecretsKnown() { // Server Hello provides a new secret, so: connectionSecrets.computeHandshakeSecrets(tlsEngine, tlsEngine.getSelectedCipher()); hasHandshakeKeys(); } public void hasHandshakeKeys() { synchronized (handshakeStateLock) { if (handshakeState.transitionAllowed(HandshakeState.HasHandshakeKeys)) { handshakeState = HandshakeState.HasHandshakeKeys; handshakeStateListeners.forEach(l -> l.handshakeStateChangedEvent(handshakeState)); } else { log.debug("Handshake state cannot be set to HasHandshakeKeys"); } } // https://tools.ietf.org/html/draft-ietf-quic-tls-29#section-4.11.1 // "Thus, a client MUST discard Initial keys when it first sends a Handshake packet (...). This results in // abandoning loss recovery state for the Initial encryption level and ignoring any outstanding Initial packets." // This is done as post-processing action to ensure ack on Initial level is sent. postProcessingActions.add(() -> { discard(PnSpace.Initial, "first Handshake message is being sent"); }); } @Override public void handshakeFinished() { connectionSecrets.computeApplicationSecrets(tlsEngine); synchronized (handshakeStateLock) { if (handshakeState.transitionAllowed(HandshakeState.HasAppKeys)) { handshakeState = HandshakeState.HasAppKeys; handshakeStateListeners.forEach(l -> l.handshakeStateChangedEvent(handshakeState)); } else { log.error("Handshake state cannot be set to HasAppKeys; current state is " + handshakeState); } } connectionState = Status.Connected; handshakeFinishedCondition.countDown(); } @Override public void newSessionTicketReceived(NewSessionTicket ticket) { addNewSessionTicket(ticket); } @Override public void extensionsReceived(List extensions) { extensions.forEach(ex -> { if (ex instanceof EarlyDataExtension) { setEarlyDataStatus(EarlyDataStatus.Accepted); log.info("Server has accepted early data."); } else if (ex instanceof QuicTransportParametersExtension) { setPeerTransportParameters(((QuicTransportParametersExtension) ex).getTransportParameters()); } }); } @Override public boolean isEarlyDataAccepted() { return false; } private void discard(PnSpace pnSpace, String reason) { sender.discard(pnSpace, reason); } @Override public ProcessResult process(InitialPacket packet, Instant time) { if (! packet.getVersion().equals(quicVersion)) { handleVersionNegotiation(packet.getVersion()); } connectionIdManager.registerInitialPeerCid(packet.getSourceConnectionId()); processFrames(packet, time); ignoreVersionNegotiation = true; return ProcessResult.Continue; } private void handleVersionNegotiation(Version packetVersion) { if (! packetVersion.equals(quicVersion)) { if (packetVersion.equals(preferredVersion) && versionNegotiationStatus == VersionNegotiationStatus.NotStarted) { versionNegotiationStatus = VersionNegotiationStatus.VersionChangeUnconfirmed; quicVersion.setVersion(packetVersion); connectionSecrets.recomputeInitialKeys(); } } } @Override public ProcessResult process(HandshakePacket packet, Instant time) { processFrames(packet, time); return ProcessResult.Continue; } @Override public ProcessResult process(ShortHeaderPacket packet, Instant time) { connectionIdManager.registerConnectionIdInUse(packet.getDestinationConnectionId()); processFrames(packet, time); return ProcessResult.Continue; } @Override public ProcessResult process(VersionNegotiationPacket vnPacket, Instant time) { if (!ignoreVersionNegotiation && !vnPacket.getServerSupportedVersions().contains(quicVersion.getVersion())) { log.info("Server doesn't support " + quicVersion + ", but only: " + ((VersionNegotiationPacket) vnPacket).getServerSupportedVersions().stream().map(v -> v.toString()).collect(Collectors.joining(", "))); throw new VersionNegotiationFailure(); } else { // Must be a corrupted packet or sent because of a corrupted packet, so ignore. log.debug("Ignoring Version Negotiation packet"); } return ProcessResult.Continue; } private volatile boolean processedRetryPacket = false; @Override public ProcessResult process(RetryPacket packet, Instant time) { if (packet.validateIntegrityTag(connectionIdManager.getOriginalDestinationConnectionId())) { if (!processedRetryPacket) { // https://tools.ietf.org/html/draft-ietf-quic-transport-18#section-17.2.5 // "A client MUST accept and process at most one Retry packet for each // connection attempt. After the client has received and processed an // Initial or Retry packet from the server, it MUST discard any // subsequent Retry packets that it receives." processedRetryPacket = true; token = packet.getRetryToken(); sender.setInitialToken(token); getCryptoStream(Initial).reset(); // Stream offset should restart from 0. byte[] peerConnectionId = packet.getSourceConnectionId(); connectionIdManager.registerInitialPeerCid(peerConnectionId); connectionIdManager.registerRetrySourceConnectionId(peerConnectionId); log.debug("Changing destination connection id into: " + bytesToHex(peerConnectionId)); generateInitialKeys(); // https://tools.ietf.org/html/draft-ietf-quic-recovery-18#section-6.2.1.1 // "A Retry or Version Negotiation packet causes a client to send another // Initial packet, effectively restarting the connection process and // resetting congestion control..." sender.getCongestionController().reset(); try { tlsEngine.startHandshake(); } catch (IOException e) { // Will not happen, as our ClientMessageSender implementation will not throw. } } else { log.error("Ignoring RetryPacket, because already processed one."); } } else { log.error("Discarding Retry packet, because integrity tag is invalid."); } return ProcessResult.Continue; } @Override public ProcessResult process(ZeroRttPacket packet, Instant time) { // Intentionally discarding packet without any action (servers should not send 0-RTT packets). return ProcessResult.Abort; } @Override public void process(HandshakeDoneFrame handshakeDoneFrame, QuicPacket packet, Instant timeReceived) { synchronized (handshakeStateLock) { if (handshakeState.transitionAllowed(HandshakeState.Confirmed)) { handshakeState = HandshakeState.Confirmed; handshakeStateListeners.forEach(l -> l.handshakeStateChangedEvent(handshakeState)); } else { log.debug("Handshake state cannot be set to Confirmed"); } } sender.discard(PnSpace.Handshake, "HandshakeDone is received"); // TODO: discard handshake keys: // https://tools.ietf.org/html/draft-ietf-quic-tls-25#section-4.10.2 // "An endpoint MUST discard its handshake keys when the TLS handshake is confirmed" } @Override public void process(NewConnectionIdFrame newConnectionIdFrame, QuicPacket packet, Instant timeReceived) { connectionIdManager.process(newConnectionIdFrame); } @Override public void process(NewTokenFrame newTokenFrame, QuicPacket packet, Instant timeReceived) { } @Override public void process(RetireConnectionIdFrame retireConnectionIdFrame, QuicPacket packet, Instant timeReceived) { connectionIdManager.process(retireConnectionIdFrame, packet.getDestinationConnectionId()); } @Override protected void immediateCloseWithError(EncryptionLevel level, long error, ErrorType errorType, String errorReason) { if (keepAliveActor != null) { keepAliveActor.shutdown(); } super.immediateCloseWithError(level, error, errorType, errorReason); } @Override protected void cryptoProcessingErrorOcurred(TlsProtocolException exception) { if (connectionState == Status.Handshaking) { handshakeError = exception.toString(); } else { log.error("Processing crypto frame failed with ", exception); } } @Override protected void peerClosedWithError(ConnectionCloseFrame closeFrame) { super.peerClosedWithError(closeFrame); if (connectionState == Status.Handshaking) { handshakeError = "Server closed connection: " + determineClosingErrorMessage(closeFrame); } } /** * Closes the connection by discarding all connection state. Do not call directly, should be called after * closing state or draining state ends. */ @Override protected void terminate() { super.terminate(); handshakeFinishedCondition.countDown(); receiver.shutdown(); socket.close(); if (receiverThread != null) { receiverThread.interrupt(); } } public void changeAddress() { try { DatagramSocket newSocket = socketFactory.createDatagramSocket(); sender.changeAddress(newSocket); receiver.changeAddress(newSocket); log.info("Changed local address to " + newSocket.getLocalPort()); } catch (SocketException e) { // Fairly impossible, as we created a socket on an ephemeral port log.error("Changing local address failed", e); } } public void updateKeys() { // https://tools.ietf.org/html/draft-ietf-quic-tls-31#section-6 // "Once the handshake is confirmed (see Section 4.1.2), an endpoint MAY initiate a key update." if (handshakeState == HandshakeState.Confirmed) { connectionSecrets.getClientAead(App).computeKeyUpdate(true); } else { log.error("Refusing key update because handshake is not yet confirmed"); } } @Override public int getMaxShortHeaderPacketOverhead() { return 1 // flag byte + connectionIdManager.getCurrentPeerConnectionId().length + 4 // max packet number size, in practice this will be mostly 1 + 16 // encryption overhead ; } public TransportParameters getTransportParameters() { return transportParams; } public TransportParameters getPeerTransportParameters() { return peerTransportParams; } void setPeerTransportParameters(TransportParameters transportParameters) { if (!verifyConnectionIds(transportParameters)) { return; } if (versionNegotiationStatus == VersionNegotiationStatus.VersionChangeUnconfirmed) { verifyVersionNegotiation(transportParameters); } peerTransportParams = transportParameters; if (flowController == null) { flowController = new FlowControl(Role.Client, peerTransportParams.getInitialMaxData(), peerTransportParams.getInitialMaxStreamDataBidiLocal(), peerTransportParams.getInitialMaxStreamDataBidiRemote(), peerTransportParams.getInitialMaxStreamDataUni(), log); streamManager.setFlowController(flowController); } else { // If the client has sent 0-rtt, the flow controller will already have been initialized with "remembered" values log.debug("Updating flow controller with new transport parameters"); // TODO: this should be postponed until all 0-rtt packets are sent flowController.updateInitialValues(peerTransportParams); } streamManager.setInitialMaxStreamsBidi(peerTransportParams.getInitialMaxStreamsBidi()); streamManager.setInitialMaxStreamsUni(peerTransportParams.getInitialMaxStreamsUni()); sender.setReceiverMaxAckDelay(peerTransportParams.getMaxAckDelay()); connectionIdManager.registerPeerCidLimit(peerTransportParams.getActiveConnectionIdLimit()); determineIdleTimeout(transportParams.getMaxIdleTimeout(), peerTransportParams.getMaxIdleTimeout()); connectionIdManager.setInitialStatelessResetToken(peerTransportParams.getStatelessResetToken()); if (processedRetryPacket) { if (peerTransportParams.getRetrySourceConnectionId() == null || ! connectionIdManager.validateRetrySourceConnectionId(peerTransportParams.getRetrySourceConnectionId())) { immediateCloseWithError(Handshake, TRANSPORT_PARAMETER_ERROR.value, "incorrect retry_source_connection_id transport parameter"); } } else { if (peerTransportParams.getRetrySourceConnectionId() != null) { immediateCloseWithError(Handshake, TRANSPORT_PARAMETER_ERROR.value, "unexpected retry_source_connection_id transport parameter"); } } peerAckDelayExponent = transportParameters.getAckDelayExponent(); sender.registerMaxUdpPayloadSize(transportParameters.getMaxUdpPayloadSize()); } private void setZeroRttTransportParameters(TransportParameters rememberedTransportParameters) { determineIdleTimeout(transportParams.getMaxIdleTimeout(), rememberedTransportParameters.getMaxIdleTimeout()); // max_udp_payload_size not used by Kwik flowController = new FlowControl(Role.Client, rememberedTransportParameters.getInitialMaxData(), rememberedTransportParameters.getInitialMaxStreamDataBidiLocal(), rememberedTransportParameters.getInitialMaxStreamDataBidiRemote(), rememberedTransportParameters.getInitialMaxStreamDataUni(), log); streamManager.setFlowController(flowController); streamManager.setInitialMaxStreamsBidi(rememberedTransportParameters.getInitialMaxStreamsBidi()); streamManager.setInitialMaxStreamsUni(rememberedTransportParameters.getInitialMaxStreamsUni()); // disable_active_migration not (yet) used by Kwik (a TODO) connectionIdManager.registerPeerCidLimit(rememberedTransportParameters.getActiveConnectionIdLimit()); } private void verifyVersionNegotiation(TransportParameters transportParameters) { assert versionNegotiationStatus == VersionNegotiationStatus.VersionChangeUnconfirmed; TransportParameters.VersionInformation versionInformation = transportParameters.getVersionInformation(); if (versionInformation == null || !versionInformation.getChosenVersion().equals(quicVersion.getVersion())) { // https://www.ietf.org/archive/id/draft-ietf-quic-version-negotiation-08.html // "clients MUST validate that the server's Chosen Version is equal to the negotiated version; if they do not // match, the client MUST close the connection with a version negotiation error. " log.error(String.format("HIERO: connection version: %s, version info: %s", quicVersion, versionInformation)); immediateCloseWithError(Handshake, VERSION_NEGOTIATION_ERROR.value, "Chosen version does not match packet version"); } else { versionNegotiationStatus = VersionNegotiationStatus.VersionNegotiated; log.info(String.format("Version negotiation resulted in changing version from %s to %s", originalVersion, quicVersion)); } } private boolean verifyConnectionIds(TransportParameters transportParameters) { // https://tools.ietf.org/html/draft-ietf-quic-transport-29#section-7.3 // "An endpoint MUST treat absence of the initial_source_connection_id // transport parameter from either endpoint or absence of the // original_destination_connection_id transport parameter from the // server as a connection error of type TRANSPORT_PARAMETER_ERROR." if (transportParameters.getInitialSourceConnectionId() == null || transportParameters.getOriginalDestinationConnectionId() == null) { log.error("Missing connection id from server transport parameter"); if (transportParameters.getInitialSourceConnectionId() == null) { immediateCloseWithError(Handshake, TRANSPORT_PARAMETER_ERROR.value, "missing initial_source_connection_id transport parameter"); } else { immediateCloseWithError(Handshake, TRANSPORT_PARAMETER_ERROR.value, "missing original_destination_connection_id transport parameter"); } return false; } // https://tools.ietf.org/html/draft-ietf-quic-transport-29#section-7.3 // "An endpoint MUST treat the following as a connection error of type TRANSPORT_PARAMETER_ERROR or PROTOCOL_VIOLATION: // * a mismatch between values received from a peer in these transport parameters and the value sent in the // corresponding Destination or Source Connection ID fields of Initial packets." if (! Arrays.equals(connectionIdManager.getCurrentPeerConnectionId(), transportParameters.getInitialSourceConnectionId())) { log.error("Source connection id does not match corresponding transport parameter"); immediateCloseWithError(Handshake, PROTOCOL_VIOLATION.value, "initial_source_connection_id transport parameter does not match"); return false; } if (! Arrays.equals(connectionIdManager.getOriginalDestinationConnectionId(), transportParameters.getOriginalDestinationConnectionId())) { log.error("Original destination connection id does not match corresponding transport parameter"); immediateCloseWithError(Handshake, PROTOCOL_VIOLATION.value, "original_destination_connection_id transport parameter does not match"); return false; } return true; } /** * Abort connection due to a fatal error in this client. No message is sent to peer; just inform client it's all over. * @param error the exception that caused the trouble */ @Override public void abortConnection(Throwable error) { if (connectionState == Status.Handshaking) { handshakeError = error.toString(); } connectionState = Status.Closing; if (error != null) { log.error("Aborting connection because of error", error); } handshakeFinishedCondition.countDown(); sender.stop(); terminate(); streamManager.abortAll(); } // https://tools.ietf.org/html/draft-ietf-quic-transport-19#section-5.1.2 // "An endpoint can change the connection ID it uses for a peer to // another available one at any time during the connection. " public byte[] nextDestinationConnectionId() { byte[] newConnectionId = connectionIdManager.nextPeerId(); if (newConnectionId != null) { log.debug("Switching to next destination connection id: " + bytesToHex(newConnectionId)); } else { log.debug("Cannot switch to next destination connection id: no connection id's available"); } return newConnectionId; } @Override protected boolean checkForStatelessResetToken(ByteBuffer data) { byte[] tokenCandidate = new byte[16]; data.position(data.limit() - 16); data.get(tokenCandidate); boolean isStatelessReset = connectionIdManager.isStatelessResetToken(tokenCandidate); return isStatelessReset; } public byte[][] newConnectionIds(int count, int retirePriorTo) { byte[][] newConnectionIds = new byte[count][]; for (int i = 0; i < count; i++) { ConnectionIdInfo cid = connectionIdManager.sendNewConnectionId(retirePriorTo); if (cid != null) { newConnectionIds[i] = cid.getConnectionId(); log.debug("New generated source connection id", cid.getConnectionId()); } } sender.flush(); return newConnectionIds; } public void retireDestinationConnectionId(Integer sequenceNumber) { connectionIdManager.retireConnectionId(sequenceNumber); } @Override protected SenderImpl getSender() { return sender; } @Override protected GlobalAckGenerator getAckGenerator() { return ackGenerator; } @Override protected TlsClientEngine getTlsEngine() { return tlsEngine; } @Override protected StreamManager getStreamManager() { return streamManager; } @Override protected int getSourceConnectionIdLength() { return connectionIdManager.getConnectionIdLength(); } @Override public byte[] getSourceConnectionId() { return connectionIdManager.getCurrentConnectionId(); } public Map getSourceConnectionIds() { return connectionIdManager.getAllConnectionIds(); } @Override public byte[] getDestinationConnectionId() { return connectionIdManager.getCurrentPeerConnectionId(); } public Map getDestinationConnectionIds() { return connectionIdManager.getAllPeerConnectionIds(); } @Override public void setPeerInitiatedStreamCallback(Consumer streamProcessor) { streamManager.setPeerInitiatedStreamCallback(streamProcessor); } // For internal use only. @Override public long getInitialMaxStreamData() { return transportParams.getInitialMaxStreamDataBidiLocal(); } @Override public void setMaxAllowedBidirectionalStreams(int max) { transportParams.setInitialMaxStreamsBidi(max); } @Override public void setMaxAllowedUnidirectionalStreams(int max) { transportParams.setInitialMaxStreamsUni(max); } @Override public void setDefaultStreamReceiveBufferSize(long size) { transportParams.setInitialMaxStreamData(size); } public FlowControl getFlowController() { return flowController; } public void addNewSessionTicket(NewSessionTicket tlsSessionTicket) { if (tlsSessionTicket.hasEarlyDataExtension()) { if (tlsSessionTicket.getEarlyDataMaxSize() != 0xffffffffL) { // https://tools.ietf.org/html/draft-ietf-quic-tls-24#section-4.5 // "Servers MUST NOT send // the "early_data" extension with a max_early_data_size set to any // value other than 0xffffffff. A client MUST treat receipt of a // NewSessionTicket that contains an "early_data" extension with any // other value as a connection error of type PROTOCOL_VIOLATION." log.error("Invalid quic new session ticket (invalid early data size); ignoring ticket."); } } newSessionTickets.add(new QuicSessionTicket(tlsSessionTicket, peerTransportParams)); } @Override public List getNewSessionTickets() { return newSessionTickets; } public EarlyDataStatus getEarlyDataStatus() { return earlyDataStatus; } public void setEarlyDataStatus(EarlyDataStatus earlyDataStatus) { this.earlyDataStatus = earlyDataStatus; } public URI getUri() { try { return new URI("//" + host + ":" + port); } catch (URISyntaxException e) { // Impossible throw new IllegalStateException(); } } @Override public InetSocketAddress getLocalAddress() { return (InetSocketAddress) socket.getLocalSocketAddress(); } @Override public InetSocketAddress getServerAddress() { return new InetSocketAddress(host, port); } @Override public List getServerCertificateChain() { return tlsEngine.getServerCertificateChain(); } @Override public boolean isConnected() { return connectionState == Status.Connected; } protected void trustAnyServerCertificate() { X509TrustManager trustAllCerts = new X509TrustManager() { @Override public java.security.cert.X509Certificate[] getAcceptedIssuers() { return null; } @Override public void checkClientTrusted( java.security.cert.X509Certificate[] certs, String authType) { } @Override public void checkServerTrusted( java.security.cert.X509Certificate[] certs, String authType) { } }; tlsEngine.setTrustManager(trustAllCerts); tlsEngine.setHostnameVerifier((hostname, serverCertificate) -> true); } private void enableQuantumReadinessTest(int nrDummyBytes) { clientHelloEnlargement = nrDummyBytes; } public static Builder newBuilder() { return new BuilderImpl(); } private static class BuilderImpl implements Builder { private String host; private int port; private QuicSessionTicket sessionTicket; private Version quicVersion = Version.getDefault(); private Version preferredVersion; private Logger log = new NullLogger(); private String proxyHost; private Path secretsFile; private Integer initialRtt; private Integer connectionIdLength; private List cipherSuites = new ArrayList<>(); private boolean omitCertificateCheck; private Integer quantumReadinessTest; private X509Certificate clientCertificate; private PrivateKey clientCertificateKey; private DatagramSocketFactory datagramSocketFactory = new DefaultDatagramSocketFactory(); @Override public QuicClientConnectionImpl build() throws SocketException, UnknownHostException { if (!quicVersion.isKnown() || !quicVersion.atLeast(Version.IETF_draft_29)) { throw new IllegalArgumentException("Quic version " + quicVersion + " not supported"); } if (host == null) { throw new IllegalStateException("Cannot create connection when URI is not set"); } if (initialRtt != null && initialRtt < 1) { throw new IllegalArgumentException("Initial RTT must be larger than 0."); } if (cipherSuites.isEmpty()) { cipherSuites.add(TlsConstants.CipherSuite.TLS_AES_128_GCM_SHA256); } QuicClientConnectionImpl quicConnection = new QuicClientConnectionImpl(host, port, sessionTicket, quicVersion, preferredVersion, log, proxyHost, secretsFile, initialRtt, connectionIdLength, cipherSuites, clientCertificate, clientCertificateKey, datagramSocketFactory); if (omitCertificateCheck) { quicConnection.trustAnyServerCertificate(); } if (quantumReadinessTest != null) { quicConnection.enableQuantumReadinessTest(quantumReadinessTest); } return quicConnection; } @Override public Builder connectTimeout(Duration duration) { return this; } @Override public Builder version(Version version) { quicVersion = version; return this; } @Override public Builder initialVersion(Version version) { quicVersion = version; return this; } @Override public Builder preferredVersion(Version version) { preferredVersion = version; return this; } @Override public Builder logger(Logger log) { this.log = log; return this; } @Override public Builder sessionTicket(QuicSessionTicket ticket) { sessionTicket = ticket; return this; } @Override public Builder proxy(String host) { proxyHost = host; return this; } @Override public Builder secrets(Path secretsFile) { this.secretsFile = secretsFile; return this; } @Override public Builder uri(URI uri) { host = uri.getHost(); port = uri.getPort(); return this; } @Override public Builder connectionIdLength(int length) { if (length < 0 || length > 20) { throw new IllegalArgumentException("Connection ID length must between 0 and 20."); } connectionIdLength = length; return this; } @Override public Builder initialRtt(int initialRtt) { this.initialRtt = initialRtt; return this; } @Override public Builder cipherSuite(TlsConstants.CipherSuite cipherSuite) { cipherSuites.add(cipherSuite); return this; } @Override public Builder noServerCertificateCheck() { omitCertificateCheck = true; return this; } @Override public Builder quantumReadinessTest(int nrOfDummyBytes) { this.quantumReadinessTest = nrOfDummyBytes; return this; } @Override public Builder clientCertificate(X509Certificate certificate) { this.clientCertificate = certificate; return this; } @Override public Builder clientCertificateKey(PrivateKey privateKey) { this.clientCertificateKey = privateKey; return this; } @Override public Builder datagramSocketFactory(DatagramSocketFactory datagramSocketFactory) { this.datagramSocketFactory = datagramSocketFactory; return this; } } }