Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Connection fails when host header field is set in the request and :authority is omitted #323

Closed
naokiiwakami opened this issue Feb 3, 2025 · 2 comments

Comments

@naokiiwakami
Copy link
Contributor

naokiiwakami commented Feb 3, 2025

When I connect to the Example server Http3ServerExample using Angie HTTP/3 proxy, request is forwarded to the server properly with its basic configuration. But if the proxy is configured to add Host header field in the request,
the HTTP/3 server fails to handle the request. Following is the exception I observed on the server side.

15:05:24.488 [nioEventLoopGroup-2-1] WARN io.netty.channel.DefaultChannelPipeline -- An exceptionCaught() event was fired, and it reached at the tail of the pipeline. It usually means the last handler in the pipeline did not handle the exception.
io.netty.incubator.codec.http3.Http3HeadersValidationException: Not all mandatory pseudo-headers included.
	at io.netty.incubator.codec.http3.Http3HeadersSink.finish(Http3HeadersSink.java:109)
	at io.netty.incubator.codec.http3.Http3FrameCodec.decodeHeaders(Http3FrameCodec.java:403)
	at io.netty.incubator.codec.http3.Http3FrameCodec.decodeFrame(Http3FrameCodec.java:252)
	at io.netty.incubator.codec.http3.Http3FrameCodec.decode(Http3FrameCodec.java:201)
	at io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:530)
	at io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:469)
	at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:290)
	at io.netty.incubator.codec.http3.Http3FrameCodec.channelRead(Http3FrameCodec.java:131)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:444)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412)
	at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:440)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
	at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)
	at io.netty.incubator.codec.quic.QuicheQuicStreamChannel$QuicStreamChannelUnsafe.recv(QuicheQuicStreamChannel.java:978)
	at io.netty.incubator.codec.quic.QuicheQuicStreamChannel$QuicStreamChannelUnsafe.beginRead(QuicheQuicStreamChannel.java:623)
	at io.netty.channel.DefaultChannelPipeline$HeadContext.read(DefaultChannelPipeline.java:1362)
	at io.netty.channel.AbstractChannelHandlerContext.invokeRead(AbstractChannelHandlerContext.java:845)
	at io.netty.channel.AbstractChannelHandlerContext.read(AbstractChannelHandlerContext.java:824)
	at io.netty.incubator.codec.http3.Http3FrameCodec.read(Http3FrameCodec.java:635)
	at io.netty.channel.AbstractChannelHandlerContext.invokeRead(AbstractChannelHandlerContext.java:851)
	at io.netty.channel.AbstractChannelHandlerContext.read(AbstractChannelHandlerContext.java:824)
	at io.netty.incubator.codec.http3.Http3FrameTypeDuplexValidationHandler.read(Http3FrameTypeDuplexValidationHandler.java:85)
	at io.netty.channel.AbstractChannelHandlerContext.invokeRead(AbstractChannelHandlerContext.java:851)
	at io.netty.channel.AbstractChannelHandlerContext.read(AbstractChannelHandlerContext.java:824)
	at io.netty.channel.DefaultChannelPipeline.read(DefaultChannelPipeline.java:1004)
	at io.netty.channel.DefaultChannelPipeline.read(DefaultChannelPipeline.java:46)
	at io.netty.incubator.codec.quic.QuicheQuicStreamChannel.read(QuicheQuicStreamChannel.java:288)
	at io.netty.incubator.codec.quic.QuicheQuicStreamChannel.read(QuicheQuicStreamChannel.java:52)
	at io.netty.channel.DefaultChannelPipeline$HeadContext.readIfIsAutoRead(DefaultChannelPipeline.java:1422)
	at io.netty.channel.DefaultChannelPipeline$HeadContext.channelActive(DefaultChannelPipeline.java:1400)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelActive(AbstractChannelHandlerContext.java:258)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelActive(AbstractChannelHandlerContext.java:238)
	at io.netty.channel.DefaultChannelPipeline.fireChannelActive(DefaultChannelPipeline.java:895)
	at io.netty.incubator.codec.quic.QuicheQuicStreamChannel$QuicStreamChannelUnsafe.register(QuicheQuicStreamChannel.java:488)
	at io.netty.channel.SingleThreadEventLoop.register(SingleThreadEventLoop.java:89)
	at io.netty.channel.SingleThreadEventLoop.register(SingleThreadEventLoop.java:83)
	at io.netty.incubator.codec.quic.QuicheQuicChannel$1.onUnhandledInboundMessage(QuicheQuicChannel.java:435)
	at io.netty.channel.DefaultChannelPipeline$TailContext.channelRead(DefaultChannelPipeline.java:1296)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:444)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412)
	at io.netty.incubator.codec.http3.Http3ConnectionHandler.channelRead(Http3ConnectionHandler.java:180)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:444)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412)
	at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:440)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
	at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)
	at io.netty.incubator.codec.quic.QuicheQuicChannel$QuicChannelUnsafe.recvStream(QuicheQuicChannel.java:1808)
	at io.netty.incubator.codec.quic.QuicheQuicChannel$QuicChannelUnsafe.processReceived(QuicheQuicChannel.java:1687)
	at io.netty.incubator.codec.quic.QuicheQuicChannel$QuicChannelUnsafe.connectionRecv(QuicheQuicChannel.java:1622)
	at io.netty.incubator.codec.quic.QuicheQuicChannel.recv(QuicheQuicChannel.java:942)
	at io.netty.incubator.codec.quic.QuicheQuicCodec$QuicCodecHeaderProcessor.process(QuicheQuicCodec.java:375)
	at io.netty.incubator.codec.quic.QuicHeaderParser.parse(QuicHeaderParser.java:130)
	at io.netty.incubator.codec.quic.QuicheQuicCodec.handleQuicPacket(QuicheQuicCodec.java:204)
	at io.netty.incubator.codec.quic.QuicheQuicCodec.channelRead(QuicheQuicCodec.java:195)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:442)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412)
	at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:440)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
	at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)
	at io.netty.channel.nio.AbstractNioMessageChannel$NioMessageUnsafe.read(AbstractNioMessageChannel.java:97)
	at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:788)
	at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:724)
	at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:650)
	at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:562)
	at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:997)
	at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
	at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
	at java.base/java.lang.Thread.run(Thread.java:829)

The problem is that Angie omits :authority header field in the request when the proxy is configured to add Host header field. The request header field checker in Http3HeadersSink class does not recognize the host header field.

Here is the header fields sent when the connection was successful:

   :method: GET
   :scheme: https
   :path: /
   :authority: backend_h3
   user-agent: curl/7.81.0
   accept: */*

Connection fails when the proxy sends following header fields:

   :method: GET
   :scheme: https
   :path: /
   host: localhost:8080
   user-agent: curl/7.81.0
   accept: */*

According to Section 7.2 of RFC 9110, :authority header field is not necessary when Host header field exists. So the behavior in the Angie should be legitimate.

I have a fix for this issue in my fork, shall I send a pull request?

In order to try running the Angie HTTP/3 proxy, I wrote a note how to set it up https://gist.github.com/naokiiwakami/bbd9788ee33a730cad56f23c041d4b4f

Following is the http server configuration in Angie when the connection is successful

upstream backend_h3 {
  server localhost:9999;  
}

server {
    listen 8080;
    server_name localhost;

    location / {
        proxy_pass https://backend_h3;
        proxy_http_version 3;
        proxy_http3_max_concurrent_streams 8192;
        proxy_socket_keepalive on;
        proxy_ssl_trusted_certificate /etc/angie/certs/cacerts.pem;
    }
}

Following is the configuration for the failed case. Directive proxy_set_header is added.

upstream backend_h3 {
  server localhost:9999;  
}

server {
    listen 8080;
    server_name localhost;

    location / {
        proxy_pass https://backend_h3;
        proxy_http_version 3;
        proxy_http3_max_concurrent_streams 8192;
        proxy_socket_keepalive on;
        proxy_ssl_trusted_certificate /etc/angie/certs/cacerts.pem;

        proxy_set_header Host $http_host;
    }
}
@normanmaurer
Copy link
Member

@naokiiwakami yes we love PRs.

@naokiiwakami
Copy link
Contributor Author

I have created a PR #324

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants