Computer Networks Module 7 – IPv4 addressing, subnetting and NAT
NAT, port translation and carrier-grade NAT
How a router rewrites addresses and ports so devices share one IPv4 address, why mappings expire, what carrier-grade NAT changes and how STUN and ICE help.
What you will learn
- Trace how a NAPT device rewrites addresses and ports in both directions
- Explain NAT mapping and filtering behaviours and why they matter for peer-to-peer applications
- Recognise carrier-grade NAT (100.64.0.0/10) in traceroutes and explain its effect on port forwarding
- Describe how STUN, TURN and ICE get traffic through NATs
Before you start
What you’ll learn · Before you start
What you will learn
- Trace how a NAPT device rewrites addresses and ports in both directions
- Explain NAT mapping and filtering behaviours and why they matter for peer-to-peer applications
- Recognise carrier-grade NAT (100.64.0.0/10) in traceroutes and explain its effect on port forwarding
- Describe how STUN, TURN and ICE get traffic through NATs
Before you start
On this page
Network address translation (NAT) lets devices with private addresses reach the Internet through a router that rewrites packet headers at the border. In network address and port translation (NAPT), also called port address translation (PAT), many devices share one public IPv4 address. On the way out the router replaces each packet’s private source address and port with its own public address and a port it picks, records the pair in a table, and reverses the change for the replies. Carrier-grade NAT (CGNAT) adds a second translation inside the ISP’s network, so the home router itself only has a shared address from 100.64.0.0/10. This lesson traces a NAPT table packet by packet in Python, explains the mapping, filtering and timer rules that decide which packets get back in, shows how to tell when you are behind carrier-grade NAT, and how STUN, TURN and ICE connect two devices that are both behind NAT.
Why NAT exists
IPv4 has about 4.3 billion addresses, and the central pool ran dry in 2011, when IANA handed its last five /8 blocks to the five regional Internet registries. Long before that, networks used the private-use blocks of RFC 1918 inside, because anyone may use them without asking a registry, but no router on the Internet carries them. NAT joins the two worlds: hosts keep their private addresses, and the border router makes their traffic look as if it came from a public address.
RFC 3022 calls this traditional NAT and describes its sessions as outbound: a host inside starts a connection, and replies come back through the state that the outgoing packets created. A connection that starts outside has nowhere to go unless someone configures a fixed mapping in advance. That one rule explains much of what NAT breaks, from game consoles that cannot host a match to cameras that cannot be reached from a phone.
Basic NAT and NAPT
RFC 3022 defines two variants of traditional NAT:
- Basic NAT rewrites addresses only. Each inside host that talks to the outside borrows one public address from a pool, so the pool must be as large as the number of hosts active at the same time. A connection from outside gets in only through a fixed one-to-one mapping, called static NAT, which suits a server that needs a public address of its own.
- NAPT rewrites the address and the transport identifier: the TCP or UDP port, or the identifier of an ICMP query such as ping. One public address then serves many inside hosts, told apart by the ports the router assigns. A connection from outside gets in only through a fixed port mapping. Home and office routers and carrier-grade NAT all work this way.
A fixed port mapping, such as “public port 8443 goes to 192.168.1.20 port 443”, is what a home router’s port forwarding page creates. It is the only way a connection that starts outside can reach an inside host through a NAPT router.
A NAPT table, packet by packet
The model below does what a home router’s NAPT does, with documentation addresses for the public side. Three inside hosts share 198.51.100.7, and nine packets pass in both directions:
# A NAPT model: inside hosts share the public address 198.51.100.7. Outbound packets create or reuse a mapping
# from (protocol, inside address, inside port) to a public port; replies are translated back; unsolicited packets
# are dropped; a mapping expires when the inside host has sent nothing through it for too long.
PUBLIC = "198.51.100.7"
TIMEOUT = {"udp": 300, "tcp": 7440} # seconds: 5 minutes (RFC 4787) and 2 hours 4 minutes (RFC 5382)
class Napt:
def __init__(self, first_port=40000):
self.next_port = first_port
self.by_inside = {} # (protocol, inside address, inside port) -> public port
self.mappings = {} # (protocol, public port) -> inside endpoint, peers contacted, last outbound time
def expire(self, now):
for key, m in list(self.mappings.items()):
if now - m["last"] > TIMEOUT[key[0]]:
del self.mappings[key], self.by_inside[m["inside"]]
def outbound(self, proto, src, dst, now):
self.expire(now)
key = (proto, *src)
port = self.by_inside.get(key)
note = "reused mapping" if port else "new mapping"
if port is None:
port, self.next_port = self.next_port, self.next_port + 1
self.by_inside[key] = port
self.mappings[(proto, port)] = {"inside": key, "peers": set(), "last": now}
m = self.mappings[(proto, port)]
m["peers"].add(dst[0])
m["last"] = now
return f"as {PUBLIC}:{port} ({note})"
def inbound(self, proto, src, port, now):
self.expire(now)
m = self.mappings.get((proto, port))
if m is None:
return "dropped: no mapping"
if src[0] not in m["peers"]: # address-dependent filtering
return f"dropped: {src[0]} was never contacted"
_, address, inside_port = m["inside"]
return f"to {address}:{inside_port}"
nat = Napt()
events = [
(0, "out", "tcp", ("192.168.1.10", 51000), ("203.0.113.80", 443)),
(1, "in", "tcp", ("203.0.113.80", 443), 40000),
(2, "out", "tcp", ("192.168.1.11", 51000), ("203.0.113.80", 443)),
(3, "out", "tcp", ("192.168.1.10", 51000), ("192.0.2.53", 443)),
(4, "in", "tcp", ("192.0.2.99", 5555), 40000),
(5, "in", "tcp", ("198.18.0.9", 22), 40123),
(10, "out", "udp", ("192.168.1.12", 5000), ("192.0.2.10", 3478)),
(200, "in", "udp", ("192.0.2.10", 3478), 40002),
(600, "in", "udp", ("192.0.2.10", 3478), 40002),
]
for t, way, proto, a, b in events:
if way == "out":
line = f"{a[0]}:{a[1]} -> {b[0]}:{b[1]}", nat.outbound(proto, a, b, t)
else:
line = f"{a[0]}:{a[1]} -> {PUBLIC}:{b}", nat.inbound(proto, a, b, t)
print(f"t={t:<4}{way:<4}{proto} {line[0]}")
print(f"{'':10}{line[1]}") Output
t=0 out tcp 192.168.1.10:51000 -> 203.0.113.80:443
as 198.51.100.7:40000 (new mapping)
t=1 in tcp 203.0.113.80:443 -> 198.51.100.7:40000
to 192.168.1.10:51000
t=2 out tcp 192.168.1.11:51000 -> 203.0.113.80:443
as 198.51.100.7:40001 (new mapping)
t=3 out tcp 192.168.1.10:51000 -> 192.0.2.53:443
as 198.51.100.7:40000 (reused mapping)
t=4 in tcp 192.0.2.99:5555 -> 198.51.100.7:40000
dropped: 192.0.2.99 was never contacted
t=5 in tcp 198.18.0.9:22 -> 198.51.100.7:40123
dropped: no mapping
t=10 out udp 192.168.1.12:5000 -> 192.0.2.10:3478
as 198.51.100.7:40002 (new mapping)
t=200 in udp 192.0.2.10:3478 -> 198.51.100.7:40002
to 192.168.1.12:5000
t=600 in udp 192.0.2.10:3478 -> 198.51.100.7:40002
dropped: no mapping
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 napt.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Read the trace line by line:
- t=0: the laptop’s first packet creates a mapping: 192.168.1.10:51000 becomes 198.51.100.7:40000.
- t=1: the reply arrives for port 40000, matches the mapping and is delivered to 192.168.1.10:51000.
- t=2: a second host uses the same source port, 51000. That is no clash inside, because the addresses differ, but outside both would be 198.51.100.7:51000, so the router gives the second host port 40001.
- t=3: the laptop sends from the same address and port to a different server, and the router reuses port 40000. That is endpoint-independent mapping, explained below.
- t=4 and t=5: a packet from a host the laptop never contacted is dropped, and so is a packet for a port with no mapping at all.
- t=10 to t=600: a UDP mapping is created, a reply after 190 seconds still gets through, and after 300 seconds without an outbound packet the mapping is gone, so the packet at t=600 is dropped.
After t=10 the router’s table holds three mappings, each with the addresses its inside host has sent to:
- TCP 192.168.1.10:51000 ↔ 198.51.100.7:40000, sent to 203.0.113.80 and 192.0.2.53
- TCP 192.168.1.11:51000 ↔ 198.51.100.7:40001, sent to 203.0.113.80
- UDP 192.168.1.12:5000 ↔ 198.51.100.7:40002, sent to 192.0.2.10
Rewriting a header has a cost the model leaves out. The TCP and UDP checksums cover a pseudo-header that contains both IP addresses, so RFC 3022 requires a NAPT router to adjust those checksums as well as the IP header checksum on every packet it translates. Real NATs also follow TCP connection state: RFC 5382 lets a NAT drop an idle established connection only after at least 2 hours and 4 minutes, but a partly open or closing one after 4 minutes.
Mapping and filtering: which packets get back in
RFC 4787 splits a NAT’s behaviour into two independent decisions, and names each instead of using the old labels “full cone”, “restricted cone”, “port restricted cone” and “symmetric” from the first STUN specification, which it found confusing.
Mapping decides which public port an outgoing packet gets:
- Endpoint-independent mapping keeps one public port for an inside address and port, whatever the destination.
- Address-dependent mapping reuses the public port only for the same destination address.
- Address and port-dependent mapping uses a new public port for every destination address and port; this is roughly what the old label “symmetric NAT” described.
Filtering decides which senders may use a mapping once it exists:
- Endpoint-independent filtering lets any host send to the mapped port.
- Address-dependent filtering lets in only addresses that the inside host has sent to.
- Address and port-dependent filtering lets in only the exact address and port pairs it has sent to.
RFC 4787 requires endpoint-independent mapping for UDP (REQ-1), and RFC 5382 requires it for TCP. For filtering it recommends endpoint-independent filtering when applications must work best, and address-dependent filtering when stricter filtering matters more. The model above used address-dependent filtering, which is why the packet at t=4 was dropped.
Mapping matters most to peer-to-peer applications such as calls and games. A device learns its public address and port from a server on the Internet and passes them to its peer. With endpoint-independent mapping the peer can use that address and port, because the NAT keeps them for every destination; both sides send a first packet towards each other, so that each NAT’s filter lets the other side in. With address and port-dependent mapping the NAT opens a different port for the peer than the one the server saw, the peer’s packets miss, and the call has to be relayed through a server instead.
Timers, keepalives and hairpinning
A NAT cannot know when a UDP conversation is over, so it uses a timer. RFC 4787 says a UDP mapping must not expire in less than two minutes (REQ-5, with an exception for some well-known ports), recommends a default of five minutes or more, and requires every outbound packet to restart the timer (REQ-6). Inbound packets may restart it too, but need not, which is why the model ignored them. An application that waits for incoming UDP, such as a call client or a push channel, sends a small keepalive more often than the shortest timer it expects to meet; STUN, among other things, can serve as that keepalive (RFC 8489).
Hairpinning covers a case that looks odd at first: two hosts behind the same NAT, one of which reaches the other through the public address and port of its mapping. RFC 4787 (REQ-9) and RFC 5382 require a NAT to support it, and to make such packets look as if they came from the sender’s public address and port, so that the two hosts behave exactly as they would from outside.
NAT is not a firewall
A NAT drops unsolicited inbound packets only as a side effect: they match no mapping. Once a mapping exists, endpoint-independent filtering admits anyone who sends to it, port forwarding opens a fixed path in, and the Port Control Protocol (RFC 6887) lets an inside application ask the router to open one. With IPv6, devices usually have global addresses and no translation sits in the path, so any protection must come from an explicit firewall; RFC 6092 describes the filtering an IPv6 home gateway can provide. Configure filtering on purpose instead of relying on NAT.
Carrier-grade NAT and 100.64.0.0/10
An ISP that does not have a public IPv4 address for every subscriber can put a large NAT in its own network and share each public address among many subscribers: carrier-grade NAT, or CGN. Each subscriber’s router then sits between two translations, its own and the ISP’s. To number the links between subscribers’ routers and the CGN without clashing with the private addresses customers use at home, RFC 6598 reserved 100.64.0.0/10 as shared address space. Packets with shared addresses must not cross from one provider to another.
Two translations on one connection behind carrier-grade NAT
Text description of the diagram
The diagram is a vertical chain of four boxes joined by arrows, read from the top.
- A laptop with the private address 192.168.1.10 sends a packet from port 51000.
- The home router does NAPT. Its LAN side is 192.168.1.1 and its WAN side has 100.72.18.9, an address from the shared space 100.64.0.0/10. It rewrites the source to 100.72.18.9:40000.
- The ISP's carrier-grade NAT has the inside address 100.64.0.1 and the public address 198.51.100.7. It rewrites the source a second time, to 198.51.100.7:61000.
- The server at 203.0.113.80:443 sees only 198.51.100.7:61000.
Replies retrace the chain: the CGN translates 198.51.100.7:61000 back to 100.72.18.9:40000, and the home router translates that back to 192.168.1.10:51000. A connection that starts on the Internet reaches the CGN first, which has no mapping for it, so a port forwarded on the home router is never used. The public addresses are documentation addresses standing in for real ones.
RFC 6888 adds requirements for CGNs on top of the ordinary NAT rules. A CGN must use the same public address for all of a subscriber’s sessions (“paired” pooling), must let the operator limit how many ports each subscriber may hold, should wait at least 120 seconds before giving a released port to someone else, and must offer subscribers a way to control their mappings, preferably the Port Control Protocol. When a subscriber reaches the limit, the CGN drops the packet that would have created the new mapping. When ports are handed out in fixed blocks, the block size decides how many subscribers one public address can serve:
# How many subscribers can share one public IPv4 address behind a carrier-grade NAT that gives each subscriber a
# fixed block of ports? Client ports map into 1024-65535 (RFC 4787, REQ-3), so 64,512 ports are available.
PORTS = 65536 - 1024
SUBSCRIBERS = 100_000
print(f"{SUBSCRIBERS:,} subscribers")
print(f"{PORTS:,} ports per public address")
print("block per address addresses needed")
for block in [512, 1024, 2048, 4096]:
per_address = PORTS // block
addresses = -(-SUBSCRIBERS // per_address) # round up
print(f"{block:>5} {per_address:>11} {addresses:>16,}") Output
100,000 subscribers 64,512 ports per public address block per address addresses needed 512 126 794 1024 63 1,588 2048 31 3,226 4096 15 6,667
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 port_budget.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Small blocks save addresses but leave a subscriber with few simultaneous connections; large blocks are kinder to busy households and cost the ISP more addresses. With 2,048 ports each, 100,000 subscribers need 3,226 public addresses instead of 100,000.
For subscribers, sharing an address has consequences, several of which RFC 6598 lists from testing:
- Port forwarding stops working. An incoming connection reaches the CGN’s public address first, and the CGN has no mapping for it, so the forward configured on the home router is never used.
- Peer-to-peer and hosting suffer. Some games fail when two subscribers behind the same public address try to connect to each other, and some peer-to-peer applications cannot accept incoming connections.
- The address no longer identifies one customer. Location services find the CGN instead of the home, and websites that limit simultaneous logins per address count everyone behind it together. RFC 6302 therefore asks Internet-facing servers to log the source port and an accurate timestamp, not just the address, so that a CGN operator can find the right subscriber.
To check whether you are behind CGN, compare the WAN address on your router’s status page with the address that websites see. If they differ, another translator sits in the path; if the WAN address is in 100.64.0.0/10, the shared space that RFC 6598 set aside for exactly this link, the translator is the ISP’s CGN. A traceroute may show the same thing as shared-space hops just past your router:
# Are you behind carrier-grade NAT? Compare the WAN address your router reports with the address a server sees,
# and look for shared (100.64.0.0/10) or private hops at the start of a traceroute.
import ipaddress
# Documentation addresses (192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24, RFC 5737) stand in for public ones.
SHARED = ipaddress.ip_network("100.64.0.0/10")
PRIVATE = [ipaddress.ip_network(n) for n in ("10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16")]
def kind(address):
a = ipaddress.ip_address(address)
if a in SHARED:
return "shared (RFC 6598)"
if any(a in n for n in PRIVATE):
return "private (RFC 1918)"
return "public"
def diagnose(wan, seen):
if wan == seen:
return "the router holds the public address"
if kind(wan) == "shared (RFC 6598)":
return "shared WAN address: carrier-grade NAT"
if kind(wan) == "private (RFC 1918)":
return "private WAN address: another NAT in front"
return "servers see another address: another NAT"
for wan, seen in [("198.51.100.23", "198.51.100.23"), ("100.72.18.9", "203.0.113.40"),
("192.168.0.2", "203.0.113.40"), ("198.51.100.23", "203.0.113.40")]:
print(f"WAN {wan:<14} seen as {seen}")
print(f" {diagnose(wan, seen)}")
print("sample traceroute:")
for hop, address in enumerate(["192.168.1.1", "100.72.160.1", "100.64.0.1", "203.0.113.17", "203.0.113.90"], 1):
print(f" {hop} {address:<14} {kind(address)}") Output
WAN 198.51.100.23 seen as 198.51.100.23 the router holds the public address WAN 100.72.18.9 seen as 203.0.113.40 shared WAN address: carrier-grade NAT WAN 192.168.0.2 seen as 203.0.113.40 private WAN address: another NAT in front WAN 198.51.100.23 seen as 203.0.113.40 servers see another address: another NAT sample traceroute: 1 192.168.1.1 private (RFC 1918) 2 100.72.160.1 shared (RFC 6598) 3 100.64.0.1 shared (RFC 6598) 4 203.0.113.17 public 5 203.0.113.90 public
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 cgnat_check.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
GATE CS
The GATE 2027 syllabus for Computer Science names “Network Address Translation” in its IPv4 topics, next to CIDR notation and fragmentation. The NAPT table, the two directions of translation and the 100.64.0.0/10 shared block in this lesson cover that item.
Getting through: STUN, TURN and ICE
When both ends of a call or a game are behind NAT, neither can simply connect to the other. Three protocols, used together, solve most cases:
- STUN (RFC 8489) lets a device ask a server on the Internet which address and port its request came from: the public mapping its NAT created, called the server-reflexive address. STUN also checks connectivity and keeps mappings alive. Its own specification stresses that it is a tool for NAT traversal, not a solution by itself.
- TURN (RFC 8656) gives a device a relayed address on a server and forwards packets between that address and the device’s peers. It works even when both NATs refuse direct traffic, at the cost of the relay’s bandwidth and an extra hop of delay.
- ICE (RFC 8445) puts them together. Each side gathers its candidates: host candidates (its own addresses), server-reflexive candidates from STUN and relayed candidates from TURN. The two sides exchange the lists through the application’s own signalling channel, test candidate pairs with STUN checks, and use the best pair that works.
ICE ranks the pairs with type preferences, and RFC 8445 recommends 126 for host candidates, 110 for peer-reflexive candidates found during the checks, 100 for server-reflexive candidates and 0 for relayed candidates. A direct path therefore always wins when it works, and the relay is the last resort. With endpoint-independent mapping, the server-reflexive candidate usually works; behind address and port-dependent mapping on both sides, the relayed candidate is often the only one left.
Interview questions
What is the difference between basic NAT and NAPT?
Basic NAT translates only addresses: each inside host that is talking to the outside borrows a public address from a pool, so the pool must be as large as the number of hosts active at once. NAPT, also called port address translation (PAT), translates the address and the transport port (or the ICMP query identifier), so many inside hosts share one public address. The router keeps a table from each inside address and port to a public port and rewrites both on the way out and back. RFC 3022 defines both as the two variants of traditional NAT. Home routers use NAPT, because a home usually gets a single public address, or only a shared one behind carrier-grade NAT.
Key takeaways
- NAPT rewrites the source address and port of outgoing packets, keeps the pair in a table and reverses it for replies; connections that start outside need a fixed mapping.
- RFC 4787 separates mapping (which public port) from filtering (who may send to it) and requires endpoint-independent mapping.
- UDP mappings may expire after two minutes of silence (five or more is the recommended default), so receivers send keepalives; established TCP mappings last at least 2 hours and 4 minutes.
- NAT is not a firewall: filtering, port forwarding and PCP decide what gets in, and IPv6 needs an explicit firewall.
- Carrier-grade NAT shares one public address among many subscribers over the 100.64.0.0/10 shared space; port forwarding on the home router then cannot work.
- STUN finds the public mapping, TURN relays, and ICE tests candidate pairs and prefers direct paths over the relay.
Exercise
Exercise · Medium · Python
Translate packets through a NAPT router
Complete the class Napt in napt.py, a model of a router that shares one public IPv4 address.
A packet is a dictionary such as {"proto": "udp", "src": ("192.168.1.10", 5000), "dst": ("192.0.2.10", 3478)}, with an address and a port for each end. now is the time in seconds.
- Napt(public, first_port=40000, filtering="endpoint-independent", udp_timeout=300, tcp_timeout=7440) sets up the router. filtering is one of "endpoint-independent", "address-dependent" and "address-and-port-dependent". - outbound(packet, now) returns a new packet whose source is (public, port). The mapping is endpoint-independent: the same protocol, inside address and inside port always get the same public port, whatever the destination. A new mapping takes first_port, the next one first_port + 1, and so on. - inbound(packet, now) returns a new packet whose destination is the inside address and port, or None when the router drops it: when no mapping has that protocol and public port, or when the filtering refuses the sender. Address-dependent filtering admits only addresses the mapping has sent to; address-and-port-dependent filtering only exact address and port pairs it has sent to. - A mapping expires when its inside host has sent nothing through it for more than the timeout of its protocol. Only outbound packets keep a mapping alive. An expired mapping is gone: an outbound packet afterwards creates a new one with the next port.
Do not change the packets you are given. The sample tests import Napt from napt.py.
Starter code · napt.py
class Napt:
"""A NAPT router that shares the address `public` among inside hosts."""
def __init__(self, public, first_port=40000, filtering="endpoint-independent", udp_timeout=300, tcp_timeout=7440):
self.public = public
# Replace this line with your code.
def outbound(self, packet, now):
"""Translate a packet from an inside host; return the packet as it leaves."""
return packet
def inbound(self, packet, now):
"""Translate a packet sent to the public address; return it, or None when it is dropped."""
return None The sample tests · test_napt.py
from napt import Napt
def packet(proto, src, dst):
return {"proto": proto, "src": src, "dst": dst}
def test_out_and_back():
"""rewrites the source on the way out and the destination on the way back"""
nat = Napt("198.51.100.7")
out = nat.outbound(packet("tcp", ("192.168.1.10", 51000), ("203.0.113.80", 443)), now=0)
assert out == packet("tcp", ("198.51.100.7", 40000), ("203.0.113.80", 443))
back = nat.inbound(packet("tcp", ("203.0.113.80", 443), ("198.51.100.7", 40000)), now=1)
assert back == packet("tcp", ("203.0.113.80", 443), ("192.168.1.10", 51000))
def test_ports_per_inside_endpoint():
"""gives each inside endpoint its own port and reuses it for every destination"""
nat = Napt("198.51.100.7", first_port=50000)
a = nat.outbound(packet("udp", ("192.168.1.10", 5000), ("192.0.2.10", 3478)), now=0)
b = nat.outbound(packet("udp", ("192.168.1.11", 5000), ("192.0.2.10", 3478)), now=1)
c = nat.outbound(packet("udp", ("192.168.1.10", 5000), ("198.51.100.99", 9000)), now=2)
d = nat.outbound(packet("tcp", ("192.168.1.10", 5000), ("192.0.2.10", 443)), now=3)
assert [p["src"][1] for p in (a, b, c, d)] == [50000, 50001, 50000, 50002]
def test_unknown_port_is_dropped():
"""drops packets for a public port that has no mapping"""
nat = Napt("198.51.100.7")
assert nat.inbound(packet("udp", ("192.0.2.10", 3478), ("198.51.100.7", 40000)), now=0) is None
nat.outbound(packet("udp", ("192.168.1.10", 5000), ("192.0.2.10", 3478)), now=1)
assert nat.inbound(packet("tcp", ("192.0.2.10", 3478), ("198.51.100.7", 40000)), now=2) is None
def test_filtering_behaviours():
"""admits other senders according to the filtering behaviour"""
results = {}
for filtering in ["endpoint-independent", "address-dependent", "address-and-port-dependent"]:
nat = Napt("198.51.100.7", filtering=filtering)
nat.outbound(packet("udp", ("192.168.1.10", 5000), ("192.0.2.10", 3478)), now=0)
same_address = nat.inbound(packet("udp", ("192.0.2.10", 3479), ("198.51.100.7", 40000)), now=1)
other_address = nat.inbound(packet("udp", ("192.0.2.99", 3478), ("198.51.100.7", 40000)), now=2)
results[filtering] = (same_address is not None, other_address is not None)
assert results == {
"endpoint-independent": (True, True),
"address-dependent": (True, False),
"address-and-port-dependent": (False, False),
}
def test_timeouts():
"""expires idle mappings after the timeout of their protocol"""
nat = Napt("198.51.100.7")
nat.outbound(packet("udp", ("192.168.1.10", 5000), ("192.0.2.10", 3478)), now=0)
nat.outbound(packet("tcp", ("192.168.1.10", 5000), ("192.0.2.10", 443)), now=0)
assert nat.inbound(packet("udp", ("192.0.2.10", 3478), ("198.51.100.7", 40000)), now=300) is not None
assert nat.inbound(packet("udp", ("192.0.2.10", 3478), ("198.51.100.7", 40000)), now=301) is None
assert nat.inbound(packet("tcp", ("192.0.2.10", 443), ("198.51.100.7", 40001)), now=7000) is not None
renewed = nat.outbound(packet("udp", ("192.168.1.10", 5000), ("192.0.2.10", 3478)), now=302)
assert renewed["src"] == ("198.51.100.7", 40002)
def test_outbound_keeps_it_alive_and_input_unchanged():
"""refreshes on outbound packets only and never changes the given packet"""
nat = Napt("198.51.100.7", udp_timeout=60)
first = packet("udp", ("192.168.1.10", 5000), ("192.0.2.10", 3478))
nat.outbound(first, now=0)
assert first == packet("udp", ("192.168.1.10", 5000), ("192.0.2.10", 3478))
nat.inbound(packet("udp", ("192.0.2.10", 3478), ("198.51.100.7", 40000)), now=50)
nat.outbound(first, now=55)
assert nat.inbound(packet("udp", ("192.0.2.10", 3478), ("198.51.100.7", 40000)), now=110) is not None
assert nat.inbound(packet("udp", ("192.0.2.10", 3478), ("198.51.100.7", 40000)), now=116) is None A hint
Keep two dictionaries: one from (proto, inside address, inside port) to the public port, and one from (proto, public port) to everything you know about the mapping (the inside endpoint, the endpoints it has sent to and the time of its last outbound packet). Remove expired mappings from both at the start of outbound and inbound. dict(packet, src=...) makes a changed copy without touching the original.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Check yourself
6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
References
- RFC 3022: Traditional IP Network Address Translator (Traditional NAT) (RFC Editor (IETF))
- RFC 4787: Network Address Translation (NAT) Behavioral Requirements for Unicast UDP (RFC Editor (IETF))
- RFC 5382: NAT Behavioral Requirements for TCP (RFC Editor (IETF))
- RFC 6598: IANA-Reserved IPv4 Prefix for Shared Address Space (RFC Editor (IETF))
- RFC 6888: Common Requirements for Carrier-Grade NATs (CGNs) (RFC Editor (IETF))
- RFC 6302: Logging Recommendations for Internet-Facing Servers (RFC Editor (IETF))
- RFC 6887: Port Control Protocol (PCP) (RFC Editor (IETF))
- RFC 6092: Recommended Simple Security Capabilities in Customer Premises Equipment (CPE) for Providing Residential IPv6 Internet Service (RFC Editor (IETF))
- RFC 8489: Session Traversal Utilities for NAT (STUN) (RFC Editor (IETF))
- RFC 8656: Traversal Using Relays around NAT (TURN): Relay Extensions to Session Traversal Utilities for NAT (STUN) (RFC Editor (IETF))
- RFC 8445: Interactive Connectivity Establishment (ICE): A Protocol for Network Address Translator (NAT) Traversal (RFC Editor (IETF))
- Free Pool of IPv4 Address Space Depleted (Number Resource Organization (NRO))
- GATE 2027 syllabus: Computer Science and Information Technology (IIT Madras)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress