I put this together after spending a few days diagnosing various issues with Starlink on OpenWrt 25.12.0. Posting here in case it saves someone else the same trouble. Tested on a GL-iNet Beryl AX (MT3000).
There are four main problems this covers:
- Starlink sends very short IPv6 prefix lifetimes (~300s valid, ~150s preferred). The OpenWrt default RA interval (up to 600s) is longer than these lifetimes, so LAN clients can see their preferred lifetime expire before the next RA arrives, causing them to stop using the address for new connections.
- fw4 egress MSS bug (openwrt/openwrt#12112) โ on OpenWrt 23.05 and earlier,
mtu_fix 1only generated an ingress MSS clamp rule. Outbound TCP SYN packets left unclamped, causing large downloads to stall. Fixed in firewall4 commit 698a533; OpenWrt 24.10+ generates both rules automatically whenmtu_fix 1is set on the wan zone. - Default TCP congestion control โ cubic and reno grow their congestion window based on RTT, which can penalise high-loss links. Hybla normalises window growth against a reference RTT.
- Default conntrack table (often 16384 on embedded routers) can exhaust on busy or IoT-heavy networks, and the default timeouts are longer than they need to be.
1. IPv6 โ DHCPv6-PD and LAN assignment
Check if wan6 already exists: uci show network.wan6
If not, or if proto is not dhcpv6:
uci set network.wan6=interface
uci set network.wan6.device='@wan'
uci set network.wan6.proto='dhcpv6'
uci set network.wan6.reqaddress='try'
uci set network.wan6.reqprefix='auto'
uci set network.lan.ip6assign='64'
uci commit network
Note on /64: Standard residential Starlink delegates a /56. A /64 cannot be sub-delegated, so LAN clients can't get their own prefix. If you only get a /64, NDP proxying is the fallback (LAN clients share the WAN /64, limited to ~250 hosts, no DHCPv6 on LAN).
2. Fix Starlink's short IPv6 prefix lifetimes
This is the only genuinely Starlink-specific odhcpd change required.
What's happening: Starlink advertises ~150s preferred / ~300s valid lifetimes on the delegated prefix. odhcpd renews the DHCPv6-PD lease every ~75s (half the preferred lifetime), so the prefix itself stays valid. The problem is on the LAN side: odhcpd advertises the remaining lease time in each RA message. With the default OpenWrt max RA interval of 600s, the preferred lifetime advertised to clients can expire before the next RA arrives, causing clients to stop using the address for new connections.
The fix: reduce the max RA interval so clients receive refreshes within the renewal cycle.
uci set dhcp.lan.ra='server'
uci set dhcp.lan.dhcpv6='server'
uci set dhcp.lan.ra_maxinterval='60'
uci set dhcp.lan.ra_mininterval='20'
uci commit dhcp
service odhcpd restart
ra_maxinterval=60 ensures clients get a refreshed RA well within the ~75s DHCPv6-PD renewal cycle. ra_mininterval=20 follows the RFC 4861 recommendation of 1/3 of the max.
What doesn't work: max_preferred_lifetime and max_valid_lifetime are maximums, not minimums. Since Starlink's lifetimes are already well below the odhcpd defaults (2700s/5400s), these settings have no effect on the delegated prefix. They only cap the ULA (fdxx:) prefix, which is not the problem. You may see these recommended in other posts โ they don't do what the authors think they do.
Note: this fixes the address churn caused by short lifetimes not being refreshed in time. It does not prevent renumbering if Starlink assigns a genuinely new prefix (e.g. after a dish reboot). In that case LAN clients will get new addresses regardless of this config.
3. DNS
uci set network.wan.peerdns='0'
uci set network.wan.dns='1.1.1.1 1.0.0.1 8.8.8.8 8.8.4.4'
uci set network.wan6.peerdns='0'
uci set network.wan6.dns='2606:4700:4700::1111 2606:4700:4700::1001 2001:4860:4860::8888 2001:4860:4860::8844'
uci commit network
4. NTP โ GPS-disciplined Stratum 1 from the dish
The Starlink dish at 192.168.100.1 has been serving GPS-disciplined NTP on port 123 since mid-2024. It is Stratum 1 โ directly from GPS, not a pool relay. Accuracy in practice is around 85โ123ยตs. No packages or extra tooling needed.
uci add_list system.ntp.server='192.168.100.1'
uci commit system
service sysntpd restart
This adds the dish alongside your existing pool servers rather than replacing them. If the dish is unreachable for any reason (bypass mode with different topology, etc.) the pool servers act as fallback. Check the result:
uci get system.ntp.server
5. MSS clamping
On OpenWrt 24.10+ (including 25.x), mtu_fix is a zone-level option โ it belongs on the wan zone, not the defaults section. On 24.10+ it already defaults to 1 on the wan zone, but set it explicitly to be safe:
WAN_ZONE=$(uci show firewall | grep -m1 "\.name='wan'" | cut -d. -f2)
uci set firewall.$WAN_ZONE.mtu_fix='1'
uci commit firewall
service firewall restart
fw4 will generate both an ingress clamp rule (mangle_forward) and an egress clamp rule (mangle_postrouting) automatically. Verify:
nft list chain inet fw4 mangle_postrouting | grep maxseg
nft list chain inet fw4 mangle_forward | grep maxseg
Both should show a rule with tcp option maxseg size set rt mtu. rt mtu uses the routing table MTU dynamically โ no need to hardcode a value like 1452.
On OpenWrt 23.05, mtu_fix 1 only generates the ingress rule. The egress rule is missing due to fw4 bug openwrt/openwrt#12112 (fixed in firewall4 commit 698a533). On 23.05 you need to add the egress rule manually via a drop-in file โ but do not use this approach on 25.x.
Drop-in files are broken on 25.12: fw4 renders its entire ruleset as a single inline nftables script. A drop-in containing a top-level table inet fw4 block causes a syntax conflict and service firewall restart fails with "unexpected table" errors. Use mtu_fix 1 on 24.10+.
6. Kernel optimisation โ hybla, fq_codel, conntrack
Install hybla (OpenWrt 25.x uses apk; older versions use opkg):
# OpenWrt 25.x
apk add kmod-tcp-hybla
# OpenWrt 23.05 / 24.10
opkg update && opkg install kmod-tcp-hybla
Append to /etc/sysctl.conf:
cat >> /etc/sysctl.conf << 'EOF'
# TCP optimisation
net.core.default_qdisc = fq_codel
net.ipv4.tcp_congestion_control = hybla
net.ipv4.tcp_fastopen = 3
net.ipv4.tcp_mtu_probing = 2
# IPv6 โ required for Starlink router mode
# accept_ra=2: Linux ignores RAs when forwarding=1; =2 overrides this so the
# router receives its upstream default route from Starlink via RA.
net.ipv6.conf.all.accept_ra = 2
net.ipv6.conf.default.accept_ra = 2
net.ipv6.conf.all.forwarding = 1
net.ipv6.conf.default.forwarding = 1
# Conntrack โ values based on official Starlink firmware sysctl.conf
# tcp_timeout_established=7440 (2h) avoids dropping long-lived NAT sessions
net.netfilter.nf_conntrack_max = 65536
net.netfilter.nf_conntrack_tcp_timeout_established = 7440
net.netfilter.nf_conntrack_tcp_timeout_syn_sent = 60
net.netfilter.nf_conntrack_tcp_timeout_syn_recv = 60
net.netfilter.nf_conntrack_tcp_timeout_fin_wait = 120
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 120
net.netfilter.nf_conntrack_tcp_timeout_close_wait = 60
net.netfilter.nf_conntrack_tcp_timeout_last_ack = 30
net.netfilter.nf_conntrack_udp_timeout = 60
net.netfilter.nf_conntrack_udp_timeout_stream = 180
net.netfilter.nf_conntrack_icmp_timeout = 30
net.netfilter.nf_conntrack_generic_timeout = 600
EOF
Apply without rebooting:
sysctl -p /etc/sysctl.conf
Verify:
sysctl net.ipv4.tcp_congestion_control
# expect: hybla
sysctl net.core.default_qdisc
# expect: fq_codel
Hybla scope: net.ipv4.tcp_congestion_control = hybla on the router only affects TCP sessions terminating at the router โ WireGuard, OpenVPN, a local proxy, etc. It has no effect on flows from LAN clients passing through NAT (browsers, streaming apps). For a plain NAT router this setting is nearly a no-op. It is useful if you run WireGuard or another TCP-based service on the router itself.
Hybla and Starlink: Hybla was designed for GEO satellites with ~500ms RTT, where the RTT bias in loss-based algorithms (cubic, reno) is severe. Starlink is LEO with ~20-50ms RTT, much closer to regular broadband โ so the RTT-normalisation benefit is much smaller than for GEO. That said, Starlink has higher packet loss than typical fibre or cable, so hybla may still be marginally better than cubic for router-terminated sessions. It is otherwise standard loss-based behaviour and fair to other flows โ unlike BBRv1 which probes aggressively.
The script installs kmod-tcp-hybla and auto-selects it if available. If not present on a given kernel build, it falls back to CDG, then BBR, then cubic.
On bufferbloat: fq_codel is set as the default qdisc here rather than CAKE. For Starlink this is the right call โ fq_codel works by measuring actual queue delay and needs no bandwidth configuration, so it adapts automatically regardless of how much your Starlink speeds vary. CAKE requires accurate bandwidth values to actually prevent bufferbloat (without them it only does flow fairness, not shaping), and re-tuning it constantly as Starlink throughput changes is not practical.
7. Restart services
service network restart && service odhcpd restart && service dnsmasq restart && service firewall restart && service sysntpd restart
8. Verify
# WAN IPv6 from Starlink
ip -6 addr show dev eth0
# LAN delegated prefix
ip -6 addr show dev br-lan
# IPv6 default route
ip -6 route show default
# Connectivity
ping6 -c 3 ipv6.google.com
# hybla active
sysctl net.ipv4.tcp_congestion_control
# MSS clamp rule present
nft list chain inet fw4 mangle_forward
On a LAN client, ip -6 addr show should show a 2xxx: address. The valid_lft and preferred_lft values will reflect Starlink's short lifetimes (~300s/~150s), but they'll be refreshed by RA messages before expiring. https://test-ipv6.com should score 10/10.
If br-lan shows no global prefix after 30โ60 seconds, try service network restart and wait another 30 seconds. DHCPv6-PD can take a moment to complete after network restart.
Hope this is useful. Happy to answer questions โ particularly around the fw4 MSS bug as that one cost me the most time to track down.
I've also put together a setup script that applies all of the above automatically on a fresh OpenWrt install: