Skip to content

Fix hang on scoped IPv6 (#184) and non-IP (#179) nameservers - #221

Open
mbroadhead wants to merge 1 commit into
alexdalitz:masterfrom
mbroadhead:fix/scoped-ipv6-nameserver-and-resolv-conf-loop
Open

Fix hang on scoped IPv6 (#184) and non-IP (#179) nameservers#221
mbroadhead wants to merge 1 commit into
alexdalitz:masterfrom
mbroadhead:fix/scoped-ipv6-nameserver-and-resolv-conf-loop

Conversation

@mbroadhead

@mbroadhead mbroadhead commented Jul 28, 2026

Copy link
Copy Markdown

Fixes #184 and #179, which turn out to be the same failure.

The bug

Config.resolve_server can't interpret an IPv6 link-local nameserver with an interface/scope specifier such as fe80::1%en0 (#184): both IPv4.create and IPv6.create raise on the %zone suffix, so it falls through to the "resolve as a hostname" branch. That branch builds a Resolver/Recursor, which re-parses /etc/resolv.conf, which contains the same unparseable nameserver, which calls resolve_server again — an infinite loop (#179).

The same loop happens for any non-IP nameserver entry in resolv.conf, including the malformed 8.8.8.8, from #179's original report.

Reproduction (macOS on a hotspot hands out a scoped link-local resolver, so /etc/resolv.conf looks like the #184 report):

nameserver fe80::feed:face:c0ff:ee00%en0
nameserver 192.168.0.1
Dnsruby::Resolver.new.query("example.com")   # hangs / SystemStackError

The fix

  • resolve_server accepts a scoped IPv6 literal verbatim instead of resolving it as a hostname.
  • parse_resolv_conf keeps only IP-literal nameservers (IPv4, IPv6, or scoped IPv6) and warns-and-skips anything else, so a non-IP entry can never enter the recursing resolve path. resolv.conf nameservers are IP literals by definition, so this also fixes the malformed-entry case in never-ending loop when trying to resolve a non-ip nameserver in /etc/resolv.conf #179.
  • check_ns accepts an explicitly-configured scoped IPv6 nameserver rather than misreading it as a domain Name.
  • PacketSender#check_ipv6 classifies a scoped IPv6 server as IPv6, so packets go over an IPv6 socket rather than IPv4.

Two small predicates (Config.scoped_ipv6?, Config.ip_nameserver?) centralise the literal validation.

Tests

test/tc_res_config_nameserver.rb (added to the offline suite) covers the predicates, resolve_server on a scoped literal, resolv.conf filtering with the exact #184 and #179 inputs, and the IPv6 socket classification. The full offline suite is green (141 runs, 1419 assertions, 0 failures), including the existing bad-nameserver tests — the legitimate user-supplied-hostname path still works and no longer loops.

Caveat

I verified there's no hang, that parsing/filtering is correct, and that a scoped server is classified as IPv6. I could not test actual packet delivery to a live link-local DNS server (none reachable from my environment); that path relies on the OS resolving the %zone via getaddrinfo, which is the standard behaviour. Happy to adjust anything.


This patch was generated by Claude Code (Opus 4.8).

…alitz#179)

Config.resolve_server could not interpret an IPv6 link-local nameserver
with an interface/scope specifier such as "fe80::1%en0" (alexdalitz#184): both
IPv4.create and IPv6.create raise on the "%zone" suffix, so it fell
through to hostname resolution. That path builds a Resolver/Recursor,
which re-parses /etc/resolv.conf, which contains the same unparseable
nameserver, calling resolve_server again — an infinite loop (alexdalitz#179). The
same loop occurs for any non-IP nameserver in resolv.conf (e.g. a
malformed "8.8.8.8," entry).

Fixes:

- resolve_server accepts a scoped IPv6 literal verbatim instead of
  resolving it as a hostname.
- parse_resolv_conf keeps only IP-literal nameservers (IPv4, IPv6, or
  scoped IPv6) and warns on/skips anything else, so a non-IP entry can
  never enter the recursing resolve path. resolv.conf nameservers must be
  IP literals by definition.
- check_ns accepts an explicitly-configured scoped IPv6 nameserver rather
  than misreading it as a domain name.
- PacketSender#check_ipv6 classifies a scoped IPv6 server as IPv6 so it is
  sent over an IPv6 socket rather than IPv4.

Adds offline regression tests (test/tc_res_config_nameserver.rb) covering
the scoped-IPv6 predicates, resolve_server, resolv.conf filtering
(including the exact alexdalitz#184 and alexdalitz#179 inputs), and the IPv6 socket
classification.
@alexdalitz

Copy link
Copy Markdown
Owner

Thanks for the patch!
I'm afraid I won't have the time to look at it until late August.
As a human volunteer who develops and maintains open source software for the benefit of humanity, I have to say that it is quite tiresome to have to spend my time replying to an AI.
I know that some humans have grown so tired of this that they refuse to accept any AI patches.
I don't go that far - it's great to improve open source software! - but I don't like spending my free time dealing with robots.
If you submit further patches to a human-run OS project, please do the maintainers the basic courtesy of some human interaction.
Again, thanks for your patch, and I will look at it when I have time in about four weeks.

@mbroadhead

Copy link
Copy Markdown
Author

Hi @alexdalitz,
I'm the human behind the claude generated PR. Let me know if you'd like me to clarify anything.
I ran into this bug when using an iphone hotspot on a mac (IPv6 link local).
The reality is I took time out of my day to submit a PR for a bug that has been around for 5 years. Without AI, I would not have had the time.
While AI generated the patch, I personally reviewed it. A new test has been added for the bug and the test suite passes.
With that being said, I don't have a deep understand of this gem and that is where you or another maintainer's review is appreciated. I can't even imagine how much time is required away from family and friends to maintain OSS, so my hat is off to you.
You can skewer me in your review and I'll dig deeper into a better fix if necessary.

@alexdalitz

Copy link
Copy Markdown
Owner

Hey thanks for your patch, and the work you've done on it. It's always good to fix issues and make things better - I'm sorry if I came across a little grumpy!
In any case, with apologies for the extended delay (although the total solar eclipse was well worth it!), I've now had a chance to review your PR.
Generally, it looks great - thank you! However, I don't think that TCP pipelining is safe here - it uses IPv4 by default, so this patch could break it. Might it be possible to fix it up so TCP pipelining works with this patch, please?
It would also be great to add a few tests to Config, to make sure that #nameserver= and parse_resolv_conf accept IPv6 literals.
Even better would be some online tests (which are necessary in Dnsruby) to show that communication with IPv6 nameservers works over UDP and TCP.
If you're not up for this, please let me know, and I will add to this PR myself.
Thanks!

@mbroadhead

Copy link
Copy Markdown
Author

Hi @alexdalitz,
Thank you for your review. I'll see if I can do your suggestions this weekend.

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.

dnsruby cannot interpret an IPv6 link-local address with interface specifier

2 participants