diff --git a/.goreleaser.yml b/.goreleaser.yml index a2fbee302..a2fd7a3f2 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -7,7 +7,7 @@ before: builds: - binary: traefik - main: ./cmd/traefik/traefik.go + main: ./cmd/traefik/ env: - CGO_ENABLED=0 ldflags: diff --git a/CHANGELOG.md b/CHANGELOG.md index 17cf18976..d193e1a66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,54 @@ +## [v2.3.0-rc3](https://github.com/containous/traefik/tree/v2.3.0-rc3) (2020-07-28) +[All Commits](https://github.com/containous/traefik/compare/v2.3.0-rc2...v2.3.0-rc3) + +**Bug fixes:** +- **[k8s,k8s/ingress]** Support Kubernetes Ingress pathType ([#7087](https://github.com/containous/traefik/pull/7087) by [rtribotte](https://github.com/rtribotte)) +- **[k8s,k8s/ingress]** Use semantic versioning to enable ingress class support ([#7065](https://github.com/containous/traefik/pull/7065) by [kevinpollet](https://github.com/kevinpollet)) +- **[provider]** file parser: skip nil value. ([#7058](https://github.com/containous/traefik/pull/7058) by [ldez](https://github.com/ldez)) + +**Documentation:** +- **[ecs]** Fix documentation for ECS ([#7107](https://github.com/containous/traefik/pull/7107) by [mmatur](https://github.com/mmatur)) +- **[k8s]** Add migration documentation for IngressClass ([#7083](https://github.com/containous/traefik/pull/7083) by [kevinpollet](https://github.com/kevinpollet)) +- **[plugins]** Update availability info ([#7060](https://github.com/containous/traefik/pull/7060) by [PCM2](https://github.com/PCM2)) + +**Misc:** +- Merge current v2.2 branch into v2.3 ([#7116](https://github.com/containous/traefik/pull/7116) by [ldez](https://github.com/ldez)) +- Merge current v2.2 branch into v2.3 ([#7086](https://github.com/containous/traefik/pull/7086) by [jbdoumenjou](https://github.com/jbdoumenjou)) + +## [v2.2.8](https://github.com/containous/traefik/tree/v2.2.8) (2020-07-28) +[All Commits](https://github.com/containous/traefik/compare/v2.2.7...v2.2.8) + +**Bug fixes:** +- **[webui]** fix: clean X-Forwarded-Prefix header for the dashboard. ([#7109](https://github.com/containous/traefik/pull/7109) by [ldez](https://github.com/ldez)) + +**Documentation:** +- **[docker]** spelling(docs/content/routing/providers/docker.md) ([#7101](https://github.com/containous/traefik/pull/7101) by [szczot3k](https://github.com/szczot3k)) +- **[k8s]** doc: add name of used key for kubernetes client auth ([#7068](https://github.com/containous/traefik/pull/7068) by [smueller18](https://github.com/smueller18)) + +## [v2.2.7](https://github.com/containous/traefik/tree/v2.2.7) (2020-07-20) +[All Commits](https://github.com/containous/traefik/compare/v2.2.6...v2.2.7) + +**Bug fixes:** +- **[server,tls]** fix: drop host port to compare with SNI. ([#7071](https://github.com/containous/traefik/pull/7071) by [ldez](https://github.com/ldez)) + +## [v2.2.6](https://github.com/containous/traefik/tree/v2.2.6) (2020-07-17) +[All Commits](https://github.com/containous/traefik/compare/v2.2.5...v2.2.6) + +**Bug fixes:** +- **[logs]** fix: access logs header names filtering is case insensitive ([#6900](https://github.com/containous/traefik/pull/6900) by [mjeanroy](https://github.com/mjeanroy)) +- **[provider]** Get Entrypoints Port Address without protocol for redirect ([#7047](https://github.com/containous/traefik/pull/7047) by [SantoDE](https://github.com/SantoDE)) +- **[tls]** Fix domain fronting ([#7064](https://github.com/containous/traefik/pull/7064) by [juliens](https://github.com/juliens)) + +**Documentation:** +- fix: documentation references. ([#7049](https://github.com/containous/traefik/pull/7049) by [ldez](https://github.com/ldez)) +- Add example for entrypoint on one ip address ([#6483](https://github.com/containous/traefik/pull/6483) by [SimonHeimberg](https://github.com/SimonHeimberg)) + +## [v2.3.0-rc2](https://github.com/containous/traefik/tree/v2.3.0-rc2) (2020-07-15) +[All Commits](https://github.com/containous/traefik/compare/v2.3.0-rc1...v2.3.0-rc2) + +**Misc:** +- fix: goreleaser build commands. + ## [v2.3.0-rc1](https://github.com/containous/traefik/tree/v2.3.0-rc1) (2020-07-15) [All Commits](https://github.com/containous/traefik/compare/v2.2.0-rc1...v2.3.0-rc1) diff --git a/docs/content/https/acme.md b/docs/content/https/acme.md index de1efe94b..34aa0a49d 100644 --- a/docs/content/https/acme.md +++ b/docs/content/https/acme.md @@ -362,7 +362,7 @@ For complete details, refer to your provider's _Additional configuration_ link. | [Zonomi](https://zonomi.com) | `zonomi` | `ZONOMI_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/zonomi) | [^1]: more information about the HTTP message format can be found [here](https://go-acme.github.io/lego/dns/httpreq/) -[^2]: [providing_credentials_to_your_application](https://cloud.google.com/docs/authentication/production#providing_credentials_to_your_application) +[^2]: [providing_credentials_to_your_application](https://cloud.google.com/docs/authentication/production) [^3]: [google/default.go](https://github.com/golang/oauth2/blob/36a7019397c4c86cf59eeab3bc0d188bac444277/google/default.go#L61-L76) [^4]: `docker stack` remark: there is no way to support terminal attached to container when deploying with `docker stack`, so you might need to run container with `docker run -it` to generate certificates using `manual` provider. [^5]: The `Global API Key` needs to be used, not the `Origin CA Key`. diff --git a/docs/content/https/tls.md b/docs/content/https/tls.md index 40d0585b4..9b1023386 100644 --- a/docs/content/https/tls.md +++ b/docs/content/https/tls.md @@ -428,6 +428,7 @@ metadata: spec: clientAuth: + # the CA certificate is extracted from key `tls.ca` of the given secrets. secretNames: - secretCA clientAuthType: RequireAndVerifyClientCert diff --git a/docs/content/migration/v2.md b/docs/content/migration/v2.md index c6fd2d983..f8ced1a5e 100644 --- a/docs/content/migration/v2.md +++ b/docs/content/migration/v2.md @@ -320,3 +320,8 @@ Since `v2.2.5` this rule has been removed, and you should not use it anymore. ### File Provider The file parser has been changed, since v2.3 the unknown options/fields in a dynamic configuration file are treated as errors. + +### IngressClass + +In `v2.3`, the support of `IngressClass`, which is available since Kubernetes version `1.18`, has been introduced. +In order to be able to use this new resource the [Kubernetes RBAC](../reference/dynamic-configuration/kubernetes-crd.md#rbac) must be updated. diff --git a/docs/content/plugins/overview.md b/docs/content/plugins/overview.md index 902b61a75..afa5c2716 100644 --- a/docs/content/plugins/overview.md +++ b/docs/content/plugins/overview.md @@ -10,6 +10,9 @@ For example, Traefik plugins can add features to modify requests or headers, iss Traefik Pilot can also monitor connected Traefik instances and issue alerts when one is not responding, or when it is subject to security vulnerabilities. +!!! note "Availability" + Plugins are available for Traefik v2.3.0-rc1 and later. + !!! danger "Experimental Features" Plugins can potentially modify the behavior of Traefik in unforeseen ways. Exercise caution when adding new plugins to production Traefik instances. diff --git a/docs/content/plugins/using-plugins.md b/docs/content/plugins/using-plugins.md index d80e92b08..65810a9b6 100644 --- a/docs/content/plugins/using-plugins.md +++ b/docs/content/plugins/using-plugins.md @@ -1,6 +1,6 @@ # Using Plugins -Since v2.3, plugins are available to any Traefik instance that is [registered](overview.md#connecting-to-traefik-pilot) with Traefik Pilot. +Plugins are available to any instance of Traefik v2.3 or later that is [registered](overview.md#connecting-to-traefik-pilot) with Traefik Pilot. Plugins are hosted on GitHub, but you can browse plugins to add to your registered Traefik instances from the Traefik Pilot UI. !!! danger "Experimental Features" diff --git a/docs/content/providers/ecs.md b/docs/content/providers/ecs.md index f4720640c..22e4fd57a 100644 --- a/docs/content/providers/ecs.md +++ b/docs/content/providers/ecs.md @@ -79,9 +79,35 @@ providers: # ... ``` -Search for services in all clusters. -If set to true the configured clusters will be ignored and the clusters will be discovered. -If set to false the services will be discovered only in configured clusters. +Search for services in clusters list. + +- If set to `true` the configured clusters will be ignored and the clusters will be discovered. +- If set to `false` the services will be discovered only in configured clusters. + +### `clusters` + +_Optional, Default=["default"]_ + +```toml tab="File (TOML)" +[providers.ecs] + cluster = ["default"] + # ... +``` + +```yaml tab="File (YAML)" +providers: + ecs: + clusters: + - default + # ... +``` + +```bash tab="CLI" +--providers.ecs.clusters=default +# ... +``` + +Search for services in clusters list. ### `exposedByDefault` diff --git a/docs/content/providers/kubernetes-ingress.md b/docs/content/providers/kubernetes-ingress.md index b0dea1fdc..c5a855147 100644 --- a/docs/content/providers/kubernetes-ingress.md +++ b/docs/content/providers/kubernetes-ingress.md @@ -258,16 +258,13 @@ Value of `kubernetes.io/ingress.class` annotation that identifies Ingress object If the parameter is non-empty, only Ingresses containing an annotation with the same value are processed. Otherwise, Ingresses missing the annotation, having an empty value, or with the value `traefik` are processed. -#### ingressClass on Kubernetes 1.18+ +!!! info "Kubernetes 1.18+" -If you cluster is running kubernetes 1.18+, -you can also leverage the newly Introduced `IngressClass` resource to define which Ingress Objects to handle. -In that case, Traefik will look for an `IngressClass` in your cluster with the controller of *traefik.io/ingress-controller* inside the spec. - -!!! note "" - Please note, the ingressClass configuration on the provider is not used then anymore. - -Please see [this article](https://kubernetes.io/blog/2020/04/02/improvements-to-the-ingress-api-in-kubernetes-1.18/) for more information. + If the Kubernetes cluster version is 1.18+, + the new `IngressClass` resource can be leveraged to identify Ingress objects that should be processed. + In that case, Traefik will look for an `IngressClass` in the cluster with the controller value equal to *traefik.io/ingress-controller*. + + Please see [this article](https://kubernetes.io/blog/2020/04/02/improvements-to-the-ingress-api-in-kubernetes-1.18/) for more information. ### `ingressEndpoint` diff --git a/docs/content/reference/dynamic-configuration/kubernetes-crd-rbac.yml b/docs/content/reference/dynamic-configuration/kubernetes-crd-rbac.yml index 3e7337031..88b788089 100644 --- a/docs/content/reference/dynamic-configuration/kubernetes-crd-rbac.yml +++ b/docs/content/reference/dynamic-configuration/kubernetes-crd-rbac.yml @@ -18,6 +18,7 @@ rules: - extensions resources: - ingresses + - ingressclasses verbs: - get - list diff --git a/docs/content/routing/entrypoints.md b/docs/content/routing/entrypoints.md index 52290000e..dc6d97d55 100644 --- a/docs/content/routing/entrypoints.md +++ b/docs/content/routing/entrypoints.md @@ -168,7 +168,7 @@ The format is: If both TCP and UDP are wanted for the same port, two entryPoints definitions are needed, such as in the example below. -??? example "Both TCP and UDP on port 3179" +??? example "Both TCP and UDP on Port 3179" ```toml tab="File (TOML)" ## Static configuration @@ -194,6 +194,30 @@ If both TCP and UDP are wanted for the same port, two entryPoints definitions ar --entryPoints.udpep.address=:3179/udp ``` +??? example "Listen on Specific IP Addresses Only" + + ```toml tab="File (TOML)" + [entryPoints.specificIPv4] + address = "192.168.2.7:8888" + [entryPoints.specificIPv6] + address = "[2001:db8::1]:8888" + ``` + + ```yaml tab="File (yaml)" + entryPoints: + specificIPv4: + address: "192.168.2.7:8888" + specificIPv6: + address: "[2001:db8::1]:8888" + ``` + + ```bash tab="CLI" + entrypoints.specificIPv4.address=192.168.2.7:8888 + entrypoints.specificIPv6.address=[2001:db8::1]:8888 + ``` + + Full details for how to specify `address` can be found in [net.Listen](https://golang.org/pkg/net/#Listen) (and [net.Dial](https://golang.org/pkg/net/#Dial)) of the doc for go. + ### Forwarded Headers You can configure Traefik to trust the forwarded headers information (`X-Forwarded-*`). diff --git a/docs/content/routing/providers/docker.md b/docs/content/routing/providers/docker.md index e19aafaa5..99128c188 100644 --- a/docs/content/routing/providers/docker.md +++ b/docs/content/routing/providers/docker.md @@ -535,7 +535,7 @@ You can declare UDP Routers and/or Services using labels. my-container: # ... labels: - - "traefik.udp.routers.my-router.entrypoint=udp" + - "traefik.udp.routers.my-router.entrypoints=udp" - "traefik.udp.services.my-service.loadbalancer.server.port=4123" ``` diff --git a/docs/content/routing/providers/kubernetes-ingress.md b/docs/content/routing/providers/kubernetes-ingress.md index 7b7ac27d0..83ef155b1 100644 --- a/docs/content/routing/providers/kubernetes-ingress.md +++ b/docs/content/routing/providers/kubernetes-ingress.md @@ -322,9 +322,23 @@ which in turn will create the resulting routers, services, handlers, etc. traefik.ingress.kubernetes.io/service.sticky.cookie.httponly: "true" ``` -### TLS +## Path Types on Kubernetes 1.18+ + +If the Kubernetes cluster version is 1.18+, +the new `pathType` property can be leveraged to define the rules matchers: -#### Communication Between Traefik and Pods +- `Exact`: This path type forces the rule matcher to `Path` +- `Prefix`: This path type forces the rule matcher to `PathPrefix` + +Please see [this documentation](https://kubernetes.io/docs/concepts/services-networking/ingress/#path-types) for more information. + +!!! warning "Multiple Matches" + In the case of multiple matches, Traefik will not ensure the priority of a Path matcher over a PathPrefix matcher, + as stated in [this documentation](https://kubernetes.io/docs/concepts/services-networking/ingress/#multiple-matches). + +## TLS + +### Communication Between Traefik and Pods Traefik automatically requests endpoint information based on the service provided in the ingress spec. Although Traefik will connect directly to the endpoints (pods), @@ -346,7 +360,7 @@ and will connect via TLS automatically. If this is not an option, you may need to skip TLS certificate verification. See the [insecureSkipVerify](../../routing/overview.md#insecureskipverify) setting for more details. -#### Certificates Management +### Certificates Management ??? example "Using a secret" diff --git a/docs/content/routing/routers/index.md b/docs/content/routing/routers/index.md index 81d5a4083..d97e079a7 100644 --- a/docs/content/routing/routers/index.md +++ b/docs/content/routing/routers/index.md @@ -472,6 +472,11 @@ It refers to a [TLS Options](../../https/tls.md#tls-options) and will be applied the TLS option is picked from the mapping mentioned above and based on the server name provided during the TLS handshake, and it all happens before routing actually occurs. +!!! info "Domain Fronting" + + In the case of domain fronting, + if the TLS options associated with the Host Header and the SNI are different then Traefik will respond with a status code `421`. + ??? example "Configuring the TLS options" ```toml tab="File (TOML)" diff --git a/integration/acme_test.go b/integration/acme_test.go index a5e34e695..9c757b61d 100644 --- a/integration/acme_test.go +++ b/integration/acme_test.go @@ -444,7 +444,7 @@ func (s *AcmeSuite) retrieveAcmeCertificate(c *check.C, testCase acmeTestCase) { // A real file is needed to have the right mode on acme.json file defer os.Remove("/tmp/acme.json") - backend := startTestServer("9010", http.StatusOK) + backend := startTestServer("9010", http.StatusOK, "") defer backend.Close() for _, sub := range testCase.subCases { diff --git a/integration/fixtures/https/https_domain_fronting.toml b/integration/fixtures/https/https_domain_fronting.toml new file mode 100644 index 000000000..80011ee8e --- /dev/null +++ b/integration/fixtures/https/https_domain_fronting.toml @@ -0,0 +1,53 @@ +[global] + checkNewVersion = false + sendAnonymousUsage = false + +[log] + level = "DEBUG" + +[entryPoints.websecure] + address = ":4443" + +[api] + insecure = true + +[providers.file] + filename = "{{ .SelfFilename }}" + +## dynamic configuration ## + +[http.routers.router1] + rule = "Host(`site1.www.snitest.com`)" + service = "service1" + [http.routers.router1.tls] + +[http.routers.router2] + rule = "Host(`site2.www.snitest.com`)" + service = "service2" + [http.routers.router2.tls] + +[http.routers.router3] + rule = "Host(`site3.www.snitest.com`)" + service = "service3" + [http.routers.router3.tls] + options = "mytls" + +[http.services.service1] + [[http.services.service1.loadBalancer.servers]] + url = "http://127.0.0.1:9010" + +[http.services.service2] + [[http.services.service2.loadBalancer.servers]] + url = "http://127.0.0.1:9020" + +[http.services.service3] + [[http.services.service3.loadBalancer.servers]] + url = "http://127.0.0.1:9030" + +[[tls.certificates]] + certFile = "fixtures/https/wildcard.www.snitest.com.cert" + keyFile = "fixtures/https/wildcard.www.snitest.com.key" + +[tls.options] + [tls.options.mytls] + maxVersion = "VersionTLS12" diff --git a/integration/headers_test.go b/integration/headers_test.go index b2e34beff..c464bf97c 100644 --- a/integration/headers_test.go +++ b/integration/headers_test.go @@ -35,7 +35,7 @@ func (s *HeadersSuite) TestCorsResponses(c *check.C) { c.Assert(err, checker.IsNil) defer cmd.Process.Kill() - backend := startTestServer("9000", http.StatusOK) + backend := startTestServer("9000", http.StatusOK, "") defer backend.Close() err = try.GetRequest(backend.URL, 500*time.Millisecond, try.StatusCodeIs(http.StatusOK)) @@ -124,7 +124,7 @@ func (s *HeadersSuite) TestSecureHeadersResponses(c *check.C) { c.Assert(err, checker.IsNil) defer cmd.Process.Kill() - backend := startTestServer("9000", http.StatusOK) + backend := startTestServer("9000", http.StatusOK, "") defer backend.Close() err = try.GetRequest(backend.URL, 500*time.Millisecond, try.StatusCodeIs(http.StatusOK)) @@ -173,7 +173,7 @@ func (s *HeadersSuite) TestMultipleSecureHeadersResponses(c *check.C) { c.Assert(err, checker.IsNil) defer cmd.Process.Kill() - backend := startTestServer("9000", http.StatusOK) + backend := startTestServer("9000", http.StatusOK, "") defer backend.Close() err = try.GetRequest(backend.URL, 500*time.Millisecond, try.StatusCodeIs(http.StatusOK)) diff --git a/integration/helloworld/helloworld.pb.go b/integration/helloworld/helloworld.pb.go index 96320f02a..61b302678 100644 --- a/integration/helloworld/helloworld.pb.go +++ b/integration/helloworld/helloworld.pb.go @@ -20,9 +20,7 @@ import ( math "math" proto "github.com/golang/protobuf/proto" -) -import ( context "context" grpc "google.golang.org/grpc" diff --git a/integration/https_test.go b/integration/https_test.go index 9ac854eba..e6c6c0553 100644 --- a/integration/https_test.go +++ b/integration/https_test.go @@ -73,8 +73,8 @@ func (s *HTTPSSuite) TestWithSNIConfigRoute(c *check.C) { err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`snitest.org`)")) c.Assert(err, checker.IsNil) - backend1 := startTestServer("9010", http.StatusNoContent) - backend2 := startTestServer("9020", http.StatusResetContent) + backend1 := startTestServer("9010", http.StatusNoContent, "") + backend2 := startTestServer("9020", http.StatusResetContent, "") defer backend1.Close() defer backend2.Close() @@ -129,8 +129,8 @@ func (s *HTTPSSuite) TestWithTLSOptions(c *check.C) { err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`snitest.org`)")) c.Assert(err, checker.IsNil) - backend1 := startTestServer("9010", http.StatusNoContent) - backend2 := startTestServer("9020", http.StatusResetContent) + backend1 := startTestServer("9010", http.StatusNoContent, "") + backend2 := startTestServer("9020", http.StatusResetContent, "") defer backend1.Close() defer backend2.Close() @@ -215,8 +215,8 @@ func (s *HTTPSSuite) TestWithConflictingTLSOptions(c *check.C) { err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`snitest.net`)")) c.Assert(err, checker.IsNil) - backend1 := startTestServer("9010", http.StatusNoContent) - backend2 := startTestServer("9020", http.StatusResetContent) + backend1 := startTestServer("9010", http.StatusNoContent, "") + backend2 := startTestServer("9020", http.StatusResetContent, "") defer backend1.Close() defer backend2.Close() @@ -733,9 +733,12 @@ func (s *HTTPSSuite) TestWithRootCAsFileForHTTPSOnBackend(c *check.C) { c.Assert(err, checker.IsNil) } -func startTestServer(port string, statusCode int) (ts *httptest.Server) { +func startTestServer(port string, statusCode int, textContent string) (ts *httptest.Server) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(statusCode) + if textContent != "" { + _, _ = w.Write([]byte(textContent)) + } }) listener, err := net.Listen("tcp", "127.0.0.1:"+port) if err != nil { @@ -787,8 +790,8 @@ func (s *HTTPSSuite) TestWithSNIDynamicConfigRouteWithNoChange(c *check.C) { err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`"+tr1.TLSClientConfig.ServerName+"`)")) c.Assert(err, checker.IsNil) - backend1 := startTestServer("9010", http.StatusNoContent) - backend2 := startTestServer("9020", http.StatusResetContent) + backend1 := startTestServer("9010", http.StatusNoContent, "") + backend2 := startTestServer("9020", http.StatusResetContent, "") defer backend1.Close() defer backend2.Close() @@ -856,8 +859,8 @@ func (s *HTTPSSuite) TestWithSNIDynamicConfigRouteWithChange(c *check.C) { err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`"+tr2.TLSClientConfig.ServerName+"`)")) c.Assert(err, checker.IsNil) - backend1 := startTestServer("9010", http.StatusNoContent) - backend2 := startTestServer("9020", http.StatusResetContent) + backend1 := startTestServer("9010", http.StatusNoContent, "") + backend2 := startTestServer("9020", http.StatusResetContent, "") defer backend1.Close() defer backend2.Close() @@ -919,7 +922,7 @@ func (s *HTTPSSuite) TestWithSNIDynamicConfigRouteWithTlsConfigurationDeletion(c err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`"+tr2.TLSClientConfig.ServerName+"`)")) c.Assert(err, checker.IsNil) - backend2 := startTestServer("9020", http.StatusResetContent) + backend2 := startTestServer("9020", http.StatusResetContent, "") defer backend2.Close() @@ -1111,3 +1114,115 @@ func (s *HTTPSSuite) TestWithSNIDynamicCaseInsensitive(c *check.C) { proto := conn.ConnectionState().NegotiatedProtocol c.Assert(proto, checker.Equals, "h2") } + +// TestWithDomainFronting verify the domain fronting behavior +func (s *HTTPSSuite) TestWithDomainFronting(c *check.C) { + backend := startTestServer("9010", http.StatusOK, "server1") + defer backend.Close() + backend2 := startTestServer("9020", http.StatusOK, "server2") + defer backend2.Close() + backend3 := startTestServer("9030", http.StatusOK, "server3") + defer backend3.Close() + + file := s.adaptFile(c, "fixtures/https/https_domain_fronting.toml", struct{}{}) + defer os.Remove(file) + cmd, display := s.traefikCmd(withConfigFile(file)) + defer display(c) + err := cmd.Start() + c.Assert(err, checker.IsNil) + defer cmd.Process.Kill() + + // wait for Traefik + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 500*time.Millisecond, try.BodyContains("Host(`site1.www.snitest.com`)")) + c.Assert(err, checker.IsNil) + + testCases := []struct { + desc string + hostHeader string + serverName string + expectedContent string + expectedStatusCode int + }{ + { + desc: "SimpleCase", + hostHeader: "site1.www.snitest.com", + serverName: "site1.www.snitest.com", + expectedContent: "server1", + expectedStatusCode: http.StatusOK, + }, + { + desc: "Simple case with port in the Host Header", + hostHeader: "site3.www.snitest.com:4443", + serverName: "site3.www.snitest.com", + expectedContent: "server3", + expectedStatusCode: http.StatusOK, + }, + { + desc: "Spaces after the host header", + hostHeader: "site3.www.snitest.com ", + serverName: "site3.www.snitest.com", + expectedContent: "server3", + expectedStatusCode: http.StatusOK, + }, + { + desc: "Spaces after the servername", + hostHeader: "site3.www.snitest.com", + serverName: "site3.www.snitest.com ", + expectedContent: "server3", + expectedStatusCode: http.StatusOK, + }, + { + desc: "Spaces after the servername and host header", + hostHeader: "site3.www.snitest.com ", + serverName: "site3.www.snitest.com ", + expectedContent: "server3", + expectedStatusCode: http.StatusOK, + }, + { + desc: "Domain Fronting with same tlsOptions should follow header", + hostHeader: "site1.www.snitest.com", + serverName: "site2.www.snitest.com", + expectedContent: "server1", + expectedStatusCode: http.StatusOK, + }, + { + desc: "Domain Fronting with same tlsOptions should follow header (2)", + hostHeader: "site2.www.snitest.com", + serverName: "site1.www.snitest.com", + expectedContent: "server2", + expectedStatusCode: http.StatusOK, + }, + { + desc: "Domain Fronting with different tlsOptions should produce a 421", + hostHeader: "site2.www.snitest.com", + serverName: "site3.www.snitest.com", + expectedContent: "", + expectedStatusCode: http.StatusMisdirectedRequest, + }, + { + desc: "Domain Fronting with different tlsOptions should produce a 421 (2)", + hostHeader: "site3.www.snitest.com", + serverName: "site1.www.snitest.com", + expectedContent: "", + expectedStatusCode: http.StatusMisdirectedRequest, + }, + { + desc: "Case insensitive", + hostHeader: "sIte1.www.snitest.com", + serverName: "sitE1.www.snitest.com", + expectedContent: "server1", + expectedStatusCode: http.StatusOK, + }, + } + + for _, test := range testCases { + test := test + + req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1:4443", nil) + c.Assert(err, checker.IsNil) + req.Host = test.hostHeader + + err = try.RequestWithTransport(req, 500*time.Millisecond, &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true, ServerName: test.serverName}}, try.StatusCodeIs(test.expectedStatusCode), try.BodyContains(test.expectedContent)) + c.Assert(err, checker.IsNil) + } +} diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index e71252e48..b3168de8e 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -2,6 +2,7 @@ package api import ( "net/http" + "net/url" "github.com/containous/traefik/v2/pkg/log" assetfs "github.com/elazarl/go-bindata-assetfs" @@ -23,11 +24,29 @@ func (g DashboardHandler) Append(router *mux.Router) { // Expose dashboard router.Methods(http.MethodGet). Path("/"). - HandlerFunc(func(response http.ResponseWriter, request *http.Request) { - http.Redirect(response, request, request.Header.Get("X-Forwarded-Prefix")+"/dashboard/", http.StatusFound) + HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { + http.Redirect(resp, req, safePrefix(req)+"/dashboard/", http.StatusFound) }) router.Methods(http.MethodGet). PathPrefix("/dashboard/"). Handler(http.StripPrefix("/dashboard/", http.FileServer(g.Assets))) } + +func safePrefix(req *http.Request) string { + prefix := req.Header.Get("X-Forwarded-Prefix") + if prefix == "" { + return "" + } + + parse, err := url.Parse(prefix) + if err != nil { + return "" + } + + if parse.Host != "" { + return "" + } + + return parse.Path +} diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go new file mode 100644 index 000000000..c945a7000 --- /dev/null +++ b/pkg/api/dashboard_test.go @@ -0,0 +1,54 @@ +package api + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_safePrefix(t *testing.T) { + testCases := []struct { + desc string + value string + expected string + }{ + { + desc: "host", + value: "https://example.com", + expected: "", + }, + { + desc: "host with path", + value: "https://example.com/foo/bar?test", + expected: "", + }, + { + desc: "path", + value: "/foo/bar", + expected: "/foo/bar", + }, + { + desc: "path without leading slash", + value: "foo/bar", + expected: "foo/bar", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + req, err := http.NewRequest(http.MethodGet, "http://localhost", nil) + require.NoError(t, err) + + req.Header.Set("X-Forwarded-Prefix", test.value) + + prefix := safePrefix(req) + + assert.Equal(t, test.expected, prefix) + }) + } +} diff --git a/pkg/config/file/raw_node.go b/pkg/config/file/raw_node.go index 8b5509ad5..9f9b3857b 100644 --- a/pkg/config/file/raw_node.go +++ b/pkg/config/file/raw_node.go @@ -28,6 +28,10 @@ func decodeRaw(node *parser.Node, vData reflect.Value, filters ...string) error sortedKeys := sortKeys(vData, filters) for _, key := range sortedKeys { + if vData.MapIndex(key).IsNil() { + continue + } + value := reflect.ValueOf(vData.MapIndex(key).Interface()) child := &parser.Node{Name: key.String()} diff --git a/pkg/config/file/raw_node_test.go b/pkg/config/file/raw_node_test.go index 5b4ae64ae..6811543cf 100644 --- a/pkg/config/file/raw_node_test.go +++ b/pkg/config/file/raw_node_test.go @@ -524,6 +524,20 @@ func Test_decodeRawToNode(t *testing.T) { }, }, }, + { + desc: "nil value", + data: map[string]interface{}{ + "fii": map[interface{}]interface{}{ + "fuu": nil, + }, + }, + expected: &parser.Node{ + Name: "traefik", + Children: []*parser.Node{ + {Name: "fii"}, + }, + }, + }, } for _, test := range testCases { diff --git a/pkg/healthcheck/healthcheck_test.go b/pkg/healthcheck/healthcheck_test.go index a1fe3f265..00b79be2f 100644 --- a/pkg/healthcheck/healthcheck_test.go +++ b/pkg/healthcheck/healthcheck_test.go @@ -448,9 +448,9 @@ func TestLBStatusUpdater(t *testing.T) { svInfo := &runtime.ServiceInfo{} lbsu := NewLBStatusUpdater(lb, svInfo) newServer, err := url.Parse("http://foo.com") - assert.Nil(t, err) + assert.NoError(t, err) err = lbsu.UpsertServer(newServer, roundrobin.Weight(1)) - assert.Nil(t, err) + assert.NoError(t, err) assert.Equal(t, len(lbsu.Servers()), 1) assert.Equal(t, len(lbsu.BalancerHandler.(*testLoadBalancer).Options()), 1) statuses := svInfo.GetAllStatus() @@ -461,7 +461,7 @@ func TestLBStatusUpdater(t *testing.T) { break } err = lbsu.RemoveServer(newServer) - assert.Nil(t, err) + assert.NoError(t, err) assert.Equal(t, len(lbsu.Servers()), 0) statuses = svInfo.GetAllStatus() assert.Equal(t, len(statuses), 1) diff --git a/pkg/middlewares/accesslog/logger.go b/pkg/middlewares/accesslog/logger.go index c876efcb3..13af24c9b 100644 --- a/pkg/middlewares/accesslog/logger.go +++ b/pkg/middlewares/accesslog/logger.go @@ -6,6 +6,7 @@ import ( "io" "net" "net/http" + "net/textproto" "net/url" "os" "path/filepath" @@ -100,6 +101,17 @@ func NewHandler(config *types.AccessLog) (*Handler, error) { Level: logrus.InfoLevel, } + // Transform headers names in config to a canonical form, to be used as is without further transformations. + if config.Fields != nil && config.Fields.Headers != nil && len(config.Fields.Headers.Names) > 0 { + fields := map[string]string{} + + for h, v := range config.Fields.Headers.Names { + fields[textproto.CanonicalMIMEHeaderKey(h)] = v + } + + config.Fields.Headers.Names = fields + } + logHandler := &Handler{ config: config, logger: logger, diff --git a/pkg/middlewares/accesslog/logger_test.go b/pkg/middlewares/accesslog/logger_test.go index 6d1567db0..eeeceba7b 100644 --- a/pkg/middlewares/accesslog/logger_test.go +++ b/pkg/middlewares/accesslog/logger_test.go @@ -41,11 +41,7 @@ var ( ) func TestLogRotation(t *testing.T) { - tempDir, err := ioutil.TempDir("", "traefik_") - if err != nil { - t.Fatalf("Error setting up temporary directory: %s", err) - } - defer os.RemoveAll(tempDir) + tempDir := createTempDir(t, "traefik_") fileName := filepath.Join(tempDir, "traefik.log") rotatedFileName := fileName + ".rotated" @@ -119,9 +115,106 @@ func lineCount(t *testing.T, fileName string) int { return count } +func TestLoggerHeaderFields(t *testing.T) { + tmpDir := createTempDir(t, CommonFormat) + + expectedValue := "expectedValue" + + testCases := []struct { + desc string + accessLogFields types.AccessLogFields + header string + expected string + }{ + { + desc: "with default mode", + header: "User-Agent", + expected: types.AccessLogDrop, + accessLogFields: types.AccessLogFields{ + DefaultMode: types.AccessLogDrop, + Headers: &types.FieldHeaders{ + DefaultMode: types.AccessLogDrop, + Names: map[string]string{}, + }, + }, + }, + { + desc: "with exact header name", + header: "User-Agent", + expected: types.AccessLogKeep, + accessLogFields: types.AccessLogFields{ + DefaultMode: types.AccessLogDrop, + Headers: &types.FieldHeaders{ + DefaultMode: types.AccessLogDrop, + Names: map[string]string{ + "User-Agent": types.AccessLogKeep, + }, + }, + }, + }, + { + desc: "with case insensitive match on header name", + header: "User-Agent", + expected: types.AccessLogKeep, + accessLogFields: types.AccessLogFields{ + DefaultMode: types.AccessLogDrop, + Headers: &types.FieldHeaders{ + DefaultMode: types.AccessLogDrop, + Names: map[string]string{ + "user-agent": types.AccessLogKeep, + }, + }, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + logFile, err := ioutil.TempFile(tmpDir, "*.log") + require.NoError(t, err) + + config := &types.AccessLog{ + FilePath: logFile.Name(), + Format: CommonFormat, + Fields: &test.accessLogFields, + } + + logger, err := NewHandler(config) + require.NoError(t, err) + defer logger.Close() + + if config.FilePath != "" { + _, err = os.Stat(config.FilePath) + require.NoError(t, err, fmt.Sprintf("logger should create %s", config.FilePath)) + } + + req := &http.Request{ + Header: map[string][]string{}, + URL: &url.URL{ + Path: testPath, + }, + } + req.Header.Set(test.header, expectedValue) + + logger.ServeHTTP(httptest.NewRecorder(), req, http.HandlerFunc(func(writer http.ResponseWriter, r *http.Request) { + writer.WriteHeader(http.StatusOK) + })) + + logData, err := ioutil.ReadFile(logFile.Name()) + require.NoError(t, err) + + if test.expected == types.AccessLogDrop { + assert.NotContains(t, string(logData), expectedValue) + } else { + assert.Contains(t, string(logData), expectedValue) + } + }) + } +} + func TestLoggerCLF(t *testing.T) { tmpDir := createTempDir(t, CommonFormat) - defer os.RemoveAll(tmpDir) logFilePath := filepath.Join(tmpDir, logFileNameSuffix) config := &types.AccessLog{FilePath: logFilePath, Format: CommonFormat} @@ -136,7 +229,6 @@ func TestLoggerCLF(t *testing.T) { func TestAsyncLoggerCLF(t *testing.T) { tmpDir := createTempDir(t, CommonFormat) - defer os.RemoveAll(tmpDir) logFilePath := filepath.Join(tmpDir, logFileNameSuffix) config := &types.AccessLog{FilePath: logFilePath, Format: CommonFormat, BufferingSize: 1024} @@ -358,7 +450,6 @@ func TestLoggerJSON(t *testing.T) { t.Parallel() tmpDir := createTempDir(t, JSONFormat) - defer os.RemoveAll(tmpDir) logFilePath := filepath.Join(tmpDir, logFileNameSuffix) @@ -642,6 +733,8 @@ func createTempDir(t *testing.T, prefix string) string { tmpDir, err := ioutil.TempDir("", prefix) require.NoError(t, err, "failed to create temp dir") + t.Cleanup(func() { _ = os.RemoveAll(tmpDir) }) + return tmpDir } diff --git a/pkg/provider/kubernetes/ingress/client.go b/pkg/provider/kubernetes/ingress/client.go index f4af6c377..fc54c48d5 100644 --- a/pkg/provider/kubernetes/ingress/client.go +++ b/pkg/provider/kubernetes/ingress/client.go @@ -5,11 +5,11 @@ import ( "errors" "fmt" "io/ioutil" - "strconv" "time" "github.com/containous/traefik/v2/pkg/log" "github.com/golang/protobuf/proto" + "github.com/hashicorp/go-version" corev1 "k8s.io/api/core/v1" extensionsv1beta1 "k8s.io/api/extensions/v1beta1" networkingv1beta1 "k8s.io/api/networking/v1beta1" @@ -55,7 +55,7 @@ type Client interface { GetSecret(namespace, name string) (*corev1.Secret, bool, error) GetEndpoints(namespace, name string) (*corev1.Endpoints, bool, error) UpdateIngressStatus(ing *networkingv1beta1.Ingress, ip, hostname string) error - GetServerVersion() (major, minor int, err error) + GetServerVersion() (*version.Version, error) } type clientWrapper struct { @@ -163,13 +163,13 @@ func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (< } } - // If the kubernetes cluster is v1.18+, we can use the new IngressClass objects - major, minor, err := c.GetServerVersion() + serverVersion, err := c.GetServerVersion() if err != nil { - return nil, err + log.WithoutContext().Errorf("Failed to get server version: %v", err) + return eventCh, nil } - if major >= 1 && minor >= 18 { + if supportsIngressClass(serverVersion) { c.clusterFactory = informers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod) c.clusterFactory.Networking().V1beta1().IngressClasses().Informer().AddEventHandler(eventHandler) c.clusterFactory.Start(stopCh) @@ -341,7 +341,7 @@ func (c *clientWrapper) GetIngressClass() (*networkingv1beta1.IngressClass, erro for _, ic := range ingressClasses { if ic.Spec.Controller == traefikDefaultIngressClassController { - return ic, err + return ic, nil } } @@ -381,23 +381,13 @@ func (c *clientWrapper) newResourceEventHandler(events chan<- interface{}) cache } // GetServerVersion returns the cluster server version, or an error. -func (c *clientWrapper) GetServerVersion() (major, minor int, err error) { - version, err := c.clientset.Discovery().ServerVersion() +func (c *clientWrapper) GetServerVersion() (*version.Version, error) { + serverVersion, err := c.clientset.Discovery().ServerVersion() if err != nil { - return 0, 0, fmt.Errorf("could not determine cluster API version: %w", err) + return nil, fmt.Errorf("could not retrieve server version: %w", err) } - major, err = strconv.Atoi(version.Major) - if err != nil { - return 0, 0, fmt.Errorf("could not determine cluster major API version: %w", err) - } - - minor, err = strconv.Atoi(version.Minor) - if err != nil { - return 0, 0, fmt.Errorf("could not determine cluster minor API version: %w", err) - } - - return major, minor, nil + return version.NewVersion(serverVersion.GitVersion) } // eventHandlerFunc will pass the obj on to the events channel or drop it. @@ -432,3 +422,11 @@ func (c *clientWrapper) isWatchedNamespace(ns string) bool { } return false } + +// IngressClass objects are supported since Kubernetes v1.18. +// See https://kubernetes.io/docs/concepts/services-networking/ingress/#ingress-class +func supportsIngressClass(serverVersion *version.Version) bool { + ingressClassVersion := version.Must(version.NewVersion("1.18")) + + return ingressClassVersion.LessThanOrEqual(serverVersion) +} diff --git a/pkg/provider/kubernetes/ingress/client_mock_test.go b/pkg/provider/kubernetes/ingress/client_mock_test.go index 6bf5716c1..410c96d52 100644 --- a/pkg/provider/kubernetes/ingress/client_mock_test.go +++ b/pkg/provider/kubernetes/ingress/client_mock_test.go @@ -5,6 +5,7 @@ import ( "io/ioutil" "github.com/containous/traefik/v2/pkg/provider/kubernetes/k8s" + "github.com/hashicorp/go-version" corev1 "k8s.io/api/core/v1" extensionsv1beta1 "k8s.io/api/extensions/v1beta1" "k8s.io/api/networking/v1beta1" @@ -20,8 +21,7 @@ type clientMock struct { endpoints []*corev1.Endpoints ingressClass *networkingv1beta1.IngressClass - serverMajor int - serverMinor int + serverVersion *version.Version apiServiceError error apiSecretError error @@ -31,11 +31,10 @@ type clientMock struct { watchChan chan interface{} } -func newClientMock(major, minor int, paths ...string) clientMock { - c := clientMock{ - serverMajor: major, - serverMinor: minor, - } +func newClientMock(serverVersion string, paths ...string) clientMock { + c := clientMock{} + + c.serverVersion = version.Must(version.NewVersion(serverVersion)) for _, path := range paths { yamlContent, err := ioutil.ReadFile(path) @@ -75,8 +74,8 @@ func (c clientMock) GetIngresses() []*v1beta1.Ingress { return c.ingresses } -func (c clientMock) GetServerVersion() (major, minor int, err error) { - return c.serverMajor, c.serverMinor, nil +func (c clientMock) GetServerVersion() (*version.Version, error) { + return c.serverVersion, nil } func (c clientMock) GetService(namespace, name string) (*corev1.Service, bool, error) { diff --git a/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-empty-pathType_endpoint.yml b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-empty-pathType_endpoint.yml new file mode 100644 index 000000000..6ed60d79c --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-empty-pathType_endpoint.yml @@ -0,0 +1,11 @@ +kind: Endpoints +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +subsets: +- addresses: + - ip: 10.10.0.1 + ports: + - port: 8080 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-empty-pathType_ingress.yml b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-empty-pathType_ingress.yml new file mode 100644 index 000000000..b01a04d51 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-empty-pathType_ingress.yml @@ -0,0 +1,16 @@ +kind: Ingress +apiVersion: networking.k8s.io/v1beta1 +metadata: + name: "" + namespace: testing + annotations: + traefik.ingress.kubernetes.io/router.pathmatcher: Path +spec: + rules: + - http: + paths: + - path: /bar + pathType: "" + backend: + serviceName: service1 + servicePort: 80 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-empty-pathType_service.yml b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-empty-pathType_service.yml new file mode 100644 index 000000000..7c58aeed5 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-empty-pathType_service.yml @@ -0,0 +1,10 @@ +kind: Service +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +spec: + ports: + - port: 80 + clusterIp: 10.0.0.1 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-exact-pathType_endpoint.yml b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-exact-pathType_endpoint.yml new file mode 100644 index 000000000..6ed60d79c --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-exact-pathType_endpoint.yml @@ -0,0 +1,11 @@ +kind: Endpoints +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +subsets: +- addresses: + - ip: 10.10.0.1 + ports: + - port: 8080 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-exact-pathType_ingress.yml b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-exact-pathType_ingress.yml new file mode 100644 index 000000000..d3f9848f6 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-exact-pathType_ingress.yml @@ -0,0 +1,14 @@ +kind: Ingress +apiVersion: networking.k8s.io/v1beta1 +metadata: + name: "" + namespace: testing +spec: + rules: + - http: + paths: + - path: /bar + pathType: Exact + backend: + serviceName: service1 + servicePort: 80 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-exact-pathType_service.yml b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-exact-pathType_service.yml new file mode 100644 index 000000000..7c58aeed5 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-exact-pathType_service.yml @@ -0,0 +1,10 @@ +kind: Service +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +spec: + ports: + - port: 80 + clusterIp: 10.0.0.1 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-implementationSpecific-pathType_endpoint.yml b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-implementationSpecific-pathType_endpoint.yml new file mode 100644 index 000000000..6ed60d79c --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-implementationSpecific-pathType_endpoint.yml @@ -0,0 +1,11 @@ +kind: Endpoints +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +subsets: +- addresses: + - ip: 10.10.0.1 + ports: + - port: 8080 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-implementationSpecific-pathType_ingress.yml b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-implementationSpecific-pathType_ingress.yml new file mode 100644 index 000000000..8b1cc9674 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-implementationSpecific-pathType_ingress.yml @@ -0,0 +1,16 @@ +kind: Ingress +apiVersion: networking.k8s.io/v1beta1 +metadata: + name: "" + namespace: testing + annotations: + traefik.ingress.kubernetes.io/router.pathmatcher: Path +spec: + rules: + - http: + paths: + - path: /bar + pathType: ImplementationSpecific + backend: + serviceName: service1 + servicePort: 80 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-implementationSpecific-pathType_service.yml b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-implementationSpecific-pathType_service.yml new file mode 100644 index 000000000..7c58aeed5 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-implementationSpecific-pathType_service.yml @@ -0,0 +1,10 @@ +kind: Service +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +spec: + ports: + - port: 80 + clusterIp: 10.0.0.1 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-ingress-annotation_endpoint.yml b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-ingress-annotation_endpoint.yml new file mode 100644 index 000000000..6ed60d79c --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-ingress-annotation_endpoint.yml @@ -0,0 +1,11 @@ +kind: Endpoints +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +subsets: +- addresses: + - ip: 10.10.0.1 + ports: + - port: 8080 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-ingress-annotation_ingress.yml b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-ingress-annotation_ingress.yml new file mode 100644 index 000000000..e223946d0 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-ingress-annotation_ingress.yml @@ -0,0 +1,15 @@ +kind: Ingress +apiVersion: networking.k8s.io/v1beta1 +metadata: + name: "" + namespace: testing + annotations: + kubernetes.io/ingress.class: traefik +spec: + rules: + - http: + paths: + - path: /bar + backend: + serviceName: service1 + servicePort: 80 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-ingress-annotation_service.yml b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-ingress-annotation_service.yml new file mode 100644 index 000000000..7c58aeed5 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-ingress-annotation_service.yml @@ -0,0 +1,10 @@ +kind: Service +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +spec: + ports: + - port: 80 + clusterIp: 10.0.0.1 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-no-pathType_endpoint.yml b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-no-pathType_endpoint.yml new file mode 100644 index 000000000..6ed60d79c --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-no-pathType_endpoint.yml @@ -0,0 +1,11 @@ +kind: Endpoints +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +subsets: +- addresses: + - ip: 10.10.0.1 + ports: + - port: 8080 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-no-pathType_ingress.yml b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-no-pathType_ingress.yml new file mode 100644 index 000000000..740d39d1c --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-no-pathType_ingress.yml @@ -0,0 +1,15 @@ +kind: Ingress +apiVersion: networking.k8s.io/v1beta1 +metadata: + name: "" + namespace: testing + annotations: + traefik.ingress.kubernetes.io/router.pathmatcher: Path +spec: + rules: + - http: + paths: + - path: /bar + backend: + serviceName: service1 + servicePort: 80 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-no-pathType_service.yml b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-no-pathType_service.yml new file mode 100644 index 000000000..7c58aeed5 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-no-pathType_service.yml @@ -0,0 +1,10 @@ +kind: Service +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +spec: + ports: + - port: 80 + clusterIp: 10.0.0.1 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-prefix-pathType_endpoint.yml b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-prefix-pathType_endpoint.yml new file mode 100644 index 000000000..6ed60d79c --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-prefix-pathType_endpoint.yml @@ -0,0 +1,11 @@ +kind: Endpoints +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +subsets: +- addresses: + - ip: 10.10.0.1 + ports: + - port: 8080 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-prefix-pathType_ingress.yml b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-prefix-pathType_ingress.yml new file mode 100644 index 000000000..2ae20e2ef --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-prefix-pathType_ingress.yml @@ -0,0 +1,14 @@ +kind: Ingress +apiVersion: networking.k8s.io/v1beta1 +metadata: + name: "" + namespace: testing +spec: + rules: + - http: + paths: + - path: /bar + pathType: Prefix + backend: + serviceName: service1 + servicePort: 80 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-prefix-pathType_service.yml b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-prefix-pathType_service.yml new file mode 100644 index 000000000..7c58aeed5 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-prefix-pathType_service.yml @@ -0,0 +1,10 @@ +kind: Service +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +spec: + ports: + - port: 80 + clusterIp: 10.0.0.1 diff --git a/pkg/provider/kubernetes/ingress/kubernetes.go b/pkg/provider/kubernetes/ingress/kubernetes.go index c6401e44e..119c0115e 100644 --- a/pkg/provider/kubernetes/ingress/kubernetes.go +++ b/pkg/provider/kubernetes/ingress/kubernetes.go @@ -183,7 +183,7 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl TCP: &dynamic.TCPConfiguration{}, } - major, minor, err := client.GetServerVersion() + serverVersion, err := client.GetServerVersion() if err != nil { log.FromContext(ctx).Errorf("Failed to get server version: %v", err) return conf @@ -191,16 +191,10 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl var ingressClass *networkingv1beta1.IngressClass - if major >= 1 && minor >= 18 { + if supportsIngressClass(serverVersion) { ic, err := client.GetIngressClass() if err != nil { - log.FromContext(ctx).Errorf("Failed to find an ingress class: %v", err) - return conf - } - - if ic == nil { - log.FromContext(ctx).Errorf("No ingress class for the traefik-controller in the cluster") - return conf + log.FromContext(ctx).Warnf("Failed to find an ingress class: %v", err) } ingressClass = ic @@ -337,8 +331,12 @@ func (p *Provider) updateIngressStatus(ing *v1beta1.Ingress, k8sClient Client) e } func (p *Provider) shouldProcessIngress(providerIngressClass string, ingress *networkingv1beta1.Ingress, ingressClass *networkingv1beta1.IngressClass) bool { - return ingressClass != nil && ingress.Spec.IngressClassName != nil && ingressClass.ObjectMeta.Name == *ingress.Spec.IngressClassName || - providerIngressClass == ingress.Annotations[annotationKubernetesIngressClass] || + // configuration through the new kubernetes ingressClass + if ingress.Spec.IngressClassName != nil { + return ingressClass != nil && ingressClass.ObjectMeta.Name == *ingress.Spec.IngressClassName + } + + return providerIngressClass == ingress.Annotations[annotationKubernetesIngressClass] || len(providerIngressClass) == 0 && ingress.Annotations[annotationKubernetesIngressClass] == traefikDefaultIngressClass } @@ -549,8 +547,13 @@ func loadRouter(rule v1beta1.IngressRule, pa v1beta1.HTTPIngressPath, rtConfig * if len(pa.Path) > 0 { matcher := defaultPathMatcher - if rtConfig != nil && rtConfig.Router != nil && rtConfig.Router.PathMatcher != "" { - matcher = rtConfig.Router.PathMatcher + + if pa.PathType == nil || *pa.PathType == "" || *pa.PathType == v1beta1.PathTypeImplementationSpecific { + if rtConfig != nil && rtConfig.Router != nil && rtConfig.Router.PathMatcher != "" { + matcher = rtConfig.Router.PathMatcher + } + } else if *pa.PathType == v1beta1.PathTypeExact { + matcher = "Path" } rules = append(rules, fmt.Sprintf("%s(`%s`)", matcher, pa.Path)) diff --git a/pkg/provider/kubernetes/ingress/kubernetes_test.go b/pkg/provider/kubernetes/ingress/kubernetes_test.go index ddd11ff7e..f1daee211 100644 --- a/pkg/provider/kubernetes/ingress/kubernetes_test.go +++ b/pkg/provider/kubernetes/ingress/kubernetes_test.go @@ -24,10 +24,10 @@ func Bool(v bool) *bool { return &v } func TestLoadConfigurationFromIngresses(t *testing.T) { testCases := []struct { - desc string - ingressClass string - serverMinor int - expected *dynamic.Configuration + desc string + ingressClass string + serverVersion string + expected *dynamic.Configuration }{ { desc: "Empty ingresses", @@ -925,8 +925,8 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { }, }, { - desc: "v18 Ingress with ingressClass", - serverMinor: 18, + desc: "v18 Ingress with ingressClass", + serverVersion: "v1.18", expected: &dynamic.Configuration{ TCP: &dynamic.TCPConfiguration{}, HTTP: &dynamic.HTTPConfiguration{ @@ -953,8 +953,148 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { }, }, { - desc: "v18 Ingress with missing ingressClass", - serverMinor: 18, + desc: "v18 Ingress with no pathType", + serverVersion: "v1.18", + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{}, + HTTP: &dynamic.HTTPConfiguration{ + Middlewares: map[string]*dynamic.Middleware{}, + Routers: map[string]*dynamic.Router{ + "testing-bar": { + Rule: "Path(`/bar`)", + Service: "testing-service1-80", + }, + }, + Services: map[string]*dynamic.Service{ + "testing-service1-80": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + PassHostHeader: Bool(true), + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:8080", + }, + }, + }, + }, + }, + }, + }, + }, + { + desc: "v18 Ingress with empty pathType", + serverVersion: "v1.18", + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{}, + HTTP: &dynamic.HTTPConfiguration{ + Middlewares: map[string]*dynamic.Middleware{}, + Routers: map[string]*dynamic.Router{ + "testing-bar": { + Rule: "Path(`/bar`)", + Service: "testing-service1-80", + }, + }, + Services: map[string]*dynamic.Service{ + "testing-service1-80": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + PassHostHeader: Bool(true), + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:8080", + }, + }, + }, + }, + }, + }, + }, + }, + { + desc: "v18 Ingress with implementationSpecific pathType", + serverVersion: "v1.18", + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{}, + HTTP: &dynamic.HTTPConfiguration{ + Middlewares: map[string]*dynamic.Middleware{}, + Routers: map[string]*dynamic.Router{ + "testing-bar": { + Rule: "Path(`/bar`)", + Service: "testing-service1-80", + }, + }, + Services: map[string]*dynamic.Service{ + "testing-service1-80": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + PassHostHeader: Bool(true), + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:8080", + }, + }, + }, + }, + }, + }, + }, + }, + { + desc: "v18 Ingress with prefix pathType", + serverVersion: "v1.18", + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{}, + HTTP: &dynamic.HTTPConfiguration{ + Middlewares: map[string]*dynamic.Middleware{}, + Routers: map[string]*dynamic.Router{ + "testing-bar": { + Rule: "PathPrefix(`/bar`)", + Service: "testing-service1-80", + }, + }, + Services: map[string]*dynamic.Service{ + "testing-service1-80": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + PassHostHeader: Bool(true), + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:8080", + }, + }, + }, + }, + }, + }, + }, + }, + { + desc: "v18 Ingress with exact pathType", + serverVersion: "v1.18", + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{}, + HTTP: &dynamic.HTTPConfiguration{ + Middlewares: map[string]*dynamic.Middleware{}, + Routers: map[string]*dynamic.Router{ + "testing-bar": { + Rule: "Path(`/bar`)", + Service: "testing-service1-80", + }, + }, + Services: map[string]*dynamic.Service{ + "testing-service1-80": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + PassHostHeader: Bool(true), + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:8080", + }, + }, + }, + }, + }, + }, + }, + }, + { + desc: "v18 Ingress with missing ingressClass", + serverVersion: "v1.18", expected: &dynamic.Configuration{ TCP: &dynamic.TCPConfiguration{}, HTTP: &dynamic.HTTPConfiguration{ @@ -964,10 +1104,39 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { }, }, }, + { + desc: "v18 Ingress with ingress annotation", + serverVersion: "v1.18", + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{}, + HTTP: &dynamic.HTTPConfiguration{ + Middlewares: map[string]*dynamic.Middleware{}, + Routers: map[string]*dynamic.Router{ + "testing-bar": { + Rule: "PathPrefix(`/bar`)", + Service: "testing-service1-80", + }, + }, + Services: map[string]*dynamic.Service{ + "testing-service1-80": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + PassHostHeader: Bool(true), + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:8080", + }, + }, + }, + }, + }, + }, + }, + }, } for _, test := range testCases { test := test + t.Run(test.desc, func(t *testing.T) { t.Parallel() @@ -993,12 +1162,12 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { paths = append(paths, generateTestFilename("_ingressclass", test.desc)) } - serverMinor := 17 - if test.serverMinor != 0 { - serverMinor = test.serverMinor + serverVersion := test.serverVersion + if serverVersion == "" { + serverVersion = "v1.17" } - clientMock := newClientMock(1, serverMinor, paths...) + clientMock := newClientMock(serverVersion, paths...) p := Provider{IngressClass: test.ingressClass} conf := p.loadConfigurationFromIngresses(context.Background(), clientMock) @@ -1179,7 +1348,7 @@ func TestGetCertificates(t *testing.T) { if test.errResult != "" { assert.EqualError(t, err, test.errResult) } else { - assert.Nil(t, err) + assert.NoError(t, err) assert.Equal(t, test.result, tlsConfigs) } }) diff --git a/pkg/provider/traefik/fixtures/redirection_with_protocol.json b/pkg/provider/traefik/fixtures/redirection_with_protocol.json new file mode 100644 index 000000000..4ffbb756c --- /dev/null +++ b/pkg/provider/traefik/fixtures/redirection_with_protocol.json @@ -0,0 +1,30 @@ +{ + "http": { + "routers": { + "web-to-websecure": { + "entryPoints": [ + "web" + ], + "middlewares": [ + "redirect-web-to-websecure" + ], + "service": "noop@internal", + "rule": "HostRegexp(`{host:.+}`)" + } + }, + "middlewares": { + "redirect-web-to-websecure": { + "redirectScheme": { + "scheme": "https", + "port": "443", + "permanent": true + } + } + }, + "services": { + "noop": {} + } + }, + "tcp": {}, + "tls": {} +} \ No newline at end of file diff --git a/pkg/provider/traefik/internal.go b/pkg/provider/traefik/internal.go index a63a2f49b..4c341f56b 100644 --- a/pkg/provider/traefik/internal.go +++ b/pkg/provider/traefik/internal.go @@ -142,7 +142,7 @@ func (i *Provider) getEntryPointPort(name string, def *static.Redirections) (str return "", fmt.Errorf("'to' entry point field references a non-existing entry point: %s", def.EntryPoint.To) } - _, port, err := net.SplitHostPort(dst.Address) + _, port, err := net.SplitHostPort(dst.GetAddress()) if err != nil { return "", fmt.Errorf("invalid entry point %q address %q: %w", name, i.staticCfg.EntryPoints[def.EntryPoint.To].Address, err) diff --git a/pkg/provider/traefik/internal_test.go b/pkg/provider/traefik/internal_test.go index dd6ef8c6a..b9e0af9c4 100644 --- a/pkg/provider/traefik/internal_test.go +++ b/pkg/provider/traefik/internal_test.go @@ -232,6 +232,28 @@ func Test_createConfiguration(t *testing.T) { }, }, }, + { + desc: "redirection_with_protocol.json", + staticCfg: static.Configuration{ + EntryPoints: map[string]*static.EntryPoint{ + "web": { + Address: ":80", + HTTP: static.HTTPConfig{ + Redirections: &static.Redirections{ + EntryPoint: &static.RedirectEntryPoint{ + To: "websecure", + Scheme: "https", + Permanent: true, + }, + }, + }, + }, + "websecure": { + Address: ":443/tcp", + }, + }, + }, + }, } for _, test := range testCases { diff --git a/pkg/server/middleware/middlewares_test.go b/pkg/server/middleware/middlewares_test.go index 5acee8396..68008b644 100644 --- a/pkg/server/middleware/middlewares_test.go +++ b/pkg/server/middleware/middlewares_test.go @@ -276,7 +276,7 @@ func TestBuilder_BuildChainWithContext(t *testing.T) { handlers, err := result.Then(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })) if test.expectedError != nil { - require.NotNil(t, err) + require.Error(t, err) require.Equal(t, test.expectedError.Error(), err.Error()) } else { require.NoError(t, err) diff --git a/pkg/server/router/tcp/router.go b/pkg/server/router/tcp/router.go index e5cee5065..f164039c7 100644 --- a/pkg/server/router/tcp/router.go +++ b/pkg/server/router/tcp/router.go @@ -5,7 +5,9 @@ import ( "crypto/tls" "errors" "fmt" + "net" "net/http" + "strings" "github.com/containous/traefik/v2/pkg/config/runtime" "github.com/containous/traefik/v2/pkg/log" @@ -99,14 +101,13 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string log.FromContext(ctx).Errorf("Error during the build of the default TLS configuration: %v", err) } - router.HTTPSHandler(handlerHTTPS, defaultTLSConf) - if len(configsHTTP) > 0 { router.AddRouteHTTPTLS("*", defaultTLSConf) } // Keyed by domain, then by options reference. tlsOptionsForHostSNI := map[string]map[string]nameAndConfig{} + tlsOptionsForHost := map[string]string{} for routerHTTPName, routerHTTPConfig := range configsHTTP { if len(routerHTTPConfig.TLS.Options) == 0 || routerHTTPConfig.TLS.Options == defaultTLSConfigName { continue @@ -141,6 +142,7 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string continue } + // domain is already in lower case thanks to the domain parsing if tlsOptionsForHostSNI[domain] == nil { tlsOptionsForHostSNI[domain] = make(map[string]nameAndConfig) } @@ -148,10 +150,52 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string routerName: routerHTTPName, TLSConfig: tlsConf, } + + if _, ok := tlsOptionsForHost[domain]; ok { + // Multiple tlsOptions fallback to default + tlsOptionsForHost[domain] = "default" + } else { + tlsOptionsForHost[domain] = routerHTTPConfig.TLS.Options + } } } } + sniCheck := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + if req.TLS == nil { + handlerHTTPS.ServeHTTP(rw, req) + return + } + + host, _, err := net.SplitHostPort(req.Host) + if err != nil { + host = req.Host + } + + host = strings.TrimSpace(host) + serverName := strings.TrimSpace(req.TLS.ServerName) + + // Domain Fronting + if !strings.EqualFold(host, serverName) { + tlsOptionSNI := findTLSOptionName(tlsOptionsForHost, serverName) + tlsOptionHeader := findTLSOptionName(tlsOptionsForHost, host) + + if tlsOptionHeader != tlsOptionSNI { + log.WithoutContext(). + WithField("host", host). + WithField("req.Host", req.Host). + WithField("req.TLS.ServerName", req.TLS.ServerName). + Debugf("TLS options difference: SNI=%s, Header:%s", tlsOptionSNI, tlsOptionHeader) + http.Error(rw, http.StatusText(http.StatusMisdirectedRequest), http.StatusMisdirectedRequest) + return + } + } + + handlerHTTPS.ServeHTTP(rw, req) + }) + + router.HTTPSHandler(sniCheck, defaultTLSConf) + logger := log.FromContext(ctx) for hostSNI, tlsConfigs := range tlsOptionsForHostSNI { if len(tlsConfigs) == 1 { @@ -248,3 +292,17 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string return router, nil } + +func findTLSOptionName(tlsOptionsForHost map[string]string, host string) string { + tlsOptions, ok := tlsOptionsForHost[host] + if ok { + return tlsOptions + } + + tlsOptions, ok = tlsOptionsForHost[strings.ToLower(host)] + if ok { + return tlsOptions + } + + return "default" +} diff --git a/pkg/server/service/tcp/service_test.go b/pkg/server/service/tcp/service_test.go index be5008d3d..b1943463d 100644 --- a/pkg/server/service/tcp/service_test.go +++ b/pkg/server/service/tcp/service_test.go @@ -193,7 +193,7 @@ func TestManager_BuildTCP(t *testing.T) { assert.EqualError(t, err, test.expectedError) require.Nil(t, handler) } else { - assert.Nil(t, err) + assert.NoError(t, err) require.NotNil(t, handler) } }) diff --git a/pkg/server/service/udp/service_test.go b/pkg/server/service/udp/service_test.go index 5308ad114..a17828d47 100644 --- a/pkg/server/service/udp/service_test.go +++ b/pkg/server/service/udp/service_test.go @@ -193,7 +193,7 @@ func TestManager_BuildUDP(t *testing.T) { assert.EqualError(t, err, test.expectedError) require.Nil(t, handler) } else { - assert.Nil(t, err) + assert.NoError(t, err) require.NotNil(t, handler) } }) diff --git a/pkg/tcp/router.go b/pkg/tcp/router.go index c66e987ce..601f18190 100644 --- a/pkg/tcp/router.go +++ b/pkg/tcp/router.go @@ -11,6 +11,7 @@ import ( "time" "github.com/containous/traefik/v2/pkg/log" + "github.com/containous/traefik/v2/pkg/types" ) // Router is a TCP router. @@ -65,7 +66,7 @@ func (r *Router) ServeTCP(conn WriteCloser) { } // FIXME Optimize and test the routing table before helloServerName - serverName = strings.ToLower(serverName) + serverName = types.CanonicalDomain(serverName) if r.routingTable != nil && serverName != "" { if target, ok := r.routingTable[serverName]; ok { target.ServeTCP(r.GetConn(conn, peeked)) diff --git a/pkg/tls/certificate_store.go b/pkg/tls/certificate_store.go index 9958e8641..524a5cdba 100644 --- a/pkg/tls/certificate_store.go +++ b/pkg/tls/certificate_store.go @@ -13,14 +13,14 @@ import ( "github.com/patrickmn/go-cache" ) -// CertificateStore store for dynamic and static certificates. +// CertificateStore store for dynamic certificates. type CertificateStore struct { DynamicCerts *safe.Safe DefaultCertificate *tls.Certificate CertCache *cache.Cache } -// NewCertificateStore create a store for dynamic and static certificates. +// NewCertificateStore create a store for dynamic certificates. func NewCertificateStore() *CertificateStore { return &CertificateStore{ DynamicCerts: &safe.Safe{}, @@ -37,7 +37,7 @@ func (c CertificateStore) getDefaultCertificateDomains() []string { x509Cert, err := x509.ParseCertificate(c.DefaultCertificate.Certificate[0]) if err != nil { - log.WithoutContext().Errorf("Could not parse default certicate: %v", err) + log.WithoutContext().Errorf("Could not parse default certificate: %v", err) return allCerts }