[PATCH] policy: ignore invalid TCP flags combo in conntrack
Robin Jarry
rjarry at redhat.com
Mon Aug 31 16:18:04 CEST 2026
Morten Brørup, Aug 31, 2026 at 13:26:
> Do not update the connection state when passed a packet with an invalid
> TCP flags combination.
>
> Clean up and optimize conntrack:
>
> The flow direction (forward/reverse) is encoded in the least significant
> bit of the connection data pointer.
> Change the value (and name) of this bit, so it has the same value as the
> conn_flow_t enum; i.e. 0 means forward, and 1 means reverse direction.
>
> Add more comments to clarify how tcp_flagstate() is implemented.
>
> Remove _Atomic from the connection timestamp; there is no need for it.
> Only update the connection timestamp if more than 1/4 second has passed
> since the last update.
>
> Remove the local variable "data" in gr_conn_insert(); it was only used
> for type casting.
>
> Use signed types for time in do_ageing(), and simplify the code.
>
> Make the code for hash iterations similar throughout the file, so the
> check for forward flow direction always comes first.
>
> Signed-off-by: Morten Brørup <mb at smartsharesystems.com>
> ---
> modules/policy/control/conntrack.c | 133 ++++++++++++++++++-----------
> modules/policy/control/conntrack.h | 2 +-
> 2 files changed, 84 insertions(+), 51 deletions(-)
>
> diff --git a/modules/policy/control/conntrack.c b/modules/policy/control/conntrack.c
> index 9b5391ff..2954f10d 100644
> --- a/modules/policy/control/conntrack.c
> +++ b/modules/policy/control/conntrack.c
> @@ -48,32 +48,45 @@ static const gr_conn_state_t generic_state_machine[CONN_S_COUNT][CONN_FLOW_COUNT
>
> typedef enum {
> TCP_FS_INVALID = 0,
> - TCP_FS_SYN,
> - TCP_FS_SYNACK,
> - TCP_FS_ACK,
> - TCP_FS_FIN,
> + TCP_FS_SYN, // 1
> + TCP_FS_SYNACK, // 2
> + TCP_FS_ACK, // 3
> + TCP_FS_FIN, // 4
> TCP_FS_COUNT,
> } tcp_flagstate_t;
>
> -static inline tcp_flagstate_t tcp_flagstate(const uint8_t tcp_flags) {
> +static inline tcp_flagstate_t tcp_flagstate(uint8_t tcp_flags) {
> tcp_flagstate_t s;
>
> // Flags are shifted to use three least significant bits, thus each
> - // flag combination has a unique number ranging from 0 to 7, e.g.
> - // TH_SYN | TH_ACK has number 6, since (0x02 | (0x10 >> 2)) == 6.
> + // flag combination has a unique number ranging from 0 to 0x7, e.g.
> + // SYN | ACK has number 0x6, since (0x02 | (0x10 >> 2)) == 0x6.
> // However, the requirement is to have number 0 for invalid cases,
> - // such as TH_SYN | TH_FIN, and to have the same number for TH_FIN
> - // and TH_FIN|TH_ACK cases. Thus, we generate a mask assigning 3
> - // bits for each number, which contains the actual case numbers:
> + // such as SYN | FIN, and to have the same number for FIN and
> + // and FIN | ACK cases. Thus, we use a lookup table indexed by
> + // the flags combination to get the enum value.
> + // The lookup table is implemented as a word of nibbles, ordered
> + // from the least to the most significant nibble, where lookup is
> + // performed by shifting down the word by index number of nibbles,
> + // and masking the least significant nibble.
> //
> - // TCP_FS_SYNACK << (6 << 2) == 0x2000000 (6 - SYN,ACK)
> - // TCP_FS_FIN << (5 << 2) == 0x0400000 (5 - FIN,ACK)
> - // ...
> + // +---+------+------+------+---------+
> + // |idx|ACK(4)|SYN(2)|FIN(1)|flagstate|
> + // +---+------+------+------+---------+
> + // | 0| | | |INVALID 0|
> + // | 1| | | X |FIN 4|
> + // | 2| | X | |SYN 1|
> + // | 3| | X | X |INVALID 0|
> + // | 4| X | | |ACK 3|
> + // | 5| X | | X |FIN 4|
> + // | 6| X | X | |SYNACK 2|
> + // | 7| X | X | X |INVALID 0|
> + // +---+------+------+------+---------+
> //
> - // Hence, OR'ed mask value is 0x2430140.
> - s = tcp_flags & (RTE_TCP_SYN_FLAG | RTE_TCP_FIN_FLAG);
> - s |= (tcp_flags & RTE_TCP_ACK_FLAG) >> 2;
> - s = (0x2430140 >> (s << 2)) & 7;
> + // Hence, the lookup table word is 0x02430140.
> + tcp_flags = (tcp_flags & (RTE_TCP_SYN_FLAG /*0x2*/ | RTE_TCP_FIN_FLAG /*0x1*/))
> + | ((tcp_flags & RTE_TCP_ACK_FLAG /*0x10*/) >> 2);
> + s = (0x02430140 >> (tcp_flags << 2)) & 0xF;
>
> assert(s < TCP_FS_COUNT);
>
> @@ -229,19 +242,31 @@ void gr_conn_update(struct conn *c, conn_flow_t flow, const struct rte_tcp_hdr *
>
> again:
> cur_state = atomic_load(&c->state);
> - if (c->fwd_key.proto == IPPROTO_TCP)
> + if (c->fwd_key.proto == IPPROTO_TCP) {
> new_state = tcp_state_machine[cur_state][flow][tcp_flagstate(tcp->tcp_flags)];
> - else
> - new_state = generic_state_machine[cur_state][flow];
>
> - // TODO: inspect TCP window to determine if packet is part of the connection.
> + // When the state machine is passed an invalid flags combination,
> + // it returns CONN_S_CLOSED.
> + // In that case, ignore the request to update the connection state.
> + if (unlikely(new_state == CONN_S_CLOSED))
> + return;
> +
> + // TODO: inspect TCP window to determine if packet is part of the connection.
> + } else
> + new_state = generic_state_machine[cur_state][flow];
>
> - if (new_state != cur_state) {
> + if (unlikely(new_state != cur_state)) {
> if (!atomic_compare_exchange_weak(&c->state, &cur_state, new_state))
> goto again;
> +
> + c->last_update = gr_clock_ns();
> + return;
> }
>
> - atomic_store(&c->last_update, gr_clock_ns());
> + // Update the timestamp if at least 1/4 second since the last update.
> + gr_clock_ns_t now = gr_clock_ns();
> + if (unlikely(now - c->last_update > GR_NS_PER_S / 4))
> + c->last_update = now;
This is a similar pattern to the FDB code which was changed recently.
You didn't mention why the atomic_* calls were removed. Could you
explain?
> }
>
> bool gr_conn_parse_key(
> @@ -328,21 +353,24 @@ static _Atomic(struct rte_hash *) conn_hash;
> static _Atomic(struct rte_mempool *) conn_pool;
> static struct event *ageing_timer;
>
> -#define CONN_FLOW_FWD_BIT ((uintptr_t)0x1)
> +#define CONN_FLOW_REV_BIT ((uintptr_t)0x1)
This change seems unrelated to the TCP flags check.
> +
> +static_assert(CONN_FLOW_REV_BIT != 0);
> +static_assert(CONN_FLOW_REV_BIT == (uintptr_t)CONN_FLOW_REV);
>
> static inline conn_flow_t conn_flow(void *data) {
> - if ((uintptr_t)data & CONN_FLOW_FWD_BIT)
> - return CONN_FLOW_FWD;
> - return CONN_FLOW_REV;
> + return (conn_flow_t)((uintptr_t)data & CONN_FLOW_REV_BIT);
> }
>
> static inline struct conn *conn_ptr(void *data) {
> - return (struct conn *)((uintptr_t)data & ~CONN_FLOW_FWD_BIT);
> + return (struct conn *)((uintptr_t)data & ~CONN_FLOW_REV_BIT);
> }
>
> static inline void *conn_data(struct conn *conn, conn_flow_t flow) {
> - if (flow == CONN_FLOW_FWD)
> - return (void *)((uintptr_t)conn | CONN_FLOW_FWD_BIT);
> + if (!__rte_constant(flow))
> + return (void *)((uintptr_t)conn | (uintptr_t)flow);
> + if (flow == CONN_FLOW_REV)
> + return (void *)((uintptr_t)conn | CONN_FLOW_REV_BIT);
> return conn;
> }
>
> @@ -359,28 +387,32 @@ struct conn *gr_conn_lookup(const struct conn_key *key, conn_flow_t *flow) {
>
> struct conn *gr_conn_insert(const struct conn_key *fwd_key, const struct conn_key *rev_key) {
> struct conn *conn;
> - void *data;
>
> // create a new connection object
> - if (rte_mempool_get(conn_pool, &data) < 0)
> + if (rte_mempool_get(conn_pool, (void **)&conn) < 0)
> return NULL;
>
> - conn = data;
> - memset(conn, 0, sizeof(*conn));
> - conn->rev_key = *rev_key;
> + static_assert(
> + offsetof(struct conn, fwd_key) == 0
> + && offsetof(struct conn, rev_key) == RTE_SIZEOF_FIELD(struct conn, fwd_key)
> + );
> conn->fwd_key = *fwd_key;
> + conn->rev_key = *rev_key;
> + memset((char *)conn + 2 * sizeof(struct conn_key),
> + 0,
> + sizeof(*conn) - 2 * sizeof(struct conn_key));
>
> - if (rte_hash_add_key_data(conn_hash, fwd_key, conn_data(data, CONN_FLOW_FWD)) < 0) {
> + if (rte_hash_add_key_data(conn_hash, fwd_key, conn_data(conn, CONN_FLOW_FWD)) < 0) {
> // hash full
> - rte_mempool_put(conn_pool, data);
> + rte_mempool_put(conn_pool, conn);
> return NULL;
> }
>
> // Also reference the conntrack by its *reverse* key for replies.
> - if (rte_hash_add_key_data(conn_hash, rev_key, conn_data(data, CONN_FLOW_REV)) < 0) {
> + if (rte_hash_add_key_data(conn_hash, rev_key, conn_data(conn, CONN_FLOW_REV)) < 0) {
> // hash full, remove forward key,
> rte_hash_del_key(conn_hash, fwd_key);
> - rte_mempool_put(conn_pool, data);
> + rte_mempool_put(conn_pool, conn);
> return NULL;
> }
>
> @@ -388,8 +420,8 @@ struct conn *gr_conn_insert(const struct conn_key *fwd_key, const struct conn_ke
> }
>
> static void do_ageing(evutil_socket_t, short /*what*/, void * /*priv*/) {
> - gr_clock_ns_t now = gr_clock_ns(), last;
> - uint64_t age, timeout;
> + gr_clock_ns_t now = gr_clock_ns();
> + time_t timeout;
> struct conn *conn;
> const void *key;
> uint32_t iter;
> @@ -397,10 +429,10 @@ static void do_ageing(evutil_socket_t, short /*what*/, void * /*priv*/) {
>
> iter = 0;
> while (rte_hash_iterate(conn_hash, &key, &data, &iter) >= 0) {
> - conn = conn_ptr(data);
> if (conn_flow(data) != CONN_FLOW_FWD)
> continue;
>
> + conn = conn_ptr(data);
Could you avoid unnecessary code churn?
> switch (atomic_load(&conn->state)) {
> case CONN_S_NEW:
> case CONN_S_SIMSYN_SENT:
> @@ -413,7 +445,10 @@ static void do_ageing(evutil_socket_t, short /*what*/, void * /*priv*/) {
> timeout = conf.timeout_tcp_established_sec;
> break;
> case IPPROTO_UDP:
> - if (conn->fwd_key.dst_id == RTE_BE16(53))
> + // RFC 4787 section 4.3 REQ-5a:
> + // UDP ports in the well-known range may have shorter timers,
> + // specific to the application running over that specific port.
> + if (conn->fwd_key.dst_id == RTE_BE16(53)) // DNS
> timeout = 2;
> else
> timeout = conf.timeout_udp_established_sec;
> @@ -440,11 +475,7 @@ static void do_ageing(evutil_socket_t, short /*what*/, void * /*priv*/) {
> break;
> }
>
> - last = atomic_load(&conn->last_update);
> - if (last > now)
> - continue;
> - age = (now - last) / GR_NS_PER_S;
> - if (age > timeout)
> + if (now - conn->last_update > timeout * GR_NS_PER_S)
> gr_conn_destroy(conn);
> }
> }
> @@ -459,6 +490,7 @@ void gr_conn_snat44_purge(struct snat44_policy *policy) {
> while (rte_hash_iterate(h, &key, &data, &next) >= 0) {
> if (conn_flow(data) != CONN_FLOW_FWD)
> continue;
> +
> conn = conn_ptr(data);
> if (conn->nat.policy == policy)
> gr_conn_destroy(conn);
> @@ -588,7 +620,7 @@ static struct api_out conntrack_list(const void * /*request*/, struct api_ctx *c
> .dst_id = conn->rev_key.dst_id,
> },
> .state = atomic_load(&conn->state),
> - .last_update = atomic_load(&conn->last_update),
> + .last_update = conn->last_update,
> };
> api_send(ctx, sizeof(ct), &ct);
> }
> @@ -604,9 +636,10 @@ static struct api_out conntrack_flush(const void * /*request*/, struct api_ctx *
>
> iter = 0;
> while (rte_hash_iterate(conn_hash, &key, &data, &iter) >= 0) {
> - conn = conn_ptr(data);
> if (conn_flow(data) != CONN_FLOW_FWD)
> continue;
> +
> + conn = conn_ptr(data);
> gr_conn_destroy(conn);
> }
>
> diff --git a/modules/policy/control/conntrack.h b/modules/policy/control/conntrack.h
> index 51f8dd7c..bdeb5ea9 100644
> --- a/modules/policy/control/conntrack.h
> +++ b/modules/policy/control/conntrack.h
> @@ -43,7 +43,7 @@ struct conn {
> struct conn_key fwd_key;
> struct conn_key rev_key;
> _Atomic(gr_conn_state_t) state;
> - _Atomic(gr_clock_ns_t) last_update;
> + gr_clock_ns_t last_update;
See my comment above. Could you motivate the removal of _Atomic() here?
> struct nat44 nat;
> };
>
Cheers,
--
Robin
# May not be combined with other discounts.
More information about the grout
mailing list