---
title: 'Address Translation'
source: 'https://academia.sh/en/courses/linux-network-administration/address-translation'
course: 'Linux Network Administration and Troubleshooting'
language: en
updated: '2026-08-17T18:09:58+00:00'
license: 'CC BY-SA 4.0'
---

# Address Translation

The surface that port forwarding opens outward is counted point by point: four rules open thirteen points and reach eight pairs today, the translated log line eliminates zero of twelve candidate sources, and at the six-hundredth second only eleven of forty translation entries remain resolvable.

The previous lesson counted how a machine could lock its own door: forty of the hundred and
twenty orderings of five steps left the administrator outside. This lesson looks the other way.
When the same machine stands at a network's exit, it does not only hold the door; it speaks on
behalf of the machines behind it and carries some inbound connections in.

Every forwarding rule opens a window next to the door it closes. The course's last lesson counts
those windows and measures the damage they do to diagnosis. This lesson does not give an
instruction in the form "open this"; it writes what opening means in **how many pairs**.

## The Gateway's Two Jobs

**Address translation** (NAT) is the changing, at the gateway, of the address or port fields in
a packet's header. It has two directions. **Source translation** applies to packets going out
from inside: the internal host's address is replaced with the gateway's address, and the gateway
keeps a translation entry so it can send the returning packet back to the correct internal host.
**Destination translation** applies to packets coming in from outside: a packet arriving at a
port on the gateway is forwarded to a point on an internal host. The second is called **port
forwarding**.

The types of translation, the translation table's five-component key, its capacity limit, and
the breaking of end-to-end connectivity were established in the **Computer Networks** course and
are not repeated here. This lesson asks two things: how large is the surface this rule opens,
and what information does this rule erase.

Placement is also this course's subject. Source translation happens at the stop **after** the
routing decision; destination translation, at the stop **before** it. Because destination
translation runs first, filtering rules see the packet in its **translated** form; an operator
who writes a rule against the external address matches nothing, and there is no error message to
show for it.

## Surface, Identity, and the Record

The setup is this: an internal network sits behind a gateway, and four forwarding rules are
written.

- **EF41** — The internal network has **12** hosts and **5** services per host; the internal
  (host, service) pair count is **60**.
- **EF42** — With no translation, the internal pairs directly reachable from outside are **0**.
- **EF43** — Four rules are written: three open a single point each, one opens a **10**-point
  range.
- **EF44** — On the host targeted by the range rule, **5** services are listening today; the
  remaining five points are closed today, and the rule covers them too.
- **EF45** — The oracle is the true source host, and `internal-host-07` is chosen.
- **EF46** — The target server's log writes a single source field; with translation in place,
  this field is the gateway's address.
- **EF47** — A translation entry drops **120 seconds** after the connection closes.
- **EF48** — The window is **600** seconds and there are **40** connections; connection `i`
  starts at multiples of the fifteenth second, and its duration is `30 + (i mod 7) * 10` seconds.
- **EF49** — The candidate set sweep is done with internal host counts of 8, 12, and 16.
- **EF50** — All numbers are exact counts; there is no randomness and no seed.

```python
"""Address translation: the surface opened, the loss of source identity, the record's lifetime."""
INTERNAL_HOSTS, SERVICES = 12, 5
TIMEOUT, WINDOW, CONNECTIONS = 120, 600, 40
RULE = (("single-a", 1, 1), ("single-b", 1, 1), ("single-c", 1, 1),
         ("range-d", 10, 5))

print("internal network:", INTERNAL_HOSTS, "hosts x", SERVICES, "services =",
      INTERNAL_HOSTS * SERVICES, "internal (host, service) pairs")
print("pairs directly reachable from outside (no translation):", 0)
print()
print("rule        points opened  listening service  reached pair  open points")
open_points = listening = 0
for ad, points, listens in RULE:
    open_points += points
    listening += listens
    print(f"  {ad:10s} {points:12d} {listens:16d} {listening:14d} {open_points:11d}")
print(f"  {'total':10s} {open_points:12d} {listening:16d} {listening:14d} {open_points:11d}")
print("  surface that can grow without the rule changing:", open_points - listening, "pairs")
print()
ORACLE = "internal-host-07"
CANDIDATE = tuple(f"internal-host-{i:02d}" for i in range(1, INTERNAL_HOSTS + 1))


def log_line(source, translated):
    return "gateway" if translated else source


def translation_table(source, readable):
    return source if readable else "no-record"


print("reading                         distinct answers  eliminated  remaining")
for ad, f in (("target's log (with translation)",
               lambda k: log_line(k, True)),
              ("target's log (without translation)",
               lambda k: log_line(k, False)),
              ("translation table (while the record is alive)",
               lambda k: translation_table(k, True)),
              ("translation table (after the record drops)",
               lambda k: translation_table(k, False))):
    answer = f(ORACLE)
    remaining = sum(1 for a in CANDIDATE if f(a) == answer)
    print(f"  {ad:38s} {len({f(a) for a in CANDIDATE}):5d} {len(CANDIDATE) - remaining:7d}"
          f" {remaining:6d}")
print()
CONNECTION_LIST = [(15 * i, 30 + (i % 7) * 10) for i in range(CONNECTIONS)]
print(f"translation entry: {CONNECTIONS} connections, {WINDOW}-second window,"
      f" the record drops {TIMEOUT} seconds after closing")
print("  look time  started  still resolvable  ratio")
for T in (0, 100, 300, 600):
    started = [(b, s) for b, s in CONNECTION_LIST if b <= T]
    resolvable = [1 for b, s in started if b + s + TIMEOUT > T]
    ratio = round(len(resolvable) / len(started), 4) if started else 0.0
    print(f"  {T:9d} {len(started):9d} {len(resolvable):17d} {ratio:9.4f}")
print()
print("candidate set sweep: internal host count changes")
for n in (8, 12, 16):
    a = tuple(f"internal-host-{i:02d}" for i in range(1, n + 1))
    remaining = sum(1 for x in a if log_line(x, True) == "gateway")
    print(f"  {n:2d} internal hosts -> translated log eliminated {len(a) - remaining:2d}"
          f" remaining {remaining:2d} | untranslated log eliminated {len(a) - 1:2d}"
          f" remaining {1:2d}")
```

```
internal network: 12 hosts x 5 services = 60 internal (host, service) pairs
pairs directly reachable from outside (no translation): 0

rule        points opened  listening service  reached pair  open points
  single-a              1                1              1           1
  single-b              1                1              2           2
  single-c              1                1              3           3
  range-d              10                5              8          13
  total                13                8              8          13
  surface that can grow without the rule changing: 5 pairs

reading                         distinct answers  eliminated  remaining
  target's log (with translation)            1       0     12
  target's log (without translation)        12      11      1
  translation table (while the record is alive)    12      11      1
  translation table (after the record drops)     1       0     12

translation entry: 40 connections, 600-second window, the record drops 120 seconds after closing
  look time  started  still resolvable  ratio
          0         1                 1    1.0000
        100         7                 7    1.0000
        300        21                12    0.5714
        600        40                11    0.2750

candidate set sweep: internal host count changes
   8 internal hosts -> translated log eliminated  0 remaining  8 | untranslated log eliminated  7 remaining  1
  12 internal hosts -> translated log eliminated  0 remaining 12 | untranslated log eliminated 11 remaining  1
  16 internal hosts -> translated log eliminated  0 remaining 16 | untranslated log eliminated 15 remaining  1
```

## The Opened Surface

The table above counts, point by point, the surface the four rules open. With no translation,
the internal pairs reachable from outside are **0**; none of the sixty pairs in the internal
network can be reached. The three single-point rules raise this to three, the fourth rule opens
a ten-point range, and the pairs reached today come to **8**.

The gap between the two numbers is this section's real subject. Open points: **13**. Pairs
reached: **8**. The **5** pairs in between are closed today because no service listens on those
points; the day a service starts on that host, they become reachable **without the rule
changing**. A rule written as a range does not define today's surface — it defines the **largest
future surface**. The number that must be measured is not eight, it is thirteen.

This ties the choice between a single-point rule and a range rule to a single number. When the
same job is written as five single-point rules instead of one range rule, the open-point count
drops from thirteen to eight, and today's function does not change.

## Three Numbers

**Oracle:** the true source, `internal-host-07` — one of twelve internal hosts. **Test:** the
target server's log writes a single source field, and with translation in place, the gateway's
address sits there; for twelve hosts, **a single answer**. **Eliminated candidates:** translated
log **0**, untranslated log **11**, translation table while the record is alive **11**, after the
record drops **0**.

This zero is the course's rule in its shortest form. The log line exists, it is readable, it is
correct, and it **eliminates no candidate**. Source translation reduces twelve distinct
identities to a single one; every line on the target side says the same thing. A record's
existing is not enough to count as a measurement; **output whose diagnostic power is not tested
counts as unmeasured.**

The bottom table shows the sweep. As the internal host count rises from eight to sixteen, the
candidates the translated log eliminates stay fixed at **0**, while the remaining count rises
from eight to sixteen. A reading with zero elimination power grows even less useful as the
candidate set grows.

## The Record's Lifetime

The only place that can bring identity back is the translation table, and that table is a memory
structure, not a log. A record lives while the connection is open and drops one hundred twenty
seconds after it closes.

Looked at the hundredth second, **seven** of the seven connections that had started are
resolvable; the ratio is **1.0000**. At the three-hundredth second, **twelve** of twenty-one; the
ratio is **0.5714**. At the six-hundredth second, only **eleven** of forty; the ratio is
**0.2750**. An operator looking with a half-hour delay cannot recover the source of
three-quarters of the connections from anywhere.

**Evidence does not wait.** The only way to keep the source permanently is to write a log line at
the moment of translation, and that is a setting; it must be turned on before the fault. When it
is not on, what remains is the target's log, and that log eliminates zero of twelve candidates.

## Summary

- Source translation runs at the stop after the routing decision, destination translation at the
  stop before it; filtering rules see the packet in its translated form, and a rule written
  against the external address does not match.
- Four forwarding rules open **13** points, and today **8** pairs are reached; the **5** pairs in
  between can open in the future without the rule changing. A range rule defines not today's
  surface but the largest future surface.
- The translated log line eliminates **0** of twelve candidate sources; the untranslated log and
  a live translation entry eliminate **11**.
- A translation entry drops 120 seconds after closing: resolvability is **1.0000** at the 100th
  second, **0.5714** at 300, **0.2750** at 600.
- When the internal host count rises from eight to sixteen, the candidates the translated log
  eliminates stay fixed at **0**, and the remaining count rises from eight to sixteen.

## Course Wrap-Up

This course asked one question thirteen times: **how many candidate causes does a test
eliminate.** In every lesson three numbers stood side by side — the oracle, the answer the test
gives, and the count of eliminated candidate causes. The course's rule was this: a test's number
is not the answer it gives but the candidate causes it eliminates; **a test whose elimination
count is not written down counts as unmeasured.**

The table below gathers the measure of all thirteen lessons in one place. **No row left blank has
been made up**: each row is written only from its own source lesson, and those lessons were not
produced in this batch.

| Lesson | Oracle | Test | Eliminated candidates / cost |
|---|---|---|---|
| Interfaces and Addresses | 8 candidate causes, 8 tests | interface test **3 distinct answers** | **6 candidates** remain in the worst case |
| The Routing Table | same 8 candidates | route test **3 answers** | worst case **5 candidates**; `wrong-gateway` and `missing-route` are not separated |
| Name Resolution | same 8 candidates | name resolution **2 answers** | `unresolved` eliminates **7**, `resolved` only **1** — the course's largest answer gap |
| Ports and Listening Services | same 8 candidates | listening list **2 answers** | only **1** candidate on the expected answer; connection attempt **7** at once |
| The Layered Diagnosis Method | `firewall-dropping` | connection attempt `timeout` | **7 eliminated, 1 remaining**; layered **33**, reverse **38**, greedy **10** (unreachable lower bound) |
| Reachability Tests | `wrong-gateway` | remote echo `no-response` | **4 eliminated, 4 remaining**; the echo **responding** also leaves 4 candidates standing |
| Packet Capture | `firewall-dropping` | unfiltered capture **14,400 lines** | **6 eliminated, 2 remaining**; elimination per line **0.0004** against **7.0000** for the connection attempt |
| Common Fault Patterns | `resolver-silent` | name resolution `unresolved` | **7 eliminated, 1 remaining**; intermittent fault 84 s, capture **42/84** at 300, **13/84** at 500 |
| Secure Shell | 9 candidates, `public-key-unauthorized` | verbose client 46 lines, verbose server log 3 lines | **5 / 7**; the listening list eliminates **1** in 12 lines; seven tests leave **2** indistinguishable groups |
| Tunneling and the Jump Server | 10 candidates, `target-firewall-dropping` | end-to-end **7** distinct answers, jump-to-target **5** | **8 / 9**; layered order **34**, end-to-end-first **28** steps; tunnel entry point **0** and **72** indirect pairs |
| The Packet-Filtering Framework | the decision is made by **policy** at the `input-filter` stop | rule list **34** lines, open tracing **12** lines | **0 / 8**; loaded counter **0**, quiet counter **6**; unreached hops **20** |
| Firewall Rules | **40** of 120 orders lock out, the final set is the same across all | dry run **6** answers, end-to-end attempt **4** answers | **5 / 5**, grouped together **0**; dry run prevents **0** lockouts, atomic load **40** |
| Address Translation | true source `internal-host-07` | translated log **1** distinct answer | **0**, remaining **12**; resolvability at the 600th second **0.2750** |

The five lessons' shared result is a single pattern. At the access layer there is no link between
the volume of output and its elimination power: the forty-six-line verbose client output
eliminated five candidates, the three-line server log eliminated seven, the thirty-four-line rule
list eliminated none, and the one-line translated log also eliminated none. Elimination power is
not in the line count, it is in **how many distinct answers** the output can produce.

The second pattern concerned the candidate set. In every lesson the list was grown and shrunk by
one candidate, and the result pointed the same way each time: the elimination count stayed fixed
while the remaining ambiguity grew. This is why, throughout this topic, the measure used was not
elimination but **remaining** and **indistinguishable group**.

The third pattern concerned time, and it took its harshest form here. With tracing off, a dropped
packet's trace was zero lines; the log a locked-out administrator would need to read was inside
the machine; the translation entries had fallen to eleven of forty by the six-hundredth second.
In all three, the evidence depended on a setting, and that setting had to be turned on before the
fault. **Evidence does not wait.**

Throughout this topic, destructive and lockout-causing commands were never written in full form,
no real key or fingerprint was written, and address translation was explained not as "open this"
but as **how many pairs get opened**. This was not a teaching restriction, it was the measurement
itself: an open surface is a countable quantity.

The next course, **Kernel Interfaces and Isolation**, enters the place this course deliberately
left out. Here, a machine had a single network stack; there, namespaces set up multiple network
stacks on the same machine, and every interface, every routing table, and every rule chain from
this course **multiplies**. Container networking, namespaces, and control groups were not covered
in this course and were left for there. The same question will be asked there too: how many
candidate causes does a test eliminate. What makes the answer harder is that the candidate set is
now split into multiple copies even inside a single machine.
