Lakukan sendiri DNS proksi di Node.JS

Paket di atas gundukan di hutan jauh untuk DNS ...
L. Kaganov "Dusun di bagian bawah"

Saat mengembangkan aplikasi jaringan, terkadang menjadi perlu untuk menjalankannya secara lokal, tetapi mengaksesnya menggunakan nama domain asli. Solusi standar terbukti adalah mendaftarkan domain di file host. The minus dari pendekatan adalah bahwa host memerlukan korespondensi yang jelas dari nama domain, yaitu tidak mendukung bintang. Yaitu jika ada domain formulir:


dom1.example.com, dom2.example.com, dom3.example.com, ................ domN.example.com, 

maka pada host Anda harus mendaftarkan semuanya. Dalam beberapa kasus, domain level ketiga tidak diketahui sebelumnya. Ada keinginan (saya menulis untuk diri sendiri, seseorang mungkin mengatakan bahwa itu normal) untuk bertahan dengan garis seperti ini:


 *.example.com 

Solusi untuk masalah ini mungkin menggunakan server DNS Anda sendiri, yang akan memproses permintaan sesuai dengan logika yang ditentukan. Ada beberapa server, baik yang benar-benar gratis dan dengan antarmuka grafis yang nyaman, seperti CoreDNS . Anda juga dapat mengubah catatan DNS di router. Akhirnya, gunakan layanan seperti xip.io , ini bukan server DNS lengkap, tetapi sangat cocok untuk beberapa tugas. Singkatnya, ada solusi siap pakai, Anda dapat menggunakan dan tidak repot.


Tetapi artikel ini menjelaskan cara lain - menulis sepeda Anda sendiri, titik awal untuk membuat alat seperti yang tercantum di atas. Kami akan menulis proksi DNS kami, yang akan mendengarkan permintaan DNS yang masuk, dan jika nama domain yang diminta ada dalam daftar, itu akan mengembalikan IP yang ditentukan, dan jika tidak, ia akan meminta server DNS yang lebih tinggi dan meneruskan tanggapan yang diterima tanpa perubahan pada program yang meminta.


Pada saat yang sama, Anda dapat mencatat permintaan dan tanggapan yang diterima. Karena DNS diperlukan oleh semua orang - browser, messenger, dan antivirus, dan layanan sistem operasi, dll., Itu bisa sangat informatif.


Prinsipnya sederhana. Dalam pengaturan koneksi jaringan untuk protokol IPv4, kami mengubah alamat server DNS ke alamat mesin dengan menjalankan proksi DNS yang ditulis sendiri (127.0.0.1, jika kami bekerja di luar jaringan), dan dalam pengaturannya kami menentukan alamat server DNS yang lebih tinggi. Dan, sepertinya, itu saja!


Kami tidak akan menggunakan fungsi standar untuk menyelesaikan nslookup dan nsresolve nama domain, sehingga pengaturan sistem DNS dan konten file host tidak akan mempengaruhi operasi program. Bergantung pada situasinya, mungkin bermanfaat atau tidak, Anda hanya perlu mengingatnya. Untuk kesederhanaan, kami membatasi diri pada implementasi fungsi dasar itu sendiri:


  • IP spoofing hanya untuk catatan tipe A (alamat host) dan kelas IN (Internet)
  • alamat IP palsu hanya versi 4
  • koneksi untuk permintaan masuk lokal melalui UDP saja
  • koneksi ke server DNS hulu melalui UDP atau TLS
  • jika ada beberapa antarmuka jaringan, permintaan lokal yang masuk akan diterima pada salah satu dari mereka
  • tidak ada dukungan EDNS

Bicara soal tes

Ada beberapa tes Unit dalam proyek ini. Benar, mereka bekerja sesuai dengan prinsip: Saya meluncurkannya, dan jika sesuatu yang waras ditampilkan di konsol, maka semuanya baik-baik saja, tetapi jika ada pengecualian, maka ada masalah. Tetapi bahkan pendekatan kikuk seperti itu memungkinkan Anda untuk berhasil melokalisasi masalah, begitu Unit.


Mulai - server pada port 53


Mari kita mulai. Pertama-tama, Anda perlu mengajarkan aplikasi untuk menerima permintaan DNS yang masuk. Kami sedang menulis server TCP sederhana yang hanya mendengarkan port 53 dan mencatat koneksi masuk. Di properti koneksi jaringan, tentukan alamat server DNS 127.0.0.1, buka aplikasi, buka browser untuk beberapa halaman - dan ... diam di konsol, browser menampilkan halaman secara normal. Yah, kita ubah TCP ke UDP, kita mulai, kita pakai browser - di browser ada kesalahan koneksi, beberapa data biner dituangkan di konsol. Jadi, sistem mengirimkan permintaan melalui UDP, dan kami akan mendengarkan koneksi yang masuk melalui UDP pada port 53. Setengah jam kerja, di mana 15 menit google cara menaikkan server TCP dan UDP di NodeJS - dan kami telah menyelesaikan tugas landasan proyek, yang menentukan struktur aplikasi masa depan. Kode tersebut adalah sebagai berikut:


 const dgram = require('dgram'); const server = dgram.createSocket('udp4'); (function() { server.on('error', (err) => { console.log(`server error:\n${err.stack}`); server.close(); }); server.on('message', async (localReq, linfo) => { console.log(localReq); //            }); server.on('listening', () => { const address = server.address(); console.log(`server listening ${address.address}:${address.port}`); }); const localListenPort = 53; const localListenAddress = 'localhost'; server.bind(localListenPort, localListenAddress); // server listening 0.0.0.0:53 }()); 

Daftar 1. Kode minimum yang diperlukan untuk menerima permintaan DNS lokal


Poin berikutnya adalah membaca pesan untuk memahami apakah perlu mengembalikan IP kami sebagai tanggapan terhadapnya, atau hanya meneruskannya.


Pesan DNS


Struktur pesan DNS dijelaskan dalam RFC-1035. Baik permintaan dan tanggapan mengikuti struktur ini dan, pada prinsipnya, berbeda dalam satu bendera bit (bidang QR) di header pesan. Pesan itu meliputi lima bagian:


 +---------------------+ | Header | +---------------------+ | Question | the question for the name server +---------------------+ | Answer | RRs answering the question +---------------------+ | Authority | RRs pointing toward an authority +---------------------+ | Additional | RRs holding additional information +---------------------+ 

Struktur Pesan DNS Umum https://tools.ietf.org/html/rfc1035#section-4.1


Pesan DNS dimulai dengan header panjang tetap (ini yang disebut bagian Header ), yang berisi bidang dari 1 bit hingga dua byte panjang (dengan demikian, satu byte di header bisa berisi beberapa bidang). Header dimulai dengan bidang ID - ini adalah pengidentifikasi permintaan 16-bit, responsnya harus memiliki ID yang sama. Berikutnya adalah bidang yang menggambarkan jenis permintaan, hasil eksekusi dan jumlah catatan di setiap bagian pesan berikutnya. Jelaskan semuanya untuk waktu yang lama, jadi siapa yang peduli - baik di RFC: https://tools.ietf.org/html/rfc1035#section-4.1.1 . Bagian Header selalu ada dalam pesan DNS.


  1 1 1 1 1 1 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | ID | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ |QR| Opcode |AA|TC|RD|RA| Z | RCODE | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | QDCOUNT | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | ANCOUNT | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | NSCOUNT | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | ARCOUNT | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 

Struktur header pesan DNS https://tools.ietf.org/html/rfc1035#section-4.1.1


Bagian pertanyaan


Bagian Pertanyaan berisi entri yang memberi tahu server secara tepat informasi apa yang dibutuhkan darinya. Secara teoritis, di bagian catatan tersebut mungkin ada satu atau beberapa, jumlahnya ditunjukkan di bidang QDCOUNT di header pesan, dan bisa 0, 1 atau lebih. Namun dalam praktiknya, bagian Pertanyaan hanya dapat berisi satu entri. Jika bagian Pertanyaan berisi beberapa catatan, dan salah satunya akan menyebabkan kesalahan saat memproses permintaan di server, situasi yang tidak ditentukan akan muncul. Meskipun server akan mengembalikan kode kesalahan di bidang RCODE dalam pesan respons, itu tidak akan dapat menunjukkan saat memproses yang merekam masalah terjadi, spesifikasi tidak menjelaskan ini. Catatan juga tidak memiliki bidang yang berisi indikasi kesalahan dan jenisnya. Oleh karena itu, ada perjanjian (tidak berdokumen), di mana bagian Pertanyaan hanya dapat berisi satu catatan, dan bidang QDCOUNT memiliki nilai 1. Juga tidak sepenuhnya jelas bagaimana memproses permintaan di sisi server, jika masih berisi beberapa catatan dalam Pertanyaan . Seseorang menyarankan untuk mengembalikan pesan dengan kesalahan permintaan. Dan, misalnya, Google DNS hanya memproses catatan pertama di bagian Pertanyaan , itu hanya mengabaikan sisanya. Rupanya, ini tetap pada kebijaksanaan pengembang layanan DNS.


Dalam respons DNS-message dari server, bagian Pertanyaan juga ada dan harus benar-benar menyalin Pertanyaan permintaan (untuk menghindari konflik, jika satu bidang ID tidak cukup).


Satu-satunya entri di bagian Pertanyaan berisi bidang: QNAME (nama domain), QTYPE (tipe), QCLASS (kelas). QTYPE dan QCLASS adalah angka bita ganda yang menunjukkan jenis dan kelas permintaan. Jenis dan kelas yang mungkin dijelaskan dalam RFC-1035 https://tools.ietf.org/html/rfc1035#section-3.2 , semuanya jelas di sana. Tetapi pada metode pencatatan nama domain kita akan membahas lebih detail di bagian "Format untuk merekam nama domain".


Dalam hal permintaan, pesan DNS paling sering diakhiri dengan bagian Pertanyaan , kadang-kadang bagian Tambahan dapat mengikutinya.


Jika kesalahan terjadi saat memproses permintaan di server (misalnya, permintaan masuk dibuat salah), pesan respons juga akan berakhir dengan bagian Pertanyaan atau Tambahan , dan bidang RCODE pada header pesan tanggapan akan berisi kode kesalahan.


Bagian Jawab , Wewenang, dan Tambahan


Bagian berikut ini adalah Jawab , Otoritas dan Tambahan ( Jawab dan Otoritas hanya terkandung dalam pesan DNS respons, tambahan dapat muncul dalam permintaan dan dalam respons). Mereka adalah opsional, mis. salah satu dari mereka mungkin ada atau tidak, tergantung pada permintaan. Bagian-bagian ini memiliki struktur yang sama dan berisi informasi dalam format yang disebut "catatan sumber daya" (catatan sumber daya , atau RR). Secara kiasan, masing-masing bagian ini adalah array dari catatan sumber daya, dan catatan adalah objek dengan bidang. Setiap bagian dapat berisi satu atau lebih catatan, jumlahnya ditunjukkan dalam bidang yang sesuai di header pesan (masing-masing ANCOUNT, NSCOUNT, ARCOUNT). Misalnya, permintaan IP untuk domain "google.com" akan mengembalikan beberapa alamat IP, jadi juga akan ada beberapa entri di bagian Jawab , satu untuk setiap alamat. Jika bagian tidak ada, maka bidang header yang sesuai berisi 0.


Setiap catatan sumber daya (RR) dimulai dengan bidang NAME yang berisi nama domain. Format bidang ini sama dengan bidang QNAME pada bagian Pertanyaan .
Di sebelah NAME adalah bidang TYPE (tipe catatan), dan CLASS (kelasnya), kedua bidang adalah angka 16-bit, menunjukkan jenis dan kelas catatan. Ini juga menyerupai bagian Pertanyaan , dengan perbedaan bahwa QTYPE dan QCLASS-nya dapat memiliki semua nilai yang sama seperti TYPE dan CLASS, dan beberapa lagi milik mereka yang unik bagi mereka. Artinya, dalam bahasa ilmiah kering, himpunan nilai QTYPE dan QCLASS adalah superset dari nilai TYPE dan CLASS. Baca lebih lanjut tentang perbedaan di https://tools.ietf.org/html/rfc1035#section-3.2.2 .
Kolom yang tersisa adalah:


  • TTL adalah angka 32-bit yang menunjukkan waktu rekaman terakhir (dalam detik).
  • RDLENGTH adalah angka 16-bit yang menunjukkan panjang bidang RDATA berikutnya dalam byte.
  • RDATA sebenarnya adalah muatan, formatnya tergantung pada jenis catatan. Misalnya, untuk catatan tipe A (alamat host) dan kelas IN (Internet) ini adalah 4 byte yang mewakili alamat IPv4.

Format Catatan Nama Domain


Format untuk merekam nama domain adalah sama untuk bidang QNAME dan NAME, serta untuk bidang RDATA, jika itu adalah CNAME, MX, NS atau catatan kelas lainnya yang mengasumsikan nama domain sebagai hasilnya.


Nama domain adalah urutan label (bagian dari nama, subdomain - ini adalah label dalam aslinya, saya tidak menemukan terjemahan yang lebih baik). Label adalah satu byte panjang yang berisi angka - panjang isi label dalam byte, diikuti oleh urutan byte dari panjang yang ditentukan. Label mengikuti satu demi satu hingga satu byte panjang yang mengandung 0. ditemui. Label pertama dapat langsung dari nol panjang, ini menunjukkan domain root (Root Domain) dengan nama domain kosong (kadang-kadang ditulis sebagai "").


Dalam versi DNS sebelumnya, byte dalam label bisa memiliki nilai mulai dari (0 hingga 255). Ada aturan yang bersifat rekomendasi mendesak: bahwa label dimulai dengan huruf, diakhiri dengan huruf atau angka, dan hanya berisi huruf, angka atau tanda hubung dalam 7-bit ASCII encoding, dengan bit nol paling signifikan. Spesifikasi EDNS saat ini sudah mensyaratkan kepatuhan dengan aturan-aturan ini dengan jelas, tanpa penyimpangan.


Dua bit paling signifikan dari byte panjang digunakan sebagai atribut tipe tag. Jika mereka nol ( 0b00xxxxxx ), maka ini adalah label normal, dan bit yang tersisa dari byte panjangnya menunjukkan jumlah byte data yang termasuk dalam komposisinya. Panjang label maksimum adalah 63 karakter. 63 dalam pengkodean biner hanya 0b00111111 .


Jika dua bit paling signifikan adalah 0 dan 1 ( 0b01xxxxxx ), masing-masing , maka ini adalah label tipe diperpanjang dari standar EDNS ( https://tools.ietf.org/html/rfc2671#section-3.1 ), yang datang kepada kami mulai 1 Februari 2019. Enam bit yang lebih rendah akan berisi nilai label. Kami tidak membahas EDNS dalam artikel ini, tetapi perlu diketahui bahwa ini juga terjadi.


Kombinasi dua bit paling signifikan, sama dengan 1 dan 0 ( 0b10xxxxxx ), dicadangkan untuk penggunaan di masa mendatang.


Jika kedua bit tinggi sama dengan 1 ( 0b11xxxxxx ), ini berarti bahwa nama domain dikompresi ( kompresi ), dan kami akan membahasnya lebih terinci.


Kompresi Nama Domain


Jadi, jika panjang byte memiliki dua bit tinggi sama dengan 1 ( 0b11xxxxxx ), ini adalah tanda kompresi nama domain. Kompresi digunakan untuk membuat pesan lebih pendek dan lebih ringkas. Ini terutama benar ketika bekerja pada UDP, ketika panjang total pesan DNS dibatasi hingga 512 byte (meskipun ini adalah standar lama, lihat https://tools.ietf.org/html/rfc1035#section-2.3.4 Batas ukuran , EDNS baru memungkinkan pengiriman pesan UPD dan lebih lama). Inti dari proses ini adalah bahwa jika pesan DNS berisi nama domain dengan subdomain tingkat atas yang sama (misalnya, mail.yandex.ru dan yandex.ru ), maka alih-alih menentukan kembali seluruh nama domain, nomor byte dalam pesan DNS dari mana Lanjutkan membaca nama domain. Ini bisa berupa byte dari pesan DNS, tidak hanya di catatan atau bagian saat ini, tetapi dengan ketentuan bahwa itu adalah byte dari panjang label domain. Anda tidak dapat merujuk ke tengah tanda. Misalkan ada domain mail.yandex.ru dalam pesan tersebut, maka dengan bantuan kompresi dimungkinkan untuk juga menunjuk domain "" yandex.ru , ru dan root (tentu saja, root lebih mudah untuk ditulis tanpa kompresi, tetapi secara teknis dimungkinkan untuk melakukan ini dengan kompresi), dan di sini untuk membuat ndex.ru tidak akan berfungsi. Juga, semua nama domain turunan akan berakhir di domain root, yaitu, tulis, katakanlah, mail.yandex juga akan gagal.


Nama domain dapat:


  • direkam sepenuhnya tanpa kompresi,
  • mulai dari tempat yang menggunakan kompresi
  • mulai dengan satu atau lebih label tanpa kompresi, dan kemudian beralih ke kompresi,
  • kosong (untuk domain root).

Misalnya, kami sedang mengkompilasi pesan DNS, dan kami sudah menemukan nama "dom3.example.com" di dalamnya, sekarang kita perlu menentukan "dom4.dom3.example.com". Dalam hal ini, Anda dapat merekam bagian "dom4" tanpa kompresi, dan kemudian beralih ke kompresi, yaitu menambahkan tautan ke "dom3.example.com". Atau sebaliknya, jika nama "dom4.dom3.example.com" sebelumnya ditemukan, maka untuk menunjukkan "dom3.example.com" Anda dapat segera menggunakan kompresi dengan merujuk pada label "dom3" di dalamnya. Apa yang tidak dapat kita lakukan adalah, seperti yang telah dikatakan, untuk menunjukkan bagian 'dom4.dom3' melalui kompresi, karena nama harus diakhiri dengan bagian tingkat atas. Jika Anda tiba-tiba perlu menentukan segmen dari tengah, maka mereka hanya ditunjukkan tanpa kompresi.


Untuk kesederhanaan, program kami tidak tahu cara menulis nama domain dengan kompresi, hanya bisa membaca. Standar memungkinkan ini, membaca harus dilaksanakan tentu saja, menulis adalah opsional. Secara teknis, pembacaan diimplementasikan seperti ini: jika dua bit paling signifikan dari satu byte panjang berisi 1, maka kita membaca byte yang mengikutinya, dan memperlakukan dua byte ini sebagai integer 16-bit unsigned integer, dengan urutan bit Big Endian. Kami membuang dua bit paling signifikan (berisi 1), membaca angka 14-bit yang dihasilkan, dan melanjutkan membaca nama domain dari byte dalam pesan DNS di bawah angka yang sesuai dengan nomor ini.


Kode untuk fungsi membaca nama domain adalah sebagai berikut:


 function readDomainName (buf, startOffset, objReturnValue = {}) { let currentByteIndex = startOffset; //    ,  DNS- ,      let initOctet = buf.readUInt8(currentByteIndex); let domain = ''; //      , ..       0, //  ,      // "the root domain name has no labels." (c) RFC-1035, p. 4.1.4. Message compression objReturnValue['endOffset'] = currentByteIndex; let lengthOctet = initOctet; while (lengthOctet > 0) { //     var label; if (lengthOctet >= 192) { //   :  0b1100 0000   const pointer = buf.readUInt16BE(currentByteIndex) - 49152; // 49152 === 0b1100 0000 0000 0000 === 192 * 256 const returnValue = {} label = readDomainName(buf, pointer, returnValue); domain += ('.' + label); objReturnValue['endOffset'] = currentByteIndex + 1; //      ,      break; } else { currentByteIndex++; label = buf.toString('ascii', currentByteIndex, currentByteIndex + lengthOctet); domain += ('.' + label); currentByteIndex += lengthOctet; lengthOctet = buf.readUInt8(currentByteIndex); objReturnValue['endOffset'] = currentByteIndex; } } return domain.substring(1); //    —  "." } 

Daftar 2. Membaca nama domain dari permintaan DNS


Kode lengkap untuk fungsi membaca catatan DNS dari buffer biner:


Daftar 3. Membaca catatan DNS dari buffer biner
 function parseDnsMessageBytes (buf) { const msgFields = {}; // (c) RFC 1035 p. 4.1.1. Header section format msgFields['ID'] = buf.readUInt16BE(0); const byte_2 = buf.readUInt8(2); //  #2 (starting from 0) const mask_QR = 0b10000000; msgFields['QR'] = !!(byte_2 & mask_QR); //  : 0 "false" => , 1 "true" =>  const mask_Opcode = 0b01111000; const opcode = (byte_2 & mask_Opcode) >>> 3; //   (): 0, 1, 2,   msgFields['Opcode'] = opcode; const mask_AA = 0b00000100; msgFields['AA'] = !!(byte_2 & mask_AA); const mask_TC = 0b00000010; msgFields['TC'] = !!(byte_2 & mask_TC); const mask_RD = 0b00000001; msgFields['RD'] = !!(byte_2 & mask_RD); const byte_3 = buf.readUInt8(3); //  #3 const mask_RA = 0b10000000; msgFields['RA'] = !!(byte_3 & mask_RA); const mask_Z = 0b01110000; msgFields['Z'] = (byte_3 & mask_Z) >>> 4; //  0,  const mask_RCODE = 0b00001111; msgFields['RCODE'] = (byte_3 & mask_RCODE); // 0 => no error; (dec) 1, 2, 3, 4, 5 - errors, see RFC msgFields['QDCOUNT'] = buf.readUInt16BE(4); //     Question,   0  1 msgFields['ANCOUNT'] = buf.readUInt16BE(6); //     Answer msgFields['NSCOUNT'] = buf.readUInt16BE(8); //     Authority msgFields['ARCOUNT'] = buf.readUInt16BE(10); //     Additional //    Question let currentByteIndex = 12; //  Question   12-  DNS- (c) RFC 1035 p. 4.1.2. Question section format msgFields['questions'] = []; for (let qdcount = 0; qdcount < msgFields['QDCOUNT']; qdcount++) { const question = {}; const resultByteIndexObj = { endOffset: undefined }; const domain = readDomainName(buf, currentByteIndex, resultByteIndexObj); currentByteIndex = resultByteIndexObj.endOffset + 1; question['domainName'] = domain; question['qtype'] = buf.readUInt16BE(currentByteIndex); // 1 => "A" record currentByteIndex += 2; question['qclass'] = buf.readUInt16BE(currentByteIndex); // 1 => "IN" Internet currentByteIndex += 2; msgFields['questions'].push(question); } // (c) RFC 1035 p. 4.1.3. Resource record format //    (Resourse Records, RR)  Answer, Authority, Additional ['answer', 'authority', 'additional'].forEach(function(section, i, arr) { let msgFieldsName, countFieldName; switch(section) { case 'answer': msgFieldsName = 'answers'; countFieldName = 'ANCOUNT'; break; case 'authority': msgFieldsName = 'authorities'; countFieldName = 'NSCOUNT'; break; case 'additional': msgFieldsName = 'additionals'; countFieldName = 'ARCOUNT'; break; } msgFields[msgFieldsName] = []; for (let recordsCount = 0; recordsCount < msgFields[countFieldName]; recordsCount++) { let record = {}; const objReturnValue = {}; const domain = readDomainName(buf, currentByteIndex, objReturnValue); currentByteIndex = objReturnValue['endOffset'] + 1; record['domainName'] = domain; record['type'] = buf.readUInt16BE(currentByteIndex); // 1 => "A" record currentByteIndex += 2; record['class'] = buf.readUInt16BE(currentByteIndex); // 1 => "IN" Internet currentByteIndex += 2; // TTL  4  record['ttl'] = buf.readUIntBE(currentByteIndex, 4); currentByteIndex += 4; record['rdlength'] = buf.readUInt16BE(currentByteIndex); currentByteIndex += 2; const rdataBinTempBuf = buf.slice(currentByteIndex, currentByteIndex + record['rdlength']); record['rdata_bin'] = Buffer.alloc(record['rdlength'], rdataBinTempBuf); if (record['type'] === 1 && record['class'] === 1) { //      IPv4,      let ipStr = ''; for (ipv4ByteIndex = 0; ipv4ByteIndex < 4; ipv4ByteIndex++) { ipStr += '.' + buf.readUInt8(currentByteIndex).toString(); currentByteIndex++; } record['IPv4'] = ipStr.substring(1); //    '.' } else { //    ,   currentByteIndex += record['rdlength']; } msgFields[msgFieldsName].push(record); } }); return msgFields; } 

3. DNS-


, . , , , . , DNS-, , . , .


, - server.on("message", () => {}) 1. :


4. DNS-
 server.on('message', async (localReq, linfo) => { const dnsRequest = functions.parseDnsMessageBytes(localReq); const question = dnsRequest.questions[0]; // currently, only one question per query is supported by DNS implementations let forgingHostParams = undefined; // ,         IP for (let i = 0; i < config.requestsToForge.length; i++) { const requestToForge = config.requestsToForge[i]; const targetDomainName = requestToForge.hostName; if (functions.domainNameMatchesTemplate(question.domainName, targetDomainName) && question.qclass === 1 && question.qtype === 1) { forgingHostParams = requestToForge; break; } } //  ,    DNS-      if (!!forgingHostParams) { const forgeIp = forgingHostParams.ip; const answers = []; answers.push({ domainName: question.domainName, type: question.qtype, class: question.qclass, ttl: forgedRequestsTTL, rdlength: 4, rdata_bin: functions.ip4StringToBuffer(forgeIp), IPv4: forgeIp }); const localDnsResponse = { ID: dnsRequest.ID, QR: dnsRequest.QR, Opcode: dnsRequest.Opcode, AA: dnsRequest.AA, TC: false, // dnsRequest.TC, RD: dnsRequest.RD, RA: true, Z: dnsRequest.Z, RCODE: 0, // dnsRequest.RCODE, 0 - no errors, look in RFC-1035 for other error conditions QDCOUNT: dnsRequest.QDCOUNT, ANCOUNT: answers.length, NSCOUNT: dnsRequest.NSCOUNT, ARCOUNT: dnsRequest.ARCOUNT, questions: dnsRequest.questions, answers: answers } //     DNS-    const responseBuf = functions.composeDnsMessageBin(localDnsResponse); console.log('response composed for: ', localDnsResponse.questions[0]); server.send(responseBuf, linfo.port, linfo.address, (err, bytes) => {}); } // ,     DNS-,         else { //     DNS-  UDP,     const responseBuf = await functions.getRemoteDnsResponseBin(localReq, upstreamDnsIP, upstreamDnsPort); //        server.send(responseBuf, linfo.port, linfo.address, (err, bytes) => {}); //     DNS-  TLS,   , .  9 } }); 

4. DNS-


TLS


DNS-. , DNS- TLS (HTTPS ). DNS- TLS TCP, , TLS . TCP, RFC-7766 DNS Transport over TCP ( https://tools.ietf.org/html/rfc7766 ). , : TLS, TCP ( , DNS TCP, TLS- TCP-, ).


TLS-


TLS- , , . , TLS-, . RFC-7858 - :


 In order to amortize TCP and TLS connection setup costs, clients and servers SHOULD NOT immediately close a connection after each response. Instead, clients and servers SHOULD reuse existing connections for subsequent queries as long as they have sufficient resources. In some cases, this means that clients and servers may need to keep idle connections open for some amount of time. () https://tools.ietf.org/html/rfc7858#section-3.4 

, TLS-, , , , , . , 30 , , , DNS-. 30 ~ ~ , 15 60 , . , . - .


TLS- NodeJS. , TLS- :


 const tls = require('tls'); const TLS_SOCKET_IDLE_TIMEOUT = 30000; //    ,     TLS- function Module(connectionOptions, funcOnData, funcOnError, funcOnClose, funcOnEnd) { let socket; function connect() { socket = tls.connect(connectionOptions, () => { console.log('client connection established:', socket.authorized ? 'authorized' : 'unauthorized'); }); socket.on('data', funcOnData); // connection.on('end', () => {}); socket.on('close', (hasTransmissionError) => { //   ,     . //   ,     console.log('connection closed; transmission error:', hasTransmissionError); }); socket.on('end', () => { console.log('remote TLS server connection closed.') }); socket.on('error', (err) => { console.log('connection error:', err); console.log('\tmessage:', err.message); console.log('\tstack:', err.stack); }) socket.setTimeout(TLS_SOCKET_IDLE_TIMEOUT); socket.on('timeout', () => { console.log('socket idle timeout, disconnected.'); socket.end(); }); } this.write = function (dataBuf) { if (socket && socket.writable) { //  ,     } else { connect(); } socket.write(dataBuf); } return this; } module.exports = Module; 

5. , TLS-


DNS-over-TLS , Google DNS. , socket = tls.connect(connectionOptions, () => {}) . NodeJS: https://nodejs.org/api/tls.html#tls_tls_connect_options_callback , .


TLS- :


 const options = { port: config.upstreamDnsTlsPort, //        host: config.upstreamDnsTlsHost } const onData = (data) => { //     , .       7 }; remoteTlsClient = new TlsClient(options, onData); 

6. TLS-


, TCP-. TCP/TLS- DNS-, , , , . TCP ( TLS), DNS- 512 , UDP (, EDNS UDP ). , DNS- UDP, . onData() 6.


 const onData = (data) => { //    DNS-,       TLS-   //    ,     2 ,       let dataCurrentPos = 0; try { while (dataCurrentPos < data.length) { const respLen = data.readUInt16BE(dataCurrentPos); respBuf = data.slice(dataCurrentPos + 2, dataCurrentPos + 2 + respLen); const respData = functions.parseDnsMessageBytes(respBuf); const requestKey = functions.getRequestIdentifier(respData); const localResponseParams = localRequestsAwaiting.get(requestKey); localRequestsAwaiting.delete(requestKey); server.send(respBuf, localResponseParams.port, localResponseParams.address, (err, bytesNum) => {}); dataCurrentPos += 2 + respLen; } } catch (err) { console.error(err); //   ,     throw err; } }; 

7. TLS- DNS- 6


DNS-


, , . , ID QNAME, QTYPE QCLASS Question :


 Since pipelined responses can arrive out of order, clients MUST match responses to outstanding queries on the same TLS connection using the Message ID. If the response contains a Question Section, the client MUST match the QNAME, QCLASS, and QTYPE fields. () https://tools.ietf.org/html/rfc7858#section-3.3 

, , , ID Question ( , ).


UDP (. 4), , -, , UDP- . , DNS-, . , -. , , UDP- -. , , .


TLS, . (IP ), , .


IP "-". , , , DNS-. , , IP , . 7:


 //        const requestKey = functions.getRequestIdentifier(respData); //    IP    ,   const localResponseParams = localRequestsAwaiting.get(requestKey); localRequestsAwaiting.delete(requestKey); //      IP   server.send(respBuf, localResponseParams.port, localResponseParams.address, (err, bytesNum) => {}); 

8. 7


TLS-:


 //   ,     const localReqParams = { address: linfo.address, port: linfo.port }; //        const requestKey = functions.getRequestIdentifier(dnsRequest); //       localRequestsAwaiting.set(requestKey, localReqParams); //       ,      const lenBuf = Buffer.alloc(2); lenBuf.writeUInt16BE(localReq.length); const prepReqBuf = Buffer.concat([lenBuf, localReq], 2 + localReq.length); remoteTlsClient.write(prepReqBuf); //  RFC-7766 p.8, 2                

9. DNS- TLS- ( . 4)



, , . JSON, , NodeJS JSON- . JSON — , . , JSON- "comment" ( ) . , , , , . , , . , - , , NodeJS. , , . , , ; , . , - .


10.
 const path = require('path'); const fs = require('fs'); const CONFIG_FILE_PATH = path.resolve('./config.json'); function Module () { // config  -,       . //    config        , //         . ,    : // const conf = config; //   conf     ,    : // const requestsToForge = config.requestsToForge; //    , requestsToForge   . const config = {}; Object.defineProperty(this, 'config', { get() { return config; }, enumerable: true }) this.initConfig = async function() { const fileContents = await readConfigFile(CONFIG_FILE_PATH); console.log('initConfig:'); console.log(fileContents); console.log('fileContents logged ^^'); const parsedConfigData = parseConfig(fileContents); Object.assign(config, parsedConfigData); }; async function readConfigFile(configPath) { const promise = new Promise((resolve, reject) => { fs.readFile(configPath, { encoding: 'utf8', flag: 'r' }, (err, data) => { if (err) { console.log('readConfigFile err to throw'); throw err; } resolve(data); }); }) .then( fileContents => { return fileContents; } ) .catch(err => { console.log('readConfigFile error: ', err); }); return promise; } function parseConfig(fileContents) { const configData = JSON.parse(fileContents); return configData; } //   ,       . //  Windows,    fs.watch     , //      ,   configReadInProgress let configReadInProgress = false; fs.watch(CONFIG_FILE_PATH, async () => { if(!configReadInProgress) { configReadInProgress = true; console.log('===== config changed, run initConfig() ====='); try { await this.initConfig(); } catch (err) { console.log('===== error initConfig(), skip =====,', err); configReadInProgress = false; } configReadInProgress = false; } else { console.log('===== config changed, initConfig() already running, skip ====='); } }); } let instance; async function getInstance() { if(!instance) { instance = new Module(); await instance.initConfig(); } return instance; } module.exports = getInstance; 

10.


Total


DNS- NodeJS, npm . , , , , .


GitHub


:


Source: https://habr.com/ru/post/id440050/


All Articles