Undock, and everything freezes
Most of my work happens on remote Linux servers, through VS Code Remote-SSH, and more and more with Claude Code running inside that remote session. My MacBook sits on a dock with an Ethernet cable most of the day. Every time I unplug it to walk to a meeting, everything freezes: the terminal, the VS Code window, and Claude Code in the middle of a task. Nothing comes back until the dead TCP connection times out and VS Code reconnects. And the Claude session did not advance while I was away.
The reason is TCP itself. A TCP connection is bound to one source address, and when the Ethernet address disappears, the connection has nowhere to go. Multipath TCP (RFC 8684, introduce by my beloved colleague Olivier Bonaventure) fixes exactly this: one connection runs over several subflows, one per interface, and traffic moves from the Ethernet subflow to the WiFi one without the application noticing. Linux supports it since kernel 5.6, and macOS has an MPTCP client in its kernel. But it’s nearly hidden !
So the question was simple: how do I make ssh on macOS open MPTCP connections? The answer took longer than expected. It ends with ssh-mptcp, a 300-line ProxyCommand that works with any ssh client, VS Code included. And yes, Claude did the 300 lines by himself. Thanks Claude.
Demo
TL;DR
On the Mac, with the Xcode command line tools installed:
git clone https://github.com/UCLouvain-ENSG/ssh-mptcp.git
cd ssh-mptcp
make && make install # installs ~/.local/bin/ssh-mptcp
make check # prints "You are using MPTCP." if your Mac can do MPTCP
Then, in ~/.ssh/config, for the servers you care about:
Host *
ProxyCommand ~/.local/bin/ssh-mptcp %h %p
That is all on the client side. VS Code Remote-SSH reads the same file, so it needs no extra setting. The server must speak MPTCP too: it’s as easy as installing mptcpize and running “mptpize enable sshd”.
The search
OpenSSH does not want it
The obvious first stop is a recent OpenSSH with an MPTCP option. There is none. Native support was proposed twice, in PR #335 and PR #547, and the maintainers declined both: they do not want a protocol that is only available on some of the platforms OpenSSH supports. Linux is reaching nearly 100% of top computer, and nearly all servers. iOS and MacOS supports it. In Android it’s just not activated. On Desktop, only Windows is left but we also have a solution. So yes, I’m a bit pissed off 🙂
The Linux tricks do not work on macOS
macOS has no IPPROTO_MPTCP. The kernel does have an AF_MULTIPATH socket family, used through connectx(2), but it is undocumented and, according to mptcp.dev, it creates MPTCP connections without the extra subflows that make them useful. The supported way in is Network.framework: nw_parameters_set_multipath_service(), available since macOS 10.14, as the mptcp.dev macOS guide explains. So whatever opens the connection has to go through Network.framework, not through a socket.
Apple already patched ssh, sort of
This is where it gets interesting. Apple ships its own fork of OpenSSH, and its sshconnect.c already connects through Network.framework. It also has a hidden, undocumented option, AppleMultipath, present since Apple’s OpenSSH-281.40.2 release (July 2022). When it is set, ssh asks for nw_multipath_service_handover through a private call, nw_parameters_create_legacy_tcp_socket(), and extracts the kernel socket from the connection, so there is no relay process.
On paper, AppleMultipath yes in ~/.ssh/config is all you need. In practice, it is fragile:
- Homebrew’s ssh refuses the option.
- Apple’s code skips the Network.framework path, and multipath with it, whenever
ConnectTimeout,ConnectionAttempts,ProxyJump,ProxyCommandor hostname canonicalization is set, or when the destination is an IP address.
In my tests, with Apple’s ssh and the option accepted, tcpdump still showed a SYN with no MPTCP option at all, to a server that does speak MPTCP. I did not find out why: the private call is closed source.
The result: ssh-mptcp
Instead of patching ssh, I moved the connection out of it. ssh already knows how to delegate its transport: ProxyCommand starts a program and speaks SSH over its stdin and stdout. ssh-mptcp is that program, about 300 lines of C on the public Network.framework API:
- ssh starts
ssh-mptcp host port. - ssh-mptcp creates an
nw_connectionto the server with plain TCP parameters,TCP_NODELAY, andnw_parameters_set_multipath_service(). Network.framework resolves the name, races IPv4 and IPv6, and asks the kernel for an MPTCP connection. - Once connected, two loops relay the bytes: stdin to
nw_connection_send(), andnw_connection_receive()to stdout. - When the Ethernet cable goes, the kernel moves the connection to the WiFi subflow. ssh, and everything running over it, never sees it.
https://github.com/UCLouvain-ENSG/ssh-mptcp
ssh only talks to a pipe. The MPTCP connection and its subflows live in the kernel, so losing the Ethernet subflow changes nothing above it.
Network.framework never hands out the underlying socket, so ssh-mptcp has to stay in the middle and copy the bytes, rather than pass a file descriptor back to ssh with ProxyUseFdpass. At SSH speeds, the extra process and the extra copy do not matter.
The -m option picks one of the three modes Apple defines:
handoveronly uses another interface once the primary one is lost.interactive, the default, also switches when the primary interface suffers loss or delay. It picks the lowest-latency path, which is what an interactive SSH session wants.aggregatespreads the traffic over all interfaces. macOS refuses it to third-party programs unless you runsudo sysctl net.inet.mptcp.allow_aggregate=1.
With -v, ssh-mptcp logs the interfaces in use and every path change to stderr, which is the easiest way to watch a handover happen. Because it is a plain ProxyCommand, it works with Apple’s ssh, Homebrew’s ssh, git, rsync, and VS Code Remote-SSH alike.
Server side
macOS only does MPTCP as a client, so the other end must be a Linux server (kernel 5.6 or later, net.mptcp.enabled = 1) whose sshd listens on an MPTCP socket. If it does not, the connection falls back to plain TCP: ssh still works, but the session will not survive the switch.
That’s actually easy, just install mptcpize and run “sudo mptcpize enable sshd” and the sshd service will be prefixed by a preloader that will change TCP by MPTCP. Don’t worry, MPTCP always fallbacks to TCP.
SSH gateway
Do you need to do this on all your server? No ! SSH ProxyCommands can be changed. Probably if your work has a bit of sense about security, you already have to hop through an SSH gateway.
That will enable to hop throu a SSH gateway (a machine just justed to hop through SSH) :
Host *_via_proxy
User XXX
ProxyCommand ssh -o StrictHostKeyChecking=no -A -X -Y -q [gateway address] -W $(basename %h _via_proxy):%p
That will use ssh-mptcp as a hop for all destination :
Host *
ControlMaster auto
ControlPath /tmp/.ssh.%h
ServerAliveInterval 1
ServerAliveCountMax 300
ProxyCommand ~/workspace/openssh-mptcp/ssh-mptcp -m interactive %h %p
The controlsocket is unrelated but allows to reuse a single connection to multiplex all client connections. More efficient and the server does not thing it’s DDOSed when you have many many connection.
With that, using ssh google.com_via_proxy will connect to google.com, via MPTCP, via your proxy.
Limitations
ssh-mptcp only covers what goes through ssh. Anything else on the Mac, including the Claude app’s own connections, still uses plain TCP, and MPTCP would also need the remote end to support it. The server must run MPTCP, and a middlebox on the path can strip the options, in which case the connection quietly falls back to TCP. macOS cannot accept MPTCP connections, so this does not help with a Mac acting as a server.
Conclusion
MPTCP on macOS has been one API call away for years, but nothing in the ssh toolchain makes that call reliably, and the one place Apple did it is hidden and fragile. A small ProxyCommand on the public API is enough: my remote sessions, VS Code, and the Claude Code agent running inside them now survive leaving the dock. The code, README and build instructions are on GitHub, under the MIT license. If you are on Windows, my older post on MPTCP with WSL2 covers the same use case there.
Sources
- ssh-mptcp on GitHub
- RFC 8684, TCP Extensions for Multipath Operation with Multiple Addresses
- mptcp.dev FAQ: MPTCP with OpenSSH, supported operating systems
- mptcp.dev: MPTCP on macOS with Network.framework
- openssh-portable PR #335 and PR #547
- Apple’s OpenSSH, sshconnect.c
- XNU sources (bsd/netinet/mptcp_usrreq.c, tcp_cache.c)
- Apple documentation: nw_multipath_service_t
- mptcp-check, the server behind check.mptcp.dev
