Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Intermediate
  • 30 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free: 30 minutes

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:

Nine packets through a NAPT router Python · napt.py
# 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

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.

A laptop reaches a server through two NATs: the home router changes its source to 100.72.18.9, then the ISP CGN to 198.51.100.7.Laptop192.168.1.10Home router (NAPT)LAN 192.168.1.1WAN 100.72.18.9ISP carrier-grade NATinside 100.64.0.1public 198.51.100.7Server203.0.113.80:443from 192.168.1.10:51000from 100.72.18.9:40000from 198.51.100.7:61000

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.

  1. A laptop with the private address 192.168.1.10 sends a packet from port 51000.
  2. 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.
  3. 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.
  4. 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:

Subscribers per public address, by port block Python · port_budget.py
# 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

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:

Diagnosing CGN from two addresses and a traceroute Python · cgnat_check.py
# 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

What Is My IP Address See the public IPv4 and IPv6 address that websites receive from your connection, to compare it with your router's WAN address. IP Address Lookup Look up who holds an address and which network routes it; shared and private addresses are recognised as such.

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.

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.

  1. Question 1 of 6 A laptop at 192.168.1.10:51000 connects to 203.0.113.80:443 through a NAPT router whose public address is 198.51.100.7, and the router picks port 40000. Which source does the server see?

    Choose one answer.

    Show the answer to question 1

    Answer: 198.51.100.7:40000

    NAPT rewrites both the source address and the source port on the way out, and keeps the pair in its table. The server only ever sees the router's public address and the port the router chose, and its replies go there.

  2. Question 2 of 6 Which mapping behaviour does RFC 4787 require of a NAT for UDP?

    Choose one answer.

    Show the answer to question 2

    Answer: Endpoint-independent mapping

    REQ-1 of RFC 4787 says a NAT MUST have endpoint-independent mapping: the same inside address and port keep the same public port whatever the destination. Without it, the address a STUN server reports is useless to a peer, and applications fall back to a relay.

  3. Question 3 of 6 Which of these observations point to carrier-grade NAT on a home connection?

    Choose every answer that is right.

    Show the answer to question 3

    Answer:

    • A what-is-my-IP page shows a different address from the router's WAN address
    • The router's WAN address is 100.72.18.9

    RFC 6598 set 100.64.0.0/10 aside for the links between customers' routers and an ISP's CGN, so such a WAN address is a strong sign, and a public address that differs from the router's own means another translator sits in the path. A private address on the LAN is normal for any home network, and a WAN address that servers see is a sign there is no CGN.

  4. Question 4 of 6 Outside the well-known ports, RFC 4787 says a NAT's UDP mapping timer must not expire in less than how many minutes?

    Type a number.

    Show the answer to question 4

    Answer: 2 minutes

    REQ-5 sets the floor at two minutes and recommends a default of five minutes or more. Applications that must receive UDP after long silences send keepalives more often than the shortest timer they expect to meet.

  5. Question 5 of 6 What does TURN do that STUN does not?

    Choose one answer.

    Show the answer to question 5

    Answer: It relays the traffic through a server when no direct path works

    STUN only reports the mapped address and port and checks connectivity. TURN gives the client a relayed address on a server and forwards packets between it and its peers, which works even when both NATs refuse direct traffic, at the cost of the relay's bandwidth.

  6. Question 6 of 6 Why does a port forwarded on the home router stay unreachable behind carrier-grade NAT?

    Choose one answer.

    Show the answer to question 6

    Answer: Connections from the Internet reach the CGN's public address first, and the CGN has no mapping for them

    The router's WAN address, 100.72.18.9 for example, is not reachable from the Internet at all. An incoming connection arrives at the CGN's public address, and without a mapping there (created by outbound traffic or by the Port Control Protocol), the CGN drops it before the home router sees it.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.