[Dnsmasq-discuss] [PATCH] Answer queued TCP queries after a stale answer

Simon Kelley simon at thekelleys.org.uk
Mon Oct 19 23:01:47 UTC 2026


This looks sensible.


The motivation for the existing behaviour is that I expected that the 
client would open a new TCP connection when it got the EOF, and this is 
faster, since the second query could proceed in parallel with the 
stale-cache refresh, rather then blocking until after the stale-cache 
refresh completes.

Given the rather dubious glibc behaviour, then this patch is a better 
solution.

patch applied.


Simon.

On 13/10/2026 16:00, Dominik Derigs wrote:
> From: DL6ER <dl6er at dl6er.de>
> 
> When `tcp_request()` answers from stale cache data, it closes the
> connection right away, so that the child process can refresh the name
> and exit. A query the client has already sent on that connection is
> never read, and its answer is lost.
> 
> glibc runs into this: after a truncated UDP reply, it sends the A and
> AAAA queries together on one TCP connection, gets EOF after the first
> answer and fails the lookup without retrying the second query. In a
> test with both answers expired, 7 of 20 such lookups lost the second
> answer.
> 
> Close the connection only if the client has not sent anything else
> yet. Otherwise, refresh the name and go on answering the queries on the
> connection, as for any other forwarded query.
> 
> Signed-off-by: DL6ER <dl6er at dl6er.de>
> ---
> Hi Simon,
> 
> with --use-stale-cache, tcp_request() closes the connection right after
> a stale answer, so a query the client has already queued on it is lost.
> glibc's A+AAAA retry over TCP after a truncated UDP reply runs into this
> and fails the lookup.
> 
> The patch keeps the connection open only when more data is already
> waiting (MSG_PEEK), refreshes the name and then goes on answering. An
> idle connection is still closed right away, as before.
> 
> Reproducer, loopback only: an upstream dnsmasq on port 5454 with --host-
> record=stale.test,192.0.2.1,2001:db8::1 --local-ttl=1, and the dnsmasq
> under test with --server=127.0.0.1#5454 --use-stale-cache. Query A and
> AAAA once over UDP, wait 1.5 s, then send both queries in one write on a
> TCP connection. Both answers arrived in 13 of 20 trials before and in 20
> of 20 with the patch.
> 
> Downstream report: https://github.com/pi-hole/FTL/issues/3167
> 
> Best regards,
> Dominik
> 
>   src/forward.c | 30 +++++++++++++++++++++++-------
>   1 file changed, 23 insertions(+), 7 deletions(-)
> 
> diff --git a/src/forward.c b/src/forward.c
> index f736c30..dae9a4f 100644
> --- a/src/forward.c
> +++ b/src/forward.c
> @@ -2406,7 +2406,7 @@ void tcp_request(int confd, time_t now, struct iovec *bigbuff,
>     unsigned char *pheader;
>     unsigned int mark = 0;
>     int have_mark = 0;
> -  int first, last, filtered, do_stale = 0;
> +  int first, last, filtered, do_stale = 0, closed = 0;
>     struct iovec out_iov[2];
>     
>     bigbuff->iov_base = NULL;
> @@ -2742,8 +2742,16 @@ void tcp_request(int confd, time_t now, struct iovec *bigbuff,
>   	    }
>   	}
>   
> +      /* Fresh data is in the cache now. Go on with the queries the client
> +	 had already sent, if the connection is still open. */
>         if (do_stale)
> -	break;
> +	{
> +	  if (closed)
> +	    break;
> +	  do_stale = 0;
> +	  daemon->log_source_addr = &peer_addr;
> +	  continue;
> +	}
>         
>         /* In case of local answer or no connections made. */
>         if (m == 0)
> @@ -2792,19 +2800,27 @@ void tcp_request(int confd, time_t now, struct iovec *bigbuff,
>         /* If we answered with stale data, this process will now try and get fresh data into
>   	 the cache and cannot therefore accept new queries. Close the incoming
>   	 connection to signal that to the client. Then set do_stale and loop round
> -	 once more to try and get fresh data, after which we exit. */
> +	 once more to try and get fresh data, after which we exit. A client which
> +	 has already sent its next query is waiting for the answer and would lose
> +	 it, so keep the connection open then and answer it after the refresh. */
>         if (stale)
>   	{
> -	  shutdown(confd, SHUT_RDWR);
> -	  close(confd);
> +	  char c;
> +	
> +	  if (recv(confd, &c, 1, MSG_PEEK | MSG_DONTWAIT) != 1)
> +	    {
> +	      shutdown(confd, SHUT_RDWR);
> +	      close(confd);
> +	      closed = 1;
> +	    }
>   	  do_stale = 1;
>   	  /* Don't mark the query with the source when refreshing stale data. */
>   	  daemon->log_source_addr = NULL;
>   	}
>       }
>     
> -/* If we ran once to get fresh data, confd is already closed. */
> -  if (!do_stale)
> +/* If we ran once to get fresh data, confd may be closed already. */
> +  if (!closed)
>       {
>         shutdown(confd, SHUT_RDWR);
>         close(confd);




More information about the Dnsmasq-discuss mailing list