Skip to content

Add a DNS protocol analyzer tab (RFC 1035) - #1221

Open
joaopdadv wants to merge 13 commits into
GyulyVGC:mainfrom
joaopdadv:feature/dns-analyzer
Open

Add a DNS protocol analyzer tab (RFC 1035)#1221
joaopdadv wants to merge 13 commits into
GyulyVGC:mainfrom
joaopdadv:feature/dns-analyzer

Conversation

@joaopdadv

Copy link
Copy Markdown

Summary

This PR adds a dedicated DNS tab that parses the DNS protocol (RFC 1035)
directly from the bytes of UDP/TCP traffic on port 53 and presents it in real time.

Today Sniffnet only does reverse-DNS lookups to enrich displayed IPs and
identifies services by port number — there is no inspection of the DNS protocol
itself. This fills that application-layer gap.

What it does

  • Manual DNS parser over &[u8] (no external DNS crate), interpreting:
    • the 12-byte header (ID, QR, OPCODE, AA, TC, RD, RA, RCODE, QD/AN/NS/AR counts);
    • the Question section, with name compression support (RFC 1035 §4.1.4);
    • the Answer resource records, with RDATA decoded per type
      (A, AAAA, CNAME, NS, PTR, MX, TXT, SOA).
  • Live log of queries/responses: time, Q/R, domain, type, RCODE, latency, answers.
  • Resolution latency by correlating responses to queries via the transaction
    ID + (client, server) endpoint pair.
  • Most-queried domains ranking.
  • Filters by record type and by response code (the type filter matches the
    query type or any answer record type, e.g. CNAME in a CNAME→A chain).

Implementation

  • New src/networking/dns/ module (parser.rs, types.rs).
  • DNS messages are extracted from headers.payload in parse_packets.rs before
    analyze_headers consumes the headers, carried to the GUI via a new
    InfoTraffic::dns_events field, and folded into gui::types::dns_state.
  • New RunningPage::Dns and gui/pages/dns_page.rs; tabs render automatically.

Testing

  • 16 DNS-specific tests (parser, latency correlation, ranking, filters) plus an
    end-to-end test that runs docs/samples/dns_sample.pcap through the real
    capture-to-parse pipeline. Full suite: 185 passing, no new warnings.
  • Cross-checked against tcpdump on the same pcap (identical field interpretation).
  • Validated live capturing real DNS traffic.

Notes / scope

  • Encrypted DNS (DoH/DoT) is out of scope — only plaintext DNS on port 53 is parsed.
  • The Authority/Additional sections are counted (NSCOUNT/ARCOUNT) but not expanded.

joaopdadv and others added 13 commits June 21, 2026 18:21
Add a new `networking::dns` module that parses DNS messages directly
from raw UDP/TCP payload bytes, with no external DNS crate:

- `types.rs`: DnsMessage, DnsEvent, DnsQuestion, DnsRecord and the
  DnsRecordType / DnsRCode / DnsRData / DnsFlags enums, with Display impls.
- `parser.rs`: `parse_dns(&[u8])` decoding the 12-byte header (bitwise
  flags), the Question section and Answer resource records, with
  compression-pointer support (RFC 1035 §4.1.4), RDATA interpreted per
  type (A, AAAA, CNAME/NS/PTR, MX, TXT, SOA) and defensive handling of
  malformed/truncated input (no panics).

Includes 9 unit tests over fixed byte vectors (A query, compressed A
response, AAAA, MX, NXDOMAIN, too-short, truncated, pointer loop).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Detect DNS traffic (UDP/TCP port 53) in the capture loop and parse the
message from the transport payload, extracted from `headers.payload`
before `analyze_headers` consumes the headers (the slice borrows
`packet.data`, so it outlives the move). DNS over TCP's 2-byte length
prefix is stripped before parsing.

Parsed messages are pushed to a new `InfoTraffic::dns_events` vector,
carried to the GUI on each tick and reset by `take_but_leave_something`;
`refresh` intentionally ignores it (events are drained GUI-side).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a dedicated "DNS" tab to the running pages:

- `gui::types::dns_state`: DnsState holds a bounded live log (VecDeque,
  cap 2000) of DnsEntry rows folded from the backend's DnsEvents.
- `gui::pages::dns_page`: renders a header + scrollable table
  (Time, Q/R, Domain, Type, RCODE, Answers), newest first, with an
  empty-state placeholder; mirrors the inspect_page layout/styles.
- Wire DnsState into Sniffer (field, construction, capture reset) and
  drain `msg.dns_events` into it in `refresh_data`.
- Register RunningPage::Dns (tab label, next/previous, Globe icon) and
  dispatch it in `view`; enable the previously-disabled Globe icon.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Correlate each response with its originating query by transaction id and
(client, server) endpoint pair to compute resolution latency, shown in a
new "Latency" column on response rows. Count queries per domain and
surface the top 5 in a "Top domains" ranking line on the DNS page.

In-flight queries are bounded (cap 10k) to limit memory when queries go
unanswered. Adds unit tests for latency correlation, orphan responses,
and per-domain ranking.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add two dropdown filters to the DNS page: record type (A, AAAA, CNAME,
MX, TXT, NS, PTR, SOA) and response code (NOERROR, NXDOMAIN, SERVFAIL,
REFUSED, FORMERR, NOTIMP). The summary line shows the filtered count and
an empty-state message distinguishes "no traffic yet" from "no matches".

DnsFilter lives in gui state, is wired through new Message variants, and
applied when rendering the log. Adds a unit test for the matching logic.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A 2-packet Ethernet/IPv4/UDP capture (query + compressed A response for
google.com) to reproduce and validate the DNS analyzer offline, with a
README describing the expected output and the Wireshark cross-check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Document how to build, run and try the DNS tab, including offline
reproduction via the sample pcap, satisfying the "installation /
execution / usage example" requirement.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Read docs/samples/dns_sample.pcap and run every packet through the exact
production path (get_sniffable_headers -> payload extraction ->
analyze_headers -> parse_dns), asserting the recovered query/response for
google.com (type A, answer 8.8.8.8). Validates the full capture-to-parse
chain, not just the parser in isolation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The ranking was laid out on a single horizontal row, which overflowed
the page width. Render it as a title followed by one domain per line.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The record-type filter compared only the query type (QTYPE), so filtering
by e.g. CNAME hid responses to A queries whose answer chain contains a
CNAME (common for CDN/Vercel-hosted domains). Track the answer section's
record types per entry and match the filter against the query type OR any
answer record type. Adds a unit test covering a CNAME-in-answer response.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Answer(s) column showed only RDATA values, so a response to an A query
whose answer chain contains a CNAME looked inconsistent when filtered by
CNAME (the Type column shows the QTYPE, A). Prefix each answer with its
record type, e.g. "CNAME cdn.example.net, A 1.2.3.4", matching dig/tcpdump
and making the answer types visible.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Complete the 12-byte DNS header parsing: read NSCOUNT (Authority) and
ARCOUNT (Additional) record counts. The sections themselves are not
expanded, but their counts are surfaced in the Answer(s) cell as a note
(e.g. "[+1 add'l]"), which also reveals EDNS OPT records. Adds parser
tests for the counts and the note.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@GyulyVGC GyulyVGC added the enhancement New feature, request, or improvement label Jun 30, 2026
@GyulyVGC

Copy link
Copy Markdown
Owner

Hey @joaopdadv thanks for your effort but I already have some plans regarding more precise name resolutions as part of #944

I'll keep this PR open for now and I'll probably take some inspiration, but given the importance of the feature and its impact I'll have to take more time to dig into it and possibly come up with a final implementation myself

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature, request, or improvement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants