Skip to content
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

Encrypted SNI #4

Open
mrbluecoat opened this issue Nov 14, 2019 · 19 comments
Open

Encrypted SNI #4

mrbluecoat opened this issue Nov 14, 2019 · 19 comments

Comments

@mrbluecoat
Copy link

Any options for dealing with encrypted SNI? https://blog.cloudflare.com/encrypted-sni/

@luigi1809
Copy link
Owner

@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 :

Test page : https://cloudflare.com/cdn-cgi/trace

Unfortunately, firefox does not fallback to unencrypted SNI and the connection is dropped.

@luigi1809
Copy link
Owner

d7d5fcf

@luigi1809 luigi1809 reopened this Nov 17, 2019
@luigi1809
Copy link
Owner

I wait for encrypted SNI to be a standard to support encrypted SNI in a better way

@mrbluecoat
Copy link
Author

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?

@luigi1809
Copy link
Owner

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

@luigi1809
Copy link
Owner

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

@mrbluecoat
Copy link
Author

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

@mrbluecoat
Copy link
Author

I'm not an iptables expert so I'm not sure if a variation of this would help: https://serverfault.com/a/843805

@luigi1809
Copy link
Owner

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

@mrbluecoat
Copy link
Author

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

@mrbluecoat
Copy link
Author

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.

@mrbluecoat
Copy link
Author

Another relevant CoreDNS reference: https://coredns.io/plugins/rewrite/

@mrbluecoat
Copy link
Author

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

@mrbluecoat
Copy link
Author

mrbluecoat commented Dec 8, 2019

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.

@luigi1809
Copy link
Owner

luigi1809 commented Dec 17, 2019

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

@mrbluecoat
Copy link
Author

But if the browser bypasses DNS entirely (like the Firefox example above) how will syncing with the local DNS server help us?

@luigi1809
Copy link
Owner

We should maintain a blacklist of DoH DNS so that browser fallbacks to the DNS set in OS by DHCP

@mrbluecoat
Copy link
Author

@mrbluecoat
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants