NAT
A firewall rewrites addresses. A flow collector sees the result. Left alone, obserae reports the same conversation twice — once as it really happened, and once again wearing the firewall’s address — and your flow matrix fills up with traffic nobody configured.
This page is where you fix that, by telling obserae what each of your firewalls translates. In return it stops reporting the duplicate, names the device the traffic crossed, and stops matching your Flow Matrix rules against a leg that does not exist.
You do not have to write those declarations from scratch. obserae watches the traffic, reads your firewalls where it can, and proposes them. Your job is to say yes or no.
This guide starts with the five things you need to work the page, then explains what obserae does with what you declare.
- Follow Learn NAT in five steps the first time.
- Come back to What a rule changes when you want to know exactly what a declaration does to your data.
- Use How obserae decides when a translation looks wrong and you need to see its reasoning.
Learn NAT in five steps
1. The problem: one conversation, reported twice
Say a workstation 10.9.0.20 reaches a website 93.184.216.34 through your
firewall fw-1, which owns the public address 203.0.113.7.
If obserae collects flows on both sides of that firewall, it sees:
| What the collector sees | What it really is |
|---|---|
10.9.0.20 → 93.184.216.34 | the conversation |
203.0.113.7 → 93.184.216.34 | the same conversation, after translation |
Nothing in a flow record says those two are the same. So obserae reports two relations, and your flow matrix says your firewall is browsing the web. Every rule you wrote about “who may reach the internet” now matches a leg that is not a real client.
2. A rule says what the device does
A NAT rule is one sentence about one device. There are two kinds, and the page shows each one as the sentence it means:
SNAT — this host is a network’s way out:
Traffic leaving
lan (10.9.0.0/24)throughfw-1is reported with its real internal source, notfw-1.
Port forward — a published socket lands somewhere inside:
TCPtraffic reaching203.0.113.7:443onfw-1is reported as reaching10.9.0.20:8443(srv-web).
Rules live on the host that performs the translation — the firewall as it appears on your cartography — because that is where they live in reality. Manage them all on the NAT page.
3. Accept what obserae proposes
Open the NAT page. Under Proposals you will find translations obserae has noticed but you have not declared, each with the evidence behind it:
TCPtraffic to203.0.113.7:443is landing on10.9.0.20:8443— that is a port forward. 3 translations, 3 clients
Two sources feed that list:
- detected — obserae correlated the two sides of a translation in your traffic. It needs at least two sightings: a single one is as likely to be a coincidence.
- from firewall — obserae read the rule off an OPNsense device you connected. This is the only source that knows about a forward nobody has used yet. See Importing from OPNsense.
Accept turns the proposal into a rule, pre-filled. Dismiss stops the suggestion. Dismissing is not the same as saying “this is not a NAT” — see Dismiss or invalidate?.
If obserae cannot tell which host performs a translation, the accept form opens with an empty host picker and says so. Pick the right one: a rule on the wrong host would quietly orient translations backwards.
4. Declare one by hand
Use Declare a rule when obserae has not seen the traffic yet, or cannot read your firewall. You will be asked for the translating host, then either the network it translates (SNAT) or the published and internal sockets (port forward).
obserae refuses declarations that contradict your map:
- a host with no interface outside the network it claims to translate has no way out, so it cannot be that network’s exit;
- a published socket can only forward to one place, so a second rule on the same socket is a conflict, not an addition.
A rule whose published address is not one of the host’s declared interfaces is accepted with a warning — a virtual IP is legitimate, but obserae cannot confirm it from the map.
5. Turn one off without deleting it
Disable keeps the rule but stops it orienting anything, which is what you want while migrating a service. Delete retracts the declaration entirely. Either takes effect on the next correlation pass — no restart.
What a rule changes in the flow matrix
This is the part worth being precise about, because it changes what your data says.
Take the earlier example: 10.9.0.20 reaches 93.184.216.34 through fw-1.
Before you declare anything, obserae may or may not correlate the two legs (it depends on whether it can prove the direction — see below). If it cannot, you get:
| Relation | Matched by your rules? |
|---|---|
10.9.0.20 → 93.184.216.34 | yes |
203.0.113.7 → 93.184.216.34 | yes — a flow your firewall never initiated |
After you declare the SNAT rule on fw-1:
| Relation | Matched by your rules? |
|---|---|
10.9.0.20 → 93.184.216.34 via fw-1 | yes |
203.0.113.7 → 93.184.216.34 | no — absorbed into the conversation above |
Concretely, four things change:
- One conversation, not two. The two legs share one correlation id and one consolidated row, reported with the real endpoints.
- The traversed device is named. The Sessions riverview labels the ribbon via fw-1, so you can see where the translation happened without hunting for it.
- Flow Matrix rules stop matching the translated leg. Only the conversation
as it really happened is offered to your rules, so
fw-1stops appearing as a client of everything on the internet. - The firewall’s own drawer gets honest. On the map,
fw-1’s recent conversations no longer list the traffic it was merely carrying.
Nothing is hidden. The translation itself stays visible: the Evidence table
on the NAT page shows every matched translation and the addresses involved, and
the translated_client_ip / translated_server_ip columns are queryable in
NFQL.
How obserae decides a translation’s direction
Merging two conversations is only safe if obserae knows which side is the original and which is the translation. It never guesses. Four proofs are tried, strongest first, and the one that decided is recorded so you can audit it — the Proof column on the Evidence table.
| Proof | What it means |
|---|---|
rule | You declared this translation on the device that performs it. |
scope | One side is a public address, the other private. Self-evident. |
map / declared | Your cartography says a host bridges the two networks. |
exporter path | Learned from the order two probes saw the traffic. |
The reason declaring rules matters: some translations can only be proven by a rule. A port forward between two private addresses, both legs seen by a single probe, is invisible to every other proof — scope says nothing (both are private), there is no second probe to compare, and the map only answers questions about the client’s network. Without a rule, that conversation stays split forever.
A declared rule also relaxes how similar the two sides’ volumes must be before
obserae will merge them. Volume agreement is a guard against pairing two
unrelated conversations; once you have stated the translation exists, a firewall
that drops, retries or re-fragments no longer breaks the match. The guard is
loosened, never removed (correlation.nat.rule_min_similarity_pct, default 40 %
against the usual 70 %).
Dismiss or invalidate?
Two different statements, deliberately kept apart:
| Where | What it does | |
|---|---|---|
| Dismiss a proposal | Proposals section | Stops the suggestion. Changes nothing about how traffic is correlated. |
| Invalidate a translation | Evidence table | Says “this is not a real NAT”. The correlator stops merging its two conversations, so both stay separate in the flow matrix. |
Use dismiss when the translation is real but you do not want to declare it. Use invalidate when obserae matched two conversations that have nothing to do with each other — a busy resolver with two similar-looking clients, for instance. Both are durable and both are reversible.
Importing from OPNsense
If you have connected an OPNsense firewall on the Devices page, obserae reads its NAT rules every collection cycle and proposes them alongside the detected ones. Nothing is applied automatically: an imported rule is a proposal until you accept it.
Two limits you need to know, because both make an empty list mean something other than “this firewall does no NAT” — the page says so in words when it applies:
- OPNsense only exposes rules created under Firewall → Automation through
its API. Rules written in the classic firewall GUI live in
config.xmland are invisible to obserae. - The port-forward endpoint requires a recent firmware (OPNsense 26.1 and later). Older builds answer 404, recorded as unavailable.
Rules obserae cannot turn into an exact declaration are skipped rather than
approximated: a port range, an alias such as WAN_address instead of a
literal address, or a missing target. Declaring a socket obserae had to guess
would be worse than declaring nothing.
Rules you disabled on the firewall are read but never proposed — obserae suggests what the device is doing, not what it could do.
Migrating from the old network-level declaration
Earlier versions declared NAT on the network (“this network’s gateway is
that host”). Those declarations are converted to SNAT rules automatically on
upgrade, and your exported cartography bundles keep importing: a network-level
nat_gateway: in YAML still works and lands as a rule on the named host.
Going forward, rules are exported under the host that performs them:
hosts:
- name: fw-1
interfaces:
- {name: lan0, network: lan, ip: 10.9.0.1}
- {name: wan0, network: wan, ip: 203.0.113.7}
nat_rules:
- {type: snat, network: lan}
- type: port_forward
protocol: tcp
external_ip: 203.0.113.7
external_port: 443
internal_ip: 10.9.0.20
internal_port: 8443
Turning detection down
The Automatic detection section at the bottom of the page has two switches:
- NAT correlation — merge the two sides of a translation at all. Off means every leg stays a separate conversation, declared rules included.
- Use rules and the map — let a declared rule, or the map itself, orient a translation a single probe sees both sides of. Off falls back to two-probe evidence only.
Both take effect immediately and are persisted. The tuning knobs behind them
(similarity floors, candidate caps, registry retention) live under
correlation.nat in the configuration file.
Both switches, along with the translations you invalidated and the proposals you
dismissed, travel in the nat: section of the
configuration bundle. They are decisions, not
observations: without them a restore would quietly resurrect a merge or a
suggestion you had already turned down. Rules you declare on a host ride the
cartography: section instead.
Each decision is written the way you would say it — an invalidated translation by its addresses and protocol, a dismissed proposal by the rule it was suggesting — so the file stays readable and reviewable in a git repository:
nat:
enabled: true
use_cartography: true
suppressions:
- nat_type: source
protocol: TCP
original_ip: 192.168.10.20
translated_ip: 203.0.113.7
pre_nat_sampler: 192.168.10.1
post_nat_sampler: 203.0.113.1
reason: asymmetric routing
dismissed_proposals:
- kind: snat
host: nas
cidr: 192.168.0.0/24
reason: not a gateway
There is no identifier to copy around: obserae recomputes it from these fields on import, and the same translation always yields the same one — which is why a decision still applies after a move to another instance, and why you can write one by hand.
Dismissals recorded before obserae stored what a proposal was about carry only an internal identifier. They cannot be written in readable form, so they are left out of the export and the daemon logs how many. Dismiss those suggestions once more and they become portable.
Querying translations
Two NFQL tables expose what the correlator recorded:
nat_relations— one row per distinct translation, with its confidence and the proof that oriented it;nat_translations— the recent per-conversation detail (15-minute window by default).
The conversations table also carries nat_type, nat_confidence_pct and the
translated_* columns. See the NFQL reference for the full schema,
and Sessions for how translated conversations are consolidated.