Так как время завершения статьи относительно случайное, прежде всего, я желаю всем вам счастливого Рождества, счастливого Нового года и счастливого Нового года! Недавно при написании небольшого скрипта с 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булев тип
boolo Тип номера
int, int8, int16, int32, int64uint, uint8, uint16, uint32, uint64
float32, float64, complex64, complex128
byte, rune, uintptr
тип oString
stringoКомпозитные типы
Массив (массив)Карта (коллекция)
Ломтик
Структура (структура)
Интерфейс
Функция
Канал
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 = 255const 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 для преобразования обычных строк доменных имен:
// Другие пакеты можно импортировать с помощью ключевого слова importimport (
"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 stringfor _, 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…