All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.netty.incubator.codec.quic.QuicheQuicChannel Maven / Gradle / Ivy

There is a newer version: 0.0.69.Final
Show newest version
/*
 * Copyright 2020 The Netty Project
 *
 * The Netty Project licenses this file to you under the Apache License,
 * version 2.0 (the "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at:
 *
 *   https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */
package io.netty.incubator.codec.quic;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.AbstractChannel;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelMetadata;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelOutboundBuffer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.ChannelPromise;
import io.netty.channel.ConnectTimeoutException;
import io.netty.channel.DefaultChannelPipeline;
import io.netty.channel.EventLoop;
import io.netty.channel.RecvByteBufAllocator;
import io.netty.channel.socket.DatagramPacket;
import io.netty.handler.ssl.SniCompletionEvent;
import io.netty.handler.ssl.SslHandshakeCompletionEvent;
import io.netty.util.AttributeKey;
import io.netty.util.collection.LongObjectHashMap;
import io.netty.util.collection.LongObjectMap;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.ImmediateEventExecutor;
import io.netty.util.concurrent.ImmediateExecutor;
import io.netty.util.concurrent.Promise;
import io.netty.util.internal.StringUtil;
import io.netty.util.internal.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory;
import org.jetbrains.annotations.Nullable;

import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLHandshakeException;
import java.io.File;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.channels.AlreadyConnectedException;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.ConnectionPendingException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLongFieldUpdater;
import java.util.function.Consumer;
import java.util.function.Function;

/**
 * {@link QuicChannel} implementation that uses quiche.
 */
final class QuicheQuicChannel extends AbstractChannel implements QuicChannel {
    private static final InternalLogger logger = InternalLoggerFactory.getInstance(QuicheQuicChannel.class);
    private static final String QLOG_FILE_EXTENSION = ".qlog";

    enum StreamRecvResult {
        /**
         * Nothing more to read from the stream.
         */
        DONE,
        /**
         * FIN flag received.
         */
        FIN,
        /**
         * Normal read without FIN flag.
         */
        OK
    }

    private enum ChannelState {
        OPEN,
        ACTIVE,
        CLOSED
    }

    private enum SendResult {
        SOME,
        NONE,
        CLOSE
    }

    private static final class CloseData implements ChannelFutureListener {
        final boolean applicationClose;
        final int err;
        final ByteBuf reason;

        CloseData(boolean applicationClose, int err, ByteBuf reason) {
            this.applicationClose = applicationClose;
            this.err = err;
            this.reason = reason;
        }

        @Override
        public void operationComplete(ChannelFuture future) {
            reason.release();
        }
    }

    private static final ChannelMetadata METADATA = new ChannelMetadata(false, 16);
    private long[] readableStreams = new long[4];
    private long[] writableStreams = new long[4];
    private final LongObjectMap streams = new LongObjectHashMap<>();
    private final QuicheQuicChannelConfig config;
    private final boolean server;
    private final QuicStreamIdGenerator idGenerator;
    private final ChannelHandler streamHandler;
    private final Map.Entry, Object>[] streamOptionsArray;
    private final Map.Entry, Object>[] streamAttrsArray;
    private final TimeoutHandler timeoutHandler;
    private final QuicConnectionIdGenerator connectionIdAddressGenerator;
    private final QuicResetTokenGenerator resetTokenGenerator;
    private final Set sourceConnectionIds = new HashSet<>();

    private Consumer freeTask;
    private Executor sslTaskExecutor;
    private boolean inFireChannelReadCompleteQueue;
    private boolean fireChannelReadCompletePending;
    private ByteBuf finBuffer;
    private ChannelPromise connectPromise;
    private ScheduledFuture connectTimeoutFuture;
    private QuicConnectionAddress connectAddress;
    private CloseData closeData;
    private QuicConnectionCloseEvent connectionCloseEvent;
    private QuicConnectionStats statsAtClose;
    private boolean supportsDatagram;
    private boolean recvDatagramPending;
    private boolean datagramReadable;
    private boolean recvStreamPending;
    private boolean streamReadable;
    private boolean handshakeCompletionNotified;
    private boolean earlyDataReadyNotified;
    private int reantranceGuard = 0;
    private static final int IN_RECV = 1 << 1;
    private static final int IN_CONNECTION_SEND = 1 << 2;
    private static final int IN_HANDLE_WRITABLE_STREAMS = 1 << 3;
    private volatile ChannelState state = ChannelState.OPEN;
    private volatile boolean timedOut;
    private volatile String traceId;
    private volatile QuicheQuicConnection connection;
    private volatile InetSocketAddress local;
    private volatile InetSocketAddress remote;

    private final ChannelFutureListener continueSendingListener = f -> {
        if (connectionSend(connection) != SendResult.NONE) {
            flushParent();
        }
    };

    private static final AtomicLongFieldUpdater UNI_STREAMS_LEFT_UPDATER =
            AtomicLongFieldUpdater.newUpdater(QuicheQuicChannel.class, "uniStreamsLeft");
    private volatile long uniStreamsLeft;

    private static final AtomicLongFieldUpdater BIDI_STREAMS_LEFT_UPDATER =
            AtomicLongFieldUpdater.newUpdater(QuicheQuicChannel.class, "bidiStreamsLeft");
    private volatile long bidiStreamsLeft;

    private QuicheQuicChannel(Channel parent, boolean server, @Nullable ByteBuffer key, InetSocketAddress local,
                              InetSocketAddress remote, boolean supportsDatagram, ChannelHandler streamHandler,
                              Map.Entry, Object>[] streamOptionsArray,
                              Map.Entry, Object>[] streamAttrsArray,
                              @Nullable Consumer freeTask,
                              @Nullable Executor sslTaskExecutor, @Nullable QuicConnectionIdGenerator connectionIdAddressGenerator,
                              @Nullable QuicResetTokenGenerator resetTokenGenerator) {
        super(parent);
        config = new QuicheQuicChannelConfig(this);
        this.freeTask = freeTask;
        this.server = server;
        this.idGenerator = new QuicStreamIdGenerator(server);
        this.connectionIdAddressGenerator = connectionIdAddressGenerator;
        this.resetTokenGenerator = resetTokenGenerator;
        if (key != null) {
            this.sourceConnectionIds.add(key);
        }

        this.supportsDatagram = supportsDatagram;
        this.local = local;
        this.remote = remote;

        this.streamHandler = streamHandler;
        this.streamOptionsArray = streamOptionsArray;
        this.streamAttrsArray = streamAttrsArray;
        timeoutHandler = new TimeoutHandler();
        this.sslTaskExecutor = sslTaskExecutor == null ? ImmediateExecutor.INSTANCE : sslTaskExecutor;
    }

    static QuicheQuicChannel forClient(Channel parent, InetSocketAddress local, InetSocketAddress remote,
                                       ChannelHandler streamHandler,
                                       Map.Entry, Object>[] streamOptionsArray,
                                       Map.Entry, Object>[] streamAttrsArray) {
        return new QuicheQuicChannel(parent, false, null, local, remote, false, streamHandler,
                streamOptionsArray, streamAttrsArray, null, null, null, null);
    }

    static QuicheQuicChannel forServer(Channel parent, ByteBuffer key, InetSocketAddress local,
                                       InetSocketAddress remote,
                                       boolean supportsDatagram, ChannelHandler streamHandler,
                                       Map.Entry, Object>[] streamOptionsArray,
                                       Map.Entry, Object>[] streamAttrsArray,
                                       Consumer freeTask, Executor sslTaskExecutor,
                                       QuicConnectionIdGenerator connectionIdAddressGenerator,
                                       QuicResetTokenGenerator resetTokenGenerator) {
        return new QuicheQuicChannel(parent, true, key, local, remote, supportsDatagram,
                streamHandler, streamOptionsArray, streamAttrsArray, freeTask,
                sslTaskExecutor, connectionIdAddressGenerator, resetTokenGenerator);
    }

    private static final int MAX_ARRAY_LEN = 128;

    private static long[] growIfNeeded(long[] array, int maxLength) {
        if (maxLength > array.length) {
            if (array.length == MAX_ARRAY_LEN) {
                return array;
            }
            // Increase by 4 until we reach MAX_ARRAY_LEN
            return new long[Math.min(MAX_ARRAY_LEN, array.length + 4)];
        }
        return array;
    }

    @Override
    public boolean isTimedOut() {
        return timedOut;
    }

    @Override
    public SSLEngine sslEngine() {
        QuicheQuicConnection connection = this.connection;
        return connection == null ? null : connection.engine();
    }

    private void notifyAboutHandshakeCompletionIfNeeded(QuicheQuicConnection conn, @Nullable SSLHandshakeException cause) {
        if (handshakeCompletionNotified) {
            return;
        }
        if (cause != null) {
            pipeline().fireUserEventTriggered(new SslHandshakeCompletionEvent(cause));
            return;
        }
        if (conn.isFreed()) {
            return;
        }
        switch (connection.engine().getHandshakeStatus()) {
            case NOT_HANDSHAKING:
            case FINISHED:
                handshakeCompletionNotified = true;
                pipeline().fireUserEventTriggered(SslHandshakeCompletionEvent.SUCCESS);
                break;
            default:
                break;
        }
    }

    @Override
    public long peerAllowedStreams(QuicStreamType type) {
        switch (type) {
            case BIDIRECTIONAL:
                return bidiStreamsLeft;
            case UNIDIRECTIONAL:
                return uniStreamsLeft;
            default:
                return 0;
        }
    }

    void attachQuicheConnection(QuicheQuicConnection connection) {
        this.connection = connection;

        byte[] traceId = Quiche.quiche_conn_trace_id(connection.address());
        if (traceId != null) {
            this.traceId = new String(traceId);
        }

        connection.init(local, remote,
                sniHostname -> pipeline().fireUserEventTriggered(new SniCompletionEvent(sniHostname)));

        // Setup QLOG if needed.
        QLogConfiguration configuration = config.getQLogConfiguration();
        if (configuration != null) {
            final String fileName;
            File file = new File(configuration.path());
            if (file.isDirectory()) {
                // Create directory if needed.
                file.mkdir();
                if (this.traceId != null) {
                    fileName = configuration.path() + File.separatorChar + this.traceId + "-" +
                            id().asShortText() + QLOG_FILE_EXTENSION;
                } else {
                    fileName = configuration.path() + File.separatorChar + id().asShortText() + QLOG_FILE_EXTENSION;
                }
            } else {
                fileName = configuration.path();
            }

            if (!Quiche.quiche_conn_set_qlog_path(connection.address(), fileName,
                    configuration.logTitle(), configuration.logDescription())) {
                logger.info("Unable to create qlog file: {} ", fileName);
            }
        }
    }

    void connectNow(Function engineProvider, Executor sslTaskExecutor,
                    Consumer freeTask, long configAddr, int localConnIdLength,
                    boolean supportsDatagram, ByteBuffer fromSockaddrMemory, ByteBuffer toSockaddrMemory)
            throws Exception {
        assert this.connection == null;
        assert this.traceId == null;
        assert this.sourceConnectionIds.isEmpty();

        this.sslTaskExecutor = sslTaskExecutor;
        this.freeTask = freeTask;

        QuicConnectionAddress address = this.connectAddress;

        if (address == QuicConnectionAddress.EPHEMERAL) {
            address = QuicConnectionAddress.random(localConnIdLength);
        }
        ByteBuffer connectId = address.id();
        if (connectId.remaining() != localConnIdLength) {
            failConnectPromiseAndThrow(new IllegalArgumentException("connectionAddress has length "
                    + connectId.remaining()
                    + " instead of " + localConnIdLength));
        }
        QuicSslEngine engine = engineProvider.apply(this);
        if (!(engine instanceof QuicheQuicSslEngine)) {
            failConnectPromiseAndThrow(new IllegalArgumentException("QuicSslEngine is not of type "
                    + QuicheQuicSslEngine.class.getSimpleName()));
            return;
        }
        if (!engine.getUseClientMode()) {
            failConnectPromiseAndThrow(new IllegalArgumentException("QuicSslEngine is not create in client mode"));
        }
        QuicheQuicSslEngine quicheEngine = (QuicheQuicSslEngine) engine;
        ByteBuf idBuffer = alloc().directBuffer(connectId.remaining()).writeBytes(connectId.duplicate());
        try {
            int fromSockaddrLen = SockaddrIn.setAddress(fromSockaddrMemory, local);
            int toSockaddrLen = SockaddrIn.setAddress(toSockaddrMemory, remote);
            QuicheQuicConnection connection = quicheEngine.createConnection(ssl ->
                    Quiche.quiche_conn_new_with_tls(Quiche.readerMemoryAddress(idBuffer),
                            idBuffer.readableBytes(), -1, -1,
                            Quiche.memoryAddressWithPosition(fromSockaddrMemory), fromSockaddrLen,
                            Quiche.memoryAddressWithPosition(toSockaddrMemory), toSockaddrLen,
                            configAddr, ssl, false));
            if (connection == null) {
                failConnectPromiseAndThrow(new ConnectException());
                return;
            }
            attachQuicheConnection(connection);
            QuicClientSessionCache sessionCache = quicheEngine.ctx.getSessionCache();
            if (sessionCache != null) {
                byte[] sessionBytes = sessionCache
                        .getSession(quicheEngine.getSession().getPeerHost(), quicheEngine.getSession().getPeerPort());
                if (sessionBytes != null) {
                    Quiche.quiche_conn_set_session(connection.address(), sessionBytes);
                }
            }
            this.supportsDatagram = supportsDatagram;
            sourceConnectionIds.add(connectId);
        } finally {
            idBuffer.release();
        }
    }

    private void failConnectPromiseAndThrow(Exception e) throws Exception {
        tryFailConnectPromise(e);
        throw e;
    }

    private boolean tryFailConnectPromise(Exception e) {
        ChannelPromise promise = connectPromise;
        if (promise != null) {
            connectPromise = null;
            promise.tryFailure(e);
            return true;
        }
        return false;
    }

    Set sourceConnectionIds() {
        return sourceConnectionIds;
    }

    boolean markInFireChannelReadCompleteQueue() {
        if (inFireChannelReadCompleteQueue) {
            return false;
        }
        inFireChannelReadCompleteQueue = true;
        return true;
    }

    private void failPendingConnectPromise() {
        ChannelPromise promise = QuicheQuicChannel.this.connectPromise;
        if (promise != null) {
            QuicheQuicChannel.this.connectPromise = null;
            promise.tryFailure(new QuicClosedChannelException(this.connectionCloseEvent));
        }
    }

    void forceClose() {
        unsafe().close(voidPromise());
    }

    @Override
    protected DefaultChannelPipeline newChannelPipeline() {
        return new DefaultChannelPipeline(this) {
            @Override
            protected void onUnhandledInboundMessage(ChannelHandlerContext ctx, Object msg) {
                if (msg instanceof QuicStreamChannel) {
                    QuicStreamChannel channel = (QuicStreamChannel) msg;
                    Quic.setupChannel(channel, streamOptionsArray, streamAttrsArray, streamHandler, logger);
                    ctx.channel().eventLoop().register(channel);
                } else {
                    super.onUnhandledInboundMessage(ctx, msg);
                }
            }
        };
    }

    @Override
    public QuicChannel flush() {
        super.flush();
        return this;
    }

    @Override
    public QuicChannel read() {
        super.read();
        return this;
    }

    @Override
    public Future createStream(QuicStreamType type, @Nullable ChannelHandler handler,
                                                  Promise promise) {
        if (eventLoop().inEventLoop()) {
            ((QuicChannelUnsafe) unsafe()).connectStream(type, handler, promise);
        } else {
            eventLoop().execute(() -> ((QuicChannelUnsafe) unsafe()).connectStream(type, handler, promise));
        }
        return promise;
    }

    @Override
    public ChannelFuture close(boolean applicationClose, int error, ByteBuf reason, ChannelPromise promise) {
        if (eventLoop().inEventLoop()) {
            close0(applicationClose, error, reason, promise);
        } else {
            eventLoop().execute(() -> close0(applicationClose, error, reason, promise));
        }
        return promise;
    }

    private void close0(boolean applicationClose, int error, ByteBuf reason, ChannelPromise promise) {
        if (closeData == null) {
            if (!reason.hasMemoryAddress()) {
                // Copy to direct buffer as that's what we need.
                ByteBuf copy = alloc().directBuffer(reason.readableBytes()).writeBytes(reason);
                reason.release();
                reason = copy;
            }
            closeData = new CloseData(applicationClose, error, reason);
            promise.addListener(closeData);
        } else {
            // We already have a close scheduled that uses a close data. Lets release the buffer early.
            reason.release();
        }
        close(promise);
    }

    @Override
    public String toString() {
        String traceId = this.traceId;
        if (traceId == null) {
            return "()" + super.toString();
        } else {
            return '(' + traceId + ')' + super.toString();
        }
    }

    @Override
    protected AbstractUnsafe newUnsafe() {
        return new QuicChannelUnsafe();
    }

    @Override
    protected boolean isCompatible(EventLoop eventLoop) {
        return parent().eventLoop() == eventLoop;
    }

    @Override
    @Nullable
    protected QuicConnectionAddress localAddress0() {
        QuicheQuicConnection connection = this.connection;
        return connection == null ? null : connection.sourceId();
    }

    @Override
    @Nullable
    protected QuicConnectionAddress remoteAddress0() {
        QuicheQuicConnection connection = this.connection;
        return connection == null ? null : connection.destinationId();
    }

    @Override
    @Nullable
    public QuicConnectionAddress localAddress() {
        // Override so we never cache as the sourceId() can change over life-time.
        return localAddress0();
    }

    @Override
    @Nullable
    public QuicConnectionAddress remoteAddress() {
        // Override so we never cache as the destinationId() can change over life-time.
        return remoteAddress0();
    }

    @Override
    @Nullable
    public SocketAddress localSocketAddress() {
        return local;
    }

    @Override
    @Nullable
    public SocketAddress remoteSocketAddress() {
        return remote;
    }

    @Override
    protected void doBind(SocketAddress socketAddress) {
        throw new UnsupportedOperationException();
    }

    @Override
    protected void doDisconnect() throws Exception {
        doClose();
    }

    @Override
    protected void doClose() throws Exception {
        if (state == ChannelState.CLOSED) {
            return;
        }
        state = ChannelState.CLOSED;

        QuicheQuicConnection conn = this.connection;
        if (conn == null || conn.isFreed()) {
            if (closeData != null) {
                closeData.reason.release();
                closeData = null;
            }
            failPendingConnectPromise();
            return;
        }


        // Call connectionSend() so we ensure we send all that is queued before we close the channel
        SendResult sendResult = connectionSend(conn);

        final boolean app;
        final int err;
        final ByteBuf reason;
        if (closeData == null) {
            app = false;
            err = 0;
            reason = Unpooled.EMPTY_BUFFER;
        } else {
            app = closeData.applicationClose;
            err = closeData.err;
            reason = closeData.reason;
            closeData = null;
        }

        failPendingConnectPromise();
        try {
            int res = Quiche.quiche_conn_close(conn.address(), app, err,
                    Quiche.readerMemoryAddress(reason), reason.readableBytes());
            if (res < 0 && res != Quiche.QUICHE_ERR_DONE) {
                throw Quiche.convertToException(res);
            }
            // As we called quiche_conn_close(...) we need to ensure we will call quiche_conn_send(...) either
            // now or we will do so once we see the channelReadComplete event.
            //
            // See https://docs.rs/quiche/0.6.0/quiche/struct.Connection.html#method.close
            if (connectionSend(conn) == SendResult.SOME) {
                sendResult = SendResult.SOME;
            }
        } finally {

            // making sure that connection statistics is available
            // even after channel is closed
            statsAtClose = collectStats0(conn, eventLoop().newPromise());
            try {
                timedOut = Quiche.quiche_conn_is_timed_out(conn.address());

                closeStreams();
                if (finBuffer != null) {
                    finBuffer.release();
                    finBuffer = null;
                }
            } finally {
                if (sendResult == SendResult.SOME) {
                    // As this is the close let us flush it asap.
                    forceFlushParent();
                } else {
                    flushParent();
                }
                conn.free();
                if (freeTask != null) {
                    freeTask.accept(this);
                }
                timeoutHandler.cancel();

                local = null;
                remote = null;
            }
        }
    }

    @Override
    protected void doBeginRead() {
        recvDatagramPending = true;
        recvStreamPending = true;
        if (datagramReadable || streamReadable) {
            ((QuicChannelUnsafe) unsafe()).recv();
        }
    }

    @Override
    protected Object filterOutboundMessage(Object msg) {
        if (msg instanceof ByteBuf) {
            return msg;
        }
        throw new UnsupportedOperationException("Unsupported message type: " + StringUtil.simpleClassName(msg));
    }

    @Override
    protected void doWrite(ChannelOutboundBuffer channelOutboundBuffer) throws Exception {
        if (!supportsDatagram) {
            throw new UnsupportedOperationException("Datagram extension is not supported");
        }
        boolean sendSomething = false;
        boolean retry = false;
        QuicheQuicConnection conn = connection;
        try {
            for (;;) {
                ByteBuf buffer = (ByteBuf) channelOutboundBuffer.current();
                if (buffer == null) {
                    break;
                }

                int readable = buffer.readableBytes();
                if (readable == 0) {
                    // Skip empty buffers.
                    channelOutboundBuffer.remove();
                    continue;
                }

                final int res;
                if (!buffer.isDirect() || buffer.nioBufferCount() > 1) {
                    ByteBuf tmpBuffer = alloc().directBuffer(readable);
                    try {
                        tmpBuffer.writeBytes(buffer, buffer.readerIndex(), readable);
                        res = sendDatagram(conn, tmpBuffer);
                    } finally {
                        tmpBuffer.release();
                    }
                } else {
                    res = sendDatagram(conn, buffer);
                }
                if (res >= 0) {
                    channelOutboundBuffer.remove();
                    sendSomething = true;
                    retry = false;
                } else {
                    if (res == Quiche.QUICHE_ERR_BUFFER_TOO_SHORT) {
                        retry = false;
                        channelOutboundBuffer.remove(new BufferUnderflowException());
                    } else if (res == Quiche.QUICHE_ERR_INVALID_STATE) {
                        throw new UnsupportedOperationException("Remote peer does not support Datagram extension");
                    } else if (res == Quiche.QUICHE_ERR_DONE) {
                        if (retry) {
                            // We already retried and it didn't work. Let's drop the datagrams on the floor.
                            for (;;) {
                                if (!channelOutboundBuffer.remove()) {
                                    // The buffer is empty now.
                                    return;
                                }
                            }
                        }
                        // Set sendSomething to false a we will call connectionSend() now.
                        sendSomething = false;
                        // If this returned DONE we couldn't write anymore. This happens if the internal queue
                        // is full. In this case we should call quiche_conn_send(...) and so make space again.
                        if (connectionSend(conn) != SendResult.NONE) {
                            forceFlushParent();
                        }
                        // Let's try again to write the message.
                        retry = true;
                    } else {
                        throw Quiche.convertToException(res);
                    }
                }
            }
        } finally {
            if (sendSomething && connectionSend(conn) != SendResult.NONE) {
                flushParent();
            }
        }
    }

    private static int sendDatagram(QuicheQuicConnection conn, ByteBuf buf) throws ClosedChannelException {
        return Quiche.quiche_conn_dgram_send(connectionAddressChecked(conn),
                Quiche.readerMemoryAddress(buf), buf.readableBytes());
    }

    @Override
    public QuicChannelConfig config() {
        return config;
    }

    @Override
    public boolean isOpen() {
        return state != ChannelState.CLOSED;
    }

    @Override
    public boolean isActive() {
        return state == ChannelState.ACTIVE;
    }

    @Override
    public ChannelMetadata metadata() {
        return METADATA;
    }

    /**
     * This may call {@link #flush()} on the parent channel if needed. The flush may be delayed until the read loop
     * is over.
     */
    private void flushParent() {
        if (!inFireChannelReadCompleteQueue) {
            forceFlushParent();
        }
    }

    /**
     * Call {@link #flush()} on the parent channel.
     */
    private void forceFlushParent() {
        parent().flush();
    }

    private static long connectionAddressChecked(@Nullable QuicheQuicConnection conn) throws ClosedChannelException {
        if (conn == null || conn.isFreed()) {
            throw new ClosedChannelException();
        }
        return conn.address();
    }

    boolean freeIfClosed() {
        QuicheQuicConnection conn = connection;
        if (conn == null || conn.isFreed()) {
            return true;
        }
        if (conn.isClosed()) {
            unsafe().close(newPromise());
            return true;
        }
        return false;
    }

    private void closeStreams() {
        if (streams.isEmpty()) {
            return;
        }
        final ClosedChannelException closedChannelException;
        if (isTimedOut()) {
            // Close the streams because of a timeout.
            closedChannelException = new QuicTimeoutClosedChannelException();
        } else {
            closedChannelException = new ClosedChannelException();
        }
        // Make a copy to ensure we not run into a situation when we change the underlying iterator from
        // another method and so run in an assert error.
        for (QuicheQuicStreamChannel stream: streams.values().toArray(new QuicheQuicStreamChannel[0])) {
            stream.unsafe().close(closedChannelException, voidPromise());
        }
        streams.clear();
    }

    void streamPriority(long streamId, byte priority, boolean incremental) throws Exception {
       int res = Quiche.quiche_conn_stream_priority(connectionAddressChecked(connection), streamId,
               priority, incremental);
       if (res < 0 && res != Quiche.QUICHE_ERR_DONE) {
           throw Quiche.convertToException(res);
       }
    }

    void streamClosed(long streamId) {
        streams.remove(streamId);
    }

    boolean isStreamLocalCreated(long streamId) {
        return (streamId & 0x1) == (server ? 1 : 0);
    }

    QuicStreamType streamType(long streamId) {
        return (streamId & 0x2) == 0 ? QuicStreamType.BIDIRECTIONAL : QuicStreamType.UNIDIRECTIONAL;
    }

    void streamShutdown(long streamId, boolean read, boolean write, int err, ChannelPromise promise) {
        QuicheQuicConnection conn = this.connection;
        final long connectionAddress;
        try {
            connectionAddress = connectionAddressChecked(conn);
        } catch (ClosedChannelException e) {
            promise.setFailure(e);
            return;
        }
        int res = 0;
        if (read) {
            res |= Quiche.quiche_conn_stream_shutdown(connectionAddress, streamId, Quiche.QUICHE_SHUTDOWN_READ, err);
        }
        if (write) {
            res |= Quiche.quiche_conn_stream_shutdown(connectionAddress, streamId, Quiche.QUICHE_SHUTDOWN_WRITE, err);
        }

        // As we called quiche_conn_stream_shutdown(...) we need to ensure we will call quiche_conn_send(...) either
        // now or we will do so once we see the channelReadComplete event.
        //
        // See https://docs.rs/quiche/0.6.0/quiche/struct.Connection.html#method.send
        if (connectionSend(conn) != SendResult.NONE) {
            // Force the flush so the shutdown can be seen asap.
            forceFlushParent();
        }
        if (res < 0 && res != Quiche.QUICHE_ERR_DONE) {
            promise.setFailure(Quiche.convertToException(res));
        } else {
            promise.setSuccess();
        }
    }

    void streamSendFin(long streamId) throws Exception {
        QuicheQuicConnection conn = connection;
        try {
            // Just write an empty buffer and set fin to true.
            int res = streamSend0(conn, streamId, Unpooled.EMPTY_BUFFER, true);
            if (res < 0 && res != Quiche.QUICHE_ERR_DONE) {
                throw Quiche.convertToException(res);
            }
        } finally {
            // As we called quiche_conn_stream_send(...) we need to ensure we will call quiche_conn_send(...) either
            // now or we will do so once we see the channelReadComplete event.
            //
            // See https://docs.rs/quiche/0.6.0/quiche/struct.Connection.html#method.send
            if (connectionSend(conn) != SendResult.NONE) {
                flushParent();
            }
        }
    }

    int streamSend(long streamId, ByteBuf buffer, boolean fin) throws ClosedChannelException {
        QuicheQuicConnection conn = connection;
        if (buffer.nioBufferCount() == 1) {
            return streamSend0(conn, streamId, buffer, fin);
        }
        ByteBuffer[] nioBuffers  = buffer.nioBuffers();
        int lastIdx = nioBuffers.length - 1;
        int res = 0;
        for (int i = 0; i < lastIdx; i++) {
            ByteBuffer nioBuffer = nioBuffers[i];
            while (nioBuffer.hasRemaining()) {
                int localRes = streamSend(conn, streamId, nioBuffer, false);
                if (localRes <= 0) {
                    return res;
                }
                res += localRes;

                nioBuffer.position(nioBuffer.position() + localRes);
            }
        }
        int localRes = streamSend(conn, streamId, nioBuffers[lastIdx], fin);
        if (localRes > 0) {
            res += localRes;
        }
        return res;
    }

    void connectionSendAndFlush() {
        if (inFireChannelReadCompleteQueue || (reantranceGuard & IN_HANDLE_WRITABLE_STREAMS) != 0) {
            return;
        }
        if (connectionSend(connection) != SendResult.NONE) {
            flushParent();
        }
    }

    private int streamSend0(QuicheQuicConnection conn, long streamId, ByteBuf buffer, boolean fin)
            throws ClosedChannelException {
        return Quiche.quiche_conn_stream_send(connectionAddressChecked(conn), streamId,
                Quiche.readerMemoryAddress(buffer), buffer.readableBytes(), fin);
    }

    private int streamSend(QuicheQuicConnection conn, long streamId, ByteBuffer buffer, boolean fin)
            throws ClosedChannelException {
        return Quiche.quiche_conn_stream_send(connectionAddressChecked(conn), streamId,
                Quiche.memoryAddressWithPosition(buffer), buffer.remaining(), fin);
    }

    StreamRecvResult streamRecv(long streamId, ByteBuf buffer) throws Exception {
        QuicheQuicConnection conn = connection;
        long connAddr = connectionAddressChecked(conn);
        if (finBuffer == null) {
            finBuffer = alloc().directBuffer(1);
        }
        int writerIndex = buffer.writerIndex();
        int recvLen = Quiche.quiche_conn_stream_recv(connAddr, streamId,
                Quiche.writerMemoryAddress(buffer), buffer.writableBytes(), Quiche.writerMemoryAddress(finBuffer));
        if (recvLen == Quiche.QUICHE_ERR_DONE) {
            return StreamRecvResult.DONE;
        } else if (recvLen < 0) {
            throw Quiche.convertToException(recvLen);
        }
        buffer.writerIndex(writerIndex + recvLen);
        return finBuffer.getBoolean(0) ? StreamRecvResult.FIN : StreamRecvResult.OK;
    }

    /**
     * Receive some data on a QUIC connection.
     */
    void recv(InetSocketAddress sender, InetSocketAddress recipient, ByteBuf buffer) {
        ((QuicChannelUnsafe) unsafe()).connectionRecv(sender, recipient, buffer);
    }

    /**
     * Return all source connection ids that are retired and so should be removed to map to the channel.
     *
     * @return retired ids.
     */
    List retiredSourceConnectionId() {
        QuicheQuicConnection connection = this.connection;
        if (connection == null || connection.isFreed()) {
            return Collections.emptyList();
        }
        long connAddr = connection.address();
        assert connAddr != -1;
        List retiredSourceIds = null;
        for (;;) {
            byte[] retired = Quiche.quiche_conn_retired_scid_next(connAddr);
            if (retired == null) {
                break;
            }
            if (retiredSourceIds == null) {
                retiredSourceIds = new ArrayList<>();
            }
            ByteBuffer retiredId = ByteBuffer.wrap(retired);
            retiredSourceIds.add(retiredId);
            sourceConnectionIds.remove(retiredId);
        }
        if (retiredSourceIds == null) {
            return Collections.emptyList();
        }
        return retiredSourceIds;
    }

    List newSourceConnectionIds() {
        if (connectionIdAddressGenerator != null && resetTokenGenerator != null ) {
            QuicheQuicConnection connection = this.connection;
            if (connection == null || connection.isFreed()) {
                return Collections.emptyList();
            }
            long connAddr = connection.address();
            // Generate all extra source ids that we can provide. This will cause frames that need to be sent. Which
            // is the reason why we might need to call connectionSendAndFlush().
            int left = Quiche.quiche_conn_scids_left(connAddr);
            if (left > 0) {
                QuicConnectionAddress sourceAddr = connection.sourceId();
                if (sourceAddr == null) {
                    return Collections.emptyList();
                }
                List generatedIds = new ArrayList<>(left);
                boolean sendAndFlush = false;
                ByteBuffer key = sourceAddr.id();
                ByteBuf connIdBuffer = alloc().directBuffer(key.remaining());

                byte[] resetTokenArray = new byte[Quic.RESET_TOKEN_LEN];
                try {
                    do {
                        ByteBuffer srcId = connectionIdAddressGenerator.newId(key.duplicate(), key.remaining())
                                .asReadOnlyBuffer();
                        connIdBuffer.clear();
                        connIdBuffer.writeBytes(srcId.duplicate());
                        ByteBuffer resetToken = resetTokenGenerator.newResetToken(srcId.duplicate());
                        resetToken.get(resetTokenArray);
                        long result = Quiche.quiche_conn_new_scid(
                                connAddr, Quiche.memoryAddress(connIdBuffer, 0, connIdBuffer.readableBytes()),
                                connIdBuffer.readableBytes(), resetTokenArray, false, -1);
                        if (result < 0) {
                            break;
                        }
                        sendAndFlush = true;
                        generatedIds.add(srcId.duplicate());
                        sourceConnectionIds.add(srcId);
                    } while (--left > 0);
                } finally {
                    connIdBuffer.release();
                }

                if (sendAndFlush) {
                    connectionSendAndFlush();
                }
                return generatedIds;
            }
        }
        return Collections.emptyList();
    }

    void writable() {
        QuicheQuicConnection conn = connection;
        SendResult result = connectionSend(conn);
        handleWritableStreams(conn);
        if (connectionSend(conn) == SendResult.SOME) {
            result = SendResult.SOME;
        }
        if (result == SendResult.SOME) {
            // The writability changed so lets flush as fast as possible.
            forceFlushParent();
        }
        freeIfClosed();
    }

    int streamCapacity(long streamId) {
        QuicheQuicConnection conn = connection;
        if (conn.isClosed()) {
            return 0;
        }
        return Quiche.quiche_conn_stream_capacity(conn.address(), streamId);
    }

    private boolean handleWritableStreams(QuicheQuicConnection conn) {
        if (conn.isFreed()) {
            return false;
        }
        reantranceGuard |= IN_HANDLE_WRITABLE_STREAMS;
        try {
            long connAddr = conn.address();
            boolean mayNeedWrite = false;

            if (Quiche.quiche_conn_is_established(connAddr) ||
                    Quiche.quiche_conn_is_in_early_data(connAddr)) {
                long writableIterator = Quiche.quiche_conn_writable(connAddr);

                int totalWritable = 0;
                try {
                    // For streams we always process all streams when at least on read was requested.
                    for (;;) {
                        int writable = Quiche.quiche_stream_iter_next(
                                writableIterator, writableStreams);
                        for (int i = 0; i < writable; i++) {
                            long streamId = writableStreams[i];
                            QuicheQuicStreamChannel streamChannel = streams.get(streamId);
                            if (streamChannel != null) {
                                int capacity = Quiche.quiche_conn_stream_capacity(connAddr, streamId);
                                if (capacity < 0) {
                                    // Let's close the channel if quiche_conn_stream_capacity(...) returns an error.
                                    streamChannel.forceClose(capacity);
                                } else if (streamChannel.writable(capacity)) {
                                    mayNeedWrite = true;
                                }
                            }
                        }
                        if (writable > 0) {
                            totalWritable += writable;
                        }
                        if (writable < writableStreams.length) {
                            // We did handle all writable streams.
                            break;
                        }
                    }
                } finally {
                    Quiche.quiche_stream_iter_free(writableIterator);
                }
                writableStreams = growIfNeeded(writableStreams, totalWritable);
            }
            return mayNeedWrite;
        } finally {
            reantranceGuard &= ~IN_HANDLE_WRITABLE_STREAMS;
        }
    }

    /**
     * Called once we receive a channelReadComplete event. This method will take care of calling
     * {@link ChannelPipeline#fireChannelReadComplete()} if needed and also to handle pending flushes of
     * writable {@link QuicheQuicStreamChannel}s.
     */
    void recvComplete() {
        try {
            QuicheQuicConnection conn = connection;
            if (conn.isFreed()) {
                // Ensure we flush all pending writes.
                forceFlushParent();
                return;
            }
            fireChannelReadCompleteIfNeeded();

            // If we had called recv we need to ensure we call send as well.
            // See https://docs.rs/quiche/0.6.0/quiche/struct.Connection.html#method.send
            connectionSend(conn);

            // We are done with the read loop, flush all pending writes now.
            forceFlushParent();
            freeIfClosed();
        } finally {
            inFireChannelReadCompleteQueue = false;
        }
    }

    private void fireChannelReadCompleteIfNeeded() {
        if (fireChannelReadCompletePending) {
            fireChannelReadCompletePending = false;
            pipeline().fireChannelReadComplete();
        }
    }

    private void fireExceptionEvents(QuicheQuicConnection conn, Throwable cause) {
        if (cause instanceof SSLHandshakeException) {
            notifyAboutHandshakeCompletionIfNeeded(conn, (SSLHandshakeException) cause);
        }
        pipeline().fireExceptionCaught(cause);
    }

    private boolean runTasksDirectly() {
        return sslTaskExecutor == null || sslTaskExecutor == ImmediateExecutor.INSTANCE ||
                sslTaskExecutor == ImmediateEventExecutor.INSTANCE;
    }

    private void runAllTaskSend(QuicheQuicConnection conn, Runnable task) {
        sslTaskExecutor.execute(decorateTaskSend(conn, task));
    }

    private void runAll(QuicheQuicConnection conn, Runnable task) {
        do {
            task.run();
        } while ((task = conn.sslTask()) != null);
    }

    private Runnable decorateTaskSend(QuicheQuicConnection conn, Runnable task) {
        return () -> {
            try {
                runAll(conn, task);
            } finally {
                // Move back to the EventLoop.
                eventLoop().execute(() -> {
                    // Call connection send to continue handshake if needed.
                    if (connectionSend(conn) != SendResult.NONE) {
                        forceFlushParent();
                    }
                    freeIfClosed();
                });
            }
        };
    }

    private SendResult connectionSendSegments(QuicheQuicConnection conn,
                                              SegmentedDatagramPacketAllocator segmentedDatagramPacketAllocator) {
        if (conn.isClosed()) {
            return SendResult.NONE;
        }
        List bufferList = new ArrayList<>(segmentedDatagramPacketAllocator.maxNumSegments());
        long connAddr = conn.address();
        int maxDatagramSize = Quiche.quiche_conn_max_send_udp_payload_size(connAddr);
        SendResult sendResult = SendResult.NONE;
        boolean close = false;
        try {
            for (;;) {
                int len = calculateSendBufferLength(connAddr, maxDatagramSize);
                ByteBuf out = alloc().directBuffer(len);

                ByteBuffer sendInfo = conn.nextSendInfo();
                InetSocketAddress sendToAddress = this.remote;

                int writerIndex = out.writerIndex();
                int written = Quiche.quiche_conn_send(
                        connAddr, Quiche.writerMemoryAddress(out), out.writableBytes(),
                        Quiche.memoryAddressWithPosition(sendInfo));
                if (written == 0) {
                    out.release();
                    // No need to create a new datagram packet. Just try again.
                    continue;
                }
                final boolean done;
                if (written < 0) {
                    done = true;
                    if (written != Quiche.QUICHE_ERR_DONE) {
                        close = Quiche.shouldClose(written);
                        Exception e = Quiche.convertToException(written);
                        if (!tryFailConnectPromise(e)) {
                            // Only fire through the pipeline if this does not fail the connect promise.
                            fireExceptionEvents(conn, e);
                        }
                    }
                } else {
                    done = false;
                }
                int size = bufferList.size();
                if (done) {
                    // We are done, release the buffer and send what we did build up so far.
                    out.release();

                    switch (size) {
                        case 0:
                            // Nothing more to write.
                            break;
                        case 1:
                            // We can write a normal datagram packet.
                            parent().write(new DatagramPacket(bufferList.get(0), sendToAddress));
                            sendResult = SendResult.SOME;
                            break;
                        default:
                            int segmentSize = segmentSize(bufferList);
                            ByteBuf compositeBuffer = Unpooled.wrappedBuffer(bufferList.toArray(new ByteBuf[0]));
                            // We had more than one buffer, create a segmented packet.
                            parent().write(segmentedDatagramPacketAllocator.newPacket(
                                    compositeBuffer, segmentSize, sendToAddress));
                            sendResult = SendResult.SOME;
                            break;
                    }
                    bufferList.clear();
                    if (close) {
                        sendResult = SendResult.CLOSE;
                    }
                    return sendResult;
                }
                out.writerIndex(writerIndex + written);

                int segmentSize = -1;
                if (conn.isSendInfoChanged()) {
                    // Change the cached address and let the user know there was a connection migration.
                    remote = QuicheSendInfo.getToAddress(sendInfo);
                    local = QuicheSendInfo.getFromAddress(sendInfo);

                    if (size > 0) {
                        // We have something in the out list already, we need to send this now and so we set the
                        // segmentSize.
                        segmentSize = segmentSize(bufferList);
                    }
                } else if (size > 0) {
                    int lastReadable = segmentSize(bufferList);
                    // Check if we either need to send now because the last buffer we added has a smaller size then this
                    // one or if we reached the maximum number of segments that we can send.
                    if (lastReadable != out.readableBytes() ||
                            size == segmentedDatagramPacketAllocator.maxNumSegments()) {
                        segmentSize = lastReadable;
                    }
                }

                // If the segmentSize is not -1 we know we need to send now what was in the out list.
                if (segmentSize != -1) {
                    final boolean stop;
                    if (size == 1) {
                        // Only one buffer in the out list, there is no need to use segments.
                        stop = writePacket(new DatagramPacket(
                                bufferList.get(0), sendToAddress), maxDatagramSize, len);
                    } else {
                        // Create a packet with segments in.
                        ByteBuf compositeBuffer = Unpooled.wrappedBuffer(bufferList.toArray(new ByteBuf[0]));
                        stop = writePacket(segmentedDatagramPacketAllocator.newPacket(
                                compositeBuffer, segmentSize, sendToAddress), maxDatagramSize, len);
                    }
                    bufferList.clear();
                    sendResult = SendResult.SOME;

                    if (stop) {
                        // Nothing left in the window, continue later. That said we still need to also
                        // write the previous filled out buffer as otherwise we would either leak or need
                        // to drop it and so produce some loss.
                        if (out.isReadable()) {
                            parent().write(new DatagramPacket(out, sendToAddress));
                        } else {
                            out.release();
                        }
                        if (close) {
                            sendResult = SendResult.CLOSE;
                        }
                        return sendResult;
                    }
                }
                // Let's add a touch with the bufferList as a hint. This will help us to debug leaks if there
                // are any.
                out.touch(bufferList);
                // store for later, so we can make use of segments.
                bufferList.add(out);
            }
        } finally {
            // NOOP
        }
    }

    private static int segmentSize(List bufferList) {
        assert !bufferList.isEmpty();
        int size = bufferList.size();
        return bufferList.get(size - 1).readableBytes();
    }

    private SendResult connectionSendSimple(QuicheQuicConnection conn) {
        if (conn.isClosed()) {
            return SendResult.NONE;
        }
        long connAddr = conn.address();
        SendResult sendResult = SendResult.NONE;
        boolean close = false;
        int maxDatagramSize = Quiche.quiche_conn_max_send_udp_payload_size(connAddr);
        for (;;) {
            ByteBuffer sendInfo = conn.nextSendInfo();

            int len = calculateSendBufferLength(connAddr, maxDatagramSize);
            ByteBuf out = alloc().directBuffer(len);
            int writerIndex = out.writerIndex();

            int written = Quiche.quiche_conn_send(
                    connAddr, Quiche.writerMemoryAddress(out), out.writableBytes(),
                    Quiche.memoryAddressWithPosition(sendInfo));

            if (written == 0) {
                // No need to create a new datagram packet. Just release and try again.
                out.release();
                continue;
            }
            if (written < 0) {
                out.release();
                if (written != Quiche.QUICHE_ERR_DONE) {
                    close = Quiche.shouldClose(written);

                    Exception e = Quiche.convertToException(written);
                    if (!tryFailConnectPromise(e)) {
                        fireExceptionEvents(conn, e);
                    }
                }
                break;
            }
            if (conn.isSendInfoChanged()) {
                // Change the cached address
                remote = QuicheSendInfo.getToAddress(sendInfo);
                local = QuicheSendInfo.getFromAddress(sendInfo);
            }
            out.writerIndex(writerIndex + written);
            boolean stop = writePacket(new DatagramPacket(out, remote), maxDatagramSize, len);
            sendResult = SendResult.SOME;
            if (stop) {
                // Nothing left in the window, continue later
                break;
            }
        }
        if (close) {
            sendResult = SendResult.CLOSE;
        }
        return sendResult;
    }

    private boolean writePacket(DatagramPacket packet, int maxDatagramSize, int len) {
        ChannelFuture future = parent().write(packet);
        if (isSendWindowUsed(maxDatagramSize, len)) {
            // Nothing left in the window, continue later
            future.addListener(continueSendingListener);
            return true;
        }
        return false;
    }

    private static boolean isSendWindowUsed(int maxDatagramSize, int len) {
        return len < maxDatagramSize;
    }

    private static int calculateSendBufferLength(long connAddr, int maxDatagramSize) {
        int len = Math.min(maxDatagramSize, Quiche.quiche_conn_send_quantum(connAddr));
        if (len <= 0) {
            // If there is no room left we just return some small number to reduce the risk of packet drop
            // while still be able to attach the listener to the write future.
            // We use the value of 8 because such an allocation will be cheap to serve from the
            // PooledByteBufAllocator while still serve our need.
            return 8;
        }
        return len;
    }

    /**
     * Write datagrams if needed and return {@code true} if something was written and we need to call
     * {@link Channel#flush()} at some point.
     */
    private SendResult connectionSend(QuicheQuicConnection conn) {
        if (conn.isFreed()) {
            return SendResult.NONE;
        }
        if ((reantranceGuard & IN_CONNECTION_SEND) != 0) {
            // Let's notify about early data if needed.
            notifyEarlyDataReadyIfNeeded(conn);
            return SendResult.NONE;
        }

        reantranceGuard |= IN_CONNECTION_SEND;
        try {
            SendResult sendResult;
            SegmentedDatagramPacketAllocator segmentedDatagramPacketAllocator =
                    config.getSegmentedDatagramPacketAllocator();
            if (segmentedDatagramPacketAllocator.maxNumSegments() > 0) {
                sendResult = connectionSendSegments(conn, segmentedDatagramPacketAllocator);
            } else {
                sendResult = connectionSendSimple(conn);
            }

            // Process / schedule all tasks that were created.

            Runnable task = conn.sslTask();
            if (task != null) {
                if (runTasksDirectly()) {
                    // Consume all tasks
                    do {
                        task.run();
                        // Notify about early data ready if needed.
                        notifyEarlyDataReadyIfNeeded(conn);
                    } while ((task = conn.sslTask()) != null);

                    // Let's try again sending after we did process all tasks.
                    // We schedule this on the EventLoop as otherwise we will get into trouble with re-entrance.
                    eventLoop().execute(new Runnable() {
                        @Override
                        public void run() {
                            // Call connection send to continue handshake if needed.
                            if (connectionSend(conn) != SendResult.NONE) {
                                forceFlushParent();
                            }
                            freeIfClosed();
                        }
                    });
                } else {
                    runAllTaskSend(conn, task);
                }
            } else {
                // Notify about early data ready if needed.
                notifyEarlyDataReadyIfNeeded(conn);
            }

            // Whenever we called connection_send we should also schedule the timer if needed.
            timeoutHandler.scheduleTimeout();
            return sendResult;
        } finally {
            reantranceGuard &= ~IN_CONNECTION_SEND;
        }
    }

    private final class QuicChannelUnsafe extends AbstractChannel.AbstractUnsafe {

        void connectStream(QuicStreamType type, @Nullable ChannelHandler handler,
                           Promise promise) {
            long streamId = idGenerator.nextStreamId(type == QuicStreamType.BIDIRECTIONAL);

            try {
                int res = streamSend0(connection, streamId, Unpooled.EMPTY_BUFFER, false);
                if (res < 0 && res != Quiche.QUICHE_ERR_DONE) {
                    throw Quiche.convertToException(res);
                }
            } catch (Exception e) {
                promise.setFailure(e);
                return;
            }
            if (type == QuicStreamType.UNIDIRECTIONAL) {
                UNI_STREAMS_LEFT_UPDATER.decrementAndGet(QuicheQuicChannel.this);
            } else {
                BIDI_STREAMS_LEFT_UPDATER.decrementAndGet(QuicheQuicChannel.this);
            }
            QuicheQuicStreamChannel streamChannel = addNewStreamChannel(streamId);
            if (handler != null) {
                streamChannel.pipeline().addLast(handler);
            }
            eventLoop().register(streamChannel).addListener((ChannelFuture f) -> {
                if (f.isSuccess()) {
                    promise.setSuccess(streamChannel);
                } else {
                    promise.setFailure(f.cause());
                    streams.remove(streamId);
                }
            });
        }

        @Override
        public void connect(SocketAddress remote, SocketAddress local, ChannelPromise channelPromise) {
            assert eventLoop().inEventLoop();
            if (server) {
                channelPromise.setFailure(new UnsupportedOperationException());
                return;
            }

            if (connectPromise != null) {
                channelPromise.setFailure(new ConnectionPendingException());
                return;
            }

            if (remote instanceof QuicConnectionAddress) {
                if (!sourceConnectionIds.isEmpty()) {
                    // If a key is assigned we know this channel was already connected.
                    channelPromise.setFailure(new AlreadyConnectedException());
                    return;
                }

                connectAddress = (QuicConnectionAddress) remote;
                connectPromise = channelPromise;

                // Schedule connect timeout.
                int connectTimeoutMillis = config().getConnectTimeoutMillis();
                if (connectTimeoutMillis > 0) {
                    connectTimeoutFuture = eventLoop().schedule(() -> {
                        ChannelPromise connectPromise = QuicheQuicChannel.this.connectPromise;
                        if (connectPromise != null && !connectPromise.isDone()
                                && connectPromise.tryFailure(new ConnectTimeoutException(
                                "connection timed out: " + remote))) {
                            close(voidPromise());
                        }
                    }, connectTimeoutMillis, TimeUnit.MILLISECONDS);
                }

                connectPromise.addListener((ChannelFuture future) -> {
                    if (future.isCancelled()) {
                        if (connectTimeoutFuture != null) {
                            connectTimeoutFuture.cancel(false);
                        }
                        connectPromise = null;
                        close(voidPromise());
                    }
                });

                parent().connect(new QuicheQuicChannelAddress(QuicheQuicChannel.this))
                        .addListener(f -> {
                            ChannelPromise connectPromise = QuicheQuicChannel.this.connectPromise;
                            if (connectPromise != null && !f.isSuccess()) {
                                connectPromise.tryFailure(f.cause());
                                // close everything after notify about failure.
                                unsafe().closeForcibly();
                            }
                        });
                return;
            }

            channelPromise.setFailure(new UnsupportedOperationException());
        }

        private void fireConnectCloseEventIfNeeded(QuicheQuicConnection conn) {
            if (connectionCloseEvent == null && !conn.isFreed()) {
                connectionCloseEvent = Quiche.quiche_conn_peer_error(conn.address());
                if (connectionCloseEvent != null) {
                    pipeline().fireUserEventTriggered(connectionCloseEvent);
                }
            }
        }

        void connectionRecv(InetSocketAddress sender, InetSocketAddress recipient, ByteBuf buffer) {
            QuicheQuicConnection conn = QuicheQuicChannel.this.connection;
            if (conn.isFreed()) {
                return;
            }
            int bufferReadable = buffer.readableBytes();
            if (bufferReadable == 0) {
                // Nothing to do here. Just return...
                // See also https://github.com/cloudflare/quiche/issues/817
                return;
            }

            reantranceGuard |= IN_RECV;
            boolean close = false;
            try {
                ByteBuf tmpBuffer = null;
                // We need to make a copy if the buffer is read only as recv(...) may modify the input buffer as well.
                // See https://docs.rs/quiche/0.6.0/quiche/struct.Connection.html#method.recv
                if (buffer.isReadOnly()) {
                    tmpBuffer = alloc().directBuffer(buffer.readableBytes());
                    tmpBuffer.writeBytes(buffer);
                    buffer = tmpBuffer;
                }
                long memoryAddress = Quiche.readerMemoryAddress(buffer);

                ByteBuffer recvInfo = conn.nextRecvInfo();
                QuicheRecvInfo.setRecvInfo(recvInfo, sender, recipient);

                remote = sender;
                local = recipient;

                try {
                    do  {
                        // Call quiche_conn_recv(...) until we consumed all bytes or we did receive some error.
                        int res = Quiche.quiche_conn_recv(conn.address(), memoryAddress, bufferReadable,
                                Quiche.memoryAddressWithPosition(recvInfo));
                        final boolean done;
                        if (res < 0) {
                            done = true;
                            if (res != Quiche.QUICHE_ERR_DONE) {
                                close = Quiche.shouldClose(res);
                                Exception e = Quiche.convertToException(res);
                                if (tryFailConnectPromise(e)) {
                                    break;
                                }
                                fireExceptionEvents(conn, e);
                            }
                        } else {
                            done = false;
                        }
                        // Process / schedule all tasks that were created.
                        Runnable task = conn.sslTask();
                        if (task != null) {
                            if (runTasksDirectly()) {
                                // Consume all tasks
                                do {
                                    task.run();
                                } while ((task = conn.sslTask()) != null);
                                processReceived(conn);
                            } else {
                                runAllTaskRecv(conn, task);
                            }
                        } else {
                            processReceived(conn);
                        }

                        if (done) {
                            break;
                        }
                        memoryAddress += res;
                        bufferReadable -= res;
                    } while (bufferReadable > 0 && !conn.isFreed());
                } finally {
                    buffer.skipBytes((int) (memoryAddress - Quiche.readerMemoryAddress(buffer)));
                    if (tmpBuffer != null) {
                        tmpBuffer.release();
                    }
                }
                if (close) {
                    // Let's close now as there is no way to recover
                    unsafe().close(newPromise());
                }
            } finally {
                reantranceGuard &= ~IN_RECV;
            }
        }

        private void processReceived(QuicheQuicConnection conn) {
            // Handle pending channelActive if needed.
            if (handlePendingChannelActive(conn)) {
                // Connection was closed right away.
                return;
            }

            notifyAboutHandshakeCompletionIfNeeded(conn, null);
            fireConnectCloseEventIfNeeded(conn);

            if (conn.isFreed()) {
                return;
            }

            long connAddr = conn.address();
            if (Quiche.quiche_conn_is_established(connAddr) ||
                    Quiche.quiche_conn_is_in_early_data(connAddr)) {
                long uniLeftOld = uniStreamsLeft;
                long bidiLeftOld = bidiStreamsLeft;
                // Only fetch new stream info when we used all our credits
                if (uniLeftOld == 0 || bidiLeftOld == 0) {
                    long uniLeft = Quiche.quiche_conn_peer_streams_left_uni(connAddr);
                    long bidiLeft = Quiche.quiche_conn_peer_streams_left_bidi(connAddr);
                    uniStreamsLeft = uniLeft;
                    bidiStreamsLeft = bidiLeft;
                    if (uniLeftOld != uniLeft || bidiLeftOld != bidiLeft) {
                        pipeline().fireUserEventTriggered(QuicStreamLimitChangedEvent.INSTANCE);
                    }
                }

                handlePathEvents(conn);

                if (handleWritableStreams(conn)) {
                    // Some data was produced, let's flush.
                    flushParent();
                }

                datagramReadable = true;
                streamReadable = true;

                recvDatagram(conn);
                recvStream(conn);
            }
        }

        private void handlePathEvents(QuicheQuicConnection conn) {
            long event;
            while (!conn.isFreed() && (event = Quiche.quiche_conn_path_event_next(conn.address())) > 0) {
                try {
                    int type = Quiche.quiche_path_event_type(event);

                    if (type == Quiche.QUICHE_PATH_EVENT_NEW) {
                        Object[] ret = Quiche.quiche_path_event_new(event);
                        InetSocketAddress local = (InetSocketAddress) ret[0];
                        InetSocketAddress peer = (InetSocketAddress) ret[1];
                        pipeline().fireUserEventTriggered(new QuicPathEvent.New(local, peer));
                    } else if (type == Quiche.QUICHE_PATH_EVENT_VALIDATED) {
                        Object[] ret = Quiche.quiche_path_event_validated(event);
                        InetSocketAddress local = (InetSocketAddress) ret[0];
                        InetSocketAddress peer = (InetSocketAddress) ret[1];
                        pipeline().fireUserEventTriggered(new QuicPathEvent.Validated(local, peer));
                    } else if (type == Quiche.QUICHE_PATH_EVENT_FAILED_VALIDATION) {
                        Object[] ret = Quiche.quiche_path_event_failed_validation(event);
                        InetSocketAddress local = (InetSocketAddress) ret[0];
                        InetSocketAddress peer = (InetSocketAddress) ret[1];
                        pipeline().fireUserEventTriggered(new QuicPathEvent.FailedValidation(local, peer));
                    } else if (type == Quiche.QUICHE_PATH_EVENT_CLOSED) {
                        Object[] ret = Quiche.quiche_path_event_closed(event);
                        InetSocketAddress local = (InetSocketAddress) ret[0];
                        InetSocketAddress peer = (InetSocketAddress) ret[1];
                        pipeline().fireUserEventTriggered(new QuicPathEvent.Closed(local, peer));
                    } else if (type == Quiche.QUICHE_PATH_EVENT_REUSED_SOURCE_CONNECTION_ID) {
                        Object[] ret = Quiche.quiche_path_event_reused_source_connection_id(event);
                        Long seq = (Long) ret[0];
                        InetSocketAddress localOld = (InetSocketAddress) ret[1];
                        InetSocketAddress peerOld = (InetSocketAddress) ret[2];
                        InetSocketAddress local = (InetSocketAddress) ret[3];
                        InetSocketAddress peer = (InetSocketAddress) ret[4];
                        pipeline().fireUserEventTriggered(
                                new QuicPathEvent.ReusedSourceConnectionId(seq, localOld, peerOld, local, peer));
                    } else if (type == Quiche.QUICHE_PATH_EVENT_PEER_MIGRATED) {
                        Object[] ret = Quiche.quiche_path_event_peer_migrated(event);
                        InetSocketAddress local = (InetSocketAddress) ret[0];
                        InetSocketAddress peer = (InetSocketAddress) ret[1];
                        pipeline().fireUserEventTriggered(new QuicPathEvent.PeerMigrated(local, peer));
                    }
                } finally {
                    Quiche.quiche_path_event_free(event);
                }
            }
        }

        private void runAllTaskRecv(QuicheQuicConnection conn, Runnable task) {
            sslTaskExecutor.execute(decorateTaskRecv(conn, task));
        }

        private Runnable decorateTaskRecv(QuicheQuicConnection conn, Runnable task) {
            return () -> {
                try {
                    runAll(conn, task);
                } finally {
                    // Move back to the EventLoop.
                    eventLoop().execute(() -> {
                        if (!conn.isFreed()) {
                            processReceived(conn);

                            // Call connection send to continue handshake if needed.
                            if (connectionSend(conn) != SendResult.NONE) {
                                forceFlushParent();
                            }

                            freeIfClosed();
                        }
                    });
                }
            };
        }
        void recv() {
            QuicheQuicConnection conn = connection;
            if ((reantranceGuard & IN_RECV) != 0 || conn.isFreed()) {
                return;
            }

            long connAddr = conn.address();
            // Check if we can read anything yet.
            if (!Quiche.quiche_conn_is_established(connAddr) &&
                    !Quiche.quiche_conn_is_in_early_data(connAddr)) {
                return;
            }

            reantranceGuard |= IN_RECV;
            try {
                recvDatagram(conn);
                recvStream(conn);
            } finally {
                fireChannelReadCompleteIfNeeded();
                reantranceGuard &= ~IN_RECV;
            }
        }

        private void recvStream(QuicheQuicConnection conn) {
            if (conn.isFreed()) {
                return;
            }
            long connAddr = conn.address();
            long readableIterator = Quiche.quiche_conn_readable(connAddr);
            int totalReadable = 0;
            if (readableIterator != -1) {
                try {
                    // For streams we always process all streams when at least on read was requested.
                    if (recvStreamPending && streamReadable) {
                        for (;;) {
                            int readable = Quiche.quiche_stream_iter_next(
                                    readableIterator, readableStreams);
                            for (int i = 0; i < readable; i++) {
                                long streamId = readableStreams[i];
                                QuicheQuicStreamChannel streamChannel = streams.get(streamId);
                                if (streamChannel == null) {
                                    recvStreamPending = false;
                                    fireChannelReadCompletePending = true;
                                    streamChannel = addNewStreamChannel(streamId);
                                    streamChannel.readable();
                                    pipeline().fireChannelRead(streamChannel);
                                } else {
                                    streamChannel.readable();
                                }
                            }
                            if (readable < readableStreams.length) {
                                // We did consume all readable streams.
                                streamReadable = false;
                                break;
                            }
                            if (readable > 0) {
                                totalReadable += readable;
                            }
                        }
                    }
                } finally {
                    Quiche.quiche_stream_iter_free(readableIterator);
                }
                readableStreams = growIfNeeded(readableStreams, totalReadable);
            }
        }

        private void recvDatagram(QuicheQuicConnection conn) {
            if (!supportsDatagram) {
                return;
            }
            while (recvDatagramPending && datagramReadable && !conn.isFreed()) {
                @SuppressWarnings("deprecation")
                RecvByteBufAllocator.Handle recvHandle = recvBufAllocHandle();
                recvHandle.reset(config());

                int numMessagesRead = 0;
                do {
                    long connAddr = conn.address();
                    int len = Quiche.quiche_conn_dgram_recv_front_len(connAddr);
                    if (len == Quiche.QUICHE_ERR_DONE) {
                        datagramReadable = false;
                        return;
                    }

                    ByteBuf datagramBuffer = alloc().directBuffer(len);
                    recvHandle.attemptedBytesRead(datagramBuffer.writableBytes());
                    int writerIndex = datagramBuffer.writerIndex();
                    long memoryAddress = Quiche.writerMemoryAddress(datagramBuffer);

                    int written = Quiche.quiche_conn_dgram_recv(connAddr,
                            memoryAddress, datagramBuffer.writableBytes());
                    if (written < 0) {
                        datagramBuffer.release();
                        if (written == Quiche.QUICHE_ERR_DONE) {
                            // We did consume all datagram packets.
                            datagramReadable = false;
                            break;
                        }
                        pipeline().fireExceptionCaught(Quiche.convertToException(written));
                    }
                    recvHandle.lastBytesRead(written);
                    recvHandle.incMessagesRead(1);
                    numMessagesRead++;
                    datagramBuffer.writerIndex(writerIndex + written);
                    recvDatagramPending = false;
                    fireChannelReadCompletePending = true;

                    pipeline().fireChannelRead(datagramBuffer);
                } while (recvHandle.continueReading() && !conn.isFreed());
                recvHandle.readComplete();

                // Check if we produced any messages.
                if (numMessagesRead > 0) {
                    fireChannelReadCompleteIfNeeded();
                }
            }
        }

        private boolean handlePendingChannelActive(QuicheQuicConnection conn) {
            if (conn.isFreed() || state == ChannelState.CLOSED) {
                return true;
            }
            if (server) {
                if (state == ChannelState.OPEN && Quiche.quiche_conn_is_established(conn.address())) {
                    // We didn't notify before about channelActive... Update state and fire the event.
                    state = ChannelState.ACTIVE;

                    pipeline().fireChannelActive();
                    notifyAboutHandshakeCompletionIfNeeded(conn, null);
                    fireDatagramExtensionEvent(conn);
                }
            } else if (connectPromise != null && Quiche.quiche_conn_is_established(conn.address())) {
                ChannelPromise promise = connectPromise;
                connectPromise = null;
                state = ChannelState.ACTIVE;

                boolean promiseSet = promise.trySuccess();
                pipeline().fireChannelActive();
                notifyAboutHandshakeCompletionIfNeeded(conn, null);
                fireDatagramExtensionEvent(conn);
                if (!promiseSet) {
                    fireConnectCloseEventIfNeeded(conn);
                    this.close(this.voidPromise());
                    return true;
                }
            }
            return false;
        }

        private void fireDatagramExtensionEvent(QuicheQuicConnection conn) {
            if (conn.isClosed()) {
                return;
            }
            long connAddr = conn.address();
            int len = Quiche.quiche_conn_dgram_max_writable_len(connAddr);
            // QUICHE_ERR_DONE means the remote peer does not support the extension.
            if (len != Quiche.QUICHE_ERR_DONE) {
                pipeline().fireUserEventTriggered(new QuicDatagramExtensionEvent(len));
            }
        }

        private QuicheQuicStreamChannel addNewStreamChannel(long streamId) {
            QuicheQuicStreamChannel streamChannel = new QuicheQuicStreamChannel(
                    QuicheQuicChannel.this, streamId);
            QuicheQuicStreamChannel old = streams.put(streamId, streamChannel);
            assert old == null;
            streamChannel.writable(streamCapacity(streamId));
            return streamChannel;
        }
    }

    /**
     * Finish the connect operation of a client channel.
     */
    void finishConnect() {
        assert !server;
        assert connection != null;
        if (connectionSend(connection) != SendResult.NONE) {
            flushParent();
        }
    }

    private void notifyEarlyDataReadyIfNeeded(QuicheQuicConnection conn) {
        if (!server && !earlyDataReadyNotified &&
                !conn.isFreed() && Quiche.quiche_conn_is_in_early_data(conn.address())) {
            earlyDataReadyNotified = true;
            pipeline().fireUserEventTriggered(SslEarlyDataReadyEvent.INSTANCE);
        }
    }

    private final class TimeoutHandler implements Runnable {
        private ScheduledFuture timeoutFuture;


        @Override
        public void run() {
            QuicheQuicConnection conn = connection;
            if (conn.isFreed()) {
                return;
            }
            if (!freeIfClosed()) {
                long connAddr = conn.address();
                timeoutFuture = null;
                // Notify quiche there was a timeout.
                Quiche.quiche_conn_on_timeout(connAddr);
                if (!freeIfClosed()) {
                    // We need to call connectionSend when a timeout was triggered.
                    // See https://docs.rs/quiche/0.6.0/quiche/struct.Connection.html#method.send.
                    if (connectionSend(conn) != SendResult.NONE) {
                        flushParent();
                    }
                    boolean closed = freeIfClosed();
                    if (!closed) {
                        // The connection is alive, reschedule.
                        scheduleTimeout();
                    }
                }
            }
        }

        // Schedule timeout.
        // See https://docs.rs/quiche/0.6.0/quiche/#generating-outgoing-packets
        void scheduleTimeout() {
            QuicheQuicConnection conn = connection;
            if (conn.isFreed()) {
                cancel();
                return;
            }
            if (conn.isClosed()) {
                cancel();
                unsafe().close(newPromise());
                return;
            }
            long nanos = Quiche.quiche_conn_timeout_as_nanos(conn.address());
            if (nanos < 0 || nanos == Long.MAX_VALUE) {
                // No timeout needed.
                cancel();
                return;
            }
            if (timeoutFuture == null) {
                timeoutFuture = eventLoop().schedule(this,
                        nanos, TimeUnit.NANOSECONDS);
            } else {
                long remaining = timeoutFuture.getDelay(TimeUnit.NANOSECONDS);
                if (remaining <= 0) {
                    // This means the timer already elapsed. In this case just cancel the future and call run()
                    // directly. This will ensure we correctly call quiche_conn_on_timeout() etc.
                    cancel();
                    run();
                } else if (remaining > nanos) {
                    // The new timeout is smaller then what was scheduled before. Let's cancel the old timeout
                    // and schedule a new one.
                    cancel();
                    timeoutFuture = eventLoop().schedule(this, nanos, TimeUnit.NANOSECONDS);
                }
            }
        }

        void cancel() {
            if (timeoutFuture != null) {
                timeoutFuture.cancel(false);
                timeoutFuture = null;
            }
        }
    }

    @Override
    public Future collectStats(Promise promise) {
        if (eventLoop().inEventLoop()) {
            collectStats0(promise);
        } else {
            eventLoop().execute(() -> collectStats0(promise));
        }
        return promise;
    }

    private void collectStats0(Promise promise) {
        QuicheQuicConnection conn = connection;
        if (conn.isFreed()) {
            promise.setSuccess(statsAtClose);
            return;
        }

        collectStats0(connection, promise);
    }

    @Nullable
    private QuicConnectionStats collectStats0(QuicheQuicConnection connection, Promise promise) {
        final long[] stats = Quiche.quiche_conn_stats(connection.address());
        if (stats == null) {
            promise.setFailure(new IllegalStateException("native quiche_conn_stats(...) failed"));
            return null;
        }

        final QuicheQuicConnectionStats connStats =
            new QuicheQuicConnectionStats(stats);
        promise.setSuccess(connStats);
        return connStats;
    }

    @Override
    public Future collectPathStats(int pathIdx, Promise promise) {
        if (eventLoop().inEventLoop()) {
            collectPathStats0(pathIdx, promise);
        } else {
            eventLoop().execute(() -> collectPathStats0(pathIdx, promise));
        }
        return promise;
    }

    private void collectPathStats0(int pathIdx, Promise promise) {
        QuicheQuicConnection conn = connection;
        if (conn.isFreed()) {
            promise.setFailure(new IllegalStateException("Connection is closed"));
            return;
        }

        final Object[] stats = Quiche.quiche_conn_path_stats(connection.address(), pathIdx);
        if (stats == null) {
            promise.setFailure(new IllegalStateException("native quiche_conn_path_stats(...) failed"));
            return;
        }
        promise.setSuccess(new QuicheQuicConnectionPathStats(stats));
    }


    @Override
    public QuicTransportParameters peerTransportParameters() {
        return connection.peerParameters();
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy