Преобразователь DNS, простой и понятный язык Go

Go

Так как время завершения статьи относительно случайное, прежде всего, я желаю всем вам счастливого Рождества, счастливого Нового года и счастливого Нового года! Недавно при написании небольшого скрипта с DNS-запросом столкнулся с требованием указать DNS-сервер для получения результата разрешения доменного имени. Поскольку Go использовался в качестве основного языка в течение последних двух лет, эта сцена просто популяризировала этот волшебный язык, созданный отцом Google. На Github уже есть много полных и зрелых библиотек классов, связанных с DNS, написанных Даниэлем.Если вам интересно, вы можете взглянуть.

DNS-протокол

Будучи древним интернет-протоколом (в этой статье упоминаются RFC 1034 и 1035, опубликованные в 1987 г.), структура протокола DNS относительно проста.

Определение структуры сообщения DNS в RFC выглядит следующим образом:

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

Header

Давайте сначала посмотрим на душу в протоколе, Заголовок:

Это, ну, давайте кратко объясним:

1.ID

Идентификатор сообщения не обязан быть уникальным, он вообще генерируется и управляется уровнем реализации протокола DNS (команда nslookup под Win инкрементируется с 0x0001. Если реализовать самому, то можно просто написать мертвую, такую как 0x1024), и ответный пакет не будет изменен. , поэтому полезно, если вам нужно различать несколько клиентских вызовов

2.FLAG

oQR

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

oOpcode

Идентификация пакета запроса, пакет ответа не будет изменен

0为标准的正向查询 1为反向查询 2为请求服务器状态 剩余为预留

oAA

Идентификатор в ответном пакете, указывающий, отвечает ли авторитетный сервер

oTC

Сообщение слишком длинное и усеченное

oRD

Идентифицировать рекурсивный пакет ответа на запрос не будет изменен

oRA

Поддерживает ли сервер рекурсивную идентификацию запроса

oZ

сдержанный

oRCODE

Идентификатор ответного пакета

0为正常 1为格式错误 2为服务器故障 3为域名错误 4为不支持 5为拒绝 剩余为预留

3.QDCOUNT

Количество вопросов в разделе «Вопросы»

4.ANCOUNT

Количество вопросов в разделе «Ответы»

5.NSCOUNT

Количество серверов имён в разделе Authority

6.ARCOUNT

Дополнительные записи в разделе Дополнительно

Зная, как выглядит заголовок, нам нужно определить структуру в Go (которую можно рассматривать как класс в Java), чтобы представить его.

Подождите, Go описывается как четырехмерный язык, который выглядит как C/C++ и использует Python, как Python.Он также имеет концепцию пакетов.Следующие преимущества для студентов, изучающих Java:

package dns // помещаем в первую строку каждого файла go, только простое имя

// !!!不需要以分号结尾,不需要java.lang形式(由文件路径定位,和Python类似)!!!

Я полагаю, что для определения структуры друзья, играющие в C/C++, знакомы с:

type Header struct {
ID uint16 // !!!Не нужно заканчивать точкой с запятой!!!
    Flag    uint16
    QDCount uint16
    ANCount uint16
    NSCount uint16
    ARCount uint16
}}

В блоке кода, который представляет структуру выше, есть две основные точки знания Go, которые необходимо понять:

1. Тип данных

oбулев тип

bool

o Тип номера

int, int8, int16, int32, int64

        uint, uint8, uint16, uint32, uint64

        float32, float64, complex64, complex128

        byte, rune, uintptr

тип oString

  string

oКомпозитные типы

Массив (массив)

Карта (коллекция)

Ломтик

Структура (структура)

Интерфейс

  Функция

Канал

2. Имя

oНоменклатура CamelCase, первый символ может быть любым Unicode или символом подчеркивания, остальные символы могут быть любыми Unicode, цифрами или символами подчеркивания, а длина не ограничена

oПри именовании пользовательских глобальных констант/переменных, структур, интерфейсов, функций/методов, если первый символ является заглавной буквой, к объекту можно получить доступ вне пакета, в противном случае его можно использовать только внутри пакета

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

func (h *Header) SetFlag(qr, opcode, aa, tc, rd, ra, rcode uint16) {
    h.Flag = qr<<15 + opcode<<11 + aa<<10 + tc<<9 + rd<<8 + ra<<7 + rcode
}

В домене пакета для определения функции-члена вам нужно всего лишь добавить структуру или указатель структуры между ключевым словом func и именем функции.Немного похоже на определение функции-члена вне класса в C/C++, функция использует . для доступа к членам объекта. Конечно, если эта переменная отсутствует, это обычная функция, соответствующая приведенным выше правилам именования, и не может напрямую обращаться к членам какой-либо структуры.

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

И операторы Go также очень просты.Есть также арифметика, отношение, логика, бит и присваивание.Они в основном аналогичны другим основным языкам.Что касается вышеприведенной концепции операции сдвига влево,я не буду ее здесь представлять.Чистые ученики могут проверить информацию самостоятельно.

Кроме того, идут также сохранить * (указатель) и & (адрес), связанный с двумя операторами указателя.

Question

Далее идет ядро ​​протокола, Вопрос:

1.QNAME

Это доменное имя, которое будет разрешено DNS-сервером.

2.QTYPE

Тип запроса. Тип TYPE, используемый в ответном пакете, является подмножеством QTYPE. Перечислите несколько наиболее распространенных. Остальные можно проверить в соответствии с вашими потребностями.

oA

1, адрес хоста

oNS

2. Авторитетный сервер доменных имен

oCNAME

5, псевдоним

oMX

15. Обмен почтой

oTXT

16, текстовая строка

o*

255, все записи

3.QCLASS

Класс запроса, CLASS, совместно используемый с пакетом ответа, является подмножеством QCLASS.

oIN

1, Интернет (обычно используют это)

oCS

2. КСНЕТ

oCH

3, ХАОС

oHS

4. Гесиод

o*

255, любой класс

Для вышеупомянутых инвариантных значений, таких как TYPE и CLASS, мы можем использовать соответствующие значения непосредственно в программе.Более стандартизированный подход — использовать константы, чтобы сделать их читабельными:

// сформировать один

const (
    TypeA     = 1
    TypeNS    = 2
    TypeCNAME = 5
    TypeMX    = 15
    TypeTXT   = 16
    ClassIN = 1
    ClassCS = 2
    ClassCH = 3
    ClassHS = 4
)

// формируем два

const QTypeAny = 255
const QClassAny = 255

Поскольку длина байтов QNAME вопроса является переменной, вы можете использовать тип слайса Go для описания структуры при определении структуры:

type Question struct {
QName []byte // байтовый срез
    QType  uint16
    QClass uint16
}

Слайс — это своего рода «динамический массив», встроенный в Go. Он содержит два свойства: начальную длину и емкость, где емкость является необязательным параметром при создании слайса:

// используем ключевое слово make для создания среза
// s := make([]byte, 10)
// s := make([]byte, 10, 100)
// Также можно использовать другую форму создания и инициализации слайса
// s := []byte{}
// Для добавления элементов сверх предопределенной длины вы можете использовать встроенную функцию append()
// s = append(s, byte(0))
Типы коллекций Go также используются аналогичным образом:
// m := make(map[string]string)
// m := map[string]string{}
// m["key"] = "value"
// Обычно значение соответствующего ключа на карте можно получить напрямую, используя индекс
// Но вы также можете использовать две переменные для получения результата индекса, вторая переменная указывает, есть ли соответствующий ключ в Map
// value, ok := m["key"]

В протоколе DNS каждый строковый сегмент в доменном имени называется меткой, и его представление — не www.domain.com, а 3www6domain3com, то есть длина метки + содержимое метки. Поэтому мы определяем функцию-член Question для преобразования обычных строк доменных имен:

// Другие пакеты можно импортировать с помощью ключевого слова import
import (
    "bytes"
    "encoding/binary"
)
// ...
func (q *Question) SetQName(qname string) {
    var buf bytes.Buffer
    for _, n := range strings.Split(qname, ".") {
binary.Write(&buf, binary.BigEndian, byte(len(n))) // длина метки
  binary.Write(&buf, binary.BigEndian, []byte(n)) // содержимое тега
    }
binary.Write(&buf, binary.BigEndian, eof) // конец на 0x00
    q.QName = buf.Bytes()
}

Синтаксис for в коде содержит несколько точек знаний Go:

1.strings.Split () возвращает индекс, а строки _ означает игнорировать, а именно после взятия только разделить строку, так как Go требует, чтобы все определенные переменные должны были использоваться

2.:= — это определение и назначение переменных в Go, что эквивалентно:

var n string
for _, n = range strings.Split(qname, ".") {
    // ...
}

3. Если переменная определена вне функции, можно использовать только форму var. Кроме того, при повторном назначении той же переменной вам нужно только =

4. Ключевое слово range подходит для обхода таких типов данных, как массивы, срезы, наборы и каналы, подобно range() в Python.

Answer

Затем ключ к протоколу, Ответ:

• NaN Е

То же, что QNAME вопроса

•тип

Тип подмножества в Qtype с вопросами

•сорт

Подмножество CLASS в QCLASS того же вопроса

• ТТЛ

Время кэширования до удаления записи ресурса, в секундах

•Длина РД

Длина Rdata

• R-данные

Это проанализированный результат, если тип запроса — запись A, его значение должно быть IP-адресом в 4-байтовом шестнадцатеричном формате.

Здесь стоит отметить два момента:

1. Запрос может соответствовать нескольким результатам, и ресурсная запись каждого результата будет соответствовать структуре ответа, то есть нам нужно определить, сколько ответов существует в ответном пакете в соответствии со значением ANCOUNT в заголовке ответный пакет

2. Ответ, Полномочия и Дополнительные имеют одинаковую структуру, официально называемую Ресурсной Записью, но конкретная структура RDATA будет отличаться в зависимости от типа запроса.Для второго пункта общее решение для объектно-ориентированных языков заключается в использовании дженерики. Но извините, Go не поддерживает дженерики, так что давайте определим интерфейс для описания

type resourceData interface {
    value() string
}

При определении свойств или переменных в структуре вы можете напрямую использовать интерфейсы как типы данных:

type Resource struct {
    Name     []byte
    Type     uint16
    Class    uint16
    TTL      uint32
    RDLength uint16
    RData    resourceData
}

Интерфейс в Go тоже ненавязчивый, любому объекту нужно всего лишь реализовать все функции, определенные в интерфейсе, а это значит, что объект реализует интерфейс:

type rdataA struct {
    addr [4]uint8
}
func (r *rdataA) value() string {
    return fmt.Sprintf("%d.%d.%d.%d", r.addr[0], r.addr[1], r.addr[2], r.addr[3])
}

Для типов запросов, таких как NS и CNAME, структура одинакова, и оба они являются доменным именем, поэтому вы можете определить базовый класс и позволить им наследоваться. К сожалению, Go также не поддерживает наследование, но мы можем использовать композицию (похоже, инженеры Google напечатали в уме шаблоны проектирования, и они хорошо это знают, будь то Android или Go, от базового ядра до интерфейса). много очевидных следов использования шаблонов проектирования) для достижения:

// Листовой объект (базовый класс)
type rdataDomain struct {
    name []byte
}
func (r *rdataDomain) value() string {
    var labels []string
    for i := 0; i < len(r.name)-1; {
        l := int(r.name[i]) //3
        labels = append(labels, string(r.name[i+1:i+l+1]))
        i += l + 1
    }
    return strings.Join(labels, ".")
} // составной объект
type rdataNS struct {
rdataDomain // Анонимный лист, который можно рассматривать как наследование, напрямую владеет всеми свойствами и функциями листового объекта
// также можно описать с помощью обычных свойств
    // domain rdataDomain
// Конечно, другие свойства, уникальные для текущей структуры, также могут быть определены
}
type rdataCNAME struct {
    rdataDomain
}

Что касается применения интерфейса, включая утверждение, когда интерфейс преобразуется в общий тип, мы можем узнать об этом, определив функцию SetRData() для Resource:

func (r *Resource) SetRData(rdata, data []byte) error {
var rd resourceData // тип интерфейса
    switch r.Type {
    case TypeA:
  rd = new(rdataA) // нормальный тип интерфейса неявный
        if len(rdata) != 4 {
            return errors.New("invalid resource record data")
        }
        for i, d := range rdata {
   
            binary.Read(bytes.NewBuffer([]byte{d}), binary.BigEndian, &rd.(*rdataA).addr[i])
// Утверждения и приведения разные, приведения в Go используются для преобразования между распространенными типами
// Разумеется, это должен быть тип, который можно преобразовать друг в друга
            // var a float64 = 1
            // b := int(a)
        }
    // ...
    }
    r.RData = rd
    return nil
}

В приведенном выше примере также скрыта функция Go, и это ошибки. Go строго разграничивает понятия ошибки и паники и считает, что ошибки являются частью бизнес-процесса, а исключения — нет (прямо противоположно Exception и Error в Java).

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

if err := r.SetRData(rdata, data); err != nil {
// обрабатываем ошибку
}

Согласно философии дизайна Go «меньше значит больше», только тогда, когда необходимо уведомить верхний уровень для необходимой обработки исключений, возвращается ошибка или возникает паника. В противном случае всегда рассматривайте возможность использования описания в виде статуса, количества и т. д. и не злоупотребляйте им.

передача информации

Наконец, мы определяем функцию для завершения пакета, отправки и распаковки:

func Ask(server, qname string) ([]net.IP, error) {
    var names []net.IP
reqData := pack(TypeA, qname)//pack
// Использовать официальный пакет NET для UDP-подключения
    conn, err := net.Dial("udp", server+":53")
    if err != nil {
        return nil, err
    }
отложить conn.Close() // отложить обработку
    conn.SetDeadline(time.Now().Add(time.Second * 3))
  // отправить пакет
    if i, err := conn.Write(reqData); err != nil || i <= 0 {
        return nil, err
    }
ответы, ошибка := unpack(conn) // распаковать
    if err != nil {
        return nil, err
    }
    for _, a := range answers {
        if a.Type != TypeA {
            continue
        }
        if ip := net.ParseIP(a.RData.value()); ip != nil {
            names = append(names, ip)
        }
    }
    return names, nil
}

Среди них официальный сетевой пакет не будет объясняться слишком много.Вы можете перейти к документации.Здесь мы сосредоточимся на ключевом слове defer.

Отсрочка означает отсрочку, за которой следует функция (вы также можете использовать анонимные функции для сборки нескольких действий), то есть отложить до тех пор, пока она не будет вызвана перед возвратом. defer — это стековая структура, когда определено несколько отложений, они вызываются в порядке «первым пришел, последним вышел». Студенты, знакомые с Java, могут сравнить его с синтаксисом finally, но finally действует только на блок кода try, а defer действует на всю функцию.

пакет

Еще раз взгляните на упакованную функцию:

func pack(qtype uint16, qname string) []byte {
// запечатать заголовок
// создание экземпляра структуры
    header := Header{
ID: 0x0001, // !!!Определение новой строки должно заканчиваться запятой!!!
        QDCount: 1,
        ANCount: 0,
        NSCount: 0,
        ARCount: 0,
    }
// можно использовать и другую форму
    // header := new(Header)
    // header.ID = 0x0001
    // header.QDCount = 1
    header.SetFlag(0, 0, 0, 0, 1, 0, 0)
// печать Вопрос
    question := Question{
        QType:  qtype,
        QClass: ClassIN,
    }
    question.SetQName(qname)
    var buf bytes.Buffer
    binary.Write(&buf, binary.BigEndian, header)
    binary.Write(&buf, binary.BigEndian, question.QName)
    binary.Write(&buf, binary.BigEndian, []uint16{question.QType, question.QClass})
    return buf.Bytes()
}

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

Распаковка

Разбираем полученный ответный пакет, достаем нужные нам данные, и готово:

func unpack(rd io.Reader) ([]*Answer, error) {
    var (
        reader = bufio.NewReader(rd)
data data []byte // буфер пакета ответа
  buf []byte // временный буфер
        err    error
        n      int
    )
// удалить заголовок
    // ...
// Вопрос о сносе
    question := new(Question)
if buf, err = reader.ReadBytes(eof);err != nil { // имя домена заканчивается на 0x00
        return nil, err
    }
    data = append(data, buf...)
    question.QName = buf
    buf = make([]byte, 4)
    if n, err = reader.Read(buf); err != nil || n < 4 {
        return nil, err
    }
    data = append(data, buf...)
    binary.Read(bytes.NewBuffer(buf[0:2]), binary.BigEndian, &question.QType)
    binary.Read(bytes.NewBuffer(buf[2:]), binary.BigEndian, &question.QClass)
// удалить ответ(ы)
    answers := make([]*Answer, header.ANCount)
    buf, _ = reader.Peek(59)
for i := 0;i         answer := new(Answer)
        // NAME
        var b byte
        var p uint16
        for {
            if b, err = reader.ReadByte(); err != nil {
                return nil, err
            }
            data = append(data, b)
если b&указатель == указатель { // указатель представляет собой байтовую константу со значением 0xC0
                buf = []byte{b ^ pointer, 0}
                if b, err = reader.ReadByte(); err != nil {
                    return nil, err
                }
                data = append(data, b)
                buf[1] = b
                binary.Read(bytes.NewBuffer(buf), binary.BigEndian, &p)
                if buf = getRefData(data, p); len(buf) == 0 {
                    return nil, errors.New("invalid answer packet")
                }
                answer.Name = append(answer.Name, buf...)
                break
            } else {
                answer.Name = append(answer.Name, b)
                if b == eof {
                    break
                }
            }
        }
// ТИП, КЛАСС, TLL, RDLENGTH и другие данные
        // ...
        // RDATA
        buf = make([]byte, int(answer.RDLength))
        if n, err = reader.Read(buf); err != nil || n < int(answer.RDLength) {
            return nil, err
        }
        data = append(data, buf...)
// Вызов ранее определенной функции SetRData() для обработки различных типов RDATA
        if err = answer.SetRData(buf, data); err != nil {
            return nil, err
        }
        answers[i] = answer
    }
// Удалить полномочия и дополнительные, если они есть
    return answers, nil
}

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

сжатие сообщений

При проектировании протокола DNS для уменьшения размера пакета данных была специально добавлена ​​схема сжатия сообщений (используется она или нет определяется реализацией DNS-сервера).Посмотрим непосредственно на исходный текст RFC:

In order to reduce the size of messages, the domain system utilizes a compression scheme which eliminates the repetition of domain names in a message. In this scheme, an entire domain name or a list of labels at the end of a domain name is replaced with a pointer to a prior occurance of the same name.

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

1. Строка тегов, оканчивающаяся на 0x00

2. Указатель

3. Строка тегов, заканчивающаяся указателем

То есть это может выглядеть так:

3www6domain3com

Это также может выглядеть так:

[pointer]

Можно и длиннее:

3www6domain[pointer]

Это, гм, хорошо, пока дизайнер доволен.

Тогда давайте внимательно посмотрим, как работает этот указатель. Во-первых, это его структура:

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

Поэтому, когда мы обрабатываем доменное имя в пакете данных, мы можем только пройти каждый байт и выполнить побитовую операцию И с ним и 0xC0.Если результат равен 0xC0, считается, что он формирует указатель с последующим байтом, и затем он выполняет побитовую операцию XOR с 0xC0 и объединяет его со следующим байтом для получения смещения, в противном случае он завершает обход до 0x00 (если описание этого процесса расплывчато, рекомендуется понять предыдущий для блока кода ).

Получив смещение, не наивно полагайте, что путем прямого смещения соответствующих байтов в наших кэшированных ответных данных можно успешно прочитать оставшиеся теги доменного имени, оканчивающиеся на 0x00. Потому что эта схема сжатия является рекурсивной! То есть строка метки, которую вы получаете со смещением, также может заканчиваться указателем! Кого Бог простит?

В этом случае давайте напишем функцию для обработки этого отдельно:

func getRefData(data []byte, p uint16) []byte {
    var refData []byte
// пройти буфер пакета ответа от начального смещения
    for i := int(p); i < len(data); i++ {
// прочитать новый указатель
        if b := data[i]; b&pointer == pointer {
            if i+1 >= len(data) {
                return []byte{}
            }
// Обновляем смещение и продолжаем обход
            binary.Read(bytes.NewBuffer([]byte{b^pointer, data[i+1]}), binary.BigEndian, &p)
            i = int(p - 1)
        } else {
            refData = append(refData, b)
// Читаем 0x00 до конца
            if b == eof {
                break
            }
        }
    }
    return refData
}

Пока что мы можем в основном удовлетворить наши потребности в разрешении доменных имен для получения IP.Обработка полномочий и дополнительных в основном такая же, как и у ответа, поэтому мы не будем продолжать здесь.

звонок в библиотеку

Теперь мы можем ввести пакет dns в наш собственный скрипт для вызова объектов в нем:

package main // Если вам нужно определить функцию входа main(), она должна быть помечена как основной пакет
import "github.com/gyyyy/dns"
// ...
// функция входа
func main() {
    if ips, err := dns.Ask("8.8.8.8", "www.domain.com"); err == nil {
  // Получаем проанализированный список IP
    }
}

Если мы хотим разрешать в пакетном режиме и установить небольшую цель, 100 миллионов доменных имен, наблюдение за тем, как они выполняются одно за другим, определенно убьет людей. Тогда давайте, наконец, взглянем на светящееся кольцо высокой параллельности, которое поставляется с Go, созданным для облачных вычислений:

func getDomain() chan string {
ch:= make (chan string, 10) // создаем канал, буферное пространство 10, для передачи данных между горутинами
go func() { // используем ключевое слово go для создания новой горутины для выполнения анонимной функции, то есть многопоточности
        for _, domain := range domainList {
            ch <- domain
        }
close(ch) // закрываем канал
    }()
    return ch
}
func main() {
var wg sync.WaitGroup // Объект в официальном пакете синхронизации, который можно использовать для блокировки основного потока и ожидания окончания выполнения всех горутин
ch := getDomain() // логика в этом методе выполняется асинхронно
for c := range ch { // цикл до закрытия канала
        wg.Add(1)
go func(domain string) { // создаем отдельную горутину для каждого доменного имени
отложить wg.Done() // эквивалентно wg.Add(-1)
            if ips, err := dns.Ask("8.8.8.8", domain); err == nil {
                // ...
            }
        }(c)
    }
    wg.Wait()
}

Конечно, вы также можете создать несколько производителей, потребителей, пулов ресурсов и несколько общедоступных DNS-серверов для улучшения параллелизма, просто как насчет вашего процессора и сети;)

Ссылаться на

1.woohoo.RFC-editor.org/PDF RFC/RFC1…

2.woohoo.RFC-editor.org/PDF RFC/RFC1…

3.GitHub.com/av Эли нет/аве…

4.github.com/miekg/dns/

5.GitHub.com/Пусть Галлей/Компьютер…