* [PATCH mptcp-next v7 0/6] mptcp: pm: in-kernel: retry subflows closed after an error
@ 2026-10-20 3:17 Anil Kaushik
2026-10-20 3:17 ` [PATCH mptcp-next v7 1/6] selftests: mptcp: connect: retry write() on EAGAIN Anil Kaushik
` (6 more replies)
0 siblings, 7 replies; 9+ messages in thread
From: Anil Kaushik @ 2026-10-20 3:17 UTC (permalink / raw)
To: mptcp; +Cc: Anil Kaushik
A subflow closed after an error is never re-created, even once the path
is back (#440).
When TCP gives up on an established subflow we created, after a
retransmission or keepalive timeout, the in-kernel PM now creates a new
one to the same addresses. Its SYN retries space out the attempts.
Their number is set by a new limit, subflow-retries, like TCP_SYNCNT:
15 by default, about 20 minutes, 0 disables it.
Patch 1 fixes a selftest tool issue seen with the new test. Patches 2
and 3 add the new limit, patch 4 the retry, patches 5 and 6 the
selftests.
Changes in v7, after the netdev Sashiko reviews of v5 and v6, and
Abdul's tests: only a path error, a retransmission or keepalive
timeout, makes the PM re-create a subflow, not a reset, so a peer
cannot make it loop; the flags are bools under the msk socket lock;
mptcp.rst documents tcp_retries2 and the keepalive needed by a subflow
that only receives data; and small fixes in the tests.
Changes in v6, after the Sashiko reviews (netdev Sashiko on v4, which
also applied to v5, and sashiko.dev on v5): only subflows that were
established are re-created, so the PM still moves on to the next
endpoints as before; removed remote addresses and the local ID 0 are
handled too; the policy checks the maximum value, so the parse_limit()
patch is gone; and the test uses the net/lib.sh helpers. Details in
each patch.
v6: https://lore.kernel.org/mptcp/20261018041748.1655711-1-anilkaushikwireless@gmail.com/
v5: https://lore.kernel.org/mptcp/20261016160926.1155262-1-anilkaushikwireless@gmail.com/
v4: https://lore.kernel.org/mptcp/20261016055010.980819-1-anilkaushikwireless@gmail.com/
v3: https://lore.kernel.org/mptcp/20261014143349.768138-1-anilkaushikwireless@gmail.com/
v2: https://lore.kernel.org/mptcp/20261013025653.523755-1-anilkaushikwireless@gmail.com/
v1: https://lore.kernel.org/mptcp/20261012171740.464517-1-anilkaushikwireless@gmail.com/
Anil Kaushik (6):
selftests: mptcp: connect: retry write() on EAGAIN
netlink: specs: mptcp: add the subflow-retries attribute
mptcp: pm: in-kernel: add the subflow-retries limit
mptcp: pm: in-kernel: retry subflows closed after an error
selftests: mptcp: pm_nl_ctl: support the subflow-retries limit
selftests: mptcp: join: validate subflow retry after link failure
Documentation/netlink/specs/mptcp_pm.yaml | 13 ++
Documentation/networking/mptcp.rst | 11 ++
include/uapi/linux/mptcp.h | 2 +-
include/uapi/linux/mptcp_pm.h | 1 +
net/mptcp/mptcp_pm_gen.c | 10 +-
net/mptcp/mptcp_pm_gen.h | 4 +-
net/mptcp/pm.c | 39 ++++
net/mptcp/pm_kernel.c | 141 +++++++++++++-
net/mptcp/protocol.c | 27 +++
net/mptcp/protocol.h | 20 ++
net/mptcp/sockopt.c | 2 +
.../selftests/net/mptcp/mptcp_connect.c | 18 ++
.../testing/selftests/net/mptcp/mptcp_join.sh | 178 +++++++++++++++++-
.../testing/selftests/net/mptcp/pm_netlink.sh | 47 ++++-
tools/testing/selftests/net/mptcp/pm_nl_ctl.c | 34 +++-
15 files changed, 526 insertions(+), 21 deletions(-)
--
2.25.1
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH mptcp-next v7 1/6] selftests: mptcp: connect: retry write() on EAGAIN
2026-10-20 3:17 [PATCH mptcp-next v7 0/6] mptcp: pm: in-kernel: retry subflows closed after an error Anil Kaushik
@ 2026-10-20 3:17 ` Anil Kaushik
2026-10-20 3:17 ` [PATCH mptcp-next v7 2/6] netlink: specs: mptcp: add the subflow-retries attribute Anil Kaushik
` (5 subsequent siblings)
6 siblings, 0 replies; 9+ messages in thread
From: Anil Kaushik @ 2026-10-20 3:17 UTC (permalink / raw)
To: mptcp; +Cc: Anil Kaushik
mptcp_connect treats any write() error as fatal. But write() can fail
with EAGAIN even after poll() said the socket was writable: the MPTCP
send buffer shrinks when a subflow is closed.
The tool then exits and the transfer fails on both sides. Instead, go
back to poll() and wait for more space. If a poll timeout is set, still
fail after as many retries, 1ms apart: poll() does not check every
cause of EAGAIN.
Fixes: 048d19d444be ("mptcp: add basic kselftest for mptcp")
Assisted-by: LLM
Signed-off-by: Anil Kaushik <anilkaushikwireless@gmail.com>
---
Notes:
v6:
- the comment says the limit only applies with a poll timeout (netdev
Sashiko)
v5:
- no change
v4:
- count the EAGAIN retries, not ms (Sashiko)
- no limit when there is no poll timeout, -t 0 (Sashiko)
v3:
- do not wait forever on EAGAIN (Sashiko)
- wrap lines over 80 columns
v2:
- Fixes tag
.../selftests/net/mptcp/mptcp_connect.c | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
diff --git a/tools/testing/selftests/net/mptcp/mptcp_connect.c b/tools/testing/selftests/net/mptcp/mptcp_connect.c
index 178d98d91fea..49017d00ab69 100644
--- a/tools/testing/selftests/net/mptcp/mptcp_connect.c
+++ b/tools/testing/selftests/net/mptcp/mptcp_connect.c
@@ -638,6 +638,7 @@ static int copyfd_io_poll(int infd, int peerfd, int outfd,
.events = POLLIN | POLLOUT,
};
unsigned int total_wlen = 0, total_rlen = 0;
+ int eagain_cnt = 0;
set_nonblock(peerfd, true);
@@ -711,6 +712,22 @@ static int copyfd_io_poll(int infd, int peerfd, int outfd,
bw = do_rnd_write(peerfd, winfo->buf + winfo->off, winfo->len);
if (bw < 0) {
+ /* POLLOUT does not guarantee that
+ * the next write will not block:
+ * the MPTCP send buffer can shrink
+ * in between, e.g. when a subflow
+ * is closed. Wait for more space,
+ * but not forever if a poll timeout
+ * is set: poll() does not check
+ * every cause of EAGAIN.
+ */
+ if (errno == EAGAIN &&
+ (poll_timeout < 0 ||
+ ++eagain_cnt <= poll_timeout)) {
+ usleep(1000);
+ continue;
+ }
+
/* expected reset, continue to read */
if (cfg_rcv_trunc &&
(errno == ECONNRESET ||
@@ -723,6 +740,7 @@ static int copyfd_io_poll(int infd, int peerfd, int outfd,
return 111;
}
+ eagain_cnt = 0;
winfo->off += bw;
winfo->len -= bw;
total_wlen += bw;
--
2.25.1
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH mptcp-next v7 2/6] netlink: specs: mptcp: add the subflow-retries attribute
2026-10-20 3:17 [PATCH mptcp-next v7 0/6] mptcp: pm: in-kernel: retry subflows closed after an error Anil Kaushik
2026-10-20 3:17 ` [PATCH mptcp-next v7 1/6] selftests: mptcp: connect: retry write() on EAGAIN Anil Kaushik
@ 2026-10-20 3:17 ` Anil Kaushik
2026-10-20 3:17 ` [PATCH mptcp-next v7 3/6] mptcp: pm: in-kernel: add the subflow-retries limit Anil Kaushik
` (4 subsequent siblings)
6 siblings, 0 replies; 9+ messages in thread
From: Anil Kaushik @ 2026-10-20 3:17 UTC (permalink / raw)
To: mptcp; +Cc: Anil Kaushik
It will be used by the in-kernel PM to set the number of SYN
retransmissions of a subflow re-created after an error, with the other
limits: add it to the set-limits and get-limits commands.
The uapi header and the generated files are updated with ynl-regen.
Assisted-by: LLM
Signed-off-by: Anil Kaushik <anilkaushikwireless@gmail.com>
---
Notes:
v7:
- the doc gives the default value (netdev Sashiko)
v6 (netdev Sashiko):
- the policy checks the maximum value, 127 (MAX_TCP_SYNCNT)
- the doc says the value works like TCP_SYNCNT
v5:
- new, split from the retry patch (Matthieu)
Documentation/netlink/specs/mptcp_pm.yaml | 13 +++++++++++++
include/uapi/linux/mptcp_pm.h | 1 +
net/mptcp/mptcp_pm_gen.c | 10 ++++++----
net/mptcp/mptcp_pm_gen.h | 4 ++--
4 files changed, 22 insertions(+), 6 deletions(-)
diff --git a/Documentation/netlink/specs/mptcp_pm.yaml b/Documentation/netlink/specs/mptcp_pm.yaml
index 39f3facc38e5..76b5407bd4c4 100644
--- a/Documentation/netlink/specs/mptcp_pm.yaml
+++ b/Documentation/netlink/specs/mptcp_pm.yaml
@@ -196,6 +196,16 @@ attribute-sets:
name: addr-remote
type: nest
nested-attributes: address
+ -
+ name: subflow-retries
+ type: u32
+ checks:
+ max: 127
+ doc: |
+ SYN retries of a subflow re-created after an error, like the
+ TCP_SYNCNT socket option, so with tcp_syn_linear_timeouts on top.
+ 15 by default, 0 to not re-create it. The maximum is
+ MAX_TCP_SYNCNT.
-
name: event-attr
enum-name: mptcp-event-attr
@@ -327,6 +337,7 @@ operations:
attributes:
- rcv-add-addrs
- subflows
+ - subflow-retries
-
name: get-limits
doc: Get protocol limits
@@ -337,10 +348,12 @@ operations:
attributes:
- rcv-add-addrs
- subflows
+ - subflow-retries
reply:
attributes:
- rcv-add-addrs
- subflows
+ - subflow-retries
-
name: set-flags
doc: Change endpoint flags
diff --git a/include/uapi/linux/mptcp_pm.h b/include/uapi/linux/mptcp_pm.h
index fe9863d75350..43e9643deda9 100644
--- a/include/uapi/linux/mptcp_pm.h
+++ b/include/uapi/linux/mptcp_pm.h
@@ -102,6 +102,7 @@ enum {
MPTCP_PM_ATTR_TOKEN,
MPTCP_PM_ATTR_LOC_ID,
MPTCP_PM_ATTR_ADDR_REMOTE,
+ MPTCP_PM_ATTR_SUBFLOW_RETRIES,
__MPTCP_ATTR_AFTER_LAST
};
diff --git a/net/mptcp/mptcp_pm_gen.c b/net/mptcp/mptcp_pm_gen.c
index c180930a8e0e..e2d4af1bc1d5 100644
--- a/net/mptcp/mptcp_pm_gen.c
+++ b/net/mptcp/mptcp_pm_gen.c
@@ -44,15 +44,17 @@ const struct nla_policy mptcp_pm_flush_addrs_nl_policy[MPTCP_PM_ENDPOINT_ADDR +
};
/* MPTCP_PM_CMD_SET_LIMITS - do */
-const struct nla_policy mptcp_pm_set_limits_nl_policy[MPTCP_PM_ATTR_SUBFLOWS + 1] = {
+const struct nla_policy mptcp_pm_set_limits_nl_policy[MPTCP_PM_ATTR_SUBFLOW_RETRIES + 1] = {
[MPTCP_PM_ATTR_RCV_ADD_ADDRS] = { .type = NLA_U32, },
[MPTCP_PM_ATTR_SUBFLOWS] = { .type = NLA_U32, },
+ [MPTCP_PM_ATTR_SUBFLOW_RETRIES] = NLA_POLICY_MAX(NLA_U32, 127),
};
/* MPTCP_PM_CMD_GET_LIMITS - do */
-const struct nla_policy mptcp_pm_get_limits_nl_policy[MPTCP_PM_ATTR_SUBFLOWS + 1] = {
+const struct nla_policy mptcp_pm_get_limits_nl_policy[MPTCP_PM_ATTR_SUBFLOW_RETRIES + 1] = {
[MPTCP_PM_ATTR_RCV_ADD_ADDRS] = { .type = NLA_U32, },
[MPTCP_PM_ATTR_SUBFLOWS] = { .type = NLA_U32, },
+ [MPTCP_PM_ATTR_SUBFLOW_RETRIES] = NLA_POLICY_MAX(NLA_U32, 127),
};
/* MPTCP_PM_CMD_SET_FLAGS - do */
@@ -127,7 +129,7 @@ const struct genl_ops mptcp_pm_nl_ops[11] = {
.validate = GENL_DONT_VALIDATE_STRICT,
.doit = mptcp_pm_nl_set_limits_doit,
.policy = mptcp_pm_set_limits_nl_policy,
- .maxattr = MPTCP_PM_ATTR_SUBFLOWS,
+ .maxattr = MPTCP_PM_ATTR_SUBFLOW_RETRIES,
.flags = GENL_UNS_ADMIN_PERM,
},
{
@@ -135,7 +137,7 @@ const struct genl_ops mptcp_pm_nl_ops[11] = {
.validate = GENL_DONT_VALIDATE_STRICT,
.doit = mptcp_pm_nl_get_limits_doit,
.policy = mptcp_pm_get_limits_nl_policy,
- .maxattr = MPTCP_PM_ATTR_SUBFLOWS,
+ .maxattr = MPTCP_PM_ATTR_SUBFLOW_RETRIES,
},
{
.cmd = MPTCP_PM_CMD_SET_FLAGS,
diff --git a/net/mptcp/mptcp_pm_gen.h b/net/mptcp/mptcp_pm_gen.h
index b9280bcb43f5..5b4dee8a0a34 100644
--- a/net/mptcp/mptcp_pm_gen.h
+++ b/net/mptcp/mptcp_pm_gen.h
@@ -23,9 +23,9 @@ extern const struct nla_policy mptcp_pm_get_addr_nl_policy[MPTCP_PM_ATTR_TOKEN +
extern const struct nla_policy mptcp_pm_flush_addrs_nl_policy[MPTCP_PM_ENDPOINT_ADDR + 1];
-extern const struct nla_policy mptcp_pm_set_limits_nl_policy[MPTCP_PM_ATTR_SUBFLOWS + 1];
+extern const struct nla_policy mptcp_pm_set_limits_nl_policy[MPTCP_PM_ATTR_SUBFLOW_RETRIES + 1];
-extern const struct nla_policy mptcp_pm_get_limits_nl_policy[MPTCP_PM_ATTR_SUBFLOWS + 1];
+extern const struct nla_policy mptcp_pm_get_limits_nl_policy[MPTCP_PM_ATTR_SUBFLOW_RETRIES + 1];
extern const struct nla_policy mptcp_pm_set_flags_nl_policy[MPTCP_PM_ATTR_ADDR_REMOTE + 1];
--
2.25.1
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH mptcp-next v7 3/6] mptcp: pm: in-kernel: add the subflow-retries limit
2026-10-20 3:17 [PATCH mptcp-next v7 0/6] mptcp: pm: in-kernel: retry subflows closed after an error Anil Kaushik
2026-10-20 3:17 ` [PATCH mptcp-next v7 1/6] selftests: mptcp: connect: retry write() on EAGAIN Anil Kaushik
2026-10-20 3:17 ` [PATCH mptcp-next v7 2/6] netlink: specs: mptcp: add the subflow-retries attribute Anil Kaushik
@ 2026-10-20 3:17 ` Anil Kaushik
2026-10-20 3:17 ` [PATCH mptcp-next v7 4/6] mptcp: pm: in-kernel: retry subflows closed after an error Anil Kaushik
` (3 subsequent siblings)
6 siblings, 0 replies; 9+ messages in thread
From: Anil Kaushik @ 2026-10-20 3:17 UTC (permalink / raw)
To: mptcp; +Cc: Anil Kaushik
A new limit for the in-kernel PM, set and read with the other limits:
the number of SYN retries of a subflow re-created after an error, like
the TCP_SYNCNT socket option. 15 by default, about 20 minutes, and 0
to not re-create it. The netlink policy checks the maximum value,
MAX_TCP_SYNCNT.
It is also exposed in struct mptcp_info, in place of the reserved field.
The next patch uses it.
Assisted-by: LLM
Signed-off-by: Anil Kaushik <anilkaushikwireless@gmail.com>
---
Notes:
v6:
- the netlink policy checks the maximum, no more parse_limit() change
(netdev Sashiko)
- the limit works like TCP_SYNCNT, say so (netdev Sashiko)
v5:
- new, split from the retry patch (Matthieu)
- the limit is also in struct mptcp_info (Matthieu)
include/uapi/linux/mptcp.h | 2 +-
net/mptcp/pm_kernel.c | 23 ++++++++++++++++++++++-
net/mptcp/protocol.h | 1 +
net/mptcp/sockopt.c | 2 ++
4 files changed, 26 insertions(+), 2 deletions(-)
diff --git a/include/uapi/linux/mptcp.h b/include/uapi/linux/mptcp.h
index 72a5d030154e..d7e3f359773d 100644
--- a/include/uapi/linux/mptcp.h
+++ b/include/uapi/linux/mptcp.h
@@ -72,7 +72,7 @@ struct mptcp_info {
__u8 mptcpi_subflows_total;
__u8 mptcpi_endp_laminar_max;
__u8 mptcpi_endp_fullmesh_max;
- __u8 reserved;
+ __u8 mptcpi_limit_subflow_retries;
__u32 mptcpi_last_data_sent;
__u32 mptcpi_last_data_recv;
__u32 mptcpi_last_ack_recv;
diff --git a/net/mptcp/pm_kernel.c b/net/mptcp/pm_kernel.c
index d3014bf57bf3..0379e1495cf4 100644
--- a/net/mptcp/pm_kernel.c
+++ b/net/mptcp/pm_kernel.c
@@ -25,6 +25,7 @@ struct pm_nl_pernet {
u8 endp_fullmesh_max;
u8 limit_add_addr_accepted;
u8 limit_extra_subflows;
+ u8 limit_subflow_retries;
u8 next_id;
DECLARE_BITMAP(id_bitmap, MPTCP_PM_MAX_ADDR_ID + 1);
};
@@ -96,6 +97,14 @@ u8 mptcp_pm_get_limit_extra_subflows(const struct mptcp_sock *msk)
}
EXPORT_SYMBOL_GPL(mptcp_pm_get_limit_extra_subflows);
+u8 mptcp_pm_get_limit_subflow_retries(const struct mptcp_sock *msk)
+{
+ struct pm_nl_pernet *pernet = pm_nl_get_pernet_from_msk(msk);
+
+ return READ_ONCE(pernet->limit_subflow_retries);
+}
+EXPORT_SYMBOL_GPL(mptcp_pm_get_limit_subflow_retries);
+
static bool has_subflow_daddr(const struct mptcp_sock *msk,
const struct mptcp_addr_info *daddr)
{
@@ -1419,7 +1428,8 @@ static int parse_limit(struct genl_info *info, int id, unsigned int *limit)
int mptcp_pm_nl_set_limits_doit(struct sk_buff *skb, struct genl_info *info)
{
struct pm_nl_pernet *pernet = genl_info_pm_nl(info);
- unsigned int rcv_addrs, subflows;
+ unsigned int rcv_addrs, subflows, retries;
+ struct nlattr *attr;
int ret;
spin_lock_bh(&pernet->lock);
@@ -1433,8 +1443,13 @@ int mptcp_pm_nl_set_limits_doit(struct sk_buff *skb, struct genl_info *info)
if (ret)
goto unlock;
+ /* the maximum, MAX_TCP_SYNCNT, is checked by the policy */
+ attr = info->attrs[MPTCP_PM_ATTR_SUBFLOW_RETRIES];
+ retries = nla_get_u32_default(attr, pernet->limit_subflow_retries);
+
WRITE_ONCE(pernet->limit_add_addr_accepted, rcv_addrs);
WRITE_ONCE(pernet->limit_extra_subflows, subflows);
+ WRITE_ONCE(pernet->limit_subflow_retries, retries);
unlock:
spin_unlock_bh(&pernet->lock);
@@ -1464,6 +1479,10 @@ int mptcp_pm_nl_get_limits_doit(struct sk_buff *skb, struct genl_info *info)
READ_ONCE(pernet->limit_extra_subflows)))
goto fail;
+ if (nla_put_u32(msg, MPTCP_PM_ATTR_SUBFLOW_RETRIES,
+ READ_ONCE(pernet->limit_subflow_retries)))
+ goto fail;
+
genlmsg_end(msg, reply);
return genlmsg_reply(msg, info);
@@ -1618,6 +1637,8 @@ static int __net_init pm_nl_init_net(struct net *net)
/* Cit. 2 subflows ought to be enough for anybody. */
pernet->limit_extra_subflows = 2;
+ /* about 20 minutes with the default RTO values */
+ pernet->limit_subflow_retries = 15;
pernet->next_id = 1;
spin_lock_init(&pernet->lock);
diff --git a/net/mptcp/protocol.h b/net/mptcp/protocol.h
index ed2f363bb915..6d571328aca9 100644
--- a/net/mptcp/protocol.h
+++ b/net/mptcp/protocol.h
@@ -1264,6 +1264,7 @@ u8 mptcp_pm_get_endp_laminar_max(const struct mptcp_sock *msk);
u8 mptcp_pm_get_endp_fullmesh_max(const struct mptcp_sock *msk);
u8 mptcp_pm_get_limit_add_addr_accepted(const struct mptcp_sock *msk);
u8 mptcp_pm_get_limit_extra_subflows(const struct mptcp_sock *msk);
+u8 mptcp_pm_get_limit_subflow_retries(const struct mptcp_sock *msk);
/* called under PM lock */
static inline void __mptcp_pm_close_subflow(struct mptcp_sock *msk)
diff --git a/net/mptcp/sockopt.c b/net/mptcp/sockopt.c
index 922f6ae5c80c..d3c4abf59ec5 100644
--- a/net/mptcp/sockopt.c
+++ b/net/mptcp/sockopt.c
@@ -1075,6 +1075,8 @@ void mptcp_diag_fill_info(struct mptcp_sock *msk, struct mptcp_info *info)
mptcp_pm_get_endp_laminar_max(msk);
info->mptcpi_endp_fullmesh_max =
mptcp_pm_get_endp_fullmesh_max(msk);
+ info->mptcpi_limit_subflow_retries =
+ mptcp_pm_get_limit_subflow_retries(msk);
}
if (__mptcp_check_fallback(msk))
--
2.25.1
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH mptcp-next v7 4/6] mptcp: pm: in-kernel: retry subflows closed after an error
2026-10-20 3:17 [PATCH mptcp-next v7 0/6] mptcp: pm: in-kernel: retry subflows closed after an error Anil Kaushik
` (2 preceding siblings ...)
2026-10-20 3:17 ` [PATCH mptcp-next v7 3/6] mptcp: pm: in-kernel: add the subflow-retries limit Anil Kaushik
@ 2026-10-20 3:17 ` Anil Kaushik
2026-10-20 8:37 ` sashiko-bot
2026-10-20 3:17 ` [PATCH mptcp-next v7 5/6] selftests: mptcp: pm_nl_ctl: support the subflow-retries limit Anil Kaushik
` (2 subsequent siblings)
6 siblings, 1 reply; 9+ messages in thread
From: Anil Kaushik @ 2026-10-20 3:17 UTC (permalink / raw)
To: mptcp; +Cc: Anil Kaushik
With the in-kernel PM, a subflow closed after an error, e.g. a timeout
when a path silently drops packets, is never re-created, even once the
path works again. For example, with a main link and a backup link,
connections move to the backup after an outage but never come back.
When TCP gives up on an established subflow created by this side, after
a retransmission or keepalive timeout, the PM now creates a new one with
the same addresses. Its SYN
retries, set like TCP_SYNCNT by the subflow-retries limit, space out the
attempts. If this new subflow cannot connect, it is not retried again,
and it is closed if the MPTCP socket is closed in the meantime. Once it
is established, it is handled like any other subflow. Re-creating the
initial subflow uses one of the additional subflows allowed by the
limits.
A subflow reset by the peer, or by anything on the path, is not
re-created: the path works, and re-creating it right away could loop.
A subflow that never got established is not re-created either: the PM
keeps trying the next endpoints, as before. Subflows aborted locally,
also by MPTCP, or using an address that has been removed are not
retried.
TCP only closes a subflow after tcp_retries2 retransmissions, about 15
minutes by default. A subflow that only receives data never gets an
error: the application has to enable TCP keepalive on the MPTCP socket,
which MPTCP applies to all the subflows. Both are documented in
mptcp.rst.
Closes: https://github.com/multipath-tcp/mptcp_net-next/issues/440
Assisted-by: LLM
Signed-off-by: Anil Kaushik <anilkaushikwireless@gmail.com>
---
Notes:
v7:
- only after a path error, a retransmission or keepalive timeout: a
reset from the peer or the path could make it loop (netdev
Sashiko)
- retry and no_retry are bools under the msk socket lock, in the
padding after close_event_done, not bits next to the reset ones
(netdev Sashiko)
- mark all the subflows using a removed address, whatever their
state (netdev Sashiko)
- doc: tcp_retries2, and keepalive for a subflow that only receives
data (Abdul Wasey's tests)
- not retried after a reset by MPTCP itself, even with an old sk_err
left by IP_RECVERR (netdev Sashiko)
- close the re-created subflows still connecting without
MPTCP_CF_PUSH: they never carried data (netdev Sashiko)
v6 (netdev Sashiko):
- only re-create subflows that were established: the PM still tries the
next endpoints when one never gets established
- only once TCP closed the subflow, to read its reset flags safely
- removed addresses, also the remote ones and the local ID 0: the dead
subflows are marked in mptcp_pm_rm_addr_or_subflow(), instead of
looking up the endpoint
v5 (Matthieu):
- split in 4 patches
- reverse xmas tree in mptcp_pm_kernel_retry_check()
- "in-kernel" in the subject, like the other in-kernel PM patches
- close the re-created subflows still connecting when the socket is
closed (Matthieu: closing the socket during a retry)
v4 (Matthieu):
- netlink limit, on by default, instead of the sysctl
- one new subflow per failure, its SYN retries set from the limit: no
socket reuse, no attempts counter, no 60s check
- no MIB counter
- not retried if the endpoint was removed, instead of the pm.c change
- called through pm.c
- new bit in the subflow context, no new hole
- commit message: MP_RST, not any RST (Sashiko)
v3 (Sashiko):
- no retry after MP_RST without the T flag, local abort or EBADMSG
- no retry on a removed endpoint or address
- use only request_bkup for the backup flag
v2:
- use the subflow sockets, no list or timer
- code moved to pm_kernel.c
Documentation/networking/mptcp.rst | 11 +++
net/mptcp/pm.c | 39 ++++++++++
net/mptcp/pm_kernel.c | 118 +++++++++++++++++++++++++++++
net/mptcp/protocol.c | 27 +++++++
net/mptcp/protocol.h | 19 +++++
5 files changed, 214 insertions(+)
diff --git a/Documentation/networking/mptcp.rst b/Documentation/networking/mptcp.rst
index b6753ffb9c9a..0f07d4a29b6b 100644
--- a/Documentation/networking/mptcp.rst
+++ b/Documentation/networking/mptcp.rst
@@ -73,6 +73,17 @@ To be able to use multiple IP addresses on a host to create multiple *subflows*
addresses can be used. This can be configured with ``ip mptcp endpoint`` for
example.
+When TCP gives up on an established subflow initiated by the in-kernel path
+manager, after a retransmission or keepalive timeout, the path manager creates
+it again, with the ``subflow-retries`` limit as its number of SYN retries, like
+``TCP_SYNCNT``. A subflow reset by the peer, or by anything on the path, is not
+created again. TCP only gives up on a subflow after ``tcp_retries2``
+retransmissions, about 15 minutes by default: shorter outages are handled by
+TCP itself. A subflow that only receives data does not notice that its path
+stopped working: the application should then enable TCP keepalive on the
+MPTCP socket (``SO_KEEPALIVE``, ``TCP_KEEPIDLE``, ``TCP_KEEPINTVL`` and
+``TCP_KEEPCNT``), which MPTCP applies to all its subflows.
+
Packet Scheduler
----------------
diff --git a/net/mptcp/pm.c b/net/mptcp/pm.c
index d7c5b50b34cc..2bb08be95cd1 100644
--- a/net/mptcp/pm.c
+++ b/net/mptcp/pm.c
@@ -694,6 +694,26 @@ void mptcp_pm_subflow_check_next(struct mptcp_sock *msk,
spin_unlock_bh(&pm->lock);
}
+/* Called with the msk socket lock held, before closing a subflow: true if
+ * it has to be re-created once closed, see mptcp_pm_subflow_retry().
+ */
+bool mptcp_pm_subflow_retry_check(struct mptcp_sock *msk,
+ const struct mptcp_subflow_context *subflow,
+ struct mptcp_pm_retry *retry)
+{
+ if (!mptcp_pm_is_kernel(msk))
+ return false;
+
+ return mptcp_pm_kernel_retry_check(msk, subflow, retry);
+}
+
+void mptcp_pm_subflow_retry(struct mptcp_sock *msk,
+ const struct mptcp_pm_retry *retry)
+{
+ if (mptcp_pm_is_kernel(msk))
+ mptcp_pm_kernel_retry(msk, retry);
+}
+
void mptcp_pm_add_addr_received(const struct sock *ssk,
const struct mptcp_addr_info *addr)
{
@@ -762,6 +782,24 @@ static void mptcp_pm_add_addr_send_ack(struct mptcp_sock *msk)
mptcp_pm_schedule_work(msk, MPTCP_PM_ADD_ADDR_SEND_ACK);
}
+/* A subflow using a removed address must not be re-created, whatever its
+ * state, e.g. closed by TCP but not collected yet, see
+ * mptcp_pm_kernel_retry_check().
+ */
+static void mptcp_pm_rm_no_retry(struct mptcp_subflow_context *subflow,
+ enum linux_mptcp_mib_field rm_type, u8 rm_id)
+{
+ u8 id;
+
+ if (rm_type == MPTCP_MIB_RMADDR)
+ id = READ_ONCE(subflow->remote_id);
+ else
+ id = subflow_get_local_id(subflow);
+
+ if (id == rm_id)
+ subflow->no_retry = true;
+}
+
static void mptcp_pm_rm_addr_or_subflow(struct mptcp_sock *msk,
const struct mptcp_rm_list *rm_list,
enum linux_mptcp_mib_field rm_type)
@@ -794,6 +832,7 @@ static void mptcp_pm_rm_addr_or_subflow(struct mptcp_sock *msk,
int how = RCV_SHUTDOWN | SEND_SHUTDOWN;
u8 id = subflow_get_local_id(subflow);
+ mptcp_pm_rm_no_retry(subflow, rm_type, rm_id);
if ((1 << inet_sk_state_load(ssk)) &
(TCPF_FIN_WAIT1 | TCPF_FIN_WAIT2 | TCPF_CLOSING | TCPF_CLOSE))
continue;
diff --git a/net/mptcp/pm_kernel.c b/net/mptcp/pm_kernel.c
index 0379e1495cf4..4ee65aba1f13 100644
--- a/net/mptcp/pm_kernel.c
+++ b/net/mptcp/pm_kernel.c
@@ -1610,6 +1610,124 @@ bool mptcp_pm_nl_check_work_pending(struct mptcp_sock *msk)
return true;
}
+/* TCP gave up on the path: a retransmission or keepalive timeout, possibly
+ * reported with an ICMP error received from the path.
+ */
+static bool mptcp_pm_retry_path_error(int err)
+{
+ switch (err) {
+ case ETIMEDOUT:
+ case EHOSTUNREACH:
+ case ENETUNREACH:
+ case EHOSTDOWN:
+ case ENETDOWN:
+ return true;
+ }
+
+ return false;
+}
+
+/* Called with the msk socket lock held, before the subflow is closed */
+bool mptcp_pm_kernel_retry_check(struct mptcp_sock *msk,
+ const struct mptcp_subflow_context *subflow,
+ struct mptcp_pm_retry *retry)
+{
+ struct sock *ssk = mptcp_subflow_tcp_sock(subflow);
+ struct sock *sk = (struct sock *)msk;
+ int err;
+
+ /* Only once TCP closed it: the acquire in inet_sk_state_load() makes
+ * sure sk_err and the resetting flag checked below are the ones of
+ * this close.
+ */
+ if (inet_sk_state_load(ssk) != TCP_CLOSE)
+ return false;
+
+ err = READ_ONCE(ssk->sk_err);
+ retry->syn_retries = mptcp_pm_get_limit_subflow_retries(msk);
+ if (!retry->syn_retries || !err || sk->sk_state != TCP_ESTABLISHED ||
+ __mptcp_check_fallback(msk))
+ return false;
+
+ /* mptcp_close_ssk() will not handle it again, e.g. the initial
+ * subflow, kept in the list once closed
+ */
+ if (subflow->close_event_done || READ_ONCE(subflow->local_id) < 0)
+ return false;
+
+ /* Only a path that worked is tried again. If a subflow never got
+ * established, the PM moves on to the next endpoints instead, and a
+ * re-created subflow that cannot connect is not retried again: its SYN
+ * retransmissions already spaced out the tries.
+ */
+ if (!READ_ONCE(subflow->fully_established))
+ return false;
+
+ /* its address has been removed, see mptcp_pm_rm_addr_or_subflow() */
+ if (subflow->no_retry)
+ return false;
+
+ /* Only when the path stopped working. A reset, from the peer or from
+ * anything on the path, means the path works: re-creating the subflow
+ * right away could loop. Not after a reset by MPTCP itself either, with
+ * mptcp_subflow_reset(), whatever sk_err says.
+ */
+ if (!mptcp_pm_retry_path_error(err) || subflow->resetting)
+ return false;
+
+ /* Only re-create subflows initiated by this side: MP_JOIN ones, or the
+ * initial one on the client side.
+ */
+ if (!subflow->request_join &&
+ (subflow->mp_join || READ_ONCE(msk->pm.server_side)))
+ return false;
+
+ mptcp_remote_address((struct sock_common *)ssk, &retry->remote);
+ retry->remote.id = READ_ONCE(subflow->remote_id);
+ if (!retry->remote.id && READ_ONCE(msk->pm.remote_deny_join_id0))
+ return false;
+
+ mptcp_local_address((struct sock_common *)ssk, &retry->local.addr);
+ retry->local.addr.port = 0;
+ retry->local.addr.id = subflow_get_local_id(subflow);
+ retry->local.flags = subflow->request_bkup ?
+ MPTCP_PM_ADDR_FLAG_BACKUP : 0;
+ retry->local.ifindex = ssk->sk_bound_dev_if;
+
+ return true;
+}
+
+/* Called with the msk socket lock held, after the subflow has been closed */
+void mptcp_pm_kernel_retry(struct mptcp_sock *msk,
+ const struct mptcp_pm_retry *retry)
+{
+ struct mptcp_subflow_context *subflow;
+ struct sock *sk = (struct sock *)msk;
+ struct sock *ssk;
+ bool allowed;
+
+ spin_lock_bh(&msk->pm.lock);
+ allowed = msk->pm.extra_subflows <
+ mptcp_pm_get_limit_extra_subflows(msk);
+ if (allowed)
+ msk->pm.extra_subflows++;
+ spin_unlock_bh(&msk->pm.lock);
+
+ if (!allowed ||
+ __mptcp_subflow_connect(sk, &retry->local, &retry->remote))
+ return;
+
+ /* the new subflow is the last one */
+ subflow = list_last_entry(&msk->conn_list, struct mptcp_subflow_context,
+ node);
+ ssk = mptcp_subflow_tcp_sock(subflow);
+
+ lock_sock_nested(ssk, SINGLE_DEPTH_NESTING);
+ WRITE_ONCE(inet_csk(ssk)->icsk_syn_retries, retry->syn_retries);
+ subflow->retry = true;
+ release_sock(ssk);
+}
+
/* Called under PM lock */
void __mptcp_pm_kernel_worker(struct mptcp_sock *msk)
{
diff --git a/net/mptcp/protocol.c b/net/mptcp/protocol.c
index 60cbd91be632..5cee1ed788ca 100644
--- a/net/mptcp/protocol.c
+++ b/net/mptcp/protocol.c
@@ -2816,6 +2816,8 @@ static void __mptcp_close_subflow(struct sock *sk)
mptcp_for_each_subflow_safe(msk, subflow, tmp) {
struct sock *ssk = mptcp_subflow_tcp_sock(subflow);
int ssk_state = inet_sk_state_load(ssk);
+ struct mptcp_pm_retry retry;
+ bool do_retry;
if (ssk_state != TCP_CLOSE &&
(ssk_state != TCP_CLOSE_WAIT ||
@@ -2827,7 +2829,10 @@ static void __mptcp_close_subflow(struct sock *sk)
if (!skb_queue_empty_lockless(&ssk->sk_receive_queue))
continue;
+ do_retry = mptcp_pm_subflow_retry_check(msk, subflow, &retry);
mptcp_close_ssk(sk, ssk, subflow);
+ if (do_retry)
+ mptcp_pm_subflow_retry(msk, &retry);
}
}
@@ -3494,6 +3499,26 @@ static void mptcp_check_listen_stop(struct sock *sk)
release_sock(ssk);
}
+/* Re-created subflows still connecting are useless once the msk is closed,
+ * and their SYN retransmissions could keep them around for a long time.
+ */
+static void mptcp_close_retry_subflows(struct sock *sk)
+{
+ struct mptcp_subflow_context *subflow, *tmp;
+ struct mptcp_sock *msk = mptcp_sk(sk);
+
+ mptcp_for_each_subflow_safe(msk, subflow, tmp) {
+ struct sock *ssk = mptcp_subflow_tcp_sock(subflow);
+
+ if (!subflow->retry || inet_sk_state_load(ssk) != TCP_SYN_SENT)
+ continue;
+
+ /* it never carried data: nothing to push or re-inject */
+ mptcp_pm_subflow_check_next(msk, subflow);
+ __mptcp_close_ssk(sk, ssk, subflow, 0);
+ }
+}
+
bool __mptcp_close(struct sock *sk, long timeout)
{
struct mptcp_subflow_context *subflow;
@@ -3520,6 +3545,8 @@ bool __mptcp_close(struct sock *sk, long timeout)
__mptcp_wr_shutdown(sk);
}
+ mptcp_close_retry_subflows(sk);
+
sk_stream_wait_close(sk, timeout);
cleanup:
diff --git a/net/mptcp/protocol.h b/net/mptcp/protocol.h
index 6d571328aca9..7e33ead76ee6 100644
--- a/net/mptcp/protocol.h
+++ b/net/mptcp/protocol.h
@@ -608,6 +608,8 @@ struct mptcp_subflow_context {
bool close_event_done; /* has done the post-closed part,
* protected by msk socket lock
*/
+ bool retry; /* re-created after an error, msk lock */
+ bool no_retry; /* uses a removed address, msk lock */
u32 lent_mem_frag;
u32 remote_nonce;
u32 local_nonce;
@@ -1137,6 +1139,18 @@ void mptcp_pm_subflow_established(struct mptcp_sock *msk);
bool mptcp_pm_nl_check_work_pending(struct mptcp_sock *msk);
void mptcp_pm_subflow_check_next(struct mptcp_sock *msk,
const struct mptcp_subflow_context *subflow);
+
+struct mptcp_pm_retry {
+ struct mptcp_pm_local local;
+ struct mptcp_addr_info remote;
+ u8 syn_retries;
+};
+
+bool mptcp_pm_subflow_retry_check(struct mptcp_sock *msk,
+ const struct mptcp_subflow_context *subflow,
+ struct mptcp_pm_retry *retry);
+void mptcp_pm_subflow_retry(struct mptcp_sock *msk,
+ const struct mptcp_pm_retry *retry);
void mptcp_pm_add_addr_received(const struct sock *ssk,
const struct mptcp_addr_info *addr);
void mptcp_pm_add_addr_echoed(struct mptcp_sock *msk,
@@ -1258,6 +1272,11 @@ void __init mptcp_pm_userspace_register(void);
void __init mptcp_pm_nl_init(void);
void mptcp_pm_worker(struct mptcp_sock *msk);
void __mptcp_pm_kernel_worker(struct mptcp_sock *msk);
+bool mptcp_pm_kernel_retry_check(struct mptcp_sock *msk,
+ const struct mptcp_subflow_context *subflow,
+ struct mptcp_pm_retry *retry);
+void mptcp_pm_kernel_retry(struct mptcp_sock *msk,
+ const struct mptcp_pm_retry *retry);
u8 mptcp_pm_get_endp_signal_max(const struct mptcp_sock *msk);
u8 mptcp_pm_get_endp_subflow_max(const struct mptcp_sock *msk);
u8 mptcp_pm_get_endp_laminar_max(const struct mptcp_sock *msk);
--
2.25.1
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH mptcp-next v7 5/6] selftests: mptcp: pm_nl_ctl: support the subflow-retries limit
2026-10-20 3:17 [PATCH mptcp-next v7 0/6] mptcp: pm: in-kernel: retry subflows closed after an error Anil Kaushik
` (3 preceding siblings ...)
2026-10-20 3:17 ` [PATCH mptcp-next v7 4/6] mptcp: pm: in-kernel: retry subflows closed after an error Anil Kaushik
@ 2026-10-20 3:17 ` Anil Kaushik
2026-10-20 3:17 ` [PATCH mptcp-next v7 6/6] selftests: mptcp: join: validate subflow retry after link failure Anil Kaushik
2026-10-21 12:44 ` [PATCH mptcp-next v7 0/6] mptcp: pm: in-kernel: retry subflows closed after an error MPTCP CI
6 siblings, 0 replies; 9+ messages in thread
From: Anil Kaushik @ 2026-10-20 3:17 UTC (permalink / raw)
To: mptcp; +Cc: Anil Kaushik
'pm_nl_ctl limits' can now set the subflow-retries limit with a third
value, and shows it when the kernel supports it.
pm_netlink.sh checks its default value, that a value above the maximum
is refused, and that it can be set. This is only done with pm_nl_ctl
for the moment: iproute2 does not support this limit yet.
Assisted-by: LLM
Signed-off-by: Anil Kaushik <anilkaushikwireless@gmail.com>
---
Notes:
v7:
- also check that the maximum value, 127, is accepted (netdev
Sashiko)
v5:
- new (Matthieu)
.../testing/selftests/net/mptcp/pm_netlink.sh | 47 ++++++++++++++++++-
tools/testing/selftests/net/mptcp/pm_nl_ctl.c | 34 +++++++++++---
2 files changed, 72 insertions(+), 9 deletions(-)
diff --git a/tools/testing/selftests/net/mptcp/pm_netlink.sh b/tools/testing/selftests/net/mptcp/pm_netlink.sh
index 21bfe1311f11..f06e4f61fa95 100755
--- a/tools/testing/selftests/net/mptcp/pm_netlink.sh
+++ b/tools/testing/selftests/net/mptcp/pm_netlink.sh
@@ -46,15 +46,20 @@ trap cleanup EXIT
mptcp_lib_ns_init ns1
+# $3: subflow retries, only shown by pm_nl_ctl, if supported
format_limits() {
local accept="${1}"
local subflows="${2}"
+ local retries="${3}"
if mptcp_lib_is_ip_mptcp; then
# with a space at the end
printf "add_addr_accepted %d subflows %d \n" "${accept}" "${subflows}"
else
printf "accept %d\nsubflows %d\n" "${accept}" "${subflows}"
+ if [ -n "${retries}" ]; then
+ printf "subflow_retries %d\n" "${retries}"
+ fi
fi
}
@@ -75,6 +80,22 @@ get_limits_nb() {
fi
}
+# The subflow retries limit is only set and shown with pm_nl_ctl for now.
+# Prints its default value if it is supported.
+get_default_retries() {
+ if mptcp_lib_is_ip_mptcp; then
+ return
+ fi
+
+ if ip netns exec "${ns1}" ./pm_nl_ctl limits |
+ grep -q "^subflow_retries"; then
+ echo 15
+ elif mptcp_lib_expect_all_features; then
+ # missing feature: the defaults limits check will fail
+ echo 15
+ fi
+}
+
format_endpoints() {
mptcp_lib_pm_nl_format_endpoints "${@}"
}
@@ -154,8 +175,10 @@ mptcp_lib_subtests_last_ts_reset
check "show_endpoints" "" "defaults addr list"
default_limits="$(get_limits)"
+default_retries="$(get_default_retries)"
if mptcp_lib_expect_all_features; then
- check "get_limits" "$(format_limits 0 2)" "defaults limits"
+ check "get_limits" "$(format_limits 0 2 "${default_retries}")" \
+ "defaults limits"
fi
add_endpoint 10.0.1.1
@@ -224,7 +247,27 @@ check "get_limits" "${default_limits}" "subflows above hard limit"
set_limits ${max_limits} ${max_limits}
flush_endpoint ## to make sure it doesn't affect the limits
-check "get_limits" "$(format_limits ${max_limits} ${max_limits})" "set limits"
+max_limits_out="$(format_limits ${max_limits} ${max_limits} \
+ "${default_retries}")"
+check "get_limits" "${max_limits_out}" "set limits"
+
+if [ -n "${default_retries}" ]; then
+ ip netns exec "${ns1}" ./pm_nl_ctl limits ${max_limits} ${max_limits} \
+ 128 2>/dev/null
+ check "get_limits" "${max_limits_out}" \
+ "subflow retries above hard limit"
+
+ ip netns exec "${ns1}" ./pm_nl_ctl limits ${max_limits} ${max_limits} 0
+ check "get_limits" "$(format_limits ${max_limits} ${max_limits} 0)" \
+ "set subflow retries"
+
+ ip netns exec "${ns1}" ./pm_nl_ctl limits ${max_limits} ${max_limits} \
+ 127
+ check "get_limits" "$(format_limits ${max_limits} ${max_limits} 127)" \
+ "set max subflow retries"
+ ip netns exec "${ns1}" ./pm_nl_ctl limits ${max_limits} ${max_limits} \
+ "${default_retries}"
+fi
flush_endpoint
add_endpoint 10.0.1.1
diff --git a/tools/testing/selftests/net/mptcp/pm_nl_ctl.c b/tools/testing/selftests/net/mptcp/pm_nl_ctl.c
index 78180da1efcc..41467b997018 100644
--- a/tools/testing/selftests/net/mptcp/pm_nl_ctl.c
+++ b/tools/testing/selftests/net/mptcp/pm_nl_ctl.c
@@ -39,7 +39,7 @@ static void syntax(char *argv[])
fprintf(stderr, "\tset [<ip>] [id <nr>] flags [no]backup|[no]fullmesh [port <nr>] [token <token>] [rip <ip>] [rport <port>]\n");
fprintf(stderr, "\tflush\n");
fprintf(stderr, "\tdump\n");
- fprintf(stderr, "\tlimits [<rcv addr max> <subflow max>]\n");
+ fprintf(stderr, "\tlimits [<rcv addr max> <subflow max> [<retry>]]\n");
fprintf(stderr, "\tevents\n");
fprintf(stderr, "\tlisten <local-ip> <local-port>\n");
exit(0);
@@ -1217,6 +1217,7 @@ int flush_addrs(int fd, int pm_family, int argc, char *argv[])
static void print_limits(struct nlmsghdr *nh, int pm_family, int total_len)
{
struct rtattr *attrs;
+ const char *name;
uint32_t max;
for (; NLMSG_OK(nh, total_len); nh = NLMSG_NEXT(nh, total_len)) {
@@ -1235,13 +1236,22 @@ static void print_limits(struct nlmsghdr *nh, int pm_family, int total_len)
while (RTA_OK(attrs, len)) {
int type = attrs->rta_type;
- if (type != MPTCP_PM_ATTR_RCV_ADD_ADDRS &&
- type != MPTCP_PM_ATTR_SUBFLOWS)
+ switch (type) {
+ case MPTCP_PM_ATTR_RCV_ADD_ADDRS:
+ name = "accept";
+ break;
+ case MPTCP_PM_ATTR_SUBFLOWS:
+ name = "subflows";
+ break;
+ case MPTCP_PM_ATTR_SUBFLOW_RETRIES:
+ name = "subflow_retries";
+ break;
+ default:
goto next;
+ }
memcpy(&max, RTA_DATA(attrs), 4);
- printf("%s %u\n", type == MPTCP_PM_ATTR_SUBFLOWS ?
- "subflows" : "accept", max);
+ printf("%s %u\n", name, max);
next:
attrs = RTA_NEXT(attrs, len);
@@ -1254,15 +1264,17 @@ int get_set_limits(int fd, int pm_family, int argc, char *argv[])
char data[NLMSG_ALIGN(sizeof(struct nlmsghdr)) +
NLMSG_ALIGN(sizeof(struct genlmsghdr)) +
1024];
- uint32_t rcv_addr = 0, subflows = 0;
+ uint32_t rcv_addr = 0, subflows = 0, retries = 0;
int cmd, len = sizeof(data);
struct nlmsghdr *nh;
int off = 0;
/* limit */
- if (argc == 4) {
+ if (argc == 4 || argc == 5) {
rcv_addr = atoi(argv[2]);
subflows = atoi(argv[3]);
+ if (argc == 5)
+ retries = atoi(argv[4]);
cmd = MPTCP_PM_CMD_SET_LIMITS;
} else {
cmd = MPTCP_PM_CMD_GET_LIMITS;
@@ -1287,6 +1299,14 @@ int get_set_limits(int fd, int pm_family, int argc, char *argv[])
memcpy(RTA_DATA(rta), &subflows, 4);
off += NLMSG_ALIGN(rta->rta_len);
+ if (argc == 5) {
+ rta = (void *)(data + off);
+ rta->rta_type = MPTCP_PM_ATTR_SUBFLOW_RETRIES;
+ rta->rta_len = RTA_LENGTH(4);
+ memcpy(RTA_DATA(rta), &retries, 4);
+ off += NLMSG_ALIGN(rta->rta_len);
+ }
+
/* do not expect a reply */
len = 0;
}
--
2.25.1
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH mptcp-next v7 6/6] selftests: mptcp: join: validate subflow retry after link failure
2026-10-20 3:17 [PATCH mptcp-next v7 0/6] mptcp: pm: in-kernel: retry subflows closed after an error Anil Kaushik
` (4 preceding siblings ...)
2026-10-20 3:17 ` [PATCH mptcp-next v7 5/6] selftests: mptcp: pm_nl_ctl: support the subflow-retries limit Anil Kaushik
@ 2026-10-20 3:17 ` Anil Kaushik
2026-10-21 12:44 ` [PATCH mptcp-next v7 0/6] mptcp: pm: in-kernel: retry subflows closed after an error MPTCP CI
6 siblings, 0 replies; 9+ messages in thread
From: Anil Kaushik @ 2026-10-20 3:17 UTC (permalink / raw)
To: mptcp; +Cc: Anil Kaushik
Test the subflow-retries limit, with its default value, and a long
connection in the background: drop the packets the client sends from
the additional subflow address with netfilter, without bringing the link
down, until the subflow is closed and re-created. Let them through again
once the SYN of the new subflow has been retransmitted more times than
tcp_syn_retries allows, and check the subflow comes back from the same
address. Then do the same with the initial subflow.
Finally, close the connection while a subflow is being re-created, and
check the netlink events. The big files used for the long connection
are emptied at the end.
The test is skipped if pm_nl_ctl does not show the new limit, or if the
filter rules cannot be set.
Assisted-by: LLM
Signed-off-by: Anil Kaushik <anilkaushikwireless@gmail.com>
---
Notes:
v6 (Sashiko):
- use the slowwait helpers from net/lib.sh
- longer first wait, while the 128 MB files are created
- skip if the filter rules cannot be set, check each drop rule
(netdev Sashiko)
- empty the big files at the end, cinsent too, keep the variables
(netdev Sashiko)
v5 (Matthieu):
- skipped if pm_nl_ctl does not show the new limit
- wait for the SYN retransmissions instead of a sleep, with
tcp_syn_linear_timeouts=0
- helper for the check after the close
- slower links with short queues, so the transfer lasts the whole
test on any machine, longer timeout (killed at the end)
v4:
- default limit, no sysctl or MIB counter
- more SYN retransmissions than tcp_syn_retries
- close the connection while a subflow is re-created
- check the netlink events
- check the waits and the new subflow port, remove the big files at
the end (Sashiko)
v3:
- check the subflow address, create the files before the background job
v2:
- one test, with netfilter
.../testing/selftests/net/mptcp/mptcp_join.sh | 178 +++++++++++++++++-
1 file changed, 174 insertions(+), 4 deletions(-)
diff --git a/tools/testing/selftests/net/mptcp/mptcp_join.sh b/tools/testing/selftests/net/mptcp/mptcp_join.sh
index f4d8afaeda4f..a99cee39da63 100755
--- a/tools/testing/selftests/net/mptcp/mptcp_join.sh
+++ b/tools/testing/selftests/net/mptcp/mptcp_join.sh
@@ -1137,7 +1137,7 @@ run_tests()
make_file "$cinfail" "client" $size
# create the input file for the failure test when
# the first failure test run
- elif [ "$test_linkfail" -ne 0 ] && [ -z "$cinfail" ]; then
+ elif [ "$test_linkfail" -ne 0 ] && [ ! -s "$cinfail" ]; then
# the client file must be considerably larger
# of the maximum expected cwin value, or the
# link utilization will be not predicable
@@ -1146,7 +1146,9 @@ run_tests()
size=$((size*8192))
size=$((size + ( RANDOM % 8192) ))
- cinfail=$(mktemp)
+ if [ -z "$cinfail" ]; then
+ cinfail=$(mktemp)
+ fi
make_file "$cinfail" "client" $size
fi
@@ -1157,12 +1159,14 @@ run_tests()
sinfail=$(mktemp)
fi
make_file "$sinfail" "server" $size
- elif [ "$test_linkfail" -eq 2 ] && [ -z "$sinfail" ]; then
+ elif [ "$test_linkfail" -eq 2 ] && [ ! -s "$sinfail" ]; then
size=$((RANDOM%16))
size=$((size+1))
size=$((size*2048))
- sinfail=$(mktemp)
+ if [ -z "$sinfail" ]; then
+ sinfail=$(mktemp)
+ fi
make_file "$sinfail" "server" $size
fi
@@ -2151,6 +2155,113 @@ wait_attempt_fail()
return 1
}
+# $1: timeout in seconds ; $2: ns ; $3: counter ; $4: increment, 1 by default
+wait_counter_inc()
+{
+ slowwait_for_counter "${1}" "${4:-1}" \
+ mptcp_lib_get_counter "${2}" "${3}" >/dev/null
+}
+
+# $1: client address ; prints the local address and port of its subflow
+sf_from()
+{
+ ip netns exec $ns2 ss -tnH state established src "${1}" |
+ awk '{ print $3; exit }'
+}
+
+has_subflow_retries()
+{
+ ip netns exec $ns2 ./pm_nl_ctl limits | grep -q "^subflow_retries"
+}
+
+# the client packets are dropped with rules in this chain
+init_nft_filter()
+{
+ init_nftables
+ if ! ip netns exec $ns2 nft list chain ip filter OUTPUT \
+ &>/dev/null; then
+ mark_as_skipped "unable to set the filter rules"
+ return 1
+ fi
+}
+
+# $1: client address
+nft_drop_from()
+{
+ ip netns exec $ns2 nft add rule ip filter OUTPUT ip saddr "${1}" drop
+}
+
+# $1: client address to block
+#
+# The link stays up, but the client packets from this address are dropped:
+# the subflow using it is closed after a few retransmissions, and re-created.
+# The packets go through again once the SYN has been retransmitted more times
+# than tcp_syn_retries allows: the new subflow still connects thanks to the
+# subflow-retries limit.
+sf_retry_after_block()
+{
+ local before ack_rx
+
+ before=$(sf_from "${1}")
+
+ print_check "subflow from ${1}"
+ if [ -z "${before}" ]; then
+ fail_test "no established subflow from ${1} before the block"
+ return
+ fi
+
+ if ! nft_drop_from "${1}"; then
+ fail_test "unable to drop the packets from ${1}"
+ return
+ fi
+ if ! wait_counter_inc "${timeout_poll}" $ns2 "MPTcpExtMPJoinSynTx"; then
+ ip netns exec $ns2 nft flush chain ip filter OUTPUT
+ fail_test "subflow from ${1} not re-created"
+ return
+ fi
+ # tcp_syn_retries=1: one more retransmission than allowed
+ if ! wait_counter_inc "${timeout_poll}" \
+ $ns2 "TcpExtTCPSynRetrans" 2; then
+ ip netns exec $ns2 nft flush chain ip filter OUTPUT
+ fail_test "SYN from ${1} not retransmitted enough"
+ return
+ fi
+ # read before the flush: the next SYN can be sent right after it
+ ack_rx=$(mptcp_lib_get_counter $ns1 "MPTcpExtMPJoinAckRx")
+ ip netns exec $ns2 nft flush chain ip filter OUTPUT
+
+ if ! slowwait "${timeout_poll}" until_counter_is ">= $((ack_rx + 1))" \
+ mptcp_lib_get_counter $ns1 "MPTcpExtMPJoinAckRx" >/dev/null; then
+ fail_test "re-created subflow from ${1} not established"
+ return
+ fi
+
+ if [ -z "$(sf_from "${1}")" ]; then
+ fail_test "no established subflow from ${1}"
+ else
+ print_ok
+ fi
+}
+
+no_sf_connecting()
+{
+ ! ip netns exec $ns2 ss -tnH state syn-sent | grep -q .
+}
+
+# $1: result of the wait for the subflow being re-created before the close
+chk_no_sf_connecting()
+{
+ print_check "no subflow connecting after close"
+ if [ "${1}" -ne 0 ]; then
+ fail_test "no subflow re-created before the close"
+ # closed with the MPTCP socket
+ elif slowwait "${timeout_poll}" no_sf_connecting; then
+ print_ok
+ else
+ fail_test "subflow still connecting after close"
+ fi
+}
+
set_userspace_pm()
{
local ns=$1
@@ -2585,6 +2696,65 @@ link_failure_tests()
chk_stale_nr $ns2 1 -1 2
chk_link_usage $ns2 ns2eth3 $cinsent 50
fi
+
+ # subflows closed after a silent link failure, then re-created: the
+ # additional one, then the initial one. Then close the connection
+ # while a subflow is being re-created.
+ if reset_with_events "subflow retry after link failure" &&
+ continue_if has_subflow_retries && init_nft_filter; then
+ # slow links, short queues: the transfer lasts the whole test on
+ # any machine, and the blocked subflow always has data in flight
+ local i
+ for i in 1 3; do
+ tc -n $ns1 qdisc add dev ns1eth$i root netem \
+ rate 4mbit limit 30 delay 1ms
+ tc -n $ns2 qdisc add dev ns2eth$i root netem \
+ rate 4mbit limit 30 delay 1ms
+ done
+ ip netns exec $ns2 sysctl -q net.ipv4.tcp_retries2=3
+ ip netns exec $ns2 sysctl -q net.ipv4.tcp_syn_retries=1
+ ip netns exec $ns2 sysctl -q net.ipv4.tcp_syn_linear_timeouts=0
+ pm_nl_set_limits $ns1 0 4
+ pm_nl_set_limits $ns2 0 2
+ pm_nl_add_endpoint $ns2 10.0.3.2 dev ns2eth3 flags subflow
+ # created here, not in the background job, for cleanup()
+ if [ -z "$cinfail" ]; then
+ cinfail=$(mktemp)
+ fi
+ if [ -z "$sinfail" ]; then
+ sinfail=$(mktemp)
+ fi
+ # killed at the end: the timeout only stops a stuck test
+ { timeout_test=600 test_linkfail=131072 \
+ run_tests $ns1 $ns2 10.0.1.1 & } 2>/dev/null
+ local tests_pid=$!
+
+ # the 128 MB files are created first, that can take a while
+ wait_counter_inc $((timeout_poll * 4)) \
+ $ns1 "MPTcpExtMPJoinAckRx"
+ sf_retry_after_block 10.0.3.2
+ sf_retry_after_block 10.0.1.2
+
+ local retrying=1
+ if nft_drop_from 10.0.3.2; then
+ wait_counter_inc "${timeout_poll}" \
+ $ns2 "MPTcpExtMPJoinSynTx"
+ retrying=$?
+ fi
+ mptcp_lib_kill_group_wait $tests_pid
+ chk_no_sf_connecting ${retrying}
+ ip netns exec $ns2 nft flush chain ip filter OUTPUT
+ kill_events_pids
+
+ join_syn_tx=4 chk_join_nr 3 3 3
+ chk_evt_nr ns2 MPTCP_LIB_EVENT_SUB_ESTABLISHED 3
+ chk_evt_nr ns2 MPTCP_LIB_EVENT_SUB_CLOSED 3 error 3
+
+ # big files: empty them, the next tests write their own content
+ : > "$cinfail"
+ : > "$sinfail"
+ : > "$cinsent"
+ fi
}
add_addr_timeout_tests()
--
2.25.1
^ permalink raw reply related [flat|nested] 9+ messages in thread
* Re: [PATCH mptcp-next v7 4/6] mptcp: pm: in-kernel: retry subflows closed after an error
2026-10-20 3:17 ` [PATCH mptcp-next v7 4/6] mptcp: pm: in-kernel: retry subflows closed after an error Anil Kaushik
@ 2026-10-20 8:37 ` sashiko-bot
0 siblings, 0 replies; 9+ messages in thread
From: sashiko-bot @ 2026-10-20 8:37 UTC (permalink / raw)
To: Anil Kaushik, mptcp
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] mptcp: __mptcp_close() disconnects retry subflows before closing them
--
commit e4e5fe628ce5b62d531a83ca2d2c9088740581ed
Author: Anil Kaushik <anilkaushikwireless@gmail.com>
mptcp: pm: in-kernel: retry subflows closed after an error
This commit teaches the in-kernel path manager to re-create established
subflows that were initiated locally and closed after a path error such
as a retransmission or keepalive timeout. It also closes any re-created
subflows that are still connecting when the MPTCP socket is closed.
> diff --git a/net/mptcp/protocol.c b/net/mptcp/protocol.c
> index 60cbd91be6320..5cee1ed788caa 100644
> --- a/net/mptcp/protocol.c
> +++ b/net/mptcp/protocol.c
[ ... ]
> @@ -3494,6 +3499,26 @@ static void mptcp_check_listen_stop(struct sock *sk)
> release_sock(ssk);
> }
>
> +/* Re-created subflows still connecting are useless once the msk is closed,
> + * and their SYN retransmissions could keep them around for a long time.
> + */
> +static void mptcp_close_retry_subflows(struct sock *sk)
> +{
> + struct mptcp_subflow_context *subflow, *tmp;
> + struct mptcp_sock *msk = mptcp_sk(sk);
> +
> + mptcp_for_each_subflow_safe(msk, subflow, tmp) {
> + struct sock *ssk = mptcp_subflow_tcp_sock(subflow);
> +
> + if (!subflow->retry || inet_sk_state_load(ssk) != TCP_SYN_SENT)
> + continue;
> +
> + /* it never carried data: nothing to push or re-inject */
> + mptcp_pm_subflow_check_next(msk, subflow);
> + __mptcp_close_ssk(sk, ssk, subflow, 0);
> + }
> +}
> +
> bool __mptcp_close(struct sock *sk, long timeout)
> {
> struct mptcp_subflow_context *subflow;
> @@ -3520,6 +3545,8 @@ bool __mptcp_close(struct sock *sk, long timeout)
> __mptcp_wr_shutdown(sk);
> }
>
> + mptcp_close_retry_subflows(sk);
> +
> sk_stream_wait_close(sk, timeout);
>
> cleanup:
[Severity: Medium]
Should mptcp_close_retry_subflows() be called before mptcp_do_fastclose()
and __mptcp_wr_shutdown(), or also handle retry subflows that have already
been disconnected?
When closing an established or TCP_CLOSE_WAIT MPTCP socket that has no
unsent data queued in mptcp_send_head(sk), __mptcp_wr_shutdown() calls
mptcp_check_send_data_fin(), which iterates over msk->conn_list and calls
mptcp_subflow_shutdown(sk, tcp_sk, SEND_SHUTDOWN) on every subflow:
net/mptcp/protocol.c:mptcp_subflow_shutdown() {
...
switch (ssk->sk_state) {
case TCP_LISTEN:
break;
case TCP_SYN_SENT:
WARN_ON_ONCE(tcp_disconnect(ssk, O_NONBLOCK));
break;
...
}
For a retry subflow in TCP_SYN_SENT, tcp_disconnect() sets ssk->sk_state to
TCP_CLOSE without invoking sk_state_change, unlinking the subflow from
msk->conn_list, or decrementing msk->pm.extra_subflows.
When mptcp_close_retry_subflows() runs right after __mptcp_wr_shutdown() (or
during close() after an earlier shutdown(fd, SHUT_WR)), it checks for
TCP_SYN_SENT:
if (!subflow->retry || inet_sk_state_load(ssk) != TCP_SYN_SENT)
continue;
Because ssk->sk_state is already TCP_CLOSE, mptcp_close_retry_subflows()
skips the retry subflow. This leaves msk->pm.extra_subflows incremented and
leaves the disconnected retry subflow on msk->conn_list, which can also
prevent sk->sk_state from transitioning to TCP_CLOSE before
sk_stream_wait_close(sk, timeout) when all other subflows are already
closed.
Similarly, on the fastclose path, calling mptcp_do_fastclose() before
mptcp_close_retry_subflows() calls tcp_send_active_reset() on the
TCP_SYN_SENT retry subflow before mptcp_close_retry_subflows() can close it.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261020031711.1896660-1-anilkaushikwireless@gmail.com?part=4
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH mptcp-next v7 0/6] mptcp: pm: in-kernel: retry subflows closed after an error
2026-10-20 3:17 [PATCH mptcp-next v7 0/6] mptcp: pm: in-kernel: retry subflows closed after an error Anil Kaushik
` (5 preceding siblings ...)
2026-10-20 3:17 ` [PATCH mptcp-next v7 6/6] selftests: mptcp: join: validate subflow retry after link failure Anil Kaushik
@ 2026-10-21 12:44 ` MPTCP CI
6 siblings, 0 replies; 9+ messages in thread
From: MPTCP CI @ 2026-10-21 12:44 UTC (permalink / raw)
To: Anil Kaushik; +Cc: mptcp
Hi Anil,
Thank you for your modifications, that's great!
Our CI did some validations and here is its report:
- KVM Validation: normal (except selftest_mptcp_join): Success! ✅
- KVM Validation: normal (only selftest_mptcp_join): Success! ✅
- KVM Validation: debug (except selftest_mptcp_join): Success! ✅
- KVM Validation: debug (only selftest_mptcp_join): Success! ✅
- KVM Validation: btf-normal (only bpftest_all): Success! ✅
- KVM Validation: btf-debug (only bpftest_all): Success! ✅
- Perf: Critical: No conclusion ❓
- Task: https://github.com/multipath-tcp/mptcp_net-next/actions/runs/39580316985
Initiator: Patchew Applier
Commits: https://github.com/multipath-tcp/mptcp_net-next/commits/9e25d469f602
Patchwork: https://patchwork.kernel.org/project/mptcp/list/?series=1189399
If there are some issues, you can reproduce them using the same environment as
the one used by the CI thanks to a docker image, e.g.:
$ cd [kernel source code]
$ docker run -v "${PWD}:${PWD}:rw" -w "${PWD}" --privileged --rm -it \
--pull always mptcp/mptcp-upstream-virtme-docker:latest \
auto-normal
For more details:
https://github.com/multipath-tcp/mptcp-upstream-virtme-docker
Please note that despite all the efforts that have been already done to have a
stable tests suite when executed on a public CI like here, it is possible some
reported issues are not due to your modifications. Still, do not hesitate to
help us improve that ;-)
Cheers,
MPTCP GH Action bot
Bot operated by Matthieu Baerts (NGI0 Core)
^ permalink raw reply [flat|nested] 9+ messages in thread
end of thread, other threads:[~2026-10-21 12:44 UTC | newest]
Thread overview: 9+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-20 3:17 [PATCH mptcp-next v7 0/6] mptcp: pm: in-kernel: retry subflows closed after an error Anil Kaushik
2026-10-20 3:17 ` [PATCH mptcp-next v7 1/6] selftests: mptcp: connect: retry write() on EAGAIN Anil Kaushik
2026-10-20 3:17 ` [PATCH mptcp-next v7 2/6] netlink: specs: mptcp: add the subflow-retries attribute Anil Kaushik
2026-10-20 3:17 ` [PATCH mptcp-next v7 3/6] mptcp: pm: in-kernel: add the subflow-retries limit Anil Kaushik
2026-10-20 3:17 ` [PATCH mptcp-next v7 4/6] mptcp: pm: in-kernel: retry subflows closed after an error Anil Kaushik
2026-10-20 8:37 ` sashiko-bot
2026-10-20 3:17 ` [PATCH mptcp-next v7 5/6] selftests: mptcp: pm_nl_ctl: support the subflow-retries limit Anil Kaushik
2026-10-20 3:17 ` [PATCH mptcp-next v7 6/6] selftests: mptcp: join: validate subflow retry after link failure Anil Kaushik
2026-10-21 12:44 ` [PATCH mptcp-next v7 0/6] mptcp: pm: in-kernel: retry subflows closed after an error MPTCP CI
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.