How NAT works: a rewrite ledger on the gateway
NAT exists because IPv4 addresses ran out: a whole household or office of private addresses (192.168.x.x) must share one public address. The mechanism is a dynamic rewrite ledger kept by the gateway. When an inside device sends a packet out — say 192.168.0.21, port 51234, headed for some server — the gateway rewrites the source to its own public address and a port it picks (99.236.1.10, port 62001) and writes one ledger row: public 62001 ↔ 192.168.0.21:51234, for this destination. Replies arriving at 62001 are looked up in the ledger, rewritten back, and delivered inside. It works exactly like a mail room with extension numbers: outsiders see one street address; the room knows which extension each parcel belongs to.
A side effect falls out for free: a packet arriving from outside with no ledger row is dropped. Unsolicited inbound traffic has nowhere to go — NAT gateways are accidental firewalls.
Ports are a pooled resource. The gateway allocates and releases them like connections from a pool, per public IP and per protocol. Allocation strategy matters: sequential ports are guessable, so random allocation is preferred; port preservation (keep the inside port if free) helps some protocols. Under carrier-grade NAT, one public address is shared by many subscribers, so ports are handed out in per-subscriber blocks. Ledger rows expire after inactivity, which is why long-lived UDP flows send keepalives (WireGuard's is 25 seconds) — the sender is not checking on the peer, it is keeping its own ledger row alive.
The four types are two independent behaviours. Mapping behaviour: when the same inside socket talks to a different destination, does it reuse the same public port (endpoint-independent — the friendly case, historically "full cone"), or get a fresh port per destination address, or per address-and-port pair? Filtering behaviour: once a row exists, who may use it to come back in — anyone, only addresses the insider sent to, or only the exact address-and-port? The hostile corner is symmetric NAT: a fresh public port for every destination, plus strict filtering.
Why symmetric NAT breaks hole punching. Peer-to-peer traversal (STUN) works by each side learning its own public address-and-port and telling the other where to send. Under symmetric NAT, the port a peer learns via STUN was allocated for the STUN server — the port allocated for the actual peer is a different one. The address you advertise is not the address that works, so direct connection fails and traffic must fall back to a relay. This is why mesh VPNs ship relays at all (Tailscale's DERP is one): not as the design, but as the fallback for the NAT type that defeats traversal.
Port forwarding is the manual exception: a permanent, hand-written ledger row — public port X always maps to this inside device — for when a service inside must be reachable from outside without having spoken first.