A Swift command-line tool to programmatically manage bandwidth rules on Grandstream WiFi access points. Perfect for home automation, parental controls, or dynamic network management.
- List bandwidth rules - View all configured rules with their settings
- Add/Update rules - Create new rules or modify existing ones
- Delete rules - Remove rules by name or MAC address
- Throttle randomized MACs - Automatically limit clients with random ("private") MAC addresses, cron-friendly
- Aliases support - Use friendly names instead of MAC addresses
- Cross-platform - Works on macOS and Linux
I want to throttle the bandwidth of specific wifi devices using my home automation system. Therefore I need a cli tool to change bandwidth rules programmatically. Grandstream supports schedules, but I need to (un)throttle things on demand.
Why would anyone do that? Well, think of the Apple TV. It still works, but cannot stream movies at 96kb/s. Also iPads, smartphones etc. It works somehow, but YouTube, TikTok etc. become unusable at low bandwidths. Did you say 'bad parenting'? Well, I call it a challenge for my kids to learn some things about networking 😬
- macOS: macOS 13+ with Swift 6.2+ (Xcode 16+)
- Linux: Swift 6.2+ with FoundationNetworking
- Access Point: Grandstream GWN series AP
swift buildFor release build:
swift build -c release# Build release version
swift build -c release
# Copy to a location in your PATH
cp .build/release/gwncli /usr/local/bin/gwncli list \
--url "https://gwn_c074ad7b2950.local" \
--username admin \
--password yourpasswordgwncli set \
--url "https://gwn_c074ad7b2950.local" \
--username admin \
--password yourpassword \
--mac AA:BB:CC:DD:EE:FF \
--ssid ssid0 \
--drate 96Kbps \
--urate 96KbpsNote: Upload/download rates must be expressed as Mbps or Kbps (case-sensitive).
By rule name:
gwncli delete \
--url "https://gwn_c074ad7b2950.local" \
--username admin \
--password yourpassword \
--rule-name rule4By MAC address (deletes all rules for that device):
gwncli delete \
--url "https://gwn_c074ad7b2950.local" \
--username admin \
--password yourpassword \
--mac AA:BB:CC:DD:EE:FFDevices using MAC randomization ("private WiFi address") have the locally administered
bit set in their MAC. The throttle-locally-administered subcommand fetches the AP's
client list and adds a bandwidth rule for every such client that does not have one yet:
gwncli throttle-locally-administered \
--url "https://gwn_c074ad7b2950.local" \
--username admin \
--password yourpassword- Default limits are
32Kbpsdown /1000Mbpsup; override with--drate/--urate. - The SSID for new rules is resolved from the SSID the client is connected to; use
--ssid ssid0to force one. --dry-runprints what would be throttled without changing anything.- The command prints one line per newly created rule and nothing at all when there is nothing to do, so it is ideal for cron. Anyone joining with a random MAC gets full bandwidth for at most one cron interval:
*/5 * * * * gwncli throttle-locally-administered --url "https://gwn_c074ad7b2950.local" --username admin --password yourpasswordTo exempt a device permanently, disable "private WiFi address" for your network on the
device, or delete its rule and add a rule with better rates via gwncli set (existing
rules are never overwritten by throttle-locally-administered).
The reverse-engineered GWN API used by this tool is documented in gwnapi.md.
Randomized MACs churn, so auto-created rules accumulate over time. The
cleanup-locally-administered subcommand deletes rules for locally administered MACs
whose client the AP does not know anymore, or has not been seen for longer than
--max-age (default 7d, accepts m/h/d suffixes):
gwncli cleanup-locally-administered \
--url "https://gwn_c074ad7b2950.local" \
--username admin \
--password yourpassword \
--aliases ~/.gwnaliases.txt \
--dry-runMACs listed in the aliases file are never deleted — the aliases file doubles as a
keep-list, so add any manually created rule for a random MAC there. Like the throttle
command it only prints when it actually deletes something, and supports --dry-run:
13 4 * * * gwncli cleanup-locally-administered --url "https://gwn_c074ad7b2950.local" --username admin --password yourpassword --aliases /home/user/.gwnaliases.txtNote that devices using a stable private address (the iOS/Android default) reuse their random MAC when they rejoin, so a cleaned-up returning device simply gets re-throttled by the next throttle cron run.
Create a file ~/.gwnaliases.txt with MAC-to-name mappings:
AA:BB:CC:DD:EE:FF Kids-iPad
11:22:33:44:55:66 AppleTV
Then use the --aliases flag:
gwncli list \
--url "https://gwn_c074ad7b2950.local" \
--username admin \
--password yourpassword \
--aliases ~/.gwnaliases.txtOutput will show friendly names:
rule0 [enabled] U: 96Kbps D:96Kbps mac: aa:bb:cc:dd:ee:ff (Kids-iPad) SSID: ssid0 "Main-WiFi"
Use --log-level to control verbosity (1=fatal, 5=debug):
gwncli list --url "..." --username admin --password pass --log-level 5Grandstream APs use self-signed certificates. On macOS, the tool ignores certificate warnings. On Linux, URLCredential(trust:) is unavailable.
Workaround: Set up nginx as a reverse proxy with proxy_ssl_verify off;:
server {
listen 443 ssl;
server_name gwn-proxy.local;
location / {
proxy_pass https://gwn_c074ad7b2950.local;
proxy_ssl_verify off;
}
}Then point gwncli to your proxy instead.
Access control example for network 192.168.1.0/24. This assumes your current public v4/v6 IP can be found in /etc/nginx/snippets/ipv4-public-address.conf and /etc/nginx/snippets/ipv6-network.conf:
server {
listen 443 ssl;
server_name gwn-proxy.local;
# SSL certificate configuration
ssl_certificate /etc/letsencrypt/live/gwn-proxy.local/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/gwn-proxy.local/privkey.pem;
location / {
# Allow local network and your public IP only
satisfy any;
allow 192.168.1.0/24;
allow fd00::/8;
include /etc/nginx/snippets/ipv4-public-address.conf;
include /etc/nginx/snippets/ipv6-network.conf;
deny all;
# Proxy to Grandstream AP
proxy_pass https://gwn_c074ad7b2950.local;
proxy_ssl_verify off;
}
}Run tests:
swift testThe test suite includes JSON parsing tests and integration test helpers.
This is a personal project for my own use, but I'm happy to receive feedback, bug reports, or suggestions! Feel free to open an issue if you find something that could be improved.
MIT License - see LICENSE file for details.