diff --git a/src/include/ipxe/dhcp.h b/src/include/ipxe/dhcp.h index 2429fae0..bcfb85cc 100644 --- a/src/include/ipxe/dhcp.h +++ b/src/include/ipxe/dhcp.h @@ -290,6 +290,9 @@ struct dhcp_client_uuid { #define DHCP_CLIENT_UUID_TYPE 0 +/** DNS domain search list */ +#define DHCP_DOMAIN_SEARCH 119 + /** Etherboot-specific encapsulated options * * This encapsulated options field is used to contain all options diff --git a/src/include/ipxe/dns.h b/src/include/ipxe/dns.h index 164c16ae..4f6cab3a 100644 --- a/src/include/ipxe/dns.h +++ b/src/include/ipxe/dns.h @@ -12,88 +12,144 @@ FILE_LICENCE ( GPL2_OR_LATER ); #include #include -/* - * Constants +/** DNS server port */ +#define DNS_PORT 53 + +/** An RFC1035-encoded DNS name */ +struct dns_name { + /** Start of data */ + void *data; + /** Offset of name within data */ + size_t offset; + /** Total length of data */ + size_t len; +}; + +/** + * Test for a DNS compression pointer * + * @v byte Initial byte + * @ret is_compressed Is a compression pointer */ +#define DNS_IS_COMPRESSED( byte ) ( (byte) & 0xc0 ) -#define DNS_TYPE_A 1 -#define DNS_TYPE_CNAME 5 -#define DNS_TYPE_AAAA 28 -#define DNS_TYPE_ANY 255 - -#define DNS_CLASS_IN 1 -#define DNS_CLASS_CS 2 -#define DNS_CLASS_CH 3 -#define DNS_CLASS_HS 4 - -#define DNS_FLAG_QUERY ( 0x00 << 15 ) -#define DNS_FLAG_RESPONSE ( 0x01 << 15 ) -#define DNS_FLAG_QR(flags) ( (flags) & ( 0x01 << 15 ) ) -#define DNS_FLAG_OPCODE_QUERY ( 0x00 << 11 ) -#define DNS_FLAG_OPCODE_IQUERY ( 0x01 << 11 ) -#define DNS_FLAG_OPCODE_STATUS ( 0x02 << 11 ) -#define DNS_FLAG_OPCODE(flags) ( (flags) & ( 0x0f << 11 ) ) -#define DNS_FLAG_RD ( 0x01 << 8 ) -#define DNS_FLAG_RA ( 0x01 << 7 ) -#define DNS_FLAG_RCODE_OK ( 0x00 << 0 ) -#define DNS_FLAG_RCODE_NX ( 0x03 << 0 ) -#define DNS_FLAG_RCODE(flags) ( (flags) & ( 0x0f << 0 ) ) - -#define DNS_PORT 53 -#define DNS_MAX_RETRIES 3 -#define DNS_MAX_CNAME_RECURSION 0x30 - -/* - * DNS protocol structures +/** + * Extract DNS compression pointer * + * @v word Initial word + * @ret offset Offset */ +#define DNS_COMPRESSED_OFFSET( word ) ( (word) & ~0xc000 ) + +/** + * Extract DNS label length + * + * @v byte Initial byte + * @ret len Label length + */ +#define DNS_LABEL_LEN( byte ) ( (byte) & ~0xc0 ) + +/** Maximum length of a single DNS label */ +#define DNS_MAX_LABEL_LEN 0x3f + +/** Maximum length of a DNS name (mandated by RFC1035 section 2.3.4) */ +#define DNS_MAX_NAME_LEN 255 + +/** Maximum depth of CNAME recursion + * + * This is a policy decision. + */ +#define DNS_MAX_CNAME_RECURSION 32 + +/** A DNS packet header */ struct dns_header { - uint16_t id; - uint16_t flags; - uint16_t qdcount; - uint16_t ancount; - uint16_t nscount; - uint16_t arcount; + /** Query identifier */ + uint16_t id; + /** Flags */ + uint16_t flags; + /** Number of question records */ + uint16_t qdcount; + /** Number of answer records */ + uint16_t ancount; + /** Number of name server records */ + uint16_t nscount; + /** Number of additional records */ + uint16_t arcount; } __attribute__ (( packed )); -struct dns_query_info { - uint16_t qtype; - uint16_t qclass; +/** Recursion desired flag */ +#define DNS_FLAG_RD 0x0100 + +/** A DNS question */ +struct dns_question { + /** Query type */ + uint16_t qtype; + /** Query class */ + uint16_t qclass; } __attribute__ (( packed )); -struct dns_query { - struct dns_header dns; - char payload[ 256 + sizeof ( struct dns_query_info ) ]; +/** DNS class "IN" */ +#define DNS_CLASS_IN 1 + +/** A DNS resource record */ +struct dns_rr_common { + /** Type */ + uint16_t type; + /** Class */ + uint16_t class; + /** Time to live */ + uint32_t ttl; + /** Resource data length */ + uint16_t rdlength; } __attribute__ (( packed )); -struct dns_rr_info_common { - uint16_t type; - uint16_t class; - uint32_t ttl; - uint16_t rdlength; -} __attribute__ (( packed )); +/** Type of a DNS "A" record */ +#define DNS_TYPE_A 1 -struct dns_rr_info_a { - struct dns_rr_info_common common; +/** A DNS "A" record */ +struct dns_rr_a { + /** Common fields */ + struct dns_rr_common common; + /** IPv4 address */ struct in_addr in_addr; } __attribute__ (( packed )); -struct dns_rr_info_aaaa { - struct dns_rr_info_common common; +/** Type of a DNS "AAAA" record */ +#define DNS_TYPE_AAAA 28 + +/** A DNS "AAAA" record */ +struct dns_rr_aaaa { + /** Common fields */ + struct dns_rr_common common; + /** IPv6 address */ struct in6_addr in6_addr; } __attribute__ (( packed )); -struct dns_rr_info_cname { - struct dns_rr_info_common common; - char cname[0]; +/** Type of a DNS "NAME" record */ +#define DNS_TYPE_CNAME 5 + +/** A DNS "CNAME" record */ +struct dns_rr_cname { + /** Common fields */ + struct dns_rr_common common; } __attribute__ (( packed )); -union dns_rr_info { - struct dns_rr_info_common common; - struct dns_rr_info_a a; - struct dns_rr_info_aaaa aaaa; - struct dns_rr_info_cname cname; +/** A DNS resource record */ +union dns_rr { + /** Common fields */ + struct dns_rr_common common; + /** "A" record */ + struct dns_rr_a a; + /** "AAAA" record */ + struct dns_rr_aaaa aaaa; + /** "CNAME" record */ + struct dns_rr_cname cname; }; +extern int dns_encode ( const char *string, struct dns_name *name ); +extern int dns_decode ( struct dns_name *name, char *data, size_t len ); +extern int dns_compare ( struct dns_name *first, struct dns_name *second ); +extern int dns_copy ( struct dns_name *src, struct dns_name *dst ); +extern int dns_skip ( struct dns_name *name ); + #endif /* _IPXE_DNS_H */ diff --git a/src/include/ipxe/settings.h b/src/include/ipxe/settings.h index 642b1d4c..d6929ecd 100644 --- a/src/include/ipxe/settings.h +++ b/src/include/ipxe/settings.h @@ -417,6 +417,7 @@ extern const struct setting_type setting_type_hexhyp __setting_type; extern const struct setting_type setting_type_hexraw __setting_type; extern const struct setting_type setting_type_uuid __setting_type; extern const struct setting_type setting_type_busdevfn __setting_type; +extern const struct setting_type setting_type_dnssl __setting_type; extern const struct setting ip_setting __setting ( SETTING_IP, ip ); diff --git a/src/net/ndp.c b/src/net/ndp.c index 8141c8de..6450aa9f 100644 --- a/src/net/ndp.c +++ b/src/net/ndp.c @@ -786,6 +786,16 @@ const struct setting ndp_dns6_setting __setting ( SETTING_IP_EXTRA, dns6 ) = { .scope = &ndp_settings_scope, }; +/** DNS search list setting */ +const struct setting ndp_dnssl_setting __setting ( SETTING_IP_EXTRA, dnssl ) = { + .name = "dnssl", + .description = "DNS search list", + .tag = NDP_TAG ( NDP_OPT_DNSSL, + offsetof ( struct ndp_dnssl_option, names ) ), + .type = &setting_type_dnssl, + .scope = &ndp_settings_scope, +}; + /**************************************************************************** * * IPv6 autoconfiguration diff --git a/src/net/udp/dhcp.c b/src/net/udp/dhcp.c index 4625d1de..e6d3eddf 100644 --- a/src/net/udp/dhcp.c +++ b/src/net/udp/dhcp.c @@ -86,6 +86,7 @@ static uint8_t dhcp_request_options_data[] = { DHCP_LOG_SERVERS, DHCP_HOST_NAME, DHCP_DOMAIN_NAME, DHCP_ROOT_PATH, DHCP_VENDOR_ENCAP, DHCP_VENDOR_CLASS_ID, DHCP_TFTP_SERVER_NAME, DHCP_BOOTFILE_NAME, + DHCP_DOMAIN_SEARCH, 128, 129, 130, 131, 132, 133, 134, 135, /* for PXE */ DHCP_EB_ENCAP, DHCP_ISCSI_INITIATOR_IQN ), DHCP_END diff --git a/src/net/udp/dhcpv6.c b/src/net/udp/dhcpv6.c index b86b8337..cbc8d794 100644 --- a/src/net/udp/dhcpv6.c +++ b/src/net/udp/dhcpv6.c @@ -979,3 +979,12 @@ const struct setting filename6_setting __setting ( SETTING_BOOT, filename ) = { .type = &setting_type_string, .scope = &ipv6_scope, }; + +/** DNS search list setting */ +const struct setting dnssl6_setting __setting ( SETTING_IP_EXTRA, dnssl ) = { + .name = "dnssl", + .description = "DNS search list", + .tag = DHCPV6_DOMAIN_LIST, + .type = &setting_type_dnssl, + .scope = &ipv6_scope, +}; diff --git a/src/net/udp/dns.c b/src/net/udp/dns.c index a93eb916..73af943d 100644 --- a/src/net/udp/dns.c +++ b/src/net/udp/dns.c @@ -26,6 +26,7 @@ FILE_LICENCE ( GPL2_OR_LATER ); #include #include #include +#include #include #include #include @@ -69,8 +70,366 @@ static union { }, }; -/** The local domain */ -static char *localdomain; +/** 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 ) ); + return ( ( len < 0 ) ? "" : 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 ""; + } +} /** A DNS request */ struct dns_request { @@ -91,14 +450,25 @@ struct dns_request { } address; /** Initial query type */ uint16_t qtype; - /** Current query packet */ - struct dns_query query; - /** Location of query info structure within current packet - * - * The query info structure is located immediately after the - * compressed name. - */ - struct dns_query_info *qinfo; + /** 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; }; @@ -138,214 +508,73 @@ static void dns_resolved ( struct dns_request *dns ) { } /** - * Compare DNS reply name against the query name from the original request + * Construct DNS question * * @v dns DNS request - * @v reply DNS reply - * @v rname Reply name - * @ret zero Names match - * @ret non-zero Names do not match + * @ret rc Return status code */ -static int dns_name_cmp ( struct dns_request *dns, - const struct dns_header *reply, - const char *rname ) { - const char *qname = dns->query.payload; - int i; +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; - while ( 1 ) { - /* Obtain next section of rname */ - while ( ( *rname ) & 0xc0 ) { - rname = ( ( ( char * ) reply ) + - ( ntohs( *((uint16_t *)rname) ) & ~0xc000 )); - } - /* Check that lengths match */ - if ( *rname != *qname ) - return -1; - /* If length is zero, we have reached the end */ - if ( ! *qname ) - return 0; - /* Check that data matches */ - for ( i = *qname + 1; i > 0 ; i-- ) { - if ( *(rname++) != *(qname++) ) - return -1; - } + /* 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 ); + + return 0; } /** - * Skip over a (possibly compressed) DNS name - * - * @v name DNS name - * @ret name Next DNS name - */ -static const char * dns_skip_name ( const char *name ) { - while ( 1 ) { - if ( ! *name ) { - /* End of name */ - return ( name + 1); - } - if ( *name & 0xc0 ) { - /* Start of a compressed name */ - return ( name + 2 ); - } - /* Uncompressed name portion */ - name += *name + 1; - } -} - -/** - * Find an RR in a reply packet corresponding to our query - * - * @v dns DNS request - * @v reply DNS reply - * @ret rr DNS RR, or NULL if not found - */ -static union dns_rr_info * dns_find_rr ( struct dns_request *dns, - const struct dns_header *reply ) { - int i, cmp; - const char *p = ( ( char * ) reply ) + sizeof ( struct dns_header ); - union dns_rr_info *rr_info; - - /* Skip over the questions section */ - for ( i = ntohs ( reply->qdcount ) ; i > 0 ; i-- ) { - p = dns_skip_name ( p ) + sizeof ( struct dns_query_info ); - } - - /* Process the answers section */ - for ( i = ntohs ( reply->ancount ) ; i > 0 ; i-- ) { - cmp = dns_name_cmp ( dns, reply, p ); - p = dns_skip_name ( p ); - rr_info = ( ( union dns_rr_info * ) p ); - if ( cmp == 0 ) - return rr_info; - p += ( sizeof ( rr_info->common ) + - ntohs ( rr_info->common.rdlength ) ); - } - - return NULL; -} - -/** - * Append DHCP domain name if available and name is not fully qualified - * - * @v string Name as a NUL-terminated string - * @ret fqdn Fully-qualified domain name, malloc'd copy - * - * The caller must free fqdn which is allocated even if the name is already - * fully qualified. - */ -static char * dns_qualify_name ( const char *string ) { - char *fqdn; - - /* Leave unchanged if already fully-qualified or no local domain */ - if ( ( ! localdomain ) || ( strchr ( string, '.' ) != NULL ) ) - return strdup ( string ); - - /* Append local domain to name */ - asprintf ( &fqdn, "%s.%s", string, localdomain ); - return fqdn; -} - -/** - * Convert a standard NUL-terminated string to a DNS name - * - * @v string Name as a NUL-terminated string - * @v buf Buffer in which to place DNS name - * @ret next Byte following constructed DNS name - * - * DNS names consist of "element" pairs. - */ -static char * dns_make_name ( const char *string, char *buf ) { - char *length_byte; - char c; - - length_byte = buf++; - *length_byte = 0; - do { - c = *(string++); - if ( ( c == '.' ) || ( c == '\0' ) ) { - if ( *length_byte ) { - length_byte = buf++; - *length_byte = 0; - } - } else { - *(buf++) = c; - (*length_byte)++; - } - } while ( c ); - - return buf; -} - -/** - * Convert an uncompressed DNS name to a NUL-terminated string - * - * @v name DNS name - * @ret string NUL-terminated string - * - * Produce a printable version of a DNS name. Used only for debugging. - */ -static inline char * dns_unmake_name ( char *name ) { - char *p; - unsigned int len; - - p = name; - while ( ( len = *p ) ) { - *(p++) = '.'; - p += len; - } - - return name + 1; -} - -/** - * Decompress a DNS name - * - * @v reply DNS replay - * @v name DNS name - * @v buf Buffer into which to decompress DNS name - * @ret next Byte following decompressed DNS name - */ -static char * dns_decompress_name ( const struct dns_header *reply, - const char *name, char *buf ) { - int i, len; - - do { - /* Obtain next section of name */ - while ( ( *name ) & 0xc0 ) { - name = ( ( char * ) reply + - ( ntohs ( *((uint16_t *)name) ) & ~0xc000 ) ); - } - /* Copy data */ - len = *name; - for ( i = len + 1 ; i > 0 ; i-- ) { - *(buf++) = *(name++); - } - } while ( len ); - return buf; -} - -/** - * Send next packet in DNS request + * Send DNS query * * @v dns DNS request + * @ret rc Return status code */ static int dns_send_packet ( struct dns_request *dns ) { - static unsigned int qid = 0; - size_t qlen; - - /* Increment query ID */ - dns->query.dns.id = htons ( ++qid ); - - DBGC ( dns, "DNS %p sending query ID %d\n", dns, qid ); + struct dns_header *query = &dns->buf.query; /* Start retransmission timer */ start_timer ( &dns->timer ); + /* Generate query identifier */ + query->id = random(); + + /* Send query */ + DBGC ( dns, "DNS %p sending query ID %#04x for %s type %s\n", dns, + ntohs ( query->id ), dns_name ( &dns->name ), + dns_type ( dns->question->qtype ) ); + /* Send the data */ - qlen = ( ( ( void * ) dns->qinfo ) - ( ( void * ) &dns->query ) - + sizeof ( dns->qinfo ) ); - return xfer_deliver_raw ( &dns->socket, &dns->query, qlen ); + return xfer_deliver_raw ( &dns->socket, query, dns->len ); } /** @@ -376,51 +605,110 @@ static void dns_timer_expired ( struct retry_timer *timer, int fail ) { static int dns_xfer_deliver ( struct dns_request *dns, struct io_buffer *iobuf, struct xfer_metadata *meta __unused ) { - const struct dns_header *reply = iobuf->data; - union dns_rr_info *rr_info; - unsigned int qtype = dns->qinfo->qtype; + 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; int rc; /* Sanity check */ - if ( iob_len ( iobuf ) < sizeof ( *reply ) ) { + 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 reply ID matches query ID */ - if ( reply->id != dns->query.dns.id ) { - DBGC ( dns, "DNS %p received unexpected reply ID %d " - "(wanted %d)\n", dns, ntohs ( reply->id ), - ntohs ( dns->query.dns.id ) ); + /* 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; } - DBGC ( dns, "DNS %p received reply ID %d\n", dns, ntohs ( reply->id )); - - /* 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 ); + /* 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. */ - while ( ( rr_info = dns_find_rr ( dns, reply ) ) ) { - switch ( rr_info->common.type ) { + 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 ) + 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_info->aaaa.in6_addr, + &rr->aaaa.in6_addr, sizeof ( dns->address.sin6.sin6_addr ) ); dns_resolved ( dns ); rc = 0; @@ -429,39 +717,56 @@ static int dns_xfer_deliver ( struct dns_request *dns, 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_info->a.in_addr; + dns->address.sin.sin_addr = rr->a.in_addr; dns_resolved ( dns ); rc = 0; goto done; case htons ( DNS_TYPE_CNAME ): - /* Found a CNAME record; update query and recurse */ - DBGC ( dns, "DNS %p found CNAME\n", dns ); - dns->qinfo = ( void * ) dns_decompress_name ( reply, - rr_info->cname.cname, - dns->query.payload ); - dns->qinfo->qtype = dns->qtype; - dns->qinfo->qclass = htons ( DNS_CLASS_IN ); - /* Terminate the operation if we recurse too far */ if ( ++dns->recursion > DNS_MAX_CNAME_RECURSION ) { DBGC ( dns, "DNS %p recursion exceeded\n", dns ); - dns_done ( dns, -ELOOP ); - rc = 0; + 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; + dns_copy ( &buf, &dns->name ); + 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_info->common.type ) ); + 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 */ @@ -472,7 +777,7 @@ static int dns_xfer_deliver ( struct dns_request *dns, * the A. */ DBGC ( dns, "DNS %p found no AAAA record; trying A\n", dns ); - dns->qinfo->qtype = htons ( DNS_TYPE_A ); + dns->question->qtype = htons ( DNS_TYPE_A ); dns_send_packet ( dns ); rc = 0; goto done; @@ -482,31 +787,49 @@ static int dns_xfer_deliver ( struct dns_request *dns, * try the CNAME. */ DBGC ( dns, "DNS %p found no A record; trying CNAME\n", dns ); - dns->qinfo->qtype = htons ( DNS_TYPE_CNAME ); + 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 A query is already set up), then - * issue it, otherwise abort. + * (i.e. if the next AAAA/A query is already set up), + * then issue it. */ - if ( dns->qinfo->qtype == dns->qtype ) { + if ( qtype == dns->qtype ) { dns_send_packet ( dns ); rc = 0; goto done; - } else { - DBGC ( dns, "DNS %p found no CNAME record\n", dns ); - dns_done ( dns, -ENXIO_NO_RECORD ); - 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 ); - dns_done ( dns, -EINVAL ); rc = -EINVAL; + dns_done ( dns, rc ); goto done; } @@ -560,7 +883,9 @@ static struct interface_descriptor dns_resolv_desc = static int dns_resolv ( struct interface *resolv, const char *name, struct sockaddr *sa ) { struct dns_request *dns; - char *fqdn; + struct dns_header *query; + size_t search_len; + int name_len; int rc; /* Fail immediately if no DNS servers */ @@ -571,15 +896,11 @@ static int dns_resolv ( struct interface *resolv, goto err_no_nameserver; } - /* Ensure fully-qualified domain name if DHCP option was given */ - fqdn = dns_qualify_name ( name ); - if ( ! fqdn ) { - rc = -ENOMEM; - goto err_qualify_name; - } + /* Determine whether or not to use search list */ + search_len = ( strchr ( name, '.' ) ? 0 : dns_search.len ); /* Allocate DNS structure */ - dns = zalloc ( sizeof ( *dns ) ); + dns = zalloc ( sizeof ( *dns ) + search_len ); if ( ! dns ) { rc = -ENOMEM; goto err_alloc_dns; @@ -589,6 +910,9 @@ static int dns_resolv ( struct interface *resolv, 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 */ switch ( nameserver.sa.sa_family ) { @@ -600,16 +924,25 @@ static int dns_resolv ( struct interface *resolv, break; default: rc = -ENOTSUP; - goto err_qtype; + goto err_type; } - /* Create query */ - dns->query.dns.flags = htons ( DNS_FLAG_QUERY | DNS_FLAG_OPCODE_QUERY | - DNS_FLAG_RD ); - dns->query.dns.qdcount = htons ( 1 ); - dns->qinfo = ( void * ) dns_make_name ( fqdn, dns->query.payload ); - dns->qinfo->qtype = dns->qtype; - dns->qinfo->qclass = htons ( DNS_CLASS_IN ); + /* 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, @@ -619,21 +952,20 @@ static int dns_resolv ( struct interface *resolv, goto err_open_socket; } - /* Send first DNS packet */ - dns_send_packet ( dns ); + /* 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 ); - free ( fqdn ); return 0; err_open_socket: - err_qtype: + err_question: + err_encode: + err_type: ref_put ( &dns->refcnt ); err_alloc_dns: - free ( fqdn ); - err_qualify_name: err_no_nameserver: return rc; } @@ -651,6 +983,56 @@ struct resolver dns_resolver __resolver ( RESOLV_NORMAL ) = { ****************************************************************************** */ +/** + * 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_IP_EXTRA, dns ) = { .name = "dns", @@ -668,6 +1050,50 @@ const struct setting dns6_setting __setting ( SETTING_IP_EXTRA, dns6 ) = { .scope = &ipv6_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 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_setting_copy ( NULL, &dnssl_setting, NULL, NULL, + &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 * @@ -689,11 +1115,23 @@ static int apply_dns_settings ( void ) { sock_ntoa ( &nameserver.sa ) ); } - /* Get local domain DHCP option */ - free ( localdomain ); - fetch_string_setting_copy ( NULL, &domain_setting, &localdomain ); - if ( localdomain ) - DBG ( "DNS local domain %s\n", localdomain ); + /* Fetch DNS search list */ + apply_dns_search(); + if ( DBG_LOG && ( dns_search.len != 0 ) ) { + struct dns_name name; + int offset; + + DBG ( "DNS search list:" ); + memcpy ( &name, &dns_search, sizeof ( name ) ); + while ( name.offset != name.len ) { + DBG ( " %s", dns_name ( &name ) ); + offset = dns_skip_search ( &name ); + if ( offset < 0 ) + break; + name.offset = offset; + } + DBG ( "\n" ); + } return 0; } diff --git a/src/tests/dns_test.c b/src/tests/dns_test.c new file mode 100644 index 00000000..52f5f19f --- /dev/null +++ b/src/tests/dns_test.c @@ -0,0 +1,605 @@ +/* + * Copyright (C) 2014 Michael Brown . + * + * 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. + */ + +FILE_LICENCE ( GPL2_OR_LATER ); + +/** @file + * + * DNS self-tests + * + */ + +/* Forcibly enable assertions */ +#undef NDEBUG + +#include +#include +#include +#include + +/** Define inline data */ +#define DATA(...) { __VA_ARGS__ } + +/** A DNS encoding test */ +struct dns_encode_test { + /** String */ + const char *string; + /** Encoded string */ + const void *data; + /** Length of encoded string */ + int len; +}; + +/** + * Define a DNS encoding test + * + * @v _name Test name + * @v _string Test string + * @v _data Expected encoded data + * @ret test DNS encoding test + */ +#define DNS_ENCODE( _name, _string, _data ) \ + static const uint8_t _name ## __data[] = _data; \ + static struct dns_encode_test _name = { \ + .string = _string, \ + .data = _name ## __data, \ + .len = sizeof ( _name ## __data ), \ + } + +/** + * Report DNS encoding test result + * + * @v test DNS encoding test + * @v file Test code file + * @v line Test code line + */ +static void dns_encode_okx ( struct dns_encode_test *test, const char *file, + unsigned int line ) { + uint8_t data[ test->len ]; + struct dns_name name; + int len; + + /* Check ability to determine length with no buffer */ + memset ( &name, 0, sizeof ( name ) ); + len = dns_encode ( test->string, &name ); + okx ( len >= 0, file, line ); + okx ( len == test->len, file, line ); + + /* Check encoded name */ + name.data = data; + name.len = sizeof ( data ); + len = dns_encode ( test->string, &name ); + okx ( len >= 0, file, line ); + if ( len >= 0 ) { + okx ( len == test->len, file, line ); + okx ( memcmp ( data, test->data, test->len ) == 0, file, line ); + DBGC ( test, "DNS encoded \"%s\" to:\n", test->string ); + DBGC_HDA ( test, 0, data, len ); + } +} +#define dns_encode_ok( test ) dns_encode_okx ( test, __FILE__, __LINE__ ) + +/** + * Report DNS encoding failure test result + * + * @v test DNS encoding test + * @v file Test code file + * @v line Test code line + */ +static void dns_encode_fail_okx ( struct dns_encode_test *test, + const char *file, unsigned int line ) { + struct dns_name name = { .data = NULL, .len = 0 }; + int len; + + len = dns_encode ( test->string, &name ); + okx ( len < 0, file, line ); +} +#define dns_encode_fail_ok( test ) \ + dns_encode_fail_okx ( test, __FILE__, __LINE__ ) + +/** A DNS decoding test */ +struct dns_decode_test { + /** Name */ + struct dns_name name; + /** Expected string */ + const char *string; +}; + +/** + * Define a DNS decoding test + * + * @v _name Test name + * @v _data RFC1035-encoded data + * @v _offset Starting offset within encoded data + * @v _string Expected decoded string + * @ret test DNS decoding test + */ +#define DNS_DECODE( _name, _data, _offset, _string ) \ + static uint8_t _name ## __data[] = _data; \ + static struct dns_decode_test _name = { \ + .name = { \ + .data = _name ## __data, \ + .offset = _offset, \ + .len = sizeof ( _name ## __data ), \ + }, \ + .string = _string, \ + } + +/** + * Report DNS decoding test result + * + * @v test DNS decoding test + * @v file Test code file + * @v line Test code line + */ +static void dns_decode_okx ( struct dns_decode_test *test, const char *file, + unsigned int line ) { + char string[ strlen ( test->string ) + 1 /* NUL */ ]; + int len; + + /* Check ability to determine length with no buffer */ + len = dns_decode ( &test->name, NULL, 0 ); + okx ( len >= 0, file, line ); + okx ( len == ( ( int ) strlen ( test->string ) ), file, line ); + + /* Check decoded string */ + len = dns_decode ( &test->name, string, sizeof ( string ) ); + okx ( len >= 0, file, line ); + if ( len >= 0 ) { + okx ( strcmp ( string, test->string ) == 0, file, line ); + DBGC ( test, "DNS decoded \"%s\" from offset %#zx in:\n", + string, test->name.offset ); + DBGC_HDA ( test, 0, test->name.data, test->name.len ); + } +} +#define dns_decode_ok( test ) dns_decode_okx ( test, __FILE__, __LINE__ ) + +/** + * Report DNS decoding failure test result + * + * @v test DNS decoding test + * @v file Test code file + * @v line Test code line + */ +static void dns_decode_fail_okx ( struct dns_decode_test *test, + const char *file, unsigned int line ) { + int len; + + len = dns_decode ( &test->name, NULL, 0 ); + okx ( len < 0, file, line ); +} +#define dns_decode_fail_ok( test ) \ + dns_decode_fail_okx ( test, __FILE__, __LINE__ ) + +/** A DNS comparison test */ +struct dns_compare_test { + /** First name */ + struct dns_name first; + /** Second name */ + struct dns_name second; +}; + +/** + * Define a DNS comparison test + * + * @v _name Test name + * @v _first_data First RFC1035-encoded data + * @v _first_offset Starting offset within first encoded data + * @v _second_data Second RFC1035-encoded data + * @v _second_offset Starting offset within second encoded data + * @ret test DNS comparison test + */ +#define DNS_COMPARE( _name, _first_data, _first_offset, _second_data, \ + _second_offset ) \ + static uint8_t _name ## __first_data[] = _first_data; \ + static uint8_t _name ## __second_data[] = _second_data; \ + static struct dns_compare_test _name = { \ + .first = { \ + .data = _name ## __first_data, \ + .offset = _first_offset, \ + .len = sizeof ( _name ## __first_data ), \ + }, \ + .second = { \ + .data = _name ## __second_data, \ + .offset = _second_offset, \ + .len = sizeof ( _name ## __second_data ), \ + }, \ + } + +/** + * Report DNS comparison test result + * + * @v test DNS comparison test + * @v file Test code file + * @v line Test code line + */ +static void dns_compare_okx ( struct dns_compare_test *test, const char *file, + unsigned int line ) { + + okx ( dns_compare ( &test->first, &test->second ) == 0, file, line ); +} +#define dns_compare_ok( test ) dns_compare_okx ( test, __FILE__, __LINE__ ) + +/** + * Report DNS comparison test failure result + * + * @v test DNS comparison test + * @v file Test code file + * @v line Test code line + */ +static void dns_compare_fail_okx ( struct dns_compare_test *test, + const char *file, unsigned int line ) { + + okx ( dns_compare ( &test->first, &test->second ) != 0, file, line ); +} +#define dns_compare_fail_ok( test ) \ + dns_compare_fail_okx ( test, __FILE__, __LINE__ ) + +/** A DNS copying test */ +struct dns_copy_test { + /** Source name */ + struct dns_name src; + /** Expected copied name */ + struct dns_name dst; +}; + +/** + * Define a DNS copying test + * + * @v _name Test name + * @v _src_data Source RFC1035-encoded data + * @v _src_offset Starting offset within source encoded data + * @v _dst_data Expected copied RFC1035-encoded data + * @v _dst_offset Starting offset withint copied encoded data + * @ret test DNS copying test + */ +#define DNS_COPY( _name, _src_data, _src_offset, _dst_data, \ + _dst_offset ) \ + static uint8_t _name ## __src_data[] = _src_data; \ + static uint8_t _name ## __dst_data[] = _dst_data; \ + static struct dns_copy_test _name = { \ + .src = { \ + .data = _name ## __src_data, \ + .offset = _src_offset, \ + .len = sizeof ( _name ## __src_data ), \ + }, \ + .dst = { \ + .data = _name ## __dst_data, \ + .offset = _dst_offset, \ + .len = sizeof ( _name ## __dst_data ), \ + }, \ + } + +/** + * Report a DNS copying test result + * + * @v test DNS copying test + * @v file Test code file + * @v line Test code line + */ +static void dns_copy_okx ( struct dns_copy_test *test, + const char *file, unsigned int line ) { + uint8_t data[ test->dst.len ]; + struct dns_name dst; + int len; + + /* Check ability to determine length with no buffer */ + memset ( &dst, 0, sizeof ( dst ) ); + len = dns_copy ( &test->src, &dst ); + okx ( len >= 0, file, line ); + okx ( len == ( ( int ) ( test->dst.len - test->dst.offset ) ), + file, line ); + + /* Check copied name */ + dst.data = data; + dst.offset = test->dst.offset; + dst.len = sizeof ( data ); + memcpy ( dst.data, test->dst.data, test->dst.offset ); + len = dns_copy ( &test->src, &dst ); + okx ( len >= 0, file, line ); + okx ( len == ( ( int ) ( test->dst.len - test->dst.offset ) ), + file, line ); + okx ( memcmp ( data, test->dst.data, sizeof ( data ) ) == 0, + file, line ); + DBGC ( test, "DNS copied:\n" ); + DBGC_HDA ( test, 0, test->src.data, test->src.len ); + DBGC_HDA ( test, 0, data, ( test->dst.offset + len ) ); +} +#define dns_copy_ok( test ) dns_copy_okx ( test, __FILE__, __LINE__ ) + +/** + * Report a DNS copying failure test result + * + * @v test DNS copying test + * @v file Test code file + * @v line Test code line + */ +static void dns_copy_fail_okx ( struct dns_copy_test *test, + const char *file, unsigned int line ) { + struct dns_name dst; + int len; + + memset ( &dst, 0, sizeof ( dst ) ); + len = dns_copy ( &test->src, &dst ); + okx ( len < 0, file, line ); +} +#define dns_copy_fail_ok( test ) dns_copy_fail_okx ( test, __FILE__, __LINE__ ) + +/** A DNS search list test */ +struct dns_list_test { + /** Search list */ + struct dns_name list; + /** Expected decoded search list */ + const char **strings; + /** Number of expected decoded string */ + unsigned int count; +}; + +/** + * Define a DNS search list test + * + * @v _name Test name + * @v _list RFC1035-encoded data + * @v _strings Expected decoded strings + * @ret test DNS search list test + */ +#define DNS_LIST( _name, _list, _strings ) \ + static uint8_t _name ## __list[] = _list; \ + static const char * _name ## __strings[] = _strings; \ + static struct dns_list_test _name = { \ + .list = { \ + .data = _name ## __list, \ + .offset = 0, \ + .len = sizeof ( _name ## __list ), \ + }, \ + .strings = _name ## __strings, \ + .count = ( sizeof ( _name ## __strings ) / \ + sizeof ( _name ## __strings[0] ) ), \ + } + +/** + * Report DNS search list test result + * + * @v test DNS search list test + * @v file Test code file + * @v line Test code line + */ +static void dns_list_okx ( struct dns_list_test *test, const char *file, + unsigned int line ) { + struct dns_name name; + unsigned int i; + + DBGC ( test, "DNS search list:\n" ); + DBGC_HDA ( test, 0, test->list.data, test->list.len ); + memcpy ( &name, &test->list, sizeof ( name ) ); + for ( i = 0 ; i < test->count ; i++ ) { + char buf[ strlen ( test->strings[i] ) + 1 /* NUL */ ]; + int len; + int offset; + + /* Decode this name */ + len = dns_decode ( &name, buf, sizeof ( buf ) ); + okx ( len >= 0, file, line ); + if ( len >= 0 ) { + okx ( len == ( ( int ) strlen ( test->strings[i] ) ), + file, line ); + okx ( strcmp ( buf, test->strings[i] ) == 0, + file, line ); + DBGC ( test, "DNS search list found \"%s\" at offset " + "%#zx\n", buf, name.offset ); + } + + /* Skip to next name */ + offset = dns_skip ( &name ); + okx ( offset >= 0, file, line ); + name.offset = offset; + } + + /* Check that we have consumed the whole search list */ + okx ( name.offset == name.len, file, line ); +} +#define dns_list_ok( test ) dns_list_okx ( test, __FILE__, __LINE__ ) + +/* Simple encoding test */ +DNS_ENCODE ( encode_simple, "ipxe.org", + DATA ( 4, 'i', 'p', 'x', 'e', 3, 'o', 'r', 'g', 0 ) ); + +/* Single-word encoding test */ +DNS_ENCODE ( encode_single, "foo", DATA ( 3, 'f', 'o', 'o', 0 ) ); + +/* Absolute encoding test */ +DNS_ENCODE ( encode_absolute, "git.ipxe.org.", + DATA ( 3, 'g', 'i', 't', 4, 'i', 'p', 'x', 'e', 3, 'o', 'r', 'g', + 0 ) ); + +/* Empty string encoding test */ +DNS_ENCODE ( encode_empty, "", DATA ( 0 ) ); + +/* Root domain encoding test */ +DNS_ENCODE ( encode_root, ".", DATA ( 0 ) ); + +/* Invalid initial dot encoding test */ +DNS_ENCODE ( encode_initial_dot, ".foo", DATA() ); + +/* Invalid double dot encoding test */ +DNS_ENCODE ( encode_double_dot, "ipxe..org", DATA() ); + +/* Invalid solo double dot encoding test */ +DNS_ENCODE ( encode_solo_double_dot, "..", DATA() ); + +/* Invalid trailing double dot encoding test */ +DNS_ENCODE ( encode_trailing_double_dot, "ipxe.org..", DATA() ); + +/* Invalid overlength label encoding test */ +DNS_ENCODE ( encode_overlength, + "this-label-is-maliciously-long-in-an-attempt-to-overflow-the-" + "length-field-and-generate-a-length-which-looks-like-a-" + "compression-pointer", DATA() ); + +/* Simple decoding test */ +DNS_DECODE ( decode_simple, + DATA ( 4, 'i', 'p', 'x', 'e', 3, 'o', 'r', 'g', 0 ), 0, + "ipxe.org" ); + +/* Compression pointer decoding test */ +DNS_DECODE ( decode_ptr, + DATA ( 3, 'o', 'r', 'g', 0, 3, 'g', 'i', 't', 4, 'i', 'p', 'x', + 'e', 0xc0, 0x00 ), 5, + "git.ipxe.org" ); + +/* Root decoding test */ +DNS_DECODE ( decode_root, + DATA ( 0 ), 0, "" ); + +/* Incomplete name decoding test */ +DNS_DECODE ( decode_incomplete_name, + DATA ( 4, 'i', 'p', 'x', 'e' ), 0, NULL ); + +/* Incomplete label decoding test */ +DNS_DECODE ( decode_incomplete_label, + DATA ( 4, 'i', 'p', 'x' ), 0, NULL ); + +/* Incomplete compression pointer decoding test */ +DNS_DECODE ( decode_incomplete_ptr, + DATA ( 3, 'o', 'r', 'g', 0, 4, 'i', 'p', 'x', 'e', 0xc0 ), 5, + NULL ); + +/* Forward reference decoding test */ +DNS_DECODE ( decode_forward, + DATA ( 0xc0, 0x02, 3, 'f', 'o', 'o', 0 ), 0, NULL ); + +/* Infinite loop decoding test */ +DNS_DECODE ( decode_infinite, + DATA ( 4, 'i', 'p', 'x', 'e', 0xc0, 0x00 ), 0, NULL ); + +/* Empty decoding test */ +DNS_DECODE ( decode_empty, + DATA (), 0, NULL ); + +/* Simple comparison test */ +DNS_COMPARE ( compare_simple, + DATA ( 4, 'i', 'p', 'x', 'e', 3, 'o', 'r', 'g', 0 ), 0, + DATA ( 4, 'i', 'p', 'x', 'e', 3, 'o', 'r', 'g', 0 ), 0 ); + +/* Compression pointer comparison test */ +DNS_COMPARE ( compare_ptr, + DATA ( 4, 'i', 'p', 'x', 'e', 3, 'o', 'r', 'g', 0 ), 0, + DATA ( 3, 'o', 'r', 'g', 0, 4, 'i', 'p', 'x', 'e', + 0xc0, 0x00 ), 5 ); + +/* Case insensitive comparison test */ +DNS_COMPARE ( compare_case, + DATA ( 4, 'i', 'p', 'x', 'e', 3, 'o', 'r', 'g', 0 ), 0, + DATA ( 4, 'i', 'p', 'x', 'e', 3, 'O', 'R', 'G', 0 ), 0 ); + +/* Mismatch comparison test */ +DNS_COMPARE ( compare_mismatch, + DATA ( 4, 'i', 'p', 'x', 'e', 3, 'o', 'r', 'g', 0 ), 0, + DATA ( 4, 'g', 'p', 'x', 'e', 3, 'o', 'r', 'g', 0 ), 0 ); + +/* Infinite loop comparison test */ +DNS_COMPARE ( compare_infinite, + DATA ( 3, 'f', 'o', 'o', 0xc0, 0x00 ), 0, + DATA ( 3, 'f', 'o', 'o', 0xc0, 0x00 ), 0 ); + +/* Simple copying test */ +DNS_COPY ( copy_simple, + DATA ( 4, 'i', 'p', 'x', 'e', 3, 'o', 'r', 'g', 0 ), 0, + DATA ( 4, 'i', 'p', 'x', 'e', 3, 'o', 'r', 'g', 0 ), 0 ); + +/* Simple copying test with offset */ +DNS_COPY ( copy_offset, + DATA ( 4, 'i', 'p', 'x', 'e', 3, 'o', 'r', 'g', 0 ), 0, + DATA ( 'f', 'o', 'o', 0, 4, 'i', 'p', 'x', 'e', + 3, 'o', 'r', 'g', 0 ), 4 ); + +/* Compression pointer copying test */ +DNS_COPY ( copy_ptr, + DATA ( 3, 'o', 'r', 'g', 0, 3, 'g', 'i', 't', 4, 'i', 'p', 'x', 'e', + 0xc0, 0x00 ), 5, + DATA ( 3, 'g', 'i', 't', 4, 'i', 'p', 'x', 'e', 3, 'o', 'r', 'g', + 0 ), 0 ); + +/* Infinite loop copying test */ +DNS_COPY ( copy_infinite, + DATA ( 4, 'l', 'o', 'o', 'p', 7, 'f', 'o', 'r', 'e', 'v', 'e', 'r', + 0xc0, 0x05 ), 0, + DATA (), 0 ); + +/* DNS search list test */ +DNS_LIST ( search, + DATA ( 4, 'i', 'p', 'x', 'e', 3, 'o', 'r', 'g', 0, + 4, 'b', 'o', 'o', 't', 0xc0, 0x00, + 3, 'd', 'e', 'v', 0xc0, 0x0a, + 11, 'n', 'e', 't', 'w', 'o', 'r', 'k', 'b', 'o', 'o', 't', + 0xc0, 0x05 ), + DATA ( "ipxe.org", "boot.ipxe.org", "dev.boot.ipxe.org", + "networkboot.org" ) ); + +/** + * Perform DNS self-test + * + */ +static void dns_test_exec ( void ) { + + /* Encoding tests */ + dns_encode_ok ( &encode_simple ); + dns_encode_ok ( &encode_single ); + dns_encode_ok ( &encode_absolute ); + dns_encode_ok ( &encode_empty ); + dns_encode_ok ( &encode_root ); + dns_encode_fail_ok ( &encode_initial_dot ); + dns_encode_fail_ok ( &encode_double_dot ); + dns_encode_fail_ok ( &encode_solo_double_dot ); + dns_encode_fail_ok ( &encode_trailing_double_dot ); + dns_encode_fail_ok ( &encode_overlength ); + + /* Decoding tests */ + dns_decode_ok ( &decode_simple ); + dns_decode_ok ( &decode_ptr ); + dns_decode_ok ( &decode_root ); + dns_decode_fail_ok ( &decode_incomplete_name ); + dns_decode_fail_ok ( &decode_incomplete_label ); + dns_decode_fail_ok ( &decode_incomplete_ptr ); + dns_decode_fail_ok ( &decode_forward ); + dns_decode_fail_ok ( &decode_infinite ); + dns_decode_fail_ok ( &decode_empty ); + + /* Comparison tests */ + dns_compare_ok ( &compare_simple ); + dns_compare_ok ( &compare_ptr ); + dns_compare_ok ( &compare_case ); + dns_compare_fail_ok ( &compare_mismatch ); + dns_compare_fail_ok ( &compare_infinite ); + + /* Copying tests */ + dns_copy_ok ( ©_simple ); + dns_copy_ok ( ©_offset ); + dns_copy_ok ( ©_ptr ); + dns_copy_fail_ok ( ©_infinite ); + + /* Search list tets */ + dns_list_ok ( &search ); +} + +/** DNS self-test */ +struct self_test dns_test __self_test = { + .name = "dns", + .exec = dns_test_exec, +}; diff --git a/src/tests/tests.c b/src/tests/tests.c index 92fa2bff..e5e096aa 100644 --- a/src/tests/tests.c +++ b/src/tests/tests.c @@ -52,3 +52,4 @@ REQUIRE_OBJECT ( cms_test ); REQUIRE_OBJECT ( pnm_test ); REQUIRE_OBJECT ( deflate_test ); REQUIRE_OBJECT ( png_test ); +REQUIRE_OBJECT ( dns_test );