The crypto path, and why a host frame is refused¶
A security association can be installed on this coprocessor and read back - that much was settled in octeon-tx-rpc.md. A frame asked to use one is still refused, and this page is why. Everything here is read out of the appliance's own binaries or measured on the appliance; where a reading was wrong it is left in, struck through, because the wrong readings are the expensive part.
Issue #185.
The symptom, in counters¶
A frame with the metadata that asks for encryption moves exactly two counters:
FROM_KN_TO_IPSEC_ENCR +N the fast path accepted the request
CRYPTO_DROP_SADB_PRE_ERR +N and then refused the frame
CRYPTO_DROP_SA_UNAVAILABLE 0 without ever consulting an association
The third line is the one that matters. SA_UNAVAILABLE is the counter for "the association you
named is not there"; it does not move. The failure is earlier than that.
The chain, function by function¶
From usfp's own disassembly - 2,411 functions, the fast path that runs on the coprocessor:
pmode_hwevt_worker_loop
-> crypto_pkt_submit.lto_priv.124 .text+0x4477d8
-> sadb_hw_entry_get .text+0x421f48
on NULL -> .text+0x434940, which increments counter 125
crypto_pkt_submit takes the association index out of the mbuf, not out of the packet:
ldrb w6, [x21, #3] the dynamic-field offset, registered at start-up
ldrh w1, [x19, x6] x19 is the rte_mbuf - a 16-bit read at that offset
bl sadb_hw_entry_get
sadb_hw_entry_get wants four things, and the first explains the counter:
saidx != 0 else -22 EINVAL <- the HANDLE is 1-based: SA_ADD index + 1
saidx - 1 < [ctx + 0x1a0] the table size
entry[+0x88] != 0 the valid flag
entry[0] == other[idx*0x528 + 0xd0] a revision match
Entry stride is 0x570; a parallel table has stride 0x528.
The field, decoded from the code that writes it¶
The symbol is usfp_ipsec_dynfield. worker_ordered builds the 64-bit word at .text+0x4256ec
and stores it with str x0, [x20, x4], where x4 is the same registered offset:
| bits | source | meaning |
|---|---|---|
| 0..15 | flow entry +0x26 (u16) |
the association index |
| 16..23 | flow entry +0x25 (u8) |
|
| 24, 27 | constant 0x9000000 |
flags |
| 32..47 | flow entry +0x22 (u16) |
a second index, passed on as submit's arg 5 |
| 48..63 | [x9 + 0xb0], byte-swapped |
Every source is a flow entry. proc_from_ips is the one path that does not look a flow up: at
.text+0x420f40 it takes the whole word from its caller's metadata at +8. That is the IPS return
path, not the host path.
What the host can and cannot say¶
It cannot put the index in the metadata. Measured, with an association really installed at
index 1 - SA_ADD answering rc 0x0000 and LO_SA_READ returning its 1,328 bytes with 1 in the
first word - five isolated test frames gave FROM_KN_TO_IPSEC_ENCR +5,
CRYPTO_DROP_SADB_PRE_ERR +5, CRYPTO_DROP_SA_UNAVAILABLE 0. Unchanged, which is what the code
above says must happen: nothing on the host path writes the dynamic field, so it reads zero and the
lookup fails on saidx != 0.
CORRECTED - the metadata layout was read out of the vendor's own source. The claim below, that
the host cannot put the index in the metadata, is wrong. SFOS_OSS-22.0.1_MR-1-490.iso ships
WARHOL_usfp.tar.gz, 190 files of Sophos's own GPL source for usfp_firewall, usfp_rh and
mv_pport, and include/metadata.h in it names the fields:
struct usfp_sp_md { /* Metadata to USFP from slowpath/kernel */
uint8_t md_valid; /* +0 */
uint8_t sa_is_out; /* +1 IPsec offload direction is output (encrypt) */
uint16_t _unused_0;
uint32_t _unused_1;
struct usfp_mflow_ident flow; /* +8 */
uint32_t sa_index; /* +12 IPsec offload SA index, 0 means no offload */
struct usfp_dos_md dos; /* +16 */
};
The two bytes this project found by bisection - 1 and 12 - are sa_is_out and sa_index, and the
index is a 32-bit field rather than the byte the driver was writing. So the index is carried in
the metadata, by the vendor as well as by us, and the paragraph below describes a dead end that was
entered because the archive on disk was never opened.
The coprocessor does not tell it which flow a frame belongs to. dp.rx_prefix prints the whole
82-byte prefix the far side writes in front of every delivered frame:
+00 00 00 00 00 00 00 00 ac length 172, big-endian
+08 00 00 00 00 00 00 03 80
+10 82 00 the port tag
+12 a2 99 43 b4 the vendor's signature
+16..+51 sixty bytes of zero
A usfp_mflow_ident is four bytes. It is not in there.
Programming a flow: the request is specified, the identifier is not available¶
RPC_CMD_FLOW_CREATE_FP is 10 and RPC_CMD_MFLOW_PROGRAM is 8. Read with
tools/dwarf-struct.py from the appliance's own usfp_rh.ko, which runs on the coprocessor's Linux
and is where rpc_cmd_cb_reg lives:
struct usfp_fpop_req_flow_create { /* 180 bytes */
+0x00 conn (112) conn_idx + usfp_conn_entry {atomic 8, session_id, qos 8, tcp 60, nat 24, lock 4}
+0x70 mflow_valid a TWO-BIT MASK: bit 0 programs the forward flow, bit 1 the reverse
+0x74 mflow_o (32) usfp_fpop_req_program_mflow
+0x94 mflow_r (32)
}
struct usfp_fpop_req_program_mflow { /* 32 bytes */
+0x00 usfp_mflow_ident mf_ident /* mflow_id:25, mflow_rev_num:6, mflow_valid:1 */
+0x04 usfp_mflow_entry_opr mf_opr
+0x1c uint32_t mflow_timeout
}
struct usfp_mflow_entry_opr { /* 24 bytes - so sa_index is at the request's +0x04 */
+0x00 opr_fl { sa_index:16, action:4, rsvd:3, dir:1, bridge_control:4, state:4 }
+0x04 opr_bf { l3_fwd_rev_num:16, dscp_override_val:8, dscp_override_en:1, rsvd:7 }
+0x08 conn_index +0x0c fw_state_rev_num +0x0e conn_rev_num
+0x10 nhop_index:24, nhop_rev_num:8
+0x14 sa_rev_num +0x16 rsvd
}
conn_fpop_flow_create writes the connection straight into its slot -
slot = [tbl+0x10] + conn_idx * 0x6c - after taking the entry lock at +0x68 and checking the
revision at +0x6a; a mismatch returns 7, an out-of-range index returns 1. flow_create refuses a
buffer shorter than 0xb3, confirming the 180 bytes exactly.
mflow_fpop_prog_both indexes just as plainly - entry = [[tbl+8]] + (ident & 0x1ffffff) * 0x14,
bounds-checked against [tbl+0x14] - and then refuses to do anything unless the entry is already
live:
w5 = [entry + 0x0c] mstate
tst w5, #0xff0000 ; b.eq not live -> this direction is skipped silently
rev_req = ident >> 25 & 0x3f
rev_now = mstate >> 8 & 0x3f
cmp ; b.ne revision mismatch -> skipped
So a flow cannot be conjured. It is created by the fast path when it sees traffic - mflow_alloc,
2,196 bytes, takes a hash and a count - and the host may only update one it already knows the
identity of.
And the identity cannot be found by reading¶
RPC_CMD_LO_MFLOW_READ is 40 and takes usfp_fpop_req_table_read {s_index, num_entries, flags,
e_index}. Its handler mflow_read refuses a response buffer of 11 bytes or less; the inner
mflow_fpop_read then computes what it needs as 116 bytes per entry:
needed = num_entries * 116
if (resp_size < needed) -> error 35 entries is the most that fits in 4,088
if (s_index + num_entries > table_size) -> error
if (num_entries != 0 && s_index > e_index) -> error
Asked for 37, it answers rc 0x0001 with no payload - which is the arithmetic above and not a fault.
Asked for 35 within the limit it answers rc 0x0000 with 116 bytes whose first word is -35, and
asked for 8 it answers -8: the first word is the negated request, which is this handler's way of
saying none of the entries you asked for are live.
The table holds 4,001,450 flows at hash positions. Thirty-five at a time is 114,000 round trips
to walk it, and the position is a hash of the 64-byte key, which the fast path computes with DPDK's
rte_hash - rte_hash_crc_init_alg is linked in, and usfp_hash_cuckoo_make_space_mw with it. A
rte_hash position comes out of its own key store, not out of the key.
The hash is CRC-32C over the whole key, seeded with zero¶
And it is not DPDK's rte_hash after all. usfp links its own cuckoo table -
usfp_hash_cuckoo_make_space_mw - and rte_hash_crc_init_alg, which only selects the CRC
implementation. The hash itself is eight instructions in worker_ordered at .text+0x4241c8:
mov w27, w28 the seed, and w28 is zero on this path
crc32cx w27, w27, [x29+0x158] eight 64-bit words
crc32cx w27, w27, [x29+0x160]
crc32cx w27, w27, [x29+0x168]
crc32cx w27, w27, [x29+0x170]
crc32cx w27, w27, [x29+0x178]
crc32cx w27, w27, [x29+0x180]
crc32cx w27, w27, [x29+0x188]
crc32cx w27, w27, [x29+0x190] 0x158..0x197 is 64 bytes - sizeof(usfp_mflow_key)
lsr w3, w27, #0x10 and the position is the top sixteen bits
crc32cx is the ARMv8 CRC-32C instruction, the Castagnoli polynomial, taking a 64-bit word at a
time. The seed is zero because the instruction two before this is cbnz w28, ... - this path runs
only when w28 is zero - and w27 is initialised from it.
So the position is computable by a host, from a key whose layout is already published above:
Nothing about it needs the coprocessor's cooperation. A host that knows a five-tuple can work out where the flow for it sits, read that position with command 40 to confirm the key matches, and then program it with command 8 - which is the whole of what was missing.
And then every hashed position was empty, which was the real answer¶
Sixteen candidate keys were built over the layout above - varying the MAC order, the port order and
the address order, since the published layout fixes the offsets but not which of a pair goes first -
for a five-tuple that was certainly live, and that pfctl -ss showed in both directions. Every one
of the sixteen positions answered empty, and so did indices 0 to 34.
The first word of a LO_MFLOW_READ reply is -(s_index + num_entries) - a read of two entries
at 16210 answers -16212 - which corrects the earlier reading of it as "the negated request": it is
the exclusive end of the range asked for, and it means nothing in this range is live.
Sixteen empty positions is not sixteen wrong keys. The table is empty.
The counters say why, and it is one bit¶
FPCNTR_FROM_WIRE_TO_KN_FORCED has been exactly equal to FPCNTR_RX_WIRE since boot - 4,474
for 4,474. Every frame this coprocessor has ever taken off the wire was pushed to the host. Over a
measured 25-second window of ordinary routed traffic:
RX_WIRE +60
FROM_WIRE_TO_KN_FORCED +60 every one of them
FROM_WIRE_TO_KN_MFLOW_LU_ERR 0 and not one flow lookup was even attempted
The second counter array - RPC_CMD_LO_WORKER_DBG_CNT_READ, command 43, the PD_DEBUG_CNT_
names - says the frames themselves were fine:
PD_DEBUG_CNT_IPV4_PKT +55
PD_DEBUG_CNT_UDP_PKT +55
PD_DEBUG_CNT_NA_NOT_IP +2 the only "not accelerated" reasons, and they
PD_DEBUG_CNT_NA_IPV6_MCAST +2 account for the other five frames
PD_DEBUG_CNT_NA_IPV6_EXT +1
Fifty-five frames parsed as clean IPv4/UDP with no refusal recorded against them, and were pushed to
the host anyway. The whole punt vocabulary was checked, and every other reason stands at zero:
LIF_OFFLOAD_DISABLED 37, LIF_UNKNOWN_VLAN_L3_MODE 38, NON_ACCEL 40, MFLOW_LU_ERR 41, the
four NHOP_ counters 44-47, EG_ERR 60. The LIF table agrees - LO_LIF_READ gives LIF 0 with
flags 0x0002, so fwd_mode is L3 and offload_disabled is clear.
So it is not the interface. It is global, and rpc_cmd_type names it in its first three entries -
three commands this driver had never issued. They are not a prerequisite for the rest: nothing in
the coprocessor's modules reads fw_state except these three setters, and every other command this
driver issues was already working. What they gate is acceleration.
They write the three fields of an eight-byte structure that lives in shared memory, which
fw_state_fpop_init looks up with ushmem_lookup and accepts only if its size is 8:
struct fw_state { /* 8 bytes */
+0x0 uint32_t fw_cfg
+0x4 uint16_t rev_num
+0x6 uint16_t l3_fwd_rev_num
}
And fw_cfg's bits are the vendor's own macro definitions, read out of the module's debug
information rather than inferred:
| bit | name |
|---|---|
0x001 |
FW_CFG_OFFLOAD |
0x002 |
FW_CFG_TCP_SEQ_CHK |
0x004 |
FW_CFG_IPS |
0x008 |
FW_CFG_FINTRACK |
0x010 |
FW_CFG_FP_PKT_DUMP |
0x020 |
FW_CFG_INJ_RECOVERY |
0x800 |
FW_CFG_DROP_IF_IPS_OFF |
FW_CFG_DEFAULT is defined there as FW_CFG_TCP_SEQ_CHK alone, and a coprocessor that has just
started has offload off - which is measured, not inferred from that macro: FORCED equals
RX_WIRE from the first frame. Nothing but the host can turn it on.
What the word holds besides that bit at start-up has not been read, and cannot be: rpc_cmd_type
has no command that returns fw_state, and the region it lives in belongs to the data-plane
application. The macro says TCP_SEQ_CHK; fp_state_init in usfp.elf allocates the region from
zeroed memory and nothing in usfp_rh.ko writes a default into it, so zero is at least as likely.
The consequence is practical and belongs in any code that touches this word: a write replaces a
value nobody has seen, so set every bit you want rather than the one you are changing.
That is the answer to this page's question. The crypto path was never refusing a frame on its own
account: the association index rides on a flow entry, a flow entry exists only where the fast path
accelerates, and the fast path accelerates nothing while bit 0 of fw_cfg is clear.
The three requests, decoded¶
All three are a few instructions each, and the host side of them is now in the driver. Each one is
two functions: a set_* wrapper that validates the request, and an fpop_* callee that does the
store - which is worth keeping straight, because disassembling the wrapper and looking for the store
will not find it.
fw_state_set_cfg_params 0x352c cmp w4,#3; b.ls -> refuse request >= 4 bytes
0x353c ldrh w1,[x1] a HALFWORD at offset 0
-> fw_state_fpop_cfg_params_set
0x7e2c str w1,[x0] stored as the 32-bit fw_cfg
fw_state_set_rev 0x3670 cmp w4,#1; b.ls -> refuse request >= 2 bytes
0x367c ldrh w1,[x1]
-> fw_state_fpop_rev_set
0x7df8 strh w1,[x2,#4] rev_num
0x7e0c bl mflow_fpop_invalidate_issue AND THROWS AWAY EVERY FLOW
fw_state_set_l3_fwd_rev 0x35c8 cmp w4,#1; b.ls -> refuse request >= 2 bytes
0x35d8 ldrh w1,[x1]
-> fw_state_fpop_l3_fwd_rev_set
0x7e1c strh w1,[x0,#6] l3_fwd_rev_num, no invalidate
The invalidate on the firewall revision is worth noticing on its own: it is the mechanism by which a
ruleset reload discards hardware offload, and it is why the flow, connection and next-hop entries
carry revision numbers that mflow_fpop_prog_both refuses a mismatch on.
Measured: the gate is the gate, and opening it changes one counter for another¶
Issued on the appliance, with the same traffic either side of it - a ping flood and an HTTP fetch,
so the frames arrive on the WAN front port and are genuinely from the wire. FW_CFG_PARAMS_SET
answered rc 0x0000 both times.
| counter | fw_cfg = 0x2 (control) |
fw_cfg = 0x3 |
|---|---|---|
RX_WIRE |
+65 | +101 |
TX_KN - delivered to the host |
+65 | +101 |
FROM_WIRE_TO_KN_FORCED |
+65 | 0 |
FROM_WIRE_TO_KN_MFLOW_NOT_ACTIVE |
0 | +101 |
One bit moves every frame from forced to the host without a lookup to looked up, no active flow,
punted to the host. TX_KN equals RX_WIRE in both columns: every frame still reached the
host, none was dropped and none bypassed the packet filter, which went on inserting states
throughout at 29 searches a second.
That is the gate identified and the risk of opening it measured rather than argued. It is also the
next blocker named by the hardware itself: MFLOW_NOT_ACTIVE. The flow table is reached now, and
it is empty.
The appliance was left working - twelve ports up, the bridge intact, the default route on the WAN
port, no packet loss - and fw_cfg was set to 0x2 afterwards. Not restored to 0x2: as the
section above says, the value before the first write was never read, and 0x2 is the macro's
default rather than an observed one. With OFFLOAD clear the other bits have nothing to act on, so
the two candidates are indistinguishable here; the honest record is that the word now holds 0x2
because a write put it there.
Which makes this a host-layer problem, as it had to be¶
With offload enabled the fast path looks for a flow; the table is empty, so it misses and punts, and
the host still sees every packet - which is the table above, measured. Nothing accelerates until a
flow is created, and a flow is
created by the host saying FLOW_CREATE_FP for a connection it has already decided to permit. On
this appliance the thing that decides is the host's own packet filter. So the remaining work is not
a coprocessor question at all:
FW_CFG_PARAMS_SETwithFW_CFG_OFFLOAD- one halfword, and the gate is open.FW_STATE_REV_SETon every ruleset reload, so stale flows are discarded.- A LIF per interface that forwards - exactly one exists today.
FLOW_CREATE_FPwhen the filter creates a state,MFLOW_INVALIDATEwhen it expires.MFLOW_PROGRAMwithsa_indexfor the flows matched to an IPsec policy - which is the original question on this page, and the last step rather than the first.
Opening the gate is a change with a security boundary in it, because an accelerated flow is a flow the host's filter no longer sees. It belongs behind the host's own decision, never behind a sysctl left set.
Where this leaves it¶
An association can be installed. A frame can be made to ask for encryption. The index that joins the two lives in a per-packet field written only from a flow, and a flow's identity is neither told to the host, nor returned by any command, nor findable by reading the table.
That last clause is no longer true twice over. The position is computable, and the reason no
read of one ever matched is that there was nothing anywhere to match: the flow table is empty
because FW_CFG_OFFLOAD has never been set, so the fast path has never accelerated a frame.
The question this page opened with is answered, and step 1 of the five is done: the driver issues
FW_CFG_PARAMS_SET, the gate has been opened and closed on the appliance, and the punt counter
moved from FORCED to MFLOW_NOT_ACTIVE with every frame still delivered to the host.
What is left is steps 2 to 5, and none of them is a coprocessor question. They are the host deciding, which means they belong to the host's packet filter and not to a sysctl.
Where it actually stands, with the source in hand¶
Every offset in this driver's SA_ADD request matches the vendor's struct usfp_fpop_req_sa_add
in include/sa_table.h, field for field - the keys, the SPI, the addresses, the lifetimes. One
field differed: rev_num at request offset 184, which this driver sent as a hard zero. The vendor
does not:
lx->rev += 1; /* usfp_ipsec.c, before every install */
rev = lx->rev; /* so a fresh index carries 1, never 0 */
and sa_table.h annotates the field uint16_t rev_num; /* rev in mflow */, which is the fourth
and last check in sadb_hw_entry_get.
It was tried, and it is not the answer. With rev_num = 1, md_valid = 1, sa_is_out = 1 and
sa_index = 1 as a 32-bit field, against an association installed at index 1 and read back with
its valid bit set (ctrl = 0x80001212, bit 31), the counters are unchanged:
So the first three checks in sadb_hw_entry_get pass - the index is non-zero, within the table, and
the entry is valid - and either the revision is compared against something other than what
SA_ADD carries, or the index never reaches the dynamic field at all. The next reading is the
bound check the metadata-supplied index passes through before it is stored, which is where a
failure silently zeroes the field rather than reporting itself.
The index reaches the field, and the lookup still refuses it¶
Three things were settled after the source turned up, and the third is a negative.
The fast path reads exactly what metadata.h declares. In usfp.elf, at the site that fills
the per-frame context:
426bf8 strh wzr, [x21, #0x26] clear the index
426c00 ldrb w1, [x1, #3] the gate byte
426c04 cbz w1, #0x426c10 zero gate -> leave the index cleared
426c08 ldr w1, [x2, #0xc] a 32-BIT load
426c0c strh w1, [x21, #0x26] and only its low sixteen bits are kept
x1 is two bytes below the metadata block and x2 is the block itself, so the gate is
metadata[1] - sa_is_out - and the index is the 32-bit metadata[12] - sa_index. The driver
writes both, and the low half is what the context keeps.
The table bound is not the failure. The bound check reads
425cd8 sub w2, w0, #1 index - 1
425ce0 ldr w3, [x1, #0x1a0] the table size
425ce4 cmp w3, w2, uxth
425ce8 b.ls #0x427340 size <= index-1 -> clear the index
so for index 1 it can only fire if the table is empty. LO_SA_READ answers rc 0x0000 at index
8192 and rc 0x0001 at 16384, so the table holds at least 8,193 entries.
And the encoding of the index is not the variable. The vendor packs an index and a revision
into one offload handle and puts a derived value in the metadata - USFP_NET_IPSEC_HANDLE_SA,
which is a different macro from the USFP_NET_IPSEC_HANDLE_TO_SAIDX it passes to the RPC calls -
so the obvious suspicion was that md->sa_index carries more than the index. Four encodings were
swept against one association installed at index 1 with revision 1, with about forty frames
genuinely leaving the named port each time:
md.sa_index |
FROM_KN_TO_IPSEC_ENCR |
CRYPTO_DROP_SADB_PRE_ERR |
|---|---|---|
0x00000001 |
+40 | +40 |
0x00010001 |
+41 | +41 |
0x00000101 |
+46 | +46 |
0x00010100 |
+40 | +40 |
Every frame enters the crypto stage and every frame is refused, in equal numbers, whatever is in the field. The two macros may well differ, but not in a way any of these four guesses captured.
Answered: the revision lives in the flow¶
Not inferred - read, from two of the vendor's own headers.
sa_table.h stores what SA_ADD sends, and annotates the field:
and mflow_table.h says where that is:
struct usfp_mflow_entry_opr_flags {
uint32_t sa_index :16; /* the flow names the association */
uint32_t action : 4;
...
};
struct usfp_mflow_entry_opr {
...
uint16_t sa_rev_num; /* and carries its revision */
uint16_t rsvd;
};
The coprocessor side stores the host's value plainly - ipsec_fpop.c:211,
sa->cfg.rev_num = req->opr.rev_num; - so the two sides of sadb_hw_entry_get's last check are
the association's rev_num and a flow entry's sa_rev_num.
A frame with no flow has no revision to be matched against. That is the whole of why every index tried, with every encoding, is refused: the first three checks pass and the fourth has nothing to compare. It also explains why the metadata carries an index at all - a flow names the association too, and the metadata path restates it rather than replacing it.
So the question this page opened with is closed, and the answer moves it:
An association can be installed, and a frame can name it. It cannot be validated without a flow, because the revision it is matched against lives in a flow entry and nowhere else.
#185 is blocked on #211, and that is now read rather than suspected. The vendor's transmit path
agrees: usfp_netdev_mv.c sets md->sa_index only when there is an offload handle, but populates
md->flow - mflow_valid, mflow_id, mflow_rev_num - on every frame unconditionally, because
the flow is what the fast path resolves everything else through.
The difference that pointed at it¶
The vendor's transmit path sets one thing this driver does not. From usfp_netdev_mv.c:
md->md_valid = 1;
md->sa_index = 0;
...
md->flow.mflow_valid = skb->ext_sfos.ext_fp.flowid_valid;
md->flow.mflow_id = skb->ext_sfos.ext_fp.flowid;
md->flow.mflow_rev_num = skb->ext_sfos.ext_fp.flowid_rev;
sa_index is set only when there is an offload handle, but flow is populated on every frame,
unconditionally. This driver leaves those four bytes zero, because the header is cleared and
nothing fills them.
That is now the only known difference between a frame the vendor sends and one this driver sends,
and it fits the one check still unaccounted for: sadb_hw_entry_get's revision comparison. If the
revision an association is matched against is resolved through the frame's flow rather than carried
beside its index, then a frame with no valid flow has no revision to match, and is refused exactly
as measured - no matter what index it names.
Which would mean the two halves of this investigation are the same problem after all: a flow is not needed to name an association, but may be needed to validate one. Testing it needs a live flow, and nothing creates one yet.
And the host cannot create a flow at all - by design¶
The vendor's headers say so in a comment, and its source enforces it. From mflow_table.h:
/*
* fw_valid is set by the fastpath firmware to indicate a flow is
* present in the flow table; host should read only
*/
uint8_t fw_valid;
/*
* host_valid is set by the host after a valid 'opr' entries has been
* loaded for this flow
*/
uint8_t host_valid;
and mflow_fpop.c, which is the only thing either MFLOW_PROGRAM or FLOW_CREATE_FP reaches:
if (mstate.fw_valid == 0 || (mstate.rev & MFLOW_REV_NUM_MASK) !=
cmd_data->mf_ident.mflow_rev_num) {
continue; /* skipped, silently */
}
mflow_fpop_prog_both only ever updates, and it skips an entry that is not already live without an
error, a return code or a counter. conn_fpop_flow_create writes the connection into its slot and
then calls it, so even the command named FLOW_CREATE_FP does not create a flow.
That corrects how this project has been reading the problem. The division is absolute: the fast path creates flows and the host programs them. There is no host-side call that makes one, and looking for the request layout that would was looking for something that does not exist.
So the question is not how to create a flow. It is why the fast path creates none - and with
offload enabled it reports MFLOW_NOT_ACTIVE on every frame, which is a lookup that reached an
entry and found it not live. The next thing to try is the connection table: CONN_CREATE_FP (11)
reaches conn_fpop_conn_create, which bounds-checks conn_idx against the table size and copies
the entry in, with no other precondition. A connection the fast path can find is the one thing the
host can publish unaided, and usfp_conn_entry is laid out in conn_table.h.
The connection table is ruled out, by measurement¶
CONN_CREATE_FP works. A connection was published at slot 0 with rev_num 1, verdict 0 and
state CONN_VALID, answered rc 0x0000, and LO_CONN_READ gave it back with its flags word at
0x40000001 - revision in the low sixteen bits, the state in the top two, exactly as written.
It changes nothing. With the connection live and the offload gate open, twelve seconds of a download and a ping flood:
RX_WIRE +97
TX_KN +97 every frame still delivered to the host
FROM_WIRE_TO_KN_MFLOW_NOT_ACTIVE +89
LO_MFLOW_READ 0..34 -35 nothing live
So a connection the fast path can find is not what it is waiting for, and the table that looked like the one thing the host could publish unaided turns out not to be the gate. Recorded so it is not tried again.
What MFLOW_NOT_ACTIVE says is that a lookup reached an entry and found it not live - the fast
path computes a position and looks there - so something decides whether to make that entry. The
next candidate is the next-hop table: a forwarding decision needs somewhere to forward to, the fast
path counts FROM_WIRE_TO_KN_NHOP_LU_NULL and three more NHOP_ reasons, and NHOP_PROGRAM is
command 6. A LIF per forwarding interface is the other half of the same thought, and exactly one
exists today.
And the appliance as configured has nothing the fast path could accelerate¶
A hardware forwarding offload carries a frame from one of its own ports to another of its own ports. On this appliance, as it is set up, no forwarded frame does that:
| LAN | igb0, an Intel NIC the coprocessor cannot see |
| WAN | oxp3, a coprocessor front port |
so every routed packet goes coprocessor, host, igb0. The coprocessor physically cannot carry it:
the other end is not its port. There is no flow for it to create, whatever else is published.
The LIF table says the same thing from the other side. One entry, and its MAC is ...f5:02, which
is oxp2 - a port with no cable in it. The interface the traffic actually arrives on, oxp3, has
no LIF at all.
So MFLOW_NOT_ACTIVE on every frame may not be a missing table entry. It may be the correct answer
to a question with no acceleratable traffic in it, and every host-side table published so far -
the connection, the association, the offload bit - was published for frames the hardware was never
in a position to forward.
What would test it is traffic whose two ends are both coprocessor front ports, with a LIF on each. That is a change to how the appliance is wired and addressed, not a command, so it is the owner's call rather than something to try while his internet is on the other end of it. The same caution applies to giving oxp3 a LIF: a LIF's MAC becomes that port's hardware filter, and a wrong one drops every frame arriving on it - measured, and it looks exactly like an unplugged cable.
The measurement traps this cost¶
dp.meta_tpl applies to every frame the interface path transmits, and not to the frame dp.xmit
posts. A sweep that set a template and sent one test frame per offset was measuring the
appliance's own WAN traffic: one row moved FROM_KN_TO_IPSEC_ENCR by 11. The isolated instrument is
dp.meta=0, which puts the asking pattern in the test frame itself. Leaving a template set asks the
coprocessor to encrypt the appliance's live traffic, so set it, measure, and clear it again.
The handle is 1-based and the SA_ADD index is not: a microflow or a metadata block names an
association by its index plus one, and sadb_hw_entry_get subtracts one before it indexes the
table - so naming an association by its index uses the slot below it, which can look exactly like
the fault being investigated. Measured with the SPI on the wire in
the handle is the index plus one.
An absent cause is still a cause. Every measurement on this page was taken against a fast path
that was not accelerating anything, and that was read for a long time as a fault in the crypto
stage. The counter that said otherwise - FROM_WIRE_TO_KN_FORCED standing exactly equal to
RX_WIRE - had been readable the whole time. What was missing was the comparison: a punt counter
that tracks the receive counter one for one is not a statistic, it is a mode. Checking whether one
counter equals another costs nothing, and it should have been the first question.
One FORCED reading was over-generalised. An earlier run found every frame accounted for by
PD_DEBUG_CNT_NA_IPV4_BMCAST and concluded that FORCED meant "broadcast cannot be offloaded". It
does cover that case, but it is the catch-all punt: 55 clean unicast frames with no NA_ reason
against them are counted as FORCED too. FPCNTR_FROM_WIRE_TO_KN_NON_ACCEL is the counter for a
parse refusal, and it stays at zero.
The discussion of this page is held in the project's GitHub Discussions. Showing it here loads giscus from giscus.app and GitHub, and writing in it needs a GitHub account - what that means.