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.