Support TLS SNI on listenTls and serve
We'd like to support SNI on TLS connections for both raw TLS sockets and Deno.serve. The underlying TLS library (https://github.com/denoland/rustls-tokio-stream) supports SNI, but we need to expose this properly.
Here's the plan to make this work:
- [ ] Plan the interface changes required. Likely interface changes are listed below. This method will be marked as
unstablefor the initial version - [ ] Implement a
TlsKeyResolverin Rust with an async function for requesting key lookups and an async function for polling pending lookups:let (cert, key) = await resolver.resolve(sni)andresolver.poll(|sni| async { return (cert, key) }) - [ ] Wrap
Rc<TlsKeyResolver>in a CPPGC struct and hook up thepollfunction in an async op. Call this new op from an async IIFE and delegate the polled request to the JS function:async() => { while (sni = op_tls_poll_sni(resolver)) { let [cert, key] = await resFunc(); op_tls_poll_sni_result(resolver, sni, cert, key); } } - [ ] Provide Rc<TlsKeyResolver> to our TlsListener and call the
resolvemethod in the SNI callback. - [ ] Plumb this infrastructure into
Deno.serve(should be easy with the previous work) - [ ] (Optional) Implement a
Deno.TlsKeyfor optimized TLS key management.
Interface changes
-
Add
TlsCertifiedKeyOptionstolib.deno_net.d.tswith the cert and key properties fromListenTlsOptions. Remove these properties fromListenTlsOptionsand inherit fromTlsServerKeyOptions. -
Change
TlsCertifiedKeyOptionsso that key isstring | Deno.CertifiedKey | Deno.CertifiedKeyResolver. -
Add
Deno.CertifiedKeyobject. The object will not have any properties at this time -- it will just be a simple wrapper object:
const key = new Deno.CertifiedKey(Deno.readTextFileSync("certificate.cer"), Deno.readTextFileSync("private.key"));
- Add
Deno.CertifiedKeyResolverobject. AddresolveCertificateas suggested by @lucacasonato to this object. Note that in the future we may wish to add a system mode to this certificate resolver.
const resolver = new Deno.CertifiedKeyResolver({ resolveCertificate: (name) {
const cert = await (await fetch(`http://certs.local/certs/${name}`)).text();
const key = await (await fetch(`http://certs.local/keys/${name}`)).text();
return new Deno.CertifiedKey(cert, key);
});
+ /** A function to return a certificate for a connection, based on the name
+ * specified in the TLS ClientHello packet (SNI). If the function throws or
+ * returns a rejected promise, the connection with the client will be aborted.
+ *
+ * The results of this function are cached by name, if the returned
+ * certificate and key are valid. Entries in the cache may be discarded at any
+ * time. This function will never be invoked in parallel for the same name.
+ *
+ * Multiple names may return the same certificate and key. */
+ resolveCertificate?(name: string): Promise<Deno.CertifiedKey>;
+ }
References:
https://github.com/denoland/deno/issues/9109 https://github.com/denoland/deno/pull/9125 https://github.com/denoland/deno/pull/20237
Does TLS SNI is what nodejs provides with the servername attribute in the fetch API? If so, I think I need this. I'm working on a local proxy to intercept some traffic, but still pass it through. I do so, by routing a CDN to localhost via /etc/hosts where my proxy runs. Part of my solution is to make an outgoing request to fetch from the destination, but cache it completely locally, but since I reroute my traffic via /etc/hosts, I cannot do this (it's just an example):
console.log(await (await fetch("https://unpkg.com/[email protected]/dist/jquery.js", {
method: "get",
cache: "no-store",
})).text());
but instead I need to do this:
console.log(await (await fetch("https://104.17.249.203/[email protected]/dist/jquery.js", {
method: "get",
cache: "no-store",
headers: {
host: "unpkg.com",
},
})).text());
but it gives me "error trying to connect: invalid peer certificate: NotValidForName". The reason why I try to skip the DNS resolve stage is so I can bypass the /etc/hosts entry, i.e. 127.0.0.1 unpkg.com.
I believe I run into this error due to the lack of the servername prop that would allow me to make an SNI request. Can somebody shed some more light onto this please?
Please note that I cannot change the URL, I literally try to intercept into a client that I cannot modify and I really want to store, cache and even modify everything that gets delivered through to improve performance and provide customizations.
It seems to work in Bruno just fine:
This issue is preventing me from using Gel Cloud, unfortunately.
Hello @mmastrac , please give us an update on the progress of this implementation. Thank you!
This is still on the roadmap, but we can't provide an estimate when this feature will be implemented.
Why did you remove it from the roadmap then?
bartlomieju removed this from the 2.0.0 milestone 28 minutes ago
Because it will not be done in time for Deno 2 release.
These docs are currently false:
Neither are implemented in Deno. Once again I'm forced to go with Node for my next project. I'm forced to abandon Deno until it prioritizes core features like SNI.
It is impossible to use Deno for full multi-domain HTTPS servers until SNI is implemented. You can make small single-domain HTTPS apps in Deno currently. You can only use one certificate.
Just checking in to see if there’s any update on when SNI support might land in a stable release? Super excited about this feature — would love to know if it’s on the roadmap yet. Thank you!