plugin/http_proxy: fix fragmented CONNECT method detection and add read deadline (#5296)

This commit is contained in:
fatedier
2026-04-27 02:30:29 +08:00
committed by GitHub
parent 410c4861c4
commit cef71fb949
3 changed files with 119 additions and 6 deletions

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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")
}
}