GitHub - rs/lagsim

GitHub

13 min read Original article ↗

lagsim

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.

Screenshot

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 init

Example 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

  1. Create a lab VLAN on your switch and assign a dedicated WiFi SSID to it
  2. Configure the Linux box with either two interfaces or VLAN sub-interfaces — one on the lab VLAN, one on the office LAN
  3. Enable IP forwarding so the box routes traffic between the two networks
  4. 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 the ifb kernel module
  • eBPF syscall support and a mountable /sys/fs/bpf
  • Root privileges
  • Go 1.24+ to build

License

MIT