tcp: accecn: detect loss ACK w/ AccECN option and add TCP_ACCECN_OPTION_PERSIST
authorChia-Yu Chang <chia-yu.chang@nokia-bell-labs.com>
Sat, 31 Jan 2026 22:25:12 +0000 (23:25 +0100)
committerPaolo Abeni <pabeni@redhat.com>
Tue, 3 Feb 2026 14:13:25 +0000 (15:13 +0100)
Detect spurious retransmission of a previously sent ACK carrying the
AccECN option after the second retransmission. Since this might be caused
by the middlebox dropping ACK with options it does not recognize, disable
the sending of the AccECN option in all subsequent ACKs. This patch
follows Section 3.2.3.2.2 of AccECN spec (RFC9768), and a new field
(accecn_opt_sent_w_dsack) is added to indicate that an AccECN option was
sent with duplicate SACK info.

Also, a new AccECN option sending mode is added to tcp_ecn_option sysctl:
(TCP_ECN_OPTION_PERSIST), which ignores the AccECN fallback policy and
persistently sends AccECN option once it fits into TCP option space.

Signed-off-by: Chia-Yu Chang <chia-yu.chang@nokia-bell-labs.com>
Acked-by: Paolo Abeni <pabeni@redhat.com>
Reviewed-by: Eric Dumazet <edumazet@google.com>
Link: https://patch.msgid.link/20260131222515.8485-13-chia-yu.chang@nokia-bell-labs.com
Signed-off-by: Paolo Abeni <pabeni@redhat.com>
Documentation/networking/ip-sysctl.rst
include/linux/tcp.h
include/net/tcp_ecn.h
net/ipv4/sysctl_net_ipv4.c
net/ipv4/tcp_input.c
net/ipv4/tcp_output.c

index bc9a016..28c7e4f 100644 (file)
@@ -482,7 +482,9 @@ tcp_ecn_option - INTEGER
        1 Send AccECN option sparingly according to the minimum option
          rules outlined in draft-ietf-tcpm-accurate-ecn.
        2 Send AccECN option on every packet whenever it fits into TCP
-         option space.
+         option space except when AccECN fallback is triggered.
+       3 Send AccECN option on every packet whenever it fits into TCP
+         option space even when AccECN fallback is triggered.
        = ============================================================
 
        Default: 2
index fbc514d..f72eef3 100644 (file)
@@ -291,7 +291,8 @@ struct tcp_sock {
        u8      nonagle     : 4,/* Disable Nagle algorithm?             */
                rate_app_limited:1;  /* rate_{delivered,interval_us} limited? */
        u8      received_ce_pending:4, /* Not yet transmit cnt of received_ce */
-               unused2:4;
+               accecn_opt_sent_w_dsack:1,/* Sent ACCECN opt in previous ACK w/ D-SACK */
+               unused2:3;
        u8      accecn_minlen:2,/* Minimum length of AccECN option sent */
                est_ecnfield:2,/* ECN field for AccECN delivered estimates */
                accecn_opt_demand:2,/* Demand AccECN option for n next ACKs */
index 49e0b86..e01653b 100644 (file)
@@ -29,6 +29,7 @@ enum tcp_accecn_option {
        TCP_ACCECN_OPTION_DISABLED = 0,
        TCP_ACCECN_OPTION_MINIMUM = 1,
        TCP_ACCECN_OPTION_FULL = 2,
+       TCP_ACCECN_OPTION_PERSIST = 3,
 };
 
 /* Apply either ECT(0) or ECT(1) based on TCP_CONG_ECT_1_NEGOTIATION flag */
@@ -406,6 +407,7 @@ static inline void tcp_accecn_init_counters(struct tcp_sock *tp)
        tp->received_ce_pending = 0;
        __tcp_accecn_init_bytes_counters(tp->received_ecn_bytes);
        __tcp_accecn_init_bytes_counters(tp->delivered_ecn_bytes);
+       tp->accecn_opt_sent_w_dsack = 0;
        tp->accecn_minlen = 0;
        tp->accecn_opt_demand = 0;
        tp->est_ecnfield = 0;
index a1a50a5..385b5b9 100644 (file)
@@ -749,7 +749,7 @@ static struct ctl_table ipv4_net_table[] = {
                .mode           = 0644,
                .proc_handler   = proc_dou8vec_minmax,
                .extra1         = SYSCTL_ZERO,
-               .extra2         = SYSCTL_TWO,
+               .extra2         = SYSCTL_THREE,
        },
        {
                .procname       = "tcp_ecn_option_beacon",
index 988d161..89526f0 100644 (file)
@@ -5046,8 +5046,11 @@ static void tcp_dsack_extend(struct sock *sk, u32 seq, u32 end_seq)
                tcp_sack_extend(tp->duplicate_sack, seq, end_seq);
 }
 
-static void tcp_rcv_spurious_retrans(struct sock *sk, const struct sk_buff *skb)
+static void tcp_rcv_spurious_retrans(struct sock *sk,
+                                    const struct sk_buff *skb)
 {
+       struct tcp_sock *tp = tcp_sk(sk);
+
        /* When the ACK path fails or drops most ACKs, the sender would
         * timeout and spuriously retransmit the same segment repeatedly.
         * If it seems our ACKs are not reaching the other side,
@@ -5067,6 +5070,14 @@ static void tcp_rcv_spurious_retrans(struct sock *sk, const struct sk_buff *skb)
        /* Save last flowlabel after a spurious retrans. */
        tcp_save_lrcv_flowlabel(sk, skb);
 #endif
+       /* Check DSACK info to detect that the previous ACK carrying the
+        * AccECN option was lost after the second retransmision, and then
+        * stop sending AccECN option in all subsequent ACKs.
+        */
+       if (tcp_ecn_mode_accecn(tp) &&
+           tp->accecn_opt_sent_w_dsack &&
+           TCP_SKB_CB(skb)->seq == tp->duplicate_sack[0].start_seq)
+               tcp_accecn_fail_mode_set(tp, TCP_ACCECN_OPT_FAIL_SEND);
 }
 
 static void tcp_send_dupack(struct sock *sk, const struct sk_buff *skb)
index 2b356fd..f44d60d 100644 (file)
@@ -715,9 +715,12 @@ static void tcp_options_write(struct tcphdr *th, struct tcp_sock *tp,
                if (tp) {
                        tp->accecn_minlen = 0;
                        tp->accecn_opt_tstamp = tp->tcp_mstamp;
+                       tp->accecn_opt_sent_w_dsack = tp->rx_opt.dsack;
                        if (tp->accecn_opt_demand)
                                tp->accecn_opt_demand--;
                }
+       } else if (tp) {
+               tp->accecn_opt_sent_w_dsack = 0;
        }
 
        if (unlikely(OPTION_SACK_ADVERTISE & options)) {
@@ -1189,7 +1192,9 @@ static unsigned int tcp_established_options(struct sock *sk, struct sk_buff *skb
        if (tcp_ecn_mode_accecn(tp)) {
                int ecn_opt = READ_ONCE(sock_net(sk)->ipv4.sysctl_tcp_ecn_option);
 
-               if (ecn_opt && tp->saw_accecn_opt && !tcp_accecn_opt_fail_send(tp) &&
+               if (ecn_opt && tp->saw_accecn_opt &&
+                   (ecn_opt >= TCP_ACCECN_OPTION_PERSIST ||
+                    !tcp_accecn_opt_fail_send(tp)) &&
                    (ecn_opt >= TCP_ACCECN_OPTION_FULL || tp->accecn_opt_demand ||
                     tcp_accecn_option_beacon_check(sk))) {
                        opts->use_synack_ecn_bytes = 0;