Rudder is an eBPF TC packet steering and multicast-to-unicast replication CLI for Linux networks. It attaches eBPF programs to the TC (Traffic Control) ingress hook, letting you define YAML rules that match packets by interface, DSCP value, source/destination IP prefix, and protocol — then rewrite headers and redirect traffic across interfaces at wire speed in the kernel.
Repository: github.com/dotchance/rudder
Keywords: eBPF, Linux TC, traffic control, packet steering, multicast replication, multicast-to-unicast, DSCP, policy routing, network engineering.
Rudder is about eBPF. Linux still uses BPF in many API names, constants, helpers, and tools, so this repository keeps those literal names when referring to kernel interfaces such as BPF_MAP_TYPE_ARRAY, BPF_OBJ_GET, bpf_redirect(), or bpftool. In explanatory prose, Rudder uses eBPF for the technology, programs, maps, object files, and learning path.
Two policy types are supported:
- Steer — Match ingress packets by DSCP, IP prefix, and protocol. Rewrite the destination IP and MAC, then redirect to a chosen egress interface. Useful for policy-based routing, traffic engineering, and DSCP-driven path selection.
- Replicate — Match multicast packets and fan them out as unicast copies to multiple egress interfaces, each with its own rewritten destination IP and MAC. Useful for multicast-to-unicast conversion across multiple downstream paths.
YAML rules
|
v
+---------------+
| Python engine | Compiles eBPF C with clang
| (engine/) | Attaches programs via `tc`
+-------+-------+ Populates eBPF maps via `bpftool`
|
+----------+----------+
| |
ebpf/steer.c ebpf/replicate.c
| |
v v
TC ingress hook TC ingress hook
(per interface) (per interface)
| |
v v
Match + rewrite IP Match multicast dst
+ redirect to egress + clone to N unicast
destinations
When you run rudder load, the engine:
- Parses YAML rule files into a validated Policy IR
- Compiles
ebpf/steer.candebpf/replicate.cwith clang to eBPF object files - Attaches both programs to TC ingress on each referenced interface via
tc filter add - Pins eBPF maps to
/sys/fs/bpf/rudder/for userspace access - Serializes rules into eBPF array maps using
bpftool - Forks a background daemon that holds state and serves CLI queries
The eBPF programs run in-kernel. On each ingress packet they iterate the rule array, match fields, rewrite the IP and Ethernet headers, fix checksums, and call bpf_redirect() (steer) or bpf_clone_redirect() (replicate).
- Linux kernel 5.15 or later (required for bounded loops in eBPF programs)
- Root privileges (eBPF and TC attachment require CAP_SYS_ADMIN)
- x86_64 architecture
Rudder is a privileged networking tool. Loading policies requires root-equivalent access because TC and eBPF program attachment need elevated Linux capabilities. Treat rule files, container images, Kubernetes manifests, and host access as privileged operational inputs.
Security reports should be submitted privately through GitHub Security Advisories. See SECURITY.md for the reporting policy and operational guidance.
Install on Ubuntu/Debian:
sudo apt-get install -y \
libbpf-dev \
linux-headers-$(uname -r) \
linux-tools-generic \
clang \
llvm \
iproute2 \
tcpdumplinux-tools-generic provides bpftool, which rudder uses to pin and populate eBPF maps. tcpdump is optional but invaluable for verifying redirected packets on egress interfaces.
pip3 install -r requirements.txtThis installs click (CLI framework), PyYAML (rule parsing), and pyroute2
(ARP neighbor table lookup).
The Scapy packet generator is optional and lives in the dev/test requirements:
pip3 install -r requirements-dev.txtThe Python engine compiles the eBPF programs automatically during rudder load, but you can also compile them manually to check for errors:
# Compile the steer program
clang -O2 -g -target bpf \
-I/usr/include \
-I/usr/include/x86_64-linux-gnu \
-c ebpf/steer.c -o /tmp/rudder_steer.o
# Compile the replicate program
clang -O2 -g -target bpf \
-I/usr/include \
-I/usr/include/x86_64-linux-gnu \
-c ebpf/replicate.c -o /tmp/rudder_replicate.oBoth commands should complete with zero warnings. If you see verifier-related errors when the program is loaded by tc, check that your kernel is 5.15 or later — earlier kernels may not support the bounded loop iteration pattern used to walk the rule array.
You can inspect the compiled objects with llvm-objdump:
llvm-objdump -d /tmp/rudder_steer.o # Disassemble eBPF instructions
llvm-objdump -h /tmp/rudder_steer.o # Show sections (should include classifier and .maps)Rules are defined in YAML files under a top-level rules key. Multiple files can be loaded simultaneously — all rules are merged, sorted by priority, and validated as a single set.
| Field | Required | Description |
|---|---|---|
name |
yes | Unique human-readable label |
priority |
yes | Integer evaluation order (lower = first). Must be unique across all files. |
type |
yes | steer or replicate |
match.interface |
yes | Ingress interface name (e.g. eth0) or any |
match.src_ip |
no | Source IP or CIDR prefix (e.g. 10.1.0.0/16). Omit to match any. |
match.dst_ip |
no | Destination IP or CIDR prefix. Omit to match any. |
match.dscp |
no | DSCP value 0-63 (the 6-bit field, not the full TOS byte). Omit to match any. |
match.ip_proto |
no | tcp, udp, or any (default: any) |
action.dst_ip |
steer | Rewrite destination IP to this exact address |
action.via |
steer | Egress interface name |
action.next_hop_mac |
no | Static next-hop MAC (aa:bb:cc:dd:ee:ff). If omitted, resolved from ARP table. |
action.targets |
replicate | List of 1-12 replication targets, each with dst_ip, via, and optional next_hop_mac |
Route all EF-marked traffic (DSCP 46) destined for 10.0.0.0/8 arriving on eth0 to 192.168.100.1 via eth2:
rules:
- name: ef-to-path-a
priority: 10
type: steer
match:
interface: eth0
dscp: 46
dst_ip: 10.0.0.0/8
action:
dst_ip: 192.168.100.1
via: eth2Take any multicast packet to 239.1.1.1 on any interface, and deliver unicast copies to three destinations:
rules:
- name: mcast-replicate-stream
priority: 20
type: replicate
match:
interface: any
dst_ip: 239.1.1.1
action:
targets:
- dst_ip: 10.10.1.1
via: eth1
- dst_ip: 10.10.2.1
via: eth2
- dst_ip: 10.10.3.1
via: eth3You can split rules across files and load them together. Priorities and names must be unique across all files:
sudo python3 rudder.py load rules/steering.yaml rules/replication.yaml rules/overrides.yamlAll commands require root.
Parse rule files, compile eBPF programs, attach TC hooks, populate maps, and start the background daemon:
sudo python3 rudder.py load rules/example_steer.yamlLoading rules from: rules/example_steer.yaml
[ok] ef-to-path-a priority=10 type=steer interface=eth0
Attaching TC hooks:
[ok] eth0 ingress
[ok] eth2 ingress
Rudder running. 1 rule active (1 steer, 0 replicate). Daemon PID: 4821
Load both steer and replicate rules at once:
sudo python3 rudder.py load rules/example_steer.yaml rules/example_replicate.yamlDisplay the active rule table:
sudo python3 rudder.py show rulesPRI NAME TYPE INTERFACE MATCH ACTION
10 ef-to-path-a steer eth0 dscp=46 dst=10.0.0.0/8 via=eth2 -> 192.168.100.1
20 mcast-replicate-stream replicate any dst=239.1.1.1 3 targets: eth1 eth2 eth3
Display per-rule packet hit counters:
sudo python3 rudder.py show statsNAME TYPE PRI HITS
ef-to-path-a steer 10 14,382
mcast-replicate-stream replicate 20 891
Dump the raw eBPF map contents with all fields decoded:
sudo python3 rudder.py show maps=== steer_rules ===
slot=0 name=ef-to-path-a ingress=eth0 src=0.0.0.0/0 dst=10.0.0.0/8 dscp=46 proto=0 -> new_dst=192.168.100.1 egress=eth2 mac=aa:bb:cc:dd:ee:ff
=== replicate_rules ===
slot=0 name=mcast-replicate-stream ingress=0 dst=239.1.1.1/32 targets=3:
-> 10.10.1.1 via eth1 mac=00:00:00:00:00:00
-> 10.10.2.1 via eth2 mac=00:00:00:00:00:00
-> 10.10.3.1 via eth3 mac=00:00:00:00:00:00
See which interfaces have rudder TC hooks attached:
sudo python3 rudder.py show interfacesINTERFACE IFINDEX HOOK
eth0 2 yes (rudder)
eth1 3 yes (rudder)
eth2 4 yes (rudder)
eth3 5 no
Inspect the runtime details that connect YAML policy to TC/eBPF state:
sudo python3 rudder.py show internalsThis command reports the daemon socket path, runtime object paths, TC filter preferences, attached interfaces, eBPF map ids, pinned representative maps, backend limits, source files, and active rule slots.
Stream real-time trace events for every matched packet. Each line shows the matched rule, event type, source/destination IPs, and egress interface:
sudo python3 rudder.py traceWARNING: rudder trace is experimental.
It demonstrates eBPF perf event output, but the userspace reader is not production-grade.
Use 'sudo python3 rudder.py show internals' to inspect the maps trace reads.
Streaming trace events (Ctrl-C to stop)...
[12:04:33.441] rule=ef-to-path-a slot=0 type=steer src=10.1.1.5 orig_dst=10.2.2.1 new_dst=192.168.100.1 egress=eth2
[12:04:33.449] rule=mcast-replicate-stream slot=0 type=replicate_clone src=10.1.1.9 orig_dst=239.1.1.1 new_dst=10.10.1.1 egress=eth1
[12:04:33.449] rule=mcast-replicate-stream slot=0 type=replicate_clone src=10.1.1.9 orig_dst=239.1.1.1 new_dst=10.10.2.1 egress=eth2
[12:04:33.449] rule=mcast-replicate-stream slot=0 type=replicate_final src=10.1.1.9 orig_dst=239.1.1.1 new_dst=10.10.3.1 egress=eth3
Press Ctrl-C to stop.
The current trace reader is intentionally small and experimental. It demonstrates how eBPF programs can emit events through perf event arrays, but the userspace perf mmap protocol has edge cases around metadata offsets, memory barriers, and ring wraparound. Treat rudder trace as a learning aid until the tracing path is replaced with a clearer, production-grade reader. See docs/tracing.md.
Update rules without stopping the daemon. Rudder parses YAML into its Policy IR, validates it, stages any new TC ingress hooks, writes the accepted policy into every loaded eBPF map instance, and only then detaches interfaces that are no longer needed. If validation or map writes fail, the daemon keeps the previous policy active and reports the failure.
sudo python3 rudder.py reload rules/updated_rules.yamlReloaded. Changes applied:
MODIFIED ef-to-path-a
ADDED be-to-path-b priority=30
REMOVED old-rule priority=50
ATTACHED eth3 TC ingress
DETACHED eth1 TC ingress
UPDATED eBPF maps: replicate_rules=2, repl_hits=2, steer_hits=2, steer_rules=2
Detach all TC hooks, remove pinned eBPF maps, and stop the daemon:
sudo python3 rudder.py stopRudder stopped.
You can verify cleanup with:
tc filter show dev eth0 ingress # Should show no rudder filters
ls /sys/fs/bpf/rudder/ 2>/dev/null # Directory should not existThe included packet generator uses Scapy to send crafted packets for validating rules. Install the dev/test requirements before using it:
pip3 install -r requirements-dev.txtSend 5 UDP packets with DSCP 46 to 10.0.0.1 on eth0, which should trigger the ef-to-path-a steer rule:
sudo python3 tests/gen_packets.py \
--mode steer \
--src-ip 10.1.1.5 \
--dst-ip 10.0.0.1 \
--dscp 46 \
--iface eth0 \
--count 5 \
--proto udpThen verify:
# Check hit counters incremented
sudo python3 rudder.py show stats
# Watch for rewritten packets on the egress interface
sudo tcpdump -i eth2 -n dst host 192.168.100.1Send 10 UDP packets to multicast group 239.1.1.1:
sudo python3 tests/gen_packets.py \
--mode replicate \
--src-ip 10.1.1.9 \
--dst-ip 239.1.1.1 \
--iface eth0 \
--count 10Verify unicast copies appear on each target interface:
sudo tcpdump -i eth1 -n dst host 10.10.1.1 &
sudo tcpdump -i eth2 -n dst host 10.10.2.1 &
sudo tcpdump -i eth3 -n dst host 10.10.3.1 &--mode steer | replicate (required)
--src-ip Source IP address (default: 10.0.0.1)
--dst-ip Destination IP address (required)
--dscp DSCP value 0-63 (default: 0)
--iface Outgoing interface (required)
--count Number of packets (default: 10)
--interval Seconds between packets (default: 0.1)
--proto tcp | udp | icmp (default: udp)
A full test cycle on a machine with eth0, eth1, eth2, and eth3:
# 1. Install dependencies
sudo apt-get install -y libbpf-dev linux-headers-$(uname -r) \
linux-tools-generic clang llvm iproute2 tcpdump
pip3 install -r requirements-dev.txt
# 2. Load steering and replication rules
sudo python3 rudder.py load rules/example_steer.yaml rules/example_replicate.yaml
# 3. Confirm TC hooks are attached
tc filter show dev eth0 ingress
# 4. Confirm eBPF maps are pinned
ls /sys/fs/bpf/rudder/
# 5. Inspect map contents
sudo python3 rudder.py show maps
# 6. Start a trace in one terminal
sudo python3 rudder.py trace
# 7. In another terminal, send test traffic
sudo python3 tests/gen_packets.py --mode steer --dst-ip 10.0.0.1 --dscp 46 --iface eth0 --count 5
# 8. Check hit counters
sudo python3 rudder.py show stats
# 9. Watch for redirected packets
sudo tcpdump -i eth2 -n dst host 192.168.100.1
# 10. Clean up
sudo python3 rudder.py stopA Dockerfile and K3s pod manifest are provided in deploy/ for a specific lab
purpose: running Rudder in a privileged Multus pod with multiple attached
interfaces. This is useful for repeatable Kubernetes network experiments, but
it is not the core Rudder runtime and is not a hardened production deployment.
See deploy/README.md for the purpose, security model, and expected lab setup.
docker build -t rudder:latest -f deploy/Dockerfile .
kubectl apply -f deploy/pod.yaml
kubectl exec -it rudder -- bashThe pod runs in privileged mode and mounts /sys/fs/bpf, /lib/modules, and /usr/src from the host. Multus NetworkAttachmentDefinition resources (rudder-net1, rudder-net2, rudder-net3 in the manifest) must be created separately to match your cluster's network topology.
Source-linked implementation notes live under docs/:
rudder/
├── .github/
│ ├── workflows/ # CI and CodeQL scanning
│ ├── ISSUE_TEMPLATE/ # Bug and feature request templates
│ └── PULL_REQUEST_TEMPLATE.md
├── rudder.py # CLI entry point (click)
├── engine/
│ ├── __init__.py
│ ├── models.py # Dataclasses: MatchSet, SteerAction, ReplicateAction, Rule
│ ├── policy.py # Policy IR shared by parsers, daemon, and manager
│ ├── loader.py # YAML parsing, validation, Policy IR creation
│ ├── manager.py # Compile, TC attach, map pinning, map population
│ ├── observer.py # Stats, map dump, trace event formatting
│ ├── perf_reader.py # Experimental ctypes perf event reader
│ ├── runtime.py # Runtime paths for daemon socket and eBPF objects
│ └── daemon.py # Background daemon with Unix socket IPC
├── ebpf/
│ ├── maps.h # Shared struct definitions and constants
│ ├── steer.c # TC classifier: DSCP/IP steering with redirect
│ └── replicate.c # TC classifier: multicast-to-unicast replication
├── docs/ # Source-linked learning notes
├── rules/
│ ├── example_steer.yaml # Example DSCP steering rule
│ └── example_replicate.yaml # Example multicast replication rule
├── tests/
│ ├── gen_packets.py # Scapy packet generator for validation
│ ├── test_policy_loader.py # Policy validation tests
│ └── test_policy_manager_reload.py # Reload rollback tests
├── deploy/
│ ├── Dockerfile # Ubuntu 22.04 container with all dependencies
│ ├── pod.yaml # K3s pod manifest with Multus annotations
│ └── README.md # Purpose and security notes for lab deployment
├── CODE_OF_CONDUCT.md # Community expectations
├── CONTRIBUTING.md # Development and validation notes
├── SECURITY.md # Vulnerability reporting and security scope
├── requirements.txt # Runtime Python dependencies
└── requirements-dev.txt # Optional dev/test dependencies
- Maximum 64 rules total (compile-time constant
MAX_RULESinebpf/maps.h) - Maximum 12 replication targets per replicate rule (
MAX_TARGETS) - IPv4 only
- No stateful connection tracking
- No VLAN or QinQ support