Merge branch v2.3 into master.

This commit is contained in:
Fernandez Ludovic 2020-07-29 12:09:30 +02:00
commit 7c039ca223
57 changed files with 1102 additions and 117 deletions

View file

@ -7,7 +7,7 @@ before:
builds: builds:
- binary: traefik - binary: traefik
main: ./cmd/traefik/traefik.go main: ./cmd/traefik/
env: env:
- CGO_ENABLED=0 - CGO_ENABLED=0
ldflags: ldflags:

View file

@ -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) ## [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) [All Commits](https://github.com/containous/traefik/compare/v2.2.0-rc1...v2.3.0-rc1)

View file

@ -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) | | [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/) [^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) [^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. [^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`. [^5]: The `Global API Key` needs to be used, not the `Origin CA Key`.

View file

@ -428,6 +428,7 @@ metadata:
spec: spec:
clientAuth: clientAuth:
# the CA certificate is extracted from key `tls.ca` of the given secrets.
secretNames: secretNames:
- secretCA - secretCA
clientAuthType: RequireAndVerifyClientCert clientAuthType: RequireAndVerifyClientCert

View file

@ -320,3 +320,8 @@ Since `v2.2.5` this rule has been removed, and you should not use it anymore.
### File Provider ### File Provider
The file parser has been changed, since v2.3 the unknown options/fields in a dynamic configuration file are treated as errors. 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.

View file

@ -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. 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" !!! danger "Experimental Features"
Plugins can potentially modify the behavior of Traefik in unforeseen ways. Plugins can potentially modify the behavior of Traefik in unforeseen ways.
Exercise caution when adding new plugins to production Traefik instances. Exercise caution when adding new plugins to production Traefik instances.

View file

@ -1,6 +1,6 @@
# Using Plugins # 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. 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" !!! danger "Experimental Features"

View file

@ -79,9 +79,35 @@ providers:
# ... # ...
``` ```
Search for services in all 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. - 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` ### `exposedByDefault`

View file

@ -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. 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. 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+, If the Kubernetes cluster version is 1.18+,
you can also leverage the newly Introduced `IngressClass` resource to define which Ingress Objects to handle. 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 your cluster with the controller of *traefik.io/ingress-controller* inside the spec. In that case, Traefik will look for an `IngressClass` in the cluster with the controller value equal to *traefik.io/ingress-controller*.
!!! note "" Please see [this article](https://kubernetes.io/blog/2020/04/02/improvements-to-the-ingress-api-in-kubernetes-1.18/) for more information.
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.
### `ingressEndpoint` ### `ingressEndpoint`

View file

@ -18,6 +18,7 @@ rules:
- extensions - extensions
resources: resources:
- ingresses - ingresses
- ingressclasses
verbs: verbs:
- get - get
- list - list

View file

@ -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. 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)" ```toml tab="File (TOML)"
## Static configuration ## 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 --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 ### Forwarded Headers
You can configure Traefik to trust the forwarded headers information (`X-Forwarded-*`). You can configure Traefik to trust the forwarded headers information (`X-Forwarded-*`).

View file

@ -535,7 +535,7 @@ You can declare UDP Routers and/or Services using labels.
my-container: my-container:
# ... # ...
labels: labels:
- "traefik.udp.routers.my-router.entrypoint=udp" - "traefik.udp.routers.my-router.entrypoints=udp"
- "traefik.udp.services.my-service.loadbalancer.server.port=4123" - "traefik.udp.services.my-service.loadbalancer.server.port=4123"
``` ```

View file

@ -322,9 +322,23 @@ which in turn will create the resulting routers, services, handlers, etc.
traefik.ingress.kubernetes.io/service.sticky.cookie.httponly: "true" 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. Traefik automatically requests endpoint information based on the service provided in the ingress spec.
Although Traefik will connect directly to the endpoints (pods), 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. 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. See the [insecureSkipVerify](../../routing/overview.md#insecureskipverify) setting for more details.
#### Certificates Management ### Certificates Management
??? example "Using a secret" ??? example "Using a secret"

View file

@ -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, 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. 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" ??? example "Configuring the TLS options"
```toml tab="File (TOML)" ```toml tab="File (TOML)"

View file

@ -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 // A real file is needed to have the right mode on acme.json file
defer os.Remove("/tmp/acme.json") defer os.Remove("/tmp/acme.json")
backend := startTestServer("9010", http.StatusOK) backend := startTestServer("9010", http.StatusOK, "")
defer backend.Close() defer backend.Close()
for _, sub := range testCase.subCases { for _, sub := range testCase.subCases {

View file

@ -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"

View file

@ -35,7 +35,7 @@ func (s *HeadersSuite) TestCorsResponses(c *check.C) {
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
defer cmd.Process.Kill() defer cmd.Process.Kill()
backend := startTestServer("9000", http.StatusOK) backend := startTestServer("9000", http.StatusOK, "")
defer backend.Close() defer backend.Close()
err = try.GetRequest(backend.URL, 500*time.Millisecond, try.StatusCodeIs(http.StatusOK)) 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) c.Assert(err, checker.IsNil)
defer cmd.Process.Kill() defer cmd.Process.Kill()
backend := startTestServer("9000", http.StatusOK) backend := startTestServer("9000", http.StatusOK, "")
defer backend.Close() defer backend.Close()
err = try.GetRequest(backend.URL, 500*time.Millisecond, try.StatusCodeIs(http.StatusOK)) 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) c.Assert(err, checker.IsNil)
defer cmd.Process.Kill() defer cmd.Process.Kill()
backend := startTestServer("9000", http.StatusOK) backend := startTestServer("9000", http.StatusOK, "")
defer backend.Close() defer backend.Close()
err = try.GetRequest(backend.URL, 500*time.Millisecond, try.StatusCodeIs(http.StatusOK)) err = try.GetRequest(backend.URL, 500*time.Millisecond, try.StatusCodeIs(http.StatusOK))

View file

@ -20,9 +20,7 @@ import (
math "math" math "math"
proto "github.com/golang/protobuf/proto" proto "github.com/golang/protobuf/proto"
)
import (
context "context" context "context"
grpc "google.golang.org/grpc" grpc "google.golang.org/grpc"

View file

@ -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`)")) err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`snitest.org`)"))
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
backend1 := startTestServer("9010", http.StatusNoContent) backend1 := startTestServer("9010", http.StatusNoContent, "")
backend2 := startTestServer("9020", http.StatusResetContent) backend2 := startTestServer("9020", http.StatusResetContent, "")
defer backend1.Close() defer backend1.Close()
defer backend2.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`)")) err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`snitest.org`)"))
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
backend1 := startTestServer("9010", http.StatusNoContent) backend1 := startTestServer("9010", http.StatusNoContent, "")
backend2 := startTestServer("9020", http.StatusResetContent) backend2 := startTestServer("9020", http.StatusResetContent, "")
defer backend1.Close() defer backend1.Close()
defer backend2.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`)")) err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`snitest.net`)"))
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
backend1 := startTestServer("9010", http.StatusNoContent) backend1 := startTestServer("9010", http.StatusNoContent, "")
backend2 := startTestServer("9020", http.StatusResetContent) backend2 := startTestServer("9020", http.StatusResetContent, "")
defer backend1.Close() defer backend1.Close()
defer backend2.Close() defer backend2.Close()
@ -733,9 +733,12 @@ func (s *HTTPSSuite) TestWithRootCAsFileForHTTPSOnBackend(c *check.C) {
c.Assert(err, checker.IsNil) 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) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(statusCode) w.WriteHeader(statusCode)
if textContent != "" {
_, _ = w.Write([]byte(textContent))
}
}) })
listener, err := net.Listen("tcp", "127.0.0.1:"+port) listener, err := net.Listen("tcp", "127.0.0.1:"+port)
if err != nil { 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+"`)")) 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) c.Assert(err, checker.IsNil)
backend1 := startTestServer("9010", http.StatusNoContent) backend1 := startTestServer("9010", http.StatusNoContent, "")
backend2 := startTestServer("9020", http.StatusResetContent) backend2 := startTestServer("9020", http.StatusResetContent, "")
defer backend1.Close() defer backend1.Close()
defer backend2.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+"`)")) 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) c.Assert(err, checker.IsNil)
backend1 := startTestServer("9010", http.StatusNoContent) backend1 := startTestServer("9010", http.StatusNoContent, "")
backend2 := startTestServer("9020", http.StatusResetContent) backend2 := startTestServer("9020", http.StatusResetContent, "")
defer backend1.Close() defer backend1.Close()
defer backend2.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+"`)")) 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) c.Assert(err, checker.IsNil)
backend2 := startTestServer("9020", http.StatusResetContent) backend2 := startTestServer("9020", http.StatusResetContent, "")
defer backend2.Close() defer backend2.Close()
@ -1111,3 +1114,115 @@ func (s *HTTPSSuite) TestWithSNIDynamicCaseInsensitive(c *check.C) {
proto := conn.ConnectionState().NegotiatedProtocol proto := conn.ConnectionState().NegotiatedProtocol
c.Assert(proto, checker.Equals, "h2") 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)
}
}

View file

@ -2,6 +2,7 @@ package api
import ( import (
"net/http" "net/http"
"net/url"
"github.com/containous/traefik/v2/pkg/log" "github.com/containous/traefik/v2/pkg/log"
assetfs "github.com/elazarl/go-bindata-assetfs" assetfs "github.com/elazarl/go-bindata-assetfs"
@ -23,11 +24,29 @@ func (g DashboardHandler) Append(router *mux.Router) {
// Expose dashboard // Expose dashboard
router.Methods(http.MethodGet). router.Methods(http.MethodGet).
Path("/"). Path("/").
HandlerFunc(func(response http.ResponseWriter, request *http.Request) { HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
http.Redirect(response, request, request.Header.Get("X-Forwarded-Prefix")+"/dashboard/", http.StatusFound) http.Redirect(resp, req, safePrefix(req)+"/dashboard/", http.StatusFound)
}) })
router.Methods(http.MethodGet). router.Methods(http.MethodGet).
PathPrefix("/dashboard/"). PathPrefix("/dashboard/").
Handler(http.StripPrefix("/dashboard/", http.FileServer(g.Assets))) 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
}

54
pkg/api/dashboard_test.go Normal file
View file

@ -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)
})
}
}

View file

@ -28,6 +28,10 @@ func decodeRaw(node *parser.Node, vData reflect.Value, filters ...string) error
sortedKeys := sortKeys(vData, filters) sortedKeys := sortKeys(vData, filters)
for _, key := range sortedKeys { for _, key := range sortedKeys {
if vData.MapIndex(key).IsNil() {
continue
}
value := reflect.ValueOf(vData.MapIndex(key).Interface()) value := reflect.ValueOf(vData.MapIndex(key).Interface())
child := &parser.Node{Name: key.String()} child := &parser.Node{Name: key.String()}

View file

@ -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 { for _, test := range testCases {

View file

@ -448,9 +448,9 @@ func TestLBStatusUpdater(t *testing.T) {
svInfo := &runtime.ServiceInfo{} svInfo := &runtime.ServiceInfo{}
lbsu := NewLBStatusUpdater(lb, svInfo) lbsu := NewLBStatusUpdater(lb, svInfo)
newServer, err := url.Parse("http://foo.com") newServer, err := url.Parse("http://foo.com")
assert.Nil(t, err) assert.NoError(t, err)
err = lbsu.UpsertServer(newServer, roundrobin.Weight(1)) 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.Servers()), 1)
assert.Equal(t, len(lbsu.BalancerHandler.(*testLoadBalancer).Options()), 1) assert.Equal(t, len(lbsu.BalancerHandler.(*testLoadBalancer).Options()), 1)
statuses := svInfo.GetAllStatus() statuses := svInfo.GetAllStatus()
@ -461,7 +461,7 @@ func TestLBStatusUpdater(t *testing.T) {
break break
} }
err = lbsu.RemoveServer(newServer) err = lbsu.RemoveServer(newServer)
assert.Nil(t, err) assert.NoError(t, err)
assert.Equal(t, len(lbsu.Servers()), 0) assert.Equal(t, len(lbsu.Servers()), 0)
statuses = svInfo.GetAllStatus() statuses = svInfo.GetAllStatus()
assert.Equal(t, len(statuses), 1) assert.Equal(t, len(statuses), 1)

View file

@ -6,6 +6,7 @@ import (
"io" "io"
"net" "net"
"net/http" "net/http"
"net/textproto"
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
@ -100,6 +101,17 @@ func NewHandler(config *types.AccessLog) (*Handler, error) {
Level: logrus.InfoLevel, 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{ logHandler := &Handler{
config: config, config: config,
logger: logger, logger: logger,

View file

@ -41,11 +41,7 @@ var (
) )
func TestLogRotation(t *testing.T) { func TestLogRotation(t *testing.T) {
tempDir, err := ioutil.TempDir("", "traefik_") tempDir := createTempDir(t, "traefik_")
if err != nil {
t.Fatalf("Error setting up temporary directory: %s", err)
}
defer os.RemoveAll(tempDir)
fileName := filepath.Join(tempDir, "traefik.log") fileName := filepath.Join(tempDir, "traefik.log")
rotatedFileName := fileName + ".rotated" rotatedFileName := fileName + ".rotated"
@ -119,9 +115,106 @@ func lineCount(t *testing.T, fileName string) int {
return count 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) { func TestLoggerCLF(t *testing.T) {
tmpDir := createTempDir(t, CommonFormat) tmpDir := createTempDir(t, CommonFormat)
defer os.RemoveAll(tmpDir)
logFilePath := filepath.Join(tmpDir, logFileNameSuffix) logFilePath := filepath.Join(tmpDir, logFileNameSuffix)
config := &types.AccessLog{FilePath: logFilePath, Format: CommonFormat} config := &types.AccessLog{FilePath: logFilePath, Format: CommonFormat}
@ -136,7 +229,6 @@ func TestLoggerCLF(t *testing.T) {
func TestAsyncLoggerCLF(t *testing.T) { func TestAsyncLoggerCLF(t *testing.T) {
tmpDir := createTempDir(t, CommonFormat) tmpDir := createTempDir(t, CommonFormat)
defer os.RemoveAll(tmpDir)
logFilePath := filepath.Join(tmpDir, logFileNameSuffix) logFilePath := filepath.Join(tmpDir, logFileNameSuffix)
config := &types.AccessLog{FilePath: logFilePath, Format: CommonFormat, BufferingSize: 1024} config := &types.AccessLog{FilePath: logFilePath, Format: CommonFormat, BufferingSize: 1024}
@ -358,7 +450,6 @@ func TestLoggerJSON(t *testing.T) {
t.Parallel() t.Parallel()
tmpDir := createTempDir(t, JSONFormat) tmpDir := createTempDir(t, JSONFormat)
defer os.RemoveAll(tmpDir)
logFilePath := filepath.Join(tmpDir, logFileNameSuffix) logFilePath := filepath.Join(tmpDir, logFileNameSuffix)
@ -642,6 +733,8 @@ func createTempDir(t *testing.T, prefix string) string {
tmpDir, err := ioutil.TempDir("", prefix) tmpDir, err := ioutil.TempDir("", prefix)
require.NoError(t, err, "failed to create temp dir") require.NoError(t, err, "failed to create temp dir")
t.Cleanup(func() { _ = os.RemoveAll(tmpDir) })
return tmpDir return tmpDir
} }

View file

@ -5,11 +5,11 @@ import (
"errors" "errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"strconv"
"time" "time"
"github.com/containous/traefik/v2/pkg/log" "github.com/containous/traefik/v2/pkg/log"
"github.com/golang/protobuf/proto" "github.com/golang/protobuf/proto"
"github.com/hashicorp/go-version"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
extensionsv1beta1 "k8s.io/api/extensions/v1beta1" extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
networkingv1beta1 "k8s.io/api/networking/v1beta1" networkingv1beta1 "k8s.io/api/networking/v1beta1"
@ -55,7 +55,7 @@ type Client interface {
GetSecret(namespace, name string) (*corev1.Secret, bool, error) GetSecret(namespace, name string) (*corev1.Secret, bool, error)
GetEndpoints(namespace, name string) (*corev1.Endpoints, bool, error) GetEndpoints(namespace, name string) (*corev1.Endpoints, bool, error)
UpdateIngressStatus(ing *networkingv1beta1.Ingress, ip, hostname string) error UpdateIngressStatus(ing *networkingv1beta1.Ingress, ip, hostname string) error
GetServerVersion() (major, minor int, err error) GetServerVersion() (*version.Version, error)
} }
type clientWrapper struct { 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 serverVersion, err := c.GetServerVersion()
major, minor, err := c.GetServerVersion()
if err != nil { 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 = informers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod)
c.clusterFactory.Networking().V1beta1().IngressClasses().Informer().AddEventHandler(eventHandler) c.clusterFactory.Networking().V1beta1().IngressClasses().Informer().AddEventHandler(eventHandler)
c.clusterFactory.Start(stopCh) c.clusterFactory.Start(stopCh)
@ -341,7 +341,7 @@ func (c *clientWrapper) GetIngressClass() (*networkingv1beta1.IngressClass, erro
for _, ic := range ingressClasses { for _, ic := range ingressClasses {
if ic.Spec.Controller == traefikDefaultIngressClassController { 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. // GetServerVersion returns the cluster server version, or an error.
func (c *clientWrapper) GetServerVersion() (major, minor int, err error) { func (c *clientWrapper) GetServerVersion() (*version.Version, error) {
version, err := c.clientset.Discovery().ServerVersion() serverVersion, err := c.clientset.Discovery().ServerVersion()
if err != nil { 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) return version.NewVersion(serverVersion.GitVersion)
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
} }
// eventHandlerFunc will pass the obj on to the events channel or drop it. // 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 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)
}

View file

@ -5,6 +5,7 @@ import (
"io/ioutil" "io/ioutil"
"github.com/containous/traefik/v2/pkg/provider/kubernetes/k8s" "github.com/containous/traefik/v2/pkg/provider/kubernetes/k8s"
"github.com/hashicorp/go-version"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
extensionsv1beta1 "k8s.io/api/extensions/v1beta1" extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
"k8s.io/api/networking/v1beta1" "k8s.io/api/networking/v1beta1"
@ -20,8 +21,7 @@ type clientMock struct {
endpoints []*corev1.Endpoints endpoints []*corev1.Endpoints
ingressClass *networkingv1beta1.IngressClass ingressClass *networkingv1beta1.IngressClass
serverMajor int serverVersion *version.Version
serverMinor int
apiServiceError error apiServiceError error
apiSecretError error apiSecretError error
@ -31,11 +31,10 @@ type clientMock struct {
watchChan chan interface{} watchChan chan interface{}
} }
func newClientMock(major, minor int, paths ...string) clientMock { func newClientMock(serverVersion string, paths ...string) clientMock {
c := clientMock{ c := clientMock{}
serverMajor: major,
serverMinor: minor, c.serverVersion = version.Must(version.NewVersion(serverVersion))
}
for _, path := range paths { for _, path := range paths {
yamlContent, err := ioutil.ReadFile(path) yamlContent, err := ioutil.ReadFile(path)
@ -75,8 +74,8 @@ func (c clientMock) GetIngresses() []*v1beta1.Ingress {
return c.ingresses return c.ingresses
} }
func (c clientMock) GetServerVersion() (major, minor int, err error) { func (c clientMock) GetServerVersion() (*version.Version, error) {
return c.serverMajor, c.serverMinor, nil return c.serverVersion, nil
} }
func (c clientMock) GetService(namespace, name string) (*corev1.Service, bool, error) { func (c clientMock) GetService(namespace, name string) (*corev1.Service, bool, error) {

View file

@ -0,0 +1,11 @@
kind: Endpoints
apiVersion: v1
metadata:
name: service1
namespace: testing
subsets:
- addresses:
- ip: 10.10.0.1
ports:
- port: 8080

View file

@ -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

View file

@ -0,0 +1,10 @@
kind: Service
apiVersion: v1
metadata:
name: service1
namespace: testing
spec:
ports:
- port: 80
clusterIp: 10.0.0.1

View file

@ -0,0 +1,11 @@
kind: Endpoints
apiVersion: v1
metadata:
name: service1
namespace: testing
subsets:
- addresses:
- ip: 10.10.0.1
ports:
- port: 8080

View file

@ -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

View file

@ -0,0 +1,10 @@
kind: Service
apiVersion: v1
metadata:
name: service1
namespace: testing
spec:
ports:
- port: 80
clusterIp: 10.0.0.1

View file

@ -0,0 +1,11 @@
kind: Endpoints
apiVersion: v1
metadata:
name: service1
namespace: testing
subsets:
- addresses:
- ip: 10.10.0.1
ports:
- port: 8080

View file

@ -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

View file

@ -0,0 +1,10 @@
kind: Service
apiVersion: v1
metadata:
name: service1
namespace: testing
spec:
ports:
- port: 80
clusterIp: 10.0.0.1

View file

@ -0,0 +1,11 @@
kind: Endpoints
apiVersion: v1
metadata:
name: service1
namespace: testing
subsets:
- addresses:
- ip: 10.10.0.1
ports:
- port: 8080

View file

@ -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

View file

@ -0,0 +1,10 @@
kind: Service
apiVersion: v1
metadata:
name: service1
namespace: testing
spec:
ports:
- port: 80
clusterIp: 10.0.0.1

View file

@ -0,0 +1,11 @@
kind: Endpoints
apiVersion: v1
metadata:
name: service1
namespace: testing
subsets:
- addresses:
- ip: 10.10.0.1
ports:
- port: 8080

View file

@ -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

View file

@ -0,0 +1,10 @@
kind: Service
apiVersion: v1
metadata:
name: service1
namespace: testing
spec:
ports:
- port: 80
clusterIp: 10.0.0.1

View file

@ -0,0 +1,11 @@
kind: Endpoints
apiVersion: v1
metadata:
name: service1
namespace: testing
subsets:
- addresses:
- ip: 10.10.0.1
ports:
- port: 8080

View file

@ -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

View file

@ -0,0 +1,10 @@
kind: Service
apiVersion: v1
metadata:
name: service1
namespace: testing
spec:
ports:
- port: 80
clusterIp: 10.0.0.1

View file

@ -183,7 +183,7 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl
TCP: &dynamic.TCPConfiguration{}, TCP: &dynamic.TCPConfiguration{},
} }
major, minor, err := client.GetServerVersion() serverVersion, err := client.GetServerVersion()
if err != nil { if err != nil {
log.FromContext(ctx).Errorf("Failed to get server version: %v", err) log.FromContext(ctx).Errorf("Failed to get server version: %v", err)
return conf return conf
@ -191,16 +191,10 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl
var ingressClass *networkingv1beta1.IngressClass var ingressClass *networkingv1beta1.IngressClass
if major >= 1 && minor >= 18 { if supportsIngressClass(serverVersion) {
ic, err := client.GetIngressClass() ic, err := client.GetIngressClass()
if err != nil { if err != nil {
log.FromContext(ctx).Errorf("Failed to find an ingress class: %v", err) log.FromContext(ctx).Warnf("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
} }
ingressClass = ic 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 { 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 || // configuration through the new kubernetes ingressClass
providerIngressClass == ingress.Annotations[annotationKubernetesIngressClass] || 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 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 { if len(pa.Path) > 0 {
matcher := defaultPathMatcher 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)) rules = append(rules, fmt.Sprintf("%s(`%s`)", matcher, pa.Path))

View file

@ -24,10 +24,10 @@ func Bool(v bool) *bool { return &v }
func TestLoadConfigurationFromIngresses(t *testing.T) { func TestLoadConfigurationFromIngresses(t *testing.T) {
testCases := []struct { testCases := []struct {
desc string desc string
ingressClass string ingressClass string
serverMinor int serverVersion string
expected *dynamic.Configuration expected *dynamic.Configuration
}{ }{
{ {
desc: "Empty ingresses", desc: "Empty ingresses",
@ -925,8 +925,8 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
}, },
}, },
{ {
desc: "v18 Ingress with ingressClass", desc: "v18 Ingress with ingressClass",
serverMinor: 18, serverVersion: "v1.18",
expected: &dynamic.Configuration{ expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{}, TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{ HTTP: &dynamic.HTTPConfiguration{
@ -953,8 +953,148 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
}, },
}, },
{ {
desc: "v18 Ingress with missing ingressClass", desc: "v18 Ingress with no pathType",
serverMinor: 18, 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{ expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{}, TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{ 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 { for _, test := range testCases {
test := test test := test
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
t.Parallel() t.Parallel()
@ -993,12 +1162,12 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
paths = append(paths, generateTestFilename("_ingressclass", test.desc)) paths = append(paths, generateTestFilename("_ingressclass", test.desc))
} }
serverMinor := 17 serverVersion := test.serverVersion
if test.serverMinor != 0 { if serverVersion == "" {
serverMinor = test.serverMinor serverVersion = "v1.17"
} }
clientMock := newClientMock(1, serverMinor, paths...) clientMock := newClientMock(serverVersion, paths...)
p := Provider{IngressClass: test.ingressClass} p := Provider{IngressClass: test.ingressClass}
conf := p.loadConfigurationFromIngresses(context.Background(), clientMock) conf := p.loadConfigurationFromIngresses(context.Background(), clientMock)
@ -1179,7 +1348,7 @@ func TestGetCertificates(t *testing.T) {
if test.errResult != "" { if test.errResult != "" {
assert.EqualError(t, err, test.errResult) assert.EqualError(t, err, test.errResult)
} else { } else {
assert.Nil(t, err) assert.NoError(t, err)
assert.Equal(t, test.result, tlsConfigs) assert.Equal(t, test.result, tlsConfigs)
} }
}) })

View file

@ -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": {}
}

View file

@ -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) 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 { if err != nil {
return "", fmt.Errorf("invalid entry point %q address %q: %w", return "", fmt.Errorf("invalid entry point %q address %q: %w",
name, i.staticCfg.EntryPoints[def.EntryPoint.To].Address, err) name, i.staticCfg.EntryPoints[def.EntryPoint.To].Address, err)

View file

@ -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 { for _, test := range testCases {

View file

@ -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) })) handlers, err := result.Then(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }))
if test.expectedError != nil { if test.expectedError != nil {
require.NotNil(t, err) require.Error(t, err)
require.Equal(t, test.expectedError.Error(), err.Error()) require.Equal(t, test.expectedError.Error(), err.Error())
} else { } else {
require.NoError(t, err) require.NoError(t, err)

View file

@ -5,7 +5,9 @@ import (
"crypto/tls" "crypto/tls"
"errors" "errors"
"fmt" "fmt"
"net"
"net/http" "net/http"
"strings"
"github.com/containous/traefik/v2/pkg/config/runtime" "github.com/containous/traefik/v2/pkg/config/runtime"
"github.com/containous/traefik/v2/pkg/log" "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) log.FromContext(ctx).Errorf("Error during the build of the default TLS configuration: %v", err)
} }
router.HTTPSHandler(handlerHTTPS, defaultTLSConf)
if len(configsHTTP) > 0 { if len(configsHTTP) > 0 {
router.AddRouteHTTPTLS("*", defaultTLSConf) router.AddRouteHTTPTLS("*", defaultTLSConf)
} }
// Keyed by domain, then by options reference. // Keyed by domain, then by options reference.
tlsOptionsForHostSNI := map[string]map[string]nameAndConfig{} tlsOptionsForHostSNI := map[string]map[string]nameAndConfig{}
tlsOptionsForHost := map[string]string{}
for routerHTTPName, routerHTTPConfig := range configsHTTP { for routerHTTPName, routerHTTPConfig := range configsHTTP {
if len(routerHTTPConfig.TLS.Options) == 0 || routerHTTPConfig.TLS.Options == defaultTLSConfigName { if len(routerHTTPConfig.TLS.Options) == 0 || routerHTTPConfig.TLS.Options == defaultTLSConfigName {
continue continue
@ -141,6 +142,7 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string
continue continue
} }
// domain is already in lower case thanks to the domain parsing
if tlsOptionsForHostSNI[domain] == nil { if tlsOptionsForHostSNI[domain] == nil {
tlsOptionsForHostSNI[domain] = make(map[string]nameAndConfig) tlsOptionsForHostSNI[domain] = make(map[string]nameAndConfig)
} }
@ -148,10 +150,52 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string
routerName: routerHTTPName, routerName: routerHTTPName,
TLSConfig: tlsConf, 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) logger := log.FromContext(ctx)
for hostSNI, tlsConfigs := range tlsOptionsForHostSNI { for hostSNI, tlsConfigs := range tlsOptionsForHostSNI {
if len(tlsConfigs) == 1 { if len(tlsConfigs) == 1 {
@ -248,3 +292,17 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string
return router, nil 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"
}

View file

@ -193,7 +193,7 @@ func TestManager_BuildTCP(t *testing.T) {
assert.EqualError(t, err, test.expectedError) assert.EqualError(t, err, test.expectedError)
require.Nil(t, handler) require.Nil(t, handler)
} else { } else {
assert.Nil(t, err) assert.NoError(t, err)
require.NotNil(t, handler) require.NotNil(t, handler)
} }
}) })

View file

@ -193,7 +193,7 @@ func TestManager_BuildUDP(t *testing.T) {
assert.EqualError(t, err, test.expectedError) assert.EqualError(t, err, test.expectedError)
require.Nil(t, handler) require.Nil(t, handler)
} else { } else {
assert.Nil(t, err) assert.NoError(t, err)
require.NotNil(t, handler) require.NotNil(t, handler)
} }
}) })

View file

@ -11,6 +11,7 @@ import (
"time" "time"
"github.com/containous/traefik/v2/pkg/log" "github.com/containous/traefik/v2/pkg/log"
"github.com/containous/traefik/v2/pkg/types"
) )
// Router is a TCP router. // Router is a TCP router.
@ -65,7 +66,7 @@ func (r *Router) ServeTCP(conn WriteCloser) {
} }
// FIXME Optimize and test the routing table before helloServerName // FIXME Optimize and test the routing table before helloServerName
serverName = strings.ToLower(serverName) serverName = types.CanonicalDomain(serverName)
if r.routingTable != nil && serverName != "" { if r.routingTable != nil && serverName != "" {
if target, ok := r.routingTable[serverName]; ok { if target, ok := r.routingTable[serverName]; ok {
target.ServeTCP(r.GetConn(conn, peeked)) target.ServeTCP(r.GetConn(conn, peeked))

View file

@ -13,14 +13,14 @@ import (
"github.com/patrickmn/go-cache" "github.com/patrickmn/go-cache"
) )
// CertificateStore store for dynamic and static certificates. // CertificateStore store for dynamic certificates.
type CertificateStore struct { type CertificateStore struct {
DynamicCerts *safe.Safe DynamicCerts *safe.Safe
DefaultCertificate *tls.Certificate DefaultCertificate *tls.Certificate
CertCache *cache.Cache CertCache *cache.Cache
} }
// NewCertificateStore create a store for dynamic and static certificates. // NewCertificateStore create a store for dynamic certificates.
func NewCertificateStore() *CertificateStore { func NewCertificateStore() *CertificateStore {
return &CertificateStore{ return &CertificateStore{
DynamicCerts: &safe.Safe{}, DynamicCerts: &safe.Safe{},
@ -37,7 +37,7 @@ func (c CertificateStore) getDefaultCertificateDomains() []string {
x509Cert, err := x509.ParseCertificate(c.DefaultCertificate.Certificate[0]) x509Cert, err := x509.ParseCertificate(c.DefaultCertificate.Certificate[0])
if err != nil { 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 return allCerts
} }