caddy icon indicating copy to clipboard operation
caddy copied to clipboard

HTTP3 + `strict_sni_host` always results in `403` status

Open uaex opened this issue 4 years ago • 18 comments

I use curl -I --http3 https://localhost/index

when strict_sni_host off, status is 200 from reverse_proxy when strict_sni_host on, status is 403 from caddy

my Caddyfile:

{
	auto_https disable_redirects

	servers {
		protocol {
			experimental_http3
			strict_sni_host
		}
	}
}

localhost {
	reverse_proxy localhost:3000
}

uaex avatar Aug 20 '21 00:08 uaex

What's in your logs? Caddy will log an error when SNI does not match the HTTP Host. It will read something like "strict host matching: TLS ServerName (%s) and HTTP Host (%s) values differ".

https://github.com/caddyserver/caddy/blob/a056fcd7ba64b6f313f08ec7a48536c726ca432b/modules/caddyhttp/server.go#L280-L299

francislavoie avatar Aug 20 '21 01:08 francislavoie

I use caddy run, and use Caddyfaile

{
    debug
    auto_https disable_redirects

	servers {
		protocol {
			experimental_http3
			strict_sni_host
		}
	}
       
    log {
        output stderr
    }
}

localhost {
	reverse_proxy localhost:3000
}

but no any logs output for this request

header:

HTTP/3 403
server: Caddy
alt-svc: h3=":443"; ma=2592000,h3-34=":443"; ma=2592000,h3-32=":443"; ma=2592000,h3-29=":443"; ma=2592000

@francislavoie

uaex avatar Aug 20 '21 02:08 uaex

Try adding this to your site block:

handle_errors {
	respond "Code:{http.error.status_code} StatusText:{http.error.status_text} Message:{http.error.message}"
}

I think the error is being passed through the error handler chain, which ends up dropping the error message without being logged by default 🤔

francislavoie avatar Aug 20 '21 02:08 francislavoie

localhost {
         handle_errors {
		respond "Code:{http.error.status_code} StatusText:{http.error.status_text} Message:{http.error.message}"
	}

	reverse_proxy localhost:3000
}

still nothing output, can you test it?

in addition, how can I use http3 only without lowering to http/2?

uaex avatar Aug 20 '21 02:08 uaex

I don't have a build of curl with HTTP/3 for Windows right now. I was able to get Firefox to connect over HTTP/3 one time, but it seems to want to pin itself to HTTP/2 😞

I think the reason handle_errors isn't printing anything is because the error route has a localhost host matcher (see the output of caddy adapt --pretty to see what I mean) when using the Caddyfile, so to get around that I'm trying to use this JSON config with the matcher removed to see if I can get the error to appear:

{
  "logging": {
    "logs": {
      "default": {
        "writer": {
          "output": "stderr"
        },
        "level": "DEBUG"
      }
    }
  },
  "apps": {
    "http": {
      "servers": {
        "srv0": {
          "listen": [
            ":443"
          ],
          "routes": [
            {
              "match": [
                {
                  "host": [
                    "localhost"
                  ]
                }
              ],
              "handle": [
                {
                  "handler": "subroute",
                  "routes": [
                    {
                      "handle": [
                        {
                          "body": "Foo",
                          "handler": "static_response"
                        }
                      ]
                    }
                  ]
                }
              ],
              "terminal": true
            }
          ],
          "errors": {
            "routes": [
              {
                "handle": [
                  {
                    "handler": "subroute",
                    "routes": [
                      {
                        "handle": [
                          {
                            "body": "Code:{http.error.status_code} StatusText:{http.error.status_text} Message:{http.error.message}",
                            "handler": "static_response"
                          }
                        ]
                      }
                    ]
                  }
                ],
                "terminal": true
              }
            ]
          },
          "automatic_https": {
            "disable_redirects": true
          },
          "strict_sni_host": true,
          "experimental_http3": true
        }
      }
    }
  }
}

Run Caddy with caddy run --config caddy.json with the above, and try again. Hopefully that should get further.

francislavoie avatar Aug 20 '21 02:08 francislavoie

😢still nothing output

~ /usr/local/opt/curl/bin/curl -I --http3 https://localhost
HTTP/3 403
server: Caddy
alt-svc: h3=":443"; ma=2592000,h3-34=":443"; ma=2592000,h3-32=":443"; ma=2592000,h3-29=":443"; ma=2592000

uaex avatar Aug 20 '21 03:08 uaex

😢still nothing output

~ /usr/local/opt/curl/bin/curl -I --http3 https://localhost
HTTP/3 403
server: Caddy
alt-svc: h3=":443"; ma=2592000,h3-34=":443"; ma=2592000,h3-32=":443"; ma=2592000,h3-29=":443"; ma=2592000

NEW FOUND:

/usr/local/opt/curl/bin/curl  --http3 https://localhost 
Code:403 StatusText:Forbidden Message:{http.error.message}

uaex avatar Aug 20 '21 03:08 uaex

Alright welp, that wasn't too useful.

I guess next thing to try, if you don't mind making some code changes and building Caddy, you can add this just before the fmt.Errorf line in modules/caddyhttp/server.go to force Caddy to print out that message:

fmt.Printf("strict host matching: TLS ServerName (%s) and HTTP Host (%s) values differ", r.TLS.ServerName, hostname)

Building Caddy is easy, just clone the repo, cd cmd/caddy then run go build. Then run with ./caddy run etc.

francislavoie avatar Aug 20 '21 03:08 francislavoie

Alright welp, that wasn't too useful.

I guess next thing to try, if you don't mind making some code changes and building Caddy, you can add this just before the fmt.Errorf line to force Caddy to print out that message:

fmt.Printf("strict host matching: TLS ServerName (%s) and HTTP Host (%s) values differ", r.TLS.ServerName, hostname)

Building Caddy is easy, just clone the repo, cd cmd/caddy then run go build. Then run with ./caddy run etc.

🤣 I download binary from host directly without sources

uaex avatar Aug 20 '21 03:08 uaex

Interesting, while scanning through our old issues, I found https://github.com/caddyserver/caddy/issues/4097 which is the same issue (strict_sni_host is implicitly enabled when client auth is enabled).

francislavoie avatar Aug 23 '21 06:08 francislavoie

With caddy config

{
  "apps": {
    "http": {
      "servers": {
        "srv0": {
          "strict_sni_host": true,
          "experimental_http3": true,
          "automatic_https": {
            "disable_redirects": true
          },
          "listen": [
            ":443"
          ],
          "logs": {},
          "errors": {
            "routes": [
              {
                "handle": [
                  {
                    "body": "{http.error}",
                    "handler": "static_response"
                  }
                ],
                "terminal": true
              }
            ]
          },
          "routes": [
            {
              "handle": [
                {
                  "body": "hello,world!\n",
                  "handler": "static_response"
                }
              ],
              "match": [
                {
                  "host": [
                    "localhost"
                  ]
                }
              ],
              "terminal": true
            }
          ]
        }
      }
    }
  },
  "logging": {
    "logs": {
      "default": {
        "level": "DEBUG"
      }
    }
  }
}

The response is (client: quic-go)

{id=3vhxga5h9} caddyhttp.(*Server).enforcementHandler (server.go:292): HTTP 403: strict host matching: TLS ServerName () and HTTP Host (localhost) values differ

In caddy log, server_name of the request is also missing

{"request": {"remote_addr": "127.0.0.1:61399", "proto": "HTTP/3", "method": "GET", "host": "localhost:443", "uri": "/", "headers": {"Accept-Encoding": ["gzip"], "User-Agent": ["quic-go HTTP/3"]}, "tls": {"resumed": false, "version": 0, "cipher_suite": 0, "proto": "", "proto_mutual": true, "server_name": ""}}

xiruizhao avatar Aug 23 '21 10:08 xiruizhao

Thanks @xiruizhao! That's helpful. I don't know why SNI would be empty though, that's pretty strange.

@marten-seemann I hate to always call you for help when it's HTTP/3 related :sweat_smile: but does this tell you anything? Could you think of a reason why SNI would be empty for HTTP/3 requests but not for HTTP/2 etc? Is Caddy doing something wrong that would prevent that from being kept?

francislavoie avatar Aug 23 '21 13:08 francislavoie

This is probably due to the fact that we can't fill the tls.ConnectionState on HTTP/3 requests, see https://github.com/lucas-clemente/quic-go/issues/2879. Any idea how to best proceed here?

marten-seemann avatar Aug 23 '21 14:08 marten-seemann

Hmm. Should we just skip strict_sni_host for HTTP/3 then? 🤔

What do you think @mholt?

francislavoie avatar Aug 23 '21 16:08 francislavoie

We should probably not skip it because it's a security feature:

https://github.com/caddyserver/caddy/blob/ce5a45db458166ff71cb776d70fd63e28196aafa/modules/caddyhttp/app.go#L170-L185

But I have no idea what to say otherwise, I don't understand HTTP/3 well enough to suggest anything :disappointed:

francislavoie avatar Aug 23 '21 16:08 francislavoie

The right fix would be to fix crypto/tls, such that one can call ConnectionState() at any time during the handshake. Unfortunately, that's not an easy thing to do.

Alternatively, we could fill in a fake tls.ConnectionState, that at least sets the ServerName.

marten-seemann avatar Aug 23 '21 16:08 marten-seemann

What parts of ConnectionState aren't necessarily ready yet? Are PeerCertificates going to be in there by that point?

The code in question in Caddy is here:

https://github.com/caddyserver/caddy/blob/a056fcd7ba64b6f313f08ec7a48536c726ca432b/modules/caddyhttp/server.go#L280-L299

So clearly r.TLS is set, so there is a ConnectionState but it just seems incomplete?

francislavoie avatar Aug 23 '21 17:08 francislavoie

No, PeerCertificates would be something that we'd need to get from crypto/tls.

marten-seemann avatar Aug 23 '21 17:08 marten-seemann

Now that https://github.com/quic-go/quic-go/pull/3636 is merged and released, we might be able to fix this one.

francislavoie avatar Feb 02 '23 07:02 francislavoie

Actually, I think this is fixed for free (no additional code changes). I can't replicate the problem with the latest version of quic-go. I get successful responses with strict_sni_host turned on (and the Host is correctly matching SNI), whereas it fails on v2.6.2.

francislavoie avatar Feb 04 '23 01:02 francislavoie

Oh that is music to my ears

mholt avatar Feb 04 '23 04:02 mholt

I'm glad to hear that. Making ConnectionState work during the handshake was no fun! (Code: https://github.com/quic-go/qtls-go1-20/commit/352e42f14f1bb2ddc88be1305c6f9d25d49916bd)

marten-seemann avatar Feb 04 '23 04:02 marten-seemann

Thank you so much for your effort, @marten-seemann -- it is very, very much appreciated!

mholt avatar Feb 04 '23 04:02 mholt