Receive Side Scaling: the Toeplitz flow hash and per-owner steering
0

Configure Feed

Select the types of activity you want to include in your feed.

rss: new package for the RSS Toeplitz flow hash and steering

Flow-to-owner steering is currently entangled in Tcp.Shard, so anything
else that wants it (the firewall's sharded NAT) would drag in the whole
TCP stack. Factor the concern into a dependency-light package (ipaddr
only) that the TCP stack and the firewall both consume: the Toeplitz hash
(Microsoft RSS / Intel 82599, verified against the canonical verification
vectors) exposed asymmetric and symmetric, an owner reduction, and the RFC
6335 dynamic-port lanes that keep a flow's return traffic on its owner. The
opam name is nox-rss because rss is the RSS-feed library.

author
Thomas Gazagnaire
date (Jul 22, 2026, 4:34 PM UTC) commit 417cd1d0
+600
+5
.gitignore
··· 1 + _build/ 2 + *.install 3 + .merlin 4 + *.opam.locked 5 + _opam/
+1
.ocamlformat
··· 1 + version = 0.29.0
+15
LICENSE.md
··· 1 + ISC License 2 + 3 + Copyright (c) 2026 Thomas Gazagnaire <thomas@gazagnaire.org> 4 + 5 + Permission to use, copy, modify, and/or distribute this software for any 6 + purpose with or without fee is hereby granted, provided that the above 7 + copyright notice and this permission notice appear in all copies. 8 + 9 + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+83
README.md
··· 1 + # nox-rss 2 + 3 + Receive Side Scaling (RSS): the Toeplitz flow hash a NIC uses to steer a packet 4 + to a receive queue, plus the pieces a share-nothing software stack needs to do 5 + the same across cores. 6 + 7 + > The opam name is `nox-rss` because `rss` is already the RSS *feed* library. 8 + > This is *Receive Side Scaling*, not Really Simple Syndication. 9 + 10 + ## Overview 11 + 12 + Distributing a network flow across N independent owners (NIC queues, CPU cores, 13 + share-nothing stacks) needs three things, and this library is exactly those 14 + three: 15 + 16 + 1. **A flow hash.** The Toeplitz hash over the connection tuple, with Microsoft's 17 + 40-byte default key -- the exact hash hardware RSS computes (Intel 82599 18 + datasheet 7.1.2.8.3). Exposed both **asymmetric** (what a NIC computes; 19 + swapping the endpoints changes the result) and **symmetric** (the endpoints 20 + are ordered first, so both directions of a flow hash equal -- what a NAT or 21 + per-core stack needs to keep a bidirectional flow on one owner). 22 + 2. **An owner reduction.** The RSS indirection that folds a 32-bit hash onto 23 + `count` owners. 24 + 3. **Ephemeral-port lanes (RFC 6335).** Each owner allocates its source/NAT 25 + ports from its own stride of the dynamic range (49152-65535), so a reply 26 + addressed to one of those ports steers back to the owner that opened the 27 + flow -- the piece that makes NAT and connect() shardable. 28 + 29 + I/O-free and dependency-light (`ipaddr` only): it computes indices and ports, 30 + it does not touch the network. Framing and dispatch belong to the caller. 31 + 32 + ## Usage 33 + 34 + <!-- $MDX skip --> 35 + ```sh 36 + $ opam install nox-rss 37 + ``` 38 + 39 + Steer a flow to one of `count` share-nothing owners, both directions alike: 40 + 41 + ```ocaml 42 + let steer ~count ~src ~dst ~src_port ~dst_port = 43 + let h = Rss.hash ~symmetric:true ~src ~dst ~ports:(src_port, dst_port) () in 44 + Rss.owner ~count h 45 + ``` 46 + 47 + An owner allocates its ports from its own lane so replies come back to it: 48 + 49 + ```ocaml 50 + let my_first_port ~count ~owner = Rss.first_port ~count ~owner 51 + let owner_of_reply ~count port = Rss.port_owner ~count port 52 + ``` 53 + 54 + ## API 55 + 56 + - `Rss.toeplitz ~key data` - the raw 32-bit Toeplitz hash (asymmetric). 57 + - `Rss.hash ~src ~dst ?ports ()` - hash a flow tuple; `~symmetric:true` for 58 + direction-independent flow affinity. 59 + - `Rss.owner ~count hash` - reduce a hash to one of `count` owners. 60 + - `Rss.port_owner ~count port` / `Rss.first_port ~count ~owner` - RFC 6335 61 + dynamic-port lanes. 62 + - `Rss.encode_flow_id` / `decode_flow_id` / `owner_of_flow_id` - embed the owner 63 + in a cross-owner flow identifier. 64 + 65 + ## Related Work 66 + 67 + RSS and its symmetric variant are standard practice; this is a pure-OCaml 68 + implementation of the same algorithms. 69 + 70 + - [Microsoft RSS](https://learn.microsoft.com/en-us/windows-hardware/drivers/network/introduction-to-receive-side-scaling) - 71 + the original NDIS RSS specification and the Toeplitz hash / default key. 72 + - [DPDK `rte_thash`](https://doc.dpdk.org/api/rte__thash_8h.html) - DPDK's 73 + Toeplitz hash; its `app/test/test_thash.c` carries the Intel 82599 74 + verification vectors this library is tested against. 75 + - Linux [RSS / RPS / RFS](https://docs.kernel.org/networking/scaling.html) - 76 + hardware and software receive steering; the symmetric-hash technique for 77 + bidirectional flow affinity. 78 + - [`rss`](https://opam.ocaml.org/packages/rss/) - the unrelated OCaml 79 + RSS/Atom *feed* library that owns the `rss` opam name. 80 + 81 + ## License 82 + 83 + ISC License. See [LICENSE.md](LICENSE.md) for details.
+8
dune
··· 1 + (env 2 + (dev 3 + (flags 4 + (:standard %{dune-warnings})))) 5 + 6 + (mdx 7 + (files README.md) 8 + (libraries nox-rss ipaddr))
+33
dune-project
··· 1 + (lang dune 3.21) 2 + (using mdx 0.4) 3 + 4 + (name nox-rss) 5 + 6 + (generate_opam_files true) 7 + (implicit_transitive_deps false) 8 + 9 + (license ISC) 10 + (authors "Thomas Gazagnaire <thomas@gazagnaire.org>") 11 + (maintainers "Thomas Gazagnaire <thomas@gazagnaire.org>") 12 + 13 + (source (tangled gazagnaire.org/ocaml-rss)) 14 + 15 + (package 16 + (name nox-rss) 17 + (synopsis "Receive Side Scaling: the Toeplitz flow hash and per-owner steering") 18 + (tags (org:blacksun network)) 19 + (description 20 + "The Toeplitz flow hash a NIC uses for Receive Side Scaling (Microsoft RSS, \ 21 + Intel 82599 datasheet 7.1.2.8.3), exposed both asymmetric (the exact hash \ 22 + hardware computes) and symmetric (both directions of a flow hash equal), \ 23 + with an owner-selection reduction and the RFC 6335 dynamic-port lanes that \ 24 + let a share-nothing software stack steer a flow -- and its return traffic -- \ 25 + to one of N owners (queues, cores, shards). The opam name is nox-rss because \ 26 + `rss' is the RSS-feed library; this is Receive Side Scaling.") 27 + (depends 28 + (ocaml (>= 4.14)) 29 + ipaddr 30 + (alcotest :with-test) 31 + (alcobar :with-test) 32 + (fmt :with-test) 33 + (mdx :with-test)))
+21
fuzz/dune
··· 1 + (executable 2 + (name fuzz) 3 + (libraries nox-rss ipaddr alcobar)) 4 + 5 + (rule 6 + (alias runtest) 7 + (enabled_if 8 + (<> %{profile} afl)) 9 + (deps fuzz.exe) 10 + (action 11 + (run %{exe:fuzz.exe}))) 12 + 13 + (rule 14 + (alias fuzz) 15 + (enabled_if 16 + (= %{profile} afl)) 17 + (deps fuzz.exe) 18 + (action 19 + (progn 20 + (run %{exe:fuzz.exe} --gen-corpus corpus) 21 + (run afl-fuzz -V 60 -i corpus -o _fuzz -- %{exe:fuzz.exe} @@))))
+1
fuzz/fuzz.ml
··· 1 + let () = Alcobar.run "rss" [ Fuzz_rss.suite ]
+50
fuzz/fuzz_rss.ml
··· 1 + open Alcobar 2 + 3 + let v4 n = Ipaddr.V4 (Ipaddr.V4.of_int32 n) 4 + 5 + (* Symmetric mode must be direction-independent for every flow. *) 6 + let test_symmetric a b sp dp = 7 + check_eq ~pp:pp_int 8 + (Rss.hash ~symmetric:true ~src:(v4 a) ~dst:(v4 b) ~ports:(sp, dp) ()) 9 + (Rss.hash ~symmetric:true ~src:(v4 b) ~dst:(v4 a) ~ports:(dp, sp) ()) 10 + 11 + (* [owner] always lands inside [0, count). *) 12 + let test_owner_range a b count0 = 13 + let count = 1 + (count0 land 0xff) in 14 + let o = Rss.owner ~count (Rss.hash ~src:(v4 a) ~dst:(v4 b) ()) in 15 + check (o >= 0 && o < count) 16 + 17 + (* Toeplitz never raises on arbitrary key/data lengths (including empty, and a 18 + key shorter than the data). *) 19 + let test_toeplitz_total key data = ignore (Rss.toeplitz ~key data) 20 + 21 + (* An owner's own lane always steers back to it (RFC 6335 partition). *) 22 + let test_lane count0 owner0 = 23 + let count = 1 + count0 in 24 + let owner = owner0 mod count in 25 + check (Rss.port_owner ~count (Rss.first_port ~count ~owner) = Some owner) 26 + 27 + (* Flow-id encode/decode round-trips, and recovers the owner. *) 28 + let test_flow_id count0 owner0 local = 29 + let count = 1 + (count0 land 0xff) in 30 + let owner = owner0 mod count in 31 + let g = Rss.encode_flow_id ~count ~owner local in 32 + if local > 0 then begin 33 + check_eq ~pp:pp_int (Rss.decode_flow_id ~count g) local; 34 + check_eq ~pp:pp_int (Rss.owner_of_flow_id ~count g) owner 35 + end 36 + else check_eq ~pp:pp_int g local 37 + 38 + let suite = 39 + ( "rss", 40 + [ 41 + test_case "symmetric both directions equal" 42 + [ int32; int32; uint16; uint16 ] 43 + test_symmetric; 44 + test_case "owner in range" [ int32; int32; uint8 ] test_owner_range; 45 + test_case "toeplitz never crashes" [ bytes; bytes ] test_toeplitz_total; 46 + test_case "RFC 6335 port lane round-trip" 47 + [ range (Rss.dynamic_port_count - 1); uint16 ] 48 + test_lane; 49 + test_case "flow-id round-trip" [ uint8; uint16; int16 ] test_flow_id; 50 + ] )
+4
fuzz/fuzz_rss.mli
··· 1 + (** Fuzz tests for the RSS flow hash and steering. *) 2 + 3 + val suite : string * Alcobar.test_case list 4 + (** Alcobar fuzz test suite. *)
+4
lib/dune
··· 1 + (library 2 + (name rss) 3 + (public_name nox-rss) 4 + (libraries ipaddr))
+101
lib/rss.ml
··· 1 + (* Microsoft's 40-byte default RSS hash key (Intel 82599 datasheet 7.1.2.8.3). *) 2 + let default_key = 3 + "\x6d\x5a\x56\xda\x25\x5b\x0e\xc2\x41\x67\x25\x3d\x43\xa3\x8f\xb0\xd0\xca\x2b\xcb\xae\x7b\x30\xb4\x77\xcb\x2d\xa3\x80\x30\xf2\x0c\x6a\x42\xb7\x3b\xbe\xac\x01\xfa" 4 + 5 + let mask32 = 0xFFFFFFFF 6 + 7 + (* The Toeplitz hash walks the data most-significant-bit first, maintaining a 8 + 32-bit window into the key that advances one bit per data bit; a set data bit 9 + XORs the current window into the result. *) 10 + let toeplitz ~key data = 11 + let klen = String.length key in 12 + let key_bit j = 13 + if j >= klen * 8 then 0 14 + else 15 + (Char.code (String.unsafe_get key (j lsr 3)) lsr (7 - (j land 7))) land 1 16 + in 17 + let window = ref 0 in 18 + for b = 0 to 3 do 19 + let byte = if b < klen then Char.code (String.unsafe_get key b) else 0 in 20 + window := (!window lsl 8) lor byte 21 + done; 22 + let result = ref 0 in 23 + let nbits = String.length data * 8 in 24 + for i = 0 to nbits - 1 do 25 + let bit = 26 + (Char.code (String.unsafe_get data (i lsr 3)) lsr (7 - (i land 7))) land 1 27 + in 28 + if bit = 1 then result := !result lxor !window; 29 + window := (!window lsl 1) lor key_bit (i + 32) land mask32 30 + done; 31 + !result land mask32 32 + 33 + let octets = function 34 + | Ipaddr.V4 ip -> Ipaddr.V4.to_octets ip 35 + | Ipaddr.V6 ip -> Ipaddr.V6.to_octets ip 36 + 37 + let port_bytes p = 38 + let b = Bytes.create 2 in 39 + Bytes.set_uint16_be b 0 p; 40 + Bytes.unsafe_to_string b 41 + 42 + (* Order two endpoints (address, then port) so the symmetric hash sees the same 43 + tuple whichever direction a flow's packet is travelling in. *) 44 + let compare_endpoint (ip0, p0) (ip1, p1) = 45 + match Ipaddr.compare ip0 ip1 with 0 -> Int.compare p0 p1 | c -> c 46 + 47 + let hash ?(key = default_key) ?(symmetric = false) ~src ~dst ?ports () = 48 + let sp, dp = match ports with Some (sp, dp) -> (sp, dp) | None -> (0, 0) in 49 + let (a_ip, a_p), (b_ip, b_p) = 50 + if symmetric && compare_endpoint (src, sp) (dst, dp) > 0 then 51 + ((dst, dp), (src, sp)) 52 + else ((src, sp), (dst, dp)) 53 + in 54 + let ports_bytes = 55 + match ports with Some _ -> port_bytes a_p ^ port_bytes b_p | None -> "" 56 + in 57 + toeplitz ~key (octets a_ip ^ octets b_ip ^ ports_bytes) 58 + 59 + let check_count who count = 60 + if count < 1 then invalid_arg (who ^ ": count must be positive") 61 + 62 + (* The default RSS indirection: a 128-entry table filled [bucket i -> i], the 63 + hash's low 7 bits picking the bucket, folded onto [count] owners. *) 64 + let owner ~count hash = 65 + check_count "Rss.owner" count; 66 + hash land 0x7f mod count 67 + 68 + let dynamic_port_first = 49152 69 + let dynamic_port_count = 65536 - dynamic_port_first 70 + 71 + let check_lane_count who count = 72 + check_count who count; 73 + if count > dynamic_port_count then 74 + invalid_arg (who ^ ": count exceeds the dynamic port range") 75 + 76 + let port_owner ~count port = 77 + check_lane_count "Rss.port_owner" count; 78 + if port < dynamic_port_first || port > 65535 then None 79 + else Some ((port - dynamic_port_first) mod count) 80 + 81 + let first_port ~count ~owner = 82 + check_lane_count "Rss.first_port" count; 83 + if owner < 0 || owner >= count then 84 + invalid_arg "Rss.first_port: owner out of range"; 85 + dynamic_port_first + owner 86 + 87 + let check_owner who count owner = 88 + check_count who count; 89 + if owner < 0 || owner >= count then invalid_arg (who ^ ": owner out of range") 90 + 91 + let encode_flow_id ~count ~owner local = 92 + check_owner "Rss.encode_flow_id" count owner; 93 + if local <= 0 then local else (local * count) + owner 94 + 95 + let decode_flow_id ~count global = 96 + check_count "Rss.decode_flow_id" count; 97 + if global <= 0 then global else global / count 98 + 99 + let owner_of_flow_id ~count global = 100 + check_count "Rss.owner_of_flow_id" count; 101 + if global <= 0 then 0 else global mod count
+87
lib/rss.mli
··· 1 + (** Receive Side Scaling (RSS): the Toeplitz flow hash a NIC uses to steer a 2 + packet to a receive queue, plus the pieces a share-nothing software stack 3 + needs to do the same across cores -- an owner-selection reduction and the 4 + RFC 6335 dynamic-port lanes that keep both directions of a flow (and its 5 + return traffic) on one owner. 6 + 7 + The hash and its 40-byte default key are Microsoft's RSS definition (Intel 8 + 82599 datasheet 7.1.2.8.3, "RSS Verification Suite"). It is exposed two 9 + ways: the default of {!hash} (and the raw {!toeplitz}) is the exact 10 + {e asymmetric} hash hardware computes -- swapping a flow's endpoints changes 11 + the result; [~symmetric:true] sorts the endpoints first so both directions 12 + hash equal, the mode a NAT or per-core stack needs to pin a bidirectional 13 + flow to one owner. *) 14 + 15 + val default_key : string 16 + (** Microsoft's 40-byte default RSS hash key (Intel 82599 7.1.2.8.3). *) 17 + 18 + val toeplitz : key:string -> string -> int 19 + (** [toeplitz ~key data] is the 32-bit Toeplitz hash of [data] under [key]: for 20 + each bit of [data] taken most-significant-first, if the bit is set the 21 + result is XORed with the 32-bit window of [key] starting at that bit index, 22 + the window advancing one bit per input bit. [key] should be at least 23 + [String.length data + 4] bytes; bytes past its end read as zero. The result 24 + is a non-negative 32-bit int (requires a 64-bit [int]). This is the 25 + asymmetric hardware-RSS hash. *) 26 + 27 + (** {1 Flow hashing} *) 28 + 29 + val hash : 30 + ?key:string -> 31 + ?symmetric:bool -> 32 + src:Ipaddr.t -> 33 + dst:Ipaddr.t -> 34 + ?ports:int * int -> 35 + unit -> 36 + int 37 + (** [hash ~src ~dst ?ports ()] is the RSS hash of a flow: {!toeplitz} over the 38 + tuple [src ++ dst] (the address bytes, 4 for IPv4, 16 for IPv6), followed by 39 + [src_port ++ dst_port] (two bytes each, big-endian) when [ports] is given. 40 + [key] defaults to {!default_key}. With [symmetric:true] (default [false]) 41 + the two endpoints -- each an address with its port -- are ordered before the 42 + tuple is built, so [hash ~src:a ~dst:b] equals [hash ~src:b ~dst:a] and both 43 + directions of a flow select one owner. [src] and [dst] must be the same 44 + address family. *) 45 + 46 + (** {1 Owner selection} *) 47 + 48 + val owner : count:int -> int -> int 49 + (** [owner ~count hash] reduces a 32-bit [hash] to one of [count] owners 50 + (queues, cores, shards) through a 128-entry RSS indirection, so a 51 + well-distributed hash spreads evenly. 52 + @raise Invalid_argument if [count] is not positive. *) 53 + 54 + (** {1 Ephemeral-port lanes (RFC 6335)} *) 55 + 56 + val dynamic_port_first : int 57 + (** [49152], the first port of the RFC 6335 Dynamic/Private range. *) 58 + 59 + val dynamic_port_count : int 60 + (** [16384], the size of the RFC 6335 Dynamic/Private range ([49152]-[65535]). 61 + *) 62 + 63 + val port_owner : count:int -> int -> int option 64 + (** [port_owner ~count port] is the owner of a dynamic [port]: 65 + [(port - 49152) mod count] for a port in the RFC 6335 range, [None] 66 + otherwise. An owner allocates its source or NAT ports from its own lane 67 + ({!first_port} and every [count]-th port above it), so a reply addressed to 68 + that port steers back to the owner that opened the flow. *) 69 + 70 + val first_port : count:int -> owner:int -> int 71 + (** [first_port ~count ~owner] is [owner]'s lowest dynamic port; its lane is 72 + that port and every [count]-th port above it, up to [65535]. *) 73 + 74 + (** {1 Cross-owner flow identifiers} *) 75 + 76 + val encode_flow_id : count:int -> owner:int -> int -> int 77 + (** [encode_flow_id ~count ~owner local] embeds [owner] in a positive [local] 78 + identifier so it is globally unique across owners; a non-positive [local] 79 + (an edge-owned sentinel) passes through unchanged. *) 80 + 81 + val decode_flow_id : count:int -> int -> int 82 + (** [decode_flow_id ~count global] recovers the owner-local identifier from 83 + {!encode_flow_id}. *) 84 + 85 + val owner_of_flow_id : count:int -> int -> int 86 + (** [owner_of_flow_id ~count global] recovers the owner embedded in [global] by 87 + {!encode_flow_id] ([0] for a non-positive, edge-owned identifier). *)
+38
nox-rss.opam
··· 1 + # This file is generated by dune, edit dune-project instead 2 + opam-version: "2.0" 3 + synopsis: 4 + "Receive Side Scaling: the Toeplitz flow hash and per-owner steering" 5 + description: 6 + "The Toeplitz flow hash a NIC uses for Receive Side Scaling (Microsoft RSS, Intel 82599 datasheet 7.1.2.8.3), exposed both asymmetric (the exact hash hardware computes) and symmetric (both directions of a flow hash equal), with an owner-selection reduction and the RFC 6335 dynamic-port lanes that let a share-nothing software stack steer a flow -- and its return traffic -- to one of N owners (queues, cores, shards). The opam name is nox-rss because `rss' is the RSS-feed library; this is Receive Side Scaling." 7 + maintainer: ["Thomas Gazagnaire <thomas@gazagnaire.org>"] 8 + authors: ["Thomas Gazagnaire <thomas@gazagnaire.org>"] 9 + license: "ISC" 10 + tags: ["org:blacksun" "network"] 11 + homepage: "https://tangled.org/gazagnaire.org/ocaml-rss" 12 + bug-reports: "https://tangled.org/gazagnaire.org/ocaml-rss/issues" 13 + depends: [ 14 + "dune" {>= "3.21"} 15 + "ocaml" {>= "4.14"} 16 + "ipaddr" 17 + "alcotest" {with-test} 18 + "alcobar" {with-test} 19 + "fmt" {with-test} 20 + "mdx" {with-test} 21 + "odoc" {with-doc} 22 + ] 23 + build: [ 24 + ["dune" "subst"] {dev} 25 + [ 26 + "dune" 27 + "build" 28 + "-p" 29 + name 30 + "-j" 31 + jobs 32 + "@install" 33 + "@runtest" {with-test} 34 + "@doc" {with-doc} 35 + ] 36 + ] 37 + dev-repo: "git+https://tangled.org/gazagnaire.org/ocaml-rss" 38 + x-maintenance-intent: ["(latest)"]
+3
test/dune
··· 1 + (test 2 + (name test) 3 + (libraries nox-rss ipaddr alcotest fmt))
+1
test/test.ml
··· 1 + let () = Alcotest.run (fst Test_rss.suite) [ Test_rss.suite ]
+139
test/test_rss.ml
··· 1 + let v4 = Ipaddr.V4.of_string_exn 2 + let ip4 s = Ipaddr.V4 (v4 s) 3 + let hex n = Fmt.str "0x%08x" n 4 + 5 + let check_hash msg expected actual = 6 + Alcotest.(check string) msg (hex expected) (hex actual) 7 + 8 + let port_bytes p = 9 + let b = Bytes.create 2 in 10 + Bytes.set_uint16_be b 0 p; 11 + Bytes.unsafe_to_string b 12 + 13 + (* Microsoft RSS "Verification Suite" (Intel 82599 datasheet 7.1.2.8.3), the 14 + canonical vectors carried verbatim in DPDK's app/test/test_thash.c: each 15 + tuple's Toeplitz hash under the default key, over the addresses alone (L3) 16 + and the addresses followed by the ports (L3L4). These are published spec 17 + constants -- the independent oracle for the asymmetric hash. A live 18 + DPDK/rte_thash interop-regen pipeline (see the interop-testing skill) is the 19 + follow-up upgrade; the values are identical either way. *) 20 + let vectors = 21 + [ 22 + ("66.9.149.187", "161.142.100.80", 2794, 1766, 0x323e8fc2, 0x51ccc178); 23 + ("199.92.111.2", "65.69.140.83", 14230, 4739, 0xd718262a, 0xc626b0ea); 24 + ("24.19.198.95", "12.22.207.184", 12898, 38024, 0xd2d0a5de, 0x5c2b394a); 25 + ("38.27.205.30", "209.142.163.6", 48228, 2217, 0x82989176, 0xafc7327f); 26 + ("153.39.163.191", "202.188.127.2", 44251, 1303, 0x5d1809c5, 0x10e828a2); 27 + ] 28 + 29 + let test_l3_vectors () = 30 + List.iter 31 + (fun (s, d, _, _, l3, _) -> 32 + let data = Ipaddr.V4.to_octets (v4 s) ^ Ipaddr.V4.to_octets (v4 d) in 33 + check_hash 34 + (Fmt.str "L3 toeplitz %s->%s" s d) 35 + l3 36 + (Rss.toeplitz ~key:Rss.default_key data); 37 + check_hash 38 + (Fmt.str "L3 hash %s->%s" s d) 39 + l3 40 + (Rss.hash ~src:(ip4 s) ~dst:(ip4 d) ())) 41 + vectors 42 + 43 + let test_l3l4_vectors () = 44 + List.iter 45 + (fun (s, d, sp, dp, _, l3l4) -> 46 + let data = 47 + Ipaddr.V4.to_octets (v4 s) 48 + ^ Ipaddr.V4.to_octets (v4 d) 49 + ^ port_bytes sp ^ port_bytes dp 50 + in 51 + check_hash 52 + (Fmt.str "L3L4 toeplitz %s->%s" s d) 53 + l3l4 54 + (Rss.toeplitz ~key:Rss.default_key data); 55 + check_hash 56 + (Fmt.str "L3L4 hash %s->%s" s d) 57 + l3l4 58 + (Rss.hash ~src:(ip4 s) ~dst:(ip4 d) ~ports:(sp, dp) ())) 59 + vectors 60 + 61 + (* Symmetric mode: both directions of a flow must hash to the same value, so a 62 + share-nothing stack pins the flow to one owner. *) 63 + let test_symmetric () = 64 + List.iter 65 + (fun (s, d, sp, dp, _, _) -> 66 + let ab = 67 + Rss.hash ~symmetric:true ~src:(ip4 s) ~dst:(ip4 d) ~ports:(sp, dp) () 68 + in 69 + let ba = 70 + Rss.hash ~symmetric:true ~src:(ip4 d) ~dst:(ip4 s) ~ports:(dp, sp) () 71 + in 72 + Alcotest.(check string) 73 + (Fmt.str "symmetric %s<->%s" s d) 74 + (hex ab) (hex ba)) 75 + vectors 76 + 77 + (* The standard (asymmetric) hash is direction-dependent -- that is why hardware 78 + RSS needs symmetric handling for bidirectional flow affinity. *) 79 + let test_asymmetric_directional () = 80 + let s, d, sp, dp, _, _ = List.hd vectors in 81 + let ab = Rss.hash ~src:(ip4 s) ~dst:(ip4 d) ~ports:(sp, dp) () in 82 + let ba = Rss.hash ~src:(ip4 d) ~dst:(ip4 s) ~ports:(dp, sp) () in 83 + Alcotest.(check bool) "asymmetric is direction-dependent" true (ab <> ba) 84 + 85 + let test_owner_range () = 86 + List.iter 87 + (fun count -> 88 + List.iter 89 + (fun (s, d, sp, dp, _, _) -> 90 + let h = Rss.hash ~src:(ip4 s) ~dst:(ip4 d) ~ports:(sp, dp) () in 91 + let o = Rss.owner ~count h in 92 + Alcotest.(check bool) 93 + (Fmt.str "owner in [0,%d)" count) 94 + true 95 + (o >= 0 && o < count)) 96 + vectors) 97 + [ 1; 2; 3; 7; 16; 32 ] 98 + 99 + (* RFC 6335 lanes: an owner's first port maps back to that owner, and the whole 100 + dynamic range partitions with no port owned by two owners. *) 101 + let test_port_lanes () = 102 + for count = 1 to 16 do 103 + for owner = 0 to count - 1 do 104 + Alcotest.(check (option int)) 105 + (Fmt.str "lane count=%d owner=%d" count owner) 106 + (Some owner) 107 + (Rss.port_owner ~count (Rss.first_port ~count ~owner)) 108 + done 109 + done; 110 + Alcotest.(check (option int)) 111 + "port below the dynamic range has no owner" None 112 + (Rss.port_owner ~count:4 1024) 113 + 114 + let test_flow_id () = 115 + let count = 4 in 116 + for owner = 0 to count - 1 do 117 + for local = 1 to 5 do 118 + let g = Rss.encode_flow_id ~count ~owner local in 119 + Alcotest.(check int) "flow-id local" local (Rss.decode_flow_id ~count g); 120 + Alcotest.(check int) "flow-id owner" owner (Rss.owner_of_flow_id ~count g) 121 + done 122 + done; 123 + Alcotest.(check int) 124 + "non-positive id passes through" (-1) 125 + (Rss.encode_flow_id ~count ~owner:0 (-1)) 126 + 127 + let suite = 128 + ( "rss", 129 + [ 130 + Alcotest.test_case "82599 verification suite: L3" `Quick test_l3_vectors; 131 + Alcotest.test_case "82599 verification suite: L3L4" `Quick 132 + test_l3l4_vectors; 133 + Alcotest.test_case "symmetric both directions equal" `Quick test_symmetric; 134 + Alcotest.test_case "asymmetric direction-dependent" `Quick 135 + test_asymmetric_directional; 136 + Alcotest.test_case "owner in range" `Quick test_owner_range; 137 + Alcotest.test_case "RFC 6335 port lanes round-trip" `Quick test_port_lanes; 138 + Alcotest.test_case "flow-id round-trip" `Quick test_flow_id; 139 + ] )
+6
test/test_rss.mli
··· 1 + (** Alcotest suite for the RSS flow hash and steering: the 82599 Toeplitz 2 + verification vectors, symmetric-mode flow affinity, owner selection, the RFC 3 + 6335 port lanes, and the flow-id round-trip. *) 4 + 5 + val suite : string * unit Alcotest.test_case list 6 + (** The test cases. *)