Daily Use

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.


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 seesWhat it really is
10.9.0.20 → 93.184.216.34the conversation
203.0.113.7 → 93.184.216.34the 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) through fw-1 is reported with its real internal source, not fw-1.

Port forward — a published socket lands somewhere inside:

TCP traffic reaching 203.0.113.7:443 on fw-1 is reported as reaching 10.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:

TCP traffic to 203.0.113.7:443 is landing on 10.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:

RelationMatched by your rules?
10.9.0.20 → 93.184.216.34yes
203.0.113.7 → 93.184.216.34yes — a flow your firewall never initiated

After you declare the SNAT rule on fw-1:

RelationMatched by your rules?
10.9.0.20 → 93.184.216.34 via fw-1yes
203.0.113.7 → 93.184.216.34no — absorbed into the conversation above

Concretely, four things change:

  1. One conversation, not two. The two legs share one correlation id and one consolidated row, reported with the real endpoints.
  2. 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.
  3. Flow Matrix rules stop matching the translated leg. Only the conversation as it really happened is offered to your rules, so fw-1 stops appearing as a client of everything on the internet.
  4. 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.

ProofWhat it means
ruleYou declared this translation on the device that performs it.
scopeOne side is a public address, the other private. Self-evident.
map / declaredYour cartography says a host bridges the two networks.
exporter pathLearned 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:

WhereWhat it does
Dismiss a proposalProposals sectionStops the suggestion. Changes nothing about how traffic is correlated.
Invalidate a translationEvidence tableSays “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.xml and 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.