pedit lets you rewrite a live packet header on the fly, which is exactly as risky as it sounds if you point it at the wrong interface. This guide attaches a pedit action to a traffic-control filter and rewrites one field, such as a TCP port or IPv4 address, using tc-pedit(8) from iproute2 6.1.0. Allow 15 minutes for a disposable test interface, longer if you must map an existing filter and qdisc first.
Warning: packet editing changes live traffic. A mistake can redirect connections, break checksums or affect a whole interface. Test on a network namespace, virtual Ethernet pair or maintenance interface. The examples use IFACE as a placeholder and need root privileges when they change qdiscs or filters.
Confirm which tc will actually run and record the package version before you copy anything from a different release's manual.
$ command -v tc
/usr/sbin/tc
$ tc -V
tc utility, iproute2-6.1.0, libbpf 1.3.0
$ dpkg-query -W iproute2
iproute2 6.1.0-1ubuntu6.4
The local manual describes raw offsets, layered header fields and an extended form. This guide uses named fields, they're easier to review than byte offsets. In this installation the action is written as action pedit, followed by optional ex, munge, a field and an operation. The installed manual's own examples repeat the word pedit a second time after the action name, so keep that spelling if you're following one of those examples literally.
Checkpoint: choose the actual device and inspect its current state before changing anything.
$ IFACE='ensTEST'
$ ip link show dev "$IFACE"
$ sudo tc qdisc show dev "$IFACE"
$ sudo tc filter show dev "$IFACE" ingress
$ sudo tc filter show dev "$IFACE" parent 1:
A filter needs a qdisc, class or block to attach to. The manual uses an HTB root qdisc for transmit traffic and an ingress qdisc for receive traffic. Only run this on an interface whose existing traffic-control setup you're allowed to replace: replace changes the root qdisc and can wipe out a live shaping policy.
# Run only on a disposable or maintenance interface.
$ sudo tc qdisc replace dev "$IFACE" root handle 1: htb
$ sudo tc qdisc add dev "$IFACE" ingress handle ffff:
Confirm both attachment points before you go further:
$ sudo tc qdisc show dev "$IFACE"
qdisc htb 1: root refcnt 2 r2q 10 default 0 direct_packets_stat 0
qdisc ingress ffff: parent ffff:fff1 ----------------
Counters and formatting vary. Look for an HTB root with handle 1: and an ingress qdisc with handle ffff:. If either command fails, stop and fix the qdisc layout rather than bolting filters onto a different parent by guesswork.
This example matches IPv4 packets with destination port 2222 and rewrites that field to 22 on the transmit-side parent. The u32 classifier picks the packets; pedit changes the header once they're selected.
$ sudo tc filter add dev "$IFACE" parent 1: u32 \
match ip dport 2222 0xffff \
action pedit pedit munge ip dport set 22
List the filter and action. Output details vary, so look for a u32 filter and a pedit action rather than matching every character:
$ sudo tc filter show dev "$IFACE" parent 1:
filter protocol ip pref 49152 u32
...
action order 1: pedit action munge ip dport set 22 pipe
The preference number and spacing can differ. If traffic isn't being rewritten, check that packets genuinely match IPv4 destination port 2222 and actually traverse this parent. A pedit action does not create a listener on port 22 or touch any application configuration.
The ex form unlocks extended layered fields and the add and decrement operations, covering IPv4 TTL, IPv6 fields, Ethernet fields, and TCP or UDP fields. This example changes an IPv4 destination address on ingress:
$ sudo tc filter add dev "$IFACE" parent ffff: u32 \
match ip sport 22 0xffff \
action pedit ex munge ip dst set 192.0.2.50
Only use documentation addresses such as 192.0.2.50 in a lab, or a real one where your routing design actually expects it. The same mode allows ip6 dst and eth dst. An edit like this can move traffic to another host or link-layer destination, so verify routes, neighbour discovery and firewall policy before pointing it at a real address.
To knock an IPv4 TTL or IPv6 hop limit down by one, use the operation supported for that field:
$ sudo tc filter add dev "$IFACE" parent 1: u32 \
match ip protocol 6 0xff \
action pedit ex munge ip ttl decrement
The installed manual limits decrement to ip ttl and ip6 hoplimit. Do not assume a software edit and a NIC offload behave identically if hardware offload is part of your design.
retain keeps bits from the existing value while the new value applies to the rest. That matters for the IPv4 DS field, where six bits are commonly used for DSCP and two for ECN. This command sets DSCP to 46 while leaving ECN alone:
$ DSCP=46
$ sudo tc filter add dev "$IFACE" parent 1: u32 \
match ip protocol 6 0xff \
action pedit ex munge ip dsfield set $((DSCP << 2)) retain 0xfc
Keep the arithmetic visible when you review the command. The manual supports retain for fields up to 32 bits, and it is a mask, not a list of bits to clear: 0xfc keeps the six high bits from the set value and leaves the two low bits untouched. Recheck the bit order against a packet capture before this goes anywhere near production.
List the filters, then delete the specific one using the selector and parent you recorded earlier. On a disposable interface, removing the qdiscs afterwards takes the attached filters with them:
$ sudo tc filter show dev "$IFACE" parent 1:
$ sudo tc filter show dev "$IFACE" ingress
$ sudo tc filter del dev "$IFACE" parent 1: pref 10
# Only if this interface had no configuration to preserve:
$ sudo tc qdisc del dev "$IFACE" root
$ sudo tc qdisc del dev "$IFACE" ingress
The preference number in the delete command is an example, not a value to guess: use whatever tc filter show actually reports, plus the classifier or handle if needed.
Safety warning: never delete the root qdisc on a production interface as generic cleanup. Restore saved qdiscs and filters from your change record instead.
tc versions in use.tc filter show.ex was applied only for extended fields or supported arithmetic operations.