* [PATCH mptcp-net v2 0/2] mptcp: fix leftover fallback after failed connect
@ 2026-09-11 15:42 ` Quanye Yang
0 siblings, 0 replies; 10+ messages in thread
From: Quanye Yang via B4 Relay @ 2026-09-11 15:42 UTC (permalink / raw)
To: mptcp
When mptcp_connect() fails before the socket reaches SS_CONNECTING,
->disconnect() is not called. Early fallback may already have set
MPTCP_FALLBACK_DONE and cleared request_mptcp, so a later connect()
on the same fd stays TCP-only.
Patch 1 undoes that state on the connect error path.
Patch 2 adds a mptcp_join.sh coverage for the same-fd retry.
Closes: https://github.com/multipath-tcp/mptcp_net-next/issues/633
---
Changes in v2:
- remove the helper function and open code the function
- also drop the NULL check on the ssk; __mptcp_nmpc_sk() already
guarantees it
- Link to v1: https://patch.msgid.link/20260910-mptcp-connect-undo-net-v1-0-446e4a4ae3e7@proton.me
---
Quanye Yang (2):
mptcp: reset msk state on early connect failure
selftests: mptcp: join: retry connect after early fallback
net/mptcp/protocol.c | 9 +-
tools/testing/selftests/net/mptcp/.gitignore | 1 +
tools/testing/selftests/net/mptcp/Makefile | 1 +
.../selftests/net/mptcp/mptcp_connect_retry.c | 216 +++++++++++++++++++++
tools/testing/selftests/net/mptcp/mptcp_join.sh | 108 +++++++++++
5 files changed, 334 insertions(+), 1 deletion(-)
---
base-commit: 38b6be101006d3e7af972999f45d4f1e8250587a
change-id: 20260910-mptcp-connect-undo-net-b80e14ae1fa2
Best regards,
--
Quanye Yang <quanyeyang@proton.me>
^ permalink raw reply [flat|nested] 10+ messages in thread
* [PATCH mptcp-net v2 0/2] mptcp: fix leftover fallback after failed connect
@ 2026-09-11 15:42 ` Quanye Yang
0 siblings, 0 replies; 10+ messages in thread
From: Quanye Yang @ 2026-09-11 15:42 UTC (permalink / raw)
To: mptcp
When mptcp_connect() fails before the socket reaches SS_CONNECTING,
->disconnect() is not called. Early fallback may already have set
MPTCP_FALLBACK_DONE and cleared request_mptcp, so a later connect()
on the same fd stays TCP-only.
Patch 1 undoes that state on the connect error path.
Patch 2 adds a mptcp_join.sh coverage for the same-fd retry.
Closes: https://github.com/multipath-tcp/mptcp_net-next/issues/633
---
Changes in v2:
- remove the helper function and open code the function
- also drop the NULL check on the ssk; __mptcp_nmpc_sk() already
guarantees it
- Link to v1: https://patch.msgid.link/20260910-mptcp-connect-undo-net-v1-0-446e4a4ae3e7@proton.me
---
Quanye Yang (2):
mptcp: reset msk state on early connect failure
selftests: mptcp: join: retry connect after early fallback
net/mptcp/protocol.c | 9 +-
tools/testing/selftests/net/mptcp/.gitignore | 1 +
tools/testing/selftests/net/mptcp/Makefile | 1 +
.../selftests/net/mptcp/mptcp_connect_retry.c | 216 +++++++++++++++++++++
tools/testing/selftests/net/mptcp/mptcp_join.sh | 108 +++++++++++
5 files changed, 334 insertions(+), 1 deletion(-)
---
base-commit: 38b6be101006d3e7af972999f45d4f1e8250587a
change-id: 20260910-mptcp-connect-undo-net-b80e14ae1fa2
Best regards,
--
Quanye Yang <quanyeyang@proton.me>
^ permalink raw reply [flat|nested] 10+ messages in thread
* [PATCH mptcp-net v2 1/2] mptcp: reset msk state on early connect failure
2026-09-11 15:42 ` Quanye Yang
@ 2026-09-11 15:42 ` Quanye Yang
-1 siblings, 0 replies; 10+ messages in thread
From: Quanye Yang via B4 Relay @ 2026-09-11 15:42 UTC (permalink / raw)
To: mptcp
From: Quanye Yang <quanyeyang@proton.me>
mptcp_connect() can fall back before the subflow SYN is sent, for
example when the netns is in an MPTCP blackhole, token allocation
fails, or MD5SIG is in use. If the subsequent subflow connect()
fails immediately (EAFNOSUPPORT, ENETUNREACH, ...),
__inet_stream_connect() returns while the socket is still
SS_UNCONNECTED and never calls ->disconnect().
The error path only dropped the token and moved the msk back to
TCP_CLOSE. MPTCP_FALLBACK_DONE, allow_subflows and
request_mptcp were left as after early fallback, so a later
connect() on the same fd stayed TCP-only.
Closes: https://github.com/multipath-tcp/mptcp_net-next/issues/633
Fixes: 0235d075a592 ("mptcp: mark as fallback even early ones")
Signed-off-by: Quanye Yang <quanyeyang@proton.me>
---
net/mptcp/protocol.c | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/net/mptcp/protocol.c b/net/mptcp/protocol.c
index e1f08f71cdb1..10817ab03cbb 100644
--- a/net/mptcp/protocol.c
+++ b/net/mptcp/protocol.c
@@ -4197,9 +4197,16 @@ static int mptcp_connect(struct sock *sk, struct sockaddr_unsized *uaddr,
* subflow_finish_connect()
*/
if (unlikely(err)) {
- /* avoid leaving a dangling token in an unconnected socket */
mptcp_token_destroy(msk);
mptcp_set_state(sk, TCP_CLOSE);
+
+ spin_lock_bh(&msk->fallback_lock);
+ msk->allow_subflows = true;
+ msk->allow_infinite_fallback = true;
+ clear_bit(MPTCP_FALLBACK_DONE, &msk->flags);
+ spin_unlock_bh(&msk->fallback_lock);
+
+ mptcp_subflow_ctx_reset(mptcp_subflow_ctx(ssk));
return err;
}
--
2.55.0
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCH mptcp-net v2 1/2] mptcp: reset msk state on early connect failure
@ 2026-09-11 15:42 ` Quanye Yang
0 siblings, 0 replies; 10+ messages in thread
From: Quanye Yang @ 2026-09-11 15:42 UTC (permalink / raw)
To: mptcp
mptcp_connect() can fall back before the subflow SYN is sent, for
example when the netns is in an MPTCP blackhole, token allocation
fails, or MD5SIG is in use. If the subsequent subflow connect()
fails immediately (EAFNOSUPPORT, ENETUNREACH, ...),
__inet_stream_connect() returns while the socket is still
SS_UNCONNECTED and never calls ->disconnect().
The error path only dropped the token and moved the msk back to
TCP_CLOSE. MPTCP_FALLBACK_DONE, allow_subflows and
request_mptcp were left as after early fallback, so a later
connect() on the same fd stayed TCP-only.
Closes: https://github.com/multipath-tcp/mptcp_net-next/issues/633
Fixes: 0235d075a592 ("mptcp: mark as fallback even early ones")
Signed-off-by: Quanye Yang <quanyeyang@proton.me>
---
net/mptcp/protocol.c | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/net/mptcp/protocol.c b/net/mptcp/protocol.c
index e1f08f71cdb1..10817ab03cbb 100644
--- a/net/mptcp/protocol.c
+++ b/net/mptcp/protocol.c
@@ -4197,9 +4197,16 @@ static int mptcp_connect(struct sock *sk, struct sockaddr_unsized *uaddr,
* subflow_finish_connect()
*/
if (unlikely(err)) {
- /* avoid leaving a dangling token in an unconnected socket */
mptcp_token_destroy(msk);
mptcp_set_state(sk, TCP_CLOSE);
+
+ spin_lock_bh(&msk->fallback_lock);
+ msk->allow_subflows = true;
+ msk->allow_infinite_fallback = true;
+ clear_bit(MPTCP_FALLBACK_DONE, &msk->flags);
+ spin_unlock_bh(&msk->fallback_lock);
+
+ mptcp_subflow_ctx_reset(mptcp_subflow_ctx(ssk));
return err;
}
--
2.55.0
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCH mptcp-net v2 2/2] selftests: mptcp: join: retry connect after early fallback
2026-09-11 15:42 ` Quanye Yang
@ 2026-09-11 15:42 ` Quanye Yang
-1 siblings, 0 replies; 10+ messages in thread
From: Quanye Yang via B4 Relay @ 2026-09-11 15:42 UTC (permalink / raw)
To: mptcp
From: Quanye Yang <quanyeyang@proton.me>
Cover leftover msk state when connect() fails before
SS_CONNECTING. Existing mptcp_connect tests always use a fresh
socket, so they cannot see a sticky fallback on the same fd.
Add a small helper that optionally issues a wrong-family connect()
on an MPTCP socket, then connects to a real IPv4 destination and
checks MPTCP_INFO. A new mptcp_join.sh group (-n) runs two cases:
- a failed connect without early fallback must not poison the retry
- after a blackhole-driven early fallback plus a failed connect,
clearing blackhole_timeout, the same fd must complete MP_CAPABLE
again
Signed-off-by: Quanye Yang <quanyeyang@proton.me>
---
tools/testing/selftests/net/mptcp/.gitignore | 1 +
tools/testing/selftests/net/mptcp/Makefile | 1 +
.../selftests/net/mptcp/mptcp_connect_retry.c | 216 +++++++++++++++++++++
tools/testing/selftests/net/mptcp/mptcp_join.sh | 108 +++++++++++
4 files changed, 326 insertions(+)
diff --git a/tools/testing/selftests/net/mptcp/.gitignore b/tools/testing/selftests/net/mptcp/.gitignore
index 833279fb34e2..5ea8f3e4985f 100644
--- a/tools/testing/selftests/net/mptcp/.gitignore
+++ b/tools/testing/selftests/net/mptcp/.gitignore
@@ -1,5 +1,6 @@
# SPDX-License-Identifier: GPL-2.0-only
mptcp_connect
+mptcp_connect_retry
mptcp_diag
mptcp_inq
mptcp_sockopt
diff --git a/tools/testing/selftests/net/mptcp/Makefile b/tools/testing/selftests/net/mptcp/Makefile
index 22ba0da2adb8..e2325d49228f 100644
--- a/tools/testing/selftests/net/mptcp/Makefile
+++ b/tools/testing/selftests/net/mptcp/Makefile
@@ -21,6 +21,7 @@ TEST_PROGS := \
TEST_GEN_FILES := \
mptcp_connect \
+ mptcp_connect_retry \
mptcp_diag \
mptcp_inq \
mptcp_sockopt \
diff --git a/tools/testing/selftests/net/mptcp/mptcp_connect_retry.c b/tools/testing/selftests/net/mptcp/mptcp_connect_retry.c
new file mode 100644
index 000000000000..a034d2ad73f7
--- /dev/null
+++ b/tools/testing/selftests/net/mptcp/mptcp_connect_retry.c
@@ -0,0 +1,216 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Same-fd MPTCP connect() retry helper.
+ *
+ * Optionally fails an initial connect() with the wrong address family, then
+ * connects to the given IPv4 destination and checks MPTCP_INFO flags.
+ */
+
+#define _GNU_SOURCE
+
+#include <errno.h>
+#include <fcntl.h>
+#include <getopt.h>
+#include <netinet/in.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <arpa/inet.h>
+#include <sys/socket.h>
+
+#include <linux/mptcp.h>
+
+#ifndef IPPROTO_MPTCP
+#define IPPROTO_MPTCP 262
+#endif
+#ifndef SOL_MPTCP
+#define SOL_MPTCP 284
+#endif
+
+static void die_perror(const char *msg)
+{
+ perror(msg);
+ exit(1);
+}
+
+static void usage(const char *argv0)
+{
+ fprintf(stderr,
+ "Usage: %s [-e] [-c] <ipv4> <port>\n"
+ " -e first connect() with a wrong sin_family (EAFNOSUPPORT)\n"
+ " -c clear net.mptcp.blackhole_timeout after the failed connect\n",
+ argv0);
+ exit(1);
+}
+
+static int getsockopt_mptcp_info(int fd, struct mptcp_info *info)
+{
+ socklen_t olen = sizeof(*info);
+
+ memset(info, 0, sizeof(*info));
+ return getsockopt(fd, SOL_MPTCP, MPTCP_INFO, info, &olen);
+}
+
+static int get_mptcp_info(int fd, struct mptcp_info *info)
+{
+ if (!getsockopt_mptcp_info(fd, info))
+ return 0;
+
+ /* Fallback sockets forward SOL_MPTCP to TCP. */
+ if (errno == EOPNOTSUPP) {
+ info->mptcpi_flags = MPTCP_INFO_FLAG_FALLBACK;
+ return 0;
+ }
+ return -1;
+}
+
+static int clear_blackhole_timeout(void)
+{
+ ssize_t n;
+ int fd;
+
+ fd = open("/proc/sys/net/mptcp/blackhole_timeout", O_WRONLY);
+ if (fd < 0)
+ return -1;
+
+ n = write(fd, "0\n", 2);
+ close(fd);
+ return n == 2 ? 0 : -1;
+}
+
+static int connect_wrong_family(int fd)
+{
+ struct sockaddr_in addr = {
+ .sin_family = AF_INET6,
+ };
+
+ if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) == 0) {
+ fprintf(stderr, "wrong-family connect() unexpectedly succeeded\n");
+ return -1;
+ }
+ if (errno != EAFNOSUPPORT) {
+ fprintf(stderr, "wrong-family connect(): unexpected errno %d (%s)\n",
+ errno, strerror(errno));
+ return -1;
+ }
+ return 0;
+}
+
+static int connect_ipv4(int fd, const char *ip, unsigned short port)
+{
+ struct sockaddr_in addr = {
+ .sin_family = AF_INET,
+ .sin_port = htons(port),
+ };
+
+ if (inet_pton(AF_INET, ip, &addr.sin_addr) != 1) {
+ fprintf(stderr, "invalid IPv4 address %s\n", ip);
+ return -1;
+ }
+ if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
+ fprintf(stderr, "connect(%s:%u): %s\n", ip, port, strerror(errno));
+ return -1;
+ }
+ return 0;
+}
+
+int main(int argc, char **argv)
+{
+ bool fail_first = false, clear_bh = false;
+ struct mptcp_info after_fail = { 0 }, after_ok, probe;
+ bool fail_fb = false, ok_fb, ok_key;
+ const char *ip;
+ unsigned short port;
+ int fd, opt, err = 0;
+ char buf[128];
+ ssize_t n;
+
+ while ((opt = getopt(argc, argv, "ec")) != -1) {
+ switch (opt) {
+ case 'e':
+ fail_first = true;
+ break;
+ case 'c':
+ clear_bh = true;
+ break;
+ default:
+ usage(argv[0]);
+ }
+ }
+
+ if (optind + 2 != argc)
+ usage(argv[0]);
+
+ ip = argv[optind];
+ port = atoi(argv[optind + 1]);
+ if (!port)
+ usage(argv[0]);
+
+ fd = socket(AF_INET, SOCK_STREAM, IPPROTO_MPTCP);
+ if (fd < 0)
+ die_perror("socket(IPPROTO_MPTCP)");
+
+ if (getsockopt_mptcp_info(fd, &probe)) {
+ fprintf(stderr, "getsockopt(MPTCP_INFO): %s\n", strerror(errno));
+ close(fd);
+ return 2;
+ }
+
+ if (fail_first && connect_wrong_family(fd)) {
+ close(fd);
+ return 1;
+ }
+
+ if (fail_first) {
+ if (get_mptcp_info(fd, &after_fail)) {
+ fprintf(stderr, "getsockopt(MPTCP_INFO) after fail: %s\n",
+ strerror(errno));
+ close(fd);
+ return 2;
+ }
+ fail_fb = after_fail.mptcpi_flags & MPTCP_INFO_FLAG_FALLBACK;
+ }
+
+ if (clear_bh && clear_blackhole_timeout()) {
+ fprintf(stderr, "unable to clear blackhole_timeout: %s\n",
+ strerror(errno));
+ close(fd);
+ return 1;
+ }
+
+ if (connect_ipv4(fd, ip, port)) {
+ close(fd);
+ return 1;
+ }
+
+ if (get_mptcp_info(fd, &after_ok)) {
+ fprintf(stderr, "getsockopt(MPTCP_INFO) after connect: %s\n",
+ strerror(errno));
+ close(fd);
+ return 2;
+ }
+
+ if (write(fd, "retry\n", 6) < 0)
+ perror("write");
+ shutdown(fd, SHUT_WR);
+ do {
+ n = read(fd, buf, sizeof(buf));
+ } while (n > 0);
+ close(fd);
+
+ ok_fb = after_ok.mptcpi_flags & MPTCP_INFO_FLAG_FALLBACK;
+ ok_key = after_ok.mptcpi_flags & MPTCP_INFO_FLAG_REMOTE_KEY_RECEIVED;
+
+ if (fail_fb) {
+ fprintf(stderr, "fallback still set after failed connect()\n");
+ err = 1;
+ }
+ if (ok_fb || !ok_key) {
+ fprintf(stderr, "second connect() did not complete MPTCP handshake\n");
+ err = 1;
+ }
+
+ return err;
+}
diff --git a/tools/testing/selftests/net/mptcp/mptcp_join.sh b/tools/testing/selftests/net/mptcp/mptcp_join.sh
index 18ce7136a2b0..dd7984414747 100755
--- a/tools/testing/selftests/net/mptcp/mptcp_join.sh
+++ b/tools/testing/selftests/net/mptcp/mptcp_join.sh
@@ -3876,6 +3876,113 @@ fail_tests()
fi
}
+# $1: ns ; $2: addr ; $3: port
+start_mptcp_listener()
+{
+ local ns="${1}"
+ local addr="${2}"
+ local port="${3}"
+ local pid
+
+ ip netns exec "${ns}" ./mptcp_connect -t ${timeout_poll} -l -p "${port}" \
+ -s MPTCP "${addr}" < "${sin}" > "${sout}" &
+ pid=$!
+ mptcp_lib_wait_local_port_listen "${ns}" "${port}"
+ echo "${pid}"
+}
+
+# Drop MP_CAPABLE SYNs until the client netns records a blackhole.
+trigger_mptcp_blackhole()
+{
+ local port spid rc=0
+ local count
+
+ print_check "trigger blackhole"
+
+ ip netns exec $ns2 sysctl -q net.mptcp.syn_retrans_before_tcp_fallback=0
+
+ if ! ip netns exec $ns2 ${iptables} -A OUTPUT -p tcp \
+ -m tcp --tcp-option 30 -j DROP; then
+ mark_as_skipped "unable to drop MP_CAPABLE SYNs"
+ return 1
+ fi
+
+ port=$(get_port)
+ spid=$(start_mptcp_listener "$ns1" 10.0.1.1 "${port}")
+
+ timeout ${timeout_test} ip netns exec $ns2 ./mptcp_connect \
+ -t ${timeout_poll} -p "${port}" -s MPTCP 10.0.1.1 \
+ < "$cin" > "$cout" || rc=$?
+ wait "${spid}" 2>/dev/null || true
+
+ ip netns exec $ns2 ${iptables} -D OUTPUT -p tcp \
+ -m tcp --tcp-option 30 -j DROP 2>/dev/null || true
+
+ if [ ${rc} -ne 0 ]; then
+ fail_test "blackhole helper connect failed (rc=${rc})"
+ return 1
+ fi
+
+ count=$(mptcp_lib_get_counter $ns2 "MPTcpExtBlackhole")
+ if [ "${count:-0}" -lt 1 ]; then
+ fail_test "got ${count:-0} Blackhole event(s) expected >= 1"
+ return 1
+ fi
+
+ print_ok
+ return 0
+}
+
+connect_retry_tests()
+{
+ # failed connect without fallback, then retry
+ if reset "retry connect after failed connect"; then
+ local port spid rc
+
+ port=$(get_port)
+ spid=$(start_mptcp_listener "$ns1" 10.0.1.1 "${port}")
+
+ print_check "MPTCP after failed connect"
+ ip netns exec $ns2 ./mptcp_connect_retry -e 10.0.1.1 "${port}"
+ rc=$?
+ wait "${spid}" 2>/dev/null || true
+
+ if [ ${rc} -eq 2 ]; then
+ mark_as_skipped "MPTCP_INFO not available"
+ elif [ ${rc} -ne 0 ]; then
+ fail_test "retry connect without fallback failed (rc=${rc})"
+ else
+ print_ok
+ fi
+ fi
+
+ # early fallback + failed connect, then retry with blackhole disabled
+ if reset_check_counter "retry connect after early fallback fail" \
+ "MPTcpExtBlackhole"; then
+ local port spid rc
+
+ if ! trigger_mptcp_blackhole; then
+ return
+ fi
+
+ port=$(get_port)
+ spid=$(start_mptcp_listener "$ns1" 10.0.1.1 "${port}")
+
+ print_check "MPTCP after sticky fallback fail"
+ ip netns exec $ns2 ./mptcp_connect_retry -e -c 10.0.1.1 "${port}"
+ rc=$?
+ wait "${spid}" 2>/dev/null || true
+
+ if [ ${rc} -eq 2 ]; then
+ mark_as_skipped "MPTCP_INFO not available"
+ elif [ ${rc} -ne 0 ]; then
+ fail_test "sticky fallback survived failed connect() (rc=${rc})"
+ else
+ print_ok
+ fi
+ fi
+}
+
# $1: ns ; $2: addr ; $3: id
userspace_pm_add_addr()
{
@@ -4617,6 +4724,7 @@ all_tests_sorted=(
m@fullmesh_tests
z@fastclose_tests
F@fail_tests
+ n@connect_retry_tests
u@userspace_tests
I@endpoint_tests
)
--
2.55.0
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCH mptcp-net v2 2/2] selftests: mptcp: join: retry connect after early fallback
@ 2026-09-11 15:42 ` Quanye Yang
0 siblings, 0 replies; 10+ messages in thread
From: Quanye Yang @ 2026-09-11 15:42 UTC (permalink / raw)
To: mptcp
Cover leftover msk state when connect() fails before
SS_CONNECTING. Existing mptcp_connect tests always use a fresh
socket, so they cannot see a sticky fallback on the same fd.
Add a small helper that optionally issues a wrong-family connect()
on an MPTCP socket, then connects to a real IPv4 destination and
checks MPTCP_INFO. A new mptcp_join.sh group (-n) runs two cases:
- a failed connect without early fallback must not poison the retry
- after a blackhole-driven early fallback plus a failed connect,
clearing blackhole_timeout, the same fd must complete MP_CAPABLE
again
Signed-off-by: Quanye Yang <quanyeyang@proton.me>
---
tools/testing/selftests/net/mptcp/.gitignore | 1 +
tools/testing/selftests/net/mptcp/Makefile | 1 +
.../selftests/net/mptcp/mptcp_connect_retry.c | 216 +++++++++++++++++++++
tools/testing/selftests/net/mptcp/mptcp_join.sh | 108 +++++++++++
4 files changed, 326 insertions(+)
diff --git a/tools/testing/selftests/net/mptcp/.gitignore b/tools/testing/selftests/net/mptcp/.gitignore
index 833279fb34e2..5ea8f3e4985f 100644
--- a/tools/testing/selftests/net/mptcp/.gitignore
+++ b/tools/testing/selftests/net/mptcp/.gitignore
@@ -1,5 +1,6 @@
# SPDX-License-Identifier: GPL-2.0-only
mptcp_connect
+mptcp_connect_retry
mptcp_diag
mptcp_inq
mptcp_sockopt
diff --git a/tools/testing/selftests/net/mptcp/Makefile b/tools/testing/selftests/net/mptcp/Makefile
index 22ba0da2adb8..e2325d49228f 100644
--- a/tools/testing/selftests/net/mptcp/Makefile
+++ b/tools/testing/selftests/net/mptcp/Makefile
@@ -21,6 +21,7 @@ TEST_PROGS := \
TEST_GEN_FILES := \
mptcp_connect \
+ mptcp_connect_retry \
mptcp_diag \
mptcp_inq \
mptcp_sockopt \
diff --git a/tools/testing/selftests/net/mptcp/mptcp_connect_retry.c b/tools/testing/selftests/net/mptcp/mptcp_connect_retry.c
new file mode 100644
index 000000000000..a034d2ad73f7
--- /dev/null
+++ b/tools/testing/selftests/net/mptcp/mptcp_connect_retry.c
@@ -0,0 +1,216 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Same-fd MPTCP connect() retry helper.
+ *
+ * Optionally fails an initial connect() with the wrong address family, then
+ * connects to the given IPv4 destination and checks MPTCP_INFO flags.
+ */
+
+#define _GNU_SOURCE
+
+#include <errno.h>
+#include <fcntl.h>
+#include <getopt.h>
+#include <netinet/in.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <arpa/inet.h>
+#include <sys/socket.h>
+
+#include <linux/mptcp.h>
+
+#ifndef IPPROTO_MPTCP
+#define IPPROTO_MPTCP 262
+#endif
+#ifndef SOL_MPTCP
+#define SOL_MPTCP 284
+#endif
+
+static void die_perror(const char *msg)
+{
+ perror(msg);
+ exit(1);
+}
+
+static void usage(const char *argv0)
+{
+ fprintf(stderr,
+ "Usage: %s [-e] [-c] <ipv4> <port>\n"
+ " -e first connect() with a wrong sin_family (EAFNOSUPPORT)\n"
+ " -c clear net.mptcp.blackhole_timeout after the failed connect\n",
+ argv0);
+ exit(1);
+}
+
+static int getsockopt_mptcp_info(int fd, struct mptcp_info *info)
+{
+ socklen_t olen = sizeof(*info);
+
+ memset(info, 0, sizeof(*info));
+ return getsockopt(fd, SOL_MPTCP, MPTCP_INFO, info, &olen);
+}
+
+static int get_mptcp_info(int fd, struct mptcp_info *info)
+{
+ if (!getsockopt_mptcp_info(fd, info))
+ return 0;
+
+ /* Fallback sockets forward SOL_MPTCP to TCP. */
+ if (errno == EOPNOTSUPP) {
+ info->mptcpi_flags = MPTCP_INFO_FLAG_FALLBACK;
+ return 0;
+ }
+ return -1;
+}
+
+static int clear_blackhole_timeout(void)
+{
+ ssize_t n;
+ int fd;
+
+ fd = open("/proc/sys/net/mptcp/blackhole_timeout", O_WRONLY);
+ if (fd < 0)
+ return -1;
+
+ n = write(fd, "0\n", 2);
+ close(fd);
+ return n == 2 ? 0 : -1;
+}
+
+static int connect_wrong_family(int fd)
+{
+ struct sockaddr_in addr = {
+ .sin_family = AF_INET6,
+ };
+
+ if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) == 0) {
+ fprintf(stderr, "wrong-family connect() unexpectedly succeeded\n");
+ return -1;
+ }
+ if (errno != EAFNOSUPPORT) {
+ fprintf(stderr, "wrong-family connect(): unexpected errno %d (%s)\n",
+ errno, strerror(errno));
+ return -1;
+ }
+ return 0;
+}
+
+static int connect_ipv4(int fd, const char *ip, unsigned short port)
+{
+ struct sockaddr_in addr = {
+ .sin_family = AF_INET,
+ .sin_port = htons(port),
+ };
+
+ if (inet_pton(AF_INET, ip, &addr.sin_addr) != 1) {
+ fprintf(stderr, "invalid IPv4 address %s\n", ip);
+ return -1;
+ }
+ if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
+ fprintf(stderr, "connect(%s:%u): %s\n", ip, port, strerror(errno));
+ return -1;
+ }
+ return 0;
+}
+
+int main(int argc, char **argv)
+{
+ bool fail_first = false, clear_bh = false;
+ struct mptcp_info after_fail = { 0 }, after_ok, probe;
+ bool fail_fb = false, ok_fb, ok_key;
+ const char *ip;
+ unsigned short port;
+ int fd, opt, err = 0;
+ char buf[128];
+ ssize_t n;
+
+ while ((opt = getopt(argc, argv, "ec")) != -1) {
+ switch (opt) {
+ case 'e':
+ fail_first = true;
+ break;
+ case 'c':
+ clear_bh = true;
+ break;
+ default:
+ usage(argv[0]);
+ }
+ }
+
+ if (optind + 2 != argc)
+ usage(argv[0]);
+
+ ip = argv[optind];
+ port = atoi(argv[optind + 1]);
+ if (!port)
+ usage(argv[0]);
+
+ fd = socket(AF_INET, SOCK_STREAM, IPPROTO_MPTCP);
+ if (fd < 0)
+ die_perror("socket(IPPROTO_MPTCP)");
+
+ if (getsockopt_mptcp_info(fd, &probe)) {
+ fprintf(stderr, "getsockopt(MPTCP_INFO): %s\n", strerror(errno));
+ close(fd);
+ return 2;
+ }
+
+ if (fail_first && connect_wrong_family(fd)) {
+ close(fd);
+ return 1;
+ }
+
+ if (fail_first) {
+ if (get_mptcp_info(fd, &after_fail)) {
+ fprintf(stderr, "getsockopt(MPTCP_INFO) after fail: %s\n",
+ strerror(errno));
+ close(fd);
+ return 2;
+ }
+ fail_fb = after_fail.mptcpi_flags & MPTCP_INFO_FLAG_FALLBACK;
+ }
+
+ if (clear_bh && clear_blackhole_timeout()) {
+ fprintf(stderr, "unable to clear blackhole_timeout: %s\n",
+ strerror(errno));
+ close(fd);
+ return 1;
+ }
+
+ if (connect_ipv4(fd, ip, port)) {
+ close(fd);
+ return 1;
+ }
+
+ if (get_mptcp_info(fd, &after_ok)) {
+ fprintf(stderr, "getsockopt(MPTCP_INFO) after connect: %s\n",
+ strerror(errno));
+ close(fd);
+ return 2;
+ }
+
+ if (write(fd, "retry\n", 6) < 0)
+ perror("write");
+ shutdown(fd, SHUT_WR);
+ do {
+ n = read(fd, buf, sizeof(buf));
+ } while (n > 0);
+ close(fd);
+
+ ok_fb = after_ok.mptcpi_flags & MPTCP_INFO_FLAG_FALLBACK;
+ ok_key = after_ok.mptcpi_flags & MPTCP_INFO_FLAG_REMOTE_KEY_RECEIVED;
+
+ if (fail_fb) {
+ fprintf(stderr, "fallback still set after failed connect()\n");
+ err = 1;
+ }
+ if (ok_fb || !ok_key) {
+ fprintf(stderr, "second connect() did not complete MPTCP handshake\n");
+ err = 1;
+ }
+
+ return err;
+}
diff --git a/tools/testing/selftests/net/mptcp/mptcp_join.sh b/tools/testing/selftests/net/mptcp/mptcp_join.sh
index 18ce7136a2b0..dd7984414747 100755
--- a/tools/testing/selftests/net/mptcp/mptcp_join.sh
+++ b/tools/testing/selftests/net/mptcp/mptcp_join.sh
@@ -3876,6 +3876,113 @@ fail_tests()
fi
}
+# $1: ns ; $2: addr ; $3: port
+start_mptcp_listener()
+{
+ local ns="${1}"
+ local addr="${2}"
+ local port="${3}"
+ local pid
+
+ ip netns exec "${ns}" ./mptcp_connect -t ${timeout_poll} -l -p "${port}" \
+ -s MPTCP "${addr}" < "${sin}" > "${sout}" &
+ pid=$!
+ mptcp_lib_wait_local_port_listen "${ns}" "${port}"
+ echo "${pid}"
+}
+
+# Drop MP_CAPABLE SYNs until the client netns records a blackhole.
+trigger_mptcp_blackhole()
+{
+ local port spid rc=0
+ local count
+
+ print_check "trigger blackhole"
+
+ ip netns exec $ns2 sysctl -q net.mptcp.syn_retrans_before_tcp_fallback=0
+
+ if ! ip netns exec $ns2 ${iptables} -A OUTPUT -p tcp \
+ -m tcp --tcp-option 30 -j DROP; then
+ mark_as_skipped "unable to drop MP_CAPABLE SYNs"
+ return 1
+ fi
+
+ port=$(get_port)
+ spid=$(start_mptcp_listener "$ns1" 10.0.1.1 "${port}")
+
+ timeout ${timeout_test} ip netns exec $ns2 ./mptcp_connect \
+ -t ${timeout_poll} -p "${port}" -s MPTCP 10.0.1.1 \
+ < "$cin" > "$cout" || rc=$?
+ wait "${spid}" 2>/dev/null || true
+
+ ip netns exec $ns2 ${iptables} -D OUTPUT -p tcp \
+ -m tcp --tcp-option 30 -j DROP 2>/dev/null || true
+
+ if [ ${rc} -ne 0 ]; then
+ fail_test "blackhole helper connect failed (rc=${rc})"
+ return 1
+ fi
+
+ count=$(mptcp_lib_get_counter $ns2 "MPTcpExtBlackhole")
+ if [ "${count:-0}" -lt 1 ]; then
+ fail_test "got ${count:-0} Blackhole event(s) expected >= 1"
+ return 1
+ fi
+
+ print_ok
+ return 0
+}
+
+connect_retry_tests()
+{
+ # failed connect without fallback, then retry
+ if reset "retry connect after failed connect"; then
+ local port spid rc
+
+ port=$(get_port)
+ spid=$(start_mptcp_listener "$ns1" 10.0.1.1 "${port}")
+
+ print_check "MPTCP after failed connect"
+ ip netns exec $ns2 ./mptcp_connect_retry -e 10.0.1.1 "${port}"
+ rc=$?
+ wait "${spid}" 2>/dev/null || true
+
+ if [ ${rc} -eq 2 ]; then
+ mark_as_skipped "MPTCP_INFO not available"
+ elif [ ${rc} -ne 0 ]; then
+ fail_test "retry connect without fallback failed (rc=${rc})"
+ else
+ print_ok
+ fi
+ fi
+
+ # early fallback + failed connect, then retry with blackhole disabled
+ if reset_check_counter "retry connect after early fallback fail" \
+ "MPTcpExtBlackhole"; then
+ local port spid rc
+
+ if ! trigger_mptcp_blackhole; then
+ return
+ fi
+
+ port=$(get_port)
+ spid=$(start_mptcp_listener "$ns1" 10.0.1.1 "${port}")
+
+ print_check "MPTCP after sticky fallback fail"
+ ip netns exec $ns2 ./mptcp_connect_retry -e -c 10.0.1.1 "${port}"
+ rc=$?
+ wait "${spid}" 2>/dev/null || true
+
+ if [ ${rc} -eq 2 ]; then
+ mark_as_skipped "MPTCP_INFO not available"
+ elif [ ${rc} -ne 0 ]; then
+ fail_test "sticky fallback survived failed connect() (rc=${rc})"
+ else
+ print_ok
+ fi
+ fi
+}
+
# $1: ns ; $2: addr ; $3: id
userspace_pm_add_addr()
{
@@ -4617,6 +4724,7 @@ all_tests_sorted=(
m@fullmesh_tests
z@fastclose_tests
F@fail_tests
+ n@connect_retry_tests
u@userspace_tests
I@endpoint_tests
)
--
2.55.0
^ permalink raw reply related [flat|nested] 10+ messages in thread
* Re: [PATCH mptcp-net v2 0/2] mptcp: fix leftover fallback after failed connect
2026-09-11 15:42 ` Quanye Yang
` (2 preceding siblings ...)
(?)
@ 2026-09-11 16:41 ` MPTCP CI
-1 siblings, 0 replies; 10+ messages in thread
From: MPTCP CI @ 2026-09-11 16:41 UTC (permalink / raw)
To: Quanye Yang; +Cc: mptcp
Hi Quanye,
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): Unstable: 1 failed test(s): selftest_mptcp_join ⚠️
- KVM Validation: debug (except selftest_mptcp_join): Success! ✅
- KVM Validation: debug (only selftest_mptcp_join): Unstable: 1 failed test(s): selftest_mptcp_join ⚠️
- KVM Validation: btf-normal (only bpftest_all): Success! ✅
- KVM Validation: btf-debug (only bpftest_all): Success! ✅
- Perf: Success! ✅
- Task: https://github.com/multipath-tcp/mptcp_net-next/actions/runs/34619136010
Initiator: Patchew Applier
Commits: https://github.com/multipath-tcp/mptcp_net-next/commits/443443b1ce28
Patchwork: https://patchwork.kernel.org/project/mptcp/list/?series=1163060
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] 10+ messages in thread
* Re: [PATCH mptcp-net v2 2/2] selftests: mptcp: join: retry connect after early fallback
2026-09-11 15:42 ` Quanye Yang
(?)
@ 2026-09-11 17:20 ` Matthieu Baerts
2026-09-12 8:42 ` quanyeyang
-1 siblings, 1 reply; 10+ messages in thread
From: Matthieu Baerts @ 2026-09-11 17:20 UTC (permalink / raw)
To: quanyeyang, mptcp
Hi Quanye,
On 11/09/2026 17:42, Quanye Yang via B4 Relay wrote:
> From: Quanye Yang <quanyeyang@proton.me>
>
> Cover leftover msk state when connect() fails before
> SS_CONNECTING. Existing mptcp_connect tests always use a fresh
> socket, so they cannot see a sticky fallback on the same fd.
>
> Add a small helper that optionally issues a wrong-family connect()
> on an MPTCP socket, then connects to a real IPv4 destination and
> checks MPTCP_INFO. A new mptcp_join.sh group (-n) runs two cases:
>
> - a failed connect without early fallback must not poison the retry
> - after a blackhole-driven early fallback plus a failed connect,
> clearing blackhole_timeout, the same fd must complete MP_CAPABLE
> again
Thank you for this test, but we cannot accept this: it is good to have a
test linked to a fix or a feature, but it has to be maintainable. If
each fix/feature adds 300+ LoC, that's unmaintainable (or LLM become
mandatory for that, but that's not what we want: we still need to be
able to read the test).
Ideally:
- create a small packetdrill test instead, using the MPTCP version [1]
- not everything can be tested with packetdrill (even if it can also be
extended) and adding code in the selftests is OK, but, if possible, it
should reuse the existing tools and helpers
So in this case here: can you have a packetdrill test instead? It should
be possible, no? If not, can you only use 'mptcp_connect' and the
existing helpers from mptcp_join.sh?
Also, mptcp_join.sh is to validate cases with multiple subflows. I don't
think you need that, right?
[1] https://github.com/multipath-tcp/packetdrill/
Cheers,
Matt
--
Sponsored by the NGI0 Core fund.
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [PATCH mptcp-net v2 2/2] selftests: mptcp: join: retry connect after early fallback
2026-09-11 17:20 ` Matthieu Baerts
@ 2026-09-12 8:42 ` quanyeyang
2026-09-12 9:15 ` Matthieu Baerts
0 siblings, 1 reply; 10+ messages in thread
From: quanyeyang @ 2026-09-12 8:42 UTC (permalink / raw)
To: Matthieu Baerts; +Cc: mptcp
On Friday, September 11th, 2026 at AM 10:20, Matthieu Baerts <matttbe@kernel.org> wrote:
> Thank you for this test, but we cannot accept this: it is good to have a
> test linked to a fix or a feature, but it has to be maintainable. If
> each fix/feature adds 300+ LoC, that's unmaintainable (or LLM become
> mandatory for that, but that's not what we want: we still need to be
> able to read the test).
>
> Ideally:
>
> - create a small packetdrill test instead, using the MPTCP version [1]
>
> - not everything can be tested with packetdrill (even if it can also be
> extended) and adding code in the selftests is OK, but, if possible, it
> should reuse the existing tools and helpers
>
> So in this case here: can you have a packetdrill test instead? It should
> be possible, no? If not, can you only use 'mptcp_connect' and the
> existing helpers from mptcp_join.sh?
>
> Also, mptcp_join.sh is to validate cases with multiple subflows. I don't
> think you need that, right?
>
> [1] https://github.com/multipath-tcp/packetdrill/
>
> Cheers,
> Matt
> --
> Sponsored by the NGI0 Core fund.
>
>
Thanks for the detailed guidance.
Agreed, 2/2 is too heavy. I will drop it and try to add a small packetdrill
script under gtests/net/mptcp/regressions/ instead: record a blackhole,
then fail connect() on the same fd before SS_CONNECTING, clear
blackhole_timeout, and check the retry still sends MP_CAPABLE
(TCP_IS_MPTCP / the 3rd ACK).
The kernel fix in v2 1/2 does not depend on that selftest. Could you
apply that one on its own?
Thanks,
Quanye
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [PATCH mptcp-net v2 2/2] selftests: mptcp: join: retry connect after early fallback
2026-09-12 8:42 ` quanyeyang
@ 2026-09-12 9:15 ` Matthieu Baerts
0 siblings, 0 replies; 10+ messages in thread
From: Matthieu Baerts @ 2026-09-12 9:15 UTC (permalink / raw)
To: quanyeyang; +Cc: mptcp
On 12/09/2026 10:42, quanyeyang wrote:
> On Friday, September 11th, 2026 at AM 10:20, Matthieu Baerts <matttbe@kernel.org> wrote:
>
>> Thank you for this test, but we cannot accept this: it is good to have a
>> test linked to a fix or a feature, but it has to be maintainable. If
>> each fix/feature adds 300+ LoC, that's unmaintainable (or LLM become
>> mandatory for that, but that's not what we want: we still need to be
>> able to read the test).
>>
>> Ideally:
>>
>> - create a small packetdrill test instead, using the MPTCP version [1]
>>
>> - not everything can be tested with packetdrill (even if it can also be
>> extended) and adding code in the selftests is OK, but, if possible, it
>> should reuse the existing tools and helpers
>>
>> So in this case here: can you have a packetdrill test instead? It should
>> be possible, no? If not, can you only use 'mptcp_connect' and the
>> existing helpers from mptcp_join.sh?
>>
>> Also, mptcp_join.sh is to validate cases with multiple subflows. I don't
>> think you need that, right?
>>
>> [1] https://github.com/multipath-tcp/packetdrill/
>>
>> Cheers,
>> Matt
>> --
>> Sponsored by the NGI0 Core fund.
>>
>>
> Thanks for the detailed guidance.
> Agreed, 2/2 is too heavy. I will drop it and try to add a small packetdrill
> script under gtests/net/mptcp/regressions/ instead: record a blackhole,
> then fail connect() on the same fd before SS_CONNECTING, clear
> blackhole_timeout, and check the retry still sends MP_CAPABLE
> (TCP_IS_MPTCP / the 3rd ACK).
Thanks, I think it should be easier and clearer to do that with packetdrill.
> The kernel fix in v2 1/2 does not depend on that selftest. Could you
> apply that one on its own?
I can, but I prefer to have the new test first, to validate it with and
without the modification.
In other words, no need to resend patch 1.
Cheers,
Matt
--
Sponsored by the NGI0 Core fund.
^ permalink raw reply [flat|nested] 10+ messages in thread
end of thread, other threads:[~2026-09-12 9:15 UTC | newest]
Thread overview: 10+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-11 15:42 [PATCH mptcp-net v2 0/2] mptcp: fix leftover fallback after failed connect Quanye Yang via B4 Relay
2026-09-11 15:42 ` Quanye Yang
2026-09-11 15:42 ` [PATCH mptcp-net v2 1/2] mptcp: reset msk state on early connect failure Quanye Yang via B4 Relay
2026-09-11 15:42 ` Quanye Yang
2026-09-11 15:42 ` [PATCH mptcp-net v2 2/2] selftests: mptcp: join: retry connect after early fallback Quanye Yang via B4 Relay
2026-09-11 15:42 ` Quanye Yang
2026-09-11 17:20 ` Matthieu Baerts
2026-09-12 8:42 ` quanyeyang
2026-09-12 9:15 ` Matthieu Baerts
2026-09-11 16:41 ` [PATCH mptcp-net v2 0/2] mptcp: fix leftover fallback after failed connect 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.