Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DNSSEC and TCP requests: large response potentially dropped? #314

Open
realbiz21 opened this issue Jul 1, 2024 · 5 comments
Open

DNSSEC and TCP requests: large response potentially dropped? #314

realbiz21 opened this issue Jul 1, 2024 · 5 comments

Comments

@realbiz21
Copy link

realbiz21 commented Jul 1, 2024

personalDNSFilter 1.50.55.3 on Android 14.

I am testing a free and open source XMPP client, Cheogram.

Cheogram performs DNSSEC validation when it connects to the user's chosen XMPP server. Cheogram includes an implementation of minidns to fulfill its needs. It is my understanding that Cheogram obtains the DNS servers from the system and communicates with those servers directly.

To perform validation, it requests the relevant DNSKEY and DS records of the XMPP server's domain components. In my environment, the records for the domain and the TLD fit within a UDP DNS packet, with a buffer size of 1024.

However, a DNSKEY query for <Root> will not fit in a UDP request of buffer size 1024. This will lead the DNS client to try a TCP request:

$ dig +dnssec +bufsize=1024 DNSKEY .

This leads to a TCP request with an 1141 byte TCP payload with 1139 byte DNS response.

For some unknown reason, when this request and response is routed through pDNSFilter (in VPN mode), the DNSKEY response for <Root> does not appear to arrive at the client. With pDNSf disabled, the exact same request and response (byte for byte sans checksums) reaches the client.

Working:

  • Android client --> LAN DNS --> Android Client

Not Working:

  • Android client --> pDNSf --> LAN DNS --> pDNSf --> LOST?

pDNSf properly sees the request from the client, displays it in the window log, requests the data from the LAN DNS servers, and receives that data from the LAN DNS servers. When logging is enabled, pDNSf logs the query and responses in the traffic log.

Is there any peculiarity with pDNSf that may cause large TCP DNS responses to get dropped?

@IngoZenz
Copy link
Owner

IngoZenz commented Jul 1, 2024

hmmh - currently pDNSf does not support TCP requests. Maybe it is related to this fact. But then it also should not log request and response...

@realbiz21
Copy link
Author

realbiz21 commented Jul 1, 2024

I have a better understanding now.

When pDNSf is not active,

  1. App uses UDP, buffer size of 1024, with UDP LAN server
  2. LAN server returns TC to the app, indicating UDP would be truncated
  3. App then uses TCP with the UDP LAN server
  4. TCP response from the LAN server arrives to the app
  5. App is happy

When pDNSf is active,

  1. App uses UDP, buffer size of 1024, with the pDNSf local VPN DNS server
  2. pDNSf uses UDP, buffer size of 1024, with the UDP LAN server
  3. LAN server returns TC to pDNSf, indicating UDP would be truncated
  4. pDNSf then does a TCP fallback with the TCP LAN server, resizing its buffers to accomodate the payload
  5. TCP response from LAN server arrives to pDNSf (DNS payload of 1139 bytes)
  6. pDNSf supplies the response to the app via UDP (DNS payload of 1139 bytes)
  7. App isn't happy (DNS payload of 1139 bytes)

What I believe is happening is that since pDNSf is the actor performing the TCP fallback, when it replies to the app, the app is still in "UDP mode."

This means its internal buffers of 1024 bytes may have been an accurate number, therefore receiving a 1139 byte UDP payload is missed, ignored, or improperly parsed by the app.

If pDNSf supported TCP for inbound DNS requests for apps, it would have the duty to return TC truncation to the app for the above scenario, so the app can switch to TCP and have appropriately-sized buffers.

Given that pDNSf does not support TCP inbound requests, the current handling by pDNSf is likely fine. Being: pDNSf can potentially return data larger than the app's indicated UDP buffer size. Therefore, the app must either use larger buffers by default or handle the case when the response is too large. Neither is ideal nor foolproof.

In this scenario, I'm going to recommend the app in question increase its default buffer sizes to the recommended value[1] of 1232 bytes, as the DNSKEY records for <Root> is a request the app makes unconditionally for all users of the app, regardless of the specific domain being DNSSEC-validated. At present, this is an 1139 byte payload. The app currently uses 1024 by default.

An ideal solution would be for pDNSf to support TCP inbound, though that's a feature request; not a bug.

[1]: https://www.dnsflagday.net/2020/#action-dns-software-vendors which reads:

It is important for DNS software vendors to comply with DNS standards, and to use a default EDNS buffer size (1232 bytes) that will not cause fragmentation on typical network links. Relevant standards include RFC 7766, RFC 6891 section 6.2.3. and RFC 6891 section 6.2.4.. The motivation for this effort is described in IETF draft intarea-frag-fragile section 6.1 and IETF draft iab-protocol-maintenance.

@IngoZenz
Copy link
Owner

IngoZenz commented Jul 1, 2024

Great analysis - I also think it is as you described.
Regarding pDNSf, there is currently no plan to support TCP requests.

@realbiz21
Copy link
Author

After making the change in the outside app to use a buffer size of 1232 instead of 1024, I think there is a bug in pDNSf that's related to this issue:

  1. App makes UDP DNS request, the packet's EDNS payload size is 1232
  2. pDNSf makes upstream UDP DNS request, the packet's EDNS payload size is 1232, but pDNSf internal bufSize is 1024
  3. pDNSf realizes the upstream response size is larger than bufSize, so increases bufSize, and attempts to resize the response buffer
  4. pDNSf returns the UDP DNS response to the app
  5. App reports an error: the DNS transaction ID in the response is a mismatch (reports always seeing a 0 for DNS transaction ID)
  6. App ignores response, tries again
  7. pDNSf performs the request without needing to change its bufSize or resize the response buffer
  8. App is happy

From my limited debugging,

In DNSServer.java, checkResizeNeeded() updates bufSize, but also does:

response.setData(new byte[bufSize],response.getOffset(),bufSize-response.getOffset());

This appears to clear the response data fully, and there isn't code that retains the original response data.

The response is then returned to the app, and the app sees the DNS transaction ID is 0. The rest of the data likely is zero as well.

Therefore, it appears that any UDP DNS response given to the app after a buffer resize will always return invalid data - any empty buffer.

My suggestions:

  1. Can pDNSf use a default bufSize of 1232?
  2. Is checkResizeNeeded() performing what is expected of it?

--

I've tested on Android with a combination of termux app, dig, and pDNSf without VPN mode (listening on 5300).

In termux,

dig @127.0.0.1 -p 5300 +dnssec dnskey .

dig uses a 1232 byte buffer by default, but can be set on demand with +bufsize=[new value]. A 'close' of pDNSf is needed to test updating of its bufSize.

@IngoZenz
Copy link
Owner

IngoZenz commented Aug 1, 2024

I am wondering if checkResizeNeeded() is used at all in this case. The resizing should happen when receiving the TCP response in readResponseFromStream().
Regarding buf size - currently pDNSf uses 1024 bytes for the overall IP packet including IP and UDP Header. So the actual UDP packet is smaller by the IP header + UDP header size (28 bytes for IP4 and 48 bytes for IP6).
So is your proposed buffer size of 1232 bytes including the IP Header size or is it for the UDP packet data only?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants