Guides

VRF & routing

Multi-tenant routing tables (VRFs) and static routes.

A VRF in siha is a named VPP FIB table. Every packet forwarded by the appliance is looked up in a table; if you do nothing, all traffic uses table 0 (the implicit default). VRFs let you run multiple isolated routing domains on a single appliance — one per tenant, one per security zone, whatever fits your topology.

VRF resource

type: VRFs.routing.siha
metadata:
  namespace: routing
  id: tenant-a          # your friendly name; also what other resources reference
spec:
  table: 100            # VPP FIB table id, 1–4294967295 (0 is reserved for default)
  family: DUAL          # IPv4 | IPv6 | DUAL (default when omitted)
  description: "Tenant A production segment"

table is the VPP-level table id — pick any non-zero integer that is unique across your VRFs. id is the human-readable COSI identifier that interface and route resources reference via their vrf field.

family controls which address-family FIBs are created in VPP:

ValueEffect
DUAL (default)both IPv4 and IPv6 FIB tables created
IPv4only IPv4 FIB created
IPv6only IPv6 FIB created

Binding interfaces to a VRF

Interfaces carry a vrf field that takes the COSI id of a VRFs.routing.siha resource. Omitting it (or leaving it empty) binds the interface to the default table 0.

For routed interfaces (PMD, host-interface, loopback, tap, VLAN/QinQ subinterfaces):

type: Interfaces.link.siha
metadata: { namespace: link, id: ext-tenant-a }
spec:
  kind: pmd
  pciAddr: "0000:05:00.0"
  adminUp: true
  addresses: ["10.100.0.1/24"]
  vrf: tenant-a          # binds this interface to the tenant-a FIB table

For bridge-domain interfaces the VRF goes on the BVI (the bridge’s L3 gateway), not on the bridge itself:

type: Interfaces.link.siha
metadata: { namespace: link, id: lan }
spec:
  kind: bridge
  adminUp: true
  members: [lan-nic]
  bvi:
    addresses: ["10.20.0.1/24"]
    vrf: tenant-a          # BVI bound to tenant-a; top-level vrf must be omitted

When an interface is bound to a VRF and pilotd assigns it an address, VPP automatically installs a connected route for the interface’s prefix into that table. You do not need a Route resource for it.

Static routes

Use a Route resource for destinations that are not directly connected — a default route, a summary prefix, or a remote subnet reachable via an upstream gateway.

type: Routes.routing.siha
metadata:
  namespace: routing
  id: default-tenant-a        # any unique id
spec:
  destination: "0.0.0.0/0"   # prefix to match (CIDR notation)
  gateway: "10.100.0.254"     # next-hop address (must be reachable in the VRF)
  vrf: tenant-a               # COSI id of the target VRF (omit for default table 0)

The vrf field resolves to the VRF’s numeric table id at reconcile time. If the referenced VRF does not exist yet, the controller surfaces an error in RouteStatus.lastError and retries — there is no hard ordering requirement in the manifest, but the VRF must exist before the route is installed in VPP.

A route in the default table (no VRF) omits the vrf field entirely:

type: Routes.routing.siha
metadata: { namespace: routing, id: default }
spec:
  destination: "0.0.0.0/0"
  gateway: "192.168.1.1"

Full example: two isolated tenants

# --- Tenant A VRF ---
type: VRFs.routing.siha
metadata: { namespace: routing, id: tenant-a }
spec:
  table: 100
  description: "Tenant A"
---
# --- Tenant B VRF ---
type: VRFs.routing.siha
metadata: { namespace: routing, id: tenant-b }
spec:
  table: 200
  description: "Tenant B"
---
# Tenant A uplink (connected /24 auto-installed, no Route needed)
type: Interfaces.link.siha
metadata: { namespace: link, id: uplink-a }
spec:
  kind: pmd
  pciAddr: "0000:05:00.0"
  adminUp: true
  addresses: ["10.100.0.1/24"]
  vrf: tenant-a
---
# Tenant A default route via its upstream gateway
type: Routes.routing.siha
metadata: { namespace: routing, id: default-a }
spec:
  destination: "0.0.0.0/0"
  gateway: "10.100.0.254"
  vrf: tenant-a
---
# Tenant B uplink
type: Interfaces.link.siha
metadata: { namespace: link, id: uplink-b }
spec:
  kind: pmd
  pciAddr: "0000:06:00.0"
  adminUp: true
  addresses: ["10.200.0.1/24"]
  vrf: tenant-b
---
# Tenant B default route
type: Routes.routing.siha
metadata: { namespace: routing, id: default-b }
spec:
  destination: "0.0.0.0/0"
  gateway: "10.200.0.254"
  vrf: tenant-b

Traffic between tenant-a and tenant-b cannot cross unless you explicitly add routes that bridge the two tables. VPP enforces the FIB isolation.

Operating

S="sihactl --sihaconfig ~/.siha/config --addr <node-ip>:6443"

# List all VRFs with their table id, family, and any error
$S get vrf

# Full spec + status for a single VRF
$S get vrf tenant-a -o yaml

# List all static routes with destination, gateway, resolved table, and errors
$S get route

# Full detail for one route
$S get route default-a -o yaml

# Watch routes as changes are applied
$S watch route

VRFStatus shows the VPP table id and address family as materialized. RouteStatus shows the resolved vrfTable (the numeric FIB table the route was installed in), which is useful for confirming that a VRF reference resolved correctly.

Callouts

Table 0 is reserved. The implicit default VRF always uses table 0; siha rejects any VRFs.routing.siha resource whose table field is 0. Use any non-zero integer (1–4294967295) for operator-defined VRFs.

Connected routes are automatic. When pilotd assigns an address to an interface bound to a VRF, VPP installs the connected prefix automatically. Do not add a Route for the interface’s own subnet — a duplicate static route is harmless but unnecessary.

VRF must exist before its routes are installed. The route controller retries on a missing VRF reference, but there is a brief window where RouteStatus.lastError shows a resolution error. Apply the VRF resource first (or in the same bundle) to avoid the retry cycle.

BGP learned routes live alongside static routes in the same FIB. The BGP controller programs its best paths into VPP FIB tables using the same table ids. If you combine BGP peers with static routes in the same VRF, make sure their prefixes don’t conflict — the more-specific prefix wins in VPP’s longest-prefix match, same as any router.