← Blog

Two computers, one firewall: OpenWrt on the Sophos XGS 107w

The Sophos XGS 87/107 is sold as a small desktop firewall. Open it up and it is actually two separate computers sharing one case: a Marvell CN9130 network processor running four Cortex-A72 cores that drives all eight gigabit ports and the SFP cage, and an AMD x86 board that exists purely to drive the Wi-Fi radio and the front USB port. Sophos's own firmware (SFOS) glues the two together and presents them as one router. This is the story of replacing both sides with OpenWrt, keeping the NPU's stock system as a fallback, and building an Ethernet link between the two boards that uses no Sophos code at all.

If you just want to use one of these, start at the project page: what it is, a quick start, and the FAQ. The code and full reference documentation are at nicologiuliani6/openwrt-sophos-xgs107.

How this was built, honestly

As with the last write-up on this site, a significant part of the code and documentation here was written with LLM assistance (Claude, through Claude Code). The physical work — opening the unit, wiring the serial adapters, cabling, power cycles, reading LEDs and ports — and the decisions about what to actually do with the hardware were mine; reverse engineering the stock system, the device tree, the kernel patches, the installer scripts, the build system and the PCIe endpoint link were done with the AI doing the heavy lifting, under my authorisation for every write to the device. I think that division of labour is worth stating plainly rather than glossing over.

Two computers pretending to be one

The panel gives it away once you know to look: 8 RJ45 ports, one SFP cage labelled F1, a front USB port, and both an RJ45 rollover console and a micro-USB console on the back — for two different UARTs. Under SFOS, one login manages the whole box; under the hood, that login is proxying into two operating systems that don't share a kernel, a filesystem or even an architecture.

NPU (CN9130)x86
ChipMarvell CN9130, 4× Cortex-A72, 2 GB RAM, 7.3 GB eMMCAMD RX-216TD, 4 GB RAM, 64 GB SATA flash
Doesthe router: 8 RJ45 + SFP via the 88E6193X switch, NAT, DHCP, firewallWi-Fi (QCA988x) and the front USB port
Stock OSSFOS on Marvell SDK Linux 4.14.207SFOS on Linux 4.14.277
ConsolettyS0, wired out through the x86's second UARTRJ45 rollover or micro-USB (Prolific PL2303), 38400 8N1

Under SFOS, the x86 reaches the switch ports only through a stack of Sophos-proprietary kernel modules — mv_armada_drv, mv_pcinet_drv, mv_giu_drv and friends, built for Linux 4.14.277 only. That's the whole reason a stock Linux or OpenWrt image on the x86 sees no ports at all: without those modules there is nothing there, by design. Mainline has to reach the NPU a different way.

What each side needed

The NPU side turned out to be the easy half. Everything relevant — the CN9130 SoC support, mvpp2 for its Ethernet, sdhci-xenon for the eMMC, and mv88e6xxx DSA for the 88E6193X switch chip — already exists in mainline Linux. No blobs, no vendor tree. The switch sits on the CN9130's MDIO bus with port 0 wired at 10 Gb/s into the SoC's mvpp2, so eight gigabit ports and an SFP cage come almost for free once the device tree describes the board correctly.

The x86 side needed the Wi-Fi driver (ath10k for the QCA988x, also mainline) and the USB port, both trivial once OpenWrt boots there at all. The problem was booting at all: SFOS's disk has only two real MBR entries, and everything else — /dev/boot, /dev/swap, /dev/var — SFOS creates itself at runtime. The installer writes the OpenWrt root filesystem over SFOS's own 3.8 GB swap area and adds a real MBR entry for it, keeps the kernel on the existing GRUB boot partition, and adds an OpenWrt boot entry as the default. One consequence follows directly from that: SFOS must never boot on the x86 again. It runs mkswap on that area at every boot, which means the moment SFOS boots there, the OpenWrt root is gone. The NPU has a much softer landing — its stock system lives in its own eMMC partition, untouched, and U-Boot falls back to it automatically if OpenWrt's kernel doesn't load.

The interesting part: giving the x86 a network card it doesn't have

The x86 board has no Ethernet interface of its own — every network path it has goes through the NPU. Under SFOS that's a proprietary multiplexed netdev on top of those Sophos kernel modules. Under OpenWrt, it needed to be something else, built from parts already in the kernel.

The CN9130 is wired to the x86 as a PCIe endpoint — from the x86's point of view, the NPU shows up as a PCI device (03:00.0, Gen3 x2). Stock U-Boot already leaves the CN9130's DesignWare PCIe controller in endpoint mode with the link up; the device tree only needs to describe it as snps,dw-pcie-ep with a control BAR and a memory window. Mainline Linux has exactly the function needed to turn a PCIe endpoint link into an Ethernet interface: pci-epf-vntb, a virtual non-transparent-bridge endpoint function, paired with the ntb_netdev driver on the host side. The NPU presents itself, the x86 loads ntb + ntb_hw_epf + ntb_transport + ntb_netdev, and out comes an ordinary interface, ntb0, on both ends — bridged into the NPU's LAN. No Sophos code anywhere in that path.

It didn't work out of the box. Four separate bugs showed up between "the function attaches" and "the link is actually fast":

  1. BAR alignment. This DesignWare core's BAR-match inbound iATU ignores the low 20 bits of the target address — the BARs are 1 MiB, so anything placed with page alignment inside them is at the wrong address as far as the hardware cares. The endpoint framework allocates its 2 KiB control area with plain page alignment, so the host was reading and writing up to 1 MiB below the real buffer: garbage memory-window counts, commands the endpoint never saw. Found by dumping the iATU registers directly and cross-checking against the stock kernel's own armada_pcie_ep_bar_map, which uses the same standard registers. Fixed by forcing 1 MiB alignment in the endpoint features and disabling the iATU windows U-Boot leaves configured.
  2. Cache coherency. With the device tree's dma-coherent property set, the host read stale DRAM — this PCIe master doesn't snoop the CPU caches. Removing the property makes both directions honest.
  3. MSI vs. MSI-X. The DesignWare core always advertises an MSI-X capability; the NTB driver preferred it, but the endpoint function only ever raises plain MSI. The NPU was writing interrupts through an empty MSI-X table, and the x86's IOMMU logged IO_PAGE_FAULT address=0x0 for its trouble. Fixed on the host side by preferring MSI when both are advertised.
  4. Throughput. Two separate problems here. Doorbells from the host are found by polling the control area — there's no interrupt for that — and the default poll interval rounds up to a full tick, 10 ms at HZ=100; dropping it to 1 ms (with the kernel already running HZ=1000) tightened that considerably. Separately, the default 64 KiB transport frame size gives the 1 MiB shared window only 15 slots, so small packets queue behind a full round trip; setting transport_mtu=2048 on both sides gives 511 slots instead. Before these two fixes, iperf3 measured 12 Mbit/s one way and 6 the other. After: 630 Mbit/s NPU→x86, 1.7 Gbit/s x86→NPU.

The result behaves like a normal, if asymmetric, link: 1.3 ms ping, the x86 reaches the Internet through the NPU, and its LuCI and SSH are reachable from any LAN port. One coupling is worth knowing before you rely on it: resetting or losing the NPU while the x86 has the link bound reboots the x86, because the PCIe endpoint device disappears out from under an active driver. It comes back and re-links itself with no intervention, and the boot order of the two sides doesn't matter — a small service on the x86 just rescans PCI until the NPU's function appears.

What it measures like

Result
Wired RJ45 ports (NPU ↔ PC)941 Mbit/s — line rate
Internal NPU↔x86 link630 Mbit/s / 1.7 Gbit/s
Wi-Fi, 5 GHz, no external antennas fitted300–500 Mbit/s down, 200–400 up

Not everything is measured yet. The SFP cage works electrically but there's no module on hand to actually test it with. Port LEDs are wired in the device tree from the stock GPIO-expander pinout, but which colour is which pin, and their active-high/low polarity, are guesses that haven't been confirmed against real hardware behaviour — I'd rather say that plainly than leave it implied that they're verified. LAN-to-LAN routing throughput hasn't been benchmarked at all. And Wi-Fi ships off and open until you set an SSID and key yourself.

Sending it back upstream

None of this needs to leave the project's own OpenWrt tree to be usable — but a board this undocumented is worth putting in front of the people who maintain the drivers it depends on, both so it doesn't bit-rot as a private patch set and so someone who knows the hardware better than I do gets a chance to catch anything wrong. Two things went out: a device-tree series describing the CN9130 XGS 107w board, and a fix to the mv88e6xxx switch driver (the 88E6191X/88E6193X don't have PTP, and the driver was assuming they did). The switch fix is reviewed and accepted by the DSA maintainer; the device tree is still waiting on a maintainer's ack. Using the board today needs neither — the same changes already ship inside this project's own image — but if they land, the next person who finds one of these on a shelf starts from mainline instead of from a private patch set.

What's left

Everything here is GPL-2.0 on GitHub, with the full documentation behind every claim above: the install steps, the architecture, the hardware reference down to the port map and PCIe endpoint register layout, and the upstreaming status. The short version — what it is, how to install it, FAQ — is on the project page. If you have one of these boxes and want to try it, or you find something that doesn't match what's documented here, open an issue or a discussion on the repo.