Build a Reversible tc-ct Conntrack and NAT Path
You will finish with a small, understandable packet path using the Linux traffic-control ct action: untracked TCP packets enter conntrack zone 2, a new connection is committed with mark 0xbb and source NAT, and established packets restore the saved NAT before being redirected. The reply path accepts only established traffic and applies reverse NAT.
The route
Jump straight to the step you need, or tick off Done means at the end.
Allow about 30 minutes for a lab test, plus time to inspect the existing traffic-control configuration. The examples are for iproute2 6.1.0, installed here as package iproute2 6.1.0-1ubuntu6.4. You need two interfaces, a test flow you control, conntrack support in the kernel, and root privileges for every command that adds or removes a qdisc or filter. Do not paste these rules into a production router: they redirect packets and can interrupt traffic.
Checkpoint
If you only need to understand the state machine, read through steps 1 to 3. The configuration in steps 4 to 6 changes live packet handling.
1. Check the local contract
Read the installed manual and identify the binary version before relying on syntax. These are read-only commands and do not need elevated privileges:
$ man tc-ct
$ tc -Version
tc utility, iproute2-6.1.0, libbpf 1.3.0
$ dpkg-query -W -f='${Package} ${Version}\n' iproute2
iproute2 6.1.0-1ubuntu6.4
The action has three useful forms. action ct zone ZONE sends a packet to conntrack. action ct zone ZONE commit creates or commits the connection and may set a mark, label or NAT rule. action ct nat restores NAT already associated with the connection. action ct clear removes conntrack state and metadata from the packet, and must be the only option on that action.
Do not confuse a conntrack zone with a firewall table or an interface. The zone is a number used to keep otherwise similar flows separate. Both directions must use the same zone if they are to find the same connection.
2. Record the lab interfaces and existing state
Choose the real interface names and addresses first. This guide uses eth0 for the incoming side and eth1 for the outgoing side, but those names are placeholders. Run these commands without sudo first:
$ ip -br link
$ ip -br address show dev eth0
$ ip -br address show dev eth1
$ tc qdisc show dev eth0
$ tc qdisc show dev eth1
$ tc filter show dev eth0 ingress
$ tc filter show dev eth1 ingress
Save the output somewhere outside this guide if you need to restore a pre-existing setup. In particular, do not assume that an ingress qdisc is empty. A device can have only one ingress qdisc, and replacing or deleting one can remove unrelated filters.
The NAT address in the example, 5.5.5.7, is also a placeholder. It must be an address appropriate for the lab's egress side. A syntactically valid rule with the wrong address can black-hole traffic.
3. Understand the packet path
The first filter handles a packet that is not yet tracked. It attaches zone 2, then jumps to chain 2:
action ct zone 2 pipe action goto chain 2
Chain 2 sees a new tracked flow. The commit action stores mark 0xbb, configures source NAT and passes the packet to a redirect:
action ct zone 2 commit mark 0xbb nat src addr 5.5.5.7 pipe action mirred egress redirect dev eth1
Later packets match the connection mark and established state. The plain nat form applies the previously configured translation:
action ct nat pipe action mirred egress redirect dev eth1
On the other interface, the same first-stage lookup puts untracked replies into zone 2. A filter then permits only an established connection with mark 0xbb and restores the reverse translation with action ct nat.
Trap: ct_state -trk means not tracked, while ct_state +trk+new and ct_state +trk+est describe tracked state. These are flower filter keys, not options to the ct action itself.
4. Add ingress hooks, with an undo plan ready
Warning
The following commands require root and change live packet processing. Run them only on a maintenance or isolated test host. Record the handles printed by your system if you need precise deletion later.
# sudo tc qdisc add dev eth0 ingress
# sudo tc qdisc add dev eth1 ingress
# sudo tc qdisc show dev eth0
# sudo tc qdisc show dev eth1
Expected output includes an ingress qdisc on each device. If either add command says that the qdisc already exists, stop. Inspect its filters and use the existing hook or schedule a controlled replacement; do not delete an unknown qdisc.
If these two qdiscs are the ones you just created and the test must be abandoned, the narrow rollback is:
# sudo tc qdisc del dev eth0 ingress
# sudo tc qdisc del dev eth1 ingress
Deleting an ingress qdisc also deletes filters attached to it. Use that rollback only when you know the qdisc was created for this test.
5. Install the forward and reply rules
Install the forward path first. The commands below are adapted from the installed tc-ct(8) example. They require root and use IPv4 TCP traffic:
# sudo tc filter add dev eth0 ingress prio 1 chain 0 proto ip flower ip_proto tcp ct_state -trk \
action ct zone 2 pipe action goto chain 2
# sudo tc filter add dev eth0 ingress prio 1 chain 2 proto ip flower ct_state +trk+new \
action ct zone 2 commit mark 0xbb nat src addr 5.5.5.7 pipe action mirred egress redirect dev eth1
# sudo tc filter add dev eth0 ingress prio 1 chain 2 proto ip flower ct_zone 2 ct_mark 0xbb ct_state +trk+est \
action ct nat pipe action mirred egress redirect dev eth1
Now install the reply path:
# sudo tc filter add dev eth1 ingress prio 1 chain 0 proto ip flower ip_proto tcp ct_state -trk \
action ct zone 2 pipe action goto chain 1
# sudo tc filter add dev eth1 ingress prio 1 chain 1 proto ip flower ct_zone 2 ct_mark 0xbb ct_state +trk+est \
action ct nat pipe action mirred egress redirect dev eth0
Verify that the filters are present. This is read-only, although the output is host-specific:
$ sudo tc filter show dev eth0 ingress
$ sudo tc filter show dev eth1 ingress
You should see filters in chains 0, 1 and 2, with ct and mirred actions. If a filter fails halfway through, do not keep adding variants. List both devices, identify which rules were accepted, and remove only the rules belonging to this test.
6. Generate one controlled flow and inspect it
From a permitted test client, make one TCP connection that should cross eth0. Keep the destination and timing known so you can distinguish this flow from unrelated traffic. Then inspect the filter counters:
$ sudo tc -s filter show dev eth0 ingress
$ sudo tc -s filter show dev eth1 ingress
Packet and byte counters should increase on the filters that saw the flow. The exact formatting and counter values vary. A zero counter means the packet did not reach that filter, often because the interface direction, protocol, address, or chain assumptions are wrong. It does not prove that conntrack is broken.
The most useful checks are the state transitions: the first packet should match -trk, the new flow should match +trk+new, and later packets should match +trk+est. If the mark does not match, check that the commit filter ran and that the same zone, 2, is used on both interfaces.
7. Remove the test without losing your place
Warning
Deleting these filters stops the forwarding path immediately. Do it only when the test traffic is stopped or when you have an approved maintenance window. Remove filters before deleting the qdiscs:
# sudo tc filter del dev eth0 ingress chain 0
# sudo tc filter del dev eth0 ingress chain 2
# sudo tc filter del dev eth1 ingress chain 0
# sudo tc filter del dev eth1 ingress chain 1
# sudo tc qdisc del dev eth0 ingress
# sudo tc qdisc del dev eth1 ingress
Check that the test configuration is gone:
$ sudo tc qdisc show dev eth0
$ sudo tc qdisc show dev eth1
$ sudo tc filter show dev eth0 ingress
$ sudo tc filter show dev eth1 ingress
These commands remove traffic-control state, not necessarily conntrack entries already created by the kernel. Existing entries age out according to conntrack timeouts. Do not flush the system-wide conntrack table as a casual cleanup step: that can break unrelated connections.
Done means
- You checked the installed iproute2 version and read the local
tc-ct(8)syntax. - You mapped the example interface names and NAT address to a controlled lab.
- You can explain the difference between zone assignment, commit, NAT restore and clear.
- A known TCP flow incremented the expected filter counters.
- You recorded existing qdiscs before making root-only changes.
- The test filters and qdiscs were removed, or an explicit rollback owner and maintenance window remain.