Skip to content

Instantly share code, notes, and snippets.

@hraftery
Last active October 26, 2023 21:08
Show Gist options
  • Save hraftery/3646bc5d07f475b3e5f64158869845b5 to your computer and use it in GitHub Desktop.
Save hraftery/3646bc5d07f475b3e5f64158869845b5 to your computer and use it in GitHub Desktop.
Gateway-Pi : connecting a HiLink cellular dongle to a Billion router

The Job

flowchart LR
    a[Cellular Dongle<br />Huawei E3372h-608] --> |USB| b[Raspberry Pi]
    b-->|Ethernet| c[Billion Router<br />BiPAC 7800NXL]
    c-->|Ethernet/WiFi| d[Home Broadband]
Loading

The Why

I have a temporary need for cellular broadband at home, and don't want to throw more e-waste at the problem. Alas it turns out my dusty old stalwart, the USB 3G/LTE compatible 7800NXL, doesn't want to play ball with the HiLink based E3372h dongle. The dongle works fine in a MacBook.

If only I could put a mini-MacBook between the dongle and the router... sounds like a job for one of the Pi's I've got stashed away!

The How

All the guides I found were either out of date, clumsy or just plain ineffective. So here's a what worked for me. I've included explanations that should help you adjust to your own needs.

The Pi

The starting point is a fresh Raspberry Pi OS Lite install, specifically 2023-05-03-raspios-bullseye-arm64-lite, configured for SSH access and connected over Ethernet.

Plug the cellular dongle into one of the Pi's USB ports. Contrary to expectations, the USB modeswitch worked for me without any effort. Once the dongle connected to cellular, the Pi had Internet access through a newly formed eth1 interface.

WiFi is no longer needed, so it can be turned off.

sudo rfkill block wifi

Then install dnsmasq. It seems like the easiest, right-sized solution to give the router what it needs to consider the Pi an Internet source.

sudo apt update
sudo apt install dnsmasq

Then add to the bottom of /etc/dhcpcd.conf:

interface eth0
static ip_address=192.168.2.1/24
nogateway

The nogateway prevents the Pi from prioritising Ethernet as a potential route to the Internet, since it's now strictly a downstream port. Obviously static sets a static IP for the Ethernet port. This will play well with dnsmasq and provide a predictable gateway address for the router.

The choice is fairly arbitrary, but I picked a subnet different from the 192.168.1.1/24 of the router and the 192.168.8.1/24 of the cellular dongle to make it easier to distinguish them.

Being a downstream port we don't need to bother with gateway and DNS settings for the Ethernet port.

By the way, I used dhcpcd.conf because while I find /etc/network/interfaces highly familiar, it feels like it's time to put it on ice. It's safe to leave it untouched. Raspberry Pi OS currently makes dhcpcd the default over NetworkManager, which suits me just fine. So using dhcpcd.conf to configure the ethernet port seems like the right thing to do.

Now on to dnsmasq. At the bottom of /etc/dnsmasq.conf add:

interface=eth0
#bind-interfaces # Causes silent failure on first boot because there's no eth0 to bind to at that time. Luckily, seems unnecessary.
#listen-address=192.168.2.1 # fails with “cannot assign requested address” because it’s redundant: https://forums.raspberrypi.com/viewtopic.php?p=1704404#p1704404
dhcp-range=192.168.2.50,192.168.2.100,12h
#server=1.1.1.1  # Forward DNS requests to Cloudflare. Not necessary at this level.
domain-needed   # Don't forward short names
bogus-priv      # Never forward addresses in the non-routed address spaces.

Honestly, the dnsmasq config was the hardest bit. I find Linux's boot behaviour progressively unpredictable over the last 10 years or so, and the various mechanisms to wait on network availability feel like working with a patchwork of bandaids. This configuration was the best I could find to ensure that a good working state would be established regardless of the sequence of power on and cable connections.

So finally, on to packet forwarding.

In /etc/sysctl.conf uncomment net.ipv4.ip_forward=1. This configures the kernel to be happy to redirect packets that arrive on one interface to another. Otherwise the kernel acts like a normal host and just drops packets not intended for itself.

There's an equivalent for IPv6: net.ipv6.conf.all.forwarding. But even though the cellular dongle establishes a IPv6 address, I haven't explored this side of things yet. It's been 25 years since IPv6 become the essential way forward, so I'm hoping I can squeeze a little more out of IPv4.

Okay, now the kernel is happy, set up the forwarding!

sudo iptables -F # Flush all the chains (ie. delete all the rules) in the default "filter" table
sudo iptables -t nat -F # Do the same for the "nat" table
sudo iptables -t nat -A POSTROUTING -o eth1 -j MASQUERADE # perform SNAT "the liberal way" by pretending to be the source of every outgoing packet 
sudo iptables -A FORWARD -i eth1 -o eth0 -m state --state RELATED,ESTABLISHED -j ACCEPT # then forward anything we inititiated coming back in on Cellular, out to Ethernet, as if weren't here
sudo iptables -A FORWARD -i eth0 -o eth1 -j ACCEPT # and vice versa

Obviously eth0 and eth1 need to be exactly the destination and source interfaces respectively. But the rest seems to work for a variety of forwarding scenarios.

Aside: a question for the audience - I initially misspelled eth0 in the last rule (I still haven't managed to eradicate the scourge that is auto-correct from my life!). Everything still seemed to work just fine, despite the rule referencing a non-existent interface. Why is it so?

Now make sure the forwarding sticks through reboots by saving the configuration and repeating it when the network interface comes up.

sudo iptables-save > pi-gateway.conf
sudo mv pi-gateway.conf /etc/pi-gateway.conf

Create /etc/network/if-up.d/iptables with contents:

#!/bin/sh
iptables-restore < /etc/pi-gateway.conf

Make it executable, and the Pi is cooked!

sudo chmod +x /etc/network/if-up.d/iptables

The Router

Given my original intention was to take advantage of the 7800NXL's 3G/LTE USB port, it seems a trifle redundant to insert another network between the LAN and the Internet. But it's just a temporary cellular broadband thing, not some cloud infrastructure thing, and the following seems to work just fine.

In the router's web admin page, click Configuration --> WAN. Add a service and pick "Ethernet". Then configure it like so:

Screenshot 2023-10-09 at 9 42 55 pm

I mostly accepted the defaults, except I turned off IPv6 as explained in the section above. The important setting is "IP over Ethernet" which seems to simply mean "DHCP client". This is easy to understand, and works just fine, though I do wonder if there's a way to take advantage of the "Bridging" option. I find Billion's powerfully capable routers to be a little let down by obscure nomenclature and lack of explanatory docs.

Anyway, that's all that needs to be done on the router side - the Pi can now be plugged into the WAN port and away you go!

Reboot all the equipment, test the network and run journalctl -u dnsmasq on the Pi to make sure everything is happy.

The Result

IMG_6559

Frankenrouter.

Addendum

I've since replaced the generic RPi3 with an exquisitely fit for purpose Rock Pi S. Raxda is doing a fine job of advancing on the Linux SBC theme, and I can't really blame them for making Debian the path of least resistance. On the down side, well, we just have a bit more work to (un)do. And in the case of the Pi S, it's Buster-era work.

Oh, and because computers are involved, "a bit more work" was grossly optimistic.

First do the Raxda dance.

sudo apt update # expect apt.radxa.com to fail at this point, but we need *some* updates for the following to work. Otherwise we have neither chicken nor egg.
sudo apt install -y wget # get the chicken (or the egg, depending on your perspective)
export DISTRO=buster-stable
#echo "deb http://apt.radxa.com/$DISTRO/ ${DISTRO%-*} main" | sudo tee -a /etc/apt/sources.list.d/apt-radxa-com.list # already present, so don't bother
wget -O - apt.radxa.com/$DISTRO/public.key | sudo apt-key add - # and there's our egg (or chicken)
sudo apt update # should be smooth sailing from here
sudo apt install -y rockchip-overlay
sudo apt install -y linux-4.4-rock-pi-s-latest rockpis-dtbo
sudo apt install -y rtl8723ds-firmware rockchip-adbd resize-assistant

Now get to work. Start by uninstalling NetworkManager. It's kryptonite for unattended installations and this is about as far from a desktop scenario as you could imagine. Fortunately, Debian has no relevant dependencies (reconsider if libbluetooth3 is important to you), so it's safe to purge.

sudo apt purge network-manager

And because I'm not a caveman, install a modern pager and add the missing ZeroConf support so the Rock Pi answers to rockpis.local and I can find it again.

sudo apt install less avahi-daemon

Lastly, the essentials:

sudo apt install iptables dnsmasq

Then, optionally, clean up after ourselves.

sudo apt autoremove

Okay, on to configuration. dhcpcd is unavailable, but systemd-networkd is installed and networking is not. So let's work with the /etc/systemd/network files instead of /etc/network/interfaces.

Create a file /etc/systemd/network/eth0.network with contents:

[Match]
Name=eth0

[Network]
Address=192.168.2.1/24

This mimics the dhcpcd config from the RPi3. Now before we forget, make sure it takes effect on reboot!

sudo systemctl enable systemd-networkd

Next, just like before, at the bottom of /etc/dnsmasq.conf add:

interface=eth0
#bind-interfaces # Causes silent failure on first boot because there's no eth0 to bind to at that time. Luckily, seems unnecessary.
#listen-address=192.168.2.1 # fails with “cannot assign requested address” because it’s redundant: https://forums.raspberrypi.com/viewtopic.php?p=1704404#p1704404
dhcp-range=192.168.2.50,192.168.2.100,12h
server=1.1.1.1  # Forward DNS requests to Cloudflare. Sadly, this is likely much, much faster than managing it ourselves.
domain-needed   # Don't forward short names
bogus-priv      # Never forward addresses in the non-routed address spaces.

Moving on to the packet forwarding. The kernel flag is the same, but instead of editing /etc/sysctl.conf lets do it properly. Create a file called /etc/sysctl.d/30-ipforward.conf with contents:

net.ipv4.ip_forward=1

By inspecting the output of update-alternatives --config iptables and sudo ls -l /sbin/ipt* I can see iptables-save and friends are expecting to be working with iptables-nft. Thinking this was my opportunity to get on board with nftables I developed some equivalent rules using nft. Alas, I couldn't get iptables to successfully save and restore the rules, whether they were created with iptables or nft. In the end, I found a combination that worked:

Create rules Save rules Restore rules
iptables iptables-save iptables-legacy-restore

But the mishmash left me a bit unsettled, so I decided to go all in on nftables. Here's how to enable nftables:

sudo systemctl enable nftables

And set the rules:

# Enable NAT. Note I choose the "ip" address family which is IPv4 only.
sudo nft add table ip nat
sudo nft add chain ip nat prerouting '{ type nat hook prerouting priority 100; }' # OMG, this requirement was hard earned: https://unix.stackexchange.com/questions/403389/using-linux-as-router-with-nftables-masquerade-not-forwarding-reply-back#comment1448354_439000
sudo nft add chain ip nat postrouting '{ type nat hook postrouting priority 100; }'
# Now we can masquerade as before
sudo nft add rule ip nat postrouting oifname eth1 masquerade
# and then forward packets as before
sudo nft add table ip filter
sudo nft add chain ip filter forward '{ type filter hook forward priority 0; policy drop; }'
sudo nft add rule ip filter forward ct state related,established accept
sudo nft add rule ip filter forward iifname eth0 oifname eth1 accept

Save the rules using nft too. Although now that I've read a few more man pages on iptables, lets draw on the standard name/path from that world:

echo "flush ruleset" > nftables.up.rules # start from scratch
sudo nft -s list ruleset > nftables.up.rules
sudo mkdir /etc/network
sudo mv nftables.up.rules /etc/network/

But more importantly, now that we've switched to systemd-networkd let's also switch from ifupdown to networkd-dispatcher to do the auto-loading on boot. Install it first:

sudo apt install networkd-dispatcher

Now we need to do the interface and state detection in the script itself (though I suspect the latter is redundant - both mind and logs seem to concur). Put a script in /etc/networkd-dispatcher/routable.d/nftables, like so:

#!/bin/sh

if [ "$IFACE" = lo ]; then
    echo "$0: ignoring $IFACE for \`$STATE'"
    exit 0
fi

case "$STATE" in
    routable)
        echo "$0: restoring nftables rules for $IFACE"
        /usr/sbin/nft -f /etc/network/nftables.up.rules
        ;;
    *)
        echo "$0: nothing to do with $IFACE for \`$STATE'"
        ;;
esac

Echoing logging information to stdout means it appears in journalctl -u networkd-dispatcher, which is convenient, and only as painful to work with as the rest of the journalctl world.

Make it executable, and the (Rock) Pi is cooked!

sudo chmod +x /etc/networkd-dispatcher/routable.d/nftables

Just kidding... there's one more obstacle. Unlike Raspberry Pi OS, usb-modeswitch is not installed in this version of Debian. So when I plug in the dongle, I get this in dmesg:

[170317.837453] usb 2-1: new high-speed USB device number 2 using ehci-platform
[170317.971587] usb 2-1: New USB device found, idVendor=12d1, idProduct=1f01
[170317.972310] usb 2-1: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[170317.973078] usb 2-1: Product: HUAWEI_MOBILE
[170317.973615] usb 2-1: Manufacturer: HUAWEI_MOBILE
[170317.974121] usb 2-1: SerialNumber: 0123456789ABCDEF
[170318.038013] usb-storage 2-1:1.0: USB Mass Storage device detected
[170318.040182] scsi host0: usb-storage 2-1:1.0
[170318.041831] usbcore: registered new interface driver usb-storage
[170318.047625] usbcore: registered new interface driver uas
[170319.043885] scsi 0:0:0:0: CD-ROM            HUAWEI   Mass Storage     2.31 PQ: 0 ANSI: 2
[170319.050697] sr 0:0:0:0: [sr0] scsi-1 drive
[170319.051183] cdrom: Uniform CD-ROM driver Revision: 3.20
[170319.053169] sr 0:0:0:0: Attached scsi CD-ROM sr0
[170319.058914] sr 0:0:0:0: Attached scsi generic sg0 type 5
[170321.422932] sr 0:0:0:0: [sr0] tag#0 FAILED Result: hostbyte=DID_OK driverbyte=DRIVER_SENSE
[170321.423847] sr 0:0:0:0: [sr0] tag#0 Sense Key : Medium Error [current] 
[170321.424537] sr 0:0:0:0: [sr0] tag#0 Add. Sense: Unrecovered read error
[170321.425221] sr 0:0:0:0: [sr0] tag#0 CDB: Read(10) 28 00 00 00 0d fc 00 00 02 00
[170321.426067] blk_update_request: critical medium error, dev sr0, sector 14320

And so on. It gets stuck enumerating it as USB mass storage instead of switching to a CDC Ethernet device. Easy fix, just install usb-modeswitch:

sudo apt install usb-modeswitch

That creates the file /lib/udev/rules.d/40-usb_modeswitch.rules which includes the lines:

# Generic entry for most Huawei devices, excluding Android phones

ATTRS{idVendor}=="12d1", ATTRS{manufacturer}!="Android", ATTR{bInterfaceNumber}=="00", ATTR{bInterfaceClass}=="08", RUN+="usb_modeswitch '/%k'"

Which is all that's needed to make the magic happen:

[12265.480426] usb 2-1: new high-speed USB device number 2 using ehci-platform
[12265.614505] usb 2-1: New USB device found, idVendor=12d1, idProduct=1f01
[12265.615221] usb 2-1: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[12265.615943] usb 2-1: Product: HUAWEI_MOBILE
[12265.616475] usb 2-1: Manufacturer: HUAWEI_MOBILE
[12265.616970] usb 2-1: SerialNumber: 0123456789ABCDEF
[12266.034203] usb-storage 2-1:1.0: USB Mass Storage device detected
[12266.035579] scsi host0: usb-storage 2-1:1.0
[12266.036773] usbcore: registered new interface driver usb-storage
[12266.043948] usbcore: registered new interface driver uas
[12266.710726] usb 2-1: USB disconnect, device number 2
[12267.148497] usb 2-1: new high-speed USB device number 3 using ehci-platform
[12267.282325] usb 2-1: New USB device found, idVendor=12d1, idProduct=14db
[12267.283037] usb 2-1: New USB device strings: Mfr=1, Product=2, SerialNumber=0
[12267.283760] usb 2-1: Product: HUAWEI_MOBILE
[12267.284211] usb 2-1: Manufacturer: HUAWEI_MOBILE
[12267.402735] cdc_ether 2-1:1.0 eth1: register 'cdc_ether' at usb-ff440000.usb-1, CDC Ethernet Device, 00:1e:10:1f:00:00
[12267.404141] usbcore: registered new interface driver cdc_ether
[12267.438204] cdc_ether 2-1:1.0 enx001e101f0000: renamed from eth1

Oh yeah, and as that last line indicates, it's also not called eth1 anymore. Either go to war with Predictable Network Interface Names, or adjust accordingly.

And a mere handful or dozen of late nights by the glow of a screen later, we're ready to kick things off with a reboot.

sudo reboot

IMG_6622

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment