[HN Gopher] Using SNI proxying and IPv6 to share port 443 betwee... ___________________________________________________________________ Using SNI proxying and IPv6 to share port 443 between webapps Author : petercooper Score : 106 points Date : 2022-04-22 10:52 UTC (1 days ago) (HTM) web link (www.agwa.name) (TXT) w3m dump (www.agwa.name) | barbazoo wrote: | Wow this is neat. | | > I have had it with standalone web servers: they're all over- | complicated and I always end up with an awkward bifurcation of | logic between my app's code and the web server's config | | Personally I've grown really fond of letting nginx terminate TLS | and proxy to the web app. It's a clean separation of concerns, | not very complicated and upgrading the cert is easy (certbot). | francislavoie wrote: | FWIW, Caddy can replace nginx and certbot for this purpose, | with less config and more robust ACME. | | Let me know if you need me to clarify, if you have any | concerns. | fffrantz wrote: | Honestly, reverse proxying anything with Caddy has been so | easy that I can't use anything else anymore. Docker | containers are utterly easy to proxy, the defaults (e.g. the | php_fastcgi directive) are sane and mostly work out of the | box, the documentation is great, and everything seems so well | thought out that one has to wonder why we put up so long with | the convolutions of Apache and Nginx. | rekoil wrote: | I take issue with the Caddy 2 admin API. It is easy to | disable, but it's enabled by default(!!!) and requires no | authentication! | | Besides that Caddy is indeed amazing and very well thought | out! | iso1210 wrote: | Caddy looks interesting, I currently use apache to proxy a | few hundered sites and it works well enough, some are | protected by client certificates, others by oidc, all then | pass the authenticated user to the downstream server in a | header, job done. | | I've managed to do this with openresty (nginx not supporting | oidc out of the box), but it doesn't fill me with confidence, | I guess it's all the lua. A quick glance at caddy shows it | likewise doesn't support oidc integration out of the box, but | instead I have to use another module that's no longer | maintained ( https://github.com/thspinto/caddy-oidc ) | francislavoie wrote: | Yeah, we defer to plugins to provide auth solutions, | because it's... a whole thing. It's best maintained outside | of the standard distribution, because there's so many ways | to approach it. | | The caddy-oidc plugin you linked was written for Caddy v1, | so it's no longer compatible. The most complete auth plugin | for Caddy v2 is https://github.com/greenpau/caddy-security, | and I think it probably does what you need. | [deleted] | e12e wrote: | Caddy as a proxy is great - I only whish it was easier to | copy apache-style auth/authz "satisfy any" (eg: whitelist | some ips, require basic auth from others). | | I also expect that as setups age, one is likely to miss | mod_rewrite - but at that point/style of setup, maybe apache | traffic server start to make sense. Or, just apache httpd of | course. | francislavoie wrote: | You can do that easily with the `remote_ip`[0] matcher | (pair it with the `not` matcher to invert the match). For | example to require `basicauth`[1] for all non-private IPv4 | ranges: @needs-auth not remote_ip | 192.168.0.0/16 172.16.0.0/12 10.0.0.0/8 basicauth | @needs-auth { Bob JDJhJDEwJEVCNmdaNEg2Ti5iejRMY | kF3MFZhZ3VtV3E1SzBWZEZ5Q3VWc0tzOEJwZE9TaFlZdEVkZDhX | } | | And for rewrites, there's the `rewrite`[2] handler. Not | sure what you're missing? | | [0] https://caddyserver.com/docs/caddyfile/matchers#remote- | ip | | [1] | https://caddyserver.com/docs/caddyfile/directives/basicauth | | [2] | https://caddyserver.com/docs/caddyfile/directives/rewrite | e12e wrote: | Right, I know it's possible (and thanks for the example) | - I still whish it was easier to specify "only authorized | given conditions x, y, z". | | In the above, access is implied, then revoked without a | valid username/password, then login is excepted for | certain conditions (IPs in this case) - and access is | implied (again). | | IMNHO one of the strengths of apache is how | authentication providers and authorization is separated | and allow for easy(ish) combinations. | | That said, there's something to be said for doubling down | on a single type of handling for access and rewrites | (matchers). | | I still prefer matchers (which resource) combined with | action/policy (allow/deny/rewrite). | francislavoie wrote: | You could also do that, by structuring it like this: | @first <whatever matcher> handle @first { | do_something } @second <whatever | matcher> handle @second { | do_something_else } handle { | error "Unauthorized" 403 } | | Which is basically an if/else-if/else structure. | | I'm not totally sure I follow how you'd like for it to be | structured. Could you give a config example? I think one | of the issues is `basicauth` needs to write a response | header to work correctly (i.e. WWW-Authenticate to tell | the browser to prompt) so it can't only act as a matcher. | zamadatix wrote: | One problem, as a lazy bastard, I've always had with the | Caddy docs is it can sometimes be hard to glance at a | page in the documentation and see "ah, that's how I'd do | <common thing>". | | Take the basicauth portion for example. If you had been | reading the docs like a book, started in the | references/tutorial sections, understood all there is to | know about how request matcher syntax works as a result, | and then read the basicauth page you would have a rock | solid understanding of how to make basicauth do what you | want here. If you land at the basicauth page from a | Google search trying to stand up a quick file-server | weekend project in a way your friends can access you | either are really on top of it and notice "[<matcher>]" | (not even mentioned in the Syntax breakdowns of the page) | is what's used in the single example below and happens to | be a path but might be a lot more or you leave without a | hint of how to do basicauth the way you wanted. It'd be | great if the syntax section breakouts just mentioned | something that triggered more or less a "and hey dumbass, | if you haven't learned how matchers work yet you need to | go do that to fully utilized this directive". | | I realize this is awfully needy, the docs have everything | you need if you read them carefully, and I absolutely | LOVE using Caddy so it's not an attempt to say it's bad | overall by any means. I wanted to point it out though | since this exact example is something I ran into a few | weekends ago. I think the problem is exacerbated by v2 | syntax being new, as well as the competing JSON syntax, | making it harder for people to find use case examples | outside of what's in the official docs. | francislavoie wrote: | Protip: you can click almost everything in code blocks in | the docs. For example, if you click `[<matcher>]`, it | brings you right to the request matcher syntax section, | which explains what you can fill in there. | | It would be redundant to write on every page what you can | use as a matcher. The Caddyfile reference docs assume | you've read | https://caddyserver.com/docs/caddyfile/concepts which | walks you through how the Caddyfile is structured, and | it'll give you the fundamentals you need to understand | the rest of the docs (I think, anyway). | | If you think we need more examples for a specific | usecase, we can definitely include those. Feel free to | propose some changes on | https://github.com/caddyserver/website, we could always | use the help! | zamadatix wrote: | I had a huge facepalm the day I realized all of the | syntax was clickable :). It's not even a uncommon feature | I just hadn't tried clicking it at for some reason! | | Yeah I wouldn't say necessarily every page needs examples | of different matchers and certainly not about what all of | the matcher options are. More a "if the token is shown in | the syntax it should have a bullet in the syntax section" | kind of approach which in the case of [<matcher>] could | be as plain and short as "[<matcher>] is a token which | allows you to control the scope a directive applies to. | For details on how see Request Matchers." to raise the | "you probably want to know more about this first" flag to | anyone that just jumped in from Google or what have you | to go read about matchers before trying to understand the | directive. | | If that makes any sense I'd be glad to raise it more | formally over on the GitHub! | mholt wrote: | Really appreciate this feedback and the positive | attitude. This is helpful, thank you! | twic wrote: | Some lazy questions ... | | > Since IPv6 addresses are 128 bits long, but IPv4 addresses are | only 32 bits, it's possible to embed IPv4 addresses in IPv6 | addresses. snid embeds the client's IP address in the lower 32 | bits of the source address which it uses to connect to the | backend. | | How does this work? snid just makes up an IP address? What socket | API calls do you make to do this? Just pick an address and bind, | and the kernel is fine with that? And it all gets routed back and | forth correctly? Do you have to configure this 64:ff9b:1::/48 | prefix on the loopback interface? | | > Encrypted Client Hello doesn't actually encrypt the initial | Client Hello message. It's still sent in the clear, but with a | decoy SNI hostname. The actual Client Hello message, with the | true SNI hostname, is encrypted and placed in an extension of the | unencrypted Client Hello. To make Encrypted Client Hello work | with snid, I just need to ensure that the decoy SNI hostname | resolves to the IPv6 address of the backend server. snid will see | this hostname and route the connection to the correct backend | server, as usual. | | How does the decoy SNI hostname get chosen? This sounds like | there needs to be a different decoy hostname for each backend | service. Does that come from DNS somehow? The client doesn't just | make it up at random? | agwa wrote: | Great questions! | | > How does this work? snid just makes up an IP address? What | socket API calls do you make to do this? Just pick an address | and bind, and the kernel is fine with that? And it all gets | routed back and forth correctly? Do you have to configure this | 64:ff9b:1::/48 prefix on the loopback interface? | | First you have to set the IP_FREEBIND socket option, which | allows binding to nonlocal/nonexistent IP address and then you | call bind with whatever address you like. To ensure the packets | get routed back properly, you need a local route for the | 64:ff9b:1::/96 prefix, which can be added with: | | ip route add local 64:ff9b:1::/96 dev lo | | > How does the decoy SNI hostname get chosen? This sounds like | there needs to be a different decoy hostname for each backend | service. Does that come from DNS somehow? The client doesn't | just make it up at random? | | The decoy hostname is specified in the ECHConfig struct[1], | which is conveyed to the client via DNS in the HTTPS record[2]. | | It does indeed mean that each backend needs its own decoy | hostname (which resolves to the IPv6 address of the backend). | This means that ECH does not hide which backend is being | connected to, but if a particular backend handles multiple | hostnames, it can hide which of those hostnames the client is | connecting to. | | [1] https://www.ietf.org/archive/id/draft-ietf-tls- | esni-14.html#... | | [2] https://www.ietf.org/archive/id/draft-ietf-dnsop-svcb- | https-... | zokier wrote: | If you have at least a /96 to dedicate for snid, then | couldn't you just use that public prefix instead of 64:ff9b | to encapsulate ipv4 address, making the setup somewhat | simpler? Also if you used public prefix then I imagine you | could even run this setup over the internet, i.e. have snid | run on some public server with dualstack and forward | connections to ipv6-only app servers. I'm imagining the | common situation where you have cgnat ipv4 + native ipv6 at | home, you could host snid on public cloud instance to expose | services running at home. | agwa wrote: | Yup, that would work. Nice idea! | tedunangst wrote: | Oops, draft-ietf-dnsop-svcb-https-08 has expired. | peter_retief wrote: | Great idea, I have been thinking of doing something similar but | am struggling to get ISP's to support Ipv6. Some actually block | Ipv6. | jraph wrote: | I have a server that hosts several websites. I wanted some of | them to be installed in a separate (systemd) container (because | they belong to the same organization). | | I use nginx's ssl_preread module to proxy https requests to the | container or to another port depending on the SNI. This is what | snid does in the article if I understood correctly (without the | DNS lookup because I don't need it, but it is able to do it too). | It works well and it's good that the nginx at the front does not | need to have the SSL certificates. In this setup, Nginx does not | need to decode anything, it just does a pass-through, so this is | quite light. It is also way simpler to setup than an actual HTTP | reverse proxy. | klysm wrote: | Well I guess I really have to learn IPv6 now | scott00 wrote: | I don't get how this works with ECH. Can anybody add some detail | to what's in the article? | agwa wrote: | Since there are several questions about Encrypted Client Hello | (ECH), and I kind of hand waved that section, I thought an | example might be useful. | | Let's say the system is running two web server daemons: a multi- | tenant blog hosting platform listening on 2001:db8::1, and a | multi-tenant bug tracker listening on 2001:db8::2. snid is on | 192.0.2.1. Your DNS records would look like this: | blogs.example.com. A 192.0.2.1 blogs.example.com. AAAA | 2001:db8::1 bugs.example.com. A 192.0.2.1 | bugs.example.com. AAAA 2001:db8::2 | | The various tenants would be CNAMEd to one of these hostnames | like: blog.domain1.example. CNAME | blogs.example.com. bugs.domain2.example. CNAME | bugs.example.com. | | The "decoy" hostnames (the "public_name" in ECH parlance) would | be blogs.example.com or bugs.example.com. Thus, ECH would hide | which tenant the client is connecting to, but would not hide the | service. Note that if the client were connecting over IPv6, an | eavesdropper would be able to determine the service anyways by | looking at the destination IP address, which is unencrypted. | gz5 wrote: | Nice solution. You can got a step further if you have the need | - your eavesdropper or malicious observer problem can be | addressed by launching the network connections from inside the | process space of your app, e.g. for golang: | https://github.com/openziti/sdk-golang | | Similarly, this eliminates the IP address dependencies. | | Sample (Java in this case - see GitHub above for various | language options): | https://blogs.oracle.com/javamagazine/post/java-zero-trust-o... | bscphil wrote: | Would it be accurate to summarize this way? | | TLS + ECH encrypts all message content, including the client | hello with the hostname, but it does _not_ encrypt a specific | message (an IP address + service identifier combination) that | uniquely specifies which program will terminate the TLS | connection. | | If you want information to be private about the host name | you're connecting to, you need not only a single public IP | address for many hosts, but also for that server to terminate | TLS for all of those hosts. | agwa wrote: | That's an accurate summary of how TLS+ECH would work with | snid. | | More generally, I don't think it's required for a single | server to terminate TLS for all hosts. If an SNI proxy server | knew the private key necessary for decrypting the ECH | extension, it could look inside it to determine where to | proxy the connection, without having to terminate TLS. | | If snid worked this way, the unencrypted SNI hostname | wouldn't need to identify the backend, which means that | clients connecting over IPv4 would have more privacy. But | snid would have to coordinate the ECH encryption key with the | backends, which would add a lot of complexity, and IPv6 | clients wouldn't benefit in any case. | karmanyaahm wrote: | This is such a neat solution. | | As part of my effort to single-stack-v6 and minimum-effort-v4, | for my self hosted services and projects, I've always wanted a | way to avoid reverse proxy configuration for each hosted app; v6 | can directly listen to a new address. | GauntletWizard wrote: | I'm a huge fan of this approach, but I would also combine it | almost equally with standard http reverse proxies; there's a lot | you can gain from having a proxy that can understand paths,buffer | requests, etc. | thayne wrote: | > To make Encrypted Client Hello work with snid, I just need to | ensure that the decoy SNI hostname resolves to the IPv6 address | of the backend server. | | Doesn't that sort of defeat the purpose of ECH? | mholt wrote: | Nice, this is kind of why I made Project Conncept. It's a | powerful TCP and UDP stream multiplexer based on Caddy: | https://github.com/mholt/caddy-l4 | | You can route raw TCP connections by using higher layer protocol | matching logic like HTTP properties, SSH, TLS ClientHello info, | and more, in composable routes that let you do nearly anything. | rasengan wrote: | Thank you for this and Caddy. | mholt wrote: | You're welcome! Thanks for the nice comment. :) We have many | contributors and several maintainers to thank as well. | ignoramous wrote: | Neat. Kind of like a highly configurable | https://github.com/inetaf/tcpproxy | | > _You can route raw TCP connections by using higher layer | protocol matching logic like HTTP properties, SSH, TLS | ClientHello info, and more, in composable routes that let you | do nearly anything._ | | How do you foresee such a setup handle QUIC? The encrypted | connection-ids, 0RTT handshakes, and roaming client-ip and | server-ips make it non trivial to proxy connections | transparently. | mholt wrote: | Good question; I'm not really sure! Will need to look into | it, or have people contribute some ideas. Feel free to start | a discussion on the issue tracker if you're interested in | this! | derefr wrote: | > Meanwhile, my preferred language, Go, has a high-quality, | memory-safe HTTPS server in the standard library that is well | suited for direct exposure on the Internet. | | I know people do _use_ Golang 's http.Server for production use- | cases. Does Google, though? Are services of Google customers ever | actually directly talking to this Golang stack, without at least | a minimal L7 WAF (as e.g. a default Nginx config does) in | between? | | I ask because there are a number of weird connection latency, | slowness, and "stuttering" problems I've experienced with | services which I know _do_ directly expose Golang servers -- e.g. | Docker Registry instances including Docker Hub; Minio instances; | go-ethereum nodes; etc. -- that I 've never experienced with any | Google service, or with any known non-Golang service. | | My hypothesis is that this is due to Golang's http.Server not | having any upper limit on simultaneous connections (because just | in CPU and memory terms, the Golang runtime can handle almost | arbitrarily many), such that eventually the bottleneck actually | becomes per-connection throughput, with clients becoming starved | for space in the machine's network ring buffer; and because this | is such an unusual bottleneck to have (usually it's only a thing | with CDNs) -- and because it causes no problems for _the server_ | , esp. if things like readiness checks are done through a | separate internal NIC -- the people running these servers don't | even notice it's happening, and so lag far behind in horizontally | scaling servers to spread out the demand for throughput. | | Or, to put that another way: the Golang http.Server isn't | _observable_ -- exposing server-internal metrics -- in the way | that actual web servers like Nginx, or even web-app server | frameworks like Jetty, are; and so it 's very hard to know when | things are silently going wrong for users, esp. in cases where | the developers of a piece of software aren't themselves running | it at scale and so never think to manually add observability for | metrics that only become relevant at scale (which authors of | generic web server software are usually aware of, but authors of | application software usually aren't.) This leads me to think | that, if Google themselves _are_ using Golang services for | anything at scale, and yet not rushing to implement such metrics | into http.Server, then they must be observing these services in a | very different way than we mere mortals do. Maybe calculating | per-flow packet-wise QoS at the edge in their fancy LANai | switches using historical statistical fingerprints of predictable | flow patterns, or something. | merb wrote: | you can use opentelemetry to get traces and metrics for golang | net/http, but since I'm not using it I have no idea what | metrics are already supported. | | btw. google does use golang services at scale (I'm not a | googler) but they probably do it like they do it with appengine | and only limit a certain amount of req/s per service | finnh wrote: | Seems like eBPF could be useful here as well, to get some | external insight into per-connection counts and behavior. | tedunangst wrote: | I wrote a web server that uses go http but not http.serve for | this reason. I wanted more control over accept and close. Nice | thing is the http library is decently composed, so you can take | all the parts you want and build up. | dcow wrote: | If the _decoy_ hostnames (as the author describes it) used in | encrypted SNI are deterministic such that you can statically | determine the real hostname the client wants, then what's the | point of encrypted SNI in the first place? | agwa wrote: | ECH can't hide which backend the client wants, but if a | particular backend handles multiple hostnames, it can hide | which of those hostnames the client wants. I went into further | detail here: https://news.ycombinator.com/item?id=31136335 | zokier wrote: | Clever. I think I'd prefer to do TLS termination at the proxy | though, something akin to stunnel. Of course snid could be used | together with stunnel, but I think it would lose the O(1) | configuration then. Just terminating tls and not touching the | http content would still avoid any of those http parsing issues | mentioned. | agwa wrote: | An earlier version did the TLS termination in the proxy. It's | true you can avoid the HTTP parsing issues, but you lose the | ability to do client certs or have backend-specific cipher/TLS | version requirements. Also, I really like it that IPv6 clients | can connect directly to the backend, bypassing any proxies. | justsomehnguy wrote: | Previous disc: https://news.ycombinator.com/item?id=31040667 | anderspitman wrote: | This is great. I think SNI is currently one of the most pragmatic | tools to work around ipv4 exhaustion. | | I like the approach described here, but in practice I prefer the | convenience of having a reverse proxy to automatically handle TLS | certs for me. That said, libraries like certmagic are making it | more feasible for every app to manage its own certs. | ignoramous wrote: | > _I think SNI is currently one of the most pragmatic tools to | work around ipv4 exhaustion._ | | Pretty much: | https://research.cloudflare.com/publications/Fayed2021/ | | See also, DNS _SVC_ ( _A_ / _B_ ) records (pseudo NAT at DNS | layer); but not many deployments use it or understand it. Note | that, SNI as a routing replacement works for TCP nicely without | much (user-space) complication. With QUIC, _transparently_ | proxying connections isn 't all that straight forward. ___________________________________________________________________ (page generated 2022-04-23 23:00 UTC)