TC-PEDIT(8)
โจ TC-PEDIT(8) โ the pedit action, bestie ๐
okay so pedit is the generic packet editor action ๐ฆ It lets you change arbitrary packet data, like literally rewriting bytes mid-flight. ngl it's giving main character energy ๐
๐ NAME
pedit โ generic packet editor action ๐
๐งฉ SYNOPSIS
tc ... action pedit [ex] munge { RAW_OP | LAYERED_OP |
EXTENDED_LAYERED_OP } [ CONTROL ]
RAW_OP := offset OFFSET { u8 | u16 | u32 } [ AT_SPEC ] CMD_SPEC
AT_SPEC := at AT offmask MASK shift SHIFT
LAYERED_OP := { ip IPHDR_FIELD | ip BEYOND_IPHDR_FIELD } CMD_SPEC
EXTENDED_LAYERED_OP := { eth ETHHDR_FIELD | ip IPHDR_FIELD | ip
EX_IPHDR_FIELD | ip6 IP6HDR_FIELD | tcp TCPHDR_FIELD | udp
UDPHDR_FIELD } CMD_SPEC
ETHHDR_FIELD := { src | dst | type }
IPHDR_FIELD := { src | dst | tos | dsfield | ihl | protocol | precedence
| nofrag | firstfrag | ce | df }
BEYOND_IPHDR_FIELD := { dport | sport | icmp_type | icmp_code }
EX_IPHDR_FIELD := { ttl }
IP6HDR_FIELD := { src | dst | traffic_class | flow_lbl | payload_len |
nexthdr | hoplimit }
TCPHDR_FIELD := { sport | dport | flags }
UDPHDR_FIELD := { sport | dport }
CMD_SPEC := { clear | invert | set VAL | add VAL | decrement | preserve
} [ retain RVAL ]
CONTROL := { reclassify | pipe | drop | shot | continue | pass | goto
chain CHAIN_INDEX }
๐ฌ DESCRIPTION
okay so the pedit action is how you change arbitrary packet data โจ You can point at the data in one of two ways. Either you give an offset and size (that's the RAW_OP way), or you name a header and field you want to edit ๐ซถ For header values the size gets picked for you automatically, based on how big that header field is. Easy peasy ๐ธ
๐ OPTIONS
exโ this turns on extended pedit ๐ฅEXTENDED_LAYERED_OPand theaddanddecrementCMD_SPECs only work in this mode, so don't forget it!offset OFFSET { u32 | u16 | u8 }โ tell it where to change data ๐OFFSETis a signed integer, and its base gets chosen automatically (so hex if you prefix it with0x, octal if you prefix it with0). The second part is how much data to change: four bytes (u32), two bytes (u16), or a single byte (u8).at AT offmask MASK shift SHIFTโ this is an optional bit ofRAW_OPthat makes theOFFSETwiggle ๐ฆ It depends on the packet data at offsetAT. That data gets binary ANDed withMASK, right-shifted bySHIFT, and then added toOFFSET.eth ETHHDR_FIELDโ change an ETH header field ๐ The keywords you can use forETHHDR_FIELDare:srcanddstโ source or destination MAC address, written in the standard formatXX:XX:XX:XX:XX:XX๐typeโ the ether-type, as a numeric value
ip IPHDR_FIELDโ change an IPv4 header field ๐ The keywords forIPHDR_FIELDare:srcanddstโ source or destination IP address, a four-byte valuetos,dsfieldandprecedenceโ the Type Of Service field, an eight-bit valueihlโ change the IP Header Length field, a four-bit valueprotocolโ the next-layer Protocol field, an eight-bit valuenofrag,firstfrag,ce,dfandmfโ change the IP header flags ๐ฉ Heads up: the value you pass tosetisn't just a single bit, it's the full byte including the flags field. Only the relevant bits of that value get respected, and the rest get ignored.
ip BEYOND_IPHDR_FIELDโ this one is supported only for non-extended layered ops ๐ It gets passed to the kernel as offsets relative to the start of the IP header, and it assumes the IP header is the minimum size (20 bytes). The keywords forBEYOND_IPHDR_FIELDare:dportandsportโ destination or source port numbers, a 16-bit value. Real talk: IPv4 headers don't actually contain this info ๐ Instead, this sets an offset that suits at least TCP and UDP, if the IP header is minimum size (20 bytes). If it isn't, this will do unexpected things.icmp_typeandicmp_codeโ again, this lets you change data past the IP header itself. It assumes an ICMP header sits right after the (minimum-sized) IP header. If it doesn't, or the IP header is bigger than the 20-byte minimum, this will do unexpected things. Both fields are eight-bit values.
ip EX_IPHDR_FIELDโ only supported whenexis used โจ The keyword forEX_IPHDR_FIELDis:ttl
ip6 IP6HDR_FIELDโ the keywords forIP6HDR_FIELDare:src,dst,traffic_class,flow_lbl,payload_len,nexthdrandhoplimit๐ง
tcp TCPHDR_FIELDโ the keywords forTCPHDR_FIELDare:sportanddportโ source or destination TCP port number, a 16-bit valueflags๐
udp UDPHDR_FIELDโ the keywords forUDPHDR_FIELDare:sportanddportโ source or destination UDP port number, a 16-bit value ๐ฆ
(The original page calls these "TCP port number" here, but it's a UDP field, so I'm keeping the meaning straight for you bestie ๐ซถ)
clearโ clear the addressed data, aka set it to zero ๐งผinvertโ swap every bit in the addressed data ๐set VALโ set the addressed data to a specific value ๐ฏ The size ofVALcomes from eitheru32,u16oru8inRAW_OP, or from the size of the addressed header field inLAYERED_OP.add VALโ add a specific value to the addressed data โ The size ofVALcomes from the size of the addressed header field inEXTENDED_LAYERED_OP. This is supported only for extended layered ops.decrementโ decrease the addressed data by one โฌ๏ธ This is supported only forip ttlandip6 hoplimit.preserveโ keep the addressed data as it is ๐ซงretain RVALโ this optional extra part ofCMD_SPEClets you exclude bits from being changed ๐ It's supported only for fields that are 32 bits or smaller.CONTROLโ these keywords control how the tree of qdisc, classes, filters and actions gets traversed after this action ๐ฆreclassifyโ restart with the first filter in the current listpipeโ continue with the next action attached to the same filterdropandshotโ drop the packet ๐continueโ continue classification with the next filter in linepassโ finish classification and return to the calling qdisc for further packet processing. This is the default โจ
๐ธ EXAMPLES
Being able to edit packet data means you could do all kinds of things ๐คญ Like, implementing port redirection. Certainly not the most useful app, but as an example it slaps:
First, qdiscs need to be set up so you can attach filters to them. For the receive path a simple ingress qdisc will do, and for the transmit path you need a classful qdisc (HTB in this case) ๐ซถ
tc qdisc replace dev eth0 root handle 1: htb
tc qdisc add dev eth0 ingress handle ffff:
Finally, add a filter with a pedit action for each direction. Here u32 matches on the port number you want to redirect from, and then pedit does the actual rewriting ๐ฅ
tc filter add dev eth0 parent 1: u32 \
match ip dport 23 0xffff \
action pedit pedit munge ip dport set 22
tc filter add dev eth0 parent ffff: u32 \
match ip sport 22 0xffff \
action pedit pedit munge ip sport set 23
tc filter add dev eth0 parent ffff: u32 \
match ip sport 22 0xffff \
action pedit ex munge ip dst set 192.168.1.199
tc filter add dev eth0 parent ffff: u32 \
match ip sport 22 0xffff \
action pedit ex munge ip6 dst set fe80::dacb:8aff:fec7:320e
tc filter add dev eth0 parent ffff: u32 \
match ip sport 22 0xffff \
action pedit ex munge eth dst set 11:22:33:44:55:66
tc filter add dev eth0 parent ffff: u32 \
match ip dport 23 0xffff \
action pedit ex munge tcp dport set 22
Want to rewrite just part of a field? Use the retain directive ๐ For example, to overwrite the DSCP part of a dsfield with $DSCP without touching ECN:
tc filter add dev eth0 ingress flower ... \
action pedit ex munge ip dsfield set $((DSCP << 2)) retain 0xfc
And vice versa, to set ECN to e.g. 1 without impacting DSCP:
tc filter add dev eth0 ingress flower ... \
action pedit ex munge ip dsfield set 1 retain 0x3
๐ฆ SEE ALSO
iproute2 ยท 12 Jan 20 ๐ that's the tea, bestie, periodt ๐ซถโจ
๐ฆ where this page comes from
- alpine 3.22: iproute2-doc 6.15.0-r0 ยท GPL-2.0-or-later
- alpine 3.23: iproute2-doc 6.17.0-r0 ยท GPL-2.0-or-later
- alpine 3.24: iproute2-doc 7.0.0-r0 ยท GPL-2.0-or-later
- arch rolling: iproute2 7.2.0-1
- arch rolling: iproute2 7.3.0-1 ยท GPL-2.0-or-later
- centos-stream 10: iproute-tc 7.0.0-2.el10 ยท GPL-2.0-or-later
- centos-stream 9: iproute-tc 6.17.0-3.el9 ยท GPL-2.0-or-later
- debian bookworm: iproute2 6.1.0-3 ยท GPL-2.0-only
- debian trixie: iproute2 6.15.0-1 ยท GPL-2.0-only
- fedora 43: iproute-tc 6.14.0-2.fc43 ยท GPL-2.0-or-later
- fedora 44: iproute-tc 6.17.0-2.fc44 ยท GPL-2.0-or-later
- nixos 26.05: iproute2 7.0.0
- nixpkgs unstable: iproute2 7.2.0
- opensuse-leap 16.0: iproute2 6.12-160000.4.1 ยท GPL-2.0-only
- opensuse-leap 16.0: iproute2 6.12-160000.4.1 ยท GPL-2.0-only
- opensuse-leap 16.0: iproute2 6.12-160000.4.1 ยท GPL-2.0-only
- opensuse-leap 16.0: iproute2 6.12-160000.4.1 ยท GPL-2.0-only
- ubuntu noble: iproute2 6.1.0-1ubuntu6 ยท GPL-2.0-only
- ubuntu noble: iproute2 6.1.0-1ubuntu6.4 ยท GPL-2.0-only
- ubuntu questing: iproute2 6.16.0-1ubuntu2
- ubuntu questing: iproute2 6.16.0-1ubuntu2.1 ยท GPL-2.0-only
- ubuntu questing: iproute2 6.16.0-1ubuntu2.2
- ubuntu resolute: iproute2 6.19.0-1ubuntu1 ยท GPL-2.0-only
- ubuntu resolute: iproute2 6.19.0-1ubuntu1.1 ยท GPL-2.0-only
- ubuntu stonking: iproute2 7.2.0-1ubuntu1 ยท GPL-2.0-only
- ๐ girlified by anthropic/claude-haiku-5.5 on : a language model's rewrite of the original, under its terms