Core Question
Core Question
When a reply packet arrives from the internet, how does the router already know where it belongs — and when was that decision actually made?
Outcome
Outcome
By the end of this session, the learner should be able to:
- name the two dictionary keys one
insert()call writes, and what each is for - explain why the reverse-direction key is computed from
entry.translated, notentry.original - trace
match()and say which of its three outcomes a given tuple produces - explain why
remove()must delete two keys instead of one
Read Order
Read Order
- Read
NatEntry - Read
ConntrackTable - Read
MatchDirectionandMatchResult - Read
insert()and its docstring closely - Read
match() - Read
remove() - Run
examples/nat/session_03_walkthrough.py
Read It Like Code
Read It Like Code
NatEntry(
original,
translated,
created_at,
)Fields That Matter
Fields That Matter
| Field | Why it matters |
|---|---|
NatEntry.original | The tuple exactly as the private host sent it. This is the forward-direction key. |
NatEntry.translated | The tuple after rewrite.py ran — what the world outside actually sees. Never used as a key directly; only its reply is. |
NatEntry.created_at | When this mapping was made. Not read by insert()/match()/remove() themselves — it exists for the timeout logic in a later session. |
ConntrackTable.entries | One dict, FiveTuple -> NatEntry. Both keys an insert() writes point at the *same* NatEntry object. |
Decision Flow
Decision Flow
match(table, packet_tuple):
packet_tuple not in table.entries -> NoMatch
packet_tuple == entry.original -> Forward
packet_tuple != entry.original (else) -> ReverseReading Lens
Reading Lens
The important move in this session is to stop asking "how does the router figure out where the reply goes when it arrives?" — because it doesn't figure anything out at arrival time. Ask instead:
- what two keys did
insert()write, back when the *outbound* packet was first seen? - is
reply_tuple(entry.translated)a lookup, or is it the same pure swap from Session 01, computed once and stored? - when
match()returnsReverse, what work is left to do — or was all the work already done?
Toy Model Boundary
Toy Model Boundary
Real conntrack keeps a state machine per flow — TCP handshake progress, half-open/established/time-wait, sequence-number tracking for some helpers — and organizes entries with expiry timers and per-protocol timeout policy, not a single flat dict. This file keeps exactly one entries mapping and no state beyond the two keys and a timestamp, so the pre-computed-reply trick — the actual thesis of this session — is visible without a state machine competing for attention. Timeout handling belongs to a later file (timeout.py) in this track, not to table.py.
Code Landmarks
Code Landmarks
insert() — read the docstring first, then the two lines of code
table.entries[entry.original] = entry table.entries[reply_tuple(entry.translated)] = entry
This is the whole session. One call to insert() writes two adjacent dictionary entries pointing at the same NatEntry:
entries[entry.original]— so a retransmit from the same private host, using the same tuple it always used, matches forward.entries[reply_tuple(entry.translated)]— the reply direction of the *translated* tuple, computed with the exact samereply_tuple()from Session 01. This is the key a reply packet arriving from the internet will present. It exists from the momentinsert()returns, before any reply has ever been seen.
Nothing "learns" the return path when the reply shows up. The lookup that happens on arrival is a plain dict get() against a key that was written earlier.
match()
A dict.get(), then one equality check. If the packet's tuple equals entry.original, it's the same direction the connection started in (Forward). Otherwise — because the only other key ever stored points at this same entry — it must be the reply direction (Reverse). There is no third possibility once you're past the None check.
remove()
Mirrors insert() exactly: pops entry.original and pops reply_tuple(entry.translated). A mapping is created as a pair of keys and it is torn down as the same pair. .pop(key, None) means removing an already-gone key is a no-op, not an error.
Failure Questions
Failure Questions
Use the source file to answer these:
- After a single
insert()call, exactly what twoFiveTuplevalues are keys intable.entries, in terms ofentry.originalandentry.translated? - Why does the second key use
reply_tuple(entry.translated)and notreply_tuple(entry.original)? What would break if it used the wrong one? - Given a tuple that matches a key in
table.entriesbut is not equal toentry.original, what doesmatch()return, and why does it not need a separate check to know that? - What does
match()return for a tuple that was never part of anyinsert()call, and which line ofmatch()produces that outcome? - Why must
remove()pop two keys instead of one? What would the table look like after a "half a remove"?
Walkthrough
Walkthrough
Run this:
PYTHONPATH=src python3 examples/nat/session_03_walkthrough.py
The walkthrough inserts one NatEntry, shows the table now holds two keys pointing at the same entry, matches the original tuple Forward and the reply of the translated tuple Reverse, misses on an unrelated tuple, then removes the entry and confirms both keys are gone.
Done When
Done When
The learner can say all of the following without looking at notes:
- "One
insert()writes two keys: the original tuple for the forward direction, and the reply of the translated tuple for the reverse direction." - "The reply path is pre-computed at insert time with
reply_tuple()— arrival-timematch()never learns anything, it only looks up." - "A mapping is created as a pair of keys and removed as the same pair; there is no such thing as removing half of it."
References
References
- RFC 2663, Section 4.1 (Traditional NAT — the address/port binding that makes return traffic addressable)
- RFC 4787, Requirement 4 (Mapping Refresh — a binding is a single mapping used for both directions of a flow, not two independently discovered ones)
Continue