Offline security exercise / source reading
DNS triage: a small offline packet puzzle
Read the supplied instructions here, then download the complete project to use the Python utilities and training materials. These are guided practice materials; reference checks do not assess independent understanding.
Archive folder: jeremy-bryant-pages/labs/dns-triage/
Lab instructions
labs/dns-triage/README.md
# DNS triage: a small offline packet puzzle **Guided practice material. All packets are SYNTHETIC. Independent mastery: NOT assessed.** Two invented clients ask an invented resolver for five names. Some replies contain an IPv4 address, one reply reports that a name does not exist, and one question has no visible reply. Your job is to support each conclusion with packet numbers and explain what the capture cannot establish. This is a new portfolio exercise, with a newly constructed scenario, packet sequence, generator, worksheet and drills. It uses no coursework task sheet, submission, data, screenshot or assessment answer. It is not a renamed submission, a record of a real incident, or a claim that Jeremy has independently completed the exercise. Common DNS protocol concepts are shared knowledge; the references below identify their sources. ## What you will practise - Find a DNS question and match its reply using the client, resolver, UDP ports, DNS ID, name and question type. - Distinguish an A answer, an NXDOMAIN response, and an unanswered question **within this file**. - Group a retry with its original question without assuming the network dropped a packet. - Measure the interval from the first observed request to its response and state its limits. An **A question** asks for an IPv4 address. A **resolver** answers DNS questions for a client. **NXDOMAIN** is response code 3, meaning the reply reports that the queried name does not exist. A response is still a response when it reports an error. ## Start here You need Python 3.9 or later and Wireshark for visual inspection. The generator and checker use Python's standard library only. No capture interface, administrator rights, VM, account or network connection is needed. Do not start a live capture; open the supplied file. From this directory, run: ```sh python lab.py --help python lab.py verify python lab.py exercise ``` If your Windows Python launcher is `py`, use `py -3` in place of `python`. `verify` checks the retained guided case. `exercise` creates `run/worksheet-37.json`; keep your own answers there. If that worksheet already exists, supply a fresh `--out run/worksheet-37-second.json` to preserve it. Open [`fixtures/guided/SYNTHETIC-dns.pcap`](fixtures/guided/SYNTHETIC-dns.pcap) in Wireshark with **File > Open**. There are 11 packets. The client addresses are `192.0.2.10` and `192.0.2.11`; the resolver is `192.0.2.53`. All names begin with `synthetic-` and end in `.test`. Choose a packet and expand **Internet Protocol**, **User Datagram Protocol** and **Domain Name System** in its details. Read the IP addresses, UDP ports, transaction ID, response flag, question and answer fields. Frame numbers are the numbers in the original file; applying a display filter does not renumber them. For timing, use the frame's **Time since reference or first frame** in seconds and subtract the first request time from the reply time. Multiply seconds by 1,000 to obtain milliseconds. ## Small filter toolkit Enter these in Wireshark's **display filter** bar after opening the file. They hide unrelated rows; they do not change or capture packets. | Purpose | Display filter | | --- | --- | | All DNS packets | `dns` | | Questions | `dns.flags.response == 0` | | Replies | `dns.flags.response == 1` | | NXDOMAIN replies | `dns.flags.response == 1 && dns.flags.rcode == 3` | | Replies containing an A address | `dns.flags.response == 1 && dns.flags.rcode == 0 && dns.a` | | One guided question, including its replies | `dns.qry.name == "synthetic-violet.test"` | | Reused transaction ID in the guided case | `dns.id == 0x800f` | Filter fields are documented in the [Wireshark DNS display filter reference](https://www.wireshark.org/docs/dfref/d/dns.html). A DNS ID alone is not a unique conversation: check endpoints, ports and question too. For an unanswered question, inspect all its matching rows in the whole file; an individual request row is not evidence that no reply exists elsewhere. ## Record and check your evidence Start with the [worked example](WORKED-EXAMPLE.md), then follow the [progressive drills](DRILLS.md). The worksheet lists the five names and clients. Fill each entry's fields: | Field | What to enter | | --- | --- | | `request_frames` | Original-file frame numbers, in order, including a repeated request | | `response_frames` | Matching reply frame numbers, or `[]` if none is visible | | `outcome` | Exactly `answer`, `nxdomain`, or `unanswered` | | `first_response_ms` | Reply time minus **first** request time, in milliseconds; `null` if unanswered | | `explanation` | A short evidence-based explanation with a limitation | | `limits` | Your overall statement of what this capture cannot establish | Keep JSON quotes and commas intact. Do not change the seed, capture hash, question names or clients. Then run: ```sh python lab.py check --answers run/worksheet-37.json ``` The checker reports fields to revisit without printing the answers. A pass means 20 objective fields match the fixture. It does **not** assess your explanations, independence or professional competence. Review those with a person. The manifest contains the answer key, so leave it closed during independent practice. ## Repeat with a changed dataset ```sh python lab.py generate --seed 91 python lab.py verify --case run/case-91 python lab.py exercise --case run/case-91 python lab.py check --case run/case-91 --answers run/worksheet-91.json ``` Open `run/case-91/SYNTHETIC-dns.pcap`. The seed changes the names assigned to outcomes, IDs, ports, answer addresses and timing. Each variant keeps the same small learning structure; changing the seed is repeated practice, not proof of broad expertise. All `run/` files are ignored by Git. The generator refuses to replace differing existing case files; choose another output directory when needed. ## Verification and boundaries ```sh python -m unittest -v test_lab ``` `lab.py` is the maintained entry point for generation, verification and worksheets. Its parser verifies the PCAP records, Ethernet/IPv4/UDP structure, IPv4 and UDP checksums, DNS questions and compressed answer names, response codes, timing order and request/reply relationships. SHA-256 hashes detect changes to the fixture files; the verifier also regenerates the expected bytes from the seed. File integrity is separate from a learner's interpretation. The retained deterministic case is in `fixtures/guided/`: the capture, a short `case.json`, and a `manifest.json` containing synthetic provenance, hashes and expected evidence. Timestamps and MAC addresses are invented. Addresses use the [RFC 5737 documentation blocks](https://www.rfc-editor.org/info/rfc5737/), and names use the [reserved `.test` domain](https://www.rfc-editor.org/info/rfc2606/). This deliberately small format covers Ethernet, IPv4, UDP and A questions only. It does not cover TCP fallback, encrypted DNS, IPv6, caching, CNAME chains, DNSSEC or NOERROR replies with no A answer. In real traffic, NOERROR alone does not guarantee an A record; inspect the answer section. The NXDOMAIN reply is intentionally minimal and carries no SOA authority record, so this exercise does not model negative caching. The verifier is for this generated format, not an arbitrary-PCAP forensic tool. An unanswered question here does not locate a fault or prove a timeout, firewall block, resolver outage or packet loss. An A answer does not establish that a website loaded. A late response cannot establish which repeated datagram prompted it, or measure resolver processing time separately from transit time. The file provides no observation after its final packet and no independent confirmation that a reply's DNS claim is correct.
Worked example
labs/dns-triage/WORKED-EXAMPLE.md
# Worked example: guided seed 37
**SYNTHETIC practice. This is an answer explanation, not evidence of learner completion.**
Use `fixtures/guided/SYNTHETIC-dns.pcap`. Other seeds produce different evidence. Start with the first question, then use the full key only after attempting the worksheet.
## One question, carefully matched
Apply `dns.qry.name == "synthetic-violet.test"`. Frame 1 asks for an A record from `192.0.2.10:60341` to `192.0.2.53:53`. Its DNS transaction ID is `0x800f` (32783). Frame 2 reverses those endpoints and repeats the same question and ID. The response has code 0 and an A address in its answer section.
That is sufficient to label this episode `answer`. It does not prove an application subsequently connected to the returned address. Frame 2 occurs 0.045 seconds after frame 1: `0.045 × 1000 = 45` milliseconds.
The worksheet entry becomes:
```json
{
"client": "192.0.2.10",
"qname": "synthetic-violet.test",
"request_frames": [1],
"response_frames": [2],
"outcome": "answer",
"first_response_ms": 45,
"explanation": "Frame 2 reverses frame 1's endpoints and matches its DNS ID and A question. It contains an A answer. This does not show an application connection."
}
```
Now apply `dns.id == 0x800f`. Frames 6 and 7 also appear. They concern `synthetic-maple.test` and the other client, `192.0.2.11:62186`. Reusing the same 16-bit ID does not make them replies to frame 1.
## Full evidence key — spoilers
The interval below always starts at the **first** request in the episode.
| Question | Client | Request frames | Reply frames | Outcome | First-request interval |
| --- | --- | --- | --- | --- | --- |
| `synthetic-violet.test` | `192.0.2.10` | 1 | 2 | `answer` | 45 ms |
| `synthetic-meadow.test` | `192.0.2.11` | 3 | 4 | `nxdomain` | 48 ms |
| `synthetic-orchard.test` | `192.0.2.10` | 5, 9 | none | `unanswered` | `null` |
| `synthetic-maple.test` | `192.0.2.11` | 6 | 7 | `answer` | 24 ms |
| `synthetic-lantern.test` | `192.0.2.10` | 8, 10 | 11 | `answer` | 1511 ms |
Frame 4 is a reply with RCODE 3 (NXDOMAIN), not a missing response. The defensible statement is that the resolver's reply reports the name as nonexistent. This constructed file does not independently validate that claim.
Frames 5 and 9 repeat the same orchard question, client port and ID. No matching response exists in the file. Record one unanswered episode with two observed requests. Do not convert that into “the firewall blocked it”; this capture cannot identify why the reply is absent.
Frames 8 and 10 repeat the lantern question. Frame 11 supplies a matching A answer. The first request is at 0.800 seconds and the response at 2.311 seconds: `(2.311 - 0.800) × 1000 = 1511` ms. Measured from the repeated request at 1.800 seconds, the interval would be 511 ms, but that is not the worksheet's requested measure. The matching identifiers do not reveal which request caused the reply.
## Example observation note
“Within this synthetic 11-packet file, three question episodes have A answers, one has an NXDOMAIN reply, and one has no visible reply. The lantern episode includes a repeated request followed by an answer 1511 ms after its first request. The orchard episode repeats without a visible answer. This evidence does not establish packet loss, application success, or a particular faulty device.”
Copying this key can check the software and your understanding of the format. It does not establish independent mastery, which remains **NOT assessed**.
Practice drills
labs/dns-triage/DRILLS.md
# Practice in three passes **Every dataset is SYNTHETIC. Independent mastery: NOT assessed.** Use the same reasoning each time: identify the question, match the flow, inspect the response, then describe only what the retained packets support. ## Pass 1 — guided Open the retained seed-37 capture and the worked example side by side. Reproduce the violet lookup using the name filter. Point to its request, reply, reversed ports, matching ID, question type, response code and A answer. Calculate 45 ms yourself from the frame times. Then explain aloud why `dns.id == 0x800f` displays two separate lookups. Finish the worksheet with the key available. Mark this pass “guided”; even a perfect automated score is not independent work. ## Pass 2 — hints only Close the worked example and create a fresh worksheet: ```sh python lab.py exercise --out run/worksheet-37-hints.json ``` Find the NXDOMAIN reply, the repeated question with no visible reply, and the repeated question that eventually receives an answer. Record the evidence and compute the first-request interval. Use hints one at a time only when stuck: 1. A reply with an error code still counts as a reply. Inspect the DNS response flag before naming the failure. 2. Filter by the full question name. Compare client/resolver addresses, UDP ports, transaction ID and A question across the resulting rows. 3. A repeated question may have an eventual reply. Search the entire file before deciding an episode is unanswered. 4. Timing begins at the first request for this exercise. Do not silently substitute the most recent retry. ```sh python lab.py check --answers run/worksheet-37-hints.json ``` Write two limitations without copying the example note. Have a reviewer ask why “no visible reply” is weaker than “the resolver failed.” Revisiting a dataset you have already seen remains rehearsal. ## Pass 3 — changed data, no key Generate a new case and worksheet. The example uses seed 91; a partner may choose another nonnegative integer up to 2147483647. ```sh python lab.py generate --seed 91 python lab.py exercise --case run/case-91 --out run/worksheet-91-independent.json ``` Open `run/case-91/SYNTHETIC-dns.pcap`. Keep its manifest and the worked example closed. The names associated with each outcome, DNS IDs, ports, answer addresses and timings differ from the guided case. Do not reuse seed-37 frame/name associations. Complete the five entries and write a short observation note. In addition to the checked fields, record the matching DNS ID, UDP ports, response code and returned address where applicable. Identify the reused DNS ID and explain how you separated its two flows. Before checking, explain what extra observations might distinguish an absent reply at the resolver from an absent reply only at the client. This is a reasoning question; do not start a real capture or contact a device. ```sh python lab.py check --case run/case-91 --answers run/worksheet-91-independent.json ``` The automated result checks packet facts only. A person should review the explanation and ask you to find the evidence again without the answer key. Record the seed, capture hash, assistance used, score and reviewer feedback in your own practice notes. No learner session or independent assessment is recorded by this repository; **independent mastery remains NOT assessed** until separately evidenced. Even an independently solved small fixture does not demonstrate competence across real networks.