Bandwidth Throttling on macOS

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 pfctl 3.

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.

Adding anchor

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 pfctl -a).

Always backup the configuration file before editing it. Then add this line (plus a newline) to /etc/pf.conf.

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.

Then reload the PF config by running

sudo pfctl -f /etc/pf.conf

Now if one lists the rules loaded by running 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 properly.

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

Here -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 the pipe.

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

  1. There is an ACM SIGCOMM CCR article on this! [return]
  2. On Linux this is done using qdisc (queue discpline) and optionally iptables. [return]
  3. So there are some subtleties: actually PF can be enabled with either -e or -E. The difference here is that -E will “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 -X flag to decrement the counter. Only when the reference count reaches zero will PF be shut down. However, one can override this mechanism by passing -d flag. [return]
  4. Units must immediately follow the numbers. Units can be [K|M]{bit/s|Byte/s}. [return]
  5. For simpler use cases, pfctl also supports reading rules from STDIN by setting flag -f followed by a dash (pfctl -f -) [return]