Files
ipxe/src/net/udp/dns.c
T
Michael Brown 2acdc92994 [dns] Always start DNS queries using the first configured DNS server
We currently define the active DNS server as a global variable.  All
queries will start by attempting to contact the active DNS server, and
the active DNS server will be changed only if we fail to get a
response.  This effectively treats the DNS server list as expressing a
weak preference ordering: we will try servers in order, but once we
have found a working server we will stick with that server for as long
as it continues to respond to queries.

Some sites are misconfigured to hand out DNS servers that do not have
a consistent worldview.  For example: the site may hand out two DNS
server addresses, the first being an internal DNS server (which is
able to resolve names in private DNS domains) and the second being a
public DNS server such as 8.8.8.8 (which will correctly return
NXDOMAIN for any private DNS domains).  This type of configuration is
fundamentally broken and should never be used, since any DNS resolver
performing a query for a name within a private DNS domain may obtain a
spurious NXDOMAIN response for a valid private DNS name.

Work around these broken configurations by treating the DNS server
list as expressing a strong preference ordering, and always starting
DNS queries from the first server in the list (rather than maintaining
a global concept of the active server).  This will have the debatable
benefit of converting permanent spurious NXDOMAIN errors into
transient spurious NXDOMAIN errors, which can at least be worked
around at a higher level (e.g. by retrying a download in a loop within
an iPXE script).

The cost of always starting DNS queries from the first server in the
list is a slight delay introduced when the first server is genuinely
unavailable.  This should be negligible in practice since DNS queries
are relatively infrequent and the failover expiry time is short.

Treating the DNS server list as a preference ordering is permitted by
the language of RFC 2132, which defines DHCP option 6 as a list in
which "[DNS] servers SHOULD be listed in order of preference".  No
specification defines a precise algorithm for how this preference
order should be applied in practice: this new approach seems as good
as any.

Requested-by: Andreas Hammarskjöld <junior@2PintSoftware.com>
Signed-off-by: Michael Brown <mcb30@ipxe.org>
2022-02-23 23:17:05 +00:00

1261 lines
31 KiB
C

/*
* Copyright (C) 2006 Michael Brown <mbrown@fensystems.co.uk>.
*
* Portions copyright (C) 2004 Anselm M. Hoffmeister
* <stockholm@users.sourceforge.net>.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of the
* License, or any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*
* You can also choose to distribute this program under the terms of
* the Unmodified Binary Distribution Licence (as given in the file
* COPYING.UBDL), provided that you have satisfied its requirements.
*/
FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL );
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <ctype.h>
#include <errno.h>
#include <byteswap.h>
#include <ipxe/refcnt.h>
#include <ipxe/iobuf.h>
#include <ipxe/xfer.h>
#include <ipxe/open.h>
#include <ipxe/resolv.h>
#include <ipxe/retry.h>
#include <ipxe/tcpip.h>
#include <ipxe/settings.h>
#include <ipxe/features.h>
#include <ipxe/job.h>
#include <ipxe/dhcp.h>
#include <ipxe/dhcpv6.h>
#include <ipxe/dns.h>
/** @file
*
* DNS protocol
*
*/
FEATURE ( FEATURE_PROTOCOL, "DNS", DHCP_EB_FEATURE_DNS, 1 );
/* Disambiguate the various error causes */
#define ENXIO_NO_RECORD __einfo_error ( EINFO_ENXIO_NO_RECORD )
#define EINFO_ENXIO_NO_RECORD \
__einfo_uniqify ( EINFO_ENXIO, 0x01, "DNS name does not exist" )
#define ENXIO_NO_NAMESERVER __einfo_error ( EINFO_ENXIO_NO_NAMESERVER )
#define EINFO_ENXIO_NO_NAMESERVER \
__einfo_uniqify ( EINFO_ENXIO, 0x02, "No DNS servers available" )
/** A DNS server list */
struct dns_server {
/** Server list */
union {
/** IPv4 addresses */
struct in_addr *in;
/** IPv6 addresses */
struct in6_addr *in6;
/** Raw data */
void *data;
};
/** Number of servers */
unsigned int count;
};
/** IPv4 DNS server list */
static struct dns_server dns4;
/** IPv6 DNS server list */
static struct dns_server dns6;
/** Total number of DNS servers */
static unsigned int dns_count;
/** The DNS search list */
static struct dns_name dns_search;
/**
* Encode a DNS name using RFC1035 encoding
*
* @v string DNS name as a string
* @v name DNS name to fill in
* @ret len Length of DNS name, or negative error
*/
int dns_encode ( const char *string, struct dns_name *name ) {
uint8_t *start = ( name->data + name->offset );
uint8_t *end = ( name->data + name->len );
uint8_t *dst = start;
size_t len = 0;
char c;
/* Encode name */
while ( ( c = *(string++) ) ) {
/* Handle '.' separators */
if ( c == '.' ) {
/* Reject consecutive '.' */
if ( ( len == 0 ) && ( dst != start ) )
return -EINVAL;
/* Terminate if this is the trailing '.' */
if ( *string == '\0' )
break;
/* Reject initial non-terminating '.' */
if ( len == 0 )
return -EINVAL;
/* Reset length */
len = 0;
} else {
/* Increment length */
len++;
/* Check for overflow */
if ( len > DNS_MAX_LABEL_LEN )
return -EINVAL;
}
/* Copy byte, update length */
if ( ++dst < end ) {
*dst = c;
dst[-len] = len;
}
}
/* Add terminating root marker */
if ( len )
dst++;
if ( dst < end )
*dst = '\0';
dst++;
return ( dst - start );
}
/**
* Find start of valid label within an RFC1035-encoded DNS name
*
* @v name DNS name
* @v offset Current offset
* @ret offset Offset of label, or negative error
*/
static int dns_label ( struct dns_name *name, size_t offset ) {
const uint8_t *byte;
const uint16_t *word;
size_t len;
size_t ptr;
while ( 1 ) {
/* Fail if we have overrun the DNS name */
if ( ( offset + sizeof ( *byte) ) > name->len )
return -EINVAL;
byte = ( name->data + offset );
/* Follow compression pointer, if applicable */
if ( DNS_IS_COMPRESSED ( *byte ) ) {
/* Fail if we have overrun the DNS name */
if ( ( offset + sizeof ( *word ) ) > name->len )
return -EINVAL;
word = ( name->data + offset );
/* Extract pointer to new offset */
ptr = DNS_COMPRESSED_OFFSET ( ntohs ( *word ) );
/* Fail if pointer does not point backwards.
* (This guarantees termination of the
* function.)
*/
if ( ptr >= offset )
return -EINVAL;
/* Continue from new offset */
offset = ptr;
continue;
}
/* Fail if we have overrun the DNS name */
len = *byte;
if ( ( offset + sizeof ( *byte ) + len ) > name->len )
return -EINVAL;
/* We have a valid label */
return offset;
}
}
/**
* Decode RFC1035-encoded DNS name
*
* @v name DNS name
* @v data Output buffer
* @v len Length of output buffer
* @ret len Length of decoded DNS name, or negative error
*/
int dns_decode ( struct dns_name *name, char *data, size_t len ) {
unsigned int recursion_limit = name->len; /* Generous upper bound */
int offset = name->offset;
const uint8_t *label;
size_t decoded_len = 0;
size_t label_len;
size_t copy_len;
while ( recursion_limit-- ) {
/* Find valid DNS label */
offset = dns_label ( name, offset );
if ( offset < 0 )
return offset;
/* Terminate if we have reached the root */
label = ( name->data + offset );
label_len = *(label++);
if ( label_len == 0 ) {
if ( decoded_len < len )
*data = '\0';
return decoded_len;
}
/* Prepend '.' if applicable */
if ( decoded_len && ( decoded_len++ < len ) )
*(data++) = '.';
/* Copy label to output buffer */
copy_len = ( ( decoded_len < len ) ? ( len - decoded_len ) : 0);
if ( copy_len > label_len )
copy_len = label_len;
memcpy ( data, label, copy_len );
data += copy_len;
decoded_len += label_len;
/* Move to next label */
offset += ( sizeof ( *label ) + label_len );
}
/* Recursion limit exceeded */
return -EINVAL;
}
/**
* Compare DNS names for equality
*
* @v first First DNS name
* @v second Second DNS name
* @ret rc Return status code
*/
int dns_compare ( struct dns_name *first, struct dns_name *second ) {
unsigned int recursion_limit = first->len; /* Generous upper bound */
int first_offset = first->offset;
int second_offset = second->offset;
const uint8_t *first_label;
const uint8_t *second_label;
size_t label_len;
size_t len;
while ( recursion_limit-- ) {
/* Find valid DNS labels */
first_offset = dns_label ( first, first_offset );
if ( first_offset < 0 )
return first_offset;
second_offset = dns_label ( second, second_offset );
if ( second_offset < 0 )
return second_offset;
/* Compare label lengths */
first_label = ( first->data + first_offset );
second_label = ( second->data + second_offset );
label_len = *(first_label++);
if ( label_len != *(second_label++) )
return -ENOENT;
len = ( sizeof ( *first_label ) + label_len );
/* Terminate if we have reached the root */
if ( label_len == 0 )
return 0;
/* Compare label contents (case-insensitively) */
while ( label_len-- ) {
if ( tolower ( *(first_label++) ) !=
tolower ( *(second_label++) ) )
return -ENOENT;
}
/* Move to next labels */
first_offset += len;
second_offset += len;
}
/* Recursion limit exceeded */
return -EINVAL;
}
/**
* Copy a DNS name
*
* @v src Source DNS name
* @v dst Destination DNS name
* @ret len Length of copied DNS name, or negative error
*/
int dns_copy ( struct dns_name *src, struct dns_name *dst ) {
unsigned int recursion_limit = src->len; /* Generous upper bound */
int src_offset = src->offset;
size_t dst_offset = dst->offset;
const uint8_t *label;
size_t label_len;
size_t copy_len;
size_t len;
while ( recursion_limit-- ) {
/* Find valid DNS label */
src_offset = dns_label ( src, src_offset );
if ( src_offset < 0 )
return src_offset;
/* Copy as an uncompressed label */
label = ( src->data + src_offset );
label_len = *label;
len = ( sizeof ( *label ) + label_len );
copy_len = ( ( dst_offset < dst->len ) ?
( dst->len - dst_offset ) : 0 );
if ( copy_len > len )
copy_len = len;
memcpy ( ( dst->data + dst_offset ), label, copy_len );
src_offset += len;
dst_offset += len;
/* Terminate if we have reached the root */
if ( label_len == 0 )
return ( dst_offset - dst->offset );
}
/* Recursion limit exceeded */
return -EINVAL;
}
/**
* Skip RFC1035-encoded DNS name
*
* @v name DNS name
* @ret offset Offset to next name, or negative error
*/
int dns_skip ( struct dns_name *name ) {
unsigned int recursion_limit = name->len; /* Generous upper bound */
int offset = name->offset;
int prev_offset;
const uint8_t *label;
size_t label_len;
while ( recursion_limit-- ) {
/* Find valid DNS label */
prev_offset = offset;
offset = dns_label ( name, prev_offset );
if ( offset < 0 )
return offset;
/* Terminate if we have reached a compression pointer */
if ( offset != prev_offset )
return ( prev_offset + sizeof ( uint16_t ) );
/* Skip this label */
label = ( name->data + offset );
label_len = *label;
offset += ( sizeof ( *label ) + label_len );
/* Terminate if we have reached the root */
if ( label_len == 0 )
return offset;
}
/* Recursion limit exceeded */
return -EINVAL;
}
/**
* Skip RFC1035-encoded DNS name in search list
*
* @v name DNS name
* @ret offset Offset to next non-empty name, or negative error
*/
static int dns_skip_search ( struct dns_name *name ) {
int offset;
/* Find next name */
offset = dns_skip ( name );
if ( offset < 0 )
return offset;
/* Skip over any subsequent empty names (e.g. due to padding
* bytes used in the NDP DNSSL option).
*/
while ( ( offset < ( ( int ) name->len ) ) &&
( *( ( uint8_t * ) ( name->data + offset ) ) == 0 ) ) {
offset++;
}
return offset;
}
/**
* Transcribe DNS name (for debugging)
*
* @v name DNS name
* @ret string Transcribed DNS name
*/
static const char * dns_name ( struct dns_name *name ) {
static char buf[256];
int len;
len = dns_decode ( name, buf, ( sizeof ( buf ) - 1 /* NUL */ ) );
return ( ( len < 0 ) ? "<INVALID>" : buf );
}
/**
* Name a DNS query type (for debugging)
*
* @v type Query type (in network byte order)
* @ret name Type name
*/
static const char * dns_type ( uint16_t type ) {
switch ( type ) {
case htons ( DNS_TYPE_A ): return "A";
case htons ( DNS_TYPE_AAAA ): return "AAAA";
case htons ( DNS_TYPE_CNAME ): return "CNAME";
default: return "<UNKNOWN>";
}
}
/** A DNS request */
struct dns_request {
/** Reference counter */
struct refcnt refcnt;
/** Name resolution interface */
struct interface resolv;
/** Data transfer interface */
struct interface socket;
/** Retry timer */
struct retry_timer timer;
/** Socket address to fill in with resolved address */
union {
struct sockaddr sa;
struct sockaddr_in sin;
struct sockaddr_in6 sin6;
} address;
/** Initial query type */
uint16_t qtype;
/** Buffer for current query */
struct {
/** Query header */
struct dns_header query;
/** Name buffer */
char name[DNS_MAX_NAME_LEN];
/** Space for question */
struct dns_question padding;
} __attribute__ (( packed )) buf;
/** Current query name */
struct dns_name name;
/** Question within current query */
struct dns_question *question;
/** Length of current query */
size_t len;
/** Offset of search suffix within current query */
size_t offset;
/** Search list */
struct dns_name search;
/** Server index */
unsigned int index;
/** Recursion counter */
unsigned int recursion;
};
/**
* Mark DNS request as complete
*
* @v dns DNS request
* @v rc Return status code
*/
static void dns_done ( struct dns_request *dns, int rc ) {
/* Stop the retry timer */
stop_timer ( &dns->timer );
/* Shut down interfaces */
intf_shutdown ( &dns->socket, rc );
intf_shutdown ( &dns->resolv, rc );
}
/**
* Mark DNS request as resolved and complete
*
* @v dns DNS request
* @v rc Return status code
*/
static void dns_resolved ( struct dns_request *dns ) {
DBGC ( dns, "DNS %p found address %s\n",
dns, sock_ntoa ( &dns->address.sa ) );
/* Return resolved address */
resolv_done ( &dns->resolv, &dns->address.sa );
/* Mark operation as complete */
dns_done ( dns, 0 );
}
/**
* Construct DNS question
*
* @v dns DNS request
* @ret rc Return status code
*/
static int dns_question ( struct dns_request *dns ) {
static struct dns_name search_root = {
.data = "",
.len = 1,
};
struct dns_name *search = &dns->search;
int len;
size_t offset;
/* Use root suffix if search list is empty */
if ( search->offset == search->len )
search = &search_root;
/* Overwrite current suffix */
dns->name.offset = dns->offset;
len = dns_copy ( search, &dns->name );
if ( len < 0 )
return len;
/* Sanity check */
offset = ( dns->name.offset + len );
if ( offset > dns->name.len ) {
DBGC ( dns, "DNS %p name is too long\n", dns );
return -EINVAL;
}
/* Construct question */
dns->question = ( ( ( void * ) &dns->buf ) + offset );
dns->question->qtype = dns->qtype;
dns->question->qclass = htons ( DNS_CLASS_IN );
/* Store length */
dns->len = ( offset + sizeof ( *(dns->question) ) );
/* Restore name */
dns->name.offset = offsetof ( typeof ( dns->buf ), name );
/* Reset query ID */
dns->buf.query.id = 0;
DBGC2 ( dns, "DNS %p question is %s type %s\n", dns,
dns_name ( &dns->name ), dns_type ( dns->question->qtype ) );
return 0;
}
/**
* Send DNS query
*
* @v dns DNS request
* @ret rc Return status code
*/
static int dns_send_packet ( struct dns_request *dns ) {
struct dns_header *query = &dns->buf.query;
union {
struct sockaddr sa;
struct sockaddr_tcpip st;
struct sockaddr_in sin;
struct sockaddr_in6 sin6;
} nameserver;
struct xfer_metadata meta;
unsigned int index;
/* Start retransmission timer */
start_timer ( &dns->timer );
/* Construct DNS server address */
memset ( &nameserver, 0, sizeof ( nameserver ) );
nameserver.st.st_port = htons ( DNS_PORT );
if ( ! dns_count ) {
DBGC ( dns, "DNS %p lost DNS servers mid query\n", dns );
return -EINVAL;
}
index = ( dns->index % dns_count );
if ( index < dns6.count ) {
nameserver.sin6.sin6_family = AF_INET6;
memcpy ( &nameserver.sin6.sin6_addr, &dns6.in6[index],
sizeof ( nameserver.sin6.sin6_addr ) );
} else {
nameserver.sin.sin_family = AF_INET;
nameserver.sin.sin_addr = dns4.in[index - dns6.count];
}
/* Construct metadata */
memset ( &meta, 0, sizeof ( meta ) );
meta.dest = &nameserver.sa;
/* Generate query identifier if applicable */
if ( ! query->id )
query->id = random();
/* Send query */
DBGC ( dns, "DNS %p sending %s query ID %#04x for %s type %s\n", dns,
sock_ntoa ( &nameserver.sa ), ntohs ( query->id ),
dns_name ( &dns->name ), dns_type ( dns->question->qtype ) );
/* Send the data */
return xfer_deliver_raw_meta ( &dns->socket, query, dns->len, &meta );
}
/**
* Handle DNS (re)transmission timer expiry
*
* @v timer Retry timer
* @v fail Failure indicator
*/
static void dns_timer_expired ( struct retry_timer *timer, int fail ) {
struct dns_request *dns =
container_of ( timer, struct dns_request, timer );
/* Terminate DNS request on failure */
if ( fail ) {
dns_done ( dns, -ETIMEDOUT );
return;
}
/* Move to next DNS server if this is a retransmission */
if ( dns->buf.query.id )
dns->index++;
/* Send DNS query */
dns_send_packet ( dns );
}
/**
* Receive new data
*
* @v dns DNS request
* @v iobuf I/O buffer
* @v meta Data transfer metadata
* @ret rc Return status code
*/
static int dns_xfer_deliver ( struct dns_request *dns,
struct io_buffer *iobuf,
struct xfer_metadata *meta __unused ) {
struct dns_header *response = iobuf->data;
struct dns_header *query = &dns->buf.query;
unsigned int qtype = dns->question->qtype;
struct dns_name buf;
union dns_rr *rr;
int offset;
size_t answer_offset;
size_t next_offset;
size_t rdlength;
size_t name_len;
int rc;
/* Sanity check */
if ( iob_len ( iobuf ) < sizeof ( *response ) ) {
DBGC ( dns, "DNS %p received underlength packet length %zd\n",
dns, iob_len ( iobuf ) );
rc = -EINVAL;
goto done;
}
/* Check response ID matches query ID */
if ( response->id != query->id ) {
DBGC ( dns, "DNS %p received unexpected response ID %#04x "
"(wanted %d)\n", dns, ntohs ( response->id ),
ntohs ( query->id ) );
rc = -EINVAL;
goto done;
}
DBGC ( dns, "DNS %p received response ID %#04x\n",
dns, ntohs ( response->id ) );
/* Check that we have exactly one question */
if ( response->qdcount != htons ( 1 ) ) {
DBGC ( dns, "DNS %p received response with %d questions\n",
dns, ntohs ( response->qdcount ) );
rc = -EINVAL;
goto done;
}
/* Skip question section */
buf.data = iobuf->data;
buf.offset = sizeof ( *response );
buf.len = iob_len ( iobuf );
offset = dns_skip ( &buf );
if ( offset < 0 ) {
rc = offset;
DBGC ( dns, "DNS %p received response with malformed "
"question: %s\n", dns, strerror ( rc ) );
goto done;
}
answer_offset = ( offset + sizeof ( struct dns_question ) );
/* Search through response for useful answers. Do this
* multiple times, to take advantage of useful nameservers
* which send us e.g. the CNAME *and* the A record for the
* pointed-to name.
*/
for ( buf.offset = answer_offset ; buf.offset != buf.len ;
buf.offset = next_offset ) {
/* Check for valid name */
offset = dns_skip ( &buf );
if ( offset < 0 ) {
rc = offset;
DBGC ( dns, "DNS %p received response with malformed "
"answer: %s\n", dns, strerror ( rc ) );
goto done;
}
/* Check for sufficient space for resource record */
rr = ( buf.data + offset );
if ( ( offset + sizeof ( rr->common ) ) > buf.len ) {
DBGC ( dns, "DNS %p received response with underlength "
"RR\n", dns );
rc = -EINVAL;
goto done;
}
rdlength = ntohs ( rr->common.rdlength );
next_offset = ( offset + sizeof ( rr->common ) + rdlength );
if ( next_offset > buf.len ) {
DBGC ( dns, "DNS %p received response with underlength "
"RR\n", dns );
rc = -EINVAL;
goto done;
}
/* Skip non-matching names */
if ( dns_compare ( &buf, &dns->name ) != 0 ) {
DBGC2 ( dns, "DNS %p ignoring response for %s type "
"%s\n", dns, dns_name ( &buf ),
dns_type ( rr->common.type ) );
continue;
}
/* Handle answer */
switch ( rr->common.type ) {
case htons ( DNS_TYPE_AAAA ):
/* Found the target AAAA record */
if ( rdlength < sizeof ( dns->address.sin6.sin6_addr )){
DBGC ( dns, "DNS %p received response with "
"underlength AAAA\n", dns );
rc = -EINVAL;
goto done;
}
dns->address.sin6.sin6_family = AF_INET6;
memcpy ( &dns->address.sin6.sin6_addr,
&rr->aaaa.in6_addr,
sizeof ( dns->address.sin6.sin6_addr ) );
dns_resolved ( dns );
rc = 0;
goto done;
case htons ( DNS_TYPE_A ):
/* Found the target A record */
if ( rdlength < sizeof ( dns->address.sin.sin_addr ) ) {
DBGC ( dns, "DNS %p received response with "
"underlength A\n", dns );
rc = -EINVAL;
goto done;
}
dns->address.sin.sin_family = AF_INET;
dns->address.sin.sin_addr = rr->a.in_addr;
dns_resolved ( dns );
rc = 0;
goto done;
case htons ( DNS_TYPE_CNAME ):
/* Terminate the operation if we recurse too far */
if ( ++dns->recursion > DNS_MAX_CNAME_RECURSION ) {
DBGC ( dns, "DNS %p recursion exceeded\n",
dns );
rc = -ELOOP;
dns_done ( dns, rc );
goto done;
}
/* Found a CNAME record; update query and recurse */
buf.offset = ( offset + sizeof ( rr->cname ) );
DBGC ( dns, "DNS %p found CNAME %s\n",
dns, dns_name ( &buf ) );
dns->search.offset = dns->search.len;
name_len = dns_copy ( &buf, &dns->name );
dns->offset = ( offsetof ( typeof ( dns->buf ), name ) +
name_len - 1 /* Strip root label */ );
if ( ( rc = dns_question ( dns ) ) != 0 ) {
dns_done ( dns, rc );
goto done;
}
next_offset = answer_offset;
break;
default:
DBGC ( dns, "DNS %p got unknown record type %d\n",
dns, ntohs ( rr->common.type ) );
break;
}
}
/* Stop the retry timer. After this point, each code path
* must either restart the timer by calling dns_send_packet(),
* or mark the DNS operation as complete by calling
* dns_done()
*/
stop_timer ( &dns->timer );
/* Determine what to do next based on the type of query we
* issued and the response we received
*/
switch ( qtype ) {
case htons ( DNS_TYPE_AAAA ):
/* We asked for an AAAA record and got nothing; try
* the A.
*/
DBGC ( dns, "DNS %p found no AAAA record; trying A\n", dns );
dns->question->qtype = htons ( DNS_TYPE_A );
dns_send_packet ( dns );
rc = 0;
goto done;
case htons ( DNS_TYPE_A ):
/* We asked for an A record and got nothing;
* try the CNAME.
*/
DBGC ( dns, "DNS %p found no A record; trying CNAME\n", dns );
dns->question->qtype = htons ( DNS_TYPE_CNAME );
dns_send_packet ( dns );
rc = 0;
goto done;
case htons ( DNS_TYPE_CNAME ):
/* We asked for a CNAME record. If we got a response
* (i.e. if the next AAAA/A query is already set up),
* then issue it.
*/
if ( qtype == dns->qtype ) {
dns_send_packet ( dns );
rc = 0;
goto done;
}
/* If we have already reached the end of the search list,
* then terminate lookup.
*/
if ( dns->search.offset == dns->search.len ) {
DBGC ( dns, "DNS %p found no CNAME record\n", dns );
rc = -ENXIO_NO_RECORD;
dns_done ( dns, rc );
goto done;
}
/* Move to next entry in search list. This can never fail,
* since we have already used this entry.
*/
DBGC ( dns, "DNS %p found no CNAME record; trying next "
"suffix\n", dns );
dns->search.offset = dns_skip_search ( &dns->search );
if ( ( rc = dns_question ( dns ) ) != 0 ) {
dns_done ( dns, rc );
goto done;
}
dns_send_packet ( dns );
goto done;
default:
assert ( 0 );
rc = -EINVAL;
dns_done ( dns, rc );
goto done;
}
done:
/* Free I/O buffer */
free_iob ( iobuf );
return rc;
}
/**
* Receive new data
*
* @v dns DNS request
* @v rc Reason for close
*/
static void dns_xfer_close ( struct dns_request *dns, int rc ) {
if ( ! rc )
rc = -ECONNABORTED;
dns_done ( dns, rc );
}
/**
* Report job progress
*
* @v dns DNS request
* @v progress Progress report to fill in
* @ret ongoing_rc Ongoing job status code (if known)
*/
static int dns_progress ( struct dns_request *dns,
struct job_progress *progress ) {
int len;
/* Show current question as progress message */
len = dns_decode ( &dns->name, progress->message,
( sizeof ( progress->message ) - 1 /* NUL */ ) );
if ( len < 0 ) {
/* Ignore undecodable names */
progress->message[0] = '\0';
}
return 0;
}
/** DNS socket interface operations */
static struct interface_operation dns_socket_operations[] = {
INTF_OP ( xfer_deliver, struct dns_request *, dns_xfer_deliver ),
INTF_OP ( intf_close, struct dns_request *, dns_xfer_close ),
};
/** DNS socket interface descriptor */
static struct interface_descriptor dns_socket_desc =
INTF_DESC ( struct dns_request, socket, dns_socket_operations );
/** DNS resolver interface operations */
static struct interface_operation dns_resolv_op[] = {
INTF_OP ( job_progress, struct dns_request *, dns_progress ),
INTF_OP ( intf_close, struct dns_request *, dns_done ),
};
/** DNS resolver interface descriptor */
static struct interface_descriptor dns_resolv_desc =
INTF_DESC ( struct dns_request, resolv, dns_resolv_op );
/**
* Resolve name using DNS
*
* @v resolv Name resolution interface
* @v name Name to resolve
* @v sa Socket address to fill in
* @ret rc Return status code
*/
static int dns_resolv ( struct interface *resolv,
const char *name, struct sockaddr *sa ) {
struct dns_request *dns;
struct dns_header *query;
size_t search_len;
int name_len;
int rc;
/* Fail immediately if no DNS servers */
if ( dns_count == 0 ) {
DBG ( "DNS not attempting to resolve \"%s\": "
"no DNS servers\n", name );
rc = -ENXIO_NO_NAMESERVER;
goto err_no_nameserver;
}
/* Determine whether or not to use search list */
search_len = ( strchr ( name, '.' ) ? 0 : dns_search.len );
/* Allocate DNS structure */
dns = zalloc ( sizeof ( *dns ) + search_len );
if ( ! dns ) {
rc = -ENOMEM;
goto err_alloc_dns;
}
ref_init ( &dns->refcnt, NULL );
intf_init ( &dns->resolv, &dns_resolv_desc, &dns->refcnt );
intf_init ( &dns->socket, &dns_socket_desc, &dns->refcnt );
timer_init ( &dns->timer, dns_timer_expired, &dns->refcnt );
memcpy ( &dns->address.sa, sa, sizeof ( dns->address.sa ) );
dns->search.data = ( ( ( void * ) dns ) + sizeof ( *dns ) );
dns->search.len = search_len;
memcpy ( dns->search.data, dns_search.data, search_len );
/* Determine initial query type */
dns->qtype = ( ( dns6.count != 0 ) ?
htons ( DNS_TYPE_AAAA ) : htons ( DNS_TYPE_A ) );
/* Construct query */
query = &dns->buf.query;
query->flags = htons ( DNS_FLAG_RD );
query->qdcount = htons ( 1 );
dns->name.data = &dns->buf;
dns->name.offset = offsetof ( typeof ( dns->buf ), name );
dns->name.len = offsetof ( typeof ( dns->buf ), padding );
name_len = dns_encode ( name, &dns->name );
if ( name_len < 0 ) {
rc = name_len;
goto err_encode;
}
dns->offset = ( offsetof ( typeof ( dns->buf ), name ) +
name_len - 1 /* Strip root label */ );
if ( ( rc = dns_question ( dns ) ) != 0 )
goto err_question;
/* Open UDP connection */
if ( ( rc = xfer_open_socket ( &dns->socket, SOCK_DGRAM,
NULL, NULL ) ) != 0 ) {
DBGC ( dns, "DNS %p could not open socket: %s\n",
dns, strerror ( rc ) );
goto err_open_socket;
}
/* Start timer to trigger first packet */
start_timer_nodelay ( &dns->timer );
/* Attach parent interface, mortalise self, and return */
intf_plug_plug ( &dns->resolv, resolv );
ref_put ( &dns->refcnt );
return 0;
err_open_socket:
err_question:
err_encode:
ref_put ( &dns->refcnt );
err_alloc_dns:
err_no_nameserver:
return rc;
}
/** DNS name resolver */
struct resolver dns_resolver __resolver ( RESOLV_NORMAL ) = {
.name = "DNS",
.resolv = dns_resolv,
};
/******************************************************************************
*
* Settings
*
******************************************************************************
*/
/**
* Format DNS search list setting
*
* @v type Setting type
* @v raw Raw setting value
* @v raw_len Length of raw setting value
* @v buf Buffer to contain formatted value
* @v len Length of buffer
* @ret len Length of formatted value, or negative error
*/
static int format_dnssl_setting ( const struct setting_type *type __unused,
const void *raw, size_t raw_len,
char *buf, size_t len ) {
struct dns_name name = {
.data = ( ( void * ) raw ),
.len = raw_len,
};
size_t remaining = len;
size_t total = 0;
int name_len;
while ( name.offset < raw_len ) {
/* Decode name */
remaining = ( ( total < len ) ? ( len - total ) : 0 );
name_len = dns_decode ( &name, ( buf + total ), remaining );
if ( name_len < 0 )
return name_len;
total += name_len;
/* Move to next name */
name.offset = dns_skip_search ( &name );
/* Add separator if applicable */
if ( name.offset != raw_len ) {
if ( total < len )
buf[total] = ' ';
total++;
}
}
return total;
}
/** A DNS search list setting type */
const struct setting_type setting_type_dnssl __setting_type = {
.name = "dnssl",
.format = format_dnssl_setting,
};
/** IPv4 DNS server setting */
const struct setting dns_setting __setting ( SETTING_IP4_EXTRA, dns ) = {
.name = "dns",
.description = "DNS server",
.tag = DHCP_DNS_SERVERS,
.type = &setting_type_ipv4,
};
/** IPv6 DNS server setting */
const struct setting dns6_setting __setting ( SETTING_IP6_EXTRA, dns6 ) = {
.name = "dns6",
.description = "DNS server",
.tag = DHCPV6_DNS_SERVERS,
.type = &setting_type_ipv6,
.scope = &dhcpv6_scope,
};
/** DNS search list */
const struct setting dnssl_setting __setting ( SETTING_IP_EXTRA, dnssl ) = {
.name = "dnssl",
.description = "DNS search list",
.tag = DHCP_DOMAIN_SEARCH,
.type = &setting_type_dnssl,
};
/**
* Apply DNS server addresses
*
*/
static void apply_dns_servers ( void ) {
int len;
/* Free existing server addresses */
free ( dns4.data );
free ( dns6.data );
dns4.data = NULL;
dns6.data = NULL;
dns4.count = 0;
dns6.count = 0;
/* Fetch DNS server addresses */
len = fetch_raw_setting_copy ( NULL, &dns_setting, &dns4.data );
if ( len >= 0 )
dns4.count = ( len / sizeof ( dns4.in[0] ) );
len = fetch_raw_setting_copy ( NULL, &dns6_setting, &dns6.data );
if ( len >= 0 )
dns6.count = ( len / sizeof ( dns6.in6[0] ) );
dns_count = ( dns4.count + dns6.count );
}
/**
* Apply DNS search list
*
*/
static void apply_dns_search ( void ) {
char *localdomain;
int len;
/* Free existing search list */
free ( dns_search.data );
memset ( &dns_search, 0, sizeof ( dns_search ) );
/* Fetch DNS search list */
len = fetch_raw_setting_copy ( NULL, &dnssl_setting, &dns_search.data );
if ( len >= 0 ) {
dns_search.len = len;
return;
}
/* If no DNS search list exists, try to fetch the local domain */
fetch_string_setting_copy ( NULL, &domain_setting, &localdomain );
if ( localdomain ) {
len = dns_encode ( localdomain, &dns_search );
if ( len >= 0 ) {
dns_search.data = malloc ( len );
if ( dns_search.data ) {
dns_search.len = len;
dns_encode ( localdomain, &dns_search );
}
}
free ( localdomain );
return;
}
}
/**
* Apply DNS settings
*
* @ret rc Return status code
*/
static int apply_dns_settings ( void ) {
void *dbgcol = &dns_count;
/* Fetch DNS server address */
apply_dns_servers();
if ( DBG_EXTRA && ( dns_count != 0 ) ) {
union {
struct sockaddr sa;
struct sockaddr_in sin;
struct sockaddr_in6 sin6;
} u;
unsigned int i;
DBGC2 ( dbgcol, "DNS servers:" );
for ( i = 0 ; i < dns6.count ; i++ ) {
u.sin6.sin6_family = AF_INET6;
memcpy ( &u.sin6.sin6_addr, &dns6.in6[i],
sizeof ( u.sin6.sin6_addr ) );
DBGC2 ( dbgcol, " %s", sock_ntoa ( &u.sa ) );
}
for ( i = 0 ; i < dns4.count ; i++ ) {
u.sin.sin_family = AF_INET;
u.sin.sin_addr = dns4.in[i];
DBGC2 ( dbgcol, " %s", sock_ntoa ( &u.sa ) );
}
DBGC2 ( dbgcol, "\n" );
}
/* Fetch DNS search list */
apply_dns_search();
if ( DBG_EXTRA && ( dns_search.len != 0 ) ) {
struct dns_name name;
int offset;
DBGC2 ( dbgcol, "DNS search list:" );
memcpy ( &name, &dns_search, sizeof ( name ) );
while ( name.offset != name.len ) {
DBGC2 ( dbgcol, " %s", dns_name ( &name ) );
offset = dns_skip_search ( &name );
if ( offset < 0 )
break;
name.offset = offset;
}
DBGC2 ( dbgcol, "\n" );
}
return 0;
}
/** DNS settings applicator */
struct settings_applicator dns_applicator __settings_applicator = {
.apply = apply_dns_settings,
};