Repository navigation
feat: UI/UX polish, DNS fallback (LAN mDNS + opt-in public), synthetic screenshots, README - #60
Conversation
Same columns, colors, and data; presentation only. - Toolbar: Pause/Resume and Refresh with shortcut tooltips, compact rate picker, grouped toggles with explanations, right-aligned filter with a clear button. `/` or Cmd+F focuses the filter; Esc clears it. - Table: bold flat sortable headers with column tooltips; long cells elide with "…" and show the full value on hover. - Sorting: process/path compare case-insensitively, and a full-key tie-break stops equal rows from swapping places on every refresh. - Status bar: bold counts, "N hidden by filter/options", an amber PAUSED badge, legend swatches with tooltips, theme-aware error color. - Empty states: spinner while the first snapshot loads; "No endpoints match" with Clear filter / Show all endpoints actions. - Terminate confirmation is an egui Modal (dimmed backdrop, Esc or outside click cancels, red destructive button). About is centered. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9R3L1T2q6TXmBmv5iBSur
When the system resolver (router/ISP DNS) has no PTR answer: - RFC 1918 / link-local IPv4 peers get one legacy unicast mDNS query to 224.0.0.251:5353 (RFC 6762 6.7). Bonjour/Avahi hosts answer for their own addresses even when the router does not; traffic stays on-link. - Globally routable peers can be asked of Quad9 or Cloudflare over UDP/53, chosen under Options -> Fallback DNS. Off by default because it sends those IPs to the provider; special-purpose ranges never go. The std-only wire code (src/ptr.rs) uses a random ID and ephemeral port, accepts replies only from the expected source with matching ID, QR, NOERROR, no TC, and the queried owner name (following RFC 2317 CNAMEs), rejects forward/looping pointers and dotted or non-printable labels, and every answer still passes is_plain_hostname. Cached misses now expire after two minutes and are dropped when the fallback setting changes. The CLI remains system-resolver only. Info.plist gains a Local Network usage string for the macOS 15+ prompt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9R3L1T2q6TXmBmv5iBSur
- screenshots/: overview (dark), Off-box only, filter (light), and the terminate confirmation, rendered from the real egui UI with synthetic endpoints in RFC 5737/3849 documentation ranges. The README hero image replaces docs/image.jpg and explains the telemetry-kill use case. - README: Screenshots and Name resolution sections; shortcuts, export location, and the Release workflow. - app: when a reverse-DNS name is shown, only the name elides; the observed "(ip):port" tail is always drawn in full, with the complete value on hover. The split is verified against the formatted text, so a lookup landing between calls cannot mis-split it. - app: colored rows paint across column gaps as one band, selected colored rows get an outline, and sort arrows and the clear button use glyphs present in egui's bundled fonts (the old arrows rendered as boxes). Wider address columns and a 1440x820 default window. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9R3L1T2q6TXmBmv5iBSur
|
@codex review for any regressions |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a272a0b6f6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| let global_unicast = s[0] & 0xe000 == 0x2000; | ||
| let documentation = s[0] == 0x2001 && s[1] == 0x0db8; | ||
| if global_unicast && !documentation { |
There was a problem hiding this comment.
Exclude all special-purpose IPv6 ranges from fallback
When public fallback DNS is enabled, this treats every address in 2000::/3 except 2001:db8::/32 as global. That includes 3fff::/20 documentation space and 2001:2::/48 benchmarking space, which the IANA IPv6 special-purpose registry marks as not globally reachable; lookup_with_fallbacks will therefore disclose these addresses to Quad9 or Cloudflare despite the UI and security documentation promising that documentation and other special-purpose addresses are never sent. Classify all non-globally-reachable special prefixes as Other rather than approximating global unicast with 2000::/3.
Useful? React with 👍 / 👎.
| if self.fallback.swap(fallback as u8, Ordering::Relaxed) != fallback as u8 { | ||
| cache_lock(&self.cache).retain(|_, v| !matches!(v, CacheValue::Missed(_))); |
There was a problem hiding this comment.
Invalidate stale in-flight lookups on fallback changes
If the user changes the fallback while a worker is blocked in the system resolver, the cache entry is Pending, so this retention leaves it intact; that worker already captured the old fallback setting and can subsequently overwrite the entry with a fresh Missed result. Requests then suppress the newly selected fallback for the full two-minute negative TTL instead of retrying immediately. Associate results with a fallback generation or otherwise discard and requeue completions started under the old setting.
Useful? React with 👍 / 👎.
| egui::CentralPanel::default().show(ctx, |ui| { | ||
| self.table(ui, &visible); | ||
| if visible.is_empty() { | ||
| self.empty_state(ui, rows.is_empty() && err.is_none()); |
There was a problem hiding this comment.
Track snapshot completion separately from row count
After the first successful snapshot returns zero endpoints—possible when a normal user has limited OS visibility or simply has no visible sockets—rows.is_empty() and err.is_none() remain identical to their initial values, so the UI displays “Waiting for the first snapshot…” forever instead of the completed empty state. Record whether at least one snapshot has completed and use that flag for waiting.
AGENTS.md reference: AGENTS.md:L66-L67
Useful? React with 👍 / 👎.
| match exchange(MDNS_GROUP, false, ip, MDNS_WAIT, |src| { | ||
| scope(src.ip()) == Scope::Lan |
There was a problem hiding this comment.
Require mDNS replies to originate from port 5353
For a LAN lookup, this predicate accepts a reply from any UDP source port as long as its IP is private or link-local. The mDNS specification requires responses to use source port 5353 and requires implementations to ignore responses from other ports; because the multicast query exposes its transaction ID and ephemeral destination port to the link, the current check lets any LAN sender inject and cache a chosen hostname from an arbitrary socket. Require both LAN scope and src.port() == 5353.
AGENTS.md reference: AGENTS.md:L71-L72
Useful? React with 👍 / 👎.
| ui.add(egui::Label::new(RichText::new(name).monospace().color(fg)).truncate()) | ||
| }) | ||
| .inner; | ||
| ui.add(egui::Label::new(RichText::new(tail).monospace().color(fg)).truncate()); |
There was a problem hiding this comment.
Keep the numeric IPv6 suffix from being truncated
For a resolved full-length IPv6 endpoint, tail is roughly 50 monospace characters including brackets and the port, which exceeds the initial 250/330-pixel address columns and can also exceed any user-resized column. name_w then falls to zero and this explicit .truncate() elides the supposedly protected numeric suffix, so an untrusted DNS name can still leave the observed IP or port hidden. Give the suffix enough minimum width or render it without truncation before allocating any remaining space to the name.
AGENTS.md reference: AGENTS.md:L71-L72
Useful? React with 👍 / 👎.
| let host = reverse_dns(ip).or_else(|| { | ||
| match ptr::scope(ip) { | ||
| Scope::Lan => ptr::mdns_ptr(ip), | ||
| Scope::Global => fallback.server().and_then(|s| ptr::unicast_ptr(ip, s)), |
There was a problem hiding this comment.
Limit public fallback queries to remote endpoints
The GUI queues both local and remote addresses, but this branch applies the public resolver to every globally routable address without knowing which side of the socket it came from. On a Mac with a public IPv4 or IPv6 local address whose system PTR lookup misses, enabling Quad9 or Cloudflare therefore sends the Mac's own address to that provider even though the UI and security documentation describe the fallback as receiving unresolved public peer IPs. Pass the endpoint role into the resolver or restrict the public fallback to remote-address requests.
AGENTS.md reference: AGENTS.md:L73-L74
Useful? React with 👍 / 👎.
| // RFC 6762: mDNS packets use IP TTL 255; routers never forward them. | ||
| let _ = sock.set_multicast_ttl_v4(255); | ||
| } | ||
| if sock.send_to(&query(id, recursion, &qname), dest).is_err() { |
There was a problem hiding this comment.
Send each mDNS query on the target peer's interface
When an RFC 1918 endpoint is reached through a VPN, secondary Ethernet adapter, or another non-default interface, the target ip appears only inside the DNS name while the packet destination is always 224.0.0.251. Because the wildcard-bound socket never selects an outbound multicast interface, the kernel routes this send according to the multicast group rather than the peer, so the query can go to the default LAN, fail to reach the target's link, and disclose the private reverse name on the wrong link. Determine the interface used to reach ip and select it for the multicast send, or skip mDNS for addresses that are not directly on-link.
AGENTS.md reference: AGENTS.md:L73-L74
Useful? React with 👍 / 👎.
Proposed changes
This is the content of #59, which was merged into the
codex/security-hardeningbranch instead ofmain(it merged right after #58, before GitHub retargeted it). The three commits are replayed onto currentmain, which already has #57 and #58. The resulting tree is file-for-file identical to #59's headbfd18ba, which passed all 9 CI checks.1. UI/UX (same columns, colors, and data; presentation only)
/or Cmd+F focuses it; Esc clears it.egui::Modal(dimmed backdrop, Esc or an outside click cancels, red destructive button). About is centered.▾ ▴rendered as boxes because egui's bundled fonts lack them. They are replaced with⏷ ⏶and✖, checked against the actual font files.(ip):portis always drawn in full.2. DNS: resolve more names when the router can't (
src/ptr.rs, std only)is_plain_hostname.-nstays numeric).Info.plistgainsNSLocalNetworkUsageDescriptionfor the macOS 15+ prompt.3. Screenshots + README
screenshots/holds four 2× PNGs rendered from the real egui UI with synthetic endpoints (RFC 5737/3849 documentation ranges, fake*.examplenames).docs/image.jpg(removed).Branch and base
codex/ui-dns-screenshots-mainmainf0e8947(includes ci: add manual Release workflow for the bundled .app #57 and fix: harden display, DNS labels, termination, and GUI export path #58)Type of change
Benefits and behavior boundaries
Validation checklist
./scripts/check.sh): pending CI on this branch. The identical tree passed all of it on feat: UI/UX polish, DNS fallback (LAN mDNS + opt-in public), synthetic screenshots, README #59: 9 of 9 checks, on both Mac architectures and on Rust 1.85.0 and stable.ptrwire format, scope, source-checked exchange against a local fake server, anddns::misses_expire_and_fallback_changes_retry_them.git diff --checkpasses.Risks to monitor
🤖 Generated with Claude Code
https://claude.ai/code/session_01K9R3L1T2q6TXmBmv5iBSur
Generated by Claude Code