From 215bc046fa51ceb3a317bc2eab411abfdd75171a Mon Sep 17 00:00:00 2001 From: vali0901 Date: Wed, 17 Jun 2026 16:01:57 +0300 Subject: [PATCH 1/3] fix old_ack NACK discard, add tcp_nack CC, remove prints --- include/net/tcp.h | 1 + net/ipv4/Kconfig | 9 ++ net/ipv4/Makefile | 1 + net/ipv4/tcp_input.c | 46 ++++----- net/ipv4/tcp_nack.c | 217 ++++++++++++++++++++++++++++++++++++++++++ net/ipv4/tcp_output.c | 2 - 6 files changed, 252 insertions(+), 24 deletions(-) create mode 100644 net/ipv4/tcp_nack.c diff --git a/include/net/tcp.h b/include/net/tcp.h index 26c144ffe971e..e14e8f78924be 100644 --- a/include/net/tcp.h +++ b/include/net/tcp.h @@ -691,6 +691,7 @@ void tcp_reset(struct sock *sk, struct sk_buff *skb); void tcp_fin(struct sock *sk); void tcp_check_space(struct sock *sk); void tcp_sack_compress_send_ack(struct sock *sk); +void tcp_update_pacing_rate(struct sock *sk); static inline void tcp_cleanup_skb(struct sk_buff *skb) { diff --git a/net/ipv4/Kconfig b/net/ipv4/Kconfig index 6d2c97f8e9ef9..dddc9451efdbf 100644 --- a/net/ipv4/Kconfig +++ b/net/ipv4/Kconfig @@ -647,6 +647,15 @@ config TCP_CONG_DCTCP For further details see: http://simula.stanford.edu/~alizade/Site/DCTCP_files/dctcp-final.pdf +config TCP_CONG_NACK + tristate "NACK-based congestion control (experimental)" + default m + help + Experimental congestion control intended for trimming experiments. + When a trimming NACK is received, this algorithm reduces the + congestion window by one MSS (one segment). Useful for quick + experiments with NACK-driven rate reduction. + config TCP_CONG_CDG tristate "CAIA Delay-Gradient (CDG)" default n diff --git a/net/ipv4/Makefile b/net/ipv4/Makefile index ec36d2ec059e8..4721940e3ce9d 100644 --- a/net/ipv4/Makefile +++ b/net/ipv4/Makefile @@ -49,6 +49,7 @@ obj-$(CONFIG_TCP_CONG_BIC) += tcp_bic.o obj-$(CONFIG_TCP_CONG_CDG) += tcp_cdg.o obj-$(CONFIG_TCP_CONG_CUBIC) += tcp_cubic.o obj-$(CONFIG_TCP_CONG_DCTCP) += tcp_dctcp.o +obj-$(CONFIG_TCP_CONG_NACK) += tcp_nack.o obj-$(CONFIG_TCP_CONG_WESTWOOD) += tcp_westwood.o obj-$(CONFIG_TCP_CONG_HSTCP) += tcp_highspeed.o obj-$(CONFIG_TCP_CONG_HYBLA) += tcp_hybla.o diff --git a/net/ipv4/tcp_input.c b/net/ipv4/tcp_input.c index ccaeeb06c4271..d55d190636581 100644 --- a/net/ipv4/tcp_input.c +++ b/net/ipv4/tcp_input.c @@ -942,7 +942,7 @@ static void tcp_rtt_estimator(struct sock *sk, long mrtt_us) tp->srtt_us = max(1U, srtt); } -static void tcp_update_pacing_rate(struct sock *sk) +void tcp_update_pacing_rate(struct sock *sk) { const struct tcp_sock *tp = tcp_sk(sk); u64 rate; @@ -3046,9 +3046,6 @@ static void __tcp_handle_trimmed(struct sock *sk, struct sk_buff *skb, bool send tp->trimming_send_nak = 1; tp->nack_seq_to_send = TCP_SKB_CB(skb)->seq; - pr_info_ratelimited("tcp_trimming: received trimmed packet seq=%u state=%u trimming_ok=%u, sending NACK (receiver)\n", - tp->nack_seq_to_send, sk->sk_state, - tp->rx_opt.trimming_ok); if (send_nack_now) tcp_send_nack_ack(sk); else @@ -3064,11 +3061,8 @@ static bool tcp_handle_trimmed(struct sock *sk, struct sk_buff *skb, enum skb_dr struct tcphdr *th = tcp_hdr(skb); bool discard = false; - printk(KERN_DEBUG "tcp_trimming: preprocessing skb seq=%u len=%u th->doff=%u trimming_ok=%u\n", - TCP_SKB_CB(skb)->seq, skb->len, th->doff, tp->rx_opt.trimming_ok); if (!tp->rx_opt.trimming_ok) { - pr_info_ratelimited("tcp_trimming: ignoring trimmed skb on non-trimming connection (receiver trimming_ok=0)\n"); *reason = SKB_DROP_REASON_NOT_SPECIFIED; discard = true; } @@ -3082,9 +3076,6 @@ static bool tcp_handle_trimmed(struct sock *sk, struct sk_buff *skb, enum skb_dr } // else, for trimmed pure ACK, just process normally without sending NACK - printk(KERN_INFO "tcp_trimming: finished preprocessing skb seq=%u len=%u, discard=%d\n", - TCP_SKB_CB(skb)->seq, skb->len, discard); - return !discard; } @@ -3187,9 +3178,8 @@ static void tcp_identify_packet_loss(struct sock *sk, int *ack_flag) if (tcp_rtx_queue_empty(sk)) return; - if (*ack_flag & FLAG_NACK) { + if (*ack_flag & FLAG_NACK) tcp_trimming_mark_lost(sk); - } if (unlikely(tcp_is_reno(tp))) { tcp_newreno_mark_lost(sk, *ack_flag & FLAG_SND_UNA_ADVANCED); @@ -3201,6 +3191,10 @@ static void tcp_identify_packet_loss(struct sock *sk, int *ack_flag) if (prior_retrans > tp->retrans_out) *ack_flag |= FLAG_LOST_RETRANS; } + + // some bug, dont remember, this was the fix + if (tcp_is_reno(tp)) + tcp_limit_reno_sacked(tp); } /* Process an event, which can update packets-in-flight not trivially. @@ -4226,6 +4220,13 @@ static int tcp_ack(struct sock *sk, const struct sk_buff *skb, int flag) tcp_rate_gen(sk, delivered, lost, is_sack_reneg, sack_state.rate); tcp_cong_control(sk, ack, delivered, flag, sack_state.rate); tcp_xmit_recovery(sk, rexmit); + + /* We got a NACK for a segment -> path is alive, no need to + * count RTO + */ + if ((flag & FLAG_NACK) && !(flag & FLAG_SET_XMIT_TIMER)) + tcp_rearm_rto(sk); + return 1; no_queue: @@ -4248,10 +4249,18 @@ static int tcp_ack(struct sock *sk, const struct sk_buff *skb, int flag) old_ack: /* If data was SACKed, tag it and see if we should send more data. * If data was DSACKed, see if we can undo a cwnd reduction. + * + * We can get a NACK inside an old_ACK because od reordering; data + * must be retransmitted anyhow. */ - if (TCP_SKB_CB(skb)->sacked) { - flag |= tcp_sacktag_write_queue(sk, skb, prior_snd_una, - &sack_state); + if (tp->rx_opt.trimming_nack_rcvd) { + flag |= FLAG_NACK; + tp->rx_opt.trimming_nack_rcvd = false; + } + if (TCP_SKB_CB(skb)->sacked || (flag & FLAG_NACK)) { + if (TCP_SKB_CB(skb)->sacked) + flag |= tcp_sacktag_write_queue(sk, skb, prior_snd_una, + &sack_state); tcp_fastretrans_alert(sk, prior_snd_una, num_dupack, &flag, &rexmit); tcp_newly_delivered(sk, delivered, flag); @@ -6735,9 +6744,6 @@ static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb, if (tp->rx_opt.trimming_ok) { inet_sk(sk)->tos = (inet_sk(sk)->tos & INET_ECN_MASK) | (DSCP_TRIMMABLE << 2); } - pr_info("tcp_trimming: client SYN_SENT->ESTABLISHED trimming_ok=%u sysctl=%u\n", - tp->rx_opt.trimming_ok, - READ_ONCE(sock_net(sk)->ipv4.sysctl_tcp_trimming)); if (tp->rx_opt.saw_tstamp) { tp->rx_opt.tstamp_ok = 1; @@ -7089,10 +7095,6 @@ tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb) if (tp->rx_opt.trimming_ok) { inet_sk(sk)->tos = (inet_sk(sk)->tos & INET_ECN_MASK) | (DSCP_TRIMMABLE << 2); } - pr_info("tcp_trimming: server SYN_RECV->ESTABLISHED trimming_ok=%u sysctl=%u fastopen_rsk=%d\n", - tp->rx_opt.trimming_ok, - READ_ONCE(sock_net(sk)->ipv4.sysctl_tcp_trimming), - req ? 1 : 0); tcp_init_wl(tp, TCP_SKB_CB(skb)->seq); diff --git a/net/ipv4/tcp_nack.c b/net/ipv4/tcp_nack.c new file mode 100644 index 0000000000000..6364556a9dd7a --- /dev/null +++ b/net/ipv4/tcp_nack.c @@ -0,0 +1,217 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * NACK-driven congestion control for trimming experiments. + * + * This congestion control reduces `snd_cwnd` by one MSS for every + * `CA_ACK_NACK` notification. It also uses PRR for smooth decrease. + * In a draft state, a lot of stub function used, does not tacke the + * trimming_ok = false case; + */ + +#include +#include + +struct nack_ca { + u32 prior_cwnd; + u32 loss_cwnd; + u32 nack_count; + u32 pkts_acked; /* last observed pkts_acked sample */ + bool nack_pending; /* CA_ACK_NACK seen in in_ack_event for this ACK */ +}; + +static unsigned int nack_init_ssthresh __read_mostly = 200; +module_param(nack_init_ssthresh, uint, 0644); +MODULE_PARM_DESC(nack_init_ssthresh, + "Initial ssthresh in segments for new connections (0 = kernel default)"); + + +static void nack_init(struct sock *sk) +{ + struct nack_ca *ca = inet_csk_ca(sk); + struct tcp_sock *tp = tcp_sk(sk); + + BUILD_BUG_ON(sizeof(struct nack_ca) > ICSK_CA_PRIV_SIZE); + + ca->prior_cwnd = 0; + ca->loss_cwnd = 0; + ca->nack_count = 0; + ca->pkts_acked = 0; + ca->nack_pending = false; + + if (nack_init_ssthresh) + tp->snd_ssthresh = nack_init_ssthresh; +} + +static u32 nack_ssthresh(struct sock *sk) +{ + const struct tcp_sock *tp = tcp_sk(sk); + + u32 ssthresh = tcp_snd_cwnd(tp); // prevent double -1 on recovery entry + + return ssthresh; +} + +static void nack_in_ack_event(struct sock *sk, u32 flags) +{ + struct nack_ca *ca = inet_csk_ca(sk); + + if (flags & CA_ACK_NACK) + ca->nack_pending = true; // just flag it here +} + +static void nack_pkts_acked(struct sock *sk, const struct ack_sample *sample) +{ + struct nack_ca *ca = inet_csk_ca(sk); + + ca->pkts_acked = sample->pkts_acked; +} + +static void nack_cwnd_event(struct sock *sk, enum tcp_ca_event ev) +{ + struct tcp_sock *tp = tcp_sk(sk); + struct nack_ca *ca = inet_csk_ca(sk); + + switch (ev) { + case CA_EVENT_LOSS: + /* Record the cwnd at loss time for diagnostics/undo. */ + ca->loss_cwnd = tcp_snd_cwnd(tp); + break; + case CA_EVENT_TX_START: + /* stub: can be used to rehash or update state at tx start */ + break; + + case CA_EVENT_COMPLETE_CWR: + /* stub: can be used to update state upon completion of CWR phase */ + break; + + default: + /* ignore other events for now */ + break; + } +} + +static u32 nack_undo_cwnd(struct sock *sk) +{ + /* No undo: return the current cwnd as the canonical value. + * SACK is expected to be disabled for these experiments so DSACK + * undo behavior is unnecessary. + */ + struct tcp_sock *tp = tcp_sk(sk); + + return tcp_snd_cwnd(tp); +} + +static void nack_set_state(struct sock *sk, u8 new_state) +{ + struct nack_ca *ca = inet_csk_ca(sk); + struct tcp_sock *tp = tcp_sk(sk); + u8 old_state = inet_csk(sk)->icsk_ca_state; + + /* set_state() is invoked by tcp_set_ca_state() *before* it stores + * the new state, so `old_state` is the state we are leaving. + */ + + /* On entering Recovery record prior cwnd for possible undo. */ + if (new_state == TCP_CA_Recovery && old_state != TCP_CA_Recovery) + ca->prior_cwnd = tcp_snd_cwnd(tp); + + /* Recovery/CWR exit: the core returns us to Open once high_seq is + * acknowledged. By this point the NACK-driven decrements have + * converged on a sending rate that stopped producing loss, so + * adopt the current cwnd as the new ssthresh -- this is the rate we + * just proved to be safe. + */ + if (new_state == TCP_CA_Open && + (old_state == TCP_CA_Recovery || old_state == TCP_CA_CWR)) { + tp->snd_ssthresh = tcp_snd_cwnd(tp); + tp->snd_cwnd_stamp = tcp_jiffies32; + } + + /* Disable the core's cwnd-undo machinery for any cwnd-reduction + * state. Our NACK-driven decrements are deliberate reactions to + * real trimming losses, not spurious reordering/RTO artifacts. If + * we leave tp->undo_marker set, an ACK that crosses high_seq makes + * tcp_try_undo_recovery() treat the episode as spurious and revert + * both snd_cwnd and snd_ssthresh (back to TCP_INFINITE_SSTHRESH), + * wiping out every NACK reduction. Clearing undo_marker here (it + * was just set by tcp_init_undo() in tcp_enter_recovery/loss, which + * runs before this set_state callback) keeps the CA authoritative + * over cwnd and lets recovery exit cleanly to Open without undo. + */ + if (new_state == TCP_CA_Recovery || new_state == TCP_CA_Loss) + tp->undo_marker = 0; +} + +static void nack_cong_control(struct sock *sk, u32 ack, int flag, + const struct rate_sample *rs) +{ + struct tcp_sock *tp = tcp_sk(sk); + struct nack_ca *ca = inet_csk_ca(sk); + + bool nack_event = ca->nack_pending; + ca->nack_pending = false; + + if (nack_event) { + ca->nack_count++; + tp->snd_ssthresh = max_t(u32, tp->snd_ssthresh - 1, 2U); + } + + if (tcp_in_cwnd_reduction(sk)) { + /* CWR or Recovery: PRR owns the window. newly_acked_sacked and + * newly_lost come from the rate sample for this ACK. + */ + tcp_cwnd_reduction(sk, rs ? rs->acked_sacked : 0, + rs ? rs->losses : 0, flag); + } else if (rs && rs->acked_sacked) { + /* Open/Disorder: standard Reno additive increase. */ + tcp_reno_cong_avoid(sk, ack, rs->acked_sacked); + } + + tcp_update_pacing_rate(sk); +} + +static size_t nack_get_info(struct sock *sk, u32 ext, int *attr, union tcp_cc_info *info) +{ + /* Stub: expose no extra fields yet. Return 0 bytes of cc info. */ + return 0; +} + +static void nack_release(struct sock *sk) +{ + /* No dynamic allocations to free; stub present for symmetry. */ +} + +static struct tcp_congestion_ops tcp_nack __read_mostly = { + .init = nack_init, + .release = nack_release, + .ssthresh = nack_ssthresh, + .cong_control = nack_cong_control, + .cong_avoid = tcp_reno_cong_avoid, + .in_ack_event = nack_in_ack_event, + .pkts_acked = nack_pkts_acked, + .cwnd_event = nack_cwnd_event, + .set_state = nack_set_state, + .undo_cwnd = nack_undo_cwnd, + .get_info = nack_get_info, + + .owner = THIS_MODULE, + .name = "nack", + .flags = TCP_CONG_NON_RESTRICTED, +}; + +static int __init tcp_nack_register(void) +{ + return tcp_register_congestion_control(&tcp_nack); +} + +static void __exit tcp_nack_unregister(void) +{ + tcp_unregister_congestion_control(&tcp_nack); +} + +module_init(tcp_nack_register); +module_exit(tcp_nack_unregister); + +MODULE_AUTHOR("Jimmy McGill"); +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("NACK-driven congestion control: 1 MSS per NACK, Reno fallbacks"); diff --git a/net/ipv4/tcp_output.c b/net/ipv4/tcp_output.c index 505b00010b31d..92fdc90080629 100644 --- a/net/ipv4/tcp_output.c +++ b/net/ipv4/tcp_output.c @@ -1025,8 +1025,6 @@ static unsigned int tcp_established_options(struct sock *sk, struct sk_buff *skb */ opts->options |= OPTION_TRIMMING_NACK; size += TCPOLEN_TRIMMING_NACK_ALIGNED; - pr_info_ratelimited("tcp_trimming: emitting NACK option seq=%u (sender)\n", - tp->nack_seq_to_send); tp->trimming_send_nak = 0; } From 479df0825c2c7596ec6dde2e627e18e2d428257e Mon Sep 17 00:00:00 2001 From: vali0901 Date: Thu, 18 Jun 2026 18:52:07 +0300 Subject: [PATCH 2/3] Add decrease_factor for cwnd reduction + set init ssthresh to infinite --- net/ipv4/tcp_nack.c | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/net/ipv4/tcp_nack.c b/net/ipv4/tcp_nack.c index 6364556a9dd7a..a8028df72e9ce 100644 --- a/net/ipv4/tcp_nack.c +++ b/net/ipv4/tcp_nack.c @@ -16,14 +16,21 @@ struct nack_ca { u32 loss_cwnd; u32 nack_count; u32 pkts_acked; /* last observed pkts_acked sample */ + u32 nack_dec_accum; /* NACKs accumulated toward the next -1 reduction */ bool nack_pending; /* CA_ACK_NACK seen in in_ack_event for this ACK */ }; -static unsigned int nack_init_ssthresh __read_mostly = 200; +static unsigned int nack_init_ssthresh __read_mostly = 0; module_param(nack_init_ssthresh, uint, 0644); MODULE_PARM_DESC(nack_init_ssthresh, "Initial ssthresh in segments for new connections (0 = kernel default)"); +static unsigned int nack_cwnd_dec_factor __read_mostly = 1; +module_param(nack_cwnd_dec_factor, uint, 0644); +MODULE_PARM_DESC(nack_cwnd_dec_factor, + "Soften the per-NACK cwnd reduction: reduce by 1/this_factor per NACK " + "(apply -1 once every this_factor NACKs; default 1 = -1 per NACK)"); + static void nack_init(struct sock *sk) { @@ -36,6 +43,7 @@ static void nack_init(struct sock *sk) ca->loss_cwnd = 0; ca->nack_count = 0; ca->pkts_acked = 0; + ca->nack_dec_accum = 0; ca->nack_pending = false; if (nack_init_ssthresh) @@ -152,8 +160,21 @@ static void nack_cong_control(struct sock *sk, u32 ack, int flag, ca->nack_pending = false; if (nack_event) { + u32 factor = max_t(u32, nack_cwnd_dec_factor, 1U); + ca->nack_count++; - tp->snd_ssthresh = max_t(u32, tp->snd_ssthresh - 1, 2U); + + /* Reduce the PRR target (snd_ssthresh) by 1 MSS for every + * `factor` NACKs, i.e. by 1/factor per NACK on average. The + * accumulator carries the fractional remainder across ACKs so + * the long-run reduction rate is exactly 1/factor. factor == 1 + * reproduces the original "1 MSS per NACK" behaviour; a larger + * factor softens the cwnd drop under heavy trimming bursts. + */ + if (++ca->nack_dec_accum >= factor) { + ca->nack_dec_accum = 0; + tp->snd_ssthresh = max_t(u32, tp->snd_ssthresh - 1, 2U); + } } if (tcp_in_cwnd_reduction(sk)) { From 5a2d9ff65f339408d24fe01086c043edc1cdf0ac Mon Sep 17 00:00:00 2001 From: vali0901 Date: Thu, 18 Jun 2026 18:57:34 +0300 Subject: [PATCH 3/3] Several bugfixes and features: - skip PAWS check for TRIMMED & NACKs - deactivate any loss-inferring mechanisms when trimming_ok (sack scoreboard, TLP, newreno_mark_lost) - fix NACK processing - remove cwnd_undo capability - add TCPCB_NACK_FORCED retransmit (disabled by default) --- include/net/tcp.h | 1 + net/ipv4/tcp_input.c | 255 +++++++++++++++++++++++++++++++++++++++--- net/ipv4/tcp_output.c | 69 +++++++++++- 3 files changed, 307 insertions(+), 18 deletions(-) diff --git a/include/net/tcp.h b/include/net/tcp.h index e14e8f78924be..5e6e485503dee 100644 --- a/include/net/tcp.h +++ b/include/net/tcp.h @@ -955,6 +955,7 @@ enum tcp_skb_cb_sacked_flags { TCPCB_LOST = (1 << 2), /* SKB is lost */ TCPCB_TAGBITS = (TCPCB_SACKED_ACKED | TCPCB_SACKED_RETRANS | TCPCB_LOST), /* All tag bits */ + TCPCB_NACK_FORCED = (1 << 3), /* Trimming: NACK-signalled, retransmit past cwnd gate */ TCPCB_REPAIRED = (1 << 4), /* SKB repaired (no skb_mstamp_ns) */ TCPCB_EVER_RETRANS = (1 << 7), /* Ever retransmitted frame */ TCPCB_RETRANS = (TCPCB_SACKED_RETRANS | TCPCB_EVER_RETRANS | diff --git a/net/ipv4/tcp_input.c b/net/ipv4/tcp_input.c index d55d190636581..ecb96033d8e52 100644 --- a/net/ipv4/tcp_input.c +++ b/net/ipv4/tcp_input.c @@ -2389,8 +2389,17 @@ static bool tcp_time_to_recover(struct sock *sk, int flag) if (tp->lost_out) return true; - /* Not-A-Trick#2 : Classic rule... */ - if (!tcp_is_rack(sk) && tcp_dupack_heuristics(tp) > tp->reordering) + /* Not-A-Trick#2 : Classic rule... + * + * Trimming/NACK: when trimming was negotiated, loss is signalled + * explicitly by NACKs (which set tp->lost_out, caught by Trick#1 + * above) and by RTO. Do not enter recovery on the dupack/SACK + * reordering heuristic: this disables dupack-driven fast retransmit + * and removes any dependency on a high tcp_reordering to suppress + * false recovery under reordering. + */ + if (!tcp_is_rack(sk) && !tp->rx_opt.trimming_ok && + tcp_dupack_heuristics(tp) > tp->reordering) return true; return false; @@ -2483,6 +2492,27 @@ static inline bool tcp_packet_delayed(const struct tcp_sock *tp) { const struct sock *sk = (const struct sock *)tp; + /* Trimming/NACK: this predicate is the single question the whole undo + * subsystem asks -- "could this 'loss' have been a mere reorder, so the + * recovery was spurious and should be undone?". For a trimming socket a + * loss is signalled by a NACK, which is hard proof that the switch + * actually trimmed the segment; it was never merely delayed. So recovery + * is never spurious and must never be undone. Answering false here is + * the one place that disables tcp_try_undo_recovery(), tcp_try_undo_loss() + * and tcp_try_undo_partial() at once, keeping the normal state-machine + * flow intact instead of scattering FLAG_NACK guards across their call + * sites. F-RTO's own undo (tcp_try_undo_loss(sk, true)) is unaffected: it + * short-circuits on frto_undo before consulting this predicate. + * + * Note for future NACK + inferred-loss hybrids: when loss may also be + * inferred (RACK/NewReno) on a trimming socket, an inferred loss CAN be + * spurious and undo should apply to it. This blanket return would then + * need to be refined to "was THIS recovery episode NACK-driven?" rather + * than keyed on trimming_ok alone. + */ + if (tp->rx_opt.trimming_ok) + return false; + if (tp->retrans_stamp && tcp_tsopt_ecr_before(tp, tp->retrans_stamp)) return true; /* got echoed TS before first retransmission */ @@ -2571,6 +2601,30 @@ static void tcp_undo_cwnd_reduction(struct sock *sk, bool unmark_loss) { struct tcp_sock *tp = tcp_sk(sk); + /* Trimming/NACK: never reverse a congestion-window reduction nor clear + * loss marks for a trimming socket. Recovery is driven by NACKs, which + * are authoritative proof of real loss -- it is never spurious, so it + * must never be undone. This is the single chokepoint every undo path + * funnels through (tcp_try_undo_recovery/_dsack/_loss/_partial), so one + * gate here closes them all, including the two doors that open once SACK + * is enabled: the DSACK "all retransmits were duplicated" counter + * (tcp_may_undo()/tcp_try_undo_dsack()'s !undo_retrans) and F-RTO + * spurious-RTO undo (tcp_try_undo_loss(sk, true)). SACK can then be on + * purely to keep packets_in_flight accurate, without it ever + * second-guessing recovery, restoring cwnd/ssthresh, or wiping a + * TCPCB_LOST/NACK_FORCED mark. + * + * undo_marker is cleared so the bookkeeping stays consistent (no undo + * is pending) and the undo predicates stop firing for the rest of the + * episode rather than repeatedly reaching this no-op. cwnd, ssthresh, + * lost_out and every per-skb loss mark are deliberately left untouched. + * Only trimming sockets take this branch; normal TCP is unaffected. + */ + if (tp->rx_opt.trimming_ok) { + tp->undo_marker = 0; + return; + } + if (unmark_loss) { struct sk_buff *skb; @@ -3127,6 +3181,9 @@ static void tcp_trimming_mark_lost(struct sock *sk) /* Single wire segment already: mark the whole skb. */ if (tcp_skb_pcount(skb) <= 1) { tcp_mark_skb_lost(sk, skb); + /* Tag as NACK-signalled so tcp_xmit_retransmit_queue() + * retransmits it past the cwnd>=pif gate. */ + TCP_SKB_CB(skb)->sacked |= TCPCB_NACK_FORCED; return; } @@ -3167,6 +3224,9 @@ static void tcp_trimming_mark_lost(struct sock *sk) mss, mss, GFP_ATOMIC); tcp_mark_skb_lost(sk, target); + /* Tag as NACK-signalled so tcp_xmit_retransmit_queue() + * retransmits it past the cwnd>=pif gate. */ + TCP_SKB_CB(target)->sacked |= TCPCB_NACK_FORCED; return; } } @@ -3178,18 +3238,37 @@ static void tcp_identify_packet_loss(struct sock *sk, int *ack_flag) if (tcp_rtx_queue_empty(sk)) return; - if (*ack_flag & FLAG_NACK) - tcp_trimming_mark_lost(sk); - - if (unlikely(tcp_is_reno(tp))) { - tcp_newreno_mark_lost(sk, *ack_flag & FLAG_SND_UNA_ADVANCED); - } else if (tcp_is_rack(sk)) { - u32 prior_retrans = tp->retrans_out; - - if (tcp_rack_mark_lost(sk)) - *ack_flag &= ~FLAG_SET_XMIT_TIMER; - if (prior_retrans > tp->retrans_out) - *ack_flag |= FLAG_LOST_RETRANS; + /* Trimming/NACK: the NACK is the authoritative loss signal and is + * already marked lost up-front in tcp_fastretrans_alert() (before its + * sack-reneging/undo early-returns). Skip ALL heuristic loss marking + * here for trimming sockets -- both NewReno head-of-line marking and + * RACK timing -- so the socket retransmits only what the receiver + * actually NACKed (plus RTO), i.e. true "NACK + RTO only" recovery. + * + * This mirrors how tcp_update_scoreboard() (the RFC3517 SACK + * scoreboard) is already gated off for trimming sockets. Without this + * gate, tcp_newreno_mark_lost() marks the rtx-queue head lost on every + * partial ACK during recovery with no NACK behind it, retransmitting + * data the receiver never reported as trimmed; under reordering one + * copy is delivered while the spurious retransmit arrives trimmed and + * late, which makes the receiver emit a phantom NACK for already- + * delivered data. Gating it removes those non-NACK retransmits and the + * phantom NACKs they cause. + * + * tcp_limit_reno_sacked() below still runs so the reno dupack-emulation + * accounting (sacked_out, used for packets-in-flight) stays consistent. + */ + if (!tp->rx_opt.trimming_ok) { + if (unlikely(tcp_is_reno(tp))) { + tcp_newreno_mark_lost(sk, *ack_flag & FLAG_SND_UNA_ADVANCED); + } else if (tcp_is_rack(sk)) { + u32 prior_retrans = tp->retrans_out; + + if (tcp_rack_mark_lost(sk)) + *ack_flag &= ~FLAG_SET_XMIT_TIMER; + if (prior_retrans > tp->retrans_out) + *ack_flag |= FLAG_LOST_RETRANS; + } } // some bug, dont remember, this was the fix @@ -3222,6 +3301,87 @@ static void tcp_fastretrans_alert(struct sock *sk, const u32 prior_snd_una, if (!tp->packets_out && tp->sacked_out) tp->sacked_out = 0; + /* Trimming/NACK: register the explicitly NACKed loss up-front, before + * the sack-reneging (B) and undo (D/E) early-returns below. Those paths + * can return without ever reaching tcp_identify_packet_loss(), which is + * where the NACK used to be turned into a lost-mark; a NACK riding such + * an ACK was then consumed (FLAG_NACK set) but never acted on, so the + * hole was repaired only by RTO. Marking here is authoritative and + * idempotent: tcp_mark_skb_lost() guards the LOST bit, and + * tcp_trimming_mark_lost() no-ops on an empty queue or an already-ACKed + * seq. It runs before tcp_time_to_recover(), so lost_out>0 drives a + * clean recovery entry, exactly as the old in-E-section call did. + * + * This is now the ONLY NACK mark site -- the FLAG_NACK branch was + * removed from tcp_identify_packet_loss() -- and tcp_fastretrans_alert() + * runs at most once per ACK (main path via tcp_ack_is_dubious(), or the + * old_ack path), so the NACKed segment is marked exactly once. + * + * Also arm the retransmit here: the sack-reneging (B) and undo (D/E) + * early-returns below can skip the bottom-of-function *rexmit assignment, + * leaving rexmit == REXMIT_NONE so tcp_ack()'s tcp_xmit_recovery() never + * runs the (forced) retransmit pass and the marked-lost hole waits for an + * RTO. rexmit is a pointer read by tcp_ack() after we return, and the + * early-returns leave it untouched, so setting it now guarantees the + * NACKed segment is retransmitted on this same ACK. + */ + if (flag & FLAG_NACK) { + tcp_trimming_mark_lost(sk); + *rexmit = REXMIT_LOST; + + /* Trimming/NACK: the up-front mark above bumped lost_out + * out-of-band -- i.e. before the C-section tcp_verify_left_out() + * consistency check below. For a NewReno socket sacked_out is not + * tied to specific skbs but is a dupack fiction that + * tcp_limit_reno_sacked() keeps pinned near packets_out; the + * freshly marked loss then claims a window slot the fiction + * already counted, so sacked_out + lost_out can momentarily exceed + * packets_out and trip WARN_ON in tcp_verify_left_out(). Re-cap + * sacked_out now (exactly as tcp_identify_packet_loss() does after + * its own marking, only that runs later in the E-section) so a + * real NACK-signalled loss correctly displaces one fictional + * dupack. SACK sockets are untouched: there sacked_out is exact + * and tcp_mark_skb_lost()'s SACKED_ACKED guard already prevents + * the double count. + */ + if (tcp_is_reno(tp)) + tcp_limit_reno_sacked(tp); + + /* Fold a re-trimming NACK storm into a single recovery + * episode. The forced retransmit pass + * (tcp_force_retransmit_nacked) sends NACK-marked segments + * regardless of where they sit relative to the recovery + * boundary tp->high_seq. When a NACK names data above + * high_seq, that segment is retransmitted (retrans_out++) but + * is not covered by the current episode; once snd_una reaches + * the old high_seq the state machine completes recovery into + * TCP_CA_Open while those forced retransmits are still in + * flight (retrans_out > 0), which trips the + * WARN_ON(retrans_out != 0) in the D-section Open branch and + * forces a fresh ssthresh cut on the next sub-burst. Pushing + * high_seq up to snd_nxt keeps snd_una < high_seq while the + * storm is ongoing, so the socket stays in Recovery until + * every NACKed segment has drained -- one episode, one + * ssthresh reduction, and a clean exit to Open with + * retrans_out == 0. + * + * Applied in every state except TCP_CA_Loss: while in + * TCP_CA_Recovery this keeps us in recovery (snd_una stays + * below high_seq) for as long as NACKs keep arriving, folding + * the whole storm into one episode; in TCP_CA_Open/Disorder/CWR + * it pre-sets the boundary for the recovery we are about to + * (re-)enter in the E-section below (see the trimming_ok + * exemption in the D-section Open branch). CA_Loss is left + * untouched: it is + * RTO-driven and owns high_seq for F-RTO, and is not reached in + * these NACK-only storms. Only trimming sockets ever set + * FLAG_NACK, so normal TCP is untouched. + */ + if (icsk->icsk_ca_state != TCP_CA_Loss && + after(tp->snd_nxt, tp->high_seq)) + tp->high_seq = tp->snd_nxt; + } + /* Now state machine starts. * A. ECE, hence prohibit cwnd undoing, the reduction is required. */ if (ece_ack) @@ -3237,8 +3397,24 @@ static void tcp_fastretrans_alert(struct sock *sk, const u32 prior_snd_una, /* D. Check state exit conditions. State can be terminated * when high_seq is ACKed. */ if (icsk->icsk_ca_state == TCP_CA_Open) { - WARN_ON(tp->retrans_out != 0 && !tp->syn_data); - tp->retrans_stamp = 0; + /* Trimming/NACK: "Open with retrans_out > 0" is a legitimate state + * for a trimming socket. The forced retransmit pass + * (tcp_force_retransmit_nacked) sends NACK-marked segments anywhere + * in the rtx queue, bypassing both the cwnd gate and the recovery + * boundary, so once a cumulative ACK completes recovery into Open + * those NACK-forced retransmits can still be in flight. The stock + * WARN_ON(retrans_out != 0) is a NewReno invariant that the forced + * pass deliberately breaks, so exempt trimming sockets from it, and + * only clear retrans_stamp once nothing is outstanding (keeping the + * retransmit clock correct for ETIMEDOUT while forced retransmits + * remain). Normal TCP keeps the original invariant check intact. + */ + if (!tp->rx_opt.trimming_ok) { + WARN_ON(tp->retrans_out != 0 && !tp->syn_data); + tp->retrans_stamp = 0; + } else if (!tp->retrans_out) { + tp->retrans_stamp = 0; + } } else if (!before(tp->snd_una, tp->high_seq)) { switch (icsk->icsk_ca_state) { case TCP_CA_CWR: @@ -3324,7 +3500,13 @@ static void tcp_fastretrans_alert(struct sock *sk, const u32 prior_snd_una, fast_rexmit = 1; } - if (!tcp_is_rack(sk) && do_lost) + /* Trimming/NACK: NACK is the primary loss signal (it sets lost_out, + * caught by tcp_time_to_recover Trick#1) together with RTO. Skip the + * RFC6675/RFC3517 SACK scoreboard for trimming sockets so no SACK-driven + * fast retransmit occurs; sacked_out accounting stays intact so SACK is + * still accurate for packets-in-flight. Only NACK and RTO retransmit. + */ + if (!tcp_is_rack(sk) && !tp->rx_opt.trimming_ok && do_lost) tcp_update_scoreboard(sk, fast_rexmit); *rexmit = REXMIT_LOST; } @@ -6154,6 +6336,45 @@ static bool tcp_validate_incoming(struct sock *sk, struct sk_buff *skb, tcp_paws_check(&tp->rx_opt, TCP_PAWS_WINDOW)) goto step1; + /* Trimming/NACK: tcp_fast_parse_options() above has already parsed a + * NACK off this segment (tp->rx_opt.trimming_nack_rcvd). Under + * reverse-path reordering the NACK-bearing pure ACK frequently arrives + * with a TSval older than ts_recent and would be PAWS-discarded here, + * destroying the (one-shot, non-redundant) NACK and forcing an RTO. The + * NACK names an absolute sequence that needs retransmission and is + * meaningful regardless of the carrier ACK's timestamp ordering, so let + * it bypass PAWS and reach tcp_ack(), where the NACK is consumed. + * + * This is safe: a stale/wrapped carrier cannot do harm because + * tcp_trimming_mark_lost() only acts on a seq still in the rtx queue + * (else it no-ops), and ts_recent is not advanced from this out-of-date + * segment (tcp_replace_ts_recent()/tcp_paws_check() still reject it). + * Scope is deliberately narrow (only when a NACK is present) so normal + * data and ACK traffic keep full PAWS protection. + */ + if (tp->rx_opt.trimming_ok && tp->rx_opt.trimming_nack_rcvd) + goto step1; + + /* Trimming: a DSCP-marked trimmed segment is not always consumed by + * tcp_handle_trimmed() at the top of tcp_rcv_established(). When its + * payload is still intact (the shave was below the trimmer's + * granularity) or it is a trimmed pure ACK, the handler lets it fall + * through into normal processing, so a reordered one reaches this PAWS + * check. Such a segment carries a congestion/loss signal (it arms a + * NACK) and in-window data the receiver needs; under forward-path + * reordering its TSval can look stale and PAWS would silently drop it, + * forcing an RTO. Let any trimmed segment bypass PAWS and reach step1. + * + * Safe for the same reasons as the NACK bypass above: the sequence + * number check (step1) still rejects out-of-window segments, and + * ts_recent is not advanced from a segment that failed PAWS + * (tcp_replace_ts_recent()/tcp_paws_check() still reject it). Scope is + * narrow (only DSCP-marked trimmed segments) so normal traffic keeps + * full PAWS protection. + */ + if (tp->rx_opt.trimming_ok && TCP_SKB_CB(skb)->trimmed) + goto step1; + reason = tcp_disordered_ack_check(sk, skb); if (!reason) goto step1; diff --git a/net/ipv4/tcp_output.c b/net/ipv4/tcp_output.c index 92fdc90080629..0f9e1cc80c2e6 100644 --- a/net/ipv4/tcp_output.c +++ b/net/ipv4/tcp_output.c @@ -2925,9 +2925,13 @@ bool tcp_schedule_loss_probe(struct sock *sk, bool advancing_rto) early_retrans = READ_ONCE(sock_net(sk)->ipv4.sysctl_tcp_early_retrans); /* Schedule a loss probe in 2*RTT for SACK capable connections * not in loss recovery, that are either limited by cwnd or application. + * + * Trimming/NACK: TLP is gated on SACK (not RACK), so it would arm as + * soon as SACK is enabled. Disable it for trimming connections so the + * only retransmit triggers remain NACK and RTO. */ if ((early_retrans != 3 && early_retrans != 4) || - !tp->packets_out || !tcp_is_sack(tp) || + !tp->packets_out || !tcp_is_sack(tp) || tp->rx_opt.trimming_ok || (icsk->icsk_ca_state != TCP_CA_Open && icsk->icsk_ca_state != TCP_CA_CWR)) return false; @@ -3513,6 +3517,62 @@ int tcp_retransmit_skb(struct sock *sk, struct sk_buff *skb, int segs) return err; } +/* Trimming/NACK: retransmit segments the receiver explicitly NACKed, + * bypassing the cwnd >= packets_in_flight gate. + * + * A NACK is authoritative proof that a specific segment was lost. With SACK + * on, sacked_out reflects the data delivered above the hole, so packets_in_flight + * deflates and the normal gate in tcp_xmit_retransmit_queue() fires by itself. + * With SACK off there is no such feedback: packets_in_flight stays inflated, the + * gate stays shut, and the hole is only repaired by an RTO. To avoid that, + * retransmit just the NACK-marked (TCPCB_NACK_FORCED) lost segments here while + * ignoring the cwnd gate. Pacing and the local TSQ limit are still honoured. + * + * The flag is one-shot: it is cleared once the segment is (re)transmitted, so a + * fresh NACK (e.g. after a re-trim) is required to force it again. Scope is + * limited to trimming sockets because only tcp_trimming_mark_lost() sets the + * flag. Returns true if the rtx-queue head was retransmitted (caller rearms the + * RTO timer). + */ +static bool __maybe_unused tcp_force_retransmit_nacked(struct sock *sk) +{ + struct tcp_sock *tp = tcp_sk(sk); + struct sk_buff *rtx_head = tcp_rtx_queue_head(sk); + struct sk_buff *skb = rtx_head; + bool sent_head = false; + + skb_rbtree_walk_from(skb) { + __u8 sacked = TCP_SKB_CB(skb)->sacked; + + /* Stop once every lost segment has been (re)transmitted. */ + if (tp->retrans_out >= tp->lost_out) + break; + /* Force only segments that are LOST and NACK-signalled, and + * not already SACKed or retransmitted. + */ + if ((sacked & (TCPCB_LOST | TCPCB_NACK_FORCED | + TCPCB_SACKED_ACKED | TCPCB_SACKED_RETRANS)) != + (TCPCB_LOST | TCPCB_NACK_FORCED)) + continue; + if (tcp_pacing_check(sk)) + break; + if (tcp_small_queue_check(sk, skb, 1)) + break; + if (tcp_retransmit_skb(sk, skb, 1)) + break; + + /* One-shot: consume the NACK force tag. */ + TCP_SKB_CB(skb)->sacked &= ~TCPCB_NACK_FORCED; + if (skb == rtx_head) + sent_head = true; + if (tcp_in_cwnd_reduction(sk)) + tp->prr_out += tcp_skb_pcount(skb); + NET_ADD_STATS(sock_net(sk), LINUX_MIB_TCPFASTRETRANS, + tcp_skb_pcount(skb)); + } + return sent_head; +} + /* This gets called after a retransmit timeout, and the initially * retransmitted data is acknowledged. It tries to continue * resending the rest of the retransmit queue, until either @@ -3530,6 +3590,13 @@ void tcp_xmit_retransmit_queue(struct sock *sk) if (!tp->packets_out) return; + /* Trimming/NACK: first force-retransmit any NACK-signalled losses past + * the cwnd>=pif gate (see tcp_force_retransmit_nacked). Only trimming + * sockets ever carry TCPCB_NACK_FORCED. */ + // if (tp->rx_opt.trimming_ok && tp->lost_out > tp->retrans_out && + // tcp_force_retransmit_nacked(sk)) + // rearm_timer = true; + rtx_head = tcp_rtx_queue_head(sk); skb = tp->retransmit_skb_hint ?: rtx_head; max_segs = tcp_tso_segs(sk, tcp_current_mss(sk));