Network Bandwidth Throttling on macOS
macOS comes with PF (packet filter, the BSD firewall) and dummynet (the BSD traffic shaper 1) which allow one to simulate various network conditions easily 2. Use dummynet to create a traffic shaper, and use PF to route packets there to get throttled/delayed/queued.
PF is disabled by default on macOS. Start by enabling it using
sudo pfctl -E
pfctl will complain that “No ALTQ support in kernel”. ALTQ is used for QoS and Apple decided not to
port it over to macOS, the reason supposedly being that macOS is a desktop operating system. But anyway,
let’s just ignore the warning here.
When reloading rules using
pfctl -f, PF flushes out all existing rules, so it is desired that the
existing ones (usually the main ruleset defined at
/etc/pf.conf) be added back after reloading rules.
To avoid hassle, we create an “anchor” in the main ruleset and from now on we will only need to reload
the anchor we created (by limiting the scope of flushing using
Always backup the configuration file before editing it. Then add this line (plus a newline) to
dummynet-anchor ts anchor ts
It defines a new anchor called “ts” to which we can add our own rules. It also specifies that the rules
loaded will include dummynet rules. Now if one lists the rules loaded
sudo pfctl -s rules, our newly created anchor will pop up here as
anchor "ts" all.
Creating a traffic shaper
Dummynet provides two primitives for traffic shaping: pipe and queue. Here a pipe queues packets in the front of it and emulates bandwidth limiter, propagation delay, buffer size, and packet loss. A queue stands for a weighted fair-queuing policy on multiple pipes. Traffic can then be directed into the pipes using PF.
What we need here is a pipe. There are a whole lot of options (refer to the
dnctl manpage). Here we will
just add a bandwidth limiter 4. We also set the buffer size so that TCP congestion control will work
sudo dnctl pipe 1 config bw 100Kbit/s queue 10
The pipe should now show up when running
sudo dnctl show.
Redirecting traffic into the traffic shaper
The last step is to redirect some traffic into the dummynet pipe, so we will head back to PF. Recall that we have created an anchor “ts” to insert our own rules. Now we write our rules in a file 5:
dummynet out proto tcp from any to any port 6000 pipe 1 dummynet out proto tcp from any port 6000 to any pipe 1
Then we load those rules into PF.
sudo pfctl -a ts -f rules.txt
-a ts tells it to only flush the “ts” anchor and
-f rules.txt specifies the file to read rules from.
The rules start with “dummynet” followed by a bunch of parameters that filters the packets. In the end
is “pipe 1” which tells PF to forward the packets to dummynet pipe 1.
A list of all possible filtering parameters can be viewed at
pf.conf manpage, section “PARAMETERS”. Here,
“out” means that the rule applies to packets going out of the interface. “any port 6000” include any IP address and
port 6000. The effect of the rules is putting all traffic coming from or going to any address at port 6000 into
Resetting the rules
To reset PF and dummynet to a clean state, we need to remove the rules. For PF, we can simply flush out the rules in anchor “ts”.
sudo pfctl -a ts -F all
For dummynet, run the following command to flush its rules. The
-q flag tells
dnctl to be quiet and is optional.
sudo dnctl -q flush
- There is an ACM SIGCOMM CCR article on this! [return]
- On Linux this is done using qdisc (queue discpline) and optionally iptables. [return]
- So there are some subtleties: actually PF can be enabled with either
-E. The difference here is that
-Ewill “increment the reference count” and return a token. PF then knows that a new process/service is using it. When shutting down PF, the process will pass the token to
-Xflag to decrement the counter. Only when the reference count reaches zero will PF be shut down. However, one can override this mechanism by passing
- Units must immediately follow the numbers. Units can be
- For simpler use cases,
pfctlalso supports reading rules from STDIN by setting flag
-ffollowed by a dash (
pfctl -f -) [return]