Skip to content

NAT and CGNAT

NAT and CGNAT configuration lives in the nat view. Public pools are keyed by name and apply on restart, because a pool index is boot-stable and embedded in every outstanding CGNAT block; the policy and DSTNAT lists that reference them apply live. This page covers the whole command set: global settings, pools, the policy list, destination NAT, fragment handling and the diagnostic path.

Global settings

Disabling NAT leaves the fast path as plain forwarding. conntrack-max is the flow-table ceiling, default 1048576. Per-protocol idle timeouts balance table size against how long an idle connection survives. udp-mapping selects the RFC 4787 behaviour — endpoint-independent is the default and is what interactive and peer-to-peer applications need.

cli
system-view
nat enable | disable
nat conntrack-max <count>                # default 1048576
nat timeout <type> <seconds>
nat udp-mapping endpoint-independent|symmetric
nat cgnat-inside <cidr>                  # default 100.64.0.0/10
nat undo <enable|conntrack-max|cgnat-inside|udp-mapping|timeout <type>>

# timeout types:
#   tcp-established  tcp-closing  tcp-close  tcp-syn
#   udp  udp-stream  udp-eim-reserve  icmp

CGNAT public pools

Pools are keyed by name, so order is irrelevant. Block size must be a power of two. Deterministic allocation emits one compliance log per block per RFC 7422; dynamic hands blocks out on demand instead, trading a record per allocation event for better address efficiency where many subscribers sit idle. Pools stage into the candidate and apply on restart — a pool index is boot-stable and embedded in every outstanding block, so the table is rebuilt at boot rather than mutated live. A policy rule cannot reference a brand-new pool until that pool is live.

cli
nat pool add <name> <ip-lo>-<ip-hi> ports <lo>-<hi> \
    block <size> [deterministic|dynamic]
nat pool remove <name>

# deterministic: block computed from the subscriber address
nat pool add pub1 203.0.113.10-203.0.113.20 \
    ports 1024-65535 block 512 deterministic

# dynamic: blocks allocated from the pool on demand
nat pool add pub2 203.0.113.30-203.0.113.40 \
    ports 1024-65535 block 256 dynamic
commit ; save                            # then RESTART

The NAT policy list

An ordered, first-match list handling inside to outside. nat-rule sends a subscriber range to a pool, or bypasses translation entirely. snat rewrites the source to a fixed target. The list applies live on commit.

cli
nat policy add nat-rule <cidr> pool <name>|bypass
nat policy add snat <cidr> to <target>
nat policy insert <N> nat-rule …|snat …   # 1-based index
nat policy delete <N>
nat policy move <N> <M>

nat policy add nat-rule 100.64.0.0/10 pool pub1
commit

Destination NAT

The mirror of the policy list — outside to inside — so a service behind CGNAT can be reached from the Internet. Same editor shape, same live apply. With dport given the form is port-based and to-port defaults to the matched dport. With dport omitted the whole public IP maps to the inside host, which is only valid when that subscriber is the sole owner of the address. Return traffic needs no rule: the first inbound packet creates a conntrack entry whose reply key is the inside-to-peer tuple.

cli
nat dstnat add <rule>
nat dstnat insert <N> <rule>
nat dstnat delete <N>
nat dstnat move <N> <M>

# <rule> = <public-ip> [proto tcp|udp|any] [dport <port>]
#          to <inside-ip> [to-port <port>] [disable]

nat dstnat add 203.0.113.5 proto tcp dport 8080 to 100.64.0.9 to-port 80
commit
show nat dstnat                          # rules + up/down pkts/bytes

Ownership is enforced

The gateway verifies that the inside subscriber genuinely owns the public address and port from its deterministic CGNAT block. A rule pointing at another subscriber's public address simply never forwards, so one customer can never hijack another's. The forward is also inactive while the subscriber is offline. One thing to watch: the forwarded public port is not reserved in the CGNAT port allocator, so choose a service port that the same subscriber's outbound flows will not be assigned.

Source and destination NAT together

Source NAT and destination NAT are two existing engines — there is no separate combined command. The web console presents them as one merged Source / Dest NAT list for convenience and routes each edit back to the underlying engine; on the CLI you use the two commands directly. nat policy is otherwise CGNAT-only, and its snat rows are the source-NAT half of that merged view. Both apply live on commit and save to nat.conf. DSTNAT rules carry live up and down packet and byte counters, bumped through the conntrack entry's recorded rule index; the same counters for snat rules, and richer five-tuple matching on both, are planned.

cli
nat policy add snat <inside-cidr> to <target>   # rewrite source
nat dstnat add <rule>                            # rewrite destination

IPv4 fragment handling

Non-first fragments carry no L4 header, so plain NAPT drops them and the datagram never reassembles — large-UDP applications such as RDP-UDP freeze. Cache mode, the default, records the first fragment's translation and applies the same IP rewrite to the trailing fragments: virtual reassembly per RFC 6888 REQ-11, with no physical reassembly cost. drop is the old behaviour and reassembly is an accepted alias of cache.

cli
cgnat fragments mode <drop|cache|reassembly>   # default cache, live
cgnat fragments timeout <secs>                 # default 30
cgnat fragments rss <enable|disable>           # ON RESTART
show cgnat fragments                           # counters + rss state

Fragment RSS co-location

The fragment cache is per-lcore, so every fragment of a datagram must reach the same worker. By default the NIC hashes the first fragment on the UDP four-tuple, because it has the L4 header, but the trailing fragments on the IPv4 two-tuple — splitting them across workers, so the trailing fragment misses the cache and the datagram is lost. That shows up as roughly 13 to 40 per cent loss on over-MTU UDP, while TCP and sub-MTU UDP are unaffected. Enabling rss drops UDP to the two-tuple so first and trailing fragments co-locate. The trade-off is that UDP between one address pair no longer spreads across workers; TCP keeps its four-tuple spread. Because the RSS hash is fixed when the ports come up, this applies on restart.

Diagnosing a one-way UDP freeze

When a subscriber's TX drops to zero while RX stays high — client sending, replies not arriving — show nat statistics splits the reason at the point a WAN reply is reverse-translated. Translation misses mean the reply hit a public address and port with no mapping, because the outside port was released or reallocated. Stale-mapping drops mean the mapping belongs to a dead session. Expired-mapping hits mean it went idle past its timeout, which is garbage-collection lag. Fragment-miss drops mean an out-of-order fragment arrived with no first fragment cached. And translation hits mean delivered — so if that climbs while TX is still zero, the loss is downstream of NAT.

cli
clear counters nat
# ... let the freeze run about 30 seconds ...
show nat statistics detail        # + recent misses, fragment counters

show nat session [<ip>]           # per-flow conntrack, timeout remaining
show nat flow [<ip>]              # active per-peer connections
show nat                          # conntrack, timeouts, pools, policy