mirror of
https://github.com/fatedier/frp.git
synced 2026-07-22 21:49:18 +08:00
server: drop HTTP/1.1 h2c upgrade handling (#5436)
Use net/http Server.Protocols and the merged golib listener path instead of the deprecated h2c handler. Keep HTTP/1.1 and cleartext HTTP/2 prior-knowledge support, while intentionally dropping HTTP/1.1 Upgrade: h2c.
This commit is contained in:
@@ -61,11 +61,6 @@ linters:
|
||||
- legacy
|
||||
- std-error-handling
|
||||
rules:
|
||||
# Keep h2c and HTTP/1.1 Upgrade:h2c compatibility until an explicit migration.
|
||||
- linters:
|
||||
- staticcheck
|
||||
path: ^pkg/util/vhost/http\.go$
|
||||
text: "^SA1019:"
|
||||
- linters:
|
||||
- errcheck
|
||||
- maligned
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
## Fixes
|
||||
|
||||
* HTTP vhost servers no longer support HTTP/1.1 `Upgrade: h2c` requests. Cleartext HTTP/2 prior-knowledge remains supported.
|
||||
* Fixed control-session replacement leaks when frpc reconnects through a half-open TCP multiplexed connection.
|
||||
* Fixed an SSH tunnel gateway panic when handling malformed exec requests.
|
||||
|
||||
2
go.mod
2
go.mod
@@ -5,7 +5,7 @@ go 1.25.0
|
||||
require (
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
||||
github.com/coreos/go-oidc/v3 v3.14.1
|
||||
github.com/fatedier/golib v0.8.0
|
||||
github.com/fatedier/golib v0.8.1-0.20260722102903-f59d12dbf4a6
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
|
||||
4
go.sum
4
go.sum
@@ -20,8 +20,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatedier/golib v0.8.0 h1:LQC8Mly/N71CUnFmgZMfQGrCA4h0tghg9pcDx58uG10=
|
||||
github.com/fatedier/golib v0.8.0/go.mod h1:ArUGvPg2cOw/py2RAuBt46nNZH2VQ5Z70p109MAZpJw=
|
||||
github.com/fatedier/golib v0.8.1-0.20260722102903-f59d12dbf4a6 h1:ZoGMMzVe+PqeJfLG2xxh1OjAcgpObIqrUqDAkgL9618=
|
||||
github.com/fatedier/golib v0.8.1-0.20260722102903-f59d12dbf4a6/go.mod h1:ArUGvPg2cOw/py2RAuBt46nNZH2VQ5Z70p109MAZpJw=
|
||||
github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6 h1:u92UUy6FURPmNsMBUuongRWC0rBqN6gd01Dzu+D21NE=
|
||||
github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6/go.mod h1:c5/tk6G0dSpXGzJN7Wk1OEie8grdSJAmeawId9Zvd34=
|
||||
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
|
||||
|
||||
@@ -28,8 +28,6 @@ import (
|
||||
|
||||
libio "github.com/fatedier/golib/io"
|
||||
"github.com/fatedier/golib/pool"
|
||||
"golang.org/x/net/http2"
|
||||
"golang.org/x/net/http2/h2c"
|
||||
|
||||
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||
"github.com/fatedier/frp/pkg/util/log"
|
||||
@@ -139,7 +137,7 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) *
|
||||
_, _ = rw.Write(getNotFoundPageContent())
|
||||
},
|
||||
}
|
||||
rp.proxy = h2c.NewHandler(proxy, &http2.Server{})
|
||||
rp.proxy = proxy
|
||||
return rp
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,94 @@
|
||||
package vhost
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||
)
|
||||
|
||||
func TestHTTPServerProtocols(t *testing.T) {
|
||||
rp := NewHTTPReverseProxy(HTTPReverseProxyOptions{}, NewRouters())
|
||||
protocols := new(http.Protocols)
|
||||
protocols.SetHTTP1(true)
|
||||
protocols.SetUnencryptedHTTP2(true)
|
||||
server := &http.Server{
|
||||
Handler: rp,
|
||||
ReadHeaderTimeout: time.Second,
|
||||
Protocols: protocols,
|
||||
}
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
|
||||
serveErr := make(chan error, 1)
|
||||
go func() {
|
||||
serveErr <- server.Serve(listener)
|
||||
}()
|
||||
defer func() {
|
||||
require.NoError(t, server.Close())
|
||||
require.ErrorIs(t, <-serveErr, http.ErrServerClosed)
|
||||
}()
|
||||
|
||||
require.True(t, server.Protocols.HTTP1())
|
||||
require.True(t, server.Protocols.UnencryptedHTTP2())
|
||||
|
||||
t.Run("HTTP/1.1", func(t *testing.T) {
|
||||
transport := &http.Transport{Protocols: httpProtocols(true, false)}
|
||||
defer transport.CloseIdleConnections()
|
||||
client := &http.Client{Transport: transport}
|
||||
response, err := client.Get("http://" + listener.Addr().String() + "/")
|
||||
require.NoError(t, err)
|
||||
defer response.Body.Close()
|
||||
|
||||
require.Equal(t, "HTTP/1.1", response.Proto)
|
||||
require.Equal(t, http.StatusNotFound, response.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("HTTP/2 prior knowledge", func(t *testing.T) {
|
||||
transport := &http.Transport{Protocols: httpProtocols(false, true)}
|
||||
defer transport.CloseIdleConnections()
|
||||
client := &http.Client{Transport: transport}
|
||||
response, err := client.Get("http://" + listener.Addr().String() + "/")
|
||||
require.NoError(t, err)
|
||||
defer response.Body.Close()
|
||||
|
||||
require.Equal(t, "HTTP/2.0", response.Proto)
|
||||
require.Equal(t, http.StatusNotFound, response.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("HTTP/1.1 Upgrade h2c", func(t *testing.T) {
|
||||
conn, err := net.Dial("tcp", listener.Addr().String())
|
||||
require.NoError(t, err)
|
||||
defer conn.Close()
|
||||
|
||||
_, err = fmt.Fprintf(conn,
|
||||
"GET / HTTP/1.1\r\nHost: %s\r\n"+
|
||||
"Connection: Upgrade, HTTP2-Settings\r\nUpgrade: h2c\r\n"+
|
||||
"HTTP2-Settings: AAMAAABkAAQCAAAAAAIAAAAA\r\n\r\n",
|
||||
listener.Addr())
|
||||
require.NoError(t, err)
|
||||
response, err := http.ReadResponse(bufio.NewReader(conn), nil)
|
||||
require.NoError(t, err)
|
||||
defer response.Body.Close()
|
||||
|
||||
require.NotEqual(t, http.StatusSwitchingProtocols, response.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
func httpProtocols(http1, unencryptedHTTP2 bool) *http.Protocols {
|
||||
protocols := new(http.Protocols)
|
||||
protocols.SetHTTP1(http1)
|
||||
protocols.SetUnencryptedHTTP2(unencryptedHTTP2)
|
||||
return protocols
|
||||
}
|
||||
|
||||
func TestCheckRouteAuthByRequest(t *testing.T) {
|
||||
rc := &RouteConfig{
|
||||
Username: "alice",
|
||||
|
||||
@@ -300,10 +300,14 @@ func NewService(cfg *v1.ServerConfig) (*Service, error) {
|
||||
svr.rc.HTTPReverseProxy = rp
|
||||
|
||||
address := net.JoinHostPort(cfg.ProxyBindAddr, strconv.Itoa(cfg.VhostHTTPPort))
|
||||
protocols := new(http.Protocols)
|
||||
protocols.SetHTTP1(true)
|
||||
protocols.SetUnencryptedHTTP2(true)
|
||||
server := &http.Server{
|
||||
Addr: address,
|
||||
Handler: rp,
|
||||
ReadHeaderTimeout: 60 * time.Second,
|
||||
Protocols: protocols,
|
||||
}
|
||||
var l net.Listener
|
||||
if httpMuxOn {
|
||||
|
||||
@@ -18,12 +18,14 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fatedier/golib/net/mux"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/fatedier/frp/pkg/auth"
|
||||
@@ -77,6 +79,71 @@ func TestWriteWithDeadlineTimesOutAndClearsDeadline(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSharedPortHTTPListenerProtocols(t *testing.T) {
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
|
||||
sharedMux := mux.NewMux(listener)
|
||||
httpListener := sharedMux.ListenHTTP(1)
|
||||
muxServeErr := make(chan error, 1)
|
||||
go func() {
|
||||
muxServeErr <- sharedMux.Serve()
|
||||
}()
|
||||
|
||||
newProtocols := func(http1, unencryptedHTTP2 bool) *http.Protocols {
|
||||
protocols := new(http.Protocols)
|
||||
protocols.SetHTTP1(http1)
|
||||
protocols.SetUnencryptedHTTP2(unencryptedHTTP2)
|
||||
return protocols
|
||||
}
|
||||
|
||||
const handlerProtocolHeader = "X-Test-Handler-Protocol"
|
||||
httpServer := &http.Server{
|
||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set(handlerProtocolHeader, r.Proto)
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}),
|
||||
ReadHeaderTimeout: time.Second,
|
||||
Protocols: newProtocols(true, true),
|
||||
}
|
||||
httpServeErr := make(chan error, 1)
|
||||
go func() {
|
||||
httpServeErr <- httpServer.Serve(httpListener)
|
||||
}()
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, httpServer.Close())
|
||||
require.ErrorIs(t, waitForResult(t, httpServeErr, "shared HTTP server to stop"), http.ErrServerClosed)
|
||||
require.NoError(t, sharedMux.Close())
|
||||
require.ErrorIs(t, waitForResult(t, muxServeErr, "shared mux to stop"), net.ErrClosed)
|
||||
})
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
http1 bool
|
||||
unencryptedHTTP2 bool
|
||||
expectedProtocol string
|
||||
}{
|
||||
{name: "HTTP/1.1", http1: true, expectedProtocol: "HTTP/1.1"},
|
||||
{name: "HTTP/2 prior knowledge", unencryptedHTTP2: true, expectedProtocol: "HTTP/2.0"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
transport := &http.Transport{
|
||||
Protocols: newProtocols(tc.http1, tc.unencryptedHTTP2),
|
||||
}
|
||||
defer transport.CloseIdleConnections()
|
||||
client := &http.Client{Transport: transport, Timeout: 3 * time.Second}
|
||||
request, err := http.NewRequestWithContext(t.Context(), http.MethodGet, "http://"+listener.Addr().String()+"/", nil)
|
||||
require.NoError(t, err)
|
||||
response, err := client.Do(request)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusNoContent, response.StatusCode)
|
||||
require.Equal(t, tc.expectedProtocol, response.Proto)
|
||||
require.Equal(t, tc.expectedProtocol, response.Header.Get(handlerProtocolHeader))
|
||||
require.NoError(t, response.Body.Close())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceControlHandoffSkipsStalePendingGeneration(t *testing.T) {
|
||||
svr := newControlTestService(t)
|
||||
metrics := newCountingServerMetrics()
|
||||
|
||||
Reference in New Issue
Block a user