Розробка

DNS over TLS — Шифруємо наші DNS-запити за допомогою Stunnel і Lua


джерело зображення

DNS (англ. Domain Name System — система доменних імен) — розподілена комп’ютерна система для отримання інформації про домени.

TLS (англ. transport layer security — Протокол захисту транспортного рівня) — забезпечує захищену передачу даних між Інтернет вузлами.

Після новини “Google Public DNS тихо включили підтримку DNS over TLS” я вирішив спробувати його. У мене є Stunnel який створить шифрований TCP тунель. Але програми зазвичай спілкуються з DNS по протоколу UDP. Тому нам потрібен проксі, який буде пересилати UDP пакети TCP потік і назад. Ми напишемо його на Lua.

 

Вся різниця між TCP і UDP DNS пакетами:

4.2.2. TCP usage
Messages sent over TCP connections use server port 53 (decimal). The message is prefixed with a two byte length field which gives the message length, excluding the two byte length field. This length field allows the low-level processing to assemble a complete message before the beginning to parse it.

RFC1035: DOMAIN NAMES — IMPLEMENTATION AND SPECIFICATION

 

Тобто робимо туди:

 

  1. беремо пакет з UDP
  2. додаємо до нього на початку пару байт у яких зазначений розмір цього пакета
  3. відправляємо в TCP канал

 

І у зворотний бік:

 

  1. читаємо з TCP пару байт тим самим отримуємо розмір пакета
  2. читаємо пакет TCP
  3. відправляємо його одержувачу по UDP

 

Налаштовуємо Stunnel

 

  1. Викачуємо кореневий сертифікат Root-R2.crt в директорію з конфіг Stunnel
  2. Конвертуємо сертифікат в PEM
    openssl x509 -inform DER -in Root-R2.crt -out Root-R2.pem -text
  3. Пишемо в stunnel.conf:

    [dns]
    client = yes
    accept = 127.0.0.1:53
    connect = 8.8.8.8:853
    CAfile = Root-R2.pem
    verifyChain = yes
    checkIP = 8.8.8.8

 

Тобто Stunnel:

 

  1. візьме шифрування TCP за адресою 127.0.0.1:53
  2. відкриє шифрований TLS-тунель до адреси 8.8.8.8:853 (Google DNS)
  3. буде передавати дані туди і назад

 

Запускаємо Stunnel

 

Роботу тунеля можна перевірити командою:

 

nslookup -vc ya.ru 127.0.0.1

 

Опція vc змушує nslookup використовувати TCP з’єднання до DNS серверу замість UDP.

 

Результат:

 

*** Can't find server name address for 127.0.0.1: Non-existent domain
Server: UnKnown
Address: 127.0.0.1

Non-authoritative answer:
Name: ya.ru
Address: (тут IP яндекса)

 

Пишемо скрипт

 

Я пишу на Lua 5.3. У ньому вже доступні бінарні операції з числами. Ну і нам знадобиться модуль Lua Socket.

 

Ім’я файлу: simple-udp-to-tcp-dns proxy.lua

 

local socket = require "socket" -- підключаємо lua socket

 

--[[--

 

Напишемо простеньку функцію яка дозволить відправити дамп пакету в консоль. Хочеться бачити, що робить проксі.

 

--]]--

 

function serialize(data)
 -- Перетворимо символи не входять в діапазони a-z 0-9 і тире в HEX подання 'xFF'
 return'"..data:gsub("[^a-z0-9-]", function(chr) return ("\x%02X"):format(chr:byte()) end).."'"
end

 

--[[--

 

TCP і UDP в назад

 

Пишемо дві функції які будуть оперувати двома каналами передачі даних.

 

--]]--

 

-- тут пакети UDP пересилаються в TCP потік
function udp_to_tcp_coroutine_function(udp_in, tcp_out, clients)
repeat
 coroutine.yield() -- повертаємо управління головного циклу
 packet, err_ip, port = udp_in:receivefrom() -- приймаємо UDP-пакет
 if then packet
 -- > - big endian
 -- I - unsigned integer
 -- 2 - 2 size bytes
 tcp_out:send(((">I2"):pack(#packet))..packet) -- додаємо розмір пакета і відправляємо в TCP
 local id = (">I2"):unpack(packet:sub(1,2)) -- читаємо ID пакету
 clients[id] = {ip=err_ip, port=port} -- записуємо адресу відправника
 print(os.date("%c", os.time()) ,err_ip, port, ">", serialize(packet)) -- відображаємо пакет в консоль
end
 until false
end

-- тут пакети з TCP потоку пересилаються адресату по UDP
function tcp_to_udp_coroutine_function(tcp_in, udp_out, clients)
repeat
 coroutine.yield() -- возврашяем управління головного циклу
 -- > - big endian
 -- I - unsigned integer
 -- 2 - 2 size bytes
 local packet = tcp_in:receive((">I2"):unpack(tcp_in:receive(2)), nil) -- приймаємо TCP пакет
 local id = (">I2"):unpack(packet:sub(1,2)) -- читаємо ID пакету
 local client = clients[id] -- знаходимо одержувача
 if client then
 udp_out:sendto(packet, client.ip, client.port) -- відправляємо пакет одержувачу по UDP
 clients[id] = nil -- очищаємо клітинку
 print(os.date("%c", os.time()) ,client.ip, client.port, "<", serialize(packet)) -- відображаємо пакет в консоль
end
 until false
end

 

--[[--

 

Обидві функції відразу після запуску виконують coroutine.yield(). Це дозволяє першим викликом передати параметри функції а далі робити coroutine.resume(co) без додаткових параметрів.

 

main

 

А тепер main функція яка виконає підготовку і запустить головний цикл.

 

--]]--

 

function main()
 local tcp_dns_socket = socket.tcp() -- готуємо сокет TCP
 local udp_dns_socket = socket.udp() -- готуємо UDP сокет

 local tcp_connected, err = tcp_dns_socket:connect("127.0.0.1", 53) -- єднаємося з TCP тунелем
 assert(tcp_connected, err) -- перевіряємо що з'єдналися
 print("tcp dns connected") -- повідомляємо що з'єдналися в консоль

 local udp_open, err = udp_dns_socket:setsockname("127.0.0.1", 53) -- відкриваємо UDP порт
 assert(udp_open, err) -- перевіряємо що відкрили
 print("udp dns port open") -- повідомляємо що UDP порт відкритий

 -- користуємося тим, що таблиці Lua дозволяють використовувати як ключ що завгодно крім nil
 -- використовуємо як ключ сокет щоб при наявності даних на ньому викликати його сопрограмму
 local coroutines = {
 [tcp_dns_socket] = coroutine.create(tcp_to_udp_coroutine_function), -- створюємо сопрограмму TCP to UDP
 [udp_dns_socket] = coroutine.create(udp_to_tcp_coroutine_function) -- створюємо сопрограмму UDP to TCP
}

 local clients = {} -- тут будуть записуватися одержувачі пакетів

 -- передаємо кожної сопрограмме сокети і таблицю одержувачів
 coroutine.resume(coroutines[tcp_dns_socket], tcp_dns_socket, udp_dns_socket, clients) 
 coroutine.resume(coroutines[udp_dns_socket], udp_dns_socket, tcp_dns_socket, clients)

 -- таблиця з якої socket.select буде вибирати сокет готовий до отримання даних
 local socket_list = {tcp_dns_socket, udp_dns_socket} 

 repeat -- запускаємо головний цикл
 -- socket.select вибирає з socket_list сокети у яких є дані на отримання в буфері
 -- і повертає нову таблицю з ними. Цикл for послідовно повертає значення з таблиці 
 for _, in_socket in ipairs(socket.select(socket_list)) do
 -- запускаємо асоційовану з отриманим сокетом сопрограмму
 local ok, err = coroutine.resume(coroutines[in_socket])
 if not ok then
 -- якщо сопрограмма завершилася з помилкою те
 udp_dns_socket:close() -- закриваємо UDP порт
 tcp_dns_socket:close() -- закриваємо з'єднання TCP
 print(err) -- виводимо помилку
 return -- завершуємо головний цикл
end
end
 until false
end

 

--[[--

 

Запускаємо головну функцію. Якщо раптом буде закрито з’єднання ми через секунду встановимо його заново викликавши main.

 

--]]--

 

repeat
 coroutine.resume(coroutine.create(main)) -- запускаємо main
 socket.sleep(1) -- перед рестартом чекаємо одну секунду
until false

 

перевіряємо

 

  1. Запускаємо stunnel

  2. Запускаємо наш скрипт

    lua5.3 simple-udp-to-tcp-dns proxy.lua
  3. Перевіряємо роботу скрипта командою

    nslookup ya.ru 127.0.0.1

    На цей раз без ‘-vc’ так так ми вже написали і запустили проксі який загортає UDP DNS запити в TCP тунель.

 

Результат:

 

*** Can't find server name address for 127.0.0.1: Non-existent domain
Server: UnKnown
Address: 127.0.0.1

Non-authoritative answer:
Name: ya.ru
Address: (тут IP яндекса)

 

Якщо все нормально можна вказати в налаштуваннях соедидения як DNS сервер “127.0.0.1”

 

висновок

Тепер наші DNS запити під захистом TLS.

Related Articles

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *

Close