the setup
when you run a command like:
ovn-nbctl lsp-set-dhcpv4-options sw0-port0 dhcpv4optsyou’re telling OVN: “this logical port should have native DHCPv4 service, using the options defined in the dhcpv4opts DHCP_Options record.” the entire DHCP machinery — flow generation, packet construction, option encoding — kicks off from this single database reference.
the DHCP implementation in OVN spans the full stack:
- NB Database stores the DHCP configuration (option values, CIDR, server parameters)
- Northd syncs option definitions to the SB database and generates logical flows
- OVN Controller reads option definitions and handles DHCP packet-Ins via pinctrl
- OVS In-Band ensures DHCP replies from the local management port aren’t blocked by flow tables
i’m going to trace each layer. buckle up.
the nb database
DHCP options table
the NB database schema defines the DHCP_Options table (ovn/ovn-nb.ovsschema:711-719):
"DHCP_Options": { "columns": { "cidr": {"type": "string"}, "options": {"type": {"key": "string", "value": "string", "min": 0, "max": "unlimited"}}, "external_ids": { "type": {"key": "string", "value": "string", "min": 0, "max": "unlimited"}}}, "isRoot": true},three columns:
cidr: The subnet CIDR that these options apply to (e.g.,"10.0.0.0/24")options: A string→string map of DHCP option name/value pairs (e.g.,"server_id" → "10.0.0.1","server_mac" → "00:00:00:00:00:01")external_ids: User-defined metadata
DHCP relay table
for DHCP relay scenarios, the NB schema defines DHCP_Relay (ovn/ovn-nb.ovsschema:720-731):
"DHCP_Relay": { "columns": { "name": {"type": "string"}, "servers": {"type": {"key": "string", "min": 0, "max": 1}}, "options": {"type": {"key": "string", "value": "string", "min": 0, "max": "unlimited"}}, "external_ids": { "type": {"key": "string", "value": "string", "min": 0, "max": "unlimited"}}}, "isRoot": true},the servers column holds a single string — the IP address (and optionally port) of the remote DHCP server to relay to.
how ports reference DHCP configuration
logical switch ports reference DHCP options via weak UUID references (ovn/ovn-nb.ovsschema:161-170):
"dhcpv4_options": {"type": {"key": {"type": "uuid", "refTable": "DHCP_Options", "refType": "weak"}, "min": 0, "max": 1}},"dhcpv6_options": {"type": {"key": {"type": "uuid", "refTable": "DHCP_Options", "refType": "weak"}, "min": 0, "max": 1}},both are optional (min 0) and single-valued (max 1). they’re weak references — if the DHCP_Options record is deleted, the reference becomes NULL rather than cascading a delete.
logical router ports reference DHCP relay via a strong UUID reference (ovn/ovn-nb.ovsschema:597-601):
"dhcp_relay": {"type": {"key": {"type": "uuid", "refTable": "DHCP_Relay", "refType": "strong"}, "min": 0, "max": 1}},and the logical switch itself has a runtime key dhcp_relay_port in its other_config map that identifies which switch port connects to the router performing relay:
"other_config": { "type": {"key": "string", "value": "string", "min": 0, "max": "unlimited"}},this is set via ovn-nbctl set logical_switch <ls> other_config:dhcp_relay_port=<port>.
northd: syncing dhcp options to sb
the ovn-northd daemon maintains the SB database’s DHCP option definition tables. this ensures that every hypervisor knows the canonical set of DHCP options and their wire formats.
the built-in option tables
northd defines two static arrays of supported options in ovn/northd/ovn-northd.c:244-293:
DHCPv4 options (supported_dhcp_opts[], 39 entries):
static struct gen_opts_map supported_dhcp_opts[] = { OFFERIP, // "offerip", code 0, type "ipv4" DHCP_OPT_NETMASK, // "netmask", code 1, type "ipv4" DHCP_OPT_ROUTER, // "router", code 3, type "ipv4" DHCP_OPT_DNS_SERVER, // "dns_server", code 6, type "ipv4" DHCP_OPT_LOG_SERVER, // "log_server", code 7, type "ipv4" DHCP_OPT_LPR_SERVER, // "lpr_server", code 9, type "ipv4" DHCP_OPT_SWAP_SERVER, // "swap_server", code 16, type "ipv4" DHCP_OPT_POLICY_FILTER, // "policy_filter", code 21, type "ipv4" DHCP_OPT_ROUTER_SOLICITATION, // "router_solicitation", code 32, type "ipv4" DHCP_OPT_NIS_SERVER, // "nis_server", code 41, type "ipv4" DHCP_OPT_NTP_SERVER, // "ntp_server", code 42, type "ipv4" DHCP_OPT_SERVER_ID, // "server_id", code 54, type "ipv4" DHCP_OPT_TFTP_SERVER, // "tftp_server", code 66, type "host_id" DHCP_OPT_CLASSLESS_STATIC_ROUTE,// "classless_static_route", code 121, type "static_routes" DHCP_OPT_MS_CLASSLESS_STATIC_ROUTE, // "ms_classless_static_route", code 249, type "static_routes" DHCP_OPT_IP_FORWARD_ENABLE, // "ip_forward_enable", code 19, type "bool" DHCP_OPT_ROUTER_DISCOVERY, // "router_discovery", code 31, type "bool" DHCP_OPT_ETHERNET_ENCAP, // "ethernet_encap", code 36, type "bool" DHCP_OPT_DEFAULT_TTL, // "default_ttl", code 23, type "uint8" DHCP_OPT_TCP_TTL, // "tcp_ttl", code 37, type "uint8" DHCP_OPT_MTU, // "mtu", code 26, type "uint16" DHCP_OPT_LEASE_TIME, // "lease_time", code 51, type "uint32" DHCP_OPT_T1, // "T1", code 58, type "uint32" DHCP_OPT_T2, // "T2", code 59, type "uint32" DHCP_OPT_WPAD, // "wpad", code 252, type "str" DHCP_OPT_BOOTFILE, // "bootfile_name", code 67, type "str" DHCP_OPT_PATH_PREFIX, // "path_prefix", code 210, type "str" DHCP_OPT_TFTP_SERVER_ADDRESS, // "tftp_server_address", code 150, type "ipv4" DHCP_OPT_HOSTNAME, // "hostname", code 12, type "str" DHCP_OPT_DOMAIN_NAME, // "domain_name", code 15, type "str" DHCP_OPT_ARP_CACHE_TIMEOUT, // "arp_cache_timeout", code 35, type "uint32" DHCP_OPT_TCP_KEEPALIVE_INTERVAL,// "tcp_keepalive_interval", code 38, type "uint32" DHCP_OPT_DOMAIN_SEARCH_LIST, // "domain_search_list", code 119, type "domains" DHCP_OPT_BOOTFILE_ALT, // "bootfile_name_alt", code 254, type "str" DHCP_OPT_BROADCAST_ADDRESS, // "broadcast_address", code 28, type "ipv4" DHCP_OPT_NETBIOS_NAME_SERVER, // "netbios_name_server", code 44, type "ipv4" DHCP_OPT_NETBIOS_NODE_TYPE, // "netbios_node_type", code 46, type "uint8" DHCP_OPT_NEXT_SERVER, // "next_server", code 253, type "ipv4"};each entry is a struct gen_opts_map defined in ovn/lib/ovn-l7.h:57-62:
struct gen_opts_map { struct hmap_node hmap_node; char *name; char *type; size_t code;};the macros like DHCP_OPT_NETMASK expand via the DHCP_OPTION macro (ovn/lib/ovn-l7.h:77-78):
#define DHCP_OPTION(NAME, CODE, TYPE) \ {.name = NAME, .code = CODE, .type = TYPE}
#define DHCP_OPT_NETMASK DHCP_OPTION("netmask", 1, "ipv4")DHCPv6 options (supported_dhcpv6_opts[], 7 entries):
static struct gen_opts_map supported_dhcpv6_opts[] = { DHCPV6_OPT_IA_ADDR, // "ia_addr", code 5, type "ipv6" DHCPV6_OPT_SERVER_ID, // "server_id", code 2, type "mac" DHCPV6_OPT_DOMAIN_SEARCH, // "domain_search", code 24, type "str" DHCPV6_OPT_DNS_SERVER, // "dns_server", code 23, type "ipv6" DHCPV6_OPT_BOOTFILE_NAME, // "bootfile_name", code 59, type "str" DHCPV6_OPT_BOOTFILE_NAME_ALT,// "bootfile_name_alt", code 254, type "str" DHCPV6_OPT_FQDN, // "fqdn", code 39, type "domain"};the sync function
the function check_and_add_supported_dhcp_opts_to_sb_db() (ovn/northd/ovn-northd.c:448-485) performs a full reconciliation:
static voidcheck_and_add_supported_dhcp_opts_to_sb_db(struct ovsdb_idl_txn *ovnsb_txn, struct ovsdb_idl *ovnsb_idl){ struct hmap dhcp_opts_to_add = HMAP_INITIALIZER(&dhcp_opts_to_add); for (size_t i = 0; (i < sizeof(supported_dhcp_opts) / sizeof(supported_dhcp_opts[0])); i++) { hmap_insert(&dhcp_opts_to_add, &supported_dhcp_opts[i].hmap_node, dhcp_opt_hash(supported_dhcp_opts[i].name)); }
const struct sbrec_dhcp_options *opt_row; SBREC_DHCP_OPTIONS_FOR_EACH_SAFE (opt_row, ovnsb_idl) { struct gen_opts_map *dhcp_opt = dhcp_opts_find(&dhcp_opts_to_add, opt_row->name); if (dhcp_opt) { if (!strcmp(dhcp_opt->type, opt_row->type) && dhcp_opt->code == opt_row->code) { hmap_remove(&dhcp_opts_to_add, &dhcp_opt->hmap_node); } else { sbrec_dhcp_options_delete(opt_row); /* Type/code mismatch → delete */ } } else { sbrec_dhcp_options_delete(opt_row); /* Unknown option → delete */ } }
struct gen_opts_map *opt; HMAP_FOR_EACH (opt, hmap_node, &dhcp_opts_to_add) { struct sbrec_dhcp_options *sbrec_dhcp_option = sbrec_dhcp_options_insert(ovnsb_txn); sbrec_dhcp_options_set_name(sbrec_dhcp_option, opt->name); sbrec_dhcp_options_set_code(sbrec_dhcp_option, opt->code); sbrec_dhcp_options_set_type(sbrec_dhcp_option, opt->type); }
hmap_destroy(&dhcp_opts_to_add);}the algorithm is straightforward:
- build an hmap of all built-in options
- walk existing SB rows — if an option matches (name + code + type), remove it from the “to add” map; if it mismatches or is unknown, delete it
- insert any remaining options from the “to add” map as new SB rows
the SB schema for these tables (ovn/ovn-sb.ovsschema:305-328):
"DHCP_Options": { "columns": { "name": {"type": "string"}, "code": {"type": {"key": {"type": "integer", "minInteger": 0, "maxInteger": 254}}}, "type": {"type": {"key": {"type": "string", "enum": ["set", ["bool", "uint8", "uint16", "uint32", "ipv4", "static_routes", "str", "host_id", "domains"]]}}}}, "isRoot": true},
"DHCPv6_Options": { "columns": { "name": {"type": "string"}, "code": {"type": {"key": {"type": "integer", "minInteger": 0, "maxInteger": 254}}}, "type": {"type": {"key": {"type": "string", "enum": ["set", ["ipv6", "str", "mac", "domain"]]}}}}, "isRoot": true},northd: generating logical flows for dhcp
this is where the real work happens. northd reads the DHCP configuration from the NB database and generates logical flows that will instruct ovn-controller how to handle DHCP packets.
pipeline stage architecture
DHCP-related pipeline stages are defined in ovn/northd/northd.h:548-605:
Logical Switch Ingress Pipeline:
| Stage | Name | Purpose |
|---|---|---|
| 28 | ls_in_dhcp_options | Match DHCP packets, encode put_dhcp_opts action |
| 29 | ls_in_dhcp_response | If put_dhcp_opts succeeded, rewrite L2/L3 headers and output |
Logical Router Ingress Pipeline (for DHCP Relay):
| Stage | Name | Purpose |
|---|---|---|
| 4 | lr_in_dhcp_relay_req | Rewrite src/dst IPs, forward DHCP request to server |
| 20 | lr_in_dhcp_relay_resp_chk | Validate DHCP relay response from server |
| 21 | lr_in_dhcp_relay_resp | Rewrite headers and deliver response to client |
register bits for DHCP state
the DHCP flow generation uses specific register bits to track state across pipeline stages (ovn/northd/northd.c:126,209-210,230):
#define REGBIT_DHCP_OPTS_RESULT "reg0[3]"#define REGBIT_DHCP_RELAY_REQ_CHK "reg9[7]"#define REGBIT_DHCP_RELAY_RESP_CHK "reg9[8]"#define REG_DHCP_RELAY_DIP_IPV4 "reg2"reg0[3](REGBIT_DHCP_OPTS_RESULT): Set to 1 by theput_dhcp_opts/put_dhcpv6_optsaction if the option encoding succeeds. The next stage (ls_in_dhcp_response) checks this bit to decide whether to generate the response packet.reg9[7](REGBIT_DHCP_RELAY_REQ_CHK): Set bydhcp_relay_req_chkaction on the router if the relay request is valid.reg9[8](REGBIT_DHCP_RELAY_RESP_CHK): Set bydhcp_relay_resp_chkaction on the router if the relay response is valid.reg2(REG_DHCP_RELAY_DIP_IPV4): Stores the original destination IP from the relay response for matching in the next stage.
the entry point
the function build_lswitch_dhcp_options_and_response() (ovn/northd/northd.c:10958-11008) is called for each logical switch port during the build_ls_lflows() pass:
static voidbuild_lswitch_dhcp_options_and_response(struct ovn_port *op, struct lflow_table *lflows, const struct shash *meter_groups){ ovs_assert(op->nbsp); if (!lsp_is_enabled(op->nbsp) || lsp_is_router(op->nbsp)) { return; }
if (!op->nbsp->dhcpv4_options && !op->nbsp->dhcpv6_options) { return; }
if (ls_dhcp_relay_port(op->od)) { /* Don't add DHCP server flows if DHCP Relay is enabled */ return; }
bool is_external = lsp_is_external(op->nbsp); if (is_external && (!ls_has_localnet_port(op->od) || !op->nbsp->ha_chassis_group)) { return; }
for (size_t i = 0; i < op->n_lsp_addrs; i++) { build_dhcpv4_options_flows(op, &op->lsp_addrs[i], op, is_external, meter_groups, lflows, op->lflow_ref); build_dhcpv6_options_flows(op, &op->lsp_addrs[i], op, is_external, meter_groups, lflows, op->lflow_ref); }}three guards before any DHCP flows are generated:
- the port must be enabled and not a router port
- the port must have DHCP options configured (
dhcpv4_optionsordhcpv6_options) - DHCP relay must NOT be enabled on this switch (checked via
ls_dhcp_relay_port())
the helper ls_dhcp_relay_port() (ovn/northd/northd.c:10079-10083) simply reads the other_config:dhcp_relay_port key:
static const char *ls_dhcp_relay_port(const struct ovn_datapath *od){ return smap_get(&od->nbs->other_config, "dhcp_relay_port");}building the DHCPv4 action string
the function build_dhcpv4_action() (ovn/northd/northd.c:5956-6040) constructs the action strings that will become logical flow actions:
static boolbuild_dhcpv4_action(struct ovn_port *op, ovs_be32 offer_ip, struct ds *options_action, struct ds *response_action, struct ds *ipv4_addr_match){ if (!op->nbsp->dhcpv4_options) { return false; }
ovs_be32 host_ip, mask; char *error = ip_parse_masked(op->nbsp->dhcpv4_options->cidr, &host_ip, &mask); if (error || ((offer_ip ^ host_ip) & mask)) { free(error); return false; }
const char *server_ip = smap_get( &op->nbsp->dhcpv4_options->options, "server_id"); const char *server_mac = smap_get( &op->nbsp->dhcpv4_options->options, "server_mac"); const char *lease_time = smap_get( &op->nbsp->dhcpv4_options->options, "lease_time");
if (!(server_ip && server_mac && lease_time)) { return false; } /* ... */three options are mandatory: server_id, server_mac, and lease_time. if any is missing, the function returns false and no DHCP flows are generated for this port. no server_mac? no DHCP. simple as that.
the function then builds two action strings:
the options action (executed in ls_in_dhcp_options):
ds_put_format(options_action, REGBIT_DHCP_OPTS_RESULT" = put_dhcp_opts(offerip = " IP_FMT", ", IP_ARGS(offer_ip)); /* ... sorted option encoding ... */ ds_put_cstr(options_action, "); next;");the response action (executed in ls_in_dhcp_response):
ds_put_format(response_action, "eth.dst = eth.src; eth.src = %s; " "ip4.src = %s; udp.src = 67; udp.dst = 68; " "outport = inport; flags.loopback = 1; output;", server_mac, server_ip);the response action swaps MAC addresses (server MAC as src, client MAC as dst), sets the source IP to the server IP, swaps UDP ports (src=67, dst=68), and outputs the packet back out the ingress port with the loopback flag set.
the generated logical flows
the build_dhcpv4_options_flows() function (ovn/northd/northd.c:9900-9995) generates three flows per port:
Flow 1: DHCP options encoding (stage ls_in_dhcp_options, priority 100):
match: "inport == <port> && eth.src == <mac> && (ip4.src == {<offer_ip>, 0.0.0.0} && ip4.dst == {<server_ip>, 255.255.255.255}) && udp.src == 68 && udp.dst == 67"action: "reg0[3] = put_dhcp_opts(offerip = <offer_ip>, netmask = <mask>, router = <router>, ...); next;"this flow matches DHCP requests (DISCOVER/REQUEST) from the client. the match on ip4.src handles both initial requests (src=0.0.0.0) and renewals (src=offered IP). the match on ip4.dst handles both directed requests (dst=server_ip) and broadcasts (dst=255.255.255.255).
the flow also includes a CoPP meter reference via WITH_CTRL_METER(copp_meter_get(COPP_DHCPV4_OPTS, ...)) and a hint via WITH_HINT(&op->nbsp->dhcpv4_options->header_).
Flow 2: DHCP response generation (stage ls_in_dhcp_response, priority 100):
match: "inport == <port> && eth.src == <mac> && ip4 && udp.src == 68 && udp.dst == 67 && reg0[3]"action: "eth.dst = eth.src; eth.src = <server_mac>; ip4.src = <server_ip>; udp.src = 67; udp.dst = 68; outport = inport; flags.loopback = 1; output;"this flow fires only if reg0[3] (REGBIT_DHCP_OPTS_RESULT) was set to 1 by the previous stage — meaning the put_dhcp_opts action succeeded.
Flow 3: ACL bypass for DHCP replies (stage S_SWITCH_OUT_ACL_EVAL, priority 34000):
match: "outport == <port> && eth.src == <server_mac> && ip4.src == <server_ip> && udp && udp.src == 67 && udp.dst == 68"action: "reg1[0] = 1; next;"this is a high-priority ACL flow that ensures DHCP replies from ovn-controller bypass any drop ACLs. it sets REGBIT_ACL_VERDICT_ALLOW (reg1[0]) to 1, allowing the packet to proceed through the ACL pipeline without being dropped. this is generated only for non-external ports.
the DHCPv6 equivalent (build_dhcpv6_options_flows, ovn/northd/northd.c:9997-10077) follows the same pattern but matches on multicast destination ff02::1:2 and UDP ports 546/547.
default flows
the function build_lswitch_dhcp_and_dns_defaults() (ovn/northd/northd.c:11016-11032) installs priority-0 default flows:
ovn_lflow_add(lflows, od, S_SWITCH_IN_DHCP_OPTIONS, 0, "1", "next;", lflow_ref);ovn_lflow_add(lflows, od, S_SWITCH_IN_DHCP_RESPONSE, 0, "1", "next;", lflow_ref);these ensure that non-DHCP packets pass through these stages without being dropped.
DHCP relay on the logical switch
when DHCP relay is enabled, the function build_lswitch_dhcp_relay_flows() (ovn/northd/northd.c:10085-10162) generates a flow at stage S_SWITCH_IN_L2_LKUP (priority 100) that redirects DHCP requests to the router port:
ds_put_format( match, "inport == %s && eth.src == %s && " "ip4.src == {0.0.0.0, %s/%u} && ip4.dst == 255.255.255.255 && " "udp.src == 68 && udp.dst == 67", op->json_key, op->lsp_addrs[0].ea_s, rp->lrp_networks.ipv4_addrs[0].network_s, rp->lrp_networks.ipv4_addrs[0].plen);ds_put_format(actions, "eth.dst = %s; outport = %s; next; /* DHCP_RELAY_REQ */", rp->lrp_networks.ea_s, sp->json_key);this rewrites the destination MAC to the router port’s MAC and outputs to the router port, effectively forwarding the DHCP request into the router pipeline.
DHCP relay on the logical router
the function build_dhcp_relay_flows_for_lrouter_port() (ovn/northd/northd.c:16815-16980) generates eight flows across three router stages. here’s the complete relay flow sequence:
request path (client → server):
Flow 1 (lr_in_ip_input, priority 110) — validate relay request:
match: "inport == <router_port> && ip4.src == {0.0.0.0, <network>/<prefix>} && ip4.dst == 255.255.255.255 && ip.frag == 0 && udp.src == 68 && udp.dst == 67"action: "reg9[7] = dhcp_relay_req_chk(<relay_ip>, <server_ip>); next;"Flow 2 (lr_in_dhcp_relay_req, priority 100) — forward valid request:
match: "inport == <router_port> && ip4.src == {0.0.0.0, <network>/<prefix>} && ip4.dst == 255.255.255.255 && udp.src == 68 && udp.dst == 67 && reg9[7]"action: "ip4.src = <relay_ip>; ip4.dst = <server_ip>; udp.src = 67; next;"Flow 3 (lr_in_dhcp_relay_req, priority 1) — drop invalid request:
match: "inport == <router_port> && ip4.src == {0.0.0.0, <network>/<prefix>} && ip4.dst == 255.255.255.255 && udp.src == 68 && udp.dst == 67 && reg9[7] == 0"action: "drop;"response path (server → client):
Flow 4 (lr_in_ip_input, priority 110) — match non-fragmented response:
match: "ip4.src == <server_ip> && ip4.dst == <relay_ip> && ip.frag == 0 && udp.src == 67 && udp.dst == 67"action: "next;"Flow 5 (lr_in_dhcp_relay_resp_chk, priority 100) — validate relay response:
match: "ip4.src == <server_ip> && ip4.dst == <relay_ip> && udp.src == 67 && udp.dst == 67"action: "reg2 = ip4.dst; reg9[8] = dhcp_relay_resp_chk(<relay_ip>, <server_ip>); next;"Flow 6 (lr_in_dhcp_relay_resp, priority 100) — deliver valid response:
match: "ip4.src == <server_ip> && reg2 == <relay_ip> && udp.src == 67 && udp.dst == 67 && reg9[8]"action: "ip4.src = <relay_ip>; udp.dst = 68; outport = <router_port>; output;"Flow 7 (lr_in_dhcp_relay_resp, priority 1) — drop invalid response:
match: "ip4.src == <server_ip> && reg2 == <relay_ip> && udp.src == 67 && udp.dst == 67 && reg9[8] == 0"action: "drop;"Flow 8 — default passthrough for each relay stage (priority 0):
"1" → "next;"the ovn action system: how put_dhcp_opts becomes openflow
the action opcodes
when northd generates a logical flow with put_dhcp_opts(...), this action must be encoded into an OpenFlow controller message. the action system defines five DHCP-related opcodes in ovn/include/ovn/actions.h:663-838:
| Opcode | Purpose | Userdata Format |
|---|---|---|
ACTION_OPCODE_PUT_DHCP_OPTS | DHCPv4 option encoding | OXM header + offset + offer_ip + DHCP options |
ACTION_OPCODE_PUT_DHCPV6_OPTS | DHCPv6 option encoding | OXM header + offset + DHCPv6 options |
ACTION_OPCODE_DHCP6_SERVER | DHCPv6 server-side handling | (actions in OF1.3 format) |
ACTION_OPCODE_DHCP_RELAY_REQ_CHK | DHCP relay request validation | relay_ip + server_ip |
ACTION_OPCODE_DHCP_RELAY_RESP_CHK | DHCP relay response validation | relay_ip + server_ip |
each opcode is preceded by an action_header (ovn/include/ovn/actions.h:854-858):
struct action_header { ovs_be32 opcode; /* One of ACTION_OPCODE_* */ uint8_t pad[4];};BUILD_ASSERT_DECL(sizeof(struct action_header) == 8);parsing DHCP actions
when ovn-controller processes a logical flow, it parses the action string via lflow_parse_actions() (ovn/controller/lflow.c:365-408):
static boollflow_parse_actions(const struct sbrec_logical_flow *lflow, const struct lflow_ctx_in *l_ctx_in, struct sset *template_vars_ref, struct ofpbuf *ovnacts_out, struct expr **prereqs_out){ bool ingress = !strcmp(lflow->pipeline, "ingress"); struct ovnact_parse_params pp = { .symtab = &symtab, .dhcp_opts = l_ctx_in->dhcp_opts, .dhcpv6_opts = l_ctx_in->dhcpv6_opts, .nd_ra_opts = l_ctx_in->nd_ra_opts, .controller_event_opts = l_ctx_in->controller_event_opts, .pipeline = ingress ? OVNACT_P_INGRESS : OVNACT_P_EGRESS, .n_tables = ingress ? LOG_PIPELINE_INGRESS_LEN : LOG_PIPELINE_EGRESS_LEN, .cur_ltable = lflow->table_id, }; /* ... */ char *error = ovnacts_parse_string(lex_str_get(&actions_s), &pp, ovnacts_out, prereqs_out);the ovnact_parse_params struct (ovn/include/ovn/actions.h:860-898) carries dhcp_opts and dhcpv6_opts as const struct hmap * — these are the option definition maps loaded from the SB database. during parsing, when the parser encounters put_dhcp_opts(offerip = ..., netmask = ..., ...), it looks up each option name (like "netmask") in the dhcp_opts hmap to resolve its DHCP option code and type.
the parser entry point for put_dhcp_opts is in parse_set_action() (ovn/lib/actions.c:5920-5927):
} else if (!strcmp(ctx->lexer->token.s, "put_dhcp_opts") && lexer_lookahead(ctx->lexer) == LEX_T_LPAREN) { parse_put_dhcp_opts(ctx, &lhs, ovnact_put_PUT_DHCPV4_OPTS( ctx->ovnacts));} else if (!strcmp(ctx->lexer->token.s, "put_dhcpv6_opts") && lexer_lookahead(ctx->lexer) == LEX_T_LPAREN) { parse_put_dhcp_opts(ctx, &lhs, ovnact_put_PUT_DHCPV6_OPTS( ctx->ovnacts));the parse_put_dhcp_opts() function (ovn/lib/actions.c:2976-2995) selects the appropriate option map based on the action type:
static voidparse_put_dhcp_opts(struct action_context *ctx, const struct expr_field *dst, struct ovnact_put_opts *po){ const struct hmap *dhcp_opts = (po->ovnact.type == OVNACT_PUT_DHCPV6_OPTS) ? ctx->pp->dhcpv6_opts : ctx->pp->dhcp_opts; const char *opts_type = (po->ovnact.type == OVNACT_PUT_DHCPV6_OPTS) ? "DHCPv6" : "DHCPv4";
parse_put_opts(ctx, dst, po, dhcp_opts, opts_type);
if (!ctx->lexer->error && po->ovnact.type == OVNACT_PUT_DHCPV4_OPTS && !find_opt(po->options, po->n_options, 0)) { lexer_error(ctx->lexer, "put_dhcp_opts requires offerip to be specified."); return; }}critical validation: DHCPv4 put_dhcp_opts requires offerip (option code 0) to be present. without it, parsing fails. makes sense — what’s a DHCP offer without an IP to offer?
encoding DHCP actions into openflow
the struct ovnact_put_opts (ovn/include/ovn/actions.h:406-411) stores the parsed options:
struct ovnact_put_opts { struct ovnact ovnact; struct expr_field dst; /* 1-bit destination field. */ struct ovnact_gen_option *options; size_t n_options;};each option is an ovnact_gen_option (ovn/include/ovn/actions.h:400-403):
struct ovnact_gen_option { const struct gen_opts_map *option; struct expr_constant_set value;};the encoding function encode_PUT_DHCPV4_OPTS() (ovn/lib/actions.c:3228-3292) converts the parsed options into an OpenFlow controller message:
static voidencode_PUT_DHCPV4_OPTS(const struct ovnact_put_opts *pdo, const struct ovnact_encode_params *ep, struct ofpbuf *ofpacts){ struct mf_subfield dst = expr_resolve_field(&pdo->dst);
size_t oc_offset = encode_start_controller_op(ACTION_OPCODE_PUT_DHCP_OPTS, true, ep->ctrl_meter_id, ofpacts); nx_put_header(ofpacts, dst.field->id, OFP15_VERSION, false); ovs_be32 ofs = htonl(dst.ofs); ofpbuf_put(ofpacts, &ofs, sizeof ofs);
/* Encode offerip first — it must be first in the DHCP response */ const struct ovnact_gen_option *offerip_opt = find_opt( pdo->options, pdo->n_options, 0); const struct expr_constant *c = vector_get_ptr(&offerip_opt->value.values, 0); ovs_be32 offerip = c->value.ipv4; ofpbuf_put(ofpacts, &offerip, sizeof offerip);
/* Encode bootfile_name (67), bootfile_name_alt (254), next_server (253) * in that specific order, then encode everything else */ /* ... */
for (size_t i = 0; i < pdo->n_options; i++) { const struct ovnact_gen_option *o = &pdo->options[i]; if (o != offerip_opt && o != boot_opt && o != boot_alt_opt && o != next_server_opt) { encode_put_dhcpv4_option(o, ofpacts); } }
encode_finish_controller_op(oc_offset, ofpacts);}the encoding order matters:
- Offer IP (always first)
- bootfile_name (option 67) — for PXE/iPXE boot
- bootfile_name_alt (option 254) — iPXE alternate bootfile
- next_server (option 253) — TFTP server address
- All other options — in the order they were defined
the final controller message format is:
[action_header: 8 bytes (opcode=PUT_DHCP_OPTS + padding)][OXM header: 4-8 bytes (field MFF_REG0, offset 3)][offer_ip: 4 bytes][DHCP option: code + len + value][DHCP option: code + len + value]...option encoding by type
the function encode_put_dhcpv4_option() (ovn/lib/actions.c:3026-3192) handles each option type differently:
static voidencode_put_dhcpv4_option(const struct ovnact_gen_option *o, struct ofpbuf *ofpacts){ uint8_t *opt_header = ofpbuf_put_zeros(ofpacts, 2); opt_header[0] = o->option->code;
const struct expr_constant *c = vector_get_ptr(&o->value.values, 0); size_t n_values = vector_len(&o->value.values); if (!strcmp(o->option->type, "bool") || !strcmp(o->option->type, "uint8")) { opt_header[1] = 1; ofpbuf_put(ofpacts, &c->value.u8_val, 1); } else if (!strcmp(o->option->type, "uint16")) { opt_header[1] = 2; ofpbuf_put(ofpacts, &c->value.be16_int, 2); } else if (!strcmp(o->option->type, "uint32")) { opt_header[1] = 4; ofpbuf_put(ofpacts, &c->value.be32_int, 4); } else if (!strcmp(o->option->type, "ipv4")) { opt_header[1] = n_values * sizeof(ovs_be32); for (size_t i = 0; i < n_values; i++) { ofpbuf_put(ofpacts, &c[i].value.ipv4, sizeof(ovs_be32)); } } else if (!strcmp(o->option->type, "static_routes")) { /* RFC 3442 classless static route encoding */ /* ... complex encoding with plen + destination + router ... */ } else if (!strcmp(o->option->type, "str")) { opt_header[1] = strlen(c->string); ofpbuf_put(ofpacts, c->string, opt_header[1]); } else if (!strcmp(o->option->type, "host_id")) { /* Can be string or IPv4 */ if (o->value.type == EXPR_C_STRING) { opt_header[1] = strlen(c->string); ofpbuf_put(ofpacts, c->string, opt_header[1]); } else { opt_header[1] = sizeof(ovs_be32); ofpbuf_put(ofpacts, &c->value.ipv4, sizeof(ovs_be32)); } } else if (!strcmp(o->option->type, "domains")) { /* RFC 1035 DNS-encoded domain search list with compression pointers */ /* ... complex encoding ... */ }}the static_routes type deserves special attention — it encodes according to RFC 3442, with variable-length destination descriptors:
Destination descriptor examples: 0 → 0 (default route) 10.0.0.0/8 → 8.10 10.0.0.0/24 → 24.10.0.0 10.229.0.128/25 → 25.10.229.0.128the domains type encodes domain search lists using RFC 1035 label compression, where shared suffixes are replaced with two-byte pointers (0xC0xx) to avoid duplication.
DHCPv6 option encoding
the DHCPv6 encoding function encode_put_dhcpv6_option() (ovn/lib/actions.c:3194-3226) is simpler:
static voidencode_put_dhcpv6_option(const struct ovnact_gen_option *o, struct ofpbuf *ofpacts){ struct dhcpv6_opt_header *opt = ofpbuf_put_uninit(ofpacts, sizeof *opt); const struct expr_constant *c = vector_get_ptr(&o->value.values, 0); size_t n_values = vector_len(&o->value.values); size_t size;
opt->code = htons(o->option->code);
if (!strcmp(o->option->type, "ipv6")) { size = n_values * sizeof(struct in6_addr); opt->len = htons(size); for (size_t i = 0; i < n_values; i++) { ofpbuf_put(ofpacts, &c[i].value.ipv6, sizeof(struct in6_addr)); } } else if (!strcmp(o->option->type, "mac")) { size = sizeof(struct eth_addr); opt->len = htons(size); ofpbuf_put(ofpacts, &c->value.mac, size); } else if (!strcmp(o->option->type, "str")) { size = strlen(c->string); opt->len = htons(size); ofpbuf_put(ofpacts, c->string, size); } else if (!strcmp(o->option->type, "domain")) { char *encoded = encode_fqdn_string(c->string, &size); opt->len = htons(size); ofpbuf_put(ofpacts, encoded, size); free(encoded); }}DHCPv6 option headers use 16-bit code and length fields (vs. 8-bit for DHCPv4).
DHCP relay action encoding
the encode_dhcpv4_relay_chk() function (ovn/lib/actions.c:2883-2901) encodes the relay check action:
static voidencode_dhcpv4_relay_chk(const struct ovnact_dhcp_relay *dhcp_relay, const struct ovnact_encode_params *ep, struct ofpbuf *ofpacts, enum action_opcode dhcp_relay_opcode){ struct mf_subfield dst = expr_resolve_field(&dhcp_relay->dst); size_t oc_offset = encode_start_controller_op(dhcp_relay_opcode, true, ep->ctrl_meter_id, ofpacts); nx_put_header(ofpacts, dst.field->id, OFP15_VERSION, false); ovs_be32 ofs = htonl(dst.ofs); ofpbuf_put(ofpacts, &ofs, sizeof ofs); ofpbuf_put(ofpacts, &dhcp_relay->relay_ipv4, sizeof(dhcp_relay->relay_ipv4)); ofpbuf_put(ofpacts, &dhcp_relay->server_ipv4, sizeof(dhcp_relay->server_ipv4)); encode_finish_controller_op(oc_offset, ofpacts);}the userdata contains: the result field (OXM header + offset), the 32-bit relay IP, and the 32-bit server IP.
ovn controller: reading dhcp options from sb
the engine node
OVN controller uses an incremental processing engine. the dhcp_options engine node (ovn/controller/ovn-controller.c:3811-3862) reads the SB DHCP option tables:
struct ed_type_dhcp_options { struct hmap v4_opts; struct hmap v6_opts;};
static enum engine_node_stateen_dhcp_options_run(struct engine_node *node, void *data){ struct ed_type_dhcp_options *dhcp_opts = data;
const struct sbrec_dhcp_options_table *dhcp_table = EN_OVSDB_GET(engine_get_input("SB_dhcp_options", node));
const struct sbrec_dhcpv6_options_table *dhcpv6_table = EN_OVSDB_GET(engine_get_input("SB_dhcpv6_options", node));
dhcp_opts_clear(&dhcp_opts->v4_opts); dhcp_opts_clear(&dhcp_opts->v6_opts);
const struct sbrec_dhcp_options *dhcp_opt_row; SBREC_DHCP_OPTIONS_TABLE_FOR_EACH (dhcp_opt_row, dhcp_table) { dhcp_opt_add(&dhcp_opts->v4_opts, dhcp_opt_row->name, dhcp_opt_row->code, dhcp_opt_row->type); }
const struct sbrec_dhcpv6_options *dhcpv6_opt_row; SBREC_DHCPV6_OPTIONS_TABLE_FOR_EACH (dhcpv6_opt_row, dhcpv6_table) { dhcp_opt_add(&dhcp_opts->v6_opts, dhcpv6_opt_row->name, dhcpv6_opt_row->code, dhcpv6_opt_row->type); } return EN_UPDATED;}the engine graph connections (ovn/controller/ovn-controller.c:7075-7078):
engine_add_input(&en_dhcp_options, &en_sb_dhcp_options, NULL);engine_add_input(&en_dhcp_options, &en_sb_dhcpv6_options, NULL);engine_add_input(&en_lflow_output, &en_dhcp_options, NULL);en_sb_dhcp_options ──→ en_dhcp_options ──→ en_lflow_outputen_sb_dhcpv6_options ─┘the NULL handler means any change in either SB table triggers a full recompute.
feeding into logical flow processing
the init_lflow_ctx() function (ovn/controller/ovn-controller.c:3995-4035) passes the DHCP option maps into the logical flow context:
struct ed_type_dhcp_options *dhcp_opts = engine_get_input_data("dhcp_options", node);/* ... */l_ctx_in->dhcp_opts = &dhcp_opts->v4_opts;l_ctx_in->dhcpv6_opts = &dhcp_opts->v6_opts;these maps flow into ovnact_parse_params during action parsing, allowing the parser to resolve option names to codes.
ovn controller pinctrl: handling dhcp packet-ins
this is where the rubber meets the road. when a DHCP packet matches a logical flow with a put_dhcp_opts action, the packet is sent to ovn-controller as a packet-in. the pinctrl module handles it.
the dispatch table
the function process_packet_in() (ovn/controller/pinctrl.c:3746-3919) is the central dispatcher. it reads the opcode from the packet-in userdata and dispatches to the appropriate handler:
switch (ntohl(ah->opcode)) { /* ... */ case ACTION_OPCODE_DHCP_RELAY_REQ_CHK: pinctrl_handle_dhcp_relay_req_chk(swconn, &packet, &pin, &userdata, &continuation); break;
case ACTION_OPCODE_DHCP_RELAY_RESP_CHK: pinctrl_handle_dhcp_relay_resp_chk(swconn, &packet, &pin, &userdata, &continuation); break;
case ACTION_OPCODE_PUT_DHCP_OPTS: pinctrl_handle_put_dhcp_opts(swconn, &packet, &pin, &headers, &userdata, &continuation); break;
case ACTION_OPCODE_PUT_DHCPV6_OPTS: pinctrl_handle_put_dhcpv6_opts(swconn, &packet, &pin, &userdata, &continuation); break;
case ACTION_OPCODE_DHCP6_SERVER: ovs_mutex_lock(&pinctrl_mutex); pinctrl_handle_dhcp6_server(swconn, &headers, &packet, &pin.flow_metadata); ovs_mutex_unlock(&pinctrl_mutex); break; /* ... */}DHCP packet validation: dhcp_get_hdr_from_pkt
before handling any DHCP packet, the function dhcp_get_hdr_from_pkt() (ovn/controller/pinctrl.c:2112-2168) validates the packet structure:
static const struct dhcp_header *dhcp_get_hdr_from_pkt(struct dp_packet *pkt_in, const char **in_dhcp_pptr, const char *end){ *in_dhcp_pptr = dp_packet_get_udp_payload(pkt_in); if (*in_dhcp_pptr == NULL) { VLOG_WARN_RL(&rl, "DHCP: Invalid or incomplete DHCP packet received"); return NULL; }
const struct dhcp_header *dhcp_hdr = (const struct dhcp_header *) *in_dhcp_pptr; (*in_dhcp_pptr) += sizeof *dhcp_hdr; if (*in_dhcp_pptr > end) { VLOG_WARN_RL(&rl, "DHCP: Invalid or incomplete DHCP packet received, " "bad data length"); return NULL; }
if (dhcp_hdr->htype != 0x1) { VLOG_WARN_RL(&rl, "DHCP: Packet is recieved with " "unsupported hardware type"); return NULL; }
if (dhcp_hdr->hlen != 0x6) { VLOG_WARN_RL(&rl, "DHCP: Packet is recieved with " "unsupported hardware length"); return NULL; }
ovs_be32 magic_cookie = htonl(DHCP_MAGIC_COOKIE); if ((*in_dhcp_pptr) + sizeof magic_cookie > end || get_unaligned_be32((const void *) (*in_dhcp_pptr)) != magic_cookie) { VLOG_WARN_RL(&rl, "DHCP: Magic cookie not present in the DHCP packet"); return NULL; }
(*in_dhcp_pptr) += sizeof magic_cookie; return dhcp_hdr;}validation checks:
- UDP payload must exist
- DHCP header must fit within the packet
- Hardware type must be Ethernet (
0x1) - Hardware length must be 6 (
0x6, ETH_ADDR_LEN) - Magic cookie (
0x63825363) must be present after the header
DHCP option parsing: dhcp_parse_options
the function dhcp_parse_options() (ovn/controller/pinctrl.c:2180-2243) walks the DHCP options area and extracts the fields needed for decision-making:
struct parsed_dhcp_options { uint8_t dhcp_msg_type; ovs_be32 request_ip; ovs_be32 server_id; ovs_be32 netmask; ovs_be32 router_ip; bool ipxe_req;};
static struct parsed_dhcp_optionsdhcp_parse_options(const char **in_dhcp_pptr, const char *end){ struct parsed_dhcp_options parsed_dhcp_opts = {0}; while ((*in_dhcp_pptr) < end) { const struct dhcp_opt_header *in_dhcp_opt = (const struct dhcp_opt_header *) *in_dhcp_pptr; if (in_dhcp_opt->code == DHCP_OPT_END) { break; } if (in_dhcp_opt->code == DHCP_OPT_PAD) { (*in_dhcp_pptr) += 1; continue; } (*in_dhcp_pptr) += sizeof *in_dhcp_opt; if ((*in_dhcp_pptr) > end) { break; } (*in_dhcp_pptr) += in_dhcp_opt->len; if ((*in_dhcp_pptr) > end) { break; }
switch (in_dhcp_opt->code) { case DHCP_OPT_MSG_TYPE: /* code 53 */ if (in_dhcp_opt->len == 1) { const uint8_t *dhcp_msg_type = DHCP_OPT_PAYLOAD(in_dhcp_opt); parsed_dhcp_opts.dhcp_msg_type = *dhcp_msg_type; } break; case DHCP_OPT_REQ_IP: /* code 50 */ if (in_dhcp_opt->len == 4) { parsed_dhcp_opts.request_ip = get_unaligned_be32( DHCP_OPT_PAYLOAD(in_dhcp_opt)); } break; case OVN_DHCP_OPT_CODE_SERVER_ID: /* code 54 */ if (in_dhcp_opt->len == 4) { parsed_dhcp_opts.server_id = get_unaligned_be32( DHCP_OPT_PAYLOAD(in_dhcp_opt)); } break; case OVN_DHCP_OPT_CODE_NETMASK: /* code 1 */ if (in_dhcp_opt->len == 4) { parsed_dhcp_opts.netmask = get_unaligned_be32( DHCP_OPT_PAYLOAD(in_dhcp_opt)); } break; case OVN_DHCP_OPT_CODE_ROUTER_IP: /* code 3 */ if (in_dhcp_opt->len == 4) { parsed_dhcp_opts.router_ip = get_unaligned_be32( DHCP_OPT_PAYLOAD(in_dhcp_opt)); } break; case DHCP_OPT_ETHERBOOT: /* code 175 */ parsed_dhcp_opts.ipxe_req = true; break; default: break; } } return parsed_dhcp_opts;}the parser specifically extracts six fields because they’re needed for validation and decision-making downstream: message type, requested IP, server ID, netmask, router IP, and iPXE request flag.
DHCPv4: building the response packet
the function pinctrl_handle_put_dhcp_opts() (ovn/controller/pinctrl.c:2604-2944) is the core DHCPv4 handler — 340 lines of packet construction. i read every single one of them. here’s the control flow:
step 1: parse userdata
const struct mf_field *f;enum ofperr ofperr = nx_pull_header(userdata, NULL, &f, NULL);/* ... */ovs_be32 *ofsp = ofpbuf_try_pull(userdata, sizeof *ofsp);ovs_be32 *offer_ip = ofpbuf_try_pull(userdata, sizeof *offer_ip);the userdata contains the result field (where to write success/failure), the offer IP, and the encoded DHCP options.
step 2: parse the incoming DHCP packet
const struct dhcp_header *in_dhcp_data = dhcp_get_hdr_from_pkt(pkt_in, &in_dhcp_ptr, end);
if (in_dhcp_data->op != DHCP_OP_REQUEST) { VLOG_WARN_RL(&rl, "Invalid opcode in the DHCP packet: %d", in_dhcp_data->op); goto exit;}
struct parsed_dhcp_options dhcp_opts = dhcp_parse_options(&in_dhcp_ptr, end);if (!dhcp_opts.request_ip) { dhcp_opts.request_ip = in_dhcp_data->ciaddr;}if the requested IP (option 50) is not present, the handler falls back to ciaddr from the DHCP header.
step 3: determine response type
switch (dhcp_opts.dhcp_msg_type) {case DHCP_MSG_DISCOVER: msg_type = DHCP_MSG_OFFER; break;case DHCP_MSG_REQUEST: { msg_type = DHCP_MSG_ACK; if (dhcp_opts.request_ip != *offer_ip) { msg_type = DHCP_MSG_NAK; } break;}case OVN_DHCP_MSG_RELEASE: /* Just log it */ break;case OVN_DHCP_MSG_INFORM: { msg_type = DHCP_MSG_ACK; /* Strip lease/netmask/T1/T2 options from reply */ break;}case OVN_DHCP_MSG_DECLINE: /* Log and exit */ goto exit;}the DISCOVER→OFFER mapping is straightforward. for REQUEST, the handler validates that the requested IP matches the offer IP — if not, it sends a NAK. for INFORM (RFC 2131 section 3.4), the handler strips time-lease and netmask options from the reply.
step 4: handle iPXE bootfile selection
the handler processes the bootfile options with iPXE awareness:
case DHCP_OPT_BOOTFILE_CODE: /* Check if bootfile_name_alt (254) follows */ if (next_dhcp_opt->code == DHCP_OPT_BOOTFILE_ALT_CODE) { if (!dhcp_opts.ipxe_req) { /* Not iPXE: skip alt, use standard bootfile */ ofpbuf_pull(reply_dhcp_opts_ptr, len); next_dhcp_opt->code = DHCP_OPT_BOOTFILE_CODE; } else { /* iPXE: use alt bootfile, skip standard */ /* ... swap logic ... */ } }step 5: construct the reply packet
uint16_t new_l4_size = UDP_HEADER_LEN + DHCP_HEADER_LEN + 16;if (msg_type != DHCP_MSG_NAK) { new_l4_size += reply_dhcp_opts_ptr->size;}
struct dp_packet pkt_out;dp_packet_init(&pkt_out, new_packet_size);
/* Copy L2 and L3 headers from pkt_in */dp_packet_put(&pkt_out, dp_packet_pull(pkt_in, pkt_in->l4_ofs), pkt_in->l4_ofs);
struct udp_header *udp = dp_packet_put( &pkt_out, dp_packet_pull(pkt_in, UDP_HEADER_LEN), UDP_HEADER_LEN);
struct dhcp_header *dhcp_data = dp_packet_put( &pkt_out, dp_packet_pull(pkt_in, DHCP_HEADER_LEN), DHCP_HEADER_LEN);dhcp_data->op = DHCP_OP_REPLY;
if (dhcp_opts.dhcp_msg_type != OVN_DHCP_MSG_INFORM) { dhcp_data->yiaddr = (msg_type == DHCP_MSG_NAK) ? 0 : *offer_ip; dhcp_data->siaddr = (msg_type == DHCP_MSG_NAK) ? 0 : next_server;}the reply packet structure is:
┌──────────────────────────────────────────────────────────────┐│ L2 Header (copied from request) │├──────────────────────────────────────────────────────────────┤│ L3 Header (copied, with ip_dst updated) │├──────────────────────────────────────────────────────────────┤│ UDP Header (src=67, dst=68) │├──────────────────────────────────────────────────────────────┤│ DHCP Header (op=REPLY, yiaddr=offer_ip, siaddr=next_server) │├──────────────────────────────────────────────────────────────┤│ Magic Cookie (0x63825363) │├──────────────────────────────────────────────────────────────┤│ Option 53: Message Type (OFFER/ACK/NAK) │├──────────────────────────────────────────────────────────────┤│ [Encoded DHCP options from userdata] │├──────────────────────────────────────────────────────────────┤│ 4 bytes padding + Option END (0xFF) │└──────────────────────────────────────────────────────────────┘step 6: handle broadcast flag
ovs_be32 ip_dst;if (!is_dhcp_flags_broadcast(dhcp_data->flags)) { ip_dst = *offer_ip;} else { ip_dst = htonl(0xffffffff);}put_16aligned_be32(&out_ip->ip_dst, ip_dst);if the client set the broadcast flag, the response is sent as a broadcast (255.255.255.255); otherwise, it’s unicast to the offered IP.
step 7: set success and resume the pipeline
success = 1;exit: if (!ofperr) { union mf_subvalue sv; sv.u8_val = success; mf_write_subfield(&dst, &sv, &pin->flow_metadata); } queue_msg(swconn, ofputil_encode_resume(pin, continuation, proto));the handler writes 1 (success) or 0 (failure) into reg0[3] via the mf_write_subfield call, then resumes the logical pipeline with the modified packet.

DHCPv6: building the response packet
the function pinctrl_handle_put_dhcpv6_opts() (ovn/controller/pinctrl.c:3138-3364) follows a similar pattern but for DHCPv6:
message type mapping:
switch (hdr->msg_type) {case DHCPV6_MSG_TYPE_SOLICIT: out_dhcpv6_msg_type = DHCPV6_MSG_TYPE_ADVT; break;case DHCPV6_MSG_TYPE_REQUEST:case DHCPV6_MSG_TYPE_CONFIRM:case DHCPV6_MSG_TYPE_DECLINE:case DHCPV6_MSG_TYPE_INFO_REQ: out_dhcpv6_msg_type = DHCPV6_MSG_TYPE_REPLY; break;case DHCPV6_MSG_TYPE_RELEASE: out_dhcpv6_msg_type = DHCPV6_MSG_TYPE_REPLY; status_only = true; break;}parsing client options:
the handler extracts IA_NA (for IAID), Client ID, User Class (for iPXE detection), and FQDN flags from the client’s packet:
for (in_opt = next_dhcpv6_opt(DHCPV6_PAYLOAD(hdr), opts_len, &len, &opt_len); in_opt; in_opt = next_dhcpv6_opt(DHCPV6_PAYLOAD(hdr), opts_len, &len, &opt_len)) { switch (ntohs(in_opt->code)) { case DHCPV6_OPT_IA_NA_CODE: iaid = opt_ia_na->iaid; break; case DHCPV6_OPT_CLIENT_ID_CODE: in_opt_client_id = in_opt; break; case DHCPV6_OPT_USER_CLASS: if (!strncmp(user_class + DHCPV6_UC_PXE_OFFSET, "iPXE", 4)) { ipxe_req = true; } break; case DHCPV6_OPT_FQDN_CODE: fqdn_flags = *(uint8_t *) DHCPV6_OPT_PAYLOAD(in_opt); break; }}composing the response:
the handler delegates option composition to compose_out_dhcpv6_opts() (ovn/controller/pinctrl.c:2967-3098), which processes each option from the userdata:
- Server ID (code 2): Re-encoded with
encode_dhcpv6_server_id_opt() - IA Address (code 5): Wrapped in an IA_NA option with the client’s IAID, T1=T2=infinity
- DNS Server (code 23): Copied with header
- Domain Search (code 24): Copied with length prefix
- Boot File URL (code 59/254): Conditionally included based on iPXE detection
- FQDN (code 39): Flags always include N=1 (server-managed); if client sent S, O is also set
packet construction:
uint16_t new_l4_size = (UDP_HEADER_LEN + 4 + sizeof *in_opt_client_id + ntohs(in_opt_client_id->len) + out_dhcpv6_opts.size);
struct dp_packet pkt_out;/* Copy L2/L3 from pkt_in *//* Copy UDP header and DHCPv6 header (preserving transaction ID) */
*out_dhcpv6 = out_dhcpv6_msg_type; /* Set message type */
/* Copy Client Identifier */dp_packet_put(&pkt_out, in_opt_client_id, sizeof *in_opt_client_id + ntohs(in_opt_client_id->len));
/* Copy composed options */dp_packet_put(&pkt_out, out_dhcpv6_opts.data, out_dhcpv6_opts.size);
/* Compute UDP checksum */uint32_t csum = packet_csum_pseudoheader6(dp_packet_l3(&pkt_out));csum = csum_continue(csum, out_udp, dp_packet_size(&pkt_out) - ...);out_udp->udp_csum = csum_finish(csum);DHCPv6 prefix delegation handling
the function pinctrl_handle_dhcp6_server() (ovn/controller/pinctrl.c:1155-1184) handles DHCPv6 server-side messages (Advertisements and Replies) for prefix delegation:
static voidpinctrl_handle_dhcp6_server(struct rconn *swconn, const struct flow *ip_flow, struct dp_packet *pkt_in, const struct match *md){ if (ip_flow->dl_type != htons(ETH_TYPE_IPV6) || ip_flow->nw_proto != IPPROTO_UDP) { return; }
struct udp_header *udp_in = dp_packet_l4(pkt_in); size_t dlen = MIN(ntohs(udp_in->udp_len), dp_packet_l4_size(pkt_in)); if (dlen < UDP_HEADER_LEN + DHCPV6_HEADER_LEN) { return; }
const struct dhcpv6_header *hdr = dp_packet_get_udp_payload(pkt_in); switch (hdr->msg_type) { case DHCPV6_MSG_TYPE_ADVT: pinctrl_parse_dhcpv6_advt(swconn, ip_flow, DHCPV6_PAYLOAD(hdr), opts_len, md); break; case DHCPV6_MSG_TYPE_REPLY: pinctrl_parse_dhcpv6_reply(DHCPV6_PAYLOAD(hdr), opts_len, ip_flow); break; }}for Advertisements, pinctrl_parse_dhcpv6_advt() (ovn/controller/pinctrl.c:908-1053) extracts IA_PD options with prefix delegation info and constructs a DHCPv6 Request packet to send back to the server:
eth_compose(&packet, (struct eth_addr) ETH_ADDR_C(33,33,00,01,00,02), ip_flow->dl_dst, ETH_TYPE_IPV6, IPV6_HEADER_LEN);
struct udp_header *udp_h = compose_ipv6(&packet, IPPROTO_UDP, &ip_flow->ipv6_dst, &in6addr_all_dhcp_agents, 0, 0, 255, data_len + UDP_HEADER_LEN + 4);/* ... */unsigned char *dhcp_hdr = (unsigned char *)(udp_h + 1);*dhcp_hdr = DHCPV6_MSG_TYPE_REQUEST;memcpy(dhcp_hdr + 4, data, data_len);DHCP relay: request validation
the function pinctrl_handle_dhcp_relay_req_chk() (ovn/controller/pinctrl.c:2245-2388) validates and modifies incoming DHCP relay requests:
validation checks (in order):
- Parse the result field from userdata
- Parse relay_ip and server_ip from userdata
- Validate DHCP packet structure via
dhcp_get_hdr_from_pkt() - Verify
op == DHCP_OP_REQUEST(opcode must be 1) - Verify
giaddr == 0(relay agent IP must not already be set) - Parse DHCP options and verify message type is present
- If
request_ipis not in option 50, fall back tociaddr
packet modification:
uint8_t hops = dhcp_data->hops + 1;if (udp->udp_csum) { udp->udp_csum = recalc_csum16(udp->udp_csum, htons((uint16_t) dhcp_data->hops), htons((uint16_t) hops));}dhcp_data->hops = hops;
dhcp_data->giaddr = *relay_ip;if (udp->udp_csum) { udp->udp_csum = recalc_csum32(udp->udp_csum, 0, dhcp_data->giaddr);}the handler increments the hop count and sets giaddr (gateway IP address) to the relay IP. UDP checksums are incrementally updated for both changes.
DHCP relay: response validation
the function pinctrl_handle_dhcp_relay_resp_chk() (ovn/controller/pinctrl.c:2390-2600) validates incoming DHCP relay responses with extensive security checks:
validation checks (in order):
- Parse result field, relay_ip, server_ip from userdata
- Validate DHCP packet structure
- Verify
op == DHCP_OP_REPLY(opcode must be 2) - Verify
giaddr != 0(must be set in the relay request) - Verify
giaddr == relay_ip(giaddr must match the relay’s IP) - Parse DHCP options and verify message type is present
- Verify server_id (option 54) is present
- Verify
server_id == server_ip(server identifier must match configured server) - For OFFER/ACK messages: verify
(yiaddr & netmask) == (giaddr & netmask)— allocated IP must be in the same subnet as the relay - For OFFER/ACK messages: if router_ip is present, log a warning if
router_ip != giaddr(but continue processing)
packet modification for delivery:
memcpy(ð->eth_dst, dhcp_data->chaddr, sizeof(eth->eth_dst));
/* Handle broadcast flag */ovs_be32 ip_dst;if (!is_dhcp_flags_broadcast(dhcp_data->flags)) { ip_dst = dhcp_data->yiaddr;} else { ip_dst = htonl(0xffffffff);}put_16aligned_be32(&out_ip->ip_dst, ip_dst);out_ip->ip_csum = recalc_csum32(out_ip->ip_csum, ip_dst_orig, ip_dst);the destination MAC is set to the client’s hardware address (from chaddr), and the destination IP is set to either the client’s IP or broadcast, depending on the BROADCAST flag.
ovs in-band: making sure dhcp actually works
OVS has an in-band control mechanism that ensures management traffic isn’t blocked by flow tables. this includes DHCP traffic from the local management port.
the DHCP output check
the function in_band_must_output_to_local_port() (ovs/ofproto/in-band.c:233-243) checks if a packet is a DHCP reply that must reach the local port:
boolin_band_must_output_to_local_port(const struct flow *flow){ return (flow->dl_type == htons(ETH_TYPE_IP) && flow->nw_proto == IPPROTO_UDP && flow->tp_src == htons(DHCP_SERVER_PORT) && flow->tp_dst == htons(DHCP_CLIENT_PORT));}this matches UDP from port 67 (server) to port 68 (client) — i.e., DHCP replies from a DHCP server running on the local OVS bridge.
the DHCP request rule
in update_rules() (ovs/ofproto/in-band.c:286-295), a rule is created to allow DHCP requests from the local port:
if (ib->n_remotes && !eth_addr_is_zero(ib->local_mac)) { match_init_catchall(&match); match_set_in_port(&match, OFPP_LOCAL); match_set_dl_type(&match, htons(ETH_TYPE_IP)); match_set_dl_src(&match, ib->local_mac); match_set_nw_proto(&match, IPPROTO_UDP); match_set_tp_src(&match, htons(DHCP_CLIENT_PORT)); match_set_tp_dst(&match, htons(DHCP_SERVER_PORT)); add_rule(ib, &match, IBR_FROM_LOCAL_DHCP);}the priority constant IBR_FROM_LOCAL_DHCP is defined as 180000 (ovs/ofproto/in-band.c:53):
enum { IBR_FROM_LOCAL_DHCP = 180000, /* (a) From local port, DHCP. */ IBR_TO_LOCAL_ARP, /* (b) To local port, ARP. */ /* ... */};this high-priority rule ensures DHCP requests from the local bridge management interface are always allowed through, even if other flow rules would block them.
control plane policing (copp) for dhcp
OVN supports rate-limiting DHCP control plane traffic via CoPP. the CoPP protocol constants are defined in ovn/lib/copp.h:22-44:
enum copp_proto { COPP_PROTO_FIRST, COPP_ARP = COPP_PROTO_FIRST, COPP_ARP_RESOLVE, COPP_DHCPV4_OPTS, COPP_DHCPV6_OPTS, COPP_DNS, /* ... */ COPP_DHCPV4_RELAY, COPP_PROTO_MAX,};these map to string names in ovn/lib/copp.c:25-44:
static char *copp_proto_names[COPP_PROTO_MAX] = { [COPP_DHCPV4_OPTS] = "dhcpv4-opts", [COPP_DHCPV6_OPTS] = "dhcpv6-opts", [COPP_DHCPV4_RELAY] = "dhcpv4-relay", /* ... */};the copp_meter_get() function (ovn/lib/copp.c:55-70) retrieves the meter name for a given protocol:
const char *copp_meter_get(enum copp_proto proto, const struct nbrec_copp *copp, const struct shash *meter_groups){ if (!copp || proto >= COPP_PROTO_MAX) { return NULL; }
const char *meter = smap_get(&copp->meters, copp_proto_names[proto]);
if (meter && shash_find(meter_groups, meter)) { return meter; }
return NULL;}when generating DHCP flows, northd attaches CoPP meters via the WITH_CTRL_METER() macro. for example, in build_dhcpv4_options_flows():
ovn_lflow_add(lflows, op->od, S_SWITCH_IN_DHCP_OPTIONS, 100, ds_cstr(&match), ds_cstr(&options_action), lflow_ref, WITH_IO_PORT(inport->key), WITH_CTRL_METER(copp_meter_get(COPP_DHCPV4_OPTS, op->od->nbs->copp, meter_groups)), WITH_HINT(&op->nbsp->dhcpv4_options->header_));this means the DHCP options flow is rate-limited by the dhcpv4-opts meter if one is configured in the NB Copp table. the DHCP relay flows use COPP_DHCPV4_RELAY for rate-limiting.
end-to-end packet walk
DHCPv4 DISCOVER → OFFER
setup:
- Logical switch port
sw0-port0with DHCP options:server_id=10.0.0.1,server_mac=00:00:00:00:00:01,lease_time=3600,netmask=255.255.255.0,router=10.0.0.1 - Port’s offer IP:
10.0.0.2
packet: client sends DHCPDISCOVER
Ethernet: src=aa:bb:cc:dd:ee:ff, dst=ff:ff:ff:ff:ff:ffIP: src=0.0.0.0, dst=255.255.255.255UDP: src=68, dst=67DHCP: op=REQUEST(1), chaddr=aa:bb:cc:dd:ee:ee Options: msg_type=DISCOVER(1)pipeline traversal:
-
ls_in_port_sec→ls_in_pre_acl→ … →ls_in_dhcp_options(stage 28) -
match at priority 100:
inport == "sw0-port0" && eth.src == "aa:bb:cc:dd:ee:ff" &&(ip4.src == {10.0.0.2, 0.0.0.0} && ip4.dst == {10.0.0.1, 255.255.255.255}) &&udp.src == 68 && udp.dst == 67✓ matches (ip4.src=0.0.0.0, ip4.dst=255.255.255.255)
-
action executes:
reg0[3] = put_dhcp_opts(offerip = 10.0.0.2, netmask = 255.255.255.0, ...); next; -
packet-in to ovn-controller: opcode
ACTION_OPCODE_PUT_DHCP_OPTS, userdata contains encoded options -
pinctrl_handle_put_dhcp_opts()validates the packet, determinesmsg_type=DISCOVER→msg_type=OFFER, constructs the reply packet -
pipeline resumes at
ls_in_dhcp_response(stage 29) -
match at priority 100:
inport == "sw0-port0" && eth.src == "aa:bb:cc:dd:ee:ff" &&ip4 && udp.src == 68 && udp.dst == 67 && reg0[3]✓ matches (reg0[3]=1)
-
action executes:
eth.dst = eth.src; eth.src = 00:00:00:00:00:01;ip4.src = 10.0.0.1; udp.src = 67; udp.dst = 68;outport = inport; flags.loopback = 1; output; -
packet sent back to client
response:
Ethernet: src=00:00:00:00:00:01, dst=aa:bb:cc:dd:ee:ffIP: src=10.0.0.1, dst=10.0.0.2UDP: src=67, dst=68DHCP: op=REPLY(2), yiaddr=10.0.0.2 Options: msg_type=OFFER(2), netmask=255.255.255.0, router=10.0.0.1, ...DHCPv4 REQUEST → ACK
same as above, but the client sends a DHCPREQUEST with msg_type=REQUEST(3) and requested_ip=10.0.0.2. the handler validates that requested_ip == offer_ip and responds with msg_type=ACK(5).
DHCPv4 REQUEST → NAK (wrong IP)
if the client sends a DHCPREQUEST with requested_ip=10.0.0.99 (which doesn’t match the offer), the handler sets msg_type=NAK(6):
case DHCP_MSG_REQUEST: { msg_type = DHCP_MSG_ACK; if (dhcp_opts.request_ip != *offer_ip) { msg_type = DHCP_MSG_NAK; } break;}the NAK response has yiaddr=0 and siaddr=0:
dhcp_data->yiaddr = (msg_type == DHCP_MSG_NAK) ? 0 : *offer_ip;dhcp_data->siaddr = (msg_type == DHCP_MSG_NAK) ? 0 : next_server;and DHCP options are not included in NAK messages:
if (msg_type != DHCP_MSG_NAK) { new_l4_size += reply_dhcp_opts_ptr->size;}DHCPv6 SOLICIT → ADVERT
packet: client sends DHCPv6 SOLICIT
Ethernet: src=aa:bb:cc:dd:ee:ff, dst=33:33:00:01:00:02IPv6: src=fe80::..., dst=ff02::1:2UDP: src=546, dst=547DHCPv6: msg_type=SOLICIT(1), transaction_id=... Options: Client_ID, IA_NA(iaid=...), ...pipeline traversal at ls_in_dhcp_options:
match:
inport == "sw0-port0" && eth.src == "aa:bb:cc:dd:ee:ff"&& ip6.mcast && ip6.dst == ff02::1:2&& udp.src == 546 && udp.dst == 547action: reg0[3] = put_dhcpv6_opts(ia_addr = fe80::..., server_id = 00:00:00:00:00:01, ...); next;
at pinctrl_handle_put_dhcpv6_opts():
- message type SOLICIT → response type ADVERT
- IAID extracted from client’s IA_NA option
- Client ID extracted and copied to response
- options composed via
compose_out_dhcpv6_opts() - Client ID + composed options assembled into the response packet
DHCP relay: client request to remote server
setup:
- Logical switch
sw0withother_config:dhcp_relay_port = "sw0-lr0" - Logical router port
lr0-sw0withdhcp_relaypointing to a DHCP_Relay withservers = "192.168.1.100" - Router port IP:
10.0.0.1/24
packet: client sends DHCPDISCOVER
Ethernet: src=aa:bb:cc:dd:ee:ff, dst=ff:ff:ff:ff:ff:ffIP: src=0.0.0.0, dst=255.255.255.255UDP: src=68, dst=67DHCP: op=REQUEST(1), chaddr=aa:bb:cc:dd:ee:ff, giaddr=0pipeline traversal:
-
ls_in_l2_lkup(stage 14): the relay flow at priority 100 matches:inport == "sw0-port0" && eth.src == "aa:bb:cc:dd:ee:ff" &&ip4.src == {0.0.0.0, 10.0.0.0/24} && ip4.dst == 255.255.255.255 &&udp.src == 68 && udp.dst == 67action:
eth.dst = <router_mac>; outport = "sw0-lr0"; next; -
lr_in_ip_input(stage 3): the relay request validation flow at priority 110 matches:inport == "lr0-sw0" &&ip4.src == {0.0.0.0, 10.0.0.0/24} && ip4.dst == 255.255.255.255 &&ip.frag == 0 && udp.src == 68 && udp.dst == 67action:
reg9[7] = dhcp_relay_req_chk(10.0.0.1, 192.168.1.100); next; -
packet-in to ovn-controller: opcode
ACTION_OPCODE_DHCP_RELAY_REQ_CHK -
pinctrl_handle_dhcp_relay_req_chk()validates the packet:op == DHCP_OP_REQUEST✓giaddr == 0✓- message type present ✓
- sets
giaddr = 10.0.0.1(relay IP) - increments
hopsto 1 - updates UDP checksum
-
lr_in_dhcp_relay_req(stage 4): the flow at priority 100 matches (reg9[7]=1):inport == "lr0-sw0" &&ip4.src == {0.0.0.0, 10.0.0.0/24} && ip4.dst == 255.255.255.255 &&udp.src == 68 && udp.dst == 67 && reg9[7]action:
ip4.src = 10.0.0.1; ip4.dst = 192.168.1.100; udp.src = 67; next; -
the packet is now addressed to the remote DHCP server and is routed normally.
DHCP relay: server response back to client
packet: DHCP server sends DHCPOFFER
Ethernet: src=<server_mac>, dst=<router_mac>IP: src=192.168.1.100, dst=10.0.0.1UDP: src=67, dst=67DHCP: op=REPLY(2), yiaddr=10.0.0.2, giaddr=10.0.0.1, chaddr=aa:bb:cc:dd:ee:ffpipeline traversal:
-
lr_in_ip_input(stage 3): the relay response flow at priority 110 matches:ip4.src == 192.168.1.100 && ip4.dst == 10.0.0.1 &&ip.frag == 0 && udp.src == 67 && udp.dst == 67action:
next; -
lr_in_dhcp_relay_resp_chk(stage 20): the flow at priority 100 matches:ip4.src == 192.168.1.100 && ip4.dst == 10.0.0.1 &&udp.src == 67 && udp.dst == 67action:
reg2 = ip4.dst; reg9[8] = dhcp_relay_resp_chk(10.0.0.1, 192.168.1.100); next; -
packet-in to ovn-controller: opcode
ACTION_OPCODE_DHCP_RELAY_RESP_CHK -
pinctrl_handle_dhcp_relay_resp_chk()validates:op == DHCP_OP_REPLY✓giaddr != 0✓ (giaddr=10.0.0.1)giaddr == relay_ip✓- message type present ✓
- server_id present ✓
server_id == server_ip✓- for OFFER:
(yiaddr & netmask) == (giaddr & netmask)✓ - sets
eth.dst = client_mac(from chaddr) - handles broadcast flag for ip_dst
-
lr_in_dhcp_relay_resp(stage 21): the flow at priority 100 matches (reg9[8]=1):ip4.src == 192.168.1.100 && reg2 == 10.0.0.1 &&udp.src == 67 && udp.dst == 67 && reg9[8]action:
ip4.src = 10.0.0.1; udp.dst = 68; outport = "lr0-sw0"; output; -
the packet is forwarded back to the logical switch and delivered to the client.

access controls: the priority 34000 acl flow
one of the most important but easily overlooked aspects of DHCP in OVN is the ACL bypass flow. without it, DHCP replies from ovn-controller would be dropped by default-deny ACLs. i spent a good while staring at “why isn’t my DHCP reply getting through?” before i found this.
for each port with DHCP options configured, northd generates a flow at stage S_SWITCH_OUT_ACL_EVAL with priority 34000 (ovn/northd/northd.c:9967-9989):
if (!is_external) { const char *server_id = smap_get( &op->nbsp->dhcpv4_options->options, "server_id"); const char *server_mac = smap_get( &op->nbsp->dhcpv4_options->options, "server_mac"); const char *lease_time = smap_get( &op->nbsp->dhcpv4_options->options, "lease_time"); ovs_assert(server_id && server_mac && lease_time); const char *dhcp_actions = REGBIT_ACL_VERDICT_ALLOW" = 1; next;"; ds_clear(&match); ds_put_format(&match, "outport == %s && eth.src == %s " "&& ip4.src == %s && udp && udp.src == 67 " "&& udp.dst == 68", op->json_key, server_mac, server_id); ovn_lflow_add(lflows, op->od, S_SWITCH_OUT_ACL_EVAL, 34000, ds_cstr(&match), dhcp_actions, lflow_ref, WITH_IO_PORT(op->key), WITH_HINT(&op->nbsp->dhcpv4_options->header_));}this flow matches DHCP replies (udp.src=67, udp.dst=68) from the server MAC and server IP to the specific logical port, and sets REGBIT_ACL_VERDICT_ALLOW (reg1[0]) to 1. priority 34000 is high enough to match before most user-defined ACLs but lower than the universal flows at 65532+.
the same pattern applies for DHCPv6 (ovn/northd/northd.c:10043-10072):
ds_put_format(&match, "outport == %s && eth.src == %s " "&& ip6.src == %s && udp && udp.src == 547 " "&& udp.dst == 546", op->json_key, server_mac, server_ip);ovn_lflow_add(lflows, op->od, S_SWITCH_OUT_ACL_EVAL, 34000, ds_cstr(&match), dhcp6_actions, lflow_ref, ...);appendix a: dhcp packet structure reference
the DHCP packet structure is defined in ovs/lib/dhcp.h:31-49:
OVS_PACKED(struct dhcp_header { uint8_t op; /* DHCP_BOOTREQUEST(1) or DHCP_BOOTREPLY(2). */ uint8_t htype; /* ARP_HRD_ETHERNET (0x1). */ uint8_t hlen; /* ETH_ADDR_LEN (0x6). */ uint8_t hops; /* Hop count; set to 0 by client. */ ovs_be32 xid; /* Transaction ID. */ ovs_be16 secs; /* Since client started address acquisition. */ ovs_be16 flags; /* DHCP_FLAGS_* (bit 15 = broadcast). */ ovs_be32 ciaddr; /* Client IP, if it has a lease for one. */ ovs_be32 yiaddr; /* Client ("your") IP address. */ ovs_be32 siaddr; /* Next server IP address. */ ovs_be32 giaddr; /* Relay agent IP address. */ uint8_t chaddr[16]; /* Client hardware address. */ char sname[64]; /* Optional server host name. */ char file[128]; /* Boot file name. */ /* Followed by variable-length options field. */});BUILD_ASSERT_DECL(DHCP_HEADER_LEN == sizeof(struct dhcp_header));key constants from ovs/lib/dhcp.h:24-64:
#define DHCP_SERVER_PORT 67#define DHCP_CLIENT_PORT 68#define DHCP_MAGIC_COOKIE 0x63825363#define DHCP_HEADER_LEN 236
#define DHCP_OP_REQUEST 1#define DHCP_OP_REPLY 2
#define DHCP_MSG_DISCOVER 1#define DHCP_MSG_OFFER 2#define DHCP_MSG_REQUEST 3#define DHCP_MSG_ACK 5#define DHCP_MSG_NAK 6
#define DHCP_OPT_PAD 0#define DHCP_OPT_REQ_IP 50#define DHCP_OPT_MSG_TYPE 53#define DHCP_OPT_END 255additional OVN-specific DHCP message types from ovn/lib/ovn-l7.h:247-249:
#define OVN_DHCP_MSG_DECLINE 4#define OVN_DHCP_MSG_RELEASE 7#define OVN_DHCP_MSG_INFORM 8the DHCPv6 header (ovn/lib/ovn-l7.h:316-321):
OVS_PACKED(struct dhcpv6_header { uint8_t msg_type; uint8_t transaction_id[3];});BUILD_ASSERT_DECL(DHCPV6_HEADER_LEN == sizeof(struct dhcpv6_header));the DHCP option header (ovn/lib/ovn-l7.h:236-244):
#define DHCP_OPT_HEADER_LEN 2struct dhcp_opt_header { uint8_t code; uint8_t len;};
#define DHCP_OPT_PAYLOAD(hdr) \ (void *)((char *)hdr + sizeof(struct dhcp_opt_header))appendix b: complete action opcode reference for dhcp
| Opcode | Enum Value | Userdata Format | Handler |
|---|---|---|---|
ACTION_OPCODE_PUT_DHCP_OPTS | (auto) | [OXM][offset][offer_ip][opt1][opt2]... | pinctrl_handle_put_dhcp_opts |
ACTION_OPCODE_PUT_DHCPV6_OPTS | (auto) | [OXM][offset][opt1][opt2]... | pinctrl_handle_put_dhcpv6_opts |
ACTION_OPCODE_DHCP6_SERVER | (auto) | (OF1.3 actions) | pinctrl_handle_dhcp6_server |
ACTION_OPCODE_DHCP_RELAY_REQ_CHK | (auto) | [OXM][offset][relay_ip][server_ip] | pinctrl_handle_dhcp_relay_req_chk |
ACTION_OPCODE_DHCP_RELAY_RESP_CHK | (auto) | [OXM][offset][relay_ip][server_ip] | pinctrl_handle_dhcp_relay_resp_chk |
the struct action_header (ovn/include/ovn/actions.h:854-858) is always the first 8 bytes of userdata:
struct action_header { ovs_be32 opcode; /* One of ACTION_OPCODE_* */ uint8_t pad[4];};appendix c: ovn dhcp option wire format types
| Type String | Wire Format | Example |
|---|---|---|
"ipv4" | 4 bytes per value, network byte order | 10.0.0.1 → 0x0a000001 |
"uint8" | 1 byte | 128 → 0x80 |
"uint16" | 2 bytes, network byte order | 1500 → 0x05dc |
"uint32" | 4 bytes, network byte order | 3600 → 0x00000e10 |
"bool" | 1 byte (0 or 1) | true → 0x01 |
"str" | Raw bytes, length = strlen | "hello" → 0x68656c6c6f |
"host_id" | String or IPv4 | "tftp.example.com" or 10.0.0.1 |
"mac" | 6 bytes (ETH_ADDR_LEN) | 00:01:02:03:04:05 → 6 bytes |
"ipv6" | 16 bytes per value | fe80::1 → 16 bytes |
"domains" | RFC 1035 DNS-encoded with compression | See Section 5.4 |
"static_routes" | RFC 3442 classless route encoding | See Section 5.4 |
"domain" | FQDN encoded per RFC 3696 | See encode_fqdn_string() |
appendix d: source file reference
| Component | File | Key Functions/Lines |
|---|---|---|
| DHCP packet structure | ovs/lib/dhcp.h | struct dhcp_header (L31-49), constants (L24-64) |
| OVN DHCP option definitions | ovn/lib/ovn-l7.h | DHCP_OPTION macros (L77-156), struct dhcp_opt_header (L236-244) |
| DHCPv6 structures | ovn/lib/ovn-l7.h | struct dhcpv6_header (L316-321), option structs (L326-411) |
| CoPP definitions | ovn/lib/copp.h | enum copp_proto (L22-44) |
| CoPP implementation | ovn/lib/copp.c | copp_meter_get() (L55-70) |
| Action opcodes | ovn/include/ovn/actions.h | ACTION_OPCODES macro (L663-844) |
| Action structs | ovn/include/ovn/actions.h | ovnact_put_opts (L406-411), ovnact_dhcp_relay (L414-419) |
| Action encoding | ovn/lib/actions.c | encode_PUT_DHCPV4_OPTS (L3228-3292), encode_PUT_DHCPV6_OPTS (L3294-3313) |
| Option encoding | ovn/lib/actions.c | encode_put_dhcpv4_option (L3026-3192), encode_put_dhcpv6_option (L3194-3226) |
| Relay encoding | ovn/lib/actions.c | encode_dhcpv4_relay_chk (L2883-2901) |
| Action parsing | ovn/lib/actions.c | parse_put_dhcp_opts (L2976-2995), parse_dhcp_relay_chk (L2846-2881) |
| NB DB schema | ovn/ovn-nb.ovsschema | DHCP_Options (L711-719), DHCP_Relay (L720-731) |
| SB DB schema | ovn/ovn-sb.ovsschema | DHCP_Options (L305-317), DHCPv6_Options (L318-328) |
| Northd option sync | ovn/northd/ovn-northd.c | supported_dhcp_opts[] (L244-283), sync functions (L448-519) |
| Northd flow generation | ovn/northd/northd.c | build_dhcpv4_options_flows (L9900-9995), build_dhcpv6_options_flows (L9997-10077) |
| Northd action building | ovn/northd/northd.c | build_dhcpv4_action (L5956-6040), build_dhcpv6_action (L6042-6121) |
| Northd relay flows | ovn/northd/northd.c | build_lswitch_dhcp_relay_flows (L10085-10162), build_dhcp_relay_flows_for_lrouter_port (L16815-16980) |
| Northd stage defs | ovn/northd/northd.h | Pipeline stages (L548-605) |
| Northd register defs | ovn/northd/northd.c | REGBIT_DHCP_OPTS_RESULT (L126), relay registers (L209-230) |
| Controller engine | ovn/controller/ovn-controller.c | en_dhcp_options_run (L3836-3862), graph (L7075-7078) |
| Controller flow parsing | ovn/controller/lflow.c | lflow_parse_actions (L365-408) |
| Controller pinctrl | ovn/controller/pinctrl.c | process_packet_in (L3746-3919), pinctrl_handle_put_dhcp_opts (L2604-2944) |
| Controller DHCPv6 | ovn/controller/pinctrl.c | pinctrl_handle_put_dhcpv6_opts (L3138-3364) |
| Controller relay req | ovn/controller/pinctrl.c | pinctrl_handle_dhcp_relay_req_chk (L2245-2388) |
| Controller relay resp | ovn/controller/pinctrl.c | pinctrl_handle_dhcp_relay_resp_chk (L2390-2600) |
| Controller packet parsing | ovn/controller/pinctrl.c | dhcp_get_hdr_from_pkt (L2112-2168), dhcp_parse_options (L2180-2243) |
| Controller DHCPv6 PD | ovn/controller/pinctrl.c | pinctrl_handle_dhcp6_server (L1155-1184) |
| OVS in-band DHCP | ovs/ofproto/in-band.c | in_band_must_output_to_local_port (L233-243), rule creation (L286-295) |
| OVS DHCP constants | ovs/lib/dhcp.h | All constants and structs |
| NB CLI | ovn/utilities/ovn-nbctl.c | DHCP CLI commands |