random_uint32() is a xorshift32 PRNG (see lib/random.c): it holds many
properties including uniformity and reversibility, so recovering the
internal state from any single observed output lets an attacker compute
every other output of that (per-thread) stream, forward and backward.
Using it for anything security-relevant is documented in random.c
itself as inappropriate.

ct->hash_basis salts every conntrack hash table, to include
connection, zone, timeout policy and ALG expectation lookups and it is
also the secret input to NAT tuple selection.  Most NAT tuple
selection does not call random_uint32() directly.  Instead,
nat_get_unique_tuple() picks both the NAT
address (get_addr_in_range()) and, in the common case, the NAT
port (set_sport_range()/set_dport_range()) via nat_range_hash(), a
deterministic hash of the flow tuple mixed with that single 32-bit
value, which was previously drawn once from random_uint32() at
conntrack_init() time.  An attacker able to recover the xorshift state
feeding that one random_uint32() call, e.g. by observing any other
output drawn from the same per-thread PRNG stream elsewhere in the
process, would learn the basis and could then predict every NAT tuple
ovs-vswitchd assigns for the lifetime of the process.

Two narrower paths draw directly from random_uint32() per new
connection rather than through the hash: the 'nat(...,random)' port
path, and the retry offset used when the hash-selected port collides
with an existing connection under range congestion.  These are
directly observable per-connection outputs.

Fix this by drawing ct->hash_basis, and the two per-connection port
paths above, from a cryptographic source: the recently added CSPRNG
support in random.c which is a ChaCha20 based CSPRNG.  Conntrack
randomness is then independent of every other random_uint32()
consumer, and the per-connection draws are no longer part of a
reversible stream.  A zero basis is redrawn because
nat_get_unique_tuple() reads it as the NAT_PERSISTENT case.

This does not, by itself, make hash-selected tuples unpredictable:
nat_range_hash() has only the 32-bit basis as a secret, so an attacker
observing a few translated tuples can still search for it offline.
Hardening the selection function against that is a separate problem.

Assisted-by: LLM (claude, opencode, sonnet, opus, qwen3.8)
Signed-off-by: Aaron Conole <[email protected]>
---
v2->v3:
  * Rename the nat_random_uint32() function to ct_random_uint32()
  * Document the narrower case this solves (just fixes the nat(random) case
    rather than claiming the entire range predictability).

 lib/conntrack.c | 23 ++++++++++++++++++++---
 1 file changed, 20 insertions(+), 3 deletions(-)

diff --git a/lib/conntrack.c b/lib/conntrack.c
index f84cdd216a..7c1001eb58 100644
--- a/lib/conntrack.c
+++ b/lib/conntrack.c
@@ -55,6 +55,23 @@ COVERAGE_DEFINE(conntrack_l4csum_err);
 COVERAGE_DEFINE(conntrack_lookup_natted_miss);
 COVERAGE_DEFINE(conntrack_zone_full);
 
+/* Conntrack salts its hash tables with a random value.  That value,
+ * and the fully-random NAT port paths, must not be predictable from
+ * an observed random_uint32() output.  Because nat_get_unique_tuple()
+ * interprets a zero value as the NAT_PERSISTENT case this never
+ * returns 0. */
+static uint32_t
+ct_random_uint32(void)
+{
+    uint32_t value;
+
+    do {
+        value = cs_random_uint32();
+    } while (!value);
+
+    return value;
+}
+
 struct conn_lookup_ctx {
     struct conn_key key;
     struct conn *conn;
@@ -252,7 +269,7 @@ conntrack_init(void)
     /* This value can be used during init (e.g. timeout_policy_init()),
      * set it first to ensure it is available.
      */
-    ct->hash_basis = random_uint32();
+    ct->hash_basis = ct_random_uint32();
 
     ovs_rwlock_init(&ct->resources_lock);
     ovs_rwlock_wrlock(&ct->resources_lock);
@@ -2557,7 +2574,7 @@ another_round:
 
     if (attempts < range && attempts >= 16) {
         attempts /= 2;
-        curr = min + (random_uint32() % range);
+        curr = min + (ct_random_uint32() % range);
         goto another_round;
     }
 
@@ -2613,7 +2630,7 @@ nat_get_unique_tuple(struct conntrack *ct, struct conn 
*conn,
     hash = nat_range_hash(fwd_key, basis, nat_info);
 
     if (nat_info->nat_flags & NAT_RANGE_RANDOM) {
-        port_off = random_uint32();
+        port_off = ct_random_uint32();
     } else if (basis) {
         port_off = hash;
     } else {
-- 
2.56.0

_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to