Merge branch 'master' of github.com:traefik/traefik
All checks were successful
Build & Push / build-and-push (push) Successful in 10m3s

This commit is contained in:
baalajimaestro 2024-09-29 14:59:29 +05:30
commit 93ec8f7636
Signed by: baalajimaestro
GPG key ID: B5B69626E67EE82A
74 changed files with 5307 additions and 5022 deletions

View file

@ -229,7 +229,7 @@ issues:
text: 'struct-tag: unknown option ''inline'' in JSON tag'
linters:
- revive
- path: pkg/server/service/bufferpool.go
- path: pkg/proxy/httputil/bufferpool.go
text: 'SA6002: argument should be pointer-like to avoid allocations'
- path: pkg/server/middleware/middlewares.go
text: "Function 'buildConstructor' has too many statements"
@ -283,3 +283,7 @@ issues:
- path: pkg/provider/acme/local_store.go
linters:
- musttag
- path: pkg/types/metrics.go
linters:
- goconst

View file

@ -104,7 +104,7 @@ test-integration: binary
#? test-gateway-api-conformance: Run the conformance tests
test-gateway-api-conformance: build-image-dirty
# In case of a new Minor/Major version, the k8sConformanceTraefikVersion needs to be updated.
GOOS=$(GOOS) GOARCH=$(GOARCH) go test ./integration -v -test.run K8sConformanceSuite -k8sConformance -k8sConformanceTraefikVersion="v3.1" $(TESTFLAGS)
GOOS=$(GOOS) GOARCH=$(GOARCH) go test ./integration -v -test.run K8sConformanceSuite -k8sConformance -k8sConformanceTraefikVersion="v3.2" $(TESTFLAGS)
.PHONY: test-ui-unit
#? test-ui-unit: Run the unit tests for the webui

View file

@ -37,6 +37,8 @@ import (
"github.com/traefik/traefik/v3/pkg/provider/aggregator"
"github.com/traefik/traefik/v3/pkg/provider/tailscale"
"github.com/traefik/traefik/v3/pkg/provider/traefik"
"github.com/traefik/traefik/v3/pkg/proxy"
"github.com/traefik/traefik/v3/pkg/proxy/httputil"
"github.com/traefik/traefik/v3/pkg/safe"
"github.com/traefik/traefik/v3/pkg/server"
"github.com/traefik/traefik/v3/pkg/server/middleware"
@ -281,10 +283,16 @@ func setupServer(staticConfiguration *static.Configuration) (*server.Server, err
log.Info().Msg("Successfully obtained SPIFFE SVID.")
}
roundTripperManager := service.NewRoundTripperManager(spiffeX509Source)
transportManager := service.NewTransportManager(spiffeX509Source)
var proxyBuilder service.ProxyBuilder = httputil.NewProxyBuilder(transportManager, semConvMetricRegistry)
if staticConfiguration.Experimental != nil && staticConfiguration.Experimental.FastProxy != nil {
proxyBuilder = proxy.NewSmartBuilder(transportManager, proxyBuilder, *staticConfiguration.Experimental.FastProxy)
}
dialerManager := tcp.NewDialerManager(spiffeX509Source)
acmeHTTPHandler := getHTTPChallengeHandler(acmeProviders, httpChallengeProvider)
managerFactory := service.NewManagerFactory(*staticConfiguration, routinesPool, observabilityMgr, roundTripperManager, acmeHTTPHandler)
managerFactory := service.NewManagerFactory(*staticConfiguration, routinesPool, observabilityMgr, transportManager, proxyBuilder, acmeHTTPHandler)
// Router factory
@ -318,7 +326,8 @@ func setupServer(staticConfiguration *static.Configuration) (*server.Server, err
// Server Transports
watcher.AddListener(func(conf dynamic.Configuration) {
roundTripperManager.Update(conf.HTTP.ServersTransports)
transportManager.Update(conf.HTTP.ServersTransports)
proxyBuilder.Update(conf.HTTP.ServersTransports)
dialerManager.Update(conf.TCP.ServersTransports)
})

View file

@ -101,7 +101,7 @@ If none are set, the default is to use the `requestHost`.
#### `sourceCriterion.ipStrategy`
The `ipStrategy` option defines two parameters that configures how Traefik determines the client IP: `depth`, and `excludedIPs`.
The `ipStrategy` option defines three parameters that configures how Traefik determines the client IP: `depth`, `excludedIPs` and `ipv6Subnet`.
!!! important "As a middleware, InFlightReq happens before the actual proxying to the backend takes place. In addition, the previous network hop only gets appended to `X-Forwarded-For` during the last stages of proxying, i.e. after it has already passed through the middleware. Therefore, during InFlightReq, as the previous network hop is not yet present in `X-Forwarded-For`, it cannot be used and/or relied upon."
@ -112,6 +112,9 @@ The `depth` option tells Traefik to use the `X-Forwarded-For` header and select
- If `depth` is greater than the total number of IPs in `X-Forwarded-For`, then the client IP is empty.
- `depth` is ignored if its value is less than or equal to 0.
If `ipStrategy.ipv6Subnet` is provided and the selected IP is IPv6, the IP is transformed into the first IP of the subnet it belongs to.
See [ipStrategy.ipv6Subnet](#ipstrategyipv6subnet) for more details.
!!! example "Example of Depth & X-Forwarded-For"
If `depth` is set to 2, and the request `X-Forwarded-For` header is `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` then the "real" client IP is `"10.0.0.1"` (at depth 4) but the IP used as the criterion is `"12.0.0.1"` (`depth=2`).
@ -218,6 +221,63 @@ http:
excludedIPs = ["127.0.0.1/32", "192.168.1.7"]
```
##### `ipStrategy.ipv6Subnet`
This strategy applies to `Depth` and `RemoteAddr` strategy only.
If `ipv6Subnet` is provided and the selected IP is IPv6, the IP is transformed into the first IP of the subnet it belongs to.
This is useful for grouping IPv6 addresses into subnets to prevent bypassing this middleware by obtaining a new IPv6.
- `ipv6Subnet` is ignored if its value is outside of 0-128 interval
!!! example "Example of ipv6Subnet"
If `ipv6Subnet` is provided, the IP is transformed in the following way.
| `IP` | `ipv6Subnet` | clientIP |
|---------------------------|--------------|-----------------------|
| `"::abcd:1111:2222:3333"` | `64` | `"::0:0:0:0"` |
| `"::abcd:1111:2222:3333"` | `80` | `"::abcd:0:0:0:0"` |
| `"::abcd:1111:2222:3333"` | `96` | `"::abcd:1111:0:0:0"` |
```yaml tab="Docker & Swarm"
labels:
- "traefik.http.middlewares.test-inflightreq.inflightreq.sourcecriterion.ipstrategy.ipv6Subnet=64"
```
```yaml tab="Kubernetes"
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: test-inflightreq
spec:
inFlightReq:
sourceCriterion:
ipStrategy:
ipv6Subnet: 64
```
```yaml tab="Consul Catalog"
- "traefik.http.middlewares.test-inflightreq.inflightreq.sourcecriterion.ipstrategy.ipv6Subnet=64"
```
```yaml tab="File (YAML)"
http:
middlewares:
test-inflightreq:
inFlightReq:
sourceCriterion:
ipStrategy:
ipv6Subnet: 64
```
```toml tab="File (TOML)"
[http.middlewares]
[http.middlewares.test-inflightreq.inflightreq]
[http.middlewares.test-inflightreq.inFlightReq.sourceCriterion.ipStrategy]
ipv6Subnet = 64
```
#### `sourceCriterion.requestHeaderName`
Name of the header used to group incoming requests.

View file

@ -75,6 +75,9 @@ The `depth` option tells Traefik to use the `X-Forwarded-For` header and take th
- If `depth` is greater than the total number of IPs in `X-Forwarded-For`, then the client IP will be empty.
- `depth` is ignored if its value is less than or equal to 0.
If `ipStrategy.ipv6Subnet` is provided and the selected IP is IPv6, the IP is transformed into the first IP of the subnet it belongs to.
See [ipStrategy.ipv6Subnet](#ipstrategyipv6subnet) for more details.
!!! example "Examples of Depth & X-Forwarded-For"
If `depth` is set to 2, and the request `X-Forwarded-For` header is `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` then the "real" client IP is `"10.0.0.1"` (at depth 4) but the IP used is `"12.0.0.1"` (`depth=2`).
@ -204,3 +207,60 @@ http:
[http.middlewares.test-ipallowlist.ipAllowList.ipStrategy]
excludedIPs = ["127.0.0.1/32", "192.168.1.7"]
```
#### `ipStrategy.ipv6Subnet`
This strategy applies to `Depth` and `RemoteAddr` strategy only.
If `ipv6Subnet` is provided and the selected IP is IPv6, the IP is transformed into the first IP of the subnet it belongs to.
This is useful for grouping IPv6 addresses into subnets to prevent bypassing this middleware by obtaining a new IPv6.
- `ipv6Subnet` is ignored if its value is outside of 0-128 interval
!!! example "Example of ipv6Subnet"
If `ipv6Subnet` is provided, the IP is transformed in the following way.
| `IP` | `ipv6Subnet` | clientIP |
|---------------------------|--------------|-----------------------|
| `"::abcd:1111:2222:3333"` | `64` | `"::0:0:0:0"` |
| `"::abcd:1111:2222:3333"` | `80` | `"::abcd:0:0:0:0"` |
| `"::abcd:1111:2222:3333"` | `96` | `"::abcd:1111:0:0:0"` |
```yaml tab="Docker & Swarm"
labels:
- "traefik.http.middlewares.test-ipallowlist.ipallowlist.sourcecriterion.ipstrategy.ipv6Subnet=64"
```
```yaml tab="Kubernetes"
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: test-ipallowlist
spec:
ipallowlist:
sourceCriterion:
ipStrategy:
ipv6Subnet: 64
```
```yaml tab="Consul Catalog"
- "traefik.http.middlewares.test-ipallowlist.ipallowlist.sourcecriterion.ipstrategy.ipv6Subnet=64"
```
```yaml tab="File (YAML)"
http:
middlewares:
test-ipallowlist:
ipallowlist:
sourceCriterion:
ipStrategy:
ipv6Subnet: 64
```
```toml tab="File (TOML)"
[http.middlewares]
[http.middlewares.test-ipallowlist.ipallowlist]
[http.middlewares.test-ipallowlist.ipallowlist.sourceCriterion.ipStrategy]
ipv6Subnet = 64
```

View file

@ -81,6 +81,9 @@ The `depth` option tells Traefik to use the `X-Forwarded-For` header and take th
- If `depth` is greater than the total number of IPs in `X-Forwarded-For`, then the client IP will be empty.
- `depth` is ignored if its value is less than or equal to 0.
If `ipStrategy.ipv6Subnet` is provided and the selected IP is IPv6, the IP is transformed into the first IP of the subnet it belongs to.
See [ipStrategy.ipv6Subnet](#ipstrategyipv6subnet) for more details.
!!! example "Examples of Depth & X-Forwarded-For"
If `depth` is set to 2, and the request `X-Forwarded-For` header is `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` then the "real" client IP is `"10.0.0.1"` (at depth 4) but the IP used for the whitelisting is `"12.0.0.1"` (`depth=2`).
@ -210,3 +213,60 @@ http:
[http.middlewares.test-ipwhitelist.ipWhiteList.ipStrategy]
excludedIPs = ["127.0.0.1/32", "192.168.1.7"]
```
#### `ipStrategy.ipv6Subnet`
This strategy applies to `Depth` and `RemoteAddr` strategy only.
If `ipv6Subnet` is provided and the selected IP is IPv6, the IP is transformed into the first IP of the subnet it belongs to.
This is useful for grouping IPv6 addresses into subnets to prevent bypassing this middleware by obtaining a new IPv6.
- `ipv6Subnet` is ignored if its value is outside of 0-128 interval
!!! example "Example of ipv6Subnet"
If `ipv6Subnet` is provided, the IP is transformed in the following way.
| `IP` | `ipv6Subnet` | clientIP |
|---------------------------|--------------|-----------------------|
| `"::abcd:1111:2222:3333"` | `64` | `"::0:0:0:0"` |
| `"::abcd:1111:2222:3333"` | `80` | `"::abcd:0:0:0:0"` |
| `"::abcd:1111:2222:3333"` | `96` | `"::abcd:1111:0:0:0"` |
```yaml tab="Docker & Swarm"
labels:
- "traefik.http.middlewares.test-ipWhiteList.ipWhiteList.sourcecriterion.ipstrategy.ipv6Subnet=64"
```
```yaml tab="Kubernetes"
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: test-ipWhiteList
spec:
ipWhiteList:
sourceCriterion:
ipStrategy:
ipv6Subnet: 64
```
```yaml tab="Consul Catalog"
- "traefik.http.middlewares.test-ipWhiteList.ipWhiteList.sourcecriterion.ipstrategy.ipv6Subnet=64"
```
```yaml tab="File (YAML)"
http:
middlewares:
test-ipWhiteList:
ipWhiteList:
sourceCriterion:
ipStrategy:
ipv6Subnet: 64
```
```toml tab="File (TOML)"
[http.middlewares]
[http.middlewares.test-ipWhiteList.ipWhiteList]
[http.middlewares.test-ipWhiteList.ipWhiteList.sourceCriterion.ipStrategy]
ipv6Subnet = 64
```

View file

@ -211,7 +211,7 @@ If none are set, the default is to use the request's remote address field (as an
#### `sourceCriterion.ipStrategy`
The `ipStrategy` option defines two parameters that configures how Traefik determines the client IP: `depth`, and `excludedIPs`.
The `ipStrategy` option defines three parameters that configures how Traefik determines the client IP: `depth`, `excludedIPs` and `ipv6Subnet`.
!!! important "As a middleware, rate-limiting happens before the actual proxying to the backend takes place. In addition, the previous network hop only gets appended to `X-Forwarded-For` during the last stages of proxying, i.e. after it has already passed through rate-limiting. Therefore, during rate-limiting, as the previous network hop is not yet present in `X-Forwarded-For`, it cannot be found and/or relied upon."
@ -222,6 +222,9 @@ The `depth` option tells Traefik to use the `X-Forwarded-For` header and select
- If `depth` is greater than the total number of IPs in `X-Forwarded-For`, then the client IP is empty.
- `depth` is ignored if its value is less than or equal to 0.
If `ipStrategy.ipv6Subnet` is provided and the selected IP is IPv6, the IP is transformed into the first IP of the subnet it belongs to.
See [ipStrategy.ipv6Subnet](#ipstrategyipv6subnet) for more details.
!!! example "Example of Depth & X-Forwarded-For"
If `depth` is set to 2, and the request `X-Forwarded-For` header is `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` then the "real" client IP is `"10.0.0.1"` (at depth 4) but the IP used as the criterion is `"12.0.0.1"` (`depth=2`).
@ -355,6 +358,63 @@ http:
excludedIPs = ["127.0.0.1/32", "192.168.1.7"]
```
##### `ipStrategy.ipv6Subnet`
This strategy applies to `Depth` and `RemoteAddr` strategy only.
If `ipv6Subnet` is provided and the selected IP is IPv6, the IP is transformed into the first IP of the subnet it belongs to.
This is useful for grouping IPv6 addresses into subnets to prevent bypassing this middleware by obtaining a new IPv6.
- `ipv6Subnet` is ignored if its value is outside of 0-128 interval
!!! example "Example of ipv6Subnet"
If `ipv6Subnet` is provided, the IP is transformed in the following way.
| `IP` | `ipv6Subnet` | clientIP |
|---------------------------|--------------|-----------------------|
| `"::abcd:1111:2222:3333"` | `64` | `"::0:0:0:0"` |
| `"::abcd:1111:2222:3333"` | `80` | `"::abcd:0:0:0:0"` |
| `"::abcd:1111:2222:3333"` | `96` | `"::abcd:1111:0:0:0"` |
```yaml tab="Docker & Swarm"
labels:
- "traefik.http.middlewares.test-ratelimit.ratelimit.sourcecriterion.ipstrategy.ipv6Subnet=64"
```
```yaml tab="Kubernetes"
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: test-ratelimit
spec:
ratelimit:
sourceCriterion:
ipStrategy:
ipv6Subnet: 64
```
```yaml tab="Consul Catalog"
- "traefik.http.middlewares.test-ratelimit.ratelimit.sourcecriterion.ipstrategy.ipv6Subnet=64"
```
```yaml tab="File (YAML)"
http:
middlewares:
test-ratelimit:
ratelimit:
sourceCriterion:
ipStrategy:
ipv6Subnet: 64
```
```toml tab="File (TOML)"
[http.middlewares]
[http.middlewares.test-ratelimit.ratelimit]
[http.middlewares.test-ratelimit.ratelimit.sourceCriterion.ipStrategy]
ipv6Subnet = 64
```
#### `sourceCriterion.requestHeaderName`
Name of the header used to group incoming requests.

View file

@ -78,7 +78,7 @@ Please use the `disableClusterScopeResources` option instead to avoid cluster sc
## v3.1 to v3.2
### Kubernetes Gateway Provider RBACs
### Kubernetes Gateway Provider Standard Channel
Starting with v3.2, the Kubernetes Gateway Provider now supports [GRPCRoute](https://gateway-api.sigs.k8s.io/api-types/grpcroute/).
@ -103,3 +103,34 @@ the `grcroutes` and `grpcroutes/status` rights have to be added.
- update
...
```
### Kubernetes Gateway Provider Experimental Channel
!!! warning "Breaking changes"
Because of a breaking change introduced in Kubernetes Gateway [v1.2.0-rc1](https://github.com/kubernetes-sigs/gateway-api/releases/tag/v1.2.0-rc1),
Traefik v3.2 only supports Kubernetes Gateway v1.2.x when experimental channel features are enabled.
Starting with v3.2, the Kubernetes Gateway Provider now supports [BackendTLSPolicy](https://gateway-api.sigs.k8s.io/api-types/backendtlspolicy/).
Therefore, in the corresponding RBACs (see [KubernetesGateway](../reference/dynamic-configuration/kubernetes-gateway.md#rbac) provider RBACs),
the `backendtlspolicies` and `backendtlspolicies/status` rights have to be added.
```yaml
...
- apiGroups:
- gateway.networking.k8s.io
resources:
- backendtlspolicies
verbs:
- get
- list
- watch
- apiGroups:
- gateway.networking.k8s.io
resources:
- backendtlspolicies/status
verbs:
- update
...
```

View file

@ -139,6 +139,28 @@ metrics:
--metrics.otlp.pushInterval=10s
```
#### `serviceName`
_Optional, Default="traefik"_
OTEL service name to use.
```yaml tab="File (YAML)"
metrics:
otlp:
serviceName: name
```
```toml tab="File (TOML)"
[metrics]
[metrics.otlp]
serviceName = "name"
```
```bash tab="CLI"
--metrics.otlp.serviceName=name
```
### HTTP configuration
_Optional_

View file

@ -56,6 +56,8 @@ _Optional, Default=15s_
Defines the polling interval.
!!! note "This option is ignored when the [watch](#watch) mode is enabled."
```yaml tab="File (YAML)"
providers:
nomad:
@ -74,6 +76,62 @@ providers:
# ...
```
### `watch`
_Optional, Default=false_
Enables the watch mode to refresh the configuration on a per-event basis.
```yaml tab="File (YAML)"
providers:
nomad:
watch: true
# ...
```
```toml tab="File (TOML)"
[providers.nomad]
watch = true
# ...
```
```bash tab="CLI"
--providers.nomad.watch
# ...
```
### `throttleDuration`
_Optional, Default=0s_
The `throttleDuration` option defines how often the provider is allowed to handle service events from Nomad.
This prevents a Nomad cluster that updates many times per second from continuously changing your Traefik configuration.
If left empty, the provider does not apply any throttling and does not drop any Nomad service events.
The value of `throttleDuration` should be provided in seconds or as a valid duration format,
see [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration).
!!! warning "This option is only compatible with the [watch](#watch) mode."
```yaml tab="File (YAML)"
providers:
nomad:
throttleDuration: 2s
# ...
```
```toml tab="File (TOML)"
[providers.nomad]
throttleDuration = "2s"
# ...
```
```bash tab="CLI"
--providers.nomad.throttleDuration=2s
# ...
```
### `prefix`
_required, Default="traefik"_

View file

@ -85,15 +85,18 @@
- "traefik.http.middlewares.middleware13.ipallowlist.ipstrategy=true"
- "traefik.http.middlewares.middleware13.ipallowlist.ipstrategy.depth=42"
- "traefik.http.middlewares.middleware13.ipallowlist.ipstrategy.excludedips=foobar, foobar"
- "traefik.http.middlewares.middleware13.ipallowlist.ipstrategy.ipv6subnet=42"
- "traefik.http.middlewares.middleware13.ipallowlist.rejectstatuscode=42"
- "traefik.http.middlewares.middleware13.ipallowlist.sourcerange=foobar, foobar"
- "traefik.http.middlewares.middleware14.ipwhitelist.ipstrategy=true"
- "traefik.http.middlewares.middleware14.ipwhitelist.ipstrategy.depth=42"
- "traefik.http.middlewares.middleware14.ipwhitelist.ipstrategy.excludedips=foobar, foobar"
- "traefik.http.middlewares.middleware14.ipwhitelist.ipstrategy.ipv6subnet=42"
- "traefik.http.middlewares.middleware14.ipwhitelist.sourcerange=foobar, foobar"
- "traefik.http.middlewares.middleware15.inflightreq.amount=42"
- "traefik.http.middlewares.middleware15.inflightreq.sourcecriterion.ipstrategy.depth=42"
- "traefik.http.middlewares.middleware15.inflightreq.sourcecriterion.ipstrategy.excludedips=foobar, foobar"
- "traefik.http.middlewares.middleware15.inflightreq.sourcecriterion.ipstrategy.ipv6subnet=42"
- "traefik.http.middlewares.middleware15.inflightreq.sourcecriterion.requestheadername=foobar"
- "traefik.http.middlewares.middleware15.inflightreq.sourcecriterion.requesthost=true"
- "traefik.http.middlewares.middleware16.passtlsclientcert.info.issuer.commonname=true"
@ -125,6 +128,7 @@
- "traefik.http.middlewares.middleware18.ratelimit.period=42s"
- "traefik.http.middlewares.middleware18.ratelimit.sourcecriterion.ipstrategy.depth=42"
- "traefik.http.middlewares.middleware18.ratelimit.sourcecriterion.ipstrategy.excludedips=foobar, foobar"
- "traefik.http.middlewares.middleware18.ratelimit.sourcecriterion.ipstrategy.ipv6subnet=42"
- "traefik.http.middlewares.middleware18.ratelimit.sourcecriterion.requestheadername=foobar"
- "traefik.http.middlewares.middleware18.ratelimit.sourcecriterion.requesthost=true"
- "traefik.http.middlewares.middleware19.redirectregex.permanent=true"

View file

@ -227,12 +227,14 @@
[http.middlewares.Middleware13.ipAllowList.ipStrategy]
depth = 42
excludedIPs = ["foobar", "foobar"]
ipv6Subnet = 42
[http.middlewares.Middleware14]
[http.middlewares.Middleware14.ipWhiteList]
sourceRange = ["foobar", "foobar"]
[http.middlewares.Middleware14.ipWhiteList.ipStrategy]
depth = 42
excludedIPs = ["foobar", "foobar"]
ipv6Subnet = 42
[http.middlewares.Middleware15]
[http.middlewares.Middleware15.inFlightReq]
amount = 42
@ -242,6 +244,7 @@
[http.middlewares.Middleware15.inFlightReq.sourceCriterion.ipStrategy]
depth = 42
excludedIPs = ["foobar", "foobar"]
ipv6Subnet = 42
[http.middlewares.Middleware16]
[http.middlewares.Middleware16.passTLSClientCert]
pem = true
@ -286,6 +289,7 @@
[http.middlewares.Middleware18.rateLimit.sourceCriterion.ipStrategy]
depth = 42
excludedIPs = ["foobar", "foobar"]
ipv6Subnet = 42
[http.middlewares.Middleware19]
[http.middlewares.Middleware19.redirectRegex]
regex = "foobar"

View file

@ -267,6 +267,7 @@ http:
excludedIPs:
- foobar
- foobar
ipv6Subnet: 42
rejectStatusCode: 42
Middleware14:
ipWhiteList:
@ -278,6 +279,7 @@ http:
excludedIPs:
- foobar
- foobar
ipv6Subnet: 42
Middleware15:
inFlightReq:
amount: 42
@ -287,6 +289,7 @@ http:
excludedIPs:
- foobar
- foobar
ipv6Subnet: 42
requestHeaderName: foobar
requestHost: true
Middleware16:
@ -333,6 +336,7 @@ http:
excludedIPs:
- foobar
- foobar
ipv6Subnet: 42
requestHeaderName: foobar
requestHost: true
Middleware19:

View file

@ -1458,6 +1458,12 @@ spec:
items:
type: string
type: array
ipv6Subnet:
description: IPv6Subnet configures Traefik to consider
all IPv6 addresses from the defined subnet as originating
from the same IP. Applies to RemoteAddrStrategy and
DepthStrategy.
type: integer
type: object
requestHeaderName:
description: RequestHeaderName defines the name of the header
@ -1491,6 +1497,11 @@ spec:
items:
type: string
type: array
ipv6Subnet:
description: IPv6Subnet configures Traefik to consider all
IPv6 addresses from the defined subnet as originating from
the same IP. Applies to RemoteAddrStrategy and DepthStrategy.
type: integer
type: object
rejectStatusCode:
description: |-
@ -1523,6 +1534,11 @@ spec:
items:
type: string
type: array
ipv6Subnet:
description: IPv6Subnet configures Traefik to consider all
IPv6 addresses from the defined subnet as originating from
the same IP. Applies to RemoteAddrStrategy and DepthStrategy.
type: integer
type: object
sourceRange:
description: SourceRange defines the set of allowed IPs (or ranges
@ -1691,6 +1707,12 @@ spec:
items:
type: string
type: array
ipv6Subnet:
description: IPv6Subnet configures Traefik to consider
all IPv6 addresses from the defined subnet as originating
from the same IP. Applies to RemoteAddrStrategy and
DepthStrategy.
type: integer
type: object
requestHeaderName:
description: RequestHeaderName defines the name of the header

View file

@ -103,18 +103,21 @@ THIS FILE MUST NOT BE EDITED BY HAND
| `traefik/http/middlewares/Middleware13/ipAllowList/ipStrategy/depth` | `42` |
| `traefik/http/middlewares/Middleware13/ipAllowList/ipStrategy/excludedIPs/0` | `foobar` |
| `traefik/http/middlewares/Middleware13/ipAllowList/ipStrategy/excludedIPs/1` | `foobar` |
| `traefik/http/middlewares/Middleware13/ipAllowList/ipStrategy/ipv6Subnet` | `42` |
| `traefik/http/middlewares/Middleware13/ipAllowList/rejectStatusCode` | `42` |
| `traefik/http/middlewares/Middleware13/ipAllowList/sourceRange/0` | `foobar` |
| `traefik/http/middlewares/Middleware13/ipAllowList/sourceRange/1` | `foobar` |
| `traefik/http/middlewares/Middleware14/ipWhiteList/ipStrategy/depth` | `42` |
| `traefik/http/middlewares/Middleware14/ipWhiteList/ipStrategy/excludedIPs/0` | `foobar` |
| `traefik/http/middlewares/Middleware14/ipWhiteList/ipStrategy/excludedIPs/1` | `foobar` |
| `traefik/http/middlewares/Middleware14/ipWhiteList/ipStrategy/ipv6Subnet` | `42` |
| `traefik/http/middlewares/Middleware14/ipWhiteList/sourceRange/0` | `foobar` |
| `traefik/http/middlewares/Middleware14/ipWhiteList/sourceRange/1` | `foobar` |
| `traefik/http/middlewares/Middleware15/inFlightReq/amount` | `42` |
| `traefik/http/middlewares/Middleware15/inFlightReq/sourceCriterion/ipStrategy/depth` | `42` |
| `traefik/http/middlewares/Middleware15/inFlightReq/sourceCriterion/ipStrategy/excludedIPs/0` | `foobar` |
| `traefik/http/middlewares/Middleware15/inFlightReq/sourceCriterion/ipStrategy/excludedIPs/1` | `foobar` |
| `traefik/http/middlewares/Middleware15/inFlightReq/sourceCriterion/ipStrategy/ipv6Subnet` | `42` |
| `traefik/http/middlewares/Middleware15/inFlightReq/sourceCriterion/requestHeaderName` | `foobar` |
| `traefik/http/middlewares/Middleware15/inFlightReq/sourceCriterion/requestHost` | `true` |
| `traefik/http/middlewares/Middleware16/passTLSClientCert/info/issuer/commonName` | `true` |
@ -147,6 +150,7 @@ THIS FILE MUST NOT BE EDITED BY HAND
| `traefik/http/middlewares/Middleware18/rateLimit/sourceCriterion/ipStrategy/depth` | `42` |
| `traefik/http/middlewares/Middleware18/rateLimit/sourceCriterion/ipStrategy/excludedIPs/0` | `foobar` |
| `traefik/http/middlewares/Middleware18/rateLimit/sourceCriterion/ipStrategy/excludedIPs/1` | `foobar` |
| `traefik/http/middlewares/Middleware18/rateLimit/sourceCriterion/ipStrategy/ipv6Subnet` | `42` |
| `traefik/http/middlewares/Middleware18/rateLimit/sourceCriterion/requestHeaderName` | `foobar` |
| `traefik/http/middlewares/Middleware18/rateLimit/sourceCriterion/requestHost` | `true` |
| `traefik/http/middlewares/Middleware19/redirectRegex/permanent` | `true` |

View file

@ -734,6 +734,12 @@ spec:
items:
type: string
type: array
ipv6Subnet:
description: IPv6Subnet configures Traefik to consider
all IPv6 addresses from the defined subnet as originating
from the same IP. Applies to RemoteAddrStrategy and
DepthStrategy.
type: integer
type: object
requestHeaderName:
description: RequestHeaderName defines the name of the header
@ -767,6 +773,11 @@ spec:
items:
type: string
type: array
ipv6Subnet:
description: IPv6Subnet configures Traefik to consider all
IPv6 addresses from the defined subnet as originating from
the same IP. Applies to RemoteAddrStrategy and DepthStrategy.
type: integer
type: object
rejectStatusCode:
description: |-
@ -799,6 +810,11 @@ spec:
items:
type: string
type: array
ipv6Subnet:
description: IPv6Subnet configures Traefik to consider all
IPv6 addresses from the defined subnet as originating from
the same IP. Applies to RemoteAddrStrategy and DepthStrategy.
type: integer
type: object
sourceRange:
description: SourceRange defines the set of allowed IPs (or ranges
@ -967,6 +983,12 @@ spec:
items:
type: string
type: array
ipv6Subnet:
description: IPv6Subnet configures Traefik to consider
all IPv6 addresses from the defined subnet as originating
from the same IP. Applies to RemoteAddrStrategy and
DepthStrategy.
type: integer
type: object
requestHeaderName:
description: RequestHeaderName defines the name of the header

View file

@ -228,6 +228,12 @@ WriteTimeout is the maximum duration before timing out writes of the response. I
`--entrypoints.<name>.udp.timeout`:
Timeout defines how long to wait on an idle session before releasing the related resources. (Default: ```3```)
`--experimental.fastproxy`:
Enable the FastProxy implementation. (Default: ```false```)
`--experimental.fastproxy.debug`:
Enable debug mode for the FastProxy implementation. (Default: ```false```)
`--experimental.kubernetesgateway`:
(Deprecated) Allow the Kubernetes gateway api provider usage. (Default: ```false```)
@ -423,6 +429,9 @@ TLS key
`--metrics.otlp.pushinterval`:
Period between calls to collect a checkpoint. (Default: ```10```)
`--metrics.otlp.servicename`:
OTEL service name to use. (Default: ```traefik```)
`--metrics.prometheus`:
Prometheus metrics exporter type. (Default: ```false```)
@ -912,6 +921,12 @@ Interval for polling Nomad API. (Default: ```15```)
`--providers.nomad.stale`:
Use stale consistency for catalog reads. (Default: ```false```)
`--providers.nomad.throttleduration`:
Watch throttle duration. (Default: ```0```)
`--providers.nomad.watch`:
Watch Nomad Service events. (Default: ```false```)
`--providers.plugin.<name>`:
Plugins configuration.

View file

@ -228,6 +228,12 @@ WriteTimeout is the maximum duration before timing out writes of the response. I
`TRAEFIK_ENTRYPOINTS_<NAME>_UDP_TIMEOUT`:
Timeout defines how long to wait on an idle session before releasing the related resources. (Default: ```3```)
`TRAEFIK_EXPERIMENTAL_FASTPROXY`:
Enable the FastProxy implementation. (Default: ```false```)
`TRAEFIK_EXPERIMENTAL_FASTPROXY_DEBUG`:
Enable debug mode for the FastProxy implementation. (Default: ```false```)
`TRAEFIK_EXPERIMENTAL_KUBERNETESGATEWAY`:
(Deprecated) Allow the Kubernetes gateway api provider usage. (Default: ```false```)
@ -423,6 +429,9 @@ TLS key
`TRAEFIK_METRICS_OTLP_PUSHINTERVAL`:
Period between calls to collect a checkpoint. (Default: ```10```)
`TRAEFIK_METRICS_OTLP_SERVICENAME`:
OTEL service name to use. (Default: ```traefik```)
`TRAEFIK_METRICS_PROMETHEUS`:
Prometheus metrics exporter type. (Default: ```false```)
@ -912,6 +921,12 @@ Interval for polling Nomad API. (Default: ```15```)
`TRAEFIK_PROVIDERS_NOMAD_STALE`:
Use stale consistency for catalog reads. (Default: ```false```)
`TRAEFIK_PROVIDERS_NOMAD_THROTTLEDURATION`:
Watch throttle duration. (Default: ```0```)
`TRAEFIK_PROVIDERS_NOMAD_WATCH`:
Watch Nomad Service events. (Default: ```false```)
`TRAEFIK_PROVIDERS_PLUGIN_<NAME>`:
Plugins configuration.

View file

@ -203,6 +203,8 @@
exposedByDefault = true
refreshInterval = "42s"
allowEmptyServices = true
watch = true
throttleDuration = "42s"
namespaces = ["foobar", "foobar"]
[providers.nomad.endpoint]
address = "foobar"
@ -340,6 +342,7 @@
addServicesLabels = true
explicitBoundaries = [42.0, 42.0]
pushInterval = "42s"
serviceName = "foobar"
[metrics.otlp.grpc]
endpoint = "foobar"
insecure = true
@ -509,6 +512,8 @@
[experimental.localPlugins.LocalDescriptor1.settings]
envs = ["foobar", "foobar"]
mounts = ["foobar", "foobar"]
[experimental.fastProxy]
debug = true
[core]
defaultRuleSyntax = "foobar"

View file

@ -236,6 +236,8 @@ providers:
exposedByDefault: true
refreshInterval: 42s
allowEmptyServices: true
watch: true
throttleDuration: 42s
namespaces:
- foobar
- foobar
@ -400,6 +402,7 @@ metrics:
- 42
- 42
pushInterval: 42s
serviceName: foobar
ping:
entryPoint: foobar
manualRouting: true
@ -572,6 +575,8 @@ experimental:
mounts:
- foobar
- foobar
fastProxy:
debug: true
kubernetesGateway: true
core:
defaultRuleSyntax: foobar

View file

@ -0,0 +1,41 @@
---
title: "Traefik FastProxy Experimental Configuration"
description: "This section of the Traefik Proxy documentation explains how to use the new FastProxy option."
---
# Traefik FastProxy Experimental Configuration
## Overview
This guide provides instructions on how to configure and use the new experimental `fastProxy` static configuration option in Traefik.
The `fastProxy` option introduces a high-performance reverse proxy designed to enhance the performance of routing.
!!! info "Limitations"
Please note that the new fast proxy implementation does not work with HTTP/2.
This means that when a H2C or HTTPS request with [HTTP2 enabled](../routing/services/index.md#disablehttp2) is sent to a backend, the fallback proxy is the regular one.
Additionnaly, observability features like tracing and OTEL semconv metrics are not supported for the moment.
!!! warning "Experimental"
The `fastProxy` option is currently experimental and subject to change in future releases.
Use with caution in production environments.
### Enabling FastProxy
The fastProxy option is a static configuration parameter.
To enable it, you need to configure it in your Traefik static configuration
```yaml tab="File (YAML)"
experimental:
fastProxy: {}
```
```toml tab="File (TOML)"
[experimental.fastProxy]
```
```bash tab="CLI"
--experimental.fastProxy
```

View file

@ -163,6 +163,7 @@ nav:
- 'Overview': 'observability/tracing/overview.md'
- 'OpenTelemetry': 'observability/tracing/opentelemetry.md'
- 'User Guides':
- 'FastProxy': 'user-guides/fastproxy.md'
- 'Kubernetes and Let''s Encrypt': 'user-guides/crd-acme/index.md'
- 'gRPC Examples': 'user-guides/grpc.md'
- 'Docker':

45
go.mod
View file

@ -6,7 +6,8 @@ require (
github.com/BurntSushi/toml v1.4.0
github.com/Masterminds/sprig/v3 v3.2.3
github.com/abbot/go-http-auth v0.0.0-00010101000000-000000000000 // No tag on the repo.
github.com/andybalholm/brotli v1.0.6
github.com/andybalholm/brotli v1.1.0
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
github.com/aws/aws-sdk-go v1.44.327
github.com/cenkalti/backoff/v4 v4.3.0
github.com/containous/alice v0.0.0-20181107144136-d83ebdd94cbd // No tag on the repo.
@ -40,7 +41,7 @@ require (
github.com/kvtools/valkeyrie v1.0.0
github.com/kvtools/zookeeper v1.0.2
github.com/mailgun/ttlmap v0.0.0-20170619185759-c1c17f74874f // No tag on the repo.
github.com/miekg/dns v1.1.59
github.com/miekg/dns v1.1.62
github.com/mitchellh/copystructure v1.2.0
github.com/mitchellh/hashstructure v1.0.0
github.com/mitchellh/mapstructure v1.5.0
@ -48,7 +49,7 @@ require (
github.com/pires/go-proxyproto v0.6.1
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // No tag on the repo.
github.com/prometheus/client_golang v1.19.1
github.com/prometheus/client_model v0.5.0
github.com/prometheus/client_model v0.6.1
github.com/quic-go/quic-go v0.47.0
github.com/rs/zerolog v1.29.0
github.com/sirupsen/logrus v1.9.3
@ -67,6 +68,7 @@ require (
github.com/traefik/yaegi v0.16.1
github.com/unrolled/render v1.0.2
github.com/unrolled/secure v1.0.9
github.com/valyala/fasthttp v1.55.0
github.com/vulcand/oxy/v2 v2.0.0
github.com/vulcand/predicate v1.2.0
go.opentelemetry.io/collector/pdata v1.10.0
@ -88,17 +90,17 @@ require (
golang.org/x/text v0.18.0
golang.org/x/time v0.5.0
golang.org/x/tools v0.25.0
google.golang.org/grpc v1.64.1
google.golang.org/grpc v1.66.2
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.30.0
k8s.io/apiextensions-apiserver v0.30.0
k8s.io/apimachinery v0.30.0
k8s.io/client-go v0.30.0
k8s.io/utils v0.0.0-20240423183400-0849a56e8f22 // No tag on the repo.
k8s.io/api v0.31.1
k8s.io/apiextensions-apiserver v0.31.1
k8s.io/apimachinery v0.31.1
k8s.io/client-go v0.31.1
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // No tag on the repo.
mvdan.cc/xurls/v2 v2.5.0
sigs.k8s.io/controller-runtime v0.18.0
sigs.k8s.io/gateway-api v1.1.0
sigs.k8s.io/gateway-api v1.2.0-rc2
sigs.k8s.io/yaml v1.4.0
)
@ -153,7 +155,7 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
github.com/bytedance/sonic v1.10.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/civo/civogo v0.3.11 // indirect
github.com/cloudflare/cloudflare-go v0.97.0 // indirect
github.com/containerd/containerd v1.7.20 // indirect
@ -171,11 +173,11 @@ require (
github.com/dnsimple/dnsimple-go v1.7.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/emicklei/go-restful/v3 v3.12.0 // indirect
github.com/evanphx/json-patch v5.7.0+incompatible // indirect
github.com/evanphx/json-patch/v5 v5.9.0 // indirect
github.com/exoscale/egoscale v0.102.3 // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/fatih/color v1.17.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/gin-gonic/gin v1.9.1 // indirect
github.com/go-errors/errors v1.0.1 // indirect
@ -252,7 +254,7 @@ require (
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/spdystream v0.2.0 // indirect
github.com/moby/spdystream v0.4.0 // indirect
github.com/moby/sys/sequential v0.5.0 // indirect
github.com/moby/sys/user v0.2.0 // indirect
github.com/moby/term v0.5.0 // indirect
@ -285,7 +287,7 @@ require (
github.com/pkg/errors v0.9.1 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/pquerna/otp v1.4.0 // indirect
github.com/prometheus/common v0.48.0 // indirect
github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/redis/go-redis/v9 v9.2.1 // indirect
@ -315,17 +317,19 @@ require (
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/transip/gotransip/v6 v6.23.0 // indirect
github.com/ultradns/ultradns-go-sdk v1.6.1-20231103022937-8589b6a // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/vinyldns/go-vinyldns v0.9.16 // indirect
github.com/vultr/govultr/v3 v3.9.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/yandex-cloud/go-genproto v0.0.0-20240318083951-4fe6125f286e // indirect
github.com/yandex-cloud/go-sdk v0.0.0-20240318084659-dfa50323a0b4 // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect
github.com/zeebo/errs v1.2.2 // indirect
go.etcd.io/etcd/api/v3 v3.5.10 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.10 // indirect
go.etcd.io/etcd/client/v3 v3.5.10 // indirect
go.etcd.io/etcd/api/v3 v3.5.14 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.14 // indirect
go.etcd.io/etcd/client/v3 v3.5.14 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect
go.opentelemetry.io/contrib/propagators/aws v1.28.0 // indirect
go.opentelemetry.io/contrib/propagators/b3 v1.28.0 // indirect
go.opentelemetry.io/contrib/propagators/jaeger v1.28.0 // indirect
@ -346,13 +350,14 @@ require (
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/h2non/gock.v1 v1.0.16 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/ns1/ns1-go.v2 v2.9.1 // indirect
gopkg.in/square/go-jose.v2 v2.5.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/klog/v2 v2.120.1 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20240423202451-8948a665c108 // indirect
nhooyr.io/websocket v1.8.7 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect

85
go.sum
View file

@ -98,8 +98,8 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/aliyun/alibaba-cloud-sdk-go v1.62.712 h1:lM7JnA9dEdDFH9XOgRNQMDTQnOjlLkDTNA7c0aWTQ30=
github.com/aliyun/alibaba-cloud-sdk-go v1.62.712/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
@ -166,8 +166,8 @@ github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyY
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
@ -276,8 +276,8 @@ github.com/exoscale/egoscale v0.102.3/go.mod h1:RPf2Gah6up+6kAEayHTQwqapzXlm93f0
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
@ -290,6 +290,8 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/getkin/kin-openapi v0.61.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4=
@ -701,8 +703,8 @@ github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3N
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/miekg/dns v1.1.47/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs=
github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk=
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
github.com/mimuret/golang-iij-dpf v0.9.1 h1:Gj6EhHJkOhr+q2RnvRPJsPMcjuVnWPSccEHyoEehU34=
github.com/mimuret/golang-iij-dpf v0.9.1/go.mod h1:sl9KyOkESib9+KRD3HaGpgi1xk7eoN2+d96LCLsME2M=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
@ -734,8 +736,8 @@ github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3N
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8=
github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=
github.com/moby/spdystream v0.4.0 h1:Vy79D6mHeJJjiPdFEL2yku1kl0chZpJfZcPpb16BRl8=
github.com/moby/spdystream v0.4.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
github.com/moby/sys/user v0.2.0 h1:OnpapJsRp25vkhw8TFG6OLJODNh/3rEwRWtJ3kakwRM=
@ -854,15 +856,15 @@ github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
@ -1039,7 +1041,10 @@ github.com/unrolled/secure v1.0.9 h1:BWRuEb1vDrBFFDdbCnKkof3gZ35I/bnHGyt0LB0TNyQ
github.com/unrolled/secure v1.0.9/go.mod h1:fO+mEan+FLB0CdEnHf6Q4ZZVNqG+5fuLFnP8p0BXDPI=
github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc=
github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8k8=
github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRVfcxxoHiOM=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/vinyldns/go-vinyldns v0.9.16 h1:GZJStDkcCk1F1AcRc64LuuMh+ENL8pHA0CVd4ulRMcQ=
@ -1050,6 +1055,8 @@ github.com/vulcand/predicate v1.2.0 h1:uFsW1gcnnR7R+QTID+FVcs0sSYlIGntoGOTb3rQJt
github.com/vulcand/predicate v1.2.0/go.mod h1:VipoNYXny6c8N381zGUWkjuuNHiRbeAZhE7Qm9c+2GA=
github.com/vultr/govultr/v3 v3.9.0 h1:63V/22mpfquRA5DenJ9EF0VozHg0k+X4dhUWcDXHPyc=
github.com/vultr/govultr/v3 v3.9.0/go.mod h1:Rd8ebpXm7jxH3MDmhnEs+zrlYW212ouhx+HeUMfHm2o=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
@ -1067,20 +1074,20 @@ github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQ
github.com/zeebo/errs v1.2.2 h1:5NFypMTuSdoySVTqlNs1dEoU21QVamMQJxW/Fii5O7g=
github.com/zeebo/errs v1.2.2/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/etcd/api/v3 v3.5.10 h1:szRajuUUbLyppkhs9K6BRtjY37l66XQQmw7oZRANE4k=
go.etcd.io/etcd/api/v3 v3.5.10/go.mod h1:TidfmT4Uycad3NM/o25fG3J07odo4GBB9hoxaodFCtI=
go.etcd.io/etcd/client/pkg/v3 v3.5.10 h1:kfYIdQftBnbAq8pUWFXfpuuxFSKzlmM5cSn76JByiT0=
go.etcd.io/etcd/client/pkg/v3 v3.5.10/go.mod h1:DYivfIviIuQ8+/lCq4vcxuseg2P2XbHygkKwFo9fc8U=
go.etcd.io/etcd/client/v3 v3.5.10 h1:W9TXNZ+oB3MCd/8UjxHTWK5J9Nquw9fQBLJd5ne5/Ao=
go.etcd.io/etcd/client/v3 v3.5.10/go.mod h1:RVeBnDz2PUEZqTpgqwAtUd8nAPf5kjyFyND7P1VkOKc=
go.etcd.io/etcd/api/v3 v3.5.14 h1:vHObSCxyB9zlF60w7qzAdTcGaglbJOpSj1Xj9+WGxq0=
go.etcd.io/etcd/api/v3 v3.5.14/go.mod h1:BmtWcRlQvwa1h3G2jvKYwIQy4PkHlDej5t7uLMUdJUU=
go.etcd.io/etcd/client/pkg/v3 v3.5.14 h1:SaNH6Y+rVEdxfpA2Jr5wkEvN6Zykme5+YnbCkxvuWxQ=
go.etcd.io/etcd/client/pkg/v3 v3.5.14/go.mod h1:8uMgAokyG1czCtIdsq+AGyYQMvpIKnSvPjFMunkgeZI=
go.etcd.io/etcd/client/v3 v3.5.14 h1:CWfRs4FDaDoSz81giL7zPpZH2Z35tbOrAJkkjMqOupg=
go.etcd.io/etcd/client/v3 v3.5.14/go.mod h1:k3XfdV/VIHy/97rqWjoUzrj9tk7GgJGH9J8L4dNXmAk=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/collector/pdata v1.10.0 h1:oLyPLGvPTQrcRT64ZVruwvmH/u3SHTfNo01pteS4WOE=
go.opentelemetry.io/collector/pdata v1.10.0/go.mod h1:IHxHsp+Jq/xfjORQMDJjSH6jvedOSTOyu3nbxqhWSYE=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg=
go.opentelemetry.io/contrib/propagators/autoprop v0.53.0 h1:4zaVLcJ5mvYw0vlk63TX62qS4qty/4jAY1BKZ1usu18=
go.opentelemetry.io/contrib/propagators/autoprop v0.53.0/go.mod h1:RPlvYtxp5D8PKnRzyPM+rwMQrvzdlfA49Sgworkg7aQ=
go.opentelemetry.io/contrib/propagators/aws v1.28.0 h1:acyTl4oyin/iLr5Nz3u7p/PKHUbLh42w/fqg9LblExk=
@ -1439,8 +1446,8 @@ google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k=
google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA=
google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0=
google.golang.org/grpc v1.66.2 h1:3QdXkuq3Bkh7w+ywLdLvM56cmGvQHUMZpiCzt6Rqaoo=
google.golang.org/grpc v1.66.2/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y=
google.golang.org/grpc/examples v0.0.0-20201130180447-c456688b1860/go.mod h1:Ly7ZA/ARzg8fnPU9TyZIxoz33sEUuWX7txiqs8lPTgE=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
@ -1466,6 +1473,8 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/h2non/gentleman.v1 v1.0.4/go.mod h1:JYuHVdFzS4MKOXe0o+chKJ4hCe6tqKKw9XH9YP6WFrg=
gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE=
@ -1510,20 +1519,20 @@ honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
k8s.io/api v0.30.0 h1:siWhRq7cNjy2iHssOB9SCGNCl2spiF1dO3dABqZ8niA=
k8s.io/api v0.30.0/go.mod h1:OPlaYhoHs8EQ1ql0R/TsUgaRPhpKNxIMrKQfWUp8QSE=
k8s.io/apiextensions-apiserver v0.30.0 h1:jcZFKMqnICJfRxTgnC4E+Hpcq8UEhT8B2lhBcQ+6uAs=
k8s.io/apiextensions-apiserver v0.30.0/go.mod h1:N9ogQFGcrbWqAY9p2mUAL5mGxsLqwgtUce127VtRX5Y=
k8s.io/apimachinery v0.30.0 h1:qxVPsyDM5XS96NIh9Oj6LavoVFYff/Pon9cZeDIkHHA=
k8s.io/apimachinery v0.30.0/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc=
k8s.io/client-go v0.30.0 h1:sB1AGGlhY/o7KCyCEQ0bPWzYDL0pwOZO4vAtTSh/gJQ=
k8s.io/client-go v0.30.0/go.mod h1:g7li5O5256qe6TYdAMyX/otJqMhIiGgTapdLchhmOaY=
k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw=
k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/api v0.31.1 h1:Xe1hX/fPW3PXYYv8BlozYqw63ytA92snr96zMW9gWTU=
k8s.io/api v0.31.1/go.mod h1:sbN1g6eY6XVLeqNsZGLnI5FwVseTrZX7Fv3O26rhAaI=
k8s.io/apiextensions-apiserver v0.31.1 h1:L+hwULvXx+nvTYX/MKM3kKMZyei+UiSXQWciX/N6E40=
k8s.io/apiextensions-apiserver v0.31.1/go.mod h1:tWMPR3sgW+jsl2xm9v7lAyRF1rYEK71i9G5dRtkknoQ=
k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U=
k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
k8s.io/client-go v0.31.1 h1:f0ugtWSbWpxHR7sjVpQwuvw9a3ZKLXX0u0itkFXufb0=
k8s.io/client-go v0.31.1/go.mod h1:sKI8871MJN2OyeqRlmA4W4KM9KBdBUpDLu/43eGemCg=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20240423202451-8948a665c108 h1:Q8Z7VlGhcJgBHJHYugJ/K/7iB8a2eSxCyxdVjJp+lLY=
k8s.io/kube-openapi v0.0.0-20240423202451-8948a665c108/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98=
k8s.io/utils v0.0.0-20240423183400-0849a56e8f22 h1:ao5hUqGhsqdm+bYbjH/pRkCs0unBGe9UyDahzs9zQzQ=
k8s.io/utils v0.0.0-20240423183400-0849a56e8f22/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A=
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8=
mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE=
nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
@ -1533,8 +1542,8 @@ rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
sigs.k8s.io/controller-runtime v0.18.0 h1:Z7jKuX784TQSUL1TIyeuF7j8KXZ4RtSX0YgtjKcSTME=
sigs.k8s.io/controller-runtime v0.18.0/go.mod h1:tuAt1+wbVsXIT8lPtk5RURxqAnq7xkpv2Mhttslg7Hw=
sigs.k8s.io/gateway-api v1.1.0 h1:DsLDXCi6jR+Xz8/xd0Z1PYl2Pn0TyaFMOPPZIj4inDM=
sigs.k8s.io/gateway-api v1.1.0/go.mod h1:ZH4lHrL2sDi0FHZ9jjneb8kKnGzFWyrTya35sWUTrRs=
sigs.k8s.io/gateway-api v1.2.0-rc2 h1:v7V7JzaBuzwOLWWyyqlkqiqBi3ANBuZGV+uyyKzwmE8=
sigs.k8s.io/gateway-api v1.2.0-rc2/go.mod h1:EpNfEXNjiYfUJypf0eZ0P5iXA9ekSGWaS1WgPaM42X0=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4=

View file

@ -1,14 +1,14 @@
apiVersion: gateway.networking.k8s.io/v1alpha1
apiVersion: gateway.networking.k8s.io/v1
date: '-'
gatewayAPIChannel: experimental
gatewayAPIVersion: v1.1.0
gatewayAPIVersion: v1.2.0-rc2
implementation:
contact:
- '@traefik/maintainers'
organization: traefik
project: traefik
url: https://traefik.io/
version: v3.1
version: v3.2
kind: ConformanceReport
mode: default
profiles:
@ -30,10 +30,13 @@ profiles:
result: success
statistics:
Failed: 0
Passed: 10
Passed: 13
Skipped: 0
supportedFeatures:
- GatewayPort8080
- HTTPRouteBackendProtocolH2C
- HTTPRouteBackendProtocolWebSocket
- HTTPRouteDestinationPortMatching
- HTTPRouteHostRewrite
- HTTPRouteMethodMatching
- HTTPRoutePathRedirect
@ -44,6 +47,7 @@ profiles:
- HTTPRouteSchemeRedirect
unsupportedFeatures:
- GatewayHTTPListenerIsolation
- GatewayInfrastructurePropagation
- GatewayStaticAddresses
- HTTPRouteBackendRequestHeaderModification
- HTTPRouteBackendTimeout

View file

@ -1458,6 +1458,12 @@ spec:
items:
type: string
type: array
ipv6Subnet:
description: IPv6Subnet configures Traefik to consider
all IPv6 addresses from the defined subnet as originating
from the same IP. Applies to RemoteAddrStrategy and
DepthStrategy.
type: integer
type: object
requestHeaderName:
description: RequestHeaderName defines the name of the header
@ -1491,6 +1497,11 @@ spec:
items:
type: string
type: array
ipv6Subnet:
description: IPv6Subnet configures Traefik to consider all
IPv6 addresses from the defined subnet as originating from
the same IP. Applies to RemoteAddrStrategy and DepthStrategy.
type: integer
type: object
rejectStatusCode:
description: |-
@ -1523,6 +1534,11 @@ spec:
items:
type: string
type: array
ipv6Subnet:
description: IPv6Subnet configures Traefik to consider all
IPv6 addresses from the defined subnet as originating from
the same IP. Applies to RemoteAddrStrategy and DepthStrategy.
type: integer
type: object
sourceRange:
description: SourceRange defines the set of allowed IPs (or ranges
@ -1691,6 +1707,12 @@ spec:
items:
type: string
type: array
ipv6Subnet:
description: IPv6Subnet configures Traefik to consider
all IPv6 addresses from the defined subnet as originating
from the same IP. Applies to RemoteAddrStrategy and
DepthStrategy.
type: integer
type: object
requestHeaderName:
description: RequestHeaderName defines the name of the header

View file

@ -0,0 +1,35 @@
[global]
checkNewVersion = false
sendAnonymousUsage = false
[log]
level = "DEBUG"
noColor = true
[entryPoints]
[entryPoints.web]
address = ":8000"
[api]
insecure = true
[providers.file]
filename = "{{ .SelfFilename }}"
[experimental]
[experimental.fastProxy]
debug = true
## dynamic configuration ##
[http.routers]
[http.routers.router1]
entrypoints = ["web"]
service = "service1"
rule = "PathPrefix(`/`)"
[http.services]
[http.services.service1]
[http.services.service1.loadBalancer]
[[http.services.service1.loadBalancer.servers]]
url = "{{ .Server }}"

View file

@ -22,6 +22,7 @@ import (
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/util/sets"
kclientset "k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"sigs.k8s.io/controller-runtime/pkg/client"
klog "sigs.k8s.io/controller-runtime/pkg/log"
@ -50,6 +51,7 @@ type K8sConformanceSuite struct {
k3sContainer *k3s.K3sContainer
kubeClient client.Client
restConfig *rest.Config
clientSet *kclientset.Clientset
}
@ -88,7 +90,7 @@ func (s *K8sConformanceSuite) SetupSuite() {
s.k3sContainer, err = k3s.Run(ctx,
k3sImage,
k3s.WithManifest("./fixtures/k8s-conformance/00-experimental-v1.1.0.yml"),
k3s.WithManifest("./fixtures/k8s-conformance/00-experimental-v1.2.0-rc2.yml"),
k3s.WithManifest("./fixtures/k8s-conformance/01-rbac.yml"),
k3s.WithManifest("./fixtures/k8s-conformance/02-traefik.yml"),
network.WithNetwork(nil, s.network),
@ -111,17 +113,17 @@ func (s *K8sConformanceSuite) SetupSuite() {
s.T().Fatal(err)
}
restConfig, err := clientcmd.RESTConfigFromKubeConfig(kubeConfigYaml)
s.restConfig, err = clientcmd.RESTConfigFromKubeConfig(kubeConfigYaml)
if err != nil {
s.T().Fatalf("Error loading Kubernetes config: %v", err)
}
s.kubeClient, err = client.New(restConfig, client.Options{})
s.kubeClient, err = client.New(s.restConfig, client.Options{})
if err != nil {
s.T().Fatalf("Error initializing Kubernetes client: %v", err)
}
s.clientSet, err = kclientset.NewForConfig(restConfig)
s.clientSet, err = kclientset.NewForConfig(s.restConfig)
if err != nil {
s.T().Fatalf("Error initializing Kubernetes REST client: %v", err)
}
@ -183,6 +185,7 @@ func (s *K8sConformanceSuite) TestK8sGatewayAPIConformance() {
GatewayClassName: "traefik",
Debug: true,
CleanupBaseResources: true,
RestConfig: s.restConfig,
TimeoutConfig: config.DefaultTimeoutConfig(),
ManifestFS: []fs.FS{&conformance.Manifests},
EnableAllSupportedFeatures: false,

View file

@ -65,6 +65,32 @@ func (s *SimpleSuite) TestSimpleDefaultConfig() {
require.NoError(s.T(), err)
}
func (s *SimpleSuite) TestSimpleFastProxy() {
var callCount int
srv1 := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
assert.Contains(s.T(), req.Header, "X-Traefik-Fast-Proxy")
callCount++
}))
defer srv1.Close()
file := s.adaptFile("fixtures/simple_fastproxy.toml", struct {
Server string
}{
Server: srv1.URL,
})
s.traefikCmd(withConfigFile(file), "--log.level=DEBUG")
// wait for traefik
err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 10*time.Second, try.BodyContains("127.0.0.1"))
require.NoError(s.T(), err)
err = try.GetRequest("http://127.0.0.1:8000/", time.Second)
require.NoError(s.T(), err)
assert.GreaterOrEqual(s.T(), 1, callCount)
}
func (s *SimpleSuite) TestWithWebConfig() {
s.cmdTraefik(withConfigFile("fixtures/simple_web.toml"))

View file

@ -0,0 +1,57 @@
package dynamic
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_GetStrategy_ipv6Subnet(t *testing.T) {
testCases := []struct {
desc string
expectError bool
ipv6Subnet *int
}{
{
desc: "Nil subnet",
},
{
desc: "Zero subnet",
expectError: true,
ipv6Subnet: intPtr(0),
},
{
desc: "Subnet greater that 128",
expectError: true,
ipv6Subnet: intPtr(129),
},
{
desc: "Valid subnet",
ipv6Subnet: intPtr(128),
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
strategy := IPStrategy{
IPv6Subnet: test.ipv6Subnet,
}
get, err := strategy.Get()
if test.expectError {
require.Error(t, err)
assert.Nil(t, get)
} else {
require.NoError(t, err)
assert.NotNil(t, get)
}
})
}
}
func intPtr(value int) *int {
return &value
}

View file

@ -1,6 +1,7 @@
package dynamic
import (
"fmt"
"net/http"
"time"
@ -405,6 +406,8 @@ type IPStrategy struct {
Depth int `json:"depth,omitempty" toml:"depth,omitempty" yaml:"depth,omitempty" export:"true"`
// ExcludedIPs configures Traefik to scan the X-Forwarded-For header and select the first IP not in the list.
ExcludedIPs []string `json:"excludedIPs,omitempty" toml:"excludedIPs,omitempty" yaml:"excludedIPs,omitempty"`
// IPv6Subnet configures Traefik to consider all IPv6 addresses from the defined subnet as originating from the same IP. Applies to RemoteAddrStrategy and DepthStrategy.
IPv6Subnet *int `json:"ipv6Subnet,omitempty" toml:"ipv6Subnet,omitempty" yaml:"ipv6Subnet,omitempty"`
// TODO(mpl): I think we should make RemoteAddr an explicit field. For one thing, it would yield better documentation.
}
@ -418,8 +421,13 @@ func (s *IPStrategy) Get() (ip.Strategy, error) {
}
if s.Depth > 0 {
if s.IPv6Subnet != nil && (*s.IPv6Subnet <= 0 || *s.IPv6Subnet > 128) {
return nil, fmt.Errorf("invalid IPv6 subnet %d value, should be greater to 0 and lower or equal to 128", *s.IPv6Subnet)
}
return &ip.DepthStrategy{
Depth: s.Depth,
Depth: s.Depth,
IPv6Subnet: s.IPv6Subnet,
}, nil
}
@ -433,7 +441,13 @@ func (s *IPStrategy) Get() (ip.Strategy, error) {
}, nil
}
return &ip.RemoteAddrStrategy{}, nil
if s.IPv6Subnet != nil && (*s.IPv6Subnet <= 0 || *s.IPv6Subnet > 128) {
return nil, fmt.Errorf("invalid IPv6 subnet %d value, should be greater to 0 and lower or equal to 128", *s.IPv6Subnet)
}
return &ip.RemoteAddrStrategy{
IPv6Subnet: s.IPv6Subnet,
}, nil
}
// +k8s:deepcopy-gen=true

View file

@ -704,6 +704,11 @@ func (in *IPStrategy) DeepCopyInto(out *IPStrategy) {
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.IPv6Subnet != nil {
in, out := &in.IPv6Subnet, &out.IPv6Subnet
*out = new(int)
**out = **in
}
return
}

View file

@ -90,10 +90,12 @@ func TestDecodeConfiguration(t *testing.T) {
"traefik.http.middlewares.Middleware8.headers.stsseconds": "42",
"traefik.http.middlewares.Middleware9.ipallowlist.ipstrategy.depth": "42",
"traefik.http.middlewares.Middleware9.ipallowlist.ipstrategy.excludedips": "foobar, fiibar",
"traefik.http.middlewares.Middleware9.ipallowlist.ipstrategy.ipv6subnet": "42",
"traefik.http.middlewares.Middleware9.ipallowlist.sourcerange": "foobar, fiibar",
"traefik.http.middlewares.Middleware10.inflightreq.amount": "42",
"traefik.http.middlewares.Middleware10.inflightreq.sourcecriterion.ipstrategy.depth": "42",
"traefik.http.middlewares.Middleware10.inflightreq.sourcecriterion.ipstrategy.excludedips": "foobar, fiibar",
"traefik.http.middlewares.Middleware10.inflightreq.sourcecriterion.ipstrategy.ipv6subnet": "42",
"traefik.http.middlewares.Middleware10.inflightreq.sourcecriterion.requestheadername": "foobar",
"traefik.http.middlewares.Middleware10.inflightreq.sourcecriterion.requesthost": "true",
"traefik.http.middlewares.Middleware11.passtlsclientcert.info.notafter": "true",
@ -123,6 +125,7 @@ func TestDecodeConfiguration(t *testing.T) {
"traefik.http.middlewares.Middleware12.ratelimit.sourcecriterion.requesthost": "true",
"traefik.http.middlewares.Middleware12.ratelimit.sourcecriterion.ipstrategy.depth": "42",
"traefik.http.middlewares.Middleware12.ratelimit.sourcecriterion.ipstrategy.excludedips": "foobar, foobar",
"traefik.http.middlewares.Middleware12.ratelimit.sourcecriterion.ipstrategy.ipv6subnet": "42",
"traefik.http.middlewares.Middleware13.redirectregex.permanent": "true",
"traefik.http.middlewares.Middleware13.redirectregex.regex": "foobar",
"traefik.http.middlewares.Middleware13.redirectregex.replacement": "foobar",
@ -392,6 +395,7 @@ func TestDecodeConfiguration(t *testing.T) {
IPStrategy: &dynamic.IPStrategy{
Depth: 42,
ExcludedIPs: []string{"foobar", "fiibar"},
IPv6Subnet: intPtr(42),
},
RequestHeaderName: "foobar",
RequestHost: true,
@ -437,6 +441,7 @@ func TestDecodeConfiguration(t *testing.T) {
IPStrategy: &dynamic.IPStrategy{
Depth: 42,
ExcludedIPs: []string{"foobar", "foobar"},
IPv6Subnet: intPtr(42),
},
RequestHeaderName: "foobar",
RequestHost: true,
@ -648,6 +653,7 @@ func TestDecodeConfiguration(t *testing.T) {
"foobar",
"fiibar",
},
IPv6Subnet: intPtr(42),
},
},
},
@ -913,6 +919,7 @@ func TestEncodeConfiguration(t *testing.T) {
IPStrategy: &dynamic.IPStrategy{
Depth: 42,
ExcludedIPs: []string{"foobar", "fiibar"},
IPv6Subnet: intPtr(42),
},
RequestHeaderName: "foobar",
RequestHost: true,
@ -957,6 +964,7 @@ func TestEncodeConfiguration(t *testing.T) {
IPStrategy: &dynamic.IPStrategy{
Depth: 42,
ExcludedIPs: []string{"foobar", "foobar"},
IPv6Subnet: intPtr(42),
},
RequestHeaderName: "foobar",
RequestHost: true,
@ -1176,6 +1184,7 @@ func TestEncodeConfiguration(t *testing.T) {
"foobar",
"fiibar",
},
IPv6Subnet: intPtr(42),
},
},
},
@ -1338,11 +1347,13 @@ func TestEncodeConfiguration(t *testing.T) {
"traefik.HTTP.Middlewares.Middleware8.Headers.STSSeconds": "42",
"traefik.HTTP.Middlewares.Middleware9.IPAllowList.IPStrategy.Depth": "42",
"traefik.HTTP.Middlewares.Middleware9.IPAllowList.IPStrategy.ExcludedIPs": "foobar, fiibar",
"traefik.HTTP.Middlewares.Middleware9.IPAllowList.IPStrategy.IPv6Subnet": "42",
"traefik.HTTP.Middlewares.Middleware9.IPAllowList.RejectStatusCode": "0",
"traefik.HTTP.Middlewares.Middleware9.IPAllowList.SourceRange": "foobar, fiibar",
"traefik.HTTP.Middlewares.Middleware10.InFlightReq.Amount": "42",
"traefik.HTTP.Middlewares.Middleware10.InFlightReq.SourceCriterion.IPStrategy.Depth": "42",
"traefik.HTTP.Middlewares.Middleware10.InFlightReq.SourceCriterion.IPStrategy.ExcludedIPs": "foobar, fiibar",
"traefik.HTTP.Middlewares.Middleware10.InFlightReq.SourceCriterion.IPStrategy.IPv6Subnet": "42",
"traefik.HTTP.Middlewares.Middleware10.InFlightReq.SourceCriterion.RequestHeaderName": "foobar",
"traefik.HTTP.Middlewares.Middleware10.InFlightReq.SourceCriterion.RequestHost": "true",
"traefik.HTTP.Middlewares.Middleware11.PassTLSClientCert.Info.NotAfter": "true",
@ -1372,6 +1383,7 @@ func TestEncodeConfiguration(t *testing.T) {
"traefik.HTTP.Middlewares.Middleware12.RateLimit.SourceCriterion.RequestHost": "true",
"traefik.HTTP.Middlewares.Middleware12.RateLimit.SourceCriterion.IPStrategy.Depth": "42",
"traefik.HTTP.Middlewares.Middleware12.RateLimit.SourceCriterion.IPStrategy.ExcludedIPs": "foobar, foobar",
"traefik.HTTP.Middlewares.Middleware12.RateLimit.SourceCriterion.IPStrategy.IPv6Subnet": "42",
"traefik.HTTP.Middlewares.Middleware13.RedirectRegex.Regex": "foobar",
"traefik.HTTP.Middlewares.Middleware13.RedirectRegex.Replacement": "foobar",
"traefik.HTTP.Middlewares.Middleware13.RedirectRegex.Permanent": "true",
@ -1486,3 +1498,7 @@ func TestEncodeConfiguration(t *testing.T) {
}
assert.Equal(t, expected, labels)
}
func intPtr(value int) *int {
return &value
}

View file

@ -7,6 +7,13 @@ type Experimental struct {
Plugins map[string]plugins.Descriptor `description:"Plugins configuration." json:"plugins,omitempty" toml:"plugins,omitempty" yaml:"plugins,omitempty" export:"true"`
LocalPlugins map[string]plugins.LocalDescriptor `description:"Local plugins configuration." json:"localPlugins,omitempty" toml:"localPlugins,omitempty" yaml:"localPlugins,omitempty" export:"true"`
FastProxy *FastProxyConfig `description:"Enable the FastProxy implementation." json:"fastProxy,omitempty" toml:"fastProxy,omitempty" yaml:"fastProxy,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
// Deprecated: KubernetesGateway provider is not an experimental feature starting with v3.1. Please remove its usage from the static configuration.
KubernetesGateway bool `description:"(Deprecated) Allow the Kubernetes gateway api provider usage." json:"kubernetesGateway,omitempty" toml:"kubernetesGateway,omitempty" yaml:"kubernetesGateway,omitempty" export:"true"`
}
// FastProxyConfig holds the FastProxy configuration.
type FastProxyConfig struct {
Debug bool `description:"Enable debug mode for the FastProxy implementation." json:"debug,omitempty" toml:"debug,omitempty" yaml:"debug,omitempty" export:"true"`
}

View file

@ -3,6 +3,7 @@ package ip
import (
"net"
"net/http"
"net/netip"
"strings"
)
@ -16,7 +17,10 @@ type Strategy interface {
}
// RemoteAddrStrategy a strategy that always return the remote address.
type RemoteAddrStrategy struct{}
type RemoteAddrStrategy struct {
// IPv6Subnet instructs the strategy to return the first IP of the subnet where IP belongs.
IPv6Subnet *int
}
// GetIP returns the selected IP.
func (s *RemoteAddrStrategy) GetIP(req *http.Request) string {
@ -24,15 +28,22 @@ func (s *RemoteAddrStrategy) GetIP(req *http.Request) string {
if err != nil {
return req.RemoteAddr
}
if s.IPv6Subnet != nil {
return getIPv6SubnetIP(ip, *s.IPv6Subnet)
}
return ip
}
// DepthStrategy a strategy based on the depth inside the X-Forwarded-For from right to left.
type DepthStrategy struct {
Depth int
// IPv6Subnet instructs the strategy to return the first IP of the subnet where IP belongs.
IPv6Subnet *int
}
// GetIP return the selected IP.
// GetIP returns the selected IP.
func (s *DepthStrategy) GetIP(req *http.Request) string {
xff := req.Header.Get(xForwardedFor)
xffs := strings.Split(xff, ",")
@ -40,7 +51,14 @@ func (s *DepthStrategy) GetIP(req *http.Request) string {
if len(xffs) < s.Depth {
return ""
}
return strings.TrimSpace(xffs[len(xffs)-s.Depth])
ip := strings.TrimSpace(xffs[len(xffs)-s.Depth])
if s.IPv6Subnet != nil {
return getIPv6SubnetIP(ip, *s.IPv6Subnet)
}
return ip
}
// PoolStrategy is a strategy based on an IP Checker.
@ -72,3 +90,23 @@ func (s *PoolStrategy) GetIP(req *http.Request) string {
return ""
}
// getIPv6SubnetIP returns the IPv6 subnet IP.
// It returns the original IP when it is not an IPv6, or if parsing the IP has failed with an error.
func getIPv6SubnetIP(ip string, ipv6Subnet int) string {
addr, err := netip.ParseAddr(ip)
if err != nil {
return ip
}
if !addr.Is6() {
return ip
}
prefix, err := addr.Prefix(ipv6Subnet)
if err != nil {
return ip
}
return prefix.Addr().String()
}

View file

@ -9,23 +9,81 @@ import (
"github.com/stretchr/testify/require"
)
const (
ipv6Basic = "::abcd:ffff:c0a8:1"
ipv6BracketsPort = "[::abcd:ffff:c0a8:1]:80"
ipv6BracketsZonePort = "[::abcd:ffff:c0a8:1%1]:80"
)
func TestRemoteAddrStrategy_GetIP(t *testing.T) {
testCases := []struct {
desc string
expected string
desc string
expected string
remoteAddr string
ipv6Subnet *int
}{
// Valid IP format
{
desc: "Use RemoteAddr",
desc: "Use RemoteAddr, ipv4",
expected: "192.0.2.1",
},
{
desc: "Use RemoteAddr, ipv6 brackets with port, no IPv6 subnet",
remoteAddr: ipv6BracketsPort,
expected: "::abcd:ffff:c0a8:1",
},
{
desc: "Use RemoteAddr, ipv6 brackets with zone and port, no IPv6 subnet",
remoteAddr: ipv6BracketsZonePort,
expected: "::abcd:ffff:c0a8:1%1",
},
// Invalid IPv6 format
{
desc: "Use RemoteAddr, ipv6 basic, missing brackets, no IPv6 subnet",
remoteAddr: ipv6Basic,
expected: ipv6Basic,
},
// Valid IP format with subnet
{
desc: "Use RemoteAddr, ipv4, ignore subnet",
expected: "192.0.2.1",
ipv6Subnet: intPtr(24),
},
{
desc: "Use RemoteAddr, ipv6 brackets with port, subnet",
remoteAddr: ipv6BracketsPort,
expected: "::abcd:0:0:0",
ipv6Subnet: intPtr(80),
},
{
desc: "Use RemoteAddr, ipv6 brackets with zone and port, subnet",
remoteAddr: ipv6BracketsZonePort,
expected: "::abcd:0:0:0",
ipv6Subnet: intPtr(80),
},
// Valid IP, invalid subnet
{
desc: "Use RemoteAddr, ipv6 brackets with port, invalid subnet",
remoteAddr: ipv6BracketsPort,
expected: "::abcd:ffff:c0a8:1",
ipv6Subnet: intPtr(500),
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
strategy := RemoteAddrStrategy{}
strategy := RemoteAddrStrategy{
IPv6Subnet: test.ipv6Subnet,
}
req := httptest.NewRequest(http.MethodGet, "http://127.0.0.1", nil)
if test.remoteAddr != "" {
req.RemoteAddr = test.remoteAddr
}
actual := strategy.GetIP(req)
assert.Equal(t, test.expected, actual)
})
@ -38,6 +96,7 @@ func TestDepthStrategy_GetIP(t *testing.T) {
depth int
xForwardedFor string
expected string
ipv6Subnet *int
}{
{
desc: "Use depth",
@ -57,13 +116,30 @@ func TestDepthStrategy_GetIP(t *testing.T) {
xForwardedFor: "10.0.0.2,10.0.0.1",
expected: "10.0.0.2",
},
{
desc: "Use depth with IPv4 subnet",
depth: 2,
xForwardedFor: "10.0.0.3,10.0.0.2,10.0.0.1",
expected: "10.0.0.2",
ipv6Subnet: intPtr(80),
},
{
desc: "Use depth with IPv6 subnet",
depth: 2,
xForwardedFor: "10.0.0.3," + ipv6Basic + ",10.0.0.1",
expected: "::abcd:0:0:0",
ipv6Subnet: intPtr(80),
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
strategy := DepthStrategy{Depth: test.depth}
strategy := DepthStrategy{
Depth: test.depth,
IPv6Subnet: test.ipv6Subnet,
}
req := httptest.NewRequest(http.MethodGet, "http://127.0.0.1", nil)
req.Header.Set(xForwardedFor, test.xForwardedFor)
actual := strategy.GetIP(req)
@ -121,3 +197,7 @@ func TestTrustedIPsStrategy_GetIP(t *testing.T) {
})
}
}
func intPtr(value int) *int {
return &value
}

View file

@ -207,7 +207,7 @@ func newOpenTelemetryMeterProvider(ctx context.Context, config *types.OTLP) (*sd
}
res, err := resource.New(ctx,
resource.WithAttributes(semconv.ServiceNameKey.String("traefik")),
resource.WithAttributes(semconv.ServiceNameKey.String(config.ServiceName)),
resource.WithAttributes(semconv.ServiceVersionKey.String(version.Version)),
resource.WithFromEnv(),
resource.WithTelemetrySDK(),

View file

@ -282,151 +282,174 @@ func TestOpenTelemetry_GaugeCollectorSet(t *testing.T) {
}
func TestOpenTelemetry(t *testing.T) {
c := make(chan *string, 5)
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gzr, err := gzip.NewReader(r.Body)
require.NoError(t, err)
body, err := io.ReadAll(gzr)
require.NoError(t, err)
req := pmetricotlp.NewExportRequest()
err = req.UnmarshalProto(body)
require.NoError(t, err)
marshalledReq, err := json.Marshal(req)
require.NoError(t, err)
bodyStr := string(marshalledReq)
c <- &bodyStr
w.WriteHeader(http.StatusOK)
}))
t.Cleanup(func() {
StopOpenTelemetry()
ts.Close()
})
var cfg types.OTLP
(&cfg).SetDefaults()
cfg.AddRoutersLabels = true
cfg.HTTP = &types.OtelHTTP{
Endpoint: ts.URL,
}
cfg.PushInterval = ptypes.Duration(10 * time.Millisecond)
registry := RegisterOpenTelemetry(context.Background(), &cfg)
require.NotNil(t, registry)
if !registry.IsEpEnabled() || !registry.IsRouterEnabled() || !registry.IsSvcEnabled() {
t.Fatalf("registry should return true for IsEnabled(), IsRouterEnabled() and IsSvcEnabled()")
tests := []struct {
desc string
serviceName string
}{
{
desc: "default",
},
{
desc: "custom-service-name",
serviceName: "custom-service-name",
},
}
expected := []string{
`({"key":"service.name","value":{"stringValue":"traefik"}})`,
`({"key":"service.version","value":{"stringValue":"` + version.Version + `"}})`,
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
c := make(chan *string, 5)
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gzr, err := gzip.NewReader(r.Body)
require.NoError(t, err)
body, err := io.ReadAll(gzr)
require.NoError(t, err)
req := pmetricotlp.NewExportRequest()
err = req.UnmarshalProto(body)
require.NoError(t, err)
marshalledReq, err := json.Marshal(req)
require.NoError(t, err)
bodyStr := string(marshalledReq)
c <- &bodyStr
w.WriteHeader(http.StatusOK)
}))
t.Cleanup(func() {
StopOpenTelemetry()
ts.Close()
})
var cfg types.OTLP
(&cfg).SetDefaults()
cfg.AddRoutersLabels = true
cfg.HTTP = &types.OtelHTTP{
Endpoint: ts.URL,
}
cfg.PushInterval = ptypes.Duration(10 * time.Millisecond)
wantServiceName := "traefik"
if test.serviceName != "" {
cfg.ServiceName = test.serviceName
wantServiceName = test.serviceName
}
registry := RegisterOpenTelemetry(context.Background(), &cfg)
require.NotNil(t, registry)
if !registry.IsEpEnabled() || !registry.IsRouterEnabled() || !registry.IsSvcEnabled() {
t.Fatalf("registry should return true for IsEnabled(), IsRouterEnabled() and IsSvcEnabled()")
}
expected := []string{
`({"key":"service.name","value":{"stringValue":"` + wantServiceName + `"}})`,
`({"key":"service.version","value":{"stringValue":"` + version.Version + `"}})`,
}
tryAssertMessage(t, c, expected)
// TODO: the len of startUnixNano is no supposed to be 20, it should be 19
expectedConfig := []string{
`({"name":"traefik_config_reloads_total","description":"Config reloads","unit":"1","sum":{"dataPoints":\[{"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`,
`({"name":"traefik_config_last_reload_success","description":"Last config reload success","unit":"ms","gauge":{"dataPoints":\[{"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\]}})`,
`({"name":"traefik_open_connections","description":"How many open connections exist, by entryPoint and protocol","unit":"1","gauge":{"dataPoints":\[{"attributes":\[{"key":"entrypoint","value":{"stringValue":"test"}},{"key":"protocol","value":{"stringValue":"TCP"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\]}})`,
}
registry.ConfigReloadsCounter().Add(1)
registry.LastConfigReloadSuccessGauge().Set(1)
registry.OpenConnectionsGauge().With("entrypoint", "test", "protocol", "TCP").Set(1)
tryAssertMessage(t, c, expectedConfig)
expectedTLSCerts := []string{
`({"name":"traefik_tls_certs_not_after","description":"Certificate expiration timestamp","unit":"ms","gauge":{"dataPoints":\[{"attributes":\[{"key":"key","value":{"stringValue":"value"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\]}})`,
}
registry.TLSCertsNotAfterTimestampGauge().With("key", "value").Set(1)
tryAssertMessage(t, c, expectedTLSCerts)
expectedEntryPoints := []string{
`({"name":"traefik_entrypoint_requests_total","description":"How many HTTP requests processed on an entrypoint, partitioned by status code, protocol, and method.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"code","value":{"stringValue":"200"}},{"key":"entrypoint","value":{"stringValue":"test1"}},{"key":"method","value":{"stringValue":"GET"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`,
`({"name":"traefik_entrypoint_requests_tls_total","description":"How many HTTP requests with TLS processed on an entrypoint, partitioned by TLS Version and TLS cipher Used.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"entrypoint","value":{"stringValue":"test2"}},{"key":"tls_cipher","value":{"stringValue":"bar"}},{"key":"tls_version","value":{"stringValue":"foo"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`,
`({"name":"traefik_entrypoint_request_duration_seconds","description":"How long it took to process the request on an entrypoint, partitioned by status code, protocol, and method.","unit":"ms","histogram":{"dataPoints":\[{"attributes":\[{"key":"entrypoint","value":{"stringValue":"test3"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","count":"1","sum":10000,"bucketCounts":\["0","0","0","0","0","0","0","0","0","0","0","0","0","0","1"\],"explicitBounds":\[0.005,0.01,0.025,0.05,0.075,0.1,0.25,0.5,0.75,1,2.5,5,7.5,10\],"min":10000,"max":10000}\],"aggregationTemporality":2}})`,
`({"name":"traefik_entrypoint_requests_bytes_total","description":"The total size of requests in bytes handled by an entrypoint, partitioned by status code, protocol, and method.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"code","value":{"stringValue":"200"}},{"key":"entrypoint","value":{"stringValue":"test1"}},{"key":"method","value":{"stringValue":"GET"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`,
`({"name":"traefik_entrypoint_responses_bytes_total","description":"The total size of responses in bytes handled by an entrypoint, partitioned by status code, protocol, and method.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"code","value":{"stringValue":"200"}},{"key":"entrypoint","value":{"stringValue":"test1"}},{"key":"method","value":{"stringValue":"GET"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`,
}
registry.EntryPointReqsCounter().With(nil, "entrypoint", "test1", "code", strconv.Itoa(http.StatusOK), "method", http.MethodGet).Add(1)
registry.EntryPointReqsTLSCounter().With("entrypoint", "test2", "tls_version", "foo", "tls_cipher", "bar").Add(1)
registry.EntryPointReqDurationHistogram().With("entrypoint", "test3").Observe(10000)
registry.EntryPointReqsBytesCounter().With("entrypoint", "test1", "code", strconv.Itoa(http.StatusOK), "method", http.MethodGet).Add(1)
registry.EntryPointRespsBytesCounter().With("entrypoint", "test1", "code", strconv.Itoa(http.StatusOK), "method", http.MethodGet).Add(1)
tryAssertMessage(t, c, expectedEntryPoints)
expectedRouters := []string{
`({"name":"traefik_router_requests_total","description":"How many HTTP requests are processed on a router, partitioned by service, status code, protocol, and method.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"code","value":{"stringValue":"(?:200|404)"}},{"key":"method","value":{"stringValue":"GET"}},{"key":"router","value":{"stringValue":"RouterReqsCounter"}},{"key":"service","value":{"stringValue":"test"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1},{"attributes":\[{"key":"code","value":{"stringValue":"(?:200|404)"}},{"key":"method","value":{"stringValue":"GET"}},{"key":"router","value":{"stringValue":"RouterReqsCounter"}},{"key":"service","value":{"stringValue":"test"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`,
`({"name":"traefik_router_requests_tls_total","description":"How many HTTP requests with TLS are processed on a router, partitioned by service, TLS Version, and TLS cipher Used.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"router","value":{"stringValue":"demo"}},{"key":"service","value":{"stringValue":"test"}},{"key":"tls_cipher","value":{"stringValue":"bar"}},{"key":"tls_version","value":{"stringValue":"foo"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`,
`({"name":"traefik_router_request_duration_seconds","description":"How long it took to process the request on a router, partitioned by service, status code, protocol, and method.","unit":"ms","histogram":{"dataPoints":\[{"attributes":\[{"key":"code","value":{"stringValue":"200"}},{"key":"router","value":{"stringValue":"demo"}},{"key":"service","value":{"stringValue":"test"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","count":"1","sum":10000,"bucketCounts":\["0","0","0","0","0","0","0","0","0","0","0","0","0","0","1"\],"explicitBounds":\[0.005,0.01,0.025,0.05,0.075,0.1,0.25,0.5,0.75,1,2.5,5,7.5,10\],"min":10000,"max":10000}\],"aggregationTemporality":2}})`,
`({"name":"traefik_router_requests_bytes_total","description":"The total size of requests in bytes handled by a router, partitioned by status code, protocol, and method.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"code","value":{"stringValue":"404"}},{"key":"method","value":{"stringValue":"GET"}},{"key":"router","value":{"stringValue":"RouterReqsCounter"}},{"key":"service","value":{"stringValue":"test"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`,
`({"name":"traefik_router_responses_bytes_total","description":"The total size of responses in bytes handled by a router, partitioned by status code, protocol, and method.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"code","value":{"stringValue":"404"}},{"key":"method","value":{"stringValue":"GET"}},{"key":"router","value":{"stringValue":"RouterReqsCounter"}},{"key":"service","value":{"stringValue":"test"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`,
}
registry.RouterReqsCounter().With(nil, "router", "RouterReqsCounter", "service", "test", "code", strconv.Itoa(http.StatusNotFound), "method", http.MethodGet).Add(1)
registry.RouterReqsCounter().With(nil, "router", "RouterReqsCounter", "service", "test", "code", strconv.Itoa(http.StatusOK), "method", http.MethodGet).Add(1)
registry.RouterReqsTLSCounter().With("router", "demo", "service", "test", "tls_version", "foo", "tls_cipher", "bar").Add(1)
registry.RouterReqDurationHistogram().With("router", "demo", "service", "test", "code", strconv.Itoa(http.StatusOK)).Observe(10000)
registry.RouterReqsBytesCounter().With("router", "RouterReqsCounter", "service", "test", "code", strconv.Itoa(http.StatusNotFound), "method", http.MethodGet).Add(1)
registry.RouterRespsBytesCounter().With("router", "RouterReqsCounter", "service", "test", "code", strconv.Itoa(http.StatusNotFound), "method", http.MethodGet).Add(1)
tryAssertMessage(t, c, expectedRouters)
expectedServices := []string{
`({"name":"traefik_service_requests_total","description":"How many HTTP requests processed on a service, partitioned by status code, protocol, and method.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"code","value":{"stringValue":"(?:200|404)"}},{"key":"method","value":{"stringValue":"GET"}},{"key":"service","value":{"stringValue":"ServiceReqsCounter"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1},{"attributes":\[{"key":"code","value":{"stringValue":"(?:200|404)"}},{"key":"method","value":{"stringValue":"GET"}},{"key":"service","value":{"stringValue":"ServiceReqsCounter"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`,
`({"name":"traefik_service_requests_tls_total","description":"How many HTTP requests with TLS processed on a service, partitioned by TLS version and TLS cipher.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"service","value":{"stringValue":"test"}},{"key":"tls_cipher","value":{"stringValue":"bar"}},{"key":"tls_version","value":{"stringValue":"foo"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`,
`({"name":"traefik_service_request_duration_seconds","description":"How long it took to process the request on a service, partitioned by status code, protocol, and method.","unit":"ms","histogram":{"dataPoints":\[{"attributes":\[{"key":"code","value":{"stringValue":"200"}},{"key":"service","value":{"stringValue":"test"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","count":"1","sum":10000,"bucketCounts":\["0","0","0","0","0","0","0","0","0","0","0","0","0","0","1"\],"explicitBounds":\[0.005,0.01,0.025,0.05,0.075,0.1,0.25,0.5,0.75,1,2.5,5,7.5,10\],"min":10000,"max":10000}\],"aggregationTemporality":2}})`,
`({"name":"traefik_service_server_up","description":"service server is up, described by gauge value of 0 or 1.","unit":"1","gauge":{"dataPoints":\[{"attributes":\[{"key":"service","value":{"stringValue":"test"}},{"key":"url","value":{"stringValue":"http://127.0.0.1"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\]}})`,
`({"name":"traefik_service_requests_bytes_total","description":"The total size of requests in bytes received by a service, partitioned by status code, protocol, and method.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"code","value":{"stringValue":"404"}},{"key":"method","value":{"stringValue":"GET"}},{"key":"service","value":{"stringValue":"ServiceReqsCounter"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`,
`({"name":"traefik_service_responses_bytes_total","description":"The total size of responses in bytes returned by a service, partitioned by status code, protocol, and method.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"code","value":{"stringValue":"404"}},{"key":"method","value":{"stringValue":"GET"}},{"key":"service","value":{"stringValue":"ServiceReqsCounter"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`,
}
registry.ServiceReqsCounter().With(nil, "service", "ServiceReqsCounter", "code", strconv.Itoa(http.StatusOK), "method", http.MethodGet).Add(1)
registry.ServiceReqsCounter().With(nil, "service", "ServiceReqsCounter", "code", strconv.Itoa(http.StatusNotFound), "method", http.MethodGet).Add(1)
registry.ServiceReqsTLSCounter().With("service", "test", "tls_version", "foo", "tls_cipher", "bar").Add(1)
registry.ServiceReqDurationHistogram().With("service", "test", "code", strconv.Itoa(http.StatusOK)).Observe(10000)
registry.ServiceServerUpGauge().With("service", "test", "url", "http://127.0.0.1").Set(1)
registry.ServiceReqsBytesCounter().With("service", "ServiceReqsCounter", "code", strconv.Itoa(http.StatusNotFound), "method", http.MethodGet).Add(1)
registry.ServiceRespsBytesCounter().With("service", "ServiceReqsCounter", "code", strconv.Itoa(http.StatusNotFound), "method", http.MethodGet).Add(1)
tryAssertMessage(t, c, expectedServices)
expectedServicesRetries := []string{
`({"attributes":\[{"key":"service","value":{"stringValue":"foobar"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1})`,
`({"attributes":\[{"key":"service","value":{"stringValue":"test"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":2})`,
}
registry.ServiceRetriesCounter().With("service", "test").Add(1)
registry.ServiceRetriesCounter().With("service", "test").Add(1)
registry.ServiceRetriesCounter().With("service", "foobar").Add(1)
tryAssertMessage(t, c, expectedServicesRetries)
// We cannot rely on the previous expected pattern,
// because this pattern was for matching only one dataPoint in the histogram,
// and as soon as the EntryPointReqDurationHistogram.Observe is called,
// it adds a new dataPoint to the histogram.
expectedEntryPointReqDuration := []string{
`({"attributes":\[{"key":"entrypoint","value":{"stringValue":"myEntrypoint"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","count":"2","sum":30000,"bucketCounts":\["0","0","0","0","0","0","0","0","0","0","0","0","0","0","2"\],"explicitBounds":\[0.005,0.01,0.025,0.05,0.075,0.1,0.25,0.5,0.75,1,2.5,5,7.5,10\],"min":10000,"max":20000})`,
}
registry.EntryPointReqDurationHistogram().With("entrypoint", "myEntrypoint").Observe(10000)
registry.EntryPointReqDurationHistogram().With("entrypoint", "myEntrypoint").Observe(20000)
tryAssertMessage(t, c, expectedEntryPointReqDuration)
})
}
tryAssertMessage(t, c, expected)
// TODO: the len of startUnixNano is no supposed to be 20, it should be 19
expectedConfig := []string{
`({"name":"traefik_config_reloads_total","description":"Config reloads","unit":"1","sum":{"dataPoints":\[{"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`,
`({"name":"traefik_config_last_reload_success","description":"Last config reload success","unit":"ms","gauge":{"dataPoints":\[{"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\]}})`,
`({"name":"traefik_open_connections","description":"How many open connections exist, by entryPoint and protocol","unit":"1","gauge":{"dataPoints":\[{"attributes":\[{"key":"entrypoint","value":{"stringValue":"test"}},{"key":"protocol","value":{"stringValue":"TCP"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\]}})`,
}
registry.ConfigReloadsCounter().Add(1)
registry.LastConfigReloadSuccessGauge().Set(1)
registry.OpenConnectionsGauge().With("entrypoint", "test", "protocol", "TCP").Set(1)
tryAssertMessage(t, c, expectedConfig)
expectedTLSCerts := []string{
`({"name":"traefik_tls_certs_not_after","description":"Certificate expiration timestamp","unit":"ms","gauge":{"dataPoints":\[{"attributes":\[{"key":"key","value":{"stringValue":"value"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\]}})`,
}
registry.TLSCertsNotAfterTimestampGauge().With("key", "value").Set(1)
tryAssertMessage(t, c, expectedTLSCerts)
expectedEntryPoints := []string{
`({"name":"traefik_entrypoint_requests_total","description":"How many HTTP requests processed on an entrypoint, partitioned by status code, protocol, and method.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"code","value":{"stringValue":"200"}},{"key":"entrypoint","value":{"stringValue":"test1"}},{"key":"method","value":{"stringValue":"GET"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`,
`({"name":"traefik_entrypoint_requests_tls_total","description":"How many HTTP requests with TLS processed on an entrypoint, partitioned by TLS Version and TLS cipher Used.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"entrypoint","value":{"stringValue":"test2"}},{"key":"tls_cipher","value":{"stringValue":"bar"}},{"key":"tls_version","value":{"stringValue":"foo"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`,
`({"name":"traefik_entrypoint_request_duration_seconds","description":"How long it took to process the request on an entrypoint, partitioned by status code, protocol, and method.","unit":"ms","histogram":{"dataPoints":\[{"attributes":\[{"key":"entrypoint","value":{"stringValue":"test3"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","count":"1","sum":10000,"bucketCounts":\["0","0","0","0","0","0","0","0","0","0","0","0","0","0","1"\],"explicitBounds":\[0.005,0.01,0.025,0.05,0.075,0.1,0.25,0.5,0.75,1,2.5,5,7.5,10\],"min":10000,"max":10000}\],"aggregationTemporality":2}})`,
`({"name":"traefik_entrypoint_requests_bytes_total","description":"The total size of requests in bytes handled by an entrypoint, partitioned by status code, protocol, and method.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"code","value":{"stringValue":"200"}},{"key":"entrypoint","value":{"stringValue":"test1"}},{"key":"method","value":{"stringValue":"GET"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`,
`({"name":"traefik_entrypoint_responses_bytes_total","description":"The total size of responses in bytes handled by an entrypoint, partitioned by status code, protocol, and method.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"code","value":{"stringValue":"200"}},{"key":"entrypoint","value":{"stringValue":"test1"}},{"key":"method","value":{"stringValue":"GET"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`,
}
registry.EntryPointReqsCounter().With(nil, "entrypoint", "test1", "code", strconv.Itoa(http.StatusOK), "method", http.MethodGet).Add(1)
registry.EntryPointReqsTLSCounter().With("entrypoint", "test2", "tls_version", "foo", "tls_cipher", "bar").Add(1)
registry.EntryPointReqDurationHistogram().With("entrypoint", "test3").Observe(10000)
registry.EntryPointReqsBytesCounter().With("entrypoint", "test1", "code", strconv.Itoa(http.StatusOK), "method", http.MethodGet).Add(1)
registry.EntryPointRespsBytesCounter().With("entrypoint", "test1", "code", strconv.Itoa(http.StatusOK), "method", http.MethodGet).Add(1)
tryAssertMessage(t, c, expectedEntryPoints)
expectedRouters := []string{
`({"name":"traefik_router_requests_total","description":"How many HTTP requests are processed on a router, partitioned by service, status code, protocol, and method.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"code","value":{"stringValue":"(?:200|404)"}},{"key":"method","value":{"stringValue":"GET"}},{"key":"router","value":{"stringValue":"RouterReqsCounter"}},{"key":"service","value":{"stringValue":"test"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1},{"attributes":\[{"key":"code","value":{"stringValue":"(?:200|404)"}},{"key":"method","value":{"stringValue":"GET"}},{"key":"router","value":{"stringValue":"RouterReqsCounter"}},{"key":"service","value":{"stringValue":"test"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`,
`({"name":"traefik_router_requests_tls_total","description":"How many HTTP requests with TLS are processed on a router, partitioned by service, TLS Version, and TLS cipher Used.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"router","value":{"stringValue":"demo"}},{"key":"service","value":{"stringValue":"test"}},{"key":"tls_cipher","value":{"stringValue":"bar"}},{"key":"tls_version","value":{"stringValue":"foo"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`,
`({"name":"traefik_router_request_duration_seconds","description":"How long it took to process the request on a router, partitioned by service, status code, protocol, and method.","unit":"ms","histogram":{"dataPoints":\[{"attributes":\[{"key":"code","value":{"stringValue":"200"}},{"key":"router","value":{"stringValue":"demo"}},{"key":"service","value":{"stringValue":"test"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","count":"1","sum":10000,"bucketCounts":\["0","0","0","0","0","0","0","0","0","0","0","0","0","0","1"\],"explicitBounds":\[0.005,0.01,0.025,0.05,0.075,0.1,0.25,0.5,0.75,1,2.5,5,7.5,10\],"min":10000,"max":10000}\],"aggregationTemporality":2}})`,
`({"name":"traefik_router_requests_bytes_total","description":"The total size of requests in bytes handled by a router, partitioned by status code, protocol, and method.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"code","value":{"stringValue":"404"}},{"key":"method","value":{"stringValue":"GET"}},{"key":"router","value":{"stringValue":"RouterReqsCounter"}},{"key":"service","value":{"stringValue":"test"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`,
`({"name":"traefik_router_responses_bytes_total","description":"The total size of responses in bytes handled by a router, partitioned by status code, protocol, and method.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"code","value":{"stringValue":"404"}},{"key":"method","value":{"stringValue":"GET"}},{"key":"router","value":{"stringValue":"RouterReqsCounter"}},{"key":"service","value":{"stringValue":"test"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`,
}
registry.RouterReqsCounter().With(nil, "router", "RouterReqsCounter", "service", "test", "code", strconv.Itoa(http.StatusNotFound), "method", http.MethodGet).Add(1)
registry.RouterReqsCounter().With(nil, "router", "RouterReqsCounter", "service", "test", "code", strconv.Itoa(http.StatusOK), "method", http.MethodGet).Add(1)
registry.RouterReqsTLSCounter().With("router", "demo", "service", "test", "tls_version", "foo", "tls_cipher", "bar").Add(1)
registry.RouterReqDurationHistogram().With("router", "demo", "service", "test", "code", strconv.Itoa(http.StatusOK)).Observe(10000)
registry.RouterReqsBytesCounter().With("router", "RouterReqsCounter", "service", "test", "code", strconv.Itoa(http.StatusNotFound), "method", http.MethodGet).Add(1)
registry.RouterRespsBytesCounter().With("router", "RouterReqsCounter", "service", "test", "code", strconv.Itoa(http.StatusNotFound), "method", http.MethodGet).Add(1)
tryAssertMessage(t, c, expectedRouters)
expectedServices := []string{
`({"name":"traefik_service_requests_total","description":"How many HTTP requests processed on a service, partitioned by status code, protocol, and method.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"code","value":{"stringValue":"(?:200|404)"}},{"key":"method","value":{"stringValue":"GET"}},{"key":"service","value":{"stringValue":"ServiceReqsCounter"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1},{"attributes":\[{"key":"code","value":{"stringValue":"(?:200|404)"}},{"key":"method","value":{"stringValue":"GET"}},{"key":"service","value":{"stringValue":"ServiceReqsCounter"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`,
`({"name":"traefik_service_requests_tls_total","description":"How many HTTP requests with TLS processed on a service, partitioned by TLS version and TLS cipher.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"service","value":{"stringValue":"test"}},{"key":"tls_cipher","value":{"stringValue":"bar"}},{"key":"tls_version","value":{"stringValue":"foo"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`,
`({"name":"traefik_service_request_duration_seconds","description":"How long it took to process the request on a service, partitioned by status code, protocol, and method.","unit":"ms","histogram":{"dataPoints":\[{"attributes":\[{"key":"code","value":{"stringValue":"200"}},{"key":"service","value":{"stringValue":"test"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","count":"1","sum":10000,"bucketCounts":\["0","0","0","0","0","0","0","0","0","0","0","0","0","0","1"\],"explicitBounds":\[0.005,0.01,0.025,0.05,0.075,0.1,0.25,0.5,0.75,1,2.5,5,7.5,10\],"min":10000,"max":10000}\],"aggregationTemporality":2}})`,
`({"name":"traefik_service_server_up","description":"service server is up, described by gauge value of 0 or 1.","unit":"1","gauge":{"dataPoints":\[{"attributes":\[{"key":"service","value":{"stringValue":"test"}},{"key":"url","value":{"stringValue":"http://127.0.0.1"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\]}})`,
`({"name":"traefik_service_requests_bytes_total","description":"The total size of requests in bytes received by a service, partitioned by status code, protocol, and method.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"code","value":{"stringValue":"404"}},{"key":"method","value":{"stringValue":"GET"}},{"key":"service","value":{"stringValue":"ServiceReqsCounter"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`,
`({"name":"traefik_service_responses_bytes_total","description":"The total size of responses in bytes returned by a service, partitioned by status code, protocol, and method.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"code","value":{"stringValue":"404"}},{"key":"method","value":{"stringValue":"GET"}},{"key":"service","value":{"stringValue":"ServiceReqsCounter"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`,
}
registry.ServiceReqsCounter().With(nil, "service", "ServiceReqsCounter", "code", strconv.Itoa(http.StatusOK), "method", http.MethodGet).Add(1)
registry.ServiceReqsCounter().With(nil, "service", "ServiceReqsCounter", "code", strconv.Itoa(http.StatusNotFound), "method", http.MethodGet).Add(1)
registry.ServiceReqsTLSCounter().With("service", "test", "tls_version", "foo", "tls_cipher", "bar").Add(1)
registry.ServiceReqDurationHistogram().With("service", "test", "code", strconv.Itoa(http.StatusOK)).Observe(10000)
registry.ServiceServerUpGauge().With("service", "test", "url", "http://127.0.0.1").Set(1)
registry.ServiceReqsBytesCounter().With("service", "ServiceReqsCounter", "code", strconv.Itoa(http.StatusNotFound), "method", http.MethodGet).Add(1)
registry.ServiceRespsBytesCounter().With("service", "ServiceReqsCounter", "code", strconv.Itoa(http.StatusNotFound), "method", http.MethodGet).Add(1)
tryAssertMessage(t, c, expectedServices)
expectedServicesRetries := []string{
`({"attributes":\[{"key":"service","value":{"stringValue":"foobar"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1})`,
`({"attributes":\[{"key":"service","value":{"stringValue":"test"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":2})`,
}
registry.ServiceRetriesCounter().With("service", "test").Add(1)
registry.ServiceRetriesCounter().With("service", "test").Add(1)
registry.ServiceRetriesCounter().With("service", "foobar").Add(1)
tryAssertMessage(t, c, expectedServicesRetries)
// We cannot rely on the previous expected pattern,
// because this pattern was for matching only one dataPoint in the histogram,
// and as soon as the EntryPointReqDurationHistogram.Observe is called,
// it adds a new dataPoint to the histogram.
expectedEntryPointReqDuration := []string{
`({"attributes":\[{"key":"entrypoint","value":{"stringValue":"myEntrypoint"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","count":"2","sum":30000,"bucketCounts":\["0","0","0","0","0","0","0","0","0","0","0","0","0","0","2"\],"explicitBounds":\[0.005,0.01,0.025,0.05,0.075,0.1,0.25,0.5,0.75,1,2.5,5,7.5,10\],"min":10000,"max":20000})`,
}
registry.EntryPointReqDurationHistogram().With("entrypoint", "myEntrypoint").Observe(10000)
registry.EntryPointReqDurationHistogram().With("entrypoint", "myEntrypoint").Observe(20000)
tryAssertMessage(t, c, expectedEntryPointReqDuration)
}
func assertMessage(t *testing.T, msg string, expected []string) {

View file

@ -2,22 +2,23 @@ package gateway
import "sigs.k8s.io/gateway-api/pkg/features"
func SupportedFeatures() []features.SupportedFeature {
return []features.SupportedFeature{
features.SupportGateway,
features.SupportGatewayPort8080,
features.SupportGRPCRoute,
features.SupportHTTPRoute,
features.SupportHTTPRouteQueryParamMatching,
features.SupportHTTPRouteMethodMatching,
features.SupportHTTPRoutePortRedirect,
features.SupportHTTPRouteSchemeRedirect,
features.SupportHTTPRouteHostRewrite,
features.SupportHTTPRoutePathRewrite,
features.SupportHTTPRoutePathRedirect,
features.SupportHTTPRouteResponseHeaderModification,
features.SupportTLSRoute,
features.SupportHTTPRouteBackendProtocolH2C,
features.SupportHTTPRouteBackendProtocolWebSocket,
func SupportedFeatures() []features.FeatureName {
return []features.FeatureName{
features.GatewayFeature.Name,
features.GatewayPort8080Feature.Name,
features.GRPCRouteFeature.Name,
features.HTTPRouteFeature.Name,
features.HTTPRouteQueryParamMatchingFeature.Name,
features.HTTPRouteMethodMatchingFeature.Name,
features.HTTPRoutePortRedirectFeature.Name,
features.HTTPRouteSchemeRedirectFeature.Name,
features.HTTPRouteHostRewriteFeature.Name,
features.HTTPRoutePathRewriteFeature.Name,
features.HTTPRoutePathRedirectFeature.Name,
features.HTTPRouteResponseHeaderModificationFeature.Name,
features.HTTPRouteBackendProtocolH2CFeature.Name,
features.HTTPRouteBackendProtocolWebSocketFeature.Name,
features.HTTPRouteDestinationPortMatchingFeature.Name,
features.TLSRouteFeature.Name,
}
}

View file

@ -49,17 +49,16 @@ func (p *Provider) loadGRPCRoutes(ctx context.Context, gatewayListeners []gatewa
}
for _, listener := range gatewayListeners {
if !matchListener(listener, route.Namespace, parentRef) {
continue
}
accepted := true
if !allowRoute(listener, route.Namespace, kindGRPCRoute) {
if !matchListener(listener, route.Namespace, parentRef) {
accepted = false
}
if accepted && !allowRoute(listener, route.Namespace, kindGRPCRoute) {
parentStatus.Conditions = updateRouteConditionAccepted(parentStatus.Conditions, string(gatev1.RouteReasonNotAllowedByListeners))
accepted = false
}
hostnames, ok := findMatchingHostnames(listener.Hostname, route.Spec.Hostnames)
if !ok {
if accepted && !ok {
parentStatus.Conditions = updateRouteConditionAccepted(parentStatus.Conditions, string(gatev1.RouteReasonNoMatchingListenerHostname))
accepted = false
}
@ -396,10 +395,10 @@ func buildGRPCMethodRule(method *gatev1.GRPCMethodMatch) string {
func buildGRPCHeaderRules(headers []gatev1.GRPCHeaderMatch) []string {
var rules []string
for _, header := range headers {
switch ptr.Deref(header.Type, gatev1.HeaderMatchExact) {
case gatev1.HeaderMatchExact:
switch ptr.Deref(header.Type, gatev1.GRPCHeaderMatchExact) {
case gatev1.GRPCHeaderMatchExact:
rules = append(rules, fmt.Sprintf("Header(`%s`,`%s`)", header.Name, header.Value))
case gatev1.HeaderMatchRegularExpression:
case gatev1.GRPCHeaderMatchRegularExpression:
rules = append(rules, fmt.Sprintf("HeaderRegexp(`%s`,`%s`)", header.Name, header.Value))
}
}

View file

@ -50,7 +50,7 @@ func Test_buildGRPCMatchRule(t *testing.T) {
},
Headers: []gatev1.GRPCHeaderMatch{
{
Type: ptr.To(gatev1.HeaderMatchExact),
Type: ptr.To(gatev1.GRPCHeaderMatchExact),
Name: "foo",
Value: "bar",
},
@ -70,7 +70,7 @@ func Test_buildGRPCMatchRule(t *testing.T) {
},
Headers: []gatev1.GRPCHeaderMatch{
{
Type: ptr.To(gatev1.HeaderMatchExact),
Type: ptr.To(gatev1.GRPCHeaderMatchExact),
Name: "foo",
Value: "bar",
},
@ -177,7 +177,7 @@ func Test_buildGRPCHeaderRules(t *testing.T) {
desc: "One exact match type",
headers: []gatev1.GRPCHeaderMatch{
{
Type: ptr.To(gatev1.HeaderMatchExact),
Type: ptr.To(gatev1.GRPCHeaderMatchExact),
Name: "foo",
Value: "bar",
},
@ -188,7 +188,7 @@ func Test_buildGRPCHeaderRules(t *testing.T) {
desc: "One regexp match type",
headers: []gatev1.GRPCHeaderMatch{
{
Type: ptr.To(gatev1.HeaderMatchRegularExpression),
Type: ptr.To(gatev1.GRPCHeaderMatchRegularExpression),
Name: "foo",
Value: ".*",
},
@ -199,12 +199,12 @@ func Test_buildGRPCHeaderRules(t *testing.T) {
desc: "One exact and regexp match type",
headers: []gatev1.GRPCHeaderMatch{
{
Type: ptr.To(gatev1.HeaderMatchExact),
Type: ptr.To(gatev1.GRPCHeaderMatchExact),
Name: "foo",
Value: "bar",
},
{
Type: ptr.To(gatev1.HeaderMatchRegularExpression),
Type: ptr.To(gatev1.GRPCHeaderMatchRegularExpression),
Name: "foo",
Value: ".*",
},

View file

@ -53,17 +53,16 @@ func (p *Provider) loadHTTPRoutes(ctx context.Context, gatewayListeners []gatewa
}
for _, listener := range gatewayListeners {
if !matchListener(listener, route.Namespace, parentRef) {
continue
}
accepted := true
if !allowRoute(listener, route.Namespace, kindHTTPRoute) {
if !matchListener(listener, route.Namespace, parentRef) {
accepted = false
}
if accepted && !allowRoute(listener, route.Namespace, kindHTTPRoute) {
parentStatus.Conditions = updateRouteConditionAccepted(parentStatus.Conditions, string(gatev1.RouteReasonNotAllowedByListeners))
accepted = false
}
hostnames, ok := findMatchingHostnames(listener.Hostname, route.Spec.Hostnames)
if !ok {
if accepted && !ok {
parentStatus.Conditions = updateRouteConditionAccepted(parentStatus.Conditions, string(gatev1.RouteReasonNoMatchingListenerHostname))
accepted = false
}

View file

@ -112,6 +112,7 @@ type ExtensionBuilderRegistry interface {
type gatewayListener struct {
Name string
Port gatev1.PortNumber
Protocol gatev1.ProtocolType
TLS *gatev1.GatewayTLSConfig
Hostname *gatev1.Hostname
@ -317,10 +318,14 @@ func (p *Provider) loadConfigurationFromGateways(ctx context.Context) *dynamic.C
}
var supportedFeatures []gatev1.SupportedFeature
for _, feature := range SupportedFeatures() {
supportedFeatures = append(supportedFeatures, gatev1.SupportedFeature(feature))
if p.ExperimentalChannel {
for _, feature := range SupportedFeatures() {
supportedFeatures = append(supportedFeatures, gatev1.SupportedFeature{Name: gatev1.FeatureName(feature)})
}
slices.SortFunc(supportedFeatures, func(a, b gatev1.SupportedFeature) int {
return strings.Compare(string(a.Name), string(b.Name))
})
}
slices.Sort(supportedFeatures)
gatewayClassNames := map[string]struct{}{}
for _, gatewayClass := range gatewayClasses {
@ -425,6 +430,7 @@ func (p *Provider) loadGatewayListeners(ctx context.Context, gateway *gatev1.Gat
GWName: gateway.Name,
GWNamespace: gateway.Namespace,
GWGeneration: gateway.Generation,
Port: listener.Port,
Protocol: listener.Protocol,
TLS: listener.TLS,
Hostname: listener.Hostname,
@ -1114,6 +1120,10 @@ func matchListener(listener gatewayListener, routeNamespace string, parentRef ga
return false
}
if parentRef.Port != nil && *parentRef.Port != listener.Port {
return false
}
return true
}

View file

@ -49,12 +49,11 @@ func (p *Provider) loadTCPRoutes(ctx context.Context, gatewayListeners []gateway
}
for _, listener := range gatewayListeners {
if !matchListener(listener, route.Namespace, parentRef) {
continue
}
accepted := true
if !allowRoute(listener, route.Namespace, kindTCPRoute) {
if !matchListener(listener, route.Namespace, parentRef) {
accepted = false
}
if accepted && !allowRoute(listener, route.Namespace, kindTCPRoute) {
parentStatus.Conditions = updateRouteConditionAccepted(parentStatus.Conditions, string(gatev1.RouteReasonNotAllowedByListeners))
accepted = false
}

View file

@ -49,17 +49,16 @@ func (p *Provider) loadTLSRoutes(ctx context.Context, gatewayListeners []gateway
}
for _, listener := range gatewayListeners {
if !matchListener(listener, route.Namespace, parentRef) {
continue
}
accepted := true
if !allowRoute(listener, route.Namespace, kindTLSRoute) {
if !matchListener(listener, route.Namespace, parentRef) {
accepted = false
}
if accepted && !allowRoute(listener, route.Namespace, kindTLSRoute) {
parentStatus.Conditions = updateRouteConditionAccepted(parentStatus.Conditions, string(gatev1.RouteReasonNotAllowedByListeners))
accepted = false
}
hostnames, ok := findMatchingHostnames(listener.Hostname, route.Spec.Hostnames)
if !ok {
if accepted && !ok {
parentStatus.Conditions = updateRouteConditionAccepted(parentStatus.Conditions, string(gatev1.RouteReasonNoMatchingListenerHostname))
accepted = false
}

View file

@ -10,6 +10,7 @@ import (
"github.com/cenkalti/backoff/v4"
"github.com/hashicorp/nomad/api"
"github.com/mitchellh/hashstructure"
"github.com/rs/zerolog/log"
ptypes "github.com/traefik/paerser/types"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
@ -93,6 +94,8 @@ type Configuration struct {
ExposedByDefault bool `description:"Expose Nomad services by default." json:"exposedByDefault,omitempty" toml:"exposedByDefault,omitempty" yaml:"exposedByDefault,omitempty" export:"true"`
RefreshInterval ptypes.Duration `description:"Interval for polling Nomad API." json:"refreshInterval,omitempty" toml:"refreshInterval,omitempty" yaml:"refreshInterval,omitempty" export:"true"`
AllowEmptyServices bool `description:"Allow the creation of services without endpoints." json:"allowEmptyServices,omitempty" toml:"allowEmptyServices,omitempty" yaml:"allowEmptyServices,omitempty" export:"true"`
Watch bool `description:"Watch Nomad Service events." json:"watch,omitempty" toml:"watch,omitempty" yaml:"watch,omitempty" export:"true"`
ThrottleDuration ptypes.Duration `description:"Watch throttle duration." json:"throttleDuration,omitempty" toml:"throttleDuration,omitempty" yaml:"throttleDuration,omitempty" export:"true"`
}
// SetDefaults sets the default values for the Nomad Traefik Provider Configuration.
@ -117,7 +120,7 @@ func (c *Configuration) SetDefaults() {
c.ExposedByDefault = true
c.RefreshInterval = ptypes.Duration(15 * time.Second)
c.DefaultRule = defaultTemplateRule
c.AllowEmptyServices = false
c.ThrottleDuration = ptypes.Duration(0)
}
type EndpointConfig struct {
@ -139,6 +142,8 @@ type Provider struct {
namespace string
client *api.Client // client for Nomad API
defaultRuleTpl *template.Template // default routing rule
lastConfiguration safe.Safe
}
// SetDefaults sets the default values for the Nomad Traefik Provider.
@ -152,6 +157,10 @@ func (p *Provider) Init() error {
return errors.New("wildcard namespace not supported")
}
if p.ThrottleDuration > 0 && !p.Watch {
return errors.New("throttle duration should not be used with polling mode")
}
defaultRuleTpl, err := provider.MakeDefaultRuleTemplate(p.DefaultRule, nil)
if err != nil {
return fmt.Errorf("error while parsing default rule: %w", err)
@ -183,32 +192,63 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
ctx, cancel := context.WithCancel(ctxLog)
defer cancel()
// load initial configuration
if err := p.loadConfiguration(ctx, configurationChan); err != nil {
return fmt.Errorf("failed to load initial nomad services: %w", err)
serviceEventsChan, err := p.pollOrWatch(ctx)
if err != nil {
return fmt.Errorf("watching Nomad events: %w", err)
}
// issue periodic refreshes in the background
// (Nomad does not support Watch style observations)
ticker := time.NewTicker(time.Duration(p.RefreshInterval))
defer ticker.Stop()
throttleDuration := time.Duration(p.ThrottleDuration)
throttledChan := throttleEvents(ctx, throttleDuration, pool, serviceEventsChan)
if throttledChan != nil {
serviceEventsChan = throttledChan
}
conf, err := p.loadConfiguration(ctx)
if err != nil {
return fmt.Errorf("loading configuration: %w", err)
}
if _, err := p.updateLastConfiguration(conf); err != nil {
return fmt.Errorf("updating last configuration: %w", err)
}
configurationChan <- dynamic.Message{
ProviderName: p.name,
Configuration: conf,
}
// enter loop where we wait for and respond to notifications
for {
select {
case <-ctx.Done():
return nil
case <-ticker.C:
}
// load services due to refresh
if err := p.loadConfiguration(ctx, configurationChan); err != nil {
return fmt.Errorf("failed to refresh nomad services: %w", err)
case event := <-serviceEventsChan:
conf, err = p.loadConfiguration(ctx)
if err != nil {
return fmt.Errorf("loading configuration: %w", err)
}
updated, err := p.updateLastConfiguration(conf)
if err != nil {
return fmt.Errorf("updating last configuration: %w", err)
}
if !updated {
logger.Debug().Msgf("Skipping Nomad event %d with no changes", event.Index)
continue
}
configurationChan <- dynamic.Message{
ProviderName: p.name,
Configuration: conf,
}
// If we're throttling, we sleep here for the throttle duration to
// enforce that we don't refresh faster than our throttle. time.Sleep
// returns immediately if p.ThrottleDuration is 0 (no throttle).
time.Sleep(throttleDuration)
}
}
}
failure := func(err error, d time.Duration) {
logger.Error().Err(err).Msgf("Provider connection error, retrying in %s", d)
logger.Error().Err(err).Msgf("Loading configuration, retrying in %s", d)
}
if retryErr := backoff.RetryNotify(
@ -223,27 +263,70 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
return nil
}
func (p *Provider) loadConfiguration(ctx context.Context, configurationC chan<- dynamic.Message) error {
func (p *Provider) pollOrWatch(ctx context.Context) (<-chan *api.Events, error) {
if p.Watch {
return p.client.EventStream().Stream(ctx,
map[api.Topic][]string{
api.TopicService: {"*"},
},
0,
&api.QueryOptions{
Namespace: p.namespace,
},
)
}
serviceEventsChan := make(chan *api.Events, 1)
go func() {
ticker := time.NewTicker(time.Duration(p.RefreshInterval))
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case t := <-ticker.C:
serviceEventsChan <- &api.Events{
Index: uint64(t.UnixNano()),
}
}
}
}()
return serviceEventsChan, nil
}
func (p *Provider) loadConfiguration(ctx context.Context) (*dynamic.Configuration, error) {
var items []item
var err error
if p.AllowEmptyServices {
items, err = p.getNomadServiceDataWithEmptyServices(ctx)
if err != nil {
return err
return nil, err
}
} else {
items, err = p.getNomadServiceData(ctx)
if err != nil {
return err
return nil, err
}
}
configurationC <- dynamic.Message{
ProviderName: p.name,
Configuration: p.buildConfig(ctx, items),
return p.buildConfig(ctx, items), nil
}
func (p *Provider) updateLastConfiguration(conf *dynamic.Configuration) (bool, error) {
confHash, err := hashstructure.Hash(conf, nil)
if err != nil {
return false, fmt.Errorf("hashing the configuration: %w", err)
}
return nil
if p.lastConfiguration.Get() == confHash {
return false, nil
}
p.lastConfiguration.Set(confHash)
return true, nil
}
func (p *Provider) getNomadServiceData(ctx context.Context) ([]item, error) {
@ -453,3 +536,38 @@ func createClient(namespace string, endpoint *EndpointConfig) (*api.Client, erro
return api.NewClient(&config)
}
// Copied from the Kubernetes provider.
func throttleEvents(ctx context.Context, throttleDuration time.Duration, pool *safe.Pool, eventsChan <-chan *api.Events) chan *api.Events {
if throttleDuration == 0 {
return nil
}
// Create a buffered channel to hold the pending event (if we're delaying processing the event due to throttling).
eventsChanBuffered := make(chan *api.Events, 1)
// Run a goroutine that reads events from eventChan and does a
// non-blocking write to pendingEvent. This guarantees that writing to
// eventChan will never block, and that pendingEvent will have
// something in it if there's been an event since we read from that channel.
pool.GoCtx(func(ctxPool context.Context) {
for {
select {
case <-ctxPool.Done():
return
case nextEvent := <-eventsChan:
select {
case eventsChanBuffered <- nextEvent:
default:
// We already have an event in eventsChanBuffered, so we'll
// do a refresh as soon as our throttle allows us to. It's fine
// to drop the event and keep whatever's in the buffer -- we
// don't do different things for different events.
log.Ctx(ctx).Debug().Msgf("Dropping event %d due to throttling", nextEvent.Index)
}
}
}
})
return eventsChanBuffered
}

129
pkg/proxy/fast/builder.go Normal file
View file

@ -0,0 +1,129 @@
package fast
import (
"crypto/tls"
"fmt"
"net"
"net/http"
"net/url"
"reflect"
"time"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/config/static"
)
// TransportManager manages transport used for backend communications.
type TransportManager interface {
Get(name string) (*dynamic.ServersTransport, error)
GetTLSConfig(name string) (*tls.Config, error)
}
// ProxyBuilder handles the connection pools for the FastProxy proxies.
type ProxyBuilder struct {
debug bool
transportManager TransportManager
// lock isn't needed because ProxyBuilder is not called concurrently.
pools map[string]map[string]*connPool
proxy func(*http.Request) (*url.URL, error)
// not goroutine safe.
configs map[string]*dynamic.ServersTransport
}
// NewProxyBuilder creates a new ProxyBuilder.
func NewProxyBuilder(transportManager TransportManager, config static.FastProxyConfig) *ProxyBuilder {
return &ProxyBuilder{
debug: config.Debug,
transportManager: transportManager,
pools: make(map[string]map[string]*connPool),
proxy: http.ProxyFromEnvironment,
configs: make(map[string]*dynamic.ServersTransport),
}
}
// Update updates all the round-tripper corresponding to the given configs.
// This method must not be used concurrently.
func (r *ProxyBuilder) Update(newConfigs map[string]*dynamic.ServersTransport) {
for configName := range r.configs {
if _, ok := newConfigs[configName]; !ok {
for _, c := range r.pools[configName] {
c.Close()
}
delete(r.pools, configName)
}
}
for newConfigName, newConfig := range newConfigs {
if !reflect.DeepEqual(newConfig, r.configs[newConfigName]) {
for _, c := range r.pools[newConfigName] {
c.Close()
}
delete(r.pools, newConfigName)
}
}
r.configs = newConfigs
}
// Build builds a new ReverseProxy with the given configuration.
func (r *ProxyBuilder) Build(cfgName string, targetURL *url.URL, passHostHeader bool) (http.Handler, error) {
proxyURL, err := r.proxy(&http.Request{URL: targetURL})
if err != nil {
return nil, fmt.Errorf("getting proxy: %w", err)
}
cfg, err := r.transportManager.Get(cfgName)
if err != nil {
return nil, fmt.Errorf("getting ServersTransport: %w", err)
}
var responseHeaderTimeout time.Duration
if cfg.ForwardingTimeouts != nil {
responseHeaderTimeout = time.Duration(cfg.ForwardingTimeouts.ResponseHeaderTimeout)
}
tlsConfig, err := r.transportManager.GetTLSConfig(cfgName)
if err != nil {
return nil, fmt.Errorf("getting TLS config: %w", err)
}
pool := r.getPool(cfgName, cfg, tlsConfig, targetURL, proxyURL)
return NewReverseProxy(targetURL, proxyURL, r.debug, passHostHeader, responseHeaderTimeout, pool)
}
func (r *ProxyBuilder) getPool(cfgName string, config *dynamic.ServersTransport, tlsConfig *tls.Config, targetURL *url.URL, proxyURL *url.URL) *connPool {
pool, ok := r.pools[cfgName]
if !ok {
pool = make(map[string]*connPool)
r.pools[cfgName] = pool
}
if connPool, ok := pool[targetURL.String()]; ok {
return connPool
}
idleConnTimeout := 90 * time.Second
dialTimeout := 30 * time.Second
if config.ForwardingTimeouts != nil {
idleConnTimeout = time.Duration(config.ForwardingTimeouts.IdleConnTimeout)
dialTimeout = time.Duration(config.ForwardingTimeouts.DialTimeout)
}
proxyDialer := newDialer(dialerConfig{
DialKeepAlive: 0,
DialTimeout: dialTimeout,
HTTP: true,
TLS: targetURL.Scheme == "https",
ProxyURL: proxyURL,
}, tlsConfig)
connPool := newConnPool(config.MaxIdleConnsPerHost, idleConnTimeout, func() (net.Conn, error) {
return proxyDialer.Dial("tcp", addrFromURL(targetURL))
})
r.pools[cfgName][targetURL.String()] = connPool
return connPool
}

163
pkg/proxy/fast/connpool.go Normal file
View file

@ -0,0 +1,163 @@
package fast
import (
"fmt"
"net"
"time"
"github.com/rs/zerolog/log"
)
// conn is an enriched net.Conn.
type conn struct {
net.Conn
idleAt time.Time // the last time it was marked as idle.
idleTimeout time.Duration
}
func (c *conn) isExpired() bool {
expTime := c.idleAt.Add(c.idleTimeout)
return c.idleTimeout > 0 && time.Now().After(expTime)
}
// connPool is a net.Conn pool implementation using channels.
type connPool struct {
dialer func() (net.Conn, error)
idleConns chan *conn
idleConnTimeout time.Duration
ticker *time.Ticker
doneCh chan struct{}
}
// newConnPool creates a new connPool.
func newConnPool(maxIdleConn int, idleConnTimeout time.Duration, dialer func() (net.Conn, error)) *connPool {
c := &connPool{
dialer: dialer,
idleConns: make(chan *conn, maxIdleConn),
idleConnTimeout: idleConnTimeout,
doneCh: make(chan struct{}),
}
if idleConnTimeout > 0 {
c.ticker = time.NewTicker(c.idleConnTimeout / 2)
go func() {
for {
select {
case <-c.ticker.C:
c.cleanIdleConns()
case <-c.doneCh:
return
}
}
}()
}
return c
}
// Close closes stop the cleanIdleConn goroutine.
func (c *connPool) Close() {
if c.idleConnTimeout > 0 {
close(c.doneCh)
c.ticker.Stop()
}
}
// AcquireConn returns an idle net.Conn from the pool.
func (c *connPool) AcquireConn() (*conn, error) {
for {
co, err := c.acquireConn()
if err != nil {
return nil, err
}
if !co.isExpired() {
return co, nil
}
// As the acquired conn is expired we can close it
// without putting it again into the pool.
if err := co.Close(); err != nil {
log.Debug().
Err(err).
Msg("Unexpected error while releasing the connection")
}
}
}
// ReleaseConn releases the given net.Conn to the pool.
func (c *connPool) ReleaseConn(co *conn) {
co.idleAt = time.Now()
c.releaseConn(co)
}
// cleanIdleConns is a routine cleaning the expired connections at a regular basis.
func (c *connPool) cleanIdleConns() {
for {
select {
case co := <-c.idleConns:
if !co.isExpired() {
c.releaseConn(co)
return
}
if err := co.Close(); err != nil {
log.Debug().
Err(err).
Msg("Unexpected error while releasing the connection")
}
default:
return
}
}
}
func (c *connPool) acquireConn() (*conn, error) {
select {
case co := <-c.idleConns:
return co, nil
default:
errCh := make(chan error, 1)
go c.askForNewConn(errCh)
select {
case co := <-c.idleConns:
return co, nil
case err := <-errCh:
return nil, err
}
}
}
func (c *connPool) releaseConn(co *conn) {
select {
case c.idleConns <- co:
// Hitting the default case means that we have reached the maximum number of idle
// connections, so we can close it.
default:
if err := co.Close(); err != nil {
log.Debug().
Err(err).
Msg("Unexpected error while releasing the connection")
}
}
}
func (c *connPool) askForNewConn(errCh chan<- error) {
co, err := c.dialer()
if err != nil {
errCh <- fmt.Errorf("create conn: %w", err)
return
}
c.releaseConn(&conn{
Conn: co,
idleAt: time.Now(),
idleTimeout: c.idleConnTimeout,
})
}

View file

@ -0,0 +1,184 @@
package fast
import (
"net"
"runtime"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestConnPool_ConnReuse(t *testing.T) {
testCases := []struct {
desc string
poolFn func(pool *connPool)
expected int
}{
{
desc: "One connection",
poolFn: func(pool *connPool) {
c1, _ := pool.AcquireConn()
pool.ReleaseConn(c1)
},
expected: 1,
},
{
desc: "Two connections with release",
poolFn: func(pool *connPool) {
c1, _ := pool.AcquireConn()
pool.ReleaseConn(c1)
c2, _ := pool.AcquireConn()
pool.ReleaseConn(c2)
},
expected: 1,
},
{
desc: "Two concurrent connections",
poolFn: func(pool *connPool) {
c1, _ := pool.AcquireConn()
c2, _ := pool.AcquireConn()
pool.ReleaseConn(c1)
pool.ReleaseConn(c2)
},
expected: 2,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
var connAlloc int
dialer := func() (net.Conn, error) {
connAlloc++
return &net.TCPConn{}, nil
}
pool := newConnPool(2, 0, dialer)
test.poolFn(pool)
assert.Equal(t, test.expected, connAlloc)
})
}
}
func TestConnPool_MaxIdleConn(t *testing.T) {
testCases := []struct {
desc string
poolFn func(pool *connPool)
maxIdleConn int
expected int
}{
{
desc: "One connection",
poolFn: func(pool *connPool) {
c1, _ := pool.AcquireConn()
pool.ReleaseConn(c1)
},
maxIdleConn: 1,
expected: 1,
},
{
desc: "Multiple connections with defered release",
poolFn: func(pool *connPool) {
for range 7 {
c, _ := pool.AcquireConn()
defer pool.ReleaseConn(c)
}
},
maxIdleConn: 5,
expected: 5,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
var keepOpenedConn int
dialer := func() (net.Conn, error) {
keepOpenedConn++
return &mockConn{closeFn: func() error {
keepOpenedConn--
return nil
}}, nil
}
pool := newConnPool(test.maxIdleConn, 0, dialer)
test.poolFn(pool)
assert.Equal(t, test.expected, keepOpenedConn)
})
}
}
func TestGC(t *testing.T) {
var isDestroyed bool
pools := map[string]*connPool{}
dialer := func() (net.Conn, error) {
c := &mockConn{closeFn: func() error {
return nil
}}
return c, nil
}
pools["test"] = newConnPool(10, 1*time.Second, dialer)
runtime.SetFinalizer(pools["test"], func(p *connPool) {
isDestroyed = true
})
c, err := pools["test"].AcquireConn()
require.NoError(t, err)
pools["test"].ReleaseConn(c)
pools["test"].Close()
delete(pools, "test")
runtime.GC()
require.True(t, isDestroyed)
}
type mockConn struct {
closeFn func() error
}
func (m *mockConn) Read(_ []byte) (n int, err error) {
panic("implement me")
}
func (m *mockConn) Write(_ []byte) (n int, err error) {
panic("implement me")
}
func (m *mockConn) Close() error {
if m.closeFn != nil {
return m.closeFn()
}
return nil
}
func (m *mockConn) LocalAddr() net.Addr {
panic("implement me")
}
func (m *mockConn) RemoteAddr() net.Addr {
panic("implement me")
}
func (m *mockConn) SetDeadline(_ time.Time) error {
panic("implement me")
}
func (m *mockConn) SetReadDeadline(_ time.Time) error {
panic("implement me")
}
func (m *mockConn) SetWriteDeadline(_ time.Time) error {
panic("implement me")
}

195
pkg/proxy/fast/dialer.go Normal file
View file

@ -0,0 +1,195 @@
package fast
import (
"bufio"
"context"
"crypto/tls"
"encoding/base64"
"errors"
"net"
"net/http"
"net/url"
"strings"
"time"
"golang.org/x/net/proxy"
)
const (
schemeHTTP = "http"
schemeHTTPS = "https"
schemeSocks5 = "socks5"
)
type dialer interface {
Dial(network, addr string) (c net.Conn, err error)
}
type dialerFunc func(network, addr string) (net.Conn, error)
func (d dialerFunc) Dial(network, addr string) (net.Conn, error) {
return d(network, addr)
}
type dialerConfig struct {
DialKeepAlive time.Duration
DialTimeout time.Duration
ProxyURL *url.URL
HTTP bool
TLS bool
}
func newDialer(cfg dialerConfig, tlsConfig *tls.Config) dialer {
if cfg.ProxyURL == nil {
return buildDialer(cfg, tlsConfig, cfg.TLS)
}
proxyDialer := buildDialer(cfg, tlsConfig, cfg.ProxyURL.Scheme == "https")
proxyAddr := addrFromURL(cfg.ProxyURL)
switch {
case cfg.ProxyURL.Scheme == schemeSocks5:
var auth *proxy.Auth
if u := cfg.ProxyURL.User; u != nil {
auth = &proxy.Auth{User: u.Username()}
auth.Password, _ = u.Password()
}
// SOCKS5 implementation do not return errors.
socksDialer, _ := proxy.SOCKS5("tcp", proxyAddr, auth, proxyDialer)
return dialerFunc(func(network, targetAddr string) (net.Conn, error) {
co, err := socksDialer.Dial("tcp", targetAddr)
if err != nil {
return nil, err
}
if cfg.TLS {
c := &tls.Config{}
if tlsConfig != nil {
c = tlsConfig.Clone()
}
if c.ServerName == "" {
host, _, _ := net.SplitHostPort(targetAddr)
c.ServerName = host
}
return tls.Client(co, c), nil
}
return co, nil
})
case cfg.HTTP && !cfg.TLS:
// Nothing to do the Proxy-Authorization header will be added by the ReverseProxy.
default:
hdr := make(http.Header)
if u := cfg.ProxyURL.User; u != nil {
username := u.Username()
password, _ := u.Password()
auth := username + ":" + password
hdr.Set("Proxy-Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(auth)))
}
return dialerFunc(func(network, targetAddr string) (net.Conn, error) {
conn, err := proxyDialer.Dial("tcp", proxyAddr)
if err != nil {
return nil, err
}
connectReq := &http.Request{
Method: http.MethodConnect,
URL: &url.URL{Opaque: targetAddr},
Host: targetAddr,
Header: hdr,
}
connectCtx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
defer cancel()
didReadResponse := make(chan struct{}) // closed after CONNECT write+read is done or fails
var resp *http.Response
// Write the CONNECT request & read the response.
go func() {
defer close(didReadResponse)
err = connectReq.Write(conn)
if err != nil {
return
}
// Okay to use and discard buffered reader here, because
// TLS server will not speak until spoken to.
br := bufio.NewReader(conn)
resp, err = http.ReadResponse(br, connectReq)
}()
select {
case <-connectCtx.Done():
conn.Close()
<-didReadResponse
return nil, connectCtx.Err()
case <-didReadResponse:
// resp or err now set
}
if err != nil {
conn.Close()
return nil, err
}
if resp.StatusCode != http.StatusOK {
_, statusText, ok := strings.Cut(resp.Status, " ")
conn.Close()
if !ok {
return nil, errors.New("unknown status code")
}
return nil, errors.New(statusText)
}
c := &tls.Config{}
if tlsConfig != nil {
c = tlsConfig.Clone()
}
if c.ServerName == "" {
host, _, _ := net.SplitHostPort(targetAddr)
c.ServerName = host
}
return tls.Client(conn, c), nil
})
}
return dialerFunc(func(network, addr string) (net.Conn, error) {
return proxyDialer.Dial("tcp", proxyAddr)
})
}
func buildDialer(cfg dialerConfig, tlsConfig *tls.Config, isTLS bool) dialer {
dialer := &net.Dialer{
Timeout: cfg.DialTimeout,
KeepAlive: cfg.DialKeepAlive,
}
if !isTLS {
return dialer
}
return &tls.Dialer{
NetDialer: dialer,
Config: tlsConfig,
}
}
func addrFromURL(u *url.URL) string {
addr := u.Host
if u.Port() == "" {
switch u.Scheme {
case schemeHTTP:
return addr + ":80"
case schemeHTTPS:
return addr + ":443"
}
}
return addr
}

553
pkg/proxy/fast/proxy.go Normal file
View file

@ -0,0 +1,553 @@
package fast
import (
"bufio"
"bytes"
"encoding/base64"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/http/httptrace"
"net/http/httputil"
"net/url"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/rs/zerolog/log"
proxyhttputil "github.com/traefik/traefik/v3/pkg/proxy/httputil"
"github.com/valyala/fasthttp"
"golang.org/x/net/http/httpguts"
)
const (
bufferSize = 32 * 1024
bufioSize = 64 * 1024
)
var hopHeaders = []string{
"Connection",
"Proxy-Connection", // non-standard but still sent by libcurl and rejected by e.g. google
"Keep-Alive",
"Proxy-Authenticate",
"Proxy-Authorization",
"Te", // canonicalized version of "TE"
"Trailer", // not Trailers per URL above; https://www.rfc-editor.org/errata_search.php?eid=4522
"Transfer-Encoding",
"Upgrade",
}
type pool[T any] struct {
pool sync.Pool
}
func (p *pool[T]) Get() T {
if tmp := p.pool.Get(); tmp != nil {
return tmp.(T)
}
var res T
return res
}
func (p *pool[T]) Put(x T) {
p.pool.Put(x)
}
type buffConn struct {
*bufio.Reader
net.Conn
}
func (b buffConn) Read(p []byte) (int, error) {
return b.Reader.Read(p)
}
type writeDetector struct {
net.Conn
written bool
}
func (w *writeDetector) Write(p []byte) (int, error) {
n, err := w.Conn.Write(p)
if n > 0 {
w.written = true
}
return n, err
}
type writeFlusher struct {
io.Writer
}
func (w *writeFlusher) Write(b []byte) (int, error) {
n, err := w.Writer.Write(b)
if f, ok := w.Writer.(http.Flusher); ok {
f.Flush()
}
return n, err
}
type timeoutError struct {
error
}
func (t timeoutError) Timeout() bool {
return true
}
func (t timeoutError) Temporary() bool {
return false
}
// ReverseProxy is the FastProxy reverse proxy implementation.
type ReverseProxy struct {
debug bool
connPool *connPool
bufferPool pool[[]byte]
readerPool pool[*bufio.Reader]
writerPool pool[*bufio.Writer]
limitReaderPool pool[*io.LimitedReader]
proxyAuth string
targetURL *url.URL
passHostHeader bool
responseHeaderTimeout time.Duration
}
// NewReverseProxy creates a new ReverseProxy.
func NewReverseProxy(targetURL *url.URL, proxyURL *url.URL, debug, passHostHeader bool, responseHeaderTimeout time.Duration, connPool *connPool) (*ReverseProxy, error) {
var proxyAuth string
if proxyURL != nil && proxyURL.User != nil && targetURL.Scheme == "http" {
username := proxyURL.User.Username()
password, _ := proxyURL.User.Password()
proxyAuth = "Basic " + base64.StdEncoding.EncodeToString([]byte(username+":"+password))
}
return &ReverseProxy{
debug: debug,
passHostHeader: passHostHeader,
targetURL: targetURL,
proxyAuth: proxyAuth,
connPool: connPool,
responseHeaderTimeout: responseHeaderTimeout,
}, nil
}
func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if req.Body != nil {
defer req.Body.Close()
}
outReq := fasthttp.AcquireRequest()
defer fasthttp.ReleaseRequest(outReq)
// This is not required as the headers are already normalized by net/http.
outReq.Header.DisableNormalizing()
for k, v := range req.Header {
for _, s := range v {
outReq.Header.Add(k, s)
}
}
removeConnectionHeaders(&outReq.Header)
for _, header := range hopHeaders {
outReq.Header.Del(header)
}
if p.proxyAuth != "" {
outReq.Header.Set("Proxy-Authorization", p.proxyAuth)
}
if httpguts.HeaderValuesContainsToken(req.Header["Te"], "trailers") {
outReq.Header.Set("Te", "trailers")
}
if p.debug {
outReq.Header.Set("X-Traefik-Fast-Proxy", "enabled")
}
reqUpType := upgradeType(req.Header)
if !isGraphic(reqUpType) {
proxyhttputil.ErrorHandler(rw, req, fmt.Errorf("client tried to switch to invalid protocol %q", reqUpType))
return
}
if reqUpType != "" {
outReq.Header.Set("Connection", "Upgrade")
outReq.Header.Set("Upgrade", reqUpType)
if reqUpType == "websocket" {
cleanWebSocketHeaders(&outReq.Header)
}
}
u2 := new(url.URL)
*u2 = *req.URL
u2.Scheme = p.targetURL.Scheme
u2.Host = p.targetURL.Host
u := req.URL
if req.RequestURI != "" {
parsedURL, err := url.ParseRequestURI(req.RequestURI)
if err == nil {
u = parsedURL
}
}
u2.Path = u.Path
u2.RawPath = u.RawPath
u2.RawQuery = strings.ReplaceAll(u.RawQuery, ";", "&")
outReq.SetHost(u2.Host)
outReq.Header.SetHost(u2.Host)
if p.passHostHeader {
outReq.Header.SetHost(req.Host)
}
outReq.SetRequestURI(u2.RequestURI())
outReq.SetBodyStream(req.Body, int(req.ContentLength))
outReq.Header.SetMethod(req.Method)
if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
// If we aren't the first proxy retain prior
// X-Forwarded-For information as a comma+space
// separated list and fold multiple headers into one.
prior, ok := req.Header["X-Forwarded-For"]
if len(prior) > 0 {
clientIP = strings.Join(prior, ", ") + ", " + clientIP
}
omit := ok && prior == nil // Go Issue 38079: nil now means don't populate the header
if !omit {
outReq.Header.Set("X-Forwarded-For", clientIP)
}
}
if err := p.roundTrip(rw, req, outReq, reqUpType); err != nil {
proxyhttputil.ErrorHandler(rw, req, err)
}
}
// Note that unlike the net/http RoundTrip:
// - we are not supporting "100 Continue" response to forward them as-is to the client.
// - we are not asking for compressed response automatically. That is because this will add an extra cost when the
// client is asking for an uncompressed response, as we will have to un-compress it, and nowadays most clients are
// already asking for compressed response (allowing "passthrough" compression).
func (p *ReverseProxy) roundTrip(rw http.ResponseWriter, req *http.Request, outReq *fasthttp.Request, reqUpType string) error {
ctx := req.Context()
trace := httptrace.ContextClientTrace(ctx)
var co *conn
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
var err error
co, err = p.connPool.AcquireConn()
if err != nil {
return fmt.Errorf("acquire connection: %w", err)
}
wd := &writeDetector{Conn: co}
err = p.writeRequest(wd, outReq)
if wd.written && trace != nil && trace.WroteRequest != nil {
// WroteRequest hook is used by the tracing middleware to detect if the request has been written.
trace.WroteRequest(httptrace.WroteRequestInfo{})
}
if err == nil {
break
}
log.Ctx(ctx).Debug().Err(err).Msg("Error while writing request")
co.Close()
if wd.written && !isReplayable(req) {
return err
}
}
br := p.readerPool.Get()
if br == nil {
br = bufio.NewReaderSize(co, bufioSize)
}
defer p.readerPool.Put(br)
br.Reset(co)
res := fasthttp.AcquireResponse()
defer fasthttp.ReleaseResponse(res)
res.Header.SetNoDefaultContentType(true)
for {
var timer *time.Timer
errTimeout := atomic.Pointer[timeoutError]{}
if p.responseHeaderTimeout > 0 {
timer = time.AfterFunc(p.responseHeaderTimeout, func() {
errTimeout.Store(&timeoutError{errors.New("timeout awaiting response headers")})
co.Close()
})
}
res.Header.SetNoDefaultContentType(true)
if err := res.Header.Read(br); err != nil {
if p.responseHeaderTimeout > 0 {
if errT := errTimeout.Load(); errT != nil {
return errT
}
}
co.Close()
return err
}
if timer != nil {
timer.Stop()
}
fixPragmaCacheControl(&res.Header)
resCode := res.StatusCode()
is1xx := 100 <= resCode && resCode <= 199
// treat 101 as a terminal status, see issue 26161
is1xxNonTerminal := is1xx && resCode != http.StatusSwitchingProtocols
if is1xxNonTerminal {
removeConnectionHeaders(&res.Header)
h := rw.Header()
for _, header := range hopHeaders {
res.Header.Del(header)
}
res.Header.VisitAll(func(key, value []byte) {
rw.Header().Add(string(key), string(value))
})
rw.WriteHeader(res.StatusCode())
// Clear headers, it's not automatically done by ResponseWriter.WriteHeader() for 1xx responses
for k := range h {
delete(h, k)
}
res.Reset()
res.Header.Reset()
res.Header.SetNoDefaultContentType(true)
continue
}
break
}
announcedTrailers := res.Header.Peek("Trailer")
// Deal with 101 Switching Protocols responses: (WebSocket, h2c, etc)
if res.StatusCode() == http.StatusSwitchingProtocols {
// As the connection has been hijacked, it cannot be added back to the pool.
handleUpgradeResponse(rw, req, reqUpType, res, buffConn{Conn: co, Reader: br})
return nil
}
removeConnectionHeaders(&res.Header)
for _, header := range hopHeaders {
res.Header.Del(header)
}
if len(announcedTrailers) > 0 {
res.Header.Add("Trailer", string(announcedTrailers))
}
res.Header.VisitAll(func(key, value []byte) {
rw.Header().Add(string(key), string(value))
})
rw.WriteHeader(res.StatusCode())
// Chunked response, Content-Length is set to -1 by FastProxy when "Transfer-Encoding: chunked" header is received.
if res.Header.ContentLength() == -1 {
cbr := httputil.NewChunkedReader(br)
b := p.bufferPool.Get()
if b == nil {
b = make([]byte, bufferSize)
}
defer p.bufferPool.Put(b)
if _, err := io.CopyBuffer(&writeFlusher{rw}, cbr, b); err != nil {
co.Close()
return err
}
res.Header.Reset()
res.Header.SetNoDefaultContentType(true)
if err := res.Header.ReadTrailer(br); err != nil {
co.Close()
return err
}
if res.Header.Len() > 0 {
var announcedTrailersKey []string
if len(announcedTrailers) > 0 {
announcedTrailersKey = strings.Split(string(announcedTrailers), ",")
}
res.Header.VisitAll(func(key, value []byte) {
for _, s := range announcedTrailersKey {
if strings.EqualFold(s, strings.TrimSpace(string(key))) {
rw.Header().Add(string(key), string(value))
return
}
}
rw.Header().Add(http.TrailerPrefix+string(key), string(value))
})
}
p.connPool.ReleaseConn(co)
return nil
}
brl := p.limitReaderPool.Get()
if brl == nil {
brl = &io.LimitedReader{}
}
defer p.limitReaderPool.Put(brl)
brl.R = br
brl.N = int64(res.Header.ContentLength())
b := p.bufferPool.Get()
if b == nil {
b = make([]byte, bufferSize)
}
defer p.bufferPool.Put(b)
if _, err := io.CopyBuffer(rw, brl, b); err != nil {
co.Close()
return err
}
p.connPool.ReleaseConn(co)
return nil
}
func (p *ReverseProxy) writeRequest(co net.Conn, outReq *fasthttp.Request) error {
bw := p.writerPool.Get()
if bw == nil {
bw = bufio.NewWriterSize(co, bufioSize)
}
defer p.writerPool.Put(bw)
bw.Reset(co)
if err := outReq.Write(bw); err != nil {
return err
}
return bw.Flush()
}
// isReplayable returns whether the request is replayable.
func isReplayable(req *http.Request) bool {
if req.Body == nil || req.Body == http.NoBody {
switch req.Method {
case http.MethodGet, http.MethodHead, http.MethodOptions, http.MethodTrace:
return true
}
// The Idempotency-Key, while non-standard, is widely used to
// mean a POST or other request is idempotent. See
// https://golang.org/issue/19943#issuecomment-421092421
if _, ok := req.Header["Idempotency-Key"]; ok {
return true
}
if _, ok := req.Header["X-Idempotency-Key"]; ok {
return true
}
}
return false
}
// isGraphic returns whether s is ASCII and printable according to
// https://tools.ietf.org/html/rfc20#section-4.2.
func isGraphic(s string) bool {
for i := range len(s) {
if s[i] < ' ' || s[i] > '~' {
return false
}
}
return true
}
type fasthttpHeader interface {
Peek(key string) []byte
Set(key string, value string)
SetBytesV(key string, value []byte)
DelBytes(key []byte)
Del(key string)
}
// removeConnectionHeaders removes hop-by-hop headers listed in the "Connection" header of h.
// See RFC 7230, section 6.1.
func removeConnectionHeaders(h fasthttpHeader) {
f := h.Peek(fasthttp.HeaderConnection)
for _, sf := range bytes.Split(f, []byte{','}) {
if sf = bytes.TrimSpace(sf); len(sf) > 0 {
h.DelBytes(sf)
}
}
}
// RFC 7234, section 5.4: Should treat Pragma: no-cache like Cache-Control: no-cache.
func fixPragmaCacheControl(header fasthttpHeader) {
if pragma := header.Peek("Pragma"); bytes.Equal(pragma, []byte("no-cache")) {
if len(header.Peek("Cache-Control")) == 0 {
header.Set("Cache-Control", "no-cache")
}
}
}
// cleanWebSocketHeaders Even if the websocket RFC says that headers should be case-insensitive,
// some servers need Sec-WebSocket-Key, Sec-WebSocket-Extensions, Sec-WebSocket-Accept,
// Sec-WebSocket-Protocol and Sec-WebSocket-Version to be case-sensitive.
// https://tools.ietf.org/html/rfc6455#page-20
func cleanWebSocketHeaders(headers fasthttpHeader) {
headers.SetBytesV("Sec-WebSocket-Key", headers.Peek("Sec-Websocket-Key"))
headers.Del("Sec-Websocket-Key")
headers.SetBytesV("Sec-WebSocket-Extensions", headers.Peek("Sec-Websocket-Extensions"))
headers.Del("Sec-Websocket-Extensions")
headers.SetBytesV("Sec-WebSocket-Accept", headers.Peek("Sec-Websocket-Accept"))
headers.Del("Sec-Websocket-Accept")
headers.SetBytesV("Sec-WebSocket-Protocol", headers.Peek("Sec-Websocket-Protocol"))
headers.Del("Sec-Websocket-Protocol")
headers.SetBytesV("Sec-WebSocket-Version", headers.Peek("Sec-Websocket-Version"))
headers.DelBytes([]byte("Sec-Websocket-Version"))
}

View file

@ -0,0 +1,311 @@
package fast
import (
"crypto/tls"
"crypto/x509"
"encoding/base64"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
"net/http/httputil"
"net/url"
"testing"
"time"
"github.com/armon/go-socks5"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/config/static"
"github.com/traefik/traefik/v3/pkg/testhelpers"
"github.com/traefik/traefik/v3/pkg/tls/generate"
)
const (
proxyHTTP = "http"
proxyHTTPS = "https"
proxySocks5 = "socks"
)
type authCreds struct {
user string
password string
}
func TestProxyFromEnvironment(t *testing.T) {
testCases := []struct {
desc string
proxyType string
tls bool
auth *authCreds
}{
{
desc: "Proxy HTTP with HTTP Backend",
proxyType: proxyHTTP,
},
{
desc: "Proxy HTTP with HTTP backend and proxy auth",
proxyType: proxyHTTP,
tls: false,
auth: &authCreds{
user: "user",
password: "password",
},
},
{
desc: "Proxy HTTP with HTTPS backend",
proxyType: proxyHTTP,
tls: true,
},
{
desc: "Proxy HTTP with HTTPS backend and proxy auth",
proxyType: proxyHTTP,
tls: true,
auth: &authCreds{
user: "user",
password: "password",
},
},
{
desc: "Proxy HTTPS with HTTP backend",
proxyType: proxyHTTPS,
},
{
desc: "Proxy HTTPS with HTTP backend and proxy auth",
proxyType: proxyHTTPS,
tls: false,
auth: &authCreds{
user: "user",
password: "password",
},
},
{
desc: "Proxy HTTPS with HTTPS backend",
proxyType: proxyHTTPS,
tls: true,
},
{
desc: "Proxy HTTPS with HTTPS backend and proxy auth",
proxyType: proxyHTTPS,
tls: true,
auth: &authCreds{
user: "user",
password: "password",
},
},
{
desc: "Proxy Socks5 with HTTP backend",
proxyType: proxySocks5,
},
{
desc: "Proxy Socks5 with HTTP backend and proxy auth",
proxyType: proxySocks5,
auth: &authCreds{
user: "user",
password: "password",
},
},
{
desc: "Proxy Socks5 with HTTPS backend",
proxyType: proxySocks5,
tls: true,
},
{
desc: "Proxy Socks5 with HTTPS backend and proxy auth",
proxyType: proxySocks5,
tls: true,
auth: &authCreds{
user: "user",
password: "password",
},
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
backendURL, backendCert := newBackendServer(t, test.tls, http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
_, _ = rw.Write([]byte("backend"))
}))
var proxyCalled bool
proxyHandler := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
proxyCalled = true
if test.auth != nil {
proxyAuth := "Basic " + base64.StdEncoding.EncodeToString([]byte(test.auth.user+":"+test.auth.password))
require.Equal(t, proxyAuth, req.Header.Get("Proxy-Authorization"))
}
if req.Method != http.MethodConnect {
proxy := httputil.NewSingleHostReverseProxy(testhelpers.MustParseURL("http://" + req.Host))
proxy.ServeHTTP(rw, req)
return
}
// CONNECT method
conn, err := net.Dial("tcp", req.Host)
require.NoError(t, err)
hj, ok := rw.(http.Hijacker)
require.True(t, ok)
rw.WriteHeader(http.StatusOK)
connHj, _, err := hj.Hijack()
require.NoError(t, err)
go func() { _, _ = io.Copy(connHj, conn) }()
_, _ = io.Copy(conn, connHj)
})
var proxyURL string
var proxyCert *x509.Certificate
switch test.proxyType {
case proxySocks5:
ln, err := net.Listen("tcp", ":0")
require.NoError(t, err)
proxyURL = fmt.Sprintf("socks5://%s", ln.Addr())
go func() {
conn, err := ln.Accept()
require.NoError(t, err)
proxyCalled = true
conf := &socks5.Config{}
if test.auth != nil {
conf.Credentials = socks5.StaticCredentials{test.auth.user: test.auth.password}
}
server, err := socks5.New(conf)
require.NoError(t, err)
// We are not checking the error, because ServeConn is blocked until the client or the backend
// connection is closed which, in some cases, raises a connection reset by peer error.
_ = server.ServeConn(conn)
err = ln.Close()
require.NoError(t, err)
}()
case proxyHTTP:
proxyServer := httptest.NewServer(proxyHandler)
t.Cleanup(proxyServer.Close)
proxyURL = proxyServer.URL
case proxyHTTPS:
proxyServer := httptest.NewServer(proxyHandler)
t.Cleanup(proxyServer.Close)
proxyURL = proxyServer.URL
proxyCert = proxyServer.Certificate()
}
certPool := x509.NewCertPool()
if proxyCert != nil {
certPool.AddCert(proxyCert)
}
if backendCert != nil {
cert, err := x509.ParseCertificate(backendCert.Certificate[0])
require.NoError(t, err)
certPool.AddCert(cert)
}
builder := NewProxyBuilder(&transportManagerMock{tlsConfig: &tls.Config{RootCAs: certPool}}, static.FastProxyConfig{})
builder.proxy = func(req *http.Request) (*url.URL, error) {
u, err := url.Parse(proxyURL)
if err != nil {
return nil, err
}
if test.auth != nil {
u.User = url.UserPassword(test.auth.user, test.auth.password)
}
return u, nil
}
reverseProxy, err := builder.Build("foo", testhelpers.MustParseURL(backendURL), false)
require.NoError(t, err)
reverseProxyServer := httptest.NewServer(reverseProxy)
t.Cleanup(reverseProxyServer.Close)
client := http.Client{Timeout: 5 * time.Second}
resp, err := client.Get(reverseProxyServer.URL)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
assert.Equal(t, "backend", string(body))
assert.True(t, proxyCalled)
})
}
}
func newCertificate(t *testing.T, domain string) *tls.Certificate {
t.Helper()
certPEM, keyPEM, err := generate.KeyPair(domain, time.Time{})
require.NoError(t, err)
certificate, err := tls.X509KeyPair(certPEM, keyPEM)
require.NoError(t, err)
return &certificate
}
func newBackendServer(t *testing.T, isTLS bool, handler http.Handler) (string, *tls.Certificate) {
t.Helper()
var ln net.Listener
var err error
var cert *tls.Certificate
scheme := "http"
domain := "backend.localhost"
if isTLS {
scheme = "https"
cert = newCertificate(t, domain)
ln, err = tls.Listen("tcp", ":0", &tls.Config{Certificates: []tls.Certificate{*cert}})
require.NoError(t, err)
} else {
ln, err = net.Listen("tcp", ":0")
require.NoError(t, err)
}
srv := &http.Server{Handler: handler}
go func() { _ = srv.Serve(ln) }()
t.Cleanup(func() { _ = srv.Close() })
_, port, err := net.SplitHostPort(ln.Addr().String())
require.NoError(t, err)
backendURL := fmt.Sprintf("%s://%s:%s", scheme, domain, port)
return backendURL, cert
}
type transportManagerMock struct {
tlsConfig *tls.Config
}
func (r *transportManagerMock) GetTLSConfig(_ string) (*tls.Config, error) {
return r.tlsConfig, nil
}
func (r *transportManagerMock) Get(_ string) (*dynamic.ServersTransport, error) {
return &dynamic.ServersTransport{}, nil
}

View file

@ -0,0 +1,693 @@
package fast
import (
"bufio"
"crypto/tls"
"errors"
"fmt"
"net"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"time"
gorillawebsocket "github.com/gorilla/websocket"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v3/pkg/testhelpers"
"golang.org/x/net/websocket"
)
func TestWebSocketTCPClose(t *testing.T) {
errChan := make(chan error, 1)
upgrader := gorillawebsocket.Upgrader{}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
defer c.Close()
for {
_, _, err := c.ReadMessage()
if err != nil {
errChan <- err
break
}
}
}))
defer srv.Close()
proxy := createProxyWithForwarder(t, srv.URL, createConnectionPool(srv.URL, nil))
proxyAddr := proxy.Listener.Addr().String()
_, conn, err := newWebsocketRequest(
withServer(proxyAddr),
withPath("/ws"),
).open()
require.NoError(t, err)
conn.Close()
serverErr := <-errChan
var wsErr *gorillawebsocket.CloseError
require.ErrorAs(t, serverErr, &wsErr)
assert.Equal(t, 1006, wsErr.Code)
}
func TestWebSocketPingPong(t *testing.T) {
upgrader := gorillawebsocket.Upgrader{
HandshakeTimeout: 10 * time.Second,
CheckOrigin: func(*http.Request) bool {
return true
},
}
mux := http.NewServeMux()
mux.HandleFunc("/ws", func(writer http.ResponseWriter, request *http.Request) {
ws, err := upgrader.Upgrade(writer, request, nil)
require.NoError(t, err)
ws.SetPingHandler(func(appData string) error {
err = ws.WriteMessage(gorillawebsocket.PongMessage, []byte(appData+"Pong"))
require.NoError(t, err)
return nil
})
_, _, _ = ws.ReadMessage()
})
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
mux.ServeHTTP(w, req)
}))
defer srv.Close()
proxy := createProxyWithForwarder(t, srv.URL, createConnectionPool(srv.URL, nil))
serverAddr := proxy.Listener.Addr().String()
headers := http.Header{}
webSocketURL := "ws://" + serverAddr + "/ws"
headers.Add("Origin", webSocketURL)
conn, resp, err := gorillawebsocket.DefaultDialer.Dial(webSocketURL, headers)
require.NoError(t, err, "Error during Dial with response: %+v", resp)
defer conn.Close()
goodErr := fmt.Errorf("signal: %s", "Good data")
badErr := fmt.Errorf("signal: %s", "Bad data")
conn.SetPongHandler(func(data string) error {
if data == "PingPong" {
return goodErr
}
return badErr
})
err = conn.WriteControl(gorillawebsocket.PingMessage, []byte("Ping"), time.Now().Add(time.Second))
require.NoError(t, err)
_, _, err = conn.ReadMessage()
if !errors.Is(err, goodErr) {
require.NoError(t, err)
}
}
func TestWebSocketEcho(t *testing.T) {
mux := http.NewServeMux()
mux.Handle("/ws", websocket.Handler(func(conn *websocket.Conn) {
msg := make([]byte, 4)
n, err := conn.Read(msg)
require.NoError(t, err)
_, err = conn.Write(msg[:n])
require.NoError(t, err)
err = conn.Close()
require.NoError(t, err)
}))
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
mux.ServeHTTP(w, req)
}))
defer srv.Close()
proxy := createProxyWithForwarder(t, srv.URL, createConnectionPool(srv.URL, nil))
serverAddr := proxy.Listener.Addr().String()
headers := http.Header{}
webSocketURL := "ws://" + serverAddr + "/ws"
headers.Add("Origin", webSocketURL)
conn, resp, err := gorillawebsocket.DefaultDialer.Dial(webSocketURL, headers)
require.NoError(t, err, "Error during Dial with response: %+v", resp)
err = conn.WriteMessage(gorillawebsocket.TextMessage, []byte("OK"))
require.NoError(t, err)
_, msg, err := conn.ReadMessage()
require.NoError(t, err)
assert.Equal(t, "OK", string(msg))
err = conn.Close()
require.NoError(t, err)
}
func TestWebSocketPassHost(t *testing.T) {
testCases := []struct {
desc string
passHost bool
expected string
}{
{
desc: "PassHost false",
passHost: false,
},
{
desc: "PassHost true",
passHost: true,
expected: "example.com",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
mux := http.NewServeMux()
mux.Handle("/ws", websocket.Handler(func(conn *websocket.Conn) {
req := conn.Request()
if test.passHost {
require.Equal(t, test.expected, req.Host)
} else {
require.NotEqual(t, test.expected, req.Host)
}
msg := make([]byte, 4)
n, err := conn.Read(msg)
require.NoError(t, err)
_, err = conn.Write(msg[:n])
require.NoError(t, err)
err = conn.Close()
require.NoError(t, err)
}))
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
mux.ServeHTTP(w, req)
}))
defer srv.Close()
proxy := createProxyWithForwarder(t, srv.URL, createConnectionPool(srv.URL, nil))
serverAddr := proxy.Listener.Addr().String()
headers := http.Header{}
webSocketURL := "ws://" + serverAddr + "/ws"
headers.Add("Origin", webSocketURL)
headers.Add("Host", "example.com")
conn, resp, err := gorillawebsocket.DefaultDialer.Dial(webSocketURL, headers)
require.NoError(t, err, "Error during Dial with response: %+v", resp)
err = conn.WriteMessage(gorillawebsocket.TextMessage, []byte("OK"))
require.NoError(t, err)
_, msg, err := conn.ReadMessage()
require.NoError(t, err)
assert.Equal(t, "OK", string(msg))
err = conn.Close()
require.NoError(t, err)
})
}
}
func TestWebSocketServerWithoutCheckOrigin(t *testing.T) {
upgrader := gorillawebsocket.Upgrader{CheckOrigin: func(r *http.Request) bool {
return true
}}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
defer c.Close()
for {
mt, message, err := c.ReadMessage()
if err != nil {
break
}
err = c.WriteMessage(mt, message)
if err != nil {
break
}
}
}))
defer srv.Close()
proxy := createProxyWithForwarder(t, srv.URL, createConnectionPool(srv.URL, nil))
defer proxy.Close()
proxyAddr := proxy.Listener.Addr().String()
resp, err := newWebsocketRequest(
withServer(proxyAddr),
withPath("/ws"),
withData("ok"),
withOrigin("http://127.0.0.2"),
).send()
require.NoError(t, err)
assert.Equal(t, "ok", resp)
}
func TestWebSocketRequestWithOrigin(t *testing.T) {
upgrader := gorillawebsocket.Upgrader{}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
defer c.Close()
for {
mt, message, err := c.ReadMessage()
if err != nil {
break
}
err = c.WriteMessage(mt, message)
if err != nil {
break
}
}
}))
defer srv.Close()
proxy := createProxyWithForwarder(t, srv.URL, createConnectionPool(srv.URL, nil))
defer proxy.Close()
proxyAddr := proxy.Listener.Addr().String()
_, err := newWebsocketRequest(
withServer(proxyAddr),
withPath("/ws"),
withData("echo"),
withOrigin("http://127.0.0.2"),
).send()
require.EqualError(t, err, "bad status")
resp, err := newWebsocketRequest(
withServer(proxyAddr),
withPath("/ws"),
withData("ok"),
).send()
require.NoError(t, err)
assert.Equal(t, "ok", resp)
}
func TestWebSocketRequestWithQueryParams(t *testing.T) {
upgrader := gorillawebsocket.Upgrader{}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
defer conn.Close()
assert.Equal(t, "test", r.URL.Query().Get("query"))
for {
mt, message, err := conn.ReadMessage()
if err != nil {
break
}
err = conn.WriteMessage(mt, message)
if err != nil {
break
}
}
}))
defer srv.Close()
proxy := createProxyWithForwarder(t, srv.URL, createConnectionPool(srv.URL, nil))
defer proxy.Close()
proxyAddr := proxy.Listener.Addr().String()
resp, err := newWebsocketRequest(
withServer(proxyAddr),
withPath("/ws?query=test"),
withData("ok"),
).send()
require.NoError(t, err)
assert.Equal(t, "ok", resp)
}
func TestWebSocketRequestWithHeadersInResponseWriter(t *testing.T) {
mux := http.NewServeMux()
mux.Handle("/ws", websocket.Handler(func(conn *websocket.Conn) {
_ = conn.Close()
}))
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
mux.ServeHTTP(w, req)
}))
defer srv.Close()
u := parseURI(t, srv.URL)
f, err := NewReverseProxy(u, nil, true, false, 0, newConnPool(1, 0, func() (net.Conn, error) {
return net.Dial("tcp", u.Host)
}))
require.NoError(t, err)
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
req.URL = parseURI(t, srv.URL)
w.Header().Set("HEADER-KEY", "HEADER-VALUE")
f.ServeHTTP(w, req)
}))
defer proxy.Close()
serverAddr := proxy.Listener.Addr().String()
headers := http.Header{}
webSocketURL := "ws://" + serverAddr + "/ws"
headers.Add("Origin", webSocketURL)
conn, resp, err := gorillawebsocket.DefaultDialer.Dial(webSocketURL, headers)
require.NoError(t, err, "Error during Dial with response: %+v", err, resp)
defer conn.Close()
assert.Equal(t, "HEADER-VALUE", resp.Header.Get("HEADER-KEY"))
}
func TestWebSocketRequestWithEncodedChar(t *testing.T) {
upgrader := gorillawebsocket.Upgrader{}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
defer conn.Close()
assert.Equal(t, "/%3A%2F%2F", r.URL.EscapedPath())
for {
mt, message, err := conn.ReadMessage()
if err != nil {
break
}
err = conn.WriteMessage(mt, message)
if err != nil {
break
}
}
}))
defer srv.Close()
proxy := createProxyWithForwarder(t, srv.URL, createConnectionPool(srv.URL, nil))
defer proxy.Close()
proxyAddr := proxy.Listener.Addr().String()
resp, err := newWebsocketRequest(
withServer(proxyAddr),
withPath("/%3A%2F%2F"),
withData("ok"),
).send()
require.NoError(t, err)
assert.Equal(t, "ok", resp)
}
func TestWebSocketUpgradeFailed(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/ws", func(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(http.StatusBadRequest)
})
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
mux.ServeHTTP(w, req)
}))
defer srv.Close()
u := parseURI(t, srv.URL)
f, err := NewReverseProxy(u, nil, true, false, 0, newConnPool(1, 0, func() (net.Conn, error) {
return net.Dial("tcp", u.Host)
}))
require.NoError(t, err)
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
path := req.URL.Path // keep the original path
if path != "/ws" {
w.WriteHeader(http.StatusOK)
return
}
// Set new backend URL
req.URL = parseURI(t, srv.URL)
req.URL.Path = path
f.ServeHTTP(w, req)
}))
defer proxy.Close()
proxyAddr := proxy.Listener.Addr().String()
conn, err := net.DialTimeout("tcp", proxyAddr, dialTimeout)
require.NoError(t, err)
defer conn.Close()
req, err := http.NewRequest(http.MethodGet, "ws://127.0.0.1/ws", nil)
require.NoError(t, err)
req.Header.Add("upgrade", "websocket")
req.Header.Add("Connection", "upgrade")
err = req.Write(conn)
require.NoError(t, err)
// First request works with 400
br := bufio.NewReader(conn)
resp, err := http.ReadResponse(br, req)
require.NoError(t, err)
assert.Equal(t, 400, resp.StatusCode)
}
func TestForwardsWebsocketTraffic(t *testing.T) {
mux := http.NewServeMux()
mux.Handle("/ws", websocket.Handler(func(conn *websocket.Conn) {
_, err := conn.Write([]byte("ok"))
require.NoError(t, err)
err = conn.Close()
require.NoError(t, err)
}))
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
mux.ServeHTTP(w, req)
}))
defer srv.Close()
proxy := createProxyWithForwarder(t, srv.URL, createConnectionPool(srv.URL, nil))
defer proxy.Close()
proxyAddr := proxy.Listener.Addr().String()
resp, err := newWebsocketRequest(
withServer(proxyAddr),
withPath("/ws"),
withData("echo"),
).send()
require.NoError(t, err)
assert.Equal(t, "ok", resp)
}
func createTLSWebsocketServer() *httptest.Server {
upgrader := gorillawebsocket.Upgrader{}
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
defer conn.Close()
for {
mt, message, err := conn.ReadMessage()
if err != nil {
break
}
err = conn.WriteMessage(mt, message)
if err != nil {
break
}
}
}))
return srv
}
func TestWebSocketTransferTLSConfig(t *testing.T) {
srv := createTLSWebsocketServer()
defer srv.Close()
proxyWithoutTLSConfig := createProxyWithForwarder(t, srv.URL, createConnectionPool(srv.URL, nil))
defer proxyWithoutTLSConfig.Close()
proxyAddr := proxyWithoutTLSConfig.Listener.Addr().String()
_, err := newWebsocketRequest(
withServer(proxyAddr),
withPath("/ws"),
withData("ok"),
).send()
require.EqualError(t, err, "bad status")
pool := createConnectionPool(srv.URL, &tls.Config{InsecureSkipVerify: true})
proxyWithTLSConfig := createProxyWithForwarder(t, srv.URL, pool)
defer proxyWithTLSConfig.Close()
proxyAddr = proxyWithTLSConfig.Listener.Addr().String()
resp, err := newWebsocketRequest(
withServer(proxyAddr),
withPath("/ws"),
withData("ok"),
).send()
require.NoError(t, err)
assert.Equal(t, "ok", resp)
}
const dialTimeout = time.Second
type websocketRequestOpt func(w *websocketRequest)
func withServer(server string) websocketRequestOpt {
return func(w *websocketRequest) {
w.ServerAddr = server
}
}
func withPath(path string) websocketRequestOpt {
return func(w *websocketRequest) {
w.Path = path
}
}
func withData(data string) websocketRequestOpt {
return func(w *websocketRequest) {
w.Data = data
}
}
func withOrigin(origin string) websocketRequestOpt {
return func(w *websocketRequest) {
w.Origin = origin
}
}
func newWebsocketRequest(opts ...websocketRequestOpt) *websocketRequest {
wsrequest := &websocketRequest{}
for _, opt := range opts {
opt(wsrequest)
}
if wsrequest.Origin == "" {
wsrequest.Origin = "http://" + wsrequest.ServerAddr
}
if wsrequest.Config == nil {
wsrequest.Config, _ = websocket.NewConfig(fmt.Sprintf("ws://%s%s", wsrequest.ServerAddr, wsrequest.Path), wsrequest.Origin)
}
return wsrequest
}
type websocketRequest struct {
ServerAddr string
Path string
Data string
Origin string
Config *websocket.Config
}
func (w *websocketRequest) send() (string, error) {
conn, _, err := w.open()
if err != nil {
return "", err
}
defer conn.Close()
if _, err := conn.Write([]byte(w.Data)); err != nil {
return "", err
}
msg := make([]byte, 512)
var n int
n, err = conn.Read(msg)
if err != nil {
return "", err
}
received := string(msg[:n])
return received, nil
}
func (w *websocketRequest) open() (*websocket.Conn, net.Conn, error) {
client, err := net.DialTimeout("tcp", w.ServerAddr, dialTimeout)
if err != nil {
return nil, nil, err
}
conn, err := websocket.NewClient(w.Config, client)
if err != nil {
return nil, nil, err
}
return conn, client, err
}
func parseURI(t *testing.T, uri string) *url.URL {
t.Helper()
out, err := url.ParseRequestURI(uri)
require.NoError(t, err)
return out
}
func createConnectionPool(target string, tlsConfig *tls.Config) *connPool {
u := testhelpers.MustParseURL(target)
return newConnPool(200, 0, func() (net.Conn, error) {
if tlsConfig != nil {
return tls.Dial("tcp", u.Host, tlsConfig)
}
return net.Dial("tcp", u.Host)
})
}
func createProxyWithForwarder(t *testing.T, uri string, pool *connPool) *httptest.Server {
t.Helper()
u := parseURI(t, uri)
proxy, err := NewReverseProxy(u, nil, false, true, 0, pool)
require.NoError(t, err)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
path := req.URL.Path // keep the original path
// Set new backend URL
req.URL = u
req.URL.Path = path
proxy.ServeHTTP(w, req)
}))
t.Cleanup(srv.Close)
return srv
}

104
pkg/proxy/fast/upgrade.go Normal file
View file

@ -0,0 +1,104 @@
package fast
import (
"bytes"
"fmt"
"io"
"net"
"net/http"
"strings"
"github.com/traefik/traefik/v3/pkg/proxy/httputil"
"github.com/valyala/fasthttp"
"golang.org/x/net/http/httpguts"
)
// switchProtocolCopier exists so goroutines proxying data back and
// forth have nice names in stacks.
type switchProtocolCopier struct {
user, backend io.ReadWriter
}
func (c switchProtocolCopier) copyFromBackend(errc chan<- error) {
_, err := io.Copy(c.user, c.backend)
errc <- err
}
func (c switchProtocolCopier) copyToBackend(errc chan<- error) {
_, err := io.Copy(c.backend, c.user)
errc <- err
}
func handleUpgradeResponse(rw http.ResponseWriter, req *http.Request, reqUpType string, res *fasthttp.Response, backConn net.Conn) {
defer backConn.Close()
resUpType := upgradeTypeFastHTTP(&res.Header)
if !strings.EqualFold(reqUpType, resUpType) {
httputil.ErrorHandler(rw, req, fmt.Errorf("backend tried to switch protocol %q when %q was requested", resUpType, reqUpType))
return
}
hj, ok := rw.(http.Hijacker)
if !ok {
httputil.ErrorHandler(rw, req, fmt.Errorf("can't switch protocols using non-Hijacker ResponseWriter type %T", rw))
return
}
backConnCloseCh := make(chan bool)
go func() {
// Ensure that the cancellation of a request closes the backend.
// See issue https://golang.org/issue/35559.
select {
case <-req.Context().Done():
case <-backConnCloseCh:
}
_ = backConn.Close()
}()
defer close(backConnCloseCh)
conn, brw, err := hj.Hijack()
if err != nil {
httputil.ErrorHandler(rw, req, fmt.Errorf("hijack failed on protocol switch: %w", err))
return
}
defer conn.Close()
for k, values := range rw.Header() {
for _, v := range values {
res.Header.Add(k, v)
}
}
if err := res.Header.Write(brw.Writer); err != nil {
httputil.ErrorHandler(rw, req, fmt.Errorf("response write: %w", err))
return
}
if err := brw.Flush(); err != nil {
httputil.ErrorHandler(rw, req, fmt.Errorf("response flush: %w", err))
return
}
errc := make(chan error, 1)
spc := switchProtocolCopier{user: conn, backend: backConn}
go spc.copyToBackend(errc)
go spc.copyFromBackend(errc)
<-errc
}
func upgradeType(h http.Header) string {
if !httpguts.HeaderValuesContainsToken(h["Connection"], "Upgrade") {
return ""
}
return h.Get("Upgrade")
}
func upgradeTypeFastHTTP(h fasthttpHeader) string {
if !bytes.Contains(h.Peek("Connection"), []byte("Upgrade")) {
return ""
}
return string(h.Peek("Upgrade"))
}

View file

@ -1,23 +1,25 @@
package service
package httputil
import "sync"
const bufferPoolSize = 32 * 1024
func newBufferPool() *bufferPool {
return &bufferPool{
pool: sync.Pool{
New: func() interface{} {
return make([]byte, bufferPoolSize)
},
},
}
}
const bufferSize = 32 * 1024
type bufferPool struct {
pool sync.Pool
}
func newBufferPool() *bufferPool {
b := &bufferPool{
pool: sync.Pool{},
}
b.pool.New = func() interface{} {
return make([]byte, bufferSize)
}
return b
}
func (b *bufferPool) Get() []byte {
return b.pool.Get().([]byte)
}

View file

@ -0,0 +1,54 @@
package httputil
import (
"crypto/tls"
"fmt"
"net/http"
"net/url"
"time"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/metrics"
)
// TransportManager manages transport used for backend communications.
type TransportManager interface {
Get(name string) (*dynamic.ServersTransport, error)
GetRoundTripper(name string) (http.RoundTripper, error)
GetTLSConfig(name string) (*tls.Config, error)
}
// ProxyBuilder handles the http.RoundTripper for httputil reverse proxies.
type ProxyBuilder struct {
bufferPool *bufferPool
transportManager TransportManager
semConvMetricsRegistry *metrics.SemConvMetricsRegistry
}
// NewProxyBuilder creates a new ProxyBuilder.
func NewProxyBuilder(transportManager TransportManager, semConvMetricsRegistry *metrics.SemConvMetricsRegistry) *ProxyBuilder {
return &ProxyBuilder{
bufferPool: newBufferPool(),
transportManager: transportManager,
semConvMetricsRegistry: semConvMetricsRegistry,
}
}
// Update does nothing.
func (r *ProxyBuilder) Update(_ map[string]*dynamic.ServersTransport) {}
// Build builds a new httputil.ReverseProxy with the given configuration.
func (r *ProxyBuilder) Build(cfgName string, targetURL *url.URL, shouldObserve, passHostHeader bool, flushInterval time.Duration) (http.Handler, error) {
roundTripper, err := r.transportManager.GetRoundTripper(cfgName)
if err != nil {
return nil, fmt.Errorf("getting RoundTripper: %w", err)
}
if shouldObserve {
// Wrapping the roundTripper with the Tracing roundTripper,
// to handle the reverseProxy client span creation.
roundTripper = newObservabilityRoundTripper(r.semConvMetricsRegistry, roundTripper)
}
return buildSingleHostProxy(targetURL, passHostHeader, flushInterval, roundTripper, r.bufferPool), nil
}

View file

@ -0,0 +1,56 @@
package httputil
import (
"crypto/tls"
"errors"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/testhelpers"
)
func TestEscapedPath(t *testing.T) {
var gotEscapedPath string
srv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
gotEscapedPath = req.URL.EscapedPath()
}))
transportManager := &transportManagerMock{
roundTrippers: map[string]http.RoundTripper{"default": &http.Transport{}},
}
p, err := NewProxyBuilder(transportManager, nil).Build("default", testhelpers.MustParseURL(srv.URL), false, true, 0)
require.NoError(t, err)
proxy := httptest.NewServer(http.HandlerFunc(p.ServeHTTP))
_, err = http.Get(proxy.URL + "/%3A%2F%2F")
require.NoError(t, err)
assert.Equal(t, "/%3A%2F%2F", gotEscapedPath)
}
type transportManagerMock struct {
roundTrippers map[string]http.RoundTripper
}
func (t *transportManagerMock) GetRoundTripper(name string) (http.RoundTripper, error) {
roundTripper, ok := t.roundTrippers[name]
if !ok {
return nil, errors.New("no transport for " + name)
}
return roundTripper, nil
}
func (t *transportManagerMock) GetTLSConfig(_ string) (*tls.Config, error) {
panic("implement me")
}
func (t *transportManagerMock) Get(_ string) (*dynamic.ServersTransport, error) {
panic("implement me")
}

View file

@ -1,4 +1,4 @@
package service
package httputil
import (
"context"
@ -23,6 +23,13 @@ type wrapper struct {
rt http.RoundTripper
}
func newObservabilityRoundTripper(semConvMetricRegistry *metrics.SemConvMetricsRegistry, rt http.RoundTripper) http.RoundTripper {
return &wrapper{
semConvMetricRegistry: semConvMetricRegistry,
rt: rt,
}
}
func (t *wrapper) RoundTrip(req *http.Request) (*http.Response, error) {
start := time.Now()
var span trace.Span
@ -42,7 +49,7 @@ func (t *wrapper) RoundTrip(req *http.Request) (*http.Response, error) {
var headers http.Header
response, err := t.rt.RoundTrip(req)
if err != nil {
statusCode = computeStatusCode(err)
statusCode = ComputeStatusCode(err)
}
if response != nil {
statusCode = response.StatusCode
@ -96,10 +103,3 @@ func (t *wrapper) RoundTrip(req *http.Request) (*http.Response, error) {
return response, err
}
func newObservabilityRoundTripper(semConvMetricRegistry *metrics.SemConvMetricsRegistry, rt http.RoundTripper) http.RoundTripper {
return &wrapper{
semConvMetricRegistry: semConvMetricRegistry,
rt: rt,
}
}

View file

@ -1,4 +1,4 @@
package service
package httputil
import (
"context"

View file

@ -1,4 +1,4 @@
package service
package httputil
import (
"context"
@ -27,7 +27,7 @@ func buildSingleHostProxy(target *url.URL, passHostHeader bool, flushInterval ti
Transport: roundTripper,
FlushInterval: flushInterval,
BufferPool: bufferPool,
ErrorHandler: errorHandler,
ErrorHandler: ErrorHandler,
}
}
@ -93,8 +93,9 @@ func isWebSocketUpgrade(req *http.Request) bool {
strings.EqualFold(req.Header.Get("Upgrade"), "websocket")
}
func errorHandler(w http.ResponseWriter, req *http.Request, err error) {
statusCode := computeStatusCode(err)
// ErrorHandler is the http.Handler called when something goes wrong when forwarding the request.
func ErrorHandler(w http.ResponseWriter, req *http.Request, err error) {
statusCode := ComputeStatusCode(err)
logger := log.Ctx(req.Context())
logger.Debug().Err(err).Msgf("%d %s", statusCode, statusText(statusCode))
@ -105,7 +106,8 @@ func errorHandler(w http.ResponseWriter, req *http.Request, err error) {
}
}
func computeStatusCode(err error) int {
// ComputeStatusCode computes the HTTP status code according to the given error.
func ComputeStatusCode(err error) int {
switch {
case errors.Is(err, io.EOF):
return http.StatusBadGateway

View file

@ -1,4 +1,4 @@
package service
package httputil
import (
"bufio"
@ -8,13 +8,13 @@ import (
"net"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"time"
gorillawebsocket "github.com/gorilla/websocket"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v3/pkg/testhelpers"
"golang.org/x/net/websocket"
)
@ -27,6 +27,7 @@ func TestWebSocketTCPClose(t *testing.T) {
return
}
defer c.Close()
for {
_, _, err := c.ReadMessage()
if err != nil {
@ -71,6 +72,7 @@ func TestWebSocketPingPong(t *testing.T) {
ws.SetPingHandler(func(appData string) error {
err = ws.WriteMessage(gorillawebsocket.PongMessage, []byte(appData+"Pong"))
require.NoError(t, err)
return nil
})
@ -97,6 +99,7 @@ func TestWebSocketPingPong(t *testing.T) {
if data == "PingPong" {
return goodErr
}
return badErr
})
@ -104,7 +107,6 @@ func TestWebSocketPingPong(t *testing.T) {
require.NoError(t, err)
_, _, err = conn.ReadMessage()
if !errors.Is(err, goodErr) {
require.NoError(t, err)
}
@ -114,12 +116,10 @@ func TestWebSocketEcho(t *testing.T) {
mux := http.NewServeMux()
mux.Handle("/ws", websocket.Handler(func(conn *websocket.Conn) {
msg := make([]byte, 4)
_, err := conn.Read(msg)
n, err := conn.Read(msg)
require.NoError(t, err)
fmt.Println(string(msg))
_, err = conn.Write(msg)
_, err = conn.Write(msg[:n])
require.NoError(t, err)
err = conn.Close()
@ -142,7 +142,10 @@ func TestWebSocketEcho(t *testing.T) {
err = conn.WriteMessage(gorillawebsocket.TextMessage, []byte("OK"))
require.NoError(t, err)
fmt.Println(conn.ReadMessage())
_, msg, err := conn.ReadMessage()
require.NoError(t, err)
assert.Equal(t, "OK", string(msg))
err = conn.Close()
require.NoError(t, err)
@ -178,11 +181,10 @@ func TestWebSocketPassHost(t *testing.T) {
}
msg := make([]byte, 4)
_, err := conn.Read(msg)
n, err := conn.Read(msg)
require.NoError(t, err)
fmt.Println(string(msg))
_, err = conn.Write(msg)
_, err = conn.Write(msg[:n])
require.NoError(t, err)
err = conn.Close()
@ -207,7 +209,10 @@ func TestWebSocketPassHost(t *testing.T) {
err = conn.WriteMessage(gorillawebsocket.TextMessage, []byte("OK"))
require.NoError(t, err)
fmt.Println(conn.ReadMessage())
_, msg, err := conn.ReadMessage()
require.NoError(t, err)
assert.Equal(t, "OK", string(msg))
err = conn.Close()
require.NoError(t, err)
@ -216,27 +221,8 @@ func TestWebSocketPassHost(t *testing.T) {
}
func TestWebSocketServerWithoutCheckOrigin(t *testing.T) {
upgrader := gorillawebsocket.Upgrader{CheckOrigin: func(r *http.Request) bool {
return true
}}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
defer c.Close()
for {
mt, message, err := c.ReadMessage()
if err != nil {
break
}
err = c.WriteMessage(mt, message)
if err != nil {
break
}
}
}))
defer srv.Close()
upgrader := gorillawebsocket.Upgrader{CheckOrigin: func(*http.Request) bool { return true }}
srv := createServer(t, upgrader, func(*http.Request) {})
proxy := createProxyWithForwarder(t, srv.URL, http.DefaultTransport)
defer proxy.Close()
@ -254,25 +240,7 @@ func TestWebSocketServerWithoutCheckOrigin(t *testing.T) {
}
func TestWebSocketRequestWithOrigin(t *testing.T) {
upgrader := gorillawebsocket.Upgrader{}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
defer c.Close()
for {
mt, message, err := c.ReadMessage()
if err != nil {
break
}
err = c.WriteMessage(mt, message)
if err != nil {
break
}
}
}))
defer srv.Close()
srv := createServer(t, gorillawebsocket.Upgrader{}, func(*http.Request) {})
proxy := createProxyWithForwarder(t, srv.URL, http.DefaultTransport)
defer proxy.Close()
@ -297,26 +265,9 @@ func TestWebSocketRequestWithOrigin(t *testing.T) {
}
func TestWebSocketRequestWithQueryParams(t *testing.T) {
upgrader := gorillawebsocket.Upgrader{}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
defer conn.Close()
srv := createServer(t, gorillawebsocket.Upgrader{}, func(r *http.Request) {
assert.Equal(t, "test", r.URL.Query().Get("query"))
for {
mt, message, err := conn.ReadMessage()
if err != nil {
break
}
err = conn.WriteMessage(mt, message)
if err != nil {
break
}
}
}))
defer srv.Close()
})
proxy := createProxyWithForwarder(t, srv.URL, http.DefaultTransport)
defer proxy.Close()
@ -341,11 +292,19 @@ func TestWebSocketRequestWithHeadersInResponseWriter(t *testing.T) {
srv := httptest.NewServer(mux)
defer srv.Close()
f := buildSingleHostProxy(parseURI(t, srv.URL), true, 0, http.DefaultTransport, nil)
transportManager := &transportManagerMock{
roundTrippers: map[string]http.RoundTripper{
"default@internal": &http.Transport{},
},
}
p, err := NewProxyBuilder(transportManager, nil).Build("default@internal", testhelpers.MustParseURL(srv.URL), false, true, 0)
require.NoError(t, err)
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
req.URL = parseURI(t, srv.URL)
req.URL = testhelpers.MustParseURL(srv.URL)
w.Header().Set("HEADER-KEY", "HEADER-VALUE")
f.ServeHTTP(w, req)
p.ServeHTTP(w, req)
}))
defer proxy.Close()
@ -363,26 +322,9 @@ func TestWebSocketRequestWithHeadersInResponseWriter(t *testing.T) {
}
func TestWebSocketRequestWithEncodedChar(t *testing.T) {
upgrader := gorillawebsocket.Upgrader{}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
defer conn.Close()
srv := createServer(t, gorillawebsocket.Upgrader{}, func(r *http.Request) {
assert.Equal(t, "/%3A%2F%2F", r.URL.EscapedPath())
for {
mt, message, err := conn.ReadMessage()
if err != nil {
break
}
err = conn.WriteMessage(mt, message)
if err != nil {
break
}
}
}))
defer srv.Close()
})
proxy := createProxyWithForwarder(t, srv.URL, http.DefaultTransport)
defer proxy.Close()
@ -407,15 +349,23 @@ func TestWebSocketUpgradeFailed(t *testing.T) {
srv := httptest.NewServer(mux)
defer srv.Close()
f := buildSingleHostProxy(parseURI(t, srv.URL), true, 0, http.DefaultTransport, nil)
transportManager := &transportManagerMock{
roundTrippers: map[string]http.RoundTripper{
"default@internal": &http.Transport{},
},
}
p, err := NewProxyBuilder(transportManager, nil).Build("default@internal", testhelpers.MustParseURL(srv.URL), false, true, 0)
require.NoError(t, err)
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
path := req.URL.Path // keep the original path
if path == "/ws" {
// Set new backend URL
req.URL = parseURI(t, srv.URL)
req.URL = testhelpers.MustParseURL(srv.URL)
req.URL.Path = path
f.ServeHTTP(w, req)
p.ServeHTTP(w, req)
} else {
w.WriteHeader(http.StatusOK)
}
@ -629,27 +579,60 @@ func (w *websocketRequest) open() (*websocket.Conn, net.Conn, error) {
return conn, client, err
}
func parseURI(t *testing.T, uri string) *url.URL {
t.Helper()
out, err := url.ParseRequestURI(uri)
require.NoError(t, err)
return out
}
func createProxyWithForwarder(t *testing.T, uri string, transport http.RoundTripper) *httptest.Server {
t.Helper()
u := parseURI(t, uri)
proxy := buildSingleHostProxy(u, true, 0, transport, nil)
u := testhelpers.MustParseURL(uri)
transportManager := &transportManagerMock{
roundTrippers: map[string]http.RoundTripper{"fwd": transport},
}
p, err := NewProxyBuilder(transportManager, nil).Build("fwd", u, false, true, 0)
require.NoError(t, err)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
path := req.URL.Path // keep the original path
// keep the original path
path := req.URL.Path
// Set new backend URL
req.URL = u
req.URL.Path = path
proxy.ServeHTTP(w, req)
p.ServeHTTP(w, req)
}))
t.Cleanup(srv.Close)
return srv
}
func createServer(t *testing.T, upgrader gorillawebsocket.Upgrader, check func(*http.Request)) *httptest.Server {
t.Helper()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
t.Logf("Error during upgrade: %v", err)
return
}
defer conn.Close()
check(r)
for {
mt, message, err := conn.ReadMessage()
if err != nil {
t.Logf("Error during read: %v", err)
break
}
err = conn.WriteMessage(mt, message)
if err != nil {
t.Logf("Error during write: %v", err)
break
}
}
}))
t.Cleanup(srv.Close)
return srv
}

View file

@ -0,0 +1,61 @@
package proxy
import (
"crypto/tls"
"fmt"
"net/http"
"net/url"
"time"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/config/static"
"github.com/traefik/traefik/v3/pkg/proxy/fast"
"github.com/traefik/traefik/v3/pkg/proxy/httputil"
"github.com/traefik/traefik/v3/pkg/server/service"
)
// TransportManager manages transport used for backend communications.
type TransportManager interface {
Get(name string) (*dynamic.ServersTransport, error)
GetRoundTripper(name string) (http.RoundTripper, error)
GetTLSConfig(name string) (*tls.Config, error)
}
// SmartBuilder is a proxy builder which returns a fast proxy or httputil proxy corresponding
// to the ServersTransport configuration.
type SmartBuilder struct {
fastProxyBuilder *fast.ProxyBuilder
proxyBuilder service.ProxyBuilder
transportManager httputil.TransportManager
}
// NewSmartBuilder creates and returns a new SmartBuilder instance.
func NewSmartBuilder(transportManager TransportManager, proxyBuilder service.ProxyBuilder, fastProxyConfig static.FastProxyConfig) *SmartBuilder {
return &SmartBuilder{
fastProxyBuilder: fast.NewProxyBuilder(transportManager, fastProxyConfig),
proxyBuilder: proxyBuilder,
transportManager: transportManager,
}
}
// Update is the handler called when the dynamic configuration is updated.
func (b *SmartBuilder) Update(newConfigs map[string]*dynamic.ServersTransport) {
b.fastProxyBuilder.Update(newConfigs)
}
// Build builds an HTTP proxy for the given URL using the ServersTransport with the given name.
func (b *SmartBuilder) Build(configName string, targetURL *url.URL, shouldObserve, passHostHeader bool, flushInterval time.Duration) (http.Handler, error) {
serversTransport, err := b.transportManager.Get(configName)
if err != nil {
return nil, fmt.Errorf("getting ServersTransport: %w", err)
}
// The fast proxy implementation cannot handle HTTP/2 requests for now.
// For the https scheme we cannot guess if the backend communication will use HTTP2,
// thus we check if HTTP/2 is disabled to use the fast proxy implementation when this is possible.
if targetURL.Scheme == "h2c" || (targetURL.Scheme == "https" && !serversTransport.DisableHTTP2) {
return b.proxyBuilder.Build(configName, targetURL, shouldObserve, passHostHeader, flushInterval)
}
return b.fastProxyBuilder.Build(configName, targetURL, passHostHeader)
}

View file

@ -0,0 +1,113 @@
package proxy
import (
"encoding/pem"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/config/static"
"github.com/traefik/traefik/v3/pkg/proxy/httputil"
"github.com/traefik/traefik/v3/pkg/server/service"
"github.com/traefik/traefik/v3/pkg/testhelpers"
"github.com/traefik/traefik/v3/pkg/types"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
)
func TestSmartBuilder_Build(t *testing.T) {
tests := []struct {
desc string
serversTransport dynamic.ServersTransport
fastProxyConfig static.FastProxyConfig
https bool
h2c bool
wantFastProxy bool
}{
{
desc: "fastproxy",
fastProxyConfig: static.FastProxyConfig{Debug: true},
wantFastProxy: true,
},
{
desc: "fastproxy with https and without DisableHTTP2",
https: true,
fastProxyConfig: static.FastProxyConfig{Debug: true},
wantFastProxy: false,
},
{
desc: "fastproxy with https and DisableHTTP2",
https: true,
serversTransport: dynamic.ServersTransport{DisableHTTP2: true},
fastProxyConfig: static.FastProxyConfig{Debug: true},
wantFastProxy: true,
},
{
desc: "fastproxy with h2c",
h2c: true,
fastProxyConfig: static.FastProxyConfig{Debug: true},
wantFastProxy: false,
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
var callCount int
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
callCount++
if test.wantFastProxy {
assert.Contains(t, r.Header, "X-Traefik-Fast-Proxy")
} else {
assert.NotContains(t, r.Header, "X-Traefik-Fast-Proxy")
}
})
var server *httptest.Server
if test.https {
server = httptest.NewUnstartedServer(handler)
server.EnableHTTP2 = false
server.StartTLS()
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: server.TLS.Certificates[0].Certificate[0]})
test.serversTransport.RootCAs = []types.FileOrContent{
types.FileOrContent(certPEM),
}
} else {
server = httptest.NewServer(h2c.NewHandler(handler, &http2.Server{}))
}
t.Cleanup(func() {
server.Close()
})
targetURL := testhelpers.MustParseURL(server.URL)
if test.h2c {
targetURL.Scheme = "h2c"
}
serversTransports := map[string]*dynamic.ServersTransport{
"test": &test.serversTransport,
}
transportManager := service.NewTransportManager(nil)
transportManager.Update(serversTransports)
httpProxyBuilder := httputil.NewProxyBuilder(transportManager, nil)
proxyBuilder := NewSmartBuilder(transportManager, httpProxyBuilder, test.fastProxyConfig)
proxyHandler, err := proxyBuilder.Build("test", targetURL, false, false, time.Second)
require.NoError(t, err)
rw := httptest.NewRecorder()
proxyHandler.ServeHTTP(rw, httptest.NewRequest(http.MethodGet, "/", http.NoBody))
assert.Equal(t, 1, callCount)
})
}
}

View file

@ -2,10 +2,12 @@ package router
import (
"context"
"crypto/tls"
"io"
"math"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
@ -18,7 +20,7 @@ import (
"github.com/traefik/traefik/v3/pkg/server/middleware"
"github.com/traefik/traefik/v3/pkg/server/service"
"github.com/traefik/traefik/v3/pkg/testhelpers"
"github.com/traefik/traefik/v3/pkg/tls"
traefiktls "github.com/traefik/traefik/v3/pkg/tls"
)
func TestRouterManager_Get(t *testing.T) {
@ -309,11 +311,12 @@ func TestRouterManager_Get(t *testing.T) {
},
})
roundTripperManager := service.NewRoundTripperManager(nil)
roundTripperManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
serviceManager := service.NewManager(rtConf.Services, nil, nil, roundTripperManager)
transportManager := service.NewTransportManager(nil)
transportManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
serviceManager := service.NewManager(rtConf.Services, nil, nil, transportManager, proxyBuilderMock{})
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil)
tlsManager := tls.NewManager()
tlsManager := traefiktls.NewManager()
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager)
@ -340,7 +343,7 @@ func TestRuntimeConfiguration(t *testing.T) {
serviceConfig map[string]*dynamic.Service
routerConfig map[string]*dynamic.Router
middlewareConfig map[string]*dynamic.Middleware
tlsOptions map[string]tls.Options
tlsOptions map[string]traefiktls.Options
expectedError int
}{
{
@ -597,7 +600,7 @@ func TestRuntimeConfiguration(t *testing.T) {
TLS: &dynamic.RouterTLSConfig{},
},
},
tlsOptions: map[string]tls.Options{},
tlsOptions: map[string]traefiktls.Options{},
expectedError: 1,
},
{
@ -624,9 +627,9 @@ func TestRuntimeConfiguration(t *testing.T) {
},
},
},
tlsOptions: map[string]tls.Options{
tlsOptions: map[string]traefiktls.Options{
"broken-tlsOption": {
ClientAuth: tls.ClientAuth{
ClientAuth: traefiktls.ClientAuth{
ClientAuthType: "foobar",
},
},
@ -655,9 +658,9 @@ func TestRuntimeConfiguration(t *testing.T) {
TLS: &dynamic.RouterTLSConfig{},
},
},
tlsOptions: map[string]tls.Options{
tlsOptions: map[string]traefiktls.Options{
"default": {
ClientAuth: tls.ClientAuth{
ClientAuth: traefiktls.ClientAuth{
ClientAuthType: "foobar",
},
},
@ -682,11 +685,12 @@ func TestRuntimeConfiguration(t *testing.T) {
},
})
roundTripperManager := service.NewRoundTripperManager(nil)
roundTripperManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
serviceManager := service.NewManager(rtConf.Services, nil, nil, roundTripperManager)
transportManager := service.NewTransportManager(nil)
transportManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
serviceManager := service.NewManager(rtConf.Services, nil, nil, transportManager, proxyBuilderMock{})
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil)
tlsManager := tls.NewManager()
tlsManager := traefiktls.NewManager()
tlsManager.UpdateConfigs(context.Background(), nil, test.tlsOptions, nil)
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager)
@ -759,11 +763,12 @@ func TestProviderOnMiddlewares(t *testing.T) {
},
})
roundTripperManager := service.NewRoundTripperManager(nil)
roundTripperManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
serviceManager := service.NewManager(rtConf.Services, nil, nil, roundTripperManager)
transportManager := service.NewTransportManager(nil)
transportManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
serviceManager := service.NewManager(rtConf.Services, nil, nil, transportManager, nil)
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil)
tlsManager := tls.NewManager()
tlsManager := traefiktls.NewManager()
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager)
@ -775,14 +780,22 @@ func TestProviderOnMiddlewares(t *testing.T) {
assert.Equal(t, []string{"m1@docker", "m2@docker", "m1@file"}, rtConf.Middlewares["chain@docker"].Chain.Middlewares)
}
type staticRoundTripperGetter struct {
type staticTransportManager struct {
res *http.Response
}
func (s staticRoundTripperGetter) Get(name string) (http.RoundTripper, error) {
func (s staticTransportManager) GetRoundTripper(_ string) (http.RoundTripper, error) {
return &staticTransport{res: s.res}, nil
}
func (s staticTransportManager) GetTLSConfig(_ string) (*tls.Config, error) {
panic("implement me")
}
func (s staticTransportManager) Get(_ string) (*dynamic.ServersTransport, error) {
panic("implement me")
}
type staticTransport struct {
res *http.Response
}
@ -829,9 +842,9 @@ func BenchmarkRouterServe(b *testing.B) {
},
})
serviceManager := service.NewManager(rtConf.Services, nil, nil, staticRoundTripperGetter{res})
serviceManager := service.NewManager(rtConf.Services, nil, nil, staticTransportManager{res}, nil)
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil)
tlsManager := tls.NewManager()
tlsManager := traefiktls.NewManager()
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager)
@ -871,7 +884,7 @@ func BenchmarkService(b *testing.B) {
},
})
serviceManager := service.NewManager(rtConf.Services, nil, nil, staticRoundTripperGetter{res})
serviceManager := service.NewManager(rtConf.Services, nil, nil, staticTransportManager{res}, nil)
w := httptest.NewRecorder()
req := testhelpers.MustNewRequest(http.MethodGet, "http://foo.bar/", nil)
@ -881,3 +894,13 @@ func BenchmarkService(b *testing.B) {
handler.ServeHTTP(w, req)
}
}
type proxyBuilderMock struct{}
func (p proxyBuilderMock) Build(_ string, _ *url.URL, _, _ bool, _ time.Duration) (http.Handler, error) {
return http.HandlerFunc(func(responseWriter http.ResponseWriter, req *http.Request) {}), nil
}
func (p proxyBuilderMock) Update(_ map[string]*dynamic.ServersTransport) {
panic("implement me")
}

View file

@ -3,7 +3,9 @@ package server
import (
"net/http"
"net/http/httptest"
"net/url"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
@ -48,9 +50,10 @@ func TestReuseService(t *testing.T) {
),
)
roundTripperManager := service.NewRoundTripperManager(nil)
roundTripperManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
managerFactory := service.NewManagerFactory(staticConfig, nil, nil, roundTripperManager, nil)
transportManager := service.NewTransportManager(nil)
transportManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
managerFactory := service.NewManagerFactory(staticConfig, nil, nil, transportManager, proxyBuilderMock{}, nil)
tlsManager := tls.NewManager()
dialerManager := tcp.NewDialerManager(nil)
@ -184,9 +187,10 @@ func TestServerResponseEmptyBackend(t *testing.T) {
},
}
roundTripperManager := service.NewRoundTripperManager(nil)
roundTripperManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
managerFactory := service.NewManagerFactory(staticConfig, nil, nil, roundTripperManager, nil)
transportManager := service.NewTransportManager(nil)
transportManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
managerFactory := service.NewManagerFactory(staticConfig, nil, nil, transportManager, proxyBuilderMock{}, nil)
tlsManager := tls.NewManager()
dialerManager := tcp.NewDialerManager(nil)
@ -228,9 +232,10 @@ func TestInternalServices(t *testing.T) {
),
)
roundTripperManager := service.NewRoundTripperManager(nil)
roundTripperManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
managerFactory := service.NewManagerFactory(staticConfig, nil, nil, roundTripperManager, nil)
transportManager := service.NewTransportManager(nil)
transportManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
managerFactory := service.NewManagerFactory(staticConfig, nil, nil, transportManager, nil, nil)
tlsManager := tls.NewManager()
dialerManager := tcp.NewDialerManager(nil)
@ -246,3 +251,13 @@ func TestInternalServices(t *testing.T) {
assert.Equal(t, http.StatusOK, responseRecorderOk.Result().StatusCode, "status code")
}
type proxyBuilderMock struct{}
func (p proxyBuilderMock) Build(_ string, _ *url.URL, _, _ bool, _ time.Duration) (http.Handler, error) {
return http.HandlerFunc(func(responseWriter http.ResponseWriter, req *http.Request) {}), nil
}
func (p proxyBuilderMock) Update(_ map[string]*dynamic.ServersTransport) {
panic("implement me")
}

View file

@ -17,7 +17,8 @@ import (
type ManagerFactory struct {
observabilityMgr *middleware.ObservabilityMgr
roundTripperManager *RoundTripperManager
transportManager *TransportManager
proxyBuilder ProxyBuilder
api func(configuration *runtime.Configuration) http.Handler
restHandler http.Handler
@ -30,12 +31,13 @@ type ManagerFactory struct {
}
// NewManagerFactory creates a new ManagerFactory.
func NewManagerFactory(staticConfiguration static.Configuration, routinesPool *safe.Pool, observabilityMgr *middleware.ObservabilityMgr, roundTripperManager *RoundTripperManager, acmeHTTPHandler http.Handler) *ManagerFactory {
func NewManagerFactory(staticConfiguration static.Configuration, routinesPool *safe.Pool, observabilityMgr *middleware.ObservabilityMgr, transportManager *TransportManager, proxyBuilder ProxyBuilder, acmeHTTPHandler http.Handler) *ManagerFactory {
factory := &ManagerFactory{
observabilityMgr: observabilityMgr,
routinesPool: routinesPool,
roundTripperManager: roundTripperManager,
acmeHTTPHandler: acmeHTTPHandler,
observabilityMgr: observabilityMgr,
routinesPool: routinesPool,
transportManager: transportManager,
proxyBuilder: proxyBuilder,
acmeHTTPHandler: acmeHTTPHandler,
}
if staticConfiguration.API != nil {
@ -73,7 +75,7 @@ func NewManagerFactory(staticConfiguration static.Configuration, routinesPool *s
// Build creates a service manager.
func (f *ManagerFactory) Build(configuration *runtime.Configuration) *InternalHandlers {
svcManager := NewManager(configuration.Services, f.observabilityMgr, f.routinesPool, f.roundTripperManager)
svcManager := NewManager(configuration.Services, f.observabilityMgr, f.routinesPool, f.transportManager, f.proxyBuilder)
var apiHandler http.Handler
if f.api != nil {

View file

@ -1,37 +0,0 @@
package service
import (
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/traefik/traefik/v3/pkg/testhelpers"
)
type staticTransport struct {
res *http.Response
}
func (t *staticTransport) RoundTrip(r *http.Request) (*http.Response, error) {
return t.res, nil
}
func BenchmarkProxy(b *testing.B) {
res := &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader("")),
}
w := httptest.NewRecorder()
req := testhelpers.MustNewRequest(http.MethodGet, "http://foo.bar/", nil)
pool := newBufferPool()
handler := buildSingleHostProxy(req.URL, false, 0, &staticTransport{res}, pool)
b.ReportAllocs()
for range b.N {
handler.ServeHTTP(w, req)
}
}

View file

@ -9,7 +9,6 @@ import (
"hash/fnv"
"math/rand"
"net/http"
"net/http/httputil"
"net/url"
"reflect"
"strings"
@ -25,6 +24,7 @@ import (
"github.com/traefik/traefik/v3/pkg/middlewares/capture"
metricsMiddle "github.com/traefik/traefik/v3/pkg/middlewares/metrics"
"github.com/traefik/traefik/v3/pkg/middlewares/observability"
"github.com/traefik/traefik/v3/pkg/proxy/httputil"
"github.com/traefik/traefik/v3/pkg/safe"
"github.com/traefik/traefik/v3/pkg/server/cookie"
"github.com/traefik/traefik/v3/pkg/server/middleware"
@ -40,17 +40,18 @@ const (
defaultMaxBodySize int64 = -1
)
// RoundTripperGetter is a roundtripper getter interface.
type RoundTripperGetter interface {
Get(name string) (http.RoundTripper, error)
// ProxyBuilder builds reverse proxy handlers.
type ProxyBuilder interface {
Build(cfgName string, targetURL *url.URL, shouldObserve, passHostHeader bool, flushInterval time.Duration) (http.Handler, error)
Update(configs map[string]*dynamic.ServersTransport)
}
// Manager The service manager.
type Manager struct {
routinePool *safe.Pool
observabilityMgr *middleware.ObservabilityMgr
bufferPool httputil.BufferPool
roundTripperManager RoundTripperGetter
routinePool *safe.Pool
observabilityMgr *middleware.ObservabilityMgr
transportManager httputil.TransportManager
proxyBuilder ProxyBuilder
services map[string]http.Handler
configs map[string]*runtime.ServiceInfo
@ -59,16 +60,16 @@ type Manager struct {
}
// NewManager creates a new Manager.
func NewManager(configs map[string]*runtime.ServiceInfo, observabilityMgr *middleware.ObservabilityMgr, routinePool *safe.Pool, roundTripperManager RoundTripperGetter) *Manager {
func NewManager(configs map[string]*runtime.ServiceInfo, observabilityMgr *middleware.ObservabilityMgr, routinePool *safe.Pool, transportManager httputil.TransportManager, proxyBuilder ProxyBuilder) *Manager {
return &Manager{
routinePool: routinePool,
observabilityMgr: observabilityMgr,
bufferPool: newBufferPool(),
roundTripperManager: roundTripperManager,
services: make(map[string]http.Handler),
configs: configs,
healthCheckers: make(map[string]*healthcheck.ServiceHealthChecker),
rand: rand.New(rand.NewSource(time.Now().UnixNano())),
routinePool: routinePool,
observabilityMgr: observabilityMgr,
transportManager: transportManager,
proxyBuilder: proxyBuilder,
services: make(map[string]http.Handler),
configs: configs,
healthCheckers: make(map[string]*healthcheck.ServiceHealthChecker),
rand: rand.New(rand.NewSource(time.Now().UnixNano())),
}
}
@ -298,9 +299,9 @@ func (m *Manager) getLoadBalancerServiceHandler(ctx context.Context, serviceName
logger.Debug().Msg("Creating load-balancer")
// TODO: should we keep this config value as Go is now handling stream response correctly?
flushInterval := dynamic.DefaultFlushInterval
flushInterval := time.Duration(dynamic.DefaultFlushInterval)
if service.ResponseForwarding != nil {
flushInterval = service.ResponseForwarding.FlushInterval
flushInterval = time.Duration(service.ResponseForwarding.FlushInterval)
}
if len(service.ServersTransport) > 0 {
@ -317,11 +318,6 @@ func (m *Manager) getLoadBalancerServiceHandler(ctx context.Context, serviceName
passHostHeader = *service.PassHostHeader
}
roundTripper, err := m.roundTripperManager.Get(service.ServersTransport)
if err != nil {
return nil, err
}
lb := wrr.New(service.Sticky, service.HealthCheck != nil)
healthCheckTargets := make(map[string]*url.URL)
@ -341,14 +337,12 @@ func (m *Manager) getLoadBalancerServiceHandler(ctx context.Context, serviceName
qualifiedSvcName := provider.GetQualifiedName(ctx, serviceName)
if m.observabilityMgr.ShouldAddTracing(qualifiedSvcName) || m.observabilityMgr.ShouldAddMetrics(qualifiedSvcName) {
// Wrapping the roundTripper with the Tracing roundTripper,
// to handle the reverseProxy client span creation.
roundTripper = newObservabilityRoundTripper(m.observabilityMgr.SemConvMetricsRegistry(), roundTripper)
shouldObserve := m.observabilityMgr.ShouldAddTracing(qualifiedSvcName) || m.observabilityMgr.ShouldAddMetrics(qualifiedSvcName)
proxy, err := m.proxyBuilder.Build(service.ServersTransport, target, shouldObserve, passHostHeader, flushInterval)
if err != nil {
return nil, fmt.Errorf("error building proxy for server URL %s: %w", server.URL, err)
}
proxy := buildSingleHostProxy(target, passHostHeader, time.Duration(flushInterval), roundTripper, m.bufferPool)
// Prevents from enabling observability for internal resources.
if m.observabilityMgr.ShouldAddAccessLogs(qualifiedSvcName) {
@ -393,6 +387,11 @@ func (m *Manager) getLoadBalancerServiceHandler(ctx context.Context, serviceName
}
if service.HealthCheck != nil {
roundTripper, err := m.transportManager.GetRoundTripper(service.ServersTransport)
if err != nil {
return nil, fmt.Errorf("getting RoundTripper: %w", err)
}
m.healthCheckers[serviceName] = healthcheck.NewServiceHealthChecker(
ctx,
m.observabilityMgr.MetricsRegistry(),

View file

@ -2,6 +2,7 @@ package service
import (
"context"
"crypto/tls"
"io"
"net/http"
"net/http/httptest"
@ -14,13 +15,14 @@ import (
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/config/runtime"
"github.com/traefik/traefik/v3/pkg/proxy/httputil"
"github.com/traefik/traefik/v3/pkg/server/provider"
"github.com/traefik/traefik/v3/pkg/testhelpers"
)
func TestGetLoadBalancer(t *testing.T) {
sm := Manager{
roundTripperManager: newRtMock(),
transportManager: &transportManagerMock{},
}
testCases := []struct {
@ -40,14 +42,14 @@ func TestGetLoadBalancer(t *testing.T) {
},
},
},
fwd: &MockForwarder{},
fwd: &forwarderMock{},
expectError: true,
},
{
desc: "Succeeds when there are no servers",
serviceName: "test",
service: &dynamic.ServersLoadBalancer{},
fwd: &MockForwarder{},
fwd: &forwarderMock{},
expectError: false,
},
{
@ -56,7 +58,7 @@ func TestGetLoadBalancer(t *testing.T) {
service: &dynamic.ServersLoadBalancer{
Sticky: &dynamic.Sticky{Cookie: &dynamic.Cookie{}},
},
fwd: &MockForwarder{},
fwd: &forwarderMock{},
expectError: false,
},
}
@ -79,11 +81,8 @@ func TestGetLoadBalancer(t *testing.T) {
}
func TestGetLoadBalancerServiceHandler(t *testing.T) {
sm := NewManager(nil, nil, nil, &RoundTripperManager{
roundTrippers: map[string]http.RoundTripper{
"default@internal": http.DefaultTransport,
},
})
pb := httputil.NewProxyBuilder(&transportManagerMock{}, nil)
sm := NewManager(nil, nil, nil, transportManagerMock{}, pb)
server1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-From", "first")
@ -139,7 +138,7 @@ func TestGetLoadBalancerServiceHandler(t *testing.T) {
desc: "Load balances between the two servers",
serviceName: "test",
service: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true),
PassHostHeader: boolPtr(true),
Servers: []dynamic.Server{
{
URL: server1.URL,
@ -254,7 +253,7 @@ func TestGetLoadBalancerServiceHandler(t *testing.T) {
desc: "PassHost doesn't pass the host instead of the IP",
serviceName: "test",
service: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(false),
PassHostHeader: boolPtr(false),
Sticky: &dynamic.Sticky{Cookie: &dynamic.Cookie{}},
Servers: []dynamic.Server{
{
@ -359,11 +358,8 @@ func TestGetLoadBalancerServiceHandler(t *testing.T) {
// This test is an adapted version of net/http/httputil.Test1xxResponses test.
func Test1xxResponses(t *testing.T) {
sm := NewManager(nil, nil, nil, &RoundTripperManager{
roundTrippers: map[string]http.RoundTripper{
"default@internal": http.DefaultTransport,
},
})
pb := httputil.NewProxyBuilder(&transportManagerMock{}, nil)
sm := NewManager(nil, nil, nil, &transportManagerMock{}, pb)
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h := w.Header()
@ -499,11 +495,7 @@ func TestManager_Build(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
manager := NewManager(test.configs, nil, nil, &RoundTripperManager{
roundTrippers: map[string]http.RoundTripper{
"default@internal": http.DefaultTransport,
},
})
manager := NewManager(test.configs, nil, nil, &transportManagerMock{}, nil)
ctx := context.Background()
if len(test.providerName) > 0 {
@ -526,30 +518,30 @@ func TestMultipleTypeOnBuildHTTP(t *testing.T) {
},
}
manager := NewManager(services, nil, nil, &RoundTripperManager{
roundTrippers: map[string]http.RoundTripper{
"default@internal": http.DefaultTransport,
},
})
manager := NewManager(services, nil, nil, &transportManagerMock{}, nil)
_, err := manager.BuildHTTP(context.Background(), "test@file")
assert.Error(t, err, "cannot create service: multi-types service not supported, consider declaring two different pieces of service instead")
}
func Bool(v bool) *bool { return &v }
func boolPtr(v bool) *bool { return &v }
type MockForwarder struct{}
type forwarderMock struct{}
func (MockForwarder) ServeHTTP(http.ResponseWriter, *http.Request) {
func (forwarderMock) ServeHTTP(http.ResponseWriter, *http.Request) {
panic("not available")
}
type rtMock struct{}
type transportManagerMock struct{}
func newRtMock() RoundTripperGetter {
return &rtMock{}
func (t transportManagerMock) GetRoundTripper(_ string) (http.RoundTripper, error) {
return &http.Transport{}, nil
}
func (r *rtMock) Get(_ string) (http.RoundTripper, error) {
return http.DefaultTransport, nil
func (t transportManagerMock) GetTLSConfig(_ string) (*tls.Config, error) {
return nil, nil
}
func (t transportManagerMock) Get(_ string) (*dynamic.ServersTransport, error) {
return &dynamic.ServersTransport{}, nil
}

View file

@ -11,6 +11,15 @@ import (
"golang.org/x/net/http2"
)
type h2cTransportWrapper struct {
*http2.Transport
}
func (t *h2cTransportWrapper) RoundTrip(req *http.Request) (*http.Response, error) {
req.URL.Scheme = "http"
return t.Transport.RoundTrip(req)
}
func newSmartRoundTripper(transport *http.Transport, forwardingTimeouts *dynamic.ForwardingTimeouts) (*smartRoundTripper, error) {
transportHTTP1 := transport.Clone()

View file

@ -22,52 +22,45 @@ import (
"github.com/traefik/traefik/v3/pkg/config/dynamic"
traefiktls "github.com/traefik/traefik/v3/pkg/tls"
"github.com/traefik/traefik/v3/pkg/types"
"golang.org/x/net/http2"
)
type h2cTransportWrapper struct {
*http2.Transport
}
func (t *h2cTransportWrapper) RoundTrip(req *http.Request) (*http.Response, error) {
req.URL.Scheme = "http"
return t.Transport.RoundTrip(req)
}
// SpiffeX509Source allows to retrieve a x509 SVID and bundle.
type SpiffeX509Source interface {
x509svid.Source
x509bundle.Source
}
// NewRoundTripperManager creates a new RoundTripperManager.
func NewRoundTripperManager(spiffeX509Source SpiffeX509Source) *RoundTripperManager {
return &RoundTripperManager{
roundTrippers: make(map[string]http.RoundTripper),
configs: make(map[string]*dynamic.ServersTransport),
spiffeX509Source: spiffeX509Source,
}
}
// RoundTripperManager handles roundtripper for the reverse proxy.
type RoundTripperManager struct {
// TransportManager handles transports for backend communication.
type TransportManager struct {
rtLock sync.RWMutex
roundTrippers map[string]http.RoundTripper
configs map[string]*dynamic.ServersTransport
tlsConfigs map[string]*tls.Config
spiffeX509Source SpiffeX509Source
}
// Update updates the roundtrippers configurations.
func (r *RoundTripperManager) Update(newConfigs map[string]*dynamic.ServersTransport) {
r.rtLock.Lock()
defer r.rtLock.Unlock()
// NewTransportManager creates a new TransportManager.
func NewTransportManager(spiffeX509Source SpiffeX509Source) *TransportManager {
return &TransportManager{
roundTrippers: make(map[string]http.RoundTripper),
configs: make(map[string]*dynamic.ServersTransport),
tlsConfigs: make(map[string]*tls.Config),
spiffeX509Source: spiffeX509Source,
}
}
for configName, config := range r.configs {
// Update updates the transport configurations.
func (t *TransportManager) Update(newConfigs map[string]*dynamic.ServersTransport) {
t.rtLock.Lock()
defer t.rtLock.Unlock()
for configName, config := range t.configs {
newConfig, ok := newConfigs[configName]
if !ok {
delete(r.configs, configName)
delete(r.roundTrippers, configName)
delete(t.configs, configName)
delete(t.roundTrippers, configName)
delete(t.tlsConfigs, configName)
continue
}
@ -76,50 +69,133 @@ func (r *RoundTripperManager) Update(newConfigs map[string]*dynamic.ServersTrans
}
var err error
r.roundTrippers[configName], err = r.createRoundTripper(newConfig)
var tlsConfig *tls.Config
if tlsConfig, err = t.createTLSConfig(newConfig); err != nil {
log.Error().Err(err).Msgf("Could not configure HTTP Transport %s TLS configuration, fallback on default TLS config", configName)
}
t.tlsConfigs[configName] = tlsConfig
t.roundTrippers[configName], err = t.createRoundTripper(newConfig, tlsConfig)
if err != nil {
log.Error().Err(err).Msgf("Could not configure HTTP Transport %s, fallback on default transport", configName)
r.roundTrippers[configName] = http.DefaultTransport
t.roundTrippers[configName] = http.DefaultTransport
}
}
for newConfigName, newConfig := range newConfigs {
if _, ok := r.configs[newConfigName]; ok {
if _, ok := t.configs[newConfigName]; ok {
continue
}
var err error
r.roundTrippers[newConfigName], err = r.createRoundTripper(newConfig)
var tlsConfig *tls.Config
if tlsConfig, err = t.createTLSConfig(newConfig); err != nil {
log.Error().Err(err).Msgf("Could not configure HTTP Transport %s TLS configuration, fallback on default TLS config", newConfigName)
}
t.tlsConfigs[newConfigName] = tlsConfig
t.roundTrippers[newConfigName], err = t.createRoundTripper(newConfig, tlsConfig)
if err != nil {
log.Error().Err(err).Msgf("Could not configure HTTP Transport %s, fallback on default transport", newConfigName)
r.roundTrippers[newConfigName] = http.DefaultTransport
t.roundTrippers[newConfigName] = http.DefaultTransport
}
}
r.configs = newConfigs
t.configs = newConfigs
}
// Get gets a roundtripper by name.
func (r *RoundTripperManager) Get(name string) (http.RoundTripper, error) {
// GetRoundTripper gets a roundtripper corresponding to the given transport name.
func (t *TransportManager) GetRoundTripper(name string) (http.RoundTripper, error) {
if len(name) == 0 {
name = "default@internal"
}
r.rtLock.RLock()
defer r.rtLock.RUnlock()
t.rtLock.RLock()
defer t.rtLock.RUnlock()
if rt, ok := r.roundTrippers[name]; ok {
if rt, ok := t.roundTrippers[name]; ok {
return rt, nil
}
return nil, fmt.Errorf("servers transport not found %s", name)
}
// Get gets transport by name.
func (t *TransportManager) Get(name string) (*dynamic.ServersTransport, error) {
if len(name) == 0 {
name = "default@internal"
}
t.rtLock.RLock()
defer t.rtLock.RUnlock()
if rt, ok := t.configs[name]; ok {
return rt, nil
}
return nil, fmt.Errorf("servers transport not found %s", name)
}
// GetTLSConfig gets a TLS config corresponding to the given transport name.
func (t *TransportManager) GetTLSConfig(name string) (*tls.Config, error) {
if len(name) == 0 {
name = "default@internal"
}
t.rtLock.RLock()
defer t.rtLock.RUnlock()
if rt, ok := t.tlsConfigs[name]; ok {
return rt, nil
}
return nil, fmt.Errorf("tls config not found %s", name)
}
func (t *TransportManager) createTLSConfig(cfg *dynamic.ServersTransport) (*tls.Config, error) {
var config *tls.Config
if cfg.Spiffe != nil {
if t.spiffeX509Source == nil {
return nil, errors.New("SPIFFE is enabled for this transport, but not configured")
}
spiffeAuthorizer, err := buildSpiffeAuthorizer(cfg.Spiffe)
if err != nil {
return nil, fmt.Errorf("unable to build SPIFFE authorizer: %w", err)
}
config = tlsconfig.MTLSClientConfig(t.spiffeX509Source, t.spiffeX509Source, spiffeAuthorizer)
}
if cfg.InsecureSkipVerify || len(cfg.RootCAs) > 0 || len(cfg.ServerName) > 0 || len(cfg.Certificates) > 0 || cfg.PeerCertURI != "" {
if config != nil {
return nil, errors.New("TLS and SPIFFE configuration cannot be defined at the same time")
}
config = &tls.Config{
ServerName: cfg.ServerName,
InsecureSkipVerify: cfg.InsecureSkipVerify,
RootCAs: createRootCACertPool(cfg.RootCAs),
Certificates: cfg.Certificates.GetCertificates(),
}
if cfg.PeerCertURI != "" {
config.VerifyPeerCertificate = func(rawCerts [][]byte, _ [][]*x509.Certificate) error {
return traefiktls.VerifyPeerCertificate(cfg.PeerCertURI, config, rawCerts)
}
}
}
return config, nil
}
// createRoundTripper creates an http.RoundTripper configured with the Transport configuration settings.
// For the settings that can't be configured in Traefik it uses the default http.Transport settings.
// An exception to this is the MaxIdleConns setting as we only provide the option MaxIdleConnsPerHost in Traefik at this point in time.
// Setting this value to the default of 100 could lead to confusing behavior and backwards compatibility issues.
func (r *RoundTripperManager) createRoundTripper(cfg *dynamic.ServersTransport) (http.RoundTripper, error) {
func (t *TransportManager) createRoundTripper(cfg *dynamic.ServersTransport, tlsConfig *tls.Config) (http.RoundTripper, error) {
if cfg == nil {
return nil, errors.New("no transport configuration given")
}
@ -142,6 +218,7 @@ func (r *RoundTripperManager) createRoundTripper(cfg *dynamic.ServersTransport)
ExpectContinueTimeout: 1 * time.Second,
ReadBufferSize: 64 * 1024,
WriteBufferSize: 64 * 1024,
TLSClientConfig: tlsConfig,
}
if cfg.ForwardingTimeouts != nil {
@ -149,41 +226,9 @@ func (r *RoundTripperManager) createRoundTripper(cfg *dynamic.ServersTransport)
transport.IdleConnTimeout = time.Duration(cfg.ForwardingTimeouts.IdleConnTimeout)
}
if cfg.Spiffe != nil {
if r.spiffeX509Source == nil {
return nil, errors.New("SPIFFE is enabled for this transport, but not configured")
}
spiffeAuthorizer, err := buildSpiffeAuthorizer(cfg.Spiffe)
if err != nil {
return nil, fmt.Errorf("unable to build SPIFFE authorizer: %w", err)
}
transport.TLSClientConfig = tlsconfig.MTLSClientConfig(r.spiffeX509Source, r.spiffeX509Source, spiffeAuthorizer)
}
if cfg.InsecureSkipVerify || len(cfg.RootCAs) > 0 || len(cfg.ServerName) > 0 || len(cfg.Certificates) > 0 || cfg.PeerCertURI != "" {
if transport.TLSClientConfig != nil {
return nil, errors.New("TLS and SPIFFE configuration cannot be defined at the same time")
}
transport.TLSClientConfig = &tls.Config{
ServerName: cfg.ServerName,
InsecureSkipVerify: cfg.InsecureSkipVerify,
RootCAs: createRootCACertPool(cfg.RootCAs),
Certificates: cfg.Certificates.GetCertificates(),
}
if cfg.PeerCertURI != "" {
transport.TLSClientConfig.VerifyPeerCertificate = func(rawCerts [][]byte, _ [][]*x509.Certificate) error {
return traefiktls.VerifyPeerCertificate(cfg.PeerCertURI, transport.TLSClientConfig, rawCerts)
}
}
}
// Return directly HTTP/1.1 transport when HTTP/2 is disabled
if cfg.DisableHTTP2 {
return &KerberosRoundTripper{
return &kerberosRoundTripper{
OriginalRoundTripper: transport,
new: func() http.RoundTripper {
return transport.Clone()
@ -195,7 +240,7 @@ func (r *RoundTripperManager) createRoundTripper(cfg *dynamic.ServersTransport)
if err != nil {
return nil, err
}
return &KerberosRoundTripper{
return &kerberosRoundTripper{
OriginalRoundTripper: rt,
new: func() http.RoundTripper {
return rt.Clone()
@ -203,11 +248,6 @@ func (r *RoundTripperManager) createRoundTripper(cfg *dynamic.ServersTransport)
}, nil
}
type KerberosRoundTripper struct {
new func() http.RoundTripper
OriginalRoundTripper http.RoundTripper
}
type stickyRoundTripper struct {
RoundTripper http.RoundTripper
}
@ -220,7 +260,12 @@ func AddTransportOnContext(ctx context.Context) context.Context {
return context.WithValue(ctx, transportKey, &stickyRoundTripper{})
}
func (k *KerberosRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
type kerberosRoundTripper struct {
new func() http.RoundTripper
OriginalRoundTripper http.RoundTripper
}
func (k *kerberosRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
value, ok := request.Context().Value(transportKey).(*stickyRoundTripper)
if !ok {
return k.OriginalRoundTripper.RoundTrip(request)

View file

@ -141,7 +141,7 @@ func TestKeepConnectionWhenSameConfiguration(t *testing.T) {
srv.TLS = &tls.Config{Certificates: []tls.Certificate{cert}}
srv.StartTLS()
rtManager := NewRoundTripperManager(nil)
transportManager := NewTransportManager(nil)
dynamicConf := map[string]*dynamic.ServersTransport{
"test": {
@ -151,9 +151,9 @@ func TestKeepConnectionWhenSameConfiguration(t *testing.T) {
}
for range 10 {
rtManager.Update(dynamicConf)
transportManager.Update(dynamicConf)
tr, err := rtManager.Get("test")
tr, err := transportManager.GetRoundTripper("test")
require.NoError(t, err)
client := http.Client{Transport: tr}
@ -173,9 +173,9 @@ func TestKeepConnectionWhenSameConfiguration(t *testing.T) {
},
}
rtManager.Update(dynamicConf)
transportManager.Update(dynamicConf)
tr, err := rtManager.Get("test")
tr, err := transportManager.GetRoundTripper("test")
require.NoError(t, err)
client := http.Client{Transport: tr}
@ -209,7 +209,7 @@ func TestMTLS(t *testing.T) {
}
srv.StartTLS()
rtManager := NewRoundTripperManager(nil)
transportManager := NewTransportManager(nil)
dynamicConf := map[string]*dynamic.ServersTransport{
"test": {
@ -227,9 +227,9 @@ func TestMTLS(t *testing.T) {
},
}
rtManager.Update(dynamicConf)
transportManager.Update(dynamicConf)
tr, err := rtManager.Get("test")
tr, err := transportManager.GetRoundTripper("test")
require.NoError(t, err)
client := http.Client{Transport: tr}
@ -348,7 +348,7 @@ func TestSpiffeMTLS(t *testing.T) {
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
rtManager := NewRoundTripperManager(test.clientSource)
transportManager := NewTransportManager(test.clientSource)
dynamicConf := map[string]*dynamic.ServersTransport{
"test": {
@ -356,9 +356,9 @@ func TestSpiffeMTLS(t *testing.T) {
},
}
rtManager.Update(dynamicConf)
transportManager.Update(dynamicConf)
tr, err := rtManager.Get("test")
tr, err := transportManager.GetRoundTripper("test")
require.NoError(t, err)
client := http.Client{Transport: tr}
@ -415,7 +415,7 @@ func TestDisableHTTP2(t *testing.T) {
srv.EnableHTTP2 = test.serverHTTP2
srv.StartTLS()
rtManager := NewRoundTripperManager(nil)
transportManager := NewTransportManager(nil)
dynamicConf := map[string]*dynamic.ServersTransport{
"test": {
@ -424,9 +424,9 @@ func TestDisableHTTP2(t *testing.T) {
},
}
rtManager.Update(dynamicConf)
transportManager.Update(dynamicConf)
tr, err := rtManager.Get("test")
tr, err := transportManager.GetRoundTripper("test")
require.NoError(t, err)
client := http.Client{Transport: tr}
@ -593,7 +593,7 @@ func TestKerberosRoundTripper(t *testing.T) {
origCount := 0
dedicatedCount := 0
rt := KerberosRoundTripper{
rt := kerberosRoundTripper{
new: func() http.RoundTripper {
return roundTripperFn(func(req *http.Request) (*http.Response, error) {
dedicatedCount++

View file

@ -116,6 +116,7 @@ type OTLP struct {
AddServicesLabels bool `description:"Enable metrics on services." json:"addServicesLabels,omitempty" toml:"addServicesLabels,omitempty" yaml:"addServicesLabels,omitempty" export:"true"`
ExplicitBoundaries []float64 `description:"Boundaries for latency metrics." json:"explicitBoundaries,omitempty" toml:"explicitBoundaries,omitempty" yaml:"explicitBoundaries,omitempty" export:"true"`
PushInterval types.Duration `description:"Period between calls to collect a checkpoint." json:"pushInterval,omitempty" toml:"pushInterval,omitempty" yaml:"pushInterval,omitempty" export:"true"`
ServiceName string `description:"OTEL service name to use." json:"serviceName,omitempty" toml:"serviceName,omitempty" yaml:"serviceName,omitempty" export:"true"`
}
// SetDefaults sets the default values.
@ -127,6 +128,7 @@ func (o *OTLP) SetDefaults() {
o.AddServicesLabels = true
o.ExplicitBoundaries = []float64{.005, .01, .025, .05, .075, .1, .25, .5, .75, 1, 2.5, 5, 7.5, 10}
o.PushInterval = types.Duration(10 * time.Second)
o.ServiceName = "traefik"
}
// Statistics provides options for monitoring request and response stats.