From 0a70a6a2d944640500edcba4af8aad1a563caf82 Mon Sep 17 00:00:00 2001 From: sword-jin Date: Thu, 28 Nov 2024 02:40:12 +0000 Subject: [PATCH] config: add VhostEnableH2C flag to support HTTP/2 cleartext upgrade --- pkg/config/flags.go | 1 + pkg/config/legacy/conversion.go | 1 + pkg/config/legacy/server.go | 5 +++ pkg/config/v1/server.go | 5 +++ pkg/util/vhost/http.go | 70 +++++++++++++++++++++------------ server/dashboard_api.go | 2 + server/service.go | 1 + 7 files changed, 60 insertions(+), 25 deletions(-) diff --git a/pkg/config/flags.go b/pkg/config/flags.go index f9d9e3e4..09d8f145 100644 --- a/pkg/config/flags.go +++ b/pkg/config/flags.go @@ -230,6 +230,7 @@ func RegisterServerConfigFlags(cmd *cobra.Command, c *v1.ServerConfig, opts ...R cmd.PersistentFlags().IntVarP(&c.QUICBindPort, "quic_bind_port", "", 0, "quic bind udp port") cmd.PersistentFlags().StringVarP(&c.ProxyBindAddr, "proxy_bind_addr", "", "0.0.0.0", "proxy bind address") cmd.PersistentFlags().IntVarP(&c.VhostHTTPPort, "vhost_http_port", "", 0, "vhost http port") + cmd.PersistentFlags().BoolVarP(&c.VhostEnableH2C, "vhost_enable_h2c", "", false, "vhost enable h2c") cmd.PersistentFlags().IntVarP(&c.VhostHTTPSPort, "vhost_https_port", "", 0, "vhost https port") cmd.PersistentFlags().Int64VarP(&c.VhostHTTPTimeout, "vhost_http_timeout", "", 60, "vhost http response header timeout") cmd.PersistentFlags().StringVarP(&c.WebServer.Addr, "dashboard_addr", "", "0.0.0.0", "dashboard address") diff --git a/pkg/config/legacy/conversion.go b/pkg/config/legacy/conversion.go index dd8c4a11..a9f0e911 100644 --- a/pkg/config/legacy/conversion.go +++ b/pkg/config/legacy/conversion.go @@ -110,6 +110,7 @@ func Convert_ServerCommonConf_To_v1(conf *ServerCommonConf) *v1.ServerConfig { out.ProxyBindAddr = conf.ProxyBindAddr out.VhostHTTPPort = conf.VhostHTTPPort + out.VhostEnableH2C = conf.VhostEnableH2C out.VhostHTTPSPort = conf.VhostHTTPSPort out.TCPMuxHTTPConnectPort = conf.TCPMuxHTTPConnectPort out.TCPMuxPassthrough = conf.TCPMuxPassthrough diff --git a/pkg/config/legacy/server.go b/pkg/config/legacy/server.go index c58f76ad..8798d3d8 100644 --- a/pkg/config/legacy/server.go +++ b/pkg/config/legacy/server.go @@ -61,6 +61,11 @@ type ServerCommonConf struct { // requests. If this value is 0, the server will not listen for HTTP // requests. By default, this value is 0. VhostHTTPPort int `ini:"vhost_http_port" json:"vhost_http_port"` + // VhostEnableH2c specifies whether to enable HTTP/2 cleartext upgrade. + // By default, this value is false. If enabled, the server will be compatible + // with http2 on cleartext, accordingly, the vhost server will use http2 + // transport to proxy the request. + VhostEnableH2C bool `json:"vhostEnableH2C,omitempty"` // VhostHTTPSPort specifies the port that the server listens for HTTPS // Vhost requests. If this value is 0, the server will not listen for HTTPS // requests. By default, this value is 0. diff --git a/pkg/config/v1/server.go b/pkg/config/v1/server.go index 3108cd34..8946eafe 100644 --- a/pkg/config/v1/server.go +++ b/pkg/config/v1/server.go @@ -47,6 +47,11 @@ type ServerConfig struct { // VhostHTTPTimeout specifies the response header timeout for the Vhost // HTTP server, in seconds. By default, this value is 60. VhostHTTPTimeout int64 `json:"vhostHTTPTimeout,omitempty"` + // VhostEnableH2c specifies whether to enable HTTP/2 cleartext upgrade. + // By default, this value is false. If enabled, the server will be compatible + // with http2 on cleartext, accordingly, the vhost server will use http2 + // transport to proxy the request. + VhostEnableH2C bool `json:"vhostEnableH2C,omitempty"` // VhostHTTPSPort specifies the port that the server listens for HTTPS // Vhost requests. If this value is 0, the server will not listen for HTTPS // requests. diff --git a/pkg/util/vhost/http.go b/pkg/util/vhost/http.go index 30f9631e..87c5a43f 100644 --- a/pkg/util/vhost/http.go +++ b/pkg/util/vhost/http.go @@ -16,6 +16,7 @@ package vhost import ( "context" + "crypto/tls" "encoding/base64" "errors" "fmt" @@ -29,6 +30,8 @@ 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" @@ -38,10 +41,11 @@ var ErrNoRouteFound = errors.New("no route found") type HTTPReverseProxyOptions struct { ResponseHeaderTimeoutS int64 + EnableH2C bool } type HTTPReverseProxy struct { - proxy *httputil.ReverseProxy + proxy http.Handler vhostRouter *Routers responseHeaderTimeout time.Duration @@ -55,6 +59,40 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) * responseHeaderTimeout: time.Duration(option.ResponseHeaderTimeoutS) * time.Second, vhostRouter: vhostRouter, } + + var transport http.RoundTripper + // Create a connection to one proxy routed by route policy. + transport = &http.Transport{ + ResponseHeaderTimeout: rp.responseHeaderTimeout, + IdleConnTimeout: 60 * time.Second, + MaxIdleConnsPerHost: 5, + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return rp.CreateConnection(ctx.Value(RouteInfoKey).(*RequestRouteInfo), true) + }, + Proxy: func(req *http.Request) (*url.URL, error) { + // Use proxy mode if there is host in HTTP first request line. + // GET http://example.com/ HTTP/1.1 + // Host: example.com + // + // Normal: + // GET / HTTP/1.1 + // Host: example.com + urlHost := req.Context().Value(RouteInfoKey).(*RequestRouteInfo).URLHost + if urlHost != "" { + return req.URL, nil + } + return nil, nil + }, + } + if option.EnableH2C { + transport = &http2.Transport{ + AllowHTTP: true, + DialTLSContext: func(ctx context.Context, network, addr string, _ *tls.Config) (net.Conn, error) { + return rp.CreateConnection(ctx.Value(RouteInfoKey).(*RequestRouteInfo), true) + }, + } + } + proxy := &httputil.ReverseProxy{ // Modify incoming requests by route policies. Rewrite: func(r *httputil.ProxyRequest) { @@ -101,29 +139,7 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) * } return nil }, - // Create a connection to one proxy routed by route policy. - Transport: &http.Transport{ - ResponseHeaderTimeout: rp.responseHeaderTimeout, - IdleConnTimeout: 60 * time.Second, - MaxIdleConnsPerHost: 5, - DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { - return rp.CreateConnection(ctx.Value(RouteInfoKey).(*RequestRouteInfo), true) - }, - Proxy: func(req *http.Request) (*url.URL, error) { - // Use proxy mode if there is host in HTTP first request line. - // GET http://example.com/ HTTP/1.1 - // Host: example.com - // - // Normal: - // GET / HTTP/1.1 - // Host: example.com - urlHost := req.Context().Value(RouteInfoKey).(*RequestRouteInfo).URLHost - if urlHost != "" { - return req.URL, nil - } - return nil, nil - }, - }, + Transport: transport, BufferPool: pool.NewBuffer(32 * 1024), ErrorLog: stdlog.New(log.NewWriteLogger(log.WarnLevel, 2), "", 0), ErrorHandler: func(rw http.ResponseWriter, req *http.Request, err error) { @@ -138,7 +154,11 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) * _, _ = rw.Write(getNotFoundPageContent()) }, } - rp.proxy = proxy + if option.EnableH2C { + rp.proxy = h2c.NewHandler(proxy, &http2.Server{}) + } else { + rp.proxy = proxy + } return rp } diff --git a/server/dashboard_api.go b/server/dashboard_api.go index f34da4ef..8aeb8519 100644 --- a/server/dashboard_api.go +++ b/server/dashboard_api.go @@ -70,6 +70,7 @@ type serverInfoResp struct { Version string `json:"version"` BindPort int `json:"bindPort"` VhostHTTPPort int `json:"vhostHTTPPort"` + VhostEnableH2C bool `json:"vhostEnableH2C"` VhostHTTPSPort int `json:"vhostHTTPSPort"` TCPMuxHTTPConnectPort int `json:"tcpmuxHTTPConnectPort"` KCPBindPort int `json:"kcpBindPort"` @@ -111,6 +112,7 @@ func (svr *Service) apiServerInfo(w http.ResponseWriter, r *http.Request) { BindPort: svr.cfg.BindPort, VhostHTTPPort: svr.cfg.VhostHTTPPort, VhostHTTPSPort: svr.cfg.VhostHTTPSPort, + VhostEnableH2C: svr.cfg.VhostEnableH2C, TCPMuxHTTPConnectPort: svr.cfg.TCPMuxHTTPConnectPort, KCPBindPort: svr.cfg.KCPBindPort, QUICBindPort: svr.cfg.QUICBindPort, diff --git a/server/service.go b/server/service.go index 27c4110d..7eb5efcf 100644 --- a/server/service.go +++ b/server/service.go @@ -281,6 +281,7 @@ func NewService(cfg *v1.ServerConfig) (*Service, error) { if cfg.VhostHTTPPort > 0 { rp := vhost.NewHTTPReverseProxy(vhost.HTTPReverseProxyOptions{ ResponseHeaderTimeoutS: cfg.VhostHTTPTimeout, + EnableH2C: cfg.VhostEnableH2C, }, svr.httpVhostRouter) svr.rc.HTTPReverseProxy = rp