Repository navigation
Description
Summary
When a UDP response is larger than the EDNS payload size the query advertised, SimpleResolver fails with WireParseException ("end of input" / "truncated record"). It does not retry over TCP. From the caller's point of view the lookup just fails (Lookup.getResult() == TRY_AGAIN, getErrorString() == "network error"), even though the same query over TCP would succeed.
This can happen with a recursive resolver that appears to return UDP responses larger than the requester's advertised payload size. A resolver that does this isn't RFC 6891-compliant. Even so, dnsjava turns it into a hard failure, and the caller can't do anything about it because it never sees the raw bytes.
Versions
- dnsjava 3.6.3 (reproduced)
- The same code paths are unchanged on current
master
What happens
SimpleResolversends the query over UDP with the default EDNS payload size (1280).NioUdpClient.Transaction.processReadyKey()reads intoByteBuffer.allocate(max), wheremaxis that payload size.DatagramChannel.read()silently discards the part of the datagram that doesn't fit.SimpleResolver.sendAsync()parses the cut-off bytes before it checksFlags.TC. Parsing throwsWireParseException, so the existing "Got truncated response ..., retrying via TCP" path is never reached.Lookupmaps theIOExceptiontoTRY_AGAIN/ "network error".
The TC-based retry doesn't help here because the TC bit is never evaluated. A resolver that sends the full response over UDP typically doesn't set TC anyway.
Reproduction
A UDP-only stub that answers any query with ~2.7 KB of TXT records, ignores the EDNS payload size, and never sets TC:
# oversized_stub.py
import socket, struct
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(("127.0.0.1", 15353))
while True:
q, addr = s.recvfrom(4096)
i = 12
while q[i] != 0:
i += q[i] + 1
question = q[12:i + 5]
answers = b""
for n in range(24):
txt = ("record-%02d=" % n + "x" * 90).encode()
rdata = bytes([len(txt)]) + txt
answers += b"\xc0\x0c" + struct.pack(">HHIH", 16, 1, 60, len(rdata)) + rdata
header = q[0:2] + b"\x81\x80" + struct.pack(">HHHH", 1, 24, 0, 0) # QR RD RA, no TC
s.sendto(header + question + answers, addr)// Repro.java
import org.xbill.DNS.*;
public class Repro {
public static void main(String[] args) throws Exception {
SimpleResolver resolver = new SimpleResolver("127.0.0.1");
resolver.setPort(15353);
Message query = Message.newQuery(
org.xbill.DNS.Record.newRecord(Name.fromString("example.com."), Type.TXT, DClass.IN));
try {
System.out.println("answers=" + resolver.send(query).getSection(Section.ANSWER).size());
} catch (Exception e) {
System.out.println(e.getClass().getName() + ": " + e.getMessage());
}
}
}dig +bufsize=4096 -p 15353 @127.0.0.1 example.com TXT shows the stub's full response (ANSWER: 24, MSG SIZE rcvd: 2741, no tc flag).
Output with dnsjava 3.6.3 (org.xbill.DNS.SimpleResolver at DEBUG):
DEBUG SimpleResolver - Sending example.com./TXT, id=22819 to udp/127.0.0.1:15353
org.xbill.DNS.WireParseException: end of input
There is no "retrying via TCP" line. Through Lookup, the result is records=null result=2 error=network error.
Depending on where the cut falls, the exception can also be WireParseException: truncated record.
Expected
A UDP response that didn't fit in the receive buffer should be handled like a truncated response and retried over TCP, the same as a response with TC set. The retry should respect ignoreTruncation.
Related
#240 reported the same exception, but from application code parsing a received message itself. In this case the cut-off happens inside dnsjava's own UDP client, so the caller has no way to detect or handle it.
Activity
ibauersachs commentedon Oct 20, 2026
Hi pssamudr, and @allenflux
Thanks for the report and PR. Before I look further into this, could you please clarify what this should solve in real-life? You mention yourself that the resolver is not compliant, so I'm very reluctant to integrate this. The TC flag is not meant for network-truncated messages, rather it indicates that the server processed the request but could not fit the entire response. There's a bunch of rules what to omit if truncation needs to be applied, and randomly reconstructing a message surely doesn't follow these rules and probably making things worse.
allenflux commentedon Oct 21, 2026
Thanks for the clarification. I agree that receiver-side buffer truncation is different from the server setting TC, and that the behavior described in #418 is non-compliant.
The intent of #421 was to recover when an oversized UDP response is cut off by the local receive buffer and parsing fails before TCP fallback. The patch does not set the TC bit or return the partial UDP answer to the caller, but it does reuse TC-tolerant parsing to reach the validation and fallback path. I understand why that is questionable.
I reproduced the reported behavior with a local stub; I don't have an independently verified production case. @pssamudr, could you provide more details about the actual resolver and deployment, ideally with a sanitized packet capture?
Would you consider a separate approach that detects local receive-buffer truncation, discards the UDP response, and retries over TCP with appropriate validation? Or would you prefer to reject this non-compliant behavior? I'd like to clarify the intended policy before proposing further changes.
pssamudr commentedon Oct 21, 2026
Real-life motivation
We hit this in a sandboxed runtime where the only reachable resolver is a local recursive forwarder we don't operate. For a large TXT RRset at a zone apex it returns the full answer over UDP — larger than the advertised 1280-byte EDNS payload, and without setting TC. On the same host
dig +tcpand other libraries get the complete answer; dnsjava alone turns it into a hard failure (TRY_AGAIN/"network error"), and the caller can't recover because the failure happens before any retry logic and the raw bytes are never surfaced.I agree the forwarder isn't RFC 6891-compliant. The ask isn't to bless that — it's that a datagram cut at the wire shouldn't become an unrecoverable
WireParseExceptionwhen TCP would succeed, which is exactly what dnsjava already does for a truncated response.It's not about trusting the server's TC flag
The trigger isn't the server saying "truncated." dnsjava's own UDP client truncates the datagram: the receive buffer is sized to the advertised payload, so
DatagramChannel.read()silently drops the overflow, andSimpleResolverparses the cut bytes and throws before ever reaching the TC→TCP retry. A server that sends the full response over UDP doesn't set TC, so the existing TC check can never catch this.Nothing is reconstructed
The cut UDP bytes are discarded — dnsjava re-queries over TCP and returns TCP's complete response, the same outcome as today's TC=1 path.
So the caller always gets a correct, fully-parsed TCP response, never a patched-together UDP one.
ibauersachs commentedon Oct 24, 2026
So I played around a little. Dig doesn't automatically fallback to TCP, it just silently accepts the larger response. Netty on the other hand notices the large response and retries with TCP. I guess we can do that too, but @allenflux current PR doesn't quite work as-is. I'll comment on the PR.