Как стать автором
Обновить

DNS over TLS — Шифруем наши DNS запросы с помощью Stunnel и Lua

Время на прочтение5 мин
Количество просмотров20K


источник изображения


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 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 for address 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

--[[--


UDP в TCP и обратно


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


--]]--


-- здесь пакеты из 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 packet then
            -- > - big endian
            -- I - unsigned integer
            -- 2 - 2 bytes size
            tcp_out:send(((">I2"):pack(#packet))..packet) -- добавляем размер пакета и отправляем в TCP
            local id = (">I2"):unpack(packet:sub(1,2))    -- читаем ID пакета
            if not clients[id] then
                clients[id] = {}
            end
            table.insert(clients[id] ,{ip=err_ip, port=port, packet=packet}) -- записываем адрес отправителя
            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 bytes size
        local packet = tcp_in:receive((">I2"):unpack(tcp_in:receive(2)), nil) -- принимаем TCP пакет
        local id = (">I2"):unpack(packet:sub(1,2))                            -- читаем ID пакета

        if clients[id] then
            for key, client in pairs(clients[id]) do
                -- сравниваем query в запросе и ответе
                if packet:find(client.packet:sub(13, -1), 13, true) == 13 then -- находим получателя
                    udp_out:sendto(packet, client.ip, client.port) -- отправляем пакет получателю по UDP
                    clients[id][key] = nil                         -- очищаем ячейку
                    -- отображаем пакет в консоль
                    print(os.date("%c", os.time()) ,client.ip, client.port, "<", serialize(packet))
                    break
                end
            end
            if not next(clients[id]) then
                clients[id] = nil
            end
        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
    local ok, err = coroutine.resume(coroutine.create(main)) -- запускаем main
    if not ok then
        print(err)
    end
    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 for address 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.


P.S. Не даём гуглу лишней информации о нас


Сразу после соединения Windows пытается зарегистрировать нас на DNS серверах гугла через наш туннель. Это отключается в продвинутых настройках DNS снятием галочки.



Есть ещё запрос на time.windows.com. Он уже не такой личный но как его отключить я так и не нашёл. Автоматическая синхронизация времени отключена.


ссылки


  1. RFC1035: DOMAIN NAMES — IMPLEMENTATION AND SPECIFICATION
  2. DNS поверх TLS
  3. simple-udp-to-tcp-dns-proxy.lua
  4. Составляем DNS-запрос вручную
Теги:
Хабы:
Всего голосов 27: ↑22 и ↓5+17
Комментарии21

Публикации

Истории

Работа

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань