REPRODUCIBILITY: Frequent on my home WLAN with a 10-minute DHCP lease: 38 times in about 20 hours, each shortly after the WLAN IPv4 address had been deleted and re-added. The manual simulation below reproduces the route loss; the kernel side of it is deterministic. With a 1-day lease for the phone it did not happen at all in 26 hours (see Workarounds).
OS VERSION: observed on 5.2.0.20 (Finlayson), connman 1.40+git26.6-1.43.1.jolla, kernel 6.12.38. The phone now runs 5.2.0.22, which has the same connman package and kernel version. I have not re-triggered the issue there (see Workarounds).
HARDWARE: Jolla Phone (2026), jp2601
UI LANGUAGE: Finnish
REGRESSION: Unknown. The code involved (connman/src/connection.c, connman/src/dhcp.c) does not seem to have changed in recent releases.
DESCRIPTION:
When ConnMan re-applies the WLAN DHCP lease, it deletes the IPv4 address from wlan0 and adds the same address back about 6 ms later. This is the only IPv4 address on wlan0, so when it goes away the kernel flushes every IPv4 route through wlan0. That includes the host route to the VPN server (<server-ip> via <gateway> dev wlan0), which ConnMan adds when the VPN connects. The kernel sends no RTM_DELROUTE for the flushed routes. ConnMan re-adds the WLAN gateway route and re-does the default route, but it does not re-add the VPN server route.
After that, WireGuard’s encrypted packets to the server follow the default route into wg0 itself. The VPN still shows “Connected” (state ready) and the WLAN service stays online, but no traffic passes until the user switches the VPN off and on. Before I had any logging, this dead state once lasted about 9.5 hours overnight (its trigger was not captured).
The WLAN link itself is not interrupted: same BSSID, carrier_changes unchanged, nothing in the wpa_supplicant or connmand journal. A real network change (moving to another WLAN) does not cause the problem, because then ConnMan reconnects the VPN and creates the route again.
PRECONDITIONS:
- A native WireGuard VPN imported from a .conf file, with Host set to a fixed IPv4 address and AllowedIPs = 0.0.0.0/0, connected over WLAN.
- A WLAN with DHCP. My router gave 10-minute leases during all the failure observations below (the 26-hour capture under Workarounds was made after the phone got a 1-day lease). Many of the events happened while the phone was idle with the screen off.
- Developer mode, for the root shell commands below.
STEPS TO REPRODUCE:
A. Natural trigger
- Connect to a WLAN with DHCP (a short lease, such as 10 minutes, makes the event frequent), then connect the WireGuard VPN.
- As root:
ip route get <server-ip>shows<server-ip> via 192.168.x.1 dev wlan0 ... - As root, start
ip -timestamp monitor address route link. - Leave the phone idle on the same WLAN for 30–60 minutes.
- When the monitor shows
Deleted ... wlan0 inet 192.168.x.y/24followed by the same address being added again, runip route show <server-ip>andip route get <server-ip>again.
B. Simulation of the same netlink sequence
devel-su
IF=wlan0; ADDR=192.168.x.y/24; GW=192.168.x.1; SRV=<server-ip>
ip route show $SRV # <server-ip> via 192.168.x.1 dev wlan0
ip addr del $ADDR dev $IF; ip addr add $ADDR brd + dev $IF
ip route replace $GW dev $IF scope link # ConnMan re-adds this route itself in the natural case
ip route show $SRV # (empty)
ip route get $SRV # <server-ip> dev wg0 src <tunnel-ip> ...
Run the checks right away. The simulation bypasses ConnMan, and one to two minutes later ConnMan reacted to the manual change on its own: it deleted the server route explicitly, then re-did the WLAN connection (with a short fallback to mobile data) and reconnected the VPN. After the natural event it does none of this.
EXPECTED RESULT:
The host route to the VPN server is kept, or restored as soon as the WLAN address and gateway are back, and the VPN keeps working. If ConnMan cannot restore the route, the VPN should at least leave the ready state (fail and reconnect) instead of staying “Connected” with no traffic.
ACTUAL RESULT:
ip route show <server-ip>is empty, andip route get <server-ip>returns<server-ip> dev wg0 src <tunnel-ip>.- wg0 TX keeps growing while RX stops. In the 9.5 h overnight case, TX reached about 2.0 GB while RX stayed at 114 kB.
- No IPv4 traffic or DNS works. The VPN shows Connected and the WLAN service stays online. Nothing leaks outside the tunnel, because everything still goes into wg0.
- The state persists until the VPN is switched off and on in Settings, which creates the route again.
Captured event from 2026-10-25, recorded with ip -timestamp monitor address route link. Timestamps are shortened, and the LAN and server addresses are replaced with placeholders:
08:17:58.189256 Deleted 34: wlan0 inet 192.168.x.y/24 brd 192.168.x.255 scope global wlan0
08:17:58.189523 Deleted 192.168.x.0/24 dev wlan0 proto kernel scope link src 192.168.x.y
08:17:58.189694 Deleted broadcast 192.168.x.255 dev wlan0 table local proto kernel scope link src 192.168.x.y
08:17:58.191913 Deleted local 192.168.x.y dev wlan0 table local proto kernel scope host src 192.168.x.y
08:17:58.195204 34: wlan0 inet 192.168.x.y/24 brd 192.168.x.255 scope global wlan0
08:17:58.195365 local 192.168.x.y dev wlan0 table local proto kernel scope host src 192.168.x.y
08:17:58.195431 broadcast 192.168.x.255 dev wlan0 table local proto kernel scope link src 192.168.x.y
08:17:58.195477 192.168.x.0/24 dev wlan0 proto kernel scope link src 192.168.x.y
08:17:58.197685 192.168.x.1 dev wlan0 scope link
08:17:58.197770 Deleted default dev wg0 scope link
08:17:58.197797 default via 192.168.x.1 dev wlan0
08:17:58.207713 default dev wg0 scope link
08:17:58.217930 Deleted default via 192.168.x.1 dev wlan0
08:17:58.278500 63.32.119.124 via 192.168.x.1 dev wlan0 <- ipv4.jolla.com (online check)
08:17:58.651262 Deleted 63.32.119.124 via 192.168.x.1 dev wlan0
(the route to <server-ip> is gone now; no "Deleted <server-ip> ..." line is ever printed)
08:18:28.787952 131: wg0: <POINTOPOINT,NOARP,DYNAMIC> mtu 1420 qdisc noqueue state DOWN group default <- VPN reconnected by a watchdog
08:18:28.789811 Deleted 131: wg0: <POINTOPOINT,NOARP,DYNAMIC> mtu 1420 qdisc noop state DOWN group default
...
08:18:29.056464 132: wg0: <POINTOPOINT,NOARP,DYNAMIC,UP,LOWER_UP> mtu 1420 qdisc noqueue state UNKNOWN group default
08:18:29.066410 <server-ip> via 192.168.x.1 dev wlan0 <- created again only by the reconnect
08:18:29.072074 default dev wg0 scope link
Note that the gateway route 192.168.x.1 dev wlan0 scope link is also added again without a “Deleted” line before it: the kernel flushed it silently together with the server route.
MODIFICATIONS:
- Developer mode and SSH are enabled.
- After the 9.5 h outage I added two workarounds: a user-space watchdog that reconnects the VPN when the route to the server points into the tunnel, and an iptables kill switch (raw table). Neither of them changes IPv4 addresses or routes (the kill switch only adds an IPv6 unreachable default route), and the 9.5 h outage happened before either was installed.
- My other self-made add-ons do not modify addresses or routes. One of them, a privacy-switch companion, turns mobile data and the cellular radio off while the hardware privacy switch is on.
ADDITIONAL INFORMATION:
Frequency
- Night of 2026-10-24/25, before I knew the trigger: the watchdog repaired the route loss 23 times, first at 18:22 and then every 12–67 minutes from 21:40 until morning, almost all on the same WLAN with no network change. That logger did not record address events.
- 2026-10-25 18:16 to 2026-10-26 13:58: 38 events. Each came at most two minutes after a same-address delete and re-add on wlan0 (the watchdog only counts an event when it sees that pairing). In the same period the WLAN link dropped only 2–3 times (
/sys/class/net/wlan0/carrier_changes).
Where the address re-apply seems to come from (connman/src/dhcp.c, connman/gdhcp/client.c)
The sequence looks like ConnMan’s DHCP lease-expiry path: the address is deleted, the same address returns after 6 ms, and then ConnMan adds 192.168.x.1 dev wlan0 scope link and briefly re-does the default route. In gdhcp, start_expire() first calls restart_dhcp(), which sends a new DISCOVER at once, and then calls the lease-lost callback: lease_lost_cb() → dhcp_invalidate() → __connman_ipconfig_address_remove(). The answer for the same address then arrives through lease_available_cb() → dhcp_valid(), which adds the address back. A plain renewal with the same address does not remove the address in lease_available_cb().
With a 10-minute lease, gdhcp sends a renew at 300 s, a second renew at about 412 s and a rebind at 525 s (start_bound(), continue_renew(), continue_rebound()). If none of the replies gets through, the lease expires at 600 s. My guess is that this happens while the phone is suspended. I did not have connmand debug logging enabled at the captured event. I can collect debug output (-d src/dhcp.c -d src/connection.c, plus CONNMAN_DHCP_DEBUG=1 for gdhcp) if that helps.
Why the route is never restored (sailfishos/connman master, connman/src/connection.c)
- The server host route is created only in
set_vpn_routes()(comment: “Special route to VPN server via gateway”). That function is called from__connman_connection_gateway_add()only whenservice_type == CONNMAN_SERVICE_TYPE_VPN, i.e. when the VPN connects. - When the WLAN service’s gateway is added again,
__connman_connection_gateway_add()takes the non-VPN path.add_host_route()adds only the gateway host route. Nothing checks whether an active VPN gateway entry hasvpn_phy_indexequal to this interface and re-adds its server route. - The rtnl watch (
connection_newgateway()/connection_delgateway()) only follows default routes, so the silent loss is never noticed. - On the kernel side, routes that are flushed because their device lost its last IPv4 address are removed without RTM_DELROUTE. A netdev discussion in 2023 concluded that this is intentional, and only routes whose preferred source address was removed now get a notification (netdev thread). The server host route has no
src, so user space sees nothing.
Possible fixes (suggestions)
- When a non-VPN service’s gateway is (re-)added, or its IPv4 address comes back, re-run the VPN host route setup for every VPN gateway entry whose
vpn_phy_indexmatches that interface. Another option is to re-check the routes after an IPv4 address deletion; in the 2023 netdev discussion this was mentioned as what NetworkManager does. - In the DHCP code, avoid deleting the address when the same address is re-acquired straight away. This would also avoid the short outage for everything else on the interface.
- As a safety net in connman-vpnd’s WireGuard plugin, periodically check that the route to the endpoint does not point to the WireGuard interface itself, and reconnect or re-add the route if it does.
Related
- The open PR sailfishos/connman#157 adds a WireGuard handshake check. It also updates the VPN route when the endpoint address changes (commits “wireguard: Update endpoint to connmand when it changes” and “WIP: provider: update VPN route when the gateway/endpoint is updated”,
connman_provider_update_gateway()in src/provider.c), and since 2026-10-26 it uses the VPN’s transport service when the VPN gateway is added (0abe890). Commits from 2026-10-29 and 2026-10-30 (up to866cae2) adjust that code and remove the old endpoint route through the transport’s interface index when the gateway is updated.- That PR covers a different trigger, which I probably also hit: with a hostname endpoint whose DNS rotates to a new server every 30 to 60 s, the code re-resolves it every 20 s and moves the WireGuard endpoint but not the host route, which gives the same loop. I saw that tunnel die while “Connected” but did not capture the route table then.
- As far as I can read it (up to
866cae2, still the latest commit on 2026-10-31), the PR does not cover this report: here the endpoint never changes, and nothing re-adds the server route when the transport’s address and gateway come back. Its handshake check probably does not catch the dead state either: as I readwg_handshake_check_cb(), it adds an error only while no peer has completed any handshake, and here the handshake had succeeded before the route was lost. I have not tested that branch, but I can test a build.
- NetworkManager had the same class of bug: Launchpad #288703 “Routes lost on DHCP lease renewal (breaks VPN)”.
- Possibly the same symptom from another user: forum post 33025/8 (2026-10-27: WLAN has no internet with WireGuard on, mobile data works, recreating the WireGuard connection “fixed” it). The route table was not posted there, so this is only a guess.
- 5.2.0.22 ships the same connman package (1.40+git26.6-1.43.1.jolla) and kernel version (6.12.38), and master has no newer commits (checked 2026-10-31), so I expect the same behaviour there. I have not re-triggered it on 5.2.0.22: with the longer lease the natural trigger did not occur in 26 hours, and I have not run the simulation there (see Workarounds).
Workarounds
- Switch the VPN off and on in Settings > VPN. This creates the route again.
- A longer DHCP lease avoids the trigger. The router’s lease time was 10 minutes; after giving the phone a 1-day lease (2026-10-26), a 26-hour capture (
ip -timestamp -4 monitor addressplus the watchdog’s repair counter, 2026-10-27 15:29 to 2026-10-28 17:46, SFOS 5.2.0.22) showed no address re-apply and no route loop at all, compared with 38 loops in about 20 hours with the 10-minute lease. That capture was also my first on 5.2.0.22, but connman is unchanged there, so I attribute the difference to the lease. The only wlan0 address events were three interface down/up events in which wlan0, the WireGuard interface and the cellular interface lost their addresses within 0.2 s of each other and came back after 18-48 s. Two of them (16:04 and 16:05) were my own privacy-switch test, which turns WLAN and the cellular radio off; the third (17:54) had the same signature, and I did not note its cause. After each one ConnMan rebuilt the VPN and its route correctly. - As root, re-adding the route should restore the tunnel without a reconnect:
ip route add <server-ip> via 192.168.x.1 dev wlan0(not tested). - Automated: a small user-space watchdog that checks
ip route get <server-ip>and reconnects the VPN through connmand (net.connman.Service Disconnect/Connect on thevpn_...service) fixes it within a few seconds. A firewall kill switch keeps traffic from leaking during the reconnect.