Esercizio - Client DNS con query e risposta costruite a mano (sul modello della prova pratica)
In questa pagina 6
Testo (sul modello della prova pratica di Reti di Calcolatori, Ing. Informatica UniPD; formato dei messaggi da RFC 1035).
Scrivere in C un mini-risolutore DNS che non usa getaddrinfo:
- costruire a mano una domanda DNS (header di 12 byte con
IDe flagRD, un nome come sequenza di etichette con lunghezza,QTYPEeQCLASS) per un nome e un tipo di record (A,AAAA,NS,CNAME,MX,TXT); - inviarla in un datagramma UDP alla porta 53 di un server DNS (il primo
nameserverdi/etc/resolv.conf, oppure indicato sulla riga di comando) con un timeout; - decodificare la risposta: header (
RCODE, contatori), sezione domande da saltare, record di risposta con i nomi compressi (puntatori) eRDATAper tipo; - verificare
IDe bitQR; se è acceso il bitTC(risposta troncata) ripetere la domanda su TCP; - includere un autotest con messaggi reali, senza rete.
Teoria: DNS, proxy web, HTTP CONNECT e gateway applicativiUn programma risolve i nomi con getaddrinfo (file hosts e poi DNS); un messaggio DNS (RFC 1035) ha un header di 12 byte (ID, flag QR/RD/RA/TC e RCODE, quattro contatori), una domanda (QNAME a etichette con lunghezza, QTYPE, QCLASS) e record di risposta con nomi eventualmente compressi da puntatori, su UDP porta 53 con ripiego su TCP se il bit TC e' acceso; un web proxy e' un intermediario scelto dal client che riceve richieste con URI assoluto, le inoltra al server (togliendo gli header hop-by-hop, aggiungendo Via e X-Forwarded-For) e puo' memorizzare le risposte; le cache si organizzano in gerarchie e con funzioni hash coerenti, le CDN le replicano vicino agli utenti; il metodo CONNECT chiede al proxy un tunnel TCP verso host:porta e dopo la risposta 2xx il proxy inoltra i byte in entrambe le direzioni senza interpretarli (HTTPS, con restrizione delle porte); un gateway o reverse proxy e' un intermediario scelto dal server che traduce o inoltra le richieste ad altri sistemi (bilanciamento, terminazione TLS, FastCGI, API gateway).DNS, proxy web, HTTP CONNECT e gateway applicativi →, Livello applicazione - DNSIl DNS (Domain Name System) traduce i nomi (www.amazon.com) negli indirizzi IP, perché le persone preferiscono i nomi e i protocolli TCP/IP usano gli indirizzi. È un database distribuito e gerarchico: albero rovesciato con radice, domini di primo livello e sottodomini (al più 128 livelli); le informazioni sono su tanti server (13 server radice) e i nuovi domini si registrano presso un registrar accreditato ICANN. Ogni ISP ha un DNS locale, il cui indirizzo l'host riceve con DHCP: l'host (resolver) gli manda la richiesta, di solito su UDP, e il DNS locale interroga radice, dominio di primo livello e server dell'organizzazione. Ogni server che impara un'associazione la tiene in cache, marcando la risposta non autoritativa, e la scarta dopo il TTL. Record (nome, tipo, valore, classe, TTL). Con il NAT il DNS deve restituire l'indirizzo pubblico. Quattro attacchi: macchina compromessa, risposta falsa all'host, avvelenamento della cache del DNS locale, server DNS malevolo.Livello applicazione - DNS →, Protocollo UDPUDP (User Datagram Protocol) è il protocollo di trasporto senza connessione e inaffidabile: rispetto a IP aggiunge soltanto la comunicazione processo-processo (numeri di porta) e un controllo d'errore facoltativo. L'intestazione è di soli 8 byte (porta sorgente, porta destinazione, lunghezza, checksum). Il checksum copre pseudo-intestazione (indirizzi IP, protocollo 17, lunghezza), intestazione e dati, ed è il complemento a uno della somma a 16 bit; se vale 0 significa "non calcolato", e un risultato 0 si trasmette come 0xFFFF. UDP non ha connessione, numeri di sequenza, controllo di flusso, di errore né di congestione: si sceglie per i messaggi brevi (DNS, DHCP, RIP, SNMP) e per le applicazioni in tempo reale, dove conta non aggiungere ritardo.Protocollo UDP →, Richiami di C per la programmazione di rete - memoria, puntatori, struct ed endiannessIn C un programma di rete maneggia byte, non oggetti: un processo ha codice, dati statici, heap e stack; i tipi hanno dimensioni fisse solo se si usano <stdint.h> (uint8_t, uint16_t, uint32_t); i dati che arrivano da un socket sono un buffer di byte con una lunghezza, NON una stringa C terminata da '\0'; i puntatori e l'aritmetica dei puntatori (buf + totale) permettono di riempire un buffer a pezzi; una struct puo' contenere byte di riempimento (padding) per l'allineamento, quindi non si spedisce con write(&s, sizeof s); sulla rete i numeri a piu' byte viaggiano in big endian (network byte order) e si convertono con htons, htonl, ntohs, ntohl, oppure si serializzano a mano con shift e maschere.Richiami di C per la programmazione di rete - memoria, puntatori, struct ed endianness →, System call POSIX, file descriptor e API delle socketLe system call sono le funzioni con cui un programma in user space chiede servizi al kernel (open, read, write, close, fork, pipe, dup2, socket, bind, listen, accept, connect); restituiscono -1 e impostano errno in caso di errore; un file descriptor e' un intero che indicizza la tabella dei file aperti del processo (0 stdin, 1 stdout, 2 stderr) e vale per file, pipe e socket; read e write possono trasferire MENO byte del richiesto (e sui socket TCP non c'e' nessun confine fra i messaggi), quindi servono cicli write_all e read_exact; l'API delle socket crea un punto finale di comunicazione (socket), lo lega a indirizzo e porta (bind), lo rende passivo (listen), accetta connessioni (accept) o si connette (connect); getaddrinfo traduce nomi e porte in indirizzi (IPv4 e IPv6); UDP usa sendto e recvfrom e conserva i confini dei datagrammi, TCP e' uno stream affidabile.System call POSIX, file descriptor e API delle socket →.
Idea
header (12 byte): ID | flag | QDCOUNT | ANCOUNT | NSCOUNT | ARCOUNT (tutti i numeri in BIG ENDIAN)
domanda: QNAME (etichette) | QTYPE (2) | QCLASS (2)
record: NAME | TYPE (2) | CLASS (2) | TTL (4) | RDLENGTH (2) | RDATA (RDLENGTH byte)Il programma ha tre livelli:
- Codifica:
dns_encode_name(www.example.com→03 www 07 example 03 com 00) edns_build_query. - Decodifica:
dns_read_name(segue i puntatori di compressione) edns_parse(una riga di testo per record). - Rete:
query_udp,query_tcpemain.
Le funzioni di codifica e decodifica non toccano la rete: lavorano su buffer, quindi si possono provare con messaggi catturati (selftest). La domanda e la risposta di prova sono messaggi veri, ottenuti interrogando un server DNS per www.example.com.
Codice
/* dns_client.c - mini risolutore DNS (RFC 1035): costruisce a mano la query, la manda in UDP porta 53 e
* decodifica la risposta (nomi compressi compresi). Riprova su TCP se il server segnala il troncamento (TC).
*
* Compilare: gcc -Wall -Wextra -o dns_client dns_client.c
* Autotest: ./dns_client --test (usa messaggi reali, senza rete)
* Usare: ./dns_client www.example.com (record A)
* ./dns_client example.com MX 8.8.8.8 (tipo e server a scelta: A AAAA NS CNAME MX TXT)
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <stdint.h>
#include <errno.h>
#undef NDEBUG /* gli assert degli autotest devono restare attivi anche con -DNDEBUG */
#include <assert.h>
#include <time.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define T_A 1
#define T_NS 2
#define T_CNAME 5
#define T_MX 15
#define T_TXT 16
#define T_AAAA 28
#define MAX_MSG 4096
/* ---------- costruzione della query ---------- */
/* "www.example.com" -> 03 'w' 'w' 'w' 07 'e' 'x' 'a' 'm' 'p' 'l' 'e' 03 'c' 'o' 'm' 00
* Ogni etichetta e' preceduta dalla sua lunghezza (max 63); il nome finisce con la lunghezza 0 (la radice). */
static int dns_encode_name(const char *name, uint8_t *out, size_t cap)
{
size_t o = 0;
while (*name) {
const char *dot = strchr(name, '.');
size_t l = dot ? (size_t)(dot - name) : strlen(name);
if (l == 0 || l > 63 || o + l + 2 > cap)
return -1; /* etichetta vuota o troppo lunga */
out[o++] = (uint8_t)l;
memcpy(out + o, name, l);
o += l;
name += l;
if (*name == '.')
name++;
}
out[o++] = 0;
return (int)o;
}
/* Header (12 byte) + una domanda: QNAME, QTYPE (2 byte), QCLASS (2 byte, 1 = IN). Tutto in big endian. */
static int dns_build_query(uint8_t *buf, size_t cap, uint16_t id, const char *name, uint16_t qtype)
{
if (cap < 12)
return -1;
memset(buf, 0, 12);
buf[0] = (uint8_t)(id >> 8);
buf[1] = (uint8_t)id;
buf[2] = 0x01; /* flags = 0x0100: QR=0 (query), opcode 0, RD=1 (ricorsione desiderata) */
buf[5] = 1; /* QDCOUNT = 1; ANCOUNT, NSCOUNT, ARCOUNT = 0 */
int n = dns_encode_name(name, buf + 12, cap - 12 - 4);
if (n < 0)
return -1;
size_t o = 12 + (size_t)n;
buf[o++] = (uint8_t)(qtype >> 8);
buf[o++] = (uint8_t)qtype;
buf[o++] = 0;
buf[o++] = 1; /* QCLASS = IN */
return (int)o;
}
/* ---------- lettura della risposta ---------- */
static unsigned rd16(const uint8_t *p) { return (unsigned)p[0] << 8 | p[1]; }
static uint32_t rd32(const uint8_t *p) { return (uint32_t)p[0] << 24 | (uint32_t)p[1] << 16 | (uint32_t)p[2] << 8 | p[3]; }
/* Legge un nome a partire da m[*pos]. Un byte che inizia con i bit 11 e' un PUNTATORE di compressione:
* i 14 bit (6 + 8) sono l'offset, dall'inizio del messaggio, dove continua il nome.
* *pos avanza fino a dopo il nome nella posizione ORIGINALE (dopo il puntatore, se c'e'). Torna 0 o -1. */
static int dns_read_name(const uint8_t *m, size_t len, size_t *pos, char *out, size_t outsz)
{
size_t p = *pos, o = 0;
int jumped = 0, hops = 0;
for (;;) {
if (p >= len)
return -1;
uint8_t l = m[p];
if (l == 0) {
p++;
break;
}
if ((l & 0xC0) == 0xC0) {
if (p + 1 >= len)
return -1;
size_t off = (size_t)(l & 0x3F) << 8 | m[p + 1];
if (!jumped)
*pos = p + 2; /* la lettura "normale" riprende dopo il puntatore */
jumped = 1;
if (off >= len || ++hops > 16)
return -1; /* puntatore fuori dal messaggio o ciclo infinito */
p = off;
continue;
}
if (l & 0xC0)
return -1; /* 01 e 10 sono riservati */
p++;
if (p + l > len || o + l + 2 > outsz)
return -1;
if (o > 0)
out[o++] = '.';
memcpy(out + o, m + p, l);
o += l;
p += l;
}
out[o] = '\0';
if (o == 0)
snprintf(out, outsz, "."); /* il nome vuoto e' la radice */
if (!jumped)
*pos = p;
return 0;
}
/* Decodifica la sezione risposte. Scrive una riga per record in lines[]. Torna il numero di record
* (o -1 se il messaggio e' malformato); *rcode = codice di risposta (0 = nessun errore, 3 = NXDOMAIN). */
static int dns_parse(const uint8_t *m, size_t len, char lines[][200], int maxlines, int *rcode)
{
if (len < 12)
return -1;
*rcode = m[3] & 0x0F;
unsigned qd = rd16(m + 4), an = rd16(m + 6);
size_t pos = 12;
char name[256];
for (unsigned i = 0; i < qd; i++) { /* le domande si saltano: nome + 4 byte */
if (dns_read_name(m, len, &pos, name, sizeof name) < 0 || pos + 4 > len)
return -1;
pos += 4;
}
int n = 0;
for (unsigned i = 0; i < an; i++) {
if (dns_read_name(m, len, &pos, name, sizeof name) < 0 || pos + 10 > len)
return -1;
unsigned type = rd16(m + pos);
uint32_t ttl = rd32(m + pos + 4);
unsigned rdlen = rd16(m + pos + 8);
pos += 10;
if (pos + rdlen > len)
return -1;
if (n >= maxlines) {
pos += rdlen;
continue;
}
char *ln = lines[n];
int k = snprintf(ln, 200, "%s\t%u\tIN\t", name, (unsigned)ttl);
size_t rp = pos; /* posizione di lavoro dentro RDATA */
char target[256];
if (type == T_A && rdlen == 4) {
snprintf(ln + k, (size_t)(200 - k), "A\t%d.%d.%d.%d", m[pos], m[pos + 1], m[pos + 2], m[pos + 3]);
} else if (type == T_AAAA && rdlen == 16) {
char ip[INET6_ADDRSTRLEN];
inet_ntop(AF_INET6, m + pos, ip, sizeof ip);
snprintf(ln + k, (size_t)(200 - k), "AAAA\t%s", ip);
} else if ((type == T_CNAME || type == T_NS) && dns_read_name(m, len, &rp, target, sizeof target) == 0) {
snprintf(ln + k, (size_t)(200 - k), "%s\t%s", type == T_CNAME ? "CNAME" : "NS", target);
} else if (type == T_MX && rdlen >= 3) {
unsigned pref = rd16(m + pos);
rp = pos + 2;
if (dns_read_name(m, len, &rp, target, sizeof target) < 0)
return -1;
snprintf(ln + k, (size_t)(200 - k), "MX\t%u %s", pref, target);
} else if (type == T_TXT) {
size_t o = 0;
k += snprintf(ln + k, (size_t)(200 - k), "TXT\t");
while (o < rdlen && k < 190) { /* TXT = una o piu' stringhe, ognuna con un byte di lunghezza */
unsigned l = m[pos + o++];
if (o + l > rdlen)
return -1;
k += snprintf(ln + k, (size_t)(200 - k), "\"%.*s\" ", (int)l, (const char *)m + pos + o);
o += l;
}
} else {
snprintf(ln + k, (size_t)(200 - k), "tipo %u (%u byte)", type, rdlen);
}
n++;
pos += rdlen;
}
return n;
}
/* ---------- rete ---------- */
/* UDP: un datagramma di domanda, un datagramma di risposta. Torna la lunghezza della risposta o -1. */
static int query_udp(const char *server, const uint8_t *q, size_t qlen, uint8_t *resp, size_t cap)
{
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd < 0)
return -1;
struct timeval tv = {.tv_sec = 3, .tv_usec = 0};
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
struct sockaddr_in sa;
memset(&sa, 0, sizeof sa);
sa.sin_family = AF_INET;
sa.sin_port = htons(53);
if (inet_pton(AF_INET, server, &sa.sin_addr) != 1) {
close(fd);
return -1;
}
ssize_t n = -1;
if (sendto(fd, q, qlen, 0, (struct sockaddr *)&sa, sizeof sa) == (ssize_t)qlen)
n = recvfrom(fd, resp, cap, 0, NULL, NULL);
close(fd);
return (int)n;
}
/* TCP (RFC 1035 par. 4.2.2): ogni messaggio e' preceduto da 2 byte con la sua lunghezza. */
static int query_tcp(const char *server, const uint8_t *q, size_t qlen, uint8_t *resp, size_t cap)
{
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0)
return -1;
struct sockaddr_in sa;
memset(&sa, 0, sizeof sa);
sa.sin_family = AF_INET;
sa.sin_port = htons(53);
if (inet_pton(AF_INET, server, &sa.sin_addr) != 1 || connect(fd, (struct sockaddr *)&sa, sizeof sa) < 0) {
close(fd);
return -1;
}
uint8_t pre[2] = {(uint8_t)(qlen >> 8), (uint8_t)qlen};
uint8_t msg[MAX_MSG];
memcpy(msg, pre, 2);
memcpy(msg + 2, q, qlen);
if (write(fd, msg, qlen + 2) != (ssize_t)(qlen + 2)) {
close(fd);
return -1;
}
uint8_t lb[2];
size_t got = 0;
ssize_t r;
while (got < 2 && (r = read(fd, lb + got, 2 - got)) > 0)
got += (size_t)r;
size_t rl = got == 2 ? rd16(lb) : 0;
if (rl == 0 || rl > cap) {
close(fd);
return -1;
}
got = 0;
while (got < rl && (r = read(fd, resp + got, rl - got)) > 0)
got += (size_t)r;
close(fd);
return got == rl ? (int)rl : -1;
}
/* Primo "nameserver" di /etc/resolv.conf (solo IPv4), altrimenti 8.8.8.8. */
static void default_server(char *out, size_t size)
{
snprintf(out, size, "8.8.8.8");
FILE *f = fopen("/etc/resolv.conf", "r");
if (f == NULL)
return;
char line[256], ip[64];
while (fgets(line, sizeof line, f))
if (sscanf(line, "nameserver %63s", ip) == 1) {
struct in_addr a;
if (inet_pton(AF_INET, ip, &a) == 1) {
snprintf(out, size, "%s", ip);
break;
}
}
fclose(f);
}
static int selftest(void)
{
/* messaggi reali: query e risposta per www.example.com tipo A (id 0x1234) */
static const uint8_t query[] = {0x12, 0x34, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 'w', 'w', 'w', 0x07,
'e', 'x', 'a', 'm', 'p', 'l', 'e', 0x03, 'c', 'o', 'm', 0x00, 0x00, 0x01, 0x00, 0x01};
uint8_t buf[512];
int n = dns_build_query(buf, sizeof buf, 0x1234, "www.example.com", T_A);
assert(n == (int)sizeof query && memcmp(buf, query, sizeof query) == 0);
assert(dns_encode_name("a..b", buf, sizeof buf) == -1);
static const uint8_t resp[] = {
0x12, 0x34, 0x81, 0x80, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, /* header: QR=1 RD=1 RA=1, 1 domanda, 2 risposte */
0x03, 'w', 'w', 'w', 0x07, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 0x03, 'c', 'o', 'm', 0x00, 0x00, 0x01, 0x00, 0x01,
0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x17, 0x00, 0x04, 0x68, 0x14, 0x17, 0x9a, /* nome = puntatore a offset 12 */
0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x17, 0x00, 0x04, 0xac, 0x42, 0x93, 0xf3};
char lines[8][200];
int rcode;
int cnt = dns_parse(resp, sizeof resp, lines, 8, &rcode);
assert(cnt == 2 && rcode == 0);
assert(strcmp(lines[0], "www.example.com\t279\tIN\tA\t104.20.23.154") == 0);
assert(strcmp(lines[1], "www.example.com\t279\tIN\tA\t172.66.147.243") == 0);
/* MX con "." (null MX, RFC 7505): preferenza 0, nome vuoto */
static const uint8_t mx[] = {
0x12, 0x34, 0x81, 0x80, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
0x07, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 0x03, 'c', 'o', 'm', 0x00, 0x00, 0x0f, 0x00, 0x01,
0xc0, 0x0c, 0x00, 0x0f, 0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x00, 0x00};
cnt = dns_parse(mx, sizeof mx, lines, 8, &rcode);
assert(cnt == 1 && strcmp(lines[0], "example.com\t5\tIN\tMX\t0 .") == 0);
/* messaggi malformati: puntatore ciclico, troncamento */
static const uint8_t loop[] = {0, 1, 0x81, 0x80, 0, 1, 0, 0, 0, 0, 0, 0, 0xc0, 0x0c, 0, 1, 0, 1};
assert(dns_parse(loop, sizeof loop, lines, 8, &rcode) == -1);
assert(dns_parse(resp, 40, lines, 8, &rcode) == -1);
puts("dns_client: autotest superato");
return 0;
}
int main(int argc, char **argv)
{
if (argc == 2 && strcmp(argv[1], "--test") == 0)
return selftest();
if (argc < 2 || argc > 4) {
fprintf(stderr, "uso: %s nome [A|AAAA|NS|CNAME|MX|TXT] [server_dns]\n", argv[0]);
return 1;
}
static const struct { const char *name; uint16_t code; } types[] = {
{"A", T_A}, {"AAAA", T_AAAA}, {"NS", T_NS}, {"CNAME", T_CNAME}, {"MX", T_MX}, {"TXT", T_TXT}};
uint16_t qtype = T_A;
if (argc >= 3) {
qtype = 0;
for (size_t i = 0; i < sizeof types / sizeof types[0]; i++)
if (strcasecmp(argv[2], types[i].name) == 0)
qtype = types[i].code;
if (qtype == 0) {
fprintf(stderr, "tipo non supportato: %s\n", argv[2]);
return 1;
}
}
char server[64];
if (argc == 4)
snprintf(server, sizeof server, "%s", argv[3]);
else
default_server(server, sizeof server);
srand((unsigned)time(NULL) ^ (unsigned)getpid());
uint16_t id = (uint16_t)rand(); /* l'ID abbina risposta e domanda (meglio se imprevedibile) */
uint8_t q[512], resp[MAX_MSG];
int qlen = dns_build_query(q, sizeof q, id, argv[1], qtype);
if (qlen < 0) {
fprintf(stderr, "nome non valido\n");
return 1;
}
int rlen = query_udp(server, q, (size_t)qlen, resp, sizeof resp);
if (rlen >= 12 && (resp[2] & 0x02)) { /* bit TC: risposta troncata, si ripete su TCP */
fprintf(stderr, "risposta troncata (TC), riprovo con TCP\n");
rlen = query_tcp(server, q, (size_t)qlen, resp, sizeof resp);
}
if (rlen < 12) {
fprintf(stderr, "nessuna risposta da %s\n", server);
return 1;
}
if (rd16(resp) != id || !(resp[2] & 0x80)) { /* ID diverso o QR=0: non e' la risposta alla nostra domanda */
fprintf(stderr, "risposta non valida (ID o flag QR)\n");
return 1;
}
char lines[32][200];
int rcode;
int n = dns_parse(resp, (size_t)rlen, lines, 32, &rcode);
if (n < 0) {
fprintf(stderr, "messaggio DNS malformato\n");
return 1;
}
static const char *rc_names[] = {"NOERROR", "FORMERR", "SERVFAIL", "NXDOMAIN", "NOTIMP", "REFUSED"};
printf(";; server %s, rcode %s, %d risposte\n", server, rcode < 6 ? rc_names[rcode] : "?", n);
for (int i = 0; i < n; i++)
printf("%s\n", lines[i]);
return rcode == 0 ? 0 : 2;
}Compilare e provare
$ gcc -Wall -Wextra -o dns_client dns_client.c
$ ./dns_client --test
dns_client: autotest superato
$ ./dns_client www.example.com
;; server 192.168.1.1, rcode NOERROR, 2 risposte
www.example.com 279 IN A 104.20.23.154
www.example.com 279 IN A 172.66.147.243
$ ./dns_client example.com MX 8.8.8.8
;; server 8.8.8.8, rcode NOERROR, 1 risposte
example.com 5 IN MX 0 .
$ ./dns_client example.com TXT
;; server 192.168.1.1, rcode NOERROR, 2 risposte
example.com 300 IN TXT "v=spf1 -all"
...
$ ./dns_client nonesiste.example.invalid
;; server 192.168.1.1, rcode NXDOMAIN, 0 risposte(Gli indirizzi e i TTL cambiano nel tempo; MX con preferenza 0 e nome . è il null MX di RFC 7505: il dominio dichiara di non ricevere posta.) Il confronto con gli strumenti standard: dig www.example.com +noedns o nslookup www.example.com devono dare le stesse risposte. Per vedere i pacchetti: tcpdump -n -i any -X udp port 53 (con sudo) mostra la domanda da 33 byte e la risposta da 65, uguali a quelle dell'autotest.
La domanda per www.example.com (tipo A, ID = 0x1234) è, byte per byte:
12 34 ID
01 00 flag: QR=0 (domanda), Opcode=0, RD=1
00 01 00 00 00 00 00 00 QDCOUNT=1, ANCOUNT=0, NSCOUNT=0, ARCOUNT=0
03 77 77 77 3 'w' 'w' 'w'
07 65 78 61 6d 70 6c 65 7 'e' 'x' 'a' 'm' 'p' 'l' 'e'
03 63 6f 6d 3 'c' 'o' 'm'
00 fine del nome
00 01 QTYPE = A
00 01 QCLASS = INe la risposta (65 byte) comincia con 12 34 81 80 00 01 00 02 ...: stesso ID, flag 0x8180 (QR=1, RD=1, RA=1, RCODE=0), una domanda e due risposte; ciascuna risposta inizia con c0 0c, un puntatore all'offset 12 dove sta il nome della domanda.
Spiegazione dei punti chiave
dns_encode_name. Scorre il nome per etichette separate da .: per ognuna scrive un byte con la lunghezza e poi i caratteri, e termina con un byte 0 (la radice). Rifiuta le etichette vuote (a..b) e più lunghe di 63 byte. I numeri 03, 07, 03 non sono caratteri '3': sono byte con valore 3, 7, 3.
dns_build_query. memset(buf, 0, 12) azzera l'header; ID in big endian (id >> 8, poi id); buf[2] = 0x01 imposta il flag RD (bit 8 del campo flag: ricorsione desiderata) con QR = 0; buf[5] = 1 è QDCOUNT = 1 (secondo byte del contatore a 16 bit). Poi il nome, QTYPE e QCLASS = 1 (IN), ciascuno a 2 byte in big endian. Nessun uso di struct con htons: si scrivono i byte uno per uno, come in Richiami di C per la programmazione di rete - memoria, puntatori, struct ed endiannessIn C un programma di rete maneggia byte, non oggetti: un processo ha codice, dati statici, heap e stack; i tipi hanno dimensioni fisse solo se si usano <stdint.h> (uint8_t, uint16_t, uint32_t); i dati che arrivano da un socket sono un buffer di byte con una lunghezza, NON una stringa C terminata da '\0'; i puntatori e l'aritmetica dei puntatori (buf + totale) permettono di riempire un buffer a pezzi; una struct puo' contenere byte di riempimento (padding) per l'allineamento, quindi non si spedisce con write(&s, sizeof s); sulla rete i numeri a piu' byte viaggiano in big endian (network byte order) e si convertono con htons, htonl, ntohs, ntohl, oppure si serializzano a mano con shift e maschere.Richiami di C per la programmazione di rete - memoria, puntatori, struct ed endianness →.
rd16 e rd32. Leggono numeri a 2 e 4 byte in big endian componendo i byte con shift: p[0] << 8 | p[1]. Non si fa mai *(uint16_t *)p: l'indirizzo può non essere allineato e l'ordine dei byte non è quello della macchina.
dns_read_name: i nomi compressi.
- Legge un byte
lalla posizionep.l == 0termina il nome. - Se
(l & 0xC0) == 0xC0è un puntatore: gli ultimi 6 bit dile tutto il byte seguente formano un offset a 14 bit, dall'inizio del messaggio, dove il nome continua. Alla prima deviazione si ricorda dove riprendere la lettura "normale" (*pos = p + 2), perché dopo il puntatore il record continua subito. - Se i bit alti sono
01o10il byte non è valido (riservati). - Altrimenti è una etichetta di
lbyte: si copia con un.davanti (se non è la prima). - Due protezioni contro i messaggi malformati o ostili: un limite di salti (16: un puntatore che punta a sé stesso farebbe ciclare per sempre) e il controllo che ogni offset e ogni etichetta restino dentro il messaggio (
p + l > len). - Il nome vuoto (la radice) si scrive come
".".
dns_parse.
- Controlla
len >= 12e leggeRCODE(m[3] & 0x0F),QDCOUNTeANCOUNT. - Salta le domande: nome più 4 byte.
- Per ogni risposta: nome (con
dns_read_name), poi 10 byte fissi (TYPE,CLASS,TTL,RDLENGTH), poiRDLENGTHbyte di dati; controlla cheRDATAstia nel messaggio. RDATAper tipo:A(4 byte →%d.%d.%d.%d),AAAA(16 byte →inet_ntop),CNAME/NS(un nome, anch'esso compresso: va letto condns_read_namepartendo daRDATA),MX(2 byte di preferenza e un nome),TXT(una o più stringhe, ciascuna con un byte di lunghezza, stampate fra virgolette con%.*s).- Alla fine di ogni record si avanza di
rdlen, indipendentemente da quanto il decodificatore ha letto. - Nel caso di errore (
-1) l'intera risposta si scarta.
query_udp. socket(AF_INET, SOCK_DGRAM, 0); SO_RCVTIMEO a 3 secondi (senza, un datagramma perso bloccherebbe il programma per sempre: UDP non ritrasmette); inet_pton per l'indirizzo del server; sendto e recvfrom.
query_tcp (RFC 1035 par. 4.2.2). Su TCP ogni messaggio è preceduto da 2 byte con la sua lunghezza (big endian): si invia len + messaggio e si legge prima la lunghezza della risposta e poi esattamente quei byte. Si usa se la risposta UDP ha il bit TC (troncata: la risposta non entrava nei 512 byte) acceso: resp[2] & 0x02.
Controlli in main. Dopo la ricezione: rd16(resp) == id (la risposta appartiene alla nostra domanda; con un ID casuale a 16 bit e porta sorgente casuale si ostacolano le risposte false) e il bit QR (resp[2] & 0x80) deve valere 1. Il codice di ritorno è 0 se RCODE è NOERROR, 2 altrimenti (NXDOMAIN: il nome non esiste).
default_server. Legge /etc/resolv.conf con sscanf(line, "nameserver %63s", ip), e usa il primo indirizzo IPv4; in mancanza 8.8.8.8.
Errori tipici
- Scrivere
ID, contatori oQTYPEin ordine host: sulla rete vanno in big endian (e leggerli con*(uint16_t *)). - Scrivere le lunghezze delle etichette come caratteri decimali invece che come byte; dimenticare il byte
0finale del nome. - Ignorare i puntatori di compressione: i nomi dei record di risposta non si decodificano e si perde la posizione.
- Non limitare i salti dei puntatori: un messaggio con un puntatore ciclico blocca il programma.
- Dopo un nome compresso, avanzare di un numero sbagliato di byte (il puntatore occupa 2 byte; la lettura "normale" riprende dopo il puntatore, non dopo il nome puntato).
- Non controllare che
RDLENGTHstia nel messaggio, o fidarsi dei contatori (ANCOUNT) senza verificare la lunghezza reale. - Non impostare il timeout su UDP o non verificare
IDeQR. - Dimenticare il ripiego su TCP per le risposte troncate; dimenticare il prefisso di 2 byte con la lunghezza su TCP.
- Confondere
NXDOMAIN(il nome non esiste) conNOERRORsenza risposte (il nome esiste ma non ha record di quel tipo).
Varianti per esercitarsi
- Aggiungere
EDNS0(recordOPTnella sezione aggiuntiva) per ricevere risposte UDP più lunghe di 512 byte. - Risoluzione iterativa: partire dai server radice e seguire i riferimenti (
NSe record glue inAuthority/Additional) fino alla risposta (Livello applicazione - DNSIl DNS (Domain Name System) traduce i nomi (www.amazon.com) negli indirizzi IP, perché le persone preferiscono i nomi e i protocolli TCP/IP usano gli indirizzi. È un database distribuito e gerarchico: albero rovesciato con radice, domini di primo livello e sottodomini (al più 128 livelli); le informazioni sono su tanti server (13 server radice) e i nuovi domini si registrano presso un registrar accreditato ICANN. Ogni ISP ha un DNS locale, il cui indirizzo l'host riceve con DHCP: l'host (resolver) gli manda la richiesta, di solito su UDP, e il DNS locale interroga radice, dominio di primo livello e server dell'organizzazione. Ogni server che impara un'associazione la tiene in cache, marcando la risposta non autoritativa, e la scarta dopo il TTL. Record (nome, tipo, valore, classe, TTL). Con il NAT il DNS deve restituire l'indirizzo pubblico. Quattro attacchi: macchina compromessa, risposta falsa all'host, avvelenamento della cache del DNS locale, server DNS malevolo.Livello applicazione - DNS →). - Risoluzione inversa (
PTR, nome4.3.2.1.in-addr.arpaper l'indirizzo1.2.3.4). - Seguire una catena di
CNAMEe ripetere la domanda per il nome canonico. - Una cache con rispetto del
TTL. - Scrivere un server DNS minimo che risponde a un nome fisso: riusa
dns_read_namee costruisce la risposta con un puntatorec0 0c.
Versione ripasso
- Testo. Mini-risolutore DNS (RFC 1035) senza
getaddrinfo: domanda costruita a mano, UDP porta 53 con timeout, decodifica della risposta (nomi compressi,RDATAper tipo), controlloID/QR, ripiego su TCP seTC, autotest con messaggi reali. - Formato. Header 12 byte (
ID, flag,QDCOUNT,ANCOUNT,NSCOUNT,ARCOUNT; big endian); domandaQNAME+QTYPE+QCLASS; recordNAME,TYPE,CLASS,TTL(4),RDLENGTH,RDATA. - Domanda per
www.example.comA (ID 0x1234).12 34 | 01 00 | 00 01 00 00 00 00 00 00 | 03 'www' 07 'example' 03 'com' 00 | 00 01 | 00 01= 33 byte. Flag0x0100=RD. Risposta 65 byte:12 34 81 80 00 01 00 02 ..., ogni rispostac0 0c 00 01 00 01 TTL(4) 00 04 <4 byte>(16 byte). dns_encode_name. Etichettelunghezza + caratteri, byte0finale; etichetta vuota o > 63 -> errore; le lunghezze sono byte, non cifre.dns_build_query.memset 12;buf[0..1] = id;buf[2] = 0x01(RD);buf[5] = 1(QDCOUNT); nome;QTYPE,QCLASS = 1in big endian. Letture conrd16/rd32(shift), mai*(uint16_t *).dns_read_name.
for (;;) { l = m[p];
if (l == 0) { p++; break; }
if ((l & 0xC0) == 0xC0) { /* puntatore: 14 bit di offset */
off = (l & 0x3F) << 8 | m[p+1];
if (!jumped) *pos = p + 2; /* la lettura normale riprende DOPO il puntatore */
jumped = 1; if (off >= len || ++hops > 16) return -1; /* limite di salti */
p = off; continue; }
if (l & 0xC0) return -1; /* 01/10 riservati */
/* etichetta di l byte, con '.' davanti se non la prima; controlli p + l <= len */ }dns_parse. Salta le domande (nome + 4); per ogni risposta nome, 10 byte (TYPE,CLASS,TTL,RDLENGTH),RDATAdentro il messaggio;A4 byte,AAAA16 (inet_ntop),CNAME/NSnome compresso (letto daRDATA),MXpreferenza + nome,TXTstringhe con byte di lunghezza; avanza sempre diRDLENGTH.- Rete. UDP:
SO_RCVTIMEO3 s,sendto/recvfrom; TCP (TC=resp[2] & 0x02): 2 byte di lunghezza davanti a ogni messaggio. Controlli:rd16(resp) == id,resp[2] & 0x80(QR). Uscita0seNOERROR,2altrimenti (NXDOMAIN= nome inesistente;NOERRORsenza risposte = nome esistente senza record di quel tipo). - Prove.
./dns_client --test->autotest superato;./dns_client www.example.com-> due recordA;./dns_client example.com MX 8.8.8.8->0 .(null MX); confronto condig +noedns;tcpdump -n -X udp port 53. - Codice essenziale (le funzioni centrali, senza commenti):
static int dns_encode_name(const char *name, uint8_t *out, size_t cap)
{
size_t o = 0;
while (*name) {
const char *dot = strchr(name, '.');
size_t l = dot ? (size_t)(dot - name) : strlen(name);
if (l == 0 || l > 63 || o + l + 2 > cap)
return -1;
out[o++] = (uint8_t)l;
memcpy(out + o, name, l);
o += l;
name += l;
if (*name == '.')
name++;
}
out[o++] = 0;
return (int)o;
}
static int dns_build_query(uint8_t *buf, size_t cap, uint16_t id, const char *name, uint16_t qtype)
{
if (cap < 12)
return -1;
memset(buf, 0, 12);
buf[0] = (uint8_t)(id >> 8);
buf[1] = (uint8_t)id;
buf[2] = 0x01;
buf[5] = 1;
int n = dns_encode_name(name, buf + 12, cap - 12 - 4);
if (n < 0)
return -1;
size_t o = 12 + (size_t)n;
buf[o++] = (uint8_t)(qtype >> 8);
buf[o++] = (uint8_t)qtype;
buf[o++] = 0;
buf[o++] = 1;
return (int)o;
}
static int dns_parse(const uint8_t *m, size_t len, char lines[][200], int maxlines, int *rcode)
{
if (len < 12)
return -1;
*rcode = m[3] & 0x0F;
unsigned qd = rd16(m + 4), an = rd16(m + 6);
size_t pos = 12;
char name[256];
for (unsigned i = 0; i < qd; i++) {
if (dns_read_name(m, len, &pos, name, sizeof name) < 0 || pos + 4 > len)
return -1;
pos += 4;
}
int n = 0;
for (unsigned i = 0; i < an; i++) {
if (dns_read_name(m, len, &pos, name, sizeof name) < 0 || pos + 10 > len)
return -1;
unsigned type = rd16(m + pos);
uint32_t ttl = rd32(m + pos + 4);
unsigned rdlen = rd16(m + pos + 8);
pos += 10;
if (pos + rdlen > len)
return -1;
if (n >= maxlines) {
pos += rdlen;
continue;
}
char *ln = lines[n];
int k = snprintf(ln, 200, "%s\t%u\tIN\t", name, (unsigned)ttl);
size_t rp = pos;
char target[256];
if (type == T_A && rdlen == 4) {
snprintf(ln + k, (size_t)(200 - k), "A\t%d.%d.%d.%d", m[pos], m[pos + 1], m[pos + 2], m[pos + 3]);
} else if (type == T_AAAA && rdlen == 16) {
char ip[INET6_ADDRSTRLEN];
inet_ntop(AF_INET6, m + pos, ip, sizeof ip);
snprintf(ln + k, (size_t)(200 - k), "AAAA\t%s", ip);
} else if ((type == T_CNAME || type == T_NS) && dns_read_name(m, len, &rp, target, sizeof target) == 0) {
snprintf(ln + k, (size_t)(200 - k), "%s\t%s", type == T_CNAME ? "CNAME" : "NS", target);
} else if (type == T_MX && rdlen >= 3) {
unsigned pref = rd16(m + pos);
rp = pos + 2;
if (dns_read_name(m, len, &rp, target, sizeof target) < 0)
return -1;
snprintf(ln + k, (size_t)(200 - k), "MX\t%u %s", pref, target);
} else if (type == T_TXT) {
size_t o = 0;
k += snprintf(ln + k, (size_t)(200 - k), "TXT\t");
while (o < rdlen && k < 190) {
unsigned l = m[pos + o++];
if (o + l > rdlen)
return -1;
k += snprintf(ln + k, (size_t)(200 - k), "\"%.*s\" ", (int)l, (const char *)m + pos + o);
o += l;
}
} else {
snprintf(ln + k, (size_t)(200 - k), "tipo %u (%u byte)", type, rdlen);
}
n++;
pos += rdlen;
}
return n;
}- Errori tipici: numeri in ordine host; lunghezze scritte come caratteri; byte
0finale mancante; puntatori ignorati o senza limite di salti; avanzamento sbagliato dopo un nome compresso;RDLENGTHe contatori non verificati; niente timeout né controllo diID/QR; ripiego TCP e prefisso di 2 byte dimenticati;NXDOMAINeNOERRORvuoto confusi.