|
|
1.1 ! root 1: /* ! 2: * Copyright (C) 2006 Michael Brown <[email protected]>. ! 3: * ! 4: * Portions copyright (C) 2004 Anselm M. Hoffmeister ! 5: * <[email protected]>. ! 6: * ! 7: * This program is free software; you can redistribute it and/or ! 8: * modify it under the terms of the GNU General Public License as ! 9: * published by the Free Software Foundation; either version 2 of the ! 10: * License, or any later version. ! 11: * ! 12: * This program is distributed in the hope that it will be useful, but ! 13: * WITHOUT ANY WARRANTY; without even the implied warranty of ! 14: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ! 15: * General Public License for more details. ! 16: * ! 17: * You should have received a copy of the GNU General Public License ! 18: * along with this program; if not, write to the Free Software ! 19: * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. ! 20: */ ! 21: ! 22: FILE_LICENCE ( GPL2_OR_LATER ); ! 23: ! 24: #include <stdint.h> ! 25: #include <stdlib.h> ! 26: #include <string.h> ! 27: #include <stdio.h> ! 28: #include <errno.h> ! 29: #include <byteswap.h> ! 30: #include <ipxe/refcnt.h> ! 31: #include <ipxe/iobuf.h> ! 32: #include <ipxe/xfer.h> ! 33: #include <ipxe/open.h> ! 34: #include <ipxe/resolv.h> ! 35: #include <ipxe/retry.h> ! 36: #include <ipxe/tcpip.h> ! 37: #include <ipxe/settings.h> ! 38: #include <ipxe/features.h> ! 39: #include <ipxe/dns.h> ! 40: ! 41: /** @file ! 42: * ! 43: * DNS protocol ! 44: * ! 45: */ ! 46: ! 47: FEATURE ( FEATURE_PROTOCOL, "DNS", DHCP_EB_FEATURE_DNS, 1 ); ! 48: ! 49: /* Disambiguate the various error causes */ ! 50: #define ENXIO_NO_RECORD __einfo_error ( EINFO_ENXIO_NO_RECORD ) ! 51: #define EINFO_ENXIO_NO_RECORD \ ! 52: __einfo_uniqify ( EINFO_ENXIO, 0x01, "DNS name does not exist" ) ! 53: #define ENXIO_NO_NAMESERVER __einfo_error ( EINFO_ENXIO_NO_NAMESERVER ) ! 54: #define EINFO_ENXIO_NO_NAMESERVER \ ! 55: __einfo_uniqify ( EINFO_ENXIO, 0x02, "No DNS servers available" ) ! 56: ! 57: /** The DNS server */ ! 58: static struct sockaddr_tcpip nameserver = { ! 59: .st_port = htons ( DNS_PORT ), ! 60: }; ! 61: ! 62: /** The local domain */ ! 63: static char *localdomain; ! 64: ! 65: /** A DNS request */ ! 66: struct dns_request { ! 67: /** Reference counter */ ! 68: struct refcnt refcnt; ! 69: /** Name resolution interface */ ! 70: struct interface resolv; ! 71: /** Data transfer interface */ ! 72: struct interface socket; ! 73: /** Retry timer */ ! 74: struct retry_timer timer; ! 75: ! 76: /** Socket address to fill in with resolved address */ ! 77: struct sockaddr sa; ! 78: /** Current query packet */ ! 79: struct dns_query query; ! 80: /** Location of query info structure within current packet ! 81: * ! 82: * The query info structure is located immediately after the ! 83: * compressed name. ! 84: */ ! 85: struct dns_query_info *qinfo; ! 86: /** Recursion counter */ ! 87: unsigned int recursion; ! 88: }; ! 89: ! 90: /** ! 91: * Mark DNS request as complete ! 92: * ! 93: * @v dns DNS request ! 94: * @v rc Return status code ! 95: */ ! 96: static void dns_done ( struct dns_request *dns, int rc ) { ! 97: ! 98: /* Stop the retry timer */ ! 99: stop_timer ( &dns->timer ); ! 100: ! 101: /* Shut down interfaces */ ! 102: intf_shutdown ( &dns->socket, rc ); ! 103: intf_shutdown ( &dns->resolv, rc ); ! 104: } ! 105: ! 106: /** ! 107: * Compare DNS reply name against the query name from the original request ! 108: * ! 109: * @v dns DNS request ! 110: * @v reply DNS reply ! 111: * @v rname Reply name ! 112: * @ret zero Names match ! 113: * @ret non-zero Names do not match ! 114: */ ! 115: static int dns_name_cmp ( struct dns_request *dns, ! 116: const struct dns_header *reply, ! 117: const char *rname ) { ! 118: const char *qname = dns->query.payload; ! 119: int i; ! 120: ! 121: while ( 1 ) { ! 122: /* Obtain next section of rname */ ! 123: while ( ( *rname ) & 0xc0 ) { ! 124: rname = ( ( ( char * ) reply ) + ! 125: ( ntohs( *((uint16_t *)rname) ) & ~0xc000 )); ! 126: } ! 127: /* Check that lengths match */ ! 128: if ( *rname != *qname ) ! 129: return -1; ! 130: /* If length is zero, we have reached the end */ ! 131: if ( ! *qname ) ! 132: return 0; ! 133: /* Check that data matches */ ! 134: for ( i = *qname + 1; i > 0 ; i-- ) { ! 135: if ( *(rname++) != *(qname++) ) ! 136: return -1; ! 137: } ! 138: } ! 139: } ! 140: ! 141: /** ! 142: * Skip over a (possibly compressed) DNS name ! 143: * ! 144: * @v name DNS name ! 145: * @ret name Next DNS name ! 146: */ ! 147: static const char * dns_skip_name ( const char *name ) { ! 148: while ( 1 ) { ! 149: if ( ! *name ) { ! 150: /* End of name */ ! 151: return ( name + 1); ! 152: } ! 153: if ( *name & 0xc0 ) { ! 154: /* Start of a compressed name */ ! 155: return ( name + 2 ); ! 156: } ! 157: /* Uncompressed name portion */ ! 158: name += *name + 1; ! 159: } ! 160: } ! 161: ! 162: /** ! 163: * Find an RR in a reply packet corresponding to our query ! 164: * ! 165: * @v dns DNS request ! 166: * @v reply DNS reply ! 167: * @ret rr DNS RR, or NULL if not found ! 168: */ ! 169: static union dns_rr_info * dns_find_rr ( struct dns_request *dns, ! 170: const struct dns_header *reply ) { ! 171: int i, cmp; ! 172: const char *p = ( ( char * ) reply ) + sizeof ( struct dns_header ); ! 173: union dns_rr_info *rr_info; ! 174: ! 175: /* Skip over the questions section */ ! 176: for ( i = ntohs ( reply->qdcount ) ; i > 0 ; i-- ) { ! 177: p = dns_skip_name ( p ) + sizeof ( struct dns_query_info ); ! 178: } ! 179: ! 180: /* Process the answers section */ ! 181: for ( i = ntohs ( reply->ancount ) ; i > 0 ; i-- ) { ! 182: cmp = dns_name_cmp ( dns, reply, p ); ! 183: p = dns_skip_name ( p ); ! 184: rr_info = ( ( union dns_rr_info * ) p ); ! 185: if ( cmp == 0 ) ! 186: return rr_info; ! 187: p += ( sizeof ( rr_info->common ) + ! 188: ntohs ( rr_info->common.rdlength ) ); ! 189: } ! 190: ! 191: return NULL; ! 192: } ! 193: ! 194: /** ! 195: * Append DHCP domain name if available and name is not fully qualified ! 196: * ! 197: * @v string Name as a NUL-terminated string ! 198: * @ret fqdn Fully-qualified domain name, malloc'd copy ! 199: * ! 200: * The caller must free fqdn which is allocated even if the name is already ! 201: * fully qualified. ! 202: */ ! 203: static char * dns_qualify_name ( const char *string ) { ! 204: char *fqdn; ! 205: ! 206: /* Leave unchanged if already fully-qualified or no local domain */ ! 207: if ( ( ! localdomain ) || ( strchr ( string, '.' ) != 0 ) ) ! 208: return strdup ( string ); ! 209: ! 210: /* Append local domain to name */ ! 211: asprintf ( &fqdn, "%s.%s", string, localdomain ); ! 212: return fqdn; ! 213: } ! 214: ! 215: /** ! 216: * Convert a standard NUL-terminated string to a DNS name ! 217: * ! 218: * @v string Name as a NUL-terminated string ! 219: * @v buf Buffer in which to place DNS name ! 220: * @ret next Byte following constructed DNS name ! 221: * ! 222: * DNS names consist of "<length>element" pairs. ! 223: */ ! 224: static char * dns_make_name ( const char *string, char *buf ) { ! 225: char *length_byte = buf++; ! 226: char c; ! 227: ! 228: while ( ( c = *(string++) ) ) { ! 229: if ( c == '.' ) { ! 230: *length_byte = buf - length_byte - 1; ! 231: length_byte = buf; ! 232: } ! 233: *(buf++) = c; ! 234: } ! 235: *length_byte = buf - length_byte - 1; ! 236: *(buf++) = '\0'; ! 237: return buf; ! 238: } ! 239: ! 240: /** ! 241: * Convert an uncompressed DNS name to a NUL-terminated string ! 242: * ! 243: * @v name DNS name ! 244: * @ret string NUL-terminated string ! 245: * ! 246: * Produce a printable version of a DNS name. Used only for debugging. ! 247: */ ! 248: static inline char * dns_unmake_name ( char *name ) { ! 249: char *p; ! 250: unsigned int len; ! 251: ! 252: p = name; ! 253: while ( ( len = *p ) ) { ! 254: *(p++) = '.'; ! 255: p += len; ! 256: } ! 257: ! 258: return name + 1; ! 259: } ! 260: ! 261: /** ! 262: * Decompress a DNS name ! 263: * ! 264: * @v reply DNS replay ! 265: * @v name DNS name ! 266: * @v buf Buffer into which to decompress DNS name ! 267: * @ret next Byte following decompressed DNS name ! 268: */ ! 269: static char * dns_decompress_name ( const struct dns_header *reply, ! 270: const char *name, char *buf ) { ! 271: int i, len; ! 272: ! 273: do { ! 274: /* Obtain next section of name */ ! 275: while ( ( *name ) & 0xc0 ) { ! 276: name = ( ( char * ) reply + ! 277: ( ntohs ( *((uint16_t *)name) ) & ~0xc000 ) ); ! 278: } ! 279: /* Copy data */ ! 280: len = *name; ! 281: for ( i = len + 1 ; i > 0 ; i-- ) { ! 282: *(buf++) = *(name++); ! 283: } ! 284: } while ( len ); ! 285: return buf; ! 286: } ! 287: ! 288: /** ! 289: * Send next packet in DNS request ! 290: * ! 291: * @v dns DNS request ! 292: */ ! 293: static int dns_send_packet ( struct dns_request *dns ) { ! 294: static unsigned int qid = 0; ! 295: size_t qlen; ! 296: ! 297: /* Increment query ID */ ! 298: dns->query.dns.id = htons ( ++qid ); ! 299: ! 300: DBGC ( dns, "DNS %p sending query ID %d\n", dns, qid ); ! 301: ! 302: /* Start retransmission timer */ ! 303: start_timer ( &dns->timer ); ! 304: ! 305: /* Send the data */ ! 306: qlen = ( ( ( void * ) dns->qinfo ) - ( ( void * ) &dns->query ) ! 307: + sizeof ( dns->qinfo ) ); ! 308: return xfer_deliver_raw ( &dns->socket, &dns->query, qlen ); ! 309: } ! 310: ! 311: /** ! 312: * Handle DNS retransmission timer expiry ! 313: * ! 314: * @v timer Retry timer ! 315: * @v fail Failure indicator ! 316: */ ! 317: static void dns_timer_expired ( struct retry_timer *timer, int fail ) { ! 318: struct dns_request *dns = ! 319: container_of ( timer, struct dns_request, timer ); ! 320: ! 321: if ( fail ) { ! 322: dns_done ( dns, -ETIMEDOUT ); ! 323: } else { ! 324: dns_send_packet ( dns ); ! 325: } ! 326: } ! 327: ! 328: /** ! 329: * Receive new data ! 330: * ! 331: * @v dns DNS request ! 332: * @v iobuf I/O buffer ! 333: * @v meta Data transfer metadata ! 334: * @ret rc Return status code ! 335: */ ! 336: static int dns_xfer_deliver ( struct dns_request *dns, ! 337: struct io_buffer *iobuf, ! 338: struct xfer_metadata *meta __unused ) { ! 339: const struct dns_header *reply = iobuf->data; ! 340: union dns_rr_info *rr_info; ! 341: struct sockaddr_in *sin; ! 342: unsigned int qtype = dns->qinfo->qtype; ! 343: int rc; ! 344: ! 345: /* Sanity check */ ! 346: if ( iob_len ( iobuf ) < sizeof ( *reply ) ) { ! 347: DBGC ( dns, "DNS %p received underlength packet length %zd\n", ! 348: dns, iob_len ( iobuf ) ); ! 349: rc = -EINVAL; ! 350: goto done; ! 351: } ! 352: ! 353: /* Check reply ID matches query ID */ ! 354: if ( reply->id != dns->query.dns.id ) { ! 355: DBGC ( dns, "DNS %p received unexpected reply ID %d " ! 356: "(wanted %d)\n", dns, ntohs ( reply->id ), ! 357: ntohs ( dns->query.dns.id ) ); ! 358: rc = -EINVAL; ! 359: goto done; ! 360: } ! 361: ! 362: DBGC ( dns, "DNS %p received reply ID %d\n", dns, ntohs ( reply->id )); ! 363: ! 364: /* Stop the retry timer. After this point, each code path ! 365: * must either restart the timer by calling dns_send_packet(), ! 366: * or mark the DNS operation as complete by calling ! 367: * dns_done() ! 368: */ ! 369: stop_timer ( &dns->timer ); ! 370: ! 371: /* Search through response for useful answers. Do this ! 372: * multiple times, to take advantage of useful nameservers ! 373: * which send us e.g. the CNAME *and* the A record for the ! 374: * pointed-to name. ! 375: */ ! 376: while ( ( rr_info = dns_find_rr ( dns, reply ) ) ) { ! 377: switch ( rr_info->common.type ) { ! 378: ! 379: case htons ( DNS_TYPE_A ): ! 380: ! 381: /* Found the target A record */ ! 382: DBGC ( dns, "DNS %p found address %s\n", ! 383: dns, inet_ntoa ( rr_info->a.in_addr ) ); ! 384: sin = ( struct sockaddr_in * ) &dns->sa; ! 385: sin->sin_family = AF_INET; ! 386: sin->sin_addr = rr_info->a.in_addr; ! 387: ! 388: /* Return resolved address */ ! 389: resolv_done ( &dns->resolv, &dns->sa ); ! 390: ! 391: /* Mark operation as complete */ ! 392: dns_done ( dns, 0 ); ! 393: rc = 0; ! 394: goto done; ! 395: ! 396: case htons ( DNS_TYPE_CNAME ): ! 397: ! 398: /* Found a CNAME record; update query and recurse */ ! 399: DBGC ( dns, "DNS %p found CNAME\n", dns ); ! 400: dns->qinfo = ( void * ) dns_decompress_name ( reply, ! 401: rr_info->cname.cname, ! 402: dns->query.payload ); ! 403: dns->qinfo->qtype = htons ( DNS_TYPE_A ); ! 404: dns->qinfo->qclass = htons ( DNS_CLASS_IN ); ! 405: ! 406: /* Terminate the operation if we recurse too far */ ! 407: if ( ++dns->recursion > DNS_MAX_CNAME_RECURSION ) { ! 408: DBGC ( dns, "DNS %p recursion exceeded\n", ! 409: dns ); ! 410: dns_done ( dns, -ELOOP ); ! 411: rc = 0; ! 412: goto done; ! 413: } ! 414: break; ! 415: ! 416: default: ! 417: DBGC ( dns, "DNS %p got unknown record type %d\n", ! 418: dns, ntohs ( rr_info->common.type ) ); ! 419: break; ! 420: } ! 421: } ! 422: ! 423: /* Determine what to do next based on the type of query we ! 424: * issued and the reponse we received ! 425: */ ! 426: switch ( qtype ) { ! 427: ! 428: case htons ( DNS_TYPE_A ): ! 429: /* We asked for an A record and got nothing; ! 430: * try the CNAME. ! 431: */ ! 432: DBGC ( dns, "DNS %p found no A record; trying CNAME\n", dns ); ! 433: dns->qinfo->qtype = htons ( DNS_TYPE_CNAME ); ! 434: dns_send_packet ( dns ); ! 435: rc = 0; ! 436: goto done; ! 437: ! 438: case htons ( DNS_TYPE_CNAME ): ! 439: /* We asked for a CNAME record. If we got a response ! 440: * (i.e. if the next A query is already set up), then ! 441: * issue it, otherwise abort. ! 442: */ ! 443: if ( dns->qinfo->qtype == htons ( DNS_TYPE_A ) ) { ! 444: dns_send_packet ( dns ); ! 445: rc = 0; ! 446: goto done; ! 447: } else { ! 448: DBGC ( dns, "DNS %p found no CNAME record\n", dns ); ! 449: dns_done ( dns, -ENXIO_NO_RECORD ); ! 450: rc = 0; ! 451: goto done; ! 452: } ! 453: ! 454: default: ! 455: assert ( 0 ); ! 456: dns_done ( dns, -EINVAL ); ! 457: rc = -EINVAL; ! 458: goto done; ! 459: } ! 460: ! 461: done: ! 462: /* Free I/O buffer */ ! 463: free_iob ( iobuf ); ! 464: return rc; ! 465: } ! 466: ! 467: /** ! 468: * Receive new data ! 469: * ! 470: * @v dns DNS request ! 471: * @v rc Reason for close ! 472: */ ! 473: static void dns_xfer_close ( struct dns_request *dns, int rc ) { ! 474: ! 475: if ( ! rc ) ! 476: rc = -ECONNABORTED; ! 477: ! 478: dns_done ( dns, rc ); ! 479: } ! 480: ! 481: /** DNS socket interface operations */ ! 482: static struct interface_operation dns_socket_operations[] = { ! 483: INTF_OP ( xfer_deliver, struct dns_request *, dns_xfer_deliver ), ! 484: INTF_OP ( intf_close, struct dns_request *, dns_xfer_close ), ! 485: }; ! 486: ! 487: /** DNS socket interface descriptor */ ! 488: static struct interface_descriptor dns_socket_desc = ! 489: INTF_DESC ( struct dns_request, socket, dns_socket_operations ); ! 490: ! 491: /** DNS resolver interface operations */ ! 492: static struct interface_operation dns_resolv_op[] = { ! 493: INTF_OP ( intf_close, struct dns_request *, dns_done ), ! 494: }; ! 495: ! 496: /** DNS resolver interface descriptor */ ! 497: static struct interface_descriptor dns_resolv_desc = ! 498: INTF_DESC ( struct dns_request, resolv, dns_resolv_op ); ! 499: ! 500: /** ! 501: * Resolve name using DNS ! 502: * ! 503: * @v resolv Name resolution interface ! 504: * @v name Name to resolve ! 505: * @v sa Socket address to fill in ! 506: * @ret rc Return status code ! 507: */ ! 508: static int dns_resolv ( struct interface *resolv, ! 509: const char *name, struct sockaddr *sa ) { ! 510: struct dns_request *dns; ! 511: char *fqdn; ! 512: int rc; ! 513: ! 514: /* Fail immediately if no DNS servers */ ! 515: if ( ! nameserver.st_family ) { ! 516: DBG ( "DNS not attempting to resolve \"%s\": " ! 517: "no DNS servers\n", name ); ! 518: rc = -ENXIO_NO_NAMESERVER; ! 519: goto err_no_nameserver; ! 520: } ! 521: ! 522: /* Ensure fully-qualified domain name if DHCP option was given */ ! 523: fqdn = dns_qualify_name ( name ); ! 524: if ( ! fqdn ) { ! 525: rc = -ENOMEM; ! 526: goto err_qualify_name; ! 527: } ! 528: ! 529: /* Allocate DNS structure */ ! 530: dns = zalloc ( sizeof ( *dns ) ); ! 531: if ( ! dns ) { ! 532: rc = -ENOMEM; ! 533: goto err_alloc_dns; ! 534: } ! 535: ref_init ( &dns->refcnt, NULL ); ! 536: intf_init ( &dns->resolv, &dns_resolv_desc, &dns->refcnt ); ! 537: intf_init ( &dns->socket, &dns_socket_desc, &dns->refcnt ); ! 538: timer_init ( &dns->timer, dns_timer_expired, &dns->refcnt ); ! 539: memcpy ( &dns->sa, sa, sizeof ( dns->sa ) ); ! 540: ! 541: /* Create query */ ! 542: dns->query.dns.flags = htons ( DNS_FLAG_QUERY | DNS_FLAG_OPCODE_QUERY | ! 543: DNS_FLAG_RD ); ! 544: dns->query.dns.qdcount = htons ( 1 ); ! 545: dns->qinfo = ( void * ) dns_make_name ( fqdn, dns->query.payload ); ! 546: dns->qinfo->qtype = htons ( DNS_TYPE_A ); ! 547: dns->qinfo->qclass = htons ( DNS_CLASS_IN ); ! 548: ! 549: /* Open UDP connection */ ! 550: if ( ( rc = xfer_open_socket ( &dns->socket, SOCK_DGRAM, ! 551: ( struct sockaddr * ) &nameserver, ! 552: NULL ) ) != 0 ) { ! 553: DBGC ( dns, "DNS %p could not open socket: %s\n", ! 554: dns, strerror ( rc ) ); ! 555: goto err_open_socket; ! 556: } ! 557: ! 558: /* Send first DNS packet */ ! 559: dns_send_packet ( dns ); ! 560: ! 561: /* Attach parent interface, mortalise self, and return */ ! 562: intf_plug_plug ( &dns->resolv, resolv ); ! 563: ref_put ( &dns->refcnt ); ! 564: free ( fqdn ); ! 565: return 0; ! 566: ! 567: err_open_socket: ! 568: err_alloc_dns: ! 569: ref_put ( &dns->refcnt ); ! 570: err_qualify_name: ! 571: free ( fqdn ); ! 572: err_no_nameserver: ! 573: return rc; ! 574: } ! 575: ! 576: /** DNS name resolver */ ! 577: struct resolver dns_resolver __resolver ( RESOLV_NORMAL ) = { ! 578: .name = "DNS", ! 579: .resolv = dns_resolv, ! 580: }; ! 581: ! 582: /****************************************************************************** ! 583: * ! 584: * Settings ! 585: * ! 586: ****************************************************************************** ! 587: */ ! 588: ! 589: /** DNS server setting */ ! 590: struct setting dns_setting __setting ( SETTING_IPv4_EXTRA ) = { ! 591: .name = "dns", ! 592: .description = "DNS server", ! 593: .tag = DHCP_DNS_SERVERS, ! 594: .type = &setting_type_ipv4, ! 595: }; ! 596: ! 597: /** Domain name setting */ ! 598: struct setting domain_setting __setting ( SETTING_IPv4_EXTRA ) = { ! 599: .name = "domain", ! 600: .description = "DNS domain", ! 601: .tag = DHCP_DOMAIN_NAME, ! 602: .type = &setting_type_string, ! 603: }; ! 604: ! 605: /** ! 606: * Apply DNS settings ! 607: * ! 608: * @ret rc Return status code ! 609: */ ! 610: static int apply_dns_settings ( void ) { ! 611: struct sockaddr_in *sin_nameserver = ! 612: ( struct sockaddr_in * ) &nameserver; ! 613: int len; ! 614: ! 615: /* Fetch DNS server address */ ! 616: nameserver.st_family = 0; ! 617: if ( ( len = fetch_ipv4_setting ( NULL, &dns_setting, ! 618: &sin_nameserver->sin_addr ) ) >= 0 ){ ! 619: nameserver.st_family = AF_INET; ! 620: DBG ( "DNS using nameserver %s\n", ! 621: inet_ntoa ( sin_nameserver->sin_addr ) ); ! 622: } ! 623: ! 624: /* Get local domain DHCP option */ ! 625: free ( localdomain ); ! 626: if ( ( len = fetch_string_setting_copy ( NULL, &domain_setting, ! 627: &localdomain ) ) < 0 ) { ! 628: DBG ( "DNS could not fetch local domain: %s\n", ! 629: strerror ( len ) ); ! 630: } ! 631: if ( localdomain ) ! 632: DBG ( "DNS local domain %s\n", localdomain ); ! 633: ! 634: return 0; ! 635: } ! 636: ! 637: /** DNS settings applicator */ ! 638: struct settings_applicator dns_applicator __settings_applicator = { ! 639: .apply = apply_dns_settings, ! 640: };
This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.