---
title: 'Packet Analysis Tools'
source: 'https://academia.sh/en/courses/network-operations/packet-analysis-tools'
course: 'Network Operations and Automation'
language: en
updated: '2026-08-17T18:07:15+00:00'
license: 'CC BY-SA 4.0'
---

# Packet Analysis Tools

The capture filter is a single lever, and it costs at both ends: in a window of ninety-eight flows, the narrowest filter never sees 91 flows and misses 21 of 22 heavy events, while unfiltered capture drops 776,196 bytes because they do not fit the buffer.

The previous two lessons both relied on the device's **own account**. The device
produced the flow record, the device answered the query, the device sent the
notification. If a device has a behavior it never reports, there is no way to learn it
by asking the device.

This lesson changes the source and looks at the bytes crossing the wire itself. An
analyzer placed at a point copies and records the packets passing through it. Copying is
not unlimited: a **capture filter** chooses which packets get copied, and the copies are
written into a limited buffer. The filter is a lever — a single rule applies to all
forty subjects' traffic at once. What this lesson measures is not what the filter sees,
but what it **fails to see**.

The use of analyzer tools and the capture procedure is the subject of the **Security
Tooling and Penetration Testing** course and is not repeated here; nor are tool names
written. What is measured here is not the tool, but the **rule** itself.

## Capture's Two Limits

Capture loses at two separate points, and the two work in opposite directions.

**The first limit is the filter.** A packet that does not pass the filter is never
copied at all. This is not a loss but a choice — until the behavior being sought falls
outside the filter. A narrow filter protects the buffer by filtering out irrelevant
traffic; in exchange, it can never bring back what it filtered out, because a packet
that was never copied does not exist.

**The second limit is the buffer.** Packets that pass the filter are written to a
buffer, and once the buffer fills, the next incoming packet is dropped. This drop is
**silent**: there is no line in the record that says "a packet was dropped here," only
packets that are not there. A wide filter fills the buffer quickly, and the drop happens
right in the middle of the traffic the filter cares about.

Both limits are tied to the same lever. When the filter is narrowed, the first limit
grows and the second shrinks; when it is widened, the reverse happens. There is no
loss-free setting in between, only a choice of **where** the loss falls.

## Filter Syntax

Capture filters are written over the packet's header fields. The dump below shows the
taught syntax; it has not been executed and carries no numeric claim.

```text
# taught filter syntax , not executed

single field
  tcp                        looks at the protocol field
  port 443                   source or destination port
  src port 443                source port only
  host station.example        source or destination host

compound
  tcp and port 443
  tcp and (port 80 or port 443 or port 8080)
  not port 22

in-packet position
  tcp[13] & 2 != 0             segments with the SYN flag set
  ip[6:2] & 0x1fff = 0         unfragmented first fragment
```

Two things deserve attention. First, the filter looks at **header fields**; it cannot
look at the payload's content, because the decision has to be made before the packet is
copied. Second, a negation like `not port 22` widens the filter — a rule that looks
narrow is actually a wide one if what it excludes is small. The measure of a filter's
width is not the length of its syntax but **the number of matching flows.**

## Snap Length and Flow Reassembly

The **snap length** determines how much of each packet is kept. If only headers are to
be kept, a few dozen bytes per packet is enough; if the full payload is wanted, the
entire packet is written, and the buffer fills ten times faster in the same span.

Snap length directly limits **flow reassembly**. Reassembly is merging the packets
belonging to the same flow by their sequence numbers to recover the original byte
stream. Headers are enough if the question is "when did this flow start, how many bytes
did it carry, how did it close." If the question is "what did this flow carry," headers
are not enough, and a truncated record can never answer that question.

Reassembly's second condition is **continuity**: if one of the flow's packets was
dropped, the flow cannot be fully reconstructed. This is why, in the measurement, a flow
can land in one of three separate outcomes — fully captured, left half-done, or never
fit into the buffer at all.

The measurement's assumptions:

- **OB14** — The forty subjects are the course's fixed population; each subject
  produces between one and four flows in the window, and the flows are ordered by start
  time.
- **OB15** — The capture buffer is **1,200,000 bytes** and is not flushed during the
  window. In a real capture, the buffer is flushed to disk; keeping it fixed here
  isolates the drop's dependence on filter width.
- **OB16** — Snap length is fixed at **96 bytes/packet** and is not a lever in this
  lesson. Only the filter is changed.
- **OB17** — The buffer fills in time order. A flow that ends by the moment it fills is
  fully captured, a flow that straddles that moment is left half-done, and a flow that
  starts afterward is never written at all.
- **OB18** — A dropped packet leaves no trace; the measurement reads the dropped bytes
  from the oracle, not from the record. In a real capture, this number can only be
  estimated from a counter.
- **OB19** — A heavy event is the oracle's knowledge. If even a single flow of a
  subject was captured, that subject's heavy events count as visible; if none of its
  flows were captured, all of them are missed.
- **OB20** — The filters are nested: every filter matches all the flows the previous
  one matched. Width is thus measured on a single one-directional axis.
- **OB21** — The observation point sees all forty subjects' flows. This is a
  convenience with no counterpart in the field; the measurement therefore only counts
  the loss from the filter and the buffer, not the loss from position.

## Measurement

```python
"""Packet capture: the capture filter is a single lever, what is measured is what it fails to see.

Part 1 - forty subjects' flows, ordered by start time.
Part 2 - five filter widths: how many flows a narrow filter misses, how many bytes a wide one drops.
"""
SEED = 20260812
BUFFER = 1_200_000   # bytes, capture buffer
SNAP = 96            # bytes, most kept per packet


def rng(seed):
    state = seed % 2147483646 + 1

    def draw(n):
        nonlocal state
        state = (state * 48271) % 2147483647
        return state % n
    return draw


def subjects(count=40, seed=SEED):
    draw, items = rng(seed), []
    for i in range(count):
        items.append({"id": i + 1, "capacity": 20 + draw(81), "latency": 5 + draw(45),
                      "class": ("interactive", "batch", "standby")[draw(3)],
                      "special": draw(9) == 0})
    return items


def events(subs, count=400, seed=SEED + 3):
    draw, items = rng(seed), []
    for i in range(count):
        s = subs[draw(len(subs))]
        items.append({"id": i + 1, "subject": s["id"], "heavy": draw(25) == 0})
    return items


def flows(subs, seed=SEED + 7):
    """Each subject produces between one and four flows; the list is ordered by start time."""
    draw, items = rng(seed), []
    for s in subs:
        for _ in range(1 + draw(4)):
            items.append({"subject": s["id"], "protocol": ("TCP", "UDP", "ICMP")[draw(3)],
                          "port": (22, 53, 80, 123, 443, 8080)[draw(6)],
                          "packets": 20 + draw(400), "size": 64 + draw(1400),
                          "start": draw(1000)})
    return sorted(items, key=lambda a: (a["start"], a["subject"]))


def capture(fl, filt, buffer=BUFFER, snap=SNAP):
    """Flows passing the filter are written to the buffer; once it fills, the remaining bytes drop."""
    filled = matched = whole = half = overflow = dropped = 0
    seen = set()
    for a in fl:
        if not filt(a):
            continue
        matched += 1
        size = a["packets"] * min(a["size"], snap)
        if filled + size <= buffer:
            filled, whole = filled + size, whole + 1
            seen.add(a["subject"])
        elif filled < buffer:
            half += 1
            dropped += filled + size - buffer
            filled = buffer
            seen.add(a["subject"])
        else:
            overflow += 1
            dropped += size
    return matched, whole, half, overflow, dropped, seen


FILTERS = {
    "TCP port 443": lambda a: a["protocol"] == "TCP" and a["port"] == 443,
    "TCP three ports": lambda a: a["protocol"] == "TCP" and a["port"] in (80, 443, 8080),
    "TCP only": lambda a: a["protocol"] == "TCP",
    "TCP and UDP": lambda a: a["protocol"] in ("TCP", "UDP"),
    "unfiltered": lambda a: True,
}

subs = subjects()
evts = events(subs)
fl = flows(subs)
print(f"subject {len(subs)} | flow {len(fl)} | packet {sum(a['packets'] for a in fl)}"
      f" | heavy event {sum(e['heavy'] for e in evts)}")
print(f"buffer {BUFFER} bytes | snap {SNAP} bytes/packet"
      f" | snapped total {sum(a['packets'] * min(a['size'], SNAP) for a in fl)} bytes"
      f" | unsnapped {sum(a['packets'] * a['size'] for a in fl)} bytes")
print()
print(f"{'filter':<15s} {'matched':>8s} {'outside':>8s} {'whole':>6s} {'half':>5s} "
      f"{'overflow':>9s} {'bytes dropped':>13s} {'invisible':>10s} {'heavy missed':>13s}")
for name, f in FILTERS.items():
    e, whole, half, over, dropped, seen = capture(fl, f)
    missed = sum(1 for x in evts if x["heavy"] and x["subject"] not in seen)
    print(f"{name:<15s} {e:8d} {len(fl) - e:8d} {whole:6d} {half:5d} {over:9d} "
          f"{dropped:13d} {len(subs) - len(seen):10d} {missed:13d}")
```

```
subject 40 | flow 98 | packet 20631 | heavy event 22
buffer 1200000 bytes | snap 96 bytes/packet | snapped total 1976196 bytes | unsnapped 15032397 bytes

filter           matched  outside  whole  half  overflow bytes dropped  invisible  heavy missed
TCP port 443           7       91      7     0         0             0         33            21
TCP three ports       17       81     17     0         0             0         23            13
TCP only              38       60     38     0         0             0         15             6
TCP and UDP           65       33     58     1         6        188868         11             4
unfiltered            98        0     65     1        32        776196          5             2
```

## What the Narrow Filter Misses

The first row is the narrowest rule, and it works exactly as intended: all seven of the
matching **7** flows are captured intact, not a single byte drops from the buffer, the
record is small, and it is easy to read. Nothing is broken on this row.

What is broken sits to the row's right. **91** of the ninety-eight flows are left
outside the filter, and **33** of the forty subjects never appear in the record at all.
**21** of the twenty-two heavy events are missed. The record is clean, because only what
was meant to be looked at entered it; that is exactly the problem — the record was built
on the assumption that what would be asked of it was already known.

As the filter widens, this side improves. With `TCP three ports`, invisible subjects
fall to **23**; with `TCP only`, to **15**; missed heavy events become **13** and **6**.
Through the third row, the buffer is never strained at all: bytes dropped is **0**. In
this range, the lever looks free.

## What the Wide Filter Misses

In the fourth row, the second limit kicks in. The `TCP and UDP` filter matches **65**
flows, but only **58** of them are captured whole: **1** flow is left half-done and
**6** flows never fit in the buffer at all. Bytes dropped is **188,868**.

In unfiltered capture, the table flips. The filter now leaves nothing outside — the
`outside` column is **0**. In exchange, only **65** of the ninety-eight flows are
captured whole, **32** never fit in the buffer at all, and **776,196** bytes drop. The
snapped total is **1,976,196** bytes; that is, roughly two-fifths of the data meant to
be captured never enters the record at all.

The most striking column is invisible subjects: in unfiltered capture, this number is
**5**, and it is not zero. Even though the filter excluded no subject, five subjects are
absent from the record, because their flows started after the moment the buffer filled.
**Widening the filter did not increase visibility — it moved the loss from the filter to
the buffer.**

The qualitative difference between the two losses matters. A flow the filter excludes
was excluded **knowingly**; whoever reads the rule can say what was left out. A flow
dropped from the buffer, by contrast, was dropped without anyone knowing, and the record
itself carries no field showing this. The second loss is more dangerous even when it
carries away fewer bytes than the first, because whoever reads the record does not
notice the gap.

## The Uncounted Third Loss

The measurement assumes the observation point sees all forty subjects' flows. This
assumption does not hold in the field, and where it fails to hold, a third loss is born.

The analyzer is placed at one point and only sees packets that **pass through** that
point. If two subjects talk to each other on the near side of that point, the traffic
between them is never copied at all — no matter how wide the filter or how large the
buffer. In the same way, an analyzer attached to one port of a switch does not see
traffic exchanged among the other ports; that requires the traffic to be separately
mirrored, and mirroring carries its own limit: if the mirrored port's speed is lower
than the combined speed of the sources, packets drop during mirroring too.

This loss differs from the other two in that it **cannot be estimated by reading the
filter rule.** What the narrow filter excludes is written into the rule itself; the
amount dropped from the buffer can be read off a counter. What the position fails to
see, though, can only be inferred from the topology, and it leaves no trace in the
capture record at all. The three losses share one consequence: **an empty record does
not mean nothing happened there.**

## Narrowing the Losses

The lever's tail cannot be removed; three narrowings make it measurable.

**First narrowing — count the drops.** The capture interface keeps a counter for
dropped packets. The counter is not sampled and is not affected by the filter; if the
record is incomplete, it is the only field that says so. Reading the counter every time
the record is opened gives the field counterpart of the `overflow` column.

**Second narrowing — choose the snap length by the question.** If the full packets in
the measured window had been kept, the total would be **15,032,397** bytes; with a
96-byte snap, it falls to **1,976,196** bytes, roughly one-seventh. The same buffer,
with the same filter, then covers a window seven times as long. If the question is a
flow's existence and timing, this trade is close to free. If the question is content,
the snap must be removed, but then the window must be shortened; the product of the two
settings is constant.

**Third narrowing — write the filter by the oracle, not the question.** In the
measurement above, the narrowest filter missed twenty-one of twenty-two heavy events.
Because which ports the heavy events pass through is unknown, a narrow filter is
gambling. The workable rule is this: the capture filter is written to cover the
subjects **invisible in telemetry**. The previous two lessons were counting who is
invisible; this lesson says where that list gets written.

## Summary

- Capture loses in two places: a packet that does not pass the filter is never copied,
  and a packet that does not fit the buffer drops silently; the two losses are opposite
  directions of the same lever.
- The narrowest filter captures **7** flows and drops no bytes at all, but it leaves
  **91** flows outside, never shows **33** subjects, and misses **21** of 22 heavy
  events.
- Unfiltered capture leaves no flow outside, but **32** flows never fit the buffer and
  **776,196** bytes drop; roughly two-fifths of the snapped total never enters the
  record.
- Even in unfiltered capture, **5** subjects stay invisible: flows that start after the
  buffer fills never enter the record, even though the filter excluded nothing.
- A flow the filter excludes is excluded knowingly; a flow dropped from the buffer
  drops unknowingly; the second loss is more dangerous even when it is smaller, because
  it leaves no trace in the record.
- A third loss sits outside the measurement and comes from the observation point:
  traffic that does not pass through that point is invisible at any filter width, and
  this gap can be read from neither the rule nor a counter — only inferred from the
  topology.

## Next Step

The three measurements so far all looked from **inside** the network: the device's
record, the device's reply, the packet crossing the wire. All three had individual
devices as their subject. The real question asked in operations, though, is asked end
to end — can one end reach the other, and how long does it take? The next lesson builds
this single indicator and asks it backwards: which of the forty subjects does an
end-to-end health value **fail to represent**, and how many subjects stay in the tail
while the indicator reads green?
