| /* |
| * 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; |
| |
| /** Current DNS server index */ |
| static unsigned int dns_index; |
| |
| /** 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; |
| /** 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, |
| }; |