mptcp: MSG_ERRQUEUE support on the parent socket
Subsystems (3)
v5 cover mptcp: MSG_ERRQUEUE support on the parent socket
MPTCP already advertises IP_RECVERR/IPV6_RECVERR as supported, but the parent socket does not currently provide usable MSG_ERRQUEUE handling.
This series wires the MPTCP socket up to the IPv4/IPv6 error queue paths. It propagates RECVERR-related sockopts to existing and future subflows, and lets recvmsg(MSG_ERRQUEUE) on the parent socket consume queued TX-timestamp and MSG_ZEROCOPY completion notifications.
A prerequisite patch factors the per-flag inet_flags propagation in sync_socket_options() into a mask-driven loop, so further inet_flags propagated by MPTCP can be added by extending the mask rather than touching the call site.
Patch 2 leverages mptcp_setsockopt_all_sf() for the setsockopt path and extends MPTCP_INET_FLAGS_MASK with the four RECVERR bits, with a single mptcp_setsockopt_recverr() helper covering both families.
Patch 3 splices subflow err skbs onto the parent at error-report time, filtering by SO_EE_ORIGIN so user-data cmsgs (TIMESTAMPING, ZEROCOPY, LOCAL) reach the parent socket while subflow-level ICMP errors are dropped to avoid leaking subflow identity through the single-path RECVERR ABI. A future MPTCP_RECERR channel is the right home for those events along with the per-fd subflow lifecycle events tracked by [1].
Changes in v5:
- 1/4: replace the WRITE_ONCE() RMW with a per-bit assign_bit() loop so the per-bit atomicity of the original inet_assign_bit() calls is preserved (Sashiko).
- 2/4: collapse the family-specific helpers into one mptcp_setsockopt_recverr() that snapshots optval into a local int, bumps msk->setsockopt_seq, and forwards via mptcp_setsockopt_all_sf() (Matthieu, Sashiko); skip family-mismatched subflows in mptcp_setsockopt_all_sf() (Sashiko).
- 3/4: filter the splice by SO_EE_ORIGIN to forward TIMESTAMPING / ZEROCOPY / LOCAL only and drop ICMP / ICMPv6 (Matthieu, Paolo); add mptcp_recv_error() to retry the splice on the pull side so a parent-side rmem-ENOMEM does not strand subflow skbs (Sashiko).
- 4/4: unchanged.
[1] https://github.com/multipath-tcp/mptcp_net-next/issues/78
David Carlier (4):
mptcp: sockopt: factor inet_flags propagation into a mask
mptcp: propagate RECVERR sockopts to subflows
mptcp: support MSG_ERRQUEUE on the parent socket
selftests: mptcp: cover IP_RECVERR sockopt propagation
net/mptcp/protocol.c | 66 ++++++++++- net/mptcp/sockopt.c | 108 ++++++++++++++---- .../selftests/net/mptcp/mptcp_sockopt.c | 55 +++++++++ 3 files changed, 203 insertions(+), 26 deletions(-)
Signature · 1 line
2.53.0
PATCH v5 1/4 mptcp: sockopt: factor inet_flags propagation into a mask
Introduce MPTCP_INET_FLAGS_MASK and replace the per-flag inet_assign_bit() calls in sync_socket_options() with a loop driven by the mask that calls assign_bit() per set bit, preserving the per-bit atomicity of the original. Further flags propagated by MPTCP can be added by extending the mask rather than touching the call site.
No functional change.
--- net/mptcp/sockopt.c | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-)
net/mptcp/sockopt.c +12 -3
@@ -16,6 +16,10 @@#define MIN_INFO_OPTLEN_SIZE 16#define MIN_FULL_INFO_OPTLEN_SIZE 40 +#define MPTCP_INET_FLAGS_MASK \ + (BIT(INET_FLAGS_TRANSPARENT) | \ + BIT(INET_FLAGS_FREEBIND) | \ + BIT(INET_FLAGS_BIND_ADDRESS_NO_PORT))static struct sock *__mptcp_tcp_fallback (struct mptcp_sock *msk) {
@@ -1536,6 +1540,9 @@ static void sync_socket_options(struct mptcp_sock *msk, struct sock *ssk) {static const unsigned int tx_rx_locks =SOCK_RCVBUF_LOCK |SOCK_SNDBUF_LOCK ;struct sock *sk = (struct sock *)msk; +unsigned long mask =MPTCP_INET_FLAGS_MASK ; +unsigned long src; +int b;bool keep_open; keep_open =sock_flag (sk,SOCK_KEEPOPEN );
@@ -1582,9 +1589,11 @@ static void sync_socket_options(struct mptcp_sock *msk, struct sock *ssk)tcp_sock_set_keepcnt (ssk, msk->keepalive_cnt);tcp_sock_set_maxseg (ssk, msk->maxseg); -inet_assign_bit (TRANSPARENT , ssk,inet_test_bit (TRANSPARENT , sk)); -inet_assign_bit (FREEBIND , ssk,inet_test_bit (FREEBIND , sk)); -inet_assign_bit (BIND_ADDRESS_NO_PORT , ssk,inet_test_bit (BIND_ADDRESS_NO_PORT , sk)); + src =READ_ONCE (inet_sk (sk)->inet_flags); + +for_each_set_bit (b, &mask,BITS_PER_LONG ) +assign_bit (b, &inet_sk (ssk)->inet_flags, src &BIT (b)); +WRITE_ONCE (inet_sk (ssk)->local_port_range,READ_ONCE (inet_sk (sk)->local_port_range)); }
Signature · 1 line
2.53.0
PATCH v5 2/4 mptcp: propagate RECVERR sockopts to subflows
In reply to David CarlierPropagate IP_RECVERR/IP_RECVERR_RFC4884 and IPV6_RECVERR/IPV6_RECVERR_RFC4884 from the MPTCP socket to existing and future subflows.
mptcp_setsockopt_recverr() snapshots optval into a local int, applies it to the parent socket via ip_setsockopt() / ipv6_setsockopt(), bumps msk->setsockopt_seq, and forwards to every subflow via mptcp_setsockopt_all_sf(). Newly-joining subflows pick up the four RECVERR bits through sync_socket_options() now that MPTCP_INET_FLAGS_MASK covers them.
mptcp_setsockopt_all_sf() skips IPv4 subflows when called with SOL_IPV6 to avoid the -ENOPROTOOPT that ip_setsockopt() returns on level mismatch in AF_INET6 msks carrying IPv4 subflows.
--- net/mptcp/sockopt.c | 95 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 77 insertions(+), 18 deletions(-)
net/mptcp/sockopt.c +77 -18
@@ -8,6 +8,8 @@#include <linux/kernel.h> #include <linux/module.h> +#include <net/ip.h> +#include <net/ipv6.h> #include <net/sock.h> #include <net/protocol.h> #include <net/tcp.h>
@@ -19,7 +21,11 @@#define MPTCP_INET_FLAGS_MASK \ (BIT(INET_FLAGS_TRANSPARENT) | \ BIT(INET_FLAGS_FREEBIND) | \ - BIT(INET_FLAGS_BIND_ADDRESS_NO_PORT)) + BIT(INET_FLAGS_BIND_ADDRESS_NO_PORT) | \ + BIT(INET_FLAGS_RECVERR) | \ + BIT(INET_FLAGS_RECVERR_RFC4884) | \ + BIT(INET_FLAGS_RECVERR6) | \ + BIT(INET_FLAGS_RECVERR6_RFC4884))static struct sock *__mptcp_tcp_fallback (struct mptcp_sock *msk) {
@@ -388,6 +394,57 @@ static int mptcp_setsockopt_sol_socket(struct mptcp_sock *msk, int optname,return -EOPNOTSUPP ; } +static int mptcp_setsockopt_all_sf (struct mptcp_sock *msk,int level, +int optname,sockptr_t optval, +unsigned int optlen) +{ +struct mptcp_subflow_context *subflow; +int ret =0 ; + +mptcp_for_each_subflow (msk, subflow) { +struct sock *ssk =mptcp_subflow_tcp_sock (subflow); + +if (level ==SOL_IPV6 && ssk->sk_family !=AF_INET6 ) +continue ; + + ret =tcp_setsockopt (ssk, level, optname, optval, optlen); +if (ret) +break ; + } +return ret; +} + +static int mptcp_setsockopt_recverr (struct mptcp_sock *msk,int level, +int optname,sockptr_t optval, +unsigned int optlen) +{ +struct sock *sk = (struct sock *)msk; +int val, ret; + +if (optlen <sizeof (int )) +return -EINVAL ; +if (copy_from_sockptr (&val, optval,sizeof (val))) +return -EFAULT ; + +if (level ==SOL_IP ) + ret =ip_setsockopt (sk, level, optname,KERNEL_SOCKPTR (&val),sizeof (val)); + +#if IS_ENABLED (CONFIG_IPV6 ) + else if (level == SOL_IPV6) + ret =ipv6_setsockopt (sk, level, optname,KERNEL_SOCKPTR (&val),sizeof (val)); +#endif + else + return -EOPNOTSUPP; +if (ret) +return ret; + +lock_sock (sk); +sockopt_seq_inc (msk); + ret =mptcp_setsockopt_all_sf (msk, level, optname,KERNEL_SOCKPTR (&val),sizeof (val)); +release_sock (sk); +return ret; +} +static int mptcp_setsockopt_v6 (struct mptcp_sock *msk,int optname,sockptr_t optval,unsigned int optlen) {
@@ -430,6 +487,10 @@ static int mptcp_setsockopt_v6(struct mptcp_sock *msk, int optname,release_sock (sk);break ; +case IPV6_RECVERR : +case IPV6_RECVERR_RFC4884 : + ret =mptcp_setsockopt_recverr (msk,SOL_IPV6 , optname, optval, optlen); +break ; }return ret;
@@ -775,6 +836,9 @@ static int mptcp_setsockopt_v4(struct mptcp_sock *msk, int optname,return mptcp_setsockopt_sol_ip_set (msk, optname, optval, optlen);case IP_TOS :return mptcp_setsockopt_v4_set_tos (msk, optname, optval, optlen); +case IP_RECVERR : +case IP_RECVERR_RFC4884 : +return mptcp_setsockopt_recverr (msk,SOL_IP , optname, optval, optlen); }return -EOPNOTSUPP ;
@@ -802,23 +866,6 @@ static int mptcp_setsockopt_first_sf_only(struct mptcp_sock *msk, int level, intreturn ret; } -static int mptcp_setsockopt_all_sf (struct mptcp_sock *msk,int level, -int optname,sockptr_t optval, -unsigned int optlen) -{ -struct mptcp_subflow_context *subflow; -int ret =0 ; - -mptcp_for_each_subflow (msk, subflow) { -struct sock *ssk =mptcp_subflow_tcp_sock (subflow); - - ret =tcp_setsockopt (ssk, level, optname, optval, optlen); -if (ret) -break ; - } -return ret; -} -static int mptcp_setsockopt_sol_tcp (struct mptcp_sock *msk,int optname,sockptr_t optval,unsigned int optlen) {
@@ -1463,6 +1510,12 @@ static int mptcp_getsockopt_v4(struct mptcp_sock *msk, int optname,case IP_LOCAL_PORT_RANGE :return mptcp_put_int_option (msk, optval, optlen,READ_ONCE (inet_sk (sk)->local_port_range)); +case IP_RECVERR : +return mptcp_put_int_option (msk, optval, optlen, +inet_test_bit (RECVERR , sk)); +case IP_RECVERR_RFC4884 : +return mptcp_put_int_option (msk, optval, optlen, +inet_test_bit (RECVERR_RFC4884 , sk)); }return -EOPNOTSUPP ;
@@ -1483,6 +1536,12 @@ static int mptcp_getsockopt_v6(struct mptcp_sock *msk, int optname,case IPV6_FREEBIND :return mptcp_put_int_option (msk, optval, optlen,inet_test_bit (FREEBIND , sk)); +case IPV6_RECVERR : +return mptcp_put_int_option (msk, optval, optlen, +inet6_test_bit (RECVERR6 , sk)); +case IPV6_RECVERR_RFC4884 : +return mptcp_put_int_option (msk, optval, optlen, +inet6_test_bit (RECVERR6_RFC4884 , sk)); }return -EOPNOTSUPP ;
Signature · 1 line
2.53.0
PATCH v5 3/4 mptcp: support MSG_ERRQUEUE on the parent socket
In reply to David CarlierSplice pending err skbs from each subflow's error queue onto the parent msk's error queue at error-report time, so poll() and recvmsg(MSG_ERRQUEUE) on the parent socket observe TX timestamps and MSG_ZEROCOPY completion notifications through the standard inet ABI.
The splice filters by SO_EE_ORIGIN: TIMESTAMPING / ZEROCOPY / LOCAL events forward to the parent because they are tied to user-handed data, not to a specific path; subflow-level ICMP errors are dropped because the legacy RECVERR ABI cannot meaningfully convey their per-subflow peer identity to single-path-aware userspace. Such events will be carried by a future MPTCP_RECERR channel.
mptcp_recv_error() retries the splice on the pull side: if sock_queue_err_skb() previously failed under rmem pressure, the skb stays on the subflow queue, and the next recvmsg(MSG_ERRQUEUE) splices it once the parent's queue has been drained.
--- net/mptcp/protocol.c | 66 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 60 insertions(+), 6 deletions(-)
net/mptcp/protocol.c +60 -6
@@ -11,6 +11,7 @@#include <linux/netdevice.h> #include <linux/sched/signal.h> #include <linux/atomic.h> +#include <linux/errqueue.h> #include <net/aligned_data.h> #include <net/rps.h> #include <net/sock.h>
@@ -815,21 +816,52 @@ static bool __mptcp_ofo_queue(struct mptcp_sock *msk)return moved; } +static bool mptcp_errqueue_skb_forwardable (const struct sk_buff *skb) +{ +u8 origin =SKB_EXT_ERR (skb)->ee.ee_origin; + +return origin ==SO_EE_ORIGIN_TIMESTAMPING || + origin ==SO_EE_ORIGIN_ZEROCOPY || + origin ==SO_EE_ORIGIN_LOCAL ; +} + +static bool __mptcp_subflow_splice_errqueue (struct sock *sk,struct sock *ssk) +{ +struct sk_buff *skb; +bool moved = false; + +while ((skb =skb_dequeue (&ssk->sk_error_queue))) { +if (!mptcp_errqueue_skb_forwardable (skb)) { +kfree_skb (skb);/* path-specific (ICMP) — belongs in MPTCP_RECERR */ +continue ; + } +if (sock_queue_err_skb (sk, skb)) { +skb_queue_head (&ssk->sk_error_queue, skb); +break ; + } + moved = true; + } + +return moved; +} +static bool __mptcp_subflow_error_report (struct sock *sk,struct sock *ssk) {int ssk_state; +bool report;int err; + report =__mptcp_subflow_splice_errqueue (sk, ssk); +/* only propagate errors on fallen-back sockets or * on MPC connect */ if (sk->sk_state !=TCP_SYN_SENT && !__mptcp_check_fallback (mptcp_sk (sk))) -return false; + goto out; err =sock_error (ssk);if (!err) -return false; - + goto out;/* We need to propagate only transition to CLOSE state. * Orphaned socket will see such state change via * subflow_sched_work_if_closed() and that path will properly
@@ -839,6 +871,11 @@ static bool __mptcp_subflow_error_report(struct sock *sk, struct sock *ssk)if (ssk_state ==TCP_CLOSE && !sock_flag (sk,SOCK_DEAD ))mptcp_set_state (sk, ssk_state);WRITE_ONCE (sk->sk_err, -err); + report = true; + +out: +if (!report) +return false;/* This barrier is coupled with smp_rmb() in mptcp_poll() */ smp_wmb ();
@@ -2286,6 +2323,23 @@ static unsigned int mptcp_inq_hint(const struct sock *sk)return 0 ; } +static int mptcp_recv_error (struct sock *sk,struct msghdr *msg,int len) +{ +struct mptcp_sock *msk =mptcp_sk (sk); +struct mptcp_subflow_context *subflow; + +lock_sock (sk); +mptcp_for_each_subflow (msk, subflow) { +struct sock *ssk =mptcp_subflow_tcp_sock (subflow); + +if (!skb_queue_empty (&ssk->sk_error_queue)) +__mptcp_subflow_splice_errqueue (sk, ssk); + } +release_sock (sk); + +return inet_recv_error (sk, msg, len); +} +static int mptcp_recvmsg (struct sock *sk,struct msghdr *msg,size_t len,int flags) {
@@ -2295,9 +2349,8 @@ static int mptcp_recvmsg(struct sock *sk, struct msghdr *msg, size_t len,int target;long timeo; -/* MSG_ERRQUEUE is really a no-op till we support IP_RECVERR */ if (unlikely (flags &MSG_ERRQUEUE )) -return inet_recv_error (sk, msg, len); +return mptcp_recv_error (sk, msg, len);lock_sock (sk);if (unlikely (sk->sk_state ==TCP_LISTEN )) {
@@ -4340,7 +4393,8 @@ static __poll_t mptcp_poll(struct file *file, struct socket *sock,/* This barrier is coupled with smp_wmb() in __mptcp_error_report() */ smp_rmb (); -if (READ_ONCE (sk->sk_err)) +if (READ_ONCE (sk->sk_err) || + !skb_queue_empty_lockless (&sk->sk_error_queue)) mask |=EPOLLERR ;return mask;
Signature · 1 line
2.53.0
PATCH v5 4/4 selftests: mptcp: cover IP_RECVERR sockopt propagation
In reply to David CarlierExercise setsockopt/getsockopt of IP_RECVERR and IPV6_RECVERR on the MPTCP parent socket, including the empty-errqueue EAGAIN contract on MSG_ERRQUEUE|MSG_DONTWAIT.
End-to-end errqueue delivery (ICMP, TX timestamps, zerocopy) depends on subflow-side producers that are out of scope for this series and will be covered by follow-up work.
--- .../selftests/net/mptcp/mptcp_sockopt.c | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+)
tools/testing/selftests/net/mptcp/mptcp_sockopt.c +55 -0
@@ -769,6 +769,60 @@ static void test_ip_tos_sockopt(int fd)xerror ("expect socklen_t == -1" ); } +static void test_ip_recverr_sockopt (int fd) +{ +struct iovec iov = { + .iov_base = &(char ){0 }, + .iov_len =1 , + }; +struct msghdr msg = { + .msg_iov = &iov, + .msg_iovlen =1 , + }; +int one =1 , zero =0 , val =-1 ; +socklen_t s =sizeof (val); +int level, optname, r; + +switch (pf) { +case AF_INET : + level =SOL_IP ; + optname =IP_RECVERR ; +break ; +case AF_INET6 : + level =SOL_IPV6 ; + optname =IPV6_RECVERR ; +break ; +default : +xerror ("Unknown pf %d\n" , pf); + } + + r =setsockopt (fd, level, optname, &one,sizeof (one)); +if (r) +die_perror ("setsockopt recverr on" ); + + r =getsockopt (fd, level, optname, &val, &s); +if (r) +die_perror ("getsockopt recverr on" ); +if (s !=sizeof (val) || val != one) +xerror ("recverr on mismatch val=%d len=%u" , val, s); + + r =recvmsg (fd, &msg,MSG_ERRQUEUE |MSG_DONTWAIT ); +if (r !=-1 || errno !=EAGAIN ) +xerror ("expected empty errqueue to return EAGAIN, ret=%d errno=%d" , r, errno); + + r =setsockopt (fd, level, optname, &zero,sizeof (zero)); +if (r) +die_perror ("setsockopt recverr off" ); + + val =-1 ; + s =sizeof (val); + r =getsockopt (fd, level, optname, &val, &s); +if (r) +die_perror ("getsockopt recverr off" ); +if (s !=sizeof (val) || val != zero) +xerror ("recverr off mismatch val=%d len=%u" , val, s); +} +static int client (int pipefd) {int fd =-1 ;
@@ -787,6 +841,7 @@ static int client(int pipefd) }test_ip_tos_sockopt (fd); +test_ip_recverr_sockopt (fd);connect_one_server (fd, pipefd);
Signature · 1 line
2.53.0
Matthieu Baerts
maintainerHi David,
On 02/05/2026 23:19, David Carlier wrote:
(...)
Quoted 1 line
Changes in v5:
Thank you for the v5. Please note that the CI was not able to apply it [1], because your patch was based on an old version of the export (or export-net, for-review, for-review-net) branches. I manually solved the conflicts and sent it to our repo, for our CI to test it.
[1]
https://patchew.org/MPTCP/[email protected]/logs/git/
Cheers,
Matt
Signature · 1 line
Sponsored by the NGI0 Core fund.
Hi and thanks ok doing it locally too indeed so many conflicts :) Regards.
Quoted 22 lines · Matthieu Baerts
Hi David,
On 02/05/2026 23:19, David Carlier wrote:
(...)
Quoted 1 line
Changes in v5:
Thank you for the v5. Please note that the CI was not able to apply it [1], because your patch was based on an old version of the export (or export-net, for-review, for-review-net) branches. I manually solved the conflicts and sent it to our repo, for our CI to test it.
[1]
https://patchew.org/MPTCP/[email protected]/logs/git/Cheers,
Matt
--
Sponsored by the NGI0 Core fund.
Hi David,
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): Unstable: 1 failed test(s): packetdrill_dss ⚠️
- KVM Validation: debug (only selftest_mptcp_join): Success! ✅
- KVM Validation: btf-normal (only bpftest_all): Success! ✅
- KVM Validation: btf-debug (only bpftest_all): Success! ✅
- Task: https://github.com/multipath-tcp/mptcp_net-next/actions/runs/25313259921
Initiator: Matthieu Baerts (NGI0)
Commits: https://github.com/multipath-tcp/mptcp_net-next/commits/3e3c0e270017
Patchwork: https://patchwork.kernel.org/project/mptcp/list/?series=1088853
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-normalFor 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)