From cef71fb949374a5362a32236cf5ed90d45496c74 Mon Sep 17 00:00:00 2001 From: fatedier Date: Mon, 27 Apr 2026 02:30:29 +0800 Subject: [PATCH] plugin/http_proxy: fix fragmented CONNECT method detection and add read deadline (#5296) --- Release.md | 4 +- pkg/plugin/client/http_proxy.go | 14 +++- pkg/plugin/client/http_proxy_test.go | 107 +++++++++++++++++++++++++++ 3 files changed, 119 insertions(+), 6 deletions(-) create mode 100644 pkg/plugin/client/http_proxy_test.go diff --git a/Release.md b/Release.md index 58fb4358..42db658a 100644 --- a/Release.md +++ b/Release.md @@ -8,9 +8,9 @@ For mixed-version deployments, upgrade frps first, then upgrade frpc. This keeps This release introduces wire protocol v2 as a transition path for future frpc/frps protocol changes. The existing wire protocol is difficult to extend without compatibility risk, and upcoming changes, including replacing deprecated stream encryption methods, require a versioned protocol. -**The default value of `transport.wireProtocol` remains `v1` in this release, but it will switch to `v2` in the next release.** Users can keep the default for now. To test v2 early, upgrade both frpc and frps to versions that support it, then set `transport.wireProtocol = "v2"` in frpc. A v2-enabled frpc cannot connect to an older frps. +**The default value of `transport.wireProtocol` remains `v1` in this release.** Users can keep the default for now. To test v2 early, upgrade both frpc and frps to versions that support it, then set `transport.wireProtocol = "v2"` in frpc. A v2-enabled frpc cannot connect to an older frps. -v1 will be deprecated when v2 becomes the default in the next release. It will continue to be supported until v0.78.0 is released, and may be removed in v0.78.0 or later. +v1 will be deprecated when v2 becomes the default in a future release. It will continue to be supported until v0.78.0 is released, and may be removed in v0.78.0 or later. ## Features diff --git a/pkg/plugin/client/http_proxy.go b/pkg/plugin/client/http_proxy.go index 2cb22bbe..0145f444 100644 --- a/pkg/plugin/client/http_proxy.go +++ b/pkg/plugin/client/http_proxy.go @@ -45,6 +45,8 @@ type HTTPProxy struct { s *http.Server } +const httpProxyReadHeaderTimeout = 60 * time.Second + func NewHTTPProxyPlugin(_ PluginContext, options v1.ClientPluginOptions) (Plugin, error) { opts := options.(*v1.HTTPProxyPluginOptions) listener := NewProxyListener() @@ -56,7 +58,7 @@ func NewHTTPProxyPlugin(_ PluginContext, options v1.ClientPluginOptions) (Plugin hp.s = &http.Server{ Handler: hp, - ReadHeaderTimeout: 60 * time.Second, + ReadHeaderTimeout: httpProxyReadHeaderTimeout, } go func() { @@ -73,16 +75,19 @@ func (hp *HTTPProxy) Handle(_ context.Context, connInfo *ConnectionInfo) { wrapConn := netpkg.WrapReadWriteCloserToConn(connInfo.Conn, connInfo.UnderlyingConn) sc, rd := libnet.NewSharedConn(wrapConn) - firstBytes := make([]byte, 7) - _, err := rd.Read(firstBytes) + firstBytes := make([]byte, len(http.MethodConnect)) + _ = wrapConn.SetReadDeadline(time.Now().Add(httpProxyReadHeaderTimeout)) + _, err := io.ReadFull(rd, firstBytes) if err != nil { + _ = wrapConn.SetReadDeadline(time.Time{}) wrapConn.Close() return } - if strings.ToUpper(string(firstBytes)) == "CONNECT" { + if strings.EqualFold(string(firstBytes), http.MethodConnect) { bufRd := bufio.NewReader(sc) request, err := http.ReadRequest(bufRd) + _ = wrapConn.SetReadDeadline(time.Time{}) if err != nil { wrapConn.Close() return @@ -91,6 +96,7 @@ func (hp *HTTPProxy) Handle(_ context.Context, connInfo *ConnectionInfo) { return } + _ = wrapConn.SetReadDeadline(time.Time{}) _ = hp.l.PutConn(sc) } diff --git a/pkg/plugin/client/http_proxy_test.go b/pkg/plugin/client/http_proxy_test.go new file mode 100644 index 00000000..c4557ece --- /dev/null +++ b/pkg/plugin/client/http_proxy_test.go @@ -0,0 +1,107 @@ +// Copyright 2026 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !frps + +package client + +import ( + "bufio" + "context" + "fmt" + "io" + "net" + "testing" + "time" + + "github.com/stretchr/testify/require" + + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +func TestHTTPProxyHandleFragmentedConnectMethod(t *testing.T) { + require := require.New(t) + + ln, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(err) + defer ln.Close() + + const payload = "ping" + echoErr := make(chan error, 1) + go func() { + conn, err := ln.Accept() + if err != nil { + echoErr <- err + return + } + defer conn.Close() + + buf := make([]byte, len(payload)) + if _, err = io.ReadFull(conn, buf); err != nil { + echoErr <- err + return + } + if string(buf) != payload { + echoErr <- fmt.Errorf("unexpected payload %q", string(buf)) + return + } + _, err = conn.Write([]byte("echo:" + payload)) + echoErr <- err + }() + + hp := &HTTPProxy{ + opts: &v1.HTTPProxyPluginOptions{}, + l: NewProxyListener(), + } + + clientConn, serverConn := net.Pipe() + defer clientConn.Close() + + go hp.Handle(context.Background(), &ConnectionInfo{ + Conn: serverConn, + UnderlyingConn: serverConn, + }) + + require.NoError(clientConn.SetDeadline(time.Now().Add(5 * time.Second))) + + targetAddr := ln.Addr().String() + req := "CONNECT " + targetAddr + " HTTP/1.1\r\nHost: " + targetAddr + "\r\n\r\n" + _, err = clientConn.Write([]byte("CON")) + require.NoError(err) + _, err = clientConn.Write([]byte(req[len("CON"):])) + require.NoError(err) + + rd := bufio.NewReader(clientConn) + status, err := rd.ReadString('\n') + require.NoError(err) + require.Equal("HTTP/1.1 200 OK\r\n", status) + line, err := rd.ReadString('\n') + require.NoError(err) + require.Equal("\r\n", line) + + _, err = clientConn.Write([]byte(payload)) + require.NoError(err) + + got := make([]byte, len("echo:"+payload)) + _, err = io.ReadFull(rd, got) + require.NoError(err) + require.Equal("echo:"+payload, string(got)) + + select { + case err := <-echoErr: + require.NoError(err) + case <-time.After(time.Second): + t.Fatal("timed out waiting for echo server") + } +}