Hi Lynxes,
Thanks for the report, and sorry for the delay. Some comments inline:
On Wed, 28 Oct 2026 14:15:59 +0100
Lynxes Exe
Hi,
A disclaimer before anything is owed: what you're about to read was written by Claude, and is the result of a bug fixing session that occurred on my personal home infrastructure. It was not intended as a bug hunt. Since it seems like this issue has been unreported and I couldn't find anything similar to it, and since what Claude reported seems potentially plausible, I'm putting it here. If AI assisted/generated bug reports are unwanted, I apologize.
At least as far as I'm concerned, in term of bugs reports, whatever makes sense and is usable is welcome. This report has a few dubious aspects (see below) but it might still be valuable, I think.
Original report -------------------------- I think a peer address can get stuck in pasta's low-RTT destination table and then lose window clamping for good.
This part wasn't clearly demonstrated.
That caused every new TCP connection from our LAN router's address to stall on RTOs until pasta was restarted. Details below.
This looks plausible and somewhat demonstrated.
I haven't found an existing report on Bugzilla or this list.
Correct, no known issues in this sense so far.
Version: pasta 2026_10_02.cba3570-3-g8ba4132 (the logic below is unchanged in current master) Setup: rootless Docker 29.6.1 via rootlesskit, --net=pasta, --port-driver=implicit, --mtu=65520 (pasta --tcp-ports=auto --udp-ports=auto --no-map-gw --ipv4-only ...) Kernel: 6.18.57 (Arch linux-lts), x86_64
What we think is happening -------------------------- We suspect the low-RTT destination table (low_rtt_dst[], tcp_rtt_dst_check() / tcp_rtt_dst_low()). Our LAN router's address, 10.0.0.1, ended up in it even though the router is several milliseconds away.
Nothing clearly confirms the "ended up in it" part (yet).
[...]
We could not read the table directly (no ptrace into pasta), so "10.0.0.1 is in the table" is inferred from this behaviour and the code path.
...I guess the problem here is training a language model about the difference between "inferred" and "suspected". :) One could ptrace() as root by the way, but that wouldn't help either. Could you make sure about this with a print in tcp_rtt_dst_check()? If that's not the case, you might need to further trace what's going on in tcp_update_seqack_wnd() instead. Rebuilding and using a modified version of passt is quick (it just depends on a C library): git clone git://passt.top/passt && cd passt [edit as needed] make && sudo make install # Or make install as root Both Podman and Docker / rootlesskit will pick up the local build under /usr/local/bin. You can cleanly remove it with 'make uninstall' as root once you're done.
Why we think 10.0.0.1 got into the table ---------------------------------------- A real RTT of 10 us or less to the router is impossible here (measured RTT 2-6 ms). Our suspicion comes from reading the kernel source; we have not caught it on a live socket:
- tcp_rtt_dst_check() tests (int)tinfo->tcpi_min_rtt > LOW_RTT_THRESHOLD. - The kernel initialises rtt_min to ~0U, both in tcp_init_sock() (net/ipv4/tcp.c) and for accepted sockets in tcp_create_openreq_child() (tcp_minisocks.c). tcpi_min_rtt stays ~0U until the first valid RTT sample. - tcp_synack_rtt_meas() takes no sample when the SYN-ACK was retransmitted (rtt_us = -1 unless !req->num_retrans). The same applies on the active side when the SYN was retransmitted (Karn). - Cast to int, ~0U becomes -1, which passes the "<= 10 us" test. - pasta runs this check in tcp_prepare_flags() when it sends the SYN (inbound) or SYN-ACK (outbound) to the guest. That is right after the host-side handshake, before any data ACK could provide a sample.
...maybe, yes.
We measured exactly such handshake retransmissions on this Wi-Fi/NAT path: 6 of 1,800 fresh connections took ~1.05 s, i.e. one 1 s retry. With hundreds of inbound and outbound connections to 10.0.0.1 per day, one retried handshake would be enough to insert it permanently.
The night the stalls became severe, the published container was also restarted about ten times while clients kept connecting, which made unusual handshakes more likely. We cannot tell exactly when the insertion happened.
Suggested fixes --------------- 1. In tcp_rtt_dst_check(), treat tcpi_min_rtt == ~0U (and 0) as "no sample" and skip the insertion. Compare as unsigned, or check tcpi_rtt / tcpi_rcv_rtt as well.
I'm not sure if we actually risk hitting 0: strictly speaking that would mean less than 1 µs which looks unlikely (even for simple veth pairs or local guests), but in practice there might be rounding errors somewhere. Do we need to skip 0? From a quick glance of the kernel paths involved I'd say we don't. Could you test this perhaps? Not build tested but I think you could modify the initial early return condition to: if (!min_rtt_cap || (int)tinfo->tcpi_min_rtt > LOW_RTT_THRESHOLD || tinfo->tcpi_min_rtt == ~0U /* not measured yet */) return;
2. Don't trust a single sample forever: give entries a lifetime, or evict an address when a later connection to it shows a min RTT above the threshold.
Reasonable. I guess we could add a timestamp and check if the entry expired (minutes?) where we update the RTT in tcp_update_seqack_wnd()... or even in tcp_rtt_dst_check(). No strong preference on my side, I didn't really think it through.
3. Log (at debug level) when an address enters the table. It was invisible here; the only visible signs were retransmissions inside the namespace.
Yeah, that would be good. I added this feature before we started logging anything, I think. Feel free to send a patch for that.
Workaround: restarting pasta clears the table.
Happy to test a patch or collect more data.
Let me know if you can check those few things I suggested, thanks. -- Stefano