Skip to content

Latest commit

 

History

9 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 

Repository files navigation

Charon

Charon is a temporary, foreground bandwidth limiter for Linux and macOS. It applies the same limit to upload and download traffic on one network interface, then restores normal networking when it exits.

The project has two native implementations:

File Platform Native traffic-control system
charon.sh Linux tc from iproute2
charon_macos.sh macOS PF and dummynet (pfctl and dnctl)

Charon does not require Homebrew, Python, containers, or a background service.

Features

  • Limits both ingress and egress traffic.
  • Runs visibly in the foreground.
  • Restores normal bandwidth on Ctrl+C, termination, or ordinary exit.
  • Prevents two Charon processes from managing the same interface.
  • Detects PID reuse instead of trusting a PID alone.
  • Recovers journaled state left by most forced or abnormal exits.
  • Uses unique, project-owned handles, filters, pipes, and anchors.
  • Refuses to overwrite pre-existing traffic-control configuration it cannot restore exactly.
  • Supports IPv4 and IPv6 traffic.
  • Makes no permanent system configuration changes.

Requirements

Linux

  • Bash
  • ip and tc from the iproute2 package
  • Root privileges through sudo
  • Kernel support for TBF, ingress qdiscs, u32 filters, and policing; these are available in normal general-purpose distribution kernels

Install iproute2 if it is missing:

# Arch Linux / CachyOS
sudo pacman -S iproute2

# Fedora
sudo dnf install iproute

# Debian / Ubuntu
sudo apt install iproute2

macOS

  • macOS with the built-in /sbin/pfctl, /usr/sbin/dnctl, and /sbin/ifconfig
  • Root privileges through sudo
  • The standard com.apple/* dummynet anchor in the active PF ruleset

No additional packages are required.

Quick start

Make the script for your operating system executable:

chmod +x charon.sh             # Linux
chmod +x charon_macos.sh       # macOS

Run Charon with an interface and rate:

# Linux
sudo ./charon.sh enp5s0 1mbit

# macOS
sudo ./charon_macos.sh en0 1mbit

Leave the terminal open while the limit is active. Press Ctrl+C to stop Charon and restore normal bandwidth.

Finding the correct interface

Running either script with --help lists the interfaces it can see:

./charon.sh --help
./charon_macos.sh --help

You can also identify the interface carrying the default route.

Linux

ip route show default

Example output:

default via 192.168.1.1 dev enp5s0 proto dhcp

The interface in this example is enp5s0.

macOS

route get default | awk '/interface:/{print $2}'

Wi-Fi is commonly en0, but the actual name should always be checked.

Rate syntax

For scripts, aliases, and cross-platform use, prefer bit-rate units:

Example Meaning
500kbit 500 kilobits per second
1mbit 1 megabit per second
25mbit 25 megabits per second
2.5gbit 2.5 gigabits per second

Units are case-insensitive, the value must be positive, and bit, kbit, mbit, and gbit may optionally be followed by /s on either platform.

The Linux version also accepts the bps, kbps, mbps, gbps, and tbps spellings understood by tc. Those denote byte-rate units in tc; use kbit, mbit, or gbit when you mean network speeds in bits per second.

On macOS, gigabit values are translated into the highest unit documented by Apple's dnctl implementation.

Optional system-wide installation

On the appropriate operating system, the script can be installed as charon:

# Run this on Linux
sudo install -m 0755 charon.sh /usr/local/bin/charon

# Or run this on macOS
sudo install -m 0755 charon_macos.sh /usr/local/bin/charon

It can then be invoked from any directory:

sudo charon en0 1mbit

Use the actual interface name for the machine.

What happens while Charon runs

  1. Arguments, privileges, dependencies, interface existence, and rate syntax are validated.
  2. Charon acquires an atomic, per-interface lock.
  3. Existing traffic-control state is checked for conflicts.
  4. Project-owned shaping rules are applied.
  5. Charon remains in the foreground until it is stopped.
  6. Cleanup removes only state identified as belonging to Charon.

The foreground process is intentional. It provides an obvious owner for the temporary rules and makes Ctrl+C a reliable off switch.

Platform behavior

Linux

Egress traffic is shaped with a Token Bucket Filter using a 32 KB burst and a 400 ms latency allowance. Ingress traffic is policed at the same configured rate with a 100 KB burst.

Linux cannot truly shape packets that have already arrived from the network. Ingress policing therefore drops excess incoming packets, allowing protocols such as TCP to reduce their sending rate. Download measurements may fluctuate more than upload measurements, especially at very low limits.

Before applying anything, Charon inspects the interface:

  • An egress qdisc hierarchy whose displayed handles are all 0: is considered implicit kernel/driver state and safe to replace temporarily. This includes common defaults such as fq_codel, noqueue, and mq.
  • Any egress qdisc with a nonzero handle is treated as user-managed, including a child beneath an implicit mq root.
  • Any existing ingress or clsact qdisc is treated as user-managed.

If user-managed state is found, Charon exits rather than destroying it. Linux cannot reliably reconstruct an arbitrary qdisc hierarchy from display output, so refusing is safer than pretending it can restore the configuration.

During setup, progress is journaled under /run. If the process is killed between individual tc operations, the next invocation for the same interface can identify and remove the incomplete Charon state. If ordinary cleanup cannot confirm removal, Charon keeps that journal instead of discarding the recovery information.

macOS

macOS uses two separate dummynet pipes: one for outgoing packets and one for incoming packets. Using separate pipes prevents the two directions from sharing one half-duplex bandwidth pool.

PF rules are loaded into a private anchor beneath com.apple/*. Charon does not replace /etc/pf.conf or flush the main PF ruleset. This protects firewall, Internet Sharing, AirDrop, VPN, and other system-managed PF state.

When PF is enabled, Charon requests its own enable-reference token. Cleanup releases that token with pfctl -X; it does not disable PF if another macOS component still needs it.

Dummynet pipe numbers are allocated from a private high-number range. A global allocator lock prevents simultaneous Charon processes on different interfaces from selecting the same pipes.

If the active PF configuration does not expose Apple's wildcard dummynet anchor, Charon exits. It will not replace a custom PF configuration merely to make itself work.

Stopping and cleanup

The normal way to stop Charon is:

Ctrl+C

It can also be terminated from another terminal:

sudo kill <pid>

Avoid kill -9. SIGKILL cannot be trapped by any program, so immediate cleanup is impossible. If Charon is killed that way, run the same script again on the same interface. Its stale-state recovery will remove journaled remnants before applying the new limit. A reboot also clears the temporary kernel state.

Do not manually delete Charon's lock file while its process is running. The lock contains the ownership information needed for conservative cleanup.

Verifying the active limit

Open a second terminal while Charon is running.

Linux

sudo tc qdisc show dev enp5s0
sudo tc filter show dev enp5s0 parent ffff:

The root qdisc should include tbf, and the ingress filter should include a police action at the requested rate.

macOS

sudo dnctl pipe show
sudo pfctl -s Anchors | grep charon

Two Charon-owned pipes should be visible with the requested bandwidth.

Real-world throughput will usually be slightly below the configured rate due to protocol overhead, queue behavior, TCP congestion control, and the speed test's own measurement method.

VPNs and virtual interfaces

Charon limits the interface named on the command line.

  • Limiting a physical interface such as en0 or enp5s0 generally also limits encrypted VPN traffic that ultimately crosses that interface.
  • Limiting a tunnel interface such as utun3, tun0, or wg0 targets traffic visible inside that tunnel.
  • Interfaces may disappear when a VPN disconnects. Stop Charon before removing the interface whenever possible.

Troubleshooting

Permission denied

Make the file executable:

chmod +x charon.sh charon_macos.sh

This script must be run with root privileges

Use sudo:

sudo ./charon.sh enp5s0 1mbit

The interface does not exist

Run the relevant script with --help, or use the default-route commands above. Do not use a friendly name such as Wi-Fi; Charon needs the kernel interface name, such as en0.

Linux reports existing custom traffic-control state

Inspect it without changing it:

sudo tc qdisc show dev enp5s0
sudo tc filter show dev enp5s0

Another network manager, QoS service, container system, VPN, or prior manual configuration may own those rules. Charon intentionally refuses to erase them. Stop the owning service or select another interface instead of blindly deleting the qdiscs.

macOS reports that the com.apple/* anchor is unavailable

The active PF main ruleset has been customized or was not loaded normally. Charon refuses to overwrite it. Restore or deliberately integrate Apple's dummynet anchor in the PF configuration before trying again.

A second instance is already managing the interface

Return to the terminal running Charon and press Ctrl+C. If the recorded PID no longer exists, the next invocation will treat the lock as stale and recover it.

The measured download rate is uneven on Linux

This is expected from ingress policing. Incoming excess packets must be dropped rather than queued before arrival. TCP will converge toward the configured rate, but short tests and very small limits can look uneven.

An SSH session becomes slow or disconnects

Charon also limits the SSH traffic crossing the selected interface. Be careful when applying very low limits remotely. Running it from a local terminal is safer when experimenting.

Scope and limitations

Charon is intentionally small and temporary. It currently does not provide:

  • Different upload and download rates
  • Per-application, per-process, per-host, or per-port limits
  • Persistent limits after reboot
  • A daemon, launch agent, or systemd service
  • A graphical interface
  • Latency, jitter, or packet-loss simulation as user-facing options

Those features require a broader policy and configuration model than Charon's current one-command, one-interface contract.

Safety model

Charon follows four rules:

  1. Validate before changing kernel state.
  2. Never silently replace traffic-control configuration that may belong to another program.
  3. Record ownership before and during multi-step setup.
  4. Remove only resources that can still be identified as Charon's.

The scripts do not access the network, download dependencies, modify startup configuration, or transmit data. All state is local and temporary.

About

Temporary, foreground bandwidth limiter for Linux and macOS

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages