Kami meningkatkan server DNS-over-HTTPS kami

Berbagai aspek pengoperasian DNS telah berulang kali disinggung oleh penulis di beberapa artikel artikel diterbitkan sebagai bagian dari blog. Pada saat yang sama, penekanan utama selalu pada peningkatan keamanan layanan Internet utama ini.

Kami meningkatkan server DNS-over-HTTPS kami

Hingga baru-baru ini, meskipun terdapat kerentanan nyata dari lalu lintas DNS, yang sebagian besar masih terlihat jelas, akibat tindakan jahat dari pihak penyedia yang berupaya meningkatkan pendapatan mereka dengan memasang iklan di konten, lembaga keamanan pemerintah, dan sensor, serta penjahat saja, prosesnya memperkuat perlindungannya, meskipun kehadiran berbagai teknologi seperti DNSSEC/DANE, DNScrypt, DNS-over-TLS dan DNS-over-HTTPS, terhenti. Dan jika solusi server, dan beberapa di antaranya sudah ada sejak lama, dikenal dan tersedia secara luas, maka dukungannya dari perangkat lunak klien masih menyisakan banyak hal yang diinginkan.

Untungnya, situasinya sedang berubah. Secara khusus, para pengembang browser Firefox yang populer аявили tentang rencana untuk mengaktifkan mode dukungan secara default DNS-over-HTTPS (DoH) segera. Hal ini akan membantu melindungi lalu lintas DNS pengguna WWW dari ancaman di atas, namun berpotensi menimbulkan ancaman baru.

1. Masalah DNS-over-HTTPS

Sekilas, awal pengenalan massal DNS-over-HTTPS ke dalam perangkat lunak Internet hanya menimbulkan reaksi positif. Namun, iblis, seperti yang mereka katakan, ada dalam detailnya.

Masalah pertama yang membatasi cakupan penggunaan DoH secara luas adalah fokusnya hanya pada lalu lintas web. Memang benar, protokol HTTP dan versi saat ini HTTP/2, yang menjadi dasar DoH, adalah dasar dari WWW. Namun Internet bukan hanya web. Ada banyak layanan populer, seperti email, berbagai pesan instan, sistem transfer file, streaming multimedia, dll, yang tidak menggunakan HTTP. Oleh karena itu, meskipun banyak orang menganggap DoH sebagai obat mujarab, hal ini ternyata tidak dapat diterapkan tanpa upaya tambahan (dan tidak perlu) untuk hal lain selain teknologi browser. Omong-omong, DNS-over-TLS sepertinya kandidat yang jauh lebih layak untuk peran ini, yang mengimplementasikan enkapsulasi lalu lintas DNS standar dalam protokol TLS standar yang aman.

Masalah kedua, yang berpotensi jauh lebih signifikan daripada yang pertama, adalah pengabaian desentralisasi bawaan DNS demi penggunaan server DoH tunggal yang ditentukan dalam pengaturan browser. Secara khusus, Mozilla menyarankan untuk menggunakan layanan dari Cloudflare. Layanan serupa juga diluncurkan oleh tokoh internet terkemuka lainnya, khususnya Google. Ternyata penerapan DNS-over-HTTPS dalam bentuk yang diusulkan saat ini hanya meningkatkan ketergantungan pengguna akhir pada layanan terbesar. Bukan rahasia lagi bahwa informasi yang dapat diberikan oleh analisis kueri DNS dapat mengumpulkan lebih banyak data tentangnya, serta meningkatkan keakuratan dan relevansinya.

Dalam hal ini, penulis adalah dan tetap menjadi pendukung penerapan massal bukan DNS-over-HTTPS, tetapi DNS-over-TLS bersama dengan DNSSEC/DANE sebagai sarana universal, aman dan tidak kondusif untuk sentralisasi lebih lanjut dari sarana Internet. untuk memastikan keamanan lalu lintas DNS. Sayangnya, karena alasan yang jelas, kita tidak dapat mengharapkan dukungan massal terhadap alternatif DoH secara cepat ke dalam perangkat lunak klien, dan hal ini masih menjadi domain para penggemar teknologi keamanan.

Namun karena kita sekarang memiliki DoH, mengapa tidak menggunakannya setelah lolos dari potensi pengawasan perusahaan melalui server mereka ke server DNS-over-HTTPS kita sendiri?

2. Protokol DNS-over-HTTPS

Jika Anda melihat standarnya RFC8484 menjelaskan protokol DNS-over-HTTPS, Anda dapat melihat bahwa sebenarnya ini adalah API web yang memungkinkan Anda merangkum paket DNS standar dalam protokol HTTP/2. Ini diimplementasikan melalui header HTTP khusus, serta konversi format biner data DNS yang dikirimkan (lihat. RFC1035 dan dokumen selanjutnya) ke dalam bentuk yang memungkinkan Anda mengirim dan menerimanya, serta bekerja dengan metadata yang diperlukan.

Menurut standar, hanya HTTP/2 dan koneksi TLS aman yang didukung.

Mengirim permintaan DNS dapat dilakukan menggunakan metode GET dan POST standar. Dalam kasus pertama, permintaan diubah menjadi string yang dikodekan base64URL, dan yang kedua, melalui isi permintaan POST dalam bentuk biner. Dalam hal ini, tipe data MIME khusus digunakan selama permintaan dan respons DNS aplikasi/pesan-dns.

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

Perhatikan juga judulnya kontrol cache: sebagai respons dari server web. Dalam parameternya usia maksimal berisi nilai TTL untuk data DNS yang dikembalikan (atau nilai minimum jika sekumpulan data tersebut dikembalikan).

Berdasarkan hal di atas, fungsi server DoH terdiri dari beberapa tahap.

  • Terima permintaan HTTP. Jika ini adalah GET maka dekode paket dari pengkodean base64URL.
  • Kirim paket ini ke server DNS.
  • Dapatkan respons dari server DNS
  • Temukan nilai TTL minimum dalam catatan yang diterima.
  • Kembalikan respons ke klien melalui HTTP.

3. Server DNS-over-HTTPS Anda sendiri

Cara paling sederhana, tercepat dan paling efektif untuk menjalankan server DNS-over-HTTPS Anda sendiri adalah dengan menggunakan server web HTTP/2 H2O, yang telah ditulis secara singkat oleh penulis (lihat β€œServer Web H2O Kinerja Tinggi").

Pilihan ini didukung oleh fakta bahwa semua kode server DoH Anda dapat diimplementasikan sepenuhnya menggunakan penerjemah yang terintegrasi ke dalam H2O itu sendiri. mruby. Selain pustaka standar, untuk bertukar data dengan server DNS, Anda memerlukan pustaka Socket (mrbgem), yang untungnya sudah disertakan dalam versi pengembangan H2O 2.3.0-beta2 saat ini hadiah di port FreeBSD. Namun, tidak sulit untuk menambahkannya ke versi sebelumnya dengan mengkloning repositori Perpustakaan soket ke katalog /deps sebelum kompilasi.

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
...

Konfigurasi server web umumnya standar.

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

Satu-satunya pengecualian adalah penangan URL / dns-query yang mana server DNS-over-HTTPS kami, yang ditulis dalam mruby dan dipanggil melalui opsi handler, sebenarnya bertanggung jawab 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: [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
}

Harap dicatat bahwa server caching lokal bertanggung jawab untuk memproses paket DNS, dalam hal ini Tidak terikat dari distribusi FreeBSD standar. Dari sudut pandang keamanan, ini adalah solusi optimal. Namun, tidak ada yang menghalangi Anda untuk menggantinya localhost ke alamat DNS lain yang ingin Anda gunakan.

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

Yang tersisa hanyalah memulai ulang H2O dan melihat hasilnya.

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

4. Pengujian

Jadi, mari kita periksa hasilnya dengan mengirimkan permintaan pengujian lagi dan melihat lalu lintas jaringan menggunakan utilitas tersebut 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

Outputnya menunjukkan bagaimana permintaan untuk menyelesaikan alamat example.com diterima dan berhasil diproses oleh server DNS.

Sekarang tinggal mengaktifkan server kami di browser Firefox. Untuk melakukan ini, Anda perlu mengubah beberapa pengaturan pada halaman konfigurasi about: config.

Kami meningkatkan server DNS-over-HTTPS kami

Pertama, ini adalah alamat API kami di mana browser akan meminta informasi DNS jaringan.trr.uri. Disarankan juga untuk menentukan IP domain dari URL ini untuk resolusi IP aman menggunakan browser itu sendiri tanpa mengakses DNS masuk jaringan.trr.bootstrapAddress. Dan terakhir, parameter itu sendiri jaringan.trr.mode termasuk penggunaan DoH. Menetapkan nilai ke "3" akan memaksa browser untuk menggunakan DNS-over-HTTPS secara eksklusif untuk resolusi nama, sedangkan "2" yang lebih andal dan aman akan memberikan prioritas pada DoH, meninggalkan pencarian DNS standar sebagai opsi cadangan.

5. KEUNTUNGAN!

Apakah artikelnya bermanfaat? Maka jangan malu-malu dan dukung dengan uang melalui formulir donasi (di bawah).

Sumber: www.habr.com

Tambah komentar