<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:w="urn:schemas-microsoft-com:office:word" xmlns:m="http://schemas.microsoft.com/office/2004/12/omml" xmlns="http://www.w3.org/TR/REC-html40"><head><meta http-equiv=Content-Type content="text/html; charset=utf-8"><meta name=Generator content="Microsoft Word 15 (filtered medium)"><style><!--
/* Font Definitions */
@font-face
{font-family:"Cambria Math";
panose-1:2 4 5 3 5 4 6 3 2 4;}
@font-face
{font-family:DengXian;
panose-1:2 1 6 0 3 1 1 1 1 1;}
@font-face
{font-family:DengXian;
panose-1:2 1 6 0 3 1 1 1 1 1;}
/* Style Definitions */
p.MsoNormal, li.MsoNormal, div.MsoNormal
{margin:0cm;
text-align:justify;
text-justify:inter-ideograph;
font-size:10.5pt;
font-family:DengXian;}
.MsoChpDefault
{mso-style-type:export-only;}
/* Page Definitions */
@page WordSection1
{size:612.0pt 792.0pt;
margin:72.0pt 90.0pt 72.0pt 90.0pt;}
div.WordSection1
{page:WordSection1;}
--></style></head><body lang=ZH-CN link=blue vlink="#954F72" style='word-wrap:break-word'><div class=WordSection1><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>Hi Simon,<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'><o:p> </o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>I'd like to submit a patch to fix an EADDRINUSE issue when dnsmasq is<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>running with --bind-dynamic (OPT_CLEVERBIND) and a network interface is<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>removed and re-added while a TCP child process is still alive.<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'><o:p> </o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>== Bug description ==<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'><o:p> </o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>When dnsmasq forks a child process to handle a TCP DNS query<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>(do_tcp_connection) or a DNSSEC UDP-to-TCP fallback (swap_to_tcp), the<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>child inherits all of the parent's listening socket file descriptors.<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>These sockets are never used by the child, but the kernel keeps the<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>underlying socket objects alive as long as any process holds a reference.<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'><o:p> </o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>If a network interface goes away and comes back while the child is still<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>running (waiting for a slow upstream response, for example), the parent<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>calls release_listener() which closes its own fd, then<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>create_bound_listeners() tries to bind() a new socket to the same<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>address:port. This bind() fails with EADDRINUSE because the child's<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>inherited copy of the old listening socket still exists in the kernel.<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'><o:p> </o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>SO_REUSEADDR does not help here — it only permits binding over sockets<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>in TIME_WAIT state, not over a socket that is still in LISTEN state<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>(held open by the child).<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'><o:p> </o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>SO_REUSEPORT would allow the bind() to succeed, but it is not a suitable<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>fix: with SO_REUSEPORT the kernel load-balances incoming connections<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>across all sockets bound to the same address. The child process never<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>calls accept() on the inherited listening socket, so any connection<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>dispatched there is silently black-holed until the child exits.<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'><o:p> </o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>== When does this happen ==<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'><o:p> </o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>The race window is open for the entire lifetime of the TCP child process<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>(bounded by CHILD_LIFETIME). It is most likely to trigger when:<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'><o:p> </o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'> 1. The upstream DNS server is slow or unresponsive, so the child<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'> process lives for a long time waiting for a reply.<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'> 2. The network environment is dynamic — interfaces are frequently<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'> removed and re-created (e.g. USB NIC unplug/replug, mobile data<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'> PDN reconnect, Android tethering toggle).<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'><o:p> </o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>Both conditions together (slow upstream + dynamic interfaces) make this<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>reliably reproducible.<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'><o:p> </o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>== Fix ==<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'><o:p> </o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>Close all inherited listener fds in the child immediately after fork,<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>in both do_tcp_connection() and swap_to_tcp(). This mirrors the<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>existing pattern of closing the netlink fd in the child.<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'><o:p> </o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>== How to test ==<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'><o:p> </o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>A simple test using /proc/<pid>/fd to verify the fix:<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'><o:p> </o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'> 1. Start a fake upstream that accepts connections but never replies:<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'><o:p> </o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'> nc -l -k -s 127.0.0.1 -p 15354 &<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'><o:p> </o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'> 2. Start dnsmasq with --bind-dynamic pointing to the fake upstream:<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'><o:p> </o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'> dnsmasq --keep-in-foreground --no-resolv \<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'> --server=127.0.0.1#15354 \<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'> --listen-address=127.0.0.1 --port=15353 \<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'> --log-queries --log-facility=-<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'><o:p> </o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'> 3. Send a TCP query to trigger a fork:<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'><o:p> </o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'> dig @127.0.0.1 -p 15353 +tcp example.com<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'><o:p> </o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'> 4. Find the child process and inspect its file descriptors:<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'><o:p> </o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'> PARENT=$(pgrep -f "dnsmasq.*15353" | head -1)<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'> CHILD=$(pgrep -P $PARENT)<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'> ls -l /proc/$PARENT/fd | grep socket<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'> ls -l /proc/$CHILD/fd | grep socket<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'><o:p> </o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'> Without the fix: the child holds copies of all parent listening<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'> socket inodes (EADDRINUSE will occur on rebind).<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'><o:p> </o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'> With the fix: the child only holds its own connection socket and<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'> the pipe to parent — no listening socket inodes are shared.<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'><o:p> </o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>---<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'> src/dnsmasq.c | 21 ++++++++++++++++++++-<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'> 1 file changed, 20 insertions(+), 1 deletion(-)<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'><o:p> </o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>diff --git a/src/dnsmasq.c b/src/dnsmasq.c<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>index adaa692..9af3e09 100644<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>--- a/src/dnsmasq.c<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>+++ b/src/dnsmasq.c<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>@@ -2015,7 +2015,8 @@ static void do_tcp_connection(struct listener *listener, time_t now, int slot)<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'> pid_t p;<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'> union mysockaddr tcp_addr;<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'> socklen_t tcp_len = sizeof(union mysockaddr);<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>- struct server *s;<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>+ struct server *s;<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>+ struct listener *l;<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'> int flags, auth_dns = 0;<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'> struct in_addr netmask;<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'> int pipefd[2];<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>@@ -2186,6 +2187,16 @@ static void do_tcp_connection(struct listener *listener, time_t now, int slot)<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'> alarm(CHILD_LIFETIME);<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'> close(pipefd[0]); /* close read end in child. */<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'> daemon->pipe_to_parent = pipefd[1];<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>+<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>+ /* Close inherited listening sockets in the child process.<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>+ These are not needed here and holding them prevents the<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>+ parent from re-binding if an interface is removed and<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>+ re-added (the child's copy causes EADDRINUSE). */<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>+ for (l = daemon->listeners; l; l = l->next)<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>+ {<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>+ if (l->fd != -1) close(l->fd);<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>+ if (l->tcpfd != -1) close(l->tcpfd);<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>+ }<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'> }<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'><o:p> </o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'> /* The connected socket inherits non-blocking<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>@@ -2232,6 +2243,7 @@ int swap_to_tcp(struct frec *forward, time_t now, int status, struct dns_header<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'> ssize_t *plen, char *name, int class, struct server *server, int *keycount, int *validatecount)<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'> {<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'> struct server *s;<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>+ struct listener *l;<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'><o:p> </o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'> if (!option_bool(OPT_DEBUG))<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'> {<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>@@ -2304,6 +2316,13 @@ int swap_to_tcp(struct frec *forward, time_t now, int status, struct dns_header<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'> daemon->forward_to_tcp = forward;<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'> daemon->header_to_tcp = header;<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'> daemon->plen_to_tcp = *plen;<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>+<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>+ /* Close inherited listening sockets in the child process. */<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>+ for (l = daemon->listeners; l; l = l->next)<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>+ {<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>+ if (l->fd != -1) close(l->fd);<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>+ if (l->tcpfd != -1) close(l->tcpfd);<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>+ }<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'> }<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'> }<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'><o:p> </o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>--<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>2.25.1<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'><o:p> </o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>The patch is attached below.<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>Thanks<o:p></o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'><o:p> </o:p></span></p><p class=MsoNormal><span lang=EN-US style='font-size:12.0pt'>Zhou Yuefu<o:p></o:p></span></p></div></body></html>