mirror of
https://github.com/fatedier/frp.git
synced 2026-07-24 05:39:19 +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
|
- legacy
|
||||||
- std-error-handling
|
- std-error-handling
|
||||||
rules:
|
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:
|
- linters:
|
||||||
- errcheck
|
- errcheck
|
||||||
- maligned
|
- maligned
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
## Fixes
|
## 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 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.
|
* 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 (
|
require (
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
||||||
github.com/coreos/go-oidc/v3 v3.14.1
|
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/google/uuid v1.6.0
|
||||||
github.com/gorilla/mux v1.8.1
|
github.com/gorilla/mux v1.8.1
|
||||||
github.com/gorilla/websocket v1.5.0
|
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.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
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/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.1-0.20260722102903-f59d12dbf4a6 h1:ZoGMMzVe+PqeJfLG2xxh1OjAcgpObIqrUqDAkgL9618=
|
||||||
github.com/fatedier/golib v0.8.0/go.mod h1:ArUGvPg2cOw/py2RAuBt46nNZH2VQ5Z70p109MAZpJw=
|
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 h1:u92UUy6FURPmNsMBUuongRWC0rBqN6gd01Dzu+D21NE=
|
||||||
github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6/go.mod h1:c5/tk6G0dSpXGzJN7Wk1OEie8grdSJAmeawId9Zvd34=
|
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=
|
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"
|
libio "github.com/fatedier/golib/io"
|
||||||
"github.com/fatedier/golib/pool"
|
"github.com/fatedier/golib/pool"
|
||||||
"golang.org/x/net/http2"
|
|
||||||
"golang.org/x/net/http2/h2c"
|
|
||||||
|
|
||||||
httppkg "github.com/fatedier/frp/pkg/util/http"
|
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||||
"github.com/fatedier/frp/pkg/util/log"
|
"github.com/fatedier/frp/pkg/util/log"
|
||||||
@@ -139,7 +137,7 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) *
|
|||||||
_, _ = rw.Write(getNotFoundPageContent())
|
_, _ = rw.Write(getNotFoundPageContent())
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
rp.proxy = h2c.NewHandler(proxy, &http2.Server{})
|
rp.proxy = proxy
|
||||||
return rp
|
return rp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,94 @@
|
|||||||
package vhost
|
package vhost
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
httppkg "github.com/fatedier/frp/pkg/util/http"
|
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) {
|
func TestCheckRouteAuthByRequest(t *testing.T) {
|
||||||
rc := &RouteConfig{
|
rc := &RouteConfig{
|
||||||
Username: "alice",
|
Username: "alice",
|
||||||
|
|||||||
@@ -300,10 +300,14 @@ func NewService(cfg *v1.ServerConfig) (*Service, error) {
|
|||||||
svr.rc.HTTPReverseProxy = rp
|
svr.rc.HTTPReverseProxy = rp
|
||||||
|
|
||||||
address := net.JoinHostPort(cfg.ProxyBindAddr, strconv.Itoa(cfg.VhostHTTPPort))
|
address := net.JoinHostPort(cfg.ProxyBindAddr, strconv.Itoa(cfg.VhostHTTPPort))
|
||||||
|
protocols := new(http.Protocols)
|
||||||
|
protocols.SetHTTP1(true)
|
||||||
|
protocols.SetUnencryptedHTTP2(true)
|
||||||
server := &http.Server{
|
server := &http.Server{
|
||||||
Addr: address,
|
Addr: address,
|
||||||
Handler: rp,
|
Handler: rp,
|
||||||
ReadHeaderTimeout: 60 * time.Second,
|
ReadHeaderTimeout: 60 * time.Second,
|
||||||
|
Protocols: protocols,
|
||||||
}
|
}
|
||||||
var l net.Listener
|
var l net.Listener
|
||||||
if httpMuxOn {
|
if httpMuxOn {
|
||||||
|
|||||||
@@ -18,12 +18,14 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/fatedier/golib/net/mux"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/fatedier/frp/pkg/auth"
|
"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) {
|
func TestServiceControlHandoffSkipsStalePendingGeneration(t *testing.T) {
|
||||||
svr := newControlTestService(t)
|
svr := newControlTestService(t)
|
||||||
metrics := newCountingServerMetrics()
|
metrics := newCountingServerMetrics()
|
||||||
|
|||||||
Reference in New Issue
Block a user