skip to content

When an IPv4 NAT rewrites an address, which checksums must it fix, and why must it also rewrite the packet quoted inside ICMP error messages?

level: seniorimportance: nice to knowfreq 16%

answer

  1. the pseudo-header covers both addresses
  2. a zero UDP checksum is left alone
  3. patch the sum, do not recompute
  4. errors quote the translated packet

basics

~20 s

An IPv4 NAT must adjust the IPv4 header checksum and the TCP or UDP checksum, whose pseudo-header includes both addresses. ICMP errors quote the translated packet, so the NAT reverts that quoted copy, or the inside host cannot match the error.

solid answer

~50 s

Changing an IPv4 address invalidates the **IPv4 header checksum**, which covers the header, and the **TCP or UDP checksum**, because both are computed over a **pseudo-header** containing the source and destination addresses, even though no TCP or UDP field changed. A UDP checksum of zero means none was sent, so RFC 3022 says to leave it. Since these are one's-complement sums, the translator **patches** each one by subtracting the old address and adding the new, without reading the payload. ICMP errors such as Destination Unreachable and Time Exceeded carry the offending packet's IP header and first payload bytes, and that copy shows the **translated** addresses. RFC 5508 requires the NAT to revert the quoted headers, keep the type and code, and fix the inner and ICMP checksums; otherwise the inside sender cannot use a Fragmentation Needed error and path MTU discovery fails.

code

pseudocode · 21 lines
pseudocode
function adjust(checksum, old_words, new_words):
    # one's-complement sums let a translator patch, not recompute
    sum = (~checksum) & 0xFFFF
    for w in old_words:               # remove old address words
        sum = ones_add(sum, (~w) & 0xFFFF)
    for w in new_words:               # add translated address words
        sum = ones_add(sum, w)
    return (~sum) & 0xFFFF

function ones_add(a, b):
    s = a + b
    return (s & 0xFFFF) + (s >> 16)   # end-around carry

function rewrite_source(pkt, new_src):
    old, new = words(pkt.ip.src), words(new_src)   # two 16-bit words each
    pkt.ip.checksum = adjust(pkt.ip.checksum, old, new)
    if pkt.proto == TCP:
        pkt.tcp.checksum = adjust(pkt.tcp.checksum, old, new)
    if pkt.proto == UDP and pkt.udp.checksum != 0:  # zero = none sent
        pkt.udp.checksum = adjust(pkt.udp.checksum, old, new)
    pkt.ip.src = new_src

go deeper

for a junior

Remember that a NAT changes more than one field: both the IP header and the TCP or UDP checksum depend on the addresses, so both get adjusted.

for a middle

Explain the pseudo-header, why a zero UDP checksum is left alone, why the ICMP query checksum is untouched, and how one's-complement arithmetic allows an incremental patch.

for a senior

Trace an ICMP error back through the translator: revert the quoted headers, keep type and code, readdress the outer header, and connect a failure here to path MTU black holes.

for a principal

Judge translator correctness by what it does to error signalling, not just data packets, since broken ICMP translation shows up as hangs that look like application faults.

## Which checksums an address rewrite breaks RFC 3022 section 4.1 says that in Basic NAT "the IP header of every packet must be modified": the source address on outbound packets, the destination on inbound ones, and the IP checksum. The rewrite reaches further than the IP header because of what each checksum covers. | Checksum | What it covers | Affected by an address rewrite? | |---|---|---| | IPv4 header checksum | the IPv4 header only (RFC 791) | yes, the address is in the header | | TCP checksum | segment plus a 96-bit IPv4 pseudo-header with source and destination address (RFC 9293) | yes, through the pseudo-header | | UDP checksum | datagram plus a pseudo-header with both addresses (RFC 768) | yes, unless the field is zero | | ICMP checksum (IPv4) | the ICMP message only (RFC 792) | no for queries; yes for errors, whose payload changes | Three details trip people up: - **TCP and UDP must be fixed even when no port changes.** The pseudo-header makes the transport checksum depend on the addresses, so a stale one causes the receiver to discard the segment. - **A UDP checksum of zero stays zero.** In IPv4 an all-zero UDP checksum means the sender computed none (RFC 768); RFC 3022 says such headers should not be modified. - **An ICMP echo is untouched by Basic NAT.** The IPv4 ICMP checksum does not include the IP addresses. NAPT, which also rewrites the query identifier, must fix it. ## Patching instead of recomputing All three checksums are one's-complement sums of 16-bit words, so the effect of one changed field is additive. RFC 3022 section 4.2 gives the method: take the difference between the before and after values and apply it to the existing checksum. A 32-bit IPv4 address is two 16-bit words, so each rewrite costs a few additions per checksum, whatever the packet's length. The pseudocode example shows the arithmetic, including the zero-UDP guard. ## ICMP errors carry a copy of the translated packet An ICMP error message such as **Destination Unreachable** (type 3) or **Time Exceeded** (type 11) includes the IP header of the datagram that caused it plus at least the first 64 bits of its data (RFC 792), enough for the original sender to find the transport ports. Consider source NAT at an office edge: 1. Inside host `10.20.4.31` sends a large TCP segment with DF set to `198.51.100.7`; the edge rewrites the source to `203.0.113.17`. 2. A router beyond the edge cannot forward it without fragmenting and returns Destination Unreachable code 4, **fragmentation needed and DF set**, to `203.0.113.17`. 3. The error's payload quotes the segment as it was seen: source `203.0.113.17`. If the edge only rewrote the outer destination, `10.20.4.31` would receive an error about a packet that, by its records, it never sent. RFC 5508 REQ-4 and RFC 3022 section 4.3 require, for an error arriving from outside with an active mapping: - revert the **embedded** IP and transport headers to their original form, here source `10.20.4.31`; - leave the ICMP type and code unchanged; - set the outer destination to the embedded packet's source after that reversion; - fix the embedded IP header checksum and the ICMP checksum, which covers the changed payload. With no matching mapping, RFC 5508 says the NAT SHOULD silently drop the error. RFC 5508 REQ-5 applies the same treatment to errors from the inside. ## What breaks when a NAT gets this wrong - **Path MTU discovery.** RFC 5382 REQ-9 says a NAT translating TCP SHOULD translate Destination Unreachable, especially type 3 code 4, to avoid the black holes of RFC 2923: connections that open but hang once full-size segments flow. - **Fast failure.** Port and host unreachable errors let TCP give up quickly instead of waiting for timeouts. - **Path tracing.** The classic traceroute relies on Time Exceeded reaching the sender with its probe intact. - **Every translated packet.** A transport checksum left stale makes the receiving host discard each segment, so the session fails at its first packet, which is why checksum adjustment is part of the translation itself rather than an option. One limit stays outside checksum work: addresses written into application payloads, such as FTP's PORT command, are invisible to header translation and need an application-level gateway.

  • Why can a NAT fix a TCP checksum without reading the segment's payload?
    The TCP checksum is a one's-complement sum of 16-bit words, and such sums are additive. Removing the old address words and adding the new ones gives the same result as recomputing over the whole segment. RFC 3022 section 4.2 gives this method, so the cost per packet is a few additions regardless of payload size.
  • What does a NAT do with an inbound ICMP error that quotes a packet it has no mapping for?
    RFC 5508 REQ-4 says it SHOULD silently drop it. Without a mapping it cannot know which inside host sent the quoted packet, so it can neither revert the embedded headers nor address the error, and forwarding it untranslated would deliver nothing useful.

saying these in an interview costs you the question

  • Basic NAT leaves TCP and UDP checksums alone because it never changes ports.
  • Only the outer IPv4 header checksum needs fixing after a NAT rewrite.
  • ICMP errors pass through a NAT unchanged because they belong to no session.
  • A UDP checksum of zero must be recomputed after translation.
  • In IPv4, the ICMP echo checksum covers the IP addresses, so ping needs it fixed.