<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet href="/feeds.xsl" type="text/xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:base="https://chameth.com/">
    <title>Chameth.com - posts like http2-and-sni but not debugging-beyond-the-debugger, docker-automatic-nginx-proxy</title>
    <subtitle>Personal homepage of Chris Smith</subtitle>
    <link href="https://chameth.com/feeds/posts/like/http2-and-sni/unlike/debugging-beyond-the-debugger,docker-automatic-nginx-proxy/" rel="self"/>
    <link href="https://chameth.com/"/>
    <icon>https://chameth.com/favicon.png</icon>
    <updated>2024-05-25T00:00:00Z</updated>
    <id>https://chameth.com/</id>
    <author>
        <name>Chris Smith</name>
    </author>
    <entry>
        <title>HTTP/2 and TLS Server Name Indication</title>
        <link href="https://chameth.com/http2-and-sni/"/>
        <updated>2024-05-25T00:00:00Z</updated>
        <id>https://chameth.com/http2-and-sni/</id>
        <content xml:lang="en" type="html">&lt;p&gt;I was recently alerted to a bug in &lt;a href=&#34;https://github.com/csmith/centauri&#34;&gt;Centauri&lt;/a&gt;,
a simple reverse proxy I wrote. The initial report was that it was
serving completely the wrong website, but only sometimes, and it behaved
differently in different browsers, and no-one else could reproduce it.&lt;/p&gt;
&lt;p&gt;I use Centauri for all of my web-facing services (including this site!) so
it’s a little surprising such a major bug would have escaped my notice.
&lt;a href=&#34;https://dataforce.org.uk&#34;&gt;Shane&lt;/a&gt;, who first noticed the bug, was persistent
though and eventually managed to figure out some exact reproduction steps.&lt;/p&gt;
&lt;!--more--&gt;
&lt;h3 id=&#34;a-brief-overview-of-centauri-and-sni&#34;&gt;A brief overview of Centauri and SNI&lt;/h3&gt;
&lt;p&gt;Centauri originally only proxied HTTPS requests&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:1&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;. When it receives a HTTPS
request, it first looks at the Server Name Indication (or SNI) field in the
TLS &lt;code&gt;ClientHello&lt;/code&gt; message. It uses this field to determine which TLS certificate
to respond with (as one Centauri instance will typically serve many websites
across many domain names, each with their own certificate). That’s what the
field exists for: before SNI, if you wanted to host two HTTPS sites on the
same machine you’d need separate IP addresses for them!&lt;/p&gt;
&lt;p&gt;Once the TLS session was established Centauri would read in the HTTP request,
select which backend it was going to be sent to based on the SNI field, and then
proxy it on. The HTTP request itself contains a &lt;code&gt;Host&lt;/code&gt; header which identifies
which host the request is for, but that will always be the same as the SNI
field… or so I thought.&lt;/p&gt;
&lt;h3 id=&#34;http-connection-reuse&#34;&gt;HTTP connection reuse&lt;/h3&gt;
&lt;p&gt;When accessing a website, your browser will request dozens of resources in a
short space of time: the webpage itself, some stylesheets, maybe some scripts,
plus any images, fonts, videos, etc. It would be extremely inefficient to open
a new connection for each individual request, as setting up the connection
requires several round trips between the client and the server.&lt;/p&gt;
&lt;p&gt;To address this issue, HTTP/1.1 formalised the idea of “persistent connections”,
which allow the client to keep a connection open and send another request once
the first has completed. HTTP/2 takes this a step much further and allows full
multiplexing — sending multiple requests at once and allowing the server to
respond out-of-order.&lt;/p&gt;
&lt;p&gt;Obviously, you can only reuse the connection if you’re requesting further
resources from the same host: if your browser makes a request to &lt;code&gt;example.com&lt;/code&gt;
and that includes a script from &lt;code&gt;example.net&lt;/code&gt;, it has to open a new connection
for the other domain. However, HTTP/2 expands this slightly:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;For “https” resources, connection reuse additionally depends on &lt;br/&gt;
having a certificate that is valid for the host in the URI.  The &lt;br/&gt;
certificate presented by the server MUST satisfy any checks that the &lt;br/&gt;
client would perform when forming a new TLS connection for the host &lt;br/&gt;
in the URI.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;putting-it-all-together&#34;&gt;Putting it all together&lt;/h3&gt;
&lt;p&gt;The reproduction steps that Shane figured out involved visiting sites hosted
on two subdomains. The first site to be visited got “stuck” and subsequent
requests to the other site were routed there instead. This only worked for
one specific domain, though, and it turns out because that domain was configured
in Centauri to use a wildcard TLS certificate (i.e., the certificate served
for the request to the first site was also valid for the second site).&lt;/p&gt;
&lt;p&gt;The certificate being valid for both sites allowed the browser to use the same
connection. This breaks my assumption that the SNI field would always match the
HTTP host, as all requests are sent over the same TLS connection that had the
SNI field set to the first site’s subdomain. While perfectly in spec, the
behaviour is quite surprising.&lt;/p&gt;
&lt;p&gt;The fix for this was trivial: Centauri now checks the HTTP Host header instead
of routing based on the SNI field. I found the bug itself interesting though,
as it has such an awkward set of conditions for it to occur:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;There must be multiple sites that share a certificate (the default behaviour
in Centauri is to obtain one certificate per site)&lt;/li&gt;
&lt;li&gt;A user must visit two of those sites&lt;/li&gt;
&lt;li&gt;The browser must still have a connection open to the first site when visiting
the second&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;It’s also one of those rare bugs where everything is working as intended, it’s
just that the intention was slightly wrong for some reason. In this case it was
because I wasn’t aware of the fairly significant shift in behaviour introduced
in HTTP/2 for that one tiny part of the spec&lt;sup id=&#34;fnref:2&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:2&#34; role=&#34;doc-noteref&#34;&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;Thanks again to &lt;a href=&#34;https://dataforce.org.uk&#34;&gt;Shane&lt;/a&gt; for the debugging he did to
figure this all out!&lt;/p&gt;
&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;
&lt;hr/&gt;
&lt;ol&gt;
&lt;li id=&#34;fn:1&#34;&gt;
&lt;p&gt;It now also proxies HTTP requests but only if they come over a Tailscale
connection. Otherwise, plain HTTP requests are redirected to HTTPS. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:1&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:2&#34;&gt;
&lt;p&gt;I think it’s this kind of thing that drives software devs to become
carpenters or farmers. You don’t suddenly get a Door 2.0 specification that
invalidates all your assumptions about how hinges work when certain people try
to open it. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:2&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
</content>
    </entry>
</feed>
