This guide assumes that you have already set up an access point deamon with DHCP Serving enabled
If not, you can follow this guide
Installing And configuring IP Forwarding
Enable forwarding
This step only requires editing the linux sysctl.conf file by calling nano /etc/sysctl.conf
:
Look for and uncomment the below line :
net.ipv4.ip_forward=1
Set up and install NAT rules
Then make sure you have installed iptables
and iproute2
packages :
sudo apt-get update
sudo apt-get install iproute2 iptables
This will bring in tc
which will allow us to set up traffic rules as well as NAT :
iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
iptables -A FORWARD -i eth0 -o wlan0 -m state --state RELATED,ESTABLISHED -j ACCEPT
iptables -A FORWARD -i wlan0 -o eth0 -j ACCEPT
Make sure that you replace your interface names so it matches the ones from your system
Finally we will make those permanent by creating /etc/iptables.ipv4.nat
which we will use upon startup to restore NAT:
sh -c "iptables-save > /etc/iptables.ipv4.nat"
We will use rc.d to set back these rules. Edit /etc/rc.local
and add iptables-restore < /etc/iptables.ipv4.nat
to it :
#!/bin/sh -e
#
# rc.local
#
# This script is executed at the end of each multiuser runlevel.
# Make sure that the script will "exit 0" on success or any other
# value on error.
#
# In order to enable or disable this script just change the execution
# bits.
#
# By default this script does nothing.
# Print the IP address
_IP=$(hostname -I) || true
if [ "$_IP" ]; then
printf "My IP address is %s\n" "$_IP"
fi
iptables-restore < /etc/iptables.ipv4.nat
exit 0
Reboot and verify that IP forwarding is working fine by running a speedtest.
If your ethernet interface and WAN are not the bottleneck in your setup, you will be able to keep the test results as a reference
Test setup and architecture used for writing this guide :
WAN (10G-BaseT) <==ETHERNET==> RaspberryPi 3 eth0(1G-BaseT) <==Forwarding==> RaspberryPi 3 wlan0(SDIO 25MHz WFx200) <==WiFi==> Samsung Galaxy S23 (speedtest.net)
Speedtest details :
DownStream tested using 36.5MB file
UpStream tested using 30.3MB file
Results were :
Downlink : 30.3 Mbps
Uplink : 23.2 Mbps
Setting Up traffic control for WLAN clients
Before jumping into traffic control, it is important to note that WiFi stations will first send data over the air according to 802.11. Traffic control occurs one step further in the AP.
802.11 Defines QoS in tis frames and priorities using 8bit ToS field, but all devices may not use this field properly (see qos_map_set in hostapd.conf). Hence this guide's focus on IP traffic management.
The goal we will try to achieve here is to share unevenly our WiFi IP bandwidth so that unwanted joining device cannot overflow the AP by dropping their traffic a step further.
Several options are available on linux systems. In this guide tc
is the utility from iproute2 that will help us maintain wlan0
traffic shaped the way we want.
We will use the following scenario to set up our system :
- Our router acts as an ethernet range extender
- Our router must accept 802.11 associations from up to 12 clients
- Our router must always keep room for up to 8 "VIP" clients (identified via MAC)
- VIP clients will be guaranteed a bandwidth of up to 625kbps (kbits/sec)
- Other clients will have to share the remaining
- Traffic control will be done at the IP level
Note : The chipset used in this scenario was the WFx200. Silicon Labs limited the number of concurrent WiFi connections to 14 in their driver sources
See line of hif_api_cmd.h
:
#define HIF_LINK_ID_MAX 14
Automatically define IP and AP settings upon startup
To enable the above we will have to share some settings across services. Therefore we will define a script that will make this easier to set everything up. To do so we will use rc.local once more :
nano /etc/rc.local
Add the following lines to it, before exit 0
:
# hostapd.conf editing
# Edit SSID
sed -i -s "s/^ssid=.*/ssid=WF200_QOS/" /etc/hostapd/hostapd.conf
# Edit max number of stations
sed -i -s "s/^max_num_sta=.*/max_num_sta=12/" /etc/hostapd/hostapd.conf
# dhcpcd.conf editing
# wlan0 static address edit, using , as regex delimitor
sudo sed -i -s "s,^[ \t]*static ip_address=.*, static ip_address=192.168.5.254/24," /etc/dhcpcd.conf
# dnsmasq.conf editing
# wlan 0 dhcp range edit
sed -i -s "s/^dhcp-range=.*/dhcp-range=192.168.5.2,192.168.5.14,255.255.255.0,24h/" /etc/dnsmasq.conf
This will automatically go edit 3 services configuration files that should be present in your system :
/etc/hostapd/hostapd.conf
/etc/dnsmasq.conf
/etc/dhcpcd.conf
It will rename our access point and limit its clients number to 12
, define our wlan0
ip to 192.168.5.1
and define our IP range served to limit it to 12 as well
At this point we have a fully functional AP which :
- Does not accept more than 12 WiFi Stations
- Does not provide more than 12 IP Addresses to DHCP clients
- Does not perform any traffic arbitration
Disable all lines in rc.local
Setting dnsmasq so it provides 2 ranges of addresses
The strategy we will be using to "discriminate" between known devices and other devices is traffic arbitration based on IP range.
We will set dnsmasq
to distributes IP adresses depending on the MAC ones of the stations
To do so we will edit /etc/dnsmasq.conf
to create TAGs based on MAC addresses :
# We target wlan0 interface
interface=wlan0
# We deine the IP range, as well as DHCP Lease time
#dhcp-range=192.168.5.2,192.168.5.14,255.255.255.0,24h
dhcp-mac=set:known_mac,A4:75:B9:*:*:*
dhcp-range=tag:known_mac,192.168.5.2,192.168.5.10,255.255.255.0,24h
dhcp-range=192.168.5.129,192.168.5.132,255.255.255.0,24h
At this point 2 sets of addresses will be distributed, allowing us to perform IP based QOS
Setting up traffic control on WLAN traffic
In order to achieve both way traffic control on both subnets we will be taking advantage of linux kernel module named ifb (Intermediate Functional Block)
By default the TC utility does not allow us to control traffic coming from WLAN clients (ingress) easily. Therefore we will redirect all ingress traffic towards an ifb interface on which we will be able to apply tc properly
Below is the TC architecture we will be using :
# wlan0 - Egress traffic going from WiFi AP to WiFi Clients
#
# +----------+
# | root 1: |
# +----------+
# |
# +-----------------------------------------------+
# | class 1:1 Overall Bandwidth set to 10 MBits/s |
# +-----------------------------------------------+
# |
# +----------------------+
# |1:10 Default Traffic |
# +----------------------+
# ifb0 - Ingress traffic going from WiFi Clients to WiFi AP (mirrored from wlan0)
#
# +----------+
# | root 1: |
# +----------+
# |
# +-----------------------------------------------+
# | class 1:1 Overall Bandwidth set to 10 MBits/s |
# +-----------------------------------------------+
# |
# +----------------------+
# |1:10 Default Traffic |
# +----------------------+
Since we measured a top 30 Mbits/s on our interface earlier, we will limit the overall interface traffic to 10Mbits/s each way
Below steps configure an ifb interface and mirrors all ingress traffic towards ifb0
We explicitly tell modprobe to set up only one interface (as it defaults to 2 otherwise)
sudo modprobe ifb numifbs=1
sudo ip link set dev ifb0 up
sudo tc qdisc add dev wlan0 handle ffff: ingress
sudo tc filter add dev wlan0 parent ffff: protocol ip u32 match u32 0 0 action mirred egress redirect dev ifb0
At this point you should see a new interface appearing using ip a
Below steps shape traffic to WLAN clients (egress rules) according to above diagram
sudo tc qdisc add dev wlan0 root handle 1: htb default 10
sudo tc class add dev wlan0 parent 1: classid 1:1 htb rate 10mbit
sudo tc class add dev wlan0 parent 1:1 classid 1:10 htb rate 625kbit
Below steps shape traffic from WLAN clients (ingress mirrored as egress) according to above diagram
sudo tc qdisc add dev ifb0 root handle 1: htb default 10
sudo tc class add dev ifb0 parent 1: classid 1:1 htb rate 10mbit
sudo tc class add dev ifb0 parent 1:1 classid 1:10 htb rate 512kbit
The architecture was the same as previously
WAN (10G-BaseT) <==ETHERNET==> RaspberryPi 3 eth0(1G-BaseT) <==Forwarding==> RaspberryPi 3 wlan0(SDIO 25MHz WFx200) <==WiFi==> Samsung Galaxy S23 (speedtest.net)
Speedtest details :
DownStream tested using 0.32MB file
UpStream tested using 0.75MB file
Results were :
Downlink : 0.57 Mbps
Uplink : 0.46 Mbps
Dual way Traffic control depending on device origin
Now that we have set up traffic control, we will add one class that we will use to slow down even more traffic from undesired devices that may have joined our AP
# wlan0 - Egress traffic going from WiFi AP to WiFi Clients
#
# +----------+
# | root 1: |
# +----------+
# |
# +-----------------------------------------------+
# | class 1:1 Overall Bandwidth set to 10 MBits/s |
# +-----------------------------------------------+
# |
# +----------------------+ +----------------------+
# |1:10 Default Traffic | | 1:20 Unknown Traffic
# +----------------------+ +----------------------+
# ifb0 - Ingress traffic going from WiFi Clients to WiFi AP (mirrored from wlan0)
#
# +----------+
# | root 1: |
# +----------+
# |
# +-----------------------------------------------+
# | class 1:1 Overall Bandwidth set to 10 MBits/s |
# +-----------------------------------------------+
# |
# +----------------------+ +----------------------+
# |1:10 Default Traffic | | 1:20 Unknown Traffic
# +----------------------+ +----------------------+
On egress traffic, we will add class 1:20 and a filter that matches traffic to clients with IPs in the upper range of 192.168.5.0 . To clearly differentiate results we will also change known IPs limits
sudo tc class add dev wlan0 parent 1:1 classid 1:10 htb rate 2mbit ceil 2mbit
sudo tc class add dev wlan0 parent 1:1 classid 1:20 htb rate 256kbit ceil 256kbit
sudo tc filter add dev wlan0 parent 1: protocol ip prio 1 u32 match ip dst 192.168.5.128/25 flowid 1:20
On ingress traffic, we will do the same for traffic coming from the upper range of 192.168.5.0
sudo tc class add dev ifb0 parent 1:1 classid 1:10 htb rate 2mbit ceil 2mbit
sudo tc class add dev ifb0 parent 1:1 classid 1:20 htb rate 256kbit ceil 256kbit
sudo tc filter add dev ifb0 parent 1: protocol ip prio 1 u32 match ip src 192.168.5.128/25 flowid 1:20
Note : Even though we have not set 2 subnets but only defined a range with dnsmasq, tc will apply rules based solely on IP and not look for any subnet
The architecture was the same as previously
WAN (10G-BaseT) <==ETHERNET==> RaspberryPi 3 eth0(1G-BaseT) <==Forwarding==> RaspberryPi 3 wlan0(SDIO 25MHz WFx200) <==WiFi==> Samsung Galaxy S23 (speedtest.net)
Speedtest details for an known IP:
DownStream tested using 1.24MB file
UpStream tested using 1.74MB file
Results were :
Downlink : 1.89 Mbps
Uplink : 1.86 Mbps
Speedtest details for an unknown IP:
DownStream tested using 0.19MB file
UpStream tested using 0.35MB file
Results were :
Downlink : 0.22 Mbps
Uplink : 0.22 Mbps
Automating QOS setup at startup
Just like IP forwarding rules we will apply those setting at startup using a script called by RC. Just for the sake of this guide we will place it in /etc :
nano /etc/tc.ipv4.wlan0
Then copy paste the below
#!/bin/bash
# Get rid of any existing qdisc on wlan0
tc qdisc del dev wlan0 root
modprobe ifb numifbs=1
ip link set dev ifb0 up
tc qdisc add dev wlan0 handle ffff: ingress
tc filter add dev wlan0 parent ffff: protocol ip u32 match u32 0 0 action mirred egress redirect dev ifb0
#Shape traffic to WLAN clients (egress rules)
tc qdisc add dev wlan0 root handle 1: htb default 10
tc class add dev wlan0 parent 1: classid 1:1 htb rate 10mbit
tc class add dev wlan0 parent 1:1 classid 1:10 htb rate 2mbit ceil 2mbit
tc class add dev wlan0 parent 1:1 classid 1:20 htb rate 256kbit ceil 256kbit
tc filter add dev wlan0 parent 1: protocol ip prio 1 u32 match ip dst 192.168.5.128/25 flowid 1:20
#Shape traffic from WLAN clients (ingress mirrored as egress)
tc qdisc add dev ifb0 root handle 1: htb default 10
tc class add dev ifb0 parent 1: classid 1:1 htb rate 10mbit
tc class add dev ifb0 parent 1:1 classid 1:10 htb rate 2mbit ceil 2mbit
tc class add dev ifb0 parent 1:1 classid 1:20 htb rate 256kbit ceil 256kbit
tc filter add dev ifb0 parent 1: protocol ip prio 1 u32 match ip src 192.168.5.128/25 flowid 1:20
Give execute permissions to the script using chmod +x /etc/tc.ipv4.wlan0.sh
And finally call it in rc.local :
nano /etc/rc.local
#!/bin/sh -e
#
# rc.local
#
# This script is executed at the end of each multiuser runlevel.
# Make sure that the script will "exit 0" on success or any other
# value on error.
#
# In order to enable or disable this script just change the execution
# bits.
#
# By default this script does nothing.
# Print the IP address
_IP=$(hostname -I) || true
if [ "$_IP" ]; then
printf "My IP address is %s\n" "$_IP"
fi
# hostapd.conf editing
# Edit SSID
sed -i -s "s/^ssid=.*/ssid=WF200_QOS/" /etc/hostapd/hostapd.conf
# Edit max number of stations
sed -i -s "s/^max_num_sta=.*/max_num_sta=12/" /etc/hostapd/hostapd.conf
# dhcpcd.conf editing
# wlan0 static address edit, using , as regex delimitor
sudo sed -i -s "s,^[ \t]*static ip_address=.*, static ip_address=192.168.5.254/24," /etc/dhcpcd.conf
# dnsmasq.con editing
# wlan 0 dhcp range edit
#sed -i -s "s/^dhcp-range=.*/dhcp-range=192.168.5.2,192.168.5.14,255.255.255.0,24h/" /etc/dnsmasq.conf
iptables-restore < /etc/iptables.ipv4.nat
/etc/tc.ipv4.wlan0.sh
exit 0
You can now reboot and check that these new settings are applied
Evenly share bandwidth among devices of a same group
What we did before was somewhat brutal, but allows us to never overload the WiFi chipset by both unknown devices, as well as known devices
We will re-work a few settings to allow our system to be more flexible
Changing DHCP Lease times
We used dnsmasq
to provide 2 ranges of addresses. Each range has a limited number of leases that can be provided to clients
However if clients end up leaving the network, dnsmasq
will only wait until the end of the lease to free a slot
Which means that lease time must be wisely chosen to reduce latency when adding/removing devices around the range limit
To do so, we will simply change the lease time in /etc/dnsmasq.conf
:
# We target wlan0 interface
interface=wlan0
# We deine the IP range, as well as DHCP Lease time
#dhcp-range=192.168.5.2,192.168.5.14,255.255.255.0,24h
dhcp-mac=set:known_mac,A4:75:B9:*:*:*
dhcp-range=tag:known_mac,192.168.5.2,192.168.5.10,255.255.255.0,2h
dhcp-range=192.168.5.129,192.168.5.132,255.255.255.0,30m
Revising tc
bandwidths
We applied Queue discpline on both egress and outgress traffic through ifb
In our case, assuming the WiFi chipset was the bottleneck, we measured ~30 Mbits/s datarate
According to Silicon Labs in this post it might be possible to achieve up to 50Mbits/s
Previously we defined a top IP datarate of 20 MBits shared evenly between egress and ingress traffic
We will change that balance as we do not expect devices to generate upload besides periodic reports and/or acks. We will set a 80%-20% instead
Another point that we had done was to not use 100% of our 1:1 classes, which means all of our allowed devices had to share an extremely downsized bandwidth
Here again we will revise these and provide higher rates and ceil values, keeping the sum of ceils within the 1:1 rate
This results in :
#!/bin/bash
# Get rid of any existing qdisc on wlan0
tc qdisc del dev wlan0 root
modprobe ifb numifbs=1
ip link set dev ifb0 up
tc qdisc add dev wlan0 handle ffff: ingress
tc filter add dev wlan0 parent ffff: protocol ip u32 match u32 0 0 action mirred egress redirect dev ifb0
#Shape traffic to WLAN clients (egress rules)
tc qdisc add dev wlan0 root handle 1: htb default 10
tc class add dev wlan0 parent 1: classid 1:1 htb rate 16mbit
tc class add dev wlan0 parent 1:1 classid 1:10 htb rate 14mbit ceil 15mbit
tc class add dev wlan0 parent 1:1 classid 1:20 htb rate 512kbit ceil 625kbit
tc filter add dev wlan0 parent 1: protocol ip prio 1 u32 match ip dst 192.168.5.128/25 flowid 1:20
#Shape traffic from WLAN clients (ingress mirrored as egress)
tc qdisc add dev ifb0 root handle 1: htb default 10
tc class add dev ifb0 parent 1: classid 1:1 htb rate 4mbit
tc class add dev ifb0 parent 1:1 classid 1:10 htb rate 2mbit ceil 3mbit
tc class add dev ifb0 parent 1:1 classid 1:20 htb rate 512kbit ceil 625kbit
tc filter add dev ifb0 parent 1: protocol ip prio 1 u32 match ip src 192.168.5.128/25 flowid 1:20
With these new settings, we have our known devices sharing a 14Mbits/s Down - 2Mbits/s Up . The remaining 4 devices allowed will share a much lower part of that : 512kbits/s Down - 512kbits/s Up.
Revising tc
policies to avoid blocking communications
Even though we just increased bandwidth for our 10 known devices (and 4 others), the Queue discipline algorithms may not be the best to ensure fair and complete packet distribution
The problem we may encounter with the previous classes is that the HTB algorithm used for our trafic shaping classes is mainly targetting traffic shaping, but not management.
Using SFQ (Stochastic Fairness Queuing) will allow us to evenly share bandwidth between connections after shaping traffic Therefore we will enable SFQ Queue Disciplines to our leaf classes
This results in :
#!/bin/bash
# Get rid of any existing qdisc on wlan0
tc qdisc del dev wlan0 root
modprobe ifb numifbs=1
ip link set dev ifb0 up
tc qdisc add dev wlan0 handle ffff: ingress
tc filter add dev wlan0 parent ffff: protocol ip u32 match u32 0 0 action mirred egress redirect dev ifb0
#Shape traffic to WLAN clients (egress rules)
tc qdisc add dev wlan0 root handle 1: htb default 10
tc class add dev wlan0 parent 1: classid 1:1 htb rate 16mbit
tc class add dev wlan0 parent 1:1 classid 1:10 htb rate 14mbit ceil 15mbit
tc class add dev wlan0 parent 1:1 classid 1:20 htb rate 512kbit ceil 625kbit
tc filter add dev wlan0 parent 1: protocol ip prio 1 u32 match ip dst 192.168.5.128/25 flowid 1:20
tc qdisc add dev wlan0 parent 1:10 handle 10: sfq perturb 10
tc qdisc add dev wlan0 parent 1:20 handle 20: sfq perturb 10
#Shape traffic from WLAN clients (ingress mirrored as egress)
tc qdisc add dev ifb0 root handle 1: htb default 10
tc class add dev ifb0 parent 1: classid 1:1 htb rate 4mbit
tc class add dev ifb0 parent 1:1 classid 1:10 htb rate 2mbit ceil 3mbit
tc class add dev ifb0 parent 1:1 classid 1:20 htb rate 512kbit ceil 625kbit
tc filter add dev ifb0 parent 1: protocol ip prio 1 u32 match ip src 192.168.5.128/25 flowid 1:20
tc qdisc add dev ifb0 parent 1:10 handle 10: sfq perturb 10
tc qdisc add dev ifb0 parent 1:20 handle 20: sfq perturb 10
Hostapd config file
A few other can be improved in such scenario is the inactivity from connected stations to free slots for new devices
# Station inactivity limit
#
# If a station does not send anything in ap_max_inactivity seconds, an
# empty data frame is sent to it in order to verify whether it is
# still in range. If this frame is not ACKed, the station will be
# disassociated and then deauthenticated. This feature is used to
# clear station table of old entries when the STAs move out of the
# range.
#
# The station can associate again with the AP if it is still in range;
# this inactivity poll is just used as a nicer way of verifying
# inactivity; i.e., client will not report broken connection because
# disassociation frame is not sent immediately without first polling
# the STA with a data frame.
# default: 300 (i.e., 5 minutes)
#ap_max_inactivity=300