* [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 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
* 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
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.