diff --git a/doc/configuration.txt b/doc/configuration.txt index fd0d89e63..de28c7f0c 100644 --- a/doc/configuration.txt +++ b/doc/configuration.txt @@ -1533,6 +1533,7 @@ The following keywords are supported in the "global" section : - deviceatlas-log-level - deviceatlas-properties-cookie - deviceatlas-separator + - dns-accept-family - expose-deprecated-directives - expose-experimental-directives - external-check @@ -2165,6 +2166,25 @@ deviceatlas-separator Sets the character separator for the API properties results. This directive is optional and set to | by default if not set. +dns-accept-family [,...] + By default, DNS resolvers accept both IPv4 and IPv6 addresses. This can be + influenced by the "resolve-prefer" keywords on server lines as well as the + family argument to the "do-resolve" action, but that is only a preference, + which does not block the other family from being used when it's alone. In + some environments where dual-stack is not usable, stumbling on an unreachable + IPv6-only DNS record can cause significant trouble as it will replace a + previous IPv4 one which would possibly have continued to work till next + request. The "dns-accept-family" global option permits to enforce usage of + only one (or both) address families. The argument is a comma-delimited list + of the following words: + - "ipv4": query and accept IPv4 addresses ("A" records) + - "ipv6": query and accept IPv6 addresses ("AAAA" records) + + When a single family is used, no request will be sent to resolvers for the + other family, and any response for the othe family will be ignored. The + default value is "ipv4,ipv6", which effectively enables both families. + See also: "resolve-prefer", "do-resolve" + expose-deprecated-directives This statement must appear before using some directives tagged as deprecated to silent warnings and make sure the config file will not be rejected. Not @@ -15871,7 +15891,9 @@ do-resolve(,[,ipv4|ipv6]) the result in the variable . It uses the DNS resolvers section pointed by . It is possible to choose a resolution preference using the optional - arguments 'ipv4' or 'ipv6'. + arguments 'ipv4' or 'ipv6'. See also the global "dns-accept-family" keyword + to enforce strict usage of a specific family. + When performing the DNS resolution, the client side connection is on pause waiting till the end of the resolution. If an IP address can be found, it is stored into . If any kind of @@ -19379,8 +19401,9 @@ resolve-prefer When DNS resolution is enabled for a server and multiple IP addresses from different families are returned, HAProxy will prefer using an IP address - from the family mentioned in the "resolve-prefer" parameter. - Available families: "ipv4" and "ipv6" + from the family mentioned in the "resolve-prefer" parameter. See also the + global "dns-accept-family" keyword to enforce strict usage of a specific + family. Available families: "ipv4" and "ipv6". Default value: ipv6 diff --git a/include/haproxy/resolvers-t.h b/include/haproxy/resolvers-t.h index e10c6fa21..916d6d2b5 100644 --- a/include/haproxy/resolvers-t.h +++ b/include/haproxy/resolvers-t.h @@ -92,6 +92,13 @@ extern struct pool_head *resolv_requester_pool; */ #define SRV_MAX_PREF_NET 5 +/* bits describing process-wide acceptable address families for DNS responses */ +enum { + RSLV_ACCEPT_IPV4 = 0x01, + RSLV_ACCEPT_IPV6 = 0x02, + RSLV_ACCEPT_MASK = RSLV_ACCEPT_IPV4 | RSLV_ACCEPT_IPV6, +}; + /* NOTE: big endian structure */ struct resolv_query_item { char name[DNS_MAX_NAME_SIZE+1]; /* query name */ diff --git a/include/haproxy/resolvers.h b/include/haproxy/resolvers.h index d4054b65f..ed4392a79 100644 --- a/include/haproxy/resolvers.h +++ b/include/haproxy/resolvers.h @@ -32,6 +32,7 @@ struct list; extern struct list sec_resolvers; extern unsigned int resolv_failed_resolutions; +extern uint resolv_accept_families; struct resolvers *find_resolvers_by_id(const char *id); struct dns_nameserver *find_nameserver_by_resolvers_and_id(struct resolvers *parent, unsigned int id); diff --git a/src/resolvers.c b/src/resolvers.c index 335ee74e7..5a886162c 100644 --- a/src/resolvers.c +++ b/src/resolvers.c @@ -69,6 +69,8 @@ DECLARE_POOL(resolv_requester_pool, "resolv_requester", sizeof(struct resolv_r static unsigned int resolution_uuid = 1; unsigned int resolv_failed_resolutions = 0; +uint resolv_accept_families = RSLV_ACCEPT_IPV4 | RSLV_ACCEPT_IPV6; + struct task *process_resolvers(struct task *t, void *context, unsigned int state); static void resolv_free_resolution(struct resolv_resolution *resolution); static void _resolv_unlink_resolution(struct resolv_requester *requester); @@ -1632,11 +1634,11 @@ int resolv_get_ip_from_response(struct resolv_response *r_res, unsigned char ip_type; record = eb32_entry(eb32, typeof(*record), link); - if (record->type == DNS_RTYPE_A) { + if (record->type == DNS_RTYPE_A && (resolv_accept_families & RSLV_ACCEPT_IPV4)) { ip_type = AF_INET; ip = &record->data.in4.sin_addr; } - else if (record->type == DNS_RTYPE_AAAA) { + else if (record->type == DNS_RTYPE_AAAA && (resolv_accept_families & RSLV_ACCEPT_IPV6)) { ip_type = AF_INET6; ip = &record->data.in6.sin6_addr; } @@ -2067,9 +2069,12 @@ int resolv_link_resolution(void *requester, int requester_type, int requester_lo hostname_dn = &srv->hostname_dn; hostname_dn_len = srv->hostname_dn_len; resolvers = srv->resolvers; - query_type = ((srv->resolv_opts.family_prio == AF_INET) + + query_type = !(resolv_accept_families & RSLV_ACCEPT_IPV6) ? DNS_RTYPE_A : + !(resolv_accept_families & RSLV_ACCEPT_IPV4) ? DNS_RTYPE_AAAA : + (srv->resolv_opts.family_prio == AF_INET) ? DNS_RTYPE_A - : DNS_RTYPE_AAAA); + : DNS_RTYPE_AAAA; break; case OBJ_TYPE_SRVRQ: @@ -2101,9 +2106,12 @@ int resolv_link_resolution(void *requester, int requester_type, int requester_lo hostname_dn = &stream->resolv_ctx.hostname_dn; hostname_dn_len = stream->resolv_ctx.hostname_dn_len; resolvers = stream->resolv_ctx.parent->arg.resolv.resolvers; - query_type = ((stream->resolv_ctx.parent->arg.resolv.opts->family_prio == AF_INET) + + query_type = !(resolv_accept_families & RSLV_ACCEPT_IPV6) ? DNS_RTYPE_A : + !(resolv_accept_families & RSLV_ACCEPT_IPV4) ? DNS_RTYPE_AAAA : + (stream->resolv_ctx.parent->arg.resolv.opts->family_prio == AF_INET) ? DNS_RTYPE_A - : DNS_RTYPE_AAAA); + : DNS_RTYPE_AAAA; break; default: goto err; @@ -2333,7 +2341,7 @@ static int resolv_process_responses(struct dns_nameserver *ns) if (!res->try) goto report_res_error; } - else { + else if ((resolv_accept_families & RSLV_ACCEPT_MASK) == (RSLV_ACCEPT_IPV4 | RSLV_ACCEPT_IPV6)) { /* Fallback from A to AAAA or the opposite and re-send * the resolution immediately. try counter is not * decremented. */ @@ -2462,9 +2470,11 @@ struct task *process_resolvers(struct task *t, void *context, unsigned int state /* Fallback from A to AAAA or the opposite and re-send * the resolution immediately. try counter is not * decremented. */ - if (res->prefered_query_type == DNS_RTYPE_A) + if (res->prefered_query_type == DNS_RTYPE_A && + (resolv_accept_families & RSLV_ACCEPT_IPV6)) res->query_type = DNS_RTYPE_AAAA; - else if (res->prefered_query_type == DNS_RTYPE_AAAA) + else if (res->prefered_query_type == DNS_RTYPE_AAAA && + (resolv_accept_families & RSLV_ACCEPT_IPV4)) res->query_type = DNS_RTYPE_A; else res->try--; @@ -3925,6 +3935,48 @@ int cfg_post_parse_resolvers() return err_code; } +/* config parser for global "dns-accept-family", accepts "ipv4", "ipv6" or both delimited by a comma */ +static int cfg_parse_dns_accept_family(char **args, int section_type, struct proxy *curpx, + const struct proxy *defpx, const char *file, int line, + char **err) +{ + char *arg, *comma; + int accept_families = 0; + + if (too_many_args(1, args, err, NULL)) + return -1; + + if (!args[1][0]) + goto usage; + + for (arg = args[1]; arg && *arg; arg = comma) { + comma = strchr(arg, ','); + if (comma) + *(comma++) = 0; + + if (strcmp(arg, "ipv4") == 0) + accept_families |= RSLV_ACCEPT_IPV4; + else if (strcmp(arg, "ipv6") == 0) + accept_families |= RSLV_ACCEPT_IPV6; + else + goto usage; + } + + resolv_accept_families = accept_families; + return 0; + usage: + memprintf(err, "'%s' expects a comma-delimited list of 'ipv4' and 'ipv6' but got '%s'.", args[0], args[1]); + return -1; +} + +/* config keyword parsers */ +static struct cfg_kw_list cfg_kws = {ILH, { + { CFG_GLOBAL, "dns-accept-family", cfg_parse_dns_accept_family }, + { 0, NULL, NULL } +}}; + +INITCALL1(STG_REGISTER, cfg_register_keywords, &cfg_kws); + REGISTER_CONFIG_SECTION("resolvers", cfg_parse_resolvers, cfg_post_parse_resolvers); REGISTER_POST_DEINIT(resolvers_deinit); REGISTER_CONFIG_POSTPARSER("dns runtime resolver", resolvers_finalize_config);