Core Question
Core Question
One public IP, one ephemeral port range, thousands of private hosts behind it — how does the NAT box hand out a port that no other flow is using, and what happens the moment it runs out?
Outcome
Outcome
By the end of this session, the learner should be able to:
- state the ephemeral range this toy allocates from, and why its size is the NAT's real capacity limit
- trace
allocate_port()'s scan order, including what happens when it wraps - explain why
release_port()does not make a freed port immediately reusable - name the outcome when the pool is exhausted, and what the caller receives instead of a port
Read Order
Read Order
- Read the module comment above
EPHEMERAL_RANGE - Read
AllocationOutcome - Read
PortAllocator - Read
allocate_port() - Read
release_port() - Run
examples/nat/session_04_walkthrough.py
Read It Like Code
Read It Like Code
PortAllocator(
public_ip,
in_use,
next_candidate,
)Fields That Matter
Fields That Matter
| Field | Why it matters |
|---|---|
public_ip | The pool belongs to one public address. A second public IP gets a second, independent PortAllocator. |
in_use | Every port currently backing a live translation. Membership here is the only thing allocate_port() checks. |
next_candidate | Where the next scan resumes. It only ever advances forward (with wraparound) — it does not rewind to a port that was just freed. |
Decision Flow
Decision Flow
scan from next_candidate, span = EPHEMERAL_RANGE size candidate not in in_use -> claim it, advance next_candidate, return the port (ALLOCATED) candidate in in_use -> try the next offset, wrapping past the top of the range back to the bottom every offset in the span tried and all in_use -> return None (POOL_EXHAUSTED)
Reading Lens
Reading Lens
The important move in this session is to stop thinking of "a free port" as something the allocator searches the whole space for, and start asking:
- where is
next_candidatesitting right now, and does the scan reach the freed port before it wraps? - is this pool's capacity a hard number, or does it just feel unlimited because the walkthrough never fills it?
- what does the caller get back on exhaustion — an exception, a sentinel, or something else?
Toy Model Boundary
Toy Model Boundary
~16k ports = the NAT's capacity per public IP per protocol per destination — the docstring's framing, not this module's code. This toy allocates from one flat EPHEMERAL_RANGE per PortAllocator, with no split by protocol or by destination. Real NAT devices frequently reuse the same public port across different destinations for the same private host (port-address translation / PAT), which multiplies effective capacity far past 16k flows — that reuse dimension does not exist here. There is also no reservation of well-known or registered ports, no per-host quota, and no randomized port selection for security (RFC 6056) — the scan is a deterministic linear probe, which makes the walkthrough's wraparound scenario reproducible but is not how a hardened NAT picks ports in practice.
Code Landmarks
Code Landmarks
The module comment above EPHEMERAL_RANGE
States the capacity framing directly: the ephemeral range is the NAT's capacity, one port per concurrent flow per protocol per destination, until that flow's conntrack entry expires (Session 05).
allocate_port()
One loop, span iterations at most. The modular arithmetic ((alloc.next_candidate - low + offset) % span) is what makes the scan wrap from 65535 back to 49152 instead of stopping at the top of the range. Returning None on a full scan is the only exhaustion signal — there is no exception.
release_port()
The mirror of allocate_port(): a single discard. It does not touch next_candidate, which is why a just-freed port is not the next one handed out — see Failure Question 2.
Failure Questions
Failure Questions
Use the source file to answer these:
allocate_port()scansrange(span)starting atnext_candidate. Ifnext_candidateis65534and both65534and65535are inin_use, what is the very next candidate the loop tries, and why?- A port is freed with
release_port()immediately afternext_candidatehas advanced past it. Why does the next call toallocate_port()not return that freed port? - What does
allocate_port()return when every port inEPHEMERAL_RANGEis inin_use, and how must a caller check for that outcome? PortAllocator.next_candidatedefaults toEPHEMERAL_RANGE[0]. What is the very first portallocate_port()returns on a freshly constructed allocator?- Two different
PortAllocatorinstances share nothing but the sameEPHEMERAL_RANGEconstant. What has to be true about theirpublic_ipfields for that to be safe?
Walkthrough
Walkthrough
Run this:
PYTHONPATH=src python3 examples/nat/session_04_walkthrough.py
The walkthrough allocates three sequential ports from an empty pool, releases the middle one and shows the next allocation skips right past the freed gap, then constructs small artificial in_use sets to force a near-exhausted pool, a fully exhausted pool, and a wraparound scan that finds a gap just below the top of the range before wrapping to the bottom.
Done When
Done When
The learner can say all of the following without looking at notes:
- "The ephemeral range's size is the NAT's real capacity per public IP, per protocol, per destination — not an implementation detail."
- "allocate_port() scans forward from next_candidate and wraps once; a freed port is not reused until the scan comes back around to it."
- "Exhaustion returns None, not an exception — the caller has to check."
References
References
- RFC 4787 (NAT UDP Behavioral Requirements — port allocation and preservation behavior for NAT mappings)
Continue