Semi-automatic whole-house iPhone internet failover

Feb 2024 - 7 min read

I have a cable broadband connection at home – Virgin Media. It’s actually reliable, which is refreshing compared to the experience had as a student living in Leicester.

The problem

It’s a problem when it is down as I work from home a lot and the rest of the family need it too.

Previously, I had used Internet Connection Sharing to provide internet via my macbook via the WAN port of my router. This worked, but it was fiddly and temperamental. The macbook provided the WAN interface with a DHCP address, so as far as my router was concerned it had a regular internet connection, albeit with an extra NAT layer.

I didn’t want to pay for a separate LTE dongle for a backup connection, which is possible with my provider. It also seems redundant as our iPhones have LTE.

The idea

What if it was as simple as connecting my iPhone to the router? The router would then:

  1. Detect the iPhone
  2. Switch over the WAN route to iPhone automatically, allowing all devices on my network internet access again with no change
  3. Reconnect to primary WAN when the iPhone disconnects

Normally WAN failover is automated. As I don’t want my iPhone permanently connected, it makes sense for this to be semi-automatic as above.

The solution

My router/gateway is a supermicro server running NixOS, with nftables and dnsmasq. It’s what I’ve settled on after trying all sorts of platforms – I’ll blog about it soon.

iPhone connected to gateway
iPhone connected to gateway

The scripts here can be applied to any linux-based gateway so long as you have the right access and repositories available.

What’s relevant about my setup is I rename all my interfaces (and virtual interfaces for VLANs) using udev rules. This allows a predictable and meaningful name, (rather than en0s0fds0f7s0ad80s98ad0f or whatever udev does nowadays….). It also makes the nftables firewall rules much easier to comprehend.

After some experimentation, I created a simple systemd service:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/usr/bin/env bash
set -e
echo "Waiting for iPhone..."
until lsusb | grep -q iPhone; do sleep 1; done

# HACK
# annoyingly, the iPhone will fail to re-pair unless usbmuxd is restarted
# to detect this, validate the connection -- the connection should be
# valid before pairing
if ! idevicepair validate >/dev/null; then
    echo "iPhone connected but cannot pair. Restarting usbmuxd..."
    systemctl restart usbmuxd
fi

# Should work. If not, fail and let systemd try again
idevicepair validate

# Will fail because it's asking for the security prompt. Wait for that
echo "You must ensure iPhone has tethering enabled!"
echo "Waiting for security prompt..."
until idevicepair pair >/dev/null; do sleep 1; done

# route via iphone -- REQUIRES udev rule to rename interface based on iPhone MAC!
ip route add default via 172.20.10.1 dev wanfailover metric 1
echo "iPhone connected, default route changed."

# wait for disconnect, no need to clean route
while lsusb | grep -q iPhone; do sleep 1; done

echo "iPhone disconnected, primary WAN restored"

What is does is use lsusb to poll for the iPhone. Once it sees the iPhone, it uses idevicepair from libimobiledevice to attempt to pair. The first attempt will result in a prompt:

Enter pin code
Enter pin code
Trust prompt
Trust prompt

Once the user has “trusted” the gateway and entered the code, the pairing will succeed. This will create a new default route; the route will have a “metric” that’s too high – the traffic will still attempt to go over the default WAN at this point.

A new route is added manually, therefore – using the renamed device, wanfailover which is the virtual ethernet adapter that the iPhone creates.

After this, the service waits for the iPhone to disconnect. As this removes the ethernet device, the associated route is removed by the kernel automatically. This is a useful side-effect of using a device name in the route – I think!

The service is configured with Restart=always on a 10 second timer; this allows systemd to effectively be the outer loop here which simplifies the code and allows for some automatic error recovery.

You may have noticed a “HACK” – unfortunately the usbmuxd service provided by libimobiledevice seems to get stuck in a state where the second pairing attempt does not work.

Testing

The route is changed almost immediately, and devices appear to have a good internet connection throughout the house, albeit slower than the >1000Mbps primary WAN.

What is a little annoying, is existing SSH sessions freeze up and eventually time out. I’d like to look into a way of forcibly resetting existing TCP steams to avoid this, though realistically it is moot as during a real WAN-down situation the switch-over requires a human to notice and won’t be instant.

Foribly resetting the stream may allow streaming services to reconnect faster.

Interestingly, my SSH sessions that were running over wireguard (using dsnet!) seamlessly persisted. This is because wireguard is naturally stateless, operating over UDP.

I used fast.com to measure throughput; a good choice in my opinion because it uses the Netflix CDN – unlike the dedicated speedtest.net ISPs can’t artificially prioritise traffic without also prioritising Netflix traffic so it’s a better indicator.

LTE in the server cupboard
LTE in the server cupboard
LTE in the loft
LTE in the loft
iPhone in the loft
iPhone in the loft

I tested in the server cupboard, and also via a 5M USB extension cable in the loft; this provided more speed but something didn’t seem quite right still – check out the uplink speed – it may have increased due to height but I think it was impacted, at least in the first instance by my CCTV cameras.

I run a remote frigate instance for 4 cameras. That means around an 8 Mbps uplink load at any given time – enough to overwhelm the iPhone’s 4G LTE connection.

So, I added a few commands to disable the CCTV VLAN on WAN failover:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#!/usr/bin/env bash
#
echo "Resetting CCTV interface..."
ip link set dev cctv up

echo "Waiting for iPhone..."
until lsusb | grep -q iPhone; do sleep 1; done

# HACK
# annoyingly, the iPhone will fail to re-pair unless usbmuxd is restarted
# to detect this, validate the connection -- the connection should be
# valid before pairing
if ! idevicepair validate >/dev/null; then
    echo "iPhone connected but cannot pair. Restarting usbmuxd..."
    systemctl restart usbmuxd
fi

# Should work. If not, fail and let systemd try again
idevicepair validate

# Will fail because it's asking for the security prompt. Wait for that
echo "You must ensure iPhone has tethering enabled!"
echo "Waiting for security prompt..."
until idevicepair pair >/dev/null; do sleep 1; done

# route via iphone -- REQUIRES udev rule to rename interface based on iPhone MAC!
ip route add default via 172.20.10.1 dev wanfailover metric 1
echo "iPhone connected, default route changed."

# Do this late to avoid toggling
echo "Bringing down CCTV to save uplink bandwidth..."
ip link set dev cctv down

# wait for disconnect, no need to clean route
while lsusb | grep -q iPhone; do sleep 1; done

echo "iPhone disconnected, primary WAN restored"

This probably worked:

Speed after CCTV hack
Speed after CCTV hack

This wasn’t in the loft, even. The downstream speed also increased – I wonder if I prevented some ACK starvation which would explain it; though this could just be network variance.

Conclusion

Semi-automatic iPhone based internet failover is a convenient an alternative for paying for a separate backup ADSL2 or LTE connection.


Thanks for reading! If you have comments or like this article, please post or upvote it on Hacker news, Twitter, Hackaday, Lobste.rs, Reddit and/or LinkedIn.

Please email me with any corrections or feedback.