Traffic micro-management: limit network bandwidth used by an OS X process

Problem: you have some silly uploader app that only knows one speed: maximum. You would love a way to make that app back off so it doesn’t saturate your uplink and badly impact the other things using your network. Previously in OS X, you would accomplish this using ipfw, but these days you’d use pfctl and dnctl.

None of that is breaking news. The reason for this post is that I thought of an easy way to make the limits apply only to a specific app’s traffic, and all of that traffic. Traditionally you’d have to identify the traffic using some combination of source or destination IP address or port, which can get quite cumbersome. PF also supports matching packets by uid and gid. Let’s use gid, and then run the target app with a custom gid :)

First, make a new unix group called throttle:

sudo dseditgroup -o create throttle

Next, give yourself permission to run things with the group id of the throttle group. To do this, edit /etc/sudoers to add a line such as this, substituting your actual user name.

username ALL=(ALL:throttle) ALL

To test the new group and sudo configuration, run ‘id -g’, and then again with a custom group id (using sudo’s -g option). The results should be different.

$ id -g
20 
$ sudo -g throttle id -g
502

Now we can instantiate the requisite PF and dummynet setup. Here’s a little script that wants to be run as root.

#!/bin/bash

# Reset dummynet to default config
dnctl -f flush

# Compose an addendum to the default config to create a new anchor
read -d '' -r PF <<EOF
dummynet-anchor "throttle"
anchor "throttle"
EOF

# Reset PF to default config and apply our addendum
(cat /etc/pf.conf && echo "$PF") | pfctl -q -f -

# Configure the new anchor
cat <<EOF | pfctl -q -a throttle -f -
no dummynet quick on lo0 all
dummynet out proto tcp group throttle pipe 1
dummynet out proto udp group throttle pipe 1
EOF

# Create the dummynet queue - adjust speed as desired
dnctl pipe 1 config bw 1Mbit/s

# Show new configs
printf "\nGlobal pf dummynet anchors:\n"
pfctl -q -s dummynet
printf "\nthrottle anchor config:\n"
pfctl -q -s dummynet -a throttle
printf "\ndummynet config:\n"
dnctl show queue

# Enable PF
pfctl -E

Finally, start the target app as follows:

sudo -g throttle open -a "Whatever"

To watch the counters on the bandwidth limiting queue:

sudo dnctl show queue

To clear all PF config / state and reset PF to defaults:

sudo pfctl -F all ; sudo pfctl -q -f /etc/pf.conf

NB: prior to 10.11.2, use of “pfctl -F all” might kernel panic your machine. This post was made public in December, but I wrote it back in August and filed the kernel panic bug then, which is fixed as of 10.11.2. Unfortunately, the bug goes back at least as far as 12F45…

To wrap up, keep in mind that this is a hack. What we’re really trying to accomplish requires both a privileged position on the network and also more expressive and fine-grained traffic controls than are provided by dummynet. As both of those things are not always available, doing dumb rate limiting on an individual host as documented above can still be useful.

While we’re here, let’s take a moment to illustrate the difference between dumb host-level rate limits (which I am derisively referring to as “traffic micro-management”) and proper traffic shaping at the network edge. Two primary goals of traffic shaping are: 1) avoid congestion at the bottleneck(s), which is typically your internet connection, and 2) maximize network utilization. Congestion happens when packets traversing the bottleneck get piled up (‘queued’) and have to wait a relatively long time to get through. This results in high ping times (known on Twitch as: “high ms”), in-game lag, and generally sluggish performance of any software that directly or indirectly uses the Internet. The way to avoid congestion at the bottleneck is to avoid sending more traffic than can pass through the bottleneck without incurring substantial additional delay. We also want to utilize as much of the bottleneck’s capacity as possible. These are somewhat opposed goals.

As an analogy for the two goals of avoiding congestion and maximizing throughput, imagine pouring stuff in a funnel as fast as possible without allowing any stuff to accumulate in the funnel.

Now imagine performing the above experiment again, but with multiple people pouring stuff in the funnel, each starting and stopping at random times with no coordination between them. Chances are good that the multi-user experiment won’t do as well at avoiding congestion and /or maximizing throughput. For the same reason, the only way to reliably do traffic shaping to avoid network congestion is to do it at the edge of the network (such as a router), where traffic shaping policies can account for and apply to all traffic that traverses the bottleneck.

About dre

I like all kinds of food.
This entry was posted in OS X, scripts, The More You Know, tutorials. Bookmark the permalink.

3 Responses to Traffic micro-management: limit network bandwidth used by an OS X process

  1. Pingback: Michael Tsai - Blog - Protecting Your Network From Photos Uploads

  2. Pingback: Filter by group with pfctl * Error 53 Iphone Fix

    • dre says:

      I’m not sure what happened here. Either this never worked, or it stopped working since I posted about it. I spent a couple hours recently trying to make it work, with no luck. I still don’t know why it’s broken, but I’m fully convinced that it is broken.

Leave a Reply