netty / netty-incubator-codec-http3

Experimental HTTP3 codec on top of QUIC
Apache License 2.0
167 stars 35 forks source link

Client remote connections fail to Cloudflare (on Windows x64) #299

Open Jire opened 3 months ago

Jire commented 3 months ago

qlog

{"qlog_version":"0.3","qlog_format":"JSON-SEQ","title":"testTitle","description":"test id=4daa99b8d91a40905c8e17805f9ba0f636de3791","trace":{"vantage_point":{"type":"client"},"title":"testTitle","description":"test id=4daa99b8d91a40905c8e17805f9ba0f636de3791","configuration":{"time_offset":0.0}}}
{"time":0.0,"name":"transport:parameters_set","data":{"owner":"local","tls_cipher":"None","disable_active_migration":false,"max_idle_timeout":5000,"max_udp_payload_size":65527,"ack_delay_exponent":3,"max_ack_delay":25,"active_connection_id_limit":2,"initial_max_data":10000000,"initial_max_stream_data_bidi_local":1000000,"initial_max_stream_data_bidi_remote":1000000,"initial_max_stream_data_uni":1000000,"initial_max_streams_bidi":100,"initial_max_streams_uni":100}}
{"time":0.5261,"name":"transport:packet_sent","data":{"header":{"packet_type":"initial","packet_number":0,"version":"1","scil":20,"dcil":16,"scid":"4daa99b8d91a40905c8e17805f9ba0f636de3791","dcid":"ae7a95cc31400e7f45e73de419708b65"},"raw":{"length":316,"payload_length":253},"send_at_time":0.5261,"frames":[{"frame_type":"crypto","offset":0,"length":249}]}}
{"time":0.5261,"name":"recovery:metrics_updated","data":{"smoothed_rtt":333.0,"rtt_variance":166.5,"congestion_window":12000,"bytes_in_flight":316,"ssthresh":18446744073709551615}}
{"time":7.5481,"name":"transport:packet_received","data":{"header":{"packet_type":"initial","packet_number":0,"version":"1","scil":20,"dcil":20,"scid":"019c09f8632ac8a9139e80f8a22ade62d89d9b78","dcid":"4daa99b8d91a40905c8e17805f9ba0f636de3791"},"raw":{"length":1200,"payload_length":23},"frames":[{"frame_type":"ack","ack_delay":0.068,"acked_ranges":[[0,0]]}]}}
{"time":7.5481,"name":"recovery:metrics_updated","data":{"min_rtt":7.022,"smoothed_rtt":7.022,"latest_rtt":7.022,"rtt_variance":3.511,"bytes_in_flight":0}}
{"time":8.7484,"name":"transport:packet_received","data":{"header":{"packet_type":"initial","packet_number":1,"version":"1","scil":20,"dcil":20,"scid":"019c09f8632ac8a9139e80f8a22ade62d89d9b78","dcid":"4daa99b8d91a40905c8e17805f9ba0f636de3791"},"raw":{"length":1200,"payload_length":22},"frames":[{"frame_type":"connection_close","error_space":"transport_error","error_code":296,"reason":""}]}}

Reproduce with this on Windows x64:

import io.netty.bootstrap.Bootstrap
import io.netty.channel.ChannelHandlerContext
import io.netty.channel.nio.NioEventLoopGroup
import io.netty.channel.socket.nio.NioDatagramChannel
import io.netty.handler.ssl.util.InsecureTrustManagerFactory
import io.netty.incubator.codec.http3.*
import io.netty.incubator.codec.quic.*
import io.netty.util.ReferenceCountUtil
import java.net.InetSocketAddress
import java.util.concurrent.TimeUnit

object JS5ProxyServer {

    @JvmStatic
    fun main(args: Array<String>) {
        val hostName = "areos.io"
        val js5Port = 443

        val group = NioEventLoopGroup(1)

        try {
            val context = QuicSslContextBuilder.forClient()
                .trustManager(InsecureTrustManagerFactory.INSTANCE)
                .applicationProtocols(*Http3.supportedApplicationProtocols())
                .build()
            val codec = Http3.newQuicClientCodecBuilder()
                .sslContext(context)
                .initialMaxStreamsBidirectional(100)
                .initialMaxStreamsUnidirectional(100)
                .initialMaxData(10000000)
                .initialMaxStreamDataBidirectionalLocal(1000000)
                .initialMaxStreamDataBidirectionalRemote(1000000)
                .initialMaxStreamDataUnidirectional(1000000)
                .maxIdleTimeout(5000, TimeUnit.MILLISECONDS)
                .build()

            val bs = Bootstrap()
            val channel = bs.group(group)
                .channel(NioDatagramChannel::class.java)
                .handler(codec)
                .bind(0)
                .sync()
                .channel()

            val quicChannel = QuicChannel.newBootstrap(channel)
                .handler(Http3ClientConnectionHandler())
                .remoteAddress(InetSocketAddress(hostName, js5Port))
                .option(
                    QuicChannelOption.QLOG,
                    QLogConfiguration("quiclog.txt", "testTitle", "test")
                )
                .connect()
                .get()

            val streamChannel = Http3.newRequestStream(quicChannel,
                object : Http3RequestStreamInboundHandler() {
                    override fun channelRead(ctx: ChannelHandlerContext, frame: Http3HeadersFrame) {
                        println("header frame: $frame")

                        ReferenceCountUtil.release(frame)
                    }

                    override fun channelRead(ctx: ChannelHandlerContext, frame: Http3DataFrame) {
                        println("data frame: $frame")

                        ReferenceCountUtil.release(frame)
                    }

                    override fun channelInputClosed(ctx: ChannelHandlerContext) {
                        println("channel input closed")

                        ctx.close()
                    }
                }).sync().now

            streamChannel
                .pipeline()
                .addLast(
                    Http3FrameToHttpObjectCodec(false, false)
                )

            val headers = DefaultHttp3Headers(false)
                .method("GET")
                .path("/flat-cache/0-0.dat")
                .authority("$hostName:$js5Port")
                .scheme("https")
            val headersFrame = DefaultHttp3HeadersFrame(headers)
            streamChannel.writeAndFlush(headersFrame)
                .addListener(QuicStreamChannel.SHUTDOWN_OUTPUT).sync()

            // Wait for the stream channel and quic channel to be closed (this will happen after we received the FIN).
            // After this is done we will close the underlying datagram channel.
            streamChannel.closeFuture().sync()

            // After we received the response lets also close the underlying QUIC channel and datagram channel.
            quicChannel.close().sync()
            channel.close().sync()
        } finally {
            group.shutdownGracefully()
        }
    }

}
normanmaurer commented 1 week ago

Unfortunately I have no windows installation to test this with. That said I wonder if you need to set the correct hostname when creating the SSlEngine:

 Http3.newQuicClientCodecBuilder().sslEngineProvider(q -> sslContext.newEngine(q.alloc(), hostname, port))

This way the correct SNI hostname is used.