The DOCA Flow model
S4·E2Three lookups before the coffee goes cold · Hotel lobby in Austin, the night before the design review
Builds on: E-switch, representors, and switchdev, DOCA Core objects and lifecycle
Before you read: what do you already know?
3 quick questions. Wrong answers are fine and expected; trying first makes the lesson stick.
After this lesson you can
- Reproduce the DOCA Flow bring-up sequence (cfg, init, ports, pair) with the exact 3.5.0 setter names.
- Distinguish vnf and switch modes and the expert suffix by what they do to a missed packet.
- Match each of the seven pipe types to its add-entry call and decide which one a given classification stage needs.
- Analyze a match specification into ignored, constant and changeable fields and predict which values entries must supply.
- Trace an entry from add-entry through doca_flow_entries_process to the completion callback and explain the per-core queue rule.
Episode 2 — Three lookups before the coffee goes cold
Ten at night and the customer’s lead developer drops into the chair opposite you with a laptop and tomorrow’s question: can the card run their deny-list, then a route lookup, and still hand suspicious flows up to their inspection process — in a single pass? Their x86 build does all three in software and it is eating the cores the clean-pipes service was supposed to sell. Two tables away the Dell SE is adding a row to the spreadsheet, next to a coffee he will also forget.
You sketch the answer on the back of the agenda, because DOCA Flow has exactly that shape. It is the GA steering library in 3.5.0, and OVS-DOCA and every sample sit on top of it.[14] What he is describing is an ACL pipe in front of an LPM pipe in front of an RSS forward: enum doca_flow_pipe_type has seven members — BASIC, CONTROL, LPM, ACL, HASH, ORDERED_LIST, CT — and each one has its own add-entry call.[1] The library looks the way it does because a rule is a hardware transaction with latency: entries are queued and completed asynchronously through doca_flow_entries_process, and each core must use its own dedicated pipe queue ID to prevent race conditions.[1] That is how you get an insertion rate instead of one round trip per rule.
Design the miss before you design the match. The miss is the mode string, and the mode string is set at init.
1Bring-up: cfg, init, ports
DOCA Flow is the GA steering library in 3.5.0; OVS-DOCA and the samples all sit on it.[14] Every program starts the same way, and flow_common.c at tag 3.5.0 is the reference for the exact names.[2]
struct doca_flow_cfg *flow_cfg;
doca_flow_cfg_create(&flow_cfg);
doca_flow_cfg_set_pipe_queues(flow_cfg, nb_queues); /* one pipe_queue per core */
doca_flow_cfg_set_mode_args(flow_cfg, "vnf,hws"); /* or "switch,hws" */
doca_flow_cfg_set_cb_entry_process(flow_cfg, check_for_valid_entry);
doca_flow_cfg_set_cb_pipe_process(flow_cfg, pipe_process_cb);
doca_flow_cfg_set_resource_mode(flow_cfg, DOCA_FLOW_RESOURCE_MODE_PORT);
/* system-level alternative: doca_flow_cfg_set_nr_counters / _set_nr_meters / _set_nr_shared_resource */
/* CT only: doca_flow_cfg_set_ct_queues / doca_flow_cfg_set_ct_ctrl_queues */
doca_flow_init(flow_cfg);
doca_flow_cfg_destroy(flow_cfg);The guide lists the remaining setters: doca_flow_cfg_set_queue_depth, doca_flow_cfg_set_rss, doca_flow_cfg_set_definitions, doca_flow_cfg_set_cb.[1] simple_fwd_vnf additionally attaches the Flow Tune server with doca_flow_tune_cfg_create, doca_flow_tune_cfg_set_profile(…, DOCA_FLOW_TUNE_PROFILE_FULL) and doca_flow_cfg_set_tune_cfg.[6]
Ports come next. For each port the helper creates a doca_flow_port_cfg, sets doca_flow_port_cfg_set_port_id, _set_actions_mem_size, and one doca_flow_port_cfg_set_nr_resources(port_cfg, DOCA_FLOW_RESOURCE_<COUNTER|METER|COUNTER_CT|RSS|PSP|ENCAP|DECAP|IPSEC_SA|EVENT_RSS|TRIM>, n) per resource, then binds the device with doca_flow_port_cfg_set_dev(port_cfg, dev) (or doca_flow_port_cfg_set_dev_rep for a representor), sets devargs such as "th_win_us=0" or "no_wire_to_wire,th_win_us=0", and calls doca_flow_port_start(port_cfg, &port); the cfg is destroyed right after.[2] simple_fwd_vnf also sets doca_flow_port_cfg_set_priv_data_size to hang per-port state off the port.[6] In vnf mode the ports are paired both ways, doca_flow_port_pair(ports[1], ports[0]) and doca_flow_port_pair(ports[0], ports[1]), so each port’s wire traffic egresses on its partner.[2] In 3.5.0 you can query what a device supports before starting with doca_flow_port_capabilities_create(doca_dev, &caps) and _destroy.[1]
Teardown is the mirror image and the order matters: doca_flow_port_pipes_flush on every port in reverse order, then doca_flow_port_stop in reverse order because in switch mode port 0 is the proxy port and must stop last, then doca_flow_destroy().[2]
Two prerequisites sit outside the API: hugepages (echo '1024' | sudo tee /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages and a hugetlbfs mount), and, when DPDK probes the ports, devargs of the form -a <pci_id>,dv_flow_en=2,dv_xmeta_en=4,representor=[pf0vf0-2].[1]
2Modes: vnf, switch, and the expert suffix
The mode string decides what happens to a packet that misses every pipe and where pipes live.[1]
mode_args |
Meaning (guide wording) |
|---|---|
vnf |
Packet arriving from a device port is processed and sent to another port; missed packets go to RSS |
switch |
Used for internal switching with representor ports only; packet forwarded from one port to another |
expert suffix |
Switch-mode variant where port_id is not added to packet metadata |
hws |
Hardware steering; every 3.5.0 sample uses vnf,hws or switch,hws |
The guide documents two modes, vnf and switch, plus the expert switch variant; those rows are its wording.[1] In switch mode mode_args also carries hairpin tuning options — hairpinq_num=[n], use_huge_mem, lock_rx_mem — when an internal wire-to-wire hairpin is created; there is no hairpin mode and no isolated suffix on the 3.5.0 page.[1] The hws observation comes from the samples themselves: flow_acl, flow_lpm and flow_rss_meta call init_doca_flow(nb_queues, "vnf,hws", …), flow_switch calls it with "switch,hws".[3][4][8] Switch samples expose the expert variant as the --expert-mode (-exp) flag.[16]
Switch mode changes where you create pipes. flow_switch initializes ports with init_doca_flow_switch_ports and then creates its pipe on doca_flow_port_switch_get(ports[0]), the switch-manager port, with a pipe-level fwd.type = DOCA_FLOW_FWD_PORT; fwd.port_id = 0xffff (changeable) and per-entry fwd.port_id = port_base + 1 + entry_index.[8] The connection-tracking samples run in "switch,hws" too, because CT needs the whole e-switch, not a pair of ports.[12]
simple_fwd_vnf takes the mode from the command line and pairs ports as vnf does; its HAIRPIN_PIPE forwards to another pipe and its RSS_PIPE forwards to software queues, so the application chooses per flow whether to hairpin in hardware or receive on the Arm.[6][7]
3Seven pipe types, seven add-entry calls
enum doca_flow_pipe_type has seven members: DOCA_FLOW_PIPE_BASIC, _CONTROL, _LPM, _ACL, _HASH, _ORDERED_LIST, _CT, chosen with doca_flow_pipe_cfg_set_type(pipe_cfg, type).[1] Every pipe is built the same way: doca_flow_pipe_cfg_create(&pipe_cfg, port), then name, type and root flag (doca_flow_pipe_cfg_set_name, _set_type, _set_is_root, wrapped by the samples’ set_flow_pipe_cfg), optional _set_nr_entries, _set_match, _set_actions, _set_monitor, _set_domain, and finally doca_flow_pipe_create(pipe_cfg, &fwd, &fwd_miss, &pipe).[2][3] What differs is the add-entry call.
solid = fwd · dashed = fwd_miss · dotted = per-entry fwd · green bar = is_root
Priority-ordered mask matching (0..1024, lowest value wins). FWD may be PORT, PIPE or DROP.
Changeable = all-ones (0xffffffff) in the pipe match, value per entry. Constant = fixed value, mask NULL. l3_type / l4_type_ext / tun.type selectors can never be changeable.
- 1.
- 2.
| Type | Add-entry call as the 3.5.0 sample writes it |
|---|---|
| BASIC | doca_flow_pipe_basic_add_entry(0, pipe, &match, 0, &actions, &monitor, &fwd, flags, status, &entry) |
| CONTROL | doca_flow_pipe_control_add_entry(0, pipe, &match, NULL /* match_mask */, NULL, NULL, NULL, NULL, &monitor, priority, &fwd, status, NULL) |
| LPM | doca_flow_pipe_lpm_add_entry(0, pipe, &match, &match_mask, 0, NULL, NULL, &fwd, flags, status, &entry) |
| ACL | doca_flow_pipe_acl_add_entry(0, pipe, &match, &match_mask, 0, NULL, priority, &fwd, flag, status, entry) |
| HASH | doca_flow_pipe_hash_add_entry(0, pipe, entry_index, 0, NULL, NULL, &fwd, flags, status, &entry) |
| ORDERED_LIST | doca_flow_pipe_ordered_list_add_entry(0, pipe, idx, &ordered_list, NULL, DOCA_FLOW_ENTRY_FLAGS_NO_WAIT, status, &entry) |
| CT | doca_flow_ct_entry_prepare(…) then doca_flow_ct_add_entry(ct_queue, pipe, entry_flags, match_origin, match_reply, actions_origin, actions_reply, fwd_origin, fwd_reply, timeout_s, ct_status, entry) |
The BASIC form is what flow_acl’s one-entry root and simple_fwd_vnf use; the first argument is the pipe queue, the fourth an action index.[3][6] A CONTROL pipe is created with _set_type(DOCA_FLOW_PIPE_CONTROL) and _set_is_root(true), and its entries carry a match, a mask the samples leave NULL, four further optional pointers also left NULL, a monitor, a per-entry priority, fwd, user context and entry.[9] LPM entries pass explicit masks such as DOCA_HTOBE32(0xffffffff), 0xffff0000 and 0x00000000 for the default route.[4] ACL entries take a priority where 0 ≤ priority ≤ 1024 and the lowest value is the highest priority; a port mask equal to the port means exact match, 0 means any, and a mask larger than the port means a range.[3] In a HASH pipe the entry index is hash_func(destination IPv4 address) mod nb_flows, and software can compute the same value with doca_flow_pipe_calc_hash(pipe, &match, &hash).[10] An ORDERED_LIST entry is a struct doca_flow_ordered_list whose elements are DOCA_FLOW_ORDERED_LIST_ELEMENT_ACTIONS, _MONITOR or _NAT64.[11] CT is covered in lesson 4.3.[12]
4Match classes, action order, monitor, fwd, completion
A pipe-level match classifies every field into one of three classes. Ignored: field value zero, no comparison performed. Constant: a fixed value across all entries, 0 < V < 0xffff, mask NULL. Changeable: per-entry values, V = 0xffff, mask NULL.[1] Passing doca_flow_pipe_cfg_set_match(pipe_cfg, &match, NULL) uses this implicit convention; passing an explicit &match_mask overrides it.[1] struct doca_flow_match groups outer, inner, tun, parser_meta and meta; parser-meta selectors such as match.parser_meta.outer_l3_type = DOCA_FLOW_L3_META_IPV4, outer_l4_type = DOCA_FLOW_L4_META_TCP or tun_type = DOCA_FLOW_TUN_META_VXLAN let a pipe test the parsed protocol rather than header bytes.[1] The samples’ 5-tuple template is exactly that: outer.l3_type = DOCA_FLOW_L3_TYPE_IP4, outer.ip4.src_ip = 0xffffffff, outer.ip4.dst_ip = 0xffffffff, outer.l4_type_ext = DOCA_FLOW_L4_TYPE_EXT_TCP, outer.tcp.l4_port.src_port = 0xffff, outer.tcp.l4_port.dst_port = 0xffff.[5]
Actions execute in a fixed order regardless of how you fill the struct: Crypto (decrypt), Decapsulation, Pop, Meta, Outer, Tun, Push, Encapsulation, Crypto (encrypt), then action descriptors (desc_array).[1] enum doca_flow_action_type offers DOCA_FLOW_ACTION_AUTO, _ADD, _COPY, and actions are attached with doca_flow_pipe_cfg_set_actions(pipe_cfg, actions_arr, actions_mask_arr, descs, nb).[1] A changeable action follows the same convention as a match: flow_rss_meta sets actions.meta.pkt_meta = UINT32_MAX at pipe level and DOCA_HTOBE32(10) in the entry.[5]
The monitor carries per-entry counters, meters, aging and mirroring. monitor.counter_type = DOCA_FLOW_RESOURCE_TYPE_NON_SHARED creates a counter per entry automatically; read it with doca_flow_resource_query_entry(entry, &query_stats) and query_stats.counter.total_pkts / total_bytes.[1][3] Meters and aging get their own lesson.
Forwarding is struct doca_flow_fwd with enum doca_flow_fwd_type. The guide uses DOCA_FLOW_FWD_PIPE (next_pipe), _PORT (port_id), _RSS, _DROP, _CHANGEABLE, _TARGET (to a target such as the kernel, doca_flow_get_target()), _TRIM (fwd.trim.trim_id, the trim shared resource) and _HASH_PIPE (dynamic hash-algorithm selection); a fwd_miss handler accepts only _PIPE, _DROP and _TARGET.[1] _ORDERED_LIST_PIPE is what an ordered-list pipe is forwarded to in the sample.[11] RSS forwarding sets fwd.rss_type = DOCA_FLOW_RESOURCE_TYPE_NON_SHARED, a queues_array, nr_queues, and hash flags such as DOCA_FLOW_RSS_IPV4_SRC or DOCA_FLOW_RSS_TCP_DST in rss.inner_flags or outer_flags; the port must have been started with nr_rss resources.[5][1]
Completion is asynchronous. Entries carry DOCA_FLOW_ENTRY_FLAGS_WAIT_FOR_BATCH or DOCA_FLOW_ENTRY_FLAGS_NO_WAIT; the application then calls doca_flow_entries_process(port, pipe_queue_id, timeout_usec, max_entries) or doca_flow_pipe_queue_drain(port, queue, NULL), and the callback void cb(struct doca_flow_pipe_entry*, uint16_t pipe_queue, enum doca_flow_entry_status, enum doca_flow_entry_op, void *user_ctx) reports DOCA_FLOW_ENTRY_STATUS_SUCCESS or not; doca_flow_pipe_entry_get_status(entry) reads it later.[1] The samples’ check_for_valid_entry sets status->failure on any non-success and increments nb_processed, and flow_process_entries compares that count with the number added.[2] The one rule that bites in production: each core must use its own dedicated pipe_queue ID when calling entry addition to prevent race conditions.[1]
5Worked: ACL then LPM then RSS, in code
simple_fwd_vnf shows the shape of a real pipeline: a root CONTROL_PIPE classifies the tunnel type, per-tunnel DOCA_FLOW_PIPE_BASIC pipes match the inner 5-tuple with a counter and monitor.aging_sec = 0xffffffff (changeable), each with fwd_miss pointing at the RSS_PIPE, and a VXLAN_ENCAP_PIPE in the egress domain forwards to the wire.[6] Its loop is rte_eth_rx_burst, classify, on a miss add an entry, doca_flow_entries_process, optionally doca_flow_aging_handle, then rte_eth_tx_burst.[7] Before running it you need hugepages and sudo mlxconfig -d <pcie_address> s FLEX_PARSER_PROFILE_ENABLE=3 plus a reboot, so the GRE, VXLAN and GTP parsers are enabled.[7] The exercise below builds a smaller pipeline from the same parts.
Design. Root is the one-entry MAIN_PIPE the ACL sample uses (IPv4 only), then three stages: an ACL deny-list, an LPM route table, and an RSS leaf. Miss paths are drawn explicitly because the e-switch needs them.[3]
Step 1, init and ports, straight from the samples.
struct flow_resources resource = {0};
uint32_t nr_shared_resources[SHARED_RESOURCE_NUM_VALUES] = {0};
struct doca_flow_port *ports[2];
uint32_t actions_mem_size[2];
struct entries_status status = {0};
resource.mode = DOCA_FLOW_RESOURCE_MODE_PORT;
resource.nr_counters = 64; /* one per entry we plan to add */
resource.nr_rss = 1; /* the RSS leaf needs an RSS resource */
init_doca_flow(nb_queues, "vnf,hws", &resource, nr_shared_resources);
ARRAY_INIT(actions_mem_size, ACTIONS_MEM_SIZE(64));
init_doca_flow_vnf_ports(2, ports, actions_mem_size, &resource);init_doca_flow wraps the cfg sequence of Segment 1; init_doca_flow_vnf_ports calls doca_dpdk_port_as_dev per port and pairs them.[2] nr_rss follows flow_rss_meta.[5]
Step 2, the RSS leaf. Leaves are built first because an upstream fwd.next_pipe needs the pointer.
static doca_error_t create_rss_pipe(struct doca_flow_port *port, struct doca_flow_pipe **pipe)
{
struct doca_flow_pipe_cfg *pipe_cfg;
struct doca_flow_match match = {0};
struct doca_flow_fwd fwd = {0}, fwd_miss = {0};
uint16_t rss_queues[1] = {0};
match.parser_meta.outer_l3_type = DOCA_FLOW_L3_META_IPV4; /* constant selector */
doca_flow_pipe_cfg_create(&pipe_cfg, port);
set_flow_pipe_cfg(pipe_cfg, "RSS_PIPE", DOCA_FLOW_PIPE_BASIC, false);
doca_flow_pipe_cfg_set_match(pipe_cfg, &match, NULL);
fwd.type = DOCA_FLOW_FWD_RSS;
fwd.rss_type = DOCA_FLOW_RESOURCE_TYPE_NON_SHARED;
fwd.rss.queues_array = rss_queues;
fwd.rss.nr_queues = 1;
fwd.rss.inner_flags = DOCA_FLOW_RSS_IPV4_SRC | DOCA_FLOW_RSS_TCP_SRC;
fwd_miss.type = DOCA_FLOW_FWD_DROP;
doca_error_t r = doca_flow_pipe_create(pipe_cfg, &fwd, &fwd_miss, pipe);
doca_flow_pipe_cfg_destroy(pipe_cfg);
return r;
}
/* one entry, everything constant, so an empty match: */
doca_flow_pipe_basic_add_entry(0, rss_pipe, &empty_match, 0, NULL, NULL, NULL,
DOCA_FLOW_ENTRY_FLAGS_NO_WAIT, &status, &rss_entry);The RSS block mirrors flow_rss_meta; the empty-match entry is how flow_acl and flow_lpm populate a constant-only pipe.[5][3]
Step 3, the LPM route table with a changeable destination.
struct doca_flow_fwd fwd = {.type = DOCA_FLOW_FWD_CHANGEABLE};
struct doca_flow_monitor counter = {.counter_type = DOCA_FLOW_RESOURCE_TYPE_NON_SHARED};
match.outer.l3_type = DOCA_FLOW_L3_TYPE_IP4;
match.outer.ip4.dst_ip = 0xffffffff; /* changeable */
doca_flow_pipe_cfg_create(&pipe_cfg, port);
set_flow_pipe_cfg(pipe_cfg, "LPM_PIPE", DOCA_FLOW_PIPE_LPM, false);
doca_flow_pipe_cfg_set_nr_entries(pipe_cfg, FLOW_COMMON_PIPE_RULES);
doca_flow_pipe_cfg_set_match(pipe_cfg, &match, NULL);
doca_flow_pipe_cfg_set_monitor(pipe_cfg, &counter);
doca_flow_pipe_create(pipe_cfg, &fwd, NULL, &lpm_pipe);
doca_flow_pipe_cfg_destroy(pipe_cfg);
/* /32 host route to the paired wire port */
match.outer.ip4.dst_ip = BE_IPV4_ADDR(10, 0, 0, 7);
match_mask.outer.ip4.dst_ip = DOCA_HTOBE32(0xffffffff);
fwd.type = DOCA_FLOW_FWD_PORT; fwd.port_id = port_id ^ 1;
doca_flow_pipe_lpm_add_entry(0, lpm_pipe, &match, &match_mask, 0, NULL, NULL, &fwd,
DOCA_FLOW_ENTRY_FLAGS_WAIT_FOR_BATCH, &status, &e[0]);
/* /16 prefix goes to software for inspection */
match_mask.outer.ip4.dst_ip = DOCA_HTOBE32(0xffff0000);
fwd.type = DOCA_FLOW_FWD_PIPE; fwd.next_pipe = rss_pipe;
doca_flow_pipe_lpm_add_entry(0, lpm_pipe, &match, &match_mask, 0, NULL, NULL, &fwd,
DOCA_FLOW_ENTRY_FLAGS_WAIT_FOR_BATCH, &status, &e[1]);
/* default route: drop */
match_mask.outer.ip4.dst_ip = DOCA_HTOBE32(0x00000000);
fwd.type = DOCA_FLOW_FWD_DROP;
doca_flow_pipe_lpm_add_entry(0, lpm_pipe, &match, &match_mask, 0, NULL, NULL, &fwd,
DOCA_FLOW_ENTRY_FLAGS_NO_WAIT, &status, &e[2]);The structure and the three masks are flow_lpm’s; the sample batches only its first entry (WAIT_FOR_BATCH) and sends the /16 and the default route with NO_WAIT, while this variant batches the first two so one drain commits all three. The sample keys on src_ip, this variant on dst_ip.[4]
Step 4, the ACL deny-list in front, miss to LPM.
struct doca_flow_fwd fwd = {.type = DOCA_FLOW_FWD_CHANGEABLE};
struct doca_flow_fwd fwd_miss = {.type = DOCA_FLOW_FWD_PIPE, .next_pipe = lpm_pipe};
match.parser_meta.outer_l3_type = DOCA_FLOW_L3_META_IPV4;
match.outer.l3_type = DOCA_FLOW_L3_TYPE_IP4;
match.outer.ip4.src_ip = 0xffffffff; match.outer.ip4.dst_ip = 0xffffffff;
match.outer.l4_type_ext = DOCA_FLOW_L4_TYPE_EXT_TCP;
match.outer.tcp.l4_port.src_port = 0xffff; match.outer.tcp.l4_port.dst_port = 0xffff;
doca_flow_pipe_cfg_create(&pipe_cfg, port);
set_flow_pipe_cfg(pipe_cfg, "ACL_PIPE", DOCA_FLOW_PIPE_ACL, false);
doca_flow_pipe_cfg_set_nr_entries(pipe_cfg, 10);
doca_flow_pipe_cfg_set_domain(pipe_cfg, DOCA_FLOW_PIPE_DOMAIN_DEFAULT);
doca_flow_pipe_cfg_set_match(pipe_cfg, &match, NULL);
doca_flow_pipe_cfg_set_monitor(pipe_cfg, &counter);
doca_flow_pipe_create(pipe_cfg, &fwd, &fwd_miss, &acl_pipe);
doca_flow_pipe_cfg_destroy(pipe_cfg);
/* deny 1.2.3.4 -> 8.8.8.8 TCP any ports, priority 10 */
match.outer.ip4.src_ip = BE_IPV4_ADDR(1, 2, 3, 4); match_mask.outer.ip4.src_ip = DOCA_HTOBE32(0xffffffff);
match.outer.ip4.dst_ip = BE_IPV4_ADDR(8, 8, 8, 8); match_mask.outer.ip4.dst_ip = DOCA_HTOBE32(0xffffffff);
match.parser_meta.outer_l4_type = DOCA_FLOW_L4_META_TCP; match_mask.parser_meta.outer_l4_type = UINT32_MAX;
match_mask.outer.tcp.l4_port.src_port = 0; match_mask.outer.tcp.l4_port.dst_port = 0; /* any port */
fwd.type = DOCA_FLOW_FWD_DROP;
doca_flow_pipe_acl_add_entry(0, acl_pipe, &match, &match_mask, 0, NULL, 10, &fwd,
DOCA_FLOW_ENTRY_FLAGS_NO_WAIT, &status, &acl_entry);Sample flow_acl fronts the ACL with a one-entry basic MAIN_PIPE root (is_root=true, IPv4 selector, fwd.type = DOCA_FLOW_FWD_PIPE; fwd.next_pipe = acl_pipe), and this variant keeps that root; the deny entry copies the sample’s first row (priority 10, is_allow=false, port masks 0).[3]
Step 5, commit and verify.
doca_flow_pipe_queue_drain(port, 0, NULL);
if (status.nb_processed != 6 || status.failure) /* 1 root + 1 rss + 3 lpm + 1 acl */
return DOCA_ERROR_BAD_STATE;
doca_flow_resource_query_entry(acl_entry, &q); /* q.counter.total_pkts */flow_process_entries in flow_common.c does exactly this check.[2]
- Init:
init_doca_flow(nb_queues, "____,hws", &resource, nr_shared_resources); setresource.mode = ____andresource.nr_rss = ____beforeinit_doca_flow_vnf_ports. - RSS leaf:
set_flow_pipe_cfg(pipe_cfg, "RSS_PIPE", ____, false);fwd.type = ____; populate withdoca_flow_pipe_basic_add_entry(0, rss_pipe, &empty_match, 0, NULL, NULL, NULL, ____, &status, &e). - LPM: pipe-level
fwd = {.type = ____};match.outer.ip4.dst_ip = ____(changeable); entries with masks0xffffffff,0xffff0000,____; the last entry uses flag____. - ACL:
set_flow_pipe_cfg(pipe_cfg, "ACL_PIPE", ____, false);fwd_miss.type = DOCA_FLOW_FWD_PIPE; fwd_miss.next_pipe = ____; entry priority range is0 to ____and the ____ value wins. - Commit:
doca_flow_pipe_queue_drain(port, ____, NULL); then checkstatus.____ == 6andstatus.____ == false.
Extend the pipeline: the /16 prefix must no longer go to software. Instead it must be counted and hairpinned to the paired port, while a new /24 inside it (10.0.5.0/24) is still sent to RSS. Also add a second ACL entry that permits 172.20.1.4 to 192.168.3.4 UDP with destination ports 80 to 3000 and higher priority than the deny rule.
Acceptance: (a) exactly the LPM masks 0xffffffff, 0xffffff00, 0xffff0000, 0x00000000 are present and you can say which entry wins for 10.0.5.9 and for 10.0.9.9; (b) the permit entry uses dst_port_mask larger than dst_port to express the range and a priority lower in value than 10; (c) status.nb_processed equals your entry count after one doca_flow_pipe_queue_drain; (d) you name the three places where DOCA_FLOW_FWD_CHANGEABLE appears.
The whiteboard in the morning
At the review you draw one root CONTROL pipe, an ACL deny-list, an LPM route table and an RSS leaf, with fwd_miss on every stage, and their developer recognizes his own pipeline in it.[3][4][5] vnf,hws, ports paired both ways, a counter on each entry, one pipe queue per core.[2] The network lead fills two more notebook pages; the SE closes two spreadsheet rows before lunch. What you tell them: “You are not writing a fast path any more. You describe the classification once, and the e-switch runs it.” Nine days later, at 02:10, the NOC calls: the first few thousand connections are perfect, and after that latency climbs and never comes back.
Lab
Pre-flight (read-only): on the Arm, sudo mst start && sudo mst status -v; sudo mlxconfig -d /dev/mst/<dev> q FLEX_PARSER_PROFILE_ENABLE; cat /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages; ls /sys/bus/auxiliary/devices/ | grep mlx5_core.sf. Record every value: they are your rollback targets.
-
Mutating: hugepages.
echo '2048' | sudo tee -a /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages. Expected: the file now reads at least 2048.[7] Rollback: write the recorded value back. -
Mutating, firmware: if
FLEX_PARSER_PROFILE_ENABLEis not 3,sudo mlxconfig -d /dev/mst/<dev> s FLEX_PARSER_PROFILE_ENABLE=3and reboot the Arm.[7] Rollback:mlxconfig … s FLEX_PARSER_PROFILE_ENABLE=<recorded>and reboot. Do not do this on a card that also runs DPL (it needs profile 4, lesson 4.5). -
Mutating: two SFs for the application. The VNF guide requires encap mode
noneon the PF FDB before creating SFs; set that per the guide’s SF section for your OS, thenmlxdevm port add pci/0000:03:00.0 flavour pcisf pfnum 0 sfnum 4(repeat withsfnum 5),mlxdevm port function set … hw_addr <mac> trust on state active, and bind eachmlx5_core.sf.<serial>on the auxiliary bus.[7] Expected: two newmlx5_core.sf.*devices. Rollback: unbind, thenmlxdevm port delof the two port indices. -
Build as in the no-hardware lab, then run:
sudo /tmp/build/simple_fwd_vnf/doca_simple_fwd_vnf -- -a aux/4 -a aux/5 -l 60. Expected: log lines for port start, pipe creation and the periodic stats timer; from the host, send VXLAN traffic toward the SF MACs and watch the per-flow counters.[7] If the log shows a pipe-create failure mentioning the parser, step 2 did not take effect (reboot needed). -
Rollback in reverse: stop the application (Ctrl-C; it stops ports in reverse order and calls
doca_flow_destroy), remove the SFs, restoreFLEX_PARSER_PROFILE_ENABLE, restore hugepages.[6]
-
Start the devel-host container from lesson m3-01. Build the reference application:
cd /opt/mellanox/doca/applications/ && meson /tmp/build -Denable_all_applications=false -Denable_simple_fwd_vnf=true && ninja -C /tmp/build. Expected:/tmp/build/simple_fwd_vnf/doca_simple_fwd_vnfexists.[7] If meson fails on a missingdoca-flowpkg-config module,pkg-config --list-all | grep docaand reinstall the devel image. -
Build one sample:
cd /opt/mellanox/doca/samples/doca_flow/flow_lpm && meson /tmp/build-lpm && ninja -C /tmp/build-lpm. Expected:/tmp/build-lpm/samples/doca_flow_lpm; run/tmp/build-lpm/samples/doca_flow_lpm -hand confirm the-a, --devand-l, --log-levelflags.[13] If not: the samples directory may be named differently;find /opt/mellanox/doca -name flow_lpm_sample.c. -
Map the code:
grep -n "doca_flow_pipe_lpm_add_entry\|DOCA_HTOBE32(0x" /opt/mellanox/doca/samples/doca_flow/flow_lpm/flow_lpm_sample.c. Expected: three add-entry calls and the masks0xffffffff,0xffff0000,0x00000000.[4] -
In the FlowPipeBuilder above, rebuild the Worked pipeline (root, ACL, LPM, RSS) and export its call list. Expected:
doca_flow_pipe_cfg_set_typeappears once per pipe withBASIC,ACL,LPM,BASIC; exactly two pipes haveFWD_CHANGEABLE; the RSS leaf hasfwd_miss = DROP. -
Write the fifteen-line init block from memory, then diff against
flow_common.cinit_doca_flow_cb. Expected: same setter order as Segment 1.[2]
Retrieval check
10 questions from memory. Answer before looking anything up; misses become flashcards.
Explain it to a Dell SE
Explain to a Dell SE, in five sentences, what a DOCA Flow pipe is and why an application adds entries to it instead of matching packets in software.
Sources
Facts in this lesson were checked against DOCA 3.5.0 docs + doca-samples tag 3.5.0, 2026-09-06. Dates are when each page was fetched.
- DOCA Flow (programming guide) · fetched 2026-09-06 · DOCA 3.5.0
- doca-samples 3.5.0: samples/doca_flow/flow_common.c · fetched 2026-09-06 · DOCA 3.5.0
- doca-samples 3.5.0: flow_acl_sample.c · fetched 2026-09-06 · DOCA 3.5.0
- doca-samples 3.5.0: flow_lpm_sample.c · fetched 2026-09-06 · DOCA 3.5.0
- doca-samples 3.5.0: flow_rss_meta_sample.c · fetched 2026-09-06 · DOCA 3.5.0
- doca-samples 3.5.0: applications/simple_fwd_vnf/simple_fwd.c · fetched 2026-09-06 · DOCA 3.5.0
- DOCA Simple Forward VNF Application Guide · fetched 2026-09-06 · DOCA 3.5.0
- doca-samples 3.5.0: flow_switch_sample.c · fetched 2026-09-06 · DOCA 3.5.0
- doca-samples 3.5.0: flow_control_pipe_sample.c · fetched 2026-09-06 · DOCA 3.5.0
- doca-samples 3.5.0: flow_hash_pipe_sample.c · fetched 2026-09-06 · DOCA 3.5.0
- doca-samples 3.5.0: flow_ordered_list_sample.c · fetched 2026-09-06 · DOCA 3.5.0
- doca-samples 3.5.0: flow_ct_udp_sample.c · fetched 2026-09-06 · DOCA 3.5.0
- doca-samples 3.5.0: samples/doca_flow/README.md (build and flags) · fetched 2026-09-06 · DOCA 3.5.0
- DOCA Libraries (index with quality levels) · fetched 2026-09-06 · DOCA 3.5.0
- doca-samples 3.5.0 full git tree (sample inventory) · fetched 2026-09-06 · DOCA 3.5.0
- doca-samples 3.5.0: flow_switch_common.c · fetched 2026-09-06 · DOCA 3.5.0
The same idea elsewhere
Other lessons that cover this ground, sometimes from another course's angle.