Introduction
In the past, I have worked with proprietary firewalls (Stormshield, Arkoon, Netasq). The experience was rather mixed: the whole thing is hard to debug, updates are gated behind licenses, prices are excessive, and there are many technical limitations.
So many reasons to rule out proprietary gear for this kind of design, in favor of a Linux-based solution (FRR, BGP, nftables): open source, innovation, and independence.
The idea: a hub-and-spoke design where the firewalls play the role of hub, and nobody talks to anyone without going through them.
Overview
Here is a simplified diagram of this design:
On the network’s routers, we instantiate a hub services VRF (HUBSVC in the configuration). This VRF holds only the default routes originated by the firewalls.
A second VRF is present: the spoke VRF. It serves as the home VRF for the client interfaces we want to isolate.
It lets us export, over EVPN, the various destinations reachable through the firewall. When machines in this VRF want to talk to other machines, their traffic is drawn in thanks to a leak of the hub services default routes.
In our diagram, the interface between r1 and riri is in the spoke VRF: when riri wants to communicate with loulou, it follows the hub services default route.
The firewall brings up IPv4 and IPv6 unicast eBGP sessions for spoke services (SPKSVC) and for hub services: EVPN stops at the spine. We could have used a single EVPN session between the firewall and the spine; keeping separate unicast sessions decouples network management from firewall management, for instance when different teams handle each.
Here is an example of traffic between riri and loulou:
For redundancy, this design can include several firewalls: the return traffic may then not take the same firewall as the outbound path. This kind of case rules out stateful firewalling: you would have to either synchronize conntrack across all firewalls (with conntrackd), or give up on state. The second option is the one chosen here, detailed in the Filtering section.
The EVPN mechanics
The whole isolation trick lies in the route-targets of the spoke VRF. Here is the relevant FRR configuration on r1:
| |
Three things to note:
- The spoke VRF exports its networks with route-target
65010:110. The spine imports them into aspoke servicesVRF and advertises them over eBGP to the firewall: the firewall therefore knows every spoke destination. - The spoke VRF imports
65010:65000, a “sentinel” route-target that deliberately matches nothing. Without it, FRR would automatically import its own export route-target, and each spoke would learn the other spokes’ networks. This line is what guarantees there is no direct route between r1 and r2. - The default route comes in via
import vrf HUBSVC, filtered by a route-map that only lets the default through:0.0.0.0/0in IPv4 (LEAK_DEFAULT4) and::/0in IPv6 (LEAK_DEFAULT6).
As a result, on r1, the spoke VRF’s routing table is minimal:
| |
loulou’s networks (198.51.100.16/28 and 2001:db8:b::/64) appear nowhere: the only way out is the default route, and it leads to the firewall.
Filtering
Here, the choice is to work with stateless filtering only. This lets us add as many firewalls as we want, without deploying any conntrack synchronization: since the return traffic may go through a different firewall than the outbound path, stateful filtering would force us to replicate state across every machine.
The downside is that each rule has to be written in both directions: swapping sources / destinations for the return traffic.
Chaining nftables rules the classic way quickly runs into performance issues, with significant jitter: each packet walks the chain rule by rule, and the processing time depends on the position of the matching rule.
Conversely, nftables sets (the successors to ipset) are far better suited to this need: the lookup cost stays nearly constant, whatever the number of entries.
| |
The first two rules allow ICMP and ICMPv6: handy for reachability tests. The rest of the filtering goes through the sets, one per address family.
A TCP connection from riri to loulou on port 443 generates two frames with symmetric headers:
| |
The set stores only one orientation, the outbound one: riri . loulou . 443 (client, server, server port). The ephemeral port (40000) is not in it.
So the return frame lands on that same entry, the second rule reads its fields in reverse:
| |
The saddr and daddr fields are swapped (source and destination trade places on the way back), and we match on sport instead of dport (the server port moves from destination to source). A single set entry therefore covers both directions.
Two sets, a few fixed rules, and the forward chain never grows: only the sets change.
Another operational advantage: sets can be modified on the fly, without reloading the ruleset.
| |
Verification
From riri, we check that loulou is reachable:
| |
And on the firewall, a tcpdump shows the traffic does go through it, in both directions:
| |
The packet comes in through the hub services VRF (the default route) and leaves through the spoke services VRF. If the nftables rule does not exist, the packet dies here: policy drop.
Conclusion
With this setup, all traffic between machines is drawn to the firewalls. Redundancy and scaling are easy.
All of it with Linux, FRR, and nftables. Zero proprietary appliances, zero licenses, and debugging done with the tools we already know. And when something is missing or blocks us, we can read the code and contribute, rather than opening a ticket with a vendor and waiting.
Deploying the configuration stays simple: no need for proprietary controllers or web interfaces to manage a large number of devices.
A complete containerlab lab reproducing this design (dual-stack IPv4/IPv6) is available on GitHub.
Further reading
- Hub-and-Spoke VPN Topology (Ivan Pepelnjak, ipSpace.net): the same design (pushing inter-spoke traffic through a central firewall via a pair of ingress/egress VRFs).
- RFC 7024, Virtual Hub-and-Spoke in BGP/MPLS VPNs: the formalization of the same route-target routing mechanism, on the BGP/MPLS side and geared toward scalability rather than filtering.