Как отправить dns запрос через сокет
Перейти к содержимому

Как отправить dns запрос через сокет

  • автор:

Использование сокетов для отправки и получения данных по протоколу TCP

Перед использованием сокета для связи с удаленными устройствами необходимо инициализировать сокет, указав протокол и сведения о сетевом адресе. Конструктор класса Socket имеет параметры, которые определяют семейство адресов, тип сокета и тип протокола, которые сокет использует для подключения. При подключении сокета клиента к сокету сервера клиент будет использовать IPEndPoint объект для указания сетевого адреса сервера.

Создание конечной точки IP-адреса

При работе с System.Net.Socketsвы представляете конечную точку сети в IPEndPoint виде объекта . Создается IPEndPoint с соответствующим номером IPAddress порта. Прежде чем начать беседу с помощью Socket, вы создадите канал данных между приложением и удаленным назначением.

В качестве уникального идентификатора службы протокол TCP/IP использует сетевой адрес и номер порта службы. Сетевой адрес идентифицирует конкретное сетевое назначение; номер порта определяет конкретную службу на этом устройстве, к которому нужно подключиться. Сочетание сетевого адреса и порта службы называется конечной точкой, которая представлена в .NET классом EndPoint . Потомок определяется для каждого поддерживаемого EndPoint семейства адресов; для семейства IP-адресов классом является IPEndPoint.

Класс Dns предоставляет службы доменных имен для приложений, использующих интернет-службы TCP/IP. Метод GetHostEntryAsync запрашивает DNS-сервер для сопоставления понятного для пользователя доменного имени (например, «host.contoso.com») с числовым интернет-адресом (например 192.168.1.1 , ). GetHostEntryAsync возвращает объект Task , который при ожидании содержит список адресов и псевдонимов для запрошенного имени. В большинстве случаев можно использовать первый адрес из возвращенного массива AddressList. Следующий код получает объект , IPAddress содержащий IP-адрес сервера host.contoso.com .

IPHostEntry ipHostInfo = await Dns.GetHostEntryAsync("host.contoso.com"); IPAddress ipAddress = ipHostInfo.AddressList[0]; 

Для ручного тестирования и отладки обычно можно использовать GetHostEntryAsync метод , чтобы получить заданное Dns.GetHostName() значение для разрешения имени localhost в IP-адрес.

Центр интернет-номеров (IANA) определяет номера портов для общих служб. Дополнительные сведения см. в разделе IANA: реестр имен служб и номеров портов транспортных протоколов). Другие службы могут использовать номера портов в диапазоне от 1024 до 65535. Следующий код объединяет IP-адрес для host.contoso.com с номером порта, чтобы создать удаленную конечную точку для подключения.

IPEndPoint ipEndPoint = new(ipAddress, 11_000); 

После определения адреса удаленного устройства и выбора порта для подключения приложение может установить подключение к удаленному устройству.

Создание Socket клиента

Создав endPoint объект , создайте сокет клиента для подключения к серверу. После подключения сокета он может отправлять и получать данные из подключения к сокету сервера.

using Socket client = new( ipEndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); await client.ConnectAsync(ipEndPoint); while (true) < // Send message. var message = "Hi friends ��!<|EOM|>"; var messageBytes = Encoding.UTF8.GetBytes(message); _ = await client.SendAsync(messageBytes, SocketFlags.None); Console.WriteLine($"Socket client sent message: \"\""); // Receive ack. var buffer = new byte[1_024]; var received = await client.ReceiveAsync(buffer, SocketFlags.None); var response = Encoding.UTF8.GetString(buffer, 0, received); if (response == "<|ACK|>") < Console.WriteLine( $"Socket client received acknowledgment: \"\""); break; > // Sample output: // Socket client sent message: "Hi friends ��!<|EOM|>" // Socket client received acknowledgment: "<|ACK|>" > client.Shutdown(SocketShutdown.Both); 

В приведенном выше коде C#:

  • Создает экземпляр нового Socket объекта с заданным endPoint семейством адресов экземпляров , SocketType.Streamи ProtocolType.Tcp.
  • Socket.ConnectAsync Вызывает метод с экземпляром в endPoint качестве аргумента.
  • В цикле while :
    • Кодирует и отправляет сообщение на сервер с помощью Socket.SendAsync.
    • Записывает отправленное сообщение в консоль.
    • Инициализирует буфер для получения данных с сервера с помощью Socket.ReceiveAsync.
    • response Когда является подтверждением, он записывается в консоль, и цикл завершается.

    Создание Socket сервера

    Чтобы создать сокет сервера, объект может прослушивать входящие подключения по любому IP-адресу, endPoint но необходимо указать номер порта. После создания сокета сервер может принимать входящие подключения и взаимодействовать с клиентами.

    using Socket listener = new( ipEndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); listener.Bind(ipEndPoint); listener.Listen(100); var handler = await listener.AcceptAsync(); while (true) < // Receive message. var buffer = new byte[1_024]; var received = await handler.ReceiveAsync(buffer, SocketFlags.None); var response = Encoding.UTF8.GetString(buffer, 0, received); var eom = "<|EOM|>"; if (response.IndexOf(eom) > -1 /* is end of message */) < Console.WriteLine( $"Socket server received message: \"\""); var ackMessage = "<|ACK|>"; var echoBytes = Encoding.UTF8.GetBytes(ackMessage); await handler.SendAsync(echoBytes, 0); Console.WriteLine( $"Socket server sent acknowledgment: \"\""); break; > // Sample output: // Socket server received message: "Hi friends ��!" // Socket server sent acknowledgment: "<|ACK|>" > 

    В приведенном выше коде C#:

    • Создает экземпляр нового Socket объекта с заданным endPoint семейством адресов экземпляров , SocketType.Streamи ProtocolType.Tcp.
    • Вызывает listener Socket.Bind метод с экземпляром в endPoint качестве аргумента для связывания сокета с сетевым адресом.
    • Метод Socket.Listen() вызывается для прослушивания входящих подключений.
    • Вызывает listener метод для Socket.AcceptAsync принятия входящего подключения к сокету handler .
    • В цикле while :
      • Вызовы Socket.ReceiveAsync для получения данных от клиента.
      • При получении данных они декодируются и записываются в консоль.
      • response Если сообщение заканчивается на <|EOM|>, подтверждение отправляется клиенту с помощью Socket.SendAsync.

      Запуск примера клиента и сервера

      Сначала запустите серверное приложение, а затем запустите клиентское приложение.

      dotnet run --project socket-server Socket server starting. Found: 172.23.64.1 available on port 9000. Socket server received message: "Hi friends ��!" Socket server sent acknowledgment: "<|ACK|>" Press ENTER to continue. 

      Клиентское приложение отправит сообщение серверу, а сервер ответит подтверждением.

      dotnet run --project socket-client Socket client starting. Found: 172.23.64.1 available on port 9000. Socket client sent message: "Hi friends ��!<|EOM|>" Socket client received acknowledgment: "<|ACK|>" Press ENTER to continue. 

      См. также раздел

      Совместная работа с нами на GitHub

      Источник этого содержимого можно найти на GitHub, где также можно создавать и просматривать проблемы и запросы на вытягивание. Дополнительные сведения см. в нашем руководстве для участников.

      События 407 и 408 регистрируются в журнале событий DNS-сервера.

      В этой статье описано решение проблемы, из-за которой не удается запросить DNS-сервер под управлением Windows 2000.

      Применимо к: Windows 2000
      Исходный номер базы знаний: 279678

      Симптомы

      Вы не можете запросить DNS-сервер под управлением Windows 2000, и в журнале событий приложения для DNS-сервера будут перечислены следующие ошибки:

      Идентификатор события: 407
      Источник: DNS
      Описание: DNS-серверу не удалось привязать сокет датаграммы (UDP) к IP_address. Данные — это ошибка.

      Идентификатор события: 408
      Источник: DNS
      Описание: DNS-серверу не удалось открыть сокет для адреса [IP_address]. Убедитесь, что это допустимый IP-адрес на этом компьютере. Если это недопустимо, используйте диалоговое окно «Интерфейсы» в разделе «Свойства сервера» диспетчера DNS, чтобы удалить его из списка IP-интерфейсов. Затем остановите и перезапустите DNS-сервер. (Если это был единственный IP-интерфейс на этом компьютере и DNS-сервер может не запуститься в результате этой ошибки. В этом случае удалите значение DNS\Parmeters\ListenAddress в разделе служб реестра и перезапустите.) Если это допустимый IP-адрес для этого компьютера, убедитесь, что не запущено ни одно другое приложение (например, другой DNS-сервер), которое будет пытаться использовать DNS-порт.

      Причина

      Эти ошибки могут возникать на компьютерах, на которых на одном сервере установлены обе следующие службы:

      • Преобразование сетевых адресов (NAT)
      • DNS ServerNAT имеет параметр прокси-сервера DNS, который позволяет клиентам DHCP направлять запросы DNS к серверу NAT. Затем клиентские запросы DNS перенаправлять на настроенный DNS-сервер сервера NAT. Прокси-сервер DNS и служба DNS-сервера не могут сосуществовать на одном узле, если хост использует тот же интерфейс и IP-адрес с параметрами по умолчанию.

      Решение

      Чтобы устранить эту проблему, используйте любой из следующих методов:

      • Используйте другой сервер для DNS-сервера вместо установки NAT и DNS-сервера на одном узле.
      • Не используйте функции распределителя DHCP и прокси-сервера DNS в NAT (вместо этого используйте службу DHCP-сервера).
      • Настройте DNS-сервер таким образом, чтобы он не прослушивал IP-адрес сетевого адаптера, который работает в качестве частного интерфейса для NAT. Для этого выполните следующие действия:
        1. Запустите оснастку DNS в консоли управления (MMC), щелкните правой кнопкой мыши DNS-сервер и выберите пункт «Свойства».
        2. Откройте вкладку «Интерфейсы», а затем в разделе «Прослушивание» установите флажок «Только следующие IP-адреса«.
        3. Щелкните IP-адрес, который не должен прослушивать сервер, и нажмите кнопку » Удалить».
        4. Нажмите кнопку « ОК» и закройте оснастку DNS.

      При удалении IP-адреса из списка интерфейсов на DNS-сервере служба DNS-сервера не отвечает на запросы DNS, направленные на этот IP-адрес. Запросы DNS, которые должны быть разрешены DNS-сервером, должны быть направлены в другие интерфейсы, прослушиваемые DNS-сервером.

      Состояние

      Корпорация Майкрософт подтвердила, что это проблема в продуктах Майкрософт, перечисленных в начале этой статьи.

      Обратная связь

      Были ли сведения на этой странице полезными?

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

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

      После новости о том что «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.

      То есть делаем туда:

      1. берём пакет из UDP
      2. добавляем к нему в начале пару байт в которых указан размер этого пакета
      3. отправляем в TCP канал

      И в обратную сторону:

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

      Настраиваем Stunnel

      Настройка простая. Пишем в stunnel.conf:

      [dns] client = yes accept = 127.0.0.1:53 connect = 8.8.8.8:853

      То есть Stunnel:

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

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

      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.

      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 пакета clients[id] = -- записываем адрес отправителя 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 пакета 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, "

      Обе функции сразу после запуска выполняют 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 = 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
      nslookup ya.ru 127.0.0.1
      *** 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.

      ссылки

      1. RFC1035: DOMAIN NAMES — IMPLEMENTATION AND SPECIFICATION
      2. DNS поверх TLS
      3. simple-udp-to-tcp-dns-proxy.lua

      Разработка простого DNS сервера на Go, согласно RFC

      В этой статье я хочу рассказать о своем опыте создания DNS сервера. Разрабатывал я его "чисто повеселиться", при разработке будем придерживаться спецификации RFC.

      DNS сервер

      Сейчас по-быстрому разберемся, в чем принцип работы DNS серверов. Чтобы сейчас читать эту статью, вы зашли на Хабр, для этого в браузере вы ввели www.habr.com, браузер же переводит этот домен в ip адрес, по типу 178.248.237.68:443, чтобы сделать запрос. Домены существуют, чтобы люди не запоминали эти сложные комбинации чисел, а запоминали только привычные нам слова. DNS сервера же переводят эти домены в нормальный для компьютера вид.
      Простая аналогия, телефонная книжка. Вместо того, чтобы запоминать мобильные номера каждого человека, мы создаем контакт и ориентируемся по заданым именам в телефонной книжке.

      DNS протокол

      DNS протокол является прикладным протоколом, который работает поверх UDP. В данном протоколе сущетствуют только один формат, который называется "Сообщение".

      структура DNS сообщения

      То есть DNS-запрос и DNS-ответ имеют одинаковый формат. Размер сообщения - 512 байт, согласно спецификации. Структуру сообщения разберем позже и по порядку.

      Начало разработки

      Для начала поднимем сервер, принимающий UDP запросы и отдающий пустые ответы, чтобы удостовериться будем просто логировать их.

      package main import ( "fmt" "log" "net" ) const Address = "127.0.0.1:2053" func main() < udpAddr, err := net.ResolveUDPAddr("udp", Address) if err != nil < log.Fatal("failed to resolve udp address", err) >udpConn, err := net.ListenUDP("udp", udpAddr) if err != nil < log.Fatal("failed to to bind to address", err) >defer udpConn.Close() log.Printf("started server on %s", Address) // размер бафенра 512 байт согласно спецификации buf := make([]byte, 512) for < size, source, err := udpConn.ReadFromUDP(buf) if err != nil < log.Println("failed to receive data", err) break >data := string(buf[:size]) log.Printf("received %d bytes from %s: %s", size, source.String(), data) response := []byte<> // пустой ответ _, err = udpConn.WriteToUDP(response, source) if err != nil < fmt.Println("Failed to send response:", err) >> > 

      С помощью утилиты nc подключились к UDP серверу и отправили запрос. Про утилиту подробнее можно узнать здесь

      • https://netcat.sourceforge.net/
      • https://habr.com/ru/articles/657613/
      • https://habr.com/ru/articles/678968/
      • https://habr.com/ru/articles/336596/

      Заголовок сообщения

      Как я указывал выше, в сообщении есть 5 секций, сейчас разберем Header (Заголовок)

      заголовок

      Размер заголовка в любом сообщении ВСЕГДА 12 байт, а числа закодированы в формате Big-Endian. Эта информация нам понадобится когда придется парсить и составлять заголовок. Также можно увидеть множество полей в заголовке, но обратим внимание на важные, по-моему мнению:

      • ID, 16 битное значение, ID ответа всегда равен ID запроса
      • QR, значение 1 для ответа и 0 для запроса
      • RCODE, статус ответа, 0 (no error)
      • QDCOUNT, количество запросов/вопросов в секции Questions в сообщении
      • ANCOUNT, количество ответов в секции Answers в ответе

      В Go можем заимплементировать заголовок таким образом:

      type Header struct

      Теперь, когда к нам приходит запрос, нужно распарсить заголовок и перенести данные из заголовка запроса в заголовок ответа. Для этого можем написать функцию для чтения первых 12 байт запроса:

      func ReadHeader(buf []byte) Header < h := Header< ID: uint16(buf[0])> 4), AA: uint16((buf[2] > 7), TC: uint16((buf[2] > 7), RD: uint16((buf[2] > 7), RA: uint16(buf[3] >> 7), Z: uint16((buf[3] > 5), QDCOUNT: uint16(buf[4]) // если в запросе OPCODE не равен нулю, то отправим ответ с кодом ошибки 4 if h.OPCODE == 0 < h.RCODE = 0 >else < h.RCODE = 4 >return h >

      Как видно по коду, приходиться использовать побайтовые сдвиги. Все данные для полей фетчим из заголовка запроса.

      Но нам также надо и закодировать сообщение в байты. Ответное сообщение кодируется сверху вниз, то есть сначала кодируем заголовок потом другие секции. Вот функция для кодировки заголовка:

      func (h Header) Encode() []byte

      Битовые сдвиги наше все!
      Для того, чтобы не запутаться, можно вернуться к этой картинке, где указаны размеры в байтах каждого поля в загаловке

      заголовок

      header := ReadHeader(buf[:12]) log.Printf("ID: %d; QR: %d; QDCount: %d\n", header.PacketID, header.QR, header.QDCount) response := header.Encode() _, err = udpConn.WriteToUDP(response, source)

      После того, как мы распарсили заголовок запроса и закодировали его для ответа, надо как-то протестить то, что мы реализовали. Для этого есть маленький DNS клиент на Python

      import socket def build_dns_query(): header = bytearray([ 0x00, 0x01, # Transaction ID 0x00, 0x00, # Flags: Standard query 0x00, 0x01, # Questions 0x00, 0x00, # Answer RRs 0x00, 0x00, # Authority RRs 0x00, 0x00 # Additional RRs ]) return header def send_dns_query(query, server, port=2053): with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: s.sendto(query, (server, port)) response, _ = s.recvfrom(1024) return response if __name__ == "__main__": dns_server = "127.0.0.1" dns_query = build_dns_query() dns_response = send_dns_query(dns_query, dns_server)

      После запуска скрипта и сервера в логах можно увидеть следующее.
      Дебагер, ну чтобы точно удостовериться)

      Questions

      Запросы (вопросы), как вам удобно, второе поле в каждом DNS запросе, чаще всего количество запросов равно 1, но бывает и несколько запросов/вопросов. Структура запроса имеет куда меньше полей.

      Сейчас размеберем каждую в подробности

      • QName, доменное имя, представленное в виде лейблов, например для habr.com будет два лейбла: habr и com.
      • QType, 16 битное число, которое показывает, что мы хотим получить. Для нашего сервера дефолтом будет значение А, потому что А - адрес хоста, полный список типов тут
      • QClass, 16 битное число, которое показывает класс запроса, например, для нашего сервера дефолтом будет значение IN, потому что IN - the Internet полный список классов тут

      Но в запросе доменное имя отправляется не сплошным текстом, а кодируется в виде последовательности лейблов , где

      • - это один байт, указывающий длину последующего лейбла
      • - сам лейбл
      • \x00 - байт, который указывает на конец последовательности лейблов

      Пример, habr.com будет выглядить так

      \x04habr\x03com\x00

      Теперь можно приступить к имплементации

      Для начала создадим тип для QClass и QType. Конечно можно было задать просто две единицы, но мне такой вариант ближе

      type Class uint16 const ( _ Type uint16 const ( _ Type = iota A NS MD MF CNAME SOA MB MG MR NULL WKS PTR HINFO MINFO MX TXT ) type Question struct

      Как и с заголовком нам нужно распарсить запрос и закодировать его для ответа

      func ReadQuestion(buf []byte) Question < start := 0 var nameParts []string for len := buf[start]; len != 0; len = buf[start] < start++ nameParts = append(nameParts, string(buf[start:start+int(len)])) start += int(len) >questionName := strings.Join(nameParts, ".") start++ questionType := binary.BigEndian.Uint16(buf[start : start+2]) questionClass := binary.BigEndian.Uint16(buf[start+2 : start+4]) q := Question < QName: questionName, QType: Type(questionType), QClass: Class(questionClass), >return q > func (q Question) Encode() []byte < domain := q.QName parts := strings.Split(domain, ".") var buf bytes.Buffer for _, label := range parts < if len(label) >0 < buf.WriteByte(byte(len(label))) buf.WriteString(label) >> buf.WriteByte(0x00) buf.Write(intToBytes(uint16(q.QType))) buf.Write(intToBytes(uint16(q.QClass))) return buf.Bytes() >

      А также видоизменим отправку ответа в main функции

      header := ReadHeader(buf[:12]) log.Printf("ID: %d; QR: %d; QDCount: %d\n", header.PacketID, header.QR, header.QDCount) question := ReadQuestion(buf[12:]) var res bytes.Buffer res.Write(header.Encode()) res.Write(question.Encode()) _, err = udpConn.WriteToUDP(res.Bytes(), source)

      И чуток видоизменим python клиент

      import socket def build_dns_query(domain: str): header = bytearray([ 0x00, 0x01, # Transaction ID 0x00, 0x00, # Flags: Standard query 0x00, 0x01, # Questions 0x00, 0x00, # Answer RRs 0x00, 0x00, # Authority RRs 0x00, 0x00 # Additional RRs ]) question = bytearray() labels = domain.split('.') for label in labels: question.append(len(label)) question.extend(label.encode('utf-8')) question.extend([0x00, 0x00, 0x01, 0x00, 0x01]) # QTYPE and QCLASS (A record, Internet) return header + question def send_dns_query(query, server, port=2053): with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: s.sendto(query, (server, port)) response, _ = s.recvfrom(1024) return response def parse_dns_response(response): print(response) print(response.hex()) if __name__ == "__main__": dns_server = "127.0.0.1" domain = "habr.com" dns_query = build_dns_query(domain) dns_response = send_dns_query(dns_query, dns_server) parse_dns_response(dns_response) 

      После запуска скрипта и сервера можно снова удостовериться в работе

      Распрасенный запрос

      Answers

      Ответ - последнее поле, которое разберем, и очень важное, потому что именно тут будет возвращаться IP адрес хоста.

      Структура ответа

      В ответе мы встречаем знакомые поля, но из новых тут

      • TTL - time-to-live, период времени в секундах, на которое может закеширироваться на сервере, размер 32 бита
      • RDLENGHT - длина RDATA, так как IP адрес это 4 байта, то будет равно 4, размер 32 бит
      • RDATA - значение, которое является ответом на запрос, в нашем случа IP адрес, к примеру 8.8.8.8

      Пример имплементации ответа и, само собой, метод для кодировки

      type Answer struct < Name string Type Type Class Class TTL uint32 Length uint32 Data [4]uint8 >func (a Answer) Encode() []byte < var rrBytes []byte domain := a.Name parts := strings.Split(domain, ".") for _, label := range parts < if len(label) >0 < rrBytes = append(rrBytes, byte(len(label))) rrBytes = append(rrBytes, []byte(label). ) >> rrBytes = append(rrBytes, 0x00) rrBytes = append(rrBytes, intToBytes(uint16(a.Type)). ) rrBytes = append(rrBytes, intToBytes(uint16(a.Class)). ) time := make([]byte, 4) binary.BigEndian.PutUint32(time, a.TTL) rrBytes = append(rrBytes, time. ) rrBytes = append(rrBytes, intToBytes(a.Length). ) ipBytes, err := net.IPv4(a.Data[0], a.Data[1], a.Data[2], a.Data[3]).MarshalText() if err != nil < return nil >rrBytes = append(rrBytes, ipBytes. ) return rrBytes > 

      Так как мы не можем запарсить ответ, то мы просто прокинем создание структуры, а также создадим мапу, где будем хранить соотношение домена к его IP

      айпишники

      answer := Answer < Name: question.QName, Type: A, Class: IN, TTL: 0, Length: net.IPv4len, Data: nameToIP[question.QName], >var res bytes.Buffer res.Write(header.Encode()) res.Write(question.Encode()) res.Write(answer.Encode()) _, err = udpConn.WriteToUDP(res.Bytes(), source)

      После запуска Python скрипта можно увидеть наш полученный IP адрес

      ip

      Резюме

      Ну подводя итоги, разработали минимальный по умениям рабочий DNS сервер.
      Надеюсь вам понравилась эта статья!

      P.S. Возможно много опечаток, не судите строго

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *