Skip to content

fix: echo edns0 opt in dns replies when the request carries one - #3041

Merged
wwqgtxx merged 1 commit into
MetaCubeX:Alphafrom
TWO666:fix-dns-opt-echo
Jul 28, 2026
Merged

fix: echo edns0 opt in dns replies when the request carries one#3041
wwqgtxx merged 1 commit into
MetaCubeX:Alphafrom
TWO666:fix-dns-opt-echo

Conversation

@TWO666

@TWO666 TWO666 commented Jul 28, 2026

Copy link
Copy Markdown

What this fixes

RFC 6891 requires a responder to include an OPT record whenever the request
carries one:

If an OPT record is present in a received request, compliant responders
MUST include an OPT record in their respective responses.
RFC 6891 §6.1.1

mihomo strips the upstream OPT before caching , which is itself correct — OPT
is a per-hop pseudo-record and must not be cached:

OPT RRs MUST NOT be cached, forwarded, or stored in or loaded from
master files.
RFC 6891 §6.1.1

mihomo/dns/util.go

Lines 69 to 72 in f298cd2

// OPT RRs MUST NOT be cached, forwarded, or stored in or loaded from master files.
msg.Extra = lo.Filter(msg.Extra, func(rr D.RR, index int) bool {
return rr.Header().Rrtype != D.TypeOPT
})

But it never generated a fresh OPT when answering, so cache hits were
plain-DNS replies. Clients such as systemd-resolved treat a missing OPT as an
EDNS0 failure (dns_server_packet_bad_opt)
and downgrade the server:

Using degraded feature set UDP instead of UDP+EDNS0 for DNS server ...

after which they periodically re-probe, adding timeouts and extra round trips.

Fix

Synthesize an OPT at the ServeMsg convergence point (covers both the DNS
listener and the TUN hijack path) when the request has one and the reply does
not:

"The DO bit of the query MUST be copied in the response."

Verification

$ # first query got OPT PSEUDOSECTION
$ dig @127.0.0.3 1024.size.dns.netmeister.org A +notcp +noall +comments +stat +edns=0
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 13592
;; flags: qr aa rd ra ad; QUERY: 1, ANSWER: 60, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; Query time: 679 msec
;; SERVER: 127.0.0.3#53(127.0.0.3) (UDP)
;; WHEN: Tue Jul 28 22:32:37 CST 2026
;; MSG SIZE  rcvd: 1017

$ # no OPT PSEUDOSECTION
$ dig @127.0.0.3 1024.size.dns.netmeister.org A +notcp +noall +comments +stat +edns=0
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 30885
;; flags: qr aa rd ra ad; QUERY: 1, ANSWER: 60, AUTHORITY: 0, ADDITIONAL: 0

;; Query time: 0 msec
;; SERVER: 127.0.0.3#53(127.0.0.3) (UDP)
;; WHEN: Tue Jul 28 22:32:38 CST 2026
;; MSG SIZE  rcvd: 1006

Before the fix the second (cached) reply has no OPT PSEUDOSECTION, and
resolvectl log-level debug shows the downgrade message above; after the fix
the OPT is present on every reply and the feature set stays UDP+EDNS0.

systemd-resolved works well

$ resolvectl dns Mihomo 127.0.0.3
$ resolvectl flush-caches
$ resolvectl reset-server-features
$ journalctl -u systemd-resolved -f
Using feature level UDP+EDNS0 for transaction 4262.
Using DNS server 127.0.0.3 for transaction 4262.
Announcing packet size 65508 in egress EDNS(0) packet.
Emitting UDP, link MTU is 1500, socket MTU is 65535, minimal MTU is 40
Sending query packet with id 4262 of size 57.
Processing query...
Received dns UDP packet of size 1017, ifindex=0, ttl=0, fragsize=0, sender=127.0.0.3, destination=127.0.0.1
Processing incoming packet of size 1017 on transaction 4262 (rcode=SUCCESS).
Verified we get a response at feature level UDP+EDNS0 from DNS server 127.0.0.3.
Regular transaction 4262 for <1024.size.dns.netmeister.org IN A> on scope dns on Mihomo/* now complete with <success> from network (unsigned; non-confidential).
Sending response packet with id 2637 on interface 1/AF_INET of size 1017.
Freeing transaction 4262.

dig dns query works well

$ dig @127.0.0.3 1024.size.dns.netmeister.org A +notcp +noall +comments +stat +edns=0
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 36699
;; flags: qr aa rd ra ad; QUERY: 1, ANSWER: 60, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; Query time: 1 msec
;; SERVER: 127.0.0.3#53(127.0.0.3) (UDP)
;; WHEN: Tue Jul 28 22:30:28 CST 2026
;; MSG SIZE  rcvd: 1017

$ # second edns query works as expected
$ dig @127.0.0.3 1024.size.dns.netmeister.org A +notcp +noall +comments +stat +edns=0
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 5104
;; flags: qr aa rd ra ad; QUERY: 1, ANSWER: 60, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; Query time: 0 msec
;; SERVER: 127.0.0.3#53(127.0.0.3) (UDP)
;; WHEN: Tue Jul 28 22:30:29 CST 2026
;; MSG SIZE  rcvd: 1017

$ # noedns works as expected
$ dig @127.0.0.3 1024.size.dns.netmeister.org A +notcp +noall +comments +stat +noedns
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 36927
;; flags: qr aa rd ra ad; QUERY: 1, ANSWER: 60, AUTHORITY: 0, ADDITIONAL: 0

;; Query time: 0 msec
;; SERVER: 127.0.0.3#53(127.0.0.3) (UDP)
;; WHEN: Tue Jul 28 22:34:57 CST 2026
;; MSG SIZE  rcvd: 1006

verify config:

mixed-port: 0
mode: direct
log-level: info
ipv6: false
dns:
  enable: true
  listen: 127.0.0.3:53
  ipv6: false
  enhanced-mode: redir-host
  nameserver:
    - 8.8.8.8
  cache-max-size: 1

Copilot AI review requested due to automatic review settings July 28, 2026 14:40

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@wwqgtxx

wwqgtxx commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

You should also add a comment in the code explaining the choice of the value 1232; otherwise, readers might be confused about where this "magic number" comes from.

@TWO666
TWO666 force-pushed the fix-dns-opt-echo branch from 0a2b7d1 to a72e017 Compare July 28, 2026 23:38
@TWO666

TWO666 commented Jul 28, 2026

Copy link
Copy Markdown
Author

You should also add a comment in the code explaining the choice of the value 1232; otherwise, readers might be confused about where this "magic number" comes from.

Added

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

Successfully merging this pull request may close these issues.

3 participants