Repository navigation
Conversation
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
| require.NoError(t, err) | ||
| defer conn.Close() | ||
|
|
||
| isMPTCP, err := conn.(*stdnet.TCPConn).MultipathTCP() |
There was a problem hiding this comment.
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.
Code Review SummaryStatus: No Issues Found | Recommendation: Merge Files Reviewed (1 files)
Scope: Incremental review from 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
Issue Details (click to expand)WARNING
Files Reviewed (2 files)
Validation: Static review only. Tests were not executed under the read-only restrictions. 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.
|
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. |
WHY
Fixes #5587.
Since Go 1.24,
net.Listen("tcp", ...)on Linux creates an MPTCP socket by default (GODEBUGmultipathtcp=2, "listeners only"; seedefaultMPTCPEnabledListenandmptcpStatusListen.get). frp declaresgo 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 inSYN_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=0fixes it for the reporter.frp has no listener that sets MPTCP explicitly. Every TCP listener uses
net.Listenwith 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 addsgodebug multipathtcp=0togo.mod. Thegocommand appliesgodebuglines of the main module to every main package and test binary built from it, somake build,Makefile.cross-compiles, the Dockerfiles andgo install github.com/fatedier/frp/cmd/...@versionall embed it. The binaries then showbuild DefaultGODEBUG=multipathtcp=0ingo 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 theGODEBUGenvironment variable.TestMultipathTCPDisabledByDefaultinpkg/util/netchecks that the defaultListenConfigandDialerdon't use MPTCP. It also opens a realnet.Listensocket 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 thego.modline. 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 atmptcp_test.go:27(ListenConfig.MultipathTCP()is true). With that assertion removed it also fails atmptcp_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 vetandgolangci-lintv2.12.2 (--build-tags noweb) report no issues.go mod tidyleavesgo.mod/go.sumunchanged.make e2e(ginkgo, 8 nodes): 261 passed, 2 skipped, both with thedevbinaries 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 withDialer.SetMultipathTCP(true), readTCPConn.MultipathTCP()and sent an HTTP request:The 404 on the dashboard is from the
nowebbuild. Running thedevbinaries withGODEBUG=multipathtcp=0(the workaround from the issue) gives the same result as this change.A
GOOS=linux GOARCH=arm64build of frpc also containsDefaultGODEBUG=multipathtcp=0.Limitations
go.modgodebuglines only apply when frp is the main module. A program that imports frp as a library keeps its own default. A build inside ago.workworkspace uses the workspace'sgodebuglines instead.