Public bug reported:

[Impact]

The systemd local 'stub' resolver handles all local DNS queries (by
default configuration used in Ubuntu), and essentially proxies all
requests to its configured upstream DNS resolvers.

Most local DNS resolution by applications uses glibc's getaddrinfo()
function.  This function is configured in various ways by the
/etc/resolv.conf file, which tells glibc what nameserver/resolver to
contact as well as how to talk to the name server.

By default, glibc performs UDP DNS queries, with a single DNS query per
UDP packet.  The UDP packet size is limited per DNS spec to 512 bytes.
For some DNS lookups, a 512 byte UDP packet is not large enough to
contain the entire response - for example, an A record lookup with a
large number (e.g. 30) of A record addresses.  This number of A record
entries is possible in some cases of load balancing.  When the DNS UDP
response size is larger than 512 bytes, the server puts as much response
as it can into the DNS UDP response, and marks the "trunacted" flag.
This lets glibc know that the DNS UDP packet did not contain the entire
response for all the A records.

When glibc sees a UDP response that is "trunacted", by default it
ignores the contents of that response and issues a new DNS query, using
TCP instead of UDP.  The TCP packet size has a higher size limit (though
see bug 1804487 which is a bug in systemd's max-sizing of TCP DNS
packets), and so *should* allow glibc to receive the entire DNS
response.

However, glibc issues DNS queries for both A and AAAA records.  When it
uses UDP, those DNS queries are separate (i.e. one UDP DNS packet with a
single A query, and one UDP DNS packet with a single AAAA query).  When
glibc uses TCP, it puts both DNS queries into a single TCP DNS packet -
the RFC refers to this as "pipelining"
(https://tools.ietf.org/html/rfc7766#section-6.2.1.1) and states that
clients SHOULD do this, and that servers MUST expect to receive
pipelined queries and SHOULD respond to all of them.  (Technically
pipelining can be separate DNS queries, one per TCP packet, but both
using the same TCP connection - but the clear intention of pipelining is
to improve TCP performance, and putting both DNS queries into a single
TCP packet is clearly more performant than using separate TCP packets).

Unfortunately, systemd's local stub resolver has only very basic support
for TCP DNS, and it handles TCP DNS queries almost identically to UDP
DNS queries - it reads the DNS query 2-byte header (containing the
length of the query data), reads in the single DNS query data, performs
lookup and sends a response to that DNS query, and closes the TCP
connection.  It does not check for "pipelined" queries in the TCP
connection.

That would be bad enough, as glibc is (rightly) expecting a response to
both its A and AAAA queries; however what glibc gets is a TCP
connection-reset error.  That is because the local systemd stub resolver
has closed its TCP socket while input data was still pending (i.e. it
never even read the second pipelined DNS query).  When the kernel sees
unread input bytes in a TCP connection that is closed, it sends a TCP
RST to the peer (i.e. glibc) and when the kernel sees the RST, it dumps
all data in its socket buffer and passes the ECONNRESET error up to the
application.  So glibc gets nothing besides a connection reset error.

Note also that even if the systemd local stub resolver's socket flushes
its input buffer before closing the TCP connection (which will avoid the
TCP RST), glibc still expects responses to both its A and AAAA queries
before systemd closes the TCP connection, and so a simple change to
systemd to flush the input buffer is not enough to fix the bug (and
would also not actually fix the bug since glibc would never get the AAAA
response).

[Test Case]

This can be reproduced on any system using a local systemd stub
resolver, when using an application that uses getaddrinfo() - such as
ssh, telnet, ping, etc - or with a simple C program that uses
getaddrinfo().  The dns name looked up must have enough A records to
overflow the 512 byte maximum for a UDP DNS packet.

Alternately, and trivially, glibc can be forced to always use TCP DNS queries 
by editing the /etc/resolv.conf file and adding:
options use-vc

With that option, glibc will fail to lookup 100% of DNS names, since all
lookups will use TCP to talk to the local systemd stub resolver, which
as explained above fails to ever correctly answer glibc's pipelined TCP
DNS queries.

Note that in default Ubuntu installs, /etc/resolv.conf is a symlink to
../run/systemd/resolve/stub-resolv.conf, which systemd thinks it owns
100% - so any manual changes to the file may be overwritten at any time.
There is no way (that I can find) to tell systemd to add any resolv.conf
options (like 'use-vc') to its managed stub-resolv.conf file, so this
test case requires re-editing the /etc/resolv.conf file intermittently,
each time systemd overwrites it.

[Regression Potential]

A (real) fix to this requires adding query TCP pipelining support to the
systemd stub resolver.  That is not a simple change, and has the
potential to cause regressions with the stub resolver's responses to
non-TCP queries, or even non-pipelined TCP queries.

[Other Info]

This bug exists upstream.

The specific bug of TCP DNS fallback not working for DNS responses larger than 
512 bytes can be worked around by editing the /etc/resolv.conf file to add:
options edns0

The EDNS0 option causes glibc to fall back to attempting UDP EDNS0 query
(which has a higher max packet size than the default 512 byte UDP DNS).
The systemd stub resolver does support EDNS0.  However, this workaround
only temporarily works - as explained above, by default /etc/resolv.conf
is a symlink to a file that systemd overwrites intermittently, which
will remove the EDNS0 option.

** Affects: systemd
     Importance: Unknown
         Status: Unknown

** Affects: systemd (Ubuntu)
     Importance: High
     Assignee: Dan Streetman (ddstreet)
         Status: In Progress

** Affects: systemd (Ubuntu Trusty)
     Importance: High
     Assignee: Dan Streetman (ddstreet)
         Status: In Progress

** Affects: systemd (Ubuntu Xenial)
     Importance: High
     Assignee: Dan Streetman (ddstreet)
         Status: In Progress

** Affects: systemd (Ubuntu Bionic)
     Importance: High
     Assignee: Dan Streetman (ddstreet)
         Status: In Progress

** Affects: systemd (Ubuntu Cosmic)
     Importance: High
     Assignee: Dan Streetman (ddstreet)
         Status: In Progress

** Affects: systemd (Ubuntu Disco)
     Importance: High
     Assignee: Dan Streetman (ddstreet)
         Status: In Progress

** Also affects: systemd (Ubuntu Disco)
   Importance: Undecided
       Status: New

** Also affects: systemd (Ubuntu Xenial)
   Importance: Undecided
       Status: New

** Also affects: systemd (Ubuntu Trusty)
   Importance: Undecided
       Status: New

** Also affects: systemd (Ubuntu Cosmic)
   Importance: Undecided
       Status: New

** Also affects: systemd (Ubuntu Bionic)
   Importance: Undecided
       Status: New

** Changed in: systemd (Ubuntu Trusty)
       Status: New => In Progress

** Changed in: systemd (Ubuntu Xenial)
       Status: New => In Progress

** Changed in: systemd (Ubuntu Bionic)
       Status: New => In Progress

** Changed in: systemd (Ubuntu Cosmic)
       Status: New => In Progress

** Changed in: systemd (Ubuntu Disco)
       Status: New => In Progress

** Changed in: systemd (Ubuntu Trusty)
     Assignee: (unassigned) => Dan Streetman (ddstreet)

** Changed in: systemd (Ubuntu Xenial)
     Assignee: (unassigned) => Dan Streetman (ddstreet)

** Changed in: systemd (Ubuntu Bionic)
     Assignee: (unassigned) => Dan Streetman (ddstreet)

** Changed in: systemd (Ubuntu Cosmic)
     Assignee: (unassigned) => Dan Streetman (ddstreet)

** Changed in: systemd (Ubuntu Disco)
     Assignee: (unassigned) => Dan Streetman (ddstreet)

** Changed in: systemd (Ubuntu Disco)
   Importance: Undecided => High

** Changed in: systemd (Ubuntu Cosmic)
   Importance: Undecided => High

** Changed in: systemd (Ubuntu Bionic)
   Importance: Undecided => High

** Changed in: systemd (Ubuntu Xenial)
   Importance: Undecided => High

** Changed in: systemd (Ubuntu Trusty)
   Importance: Undecided => High

-- 
You received this bug notification because you are a member of Ubuntu
Bugs, which is subscribed to Ubuntu.
https://bugs.launchpad.net/bugs/1811471

Title:
  local resolver stub fails to handle multiple TCP dns queries

To manage notifications about this bug go to:
https://bugs.launchpad.net/systemd/+bug/1811471/+subscriptions

-- 
ubuntu-bugs mailing list
ubuntu-bugs@lists.ubuntu.com
https://lists.ubuntu.com/mailman/listinfo/ubuntu-bugs

Reply via email to