Vi höjer vår DNS-over-HTTPS-server

Olika aspekter av DNS-drift har redan vid upprepade tillfällen berörts av författaren i ett antal artiklar publiceras som en del av bloggen. Samtidigt har huvudvikten alltid legat på att förbättra säkerheten för denna viktiga Internettjänst.

Vi höjer vår DNS-over-HTTPS-server

Tills nyligen, trots den uppenbara sårbarheten hos DNS-trafik, som fortfarande till största delen överförs i det klara, till skadliga handlingar från leverantörers sida som försöker öka sina inkomster genom att bädda in reklam i innehåll, statliga säkerhetsbyråer och censur, såväl som helt enkelt kriminella, processen stärka dess skydd, trots närvaron av olika tekniker som DNSSEC/DANE, DNScrypt, DNS-over-TLS och DNS-over-HTTPS, stannade. Och om serverlösningar, och några av dem har funnits ganska länge, är allmänt kända och tillgängliga, lämnar deras stöd från klientprogramvara mycket övrigt att önska.

Lyckligtvis håller situationen på att förändras. I synnerhet utvecklarna av den populära webbläsaren Firefox uppgav om planer på att aktivera supportläge som standard DNS-över-HTTPS (DoH) snart. Detta bör hjälpa till att skydda WWW-användarens DNS-trafik från ovanstående hot, men kan potentiellt introducera nya.

1. DNS-over-HTTPS-problem

Vid första anblicken orsakar den första massintroduktionen av DNS-over-HTTPS i Internetprogramvaran bara en positiv reaktion. Men djävulen, som de säger, ligger i detaljerna.

Det första problemet som begränsar omfattningen av DoH:s utbredda användning är dess fokus enbart på webbtrafik. Faktum är att HTTP-protokollet och dess nuvarande version HTTP/2, som DoH är baserat på, är grunden för WWW. Men Internet är inte bara webben. Det finns många populära tjänster, som e-post, olika snabbmeddelanden, filöverföringssystem, multimediastreaming, etc. som inte använder HTTP. Så trots mångas uppfattning om DoH som ett universalmedel, visar det sig vara otillämpligt utan ytterligare (och onödiga) ansträngningar för något annat än webbläsarteknik. Förresten, DNS-over-TLS ser ut som en mycket mer värdig kandidat för denna roll, som implementerar inkapslingen av standard DNS-trafik i det säkra standard-TLS-protokollet.

Det andra problemet, som potentiellt är mycket mer betydande än det första, är det faktiska övergivandet av den inneboende decentraliseringen av DNS genom design till förmån för att använda en enda DoH-server som anges i webbläsarinställningarna. I synnerhet föreslår Mozilla att du använder en tjänst från Cloudflare. En liknande tjänst lanserades också av andra framstående internetfigurer, särskilt Google. Det visar sig att implementeringen av DNS-over-HTTPS i den form som det för närvarande föreslås bara ökar slutanvändarnas beroende av de största tjänsterna. Det är ingen hemlighet att informationen som analys av DNS-frågor kan ge kan samla in ännu mer data om den, samt öka dess noggrannhet och relevans.

I detta avseende var och förblir författaren en anhängare av massimplementeringen, inte av DNS-over-HTTPS, utan av DNS-over-TLS tillsammans med DNSSEC/DANE som ett universellt, säkert och inte gynnsamt för ytterligare centralisering av Internet. för att säkerställa säkerheten för DNS-trafik. Tyvärr kan man av uppenbara skäl inte förvänta sig ett snabbt införande av massstöd för DoH-alternativ i klientprogramvaran, och det är fortfarande en domän för säkerhetsteknikentusiaster.

Men eftersom vi nu har DoH, varför inte använda det efter att ha undkommit potentiell övervakning av företag genom deras servrar till vår egen DNS-over-HTTPS-server?

2. DNS-over-HTTPS-protokoll

Om man ser till standarden RFC8484 När du beskriver DNS-over-HTTPS-protokollet kan du se att det faktiskt är ett webb-API som låter dig kapsla in ett standard-DNS-paket i HTTP/2-protokollet. Detta implementeras genom speciella HTTP-rubriker, såväl som konvertering av det binära formatet för överförda DNS-data (se. RFC1035 och efterföljande dokument) i ett formulär som låter dig överföra och ta emot dem, samt arbeta med nödvändig metadata.

Enligt standarden stöds endast HTTP/2 och en säker TLS-anslutning.

Att skicka en DNS-förfrågan kan göras med standardmetoderna GET och POST. I det första fallet omvandlas begäran till en base64URL-kodad sträng, och i det andra genom kroppen av POST-begäran i binär form. I det här fallet används en speciell MIME-datatyp under DNS-förfrågan och -svar applikation/dns-meddelande.

root@eprove:~ # curl -H 'accept: application/dns-message' 'https://my.domaint/dns-query?dns=q80BAAABAAAAAAAAB2V4YW1wbGUDY29tAAABAAE' -v
*   Trying 2001:100:200:300::400:443...
* TCP_NODELAY set
* Connected to eprove.net (2001:100:200:300::400) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: /usr/local/share/certs/ca-root-nss.crt
  CApath: none
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* ALPN, server accepted to use h2
* Server certificate:
*  subject: CN=my.domain
*  start date: Jul 22 00:07:13 2019 GMT
*  expire date: Oct 20 00:07:13 2019 GMT
*  subjectAltName: host "my.domain" matched cert's "my.domain"
*  issuer: C=US; O=Let's Encrypt; CN=Let's Encrypt Authority X3
*  SSL certificate verify ok.
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x801441000)
> GET /dns-query?dns=q80BAAABAAAAAAAAB2V4YW1wbGUDY29tAAABAAE HTTP/2
> Host: eprove.net
> User-Agent: curl/7.65.3
> accept: application/dns-message
>
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* Connection state changed (MAX_CONCURRENT_STREAMS == 100)!
< HTTP/2 200
< server: h2o/2.3.0-beta2
< content-type: application/dns-message
< cache-control: max-age=86274
< date: Thu, 12 Sep 2019 13:07:25 GMT
< strict-transport-security: max-age=15768000; includeSubDomains; preload
< content-length: 45
<
Warning: Binary output can mess up your terminal. Use "--output -" to tell
Warning: curl to output it to your terminal anyway, or consider "--output
Warning: <FILE>" to save to a file.
* Failed writing body (0 != 45)
* stopped the pause stream!
* Connection #0 to host eprove.net left intact

Var också uppmärksam på titeln cache-kontroll: i svaret från webbservern. I parametern max-ålder innehåller TTL-värdet för DNS-posten som returneras (eller minimivärdet om en uppsättning av dem returneras).

Baserat på ovanstående består funktionen av en DoH-server av flera steg.

  • Ta emot en HTTP-förfrågan. Om detta är en GET så avkoda paketet från base64URL-kodning.
  • Skicka detta paket till DNS-servern.
  • Få ett svar från DNS-servern
  • Hitta det lägsta TTL-värdet i de mottagna posterna.
  • Returnera ett svar till klienten via HTTP.

3. Din egen DNS-over-HTTPS-server

Det enklaste, snabbaste och mest effektiva sättet att köra din egen DNS-over-HTTPS-server är att använda en HTTP/2-webbserver H2O, som författaren redan har skrivit kort om (se "Högpresterande H2O-webbserver").

Detta val stöds av det faktum att all kod för din egen DoH-server kan implementeras fullt ut med hjälp av tolken integrerad i själva H2O mruby. Förutom standardbibliotek, för att utbyta data med DNS-servern, behöver du (mrbgem) Socket-biblioteket, som lyckligtvis redan ingår i den nuvarande utvecklingsversionen av H2O 2.3.0-beta2 närvarande i FreeBSD-portar. Det är dock inte svårt att lägga till den i någon tidigare version genom att klona förvaret Socket bibliotek till katalogen /deps före sammanställning.

root@beta:~ # uname -v
FreeBSD 12.0-RELEASE-p10 GENERIC
root@beta:~ # cd /usr/ports/www/h2o
root@beta:/usr/ports/www/h2o # make extract
===>  License MIT BSD2CLAUSE accepted by the user
===>   h2o-2.2.6 depends on file: /usr/local/sbin/pkg - found
===> Fetching all distfiles required by h2o-2.2.6 for building
===>  Extracting for h2o-2.2.6.
=> SHA256 Checksum OK for h2o-h2o-v2.2.6_GH0.tar.gz.
===>   h2o-2.2.6 depends on file: /usr/local/bin/ruby26 - found
root@beta:/usr/ports/www/h2o # cd work/h2o-2.2.6/deps/
root@beta:/usr/ports/www/h2o/work/h2o-2.2.6/deps # git clone https://github.com/iij/mruby-socket.git
Клонирование в «mruby-socket»…
remote: Enumerating objects: 385, done.
remote: Total 385 (delta 0), reused 0 (delta 0), pack-reused 385
Получение объектов: 100% (385/385), 98.02 KiB | 647.00 KiB/s, готово.
Определение изменений: 100% (208/208), готово.
root@beta:/usr/ports/www/h2o/work/h2o-2.2.6/deps # ll
total 181
drwxr-xr-x   9 root  wheel  18 12 авг.  16:09 brotli/
drwxr-xr-x   2 root  wheel   4 12 авг.  16:09 cloexec/
drwxr-xr-x   2 root  wheel   5 12 авг.  16:09 golombset/
drwxr-xr-x   4 root  wheel  35 12 авг.  16:09 klib/
drwxr-xr-x   2 root  wheel   5 12 авг.  16:09 libgkc/
drwxr-xr-x   4 root  wheel  26 12 авг.  16:09 libyrmcds/
drwxr-xr-x  13 root  wheel  32 12 авг.  16:09 mruby/
drwxr-xr-x   5 root  wheel  11 12 авг.  16:09 mruby-digest/
drwxr-xr-x   5 root  wheel  10 12 авг.  16:09 mruby-dir/
drwxr-xr-x   5 root  wheel  10 12 авг.  16:09 mruby-env/
drwxr-xr-x   4 root  wheel   9 12 авг.  16:09 mruby-errno/
drwxr-xr-x   5 root  wheel  14 12 авг.  16:09 mruby-file-stat/
drwxr-xr-x   5 root  wheel  10 12 авг.  16:09 mruby-iijson/
drwxr-xr-x   5 root  wheel  11 12 авг.  16:09 mruby-input-stream/
drwxr-xr-x   6 root  wheel  11 12 авг.  16:09 mruby-io/
drwxr-xr-x   5 root  wheel  10 12 авг.  16:09 mruby-onig-regexp/
drwxr-xr-x   4 root  wheel  10 12 авг.  16:09 mruby-pack/
drwxr-xr-x   5 root  wheel  10 12 авг.  16:09 mruby-require/
drwxr-xr-x   6 root  wheel  10 12 сент. 16:10 mruby-socket/
drwxr-xr-x   2 root  wheel   9 12 авг.  16:09 neverbleed/
drwxr-xr-x   2 root  wheel  13 12 авг.  16:09 picohttpparser/
drwxr-xr-x   2 root  wheel   4 12 авг.  16:09 picotest/
drwxr-xr-x   9 root  wheel  16 12 авг.  16:09 picotls/
drwxr-xr-x   4 root  wheel   8 12 авг.  16:09 ssl-conservatory/
drwxr-xr-x   8 root  wheel  18 12 авг.  16:09 yaml/
drwxr-xr-x   2 root  wheel   8 12 авг.  16:09 yoml/
root@beta:/usr/ports/www/h2o/work/h2o-2.2.6/deps # cd ../../..
root@beta:/usr/ports/www/h2o # make install clean
...

Webbserverkonfigurationen är i allmänhet standard.

root@beta:/usr/ports/www/h2o #  cd /usr/local/etc/h2o/
root@beta:/usr/local/etc/h2o # cat h2o.conf
# this sample config gives you a feel for how h2o can be used
# and a high-security configuration for TLS and HTTP headers
# see https://h2o.examp1e.net/ for detailed documentation
# and h2o --help for command-line options and settings

# v.20180207 (c)2018 by Max Kostikov http://kostikov.co e-mail: [email protected]

user: www
pid-file: /var/run/h2o.pid
access-log:
    path: /var/log/h2o/h2o-access.log
    format: "%h %v %l %u %t "%r" %s %b "%{Referer}i" "%{User-agent}i""
error-log: /var/log/h2o/h2o-error.log

expires: off
compress: on
file.dirlisting: off
file.send-compressed: on

file.index: [ 'index.html', 'index.php' ]

listen:
    port: 80
listen:
    port: 443
    ssl:
        cipher-suite: ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS
        cipher-preference: server
        dh-file: /etc/ssl/dhparams.pem
        certificate-file: /usr/local/etc/letsencrypt/live/eprove.net/fullchain.pem
        key-file: /usr/local/etc/letsencrypt/live/my.domain/privkey.pem

hosts:
    "*.my.domain":
        paths: &go_tls
            "/":
                redirect:
                    status: 301
                    url: https://my.domain/
    "my.domain:80":
        paths: *go_tls
    "my.domain:443":
        header.add: "Strict-Transport-Security: max-age=15768000; includeSubDomains; preload"
        paths:
            "/dns-query":
               mruby.handler-file: /usr/local/etc/h2o/h2odoh.rb

Det enda undantaget är URL-hanteraren / dns-fråga som vår DNS-over-HTTPS-server, skriven i mruby och anropad via hanteraralternativet, faktiskt är ansvarig för mruby.handler-fil.

root@beta:/usr/local/etc/h2o # cat h2odoh.rb
# H2O HTTP/2 web server as DNS-over-HTTP service
# v.20190908 (c)2018-2019 Max Kostikov https://kostikov.co e-mail: [email protected]

proc {|env|
    if env['HTTP_ACCEPT'] == "application/dns-message"
        case env['REQUEST_METHOD']
            when "GET"
                req = env['QUERY_STRING'].gsub(/^dns=/,'')
                # base64URL decode
                req = req.tr("-_", "+/")
                if !req.end_with?("=") && req.length % 4 != 0
                    req = req.ljust((req.length + 3) & ~3, "=")
                end
                req = req.unpack1("m")
            when "POST"
                req = env['rack.input'].read
            else
                req = ""
        end
        if req.empty?
            [400, { 'content-type' => 'text/plain' }, [ "Bad Request" ]]
        else
            # --- ask DNS server
            sock = UDPSocket.new
            sock.connect("localhost", 53)
            sock.send(req, 0)
            str = sock.recv(4096)
            sock.close
            # --- find lowest TTL in response
            nans = str[6, 2].unpack1('n') # number of answers
            if nans > 0 # no DNS failure
                shift = 12
                ttl = 0
                while nans > 0
                    # process domain name compression
                    if str[shift].unpack1("C") < 192
                        shift = str.index("x00", shift) + 5
                        if ttl == 0 # skip question section
                            next
                        end
                    end
                    shift += 6
                    curttl = str[shift, 4].unpack1('N')
                    shift += str[shift + 4, 2].unpack1('n') + 6 # responce data size
                    if ttl == 0 or ttl > curttl
                        ttl = curttl
                    end
                    nans -= 1
                 end
                 cc = 'max-age=' + ttl.to_s
            else
                 cc = 'no-cache'
            end
            [200, { 'content-type' => 'application/dns-message', 'content-length' => str.size, 'cache-control' => cc }, [ str ] ]
        end
    else
        [415, { 'content-type' => 'text/plain' }, [ "Unsupported Media Type" ]]
    end
}

Observera att den lokala cacheservern är ansvarig för att bearbeta DNS-paket, i detta fall Obundet från den vanliga FreeBSD-distributionen. Ur säkerhetssynpunkt är detta den optimala lösningen. Inget hindrar dig dock från att byta ut lokalvärd till en annan DNS-adress som du tänker använda.

root@beta:/usr/local/etc/h2o # local-unbound verison
usage:  local-unbound [options]
        start unbound daemon DNS resolver.
-h      this help
-c file config file to read instead of /var/unbound/unbound.conf
        file format is described in unbound.conf(5).
-d      do not fork into the background.
-p      do not create a pidfile.
-v      verbose (more times to increase verbosity)
Version 1.8.1
linked libs: mini-event internal (it uses select), OpenSSL 1.1.1a-freebsd  20 Nov 2018
linked modules: dns64 respip validator iterator
BSD licensed, see LICENSE in source package for details.
Report bugs to [email protected]
root@eprove:/usr/local/etc/h2o # sockstat -46 | grep unbound
unbound  local-unbo 69749 3  udp6   ::1:53                *:*
unbound  local-unbo 69749 4  tcp6   ::1:53                *:*
unbound  local-unbo 69749 5  udp4   127.0.0.1:53          *:*
unbound  local-unbo 69749 6  tcp4   127.0.0.1:53          *:*

Allt som återstår är att starta om H2O och se vad som kommer av det.

root@beta:/usr/local/etc/h2o # service h2o restart
Stopping h2o.
Waiting for PIDS: 69871.
Starting h2o.
start_server (pid:70532) starting now...

4. Testning

Så låt oss kontrollera resultaten genom att skicka en testförfrågan igen och titta på nätverkstrafiken med hjälp av verktyget tcpdump.

root@beta/usr/local/etc/h2o # curl -H 'accept: application/dns-message' 'https://my.domain/dns-query?dns=q80BAAABAAAAAAAAB2V4YW1wbGUDY29tAAABAAE'
Warning: Binary output can mess up your terminal. Use "--output -" to tell
Warning: curl to output it to your terminal anyway, or consider "--output
Warning: <FILE>" to save to a file.
...
root@beta:~ # tcpdump -n -i lo0 udp port 53 -xx -XX -vv
tcpdump: listening on lo0, link-type NULL (BSD loopback), capture size 262144 bytes
16:32:40.420831 IP (tos 0x0, ttl 64, id 37575, offset 0, flags [none], proto UDP (17), length 57, bad cksum 0 (->e9ea)!)
    127.0.0.1.21070 > 127.0.0.1.53: [bad udp cksum 0xfe38 -> 0x33e3!] 43981+ A? example.com. (29)
        0x0000:  0200 0000 4500 0039 92c7 0000 4011 0000  ....E..9....@...
        0x0010:  7f00 0001 7f00 0001 524e 0035 0025 fe38  ........RN.5.%.8
        0x0020:  abcd 0100 0001 0000 0000 0000 0765 7861  .............exa
        0x0030:  6d70 6c65 0363 6f6d 0000 0100 01         mple.com.....
16:32:40.796507 IP (tos 0x0, ttl 64, id 37590, offset 0, flags [none], proto UDP (17), length 73, bad cksum 0 (->e9cb)!)
    127.0.0.1.53 > 127.0.0.1.21070: [bad udp cksum 0xfe48 -> 0x43fa!] 43981 q: A? example.com. 1/0/0 example.com. A 93.184.216.34 (45)
        0x0000:  0200 0000 4500 0049 92d6 0000 4011 0000  ....E..I....@...
        0x0010:  7f00 0001 7f00 0001 0035 524e 0035 fe48  .........5RN.5.H
        0x0020:  abcd 8180 0001 0001 0000 0000 0765 7861  .............exa
        0x0030:  6d70 6c65 0363 6f6d 0000 0100 01c0 0c00  mple.com........
        0x0040:  0100 0100 0151 8000 045d b8d8 22         .....Q...].."
^C
2 packets captured
23 packets received by filter
0 packets dropped by kernel

Utdata visar hur begäran om att lösa adressen example.com togs emot och bearbetades framgångsrikt av DNS-servern.

Nu återstår bara att aktivera vår server i webbläsaren Firefox. För att göra detta måste du ändra flera inställningar på konfigurationssidorna about: config.

Vi höjer vår DNS-over-HTTPS-server

För det första är detta adressen till vårt API där webbläsaren kommer att begära DNS-information network.trr.uri. Det rekommenderas också att ange domänens IP från denna URL för säker IP-upplösning med hjälp av webbläsaren själv utan att komma åt DNS i network.trr.bootstrapAddress. Och slutligen själva parametern nätverk.trr.läge inklusive användningen av DoH. Om du ställer in värdet på "3" tvingas webbläsaren att enbart använda DNS-över-HTTPS för namnupplösning, medan den mer pålitliga och säkra "2" kommer att prioritera DoH, vilket lämnar standard DNS-sökning som ett reservalternativ.

5. VINST!

Var artikeln till hjälp? Var då inte blyg och stöd med pengar genom donationsformuläret (nedan).

Källa: will.com

Lägg en kommentar