Podižemo svoj DNS-over-HTTPS poslužitelj

Različiti aspekti korištenja DNS-a već su više puta spominjani od strane autora u nizu članaka objavljenih u okviru bloga. Pri tome, glavni naglasak uvijek je stavljen na povećanje sigurnosti ove ključne usluge za cijeli Internet.

Podižemo svoj DNS-over-HTTPS poslužitelj

Do nedavnog vremena, unatoč očiglednoj ranjivosti DNS prometa koji se, do danas, većim dijelom prenosi u otvorenom obliku, za zlonamjerne radnje s strane pružatelja usluga koji teže povećati svoje prihode kroz ugradnju oglasa u sadržaj, državnih organa i cenzure, kao i jednostavno kriminalaca, proces jačanja njegove zaštite, unatoč postojanju raznih tehnologija kao što su DNSSEC/DANE, DNScrypt, DNS-over-TLS i DNS-over-HTTPS, stagnira. I iako su serverska rješenja, a neka od njih već postoje prilično dugo, široko poznata i dostupna, njihova podrška od strane klijentskog softvera ostavlja mnogo toga da se poželi.

Srećom, situacija se mijenja. Konkretno, programeri popularnog preglednika Firefox su izjavili objavili su planove o uključenju način podrške za DNS-over-HTTPS (DoH) kao zadano u bliskoj budućnosti. To bi trebalo pomoći u zaštiti korisničkog DNS prometa WWW od prethodno spomenutih prijetnji, ali potencijalno može izazvati nove.

1. Problemi DNS-over-HTTPS

Na prvi pogled, masovna implementacija DNS-over-HTTPS u softveru koji radi na Internetu izaziva samo pozitivne reakcije. Međutim, kao što kažu, đavo je u detaljima.

Prvi problem koji ograničava opseg masovne primjene DoH je njegova orijentacija isključivo na web promet. Doista, HTTP protokol i njegova aktualna verzija HTTP/2, na kojoj se temelji DoH, osnova su WWW-a. Ali Internet nije samo web. Postoji mnoštvo popularnih usluga, kao što su e-pošta, razne poruke, sustavi prenosa datoteka, streaming multimeda i drugo, koje ne koriste HTTP. Tako, unatoč tome što mnogi DoH doživljavaju kao panaceju, on se pokazuje neprikladnim bez dodatnih (i nepotrebnih) napora za išta drugo osim tehnologija preglednika. Usput, za ovu ulogu znatno prikladniji je DNS-over-TLS, koji ostvaruje enkapsulaciju standardnog DNS prometa u zaštićeni standardni protokol TLS.

Drugi problem, koji je potencijalno mnogo značajniji od prvog, jeste stvarni odustanak od inherentne DNS decentralizacije u korist korištenja jednog DoH servera navedenog u postavkama preglednika. Konkretno, Mozilla preporučuje korištenje usluge od Cloudflare-a. Sličnu uslugu pokrenuli su i drugi istaknuti internet giganti, uključujući Google. Tako da implementacija DNS-over-HTTPS u obliku kako se sada predlaže, samo povećava zavisnost krajnjih korisnika od najvećih usluga. Nije tajna da informacije koje analize DNS upita mogu prikupiti obezbjeđuju još više podataka o korisnicima, kao i povećati njihovu tačnost i relevantnost.

U tom kontekstu, autor je bio i ostaje zagovornik masovne implementacije ne DNS-over-HTTPS, već DNS-over-TLS zajedno sa DNSSEC/DANE kao univerzalnog, sigurnog sredstva koje ne doprinosi daljoj centralizaciji interneta za osiguranje sigurnosti DNS saobraćaja. Nažalost, ne treba očekivati brzu implementaciju masovne podrške alternativama DoH u klijentskom softveru zbog očiglednih razloga, a trenutno to ostaje samo entuzijastima sigurnih tehnologija.

Ali, sada kada smo dobili DoH, zašto ga ne iskoristiti, odlazeći od potencijalnog nadzora strane korporacija putem njihovih servera na vlastiti DNS-over-HTTPS server?

2. Protokol DNS-over-HTTPS

Ako se pogleda standard RFC8484 koji opisuje protokol DNS-over-HTTPS, može se primijetiti da on, u suštini, predstavlja web API koji omogućava inkapsulaciju standardnog DNS paketa unutar HTTP/2 protokola. Ovo se ostvaruje putem posebnih HTTP zaglavlja, kao i konverzijom binarnog formata prenesenih DNS podataka (vidi RFC1035 i sljedeće dokumente) u oblik koji omogućava prenos i prijem, kao i rad sa potrebnim metapodacima.

Prema standardu, podržava se samo HTTP/2 i zaštićena TLS veza.

Slanje DNS upita može se vršiti standardnim metodama GET i POST. U prvom slučaju, upit se transformiše u base64URL-encoded string, a u drugom — kroz telo POST upita u binarnom obliku. Pri tome se koristi poseban MIME tip podataka za DNS upite i odgovore application/dns-message.

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: " to save to a file.
* Failed writing body (0 != 45)
* stopped the pause stream!
* Connection #0 to host eprove.net left intact

Također obratite pažnju na zaglavlje cache-control: u odgovoru web poslužitelja. U parametru max-age sadrži vrijednost TTL za vraćeni DNS zapis (ili minimalnu vrijednost ako se vraća skup).

Na osnovu onoga što je navedeno, rad DoH poslužitelja sastoji se od nekoliko faza.

  • Primiti HTTP zahtjev. Ako je to GET, dekodirati paket iz base64URL kodiranja.
  • Poslati ovaj paket DNS poslužitelju.
  • Primiti odgovor od DNS poslužitelja
  • Pronaći minimalnu vrijednost TTL u primljenim zapisima.
  • Vratiti klijentu odgovor putem HTTP-a.

3. Vaš DNS-over-HTTPS poslužitelj

Najjednostavniji, najbrži i najefikasniji način za pokretanje vlastitog DNS-over-HTTPS poslužitelja je korištenje HTTP/2 web poslužitelja H2O, o kojem je autor već ukratko pisao (vidi "Visoko performantan web poslužitelj H2O«).

U korist ovog izbora igra činjenica da se cijeli kod vlastitog DoH poslužitelja može potpuno implementirati putem integriranog u H2O interpreter mruby. Osim standardnih biblioteka, za razmjenu podataka s DNS poslužiteljem potrebna je biblioteka (mrbgem) Socket, koja je, srećom, već uključena u trenutnu razvojnu verziju H2O 2.3.0-beta2 prisutnom u portovima FreeBSD-a. Ipak, nije teško dodati je i u svaku prethodnu verziju klonirajući repozitorij biblioteke Socket u katalog /deps prije kompilacije.

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
Klono se u “mruby-socket”…
remote: Enumerating objects: 385, done.
remote: Total 385 (delta 0), reused 0 (delta 0), pack-reused 385
Primanje objekata: 100% (385/385), 98.02 KiB | 647.00 KiB/s, gotovo.
Određivanje promjena: 100% (208/208), gotovo.
root@beta:/usr/ports/www/h2o/work/h2o-2.2.6/deps # ll
total 181
drwxr-xr-x   9 root  wheel  18 12 kol.  16:09 brotli/
drwxr-xr-x   2 root  wheel   4 12 kol.  16:09 cloexec/
drwxr-xr-x   2 root  wheel   5 12 kol.  16:09 golombset/
drwxr-xr-x   4 root  wheel  35 12 kol.  16:09 klib/
drwxr-xr-x   2 root  wheel   5 12 kol.  16:09 libgkc/
drwxr-xr-x   4 root  wheel  26 12 kol.  16:09 libyrmcds/
drwxr-xr-x  13 root  wheel  32 12 kol.  16:09 mruby/
drwxr-xr-x   5 root  wheel  11 12 kol.  16:09 mruby-digest/
drwxr-xr-x   5 root  wheel  10 12 kol.  16:09 mruby-dir/
drwxr-xr-x   5 root  wheel  10 12 kol.  16:09 mruby-env/
drwxr-xr-x   4 root  wheel   9 12 kol.  16:09 mruby-errno/
drwxr-xr-x   5 root  wheel  14 12 kol.  16:09 mruby-file-stat/
drwxr-xr-x   5 root  wheel  10 12 kol.  16:09 mruby-iijson/
drwxr-xr-x   5 root  wheel  11 12 kol.  16:09 mruby-input-stream/
drwxr-xr-x   6 root  wheel  11 12 kol.  16:09 mruby-io/
drwxr-xr-x   5 root  wheel  10 12 kol.  16:09 mruby-onig-regexp/
drwxr-xr-x   4 root  wheel  10 12 kol.  16:09 mruby-pack/
drwxr-xr-x   5 root  wheel  10 12 kol.  16:09 mruby-require/
drwxr-xr-x   6 root  wheel  10 12 ruj. 16:10 mruby-socket/
drwxr-xr-x   2 root  wheel   9 12 kol.  16:09 neverbleed/
drwxr-xr-x   2 root  wheel  13 12 kol.  16:09 picohttpparser/
drwxr-xr-x   2 root  wheel   4 12 kol.  16:09 picotest/
drwxr-xr-x   9 root  wheel  16 12 kol.  16:09 picotls/
drwxr-xr-x   4 root  wheel   8 12 kol.  16:09 ssl-conservatory/
drwxr-xr-x   8 root  wheel  18 12 kol.  16:09 yaml/
drwxr-xr-x   2 root  wheel   8 12 kol.  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
...

Konfiguracija web poslužitelja je, u cjelini, standardna.

root@beta:\/usr\/ports\/www\/h2o #  cd \/usr\/local\/etc\/h2o\/\nroot@beta:\/usr\/local\/etc\/h2o # cat h2o.conf\n# Ova uzorak konfiguracije daje vam osjećaj kako se h2o može koristiti\n# i visoko-zaštićena konfiguracija za TLS i HTTP zaglavlja\n# vidi https:\/\/h2o.examp1e.net\/ za detaljnu dokumentaciju\n# i h2o --help za opcije i postavke naredbenog retka\n\n# v.20180207 (c)2018 Max Kostikov http:\/\/kostikov.co e-mail: max@kostikov.co\n\nuser: www\npid-file: \/var\/run\/h2o.pid\naccess-log:\n    path: \/var\/log\/h2o\/h2o-access.log\n    format: "%h %v %l %u %t \"%r\" %s %b \"%{Referer}i\" \"%{User-agent}i\""\nerror-log: \/var\/log\/h2o\/h2o-error.log\n\nexpires: off\ncompress: on\nfile.dirlisting: off\nfile.send-compressed: on\n\nfile.index: [ 'index.html', 'index.php' ]\n\nlisten:\n    port: 80\nlisten:\n    port: 443\n    ssl:\n        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\n        cipher-preference: server\n        dh-file: \/etc\/ssl\/dhparams.pem\n        certificate-file: \/usr\/local\/etc\/letsencrypt\/live\/eprove.net\/fullchain.pem\n        key-file: \/usr\/local\/etc\/letsencrypt\/live\/my.domain\/privkey.pem\n\nhosts:\n    \"*.my.domain\":\n        paths: &go_tls\n            \"\/\":\n                redirect:\n                    status: 301\n                    url: https:\/\/my.domain\/\n    \"my.domain:80\":\n        paths: *go_tls\n    \"my.domain:443\":\n        header.add: \"Strict-Transport-Security: max-age=15768000; includeSubDomains; preload\"\n        paths:\n            \"\/dns-query\":\n               mruby.handler-file: \/usr\/local\/etc\/h2o\/h2odoh.rb

Izuzetak je samo URL obrađivača /dns-query za koji je odgovoran naš DNS-over-HTTPS server, napisan u mruby i pozvan putem opcije obrađivača mruby.handler-file.

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: max@kostikov.co

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")  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
}

Imajte na umu da lokalni keširajući server upravlja obradom DNS paketa, u ovom slučaju Unbound iz standardne ponude FreeBSD. S aspekta sigurnosti, ovo je optimalno rješenje. Međutim, ništa ne sprječava da se zamijeni localhost s adresom drugog DNS-a kojeg planirate koristiti.

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 unbound-bugs@nlnetlabs.nl
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          *:*

Preostaje da se ponovno pokrene H2O i vidi što je iz svega ovoga proizašlo.

root@beta:⁄usr⁄local⁄etc⁄h2o # service h2o restart
Zaustavljanje h2o.
Čekanje na PID-ove: 69871.
Pokretanje h2o.
start_server (pid:70532) se sada pokreće...

4. Testiranje

Dakle, provjerimo rezultate šaljemo novi probni upit i gledajući mrežni promet pomoću alata tcpdump.

root@beta⁄usr⁄local⁄etc⁄h2o # curl -H 'accept: application⁄dns-message' 'https:⁄⁄my.domain⁄dns-query?dns=q80BAAABAAAAAAAAB2V4YW1wbGUDY29tAAABAAE'
Upozorenje: Binarni ishod može pokvariti vaš terminal. Upotrijebite "--output -" da kažete
Upozorenje: curl da ga ispiše na vaš terminal, ili razmotrite "--output
Upozorenje: " da biste ga spremili u datoteku.
...
root@beta:~ # tcpdump -n -i lo0 udp port 53 -xx -XX -vv
tcpdump: sluša na lo0, tip veze NULL (BSD loopback), veličina snimanja 262144 bajtova
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 paketa uhvaćena
23 paketa primljeno filtriranjem
0 paketa bačeno od strane jezgre

U ishodu se vidi kako je upit za razrješavanje adrese example.com primljen i uspješno obrađen od strane DNS servera.

Sada je potrebno aktivirati naš server u pregledniku Firefox. Da biste to učinili, na stranicama konfiguracije trebate promijeniti nekoliko postavki about:config.

Podižemo svoj DNS-over-HTTPS poslužitelj

Prvo, to je adresa našeg API-a preko kojeg će preglednik upititi DNS informacije na network.trr.uri. Također, preporučuje se da navedete IP domene s ovog URL-a za sigurno razrješavanje u IP putem samog preglednika bez upućivanja na DNS u network.trr.bootstrapAddress. I, konačno, točno taj parametar network.trr.mode koji omogućuje korištenje DoH. Postavljanje vrijednosti na '3' natjerat će preglednik da koristi isključivo DNS-over-HTTPS za razrješavanje imena, dok sigurnija i pouzdana '2' daje prednost DoH ostavljajući standardno upućivanje na DNS kao rezervnu opciju.

5. PROFIT!

Je li članak bio koristan? Onda vas molim da ne oklijevate i podržite nas donacijama preko obrasca (ispod).

Izvor: habr.com

Kupite pouzdan hosting za web stranice s DDoS zaštitom, VPS VDS servere 🔥 Kupite pouzdan hosting za web stranice s DDoS zaštitom, VPS VDS servere | ProHoster