libs/libc/netdb/lib_dnsquery.c: harden against DNS spoofing. This commit implements most of the RFC 5452 guidelines for making DNS more resilient. We now verify response matches against what was queried and use unpredictable query IDs. It is also checked that response come from correct DNS server. Also fixes a buffer overflow when querying hostnames longer than CONFIG_NETDB_DNSCLIENT_NAMESIZE.
This commit is contained in:
parent
be1567d924
commit
23aa2839c3
@ -2,7 +2,8 @@
|
||||
* include/nuttx/net/dns.h
|
||||
* DNS resolver code header file.
|
||||
*
|
||||
* Copyright (C) 2007-2009, 2011-2012, 2014-2015 Gregory Nutt. All rights reserved.
|
||||
* Copyright (C) 2007-2009, 2011-2012, 2014-2015, 2018 Gregory Nutt. All
|
||||
* rights reserved.
|
||||
* Author: Gregory Nutt <gnutt@nuttx.org>
|
||||
*
|
||||
* Inspired by/based on uIP logic by Adam Dunkels:
|
||||
@ -53,6 +54,7 @@
|
||||
/****************************************************************************
|
||||
* Pre-processor Definitions
|
||||
****************************************************************************/
|
||||
|
||||
/* DNS classes */
|
||||
|
||||
#define DNS_CLASS_IN 1 /* RFC 1035 Internet */
|
||||
@ -144,6 +146,14 @@ struct dns_header_s
|
||||
uint16_t numextrarr;
|
||||
};
|
||||
|
||||
/* The DNS question message structure */
|
||||
|
||||
struct dns_question_s
|
||||
{
|
||||
uint16_t type;
|
||||
uint16_t class;
|
||||
};
|
||||
|
||||
/* The DNS answer message structure */
|
||||
|
||||
struct dns_answer_s
|
||||
@ -220,3 +230,4 @@ int dns_foreach_nameserver(dns_callback_t callback, FAR void *arg);
|
||||
#endif
|
||||
|
||||
#endif /* __INCLUDE_NUTTX_NET_DNS_H */
|
||||
|
||||
|
@ -49,8 +49,10 @@
|
||||
#include <nuttx/config.h>
|
||||
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <errno.h>
|
||||
#include <debug.h>
|
||||
#include <assert.h>
|
||||
|
||||
#include <arpa/inet.h>
|
||||
|
||||
@ -65,12 +67,16 @@
|
||||
|
||||
/* Buffer sizes
|
||||
*
|
||||
* The SEND_BUFFER_SIZE depends the configured DNS name size,
|
||||
* The SEND_BUFFER_SIZE depends the configured DNS name size:
|
||||
*
|
||||
* sizeof(DNS query0 = Header (12 bytes) + DNS Name (Variable) +
|
||||
* Query type (2 bytes) + Query Class (2 bytes)
|
||||
*
|
||||
* sizeof(DNS Name) = Encoded length (1 byte) + Maximum length of name +
|
||||
* NUL-terminator (1 byte)
|
||||
*/
|
||||
|
||||
#define SEND_BUFFER_SIZE (32 + CONFIG_NETDB_DNSCLIENT_NAMESIZE)
|
||||
#define SEND_BUFFER_SIZE (16 + CONFIG_NETDB_DNSCLIENT_NAMESIZE + 2)
|
||||
#define RECV_BUFFER_SIZE CONFIG_NETDB_DNSCLIENT_MAXRESPONSE
|
||||
|
||||
/****************************************************************************
|
||||
@ -86,11 +92,26 @@ struct dns_query_s
|
||||
FAR socklen_t *addrlen; /* Length of the address */
|
||||
};
|
||||
|
||||
/****************************************************************************
|
||||
* Private Data
|
||||
****************************************************************************/
|
||||
/* Query info to check response against. */
|
||||
|
||||
static uint8_t g_seqno; /* Sequence number of the next request */
|
||||
struct dns_query_info_s
|
||||
{
|
||||
union
|
||||
{
|
||||
#ifdef CONFIG_NET_IPv4
|
||||
struct in_addr srv_ipv4; /* DNS server address */
|
||||
#endif
|
||||
#ifdef CONFIG_NET_IPv6
|
||||
struct in6_addr srv_ipv6; /* DNS server address */
|
||||
#endif
|
||||
} u;
|
||||
in_port_t srv_port; /* DNS server port */
|
||||
uint16_t id; /* Query ID */
|
||||
uint16_t rectype; /* Queried record type */
|
||||
uint16_t qnamelen; /* Queried hostname length */
|
||||
char qname[CONFIG_NETDB_DNSCLIENT_NAMESIZE+2]; /* Queried hostname in encoded
|
||||
* format + NUL */
|
||||
};
|
||||
|
||||
/****************************************************************************
|
||||
* Private Functions
|
||||
@ -124,7 +145,7 @@ static FAR uint8_t *dns_parse_name(FAR uint8_t *query, FAR uint8_t *queryend)
|
||||
|
||||
/* Check for a leading or trailing pointer.*/
|
||||
|
||||
if (n & 0xC0)
|
||||
if ((n & 0xc0) != 0)
|
||||
{
|
||||
/* Eat second pointer byte and terminate search */
|
||||
|
||||
@ -156,6 +177,24 @@ static FAR uint8_t *dns_parse_name(FAR uint8_t *query, FAR uint8_t *queryend)
|
||||
return query;
|
||||
}
|
||||
|
||||
/****************************************************************************
|
||||
* Name: dns_alloc_id
|
||||
*
|
||||
* Description:
|
||||
* Gets a new ID for query. Function manufactures a reasonably unique ID,
|
||||
* but we accept rare collisions, since they are harmless when resolver
|
||||
* clients retry failed DNS few times.
|
||||
*
|
||||
****************************************************************************/
|
||||
|
||||
static inline uint16_t dns_alloc_id(void)
|
||||
{
|
||||
struct timespec ts;
|
||||
|
||||
clock_gettime(CLOCK_REALTIME, &ts);
|
||||
return (uint32_t)ts.tv_nsec + ((uint32_t)ts.tv_nsec >> 16);
|
||||
}
|
||||
|
||||
/****************************************************************************
|
||||
* Name: dns_send_query
|
||||
*
|
||||
@ -166,63 +205,94 @@ static FAR uint8_t *dns_parse_name(FAR uint8_t *query, FAR uint8_t *queryend)
|
||||
****************************************************************************/
|
||||
|
||||
static int dns_send_query(int sd, FAR const char *name,
|
||||
FAR union dns_server_u *uaddr, uint16_t rectype)
|
||||
FAR union dns_server_u *uaddr, uint16_t rectype,
|
||||
FAR struct dns_query_info_s *qinfo)
|
||||
{
|
||||
FAR struct dns_header_s *hdr;
|
||||
FAR uint8_t *dest;
|
||||
FAR uint8_t *nptr;
|
||||
FAR char *qname;
|
||||
FAR char *qptr;
|
||||
FAR const char *src;
|
||||
uint8_t buffer[SEND_BUFFER_SIZE];
|
||||
uint8_t seqno;
|
||||
uint16_t id;
|
||||
socklen_t addrlen;
|
||||
int errcode;
|
||||
int ret;
|
||||
int len;
|
||||
int n;
|
||||
|
||||
/* Increment the sequence number */
|
||||
/* Get a new ID for query */
|
||||
|
||||
dns_semtake();
|
||||
seqno = g_seqno++;
|
||||
dns_semgive();
|
||||
id = dns_alloc_id();
|
||||
|
||||
/* Initialize the request header */
|
||||
|
||||
hdr = (FAR struct dns_header_s *)buffer;
|
||||
memset(hdr, 0, sizeof(*hdr));
|
||||
hdr->id = htons(seqno);
|
||||
hdr->id = htons(id);
|
||||
hdr->flags1 = DNS_FLAG1_RD;
|
||||
hdr->numquestions = HTONS(1);
|
||||
dest = buffer + sizeof(*hdr);
|
||||
|
||||
/* Convert hostname into suitable query format. */
|
||||
/* Convert hostname into suitable query format.
|
||||
*
|
||||
* There is space for CONFIG_NETDB_DNSCLIENT_NAMESIZE
|
||||
* plus one pre-pended name length and NUL-terminator
|
||||
* (other pre-pended name lengths replace dots).
|
||||
*/
|
||||
|
||||
src = name - 1;
|
||||
dest = buffer + sizeof(*hdr);
|
||||
qname = qinfo->qname;
|
||||
len = 0;
|
||||
do
|
||||
{
|
||||
/* Copy the name string */
|
||||
/* Copy the name string to both query and saved info. */
|
||||
|
||||
src++;
|
||||
nptr = dest++;
|
||||
for (n = 0; *src != '.' && *src != 0; src++)
|
||||
qptr = qname++;
|
||||
len++;
|
||||
|
||||
for (n = 0;
|
||||
*src != '.' && *src != 0 &&
|
||||
len <= CONFIG_NETDB_DNSCLIENT_NAMESIZE;
|
||||
src++)
|
||||
{
|
||||
*dest++ = *(uint8_t *)src;
|
||||
*dest++ = *(FAR uint8_t *)src;
|
||||
*qname++ = *(FAR uint8_t *)src;
|
||||
n++;
|
||||
len++;
|
||||
}
|
||||
|
||||
/* Pre-pend the name length */
|
||||
|
||||
*nptr = n;
|
||||
*qptr = n;
|
||||
}
|
||||
while (*src != '\0');
|
||||
while (*src != '\0' && len <= CONFIG_NETDB_DNSCLIENT_NAMESIZE);
|
||||
|
||||
/* Add NUL termination, DNS record type, and DNS class */
|
||||
/* Add NUL termination */
|
||||
|
||||
*dest++ = '\0';
|
||||
*qname++ = '\0';
|
||||
|
||||
/* Store name length to saved info */
|
||||
|
||||
DEBUGASSERT(len <= CONFIG_NETDB_DNSCLIENT_NAMESIZE + 1);
|
||||
DEBUGASSERT(qname - qinfo->qname == len + 1);
|
||||
qinfo->qnamelen = len;
|
||||
|
||||
/* Add DNS record type, and DNS class */
|
||||
|
||||
*dest++ = '\0'; /* NUL termination */
|
||||
*dest++ = (rectype >> 8); /* DNS record type (big endian) */
|
||||
*dest++ = (rectype & 0xff);
|
||||
*dest++ = (DNS_CLASS_IN >> 8); /* DNS record class (big endian) */
|
||||
*dest++ = (DNS_CLASS_IN & 0xff);
|
||||
|
||||
qinfo->rectype = htons(rectype);
|
||||
qinfo->id = hdr->id;
|
||||
|
||||
/* Send the request */
|
||||
|
||||
#ifdef CONFIG_NET_IPv4
|
||||
@ -231,6 +301,8 @@ static int dns_send_query(int sd, FAR const char *name,
|
||||
#endif
|
||||
{
|
||||
addrlen = sizeof(struct sockaddr_in);
|
||||
qinfo->u.srv_ipv4 = uaddr->ipv4.sin_addr;
|
||||
qinfo->srv_port = uaddr->ipv4.sin_port;
|
||||
}
|
||||
#endif
|
||||
|
||||
@ -240,6 +312,8 @@ static int dns_send_query(int sd, FAR const char *name,
|
||||
#endif
|
||||
{
|
||||
addrlen = sizeof(struct sockaddr_in6);
|
||||
qinfo->u.srv_ipv6 = uaddr->ipv6.sin6_addr;
|
||||
qinfo->srv_port = uaddr->ipv6.sin6_port;
|
||||
}
|
||||
#endif
|
||||
|
||||
@ -266,23 +340,28 @@ static int dns_send_query(int sd, FAR const char *name,
|
||||
****************************************************************************/
|
||||
|
||||
static int dns_recv_response(int sd, FAR struct sockaddr *addr,
|
||||
FAR socklen_t *addrlen)
|
||||
FAR socklen_t *addrlen,
|
||||
FAR struct dns_query_info_s *qinfo)
|
||||
{
|
||||
FAR uint8_t *nameptr;
|
||||
FAR uint8_t *namestart;
|
||||
FAR uint8_t *endofbuffer;
|
||||
char buffer[RECV_BUFFER_SIZE];
|
||||
FAR struct dns_answer_s *ans;
|
||||
FAR struct dns_header_s *hdr;
|
||||
#if 0 /* Not used */
|
||||
uint8_t nquestions;
|
||||
#endif
|
||||
uint8_t nanswers;
|
||||
FAR struct dns_question_s *que;
|
||||
uint16_t nquestions;
|
||||
uint16_t nanswers;
|
||||
union dns_server_u recvaddr;
|
||||
socklen_t raddrlen;
|
||||
int errcode;
|
||||
int ret;
|
||||
|
||||
/* Receive the response */
|
||||
|
||||
ret = _NX_RECV(sd, buffer, RECV_BUFFER_SIZE, 0);
|
||||
raddrlen = sizeof(recvaddr.addr);
|
||||
ret = _NX_RECVFROM(sd, buffer, RECV_BUFFER_SIZE, 0,
|
||||
&recvaddr.addr, &raddrlen);
|
||||
if (ret < 0)
|
||||
{
|
||||
errcode = -_NX_GETERRNO(ret);
|
||||
@ -290,6 +369,53 @@ static int dns_recv_response(int sd, FAR struct sockaddr *addr,
|
||||
return errcode;
|
||||
}
|
||||
|
||||
#ifdef CONFIG_NET_IPv4
|
||||
/* Check for an IPv4 address */
|
||||
|
||||
if (recvaddr.addr.sa_family == AF_INET)
|
||||
{
|
||||
if (memcmp(&recvaddr.ipv4.sin_addr, &qinfo->u.srv_ipv4,
|
||||
sizeof(recvaddr.ipv4.sin_addr)) != 0)
|
||||
{
|
||||
/* Not response from DNS server. */
|
||||
|
||||
nerr("ERROR: DNS packet from wrong address\n");
|
||||
return -EBADMSG;
|
||||
}
|
||||
|
||||
if (recvaddr.ipv4.sin_port != qinfo->srv_port)
|
||||
{
|
||||
/* Not response from DNS server. */
|
||||
|
||||
nerr("ERROR: DNS packet from wrong port\n");
|
||||
return -EBADMSG;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef CONFIG_NET_IPv6
|
||||
/* Check for an IPv6 address */
|
||||
|
||||
if (recvaddr.addr.sa_family == AF_INET6)
|
||||
{
|
||||
if (memcmp(&recvaddr.ipv6.sin6_addr, &qinfo->u.srv_ipv6,
|
||||
sizeof(recvaddr.ipv6.sin6_addr)) != 0)
|
||||
{
|
||||
/* Not response from DNS server. */
|
||||
|
||||
nerr("ERROR: DNS packet from wrong address\n");
|
||||
return -EBADMSG;
|
||||
}
|
||||
|
||||
if (recvaddr.ipv6.sin6_port != qinfo->srv_port)
|
||||
{
|
||||
/* Not response from DNS server. */
|
||||
|
||||
nerr("ERROR: DNS packet from wrong port\n");
|
||||
return -EBADMSG;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (ret < sizeof(*hdr))
|
||||
{
|
||||
/* DNS header can't fit in received data */
|
||||
@ -316,39 +442,55 @@ static int dns_recv_response(int sd, FAR struct sockaddr *addr,
|
||||
return -EPROTO;
|
||||
}
|
||||
|
||||
/* Check for matching ID. */
|
||||
|
||||
if (hdr->id != qinfo->id)
|
||||
{
|
||||
nerr("ERROR: DNS wrong response ID (expected %d, got %d)\n",
|
||||
htons(qinfo->id), htons(hdr->id));
|
||||
return -EBADMSG;
|
||||
}
|
||||
|
||||
/* We only care about the question(s) and the answers. The authrr
|
||||
* and the extrarr are simply discarded.
|
||||
*/
|
||||
|
||||
#if 0 /* Not used */
|
||||
nquestions = htons(hdr->numquestions);
|
||||
#endif
|
||||
nanswers = htons(hdr->numanswers);
|
||||
|
||||
/* Skip the name in the question. TODO: This should really be
|
||||
* checked against the name in the question, to be sure that they
|
||||
* match.
|
||||
/* We only ever send queries with one question. */
|
||||
|
||||
if (nquestions != 1)
|
||||
{
|
||||
nerr("ERROR: DNS wrong number of questions %d\n", nquestions);
|
||||
return -EBADMSG;
|
||||
}
|
||||
|
||||
/* Skip the name in the answer, but do check that it
|
||||
* matches against the name in the question.
|
||||
*/
|
||||
|
||||
#if defined(CONFIG_DEBUG_NET) && defined(CONFIG_DEBUG_INFO)
|
||||
{
|
||||
int d = 64;
|
||||
|
||||
nameptr = dns_parse_name((uint8_t *)buffer + 12, endofbuffer);
|
||||
namestart = (uint8_t *)buffer + sizeof(*hdr);
|
||||
nameptr = dns_parse_name(namestart, endofbuffer);
|
||||
if (nameptr == endofbuffer)
|
||||
{
|
||||
return -EILSEQ;
|
||||
}
|
||||
|
||||
nameptr += 4;
|
||||
#if defined(CONFIG_DEBUG_NET) && defined(CONFIG_DEBUG_INFO)
|
||||
{
|
||||
int d = 64;
|
||||
FAR uint8_t *ptr = nameptr + sizeof(struct dns_question_s);
|
||||
|
||||
lib_dumpbuffer("namestart: ", namestart, nameptr - namestart);
|
||||
|
||||
for (; ; )
|
||||
{
|
||||
ninfo("%02X %02X %02X %02X %02X %02X %02X %02X \n",
|
||||
nameptr[0], nameptr[1], nameptr[2], nameptr[3],
|
||||
nameptr[4], nameptr[5], nameptr[6], nameptr[7]);
|
||||
ptr[0], ptr[1], ptr[2], ptr[3],
|
||||
ptr[4], ptr[5], ptr[6], ptr[7]);
|
||||
|
||||
nameptr += 8;
|
||||
ptr += 8;
|
||||
d -= 8;
|
||||
if (d < 0)
|
||||
{
|
||||
@ -358,13 +500,40 @@ static int dns_recv_response(int sd, FAR struct sockaddr *addr,
|
||||
}
|
||||
#endif
|
||||
|
||||
nameptr = dns_parse_name((uint8_t *)buffer + 12, endofbuffer);
|
||||
if (nameptr == endofbuffer)
|
||||
/* Since dns_parse_name() skips any pointer bytes,
|
||||
* we cannot compare for equality here.
|
||||
*/
|
||||
|
||||
if (nameptr - namestart < qinfo->qnamelen)
|
||||
{
|
||||
return -EILSEQ;
|
||||
nerr("ERROR: DNS response name wrong length\n");
|
||||
return -EBADMSG;
|
||||
}
|
||||
|
||||
nameptr += 4;
|
||||
/* qname is NUL-terminated and we must include NUL to the comparison. */
|
||||
|
||||
if (memcmp(namestart, qinfo->qname, qinfo->qnamelen + 1) != 0)
|
||||
{
|
||||
nerr("ERROR: DNS response with wrong name\n");
|
||||
return -EBADMSG;
|
||||
}
|
||||
|
||||
/* Validate query type and class */
|
||||
|
||||
que = (FAR struct dns_question_s *)nameptr;
|
||||
ninfo("Question: type=%04x, class=%04x\n",
|
||||
htons(que->type), htons(que->class));
|
||||
|
||||
if (que->type != qinfo->rectype ||
|
||||
que->class != HTONS(DNS_CLASS_IN))
|
||||
{
|
||||
nerr("ERROR: DNS response with wrong question\n");
|
||||
return -EBADMSG;
|
||||
}
|
||||
|
||||
/* Skip over question */
|
||||
|
||||
nameptr += sizeof(struct dns_question_s);
|
||||
|
||||
for (; nanswers > 0; nanswers--)
|
||||
{
|
||||
@ -383,7 +552,9 @@ static int dns_recv_response(int sd, FAR struct sockaddr *addr,
|
||||
(htons(ans->ttl[0]) << 16) | htons(ans->ttl[1]),
|
||||
htons(ans->len));
|
||||
|
||||
/* Check for IPv4/6 address type and Internet class. Others are discarded. */
|
||||
/* Check for IPv4/6 address type and Internet class. Others are
|
||||
* discarded.
|
||||
*/
|
||||
|
||||
#ifdef CONFIG_NET_IPv4
|
||||
if (ans->type == HTONS(DNS_RECTYPE_A) &&
|
||||
@ -482,10 +653,13 @@ static int dns_query_callback(FAR void *arg, FAR struct sockaddr *addr,
|
||||
FAR socklen_t addrlen)
|
||||
{
|
||||
FAR struct dns_query_s *query = (FAR struct dns_query_s *)arg;
|
||||
FAR struct dns_query_info_s qinfo;
|
||||
int retries;
|
||||
int ret;
|
||||
|
||||
/* Loop while receive timeout errors occur and there are remaining retries */
|
||||
/* Loop while receive timeout errors occur and there are remaining
|
||||
* retries.
|
||||
*/
|
||||
|
||||
for (retries = 0; retries < CONFIG_NETDB_DNSCLIENT_RETRIES; retries++)
|
||||
{
|
||||
@ -511,7 +685,7 @@ static int dns_query_callback(FAR void *arg, FAR struct sockaddr *addr,
|
||||
|
||||
ret = dns_send_query(query->sd, query->hostname,
|
||||
(FAR union dns_server_u *)addr,
|
||||
DNS_RECTYPE_A);
|
||||
DNS_RECTYPE_A, &qinfo);
|
||||
if (ret < 0)
|
||||
{
|
||||
/* Return zero to skip this address and try the next
|
||||
@ -525,7 +699,8 @@ static int dns_query_callback(FAR void *arg, FAR struct sockaddr *addr,
|
||||
|
||||
/* Obtain the IPv4 response */
|
||||
|
||||
ret = dns_recv_response(query->sd, query->addr, query->addrlen);
|
||||
ret = dns_recv_response(query->sd, query->addr, query->addrlen,
|
||||
&qinfo);
|
||||
if (ret >= 0)
|
||||
{
|
||||
/* IPv4 response received successfully */
|
||||
@ -533,7 +708,8 @@ static int dns_query_callback(FAR void *arg, FAR struct sockaddr *addr,
|
||||
#if CONFIG_NETDB_DNSCLIENT_ENTRIES > 0
|
||||
/* Save the answer in the DNS cache */
|
||||
|
||||
dns_save_answer(query->hostname, query->addr, *query->addrlen);
|
||||
dns_save_answer(query->hostname, query->addr,
|
||||
*query->addrlen);
|
||||
#endif
|
||||
/* Return 1 to indicate to (1) stop the traversal, and (2)
|
||||
* indicate that the address was found.
|
||||
@ -592,7 +768,7 @@ static int dns_query_callback(FAR void *arg, FAR struct sockaddr *addr,
|
||||
|
||||
ret = dns_send_query(query->sd, query->hostname,
|
||||
(FAR union dns_server_u *)addr,
|
||||
DNS_RECTYPE_AAAA);
|
||||
DNS_RECTYPE_AAAA, &qinfo);
|
||||
if (ret < 0)
|
||||
{
|
||||
/* Return zero to skip this address and try the next
|
||||
@ -606,7 +782,8 @@ static int dns_query_callback(FAR void *arg, FAR struct sockaddr *addr,
|
||||
|
||||
/* Obtain the IPv6 response */
|
||||
|
||||
ret = dns_recv_response(query->sd, query->addr, query->addrlen);
|
||||
ret = dns_recv_response(query->sd, query->addr, query->addrlen,
|
||||
&qinfo);
|
||||
if (ret >= 0)
|
||||
{
|
||||
/* IPv6 response received successfully */
|
||||
|
Loading…
x
Reference in New Issue
Block a user