Guides
DHCP
Serve DHCP to clients behind SiHA (embedded server on an L2 segment), or relay to an upstream server.
SiHA gives out addresses two ways, and you pick per segment:
- Serve — an embedded DHCP server hands out leases from a pool, honours static MAC→IP reservations, and persists leases across restarts. One server per L2 segment (typically per VRF).
- Relay — forward a segment’s requests to an existing upstream DHCP server reachable through the routed network.
Both are services SiHA offers to the clients behind it — not the appliance’s own address (the management NIC is a separate concern).
Serve
A served segment is three resources that work together. SiHA terminates the
segment as an L2 bridge; the bridge’s BVI holds the gateway, a tap
holds the DHCP server address, and the DHCPServer defines the pool.
# 1. The bridge — its BVI is the segment gateway (DHCP option "router").
type: Interfaces.link.siha
metadata: { namespace: link, id: lan }
spec:
kind: bridge
adminUp: true
members: [lan-nic] # the physical port(s) on the segment
bvi:
addresses: ["10.20.0.1/24"] # the gateway clients are handed
# vrf: tenant-a # omit for the default VRF
---
# 2. The server tap — its kernel IP is the DHCP server address.
type: Interfaces.link.siha
metadata: { namespace: link, id: dhcp-tap }
spec:
kind: tap
adminUp: true
hostKernel: dhcplan # host-side netdev name
hostAddresses: ["10.20.0.2/24"] # the DHCP server identifier
l2Bridge: lan # attach this tap to the bridge above
---
# 3. The DHCP server.
type: DHCPServers.dhcp.siha
metadata: { namespace: dhcp, id: lan }
spec:
bridge: lan
serverTap: dhcp-tap
subnet: 10.20.0.0/24
gateway: 10.20.0.1 # must equal the BVI address
pool: { start: 10.20.0.100, stop: 10.20.0.200 }
leaseSeconds: 3600
dns: ["10.20.0.1"]
domain: lan.internal
reservations:
- { mac: "aa:bb:cc:00:11:22", ip: 10.20.0.77 }
allowFrom: ["aa:bb:cc"] # see "Scoping clients" below
The gateway (10.20.0.1, on the BVI) and the server address (10.20.0.2, on
the tap) are deliberately two different addresses on the same segment. That
split is what lets a client renew cleanly: it renews to the server address, and
the bridge delivers that directly to the tap while the BVI stays the gateway.
You don’t have to think about it — just give the tap its own address in the
subnet, distinct from the gateway.
config apply cross-checks the three: the serverTap must be a kind: tap
whose l2Bridge is the bridge, gateway must equal the BVI address, and no
two servers may share a tap.
Reservations
A reservation pins a client (by MAC) to a fixed address. Reservation addresses must be inside the subnet, must differ from the gateway and server address, and may fall inside the pool range (the server skips them when handing out dynamic leases).
Scoping clients (allowFrom)
allowFrom is required and fail-closed: a server with no allowFrom
serves nobody. It matches the client hardware address (chaddr) — each entry is
a full MAC (aa:bb:cc:dd:ee:ff) or a 3-byte OUI prefix (aa:bb:cc). A source
IP range is not accepted: a client discovering an address has no IP yet, so
IP-based scoping cannot gate it. To serve everyone, list the wildcard OUI set
explicitly.
Relay
Where an upstream DHCP server already exists, relay to it instead of serving:
type: DHCPProxies.dhcp.siha
metadata: { namespace: dhcp, id: tenant-b }
spec:
vrf: tenant-b # the client-facing VRF (empty = default)
servers: ["10.99.0.10"] # upstream DHCP servers, reachable via the FIB
srcAddress: 10.30.0.1 # SiHA's relay-agent source address
At most one relay may exist per client-facing VRF.
Operating
S="sihactl --sihaconfig ~/.siha/config --addr <node-ip>:6443"
$S get dhcpserver # list servers + status (listening, leases used)
$S get dhcpserver lan -o yaml # full status: leases, offers/acks/naks, errors
$S get dhcpproxy # list relays
# On a client behind the segment:
dhclient -v eth0 # acquire
DHCPServerStatus reports whether the socket is listening, the mirrored
subnet/gateway/server-id, leasesUsed/leasesFree, cumulative
offers/acks/naks/declines, and the last error. The same counters are exposed to
the metrics plane.
In a fleet.yaml
The examples above are the raw COSI resources. If you render your config from a
fleet.yaml with sihactl gen config, you declare the bridge and tap as
explicit entries in network.interfaces, and the DHCP server under
dhcp.servers. The server itself declares which bridge and tap to use:
appliances:
- name: fw1
endpoint: "192.0.2.10:6443"
network:
interfaces:
- name: lan-nic
pci: "0000:06:00.0"
- name: lan
bridge:
members: [lan-nic]
bvi:
addresses: ["10.20.0.1/24"]
- name: dhcp-tap
tap:
hostDevice: dhcplan
hostAddresses: ["10.20.0.2/24"]
l2Bridge: lan
dhcp:
servers:
- name: lan
bridge: lan
serverTap: dhcp-tap
subnet: 10.20.0.0/24
gateway: 10.20.0.1
pool: { start: 10.20.0.100, stop: 10.20.0.200 }
allowFrom: ["fa:16:3e"]
gen config renders this to exactly the resources shown above — the fleet.yaml
source is a convenience, not a different runtime. The bridge stays a first-class
interface (bind ACLs / NAT / VRRP to it). The same guardrails apply at gen time:
a missing bridge, a serverTap that is not a tap attached to the declared
bridge, a gateway that is not the BVI address, an empty allowFrom, or a name
clash are rejected before you apply.
On a cloud fabric
The serve model needs the segment’s fabric to let SiHA’s DHCP traffic through. On a cloud (or any filtered virtual bridge), that means the served network must have anti-spoofing disabled on its ports (or SiHA’s pool CIDR in the allowed address pairs) and no second DHCP server on it — otherwise the fabric drops SiHA’s replies as rogue-DHCP, or a second server races it. An isolated L2 segment with those two settings is all it takes; nothing else is required. Relay is less fabric-sensitive — it just needs the upstream servers to be routable.
Serve is validated end-to-end (acquire + renew) both in the lab and on a public cloud.