resolv: Implement no-aaaa stub resolver option

Message ID 8735gnxd82.fsf@oldenburg.str.redhat.com
State Superseded
Headers
Series resolv: Implement no-aaaa stub resolver option |

Checks

Context Check Description
dj/TryBot-apply_patch success Patch applied to master at the time it was sent
dj/TryBot-32bit success Build for i686

Commit Message

Florian Weimer June 2, 2022, 4:05 p.m. UTC
  Tested on i686-linux-gnu and x86_64-linux-gnu.

---
 NEWS                                  |  12 +
 resolv/Makefile                       |   3 +
 resolv/nss_dns/dns-host.c             |  52 +++-
 resolv/res-noaaaa.c                   | 143 ++++++++++
 resolv/res_debug.c                    |   1 +
 resolv/res_init.c                     |   1 +
 resolv/res_query.c                    |  24 +-
 resolv/res_send.c                     |   9 +-
 resolv/resolv-internal.h              |   8 +
 resolv/resolv.h                       |   1 +
 resolv/tst-resolv-noaaaa.c            | 483 ++++++++++++++++++++++++++++++++++
 resolv/tst-resolv-res_init-skeleton.c |  10 +
 12 files changed, 735 insertions(+), 12 deletions(-)
  

Comments

Carlos O'Donell June 2, 2022, 7:17 p.m. UTC | #1
On 6/2/22 12:05, Florian Weimer wrote:
> Tested on i686-linux-gnu and x86_64-linux-gnu.
> 
> ---

Have a look at the typos. Looking forward to a v2. As a diagnostic option I think
this will really help downstream remove the AAAA queries as a potential source of
problems when diagnosing network issues.

Reviewed the following files and their interfaces:

resolv/res_query.c
	- context_search_query may need a hook. [OK in __res_context_query]
resolv/res_send.c
	- context_send_common may need a hook [OK]
resolv/compat-gethnamaddr.c
	- Single call in res_gethostbyname2_context.
	- Call does not issue additional AAAA. [Not needed]
resolv/nss_dns/dns-network.c
	- Skip this because we said we will only impact host lookupss. [Not needed]
resolv/nss_dns/dns-host.c
	- 3 calls to __res_context_search.
	- 2 in gethostbyname3_context, 1 in _nss_dns_gethostbyname4_r
	- gethostbyname3_context does not issue additional AAAA. [Not needed]
	- _nss_dns_gethostbyname4_r with T_QUERY_A_AND_AAAA must be disabled. [OK]

The design of the no-aaaa stub resolver option looks correct to me.

I'll do another review in v2, but it looks like it could go in since the impact
is restricted to very specific code paths and does not change the buffer management
in send_vc or send_dg.

>  NEWS                                  |  12 +
>  resolv/Makefile                       |   3 +
>  resolv/nss_dns/dns-host.c             |  52 +++-
>  resolv/res-noaaaa.c                   | 143 ++++++++++
>  resolv/res_debug.c                    |   1 +
>  resolv/res_init.c                     |   1 +
>  resolv/res_query.c                    |  24 +-
>  resolv/res_send.c                     |   9 +-
>  resolv/resolv-internal.h              |   8 +
>  resolv/resolv.h                       |   1 +
>  resolv/tst-resolv-noaaaa.c            | 483 ++++++++++++++++++++++++++++++++++
>  resolv/tst-resolv-res_init-skeleton.c |  10 +
>  12 files changed, 735 insertions(+), 12 deletions(-)
> 
> diff --git a/NEWS b/NEWS
> index ad0c08d8ca..9e511cdc6a 100644
> --- a/NEWS
> +++ b/NEWS
> @@ -20,6 +20,18 @@ Major new features:
>    have been added.  The pidfd functionality provides access to a process
>    while avoiding the issue of PID reuse on tranditional Unix systems.
>  
> +* The “no-aaaa” DNS stob resolver option has been added.  System

s/stob/stub/g

> +  administrators can use it to suppress AAAA queries made by the stub
> +  resolver, including AAAA lookups triggered by NSS-based interfaces
> +  such as getaddrinfo.  Only DNS lookups are affected: IPv6 data in
> +  /etc/hosts is still used, getaddrinfo with AI_PASSIVE will still
> +  produce IPv6 addresses, and configured IPv6 name servers are still
> +  used.  To produce correct Name Error (NXDOMAIN) results, AAAA queries
> +  are translated to A queries.  The new resolver option is intended
> +  preliminary for diagnostic purposes, to rule out that AAAA DNS queries
> +  have adverse impact.  It is incompatible with EDNS0 usage and DNSSEC
> +  validation by applications.

OK.

> +
>  Deprecated and removed features, and other changes affecting compatibility:
>  
>  * Support for prelink will be removed in the next release; this includes
> diff --git a/resolv/Makefile b/resolv/Makefile
> index 438672786f..5b15321f9b 100644
> --- a/resolv/Makefile
> +++ b/resolv/Makefile
> @@ -51,6 +51,7 @@ routines := \
>    nss_dns_functions \
>    res-close \
>    res-name-checking \
> +  res-noaaaa \

OK. New file.

>    res-state \
>    res_context_hostalias \
>    res_enable_icmp \
> @@ -92,6 +93,7 @@ tests += \
>    tst-resolv-binary \
>    tst-resolv-edns \
>    tst-resolv-network \
> +  tst-resolv-noaaaa \

OK. New test.

>    tst-resolv-nondecimal \
>    tst-resolv-res_init-multi \
>    tst-resolv-search \
> @@ -265,6 +267,7 @@ $(objpfx)tst-resolv-res_init-multi: $(objpfx)libresolv.so \
>    $(shared-thread-library)
>  $(objpfx)tst-resolv-res_init-thread: $(objpfx)libresolv.so \
>    $(shared-thread-library)
> +$(objpfx)tst-resolv-noaaaa: $(objpfx)libresolv.so $(shared-thread-library)

OK. Test uses libresolv.so.

>  $(objpfx)tst-resolv-nondecimal: $(objpfx)libresolv.so $(shared-thread-library)
>  $(objpfx)tst-resolv-qtypes: $(objpfx)libresolv.so $(shared-thread-library)
>  $(objpfx)tst-resolv-rotate: $(objpfx)libresolv.so $(shared-thread-library)
> diff --git a/resolv/nss_dns/dns-host.c b/resolv/nss_dns/dns-host.c
> index 913a5cb82f..544cffbecd 100644
> --- a/resolv/nss_dns/dns-host.c
> +++ b/resolv/nss_dns/dns-host.c

OK, gethostbyname3_context does not issue additional AAAA.

> @@ -124,6 +124,14 @@ static enum nss_status gaih_getanswer (const querybuf *answer1, int anslen1,
>  				       char *buffer, size_t buflen,
>  				       int *errnop, int *h_errnop,
>  				       int32_t *ttlp);
> +static enum nss_status gaih_getanswer_noaaaa (const querybuf *answer1,
> +					      int anslen1,
> +					      const char *qname,
> +					      struct gaih_addrtuple **pat,
> +					      char *buffer, size_t buflen,
> +					      int *errnop, int *h_errnop,
> +					      int32_t *ttlp);


OK. New internal function gaih_getanswer_noaaaa().


> +
>  
>  static enum nss_status gethostbyname3_context (struct resolv_context *ctx,
>  					       const char *name, int af,
> @@ -369,17 +377,31 @@ _nss_dns_gethostbyname4_r (const char *name, struct gaih_addrtuple **pat,
>    int resplen2 = 0;
>    int ans2p_malloced = 0;
>  
> +
>    int olderr = errno;
> -  int n = __res_context_search (ctx, name, C_IN, T_QUERY_A_AND_AAAA,

OK. Don't immediately search.

> +  int n;
> +
> +  if ((ctx->resp->options & RES_NOAAAA) == 0)

OK. Check the flag.

> +    {
> +      n = __res_context_search (ctx, name, C_IN, T_QUERY_A_AND_AAAA,
>  				host_buffer.buf->buf, 2048, &host_buffer.ptr,
>  				&ans2p, &nans2p, &resplen2, &ans2p_malloced);

OK. Query.

> -  if (n >= 0)
> -    {
> -      status = gaih_getanswer (host_buffer.buf, n, (const querybuf *) ans2p,
> -			       resplen2, name, pat, buffer, buflen,
> -			       errnop, herrnop, ttlp);
> +      if (n >= 0)
> +	status = gaih_getanswer (host_buffer.buf, n, (const querybuf *) ans2p,
> +				 resplen2, name, pat, buffer, buflen,
> +				 errnop, herrnop, ttlp);

OK. Indent.

>      }
>    else
> +    {
> +      n = __res_context_search (ctx, name, C_IN, T_A,
> +				host_buffer.buf->buf, 2048, NULL,
> +				NULL, NULL, NULL, NULL);

OK. T_A only with no second buffer for AAAA.

> +      if (n >= 0)
> +	status = gaih_getanswer_noaaaa (host_buffer.buf, n,
> +					name, pat, buffer, buflen,
> +					errnop, herrnop, ttlp);

OK. Have a result, but extract just the A.

> +    }
> +  if (n < 0)
>      {
>        switch (errno)
>  	{
> @@ -1387,3 +1409,21 @@ gaih_getanswer (const querybuf *answer1, int anslen1, const querybuf *answer2,
>  
>    return status;
>  }
> +
> +/* Variant of gaih_getanswer without a second (AAAA) response.  */
> +static enum nss_status
> +gaih_getanswer_noaaaa (const querybuf *answer1, int anslen1, const char *qname,
> +		       struct gaih_addrtuple **pat,
> +		       char *buffer, size_t buflen,
> +		       int *errnop, int *h_errnop, int32_t *ttlp)
> +{
> +  int first = 1;
> +
> +  enum nss_status status = NSS_STATUS_NOTFOUND;
> +  if (anslen1 > 0)
> +    status = gaih_getanswer_slice (answer1, anslen1, qname,
> +				   &pat, &buffer, &buflen,
> +				   errnop, h_errnop, ttlp,
> +				   &first);

OK.

> +  return status;
> +}
> diff --git a/resolv/res-noaaaa.c b/resolv/res-noaaaa.c
> new file mode 100644
> index 0000000000..a71b34a19d
> --- /dev/null
> +++ b/resolv/res-noaaaa.c
> @@ -0,0 +1,143 @@
> +/* Implement suppression of AAAA queries.
> +   Copyright (C) 2022 Free Software Foundation, Inc.
> +   This file is part of the GNU C Library.
> +
> +   The GNU C Library is free software; you can redistribute it and/or
> +   modify it under the terms of the GNU Lesser General Public
> +   License as published by the Free Software Foundation; either
> +   version 2.1 of the License, or (at your option) any later version.
> +
> +   The GNU C Library 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
> +   Lesser General Public License for more details.
> +
> +   You should have received a copy of the GNU Lesser General Public
> +   License along with the GNU C Library; if not, see
> +   <https://www.gnu.org/licenses/>.  */
> +
> +#include <resolv.h>
> +#include <string.h>
> +#include <resolv-internal.h>
> +#include <resolv_context.h>
> +#include <arpa/nameser.h>
> +
> +/* Returns true if the question type at P matches EXPECTED, and the
> +   class is IN.  */
> +static bool
> +qtype_matches (const unsigned char *p, int expected)
> +{
> +  /* This assumes that T_A/C_IN constants are less than 256, which
> +     they are.  */
> +  return p[0] == 0 && p[1] == expected && p[2] == 0 && p[3] == C_IN;

OK.

> +}
> +
> +/* Handle RES_NOAAAA translation of AAAA queries.  To produce a Name
> +   Error (NXDOMAIN) repsonse for domain names that do not exist, it is
> +   still necessary to send a query.  Using question type A is a
> +   conservative choice.  In the returned answer, it is necessary to
> +   switch back the question type to AAAA.  */
> +bool
> +__res_handle_no_aaaa (struct resolv_context *ctx,
> +                      const unsigned char *buf, int buflen,
> +                      unsigned char *ans, int anssiz, int *result)
> +{
> +  /* AAAA mode is not active, or the query looks invalid (will not be
> +     able to be parsed).  */
> +  if ((ctx->resp->options & RES_NOAAAA) == 0
> +      || buflen <= sizeof (HEADER))
> +    return false;
> +
> +  /* The replacement A query is produced here.  */
> +  struct
> +  {
> +    HEADER header;
> +    unsigned char question[NS_MAXCDNAME + 4];
> +  } replacement;
> +  memcpy (&replacement.header, buf, sizeof (replacement.header));
> +
> +  if (replacement.header.qr
> +      || replacement.header.opcode != 0
> +      || replacement.header.rcode != 0
> +      || ntohs (replacement.header.qdcount) != 1
> +      || ntohs (replacement.header.ancount) != 0
> +      || ntohs (replacement.header.nscount) != 0)
> +    /* Not a well-formed question.  Let the core resolver code produce
> +       the proper error.  */
> +    return false;
> +
> +  /* Disable EDNS0.  */
> +  replacement.header.arcount = htons (0);

OK. Disable EDNS0 because we changed the query.

> +
> +  /* Extract the QNAME.  */
> +  int ret = __ns_name_unpack (buf, buf + buflen, buf + sizeof (HEADER),
> +                              replacement.question, NS_MAXCDNAME);
> +  if (ret < 0)
> +    /* Format error.  */
> +    return false;
> +
> +  /* Compute the end of the question name.  */
> +  const unsigned char *after_question = buf + sizeof (HEADER) + ret;

OK.

> +
> +  /* Check that we are dealing with an AAAA query.  */
> +  if (buf + buflen - after_question < 4
> +      || !qtype_matches (after_question, T_AAAA))
> +    return false;

OK.

> +
> +  /* Find the place to store the type/class data in the replacement
> +     query.  */
> +  after_question = replacement.question;
> +  /* This cannot fail because __ns_name_unpack above produced a valid
> +     domain name.  */
> +  (void) __ns_name_skip (&after_question, &replacement.question[NS_MAXCDNAME]);
> +  unsigned char *start_of_query = (unsigned char *) &replacement;
> +  const unsigned char *end_of_query = after_question + 4;
> +
> +  /* Produce an A/IN query.  */
> +  {
> +    unsigned char *p = (unsigned char *) after_question;
> +    p[0] = 0;
> +    p[1] = T_A;

OK.

> +    p[2] = 0;
> +    p[3] = C_IN;
> +  }
> +
> +  /* Clear the output buffer, to avoid reading undefined data when
> +     rewriting the result from A to AAAA.  */
> +  memset (ans, 0, anssiz);

OK.

> +
> +  /* Always perform the message translation, independent of the error
> +     code.  */
> +  ret = __res_context_send (ctx,
> +                            start_of_query, end_of_query - start_of_query,
> +                            NULL, 0, ans, anssiz,
> +                            NULL, NULL, NULL, NULL, NULL);

OK.

> +
> +  /* Patch in the AAAA question type if there is room and the A query
> +     type was received.  */
> +  after_question = ans + sizeof (HEADER);
> +  if (__ns_name_skip (&after_question, ans + anssiz) == 0
> +      && ans + anssiz - after_question >= 4
> +      && qtype_matches (after_question, T_A))
> +    {
> +      ((unsigned char *) after_question)[1] = T_AAAA;
> +
> +      /* Create an aligned copy of the header.  Hide all data exception

s/exception/except/g

> +         the question from the response.  Put back the header.  There is
> +         no need to change the response code.  The zero answer count turns
> +         a positive response with data into a no-data response.  */
> +      memcpy (&replacement.header, ans, sizeof (replacement.header));
> +      replacement.header.ancount = htons (0);
> +      replacement.header.nscount = htons (0);
> +      replacement.header.arcount = htons (0);
> +      memcpy (ans, &replacement.header, sizeof (replacement.header));
> +
> +      /* Truncate the reply.  */
> +      if (ret <= 0)
> +        *result = ret;
> +      else
> +        *result = after_question - ans + 4;
> +    }
> +
> +  return true;

OK.

> +}
> diff --git a/resolv/res_debug.c b/resolv/res_debug.c
> index 030df0aa90..b0fe69b1aa 100644
> --- a/resolv/res_debug.c
> +++ b/resolv/res_debug.c
> @@ -613,6 +613,7 @@ p_option(u_long option) {
>  	case RES_NOTLDQUERY:	return "no-tld-query";
>  	case RES_NORELOAD:	return "no-reload";
>  	case RES_TRUSTAD:	return "trust-ad";
> +	case RES_NOAAAA:	return "no-aaaa";

OK. New option.

>  				/* XXX nonreentrant */
>  	default:		sprintf(nbuf, "?0x%lx?", (u_long)option);
>  				return (nbuf);
> diff --git a/resolv/res_init.c b/resolv/res_init.c
> index cbc16ba021..2c0bea658e 100644
> --- a/resolv/res_init.c
> +++ b/resolv/res_init.c
> @@ -695,6 +695,7 @@ res_setoptions (struct resolv_conf_parser *parser, const char *options)
>              { STRnLEN ("no-reload"), 0, RES_NORELOAD },
>              { STRnLEN ("use-vc"), 0, RES_USEVC },
>              { STRnLEN ("trust-ad"), 0, RES_TRUSTAD },
> +            { STRnLEN ("no-aaaa"), 0, RES_NOAAAA },

OK.

>            };
>  #define noptions (sizeof (options) / sizeof (options[0]))
>            for (int i = 0; i < noptions; ++i)
> diff --git a/resolv/res_query.c b/resolv/res_query.c
> index 3b5c604261..049de91b95 100644
> --- a/resolv/res_query.c
> +++ b/resolv/res_query.c
> @@ -204,10 +204,26 @@ __res_context_query (struct resolv_context *ctx, const char *name,
>  			free (buf);
>  		return (n);
>  	}
> -	assert (answerp == NULL || (void *) *answerp == (void *) answer);
> -	n = __res_context_send (ctx, query1, nquery1, query2, nquery2, answer,
> -				anslen, answerp, answerp2, nanswerp2, resplen2,
> -				answerp2_malloced);

OK. Not in send_vc or send_dg, but one level above.

> +
> +	/* Suppress AAAA lookups if required.  __res_handle_no_aaaa
> +	   checks RES_NOAAAA first, so avoids parsing the
> +	   just-generated query packet in most cases.  nss_dns avoids
> +	   using T_QUERY_A_AND_AAAA in RES_NOAAAA mode, so there is no
> +	   need to handle it here.  */
> +	if (type == T_AAAA && __res_handle_no_aaaa (ctx, query1, nquery1,
> +						    answer, anslen, &n))
> +	  /* There must be no second query for AAAA queries.  The code
> +	     below is still needed to translate NODATA responses.  */
> +	  assert (query2 == NULL);

OK.

> +	else
> +	  {
> +	    assert (answerp == NULL || (void *) *answerp == (void *) answer);
> +	    n = __res_context_send (ctx, query1, nquery1, query2, nquery2,
> +				    answer, anslen,
> +				    answerp, answerp2, nanswerp2, resplen2,
> +				    answerp2_malloced);

OK. __res_context_send as before.

> +	  }
> +
>  	if (use_malloc)
>  		free (buf);
>  	if (n < 0) {
> diff --git a/resolv/res_send.c b/resolv/res_send.c
> index d6c85fd7a2..6a08e729a4 100644
> --- a/resolv/res_send.c
> +++ b/resolv/res_send.c
> @@ -438,8 +438,13 @@ context_send_common (struct resolv_context *ctx,
>        RES_SET_H_ERRNO (&_res, NETDB_INTERNAL);
>        return -1;
>      }
> -  int result = __res_context_send (ctx, buf, buflen, NULL, 0, ans, anssiz,
> -				   NULL, NULL, NULL, NULL, NULL);
> +
> +  int result;
> +  if (__res_handle_no_aaaa (ctx, buf, buflen, ans, anssiz, &result))
> +    return result;

OK.

> +
> +  result = __res_context_send (ctx, buf, buflen, NULL, 0, ans, anssiz,
> +			       NULL, NULL, NULL, NULL, NULL);

OK. Otherwise as before.

>    __resolv_context_put (ctx);
>    return result;
>  }
> diff --git a/resolv/resolv-internal.h b/resolv/resolv-internal.h
> index 9d2e832d68..774dab0e56 100644
> --- a/resolv/resolv-internal.h
> +++ b/resolv/resolv-internal.h
> @@ -85,6 +85,14 @@ int __res_context_send (struct resolv_context *, const unsigned char *, int,
>                          int *, int *, int *);
>  libc_hidden_proto (__res_context_send)
>  
> +/* Return true if the query has been handled in RES_NOAAAA mode.  For
> +   that, RES_NOAAAA must be activ, and the question type must be AAAA.

s/activ/active/g

> +   The caller is expected to return *RESULT as the return value.  */
> +bool __res_handle_no_aaaa (struct resolv_context *ctx,
> +                           const unsigned char *buf, int buflen,
> +                           unsigned char *ans, int anssiz, int *result)
> +  attribute_hidden;
> +
>  /* Internal function similar to res_hostalias.  */
>  const char *__res_context_hostalias (struct resolv_context *,
>                                       const char *, char *, size_t);
> diff --git a/resolv/resolv.h b/resolv/resolv.h
> index e7c8d44645..3a79ffea57 100644
> --- a/resolv/resolv.h
> +++ b/resolv/resolv.h
> @@ -132,6 +132,7 @@ struct res_sym {
>  					   as a TLD.  */
>  #define RES_NORELOAD    0x02000000 /* No automatic configuration reload.  */
>  #define RES_TRUSTAD     0x04000000 /* Request AD bit, keep it in responses.  */
> +#define RES_NOAAAA      0x08000000 /* Suppress AAAA queries.  */

OK.

>  
>  #define RES_DEFAULT	(RES_RECURSE|RES_DEFNAMES|RES_DNSRCH)
>  
> diff --git a/resolv/tst-resolv-noaaaa.c b/resolv/tst-resolv-noaaaa.c
> new file mode 100644
> index 0000000000..9611d80720
> --- /dev/null
> +++ b/resolv/tst-resolv-noaaaa.c
> @@ -0,0 +1,483 @@
> +/* Test the RES_NOAAAA resolver option.

OK.

> +   Copyright (C) 2022 Free Software Foundation, Inc.
> +   This file is part of the GNU C Library.
> +
> +   The GNU C Library is free software; you can redistribute it and/or
> +   modify it under the terms of the GNU Lesser General Public
> +   License as published by the Free Software Foundation; either
> +   version 2.1 of the License, or (at your option) any later version.
> +
> +   The GNU C Library 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
> +   Lesser General Public License for more details.
> +
> +   You should have received a copy of the GNU Lesser General Public
> +   License along with the GNU C Library; if not, see
> +   <https://www.gnu.org/licenses/>.  */
> +
> +#include <errno.h>
> +#include <netdb.h>
> +#include <resolv.h>
> +#include <stdlib.h>
> +#include <support/check.h>
> +#include <support/check_nss.h>

OK. check_nss provides check_hostent to verify that the host has the result we expect.

> +#include <support/resolv_test.h>
> +#include <support/support.h>
> +
> +/* Used to keep track of the number of queries.  */
> +static volatile unsigned int queries;
> +
> +static void
> +response (const struct resolv_response_context *ctx,
> +          struct resolv_response_builder *b,
> +          const char *qname, uint16_t qclass, uint16_t qtype)
> +{
> +  /* Each test should only send one query.  */
> +  ++queries;
> +  TEST_COMPARE (queries, 1);
> +
> +  /* AAAA queries are supposedto be disabled.  */

s/supposedto/supposed to/g

> +  TEST_VERIFY (qtype != T_AAAA);

OK.

> +  TEST_COMPARE (qclass, C_IN);

OK.

> +
> +  /* The only other query type besides A is PTR.  */
> +  if (qtype != T_A)
> +    TEST_COMPARE (qtype, T_PTR);

OK.

> +
> +  int an, ns, ar;
> +  char *tail;
> +  if (sscanf (qname, "an%d.ns%d.ar%d.%ms", &an, &ns, &ar, &tail) != 4)
> +    FAIL_EXIT1 ("invalid QNAME: %s\n", qname);
> +  TEST_COMPARE_STRING (tail, "example");
> +  free (tail);
> +
> +  if (an < 0 || ns < 0 || ar < 0)
> +    {
> +      struct resolv_response_flags flags = { .rcode = NXDOMAIN, };
> +      resolv_response_init (b, flags);
> +      resolv_response_add_question (b, qname, qclass, qtype);
> +      return;
> +    }
> +
> +  struct resolv_response_flags flags = {};
> +  resolv_response_init (b, flags);
> +  resolv_response_add_question (b, qname, qclass, qtype);
> +
> +  resolv_response_section (b, ns_s_an);
> +  for (int i = 0; i < an; ++i)
> +    {
> +      resolv_response_open_record (b, qname, qclass, qtype, 60);
> +      switch (qtype)
> +        {
> +        case T_A:
> +          char ipv4[4] = {192, 0, 2, i + 1};
> +          resolv_response_add_data (b, &ipv4, sizeof (ipv4));
> +          break;
> +
> +        case T_PTR:
> +          char *name = xasprintf ("ptr-%d", i);
> +          resolv_response_add_name (b, name);
> +          free (name);
> +          break;
> +        }
> +      resolv_response_close_record (b);
> +    }
> +
> +  resolv_response_section (b, ns_s_ns);
> +  for (int i = 0; i < ns; ++i)
> +    {
> +      resolv_response_open_record (b, qname, qclass, T_NS, 60);
> +      char *name = xasprintf ("ns%d.example.net", i);
> +      resolv_response_add_name (b, name);
> +      free (name);
> +      resolv_response_close_record (b);
> +    }
> +
> +  resolv_response_section (b, ns_s_ar);
> +  int addr = 1;
> +  for (int i = 0; i < ns; ++i)
> +    {
> +      char *name = xasprintf ("ns%d.example.net", i);
> +      for (int j = 0; j < ar; ++j)
> +        {
> +          resolv_response_open_record (b, name, qclass, T_A, 60);
> +          char ipv4[4] = {192, 0, 2, addr};
> +          resolv_response_add_data (b, &ipv4, sizeof (ipv4));
> +          resolv_response_close_record (b);
> +
> +          resolv_response_open_record (b, name, qclass, T_AAAA, 60);
> +          char ipv6[16]
> +            = {0x20, 0x01, 0xd, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, addr};
> +          resolv_response_add_data (b, &ipv6, sizeof (ipv6));
> +          resolv_response_close_record (b);
> +
> +          ++addr;
> +        }
> +      free (name);
> +    }
> +}
> +
> +/* Number of modes.  Lowest bit encodes *n* function vs implicit _res
> +   argument.  The mode numbers themselves are arbitrary.  */
> +enum { mode_count = 8 };
> +
> +/* res_send-like modes do not perform error translation.  */
> +enum { first_send_mode = 6 };
> +
> +static int
> +libresolv_query (unsigned int mode, const char *qname, uint16_t qtype,
> +                 unsigned char *buf, size_t buflen)
> +{
> +  int saved_errno = errno;
> +
> +  TEST_VERIFY_EXIT (mode < mode_count);
> +
> +  switch (mode)
> +    {
> +    case 0:
> +      return res_query (qname, C_IN, qtype, buf, buflen);
> +    case 1:
> +      return res_nquery (&_res, qname, C_IN, qtype, buf, buflen);
> +    case 2:
> +      return res_search (qname, C_IN, qtype, buf, buflen);
> +    case 3:
> +      return res_nsearch (&_res, qname, C_IN, qtype, buf, buflen);
> +    case 4:
> +      return res_querydomain (qname, "", C_IN, qtype, buf, buflen);
> +    case 5:
> +      return res_nquerydomain (&_res, qname, "", C_IN, qtype, buf, buflen);
> +    case 6:
> +      {
> +        unsigned char querybuf[512];
> +        int ret = res_mkquery (QUERY, qname, C_IN, qtype,
> +                               NULL, 0, NULL, querybuf, sizeof (querybuf));
> +        TEST_VERIFY_EXIT (ret > 0);
> +        errno = saved_errno;
> +        return res_send (querybuf, ret, buf, buflen);
> +      }
> +    case 7:
> +      {
> +        unsigned char querybuf[512];
> +        int ret = res_nmkquery (&_res, QUERY, qname, C_IN, qtype,
> +                                NULL, 0, NULL, querybuf, sizeof (querybuf));
> +        TEST_VERIFY_EXIT (ret > 0);
> +        errno = saved_errno;
> +        return res_nsend (&_res, querybuf, ret, buf, buflen);
> +      }
> +    }
> +  __builtin_unreachable ();
> +}

OK.

> +
> +static int
> +do_test (void)
> +{
> +  struct resolv_test *obj = resolv_test_start

OK. Call resolv_test_start with the closure required start and run the interposed in-process resolver.

> +    ((struct resolv_redirect_config)
> +     {
> +       .response_callback = response

OK. The callback is the function above "response" as expected.

> +     });
> +
> +  _res.options |= RES_NOAAAA;

OK.

> +
> +  check_hostent ("an1.ns2.ar1.example",
> +                 gethostbyname ("an1.ns2.ar1.example"),
> +                 "name: an1.ns2.ar1.example\n"
> +                 "address: 192.0.2.1\n");
> +  queries = 0;
> +  check_hostent ("an0.ns2.ar1.example",
> +                 gethostbyname ("an0.ns2.ar1.example"),
> +                 "error: NO_ADDRESS\n");
> +  queries = 0;
> +  check_hostent ("an-1.ns2.ar1.example",
> +                 gethostbyname ("an-1.ns2.ar1.example"),
> +                 "error: HOST_NOT_FOUND\n");
> +  queries = 0;
> +
> +  check_hostent ("an1.ns2.ar1.example AF_INET",
> +                 gethostbyname2 ("an1.ns2.ar1.example", AF_INET),
> +                 "name: an1.ns2.ar1.example\n"
> +                 "address: 192.0.2.1\n");
> +  queries = 0;
> +  check_hostent ("an0.ns2.ar1.example AF_INET",
> +                 gethostbyname2 ("an0.ns2.ar1.example", AF_INET),
> +                 "error: NO_ADDRESS\n");
> +  queries = 0;
> +  check_hostent ("an-1.ns2.ar1.example AF_INET",
> +                 gethostbyname2 ("an-1.ns2.ar1.example", AF_INET),
> +                 "error: HOST_NOT_FOUND\n");
> +  queries = 0;
> +
> +  check_hostent ("an1.ns2.ar1.example AF_INET6",
> +                 gethostbyname2 ("an1.ns2.ar1.example", AF_INET6),
> +                 "error: NO_ADDRESS\n");
> +  queries = 0;
> +  check_hostent ("an0.ns2.ar1.example AF_INET6",
> +                 gethostbyname2 ("an0.ns2.ar1.example", AF_INET6),
> +                 "error: NO_ADDRESS\n");
> +  queries = 0;
> +  check_hostent ("an-1.ns2.ar1.example AF_INET6",
> +                 gethostbyname2 ("an-1.ns2.ar1.example", AF_INET6),
> +                 "error: HOST_NOT_FOUND\n");
> +  queries = 0;
> +
> +  /* Multiple addresses.  */
> +  check_hostent ("an2.ns0.ar0.example",
> +                 gethostbyname ("an2.ns0.ar0.example"),
> +                 "name: an2.ns0.ar0.example\n"
> +                 "address: 192.0.2.1\n"
> +                 "address: 192.0.2.2\n");
> +  queries = 0;
> +  check_hostent ("an2.ns0.ar0.example AF_INET6",
> +                 gethostbyname2 ("an2.ns0.ar0.example", AF_INET6),
> +                 "error: NO_ADDRESS\n");
> +  queries = 0;
> +
> +  /* getaddrinfo checks with one address.  */
> +  struct addrinfo *ai;
> +  int ret;
> +  ret = getaddrinfo ("an1.ns2.ar1.example", "80",
> +                     &(struct addrinfo)
> +                     {
> +                       .ai_family = AF_INET,
> +                       .ai_socktype = SOCK_STREAM,
> +                     }, &ai);
> +  check_addrinfo ("an1.ns2.ar1.example (AF_INET)", ai, ret,
> +                  "address: STREAM/TCP 192.0.2.1 80\n");
> +  freeaddrinfo (ai);
> +  queries = 0;
> +  ret = getaddrinfo ("an1.ns2.ar1.example", "80",
> +                     &(struct addrinfo)
> +                     {
> +                       .ai_family = AF_INET6,
> +                       .ai_socktype = SOCK_STREAM,
> +                     }, &ai);
> +  check_addrinfo ("an1.ns2.ar1.example (AF_INET6)", ai, ret,
> +                  "error: No address associated with hostname\n");
> +  queries = 0;
> +  ret = getaddrinfo ("an1.ns2.ar1.example", "80",
> +                     &(struct addrinfo)
> +                     {
> +                       .ai_family = AF_UNSPEC,
> +                       .ai_socktype = SOCK_STREAM,
> +                     }, &ai);
> +  check_addrinfo ("an1.ns2.ar1.example (AF_UNSPEC)", ai, ret,
> +                  "address: STREAM/TCP 192.0.2.1 80\n");
> +  freeaddrinfo (ai);
> +  queries = 0;
> +
> +  /* getaddrinfo checks with three addresses.  */
> +  ret = getaddrinfo ("an3.ns2.ar1.example", "80",
> +                     &(struct addrinfo)
> +                     {
> +                       .ai_family = AF_INET,
> +                       .ai_socktype = SOCK_STREAM,
> +                     }, &ai);
> +  check_addrinfo ("an3.ns2.ar1.example (AF_INET)", ai, ret,
> +                  "address: STREAM/TCP 192.0.2.1 80\n"
> +                  "address: STREAM/TCP 192.0.2.2 80\n"
> +                  "address: STREAM/TCP 192.0.2.3 80\n");
> +  freeaddrinfo (ai);
> +  queries = 0;
> +  ret = getaddrinfo ("an3.ns2.ar1.example", "80",
> +                     &(struct addrinfo)
> +                     {
> +                       .ai_family = AF_INET6,
> +                       .ai_socktype = SOCK_STREAM,
> +                     }, &ai);
> +  check_addrinfo ("an3.ns2.ar1.example (AF_INET6)", ai, ret,
> +                  "error: No address associated with hostname\n");
> +  queries = 0;
> +  ret = getaddrinfo ("an3.ns2.ar1.example", "80",
> +                     &(struct addrinfo)
> +                     {
> +                       .ai_family = AF_UNSPEC,
> +                       .ai_socktype = SOCK_STREAM,
> +                     }, &ai);
> +  check_addrinfo ("an3.ns2.ar1.example (AF_UNSPEC)", ai, ret,
> +                  "address: STREAM/TCP 192.0.2.1 80\n"
> +                  "address: STREAM/TCP 192.0.2.2 80\n"
> +                  "address: STREAM/TCP 192.0.2.3 80\n");
> +  freeaddrinfo (ai);
> +  queries = 0;
> +
> +  /* getaddrinfo checks with no address.  */
> +  ret = getaddrinfo ("an0.ns2.ar1.example", "80",
> +                     &(struct addrinfo)
> +                     {
> +                       .ai_family = AF_INET,
> +                       .ai_socktype = SOCK_STREAM,
> +                     }, &ai);
> +  check_addrinfo ("an0.ns2.ar1.example (AF_INET)", ai, ret,
> +                  "error: No address associated with hostname\n");
> +  queries = 0;
> +  ret = getaddrinfo ("an0.ns2.ar1.example", "80",
> +                     &(struct addrinfo)
> +                     {
> +                       .ai_family = AF_INET6,
> +                       .ai_socktype = SOCK_STREAM,
> +                     }, &ai);
> +  check_addrinfo ("an0.ns2.ar1.example (AF_INET6)", ai, ret,
> +                  "error: No address associated with hostname\n");
> +  queries = 0;
> +  ret = getaddrinfo ("an0.ns2.ar1.example", "80",
> +                     &(struct addrinfo)
> +                     {
> +                       .ai_family = AF_UNSPEC,
> +                       .ai_socktype = SOCK_STREAM,
> +                     }, &ai);
> +  check_addrinfo ("an-1.ns2.ar1.example (AF_UNSPEC)", ai, ret,
> +                  "error: No address associated with hostname\n");
> +  queries = 0;
> +
> +  /* getaddrinfo checks with NXDOMAIN.  */
> +  ret = getaddrinfo ("an-1.ns2.ar1.example", "80",
> +                     &(struct addrinfo)
> +                     {
> +                       .ai_family = AF_INET,
> +                       .ai_socktype = SOCK_STREAM,
> +                     }, &ai);
> +  check_addrinfo ("an-1.ns2.ar1.example (AF_INET)", ai, ret,
> +                  "error: Name or service not known\n");
> +  queries = 0;
> +  ret = getaddrinfo ("an-1.ns2.ar1.example", "80",
> +                     &(struct addrinfo)
> +                     {
> +                       .ai_family = AF_INET6,
> +                       .ai_socktype = SOCK_STREAM,
> +                     }, &ai);
> +  check_addrinfo ("an-1.ns2.ar1.example (AF_INET6)", ai, ret,
> +                  "error: Name or service not known\n");
> +  queries = 0;
> +  ret = getaddrinfo ("an-1.ns2.ar1.example", "80",
> +                     &(struct addrinfo)
> +                     {
> +                       .ai_family = AF_UNSPEC,
> +                       .ai_socktype = SOCK_STREAM,
> +                     }, &ai);
> +  check_addrinfo ("an-1.ns2.ar1.example (AF_UNSPEC)", ai, ret,
> +                  "error: Name or service not known\n");
> +  queries = 0;
> +
> +  for (unsigned int mode = 0; mode < mode_count; ++mode)
> +    {
> +      printf ("info: mode=%d\n", mode);
> +      unsigned char *buf;
> +      int ret;
> +
> +      buf = malloc (512);
> +      ret = libresolv_query (mode, "an1.ns2.ar1.example", T_A, buf, 512);
> +      TEST_VERIFY_EXIT (ret > 0);
> +      check_dns_packet ("an1.ns2.ar1.example A", buf, ret,
> +                        "name: an1.ns2.ar1.example\n"
> +                        "address: 192.0.2.1\n");
> +      free (buf);
> +      queries = 0;
> +
> +      /* NODATA response for A.  */
> +      buf = malloc (512);
> +      errno = 0;
> +      ret = libresolv_query (mode, "an0.ns2.ar1.example", T_A, buf, 512);
> +      if (mode < first_send_mode)
> +        {
> +          TEST_COMPARE (ret, -1);
> +          TEST_COMPARE (errno, 0);
> +          TEST_COMPARE (h_errno, NO_ADDRESS);
> +        }
> +      else
> +        {
> +          TEST_VERIFY_EXIT (ret > 0);
> +          TEST_COMPARE (((HEADER *)buf)->rcode, 0);
> +          check_dns_packet ("an1.ns2.ar1.example A", buf, ret,
> +                            "name: an0.ns2.ar1.example\n");
> +        }
> +      free (buf);
> +      queries = 0;
> +
> +      /* NXDOMAIN response for A.  */
> +      buf = malloc (512);
> +      errno = 0;
> +      ret = libresolv_query (mode, "an-1.ns2.ar1.example", T_A, buf, 512);
> +      if (mode < first_send_mode)
> +        {
> +          TEST_COMPARE (ret, -1);
> +          TEST_COMPARE (errno, 0);
> +          TEST_COMPARE (h_errno, HOST_NOT_FOUND);
> +        }
> +      else
> +        {
> +          TEST_VERIFY_EXIT (ret > 0);
> +          TEST_COMPARE (((HEADER *)buf)->rcode, NXDOMAIN);
> +          check_dns_packet ("an1.ns2.ar1.example A", buf, ret,
> +                            "name: an-1.ns2.ar1.example\n");
> +        }
> +      free (buf);
> +      queries = 0;
> +
> +      /* NODATA response for AAAA.  */
> +      buf = malloc (512);
> +      errno = 0;
> +      ret = libresolv_query (mode, "an1.ns2.ar1.example", T_AAAA, buf, 512);
> +      if (mode < first_send_mode)
> +        {
> +          TEST_COMPARE (ret, -1);
> +          TEST_COMPARE (errno, 0);
> +          TEST_COMPARE (h_errno, NO_ADDRESS);
> +        }
> +      else
> +        {
> +          TEST_VERIFY_EXIT (ret > 0);
> +          TEST_COMPARE (((HEADER *)buf)->rcode, 0);
> +          check_dns_packet ("an1.ns2.ar1.example A", buf, ret,
> +                            "name: an1.ns2.ar1.example\n");
> +        }
> +      free (buf);
> +      queries = 0;
> +
> +      /* NODATA response for AAAA (original is already NODATA).  */
> +      buf = malloc (512);
> +      errno = 0;
> +      ret = libresolv_query (mode, "an0.ns2.ar1.example", T_AAAA, buf, 512);
> +      if (mode < first_send_mode)
> +        {
> +          TEST_COMPARE (ret, -1);
> +          TEST_COMPARE (errno, 0);
> +          TEST_COMPARE (h_errno, NO_ADDRESS);
> +        }
> +      else
> +        {
> +          TEST_VERIFY_EXIT (ret > 0);
> +          TEST_COMPARE (((HEADER *)buf)->rcode, 0);
> +          check_dns_packet ("an0.ns2.ar1.example A", buf, ret,
> +                            "name: an0.ns2.ar1.example\n");
> +        }
> +      free (buf);
> +      queries = 0;
> +
> +      /* NXDOMAIN response.  */
> +      buf = malloc (512);
> +      errno = 0;
> +      ret = libresolv_query (mode, "an-1.ns2.ar1.example", T_AAAA, buf, 512);
> +      if (mode < first_send_mode)
> +        {
> +          TEST_COMPARE (ret, -1);
> +          TEST_COMPARE (errno, 0);
> +          TEST_COMPARE (h_errno, HOST_NOT_FOUND);
> +        }
> +      else
> +        {
> +          TEST_VERIFY_EXIT (ret > 0);
> +          TEST_COMPARE (((HEADER *)buf)->rcode, NXDOMAIN);
> +          check_dns_packet ("an-1.ns2.ar1.example A", buf, ret,
> +                            "name: an-1.ns2.ar1.example\n");
> +        }
> +      free (buf);
> +      queries = 0;
> +    }
> +
> +  resolv_test_end (obj);
> +
> +  return 0;
> +}
> +
> +#include <support/test-driver.c>
> diff --git a/resolv/tst-resolv-res_init-skeleton.c b/resolv/tst-resolv-res_init-skeleton.c
> index cf81d1d162..b5749452ef 100644
> --- a/resolv/tst-resolv-res_init-skeleton.c
> +++ b/resolv/tst-resolv-res_init-skeleton.c
> @@ -128,6 +128,7 @@ print_resp (FILE *fp, res_state resp)
>          print_option_flag (fp, &options, RES_NOTLDQUERY, "no-tld-query");
>          print_option_flag (fp, &options, RES_NORELOAD, "no-reload");
>          print_option_flag (fp, &options, RES_TRUSTAD, "trust-ad");
> +        print_option_flag (fp, &options, RES_NOAAAA, "no-aaaa");

OK.

>          fputc ('\n', fp);
>          if (options != 0)
>            fprintf (fp, "; error: unresolved option bits: 0x%x\n", options);
> @@ -721,6 +722,15 @@ struct test_case test_cases[] =
>       "nameserver 192.0.2.1\n"
>       "; nameserver[0]: [192.0.2.1]:53\n"
>      },
> +    {.name = "no-aaaa flag",
> +     .conf = "options no-aaaa\n"
> +     "nameserver 192.0.2.1\n",
> +     .expected = "options no-aaaa\n"
> +     "search example.com\n"
> +     "; search[0]: example.com\n"
> +     "nameserver 192.0.2.1\n"
> +     "; nameserver[0]: [192.0.2.1]:53\n"
> +    },

OK.

>      { NULL }
>    };
>  
>
  

Patch

diff --git a/NEWS b/NEWS
index ad0c08d8ca..9e511cdc6a 100644
--- a/NEWS
+++ b/NEWS
@@ -20,6 +20,18 @@  Major new features:
   have been added.  The pidfd functionality provides access to a process
   while avoiding the issue of PID reuse on tranditional Unix systems.
 
+* The “no-aaaa” DNS stob resolver option has been added.  System
+  administrators can use it to suppress AAAA queries made by the stub
+  resolver, including AAAA lookups triggered by NSS-based interfaces
+  such as getaddrinfo.  Only DNS lookups are affected: IPv6 data in
+  /etc/hosts is still used, getaddrinfo with AI_PASSIVE will still
+  produce IPv6 addresses, and configured IPv6 name servers are still
+  used.  To produce correct Name Error (NXDOMAIN) results, AAAA queries
+  are translated to A queries.  The new resolver option is intended
+  preliminary for diagnostic purposes, to rule out that AAAA DNS queries
+  have adverse impact.  It is incompatible with EDNS0 usage and DNSSEC
+  validation by applications.
+
 Deprecated and removed features, and other changes affecting compatibility:
 
 * Support for prelink will be removed in the next release; this includes
diff --git a/resolv/Makefile b/resolv/Makefile
index 438672786f..5b15321f9b 100644
--- a/resolv/Makefile
+++ b/resolv/Makefile
@@ -51,6 +51,7 @@  routines := \
   nss_dns_functions \
   res-close \
   res-name-checking \
+  res-noaaaa \
   res-state \
   res_context_hostalias \
   res_enable_icmp \
@@ -92,6 +93,7 @@  tests += \
   tst-resolv-binary \
   tst-resolv-edns \
   tst-resolv-network \
+  tst-resolv-noaaaa \
   tst-resolv-nondecimal \
   tst-resolv-res_init-multi \
   tst-resolv-search \
@@ -265,6 +267,7 @@  $(objpfx)tst-resolv-res_init-multi: $(objpfx)libresolv.so \
   $(shared-thread-library)
 $(objpfx)tst-resolv-res_init-thread: $(objpfx)libresolv.so \
   $(shared-thread-library)
+$(objpfx)tst-resolv-noaaaa: $(objpfx)libresolv.so $(shared-thread-library)
 $(objpfx)tst-resolv-nondecimal: $(objpfx)libresolv.so $(shared-thread-library)
 $(objpfx)tst-resolv-qtypes: $(objpfx)libresolv.so $(shared-thread-library)
 $(objpfx)tst-resolv-rotate: $(objpfx)libresolv.so $(shared-thread-library)
diff --git a/resolv/nss_dns/dns-host.c b/resolv/nss_dns/dns-host.c
index 913a5cb82f..544cffbecd 100644
--- a/resolv/nss_dns/dns-host.c
+++ b/resolv/nss_dns/dns-host.c
@@ -124,6 +124,14 @@  static enum nss_status gaih_getanswer (const querybuf *answer1, int anslen1,
 				       char *buffer, size_t buflen,
 				       int *errnop, int *h_errnop,
 				       int32_t *ttlp);
+static enum nss_status gaih_getanswer_noaaaa (const querybuf *answer1,
+					      int anslen1,
+					      const char *qname,
+					      struct gaih_addrtuple **pat,
+					      char *buffer, size_t buflen,
+					      int *errnop, int *h_errnop,
+					      int32_t *ttlp);
+
 
 static enum nss_status gethostbyname3_context (struct resolv_context *ctx,
 					       const char *name, int af,
@@ -369,17 +377,31 @@  _nss_dns_gethostbyname4_r (const char *name, struct gaih_addrtuple **pat,
   int resplen2 = 0;
   int ans2p_malloced = 0;
 
+
   int olderr = errno;
-  int n = __res_context_search (ctx, name, C_IN, T_QUERY_A_AND_AAAA,
+  int n;
+
+  if ((ctx->resp->options & RES_NOAAAA) == 0)
+    {
+      n = __res_context_search (ctx, name, C_IN, T_QUERY_A_AND_AAAA,
 				host_buffer.buf->buf, 2048, &host_buffer.ptr,
 				&ans2p, &nans2p, &resplen2, &ans2p_malloced);
-  if (n >= 0)
-    {
-      status = gaih_getanswer (host_buffer.buf, n, (const querybuf *) ans2p,
-			       resplen2, name, pat, buffer, buflen,
-			       errnop, herrnop, ttlp);
+      if (n >= 0)
+	status = gaih_getanswer (host_buffer.buf, n, (const querybuf *) ans2p,
+				 resplen2, name, pat, buffer, buflen,
+				 errnop, herrnop, ttlp);
     }
   else
+    {
+      n = __res_context_search (ctx, name, C_IN, T_A,
+				host_buffer.buf->buf, 2048, NULL,
+				NULL, NULL, NULL, NULL);
+      if (n >= 0)
+	status = gaih_getanswer_noaaaa (host_buffer.buf, n,
+					name, pat, buffer, buflen,
+					errnop, herrnop, ttlp);
+    }
+  if (n < 0)
     {
       switch (errno)
 	{
@@ -1387,3 +1409,21 @@  gaih_getanswer (const querybuf *answer1, int anslen1, const querybuf *answer2,
 
   return status;
 }
+
+/* Variant of gaih_getanswer without a second (AAAA) response.  */
+static enum nss_status
+gaih_getanswer_noaaaa (const querybuf *answer1, int anslen1, const char *qname,
+		       struct gaih_addrtuple **pat,
+		       char *buffer, size_t buflen,
+		       int *errnop, int *h_errnop, int32_t *ttlp)
+{
+  int first = 1;
+
+  enum nss_status status = NSS_STATUS_NOTFOUND;
+  if (anslen1 > 0)
+    status = gaih_getanswer_slice (answer1, anslen1, qname,
+				   &pat, &buffer, &buflen,
+				   errnop, h_errnop, ttlp,
+				   &first);
+  return status;
+}
diff --git a/resolv/res-noaaaa.c b/resolv/res-noaaaa.c
new file mode 100644
index 0000000000..a71b34a19d
--- /dev/null
+++ b/resolv/res-noaaaa.c
@@ -0,0 +1,143 @@ 
+/* Implement suppression of AAAA queries.
+   Copyright (C) 2022 Free Software Foundation, Inc.
+   This file is part of the GNU C Library.
+
+   The GNU C Library is free software; you can redistribute it and/or
+   modify it under the terms of the GNU Lesser General Public
+   License as published by the Free Software Foundation; either
+   version 2.1 of the License, or (at your option) any later version.
+
+   The GNU C Library 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
+   Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public
+   License along with the GNU C Library; if not, see
+   <https://www.gnu.org/licenses/>.  */
+
+#include <resolv.h>
+#include <string.h>
+#include <resolv-internal.h>
+#include <resolv_context.h>
+#include <arpa/nameser.h>
+
+/* Returns true if the question type at P matches EXPECTED, and the
+   class is IN.  */
+static bool
+qtype_matches (const unsigned char *p, int expected)
+{
+  /* This assumes that T_A/C_IN constants are less than 256, which
+     they are.  */
+  return p[0] == 0 && p[1] == expected && p[2] == 0 && p[3] == C_IN;
+}
+
+/* Handle RES_NOAAAA translation of AAAA queries.  To produce a Name
+   Error (NXDOMAIN) repsonse for domain names that do not exist, it is
+   still necessary to send a query.  Using question type A is a
+   conservative choice.  In the returned answer, it is necessary to
+   switch back the question type to AAAA.  */
+bool
+__res_handle_no_aaaa (struct resolv_context *ctx,
+                      const unsigned char *buf, int buflen,
+                      unsigned char *ans, int anssiz, int *result)
+{
+  /* AAAA mode is not active, or the query looks invalid (will not be
+     able to be parsed).  */
+  if ((ctx->resp->options & RES_NOAAAA) == 0
+      || buflen <= sizeof (HEADER))
+    return false;
+
+  /* The replacement A query is produced here.  */
+  struct
+  {
+    HEADER header;
+    unsigned char question[NS_MAXCDNAME + 4];
+  } replacement;
+  memcpy (&replacement.header, buf, sizeof (replacement.header));
+
+  if (replacement.header.qr
+      || replacement.header.opcode != 0
+      || replacement.header.rcode != 0
+      || ntohs (replacement.header.qdcount) != 1
+      || ntohs (replacement.header.ancount) != 0
+      || ntohs (replacement.header.nscount) != 0)
+    /* Not a well-formed question.  Let the core resolver code produce
+       the proper error.  */
+    return false;
+
+  /* Disable EDNS0.  */
+  replacement.header.arcount = htons (0);
+
+  /* Extract the QNAME.  */
+  int ret = __ns_name_unpack (buf, buf + buflen, buf + sizeof (HEADER),
+                              replacement.question, NS_MAXCDNAME);
+  if (ret < 0)
+    /* Format error.  */
+    return false;
+
+  /* Compute the end of the question name.  */
+  const unsigned char *after_question = buf + sizeof (HEADER) + ret;
+
+  /* Check that we are dealing with an AAAA query.  */
+  if (buf + buflen - after_question < 4
+      || !qtype_matches (after_question, T_AAAA))
+    return false;
+
+  /* Find the place to store the type/class data in the replacement
+     query.  */
+  after_question = replacement.question;
+  /* This cannot fail because __ns_name_unpack above produced a valid
+     domain name.  */
+  (void) __ns_name_skip (&after_question, &replacement.question[NS_MAXCDNAME]);
+  unsigned char *start_of_query = (unsigned char *) &replacement;
+  const unsigned char *end_of_query = after_question + 4;
+
+  /* Produce an A/IN query.  */
+  {
+    unsigned char *p = (unsigned char *) after_question;
+    p[0] = 0;
+    p[1] = T_A;
+    p[2] = 0;
+    p[3] = C_IN;
+  }
+
+  /* Clear the output buffer, to avoid reading undefined data when
+     rewriting the result from A to AAAA.  */
+  memset (ans, 0, anssiz);
+
+  /* Always perform the message translation, independent of the error
+     code.  */
+  ret = __res_context_send (ctx,
+                            start_of_query, end_of_query - start_of_query,
+                            NULL, 0, ans, anssiz,
+                            NULL, NULL, NULL, NULL, NULL);
+
+  /* Patch in the AAAA question type if there is room and the A query
+     type was received.  */
+  after_question = ans + sizeof (HEADER);
+  if (__ns_name_skip (&after_question, ans + anssiz) == 0
+      && ans + anssiz - after_question >= 4
+      && qtype_matches (after_question, T_A))
+    {
+      ((unsigned char *) after_question)[1] = T_AAAA;
+
+      /* Create an aligned copy of the header.  Hide all data exception
+         the question from the response.  Put back the header.  There is
+         no need to change the response code.  The zero answer count turns
+         a positive response with data into a no-data response.  */
+      memcpy (&replacement.header, ans, sizeof (replacement.header));
+      replacement.header.ancount = htons (0);
+      replacement.header.nscount = htons (0);
+      replacement.header.arcount = htons (0);
+      memcpy (ans, &replacement.header, sizeof (replacement.header));
+
+      /* Truncate the reply.  */
+      if (ret <= 0)
+        *result = ret;
+      else
+        *result = after_question - ans + 4;
+    }
+
+  return true;
+}
diff --git a/resolv/res_debug.c b/resolv/res_debug.c
index 030df0aa90..b0fe69b1aa 100644
--- a/resolv/res_debug.c
+++ b/resolv/res_debug.c
@@ -613,6 +613,7 @@  p_option(u_long option) {
 	case RES_NOTLDQUERY:	return "no-tld-query";
 	case RES_NORELOAD:	return "no-reload";
 	case RES_TRUSTAD:	return "trust-ad";
+	case RES_NOAAAA:	return "no-aaaa";
 				/* XXX nonreentrant */
 	default:		sprintf(nbuf, "?0x%lx?", (u_long)option);
 				return (nbuf);
diff --git a/resolv/res_init.c b/resolv/res_init.c
index cbc16ba021..2c0bea658e 100644
--- a/resolv/res_init.c
+++ b/resolv/res_init.c
@@ -695,6 +695,7 @@  res_setoptions (struct resolv_conf_parser *parser, const char *options)
             { STRnLEN ("no-reload"), 0, RES_NORELOAD },
             { STRnLEN ("use-vc"), 0, RES_USEVC },
             { STRnLEN ("trust-ad"), 0, RES_TRUSTAD },
+            { STRnLEN ("no-aaaa"), 0, RES_NOAAAA },
           };
 #define noptions (sizeof (options) / sizeof (options[0]))
           for (int i = 0; i < noptions; ++i)
diff --git a/resolv/res_query.c b/resolv/res_query.c
index 3b5c604261..049de91b95 100644
--- a/resolv/res_query.c
+++ b/resolv/res_query.c
@@ -204,10 +204,26 @@  __res_context_query (struct resolv_context *ctx, const char *name,
 			free (buf);
 		return (n);
 	}
-	assert (answerp == NULL || (void *) *answerp == (void *) answer);
-	n = __res_context_send (ctx, query1, nquery1, query2, nquery2, answer,
-				anslen, answerp, answerp2, nanswerp2, resplen2,
-				answerp2_malloced);
+
+	/* Suppress AAAA lookups if required.  __res_handle_no_aaaa
+	   checks RES_NOAAAA first, so avoids parsing the
+	   just-generated query packet in most cases.  nss_dns avoids
+	   using T_QUERY_A_AND_AAAA in RES_NOAAAA mode, so there is no
+	   need to handle it here.  */
+	if (type == T_AAAA && __res_handle_no_aaaa (ctx, query1, nquery1,
+						    answer, anslen, &n))
+	  /* There must be no second query for AAAA queries.  The code
+	     below is still needed to translate NODATA responses.  */
+	  assert (query2 == NULL);
+	else
+	  {
+	    assert (answerp == NULL || (void *) *answerp == (void *) answer);
+	    n = __res_context_send (ctx, query1, nquery1, query2, nquery2,
+				    answer, anslen,
+				    answerp, answerp2, nanswerp2, resplen2,
+				    answerp2_malloced);
+	  }
+
 	if (use_malloc)
 		free (buf);
 	if (n < 0) {
diff --git a/resolv/res_send.c b/resolv/res_send.c
index d6c85fd7a2..6a08e729a4 100644
--- a/resolv/res_send.c
+++ b/resolv/res_send.c
@@ -438,8 +438,13 @@  context_send_common (struct resolv_context *ctx,
       RES_SET_H_ERRNO (&_res, NETDB_INTERNAL);
       return -1;
     }
-  int result = __res_context_send (ctx, buf, buflen, NULL, 0, ans, anssiz,
-				   NULL, NULL, NULL, NULL, NULL);
+
+  int result;
+  if (__res_handle_no_aaaa (ctx, buf, buflen, ans, anssiz, &result))
+    return result;
+
+  result = __res_context_send (ctx, buf, buflen, NULL, 0, ans, anssiz,
+			       NULL, NULL, NULL, NULL, NULL);
   __resolv_context_put (ctx);
   return result;
 }
diff --git a/resolv/resolv-internal.h b/resolv/resolv-internal.h
index 9d2e832d68..774dab0e56 100644
--- a/resolv/resolv-internal.h
+++ b/resolv/resolv-internal.h
@@ -85,6 +85,14 @@  int __res_context_send (struct resolv_context *, const unsigned char *, int,
                         int *, int *, int *);
 libc_hidden_proto (__res_context_send)
 
+/* Return true if the query has been handled in RES_NOAAAA mode.  For
+   that, RES_NOAAAA must be activ, and the question type must be AAAA.
+   The caller is expected to return *RESULT as the return value.  */
+bool __res_handle_no_aaaa (struct resolv_context *ctx,
+                           const unsigned char *buf, int buflen,
+                           unsigned char *ans, int anssiz, int *result)
+  attribute_hidden;
+
 /* Internal function similar to res_hostalias.  */
 const char *__res_context_hostalias (struct resolv_context *,
                                      const char *, char *, size_t);
diff --git a/resolv/resolv.h b/resolv/resolv.h
index e7c8d44645..3a79ffea57 100644
--- a/resolv/resolv.h
+++ b/resolv/resolv.h
@@ -132,6 +132,7 @@  struct res_sym {
 					   as a TLD.  */
 #define RES_NORELOAD    0x02000000 /* No automatic configuration reload.  */
 #define RES_TRUSTAD     0x04000000 /* Request AD bit, keep it in responses.  */
+#define RES_NOAAAA      0x08000000 /* Suppress AAAA queries.  */
 
 #define RES_DEFAULT	(RES_RECURSE|RES_DEFNAMES|RES_DNSRCH)
 
diff --git a/resolv/tst-resolv-noaaaa.c b/resolv/tst-resolv-noaaaa.c
new file mode 100644
index 0000000000..9611d80720
--- /dev/null
+++ b/resolv/tst-resolv-noaaaa.c
@@ -0,0 +1,483 @@ 
+/* Test the RES_NOAAAA resolver option.
+   Copyright (C) 2022 Free Software Foundation, Inc.
+   This file is part of the GNU C Library.
+
+   The GNU C Library is free software; you can redistribute it and/or
+   modify it under the terms of the GNU Lesser General Public
+   License as published by the Free Software Foundation; either
+   version 2.1 of the License, or (at your option) any later version.
+
+   The GNU C Library 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
+   Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public
+   License along with the GNU C Library; if not, see
+   <https://www.gnu.org/licenses/>.  */
+
+#include <errno.h>
+#include <netdb.h>
+#include <resolv.h>
+#include <stdlib.h>
+#include <support/check.h>
+#include <support/check_nss.h>
+#include <support/resolv_test.h>
+#include <support/support.h>
+
+/* Used to keep track of the number of queries.  */
+static volatile unsigned int queries;
+
+static void
+response (const struct resolv_response_context *ctx,
+          struct resolv_response_builder *b,
+          const char *qname, uint16_t qclass, uint16_t qtype)
+{
+  /* Each test should only send one query.  */
+  ++queries;
+  TEST_COMPARE (queries, 1);
+
+  /* AAAA queries are supposedto be disabled.  */
+  TEST_VERIFY (qtype != T_AAAA);
+  TEST_COMPARE (qclass, C_IN);
+
+  /* The only other query type besides A is PTR.  */
+  if (qtype != T_A)
+    TEST_COMPARE (qtype, T_PTR);
+
+  int an, ns, ar;
+  char *tail;
+  if (sscanf (qname, "an%d.ns%d.ar%d.%ms", &an, &ns, &ar, &tail) != 4)
+    FAIL_EXIT1 ("invalid QNAME: %s\n", qname);
+  TEST_COMPARE_STRING (tail, "example");
+  free (tail);
+
+  if (an < 0 || ns < 0 || ar < 0)
+    {
+      struct resolv_response_flags flags = { .rcode = NXDOMAIN, };
+      resolv_response_init (b, flags);
+      resolv_response_add_question (b, qname, qclass, qtype);
+      return;
+    }
+
+  struct resolv_response_flags flags = {};
+  resolv_response_init (b, flags);
+  resolv_response_add_question (b, qname, qclass, qtype);
+
+  resolv_response_section (b, ns_s_an);
+  for (int i = 0; i < an; ++i)
+    {
+      resolv_response_open_record (b, qname, qclass, qtype, 60);
+      switch (qtype)
+        {
+        case T_A:
+          char ipv4[4] = {192, 0, 2, i + 1};
+          resolv_response_add_data (b, &ipv4, sizeof (ipv4));
+          break;
+
+        case T_PTR:
+          char *name = xasprintf ("ptr-%d", i);
+          resolv_response_add_name (b, name);
+          free (name);
+          break;
+        }
+      resolv_response_close_record (b);
+    }
+
+  resolv_response_section (b, ns_s_ns);
+  for (int i = 0; i < ns; ++i)
+    {
+      resolv_response_open_record (b, qname, qclass, T_NS, 60);
+      char *name = xasprintf ("ns%d.example.net", i);
+      resolv_response_add_name (b, name);
+      free (name);
+      resolv_response_close_record (b);
+    }
+
+  resolv_response_section (b, ns_s_ar);
+  int addr = 1;
+  for (int i = 0; i < ns; ++i)
+    {
+      char *name = xasprintf ("ns%d.example.net", i);
+      for (int j = 0; j < ar; ++j)
+        {
+          resolv_response_open_record (b, name, qclass, T_A, 60);
+          char ipv4[4] = {192, 0, 2, addr};
+          resolv_response_add_data (b, &ipv4, sizeof (ipv4));
+          resolv_response_close_record (b);
+
+          resolv_response_open_record (b, name, qclass, T_AAAA, 60);
+          char ipv6[16]
+            = {0x20, 0x01, 0xd, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, addr};
+          resolv_response_add_data (b, &ipv6, sizeof (ipv6));
+          resolv_response_close_record (b);
+
+          ++addr;
+        }
+      free (name);
+    }
+}
+
+/* Number of modes.  Lowest bit encodes *n* function vs implicit _res
+   argument.  The mode numbers themselves are arbitrary.  */
+enum { mode_count = 8 };
+
+/* res_send-like modes do not perform error translation.  */
+enum { first_send_mode = 6 };
+
+static int
+libresolv_query (unsigned int mode, const char *qname, uint16_t qtype,
+                 unsigned char *buf, size_t buflen)
+{
+  int saved_errno = errno;
+
+  TEST_VERIFY_EXIT (mode < mode_count);
+
+  switch (mode)
+    {
+    case 0:
+      return res_query (qname, C_IN, qtype, buf, buflen);
+    case 1:
+      return res_nquery (&_res, qname, C_IN, qtype, buf, buflen);
+    case 2:
+      return res_search (qname, C_IN, qtype, buf, buflen);
+    case 3:
+      return res_nsearch (&_res, qname, C_IN, qtype, buf, buflen);
+    case 4:
+      return res_querydomain (qname, "", C_IN, qtype, buf, buflen);
+    case 5:
+      return res_nquerydomain (&_res, qname, "", C_IN, qtype, buf, buflen);
+    case 6:
+      {
+        unsigned char querybuf[512];
+        int ret = res_mkquery (QUERY, qname, C_IN, qtype,
+                               NULL, 0, NULL, querybuf, sizeof (querybuf));
+        TEST_VERIFY_EXIT (ret > 0);
+        errno = saved_errno;
+        return res_send (querybuf, ret, buf, buflen);
+      }
+    case 7:
+      {
+        unsigned char querybuf[512];
+        int ret = res_nmkquery (&_res, QUERY, qname, C_IN, qtype,
+                                NULL, 0, NULL, querybuf, sizeof (querybuf));
+        TEST_VERIFY_EXIT (ret > 0);
+        errno = saved_errno;
+        return res_nsend (&_res, querybuf, ret, buf, buflen);
+      }
+    }
+  __builtin_unreachable ();
+}
+
+static int
+do_test (void)
+{
+  struct resolv_test *obj = resolv_test_start
+    ((struct resolv_redirect_config)
+     {
+       .response_callback = response
+     });
+
+  _res.options |= RES_NOAAAA;
+
+  check_hostent ("an1.ns2.ar1.example",
+                 gethostbyname ("an1.ns2.ar1.example"),
+                 "name: an1.ns2.ar1.example\n"
+                 "address: 192.0.2.1\n");
+  queries = 0;
+  check_hostent ("an0.ns2.ar1.example",
+                 gethostbyname ("an0.ns2.ar1.example"),
+                 "error: NO_ADDRESS\n");
+  queries = 0;
+  check_hostent ("an-1.ns2.ar1.example",
+                 gethostbyname ("an-1.ns2.ar1.example"),
+                 "error: HOST_NOT_FOUND\n");
+  queries = 0;
+
+  check_hostent ("an1.ns2.ar1.example AF_INET",
+                 gethostbyname2 ("an1.ns2.ar1.example", AF_INET),
+                 "name: an1.ns2.ar1.example\n"
+                 "address: 192.0.2.1\n");
+  queries = 0;
+  check_hostent ("an0.ns2.ar1.example AF_INET",
+                 gethostbyname2 ("an0.ns2.ar1.example", AF_INET),
+                 "error: NO_ADDRESS\n");
+  queries = 0;
+  check_hostent ("an-1.ns2.ar1.example AF_INET",
+                 gethostbyname2 ("an-1.ns2.ar1.example", AF_INET),
+                 "error: HOST_NOT_FOUND\n");
+  queries = 0;
+
+  check_hostent ("an1.ns2.ar1.example AF_INET6",
+                 gethostbyname2 ("an1.ns2.ar1.example", AF_INET6),
+                 "error: NO_ADDRESS\n");
+  queries = 0;
+  check_hostent ("an0.ns2.ar1.example AF_INET6",
+                 gethostbyname2 ("an0.ns2.ar1.example", AF_INET6),
+                 "error: NO_ADDRESS\n");
+  queries = 0;
+  check_hostent ("an-1.ns2.ar1.example AF_INET6",
+                 gethostbyname2 ("an-1.ns2.ar1.example", AF_INET6),
+                 "error: HOST_NOT_FOUND\n");
+  queries = 0;
+
+  /* Multiple addresses.  */
+  check_hostent ("an2.ns0.ar0.example",
+                 gethostbyname ("an2.ns0.ar0.example"),
+                 "name: an2.ns0.ar0.example\n"
+                 "address: 192.0.2.1\n"
+                 "address: 192.0.2.2\n");
+  queries = 0;
+  check_hostent ("an2.ns0.ar0.example AF_INET6",
+                 gethostbyname2 ("an2.ns0.ar0.example", AF_INET6),
+                 "error: NO_ADDRESS\n");
+  queries = 0;
+
+  /* getaddrinfo checks with one address.  */
+  struct addrinfo *ai;
+  int ret;
+  ret = getaddrinfo ("an1.ns2.ar1.example", "80",
+                     &(struct addrinfo)
+                     {
+                       .ai_family = AF_INET,
+                       .ai_socktype = SOCK_STREAM,
+                     }, &ai);
+  check_addrinfo ("an1.ns2.ar1.example (AF_INET)", ai, ret,
+                  "address: STREAM/TCP 192.0.2.1 80\n");
+  freeaddrinfo (ai);
+  queries = 0;
+  ret = getaddrinfo ("an1.ns2.ar1.example", "80",
+                     &(struct addrinfo)
+                     {
+                       .ai_family = AF_INET6,
+                       .ai_socktype = SOCK_STREAM,
+                     }, &ai);
+  check_addrinfo ("an1.ns2.ar1.example (AF_INET6)", ai, ret,
+                  "error: No address associated with hostname\n");
+  queries = 0;
+  ret = getaddrinfo ("an1.ns2.ar1.example", "80",
+                     &(struct addrinfo)
+                     {
+                       .ai_family = AF_UNSPEC,
+                       .ai_socktype = SOCK_STREAM,
+                     }, &ai);
+  check_addrinfo ("an1.ns2.ar1.example (AF_UNSPEC)", ai, ret,
+                  "address: STREAM/TCP 192.0.2.1 80\n");
+  freeaddrinfo (ai);
+  queries = 0;
+
+  /* getaddrinfo checks with three addresses.  */
+  ret = getaddrinfo ("an3.ns2.ar1.example", "80",
+                     &(struct addrinfo)
+                     {
+                       .ai_family = AF_INET,
+                       .ai_socktype = SOCK_STREAM,
+                     }, &ai);
+  check_addrinfo ("an3.ns2.ar1.example (AF_INET)", ai, ret,
+                  "address: STREAM/TCP 192.0.2.1 80\n"
+                  "address: STREAM/TCP 192.0.2.2 80\n"
+                  "address: STREAM/TCP 192.0.2.3 80\n");
+  freeaddrinfo (ai);
+  queries = 0;
+  ret = getaddrinfo ("an3.ns2.ar1.example", "80",
+                     &(struct addrinfo)
+                     {
+                       .ai_family = AF_INET6,
+                       .ai_socktype = SOCK_STREAM,
+                     }, &ai);
+  check_addrinfo ("an3.ns2.ar1.example (AF_INET6)", ai, ret,
+                  "error: No address associated with hostname\n");
+  queries = 0;
+  ret = getaddrinfo ("an3.ns2.ar1.example", "80",
+                     &(struct addrinfo)
+                     {
+                       .ai_family = AF_UNSPEC,
+                       .ai_socktype = SOCK_STREAM,
+                     }, &ai);
+  check_addrinfo ("an3.ns2.ar1.example (AF_UNSPEC)", ai, ret,
+                  "address: STREAM/TCP 192.0.2.1 80\n"
+                  "address: STREAM/TCP 192.0.2.2 80\n"
+                  "address: STREAM/TCP 192.0.2.3 80\n");
+  freeaddrinfo (ai);
+  queries = 0;
+
+  /* getaddrinfo checks with no address.  */
+  ret = getaddrinfo ("an0.ns2.ar1.example", "80",
+                     &(struct addrinfo)
+                     {
+                       .ai_family = AF_INET,
+                       .ai_socktype = SOCK_STREAM,
+                     }, &ai);
+  check_addrinfo ("an0.ns2.ar1.example (AF_INET)", ai, ret,
+                  "error: No address associated with hostname\n");
+  queries = 0;
+  ret = getaddrinfo ("an0.ns2.ar1.example", "80",
+                     &(struct addrinfo)
+                     {
+                       .ai_family = AF_INET6,
+                       .ai_socktype = SOCK_STREAM,
+                     }, &ai);
+  check_addrinfo ("an0.ns2.ar1.example (AF_INET6)", ai, ret,
+                  "error: No address associated with hostname\n");
+  queries = 0;
+  ret = getaddrinfo ("an0.ns2.ar1.example", "80",
+                     &(struct addrinfo)
+                     {
+                       .ai_family = AF_UNSPEC,
+                       .ai_socktype = SOCK_STREAM,
+                     }, &ai);
+  check_addrinfo ("an-1.ns2.ar1.example (AF_UNSPEC)", ai, ret,
+                  "error: No address associated with hostname\n");
+  queries = 0;
+
+  /* getaddrinfo checks with NXDOMAIN.  */
+  ret = getaddrinfo ("an-1.ns2.ar1.example", "80",
+                     &(struct addrinfo)
+                     {
+                       .ai_family = AF_INET,
+                       .ai_socktype = SOCK_STREAM,
+                     }, &ai);
+  check_addrinfo ("an-1.ns2.ar1.example (AF_INET)", ai, ret,
+                  "error: Name or service not known\n");
+  queries = 0;
+  ret = getaddrinfo ("an-1.ns2.ar1.example", "80",
+                     &(struct addrinfo)
+                     {
+                       .ai_family = AF_INET6,
+                       .ai_socktype = SOCK_STREAM,
+                     }, &ai);
+  check_addrinfo ("an-1.ns2.ar1.example (AF_INET6)", ai, ret,
+                  "error: Name or service not known\n");
+  queries = 0;
+  ret = getaddrinfo ("an-1.ns2.ar1.example", "80",
+                     &(struct addrinfo)
+                     {
+                       .ai_family = AF_UNSPEC,
+                       .ai_socktype = SOCK_STREAM,
+                     }, &ai);
+  check_addrinfo ("an-1.ns2.ar1.example (AF_UNSPEC)", ai, ret,
+                  "error: Name or service not known\n");
+  queries = 0;
+
+  for (unsigned int mode = 0; mode < mode_count; ++mode)
+    {
+      printf ("info: mode=%d\n", mode);
+      unsigned char *buf;
+      int ret;
+
+      buf = malloc (512);
+      ret = libresolv_query (mode, "an1.ns2.ar1.example", T_A, buf, 512);
+      TEST_VERIFY_EXIT (ret > 0);
+      check_dns_packet ("an1.ns2.ar1.example A", buf, ret,
+                        "name: an1.ns2.ar1.example\n"
+                        "address: 192.0.2.1\n");
+      free (buf);
+      queries = 0;
+
+      /* NODATA response for A.  */
+      buf = malloc (512);
+      errno = 0;
+      ret = libresolv_query (mode, "an0.ns2.ar1.example", T_A, buf, 512);
+      if (mode < first_send_mode)
+        {
+          TEST_COMPARE (ret, -1);
+          TEST_COMPARE (errno, 0);
+          TEST_COMPARE (h_errno, NO_ADDRESS);
+        }
+      else
+        {
+          TEST_VERIFY_EXIT (ret > 0);
+          TEST_COMPARE (((HEADER *)buf)->rcode, 0);
+          check_dns_packet ("an1.ns2.ar1.example A", buf, ret,
+                            "name: an0.ns2.ar1.example\n");
+        }
+      free (buf);
+      queries = 0;
+
+      /* NXDOMAIN response for A.  */
+      buf = malloc (512);
+      errno = 0;
+      ret = libresolv_query (mode, "an-1.ns2.ar1.example", T_A, buf, 512);
+      if (mode < first_send_mode)
+        {
+          TEST_COMPARE (ret, -1);
+          TEST_COMPARE (errno, 0);
+          TEST_COMPARE (h_errno, HOST_NOT_FOUND);
+        }
+      else
+        {
+          TEST_VERIFY_EXIT (ret > 0);
+          TEST_COMPARE (((HEADER *)buf)->rcode, NXDOMAIN);
+          check_dns_packet ("an1.ns2.ar1.example A", buf, ret,
+                            "name: an-1.ns2.ar1.example\n");
+        }
+      free (buf);
+      queries = 0;
+
+      /* NODATA response for AAAA.  */
+      buf = malloc (512);
+      errno = 0;
+      ret = libresolv_query (mode, "an1.ns2.ar1.example", T_AAAA, buf, 512);
+      if (mode < first_send_mode)
+        {
+          TEST_COMPARE (ret, -1);
+          TEST_COMPARE (errno, 0);
+          TEST_COMPARE (h_errno, NO_ADDRESS);
+        }
+      else
+        {
+          TEST_VERIFY_EXIT (ret > 0);
+          TEST_COMPARE (((HEADER *)buf)->rcode, 0);
+          check_dns_packet ("an1.ns2.ar1.example A", buf, ret,
+                            "name: an1.ns2.ar1.example\n");
+        }
+      free (buf);
+      queries = 0;
+
+      /* NODATA response for AAAA (original is already NODATA).  */
+      buf = malloc (512);
+      errno = 0;
+      ret = libresolv_query (mode, "an0.ns2.ar1.example", T_AAAA, buf, 512);
+      if (mode < first_send_mode)
+        {
+          TEST_COMPARE (ret, -1);
+          TEST_COMPARE (errno, 0);
+          TEST_COMPARE (h_errno, NO_ADDRESS);
+        }
+      else
+        {
+          TEST_VERIFY_EXIT (ret > 0);
+          TEST_COMPARE (((HEADER *)buf)->rcode, 0);
+          check_dns_packet ("an0.ns2.ar1.example A", buf, ret,
+                            "name: an0.ns2.ar1.example\n");
+        }
+      free (buf);
+      queries = 0;
+
+      /* NXDOMAIN response.  */
+      buf = malloc (512);
+      errno = 0;
+      ret = libresolv_query (mode, "an-1.ns2.ar1.example", T_AAAA, buf, 512);
+      if (mode < first_send_mode)
+        {
+          TEST_COMPARE (ret, -1);
+          TEST_COMPARE (errno, 0);
+          TEST_COMPARE (h_errno, HOST_NOT_FOUND);
+        }
+      else
+        {
+          TEST_VERIFY_EXIT (ret > 0);
+          TEST_COMPARE (((HEADER *)buf)->rcode, NXDOMAIN);
+          check_dns_packet ("an-1.ns2.ar1.example A", buf, ret,
+                            "name: an-1.ns2.ar1.example\n");
+        }
+      free (buf);
+      queries = 0;
+    }
+
+  resolv_test_end (obj);
+
+  return 0;
+}
+
+#include <support/test-driver.c>
diff --git a/resolv/tst-resolv-res_init-skeleton.c b/resolv/tst-resolv-res_init-skeleton.c
index cf81d1d162..b5749452ef 100644
--- a/resolv/tst-resolv-res_init-skeleton.c
+++ b/resolv/tst-resolv-res_init-skeleton.c
@@ -128,6 +128,7 @@  print_resp (FILE *fp, res_state resp)
         print_option_flag (fp, &options, RES_NOTLDQUERY, "no-tld-query");
         print_option_flag (fp, &options, RES_NORELOAD, "no-reload");
         print_option_flag (fp, &options, RES_TRUSTAD, "trust-ad");
+        print_option_flag (fp, &options, RES_NOAAAA, "no-aaaa");
         fputc ('\n', fp);
         if (options != 0)
           fprintf (fp, "; error: unresolved option bits: 0x%x\n", options);
@@ -721,6 +722,15 @@  struct test_case test_cases[] =
      "nameserver 192.0.2.1\n"
      "; nameserver[0]: [192.0.2.1]:53\n"
     },
+    {.name = "no-aaaa flag",
+     .conf = "options no-aaaa\n"
+     "nameserver 192.0.2.1\n",
+     .expected = "options no-aaaa\n"
+     "search example.com\n"
+     "; search[0]: example.com\n"
+     "nameserver 192.0.2.1\n"
+     "; nameserver[0]: [192.0.2.1]:53\n"
+    },
     { NULL }
   };