diff --git a/docs/content/reference/dynamic-configuration/file.toml b/docs/content/reference/dynamic-configuration/file.toml index ea5e29fcf..6f2cd4d52 100644 --- a/docs/content/reference/dynamic-configuration/file.toml +++ b/docs/content/reference/dynamic-configuration/file.toml @@ -275,6 +275,7 @@ insecureSkipVerify = true rootCAs = ["foobar", "foobar"] maxIdleConnsPerHost = 42 + disableHTTP2 = true [[http.serversTransports.ServersTransport0.certificates]] certFile = "foobar" @@ -292,6 +293,7 @@ insecureSkipVerify = true rootCAs = ["foobar", "foobar"] maxIdleConnsPerHost = 42 + disableHTTP2 = true [[http.serversTransports.ServersTransport1.certificates]] certFile = "foobar" diff --git a/docs/content/reference/dynamic-configuration/file.yaml b/docs/content/reference/dynamic-configuration/file.yaml index f707c4a03..563eb3cf2 100644 --- a/docs/content/reference/dynamic-configuration/file.yaml +++ b/docs/content/reference/dynamic-configuration/file.yaml @@ -327,6 +327,7 @@ http: dialTimeout: 42s responseHeaderTimeout: 42s idleConnTimeout: 42s + disableHTTP2: true ServersTransport1: serverName: foobar insecureSkipVerify: true @@ -343,6 +344,7 @@ http: dialTimeout: 42s responseHeaderTimeout: 42s idleConnTimeout: 42s + disableHTTP2: true tcp: routers: TCPRouter0: diff --git a/docs/content/reference/dynamic-configuration/kubernetes-crd-resource.yml b/docs/content/reference/dynamic-configuration/kubernetes-crd-resource.yml index 332853401..56c5e7419 100644 --- a/docs/content/reference/dynamic-configuration/kubernetes-crd-resource.yml +++ b/docs/content/reference/dynamic-configuration/kubernetes-crd-resource.yml @@ -214,3 +214,4 @@ spec: dialTimeout: 42s responseHeaderTimeout: 42s idleConnTimeout: 42s + disableHTTP2: true diff --git a/docs/content/reference/dynamic-configuration/traefik.containo.us_serverstransports.yaml b/docs/content/reference/dynamic-configuration/traefik.containo.us_serverstransports.yaml index 43c9e6db9..0e7efb766 100644 --- a/docs/content/reference/dynamic-configuration/traefik.containo.us_serverstransports.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.containo.us_serverstransports.yaml @@ -37,6 +37,9 @@ spec: items: type: string type: array + disableHTTP2: + description: Disable HTTP/2 for connections with backend servers. + type: boolean forwardingTimeouts: description: Timeouts for requests forwarded to the backend servers. properties: diff --git a/docs/content/routing/services/index.md b/docs/content/routing/services/index.md index 2d69d42e6..5add1bbbd 100644 --- a/docs/content/routing/services/index.md +++ b/docs/content/routing/services/index.md @@ -705,6 +705,37 @@ spec: maxIdleConnsPerHost: 7 ``` +#### `disableHTTP2` + +_Optional, Default=false_ + +`disableHTTP2` disables HTTP/2 for connections with backend servers. + +```toml tab="File (TOML)" +## Dynamic configuration +[http.serversTransports.mytransport] + disableHTTP2 = true +``` + +```yaml tab="File (YAML)" +## Dynamic configuration +http: + serversTransports: + mytransport: + disableHTTP2: true +``` + +```yaml tab="Kubernetes" +apiVersion: traefik.containo.us/v1alpha1 +kind: ServersTransport +metadata: + name: mytransport + namespace: default + +spec: + disableHTTP2: true +``` + #### `forwardingTimeouts` `forwardingTimeouts` is about a number of timeouts relevant to when forwarding requests to the backend servers. diff --git a/integration/fixtures/k8s/01-traefik-crd.yml b/integration/fixtures/k8s/01-traefik-crd.yml index 53b7a921c..956e36f2f 100644 --- a/integration/fixtures/k8s/01-traefik-crd.yml +++ b/integration/fixtures/k8s/01-traefik-crd.yml @@ -950,6 +950,9 @@ spec: items: type: string type: array + disableHTTP2: + description: Disable HTTP/2 for connections with backend servers. + type: boolean forwardingTimeouts: description: Timeouts for requests forwarded to the backend servers. properties: diff --git a/pkg/config/dynamic/http_config.go b/pkg/config/dynamic/http_config.go index 1d727a2b8..ec359a656 100644 --- a/pkg/config/dynamic/http_config.go +++ b/pkg/config/dynamic/http_config.go @@ -208,6 +208,7 @@ type ServersTransport struct { Certificates tls.Certificates `description:"Certificates for mTLS." json:"certificates,omitempty" toml:"certificates,omitempty" yaml:"certificates,omitempty" export:"true"` MaxIdleConnsPerHost int `description:"If non-zero, controls the maximum idle (keep-alive) to keep per-host. If zero, DefaultMaxIdleConnsPerHost is used" json:"maxIdleConnsPerHost,omitempty" toml:"maxIdleConnsPerHost,omitempty" yaml:"maxIdleConnsPerHost,omitempty" export:"true"` ForwardingTimeouts *ForwardingTimeouts `description:"Timeouts for requests forwarded to the backend servers." json:"forwardingTimeouts,omitempty" toml:"forwardingTimeouts,omitempty" yaml:"forwardingTimeouts,omitempty" export:"true"` + DisableHTTP2 bool `description:"Disable HTTP/2 for connections with backend servers." json:"disableHTTP2,omitempty" toml:"disableHTTP2,omitempty" yaml:"disableHTTP2,omitempty" export:"true"` } // +k8s:deepcopy-gen=true diff --git a/pkg/provider/kubernetes/crd/traefik/v1alpha1/serverstransport.go b/pkg/provider/kubernetes/crd/traefik/v1alpha1/serverstransport.go index 130598214..dd8b00ff5 100644 --- a/pkg/provider/kubernetes/crd/traefik/v1alpha1/serverstransport.go +++ b/pkg/provider/kubernetes/crd/traefik/v1alpha1/serverstransport.go @@ -33,6 +33,8 @@ type ServersTransportSpec struct { MaxIdleConnsPerHost int `json:"maxIdleConnsPerHost,omitempty"` // Timeouts for requests forwarded to the backend servers. ForwardingTimeouts *ForwardingTimeouts `json:"forwardingTimeouts,omitempty"` + // Disable HTTP/2 for connections with backend servers. + DisableHTTP2 bool `json:"disableHTTP2,omitempty"` } // +k8s:deepcopy-gen=true diff --git a/pkg/server/service/roundtripper.go b/pkg/server/service/roundtripper.go index 96384c4bc..364cd9e69 100644 --- a/pkg/server/service/roundtripper.go +++ b/pkg/server/service/roundtripper.go @@ -100,7 +100,7 @@ func (r *RoundTripperManager) Get(name string) (http.RoundTripper, error) { // createRoundTripper creates an http.RoundTripper configured with the Transport configuration settings. // For the settings that can't be configured in Traefik it uses the default http.Transport settings. -// An exception to this is the MaxIdleConns setting as we only provide the option MaxIdleConnsPerHostin Traefik at this point in time. +// An exception to this is the MaxIdleConns setting as we only provide the option MaxIdleConnsPerHost in Traefik at this point in time. // Setting this value to the default of 100 could lead to confusing behavior and backwards compatibility issues. func createRoundTripper(cfg *dynamic.ServersTransport) (http.RoundTripper, error) { if cfg == nil { @@ -127,15 +127,6 @@ func createRoundTripper(cfg *dynamic.ServersTransport) (http.RoundTripper, error WriteBufferSize: 64 * 1024, } - transport.RegisterProtocol("h2c", &h2cTransportWrapper{ - Transport: &http2.Transport{ - DialTLS: func(netw, addr string, cfg *tls.Config) (net.Conn, error) { - return net.Dial(netw, addr) - }, - AllowHTTP: true, - }, - }) - if cfg.ForwardingTimeouts != nil { transport.ResponseHeaderTimeout = time.Duration(cfg.ForwardingTimeouts.ResponseHeaderTimeout) transport.IdleConnTimeout = time.Duration(cfg.ForwardingTimeouts.IdleConnTimeout) @@ -150,6 +141,20 @@ func createRoundTripper(cfg *dynamic.ServersTransport) (http.RoundTripper, error } } + // Return directly HTTP/1.1 transport when HTTP/2 is disabled + if cfg.DisableHTTP2 { + return transport, nil + } + + transport.RegisterProtocol("h2c", &h2cTransportWrapper{ + Transport: &http2.Transport{ + DialTLS: func(netw, addr string, cfg *tls.Config) (net.Conn, error) { + return net.Dial(netw, addr) + }, + AllowHTTP: true, + }, + }) + return newSmartRoundTripper(transport) } diff --git a/pkg/server/service/roundtripper_test.go b/pkg/server/service/roundtripper_test.go index 7049bbf83..b58a26e33 100644 --- a/pkg/server/service/roundtripper_test.go +++ b/pkg/server/service/roundtripper_test.go @@ -227,3 +227,69 @@ func TestMTLS(t *testing.T) { assert.Equal(t, http.StatusOK, resp.StatusCode) } + +func TestDisableHTTP2(t *testing.T) { + testCases := []struct { + desc string + disableHTTP2 bool + serverHTTP2 bool + expectedProto string + }{ + { + desc: "HTTP1 capable client with HTTP1 server", + disableHTTP2: true, + expectedProto: "HTTP/1.1", + }, + { + desc: "HTTP1 capable client with HTTP2 server", + disableHTTP2: true, + serverHTTP2: true, + expectedProto: "HTTP/1.1", + }, + { + desc: "HTTP2 capable client with HTTP1 server", + expectedProto: "HTTP/1.1", + }, + { + desc: "HTTP2 capable client with HTTP2 server", + serverHTTP2: true, + expectedProto: "HTTP/2.0", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + srv := httptest.NewUnstartedServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(http.StatusOK) + })) + + srv.EnableHTTP2 = test.serverHTTP2 + srv.StartTLS() + + rtManager := NewRoundTripperManager() + + dynamicConf := map[string]*dynamic.ServersTransport{ + "test": { + DisableHTTP2: test.disableHTTP2, + InsecureSkipVerify: true, + }, + } + + rtManager.Update(dynamicConf) + + tr, err := rtManager.Get("test") + require.NoError(t, err) + + client := http.Client{Transport: tr} + + resp, err := client.Get(srv.URL) + require.NoError(t, err) + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, test.expectedProto, resp.Proto) + }) + } +}