https://github.com/karolhendzel725-cloud/fixed-clumsy
Maintained fork of jagt/clumsy — a Windows utility to deliberately worsen network conditions (lag, drop, throttle, tamper, …) via WinDivert. Adds fixes (RNG seed #94, single-thread lag rework #35), OOM hardening + unit tests, and configurable lag jitter (#33).
https://github.com/karolhendzel725-cloud/fixed-clumsy
c latency network-emulation network-simulation network-testing networking packet-loss packet-manipulation windivert windows
Last synced: 7 days ago
JSON representation
Maintained fork of jagt/clumsy — a Windows utility to deliberately worsen network conditions (lag, drop, throttle, tamper, …) via WinDivert. Adds fixes (RNG seed #94, single-thread lag rework #35), OOM hardening + unit tests, and configurable lag jitter (#33).
- Host: GitHub
- URL: https://github.com/karolhendzel725-cloud/fixed-clumsy
- Owner: Karolhendzel725-cloud
- License: mit
- Created: 2026-06-13T18:33:23.000Z (7 days ago)
- Default Branch: master
- Last Pushed: 2026-06-13T22:26:44.000Z (7 days ago)
- Last Synced: 2026-06-14T00:09:35.672Z (7 days ago)
- Topics: c, latency, network-emulation, network-simulation, network-testing, networking, packet-loss, packet-manipulation, windivert, windows
- Language: C
- Size: 55 MB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# clumsy
__clumsy makes your network condition on Windows significantly worse, but in a managed and interactive manner.__
Leveraging the awesome [WinDivert](http://reqrypt.org/windivert.html), clumsy stops living network packets and capture them, lag/drop/tamper/.. the packets on demand, then send them away. Whether you want to track down weird bugs related to broken network, or evaluate your application on poor connections, clumsy will come in handy:
* No installation.
* No need for proxy setup or code change in your application.
* System wide network capturing means it works on any application.
* Works even if you're offline (ie, connecting from localhost to localhost).
* Your application keeps running, while clumsy can start and stop anytime.
* Interactive control how bad the network can be, with enough visual feedback to tell you what's going on.
See [this page](http://jagt.github.io/clumsy) for more info and build instructions.
## About this fork (fixed-clumsy)
**fixed-clumsy** is a fork of [jagt/clumsy](https://github.com/jagt/clumsy) (by [@jagt](https://github.com/jagt), MIT licensed) with bug fixes applied. All credit for the original tool goes upstream; this fork only adds the changes listed below.
### Fixes applied
- **RNG seeded on the wrong thread ([#94](https://github.com/jagt/clumsy/issues/94))** — `rand()` (used by every module via `calcChance()`) runs on the WinDivert worker threads, but `srand()` was only called on the main thread. Under the Microsoft CRT the `rand()` seed is *thread-local*, so the worker threads started from the default seed and the drop / duplicate / out-of-order / throttle / reset probabilities were effectively deterministic — and identical between the two worker threads. Fixed by seeding each worker thread (new `seedRand()` helper called at the start of `divertReadLoop` / `divertClockLoop`). *(commit `22b42c3`)*
- **Scheduling rework — lag latency floor ([#35](https://github.com/jagt/clumsy/pull/35))** — port of @dimbleby's rework: the read loop only enqueues packets and signals an event, while a single processing thread does all the work and wakes either on the event or after a per-module reschedule delay. `lag` now asks to wake exactly when its oldest buffered packet is due instead of waiting up to `CLOCK_WAITMS` (40 ms). This removes the latency floor/jitter reported in issues [#87](https://github.com/jagt/clumsy/issues/87), [#58](https://github.com/jagt/clumsy/issues/58), [#134](https://github.com/jagt/clumsy/issues/134). *Measured on loopback UDP (see [scripts/run_lag_test.ps1](scripts/run_lag_test.ps1)): for a 50 ms lag setting the one-way delay is ~54 ms with ~5 ms spread, versus the old design's up-to-40 ms jitter.*
- **Out-of-memory hardening + first unit tests** — `createNode` (and `crate_stats_new`) now check their allocations and return `NULL` on OOM instead of dereferencing it; callers drop the packet rather than crashing (release builds are windowed, so this used to be a silent crash). The pure list logic (`swapNode`, moved into `packet.c`) and the bandwidth rate stats (`CRateStats`, extracted into `ratestats.{c,h}`) are now covered by unit tests under [test/](test/), runnable with [test/run_tests.ps1](test/run_tests.ps1) via `zig cc`. *(commits `df51177`, `ee4329c`)*
- **Configurable lag jitter ([#33](https://github.com/jagt/clumsy/pull/33))** — port of @dimbleby's PR onto the reworked scheduler: a new *Variation (ms)* control (CLI `--lag-variation`) adds uniform jitter so each packet's delay is drawn from `[lag − variation, lag + variation]`. *Verified on loopback UDP: at lag = 50 ms the one-way-delay stdev grows from 3.1 ms (variation 0) to 17.1 ms (variation 30), matching the 30/√3 ≈ 17.3 ms of a uniform ±30 ms distribution.* *(commit `f41c30a`)*
- **Packet pool allocator** — the data path previously did two `malloc`s (node + payload) per captured packet plus two `free`s, the long-standing *"using malloc in the loop is not good for performance"* TODO. Each node is now a single allocation (the `PacketNode` header followed by an inline payload buffer), and freed nodes are recycled on per-size-class free lists instead of being returned to the CRT, so warm steady-state traffic stops hitting `malloc`/`free` entirely. The `createNode`/`freeNode` interface is unchanged, so every module benefits transparently. Worst-case idle memory is bounded (~6 MiB across the three classes); the pool is released on stop. Covered by new unit tests in [test/test_packet.c](test/test_packet.c) (recycle, size-class selection, oversize bypass).
- **Tamper/reset checksum fix + tamper tests** — `WinDivertHelperCalcChecksums` was called with `pAddr = NULL`, a leftover from the WinDivert 1.x port (whose signature had no address argument). Under WinDivert 2.x that argument is `__out_opt`: passing it lets WinDivert update the address's checksum/offload flags so the recomputed checksums stay consistent when the packet is re-injected — the likely cause of the old `// FIXME checksum seems to have some problem`. Both [tamper.c](src/tamper.c) and [reset.c](src/reset.c) now pass `&pac->addr`. The pure payload-corruption logic was extracted into [tamper_core.{c,h}](src/tamper_core.c) and unit tested in [test/test_tamper.c](test/test_tamper.c), including an exhaustive check that the corrupted slice stays within bounds for every length up to a full 64 KiB packet. Verified two ways: a driver-free harness ([test/verify_checksum.c](test/verify_checksum.c)) that recomputes checksums via WinDivert's user-mode helpers and validates them with an independent RFC 1071 implementation (confirms tamper breaks the UDP checksum and `CalcChecksums(&addr)` makes it valid again and sets the address flags); and an end-to-end live run with clumsy elevated tampering loopback UDP ([scripts/loopback_tamper_test.py](scripts/loopback_tamper_test.py)) — 20/20 datagrams corrupted yet delivered. *(Note: Windows loopback doesn't validate UDP checksums, so the delivery difference between checksum-on and -off isn't observable there; the math is confirmed by the driver-free harness.)*
- **Shutdown `closeDown`/`startUp` pairing + handle leak** — on stop, the clock loop ran each module's `closeDown` gated on `enabledFlag` instead of `lastEnabled` (the gate the normal consume step uses), breaking the `startUp`↔`closeDown` pairing in racy toggle windows. Two manifestations: `closeDown` without a prior `startUp` (e.g. enabling Lag and hitting Stop at the wrong instant ran `lagCloseDown` on an uninitialised buffer → NULL deref on the first run), and a skipped `closeDown` (buffer not flushed + leaked nodes the pool can't reclaim). The shutdown path now gates on `lastEnabled`, identical to the consume step. Same commit also fixes a handle leak: `divertStop` never called `CloseHandle` on the mutex / event / loop thread / clock thread, leaking 4 handles per Start/Stop cycle; they're now closed after the thread join and recreated on the next start. Verified end-to-end with an A/B harness over the real `divertStart`/`divertStop` ([test/divert_cycle.c](test/divert_cycle.c)): fixed build +0 handles/cycle, leaky build +4.00 handles/cycle over 60 cycles. *(commit `5bd566a`)*
- **Config parsing hardened + extracted** — `loadConfig` had a real out-of-bounds bug: the parse loop incremented the record count without ever bounding it against `CONFIG_MAX_RECORDS` (64), so a `config.txt` with many short lines wrote well past `filters[64]` and corrupted adjacent globals. Fixed by bounding the loop, plus `isspace((unsigned char))` casts, a `p == NULL` guard on the module-path split, and the missing `fclose`. The pure in-place parsing logic was extracted into [config_parser.{c,h}](src/config_parser.c) and covered by 39 checks in [test/test_config.c](test/test_config.c), including the anti-overflow invariant. *(commit `618f06a`)*
- **Parameterized setter robustness (`setFromParameter`)** — parameterized/CLI mode applied each value then manually fired the widget's callback, *guessing* the type from which callback happened to be present (only IupToggle/IupText handled), with a `// FIXME ... Iup lacks a way to get widget's type`. The premise was wrong — IUP exposes the class via `IupGetClassName` — and guessing could fire a callback with the wrong signature (IupText's legacy `ACTION` is `(ih, int, char*)`). [setFromParameter](src/utils.c) now dispatches explicitly on the widget class (`toggle` → `ACTION(ih, state)`, `text` → `VALUECHANGED_CB(ih)`, anything else logged instead of guessed). Verified elevated with the real driver ([scripts/run_setparam_test.ps1](scripts/run_setparam_test.ps1)): all toggle/text params dispatch (0 unhandled) and `--drop on --drop-chance 100` on loopback delivers 0/40. *(commit `6666d4e`)*
- **lag/throttle packet pickup — O(n²) worst case removed** — both modules moved matching packets into their buffer by popping the current node and restarting the scan from `tail->prev`, re-walking the trailing non-matching packets on every move (quadratic under partial inbound/outbound filtering). They now capture `prev` before the pop and walk the list once; the buffer order is identical. Verified on loopback ([scripts/run_scan_test.ps1](scripts/run_scan_test.ps1)): lag delivers 40/40 (mean ~107 ms), throttle delivers 40/40 and engages. *(commit `472daff`)*
- **Module unit tests; inbound-ICMP FIXME verified** — `drop` / `duplicate` / `ood` `process()` are now unit-tested directly through their public `Module` structs (no module-code refactor) with a test-controlled `calcChance` stub, in [test/test_modules.c](test/test_modules.c) (29 checks; the full suite is now 235 checks, green via [test/run_tests.ps1](test/run_tests.ps1)). Separately, the stale `// FIXME inbound injection ... failing with a very high percentage` in [divert.c](src/divert.c) was confirmed obsolete under WinDivert 2.2 — inbound ICMP reinjects on the first `WinDivertSend` ([scripts/run_icmp_test.ps1](scripts/run_icmp_test.ps1)) — and replaced with a verified note. *(commits `c3c976f`, `6843a7f`)*
### Building
Built with **Zig 0.9.1** (the `build.zig` uses the pre-0.10 API). On Windows, point `rc.exe` at an installed Windows SDK if the default doesn't exist:
```sh
zig build -Darch=x64 -Dconf=Debug -Dsign=A -Dwindows_kit_bin_root="C:/Program Files (x86)/Windows Kits/10/bin/10.0.26100.0"
```
## Details
Simulate network latency, delay, packet loss with clumsy on Windows 7/8/10:

## License
MIT