Eelco Chaudron <[email protected]> writes:

> On 27 Aug 2026, at 17:23, Aaron Conole via dev wrote:
> 
> > 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.
> >
> > 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 a single 32-bit value:
> > ct->hash_basis.  That basis was previously drawn once from
> > random_uint32() at conntrack_init() time.  Because it is a single,
> > long-lived value on which every NAT address and default-port choice
> > depends, 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--could
> > predict every NAT tuple ovs-vswitchd will assign.  In effect, it was
> > derived from a shared, reversible state.
> >
> > 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: OpenSSL's PRNG when
> > compiled against OpenSSL 1.1.0+ (checking RAND_status() to catch the
> > case where it has not been seeded properly despite RAND_bytes()
> > succeeding, the same check already used in lib/stream-ssl.c), falling
> > back to the system entropy pool.
> >
> > If neither source can provide randomness, do not silently downgrade
> > to the non-cryptographic PRNG.  nat_random_uint32() instead reports
> > failure, and its callers propagate that as NAT tuple exhaustion:
> > nat_get_unique_l4() and nat_get_unique_tuple() both return false, so
> > the connection attempt is failed the same way as any other allocation
> > exhaustion (see the nat_res_exhaustion path in conn_update_state()),
> > and each occurrence increments the new conntrack_entropy_failed
> > coverage counter so operators can see it happening.  There is also a
> > new point for ovs-vswitchd failure - at the conntrack initialization
> > if sufficient entropy cannot be pulled for ct->hash_basis.
> >
> > Signed-off-by: Aaron Conole <[email protected]>
> 
> Hi Aaron,
> 
> Thanks for the patch. See some comments below. As Ilya mentioned offline,
> it might be good to get some performance numbers with this change to see
> the impact of the RAND_bytes() usage.

I'll do some testing.

> //Eelco
> 
> > ---
> >  lib/conntrack.c | 76 ++++++++++++++++++++++++++++++++++++++++++++++---
> >  1 file changed, 72 insertions(+), 4 deletions(-)
> >
> > diff --git a/lib/conntrack.c b/lib/conntrack.c
> > index f84cdd216a..d7fa6bd746 100644
> > --- a/lib/conntrack.c
> > +++ b/lib/conntrack.c
> > @@ -22,6 +22,10 @@
> >  #include <netinet/icmp6.h>
> >  #include <string.h>
> >
> > +#ifdef HAVE_OPENSSL
> > +#include <openssl/rand.h>
> > +#endif
> > +
> >  #include "conntrack.h"
> >  #include "conntrack-private.h"
> >  #include "conntrack-tp.h"
> > @@ -30,6 +34,7 @@
> >  #include "csum.h"
> >  #include "ct-dpif.h"
> >  #include "dp-packet.h"
> > +#include "entropy.h"
> >  #include "flow.h"
> >  #include "netdev.h"
> >  #include "odp-netlink.h"
> > @@ -47,6 +52,45 @@
> >
> >  VLOG_DEFINE_THIS_MODULE(conntrack);
> >
> > +/* Counts calls to nat_random_uint32() that could not obtain any
> > + * cryptographic randomness at all (neither OpenSSL nor the system
> > + * entropy pool).  Each occurrence corresponds to one failed NAT tuple
> > + * allocation attempt. */
> > +COVERAGE_DEFINE(conntrack_entropy_failed);
> > +
> > +/* NAT tuple selection (both the address/port hash basis and the
> > + * fully-random port paths) must not be predictable from an observed
> > + * random_uint32() output, so it draws from a cryptographic source here
> > + * instead of the general-purpose xorshift32 PRNG in random.c.
> > + *
> > + * When compiled against OpenSSL 1.1.0+, this uses OpenSSL's PRNG.  If
> > + * that is unavailable, or RAND_status() reports that it is not properly
> > + * seeded (see the same check in lib/stream-ssl.c), it falls back to the
> > + * system entropy pool.  If neither source can provide randomness, this
> > + * returns false rather than falling back to the non-cryptographic PRNG:
> > + * callers must fail the NAT tuple allocation (and, transitively, the
> > + * connection attempt) instead of silently downgrading its quality. */
> > +static bool
> > +nat_random_uint32(uint32_t *r)
> > +{
> > +#ifdef HAVE_OPENSSL
> > +    if (RAND_bytes((uint8_t *) r, sizeof *r) == 1 && RAND_status()) {
> 
> No idea how expensive this is, and/or if it takes systemcalls.
> This is used in the fast path, so maybe we should do some benchmarking?

AFAICT, RAND_bytes doesn't take system calls unless the prng isn't
seeded properly.

> > +        return true;
> > +    }
> > +
> > +    static struct vlog_rate_limit rl1 = VLOG_RATE_LIMIT_INIT(1, 5);
> > +    VLOG_WARN_RL(&rl1, "RAND_bytes unreliable, falling back to system "
> > +                 "entropy pool for NAT tuple selection");
> > +#endif
> > +
> > +    if (!get_entropy(r, sizeof *r)) {
> 
> The fallback is expensive; it does open/read/close() on /dev/urandom.

Yes - OTOH, it is considered a security issue otherwise.  Not sure how
best to deal with not having RAND_bytes() available.  We shouldn't try
to implement our own CSPRNG (given we already support linking to
openssl).  Should we not support NAT in this case?  Maybe set a flag
that we are running in an 'insecure' configuration?

> > +        return true;
> > +    }
> > +
> > +    COVERAGE_INC(conntrack_entropy_failed);
> > +    return false;
> > +}
> > +
> >  COVERAGE_DEFINE(conntrack_full);
> >  COVERAGE_DEFINE(conntrack_l3csum_checked);
> >  COVERAGE_DEFINE(conntrack_l3csum_err);
> > @@ -251,8 +295,16 @@ 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();
> > +     *
> > +     * It is also the basis that nat_range_hash() mixes into every NAT
> > +     * address and (non-random) NAT port choice, so it must come from
> > +     * nat_random_uint32() rather than the predictable general-purpose
> > +     * PRNG--otherwise all NAT tuples derived from it are only as
> 
> The -- looks odd here, maybe an AI text cut/paste. Guess a '; ' would be 
> better here.

Yes, I had AI writing a test case (and it wrote some comments).  The
test case stopped making sense (because it could take a long time to
converge on the xorshift reversal).

> > +     * unpredictable as that single 32-bit xorshift output. */
> 
> Also, to a non-native speaker, the 'unpredictable' reads odd. Maybe something 
> like;
> 
>  * PRNG; otherwise, all NAT tuples derived from it would be
>  * predictable from that PRNG output.

Sure, I can change it.

> > +    if (!nat_random_uint32(&ct->hash_basis)) {
> > +        VLOG_FATAL("conntrack: unable to obtain cryptographic randomness "
> > +                   "to initialize the NAT hash basis");
> 
> Are we ok with not having a backup?

It's a good question, see above.

> > +    }
> >
> >      ovs_rwlock_init(&ct->resources_lock);
> >      ovs_rwlock_wrlock(&ct->resources_lock);
> > @@ -2556,8 +2608,19 @@ another_round:
> >      }
> >
> >      if (attempts < range && attempts >= 16) {
> > +        uint32_t r;
> > +
> > +        if (!nat_random_uint32(&r)) {
> > +            /* CPRNG wasn't available, return false in this case.  It is
> > +             * possible that the entropy pool is only temporarily 
> > unaviable,
> > +             * but bailing on this connection attempt should be okay since
> > +             * since we don't want to waste cpu cycles for an event that 
> > may
> > +             * take a while. */
> > +            return false;
> > +        }
> > +
> >          attempts /= 2;
> > -        curr = min + (random_uint32() % range);
> > +        curr = min + (r % range);
> >          goto another_round;
> >      }
> >
> > @@ -2613,7 +2676,12 @@ 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();
> > +        if (!nat_random_uint32(&port_off)) {
> > +            /* The entropy failure here will reflect that we're resource
> > +             * exhausted.  It is a bit confusing because we're out of
> > +             * entropy rather than out of actual NAT range. */
> > +            return false;
> > +        }
> >      } else if (basis) {
> >          port_off = hash;
> >      } else {
> > -- 
> > 2.55.0
> >
> > _______________________________________________
> > dev mailing list
> > [email protected]
> > https://mail.openvswitch.org/mailman/listinfo/ovs-dev

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

Reply via email to