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 | |
|---|---|---|
| Chip | Marvell CN9130, 4× Cortex-A72, 2 GB RAM, 7.3 GB eMMC | AMD RX-216TD, 4 GB RAM, 64 GB SATA flash |
| Does | the router: 8 RJ45 + SFP via the 88E6193X switch, NAT, DHCP, firewall | Wi-Fi (QCA988x) and the front USB port |
| Stock OS | SFOS on Marvell SDK Linux 4.14.207 | SFOS on Linux 4.14.277 |
| Console | ttyS0, wired out through the x86's second UART | RJ45 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":
-
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. -
Cache coherency. With the device tree's
dma-coherentproperty set, the host read stale DRAM — this PCIe master doesn't snoop the CPU caches. Removing the property makes both directions honest. -
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=0x0for its trouble. Fixed on the host side by preferring MSI when both are advertised. -
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 runningHZ=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; settingtransport_mtu=2048on 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 link | 630 Mbit/s / 1.7 Gbit/s |
| Wi-Fi, 5 GHz, no external antennas fitted | 300–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
- SFP. Wired and enabled, never tested with an actual module.
- Port LEDs. Polarity and colour mapping are guesses pending confirmation.
- Routing throughput LAN-to-LAN, and with a host on the WAN side: not measured.
install.sh --serialend to end, unattended, hasn't been run on hardware in its current form.- 2.4 GHz Wi-Fi and range: untested (only one 5 GHz client, no external antennas, was measured).
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.