Encrypted SNI
mrbluecoat opened this issue · 19 comments
Any options for dealing with encrypted SNI? https://blog.cloudflare.com/encrypted-sni/
@mrbluecoat thanks for your report
Encrypted SNI can not be decrypted by Man-in-the-middle.
I changed the code to drop the connection when encrypted SNI is detected (TLS extension 65486). Things gonna change as Encrypted SNI standard is a draft.
Tested with firefox 70. Cloudfare DoH + ESNI via about:config :
- network.security.esni.enabled true
- network.trr.mode 3
- network.trr.custom_uri https://1.1.1.1/dns-query
- network.trr.uri https://1.1.1.1/dns-query
Test page : https://cloudflare.com/cdn-cgi/trace
Unfortunately, firefox does not fallback to unencrypted SNI and the connection is dropped.
I wait for encrypted SNI to be a standard to support encrypted SNI in a better way
Thanks for the update. Dropping Cloudflare traffic for hosts using ESNI is a tolerable stopgap but is there any long-term solution for monitoring home/work traffic with ESNI? Perhaps client cert approach like mitmproxy?
key to encrypt SNI is provided by DNS.
In Cloudflare current implementation of the draft RFC, the key is obtained by requesting a DNS TXT entry of the root domain name prefixed by _esni.
dig _esni.cloudflare.com TXT
; <<>> DiG 9.10.3-P4-Debian <<>> _esni.cloudflare.com TXT
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 42475
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 13, ADDITIONAL: 27
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;_esni.cloudflare.com. IN TXT
;; ANSWER SECTION:
_esni.cloudflare.com. 3600 IN TXT "/wFF7do4ACQAHQAgWcWTew6M6glGwqxZ2wjf7xpok65Xu9FkJsrrHCJ+G1MAAhMBAQQAAAAAXc5pEAAAAABd1lIQAAA="
Current draft says that a ESNI DNS type is used instead of TXT.
Since ESNI key is provided by DNS, if you control the DNS, you can deny client from receiving the SNI key so that the browser does not encrypt the SNI
It seems bind has no option to block DNS request type but unbound has
https://serverfault.com/questions/744613/block-any-request-in-bind
It's worth looking at unbound
Very promising! Thanks for your research, I really appreciate it. I'm working on an embedded system with only 512MB of memory so I wonder if unbound or knot-resolver would be lightweight enough. I'll look into them and FTL/dnsmasq: https://github.com/pi-hole/FTL/blob/master/dnsmasq_interface.c#L47
I'm not an iptables expert so I'm not sure if a variation of this would help: https://serverfault.com/a/843805
I would not use iptables for filtering DNS type query. I presume it would take too much ressource.
Filtering by the server should be more performant. To be tested with unbound
policy.add(function (req, query)
if query.stype == kres.type.ANY then
return policy.DROP
elseif query.stype == kres.type.ESNI then
return policy.DROP
end
end)
If you have a procedure to install unbound, please share it
Using https://docs.pi-hole.net/guides/unbound/ as a guide:
sudo apt install -y unbound dnsutils
wget -O root.hints https://www.internic.net/domain/named.root && sudo mv root.hints /var/lib/unbound/
cat >> /etc/unbound/unbound.conf.d/custom.conf <<EOL
server:
logfile: "/var/log/unbound/unbound.log"
verbosity: 3
port: 53
do-ip4: yes
do-ip6: no
do-udp: yes
do-tcp: yes
root-hints: "/var/lib/unbound/root.hints"
harden-glue: yes
harden-dnssec-stripped: yes
use-caps-for-id: no
edns-buffer-size: 1472
prefetch: yes
num-threads: 1
so-rcvbuf: 1m
private-address: 192.168.0.0/16
private-address: 169.254.0.0/16
private-address: 172.16.0.0/12
private-address: 10.0.0.0/8
private-address: fd00::/8
private-address: fe80::/10
EOL
sudo service unbound start
dig sigfail.verteiltesysteme.net @127.0.0.1 -p 53 | grep SERVFAIL
dig sigok.verteiltesysteme.net @127.0.0.1 -p 53 | grep NOERROR
I haven't found an example to block type ESNI
with unbound (the policy example above is for Knot). Another potential option is to clone https://coredns.io/plugins/any/ for ESNI.
Another relevant CoreDNS reference: https://coredns.io/plugins/rewrite/
I'm slowly making progress on this. I've been able to install and configure Knot-Resolver to confirm the proposed approach above works.
Install the latest Knot-Resolver:
wget https://secure.nic.cz/files/knot-resolver/knot-resolver-release.deb
dpkg -i knot-resolver-release.deb
apt update
apt install -y knot-resolver knot-dnsutils lua-cqueues
Configure Knot-Resolver:
cat > /etc/knot-resolver/kresd.conf <<EOF
-- Knot DNS Resolver configuration in Lua
verbose(true)
-- Enable modules
modules = {
'policy',
'view',
'hints',
'serve_stale < cache',
'workarounds < iterate',
'stats',
'predict',
'prefill'
}
-- Disable IPv6
net.ipv6 = false
-- Switch to unprivileged user --
user('knot-resolver','knot-resolver')
-- Set the size of the cache to 150 MB
cache.size = 150 * MB
-- Accept all requests from these subnets
view:addr('127.0.0.1/8', function (req, qry) return policy.PASS end)
view:addr('10.0.0.0/8', function (req, qry) return policy.PASS end)
view:addr('172.16.0.0/12', function (req, qry) return policy.PASS end)
view:addr('169.254.0.0/16', function (req, qry) return policy.PASS end)
view:addr('192.168.0.1/16', function (req, qry) return policy.PASS end)
-- Drop everything that hasn't matched
view:addr('0.0.0.0/0', function (req, qry) return policy.DROP end)
-- Prevent ESNI
policy.add(policy.pattern(policy.DENY, '\5_esni'))
policy.add(function (req, query)
if query.stype == kres.type.ANY then
return policy.DROP
elseif query.stype == kres.type.ESNI then
return policy.DROP
end
end)
-- DNSSEC validation enabled by default in v4+ (no config needed)
-- Root hints
hints.root_file = '/usr/share/dns/root.hints'
-- Daily refresh
prefill.config({
['.'] = {
url = 'https://www.internic.net/domain/root.zone',
ca_file = '/etc/ssl/certs/ca-certificates.crt',
interval = 86400 -- seconds
}
})
-- Forward queries to CleanBrowsing via DNS-over-TLS (DoT)
policy.add(policy.all(policy.TLS_FORWARD({
{'185.228.168.168', hostname='family-filter-dns.cleanbrowsing.org'},
{'185.228.169.168', hostname='family-filter-dns.cleanbrowsing.org'}
})))
-- Prefetch learning (20-minute blocks over 24 hours)
predict.config({ window = 20, period = 72 })
EOF
Start Knot-Resolver on a couple CPU cores:
systemctl enable --now kresd@{1..2}.service
Run basic tests:
kdig google.com | grep -q NOERROR && echo DNS test 1/2: PASS || echo DNS test 1/2: FAIL
kdig blah.google.com | grep -q NXDOMAIN && echo DNS test 2/2: PASS || echo DNS test 2/2: FAIL
kdig sigok.verteiltesysteme.net +dnssec | grep -q NOERROR && echo DNSSEC test 1/2: PASS || echo DNS test 1/2: FAIL
kdig sigfail.verteiltesysteme.net +dnssec | grep -q SERVFAIL && echo DNSSEC test 2/2: PASS || echo DNS test 2/2: FAIL
kdig -d @185.228.168.168 +tls-ca +tls-host=family-filter-dns.cleanbrowsing.org example.com | grep -q trusted && echo DoT test: PASS || echo DoT test: FAIL
Prerequisites for ESNI test: compile ESNI-enabled OpenSSL and curl
Run ESNI test:
cd $HOME/code/curl
./curl-esni https://www.cloudflare.com/cdn-cgi/trace 2> /dev/null | grep -q sni=plaintext && echo ESNI test: PASS || echo ESNI test: FAIL
P.S. I confirmed your Firefox testing. It only works as desired if network.trr.mode
is set to 0 or 5
0: Off by default
1: Firefox will choose based on which is faster
2: TRR preferred, fall back to DNS on failure
3: TRR only, no DNS fallback
5: TRR completely disabled
Example bypass:
network.security.esni.enabled true
network.trr.mode 3
network.trr.bootstrapAddress 104.16.249.249
Further testing will be needed to see if we can IP spoof the common public DNS endpoint IPs via #6 to force DNS resolution via our local knot-resolver install that blocks ESNI. Not a perfect solution since the list of those IPs will be constantly growing but it's a start at least and will capture most DNS circumnavigation attempts.
Filtering TLS packet with ESNI field is not a good approach in long term because there will be no fallback to plaintext SNI in case connection with ESNI does not work. It would result in fitering legitimate website, which we do not want to.
I think the best approach would be to sync with the local DNS to get the ESNI entry in DNS request in addition to A/AAAA entries that associate domain name to IP address. Keep in cache couple of IP address / computed ESNI for domain name so that we can take the decision whether to filter ESNI in middle as we do currently with plaintext SNI
But if the browser bypasses DNS entirely (like the Firefox example above) how will syncing with the local DNS server help us?
We should maintain a blacklist of DoH DNS so that browser fallbacks to the DNS set in OS by DHCP
I see. Something like https://github.com/bambenek/block-doh
Another list: https://github.com/curl/curl/wiki/DNS-over-HTTPS