Skip to content

fix: disable Multipath TCP on listeners by default - #5611

Open
Dv2Corn wants to merge 2 commits into
fatedier:devfrom
Dv2Corn:fix/disable-mptcp-listeners
Open

Dv2Corn wants to merge 2 commits into
fatedier:devfrom
Dv2Corn:fix/disable-mptcp-listeners

Conversation

@Dv2Corn

@Dv2Corn Dv2Corn commented Nov 11, 2026

Copy link
Copy Markdown

WHY

Fixes #5587.

Since Go 1.24, net.Listen("tcp", ...) on Linux creates an MPTCP socket by default (GODEBUG multipathtcp=2, "listeners only"; see defaultMPTCPEnabledListen and mptcpStatusListen.get). frp declares go 1.26.0, so frpc and frps get that default. Some vendor kernels (the report is an OpenWrt aarch64 router on 5.4) create the MPTCP socket without error but never finish the handshake on it: clients stay in SYN_RECV. Go only falls back to plain TCP when creating the MPTCP socket fails, so it can't detect this. Outgoing connections still work because Go dialers don't use MPTCP by default, which is why the login to frps succeeds while every local listening port is dead. GODEBUG=multipathtcp=0 fixes it for the reporter.

frp has no listener that sets MPTCP explicitly. Every TCP listener uses net.Listen with the default, so all of them are affected, not only the admin server and the visitors:

Rather than calling SetMultipathTCP(false) at each of these sites (and every future one), this adds godebug multipathtcp=0 to go.mod. The go command applies godebug lines of the main module to every main package and test binary built from it, so make build, Makefile.cross-compiles, the Dockerfiles and go install github.com/fatedier/frp/cmd/...@version all embed it. The binaries then show build DefaultGODEBUG=multipathtcp=0 in go version -m. This restores the pre-Go 1.24 behaviour for listeners. Dialers already default to plain TCP and keep doing so. KCP and QUIC run over UDP and are not affected. A user can still override it at run time with the GODEBUG environment variable.

TestMultipathTCPDisabledByDefault in pkg/util/net checks that the default ListenConfig and Dialer don't use MPTCP. It also opens a real net.Listen socket and dials it with MPTCP requested. MPTCP is only negotiated if the listener is an MPTCP socket, so on a kernel with MPTCP enabled the test fails without the go.mod line. On kernels without MPTCP and on other platforms the socket part passes trivially.

Tests

Tested in a Go 1.26.10 container on a Linux 6.8 kernel with net.mptcp.enabled = 1.

  • New test on current dev: fails at mptcp_test.go:27 (ListenConfig.MultipathTCP() is true). With that assertion removed it also fails at mptcp_test.go:44 ("listener created by net.Listen uses MPTCP"). With this change it passes.

  • make test: all packages pass except the new test before the change, and all pass after. make vet and golangci-lint v2.12.2 (--build-tags noweb) report no issues. go mod tidy leaves go.mod/go.sum unchanged.

  • make e2e (ginkgo, 8 nodes): 261 passed, 2 skipped, both with the dev binaries and with this change.

  • End to end with make build: frps (bind port, dashboard, vhost HTTP) and frpc (admin web server, a tcp proxy, an http proxy, an stcp proxy plus stcp visitor). A small client dialed every listening port with Dialer.SetMultipathTCP(true), read TCPConn.MultipathTCP() and sent an HTTP request:

    # dev                                         # this change
    go version -m: (no DefaultGODEBUG)            build DefaultGODEBUG=multipathtcp=0
    ss -lnM: 6000 7000 7400 7500 8080 9000        ss -lnM: (none)
    frps-bindPort     7000 MultipathTCP=true      MultipathTCP=false
    frps-dashboard    7500 MultipathTCP=true      MultipathTCP=false  HTTP/1.1 404
    frps-tcp-proxy    6000 MultipathTCP=true      MultipathTCP=false  HTTP/1.1 200
    frps-vhost-http   8080 MultipathTCP=true      MultipathTCP=false  HTTP/1.1 200
    frpc-admin        7400 MultipathTCP=true      MultipathTCP=false  HTTP/1.1 200
    frpc-stcp-visitor 9000 MultipathTCP=true      MultipathTCP=false  HTTP/1.1 200
    

    The 404 on the dashboard is from the noweb build. Running the dev binaries with GODEBUG=multipathtcp=0 (the workaround from the issue) gives the same result as this change.

  • A GOOS=linux GOARCH=arm64 build of frpc also contains DefaultGODEBUG=multipathtcp=0.

Limitations

  • I don't have an affected vendor kernel, so the handshake hang itself isn't reproduced here. The tests show that frp no longer creates the MPTCP listening sockets that hang on that kernel, the same thing the reporter's workaround does.
  • go.mod godebug lines only apply when frp is the main module. A program that imports frp as a library keeps its own default. A build inside a go.work workspace uses the workspace's godebug lines instead.
  • Hosts where MPTCP-capable clients connected over MPTCP since Go 1.24 now get plain TCP, as with frp releases built with Go 1.23 and earlier. frp never had an option for this, so the MPTCP listeners were only a side effect of the toolchain upgrade.

Since Go 1.24, net.Listen creates MPTCP sockets on Linux by default
(GODEBUG multipathtcp=2). Some vendor kernels accept the MPTCP socket
but never complete the handshake on it, so every frpc/frps TCP listener
(admin web server, visitors, proxy and vhost ports) stops accepting
connections there, while outgoing connections still work.

Set "godebug multipathtcp=0" in go.mod. It applies to every main package
and test binary built from this module, so all net.Listen calls go back
to plain TCP sockets, as before Go 1.24. Dialers already default to
plain TCP and keep doing so.

Fixes fatedier#5587
Comment thread pkg/util/net/mptcp_test.go Outdated
require.NoError(t, err)
defer conn.Close()

isMPTCP, err := conn.(*stdnet.TCPConn).MultipathTCP()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Check the listener side to avoid false failures on older Linux kernels

On MPTCP-capable Linux kernels older than 5.16 (for example 5.10/5.15), TCPConn.MultipathTCP() cannot reliably detect TCP fallback and can report true for a socket created with IPPROTO_MPTCP. This client explicitly opts into MPTCP with d.SetMultipathTCP(true), so it can return true here even though net.Listen correctly created a plain TCP listener. Consequently the new test, and make test, fail on those hosts despite the fix working. Accept the connection and inspect the server-side TCPConn instead, so the assertion checks the listener's protocol rather than the client's requested protocol.

@kilo-code-bot

kilo-code-bot Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (1 files)
  • pkg/util/net/mptcp_test.go

Scope: Incremental review from 1edc541974b2bf42ee31f837038161d21990268e to a26a21fcf4347ffe23683f2ef2a08859315a337f.

Validation: Static review only. Tests were not executed under the read-only restrictions.

Previous Review Summary (commit 1edc541)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 1edc541)

Status: 1 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
pkg/util/net/mptcp_test.go 42 The explicitly MPTCP-enabled client can report MPTCP after TCP fallback on pre-5.16 Linux kernels, falsely failing the test against a correctly configured plain-TCP listener. Inspect the accepted server-side connection instead.
Files Reviewed (2 files)
  • go.mod - 0 issues
  • pkg/util/net/mptcp_test.go - 1 issue

Validation: Static review only. Tests were not executed under the read-only restrictions.

Fix these issues in Kilo Cloud


Reviewed by gpt-6.1-sol · Input: 33 · Output: 2.7K · Cached: 240.8K

On kernels older than 5.16, Go reports MultipathTCP() as true for any
socket created with IPPROTO_MPTCP, even after the connection fell back
to plain TCP. The client side dials with MPTCP on purpose, so the old
check could fail there against a plain TCP listener.

The accepted connection is only an MPTCP socket when the listener is
one, so check that side instead.
@Dv2Corn

Dv2Corn commented Nov 13, 2026

Copy link
Copy Markdown
Author

Good catch, thanks. I pushed a follow-up that checks MultipathTCP() on the accepted connection instead of the client one: on kernels older than 5.16 the client side reports true for any socket opened with MPTCP, even after it fell back to plain TCP, while the accepted socket is only MPTCP when the listener is. I ran the test and the whole pkg/util/net package with -race -count=20 on a 6.8 kernel, and checked that the new check still fails when the listener is forced back to MPTCP with GODEBUG=multipathtcp=2.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

frpc 0.66.0+ (built with Go 1.24+) listening sockets never accept connections on Linux 5.4 aarch64

1 participant