From 564fbb916b4f1cce66f98f74f2d6450ed54b9578 Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Mon, 16 Feb 2026 11:25:51 +0100 Subject: [PATCH] fix(routes): add IPIP return path on leader when local=false When running with --local=false and --compatibility=cilium, the leader node did not create IPIP return routes for non-leader nodes in the same location. This caused asymmetric routing: non-leaders encapsulated traffic to the leader via IPIP (through Cilium's VxLAN overlay), but the leader sent replies directly via the physical interface, which could be dropped by cloud networks blocking IP protocol 4 or by reverse path filtering on the non-leaders. Add a new routing block for the !local case that creates: - Routes in table 1107 using the overlay gateway (Cilium internal IP) so IPIP outer packets traverse the VxLAN tunnel - Policy rules matching traffic arriving on the WireGuard interface (iif kilo0) destined for non-leader private IPs This only activates when the encapsulator returns a gateway different from the node's private IP, i.e. when an overlay like Cilium is in use. Co-Authored-By: Claude Signed-off-by: Andrei Kvapil --- pkg/mesh/routes.go | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/pkg/mesh/routes.go b/pkg/mesh/routes.go index b02b962d..ec79b8e2 100644 --- a/pkg/mesh/routes.go +++ b/pkg/mesh/routes.go @@ -191,6 +191,35 @@ func (t *Topology) Routes(kiloIfaceName string, kiloIface, privIface, tunlIface } } } + // When not managing local routes, the leader still needs to + // route return WireGuard traffic through IPIP when non-leaders + // use overlay routing (e.g. Cilium) to reach the leader. + // Use the overlay gateway (e.g. Cilium internal IP) so the + // IPIP outer packet is routed through the overlay tunnel, + // since direct IPIP may be blocked by the cloud network. + if !local && t.privateIP != nil && enc.Strategy() != encapsulation.Never { + for i := range segment.cidrs { + if segment.privateIPs[i].Equal(t.privateIP.IP) { + continue + } + nodeGw := enc.Gw(nil, segment.privateIPs[i], segment.ciliumInternalIPs[i], segment.cidrs[i]) + if nodeGw != nil && !nodeGw.Equal(segment.privateIPs[i]) { + routes = append(routes, &netlink.Route{ + Dst: oneAddressCIDR(segment.privateIPs[i]), + Flags: int(netlink.FLAG_ONLINK), + Gw: nodeGw, + LinkIndex: tunlIface, + Protocol: unix.RTPROT_STATIC, + Table: kiloTableIndex, + }) + rules = append(rules, defaultRule(&netlink.Rule{ + Dst: oneAddressCIDR(segment.privateIPs[i]), + Table: kiloTableIndex, + IifName: kiloIfaceName, + })) + } + } + } // Continuing here prevents leaders form adding routes via WireGuard to // nodes in their own location. continue