Network condition simulator for Linux routers. Injects latency, jitter, packet loss, reordering, and duplication per client IP using tc/netem/ifb.
Comes with built-in profiles for common network conditions (3G, LTE, Satellite, Starlink, etc.) and an interactive TUI to manage them. Profiles support asymmetric upload/download parameters to model real-world links.
How it works
lagsim sets up an HTB qdisc tree on your LAN interface with per-client classes and netem leaf qdiscs. Ingress traffic is redirected through an IFB device so both upload and download are conditioned independently.
An optional upstream profile is applied on the WAN/uplink interface as a second hop. Every forwarded LAN client then gets that treatment in addition to its own device or group profile. Traffic to or from the router itself (SSH, DNS, DHCP) bypasses the WAN hop so a lossy upstream profile cannot lock you out.
LAN clients <──eth0──> router <──wan0──> internet
│ │
HTB + netem HTB + netem
(per client) (shared upstream)
IFB + netem IFB + netem
Each direction gets its own netem parameters, so profiles can model asymmetric links (e.g., DSL with fast download / slow upload, or cellular with higher uplink loss).
Install
go build -o lagsim .
sudo cp lagsim /usr/local/bin/Usage
Interactive TUI
| Key | Action |
|---|---|
↑/k, ↓/j |
Navigate client list |
Enter |
Open profile selection or group actions |
g |
Create, join, or manage a shared profile group |
e |
Edit device name (requires MAC) |
r, Delete |
Remove profile from client |
Esc |
Cancel / back to list |
Ctrl+U |
Clear name in edit mode |
q, Ctrl+C |
Quit |
The first row is upstream (WAN). Assigning a profile there shapes all forwarded LAN traffic in addition to each device's own profile, without affecting SSH to the router.
CLI
# List clients and their profiles sudo lagsim list # Show available profiles sudo lagsim profiles # Apply a profile to a client sudo lagsim apply 192.168.1.100 3G # Apply a shared WAN profile to all forwarded LAN traffic sudo lagsim upstream apply Satellite # Remove the WAN hop (per-client profiles stay) sudo lagsim upstream remove # Remove conditioning from a client sudo lagsim remove 192.168.1.100 # Create a shared 3G profile group sudo lagsim groups create living-room 3G 192.168.1.100 192.168.1.101 # Keep clients in a group without applying network conditioning sudo lagsim groups create living-room "(none)" 192.168.1.100 192.168.1.101 # Add or remove members without recreating the shared queue sudo lagsim groups member add living-room 192.168.1.102 sudo lagsim groups member remove living-room 192.168.1.102 # List or delete groups sudo lagsim groups list sudo lagsim groups delete living-room # Initialize tc infrastructure and restore saved assignments sudo lagsim init # Tear down all tc rules sudo lagsim teardown # Dump raw tc state for debugging sudo lagsim status
Flags
| Flag | Description |
|---|---|
-c, --config |
Config file path (default ~/.config/lagsim.yaml) |
--dry-run |
Print tc commands without executing |
-v, --verbose |
Verbose output |
On first run, lagsim auto-detects the LAN interface and subnet. If multiple interfaces are found, it prompts you to choose.
Built-in profiles
Each parameter is applied per-direction (egress + ingress), so effective RTT is roughly 2x the delay value. Asymmetric values show ▲ upload and ▼ download on separate lines.
| Profile | Delay | Jitter | Dist | Loss | Reorder | Slot | Rate |
|---|---|---|---|---|---|---|---|
| 3G | 100ms | ▲ 50ms ▼ 30ms |
paretonormal | ▲ 2.5% ▼ 1.5% |
– | 40ms 10ms | ▲ 0.5 Mbps ▼ 2 Mbps |
| LTE | 20ms | ▲ 8ms ▼ 5ms |
paretonormal | ▲ 1% ▼ 0.5% |
– | 10ms 3ms | ▲ 15 Mbps ▼ 50 Mbps |
| 5G | 5ms | 1ms | paretonormal | ▲ 0.1% ▼ 0.05% |
– | – | ▲ 100 Mbps ▼ 300 Mbps |
| Edge-2G | 150ms | ▲ 100ms ▼ 60ms |
paretonormal | ▲ 8% ▼ 5% |
– | 80ms 20ms | ▲ 0.05 Mbps ▼ 0.1 Mbps |
| Lossy-WiFi | 5ms | 3ms | pareto | 3% | 1% gap 5 | 5ms 2ms | 20 Mbps |
| Starlink | 20ms | ▲ 10ms ▼ 5ms |
normal | ▲ 1% ▼ 0.5% |
0.5% | – | ▲ 20 Mbps ▼ 100 Mbps |
| Satellite | 300ms | ▲ 50ms ▼ 30ms |
normal | ▲ 2.5% ▼ 1.5% |
– | – | ▲ 1 Mbps ▼ 5 Mbps |
| DSL | 15ms | 3ms | normal | 0.2% | – | – | ▲ 3 Mbps ▼ 25 Mbps |
| Cable | 5ms | 1ms | normal | 0.05% | – | – | ▲ 20 Mbps ▼ 200 Mbps |
| Airplane-WiFi | 150ms | ▲ 50ms ▼ 30ms |
pareto | ▲ 5% ▼ 3% |
1% gap 5 | 30ms 10ms | ▲ 1 Mbps ▼ 2 Mbps |
| Congested | 50ms | 40ms | paretonormal | 5% | 2% gap 3 | – | ▲ 0.5 Mbps ▼ 1 Mbps |
| Bursty | 10ms | 2ms | – | gemodel (burst) | – | – | 50 Mbps |
| Intermittent-WiFi | 5ms | 2ms | normal | – | – | – | 100 Mbps |
| ↳ burst: 500ms every 5s | 200ms | 100ms | pareto | 20% | 5% | – | 100 Mbps |
| ECN-Datacenter | 1ms | 0.5ms | normal | 2% ecn | – | – | 1 Gbps |
| ECN-WAN | 25ms | 5ms | normal | 0.5% ecn | – | – | ▲ 50 Mbps ▼ 100 Mbps |
Built-in profiles are defined in code, not written to the config file.
Configuration
Configuration is stored in ~/.config/lagsim.yaml:
interfaces: lan: eth0 # LAN-facing interface (auto-detected on first run) wan: eth1 # WAN/uplink interface (auto-detected from the default route) ifb: ifb0 # IFB device (created automatically) ifb_wan: ifb1 # IFB device for WAN ingress (created automatically) subnet: 192.168.1.0/24 root_rate: 1gbit upstream: Satellite profiles: # Override a built-in profile 3G: delay: 100ms jitter: 30ms correlation: 25% loss: 1.5% rate: 2mbit upload: rate: 0.5mbit # Add a custom profile My-VPN: delay: 30ms jitter: 5ms loss: 0.1% rate: 50mbit # Disable a built-in profile Edge-2G: null assignments: 192.168.1.100: 3G 192.168.1.101: LTE groups: living-room: profile: Cable members: - 192.168.1.110 - 192.168.1.111 names: aa:bb:cc:dd:ee:f0: Living Room TV aa:bb:cc:dd:ee:f1: Dad's Phone
Shared profile groups
A group routes every member IP through one HTB class and one netem queue per
direction. The selected profile's rate is an aggregate cap: two active
devices in a 2mbit group share 2 Mbit/s, rather than receiving 2 Mbit/s
each. Because the netem FIFO is shared, a device that builds a backlog can
also increase queueing delay or drops for the other members; this is the
intended contention model.
An IP may be in one group or have an individual assignments profile, but not
both. Manage group membership with lagsim groups or press g for the
selected device in the TUI. The generated class_id is an internal stable
identifier and is saved automatically after a group is first applied.
Groups accept any IPv4 client routed through the selected interface, including
clients whose subnet differs from the router-facing interface address.
Groups may have no active profile; select (none) in the TUI or pass
"(none)" to lagsim groups create. Their members remain grouped and are
shown in orange, but receive no traffic conditioning.
Burst profiles are supported for groups. Every member switches between the normal and burst paths at the same time.
Upstream (WAN) profile
upstream is a second hop on the WAN/uplink interface. Every forwarded packet
from the LAN subnet traverses that shared HTB/netem queue and its
per-device or group queue, so delays add, loss compounds, and the upstream
rate is an aggregate cap shared by all clients.
Router-local traffic is excluded: filters match the LAN subnet with
protocol ip only, and every IPv4 address on the LAN and WAN interfaces is
passed unshaped. SSH from the office LAN to the box therefore stays usable
even if the upstream profile is extremely lossy. Per-device profiles on the
LAN interface are unchanged and can still affect that client's sessions to
the router.
If the LAN side is a VLAN subinterface (for example enp2s0.30) and the WAN
side is the parent (enp2s0), tagged VLAN frames are not redirected — only
untagged IPv4 matching the LAN subnet is classified on the parent.
Set the hop from the TUI's pinned upstream row, or with:
sudo lagsim upstream apply Satellite sudo lagsim upstream remove
lagsim init restores a saved upstream profile. Removing it deletes the
WAN HTB/IFB qdiscs and leaves per-client LAN rules in place. The WAN
interface is auto-detected from the IPv4 default route when interfaces.wan
is empty.
Custom profiles
Only profiles that differ from the built-in defaults are saved to the config file. You can:
- Add custom profiles alongside the built-ins
- Override a built-in by redefining it (full replacement, not merged)
- Disable a built-in by setting it to
null
Profile parameters
| Parameter | Description | Example |
|---|---|---|
delay |
Base latency added to each packet | 100ms |
jitter |
Random variation added to delay | 30ms |
correlation |
How much each packet's delay correlates with the previous | 25% |
distribution |
Jitter distribution: normal, pareto, or paretonormal |
paretonormal |
loss |
Packet loss — random or bursty (see below) | 1.5% |
ecn |
Mark packets with ECN CE bit instead of dropping (see below) | true |
duplicate |
Packet duplication probability | 0.5% |
reorder |
Packet reordering — random or with gap (see below) | 1% or 1% gap 5 |
corrupt |
Packet corruption probability | 0.1% |
rate |
Bandwidth limit | 2mbit |
slot |
Packet batching interval — holds then releases in bursts | 20ms 5ms |
All parameters are optional except delay. Values use tc/netem syntax.
Delay distribution
Without a distribution, jitter is uniformly random. Setting distribution shapes how jitter values are picked:
normal— bell curve around the base delay. Good for stable links (DSL, cable, satellite) where variation is symmetric.pareto— heavy-tailed: most packets are near the base delay, but occasional packets get much larger spikes. Good for WiFi and other interference-prone links.paretonormal— blend of both: normal most of the time with pareto-like tail spikes. Good for cellular networks where handoffs and contention cause intermittent latency bursts.
Bursty loss
The loss field supports netem's Gilbert-Elliott model for realistic bursty loss patterns — periods of clean transmission interrupted by short bursts of heavy packet loss:
loss: "gemodel p r 1-h 1-k"
| Parameter | Meaning |
|---|---|
p |
Probability of entering the bad (lossy) state |
r |
Probability of returning to the good state |
1-h |
Loss rate in the bad state (e.g., 100% = total blackout) |
1-k |
Loss rate in the good state (e.g., 0% = no baseline loss) |
Example: loss: "gemodel 0.5% 15% 100% 0%" — clean most of the time, with occasional short bursts (~7 packets) of 100% loss. This models WiFi interference, cellular handoffs, or buffer overflows.
ECN marking
When ecn: true is set alongside loss, packets are marked with the ECN CE (Congestion Experienced) bit instead of being dropped. The packet still arrives, but ECN-aware TCP stacks treat it as a congestion signal and slow down. Useful for testing DCTCP, BBR, or QUIC ECN behavior:
profiles: My-ECN-Test: delay: 5ms loss: 1% ecn: true rate: 1gbit
Reorder with gap
By default, reorder randomly reorders packets. Adding gap N makes it deterministic: every Nth packet is reordered with the given probability. This is more realistic for triggering TCP fast-retransmit (which fires after 3 duplicate ACKs):
reorder: "1% gap 5" # every 5th packet has a 1% chance of being reordered
Slot-based emission
The slot parameter batches packets into time slots instead of sending them individually. Packets are held and released in bursts, simulating WiFi TDMA scheduling or cellular resource allocation:
slot: "20ms 5ms" # release a batch every 20ms ± 5ms jitter
This is especially noticeable for interactive traffic (VoIP, gaming) where micro-bursts affect perceived quality even when average throughput is fine.
Asymmetric profiles
Base parameters apply to both directions. Add download and/or upload sections to override specific parameters per direction — only the fields you specify are overridden, the rest inherit from the base:
profiles: My-Satellite: delay: 300ms jitter: 30ms loss: 1.5% rate: 5mbit upload: rate: 1mbit # slower upload jitter: 50ms # more jitter on uplink loss: 2.5% # more loss on uplink download: rate: 10mbit # faster download
Periodic impairment bursts
Profiles can alternate between normal conditions and a short, correlated burst
of latency, jitter, loss, reordering, or a different rate. interval is the
time spent in the normal state; it is not a start-to-start period.
profiles: Intermittent-WiFi: delay: 5ms jitter: 2ms rate: 100mbit burst: interval: 5s duration: 500ms delay: 200ms jitter: 100ms distribution: pareto loss: 20% reorder: 5%
Burst values overlay the normal profile. They can also contain download and
upload sections, which overlay the normal directional settings.
lagsim apply creates normal and burst HTB/netem paths, installs a pinned eBPF
classifier, updates its assignment map, and exits. The classifier selects a
path from kernel monotonic time, so no lagsim daemon or userspace scheduler is
required. Upload and download use the same start time and remain synchronized.
lagsim apply
│
┌────────────────┴────────────────┐
│ resolve normal + burst profile │
└────────────────┬────────────────┘
│
┌──────────────────┴──────────────────┐
│ │
LAN/download (destination IP) IFB/upload (source IP)
│ │
┌──────┴──────┐ ┌──────┴──────┐
│ normal HTB │ │ normal HTB │
│ + netem │ │ + netem │
├─────────────┤ ├─────────────┤
│ burst HTB │ │ burst HTB │
│ + netem │ │ + netem │
└──────▲──────┘ └──────▲──────┘
│ │
└──────── pinned eBPF classifiers ────┘
│
assignment map lookup
IP → start, interval, duration,
normal class, burst class
│
kernel monotonic time
│
phase < interval? normal : burst
The eBPF programs and map are pinned under /sys/fs/bpf/lagsim. They survive
the CLI process but not a reboot, so continue running lagsim init at boot to
restore saved assignments. lagsim teardown removes both TC and pinned eBPF
state.
For a dry run, lagsim --dry-run apply IP Intermittent-WiFi prints both class
configurations and the eBPF assignment update without persisting it.
Device names
Custom names are keyed by MAC address so they follow the device across IP changes. Edit names in the TUI with e, or set them directly in the config under names.
Persistence
Assignments persist across reboots. Run lagsim init at startup to restore them (e.g. via systemd or cron @reboot):
# crontab -e
@reboot /usr/local/bin/lagsim initExample deployment: lab VLAN router
A practical way to use lagsim is to set up a dedicated Linux box as a router between a lab VLAN and the rest of your network. All devices on the lab VLAN — phones, tablets, TVs, IoT devices — get their traffic conditioned without any client-side configuration. Traffic between lab devices and anything on the other side (dev workstations, servers, the internet) goes through lagsim.
┌──────────┐ ┌──────────┐ ┌──────────┐
│ dev │ │ dev │ │ internet │
│workstat. │ │ server │ │ gateway │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
─────┴─────────────┴─────────────┴──── office LAN
│
│ eth0 (or eth0.100 VLAN tag)
┌──────┴──────┐
│ lagsim │
│ linux box │
└──────┬──────┘
│ eth1 (or eth0.200 VLAN tag)
│
┌────────────┼───────────┐
│ │ │
┌────┴────┐ ┌─────┴─────┐ ┌───┴───┐
│ phone │ │ tablet │ │ TV │
└─────────┘ └───────────┘ └───────┘
Lab WiFi (dedicated SSID)
The Linux box can use two physical interfaces (e.g., eth0 for the office LAN, eth1 for the lab) or a single interface with VLAN tagging (e.g., eth0.100 and eth0.200).
Setup
- Create a lab VLAN on your switch and assign a dedicated WiFi SSID to it
- Configure the Linux box with either two interfaces or VLAN sub-interfaces — one on the lab VLAN, one on the office LAN
- Enable IP forwarding so the box routes traffic between the two networks
- Run lagsim on the lab-facing interface:
sudo sysctl -w net.ipv4.ip_forward=1 sudo lagsim
lagsim auto-detects the lab interface and discovers devices via ARP. You can then assign different profiles to different devices — for example, put a phone on "3G" and a TV on "Satellite" simultaneously.
This lets you test how your client/server application behaves under realistic network conditions: the clients are real devices on the lab VLAN, and the servers run on your workstation or dev servers on the office LAN — all traffic between them passes through lagsim.
Requirements
- Linux with
tc,ip, and theifbkernel module - eBPF syscall support and a mountable
/sys/fs/bpf - Root privileges
- Go 1.24+ to build
License
MIT

