Repository: githedgehog/dataplane
Pull request #340: Simple packet hash for ip traffic
Author: login: Fredi-raspall
Created: 2025-03-09T19:13:44Z | Merged: 2025-03-10T17:09:05Z

@daniel-noland I was going to move Packet to a separate crate (having packet_metadata and this new packet_hash as submodules), but did not do that since I remembered you saying that you had that change in a separate branch.


Diff:
diff --git a/Cargo.lock b/Cargo.lock
index a31e5de041..72ee3c8e2e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -17,6 +17,18 @@ version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
 
+[[package]]
+name = "ahash"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "version_check",
+ "zerocopy 0.7.35",
+]
+
 [[package]]
 name = "aho-corasick"
 version = "1.1.3"
@@ -377,6 +389,7 @@ dependencies = [
 name = "dataplane"
 version = "0.1.0"
 dependencies = [
+ "ahash",
  "clap",
  "ctrlc",
  "dataplane-id",
diff --git a/Cargo.toml b/Cargo.toml
index 810850f03e..34b7b2ea68 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -35,6 +35,7 @@ net = { path = "./net" }
 routing = { path = "./routing" }
 
 # External
+ahash = { version = "0.8.11", default-features = false, features = [] }
 arbitrary = { version = "1.4.1", default-features = false, features = [] }
 arrayvec = { version = "0.7.6", default-features = false, features = [] }
 bindgen = { version = "0.71.1", default-features = false, features = [] }
diff --git a/dataplane/Cargo.toml b/dataplane/Cargo.toml
index 999e54b0a9..34cc67d798 100644
--- a/dataplane/Cargo.toml
+++ b/dataplane/Cargo.toml
@@ -6,6 +6,7 @@ publish = false
 license = "Apache-2.0"
 
 [dependencies]
+ahash = { workspace = true }
 clap = { workspace = true, features = ["derive"] }
 ctrlc = { workspace = true, features = ["termination"] }
 dpdk = { workspace = true }
diff --git a/dataplane/src/main.rs b/dataplane/src/main.rs
index 9cbc3ba826..085ebbaf37 100644
--- a/dataplane/src/main.rs
+++ b/dataplane/src/main.rs
@@ -8,6 +8,7 @@
 mod args;
 mod nat;
 mod packet;
+mod packet_hash;
 mod packet_meta;
 mod pipeline;
 
diff --git a/dataplane/src/packet_hash.rs b/dataplane/src/packet_hash.rs
new file mode 100644
index 0000000000..2cc7eb709e
--- /dev/null
+++ b/dataplane/src/packet_hash.rs
@@ -0,0 +1,54 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright Open Network Fabric Authors
+//
+//! Module to compute packet hashes
+
+use super::Packet;
+use ahash::AHasher;
+use net::buffer::PacketBufferMut;
+use net::headers::{Net, Transport, TryHeaders, TryIp, TryTransport};
+use std::hash::{Hash, Hasher};
+
+impl<Buf: PacketBufferMut> Packet<Buf> {
+    #[allow(unused)]
+    /// Computes a hash over a `Packet` object if it contains an ipv4 or ipv6 packet,
+    /// using invariant fields of the ip header and common transport headers,
+    /// if present, using the specified Hasher.
+    pub fn hash_ip<H: Hasher>(&self, state: &mut H) {
+        if let Some(ip) = self.headers().try_ip() {
+            match ip {
+                Net::Ipv4(ipv4) => {
+                    ipv4.source().hash(state);
+                    ipv4.destination().hash(state);
+                    ipv4.protocol().hash(state);
+                }
+                Net::Ipv6(ipv6) => {
+                    ipv6.source().hash(state);
+                    ipv6.destination().hash(state);
+                    ipv6.next_header().hash(state);
+                }
+            }
+            if let Some(transport) = self.headers().try_transport() {
+                match transport {
+                    Transport::Tcp(tcp) => {
+                        tcp.source().hash(state);
+                        tcp.destination().hash(state);
+                    }
+                    Transport::Udp(udp) => {
+                        udp.source().hash(state);
+                        udp.destination().hash(state);
+                    }
+                    &Transport::Icmp4(_) | &Transport::Icmp6(_) => {}
+                }
+            }
+        }
+    }
+
+    #[allow(unused)]
+    /// Uses the ip hash `Packet` method to provide a value in the range [first, last].
+    pub fn packet_hash_ecmp(&self, first: u8, last: u8) -> u64 {
+        let mut hasher = AHasher::default();
+        self.hash_ip(&mut hasher);
+        hasher.finish() % u64::from(last - first + 1) + u64::from(first)
+    }
+}


Reviews:
user: login: qmonnet
body: Looks good, thanks! Some questions/observations:

- Why ahash?

- Maybe add some unit test, for example simply trying to hash a packet and check that we obtain a hash within the bounds?
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-10T13:36:31Z
user: login: qmonnet
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-10T15:50:09Z
user: login: Fredi-raspall
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T17:01:11Z

Review comments:
diff_hunk: @@ -0,0 +1,59 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright Open Network Fabric Authors
+//
+//! Module to compute packet hashes
+
+use super::Packet;
+use net::buffer::PacketBufferMut;
+use net::headers::{TryHeaders, TryTransport};
+
+use ahash::AHasher;
+use net::headers::Net;
+use net::headers::Transport;
+use net::headers::TryIp;
+use std::hash::Hash;
+use std::hash::Hasher;
path: dataplane/src/packet_hash.rs
user: login: qmonnet
body: Formatting nit:

```suggestion
use ahash::AHasher;
use net::buffer::PacketBufferMut;
use net::headers::{Net, Transport, TryHeaders, TryIp, TryTransport};
use std::hash::{Hash, Hasher};
```

(Not tested.)
created_at: 2025-03-10T13:33:06Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,59 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright Open Network Fabric Authors
+//
+//! Module to compute packet hashes
+
+use super::Packet;
+use net::buffer::PacketBufferMut;
+use net::headers::{TryHeaders, TryTransport};
+
+use ahash::AHasher;
+use net::headers::Net;
+use net::headers::Transport;
+use net::headers::TryIp;
+use std::hash::Hash;
+use std::hash::Hasher;
path: dataplane/src/packet_hash.rs
user: login: Fredi-raspall
body: @qmonnet I have manually cleaned up the imports since you said the above wasn't tested.
Thanks!
created_at: 2025-03-10T17:01:11Z
author_association: CONTRIBUTOR

PR comments:
user: login: Fredi-raspall
created_at: 2025-03-10T14:21:10Z
body: > Looks good, thanks! Some questions/observations:
> 
> * Why ahash?

It should be in principle significantly faster than the default hasher.

author_association: CONTRIBUTOR
user: login: Fredi-raspall
created_at: 2025-03-10T14:44:39Z
body: > * Maybe add some unit test, for example simply trying to hash a packet and check that we obtain a hash within the bounds?

That can be done, yes. However, we will need to build a number of packets. Isn't https://github.com/githedgehog/dataplane/pull/340/files#diff-b9135b759a2b2802c52b1e1943f1fea907c86f6e8c7ccf28586a4010d41cf959R57 
just enough for that ?

author_association: CONTRIBUTOR
user: login: qmonnet
created_at: 2025-03-10T14:50:11Z
body: Can't you simply reuse `build_test_ipv4_packet()` like in other dataplane tests?
It would be nice to have some test if it's straightforward to do, otherwise if generating the required boilerplate is too much work then yes, I can live with your current code :slightly_smiling_face: 
author_association: MEMBER
user: login: Fredi-raspall
created_at: 2025-03-10T15:46:16Z
body: > Can't you simply reuse `build_test_ipv4_packet()` like in other dataplane tests? It would be nice to have some test if it's straightforward to do, otherwise if generating the required boilerplate is too much work then yes, I can live with your current code 🙂

 `build_test_ipv4_packet()` has the addresses hardcoded and won't be of much help here. I believe I have a variant of it that allows setting distinct addresses and the like in a branch that has not yet been merged. I can add such tests when those test utils are merged if that's fine with you ?
author_association: CONTRIBUTOR
user: login: qmonnet
created_at: 2025-03-10T15:49:52Z
body: Sure, fine by me, thank you!
author_association: MEMBER
user: login: github-actions[bot]
created_at: 2025-03-10T17:04:03Z
body: ### :warning: One or more optional CI steps have failed!

_[click here to lament thy folly](https://github.com/githedgehog/dataplane/actions/runs/13770407833)_
author_association: CONTRIBUTOR
user: login: qmonnet
created_at: 2025-03-10T17:15:12Z
body: Looks like we're getting some additional changes to Cargo.lock that should have been committed, although I'm not 100% certain they're related to this PR. Doesn't mind much, we'll update Cargo.lock next time someone pushes code.
author_association: MEMBER