Openwrt one Poor man's bufferbloat mitigation

ECN (Explicit Congestion Notification) is an extension to TCP/IP, defined in RFC 3618 and updated in RFC 9330. It allows end-to-end notification of packet congestion, without having to drop packets. It does this by marking the packets to signal impending congestion, which causes the upstream endpoint to dial back its send rate.

It works for upload and download bandwidth.

But, to get it to work, you’ve got more configuring to do than with typical bufferbloat mitigation strategies… you have to configure each lan device and the router to enable ECN for each.

For the router:

vi /etc/sysctl.conf

… press i to enter editing mode, then paste the following into the file:

net.ipv4.tcp_ecn = 1
net.ipv4.tcp_ecn_fallback = 1

… then press Esc to exit editing mode, then :wq to save and exit, then issue:

sysctl -p

… to load the settings.

That setting enables ECN for IPv4 and IPv6. If you’re running https-dns-proxy, that’ll set ECN for DNS queries.

If you’re using https-dns-proxy, you can ascertain that it’s working by issuing:

tcpdump -v -i eth0 -c 5 "tcp port 443"

… and you’ll see output like:

19:56:56.576950 IP6 (class 0x02, flowlabel 0x8a1ba, hlim 127, next-header TCP (6), payload length 32) 2601:2c1:9480:a918:201d:ae47:5ad9:758e.55294 > 2603:1063:16:120::365:7ea3.443: Flags [SEW], cksum 0x517b (correct), seq 1731337521, win 65535, options [mss 1440,nop,wscale 8,nop,nop,sackOK], length 0

class 0x02 is the IPv6 Traffic Class field. In hexadecimal, 0x02 translates to binary 00000010. The last two bits are 10 , which explicitly stands for ECT(0) (ECN-Capable Transport).

[SEW] stands for:

  • S = SYN (Synchronize, establishing a connection).
  • E = ECE (ECN-Echo capability advertisement).
  • W = CWR (Congestion Window Reduced capability advertisement).

The response packet: Packet #5 (Inbound SYN-ACK Response)

19:56:56.606035 IP6 (class 0x02, flowlabel 0x4cea4 … ) REMOTE_IP.443 > LAN_IP.55294: Flags [S.E]

  1. class 0x02: The remote internet server responded with 0x02 (ECT(0)), confirming that it also supports ECN and has enabled it for this download stream.
  2. Flags [S.E]:
    • S = SYN
    • E = ECE (ECN-Echo)

Because the remote server sent back the E flag in response to the router’s request, the ECN negotiation completed successfully.

ECN in a router depends upon some form of AQM (Active Queue Management) being present. Because OpenWRT uses FQ-CoDel out-of-the-box (it’s just unthrottled, because the developers have no way of knowing what bandwidth you’ve paid for), ECN works without having to change anything as regards the stock traffic shaper.

That takes care of traffic to / from the router itself. Now let’s look at the lan devices:

For Windows:

Start Powershell as Administrator, then issue:

netsh int tcp set global ecncapability=enabled

… it should return ‘Ok.’.

For Apple (macOS) devices:

Start terminal, then issue:

sudo sysctl -w net.inet.tcp.ecn=1

For iOS devices:

For modern (11+) iOS devices, ECN is enabled by default.

For Linux:

Start terminal, then issue:

sudo sysctl -w net.ipv4.tcp_ecn=1

… to set it until the next reboot, or follow the instructions for the router (above) to make it permanent.

For Android:

You must have a rooted Android device. It’s not possible to enable ECN on a non-rooted device.

Connect to the phone from your computer via ADB (Android Debug Bridge).

In the Debug shell, issue:

su
sysctl -w net.ipv4.tcp_ecn=1

… to set it until the next reboot. You’ll have to use something like Kernel Adjuter or Magisk to run a script each time the OS is rebooted, to set it each time.

For Chrome:

Put your Chrome device into Developer Mode.

Open crosh, type shell, and run sudo sysctl -w net.ipv4.tcp_ecn=1

It should be noted that the Chrome browser automatically enables ECN.

[ EDIT: Whew! Getting the firewall to mark packets is labrinthine! Working now. ]

Now the fun part… you can tune ECN to start marking packets as soon as the first hint of congestion occurs. Out-of-the-box, FQ-CoDel lets 5 msec worth of bufferbloat build up. The code below drops that baseline down to 2500 usec (2.5 msec), and clamps down as far as 500 usec (0.5 msec) on high load.

The code below shapes both upload and download. It varies ecn ce_threshold as the datarate varies, to make the traffic shaping smoother.

I’ve updated the code below to give higher priority to https-dns-proxy traffic. If you don’t have https-dns-proxy installed, you can comment those lines out, or remove them. The firewall marks the DoH packets, then wan_throttle uses that mark to chuck those packets into their own, higher-priority data stream.

NOTE: The code below works perfectly even with Software Flow Offloading enabled. We steal the inbound packets for shaping before SFO even sees them. We shape at Layer 2 and offload at Layer 3. Once they’re shaped, they go up the stack to Layer 3. Hence we get inbound traffic shaping and low CPU utilization.

Use:

ip link show

… to enumerate your interfaces. Find the interface which your router uses to connect to the modem… for the OpenWRT One router, it’s usually eth0.

We can adapt the original wan_throttle from my prior posts to enable ECN and L4S… copy and paste this into the terminal window, then press Enter:

cat << 'EOF' > /etc/init.d/wan_throttle
#!/bin/sh /etc/rc.common

START=95

# Register 'status' as a custom command
EXTRA_COMMANDS="status"
EXTRA_HELP="	status	Show traffic control statistics"

# ===	===	===	===	===	===	===
# USER SETTINGS - DO NOT CHANGE ANYTHING ELSE!

LOGGING=1 # Options: 0 - disabled | 1 - enabled

# QUANTUM SHOULD BE A MULTIPLE OF MTU + ETHERNET HEADER SIZE
# DEFAULT: 1500 + 14 = 1514
QUANTUM=3028

WAN_INTERFACE="eth0"

# THROTTLED SPEEDS (wan_throttle started)
THROTTLE_UP_SPEED="33mbit"
THROTTLE_DOWN_SPEED="200mbit"

# LINE SPEEDS (wan_throttle stopped)
FULL_UP_SPEED="45mbit"   # ISP upload speed
FULL_DOWN_SPEED="1000mbit" # ISP download speed
# ===	===	===	===	===	===	===

# Account for protocol overhead
THROTTLE_UP="$(( (u = ${THROTTLE_UP_SPEED%mbit}) + (u / 10) ))mbit"
THROTTLE_DOWN="$(( (u = ${THROTTLE_DOWN_SPEED%mbit}) + (u / 10) ))mbit"

BASE_CE_THRESH=2500
MIN_UPLOAD_CE=500
MIN_DOWNLOAD_CE=500
UP_PID_FILE="/tmp/wan_throttle_up.pid"
DOWN_PID_FILE="/tmp/wan_throttle_down.pid"

setup_structure_if_missing() {
    insmod sch_htb >/dev/null 2>/dev/null
    insmod act_mirred >/dev/null 2>/dev/null
    insmod cls_u32 >/dev/null 2>/dev/null

    if lsmod | grep -q "^ifb" && ! ip link show dev ifb0 >/dev/null 2>&1; then
        rmmod ifb 2>/dev/null
    fi

    modprobe ifb numifbs=1 >/dev/null 2>&1
    ip link set dev ifb0 up >/dev/null 2>&1

    # Provision TX queue
    if ! tc qdisc show dev ${WAN_INTERFACE} | grep -q "root htb"; then
        tc qdisc del dev ${WAN_INTERFACE} root 2>/dev/null

        # Set up root HTB
        tc qdisc add dev ${WAN_INTERFACE} root handle 10: htb default 11

        # Fast-track lane (class 10:10) for DoH queries
        tc class add dev ${WAN_INTERFACE} parent 10: classid 10:10 htb rate 5mbit ceil ${FULL_UP_SPEED} prio 0

        # General traffic (class 10:11)
        tc class add dev ${WAN_INTERFACE} parent 10: classid 10:11 htb rate ${THROTTLE_UP} ceil ${THROTTLE_UP} quantum ${QUANTUM} prio 1

        # Attach fq_codel leaf to class 10:11
        tc qdisc add dev ${WAN_INTERFACE} parent 10:11 handle 110: fq_codel quantum ${QUANTUM} target 5ms interval 100ms ecn ce_threshold "${BASE_CE_THRESH}us" ce_threshold_selector 0x01/0x01 memory_limit 32Mb drop_batch 0

        # ====================
        # https-dns-proxy higher-priority bypass via Firewall Mark (fwmark)
        # Matches any packet internally stamped with mark '4' and fast-tracks it
        tc filter add dev ${WAN_INTERFACE} parent 10: protocol ip prio 1 handle 4 fw flowid 10:10
        # ====================
        # Catch-all: All remaining traffic to the standard fq_codel leaf queue
        tc filter add dev ${WAN_INTERFACE} parent 10: protocol all prio 2 u32 match u32 0 0 flowid 10:11
    fi

    # Provision RX queue
    if ! tc qdisc show dev ${WAN_INTERFACE} | grep -q "clsact" || ! tc qdisc show dev ifb0 | grep -q "htb"; then
        tc qdisc del dev ${WAN_INTERFACE} clsact 2>/dev/null
        tc qdisc del dev ifb0 root 2>/dev/null

        tc qdisc add dev ${WAN_INTERFACE} clsact

        tc filter add dev ${WAN_INTERFACE} ingress protocol all u32 match u32 0 0 action mirred egress redirect dev ifb0

        tc qdisc add dev ifb0 root handle 1: htb default 11
        tc class add dev ifb0 parent 1: classid 1:11 htb rate ${THROTTLE_DOWN} ceil ${THROTTLE_DOWN} quantum ${QUANTUM}
        tc qdisc add dev ifb0 parent 1:11 handle 110: fq_codel quantum ${QUANTUM} target 5ms interval 100ms ecn ce_threshold "${BASE_CE_THRESH}us" ce_threshold_selector 0x01/0x01 memory_limit 32Mb drop_batch 0
    fi
}

run_upload_monitor() {
    local UP_MODIFIED=0

    while true; do
        local SYN_COUNT=$(ss -H -t state syn-sent | wc -l)
        if [ "${SYN_COUNT}" -gt 0 ]; then
            local NEW_CE=$(( BASE_CE_THRESH - (SYN_COUNT * 250) ))
            [ "${NEW_CE}" -lt "${MIN_UPLOAD_CE}" ] && NEW_CE=${MIN_UPLOAD_CE}

            tc qdisc change dev "${WAN_INTERFACE}" parent 10:11 handle 110: fq_codel quantum ${QUANTUM} target 5ms interval 100ms ecn ce_threshold "${NEW_CE}us" ce_threshold_selector 0x01/0x01 memory_limit 32Mb drop_batch 0
            [ "${LOGGING}" -eq 1 ] && logger -t wan_throttle "CONGESTION++: ${SYN_COUNT} SYN-SENTs.	Upload ECN set to ${NEW_CE}us."
            UP_MODIFIED=1
        else
            if [ "${UP_MODIFIED}" -eq 1 ]; then
                tc qdisc change dev "${WAN_INTERFACE}" parent 10:11 handle 110: fq_codel quantum ${QUANTUM} target 5ms interval 100ms ecn ce_threshold "${BASE_CE_THRESH}us" ce_threshold_selector 0x01/0x01 memory_limit 32Mb drop_batch 0
                [ "${LOGGING}" -eq 1 ] && logger -t wan_throttle "CONGESTION--. ${SYN_COUNT} SYN-SENTs.	Upload ECN set to ${BASE_CE_THRESH}us."
                UP_MODIFIED=0
            fi
        fi
        sleep 2
    done
}

run_download_monitor() {
    local OLD_BYTES
    OLD_BYTES=$(cat /sys/class/net/ifb0/statistics/rx_bytes 2>/dev/null)
    local DOWN_MODIFIED=0

    while true; do
        sleep 1
        local NEW_BYTES
        NEW_BYTES=$(cat /sys/class/net/ifb0/statistics/rx_bytes 2>/dev/null)
        [ -z "${NEW_BYTES}" ] || [ -z "${OLD_BYTES}" ] && continue

        local BYTES_DIFF=$((NEW_BYTES - OLD_BYTES))
        OLD_BYTES=${NEW_BYTES}
        local LIVE_MBPS=$(( (BYTES_DIFF * 8) / 1048576 ))

        if [ -n "${DOWN_RAW}" ] && [ "${DOWN_RAW}" -gt 0 ]; then
            local UTILIZATION=$(( (LIVE_MBPS * 100) / DOWN_RAW ))
        else
            local UTILIZATION=0
        fi

        if [ "${UTILIZATION}" -gt 80 ]; then
            local LOAD_OVER_80=$((UTILIZATION - 80))
            local NEW_CE=$(( BASE_CE_THRESH - (LOAD_OVER_80 * 50) ))
            [ "${NEW_CE}" -lt "${MIN_DOWNLOAD_CE}" ] && NEW_CE=${MIN_DOWNLOAD_CE}

            tc qdisc change dev ifb0 parent 1:11 handle 110: fq_codel quantum ${QUANTUM} target 5ms interval 100ms ecn ce_threshold "${NEW_CE}us" ce_threshold_selector 0x01/0x01 memory_limit 32Mb drop_batch 0
            [ "${LOGGING}" -eq 1 ] && logger -t wan_throttle "HIGH LOAD: ${UTILIZATION}% (${LIVE_MBPS} Mbps).	Download ECN set to ${NEW_CE}us."
            DOWN_MODIFIED=1
        else
            if [ "${DOWN_MODIFIED}" -eq 1 ]; then
                tc qdisc change dev ifb0 parent 1:11 handle 110: fq_codel quantum ${QUANTUM} target 5ms interval 100ms ecn ce_threshold "${BASE_CE_THRESH}us" ce_threshold_selector 0x01/0x01 memory_limit 32Mb drop_batch 0
                [ "${LOGGING}" -eq 1 ] && logger -t wan_throttle "LOW LOAD: ${UTILIZATION}% (${LIVE_MBPS} Mbps).	Download ECN set to ${BASE_CE_THRESH}us."
                DOWN_MODIFIED=0
            fi
        fi
    done
}

start() {
    setup_structure_if_missing

    tc qdisc del dev ifb0 root 2>/dev/null
    tc qdisc add dev ifb0 root handle 1: htb default 11
    tc class add dev ifb0 parent 1: classid 1:11 htb rate ${THROTTLE_DOWN} ceil ${THROTTLE_DOWN} quantum ${QUANTUM}
    tc qdisc add dev ifb0 parent 1:11 handle 110: fq_codel quantum ${QUANTUM} target 5ms interval 100ms ecn ce_threshold "${BASE_CE_THRESH}us" ce_threshold_selector 0x01/0x01 memory_limit 32Mb drop_batch 0

    [ -f "${UP_PID_FILE}" ] && kill -9 "$(cat "${UP_PID_FILE}")" 2>/dev/null
    [ -f "${DOWN_PID_FILE}" ] && kill -9 "$(cat "${DOWN_PID_FILE}")" 2>/dev/null
    run_upload_monitor & echo $! > "${UP_PID_FILE}"
    run_download_monitor & echo $! > "${DOWN_PID_FILE}"

    logger -t wan_throttle "wan_throttle started. Up: ${THROTTLE_UP_SPEED}, Down: ${THROTTLE_DOWN_SPEED}"
}

stop() {
    # Terminate monitor subroutines via PID files
    if [ -f "${UP_PID_FILE}" ]; then
        kill -9 "$(cat "${UP_PID_FILE}")" 2>/dev/null
        rm -f "${UP_PID_FILE}"
    fi
    if [ -f "${DOWN_PID_FILE}" ]; then
        kill -9 "$(cat "${DOWN_PID_FILE}")" 2>/dev/null
        rm -f "${DOWN_PID_FILE}"
    fi

    # Remove traffic shaping structures (WAN and IFB)
    # This removes class 10:10 & 10:11, filters, and fq_codel leaves.
    tc qdisc del dev ${WAN_INTERFACE} root 2>/dev/null
    tc qdisc del dev ${WAN_INTERFACE} clsact 2>/dev/null
    tc qdisc del dev ifb0 root 2>/dev/null

    # Set IFB loopback device in down state.
    ip link set dev ifb0 down 2>/dev/null

    logger -t wan_throttle "wan_throttle stopped."
}

status() {
    echo "===	===	===	===	===	===" | tee >(logger -t wan_throttle)
    echo "===	===	UL STATUS	===	===" | tee >(logger -t wan_throttle)
    echo "===	===	===	===	===	===" | tee >(logger -t wan_throttle)
    WAN_STAT=$( tc -s qdisc show dev ${WAN_INTERFACE} ) && echo "${WAN_STAT}" | tee >(logger -t wan_throttle)
    echo "===	===	===	===	===	===" | tee >(logger -t wan_throttle)
    echo "===	===	DL STATUS	===	===" | tee >(logger -t wan_throttle)
    echo "===	===	===	===	===	===" | tee >(logger -t wan_throttle)
    WAN_STAT=$( tc -s qdisc show dev ifb0 ) && echo "${WAN_STAT}" | tee >(logger -t wan_throttle)
}
EOF

Make it executable

chmod +x /etc/init.d/wan_throttle

Enable the service:

/etc/init.d/wan_throttle enable

Start the service:

/etc/init.d/wan_throttle start

Create the file that marks https-dns-proxy packets:

vi /usr/share/nftables.d/ruleset-post/90-doh-tc-bypass.nft

… press i to enter editing mode, then paste the following in:

table inet fw4 {
    chain mangle_postrouting {
        meta skuid nobody th dport 443 meta mark set 4 comment "Stamp both TCP and UDP HTTP/3 local DoH with mark 4"
    }
}

… press Esc to exit editing mode, then :wq to save and exit, then reload the firewall:

fw4 reload

… and restart wan_throttle:

/etc/init.d/wan_throttle stop
/etc/init.d/wan_throttle start

Note the code above:

meta skuid nobody
           ^^^^^^

image

… it marks only those packets coming from https-dns-proxy, so wan_throttle can give those packets their own fast-lane.

Make sure the files survive firmware updates. Put:

/etc/init.d/wan_throttle
/usr/share/nftables.d/ruleset-post/90-doh-tc-bypass.nft

… into the list at System >> Backup / Flash Firmware >> Configuration.

Look at the tc stats for class 10:10 (the higher-priority bypass for https-dns-proxy traffic):

tc -s class show dev eth0

… you should see something like this:

class htb 10:11 root leaf 110: prio 0 rate 36Mbit ceil 36Mbit burst 1600b cburst 1600b
 Sent 25995848 bytes 66956 pkt (dropped 0, overlimits 6950 requeues 0) 
 backlog 0b 0p requeues 0
 lended: 56471 borrowed: 0 giants: 0
 tokens: 5226 ctokens: 5226

class htb 10:10 root prio 0 rate 5Mbit ceil 45Mbit burst 1600b cburst 1600b
 Sent 194306 bytes 1485 pkt (dropped 0, overlimits 0 requeues 0) 
 backlog 0b 0p requeues 0
 lended: 1485 borrowed: 0 giants: 0
 tokens: 38350 ctokens: 4261

class fq_codel 110:7a parent 110: 
 (dropped 0, overlimits 0 requeues 0) 
 backlog 0b 0p requeues 0
  deficit 286 count 0 lastcount 0 ldelay 754us
class fq_codel 110:1c7 parent 110: 
 (dropped 0, overlimits 0 requeues 0) 
 backlog 0b 0p requeues 0
  deficit 2933 count 0 lastcount 0 ldelay 4us

… 1485 packets through the class 10:10 higher-priority bypass lane.

That last class (110:7a) is dynamic, set up by FQ_CoDel, one for each data stream… you’ll see it creating and removing classes as the traffic rate varies. It uses those to attempt to give fair bandwidth to each stream.

You can ascertain that the packet mangling is working by issuing:

nft list chain inet fw4 mangle_postrouting

… which will output something like:

table inet fw4 {
	chain mangle_postrouting {
		type filter hook postrouting priority mangle; policy accept;
		oifname "eth0" tcp flags & (fin | syn | rst) == syn tcp option maxseg size set rt mtu comment "!fw4: Zone wan IPv4/IPv6 egress MTU fixing"
		oifname "phy2-sta0" tcp flags & (fin | syn | rst) == syn tcp option maxseg size set rt mtu comment "!fw4: Zone hotspot IPv4/IPv6 egress MTU fixing"
		meta skuid 65534 th dport 443 meta mark set 0x00000004 comment "Stamp both TCP and UDP HTTP/3 local DoH with mark 4"
	}
}

Now, your https-dns-proxy DoH will work, even when you’re pushing bandwidth to the limits you’ve set in the wan_throttle script, because it’s at a higher priority.

You’ll have to experiment with it to get the right bandwidth settings for THROTTLE_UP_SPEED and THROTTLE_DOWN_SPEED… it should be the slowest-speed interface. For me, that’s WiFi for THROTTLE_DOWN_SPEED, and the ISP’s upload bandwidth limit for THROTTLE_UP_SPEED. If I set it to 200 Mbps download speed, I get +4 ms latency on high download rate, that bufferbloat occurring at the phy0-ap0 and phy1-ap0 interfaces (or at br-lan, given that this is a WiFi-only network).

If you’ve got logging enabled, during high-bandwidth periods, you’ll see logs something like:

wan_throttle: HIGH LOAD: 100% (201 Mbps). Download ECN set to 1500us.
wan_throttle: HIGH LOAD: 107% (215 Mbps). Download ECN set to 1150us.
wan_throttle: HIGH LOAD: 107% (214 Mbps). Download ECN set to 1150us.
wan_throttle: LOW LOAD: 63% (126 Mbps). Download ECN set to 2500us.
wan_throttle: HIGH LOAD: 103% (206 Mbps). Download ECN set to 1350us.
wan_throttle: HIGH LOAD: 107% (215 Mbps). Download ECN set to 1150us.
wan_throttle: HIGH LOAD: 109% (218 Mbps). Download ECN set to 1050us.
wan_throttle: HIGH LOAD: 103% (207 Mbps). Download ECN set to 1350us.
wan_throttle: LOW LOAD: 15% (30 Mbps). Download ECN set to 2500us.
wan_throttle: HIGH LOAD: 88% (177 Mbps). Download ECN set to 2100us.
wan_throttle: HIGH LOAD: 104% (209 Mbps). Download ECN set to 1300us.
wan_throttle: HIGH LOAD: 105% (211 Mbps). Download ECN set to 1250us.
wan_throttle: HIGH LOAD: 107% (214 Mbps). Download ECN set to 1150us.
wan_throttle: HIGH LOAD: 85% (170 Mbps). Download ECN set to 2250us.
wan_throttle: LOW LOAD: 0% (0 Mbps). Download ECN set to 2500us.

Take a look at the download stats:

===	 ===	===	===	===	===
===	=== DL STATUS	===	===
===	 === ===	===	===	===
qdisc htb 1: root refcnt 2 r2q 10 default 0x11 direct_packets_stat 0 direct_qlen 32
Sent 400692714 bytes 307245 pkt (dropped 1, overlimits 130172 requeues 18)
backlog 0b 0p requeues 18
qdisc fq_codel 110: parent 1:11 limit 10240p flows 1024 quantum 3028 target 5ms ce_threshold 2.5ms ce_threshold_selector 0x1/0x1 interval 100ms memory_limit 32Mb ecn drop_batch 64
Sent 400692714 bytes 307245 pkt (dropped 1, overlimits 0 requeues 0)
backlog 0b 0p requeues 0
maxpacket 60576 drop_overlimit 0 new_flow_count 24678 ecn_mark 1497 ce_mark 1497
new_flows_len 0 old_flows_len 4

1 packet dropped, and 1497 ecn_mark’d.

Here’s a screenshot from btop:

An enhancement:

Install the kmod-tcp-bbr package, and issue:

vi /etc/sysctl.conf

… press i to enter editing mode, then paste this in:

# Enable Explicit Congestion Notification and BBR Congestion Control for wan_throttle use
net.ipv4.tcp_ecn = 1
net.ipv4.tcp_ecn_fallback = 1
net.ipv4.tcp_congestion_control = bbr

… press Esc to exit editing mode, then :wq to save and exit. Then issue:

sysctl -p

… to load the new setting.

This caused my bufferbloat test jitter to reduce to single-digit values, which finally got all the checkmarks in the lower-left panel (results with THROTTLE_UP_SPEED="33mbit", and THROTTLE_DOWN_SPEED="200mbit"):

If you use BBR instead of CUBIC on all of the configurable lan devices, as well, the traffic shaping is that much smoother.

For instance, for Linux:

sudo modprobe tcp_bbr
sudo sysctl -w net.ipv4.tcp_congestion_control=bbr
sudo sysctl -w net.core.default_qdisc = fq_codel
sudo sysctl -p

And for Windows:

Run this first and record the results, so you can reset it to defaults if you want:

Get-NetTCPSetting | Select SettingName, CongestionProvider

Then switch to BBR:

netsh int tcp set supplemental Template=Internet CongestionProvider=bbr2
netsh int tcp set supplemental Template=Datacenter CongestionProvider=bbr2
netsh int tcp set supplemental Template=Compat CongestionProvider=bbr2
netsh int tcp set supplemental Template=DatacenterCustom CongestionProvider=bbr2
netsh int tcp set supplemental Template=InternetCustom CongestionProvider=bbr2

Get-NetTCPSetting | Select SettingName, CongestionProvider

… which should return:

SettingName        CongestionProvider
-----------        ------------------
Automatic
InternetCustom     BBR2
DatacenterCustom   BBR2
Compat             BBR2
Datacenter         BBR2
Internet           BBR2

Another enhancement… for the router and all the lan devices you can configure, set these in sysctl.conf (in the router, it’s at /etc/sysctl.conf):

net.ipv4.tcp_pacing_ca_ratio = 105
net.ipv4.tcp_pacing_ss_ratio = 105

The first one allows the device to seek for 5% more bandwidth if it’s available, but at a lower rate of increase than default, which slows down the bandwidth surges and dips.

The second one slows down the ramp-up of slow-start to +5% of current bandwidth… it still ramps pretty quickly, but it’s not slamming wide open like it does by default. It allows wan_throttle to settle into a narrow ECN range that keeps maximum bandwidth pretty stable.

Be sure to reload the settings via:

sysctl -p

Another enhancement:

I was reading here about how CloudFlare set their servers up for high throughput and low latency. While I can’t do a lot of what CloudFlare did (which would require recompiling the kernel), we can limit the size of the unsent bytes in the write queue. CloudFlare sets their net.ipv4.tcp_notsent_lowat = 131072 (128 KB), but that spikes CPU utilization (increases sendmsg() calls when using non-blocking sockets, and increases number of context switches for blocking sockets). CloudFlare can afford that, we can’t. I’ve set net.ipv4.tcp_notsent_lowat = 524288 (512 KB). I’m still testing to find the best value.

vi /etc/sysctl.conf

… press i to enter editing mode, then paste:

# Reduce net.ipv4.tcp_notsent_lowat from 4294967295 to 524288
# Cloudflare sets theirs to 131072 to limit the size of unsent bytes in the write queue.
net.ipv4.tcp_notsent_lowat = 524288

… press Esc to exit editing mode, then :wq to save and exit, then issue:

sysctl -p

… which got me:

image

… for the first time on the bufferspeed.com bufferbloat test (with THROTTLE_UP_SPEED="33mbit" and THROTTLE_DOWN_SPEED="200mbit").

Final note: You’ll notice in your logs:

daemon.notice: ttyd[3664]: [2026/10/12 19:13:39:0701] N: rops_handle_POLLIN_netlink: DELADDR

… when you stop /etc/init.d/wan_throttle.

When ttyd (the terminal daemon) starts up, it opens a Netlink routing socket (NETLINK_ROUTE). In Linux, Netlink is a kernel-to-user-space messaging system used to broadcast networking events (like an interface going up/down, or an IP address being assigned/removed).

By default, a program listening to NETLINK_ROUTE receives a stream of network change messages for all interfaces on the router, not just the interface the program is bound to.

When wan_throttle puts ifb0 into a down state, the Linux kernel removes any IP/address properties associated with that interface. The kernel broadcasts an RTM_DELADDR (Delete Address) message over Netlink to notify the system.

Because ttyd is listening globally, its Netlink polling loop captures the kernel’s broadcast event and spits out rops_handle_POLLIN_netlink: DELADDR into the system logs

[ EDIT: updated /etc/init.d/wan_throttle to code around an ifb0 glitch. ]

If you want to use CAKE, we’ll change the packet-mangle:

vi /usr/share/nftables.d/ruleset-post/90-doh-tc-bypass.nft

… press i to enter editing mode, and erase any already-existing text (you’ll have to go to the end and backspace to the beginning to erase the text), then paste in:

table inet fw4 {
    chain doh_mangle_output {
        type route hook output priority mangle; policy accept;

        # DSCP46 (Expedited Forwarding) injection with active counters
        meta skuid nobody ip protocol tcp tcp dport 443 counter ip dscp set ef comment "IPv4 DoH DSCP46"
        meta skuid nobody ip protocol udp udp dport 443 counter ip dscp set ef comment "IPv4 DoH HTTP/3 DSCP46"
        meta skuid nobody ip6 nexthdr tcp tcp dport 443 counter ip6 dscp set ef comment "IPv6 DoH DSCP46"
        meta skuid nobody ip6 nexthdr udp udp dport 443 counter ip6 dscp set ef comment "IPv6 DoH HTTP/3 DSCP46"
    }
}

… press Esc to exit editing mode, then :wq to save and exit, then issue:

fw4 reload

… which should return something like:

Automatically including '/usr/share/nftables.d/ruleset-post/20-https-dns-proxy-notrack.nft'
Automatically including '/usr/share/nftables.d/ruleset-post/90-doh-tc-bypass.nft'

We set https-dns-proxy to mark the packets itself.

… click the Edit button for each DoH listener… image

… and set DSCP Codepoint to 46 for each DoH listener.

… but my setup strips the DSCP Codepoint before CAKE sees it, hence the packet-mangle above to reinforce the DSCP Codepoint setting.

In /etc/config/https-dns-proxy, it’ll look like this:

config https-dns-proxy
	option bootstrap_dns '127.0.0.1,127.0.0.2,127.0.0.3,::1'
	option resolver_url 'https://dnsforge.de/dns-query'
	option listen_port '5050'
	option listen_addr '127.0.0.1'
	option force_ip_family 'ipv4'
	option dscp_codepoint '46'

We set option dscp_codepoint '46' for each DoH listener in https-dns-proxy to classify DoH traffic into CAKE’s Voice / Signaling Tin (queue), which gives it higher upload and download priority.

You can ascertain that the packet-mangle is working by issuing:

nft list chain inet fw4 doh_mangle_output

… which will return something like:

table inet fw4 {
	chain doh_mangle_output {
		type route hook output priority mangle; policy accept;
		meta skuid 65534 ip protocol tcp tcp dport 443 counter packets 354 bytes 40169 ip dscp set ef comment "IPv4 DoH DSCP46"
		meta skuid 65534 ip protocol udp udp dport 443 counter packets 0 bytes 0 ip dscp set ef comment "IPv4 DoH HTTP/3 DSCP46"
		meta skuid 65534 ip6 nexthdr tcp tcp dport 443 counter packets 210 bytes 28633 ip6 dscp set ef comment "IPv6 DoH DSCP46"
		meta skuid 65534 ip6 nexthdr udp udp dport 443 counter packets 0 bytes 0 ip6 dscp set ef comment "IPv6 DoH HTTP/3 DSCP46"
	}
}

… then waiting a minute, then issuing the same again:

nft list chain inet fw4 doh_mangle_output

… and you should see the packets and bytes counters incrementing.

Then we set up the /etc/init.d/wan_throttle code:

cat << 'EOF' > /etc/init.d/wan_throttle
#!/bin/sh /etc/rc.common
START=97

EXTRA_COMMANDS="status"
EXTRA_HELP="	status	Show traffic control statistics"

# USER SETTINGS - DO NOT CHANGE ANYTHING ELSE!
# ===	===	===	===	===	===	===
WAN_PROTO="docsis"		# Options: "docsis" | ""

WAN_INTERFACE="eth0"		# The interface connected to your modem

THROTTLE_UP_SPEED="33mbit"	# Target upload speed
THROTTLE_DOWN_SPEED="200mbit"	# Target download speed
# ===	===	===	===	===	===	===

setup_structure_if_missing() {
    local DOCSIS=""
    [ "${WAN_PROTO}" = "docsis" ] && DOCSIS="overhead 22 mpu 64"

    # Load CAKE module
    insmod sch_cake >/dev/null 2>/dev/null

    # --- Provision TX queues ---
    if ! tc qdisc show dev ${WAN_INTERFACE} | grep -q "root cake"; then
        tc qdisc del dev ${WAN_INTERFACE} root 2>/dev/null
        tc qdisc add dev ${WAN_INTERFACE} root handle 10: cake bandwidth ${THROTTLE_UP_SPEED} egress diffserv4 flows ack-filter wash rtt 70ms ${DOCSIS}

    fi

    # --- Provision RX queues ---
    if ! tc qdisc show dev ${WAN_INTERFACE} | grep -q "ingress"; then
        # Start with a clean slate
        tc filter del dev ${WAN_INTERFACE} ingress 2>/dev/null
        tc qdisc del dev ${WAN_INTERFACE} ingress 2>/dev/null
        tc qdisc del dev ifb0 root 2>/dev/null

        # Load ifb and act_mirred to mirror inbound traffic to the IFB
        insmod ifb >/dev/null 2>/dev/null

        if ! ip link show ifb0 >/dev/null 2>/dev/null; then
            ip link add name ifb0 type ifb
        fi

        insmod act_mirred >/dev/null 2>/dev/null
        ip link set dev ifb0 up

        # Create the ingress hook
        tc qdisc add dev ${WAN_INTERFACE} ingress

        # Use mirred to mirror all RX packets over to ifb0
        tc filter add dev ${WAN_INTERFACE} parent ffff: protocol all prio 1 u32 match u32 0 0 action mirred egress redirect dev ifb0

        # Attach CAKE to ifb0
        tc qdisc add dev ifb0 root handle 20: cake bandwidth ${THROTTLE_DOWN_SPEED} diffserv4 hosts rtt 70ms ${DOCSIS}
    fi
}

start() {
    setup_structure_if_missing
    logger -t wan_throttle "wan_throttle started. Up: ${THROTTLE_UP_SPEED}, Down: ${THROTTLE_DOWN_SPEED}"
    status
}

stop() {
    tc qdisc del dev ${WAN_INTERFACE} root 2>/dev/null
    tc qdisc del dev ${WAN_INTERFACE} ingress 2>/dev/null
    tc qdisc del dev ${WAN_INTERFACE} clsact 2>/dev/null
    tc qdisc del dev ifb0 root 2>/dev/null
    ip link set dev ifb0 down 2>/dev/null
    logger -t wan_throttle "wan_throttle stopped."
}

status() {
    echo "===	===	UL STATUS	===	===" | tee >(logger -t wan_throttle)
    echo "===	===	===	===	===	===" | tee >(logger -t wan_throttle)
    WAN_STAT=$( tc -s qdisc show dev ${WAN_INTERFACE} root ) && echo "${WAN_STAT}" | tee >(logger -t wan_throttle)
    echo ""
    echo "===	===	DL STATUS	===	===" | tee >(logger -t wan_throttle)
    echo "===	===	===	===	===	===" | tee >(logger -t wan_throttle)
    WAN_STAT=$( tc -s qdisc show dev ifb0 root ) && echo "${WAN_STAT}" | tee >(logger -t wan_throttle)
}
EOF

If you’re not on a DOCSIS (cable) connection, set WAN_PROTO="". Setting WAN_PROTO="docsis" just gives Linux Traffic Control a bit more room for the DOCSIS protocol overhead. We set DOCSIS="overhead 22 mpu 64" despite DOCSIS overhead being 18 bytes, just in case the ISP is using the 4-byte VLAN tag.

For networks which are solely or majority-WiFi, we must set up the CAKE queues to mimic the WiFi queues under the WMM (WiFi MultiMedia) standard for IEEE 802.11e Quality of Service (QoS), otherwise known as EDCA (Enhanced Distributed Channel Access):

WiFi queue | CAKE queue

  1. Voice (AC_VO) → Maps to CAKE’s Voice / Signaling Tin
  2. Video (AC_VI) → Maps to CAKE’s Video Tin
  3. Best Effort (AC_BE) → Maps to CAKE’s Best Effort Tin
  4. Background (AC_BK) → Maps to CAKE’s Bulk Tin

… this matches the 4 datastream classifications of WiFi to CAKE’s 4 datasteam classifications under diffserv4. If you set a higher or lower diffserv, and you have WiFi, the NIC has to chunk between the classes. For instance, if you set diffserv8, the NIC has to convert CAKE’s 8 queues into its own 4 queues. That happens outside of CAKE’s control, which can break CAKE’s buffer calculations, resulting in WiFi bufferbloat and jitter.

NOTE: When you first run /etc/init.d/wan_throttle, then do a bufferbloat test, you’ll notice that download speed is really low (mine was only 25.2 Mbps down and 27 Mbps up)… CAKE is in ‘learning mode’… after it figures out your internet connection’s parameters (which will take a few minutes of you using the internet… the harder you use it while CAKE is in learning mode, the quicker it’ll learn), you’ll see the download rate rise to your set limit.

/etc/init.d/wan_throttle status

===	===	UL STATUS	===	===
===	===	===	===	===	===
qdisc cake 10: root refcnt 17 bandwidth 33Mbit diffserv4 flows nonat wash ack-filter split-gso rtt 70ms noatm overhead 22 mpu 64 
 Sent 420559148 bytes 1689395 pkt (dropped 34573, overlimits 1226115 requeues 1) backlog 0b 0p requeues 1
 memory used: 1675008b of 4Mb
 capacity estimate: 33Mbit
 min/max network layer size:           28 /    1500
 min/max overhead-adjusted size:       64 /    1522
 active queues:                         0
 average network hdr offset:           14

                   Bulk  Best Effort        Video        Voice
  thresh       2062Kbit       33Mbit    16500Kbit     8250Kbit
  target         8.81ms        3.5ms        3.5ms        3.5ms
  interval       75.3ms         70ms         70ms         70ms
  pk_delay          0us         51us        304us         28us
  av_delay          0us          9us         30us          6us
  sp_delay          0us          1us          2us          1us
  backlog            0b           0b           0b           0b
  pkts                0      1704884         1392        17692
  bytes               0    420311475       409073      2420094
  way_inds            0       127431            0          120
  way_miss            0        16896          102         1309
  way_cols            0            0            0            0
  drops               0          112            0            0
  marks               0        65773            0            0
  ack_drop            0        34461            0            0
  sp_flows            0            1            0            1
  bk_flows            0            1            0            0
  un_flows            0            0            0            0
  max_len             0        42636         2543          625
  quantum           300         1007          503          300

===	===	DL STATUS	===	===
===	===	===	===	===	===
qdisc cake 20: root refcnt 2 bandwidth 200Mbit diffserv4 hosts nonat nowash no-ack-filter split-gso rtt 70ms noatm overhead 22 mpu 64 
 Sent 5389044968 bytes 4578296 pkt (dropped 5569, overlimits 3950297 requeues 0) backlog 0b 0p requeues 0
 memory used: 7367936b of 7000000b
 capacity estimate: 200Mbit
 min/max network layer size:           46 /    1500
 min/max overhead-adjusted size:       68 /    1522
 active queues:                         0
 average network hdr offset:           14

                   Bulk  Best Effort        Video        Voice
  thresh      12500Kbit      200Mbit      100Mbit       50Mbit
  target          3.5ms        3.5ms        3.5ms        3.5ms
  interval         70ms         70ms         70ms         70ms
  pk_delay          0us       1.22ms         14us         16us
  av_delay          0us        218us          1us          7us
  sp_delay          0us          8us          1us          5us
  backlog            0b           0b           0b           0b
  pkts                0      4583151           33          681
  bytes               0   5397274035         1980        40860
  way_inds            0       703627            0            0
  way_miss            0         6011           18            0
  way_cols            0            0            0            0
  drops               0         5569            0            0
  marks               0        26998            0            0
  ack_drop            0            0            0            0
  sp_flows            0            1            1            0
  bk_flows            0            1            0            0
  un_flows            0            0            0            0
  max_len             0        68130           60           60
  quantum           381         1514         1514         1514

The DoH packets, inbound and outbound, were shuttled over the highest-priority (Voice) queue, with a peak delay of 28us up / 16us down and an average delay of 6us up / 7us down.

On upload, the router dropped 34461 ACKs (to save upload bandwidth) and 112 packets, for a total of ‘dropped 34573’. It ECN-marked 65773 packets.

On download, the router dropped 0 ACKs and 5569 packets, for a total of ‘dropped 5569’. It ECN-marked 26998 packets. The 5569 dropped packets is likely because not all of my lan devices can have ECN enabled, and because I was pushing bandwidth to the limit.

The router processed 1689395 packets up / 4578296 packets down, ECN-marking 65773 (3.893%) up / 26998 (0.59%) down to slow down ECN-compatible traffic. It resorted to dropping 112 (0.0066%) up / 5569 (0.122%) down, a combination of reaching its limit on ECN-marking and the queues were still full, and non-ECN-enabled lan devices.

An enhancement:

vi /etc/sysctl.conf

… press i to enter editing mode, then paste in:

net.ipv4.tcp_no_metrics_save = 1

… then press Esc to exit editing mode, then :wq to save and exit, then:

sysctl -p

… to load the new setting.

The above tells the kernel not to save connection metrics (RTT, slow-start threshold, congestion window)… CAKE takes care of flow control. The router no longer saving TCP metrics reduces CPU utilization a tiny bit.

Another enhancement: Do away with another source of bufferbloat.

Disable Ethernet Flow Control… in System >> Startup >> Local Startup, paste this:

# Disable eth0 Ethernet Flow Control so CAKE has full control
# ===========================================
(
ethtool -A eth0 autoneg off rx off tx off
) &
# ===========================================

… you can ascertain that Ethernet Flow Control is off via:

ethtool -a eth0

… which should return:

Pause parameters for eth0:
Autonegotiate:	off
RX:		off
TX:		off

IEEE 802.3x Ethernet Flow Control actually causes bufferbloat by design. When a receive buffer fills, a PAUSE frame is sent, which tells the sending NIC to stop transmitting packets. Ethernet Flow Control pushes the backlog backward. It keeps packets trapped in device buffers, which can severely spike latency. It can also make it more difficult for CAKE to shape traffic flow.

Ethernet Flow Control (IEEE 802.3x) CAKE SQM
Layer Layer 2 (Data Link) Layer 3 / 4 (Network / Transport)
Granularity All-or-Nothing: Pauses the entire physical link. Per-Flow / Per-Host: Isolates specific connections.
Mechanism Sends PAUSE frames to stop the sending NIC. Uses AQM (CoDel) to ECN-mark or drop packets.

You can see how eth0 getting a PAUSE frame would be a Bad Thing, right? That PAUSE frame pauses the entire physical link… traffic stops for all downstream devices.

NOTE: This is not the Full Duplex speed auto-negotiation… that is disabled via:

ethtool -s eth0 autoneg off

… turning that off forces your NIC to drop back to 100Mbps half-duplex or (for older devices) 10 Mbps half-duplex. Don’t do that.

You can ascertain that eth0 is still at 2.5 Gbps via

ethtool eth0 | grep Speed

… which should return:

Speed: 2500Mb/s

Can you tell me, why are you spamming this forum with these threads every few weeks ? They are not related to BananaPI hardware in any way whatsoever.

LS3480 wrote:

“Can you tell me, why are you spamming this forum with these threads every few weeks ? They are not related to BananaPI hardware in any way whatsoever.”

Uh… the OpenWRT One is a Banana-Pi co-developed product.

This is specifically a sub-forum for the OpenWRT One router (aka the BPI-R2 Mini).

Those visiting the banana-pi forum’s OpenWRT One sub-forum do so because they want to investigate what this particular router is capable of. My posts show people how they can customize their router to their taste… the OpenWRT One being developed specifically for the purpose of experimentation with it.

How, exactly, is it “spam” (your word) to show a plethora of customizations for this particular router, said router being designed for said customizations, in the sub-forum designated for this particular router, on the forum from the company which co-developed this particular router?

This forum is not about OpenWrt system per se. It’s about hardware made by Sinovoip. Frank-w runs Linux on his bpi boards, on OpenWrt One you can run other systems as well. It is spam when you make new thread about something every few weeks, and the other relevant threads, such as BE14 NIC v2.1 get buried under your stuff. On main page i counted 10 threads made by you in last two weeks.

LS3480 wrote:

“This forum is not about OpenWrt system per se. It’s about hardware made by Sinovoip. Frank-w runs Linux on his bpi boards, on OpenWrt One you can run other systems as well. It is spam when you make new thread about something every few weeks, and the other relevant threads, such as BE14 NIC v2.1 get buried under your stuff. On main page i counted 10 threads made by you in last two weeks.”

What other “relevant threads” (your words) have occurred recently in this sub-forum? I’m the only one posting here. The last post by someone else was 07 Aug 2026… that thread being “buried” (your word) only because there are no other replies in that thread… or weren’t you aware that new replies bring a thread back up to the top of the list of threads, preventing it from being “buried” (your word)?

In the past two weeks, I’ve made exactly 8 comments and 1 new post.

What, exactly, are you complaining about? Your inability to understand how the forum works? That the forum is working exactly as designed? That one of your threads has no traffic and thus got “buried” (your word)? That you can’t count past 8 reliably? That 8 comments and 1 post over the course of 2 weeks overwhelms your reading ability? That you can’t figure out how to set a particular thread as a Favorite or Bookmark in your browser?