public inbox for passt-dev@passt.top
 help / color / mirror / code / Atom feed
From: Stefano Brivio <sbrivio@redhat.com>
To: Lynxes Exe <lynxes.exe@gmail.com>
Cc: passt-dev@passt.top
Subject: Re: tcp: peer stuck in low_rtt_dst table loses window clamping, every new connection stalls on RTOs
Date: Wed, 04 Nov 2026 14:53:22 +0100 (CET)	[thread overview]
Message-ID: <20261104145322.2dc73741@elisabeth> (raw)
In-Reply-To: <CAJPNJUuUYSkW776P0hKmweHnKOqXtsXqwm4HXP-+jorXrSfFFg@mail.gmail.com>

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 <lynxes.exe@gmail.com> wrote:

> 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


      parent reply	other threads:[~2026-11-04 13:53 UTC|newest]

Thread overview: 3+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-28 13:15 Lynxes Exe
2026-10-29  1:11 ` David Gibson
2026-11-04 13:53 ` Stefano Brivio [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20261104145322.2dc73741@elisabeth \
    --to=sbrivio@redhat.com \
    --cc=lynxes.exe@gmail.com \
    --cc=passt-dev@passt.top \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
Code repositories for project(s) associated with this public inbox

	https://passt.top/passt

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for IMAP folder(s).