-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
caddytls: Encrypted ClientHello (ECH) #6862
base: master
Are you sure you want to change the base?
Conversation
I've added Caddyfile support, for those hoping for an easier way to try it out. (See edited post above with an example.) |
(Sorry if these comments belong in a new issue/forum post instead) I haven't tested this out yet, but it looks like the current implementation only sets the Lines 409 to 418 in ef95642
I'm currently using multiple other keys in my I'm also currently using the Also, Line 413 in ef95642
And this one is maybe better for a new issue, but if Caddy is setting |
@gucci-on-fleek Thanks for the great feedback.
That's good to know. Correct; I guess I first have to
We can do that. When are SVCB RRs to be used versus HTTPS RRs? (Should this be user-configurable or should we just set both?)
Oops, thanks for catching that!
I will look into it... that might be a little trickier. |
(FYI, I'm just a hobbyist, so some of the things that I'm saying here might be wildly incorrect)
The problem with just adding to an existing record is that automation tools like DNSControl, octoDNS, and Terraform DNS generally assume that they “own” any RRs that they set, so if you use them to set an An alternative solution would be to support setting arbitrary key–value pairs from Caddy, but this doesn't quite seem right, since that would be both fairly complicated and completely out-of-scope for a web server. Or another option would be to just manually copy the I actually can't think of any good solutions to this problem, but this is probably a fairly niche use case, so your solution of “
It's not allowed to set a ;; HTTP
_http._tcp.www.example.com. SRV 0 1 80 www.example.com.
; _http.www.example.com. SVCB 1 www.example.com. ( port="80" )
;; HTTPS
_https._tcp.www.example.com. SRV 0 1 443 www.example.com.
; _https.www.example.com. SVCB 1 www.example.com. ( alpn="http/1.1" port="443" )
www.example.com. HTTPS 1 www.example.com.
;; HTTP/3
_https._udp.www.example.com. SRV 0 1 443 www.example.com.
; _https.www.example.com. SVCB 1 www.example.com. ( alpn="h3" port="443" )
www.example.com. HTTPS 1 www.example.com. ( alpn="h3" )
;; HTTPS, alternate port
_https._tcp.alt.example.com. SRV 0 1 8443 alt.example.com.
; _https.alt.example.com. SVCB 1 alt.example.com. ( alpn="http/1.1" port="8443" )
_8443._https.alt.example.com. HTTPS 1 alt.example.com. ( port="8443" )
;; DNS
_dns._udp.ns.example.com. SRV 0 1 53 ns.example.com.
;; DNS-over-TLS
_domain-s._tcp.ns.example.com. SRV 0 1 853 ns.example.com.
_dns.ns.example.com. SVCB 1 ns.example.com. ( alpn="dot" port="853" ) (The commented-out SVCB RRs would behave the same as the HTTPS RRs if they were valid.) Anyways, I'd suggest Caddyfile syntax something like the following # Long form
ech {
# The hostname to use in the ECH wrapper connection.
hostname "ech.example.com" # String, mandatory
# The protocol name to use for the HTTPS/SVCB RR
protocol "https" # String, optional (defaults to "https")
# Port is only needed if you're using a non-default port for that
# specific protocol, and shouldn't be set otherwise.
port 1234 # Integer, optional (defaults to null)
}
# Short form
ech ech.example.com # Maps to `ech { hostname "ech.example.com" }` that behaves something like ech_hostname = <ech.hostname Caddyfile value>
current_hostname = <the hostname for the domain where we're providing ECH>
if protocol == "https" or protocol == null then
rr_type = "HTTPS"
protocol = "https"
elseif protocol == "http" then
-- There seems to be no valid way to set a SVCB record for HTTP, but
-- even if there were, it wouldn't make sense for ECH
throw "Invalid protocol"
else
rr_type = "SVCB"
end
if protocol == "https" and (port == 443 or port == null) then
-- target_hostname is where we'll create the DNS record
target_hostname = current_hostname
elseif port == null then
target_hostname = "_" + protocol + "." + current_hostname
else
target_hostname = "_" + port + "._" + protocol + "." + current_hostname
end But since this only matters if you're using the l4 module, this shouldn't hold up the rest of the ECH support and can easily wait for later.
👍, thanks. This doesn't affect me, but another problem that I thought of is what would happen if you're running two Caddy servers? Right now, you can just set two different Also, thanks for the quick reply to my messages, and thanks for developing Caddy! |
Thanks for the great reply! There's a lot of really helpful information in there.
Yep... I'm aware of those projects (having taken some inspiration from them when making libdns) - and it could be quite a problem. The "ownership" model is in conflict with this default approach. However, this is the default approach, since most people are probably not using DNS ownership tools.
I designed publishing to be modular, so there could be a third-party publisher module written for DNSControl, for example, which instructs DNSControl to publish the record, rather than Caddy publishing it directly. This would delegate control back to the "owner" software and should avoid problems.
I like that a lot. Since this PR is already going to be a lot of work, I might defer some of those advanced customization features for later, as you suggested; but I am almost done with the SvcParams parser that will at least help us avoid trampling existing HTTPS records. |
@gucci-on-fleek Okay, I've pushed a commit that I've tested that will augment existing HTTPS records and only overwrite the |
Alright, I've deployed the latest commit, and I've confirmed with Wireshark that ECH is working as expected. But it only works half of the time since instead of replacing the old $ dig +nostats +nocmd @ns.maxchernoff.ca. www.maxchernoff.ca HTTPS
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 4086
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;www.maxchernoff.ca. IN HTTPS
;; ANSWER SECTION:
www.maxchernoff.ca. 29 IN HTTPS 1 . alpn="h3,h2" ipv4hint=152.53.36.213 ech=AE3+DQBJRQAgACBUJfUrC9bM8kOoM2P22kiTSBcSSllK6HJDlnF/X0AoewAMAAEAAQABAAIAAQADIhJlY2gubWF4Y2hlcm5vZmYuY2EAAA== ipv6hint=2a0a:4cc0:2000:172::1
www.maxchernoff.ca. 29 IN HTTPS 1 . alpn="h3,h2" ipv4hint=152.53.36.213 ipv6hint=2a0a:4cc0:2000:172::1 And even weirder, it only seems to have set the
which probably has something to do with what's happening. I've attached the Caddy debug logs and a dump of the entire DNS zone, but let me know if you need anything else. |
@gucci-on-fleek Hm, that may be a bug in the libdns package you're using (RFC2136 in this case) -- I had to patch the libdns/cloudflare package to properly support HTTPS records, since their Value field is a conglomerate of other fields like Priority and Target. A couple years ago I also had to extend the libdns package to support SRV records for similar reasons. In your case it looks like SetRecords is doing more like what AppendRecords is supposed to do. Most libdns packages were made for ACME transactions which only Append and then Delete records; whereas ECH uses the other two methods, Get and Set. All packages are ideally supposed to implement all 4 interfaces (and from what I've observed, many do.) The cloudflare package properly did implement all 4, but still had to be patched slightly due to HTTPS records' structured Value field. Anyway, that might be something to open a bug report with in libdns/rfc2136. I'd be curious if you have any domains at Cloudflare, whether it works for you. |
Good catch, that definitely seems to be what's happening: https://github.com/libdns/rfc2136/blob/5ee7f48743922d811ad6daf336345ea4bb059eaf/provider.go#L69-L71 I'll try and submit a PR to libdns/rfc2136 to fix that later this week when I have time. |
This is the initial implementation for Encrypted ClientHello (ECH).
I have verified during testing with Firefox + Wireshark that only the outer name(s) appear on the wire in plaintext; the actual server names do not. (Firefox requires DoH enabled.)
Current features:
Still TODO:
Automatically rotate keys(blocked by proposal: crypto/tls: add GetEncryptedClientHelloKeys callback golang/go#71920 -- update: slated for Go 1.25, so deferring to a later PR)Customizable TTL(related to rotating keys)Here's a sample Caddyfile that should be the minimum required to get ECH to work:
(Be sure to replace with your actual values.)
This minimal, opinionated tells Caddy to serve your site,
example.com
, as usual (with automatic HTTPS, a certificate, the whole bit), but to also manage a certain forech.example.net
. This config is opinionated because it is so minimal, in that it enables ECH for all sites, protecting them behind the domain listed in theech
global option. (Each outer name correlates to an ECH config.) It also publishes all ECH configs (one in this case) to all publishers (one DNS publisher, in this case).(The new
dns
global option specifies a DNS provider to use if none other is specified but one is needed. It does not enable the ACME DNS challenge in the way theacme_dns
global option does.)Similarly, here's a sample JSON config with debug logs enabled; be sure to fill out your actual domain names (both inner and outer) and set your DNS provider accordingly. I have a Cloudflare one here since I used it for testing.
This tells Caddy to serve a site (
example.com
) over HTTPS with auto-managing certificates, as usual, and the TLS app has ECH enabled, so it will use the global DNS module (cloudflaer
) to publish the config for the outer name (ech.example.net
).To test this, open Firefox and ensure DNS-over-HTTPS is enabled. Then go to about:networking#dns and "Clear DNS cache" to ensure your test has a clean slate.
When you run this config, wait about 1-2 seconds or for a cert to be provisioned, then, assuming an empty DNS cache, when you load your site (
example.com
) in Firefox, it will find the HTTPS-type DNS record and use that to employ ECH for the connection. Verify with Wireshark.You can compile with this patch and a DNS plugin like so:
All config APIs are subject to change. And no security guarantees at this time. Please try it out!! Thank you!