AVR+ENC28J60 DNS - история внедрения

Прочитал я цикл статей и потребовалось мне внедрить очередную плюшку в виде распознавания IP адреса по имени хоста. Пришлось поломать голову над передачей запроса на сервер, а потом еще и как расковырять ответ от сервера — оформление кода чуток страдает — но все работает. Код полностью совместим с предыдущими топиками.
Все запросы на DNS сервер отправляют при помощи UDP или TCP пакетов. TCP использовать религия не позволяет почему-то, разбираем работу на уровне UDP. В силу того что я использовал связку с DHCP — будем все паковать в структуры аналогично тому как делали при работе с NTP сервером.

Первое — структура запроса к DNS:

typedef struct dns_query {
	uint16_t TransactionID;		//Transaction identifier
	uint16_t Flags;				//Flags for query
	uint16_t Questions;			//Number of questions
	uint16_t AnswerRS;			//Number of answers
	uint16_t AuthorityRR;		//Number of authorities sections
	uint16_t AdditionalRR;		//Number of additional sections
	uint8_t	 Queries[];			//Query/Answers for name resolving
} dns_query_t;


Все флаги объявлены в хидере, посмотрите — при обычном разрешении запроса ИМЯ в IP адрес — ничего править не придется. Но понимать что и откуда надо.
На момент выполнения запроса мы уже должны иметь IP адрес и знать адрес DNS сервера (его может выдать DHCP или можно самостоятельно прописать ручками).
Одно из самых заморочных полей в этой структуре — Queries[]. Туда нужно в определенном формате впихнуть имя узла. Формат следующий:
ИМЯ УЗЛА
ТИП УЗЛА
КЛАСС УЗЛА

Тип и класс узла — вполне себе двухбайтовые переменные, фиксированной длины. А вот ИМЯ — имеет определенный формат. Длина переменная. Разработчики закодировали его следующим образом:
  • все буквы должны быть ЗАГЛАВНЫМИ
  • точки не допускаются, но являются разделителями
  • первым байтом идет длина до разделителя, затем сам разделитель
  • запрос заканчивается нулевым байтом — 0х00

Ничего не понятно, мне тоже, давайте на примере: попробуем узнать IP адрес google.com. Запрос имени будет выглядеть следующим образом:
6GOOGLE3COM0


Понятнее стало? Нет? Тогда внизу есть ссылки на первоисточники — идем втыкать что и как.
ТИП и КЛАСС узла для нас определен — 0х0001 — имеет название TYPE A — т.е. HOST — этими данными мы сообщаем DNS'у что мы пытаемся распознать именно адрес узла, а не его домена, адрес DNS сервера который нам выдал адрес хоста или еще какую-то информацию.

Ну что же — давайте теперь попробуем упаковать все это. В файл lan.h добавим следующее (все флаги прописал — хоть и не использовал):


#define WITH_DNS
..............................................................
#ifndef WITH_DNS
#	define IP_DNS				inet_addr(192,168,0,1)
#endif

..............................................................

/*
	*	DNS
*/

#define DNS_PORT				htons(53)
#define DNS_CLIENT_PORT			htons(53)
#define DNS_RECURSIVE_QUERY		1

typedef struct dns_query {
	uint16_t TransactionID;		//Transaction identifier
	uint16_t Flags;				//Flags for query
	uint16_t Questions;			//Number of questions
	uint16_t AnswerRS;			//Number of answers
	uint16_t AuthorityRR;		//Number of authorities sections
	uint16_t AdditionalRR;		//Number of additional sections
	uint8_t	 Queries[];			//Query/Answers for name resolving
} dns_query_t;


//DNS FLAGS POSSIBLE
#define DNS_FLAG_QUERY				0x00
#define DNS_FLAG_RESPONSE			0x01
#define DNS_FLAG_STANDART_QUERY		(0x00<<1)
#define DNS_FLAG_INVERSE_QUERY		(0x04<<1)
#define	DNS_FLAG_NAUTH_ANSWER		(0x00<<5)
#define DNS_FLAG_AUTH_ANSWER 		(0x01<<5)
#define DNS_FLAG_MES_NTRUNC 		(0x00<<6)
#define DNS_FLAG_MES_TRUNC 			(0x01<<6)
#define DNS_FLAG_NRECURSIVE_QUERY	(0x00<<7)
#define DNS_FLAG_RECURSIVE_QUERY	(0x01<<7)
#define DNS_FLAG_RECURSION_NAV		(0x00<<8)
#define DNS_FLAG_RECURSION_AV		(0x01<<8)	//Recursion available in query
#define DNS_FLAG_ANSWER_NAUTH		(0x00<<10)
#define DNS_FLAG_ANSWER_AUTH	 	(0x01<<10)	//Answer authorized


//DNS FLAG ERRORS (in answer section)
#define DNS_FLAG_ERROR_OK				(0x00<<12)	//Answer ok, no error
#define DNS_FLAG_ERROR_FORMAT_ERROR		(0x04<<12)	//Packet format error
#define DNS_FLAG_ERROR_SERVER_FAILURE	(0x02<<12)	//Server failure
#define DNS_FLAG_ERROR_NOT_EXIST		(0x01<<12)	//Answer ok, no error


//Query name types
#define DNS_QUERY_TYPE_A			0x01 	//IP address
#define DNS_QUERY_TYPE_NS			0x02	//DNS Server
#define DNS_QUERY_TYPE_CNAME		0x05	//Canonical name
#define DNS_QUERY_TYPE_PTR			0x0C	//Pointer
#define DNS_QUERY_TYPE_HINFO		0x0D	//Host info
#define DNS_QUERY_TYPE_MX			0x0F	//Mail exchange record
#define DNS_QUERY_TYPE_AXFR 		0xFC	//Zone request for transfering
#define DNS_QUERY_TYPE_ANY 			0xFF 	//Query for all records transfering


В файл lan.c, добавим вот такой код:

#ifdef WITH_DNS

uint8_t	dns_query_id = 0;
uint8_t dns_query_length = 0;

uint8_t send_DNS_query(char* dnsName)
{
	eth_frame_t *frame = (void*)net_buf;
	ip_packet_t *ip = (void*)(frame->data);
	udp_packet_t *udp = (void*)(ip->data);
	dns_query_t *dns = (void*)(udp->data);

	uint8_t dns_len_pos, dns_len, cnt, pn, dat;

	//ip->to_addr = inet_addr(8,8,8,8);
	ip->to_addr = ip_gateway;
	udp->to_port = DNS_PORT;
	udp->from_port = DNS_PORT;

	//Header 
	dns->TransactionID = htons(dns_query_id++);
        dns->Flags = htons((uint16_t) (DNS_FLAG_QUERY|DNS_FLAG_RECURSIVE_QUERY|DNS_FLAG_RECURSION_AV));
	dns->Questions = htons(1);
	dns->AnswerRS = 0;
	dns->AuthorityRR = 0;
	dns->AdditionalRR = 0;

	//Data for request
	cnt = 1;
	pn=0;
	dns_len_pos = 0;
	dns_len=0;

	while ((dat = *(dnsName++))) {

				xprintf(PSTR("%c"), dat);
                if (dat=='.'){
                        dns->Queries[pn+dns_len_pos]=dns_len; // fill the length field
                        xprintf(PSTR(" %d"), dns_len);
                        dns_len=255;
                        dns_len_pos=cnt;
                        //continue;
                }
                dns->Queries[pn+cnt]=dat;
                dns_len++;
                cnt++;
        }
        xputs(PSTR("\n"));
    dns->Queries[pn+dns_len_pos]=dns_len;
    dns->Queries[cnt] = 0; //Finishing name query

    //cnt++;
    dns->Queries[cnt+1] = 0;
    dns->Queries[cnt+2] = DNS_QUERY_TYPE_A;
    dns->Queries[cnt+3] = 0;
    dns->Queries[cnt+4] = DNS_QUERY_TYPE_A;

    dns_query_length = cnt+4;	//Save the request length for response disassembly in future


	if (!udp_send(frame, (cnt+5+12))) {
		//xputs(PSTR("DNS SEND ERROR\n"));
		return 1;
	} else {
		//xputs(PSTR("DNS REQUEST SENT\n"));
		return 0;
	}	
}


По коду достаточно много закомменченных printf — для дежукинга, и кому что непонятно — глянуть, что и зачем.

Смотрим в сниффер:


Видим что запрос на сервер отправился, заполнение его соответствует тому о чем говорили выше. Кстати wireshark — вполне хорошо анализирует пакеты, и при косяке запросто скажет Вам о том что у Вас косяк.

Едем дальше — разбор пакета, полученного от DNS задачка впринципе тривиальная. НО с нюансами.
Смотрим что нам ответил сервер:

наотвечал он нам столько, что аж глаза разбегаются — прежде всего, анализируем поля заголовка — AuthorityRR, AdditionalRR, AnswerRS — если они пустые — то ловить нам нечего и ответ на запрос можно отправлять в топку. Еще нюанс — разные сервера могу возвращать ответы в разные части заголовка, т.е. если сделать запрос к яндексу — все будет разложено совершенно в других местах. По-этому анализировать надо все секции, до тех пор, пока не найдем нужную нам.
Конечно же надо анализировать еще и флаги ошибок заголовка — там содержится код ошибки (как это не тривиально) если что не так.
Далее необходимо разобрать каждую секцию. Тут тоже все просто, формат секции:
ИМЯ - 2 байта в сжатом виде (по документации вид может быть и не сжатый, но я с таким не сталкивался - трафиик экономят
ТИП - 2 байта - то о чем говорили выше - нас интересует TYPE A
КЛАСС - 2 байта - см пред поле
TTL - 4 байта - время жизни данной связки ИМЯ-АДРЕС
Длина - 2 байта - поле описывает сколько в след поле байт данных (в нашем случае требуется 4 байта, мы ведь используем TCP/IP v4 - да и интересует нас только IP адрес)
Данные


Тут я быстренько накидал код, который проверял бы валидность поля TYPE на соответствие типу и был счастлив с блаженной улыбкой на лице, дергая туда-сюда (не то что Вы подумали) кабель Ethernet из устройства, наблюдать как у меня постоянно возвращался IP запрашиваемого хоста. Пока не увидел что IP изменился…
Начал разбираться, и выяснил вот такую штуку — очень часто в запросе возвращался IP не только хоста, но и сервера имен, и тип записи тоже был TYPE A. На скриншотике я выделил данную секцию:

Тут надо заметить что информация о том что это не имя запрашиваемого хоста в данной секции все-таки есть, но она закодирована 2 байтами, я долго не мог воткнуть что она означает (0xC00C), а оказалось все просто. В данных двух байтах, первые 2 бита используются как признак того что используется сокращенный тип записи (0b1100000 = 0xC0). Остальное — это смещение от начала пакета, где начинается это имя — в ответе на запрос есть вся информация, которую мы отправляли запросом. Заголовок имеет длину 12 байт, в hex — это 0х0С. Уф — опять мудрено. Смотрим на скриншот:


Таким образом при анализе ответа надо смотреть на поле имя — оно должно быть 0xC00C, на поле ТИП -нам нужен TYPE A, и длину данных — 4 байта. Данный алгоритм реализуется следующий код:


uint8_t dns_filter(void)
{
	uint16_t cnt;

	//xputs(PSTR("DNS filter\n"));

	eth_frame_t *frame = (void*)net_buf;
	ip_packet_t *ip = (void*)(frame->data);
	udp_packet_t *udp = (void*)(ip->data);
	dns_query_t *dns = (void*)(udp->data);

	//Disassembly packet for reply
	//Check for query ID
	if (dns->TransactionID =! (dns_query_id-1)) 
	{
		//xputs(PSTR("DNS: Query ID is OUR\n"));		
		return 1;
	}	

	//Check server for recursion - answer without recursion can't be used
	if ((dns->Flags&DNS_FLAG_RECURSIVE_QUERY) != DNS_FLAG_RECURSIVE_QUERY) {
		//xputs(PSTR("DNS: NOT Recursion answer\n"));
		return 1;
	}

	//Answer for HOST IPs in Additional RR section
	if ((dns->AdditionalRR == 0)&&(dns->AuthorityRR == 0)&&(dns->AnswerRS == 0)) {
		//xputs(PSTR("DNS: NO answers\n"));
		return 1;
	}

	//We have to scan all sections to find answer for host - different servers can return HOST addr in different sections

	//Skip for query length, we saved it before answer
	cnt = dns_query_length+1;

	//Scan answer

	//Header for answer structed as follow
	/*
	*	2 bytes - Server name
	*	2 bytes - Type - here we have to look for TYPE A record
	*	2 bytes - Class
	*	4 bytes - Time to live
	*	2 bytes - Data length in next section
	*			here length is 12 bytes
	*	Server name (length in previos bytes)
	*/
	//xprintf(PSTR("CNT val %02X\n"), cnt);
	//xprintf(PSTR("UDP length val %05d\n"), htons(udp->len));
	while(cnt < (htons(udp->len)-12)) {
		//Check type of record
		//xprintf(PSTR("CNT val %02X\n"), cnt);
		//xprintf(PSTR("DNS record type %02X\n"), ((dns->Queries[cnt+2]<<8)|dns->Queries[cnt+3]));

		if (((dns->Queries[cnt+0]<<8)|dns->Queries[cnt+1]) != 0xC00C) {
			//xputs(PSTR("DNS: alias not for query\n"));
			cnt = cnt + 12 + ((dns->Queries[cnt+10]<<8)|(dns->Queries[cnt+11]));
			continue;
		}

		if (((dns->Queries[cnt+2]<<8)|dns->Queries[cnt+3]) != DNS_QUERY_TYPE_A) {			
			//_delay_ms(50);
			//Move pointer to next record
			cnt = cnt + 12 + ((dns->Queries[cnt+10]<<8)|(dns->Queries[cnt+11]));
			//Next iteration
			continue;
		}

                if (((dns->Queries[cnt+10]<<8)|dns->Queries[cnt+11]) != 4) {			
			//_delay_ms(50);
			//Move pointer to next record
			cnt = cnt + 12 + ((dns->Queries[cnt+10]<<8)|(dns->Queries[cnt+11]));
			//Next iteration
			continue;
		}

		// xprintf(PSTR("DNS GOT IP: %03d.%03d.%03d.%03d\n"),\			
		// 		(uint8_t) (dns->Queries[cnt+12]),\
		// 		(uint8_t) (dns->Queries[cnt+13]),\
		// 		(uint8_t) (dns->Queries[cnt+14]),\
		// 		(uint8_t) (dns->Queries[cnt+15]));

		resolve_inet_addr = inet_addr(dns->Queries[cnt+12],\
										dns->Queries[cnt+13],\
										dns->Queries[cnt+14],\
										dns->Queries[cnt+15]);

		break;
	}


	return 0;
}

#endif


Втыкивание двух вызовов в существующую библиотеку для Вас монстров не составит труда, я вроде и так все разжевал, а я с Вашего позволения ушел спать — ибо поздно.
Адекватность кода под сомнением, но он прозрачен даже для новичков, практически нет работы с указателями — все на уровне массивов, хотелось бы чтобы было меньше вопросов.
Код 100% рабочий — на Вашей совести только втыкание его куда надо.

Список используемой литературы:
Формат пакета запроса
Формат пакета ответа
Тоже но по русски — для тех кто плохо учился в школе
Сырцы аналогичного данному циклу статей проекта
Это для любителей почитать — много воды, куча если, перекрестные ссылки — но стандарт, а не творчество Васи Пупкина

Все, я закончил — досвиданья.

2Di — прошу не ругаться, первый раз тут статью оформляю.
  • +16
  • 24 марта 2013, 22:05
  • mamonth

Комментарии (47)

RSS свернуть / развернуть
+ за дежукинг
0
Дежукинг = дебаггинг? Я не очень-то знаком с жаргоном :)
0
Совершенно верно. Смешение языков — Bug — жук.
+1
))
0
«Код полностью совместим с предИдущими топиками.» -> «Код полностью совместим с предЫдущими топиками.»
+1
Вы правы — пофиксю.
0
Если уж нести по кочкам орфографию, то можно так всю статью раскритиковать, но я, пожалуй, воздержусь — главное, смысл ясен и понятен. Автору респект за статью :)
0
Надеюсь что смысл донес — остальное — приложение, не техдокументацию пишу ))
0
Точнее, больше с пунктуацией проблем, чем с орфографией. Ну ладно, молчу :)
0
я же не для критики написал, а для того чтобы исправили, чтобы статья стала еще красивее.
0
По правилам хорошего тона, такие замечания пишут в ЛС, а не комментарии к топику. И в ленте не всплывает, и автор правит очепятки с ошиПками=)
0
Эээ, да, верно, я должен извиниться перед автором :)
0
Господа — все проехали забыли )))
0
я тоже извиняюсь, в следующий раз учту. Так бы и свой комментарий удалил, да невижу кнопки удаления.
0
Кнопка есть только для админов — критику обоснованную чту. Правда она пока была только по орфографии…
0
Я, конечно не JD в программировании, но вопрос то all — кто нибудь пробовал запилить этот код в реальный прожект? А попробовал — первое же формирование запроса к dns северу дало FORMAT ERROR. При детальном рассмотрении запрос ресолва ru.pool.ntp.org выдал 2ru5pool4ntp3org в Queries? что есть сильно НЕПРАВИЛЬНО.
Автор — комментарии???
0
Вах. Автора рассмотрит код и выдаст ответ к вечеру. Также прошу резолвить какой-нить запрос типа google.com
Спасибо за репорт.
0
Обещанный ответ…
В цикле, в проверке условия

if (dat=='.'){
..............
}

надо добавить continue; Код пофиксил. Большое спасибо за репорт о баге...
0
Стоп, поторипился…
0
Все, теперь работает. Также изменил заполнения поля флагов — пропустил big-little endian — из-за этого бывают косяки.

Гляньте плиз.
0
бывают=бывали чего-то мне сегодня не пишется правильно — понедельник
0
Сегодня занят весь день был. Завтра или после завтра обязательно попробую новый код и отпишусь. Спасибо.
0
Новостей нет?
+1
Прошу прощения, — обещал отписаться и пропал. Неделя была бешеная, некогда было проверить.
Докладываю — включил новый код в свой проект ntp часов — все отлично работает. Отправляется правильный запрос и корректно парсится ответ. 2Автор, — еще небольшая корректировка кода — к коде идущем после слов «В файл lan.c, добавим вот такой код:» в конце не хватает закрывающего #endif для конструкции #ifdef WITH_DNS.
Ну это так — лирика.
Вообщем автору большой респект за труды.
0
Я рад что тесты прошли на ок.

Спасибо Вам за репорт и проверку — признателен, я почему-то не учел что адрес может содержать более 1 точки. )))

Код обновил.
0
Раз такая пьянка…
1. Код обновил не там… :) секцией выше надо, там где в начале #ifdef WITH_DNS
2. Вчера весь вечер пытался прикрутить к своему проекту функцию xprintf, и еже с ней. Я достаточно недавно начал мк-ры изучать, и как понял эта функция входит в библиотеку позволяющую отсылать, как правило, отладочную информацию, в usart или spi, но в процессе прикручивания настолько заблудился в коде (который в итоге скомппилился, но в протеусе не заработал), что пришлось от него отказаться. Всю диагностику в итоге кидаю на порт конкретного ip, а там вешаю nc.exe и смотрю что пришло (логируя в файл — очень удобно). Я к чему — может приведешь пример рабочго кода где используются функция xprintf (и еже с ней). Для моего уровня достаточно 1 — где взять исходники 2 — дефайны, 3 — инициализация, 4 — пример отправки простенькой строки.
Разумеется если не сложно.
Заранее спасибо.
0
1. Код обновил там где — ибо вставлять все равно надо два вызова, а значит и директиву условной компиляции надо втыкать после второго.
2. Эта либа от Elm Chan — elm-chan.org/ — немного весит, хорошо линкуется, много умеет. А вот из какого его проекта я ее выдрал — не помню. По-моему из FatFs.
0
Эта функция (xprintf) из библиотеки Embedded String Functions.
0
Библиотека xprintf лежит здесь: elm-chan.org/fsw/strf/xprintf.html
0
В догонку. Тут вроде все камрады — поэтому на ты… :)
0
ок ))
0
Спасибо — пошел ссылку курить…
0
Как сделать чтобы, я присвоил имя контроллеру, допустим test, и с ПК мог сделать ping test?
0
  • avatar
  • VIC
  • 10 января 2014, 03:11
Прописать его в DNS. Самое простое — присвоить ему статический IP и вписать в hosts строчку вида
192.168.1.42 mycontroller
0
Так не подходит ( у меня динамический IP, стек uIP. Не знаете как там решить этот вопрос? Я так понимаю это все относиться к DNS. в uIP только нашел resolv_query(«google.com»); получаю ip google.com но не то что нужно
0
Данная ветка не относится к авторизации у сервера ДНС, только сделать запрос, чтобы потом получить IP адрес хоста.

То что Вам нужно малость из другой оперы… Необходимо авторизоваться в DNS, но это не то что реализует данный кусок кода. Необходимо покурить мануалы и написать необходимый функционал.
0
Для этого нужно использовать сервис динамического DNS, вроде DynDNS или NoIP. Ну или, если требуется только в своей локалке — поднять там свой DNS и прописывать уже в нем. Самим устройством или же DHCP-сервером, который выдает ему адрес. Полагаю, как-то это можно сделать, но подробностей не знаю, хотя они меня и самого немного интересуют.
0
в DHCP протоколе есть Server Host Name, записываю туда свое имя, смотрю через Wireshark, имя есть в поле. Но пинг по имени не проходит (
0
И не пройдет. Если работает винда — то там механизм определения адреса по имени работает иначе, чем в классическом DNS.
0
Можете попробовать поднять на винде 2003 (там это точно было) DHCP и DNS. В таком случае, если винда выдаст адрес, то она может зарегистрировать устройство в своем DNS.

Чтобы все это работало в динамике, на Win сетях надо колупать MSDN и работу сети разбирать… более не подскажу ничего толком, т.к. не лазил.
0
Нет, ситуация малость сложнее.
0
Покрутите DHCP, что бы он конкретному MAC-адресу всегда выдавал один и тот же IP.
0
А как это повлияет на распознавание имен?
0
Можно в статический DNS (тот же hosts в простейшем случае) прописать будет.
-1
Именно. Раз перестанет плавать IP станет достаточно обычного статического DNS.
0
О, дельная мысль впринципе… :)
0
«Зачем делать сложным, то, что проще простого?» (с) НП :)
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.