[PATCH] udp: Split activity timeouts for UDP flows
Frequent DNS queries over UDP from a container or guest can result
in many sockets shown in ss(8), typically one per flow. This is
expected and harmless, but it can make the output of ss(8) look
noisy and potentially concern users.
This patch splits UDP flow timeouts into two, mirroring the Linux
kernel, and sources the values from kernel parameters. The shorter
timeout is applied to unidirectional flows and minimal bidirectional
exchanges (single datagram and reply), while the longer timeout is
used for bidirectional flows with multiple datagrams on either side.
Link: https://bugs.passt.top/show_bug.cgi?id=197
Suggested-by: Stefano Brivio
I haven't tested this yet, just two exceedingly minor nits (I can fix
it up on merge if you're fine with it and if no other changes are
needed):
On Thu, 12 Feb 2026 16:04:14 +0800
Yumei Huang
Frequent DNS queries over UDP from a container or guest can result in many sockets shown in ss(8), typically one per flow. This is expected and harmless, but it can make the output of ss(8) look noisy and potentially concern users.
This patch splits UDP flow timeouts into two, mirroring the Linux kernel, and sources the values from kernel parameters. The shorter timeout is applied to unidirectional flows and minimal bidirectional exchanges (single datagram and reply), while the longer timeout is used for bidirectional flows with multiple datagrams on either side.
Link: https://bugs.passt.top/show_bug.cgi?id=197 Suggested-by: Stefano Brivio
Signed-off-by: Yumei Huang --- udp.c | 33 ++++++++++++++++++++++++++++++++- udp.h | 4 ++++ udp_flow.c | 11 ++++++++--- udp_flow.h | 13 +++++++++++++ 4 files changed, 57 insertions(+), 4 deletions(-) diff --git a/udp.c b/udp.c index b2383e2..3afec35 100644 --- a/udp.c +++ b/udp.c @@ -26,7 +26,10 @@ * * We track pseudo-connections of this type as flow table entries of type * FLOW_UDP. We store the time of the last traffic on the flow in uflow->ts, - * and let the flow expire if there is no traffic for UDP_CONN_TIMEOUT seconds. + * and let the flow expire if there is no traffic for UDP_TIMEOUT seconds for + * unidirectional flows and flows with only one datagram and one reply, or + * UDP_TIMEOUT_STREAM seconds for bidirectional flows with more than one + * datagram on either side. * * NOTE: This won't handle multicast protocols, or some protocols with different * port usage. We'll need specific logic if we want to handle those. @@ -118,6 +121,13 @@
#define UDP_MAX_FRAMES 32 /* max # of frames to receive at once */
+#define UDP_TIMEOUT "/proc/sys/net/netfilter/nf_conntrack_udp_timeout" +#define UDP_TIMEOUT_STREAM \ + "/proc/sys/net/netfilter/nf_conntrack_udp_timeout_stream" + +#define UDP_TIMEOUT_DEFAULT 30 /* s */ +#define UDP_TIMEOUT_STREAM_DEFAULT 120 /* s */ + /* Maximum UDP data to be returned in ICMP messages */ #define ICMP4_MAX_DLEN 8 #define ICMP6_MAX_DLEN (IPV6_MIN_MTU \ @@ -954,6 +964,7 @@ void udp_sock_handler(const struct ctx *c, union epoll_ref ref,
flow_trace(uflow, "Received data on reply socket"); uflow->ts = now->tv_sec; + udp_flow_activity(uflow, !tosidx.sidei);
if (pif_is_socket(topif)) { udp_sock_to_sock(c, ref.fd, n, tosidx); @@ -1179,6 +1190,24 @@ static void udp_splice_iov_init(void) } }
+/** + * udp_get_timeout_params() - Get host kernel UDP timeout parameters + * @c: Execution context + */ +static void udp_get_timeout_params(struct ctx *c) +{ + intmax_t v; + + v = read_file_integer(UDP_TIMEOUT, UDP_TIMEOUT_DEFAULT); + c->udp.timeout = v; + + v = read_file_integer(UDP_TIMEOUT_STREAM, UDP_TIMEOUT_STREAM_DEFAULT); + c->udp.stream_timeout = v; + + debug("Using UDP timeout parameters, timeout: %d, stream_timeout: %d", + c->udp.timeout, c->udp.stream_timeout); +} + /** * udp_init() - Initialise per-socket data, and sockets in namespace * @c: Execution context @@ -1189,6 +1218,8 @@ int udp_init(struct ctx *c) { ASSERT(!c->no_udp);
+ udp_get_timeout_params(c); + udp_iov_init(c);
if (fwd_listen_sync(c, &c->udp.fwd_in, PIF_HOST, IPPROTO_UDP) < 0) diff --git a/udp.h b/udp.h index 2b91d72..da9c2df 100644 --- a/udp.h +++ b/udp.h @@ -24,11 +24,15 @@ void udp_update_l2_buf(const unsigned char *eth_d); * @fwd_in: Port forwarding configuration for inbound packets * @fwd_out: Port forwarding configuration for outbound packets * @timer_run: Timestamp of most recent timer run + * @timeout: Timeout for unidirectional flows (in s) + * @stream_timeout: Timeout for stream-like flows (in s) */ struct udp_ctx { struct fwd_ports fwd_in; struct fwd_ports fwd_out; struct timespec timer_run; + int timeout; + int stream_timeout; };
#endif /* UDP_H */ diff --git a/udp_flow.c b/udp_flow.c index 1f5e84e..9b22586 100644 --- a/udp_flow.c +++ b/udp_flow.c @@ -17,8 +17,6 @@ #include "udp_internal.h" #include "epoll_ctl.h"
-#define UDP_CONN_TIMEOUT 180 /* s, timeout for ephemeral or local bind */ - /** * udp_at_sidx() - Get UDP specific flow at given sidx * @sidx: Flow and side to retrieve @@ -152,6 +150,7 @@ static flow_sidx_t udp_flow_new(const struct ctx *c, union flow *flow, uflow->ts = now->tv_sec; uflow->s[INISIDE] = uflow->s[TGTSIDE] = -1; uflow->ttl[INISIDE] = uflow->ttl[TGTSIDE] = 0; + uflow->activity[INISIDE] = uflow->activity[TGTSIDE] = 0;
flow_foreach_sidei(sidei) { if (pif_is_socket(uflow->f.pif[sidei])) @@ -362,7 +361,13 @@ bool udp_flow_defer(const struct ctx *c, struct udp_flow *uflow, bool udp_flow_timer(const struct ctx *c, struct udp_flow *uflow, const struct timespec *now) { - if (now->tv_sec - uflow->ts <= UDP_CONN_TIMEOUT) + int timeout = c->udp.timeout; + + if (uflow->activity[TGTSIDE] && + (uflow->activity[INISIDE] > 1 || uflow->activity[TGTSIDE] > 1)) + timeout = c->udp.stream_timeout; + + if (now->tv_sec - uflow->ts <= timeout) return false;
udp_flow_close(c, uflow); diff --git a/udp_flow.h b/udp_flow.h index 14e0f92..158a0f6 100644 --- a/udp_flow.h +++ b/udp_flow.h @@ -16,6 +16,7 @@ * @flush1: @s[1] may have datagrams queued for other flows * @ts: Activity timestamp * @s: Socket fd (or -1) for each side of the flow + * @activity: Activity for each side of the flow
It's not clear what the measurement unit is, here. I think we should also specify that it's packets *coming from* (right?), not going to, each side of the flow. Maybe: * @activity: Packets seen from each side of the flow, up to UINT8_MAX ...if I got it right?
*/ struct udp_flow { /* Must be first element */ @@ -29,8 +30,20 @@ struct udp_flow {
time_t ts; int s[SIDES]; + uint8_t activity[SIDES]; };
+/** + * udp_flow_activity() - Track activity of a udp flow
UDP
+ * @uflow: UDP flow + * @sidei: Side index of the flow
Maybe we can specify here: (INISIDE or TGTSIDE)
+ */ +static inline void udp_flow_activity(struct udp_flow *uflow, unsigned int sidei) +{ + if (uflow->activity[sidei] < UINT8_MAX) + uflow->activity[sidei]++; +} + struct udp_flow *udp_at_sidx(flow_sidx_t sidx); flow_sidx_t udp_flow_from_sock(const struct ctx *c, uint8_t pif, const union inany_addr *dst, in_port_t port,
Everything else looks good to me! (But again, I haven't tested this yet). -- Stefano
On Thu, Feb 12, 2026 at 4:59 PM Stefano Brivio
I haven't tested this yet, just two exceedingly minor nits (I can fix it up on merge if you're fine with it and if no other changes are needed):
Sure, I'm fine with it.
On Thu, 12 Feb 2026 16:04:14 +0800 Yumei Huang
wrote: Frequent DNS queries over UDP from a container or guest can result in many sockets shown in ss(8), typically one per flow. This is expected and harmless, but it can make the output of ss(8) look noisy and potentially concern users.
This patch splits UDP flow timeouts into two, mirroring the Linux kernel, and sources the values from kernel parameters. The shorter timeout is applied to unidirectional flows and minimal bidirectional exchanges (single datagram and reply), while the longer timeout is used for bidirectional flows with multiple datagrams on either side.
Link: https://bugs.passt.top/show_bug.cgi?id=197 Suggested-by: Stefano Brivio
Signed-off-by: Yumei Huang --- udp.c | 33 ++++++++++++++++++++++++++++++++- udp.h | 4 ++++ udp_flow.c | 11 ++++++++--- udp_flow.h | 13 +++++++++++++ 4 files changed, 57 insertions(+), 4 deletions(-) diff --git a/udp.c b/udp.c index b2383e2..3afec35 100644 --- a/udp.c +++ b/udp.c @@ -26,7 +26,10 @@ * * We track pseudo-connections of this type as flow table entries of type * FLOW_UDP. We store the time of the last traffic on the flow in uflow->ts, - * and let the flow expire if there is no traffic for UDP_CONN_TIMEOUT seconds. + * and let the flow expire if there is no traffic for UDP_TIMEOUT seconds for + * unidirectional flows and flows with only one datagram and one reply, or + * UDP_TIMEOUT_STREAM seconds for bidirectional flows with more than one + * datagram on either side. * * NOTE: This won't handle multicast protocols, or some protocols with different * port usage. We'll need specific logic if we want to handle those. @@ -118,6 +121,13 @@
#define UDP_MAX_FRAMES 32 /* max # of frames to receive at once */
+#define UDP_TIMEOUT "/proc/sys/net/netfilter/nf_conntrack_udp_timeout" +#define UDP_TIMEOUT_STREAM \ + "/proc/sys/net/netfilter/nf_conntrack_udp_timeout_stream" + +#define UDP_TIMEOUT_DEFAULT 30 /* s */ +#define UDP_TIMEOUT_STREAM_DEFAULT 120 /* s */ + /* Maximum UDP data to be returned in ICMP messages */ #define ICMP4_MAX_DLEN 8 #define ICMP6_MAX_DLEN (IPV6_MIN_MTU \ @@ -954,6 +964,7 @@ void udp_sock_handler(const struct ctx *c, union epoll_ref ref,
flow_trace(uflow, "Received data on reply socket"); uflow->ts = now->tv_sec; + udp_flow_activity(uflow, !tosidx.sidei);
if (pif_is_socket(topif)) { udp_sock_to_sock(c, ref.fd, n, tosidx); @@ -1179,6 +1190,24 @@ static void udp_splice_iov_init(void) } }
+/** + * udp_get_timeout_params() - Get host kernel UDP timeout parameters + * @c: Execution context + */ +static void udp_get_timeout_params(struct ctx *c) +{ + intmax_t v; + + v = read_file_integer(UDP_TIMEOUT, UDP_TIMEOUT_DEFAULT); + c->udp.timeout = v; + + v = read_file_integer(UDP_TIMEOUT_STREAM, UDP_TIMEOUT_STREAM_DEFAULT); + c->udp.stream_timeout = v; + + debug("Using UDP timeout parameters, timeout: %d, stream_timeout: %d", + c->udp.timeout, c->udp.stream_timeout); +} + /** * udp_init() - Initialise per-socket data, and sockets in namespace * @c: Execution context @@ -1189,6 +1218,8 @@ int udp_init(struct ctx *c) { ASSERT(!c->no_udp);
+ udp_get_timeout_params(c); + udp_iov_init(c);
if (fwd_listen_sync(c, &c->udp.fwd_in, PIF_HOST, IPPROTO_UDP) < 0) diff --git a/udp.h b/udp.h index 2b91d72..da9c2df 100644 --- a/udp.h +++ b/udp.h @@ -24,11 +24,15 @@ void udp_update_l2_buf(const unsigned char *eth_d); * @fwd_in: Port forwarding configuration for inbound packets * @fwd_out: Port forwarding configuration for outbound packets * @timer_run: Timestamp of most recent timer run + * @timeout: Timeout for unidirectional flows (in s) + * @stream_timeout: Timeout for stream-like flows (in s) */ struct udp_ctx { struct fwd_ports fwd_in; struct fwd_ports fwd_out; struct timespec timer_run; + int timeout; + int stream_timeout; };
#endif /* UDP_H */ diff --git a/udp_flow.c b/udp_flow.c index 1f5e84e..9b22586 100644 --- a/udp_flow.c +++ b/udp_flow.c @@ -17,8 +17,6 @@ #include "udp_internal.h" #include "epoll_ctl.h"
-#define UDP_CONN_TIMEOUT 180 /* s, timeout for ephemeral or local bind */ - /** * udp_at_sidx() - Get UDP specific flow at given sidx * @sidx: Flow and side to retrieve @@ -152,6 +150,7 @@ static flow_sidx_t udp_flow_new(const struct ctx *c, union flow *flow, uflow->ts = now->tv_sec; uflow->s[INISIDE] = uflow->s[TGTSIDE] = -1; uflow->ttl[INISIDE] = uflow->ttl[TGTSIDE] = 0; + uflow->activity[INISIDE] = uflow->activity[TGTSIDE] = 0;
flow_foreach_sidei(sidei) { if (pif_is_socket(uflow->f.pif[sidei])) @@ -362,7 +361,13 @@ bool udp_flow_defer(const struct ctx *c, struct udp_flow *uflow, bool udp_flow_timer(const struct ctx *c, struct udp_flow *uflow, const struct timespec *now) { - if (now->tv_sec - uflow->ts <= UDP_CONN_TIMEOUT) + int timeout = c->udp.timeout; + + if (uflow->activity[TGTSIDE] && + (uflow->activity[INISIDE] > 1 || uflow->activity[TGTSIDE] > 1)) + timeout = c->udp.stream_timeout; + + if (now->tv_sec - uflow->ts <= timeout) return false;
udp_flow_close(c, uflow); diff --git a/udp_flow.h b/udp_flow.h index 14e0f92..158a0f6 100644 --- a/udp_flow.h +++ b/udp_flow.h @@ -16,6 +16,7 @@ * @flush1: @s[1] may have datagrams queued for other flows * @ts: Activity timestamp * @s: Socket fd (or -1) for each side of the flow + * @activity: Activity for each side of the flow
It's not clear what the measurement unit is, here. I think we should also specify that it's packets *coming from* (right?), not going to, each side of the flow. Maybe:
* @activity: Packets seen from each side of the flow, up to UINT8_MAX
...if I got it right?
Yes, you got it right.
*/ struct udp_flow { /* Must be first element */ @@ -29,8 +30,20 @@ struct udp_flow {
time_t ts; int s[SIDES]; + uint8_t activity[SIDES]; };
+/** + * udp_flow_activity() - Track activity of a udp flow
UDP
+ * @uflow: UDP flow + * @sidei: Side index of the flow
Maybe we can specify here:
(INISIDE or TGTSIDE)
+ */ +static inline void udp_flow_activity(struct udp_flow *uflow, unsigned int sidei) +{ + if (uflow->activity[sidei] < UINT8_MAX) + uflow->activity[sidei]++; +} + struct udp_flow *udp_at_sidx(flow_sidx_t sidx); flow_sidx_t udp_flow_from_sock(const struct ctx *c, uint8_t pif, const union inany_addr *dst, in_port_t port,
Everything else looks good to me! (But again, I haven't tested this yet).
Thanks, just let me know if other changes are needed.
-- Stefano
-- Thanks, Yumei Huang
Oops, I missed one point at a first review, and also during a quick
test.
I just tried outbound DNS queries in pasta with single responses, not
inbound traffic or passt in vhost-user mode. Then I realised
that:
On Thu, 12 Feb 2026 16:04:14 +0800
Yumei Huang
[...] @@ -954,6 +964,7 @@ void udp_sock_handler(const struct ctx *c, union epoll_ref ref,
flow_trace(uflow, "Received data on reply socket"); uflow->ts = now->tv_sec; + udp_flow_activity(uflow, !tosidx.sidei);
...this only covers three of the four paths we need to act upon: 1. inbound datagrams received on the reply socket via udp_buf_sock_to_tap(), called from here 2. inbound datagrams received on the reply socket in passt's vhost-user mode, that's udp_vu_sock_recv(), also called from here 3. "spliced" sockets (that's not really the case for UDP, we can't call splice(), but a pair of recvmmsg() / sendmmsg()), that is, loopback UDP traffic, handled by udp_sock_to_sock(), called from here as well but not: 4. outbound, non-spliced datagrams from container/guest: that's udp_tap_handler(), in both vhost-user and non-vhost-user cases, or udp_flow_from_tap() in udp_flow.c. I guess we want to take care of this directly from udp_flow_from_tap(), for consistency, because that's also where we update the timestamp value: sidx = flow_lookup_sa(c, IPPROTO_UDP, pif, s_in, dst, port); if ((uflow = udp_at_sidx(sidx))) { uflow->ts = now->tv_sec; ^^^ here return flow_sidx_opposite(sidx); } I haven't really tested this side of it but it should be fairly easy with socat and a UDP "server" inside pasta or a guest. Another thing I noticed later:
[...]
diff --git a/udp_flow.h b/udp_flow.h index 14e0f92..158a0f6 100644 --- a/udp_flow.h +++ b/udp_flow.h @@ -16,6 +16,7 @@ * @flush1: @s[1] may have datagrams queued for other flows * @ts: Activity timestamp * @s: Socket fd (or -1) for each side of the flow + * @activity: Activity for each side of the flow */ struct udp_flow { /* Must be first element */ @@ -29,8 +30,20 @@ struct udp_flow {
time_t ts; int s[SIDES]; + uint8_t activity[SIDES]; };
+/** + * udp_flow_activity() - Track activity of a udp flow + * @uflow: UDP flow + * @sidei: Side index of the flow + */ +static inline void udp_flow_activity(struct udp_flow *uflow, unsigned int sidei) +{ + if (uflow->activity[sidei] < UINT8_MAX) + uflow->activity[sidei]++; +}
This is an inline function in a header file for no good reason. It could be a normal static function in udp.c. See also: https://www.kernel.org/doc/html/latest/process/coding-style.html#the-inline-... ...and yes, it's two lines of code, but there's really no reason to decide we want to inline this instead of letting the compiler decide. -- Stefano
On Fri, Feb 13, 2026 at 5:51 AM Stefano Brivio
Oops, I missed one point at a first review, and also during a quick test.
I just tried outbound DNS queries in pasta with single responses, not inbound traffic or passt in vhost-user mode. Then I realised that:
On Thu, 12 Feb 2026 16:04:14 +0800 Yumei Huang
wrote: [...] @@ -954,6 +964,7 @@ void udp_sock_handler(const struct ctx *c, union epoll_ref ref,
flow_trace(uflow, "Received data on reply socket"); uflow->ts = now->tv_sec; + udp_flow_activity(uflow, !tosidx.sidei);
...this only covers three of the four paths we need to act upon:
1. inbound datagrams received on the reply socket via udp_buf_sock_to_tap(), called from here
2. inbound datagrams received on the reply socket in passt's vhost-user mode, that's udp_vu_sock_recv(), also called from here
3. "spliced" sockets (that's not really the case for UDP, we can't call splice(), but a pair of recvmmsg() / sendmmsg()), that is, loopback UDP traffic, handled by udp_sock_to_sock(), called from here as well
but not:
4. outbound, non-spliced datagrams from container/guest: that's udp_tap_handler(), in both vhost-user and non-vhost-user cases, or udp_flow_from_tap() in udp_flow.c.
I guess we want to take care of this directly from udp_flow_from_tap(), for consistency, because that's also where we update the timestamp value:
sidx = flow_lookup_sa(c, IPPROTO_UDP, pif, s_in, dst, port); if ((uflow = udp_at_sidx(sidx))) { uflow->ts = now->tv_sec;
^^^ here
return flow_sidx_opposite(sidx); }
I haven't really tested this side of it but it should be fairly easy with socat and a UDP "server" inside pasta or a guest.
Somehow, it worked well in my tests with pasta, it looks like the if condition always returns false. But now when I test with passt, it becomes an issue and we need to track the activity here as you mentioned. Besides, I also noticed we update the timestamp value in udp_flow_from_sock() as well. I feel we should call udp_flow_activity() there too, but couldn't come up with a test to prove it. On top of it, I just found two other issues. 1. in udp_flow_new(), we should initialize uflow->activity[INISIDE] to 1 instead of 0. Otherwise, we fail to track the first datagram. 2. I guess we need to add the profs entries (nf_conntrack_udp_timeout and nf_conntrack_udp_timeout_stream) to apparmor like the tcp ones in https://passt.top/passt/commit/?id=2aa63237109b97a55c85e4c86c72db0d055bfe7a. I don't have an environment to test it now. Maybe I can set up a debian vm later.
Another thing I noticed later:
[...]
diff --git a/udp_flow.h b/udp_flow.h index 14e0f92..158a0f6 100644 --- a/udp_flow.h +++ b/udp_flow.h @@ -16,6 +16,7 @@ * @flush1: @s[1] may have datagrams queued for other flows * @ts: Activity timestamp * @s: Socket fd (or -1) for each side of the flow + * @activity: Activity for each side of the flow */ struct udp_flow { /* Must be first element */ @@ -29,8 +30,20 @@ struct udp_flow {
time_t ts; int s[SIDES]; + uint8_t activity[SIDES]; };
+/** + * udp_flow_activity() - Track activity of a udp flow + * @uflow: UDP flow + * @sidei: Side index of the flow + */ +static inline void udp_flow_activity(struct udp_flow *uflow, unsigned int sidei) +{ + if (uflow->activity[sidei] < UINT8_MAX) + uflow->activity[sidei]++; +}
This is an inline function in a header file for no good reason. It could be a normal static function in udp.c. See also:
https://www.kernel.org/doc/html/latest/process/coding-style.html#the-inline-...
...and yes, it's two lines of code, but there's really no reason to decide we want to inline this instead of letting the compiler decide.
I see. I will remove the inline and move the function to udp.c
-- Stefano
-- Thanks, Yumei Huang
On Fri, Feb 13, 2026 at 2:45 PM Yumei Huang
On Fri, Feb 13, 2026 at 5:51 AM Stefano Brivio
wrote: Oops, I missed one point at a first review, and also during a quick test.
I just tried outbound DNS queries in pasta with single responses, not inbound traffic or passt in vhost-user mode. Then I realised that:
On Thu, 12 Feb 2026 16:04:14 +0800 Yumei Huang
wrote: [...] @@ -954,6 +964,7 @@ void udp_sock_handler(const struct ctx *c, union epoll_ref ref,
flow_trace(uflow, "Received data on reply socket"); uflow->ts = now->tv_sec; + udp_flow_activity(uflow, !tosidx.sidei);
...this only covers three of the four paths we need to act upon:
1. inbound datagrams received on the reply socket via udp_buf_sock_to_tap(), called from here
2. inbound datagrams received on the reply socket in passt's vhost-user mode, that's udp_vu_sock_recv(), also called from here
3. "spliced" sockets (that's not really the case for UDP, we can't call splice(), but a pair of recvmmsg() / sendmmsg()), that is, loopback UDP traffic, handled by udp_sock_to_sock(), called from here as well
but not:
4. outbound, non-spliced datagrams from container/guest: that's udp_tap_handler(), in both vhost-user and non-vhost-user cases, or udp_flow_from_tap() in udp_flow.c.
I guess we want to take care of this directly from udp_flow_from_tap(), for consistency, because that's also where we update the timestamp value:
sidx = flow_lookup_sa(c, IPPROTO_UDP, pif, s_in, dst, port); if ((uflow = udp_at_sidx(sidx))) { uflow->ts = now->tv_sec;
^^^ here
return flow_sidx_opposite(sidx); }
I haven't really tested this side of it but it should be fairly easy with socat and a UDP "server" inside pasta or a guest.
Somehow, it worked well in my tests with pasta, it looks like the if condition always returns false.
Oh, just realized I was testing against spliced datagrams. Now I can reproduce the issue with non-spliced. But now when I test with passt, it becomes an issue and we need to track
the activity here as you mentioned.
Besides, I also noticed we update the timestamp value in udp_flow_from_sock() as well. I feel we should call udp_flow_activity() there too, but couldn't come up with a test to prove it.
On top of it, I just found two other issues. 1. in udp_flow_new(), we should initialize uflow->activity[INISIDE] to 1 instead of 0. Otherwise, we fail to track the first datagram. 2. I guess we need to add the profs entries (nf_conntrack_udp_timeout and nf_conntrack_udp_timeout_stream) to apparmor like the tcp ones in https://passt.top/passt/commit/?id=2aa63237109b97a55c85e4c86c72db0d055bfe7a. I don't have an environment to test it now. Maybe I can set up a debian vm later.
Another thing I noticed later:
[...]
diff --git a/udp_flow.h b/udp_flow.h index 14e0f92..158a0f6 100644 --- a/udp_flow.h +++ b/udp_flow.h @@ -16,6 +16,7 @@ * @flush1: @s[1] may have datagrams queued for other flows * @ts: Activity timestamp * @s: Socket fd (or -1) for each side of the flow + * @activity: Activity for each side of the flow */ struct udp_flow { /* Must be first element */ @@ -29,8 +30,20 @@ struct udp_flow {
time_t ts; int s[SIDES]; + uint8_t activity[SIDES]; };
+/** + * udp_flow_activity() - Track activity of a udp flow + * @uflow: UDP flow + * @sidei: Side index of the flow + */ +static inline void udp_flow_activity(struct udp_flow *uflow, unsigned int sidei) +{ + if (uflow->activity[sidei] < UINT8_MAX) + uflow->activity[sidei]++; +}
This is an inline function in a header file for no good reason. It could be a normal static function in udp.c. See also:
https://www.kernel.org/doc/html/latest/process/coding-style.html#the-inline-...
...and yes, it's two lines of code, but there's really no reason to decide we want to inline this instead of letting the compiler decide.
I see. I will remove the inline and move the function to udp.c
-- Stefano
-- Thanks,
Yumei Huang
-- Thanks, Yumei Huang
On Fri, 13 Feb 2026 14:45:24 +0800
Yumei Huang
On Fri, Feb 13, 2026 at 5:51 AM Stefano Brivio
wrote: Oops, I missed one point at a first review, and also during a quick test.
I just tried outbound DNS queries in pasta with single responses, not inbound traffic or passt in vhost-user mode. Then I realised that:
On Thu, 12 Feb 2026 16:04:14 +0800 Yumei Huang
wrote: [...] @@ -954,6 +964,7 @@ void udp_sock_handler(const struct ctx *c, union epoll_ref ref,
flow_trace(uflow, "Received data on reply socket"); uflow->ts = now->tv_sec; + udp_flow_activity(uflow, !tosidx.sidei);
...this only covers three of the four paths we need to act upon:
1. inbound datagrams received on the reply socket via udp_buf_sock_to_tap(), called from here
2. inbound datagrams received on the reply socket in passt's vhost-user mode, that's udp_vu_sock_recv(), also called from here
3. "spliced" sockets (that's not really the case for UDP, we can't call splice(), but a pair of recvmmsg() / sendmmsg()), that is, loopback UDP traffic, handled by udp_sock_to_sock(), called from here as well
but not:
4. outbound, non-spliced datagrams from container/guest: that's udp_tap_handler(), in both vhost-user and non-vhost-user cases, or udp_flow_from_tap() in udp_flow.c.
I guess we want to take care of this directly from udp_flow_from_tap(), for consistency, because that's also where we update the timestamp value:
sidx = flow_lookup_sa(c, IPPROTO_UDP, pif, s_in, dst, port); if ((uflow = udp_at_sidx(sidx))) { uflow->ts = now->tv_sec;
^^^ here
return flow_sidx_opposite(sidx); }
I haven't really tested this side of it but it should be fairly easy with socat and a UDP "server" inside pasta or a guest.
Somehow, it worked well in my tests with pasta, it looks like the if condition always returns false.
Hmm, weird, it should return false only for the first *inbound* datagram of a UDP flow.
But now when I test with passt, it becomes an issue and we need to track the activity here as you mentioned.
Besides, I also noticed we update the timestamp value in udp_flow_from_sock() as well. I feel we should call udp_flow_activity() there too, but couldn't come up with a test to prove it.
I haven't really checked, but udp_sock_handler() should anyway be called for the datagram triggering udp_flow_from_sock(), so I don't think you need an extra call to udp_flow_activity() there. But you should check that with a pair of debugging prints, I guess.
On top of it, I just found two other issues. 1. in udp_flow_new(), we should initialize uflow->activity[INISIDE] to 1 instead of 0. Otherwise, we fail to track the first datagram.
Same here, I *thought* that calling udp_flow_activity() from udp_sock_handler() *and* udp_tap_handler() would anyway account for the first datagram, but I didn't check.
2. I guess we need to add the profs entries (nf_conntrack_udp_timeout and nf_conntrack_udp_timeout_stream) to apparmor like the tcp ones in https://passt.top/passt/commit/?id=2aa63237109b97a55c85e4c86c72db0d055bfe7a. I don't have an environment to test it now. Maybe I can set up a debian vm later.
Ah, right, good catch. The rules are quite obvious, so you can just add them to the patch, and I'll test them later on Debian anyway. -- Stefano
On Fri, Feb 13, 2026 at 3:08 PM Stefano Brivio
On Fri, 13 Feb 2026 14:45:24 +0800 Yumei Huang
wrote: On Fri, Feb 13, 2026 at 5:51 AM Stefano Brivio
wrote: Oops, I missed one point at a first review, and also during a quick test.
I just tried outbound DNS queries in pasta with single responses, not inbound traffic or passt in vhost-user mode. Then I realised that:
On Thu, 12 Feb 2026 16:04:14 +0800 Yumei Huang
wrote: [...] @@ -954,6 +964,7 @@ void udp_sock_handler(const struct ctx *c, union epoll_ref ref,
flow_trace(uflow, "Received data on reply socket"); uflow->ts = now->tv_sec; + udp_flow_activity(uflow, !tosidx.sidei);
...this only covers three of the four paths we need to act upon:
1. inbound datagrams received on the reply socket via udp_buf_sock_to_tap(), called from here
2. inbound datagrams received on the reply socket in passt's vhost-user mode, that's udp_vu_sock_recv(), also called from here
3. "spliced" sockets (that's not really the case for UDP, we can't call splice(), but a pair of recvmmsg() / sendmmsg()), that is, loopback UDP traffic, handled by udp_sock_to_sock(), called from here as well
but not:
4. outbound, non-spliced datagrams from container/guest: that's udp_tap_handler(), in both vhost-user and non-vhost-user cases, or udp_flow_from_tap() in udp_flow.c.
I guess we want to take care of this directly from udp_flow_from_tap(), for consistency, because that's also where we update the timestamp value:
sidx = flow_lookup_sa(c, IPPROTO_UDP, pif, s_in, dst, port); if ((uflow = udp_at_sidx(sidx))) { uflow->ts = now->tv_sec;
^^^ here
return flow_sidx_opposite(sidx); }
I haven't really tested this side of it but it should be fairly easy with socat and a UDP "server" inside pasta or a guest.
Somehow, it worked well in my tests with pasta, it looks like the if condition always returns false.
Hmm, weird, it should return false only for the first *inbound* datagram of a UDP flow.
But now when I test with passt, it becomes an issue and we need to track the activity here as you mentioned.
Besides, I also noticed we update the timestamp value in udp_flow_from_sock() as well. I feel we should call udp_flow_activity() there too, but couldn't come up with a test to prove it.
I haven't really checked, but udp_sock_handler() should anyway be called for the datagram triggering udp_flow_from_sock(), so I don't think you need an extra call to udp_flow_activity() there.
But you should check that with a pair of debugging prints, I guess.
Actually I did. udp_sock_handler() is called everytime there is new data from the socket. But in my test, udp_flow_from_sock() is only called for the first datagram, so the if condition after flow_lookup_sa() always returns false, and a new UDP flow is created. Tried either spliced / non-spliced, pasta / passt case, no exceptions observed. I was wondering if there is a scenario I'm not aware of.
On top of it, I just found two other issues. 1. in udp_flow_new(), we should initialize uflow->activity[INISIDE] to 1 instead of 0. Otherwise, we fail to track the first datagram.
Same here, I *thought* that calling udp_flow_activity() from udp_sock_handler() *and* udp_tap_handler() would anyway account for the first datagram, but I didn't check.
udp_sock_handler() is only called *after* the flow is created. But only when the first datagram comes, we create the flow. Similarly, udp_flow_from_tap() (called by udp_tap_handler()) calls udp_flow_new() to create a new flow for the first datagram too. That's why we missed the first one.
2. I guess we need to add the profs entries (nf_conntrack_udp_timeout and nf_conntrack_udp_timeout_stream) to apparmor like the tcp ones in
https://passt.top/passt/commit/?id=2aa63237109b97a55c85e4c86c72db0d055bfe7a .
I don't have an environment to test it now. Maybe I can set up a debian vm later.
Ah, right, good catch. The rules are quite obvious, so you can just add them to the patch, and I'll test them later on Debian anyway.
Great, will do!
-- Stefano
-- Thanks, Yumei Huang
[Side note: can you disable sending HTML emails, otherwise they won't
be archived for the passt-dev list for simplicity / security? Thanks]
On Fri, 13 Feb 2026 15:49:41 +0800
Yumei Huang
On Fri, Feb 13, 2026 at 3:08 PM Stefano Brivio
wrote: On Fri, 13 Feb 2026 14:45:24 +0800 Yumei Huang
wrote: On Fri, Feb 13, 2026 at 5:51 AM Stefano Brivio
wrote: Oops, I missed one point at a first review, and also during a quick test.
I just tried outbound DNS queries in pasta with single responses, not inbound traffic or passt in vhost-user mode. Then I realised that:
On Thu, 12 Feb 2026 16:04:14 +0800 Yumei Huang
wrote: [...] @@ -954,6 +964,7 @@ void udp_sock_handler(const struct ctx *c, union epoll_ref ref,
flow_trace(uflow, "Received data on reply socket"); uflow->ts = now->tv_sec; + udp_flow_activity(uflow, !tosidx.sidei);
...this only covers three of the four paths we need to act upon:
1. inbound datagrams received on the reply socket via udp_buf_sock_to_tap(), called from here
2. inbound datagrams received on the reply socket in passt's vhost-user mode, that's udp_vu_sock_recv(), also called from here
3. "spliced" sockets (that's not really the case for UDP, we can't call splice(), but a pair of recvmmsg() / sendmmsg()), that is, loopback UDP traffic, handled by udp_sock_to_sock(), called from here as well
but not:
4. outbound, non-spliced datagrams from container/guest: that's udp_tap_handler(), in both vhost-user and non-vhost-user cases, or udp_flow_from_tap() in udp_flow.c.
I guess we want to take care of this directly from udp_flow_from_tap(), for consistency, because that's also where we update the timestamp value:
sidx = flow_lookup_sa(c, IPPROTO_UDP, pif, s_in, dst, port); if ((uflow = udp_at_sidx(sidx))) { uflow->ts = now->tv_sec;
^^^ here
return flow_sidx_opposite(sidx); }
I haven't really tested this side of it but it should be fairly easy with socat and a UDP "server" inside pasta or a guest.
Somehow, it worked well in my tests with pasta, it looks like the if condition always returns false.
Hmm, weird, it should return false only for the first *inbound* datagram of a UDP flow.
But now when I test with passt, it becomes an issue and we need to track the activity here as you mentioned.
Besides, I also noticed we update the timestamp value in udp_flow_from_sock() as well. I feel we should call udp_flow_activity() there too, but couldn't come up with a test to prove it.
I haven't really checked, but udp_sock_handler() should anyway be called for the datagram triggering udp_flow_from_sock(), so I don't think you need an extra call to udp_flow_activity() there.
But you should check that with a pair of debugging prints, I guess.
Actually I did. udp_sock_handler() is called everytime there is new data from the socket.
Okay, so the udp_flow_activity() you already added (at least for the socket -> tap path) is enough, right...?
But in my test, udp_flow_from_sock() is only called for the first datagram, so the if condition after flow_lookup_sa() always returns false, and a new UDP flow is created.
Ah, right! See below.
Tried either spliced / non-spliced, pasta / passt case, no exceptions observed. I was wondering if there is a scenario I'm not aware of.
Yes, I think it's just for one corner case David described in the "Flow sockets" section of the "Theory of Operation" documentation in udp.c: * NOTE: A flow socket can have a bound address overlapping with a listening * socket. That will happen naturally for flows initiated from a socket, but is * also possible (though unlikely) for tap initiated flows, depending on the * source port. We assume datagrams for the flow will come to a connect()ed * socket in preference to a listening socket. The sample program * doc/platform-requirements/reuseaddr-priority.c documents and tests that * assumption. ...if they don't come through the connect()ed socket, we would end up in that case. Long story short, we need to update the activity array there as well, because it could happen. I'm not sure if reuseaddr-priority.c can be used to test this case together with pasta, I don't think it's really needed though.
On top of it, I just found two other issues. 1. in udp_flow_new(), we should initialize uflow->activity[INISIDE] to 1 instead of 0. Otherwise, we fail to track the first datagram.
Same here, I *thought* that calling udp_flow_activity() from udp_sock_handler() *and* udp_tap_handler() would anyway account for the first datagram, but I didn't check.
udp_sock_handler() is only called *after* the flow is created. But only when the first datagram comes, we create the flow. Similarly, udp_flow_from_tap() (called by udp_tap_handler()) calls udp_flow_new() to create a new flow for the first datagram too. That's why we missed the first one.
Oh, I see, thanks for the explanation. -- Stefano
On Fri, Feb 13, 2026 at 5:13 PM Stefano Brivio
[Side note: can you disable sending HTML emails, otherwise they won't be archived for the passt-dev list for simplicity / security? Thanks]
Yeah, sorry about that, will keep in mind to enable the plain text mode.
On Fri, 13 Feb 2026 15:49:41 +0800 Yumei Huang
wrote: On Fri, Feb 13, 2026 at 3:08 PM Stefano Brivio
wrote: On Fri, 13 Feb 2026 14:45:24 +0800 Yumei Huang
wrote: On Fri, Feb 13, 2026 at 5:51 AM Stefano Brivio
wrote: Oops, I missed one point at a first review, and also during a quick test.
I just tried outbound DNS queries in pasta with single responses, not inbound traffic or passt in vhost-user mode. Then I realised that:
On Thu, 12 Feb 2026 16:04:14 +0800 Yumei Huang
wrote: [...] @@ -954,6 +964,7 @@ void udp_sock_handler(const struct ctx *c, union epoll_ref ref,
flow_trace(uflow, "Received data on reply socket"); uflow->ts = now->tv_sec; + udp_flow_activity(uflow, !tosidx.sidei);
...this only covers three of the four paths we need to act upon:
1. inbound datagrams received on the reply socket via udp_buf_sock_to_tap(), called from here
2. inbound datagrams received on the reply socket in passt's vhost-user mode, that's udp_vu_sock_recv(), also called from here
3. "spliced" sockets (that's not really the case for UDP, we can't call splice(), but a pair of recvmmsg() / sendmmsg()), that is, loopback UDP traffic, handled by udp_sock_to_sock(), called from here as well
but not:
4. outbound, non-spliced datagrams from container/guest: that's udp_tap_handler(), in both vhost-user and non-vhost-user cases, or udp_flow_from_tap() in udp_flow.c.
I guess we want to take care of this directly from udp_flow_from_tap(), for consistency, because that's also where we update the timestamp value:
sidx = flow_lookup_sa(c, IPPROTO_UDP, pif, s_in, dst, port); if ((uflow = udp_at_sidx(sidx))) { uflow->ts = now->tv_sec;
^^^ here
return flow_sidx_opposite(sidx); }
I haven't really tested this side of it but it should be fairly easy with socat and a UDP "server" inside pasta or a guest.
Somehow, it worked well in my tests with pasta, it looks like the if condition always returns false.
Hmm, weird, it should return false only for the first *inbound* datagram of a UDP flow.
But now when I test with passt, it becomes an issue and we need to track the activity here as you mentioned.
Besides, I also noticed we update the timestamp value in udp_flow_from_sock() as well. I feel we should call udp_flow_activity() there too, but couldn't come up with a test to prove it.
I haven't really checked, but udp_sock_handler() should anyway be called for the datagram triggering udp_flow_from_sock(), so I don't think you need an extra call to udp_flow_activity() there.
But you should check that with a pair of debugging prints, I guess.
Actually I did. udp_sock_handler() is called everytime there is new data from the socket.
Okay, so the udp_flow_activity() you already added (at least for the socket -> tap path) is enough, right...?
Yes, it's enough for the socket->tap path.
But in my test, udp_flow_from_sock() is only called for the first datagram, so the if condition after flow_lookup_sa() always returns false, and a new UDP flow is created.
Ah, right! See below.
Tried either spliced / non-spliced, pasta / passt case, no exceptions observed. I was wondering if there is a scenario I'm not aware of.
Yes, I think it's just for one corner case David described in the "Flow sockets" section of the "Theory of Operation" documentation in udp.c:
* NOTE: A flow socket can have a bound address overlapping with a listening * socket. That will happen naturally for flows initiated from a socket, but is * also possible (though unlikely) for tap initiated flows, depending on the * source port. We assume datagrams for the flow will come to a connect()ed * socket in preference to a listening socket. The sample program * doc/platform-requirements/reuseaddr-priority.c documents and tests that * assumption.
...if they don't come through the connect()ed socket, we would end up in that case.
Long story short, we need to update the activity array there as well, because it could happen. I'm not sure if reuseaddr-priority.c can be used to test this case together with pasta, I don't think it's really needed though.
Thanks, I will add that and send v2 soon.
On top of it, I just found two other issues. 1. in udp_flow_new(), we should initialize uflow->activity[INISIDE] to 1 instead of 0. Otherwise, we fail to track the first datagram.
Same here, I *thought* that calling udp_flow_activity() from udp_sock_handler() *and* udp_tap_handler() would anyway account for the first datagram, but I didn't check.
udp_sock_handler() is only called *after* the flow is created. But only when the first datagram comes, we create the flow. Similarly, udp_flow_from_tap() (called by udp_tap_handler()) calls udp_flow_new() to create a new flow for the first datagram too. That's why we missed the first one.
Oh, I see, thanks for the explanation.
-- Stefano
-- Thanks, Yumei Huang
On Fri, 13 Feb 2026 17:54:56 +0800
Yumei Huang
On Fri, Feb 13, 2026 at 5:13 PM Stefano Brivio
wrote: [Side note: can you disable sending HTML emails, otherwise they won't be archived for the passt-dev list for simplicity / security? Thanks]
Yeah, sorry about that, will keep in mind to enable the plain text mode.
On Fri, 13 Feb 2026 15:49:41 +0800 Yumei Huang
wrote: On Fri, Feb 13, 2026 at 3:08 PM Stefano Brivio
wrote: On Fri, 13 Feb 2026 14:45:24 +0800 Yumei Huang
wrote: On Fri, Feb 13, 2026 at 5:51 AM Stefano Brivio
wrote: Oops, I missed one point at a first review, and also during a quick test.
I just tried outbound DNS queries in pasta with single responses, not inbound traffic or passt in vhost-user mode. Then I realised that:
On Thu, 12 Feb 2026 16:04:14 +0800 Yumei Huang
wrote: > [...] > @@ -954,6 +964,7 @@ void udp_sock_handler(const struct ctx *c, union epoll_ref ref, > > flow_trace(uflow, "Received data on reply socket"); > uflow->ts = now->tv_sec; > + udp_flow_activity(uflow, !tosidx.sidei);
...this only covers three of the four paths we need to act upon:
1. inbound datagrams received on the reply socket via udp_buf_sock_to_tap(), called from here
2. inbound datagrams received on the reply socket in passt's vhost-user mode, that's udp_vu_sock_recv(), also called from here
3. "spliced" sockets (that's not really the case for UDP, we can't call splice(), but a pair of recvmmsg() / sendmmsg()), that is, loopback UDP traffic, handled by udp_sock_to_sock(), called from here as well
but not:
4. outbound, non-spliced datagrams from container/guest: that's udp_tap_handler(), in both vhost-user and non-vhost-user cases, or udp_flow_from_tap() in udp_flow.c.
I guess we want to take care of this directly from udp_flow_from_tap(), for consistency, because that's also where we update the timestamp value:
sidx = flow_lookup_sa(c, IPPROTO_UDP, pif, s_in, dst, port); if ((uflow = udp_at_sidx(sidx))) { uflow->ts = now->tv_sec;
^^^ here
return flow_sidx_opposite(sidx); }
I haven't really tested this side of it but it should be fairly easy with socat and a UDP "server" inside pasta or a guest.
Somehow, it worked well in my tests with pasta, it looks like the if condition always returns false.
Hmm, weird, it should return false only for the first *inbound* datagram of a UDP flow.
But now when I test with passt, it becomes an issue and we need to track the activity here as you mentioned.
Besides, I also noticed we update the timestamp value in udp_flow_from_sock() as well. I feel we should call udp_flow_activity() there too, but couldn't come up with a test to prove it.
I haven't really checked, but udp_sock_handler() should anyway be called for the datagram triggering udp_flow_from_sock(), so I don't think you need an extra call to udp_flow_activity() there.
But you should check that with a pair of debugging prints, I guess.
Actually I did. udp_sock_handler() is called everytime there is new data from the socket.
Okay, so the udp_flow_activity() you already added (at least for the socket -> tap path) is enough, right...?
Yes, it's enough for the socket->tap path.
But in my test, udp_flow_from_sock() is only called for the first datagram, so the if condition after flow_lookup_sa() always returns false, and a new UDP flow is created.
Ah, right! See below.
Tried either spliced / non-spliced, pasta / passt case, no exceptions observed. I was wondering if there is a scenario I'm not aware of.
Yes, I think it's just for one corner case David described in the "Flow sockets" section of the "Theory of Operation" documentation in udp.c:
* NOTE: A flow socket can have a bound address overlapping with a listening * socket. That will happen naturally for flows initiated from a socket, but is * also possible (though unlikely) for tap initiated flows, depending on the * source port. We assume datagrams for the flow will come to a connect()ed * socket in preference to a listening socket. The sample program * doc/platform-requirements/reuseaddr-priority.c documents and tests that * assumption.
...if they don't come through the connect()ed socket, we would end up in that case.
Long story short, we need to update the activity array there as well, because it could happen. I'm not sure if reuseaddr-priority.c can be used to test this case together with pasta, I don't think it's really needed though.
Thanks, I will add that and send v2 soon.
Actually, if we always want to update the 'activity' array when we update the timestamp, maybe you could add a helper that does both, update 'ts' and 'activity'. It could still be called ...activity() because the timestamp is also an activity timestamp. I haven't checked all the possible paths though, I'm not sure if it's the right thing to do. -- Stefano
On Fri, Feb 13, 2026 at 6:00 PM Stefano Brivio
On Fri, 13 Feb 2026 17:54:56 +0800 Yumei Huang
wrote: On Fri, Feb 13, 2026 at 5:13 PM Stefano Brivio
wrote: [Side note: can you disable sending HTML emails, otherwise they won't be archived for the passt-dev list for simplicity / security? Thanks]
Yeah, sorry about that, will keep in mind to enable the plain text mode.
On Fri, 13 Feb 2026 15:49:41 +0800 Yumei Huang
wrote: On Fri, Feb 13, 2026 at 3:08 PM Stefano Brivio
wrote: On Fri, 13 Feb 2026 14:45:24 +0800 Yumei Huang
wrote: On Fri, Feb 13, 2026 at 5:51 AM Stefano Brivio
wrote: > Oops, I missed one point at a first review, and also during a quick > test. > > I just tried outbound DNS queries in pasta with single responses, not > inbound traffic or passt in vhost-user mode. Then I realised > that: > > On Thu, 12 Feb 2026 16:04:14 +0800 > Yumei Huang
wrote: > > > [...] > > @@ -954,6 +964,7 @@ void udp_sock_handler(const struct ctx *c, union > epoll_ref ref, > > > > flow_trace(uflow, "Received data on reply socket"); > > uflow->ts = now->tv_sec; > > + udp_flow_activity(uflow, !tosidx.sidei); > > ...this only covers three of the four paths we need to act upon: > > 1. inbound datagrams received on the reply socket via > udp_buf_sock_to_tap(), called from here > > 2. inbound datagrams received on the reply socket in passt's vhost-user > mode, that's udp_vu_sock_recv(), also called from here > > 3. "spliced" sockets (that's not really the case for UDP, we can't call > splice(), but a pair of recvmmsg() / sendmmsg()), that is, loopback > UDP traffic, handled by udp_sock_to_sock(), called from here as well > > but not: > > 4. outbound, non-spliced datagrams from container/guest: that's > udp_tap_handler(), in both vhost-user and non-vhost-user cases, or > udp_flow_from_tap() in udp_flow.c. > > I guess we want to take care of this directly from udp_flow_from_tap(), > for consistency, because that's also where we update the timestamp > value: > > sidx = flow_lookup_sa(c, IPPROTO_UDP, pif, s_in, dst, port); > if ((uflow = udp_at_sidx(sidx))) { > uflow->ts = now->tv_sec; > > ^^^ here > > return flow_sidx_opposite(sidx); > } > > I haven't really tested this side of it but it should be fairly easy > with socat and a UDP "server" inside pasta or a guest. Somehow, it worked well in my tests with pasta, it looks like the if condition always returns false.
Hmm, weird, it should return false only for the first *inbound* datagram of a UDP flow.
But now when I test with passt, it becomes an issue and we need to track the activity here as you mentioned.
Besides, I also noticed we update the timestamp value in udp_flow_from_sock() as well. I feel we should call udp_flow_activity() there too, but couldn't come up with a test to prove it.
I haven't really checked, but udp_sock_handler() should anyway be called for the datagram triggering udp_flow_from_sock(), so I don't think you need an extra call to udp_flow_activity() there.
But you should check that with a pair of debugging prints, I guess.
Actually I did. udp_sock_handler() is called everytime there is new data from the socket.
Okay, so the udp_flow_activity() you already added (at least for the socket -> tap path) is enough, right...?
Yes, it's enough for the socket->tap path.
But in my test, udp_flow_from_sock() is only called for the first datagram, so the if condition after flow_lookup_sa() always returns false, and a new UDP flow is created.
Ah, right! See below.
Tried either spliced / non-spliced, pasta / passt case, no exceptions observed. I was wondering if there is a scenario I'm not aware of.
Yes, I think it's just for one corner case David described in the "Flow sockets" section of the "Theory of Operation" documentation in udp.c:
* NOTE: A flow socket can have a bound address overlapping with a listening * socket. That will happen naturally for flows initiated from a socket, but is * also possible (though unlikely) for tap initiated flows, depending on the * source port. We assume datagrams for the flow will come to a connect()ed * socket in preference to a listening socket. The sample program * doc/platform-requirements/reuseaddr-priority.c documents and tests that * assumption.
...if they don't come through the connect()ed socket, we would end up in that case.
Long story short, we need to update the activity array there as well, because it could happen. I'm not sure if reuseaddr-priority.c can be used to test this case together with pasta, I don't think it's really needed though.
Thanks, I will add that and send v2 soon.
Actually, if we always want to update the 'activity' array when we update the timestamp, maybe you could add a helper that does both, update 'ts' and 'activity'.
It could still be called ...activity() because the timestamp is also an activity timestamp.
Good idea. Guess I sent v2 too soon.
I haven't checked all the possible paths though, I'm not sure if it's the right thing to do.
I will check tmr. If no other changes are needed, I will sent v3 then.
-- Stefano
-- Thanks, Yumei Huang
On Fri, 13 Feb 2026 18:04:59 +0800
Yumei Huang
On Fri, Feb 13, 2026 at 6:00 PM Stefano Brivio
wrote: On Fri, 13 Feb 2026 17:54:56 +0800 Yumei Huang
wrote: On Fri, Feb 13, 2026 at 5:13 PM Stefano Brivio
wrote: [Side note: can you disable sending HTML emails, otherwise they won't be archived for the passt-dev list for simplicity / security? Thanks]
Yeah, sorry about that, will keep in mind to enable the plain text mode.
On Fri, 13 Feb 2026 15:49:41 +0800 Yumei Huang
wrote: On Fri, Feb 13, 2026 at 3:08 PM Stefano Brivio
wrote: On Fri, 13 Feb 2026 14:45:24 +0800 Yumei Huang
wrote: > On Fri, Feb 13, 2026 at 5:51 AM Stefano Brivio
wrote: > > > Oops, I missed one point at a first review, and also during a quick > > test. > > > > I just tried outbound DNS queries in pasta with single responses, not > > inbound traffic or passt in vhost-user mode. Then I realised > > that: > > > > On Thu, 12 Feb 2026 16:04:14 +0800 > > Yumei Huang wrote: > > > > > [...] > > > @@ -954,6 +964,7 @@ void udp_sock_handler(const struct ctx *c, union > > epoll_ref ref, > > > > > > flow_trace(uflow, "Received data on reply socket"); > > > uflow->ts = now->tv_sec; > > > + udp_flow_activity(uflow, !tosidx.sidei); > > > > ...this only covers three of the four paths we need to act upon: > > > > 1. inbound datagrams received on the reply socket via > > udp_buf_sock_to_tap(), called from here > > > > 2. inbound datagrams received on the reply socket in passt's vhost-user > > mode, that's udp_vu_sock_recv(), also called from here > > > > 3. "spliced" sockets (that's not really the case for UDP, we can't call > > splice(), but a pair of recvmmsg() / sendmmsg()), that is, loopback > > UDP traffic, handled by udp_sock_to_sock(), called from here as well > > > > but not: > > > > 4. outbound, non-spliced datagrams from container/guest: that's > > udp_tap_handler(), in both vhost-user and non-vhost-user cases, or > > udp_flow_from_tap() in udp_flow.c. > > > > I guess we want to take care of this directly from udp_flow_from_tap(), > > for consistency, because that's also where we update the timestamp > > value: > > > > sidx = flow_lookup_sa(c, IPPROTO_UDP, pif, s_in, dst, port); > > if ((uflow = udp_at_sidx(sidx))) { > > uflow->ts = now->tv_sec; > > > > ^^^ here > > > > return flow_sidx_opposite(sidx); > > } > > > > I haven't really tested this side of it but it should be fairly easy > > with socat and a UDP "server" inside pasta or a guest. > > Somehow, it worked well in my tests with pasta, it looks like the if > condition always returns false. Hmm, weird, it should return false only for the first *inbound* datagram of a UDP flow.
> But now when I test with passt, it becomes > an issue and we need to track the activity here as you mentioned. > > Besides, I also noticed we update the timestamp value in > udp_flow_from_sock() as well. I feel we should call udp_flow_activity() > there too, but couldn't come up with a test to prove it.
I haven't really checked, but udp_sock_handler() should anyway be called for the datagram triggering udp_flow_from_sock(), so I don't think you need an extra call to udp_flow_activity() there.
But you should check that with a pair of debugging prints, I guess.
Actually I did. udp_sock_handler() is called everytime there is new data from the socket.
Okay, so the udp_flow_activity() you already added (at least for the socket -> tap path) is enough, right...?
Yes, it's enough for the socket->tap path.
But in my test, udp_flow_from_sock() is only called for the first datagram, so the if condition after flow_lookup_sa() always returns false, and a new UDP flow is created.
Ah, right! See below.
Tried either spliced / non-spliced, pasta / passt case, no exceptions observed. I was wondering if there is a scenario I'm not aware of.
Yes, I think it's just for one corner case David described in the "Flow sockets" section of the "Theory of Operation" documentation in udp.c:
* NOTE: A flow socket can have a bound address overlapping with a listening * socket. That will happen naturally for flows initiated from a socket, but is * also possible (though unlikely) for tap initiated flows, depending on the * source port. We assume datagrams for the flow will come to a connect()ed * socket in preference to a listening socket. The sample program * doc/platform-requirements/reuseaddr-priority.c documents and tests that * assumption.
...if they don't come through the connect()ed socket, we would end up in that case.
Long story short, we need to update the activity array there as well, because it could happen. I'm not sure if reuseaddr-priority.c can be used to test this case together with pasta, I don't think it's really needed though.
Thanks, I will add that and send v2 soon.
Actually, if we always want to update the 'activity' array when we update the timestamp, maybe you could add a helper that does both, update 'ts' and 'activity'.
It could still be called ...activity() because the timestamp is also an activity timestamp.
Good idea. Guess I sent v2 too soon.
I haven't checked all the possible paths though, I'm not sure if it's the right thing to do.
I will check tmr. If no other changes are needed, I will sent v3 then.
Okay, thanks! -- Stefano
On Fri, Feb 13, 2026 at 6:17 PM Stefano Brivio
On Fri, 13 Feb 2026 18:04:59 +0800 Yumei Huang
wrote: On Fri, Feb 13, 2026 at 6:00 PM Stefano Brivio
wrote: On Fri, 13 Feb 2026 17:54:56 +0800 Yumei Huang
wrote: On Fri, Feb 13, 2026 at 5:13 PM Stefano Brivio
wrote: [Side note: can you disable sending HTML emails, otherwise they won't be archived for the passt-dev list for simplicity / security? Thanks]
Yeah, sorry about that, will keep in mind to enable the plain text mode.
On Fri, 13 Feb 2026 15:49:41 +0800 Yumei Huang
wrote: On Fri, Feb 13, 2026 at 3:08 PM Stefano Brivio
wrote: > On Fri, 13 Feb 2026 14:45:24 +0800 > Yumei Huang
wrote: > > > On Fri, Feb 13, 2026 at 5:51 AM Stefano Brivio > wrote: > > > > > Oops, I missed one point at a first review, and also during a quick > > > test. > > > > > > I just tried outbound DNS queries in pasta with single responses, not > > > inbound traffic or passt in vhost-user mode. Then I realised > > > that: > > > > > > On Thu, 12 Feb 2026 16:04:14 +0800 > > > Yumei Huang wrote: > > > > > > > [...] > > > > @@ -954,6 +964,7 @@ void udp_sock_handler(const struct ctx *c, > union > > > epoll_ref ref, > > > > > > > > flow_trace(uflow, "Received data on reply socket"); > > > > uflow->ts = now->tv_sec; > > > > + udp_flow_activity(uflow, !tosidx.sidei); > > > > > > ...this only covers three of the four paths we need to act upon: > > > > > > 1. inbound datagrams received on the reply socket via > > > udp_buf_sock_to_tap(), called from here > > > > > > 2. inbound datagrams received on the reply socket in passt's vhost-user > > > mode, that's udp_vu_sock_recv(), also called from here > > > > > > 3. "spliced" sockets (that's not really the case for UDP, we can't call > > > splice(), but a pair of recvmmsg() / sendmmsg()), that is, loopback > > > UDP traffic, handled by udp_sock_to_sock(), called from here as well > > > > > > but not: > > > > > > 4. outbound, non-spliced datagrams from container/guest: that's > > > udp_tap_handler(), in both vhost-user and non-vhost-user cases, or > > > udp_flow_from_tap() in udp_flow.c. > > > > > > I guess we want to take care of this directly from > udp_flow_from_tap(), > > > for consistency, because that's also where we update the timestamp > > > value: > > > > > > sidx = flow_lookup_sa(c, IPPROTO_UDP, pif, s_in, dst, port); > > > if ((uflow = udp_at_sidx(sidx))) { > > > uflow->ts = now->tv_sec; > > > > > > ^^^ here > > > > > > return flow_sidx_opposite(sidx); > > > } > > > > > > I haven't really tested this side of it but it should be fairly easy > > > with socat and a UDP "server" inside pasta or a guest. > > > > Somehow, it worked well in my tests with pasta, it looks like the if > > condition always returns false. > > Hmm, weird, it should return false only for the first *inbound* datagram > of a UDP flow. > > > But now when I test with passt, it becomes > > an issue and we need to track the activity here as you mentioned. > > > > Besides, I also noticed we update the timestamp value in > > udp_flow_from_sock() as well. I feel we should call udp_flow_activity() > > there too, but couldn't come up with a test to prove it. > > I haven't really checked, but udp_sock_handler() should anyway be > called for the datagram triggering udp_flow_from_sock(), so I don't > think you need an extra call to udp_flow_activity() there. > > But you should check that with a pair of debugging prints, I guess. Actually I did. udp_sock_handler() is called everytime there is new data from the socket.
Okay, so the udp_flow_activity() you already added (at least for the socket -> tap path) is enough, right...?
Yes, it's enough for the socket->tap path.
But in my test, udp_flow_from_sock() is only called for the first datagram, so the if condition after flow_lookup_sa() always returns false, and a new UDP flow is created.
Ah, right! See below.
Tried either spliced / non-spliced, pasta / passt case, no exceptions observed. I was wondering if there is a scenario I'm not aware of.
Yes, I think it's just for one corner case David described in the "Flow sockets" section of the "Theory of Operation" documentation in udp.c:
Just have another look, instead of this case, I feel it's more like the one described in https://passt.top/passt/commit/udp_flow.c?id=9725e79888374a4e4060a2d798f3407..., which is packets arriving between bind() and connect(), and udp_sock_fwd() / udp_flow_from_sock() is called again to forward the packets. In this case, we should count the activity. The timestamp might not be so accurate, but should be very close. So I'm keeping it.
* NOTE: A flow socket can have a bound address overlapping with a listening * socket. That will happen naturally for flows initiated from a socket, but is * also possible (though unlikely) for tap initiated flows, depending on the * source port. We assume datagrams for the flow will come to a connect()ed * socket in preference to a listening socket. The sample program * doc/platform-requirements/reuseaddr-priority.c documents and tests that * assumption.
...if they don't come through the connect()ed socket, we would end up in that case.
Long story short, we need to update the activity array there as well, because it could happen. I'm not sure if reuseaddr-priority.c can be used to test this case together with pasta, I don't think it's really needed though.
Thanks, I will add that and send v2 soon.
Actually, if we always want to update the 'activity' array when we update the timestamp, maybe you could add a helper that does both, update 'ts' and 'activity'.
It could still be called ...activity() because the timestamp is also an activity timestamp.
Good idea. Guess I sent v2 too soon.
I haven't checked all the possible paths though, I'm not sure if it's the right thing to do.
I will check tmr. If no other changes are needed, I will sent v3 then.
Okay, thanks!
-- Stefano
-- Thanks, Yumei Huang
On Sat, 14 Feb 2026 15:20:26 +0800
Yumei Huang
On Fri, Feb 13, 2026 at 6:17 PM Stefano Brivio
wrote: On Fri, 13 Feb 2026 18:04:59 +0800 Yumei Huang
wrote: On Fri, Feb 13, 2026 at 6:00 PM Stefano Brivio
wrote: On Fri, 13 Feb 2026 17:54:56 +0800 Yumei Huang
wrote: On Fri, Feb 13, 2026 at 5:13 PM Stefano Brivio
wrote: On Fri, 13 Feb 2026 15:49:41 +0800 Yumei Huang
wrote: > But in my test, udp_flow_from_sock() is only called for > the first datagram, so the if condition after flow_lookup_sa() always > returns false, and a new UDP flow is created.
Ah, right! See below.
> Tried either spliced / > non-spliced, pasta / passt case, no exceptions observed. I was wondering > if there is a scenario I'm not aware of.
Yes, I think it's just for one corner case David described in the "Flow sockets" section of the "Theory of Operation" documentation in udp.c:
[...]
Just have another look, instead of this case, I feel it's more like the one described in https://passt.top/passt/commit/udp_flow.c?id=9725e79888374a4e4060a2d798f3407..., which is packets arriving between bind() and connect(), and udp_sock_fwd() / udp_flow_from_sock() is called again to forward the packets. In this case, we should count the activity. The timestamp might not be so accurate, but should be very close. So I'm keeping it.
Hmm, I guess you're right, and actually that path shouldn't be called for listening sockets. In any case, if you update the packet count whenever the timestamp is updated, as you did on v3, that should be correct. Running tests there in a bit. -- Stefano
participants (2)
-
Stefano Brivio
-
Yumei Huang