Merge branch 'master' of github.com:traefik/traefik
All checks were successful
Build & Push / build-and-push (push) Successful in 10m3s
All checks were successful
Build & Push / build-and-push (push) Successful in 10m3s
This commit is contained in:
commit
93ec8f7636
74 changed files with 5307 additions and 5022 deletions
|
@ -229,7 +229,7 @@ issues:
|
||||||
text: 'struct-tag: unknown option ''inline'' in JSON tag'
|
text: 'struct-tag: unknown option ''inline'' in JSON tag'
|
||||||
linters:
|
linters:
|
||||||
- revive
|
- revive
|
||||||
- path: pkg/server/service/bufferpool.go
|
- path: pkg/proxy/httputil/bufferpool.go
|
||||||
text: 'SA6002: argument should be pointer-like to avoid allocations'
|
text: 'SA6002: argument should be pointer-like to avoid allocations'
|
||||||
- path: pkg/server/middleware/middlewares.go
|
- path: pkg/server/middleware/middlewares.go
|
||||||
text: "Function 'buildConstructor' has too many statements"
|
text: "Function 'buildConstructor' has too many statements"
|
||||||
|
@ -283,3 +283,7 @@ issues:
|
||||||
- path: pkg/provider/acme/local_store.go
|
- path: pkg/provider/acme/local_store.go
|
||||||
linters:
|
linters:
|
||||||
- musttag
|
- musttag
|
||||||
|
- path: pkg/types/metrics.go
|
||||||
|
linters:
|
||||||
|
- goconst
|
||||||
|
|
||||||
|
|
2
Makefile
2
Makefile
|
@ -104,7 +104,7 @@ test-integration: binary
|
||||||
#? test-gateway-api-conformance: Run the conformance tests
|
#? test-gateway-api-conformance: Run the conformance tests
|
||||||
test-gateway-api-conformance: build-image-dirty
|
test-gateway-api-conformance: build-image-dirty
|
||||||
# In case of a new Minor/Major version, the k8sConformanceTraefikVersion needs to be updated.
|
# 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
|
.PHONY: test-ui-unit
|
||||||
#? test-ui-unit: Run the unit tests for the webui
|
#? test-ui-unit: Run the unit tests for the webui
|
||||||
|
|
|
@ -37,6 +37,8 @@ import (
|
||||||
"github.com/traefik/traefik/v3/pkg/provider/aggregator"
|
"github.com/traefik/traefik/v3/pkg/provider/aggregator"
|
||||||
"github.com/traefik/traefik/v3/pkg/provider/tailscale"
|
"github.com/traefik/traefik/v3/pkg/provider/tailscale"
|
||||||
"github.com/traefik/traefik/v3/pkg/provider/traefik"
|
"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/safe"
|
||||||
"github.com/traefik/traefik/v3/pkg/server"
|
"github.com/traefik/traefik/v3/pkg/server"
|
||||||
"github.com/traefik/traefik/v3/pkg/server/middleware"
|
"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.")
|
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)
|
dialerManager := tcp.NewDialerManager(spiffeX509Source)
|
||||||
acmeHTTPHandler := getHTTPChallengeHandler(acmeProviders, httpChallengeProvider)
|
acmeHTTPHandler := getHTTPChallengeHandler(acmeProviders, httpChallengeProvider)
|
||||||
managerFactory := service.NewManagerFactory(*staticConfiguration, routinesPool, observabilityMgr, roundTripperManager, acmeHTTPHandler)
|
managerFactory := service.NewManagerFactory(*staticConfiguration, routinesPool, observabilityMgr, transportManager, proxyBuilder, acmeHTTPHandler)
|
||||||
|
|
||||||
// Router factory
|
// Router factory
|
||||||
|
|
||||||
|
@ -318,7 +326,8 @@ func setupServer(staticConfiguration *static.Configuration) (*server.Server, err
|
||||||
|
|
||||||
// Server Transports
|
// Server Transports
|
||||||
watcher.AddListener(func(conf dynamic.Configuration) {
|
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)
|
dialerManager.Update(conf.TCP.ServersTransports)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -101,7 +101,7 @@ If none are set, the default is to use the `requestHost`.
|
||||||
|
|
||||||
#### `sourceCriterion.ipStrategy`
|
#### `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."
|
!!! 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.
|
- 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.
|
- `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"
|
!!! 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`).
|
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"]
|
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`
|
#### `sourceCriterion.requestHeaderName`
|
||||||
|
|
||||||
Name of the header used to group incoming requests.
|
Name of the header used to group incoming requests.
|
||||||
|
|
|
@ -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.
|
- 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.
|
- `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"
|
!!! 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`).
|
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]
|
[http.middlewares.test-ipallowlist.ipAllowList.ipStrategy]
|
||||||
excludedIPs = ["127.0.0.1/32", "192.168.1.7"]
|
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
|
||||||
|
```
|
||||||
|
|
|
@ -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.
|
- 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.
|
- `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"
|
!!! 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`).
|
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]
|
[http.middlewares.test-ipwhitelist.ipWhiteList.ipStrategy]
|
||||||
excludedIPs = ["127.0.0.1/32", "192.168.1.7"]
|
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
|
||||||
|
```
|
||||||
|
|
|
@ -211,7 +211,7 @@ If none are set, the default is to use the request's remote address field (as an
|
||||||
|
|
||||||
#### `sourceCriterion.ipStrategy`
|
#### `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."
|
!!! 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.
|
- 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.
|
- `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"
|
!!! 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`).
|
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"]
|
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`
|
#### `sourceCriterion.requestHeaderName`
|
||||||
|
|
||||||
Name of the header used to group incoming requests.
|
Name of the header used to group incoming requests.
|
||||||
|
|
|
@ -78,7 +78,7 @@ Please use the `disableClusterScopeResources` option instead to avoid cluster sc
|
||||||
|
|
||||||
## v3.1 to v3.2
|
## 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/).
|
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
|
- 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
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
|
@ -139,6 +139,28 @@ metrics:
|
||||||
--metrics.otlp.pushInterval=10s
|
--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
|
### HTTP configuration
|
||||||
|
|
||||||
_Optional_
|
_Optional_
|
||||||
|
|
|
@ -56,6 +56,8 @@ _Optional, Default=15s_
|
||||||
|
|
||||||
Defines the polling interval.
|
Defines the polling interval.
|
||||||
|
|
||||||
|
!!! note "This option is ignored when the [watch](#watch) mode is enabled."
|
||||||
|
|
||||||
```yaml tab="File (YAML)"
|
```yaml tab="File (YAML)"
|
||||||
providers:
|
providers:
|
||||||
nomad:
|
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`
|
### `prefix`
|
||||||
|
|
||||||
_required, Default="traefik"_
|
_required, Default="traefik"_
|
||||||
|
|
|
@ -85,15 +85,18 @@
|
||||||
- "traefik.http.middlewares.middleware13.ipallowlist.ipstrategy=true"
|
- "traefik.http.middlewares.middleware13.ipallowlist.ipstrategy=true"
|
||||||
- "traefik.http.middlewares.middleware13.ipallowlist.ipstrategy.depth=42"
|
- "traefik.http.middlewares.middleware13.ipallowlist.ipstrategy.depth=42"
|
||||||
- "traefik.http.middlewares.middleware13.ipallowlist.ipstrategy.excludedips=foobar, foobar"
|
- "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.rejectstatuscode=42"
|
||||||
- "traefik.http.middlewares.middleware13.ipallowlist.sourcerange=foobar, foobar"
|
- "traefik.http.middlewares.middleware13.ipallowlist.sourcerange=foobar, foobar"
|
||||||
- "traefik.http.middlewares.middleware14.ipwhitelist.ipstrategy=true"
|
- "traefik.http.middlewares.middleware14.ipwhitelist.ipstrategy=true"
|
||||||
- "traefik.http.middlewares.middleware14.ipwhitelist.ipstrategy.depth=42"
|
- "traefik.http.middlewares.middleware14.ipwhitelist.ipstrategy.depth=42"
|
||||||
- "traefik.http.middlewares.middleware14.ipwhitelist.ipstrategy.excludedips=foobar, foobar"
|
- "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.middleware14.ipwhitelist.sourcerange=foobar, foobar"
|
||||||
- "traefik.http.middlewares.middleware15.inflightreq.amount=42"
|
- "traefik.http.middlewares.middleware15.inflightreq.amount=42"
|
||||||
- "traefik.http.middlewares.middleware15.inflightreq.sourcecriterion.ipstrategy.depth=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.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.requestheadername=foobar"
|
||||||
- "traefik.http.middlewares.middleware15.inflightreq.sourcecriterion.requesthost=true"
|
- "traefik.http.middlewares.middleware15.inflightreq.sourcecriterion.requesthost=true"
|
||||||
- "traefik.http.middlewares.middleware16.passtlsclientcert.info.issuer.commonname=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.period=42s"
|
||||||
- "traefik.http.middlewares.middleware18.ratelimit.sourcecriterion.ipstrategy.depth=42"
|
- "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.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.requestheadername=foobar"
|
||||||
- "traefik.http.middlewares.middleware18.ratelimit.sourcecriterion.requesthost=true"
|
- "traefik.http.middlewares.middleware18.ratelimit.sourcecriterion.requesthost=true"
|
||||||
- "traefik.http.middlewares.middleware19.redirectregex.permanent=true"
|
- "traefik.http.middlewares.middleware19.redirectregex.permanent=true"
|
||||||
|
|
|
@ -227,12 +227,14 @@
|
||||||
[http.middlewares.Middleware13.ipAllowList.ipStrategy]
|
[http.middlewares.Middleware13.ipAllowList.ipStrategy]
|
||||||
depth = 42
|
depth = 42
|
||||||
excludedIPs = ["foobar", "foobar"]
|
excludedIPs = ["foobar", "foobar"]
|
||||||
|
ipv6Subnet = 42
|
||||||
[http.middlewares.Middleware14]
|
[http.middlewares.Middleware14]
|
||||||
[http.middlewares.Middleware14.ipWhiteList]
|
[http.middlewares.Middleware14.ipWhiteList]
|
||||||
sourceRange = ["foobar", "foobar"]
|
sourceRange = ["foobar", "foobar"]
|
||||||
[http.middlewares.Middleware14.ipWhiteList.ipStrategy]
|
[http.middlewares.Middleware14.ipWhiteList.ipStrategy]
|
||||||
depth = 42
|
depth = 42
|
||||||
excludedIPs = ["foobar", "foobar"]
|
excludedIPs = ["foobar", "foobar"]
|
||||||
|
ipv6Subnet = 42
|
||||||
[http.middlewares.Middleware15]
|
[http.middlewares.Middleware15]
|
||||||
[http.middlewares.Middleware15.inFlightReq]
|
[http.middlewares.Middleware15.inFlightReq]
|
||||||
amount = 42
|
amount = 42
|
||||||
|
@ -242,6 +244,7 @@
|
||||||
[http.middlewares.Middleware15.inFlightReq.sourceCriterion.ipStrategy]
|
[http.middlewares.Middleware15.inFlightReq.sourceCriterion.ipStrategy]
|
||||||
depth = 42
|
depth = 42
|
||||||
excludedIPs = ["foobar", "foobar"]
|
excludedIPs = ["foobar", "foobar"]
|
||||||
|
ipv6Subnet = 42
|
||||||
[http.middlewares.Middleware16]
|
[http.middlewares.Middleware16]
|
||||||
[http.middlewares.Middleware16.passTLSClientCert]
|
[http.middlewares.Middleware16.passTLSClientCert]
|
||||||
pem = true
|
pem = true
|
||||||
|
@ -286,6 +289,7 @@
|
||||||
[http.middlewares.Middleware18.rateLimit.sourceCriterion.ipStrategy]
|
[http.middlewares.Middleware18.rateLimit.sourceCriterion.ipStrategy]
|
||||||
depth = 42
|
depth = 42
|
||||||
excludedIPs = ["foobar", "foobar"]
|
excludedIPs = ["foobar", "foobar"]
|
||||||
|
ipv6Subnet = 42
|
||||||
[http.middlewares.Middleware19]
|
[http.middlewares.Middleware19]
|
||||||
[http.middlewares.Middleware19.redirectRegex]
|
[http.middlewares.Middleware19.redirectRegex]
|
||||||
regex = "foobar"
|
regex = "foobar"
|
||||||
|
|
|
@ -267,6 +267,7 @@ http:
|
||||||
excludedIPs:
|
excludedIPs:
|
||||||
- foobar
|
- foobar
|
||||||
- foobar
|
- foobar
|
||||||
|
ipv6Subnet: 42
|
||||||
rejectStatusCode: 42
|
rejectStatusCode: 42
|
||||||
Middleware14:
|
Middleware14:
|
||||||
ipWhiteList:
|
ipWhiteList:
|
||||||
|
@ -278,6 +279,7 @@ http:
|
||||||
excludedIPs:
|
excludedIPs:
|
||||||
- foobar
|
- foobar
|
||||||
- foobar
|
- foobar
|
||||||
|
ipv6Subnet: 42
|
||||||
Middleware15:
|
Middleware15:
|
||||||
inFlightReq:
|
inFlightReq:
|
||||||
amount: 42
|
amount: 42
|
||||||
|
@ -287,6 +289,7 @@ http:
|
||||||
excludedIPs:
|
excludedIPs:
|
||||||
- foobar
|
- foobar
|
||||||
- foobar
|
- foobar
|
||||||
|
ipv6Subnet: 42
|
||||||
requestHeaderName: foobar
|
requestHeaderName: foobar
|
||||||
requestHost: true
|
requestHost: true
|
||||||
Middleware16:
|
Middleware16:
|
||||||
|
@ -333,6 +336,7 @@ http:
|
||||||
excludedIPs:
|
excludedIPs:
|
||||||
- foobar
|
- foobar
|
||||||
- foobar
|
- foobar
|
||||||
|
ipv6Subnet: 42
|
||||||
requestHeaderName: foobar
|
requestHeaderName: foobar
|
||||||
requestHost: true
|
requestHost: true
|
||||||
Middleware19:
|
Middleware19:
|
||||||
|
|
|
@ -1458,6 +1458,12 @@ spec:
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
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
|
type: object
|
||||||
requestHeaderName:
|
requestHeaderName:
|
||||||
description: RequestHeaderName defines the name of the header
|
description: RequestHeaderName defines the name of the header
|
||||||
|
@ -1491,6 +1497,11 @@ spec:
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
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
|
type: object
|
||||||
rejectStatusCode:
|
rejectStatusCode:
|
||||||
description: |-
|
description: |-
|
||||||
|
@ -1523,6 +1534,11 @@ spec:
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
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
|
type: object
|
||||||
sourceRange:
|
sourceRange:
|
||||||
description: SourceRange defines the set of allowed IPs (or ranges
|
description: SourceRange defines the set of allowed IPs (or ranges
|
||||||
|
@ -1691,6 +1707,12 @@ spec:
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
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
|
type: object
|
||||||
requestHeaderName:
|
requestHeaderName:
|
||||||
description: RequestHeaderName defines the name of the header
|
description: RequestHeaderName defines the name of the header
|
||||||
|
|
|
@ -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/depth` | `42` |
|
||||||
| `traefik/http/middlewares/Middleware13/ipAllowList/ipStrategy/excludedIPs/0` | `foobar` |
|
| `traefik/http/middlewares/Middleware13/ipAllowList/ipStrategy/excludedIPs/0` | `foobar` |
|
||||||
| `traefik/http/middlewares/Middleware13/ipAllowList/ipStrategy/excludedIPs/1` | `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/rejectStatusCode` | `42` |
|
||||||
| `traefik/http/middlewares/Middleware13/ipAllowList/sourceRange/0` | `foobar` |
|
| `traefik/http/middlewares/Middleware13/ipAllowList/sourceRange/0` | `foobar` |
|
||||||
| `traefik/http/middlewares/Middleware13/ipAllowList/sourceRange/1` | `foobar` |
|
| `traefik/http/middlewares/Middleware13/ipAllowList/sourceRange/1` | `foobar` |
|
||||||
| `traefik/http/middlewares/Middleware14/ipWhiteList/ipStrategy/depth` | `42` |
|
| `traefik/http/middlewares/Middleware14/ipWhiteList/ipStrategy/depth` | `42` |
|
||||||
| `traefik/http/middlewares/Middleware14/ipWhiteList/ipStrategy/excludedIPs/0` | `foobar` |
|
| `traefik/http/middlewares/Middleware14/ipWhiteList/ipStrategy/excludedIPs/0` | `foobar` |
|
||||||
| `traefik/http/middlewares/Middleware14/ipWhiteList/ipStrategy/excludedIPs/1` | `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/0` | `foobar` |
|
||||||
| `traefik/http/middlewares/Middleware14/ipWhiteList/sourceRange/1` | `foobar` |
|
| `traefik/http/middlewares/Middleware14/ipWhiteList/sourceRange/1` | `foobar` |
|
||||||
| `traefik/http/middlewares/Middleware15/inFlightReq/amount` | `42` |
|
| `traefik/http/middlewares/Middleware15/inFlightReq/amount` | `42` |
|
||||||
| `traefik/http/middlewares/Middleware15/inFlightReq/sourceCriterion/ipStrategy/depth` | `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/0` | `foobar` |
|
||||||
| `traefik/http/middlewares/Middleware15/inFlightReq/sourceCriterion/ipStrategy/excludedIPs/1` | `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/requestHeaderName` | `foobar` |
|
||||||
| `traefik/http/middlewares/Middleware15/inFlightReq/sourceCriterion/requestHost` | `true` |
|
| `traefik/http/middlewares/Middleware15/inFlightReq/sourceCriterion/requestHost` | `true` |
|
||||||
| `traefik/http/middlewares/Middleware16/passTLSClientCert/info/issuer/commonName` | `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/depth` | `42` |
|
||||||
| `traefik/http/middlewares/Middleware18/rateLimit/sourceCriterion/ipStrategy/excludedIPs/0` | `foobar` |
|
| `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/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/requestHeaderName` | `foobar` |
|
||||||
| `traefik/http/middlewares/Middleware18/rateLimit/sourceCriterion/requestHost` | `true` |
|
| `traefik/http/middlewares/Middleware18/rateLimit/sourceCriterion/requestHost` | `true` |
|
||||||
| `traefik/http/middlewares/Middleware19/redirectRegex/permanent` | `true` |
|
| `traefik/http/middlewares/Middleware19/redirectRegex/permanent` | `true` |
|
||||||
|
|
|
@ -734,6 +734,12 @@ spec:
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
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
|
type: object
|
||||||
requestHeaderName:
|
requestHeaderName:
|
||||||
description: RequestHeaderName defines the name of the header
|
description: RequestHeaderName defines the name of the header
|
||||||
|
@ -767,6 +773,11 @@ spec:
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
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
|
type: object
|
||||||
rejectStatusCode:
|
rejectStatusCode:
|
||||||
description: |-
|
description: |-
|
||||||
|
@ -799,6 +810,11 @@ spec:
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
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
|
type: object
|
||||||
sourceRange:
|
sourceRange:
|
||||||
description: SourceRange defines the set of allowed IPs (or ranges
|
description: SourceRange defines the set of allowed IPs (or ranges
|
||||||
|
@ -967,6 +983,12 @@ spec:
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
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
|
type: object
|
||||||
requestHeaderName:
|
requestHeaderName:
|
||||||
description: RequestHeaderName defines the name of the header
|
description: RequestHeaderName defines the name of the header
|
||||||
|
|
|
@ -228,6 +228,12 @@ WriteTimeout is the maximum duration before timing out writes of the response. I
|
||||||
`--entrypoints.<name>.udp.timeout`:
|
`--entrypoints.<name>.udp.timeout`:
|
||||||
Timeout defines how long to wait on an idle session before releasing the related resources. (Default: ```3```)
|
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`:
|
`--experimental.kubernetesgateway`:
|
||||||
(Deprecated) Allow the Kubernetes gateway api provider usage. (Default: ```false```)
|
(Deprecated) Allow the Kubernetes gateway api provider usage. (Default: ```false```)
|
||||||
|
|
||||||
|
@ -423,6 +429,9 @@ TLS key
|
||||||
`--metrics.otlp.pushinterval`:
|
`--metrics.otlp.pushinterval`:
|
||||||
Period between calls to collect a checkpoint. (Default: ```10```)
|
Period between calls to collect a checkpoint. (Default: ```10```)
|
||||||
|
|
||||||
|
`--metrics.otlp.servicename`:
|
||||||
|
OTEL service name to use. (Default: ```traefik```)
|
||||||
|
|
||||||
`--metrics.prometheus`:
|
`--metrics.prometheus`:
|
||||||
Prometheus metrics exporter type. (Default: ```false```)
|
Prometheus metrics exporter type. (Default: ```false```)
|
||||||
|
|
||||||
|
@ -912,6 +921,12 @@ Interval for polling Nomad API. (Default: ```15```)
|
||||||
`--providers.nomad.stale`:
|
`--providers.nomad.stale`:
|
||||||
Use stale consistency for catalog reads. (Default: ```false```)
|
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>`:
|
`--providers.plugin.<name>`:
|
||||||
Plugins configuration.
|
Plugins configuration.
|
||||||
|
|
||||||
|
|
|
@ -228,6 +228,12 @@ WriteTimeout is the maximum duration before timing out writes of the response. I
|
||||||
`TRAEFIK_ENTRYPOINTS_<NAME>_UDP_TIMEOUT`:
|
`TRAEFIK_ENTRYPOINTS_<NAME>_UDP_TIMEOUT`:
|
||||||
Timeout defines how long to wait on an idle session before releasing the related resources. (Default: ```3```)
|
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`:
|
`TRAEFIK_EXPERIMENTAL_KUBERNETESGATEWAY`:
|
||||||
(Deprecated) Allow the Kubernetes gateway api provider usage. (Default: ```false```)
|
(Deprecated) Allow the Kubernetes gateway api provider usage. (Default: ```false```)
|
||||||
|
|
||||||
|
@ -423,6 +429,9 @@ TLS key
|
||||||
`TRAEFIK_METRICS_OTLP_PUSHINTERVAL`:
|
`TRAEFIK_METRICS_OTLP_PUSHINTERVAL`:
|
||||||
Period between calls to collect a checkpoint. (Default: ```10```)
|
Period between calls to collect a checkpoint. (Default: ```10```)
|
||||||
|
|
||||||
|
`TRAEFIK_METRICS_OTLP_SERVICENAME`:
|
||||||
|
OTEL service name to use. (Default: ```traefik```)
|
||||||
|
|
||||||
`TRAEFIK_METRICS_PROMETHEUS`:
|
`TRAEFIK_METRICS_PROMETHEUS`:
|
||||||
Prometheus metrics exporter type. (Default: ```false```)
|
Prometheus metrics exporter type. (Default: ```false```)
|
||||||
|
|
||||||
|
@ -912,6 +921,12 @@ Interval for polling Nomad API. (Default: ```15```)
|
||||||
`TRAEFIK_PROVIDERS_NOMAD_STALE`:
|
`TRAEFIK_PROVIDERS_NOMAD_STALE`:
|
||||||
Use stale consistency for catalog reads. (Default: ```false```)
|
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>`:
|
`TRAEFIK_PROVIDERS_PLUGIN_<NAME>`:
|
||||||
Plugins configuration.
|
Plugins configuration.
|
||||||
|
|
||||||
|
|
|
@ -203,6 +203,8 @@
|
||||||
exposedByDefault = true
|
exposedByDefault = true
|
||||||
refreshInterval = "42s"
|
refreshInterval = "42s"
|
||||||
allowEmptyServices = true
|
allowEmptyServices = true
|
||||||
|
watch = true
|
||||||
|
throttleDuration = "42s"
|
||||||
namespaces = ["foobar", "foobar"]
|
namespaces = ["foobar", "foobar"]
|
||||||
[providers.nomad.endpoint]
|
[providers.nomad.endpoint]
|
||||||
address = "foobar"
|
address = "foobar"
|
||||||
|
@ -340,6 +342,7 @@
|
||||||
addServicesLabels = true
|
addServicesLabels = true
|
||||||
explicitBoundaries = [42.0, 42.0]
|
explicitBoundaries = [42.0, 42.0]
|
||||||
pushInterval = "42s"
|
pushInterval = "42s"
|
||||||
|
serviceName = "foobar"
|
||||||
[metrics.otlp.grpc]
|
[metrics.otlp.grpc]
|
||||||
endpoint = "foobar"
|
endpoint = "foobar"
|
||||||
insecure = true
|
insecure = true
|
||||||
|
@ -509,6 +512,8 @@
|
||||||
[experimental.localPlugins.LocalDescriptor1.settings]
|
[experimental.localPlugins.LocalDescriptor1.settings]
|
||||||
envs = ["foobar", "foobar"]
|
envs = ["foobar", "foobar"]
|
||||||
mounts = ["foobar", "foobar"]
|
mounts = ["foobar", "foobar"]
|
||||||
|
[experimental.fastProxy]
|
||||||
|
debug = true
|
||||||
|
|
||||||
[core]
|
[core]
|
||||||
defaultRuleSyntax = "foobar"
|
defaultRuleSyntax = "foobar"
|
||||||
|
|
|
@ -236,6 +236,8 @@ providers:
|
||||||
exposedByDefault: true
|
exposedByDefault: true
|
||||||
refreshInterval: 42s
|
refreshInterval: 42s
|
||||||
allowEmptyServices: true
|
allowEmptyServices: true
|
||||||
|
watch: true
|
||||||
|
throttleDuration: 42s
|
||||||
namespaces:
|
namespaces:
|
||||||
- foobar
|
- foobar
|
||||||
- foobar
|
- foobar
|
||||||
|
@ -400,6 +402,7 @@ metrics:
|
||||||
- 42
|
- 42
|
||||||
- 42
|
- 42
|
||||||
pushInterval: 42s
|
pushInterval: 42s
|
||||||
|
serviceName: foobar
|
||||||
ping:
|
ping:
|
||||||
entryPoint: foobar
|
entryPoint: foobar
|
||||||
manualRouting: true
|
manualRouting: true
|
||||||
|
@ -572,6 +575,8 @@ experimental:
|
||||||
mounts:
|
mounts:
|
||||||
- foobar
|
- foobar
|
||||||
- foobar
|
- foobar
|
||||||
|
fastProxy:
|
||||||
|
debug: true
|
||||||
kubernetesGateway: true
|
kubernetesGateway: true
|
||||||
core:
|
core:
|
||||||
defaultRuleSyntax: foobar
|
defaultRuleSyntax: foobar
|
||||||
|
|
41
docs/content/user-guides/fastproxy.md
Normal file
41
docs/content/user-guides/fastproxy.md
Normal 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
|
||||||
|
```
|
|
@ -163,6 +163,7 @@ nav:
|
||||||
- 'Overview': 'observability/tracing/overview.md'
|
- 'Overview': 'observability/tracing/overview.md'
|
||||||
- 'OpenTelemetry': 'observability/tracing/opentelemetry.md'
|
- 'OpenTelemetry': 'observability/tracing/opentelemetry.md'
|
||||||
- 'User Guides':
|
- 'User Guides':
|
||||||
|
- 'FastProxy': 'user-guides/fastproxy.md'
|
||||||
- 'Kubernetes and Let''s Encrypt': 'user-guides/crd-acme/index.md'
|
- 'Kubernetes and Let''s Encrypt': 'user-guides/crd-acme/index.md'
|
||||||
- 'gRPC Examples': 'user-guides/grpc.md'
|
- 'gRPC Examples': 'user-guides/grpc.md'
|
||||||
- 'Docker':
|
- 'Docker':
|
||||||
|
|
45
go.mod
45
go.mod
|
@ -6,7 +6,8 @@ require (
|
||||||
github.com/BurntSushi/toml v1.4.0
|
github.com/BurntSushi/toml v1.4.0
|
||||||
github.com/Masterminds/sprig/v3 v3.2.3
|
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/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/aws/aws-sdk-go v1.44.327
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0
|
github.com/cenkalti/backoff/v4 v4.3.0
|
||||||
github.com/containous/alice v0.0.0-20181107144136-d83ebdd94cbd // No tag on the repo.
|
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/valkeyrie v1.0.0
|
||||||
github.com/kvtools/zookeeper v1.0.2
|
github.com/kvtools/zookeeper v1.0.2
|
||||||
github.com/mailgun/ttlmap v0.0.0-20170619185759-c1c17f74874f // No tag on the repo.
|
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/copystructure v1.2.0
|
||||||
github.com/mitchellh/hashstructure v1.0.0
|
github.com/mitchellh/hashstructure v1.0.0
|
||||||
github.com/mitchellh/mapstructure v1.5.0
|
github.com/mitchellh/mapstructure v1.5.0
|
||||||
|
@ -48,7 +49,7 @@ require (
|
||||||
github.com/pires/go-proxyproto v0.6.1
|
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/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_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/quic-go/quic-go v0.47.0
|
||||||
github.com/rs/zerolog v1.29.0
|
github.com/rs/zerolog v1.29.0
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
|
@ -67,6 +68,7 @@ require (
|
||||||
github.com/traefik/yaegi v0.16.1
|
github.com/traefik/yaegi v0.16.1
|
||||||
github.com/unrolled/render v1.0.2
|
github.com/unrolled/render v1.0.2
|
||||||
github.com/unrolled/secure v1.0.9
|
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/oxy/v2 v2.0.0
|
||||||
github.com/vulcand/predicate v1.2.0
|
github.com/vulcand/predicate v1.2.0
|
||||||
go.opentelemetry.io/collector/pdata v1.10.0
|
go.opentelemetry.io/collector/pdata v1.10.0
|
||||||
|
@ -88,17 +90,17 @@ require (
|
||||||
golang.org/x/text v0.18.0
|
golang.org/x/text v0.18.0
|
||||||
golang.org/x/time v0.5.0
|
golang.org/x/time v0.5.0
|
||||||
golang.org/x/tools v0.25.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/natefinch/lumberjack.v2 v2.2.1
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
k8s.io/api v0.30.0
|
k8s.io/api v0.31.1
|
||||||
k8s.io/apiextensions-apiserver v0.30.0
|
k8s.io/apiextensions-apiserver v0.31.1
|
||||||
k8s.io/apimachinery v0.30.0
|
k8s.io/apimachinery v0.31.1
|
||||||
k8s.io/client-go v0.30.0
|
k8s.io/client-go v0.31.1
|
||||||
k8s.io/utils v0.0.0-20240423183400-0849a56e8f22 // No tag on the repo.
|
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // No tag on the repo.
|
||||||
mvdan.cc/xurls/v2 v2.5.0
|
mvdan.cc/xurls/v2 v2.5.0
|
||||||
sigs.k8s.io/controller-runtime v0.18.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
|
sigs.k8s.io/yaml v1.4.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -153,7 +155,7 @@ require (
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
|
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
|
||||||
github.com/bytedance/sonic v1.10.0 // 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/civo/civogo v0.3.11 // indirect
|
||||||
github.com/cloudflare/cloudflare-go v0.97.0 // indirect
|
github.com/cloudflare/cloudflare-go v0.97.0 // indirect
|
||||||
github.com/containerd/containerd v1.7.20 // 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/dnsimple/dnsimple-go v1.7.0 // indirect
|
||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
github.com/emicklei/go-restful/v3 v3.12.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/evanphx/json-patch/v5 v5.9.0 // indirect
|
||||||
github.com/exoscale/egoscale v0.102.3 // 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/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/ghodss/yaml v1.0.0 // indirect
|
||||||
github.com/gin-gonic/gin v1.9.1 // indirect
|
github.com/gin-gonic/gin v1.9.1 // indirect
|
||||||
github.com/go-errors/errors v1.0.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/mitchellh/reflectwalk v1.0.2 // indirect
|
||||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||||
github.com/moby/patternmatcher v0.6.0 // 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/sequential v0.5.0 // indirect
|
||||||
github.com/moby/sys/user v0.2.0 // indirect
|
github.com/moby/sys/user v0.2.0 // indirect
|
||||||
github.com/moby/term v0.5.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/pkg/errors v0.9.1 // indirect
|
||||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||||
github.com/pquerna/otp v1.4.0 // 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/prometheus/procfs v0.15.1 // indirect
|
||||||
github.com/quic-go/qpack v0.5.1 // indirect
|
github.com/quic-go/qpack v0.5.1 // indirect
|
||||||
github.com/redis/go-redis/v9 v9.2.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/tklauser/numcpus v0.6.1 // indirect
|
||||||
github.com/transip/gotransip/v6 v6.23.0 // indirect
|
github.com/transip/gotransip/v6 v6.23.0 // indirect
|
||||||
github.com/ultradns/ultradns-go-sdk v1.6.1-20231103022937-8589b6a // 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/vinyldns/go-vinyldns v0.9.16 // indirect
|
||||||
github.com/vultr/govultr/v3 v3.9.0 // 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-genproto v0.0.0-20240318083951-4fe6125f286e // indirect
|
||||||
github.com/yandex-cloud/go-sdk v0.0.0-20240318084659-dfa50323a0b4 // indirect
|
github.com/yandex-cloud/go-sdk v0.0.0-20240318084659-dfa50323a0b4 // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.3 // indirect
|
github.com/yusufpapurcu/wmi v1.2.3 // indirect
|
||||||
github.com/zeebo/errs v1.2.2 // indirect
|
github.com/zeebo/errs v1.2.2 // indirect
|
||||||
go.etcd.io/etcd/api/v3 v3.5.10 // indirect
|
go.etcd.io/etcd/api/v3 v3.5.14 // indirect
|
||||||
go.etcd.io/etcd/client/pkg/v3 v3.5.10 // indirect
|
go.etcd.io/etcd/client/pkg/v3 v3.5.14 // indirect
|
||||||
go.etcd.io/etcd/client/v3 v3.5.10 // indirect
|
go.etcd.io/etcd/client/v3 v3.5.14 // indirect
|
||||||
go.opencensus.io v0.24.0 // 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/aws v1.28.0 // indirect
|
||||||
go.opentelemetry.io/contrib/propagators/b3 v1.28.0 // indirect
|
go.opentelemetry.io/contrib/propagators/b3 v1.28.0 // indirect
|
||||||
go.opentelemetry.io/contrib/propagators/jaeger 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/api v0.0.0-20240701130421-f6361c86f094 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc 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
|
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/h2non/gock.v1 v1.0.16 // indirect
|
||||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/ns1/ns1-go.v2 v2.9.1 // indirect
|
gopkg.in/ns1/ns1-go.v2 v2.9.1 // indirect
|
||||||
gopkg.in/square/go-jose.v2 v2.5.1 // indirect
|
gopkg.in/square/go-jose.v2 v2.5.1 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // 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
|
k8s.io/kube-openapi v0.0.0-20240423202451-8948a665c108 // indirect
|
||||||
nhooyr.io/websocket v1.8.7 // indirect
|
nhooyr.io/websocket v1.8.7 // indirect
|
||||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
|
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
|
||||||
|
|
85
go.sum
85
go.sum
|
@ -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/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 h1:lM7JnA9dEdDFH9XOgRNQMDTQnOjlLkDTNA7c0aWTQ30=
|
||||||
github.com/aliyun/alibaba-cloud-sdk-go v1.62.712/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=
|
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.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||||
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
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/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/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=
|
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/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 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.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
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-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-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
|
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.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.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.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
|
||||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
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 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
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.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
|
||||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
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/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 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
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=
|
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.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.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.47/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
|
||||||
github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs=
|
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
|
||||||
github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk=
|
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 h1:Gj6EhHJkOhr+q2RnvRPJsPMcjuVnWPSccEHyoEehU34=
|
||||||
github.com/mimuret/golang-iij-dpf v0.9.1/go.mod h1:sl9KyOkESib9+KRD3HaGpgi1xk7eoN2+d96LCLsME2M=
|
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=
|
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/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 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
|
||||||
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
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.4.0 h1:Vy79D6mHeJJjiPdFEL2yku1kl0chZpJfZcPpb16BRl8=
|
||||||
github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=
|
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 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
|
||||||
github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
|
github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
|
||||||
github.com/moby/sys/user v0.2.0 h1:OnpapJsRp25vkhw8TFG6OLJODNh/3rEwRWtJ3kakwRM=
|
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-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.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.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||||
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
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.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.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||||
github.com/prometheus/common v0.4.1/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.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.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
|
||||||
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
|
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
|
||||||
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
|
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-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.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||||
github.com/prometheus/procfs v0.0.2/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/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 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc=
|
||||||
github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
|
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/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.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
||||||
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
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=
|
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/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 h1:63V/22mpfquRA5DenJ9EF0VozHg0k+X4dhUWcDXHPyc=
|
||||||
github.com/vultr/govultr/v3 v3.9.0/go.mod h1:Rd8ebpXm7jxH3MDmhnEs+zrlYW212ouhx+HeUMfHm2o=
|
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/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/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=
|
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 h1:5NFypMTuSdoySVTqlNs1dEoU21QVamMQJxW/Fii5O7g=
|
||||||
github.com/zeebo/errs v1.2.2/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
|
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/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.14 h1:vHObSCxyB9zlF60w7qzAdTcGaglbJOpSj1Xj9+WGxq0=
|
||||||
go.etcd.io/etcd/api/v3 v3.5.10/go.mod h1:TidfmT4Uycad3NM/o25fG3J07odo4GBB9hoxaodFCtI=
|
go.etcd.io/etcd/api/v3 v3.5.14/go.mod h1:BmtWcRlQvwa1h3G2jvKYwIQy4PkHlDej5t7uLMUdJUU=
|
||||||
go.etcd.io/etcd/client/pkg/v3 v3.5.10 h1:kfYIdQftBnbAq8pUWFXfpuuxFSKzlmM5cSn76JByiT0=
|
go.etcd.io/etcd/client/pkg/v3 v3.5.14 h1:SaNH6Y+rVEdxfpA2Jr5wkEvN6Zykme5+YnbCkxvuWxQ=
|
||||||
go.etcd.io/etcd/client/pkg/v3 v3.5.10/go.mod h1:DYivfIviIuQ8+/lCq4vcxuseg2P2XbHygkKwFo9fc8U=
|
go.etcd.io/etcd/client/pkg/v3 v3.5.14/go.mod h1:8uMgAokyG1czCtIdsq+AGyYQMvpIKnSvPjFMunkgeZI=
|
||||||
go.etcd.io/etcd/client/v3 v3.5.10 h1:W9TXNZ+oB3MCd/8UjxHTWK5J9Nquw9fQBLJd5ne5/Ao=
|
go.etcd.io/etcd/client/v3 v3.5.14 h1:CWfRs4FDaDoSz81giL7zPpZH2Z35tbOrAJkkjMqOupg=
|
||||||
go.etcd.io/etcd/client/v3 v3.5.10/go.mod h1:RVeBnDz2PUEZqTpgqwAtUd8nAPf5kjyFyND7P1VkOKc=
|
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.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
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 h1:oLyPLGvPTQrcRT64ZVruwvmH/u3SHTfNo01pteS4WOE=
|
||||||
go.opentelemetry.io/collector/pdata v1.10.0/go.mod h1:IHxHsp+Jq/xfjORQMDJjSH6jvedOSTOyu3nbxqhWSYE=
|
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.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA=
|
||||||
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/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 h1:4zaVLcJ5mvYw0vlk63TX62qS4qty/4jAY1BKZ1usu18=
|
||||||
go.opentelemetry.io/contrib/propagators/autoprop v0.53.0/go.mod h1:RPlvYtxp5D8PKnRzyPM+rwMQrvzdlfA49Sgworkg7aQ=
|
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=
|
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.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.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k=
|
||||||
google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
|
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.66.2 h1:3QdXkuq3Bkh7w+ywLdLvM56cmGvQHUMZpiCzt6Rqaoo=
|
||||||
google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0=
|
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/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-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
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/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/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/gentleman.v1 v1.0.4/go.mod h1:JYuHVdFzS4MKOXe0o+chKJ4hCe6tqKKw9XH9YP6WFrg=
|
||||||
gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE=
|
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-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.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
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.31.1 h1:Xe1hX/fPW3PXYYv8BlozYqw63ytA92snr96zMW9gWTU=
|
||||||
k8s.io/api v0.30.0/go.mod h1:OPlaYhoHs8EQ1ql0R/TsUgaRPhpKNxIMrKQfWUp8QSE=
|
k8s.io/api v0.31.1/go.mod h1:sbN1g6eY6XVLeqNsZGLnI5FwVseTrZX7Fv3O26rhAaI=
|
||||||
k8s.io/apiextensions-apiserver v0.30.0 h1:jcZFKMqnICJfRxTgnC4E+Hpcq8UEhT8B2lhBcQ+6uAs=
|
k8s.io/apiextensions-apiserver v0.31.1 h1:L+hwULvXx+nvTYX/MKM3kKMZyei+UiSXQWciX/N6E40=
|
||||||
k8s.io/apiextensions-apiserver v0.30.0/go.mod h1:N9ogQFGcrbWqAY9p2mUAL5mGxsLqwgtUce127VtRX5Y=
|
k8s.io/apiextensions-apiserver v0.31.1/go.mod h1:tWMPR3sgW+jsl2xm9v7lAyRF1rYEK71i9G5dRtkknoQ=
|
||||||
k8s.io/apimachinery v0.30.0 h1:qxVPsyDM5XS96NIh9Oj6LavoVFYff/Pon9cZeDIkHHA=
|
k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U=
|
||||||
k8s.io/apimachinery v0.30.0/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc=
|
k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
|
||||||
k8s.io/client-go v0.30.0 h1:sB1AGGlhY/o7KCyCEQ0bPWzYDL0pwOZO4vAtTSh/gJQ=
|
k8s.io/client-go v0.31.1 h1:f0ugtWSbWpxHR7sjVpQwuvw9a3ZKLXX0u0itkFXufb0=
|
||||||
k8s.io/client-go v0.30.0/go.mod h1:g7li5O5256qe6TYdAMyX/otJqMhIiGgTapdLchhmOaY=
|
k8s.io/client-go v0.31.1/go.mod h1:sKI8871MJN2OyeqRlmA4W4KM9KBdBUpDLu/43eGemCg=
|
||||||
k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw=
|
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||||
k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
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 h1:Q8Z7VlGhcJgBHJHYugJ/K/7iB8a2eSxCyxdVjJp+lLY=
|
||||||
k8s.io/kube-openapi v0.0.0-20240423202451-8948a665c108/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98=
|
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-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A=
|
||||||
k8s.io/utils v0.0.0-20240423183400-0849a56e8f22/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
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 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8=
|
||||||
mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE=
|
mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE=
|
||||||
nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
|
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=
|
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 h1:Z7jKuX784TQSUL1TIyeuF7j8KXZ4RtSX0YgtjKcSTME=
|
||||||
sigs.k8s.io/controller-runtime v0.18.0/go.mod h1:tuAt1+wbVsXIT8lPtk5RURxqAnq7xkpv2Mhttslg7Hw=
|
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.2.0-rc2 h1:v7V7JzaBuzwOLWWyyqlkqiqBi3ANBuZGV+uyyKzwmE8=
|
||||||
sigs.k8s.io/gateway-api v1.1.0/go.mod h1:ZH4lHrL2sDi0FHZ9jjneb8kKnGzFWyrTya35sWUTrRs=
|
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 h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
|
||||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
|
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=
|
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4=
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
apiVersion: gateway.networking.k8s.io/v1alpha1
|
apiVersion: gateway.networking.k8s.io/v1
|
||||||
date: '-'
|
date: '-'
|
||||||
gatewayAPIChannel: experimental
|
gatewayAPIChannel: experimental
|
||||||
gatewayAPIVersion: v1.1.0
|
gatewayAPIVersion: v1.2.0-rc2
|
||||||
implementation:
|
implementation:
|
||||||
contact:
|
contact:
|
||||||
- '@traefik/maintainers'
|
- '@traefik/maintainers'
|
||||||
organization: traefik
|
organization: traefik
|
||||||
project: traefik
|
project: traefik
|
||||||
url: https://traefik.io/
|
url: https://traefik.io/
|
||||||
version: v3.1
|
version: v3.2
|
||||||
kind: ConformanceReport
|
kind: ConformanceReport
|
||||||
mode: default
|
mode: default
|
||||||
profiles:
|
profiles:
|
||||||
|
@ -30,10 +30,13 @@ profiles:
|
||||||
result: success
|
result: success
|
||||||
statistics:
|
statistics:
|
||||||
Failed: 0
|
Failed: 0
|
||||||
Passed: 10
|
Passed: 13
|
||||||
Skipped: 0
|
Skipped: 0
|
||||||
supportedFeatures:
|
supportedFeatures:
|
||||||
- GatewayPort8080
|
- GatewayPort8080
|
||||||
|
- HTTPRouteBackendProtocolH2C
|
||||||
|
- HTTPRouteBackendProtocolWebSocket
|
||||||
|
- HTTPRouteDestinationPortMatching
|
||||||
- HTTPRouteHostRewrite
|
- HTTPRouteHostRewrite
|
||||||
- HTTPRouteMethodMatching
|
- HTTPRouteMethodMatching
|
||||||
- HTTPRoutePathRedirect
|
- HTTPRoutePathRedirect
|
||||||
|
@ -44,6 +47,7 @@ profiles:
|
||||||
- HTTPRouteSchemeRedirect
|
- HTTPRouteSchemeRedirect
|
||||||
unsupportedFeatures:
|
unsupportedFeatures:
|
||||||
- GatewayHTTPListenerIsolation
|
- GatewayHTTPListenerIsolation
|
||||||
|
- GatewayInfrastructurePropagation
|
||||||
- GatewayStaticAddresses
|
- GatewayStaticAddresses
|
||||||
- HTTPRouteBackendRequestHeaderModification
|
- HTTPRouteBackendRequestHeaderModification
|
||||||
- HTTPRouteBackendTimeout
|
- HTTPRouteBackendTimeout
|
File diff suppressed because it is too large
Load diff
|
@ -1458,6 +1458,12 @@ spec:
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
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
|
type: object
|
||||||
requestHeaderName:
|
requestHeaderName:
|
||||||
description: RequestHeaderName defines the name of the header
|
description: RequestHeaderName defines the name of the header
|
||||||
|
@ -1491,6 +1497,11 @@ spec:
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
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
|
type: object
|
||||||
rejectStatusCode:
|
rejectStatusCode:
|
||||||
description: |-
|
description: |-
|
||||||
|
@ -1523,6 +1534,11 @@ spec:
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
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
|
type: object
|
||||||
sourceRange:
|
sourceRange:
|
||||||
description: SourceRange defines the set of allowed IPs (or ranges
|
description: SourceRange defines the set of allowed IPs (or ranges
|
||||||
|
@ -1691,6 +1707,12 @@ spec:
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
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
|
type: object
|
||||||
requestHeaderName:
|
requestHeaderName:
|
||||||
description: RequestHeaderName defines the name of the header
|
description: RequestHeaderName defines the name of the header
|
||||||
|
|
35
integration/fixtures/simple_fastproxy.toml
Normal file
35
integration/fixtures/simple_fastproxy.toml
Normal 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 }}"
|
|
@ -22,6 +22,7 @@ import (
|
||||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||||
"k8s.io/apimachinery/pkg/util/sets"
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
kclientset "k8s.io/client-go/kubernetes"
|
kclientset "k8s.io/client-go/kubernetes"
|
||||||
|
"k8s.io/client-go/rest"
|
||||||
"k8s.io/client-go/tools/clientcmd"
|
"k8s.io/client-go/tools/clientcmd"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
klog "sigs.k8s.io/controller-runtime/pkg/log"
|
klog "sigs.k8s.io/controller-runtime/pkg/log"
|
||||||
|
@ -50,6 +51,7 @@ type K8sConformanceSuite struct {
|
||||||
|
|
||||||
k3sContainer *k3s.K3sContainer
|
k3sContainer *k3s.K3sContainer
|
||||||
kubeClient client.Client
|
kubeClient client.Client
|
||||||
|
restConfig *rest.Config
|
||||||
clientSet *kclientset.Clientset
|
clientSet *kclientset.Clientset
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,7 +90,7 @@ func (s *K8sConformanceSuite) SetupSuite() {
|
||||||
|
|
||||||
s.k3sContainer, err = k3s.Run(ctx,
|
s.k3sContainer, err = k3s.Run(ctx,
|
||||||
k3sImage,
|
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/01-rbac.yml"),
|
||||||
k3s.WithManifest("./fixtures/k8s-conformance/02-traefik.yml"),
|
k3s.WithManifest("./fixtures/k8s-conformance/02-traefik.yml"),
|
||||||
network.WithNetwork(nil, s.network),
|
network.WithNetwork(nil, s.network),
|
||||||
|
@ -111,17 +113,17 @@ func (s *K8sConformanceSuite) SetupSuite() {
|
||||||
s.T().Fatal(err)
|
s.T().Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
restConfig, err := clientcmd.RESTConfigFromKubeConfig(kubeConfigYaml)
|
s.restConfig, err = clientcmd.RESTConfigFromKubeConfig(kubeConfigYaml)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.T().Fatalf("Error loading Kubernetes config: %v", err)
|
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 {
|
if err != nil {
|
||||||
s.T().Fatalf("Error initializing Kubernetes client: %v", err)
|
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 {
|
if err != nil {
|
||||||
s.T().Fatalf("Error initializing Kubernetes REST client: %v", err)
|
s.T().Fatalf("Error initializing Kubernetes REST client: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -183,6 +185,7 @@ func (s *K8sConformanceSuite) TestK8sGatewayAPIConformance() {
|
||||||
GatewayClassName: "traefik",
|
GatewayClassName: "traefik",
|
||||||
Debug: true,
|
Debug: true,
|
||||||
CleanupBaseResources: true,
|
CleanupBaseResources: true,
|
||||||
|
RestConfig: s.restConfig,
|
||||||
TimeoutConfig: config.DefaultTimeoutConfig(),
|
TimeoutConfig: config.DefaultTimeoutConfig(),
|
||||||
ManifestFS: []fs.FS{&conformance.Manifests},
|
ManifestFS: []fs.FS{&conformance.Manifests},
|
||||||
EnableAllSupportedFeatures: false,
|
EnableAllSupportedFeatures: false,
|
||||||
|
|
|
@ -65,6 +65,32 @@ func (s *SimpleSuite) TestSimpleDefaultConfig() {
|
||||||
require.NoError(s.T(), err)
|
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() {
|
func (s *SimpleSuite) TestWithWebConfig() {
|
||||||
s.cmdTraefik(withConfigFile("fixtures/simple_web.toml"))
|
s.cmdTraefik(withConfigFile("fixtures/simple_web.toml"))
|
||||||
|
|
||||||
|
|
57
pkg/config/dynamic/middleware_test.go
Normal file
57
pkg/config/dynamic/middleware_test.go
Normal 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
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
package dynamic
|
package dynamic
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -405,6 +406,8 @@ type IPStrategy struct {
|
||||||
Depth int `json:"depth,omitempty" toml:"depth,omitempty" yaml:"depth,omitempty" export:"true"`
|
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 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"`
|
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.
|
// 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.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{
|
return &ip.DepthStrategy{
|
||||||
Depth: s.Depth,
|
Depth: s.Depth,
|
||||||
|
IPv6Subnet: s.IPv6Subnet,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -433,7 +441,13 @@ func (s *IPStrategy) Get() (ip.Strategy, error) {
|
||||||
}, nil
|
}, 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
|
// +k8s:deepcopy-gen=true
|
||||||
|
|
|
@ -704,6 +704,11 @@ func (in *IPStrategy) DeepCopyInto(out *IPStrategy) {
|
||||||
*out = make([]string, len(*in))
|
*out = make([]string, len(*in))
|
||||||
copy(*out, *in)
|
copy(*out, *in)
|
||||||
}
|
}
|
||||||
|
if in.IPv6Subnet != nil {
|
||||||
|
in, out := &in.IPv6Subnet, &out.IPv6Subnet
|
||||||
|
*out = new(int)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -90,10 +90,12 @@ func TestDecodeConfiguration(t *testing.T) {
|
||||||
"traefik.http.middlewares.Middleware8.headers.stsseconds": "42",
|
"traefik.http.middlewares.Middleware8.headers.stsseconds": "42",
|
||||||
"traefik.http.middlewares.Middleware9.ipallowlist.ipstrategy.depth": "42",
|
"traefik.http.middlewares.Middleware9.ipallowlist.ipstrategy.depth": "42",
|
||||||
"traefik.http.middlewares.Middleware9.ipallowlist.ipstrategy.excludedips": "foobar, fiibar",
|
"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.Middleware9.ipallowlist.sourcerange": "foobar, fiibar",
|
||||||
"traefik.http.middlewares.Middleware10.inflightreq.amount": "42",
|
"traefik.http.middlewares.Middleware10.inflightreq.amount": "42",
|
||||||
"traefik.http.middlewares.Middleware10.inflightreq.sourcecriterion.ipstrategy.depth": "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.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.requestheadername": "foobar",
|
||||||
"traefik.http.middlewares.Middleware10.inflightreq.sourcecriterion.requesthost": "true",
|
"traefik.http.middlewares.Middleware10.inflightreq.sourcecriterion.requesthost": "true",
|
||||||
"traefik.http.middlewares.Middleware11.passtlsclientcert.info.notafter": "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.requesthost": "true",
|
||||||
"traefik.http.middlewares.Middleware12.ratelimit.sourcecriterion.ipstrategy.depth": "42",
|
"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.excludedips": "foobar, foobar",
|
||||||
|
"traefik.http.middlewares.Middleware12.ratelimit.sourcecriterion.ipstrategy.ipv6subnet": "42",
|
||||||
"traefik.http.middlewares.Middleware13.redirectregex.permanent": "true",
|
"traefik.http.middlewares.Middleware13.redirectregex.permanent": "true",
|
||||||
"traefik.http.middlewares.Middleware13.redirectregex.regex": "foobar",
|
"traefik.http.middlewares.Middleware13.redirectregex.regex": "foobar",
|
||||||
"traefik.http.middlewares.Middleware13.redirectregex.replacement": "foobar",
|
"traefik.http.middlewares.Middleware13.redirectregex.replacement": "foobar",
|
||||||
|
@ -392,6 +395,7 @@ func TestDecodeConfiguration(t *testing.T) {
|
||||||
IPStrategy: &dynamic.IPStrategy{
|
IPStrategy: &dynamic.IPStrategy{
|
||||||
Depth: 42,
|
Depth: 42,
|
||||||
ExcludedIPs: []string{"foobar", "fiibar"},
|
ExcludedIPs: []string{"foobar", "fiibar"},
|
||||||
|
IPv6Subnet: intPtr(42),
|
||||||
},
|
},
|
||||||
RequestHeaderName: "foobar",
|
RequestHeaderName: "foobar",
|
||||||
RequestHost: true,
|
RequestHost: true,
|
||||||
|
@ -437,6 +441,7 @@ func TestDecodeConfiguration(t *testing.T) {
|
||||||
IPStrategy: &dynamic.IPStrategy{
|
IPStrategy: &dynamic.IPStrategy{
|
||||||
Depth: 42,
|
Depth: 42,
|
||||||
ExcludedIPs: []string{"foobar", "foobar"},
|
ExcludedIPs: []string{"foobar", "foobar"},
|
||||||
|
IPv6Subnet: intPtr(42),
|
||||||
},
|
},
|
||||||
RequestHeaderName: "foobar",
|
RequestHeaderName: "foobar",
|
||||||
RequestHost: true,
|
RequestHost: true,
|
||||||
|
@ -648,6 +653,7 @@ func TestDecodeConfiguration(t *testing.T) {
|
||||||
"foobar",
|
"foobar",
|
||||||
"fiibar",
|
"fiibar",
|
||||||
},
|
},
|
||||||
|
IPv6Subnet: intPtr(42),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -913,6 +919,7 @@ func TestEncodeConfiguration(t *testing.T) {
|
||||||
IPStrategy: &dynamic.IPStrategy{
|
IPStrategy: &dynamic.IPStrategy{
|
||||||
Depth: 42,
|
Depth: 42,
|
||||||
ExcludedIPs: []string{"foobar", "fiibar"},
|
ExcludedIPs: []string{"foobar", "fiibar"},
|
||||||
|
IPv6Subnet: intPtr(42),
|
||||||
},
|
},
|
||||||
RequestHeaderName: "foobar",
|
RequestHeaderName: "foobar",
|
||||||
RequestHost: true,
|
RequestHost: true,
|
||||||
|
@ -957,6 +964,7 @@ func TestEncodeConfiguration(t *testing.T) {
|
||||||
IPStrategy: &dynamic.IPStrategy{
|
IPStrategy: &dynamic.IPStrategy{
|
||||||
Depth: 42,
|
Depth: 42,
|
||||||
ExcludedIPs: []string{"foobar", "foobar"},
|
ExcludedIPs: []string{"foobar", "foobar"},
|
||||||
|
IPv6Subnet: intPtr(42),
|
||||||
},
|
},
|
||||||
RequestHeaderName: "foobar",
|
RequestHeaderName: "foobar",
|
||||||
RequestHost: true,
|
RequestHost: true,
|
||||||
|
@ -1176,6 +1184,7 @@ func TestEncodeConfiguration(t *testing.T) {
|
||||||
"foobar",
|
"foobar",
|
||||||
"fiibar",
|
"fiibar",
|
||||||
},
|
},
|
||||||
|
IPv6Subnet: intPtr(42),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1338,11 +1347,13 @@ func TestEncodeConfiguration(t *testing.T) {
|
||||||
"traefik.HTTP.Middlewares.Middleware8.Headers.STSSeconds": "42",
|
"traefik.HTTP.Middlewares.Middleware8.Headers.STSSeconds": "42",
|
||||||
"traefik.HTTP.Middlewares.Middleware9.IPAllowList.IPStrategy.Depth": "42",
|
"traefik.HTTP.Middlewares.Middleware9.IPAllowList.IPStrategy.Depth": "42",
|
||||||
"traefik.HTTP.Middlewares.Middleware9.IPAllowList.IPStrategy.ExcludedIPs": "foobar, fiibar",
|
"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.RejectStatusCode": "0",
|
||||||
"traefik.HTTP.Middlewares.Middleware9.IPAllowList.SourceRange": "foobar, fiibar",
|
"traefik.HTTP.Middlewares.Middleware9.IPAllowList.SourceRange": "foobar, fiibar",
|
||||||
"traefik.HTTP.Middlewares.Middleware10.InFlightReq.Amount": "42",
|
"traefik.HTTP.Middlewares.Middleware10.InFlightReq.Amount": "42",
|
||||||
"traefik.HTTP.Middlewares.Middleware10.InFlightReq.SourceCriterion.IPStrategy.Depth": "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.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.RequestHeaderName": "foobar",
|
||||||
"traefik.HTTP.Middlewares.Middleware10.InFlightReq.SourceCriterion.RequestHost": "true",
|
"traefik.HTTP.Middlewares.Middleware10.InFlightReq.SourceCriterion.RequestHost": "true",
|
||||||
"traefik.HTTP.Middlewares.Middleware11.PassTLSClientCert.Info.NotAfter": "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.RequestHost": "true",
|
||||||
"traefik.HTTP.Middlewares.Middleware12.RateLimit.SourceCriterion.IPStrategy.Depth": "42",
|
"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.ExcludedIPs": "foobar, foobar",
|
||||||
|
"traefik.HTTP.Middlewares.Middleware12.RateLimit.SourceCriterion.IPStrategy.IPv6Subnet": "42",
|
||||||
"traefik.HTTP.Middlewares.Middleware13.RedirectRegex.Regex": "foobar",
|
"traefik.HTTP.Middlewares.Middleware13.RedirectRegex.Regex": "foobar",
|
||||||
"traefik.HTTP.Middlewares.Middleware13.RedirectRegex.Replacement": "foobar",
|
"traefik.HTTP.Middlewares.Middleware13.RedirectRegex.Replacement": "foobar",
|
||||||
"traefik.HTTP.Middlewares.Middleware13.RedirectRegex.Permanent": "true",
|
"traefik.HTTP.Middlewares.Middleware13.RedirectRegex.Permanent": "true",
|
||||||
|
@ -1486,3 +1498,7 @@ func TestEncodeConfiguration(t *testing.T) {
|
||||||
}
|
}
|
||||||
assert.Equal(t, expected, labels)
|
assert.Equal(t, expected, labels)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func intPtr(value int) *int {
|
||||||
|
return &value
|
||||||
|
}
|
||||||
|
|
|
@ -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"`
|
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"`
|
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.
|
// 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"`
|
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"`
|
||||||
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package ip
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -16,7 +17,10 @@ type Strategy interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoteAddrStrategy a strategy that always return the remote address.
|
// 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.
|
// GetIP returns the selected IP.
|
||||||
func (s *RemoteAddrStrategy) GetIP(req *http.Request) string {
|
func (s *RemoteAddrStrategy) GetIP(req *http.Request) string {
|
||||||
|
@ -24,15 +28,22 @@ func (s *RemoteAddrStrategy) GetIP(req *http.Request) string {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return req.RemoteAddr
|
return req.RemoteAddr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.IPv6Subnet != nil {
|
||||||
|
return getIPv6SubnetIP(ip, *s.IPv6Subnet)
|
||||||
|
}
|
||||||
|
|
||||||
return ip
|
return ip
|
||||||
}
|
}
|
||||||
|
|
||||||
// DepthStrategy a strategy based on the depth inside the X-Forwarded-For from right to left.
|
// DepthStrategy a strategy based on the depth inside the X-Forwarded-For from right to left.
|
||||||
type DepthStrategy struct {
|
type DepthStrategy struct {
|
||||||
Depth int
|
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 {
|
func (s *DepthStrategy) GetIP(req *http.Request) string {
|
||||||
xff := req.Header.Get(xForwardedFor)
|
xff := req.Header.Get(xForwardedFor)
|
||||||
xffs := strings.Split(xff, ",")
|
xffs := strings.Split(xff, ",")
|
||||||
|
@ -40,7 +51,14 @@ func (s *DepthStrategy) GetIP(req *http.Request) string {
|
||||||
if len(xffs) < s.Depth {
|
if len(xffs) < s.Depth {
|
||||||
return ""
|
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.
|
// PoolStrategy is a strategy based on an IP Checker.
|
||||||
|
@ -72,3 +90,23 @@ func (s *PoolStrategy) GetIP(req *http.Request) string {
|
||||||
|
|
||||||
return ""
|
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()
|
||||||
|
}
|
||||||
|
|
|
@ -9,23 +9,81 @@ import (
|
||||||
"github.com/stretchr/testify/require"
|
"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) {
|
func TestRemoteAddrStrategy_GetIP(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
desc string
|
desc string
|
||||||
expected string
|
expected string
|
||||||
|
remoteAddr string
|
||||||
|
ipv6Subnet *int
|
||||||
}{
|
}{
|
||||||
|
// Valid IP format
|
||||||
{
|
{
|
||||||
desc: "Use RemoteAddr",
|
desc: "Use RemoteAddr, ipv4",
|
||||||
expected: "192.0.2.1",
|
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 {
|
for _, test := range testCases {
|
||||||
t.Run(test.desc, func(t *testing.T) {
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
strategy := RemoteAddrStrategy{}
|
strategy := RemoteAddrStrategy{
|
||||||
|
IPv6Subnet: test.ipv6Subnet,
|
||||||
|
}
|
||||||
req := httptest.NewRequest(http.MethodGet, "http://127.0.0.1", nil)
|
req := httptest.NewRequest(http.MethodGet, "http://127.0.0.1", nil)
|
||||||
|
if test.remoteAddr != "" {
|
||||||
|
req.RemoteAddr = test.remoteAddr
|
||||||
|
}
|
||||||
actual := strategy.GetIP(req)
|
actual := strategy.GetIP(req)
|
||||||
assert.Equal(t, test.expected, actual)
|
assert.Equal(t, test.expected, actual)
|
||||||
})
|
})
|
||||||
|
@ -38,6 +96,7 @@ func TestDepthStrategy_GetIP(t *testing.T) {
|
||||||
depth int
|
depth int
|
||||||
xForwardedFor string
|
xForwardedFor string
|
||||||
expected string
|
expected string
|
||||||
|
ipv6Subnet *int
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
desc: "Use depth",
|
desc: "Use depth",
|
||||||
|
@ -57,13 +116,30 @@ func TestDepthStrategy_GetIP(t *testing.T) {
|
||||||
xForwardedFor: "10.0.0.2,10.0.0.1",
|
xForwardedFor: "10.0.0.2,10.0.0.1",
|
||||||
expected: "10.0.0.2",
|
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 {
|
for _, test := range testCases {
|
||||||
t.Run(test.desc, func(t *testing.T) {
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
t.Parallel()
|
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 := httptest.NewRequest(http.MethodGet, "http://127.0.0.1", nil)
|
||||||
req.Header.Set(xForwardedFor, test.xForwardedFor)
|
req.Header.Set(xForwardedFor, test.xForwardedFor)
|
||||||
actual := strategy.GetIP(req)
|
actual := strategy.GetIP(req)
|
||||||
|
@ -121,3 +197,7 @@ func TestTrustedIPsStrategy_GetIP(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func intPtr(value int) *int {
|
||||||
|
return &value
|
||||||
|
}
|
||||||
|
|
|
@ -207,7 +207,7 @@ func newOpenTelemetryMeterProvider(ctx context.Context, config *types.OTLP) (*sd
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := resource.New(ctx,
|
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.WithAttributes(semconv.ServiceVersionKey.String(version.Version)),
|
||||||
resource.WithFromEnv(),
|
resource.WithFromEnv(),
|
||||||
resource.WithTelemetrySDK(),
|
resource.WithTelemetrySDK(),
|
||||||
|
|
|
@ -282,6 +282,21 @@ func TestOpenTelemetry_GaugeCollectorSet(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOpenTelemetry(t *testing.T) {
|
func TestOpenTelemetry(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
desc string
|
||||||
|
serviceName string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "default",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "custom-service-name",
|
||||||
|
serviceName: "custom-service-name",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
c := make(chan *string, 5)
|
c := make(chan *string, 5)
|
||||||
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -317,6 +332,12 @@ func TestOpenTelemetry(t *testing.T) {
|
||||||
}
|
}
|
||||||
cfg.PushInterval = ptypes.Duration(10 * time.Millisecond)
|
cfg.PushInterval = ptypes.Duration(10 * time.Millisecond)
|
||||||
|
|
||||||
|
wantServiceName := "traefik"
|
||||||
|
if test.serviceName != "" {
|
||||||
|
cfg.ServiceName = test.serviceName
|
||||||
|
wantServiceName = test.serviceName
|
||||||
|
}
|
||||||
|
|
||||||
registry := RegisterOpenTelemetry(context.Background(), &cfg)
|
registry := RegisterOpenTelemetry(context.Background(), &cfg)
|
||||||
require.NotNil(t, registry)
|
require.NotNil(t, registry)
|
||||||
|
|
||||||
|
@ -325,7 +346,7 @@ func TestOpenTelemetry(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
expected := []string{
|
expected := []string{
|
||||||
`({"key":"service.name","value":{"stringValue":"traefik"}})`,
|
`({"key":"service.name","value":{"stringValue":"` + wantServiceName + `"}})`,
|
||||||
`({"key":"service.version","value":{"stringValue":"` + version.Version + `"}})`,
|
`({"key":"service.version","value":{"stringValue":"` + version.Version + `"}})`,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -427,6 +448,8 @@ func TestOpenTelemetry(t *testing.T) {
|
||||||
registry.EntryPointReqDurationHistogram().With("entrypoint", "myEntrypoint").Observe(20000)
|
registry.EntryPointReqDurationHistogram().With("entrypoint", "myEntrypoint").Observe(20000)
|
||||||
|
|
||||||
tryAssertMessage(t, c, expectedEntryPointReqDuration)
|
tryAssertMessage(t, c, expectedEntryPointReqDuration)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func assertMessage(t *testing.T, msg string, expected []string) {
|
func assertMessage(t *testing.T, msg string, expected []string) {
|
||||||
|
|
|
@ -2,22 +2,23 @@ package gateway
|
||||||
|
|
||||||
import "sigs.k8s.io/gateway-api/pkg/features"
|
import "sigs.k8s.io/gateway-api/pkg/features"
|
||||||
|
|
||||||
func SupportedFeatures() []features.SupportedFeature {
|
func SupportedFeatures() []features.FeatureName {
|
||||||
return []features.SupportedFeature{
|
return []features.FeatureName{
|
||||||
features.SupportGateway,
|
features.GatewayFeature.Name,
|
||||||
features.SupportGatewayPort8080,
|
features.GatewayPort8080Feature.Name,
|
||||||
features.SupportGRPCRoute,
|
features.GRPCRouteFeature.Name,
|
||||||
features.SupportHTTPRoute,
|
features.HTTPRouteFeature.Name,
|
||||||
features.SupportHTTPRouteQueryParamMatching,
|
features.HTTPRouteQueryParamMatchingFeature.Name,
|
||||||
features.SupportHTTPRouteMethodMatching,
|
features.HTTPRouteMethodMatchingFeature.Name,
|
||||||
features.SupportHTTPRoutePortRedirect,
|
features.HTTPRoutePortRedirectFeature.Name,
|
||||||
features.SupportHTTPRouteSchemeRedirect,
|
features.HTTPRouteSchemeRedirectFeature.Name,
|
||||||
features.SupportHTTPRouteHostRewrite,
|
features.HTTPRouteHostRewriteFeature.Name,
|
||||||
features.SupportHTTPRoutePathRewrite,
|
features.HTTPRoutePathRewriteFeature.Name,
|
||||||
features.SupportHTTPRoutePathRedirect,
|
features.HTTPRoutePathRedirectFeature.Name,
|
||||||
features.SupportHTTPRouteResponseHeaderModification,
|
features.HTTPRouteResponseHeaderModificationFeature.Name,
|
||||||
features.SupportTLSRoute,
|
features.HTTPRouteBackendProtocolH2CFeature.Name,
|
||||||
features.SupportHTTPRouteBackendProtocolH2C,
|
features.HTTPRouteBackendProtocolWebSocketFeature.Name,
|
||||||
features.SupportHTTPRouteBackendProtocolWebSocket,
|
features.HTTPRouteDestinationPortMatchingFeature.Name,
|
||||||
|
features.TLSRouteFeature.Name,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,17 +49,16 @@ func (p *Provider) loadGRPCRoutes(ctx context.Context, gatewayListeners []gatewa
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, listener := range gatewayListeners {
|
for _, listener := range gatewayListeners {
|
||||||
if !matchListener(listener, route.Namespace, parentRef) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
accepted := true
|
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))
|
parentStatus.Conditions = updateRouteConditionAccepted(parentStatus.Conditions, string(gatev1.RouteReasonNotAllowedByListeners))
|
||||||
accepted = false
|
accepted = false
|
||||||
}
|
}
|
||||||
hostnames, ok := findMatchingHostnames(listener.Hostname, route.Spec.Hostnames)
|
hostnames, ok := findMatchingHostnames(listener.Hostname, route.Spec.Hostnames)
|
||||||
if !ok {
|
if accepted && !ok {
|
||||||
parentStatus.Conditions = updateRouteConditionAccepted(parentStatus.Conditions, string(gatev1.RouteReasonNoMatchingListenerHostname))
|
parentStatus.Conditions = updateRouteConditionAccepted(parentStatus.Conditions, string(gatev1.RouteReasonNoMatchingListenerHostname))
|
||||||
accepted = false
|
accepted = false
|
||||||
}
|
}
|
||||||
|
@ -396,10 +395,10 @@ func buildGRPCMethodRule(method *gatev1.GRPCMethodMatch) string {
|
||||||
func buildGRPCHeaderRules(headers []gatev1.GRPCHeaderMatch) []string {
|
func buildGRPCHeaderRules(headers []gatev1.GRPCHeaderMatch) []string {
|
||||||
var rules []string
|
var rules []string
|
||||||
for _, header := range headers {
|
for _, header := range headers {
|
||||||
switch ptr.Deref(header.Type, gatev1.HeaderMatchExact) {
|
switch ptr.Deref(header.Type, gatev1.GRPCHeaderMatchExact) {
|
||||||
case gatev1.HeaderMatchExact:
|
case gatev1.GRPCHeaderMatchExact:
|
||||||
rules = append(rules, fmt.Sprintf("Header(`%s`,`%s`)", header.Name, header.Value))
|
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))
|
rules = append(rules, fmt.Sprintf("HeaderRegexp(`%s`,`%s`)", header.Name, header.Value))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,7 +50,7 @@ func Test_buildGRPCMatchRule(t *testing.T) {
|
||||||
},
|
},
|
||||||
Headers: []gatev1.GRPCHeaderMatch{
|
Headers: []gatev1.GRPCHeaderMatch{
|
||||||
{
|
{
|
||||||
Type: ptr.To(gatev1.HeaderMatchExact),
|
Type: ptr.To(gatev1.GRPCHeaderMatchExact),
|
||||||
Name: "foo",
|
Name: "foo",
|
||||||
Value: "bar",
|
Value: "bar",
|
||||||
},
|
},
|
||||||
|
@ -70,7 +70,7 @@ func Test_buildGRPCMatchRule(t *testing.T) {
|
||||||
},
|
},
|
||||||
Headers: []gatev1.GRPCHeaderMatch{
|
Headers: []gatev1.GRPCHeaderMatch{
|
||||||
{
|
{
|
||||||
Type: ptr.To(gatev1.HeaderMatchExact),
|
Type: ptr.To(gatev1.GRPCHeaderMatchExact),
|
||||||
Name: "foo",
|
Name: "foo",
|
||||||
Value: "bar",
|
Value: "bar",
|
||||||
},
|
},
|
||||||
|
@ -177,7 +177,7 @@ func Test_buildGRPCHeaderRules(t *testing.T) {
|
||||||
desc: "One exact match type",
|
desc: "One exact match type",
|
||||||
headers: []gatev1.GRPCHeaderMatch{
|
headers: []gatev1.GRPCHeaderMatch{
|
||||||
{
|
{
|
||||||
Type: ptr.To(gatev1.HeaderMatchExact),
|
Type: ptr.To(gatev1.GRPCHeaderMatchExact),
|
||||||
Name: "foo",
|
Name: "foo",
|
||||||
Value: "bar",
|
Value: "bar",
|
||||||
},
|
},
|
||||||
|
@ -188,7 +188,7 @@ func Test_buildGRPCHeaderRules(t *testing.T) {
|
||||||
desc: "One regexp match type",
|
desc: "One regexp match type",
|
||||||
headers: []gatev1.GRPCHeaderMatch{
|
headers: []gatev1.GRPCHeaderMatch{
|
||||||
{
|
{
|
||||||
Type: ptr.To(gatev1.HeaderMatchRegularExpression),
|
Type: ptr.To(gatev1.GRPCHeaderMatchRegularExpression),
|
||||||
Name: "foo",
|
Name: "foo",
|
||||||
Value: ".*",
|
Value: ".*",
|
||||||
},
|
},
|
||||||
|
@ -199,12 +199,12 @@ func Test_buildGRPCHeaderRules(t *testing.T) {
|
||||||
desc: "One exact and regexp match type",
|
desc: "One exact and regexp match type",
|
||||||
headers: []gatev1.GRPCHeaderMatch{
|
headers: []gatev1.GRPCHeaderMatch{
|
||||||
{
|
{
|
||||||
Type: ptr.To(gatev1.HeaderMatchExact),
|
Type: ptr.To(gatev1.GRPCHeaderMatchExact),
|
||||||
Name: "foo",
|
Name: "foo",
|
||||||
Value: "bar",
|
Value: "bar",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Type: ptr.To(gatev1.HeaderMatchRegularExpression),
|
Type: ptr.To(gatev1.GRPCHeaderMatchRegularExpression),
|
||||||
Name: "foo",
|
Name: "foo",
|
||||||
Value: ".*",
|
Value: ".*",
|
||||||
},
|
},
|
||||||
|
|
|
@ -53,17 +53,16 @@ func (p *Provider) loadHTTPRoutes(ctx context.Context, gatewayListeners []gatewa
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, listener := range gatewayListeners {
|
for _, listener := range gatewayListeners {
|
||||||
if !matchListener(listener, route.Namespace, parentRef) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
accepted := true
|
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))
|
parentStatus.Conditions = updateRouteConditionAccepted(parentStatus.Conditions, string(gatev1.RouteReasonNotAllowedByListeners))
|
||||||
accepted = false
|
accepted = false
|
||||||
}
|
}
|
||||||
hostnames, ok := findMatchingHostnames(listener.Hostname, route.Spec.Hostnames)
|
hostnames, ok := findMatchingHostnames(listener.Hostname, route.Spec.Hostnames)
|
||||||
if !ok {
|
if accepted && !ok {
|
||||||
parentStatus.Conditions = updateRouteConditionAccepted(parentStatus.Conditions, string(gatev1.RouteReasonNoMatchingListenerHostname))
|
parentStatus.Conditions = updateRouteConditionAccepted(parentStatus.Conditions, string(gatev1.RouteReasonNoMatchingListenerHostname))
|
||||||
accepted = false
|
accepted = false
|
||||||
}
|
}
|
||||||
|
|
|
@ -112,6 +112,7 @@ type ExtensionBuilderRegistry interface {
|
||||||
type gatewayListener struct {
|
type gatewayListener struct {
|
||||||
Name string
|
Name string
|
||||||
|
|
||||||
|
Port gatev1.PortNumber
|
||||||
Protocol gatev1.ProtocolType
|
Protocol gatev1.ProtocolType
|
||||||
TLS *gatev1.GatewayTLSConfig
|
TLS *gatev1.GatewayTLSConfig
|
||||||
Hostname *gatev1.Hostname
|
Hostname *gatev1.Hostname
|
||||||
|
@ -317,10 +318,14 @@ func (p *Provider) loadConfigurationFromGateways(ctx context.Context) *dynamic.C
|
||||||
}
|
}
|
||||||
|
|
||||||
var supportedFeatures []gatev1.SupportedFeature
|
var supportedFeatures []gatev1.SupportedFeature
|
||||||
|
if p.ExperimentalChannel {
|
||||||
for _, feature := range SupportedFeatures() {
|
for _, feature := range SupportedFeatures() {
|
||||||
supportedFeatures = append(supportedFeatures, gatev1.SupportedFeature(feature))
|
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{}{}
|
gatewayClassNames := map[string]struct{}{}
|
||||||
for _, gatewayClass := range gatewayClasses {
|
for _, gatewayClass := range gatewayClasses {
|
||||||
|
@ -425,6 +430,7 @@ func (p *Provider) loadGatewayListeners(ctx context.Context, gateway *gatev1.Gat
|
||||||
GWName: gateway.Name,
|
GWName: gateway.Name,
|
||||||
GWNamespace: gateway.Namespace,
|
GWNamespace: gateway.Namespace,
|
||||||
GWGeneration: gateway.Generation,
|
GWGeneration: gateway.Generation,
|
||||||
|
Port: listener.Port,
|
||||||
Protocol: listener.Protocol,
|
Protocol: listener.Protocol,
|
||||||
TLS: listener.TLS,
|
TLS: listener.TLS,
|
||||||
Hostname: listener.Hostname,
|
Hostname: listener.Hostname,
|
||||||
|
@ -1114,6 +1120,10 @@ func matchListener(listener gatewayListener, routeNamespace string, parentRef ga
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if parentRef.Port != nil && *parentRef.Port != listener.Port {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -49,12 +49,11 @@ func (p *Provider) loadTCPRoutes(ctx context.Context, gatewayListeners []gateway
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, listener := range gatewayListeners {
|
for _, listener := range gatewayListeners {
|
||||||
if !matchListener(listener, route.Namespace, parentRef) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
accepted := true
|
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))
|
parentStatus.Conditions = updateRouteConditionAccepted(parentStatus.Conditions, string(gatev1.RouteReasonNotAllowedByListeners))
|
||||||
accepted = false
|
accepted = false
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,17 +49,16 @@ func (p *Provider) loadTLSRoutes(ctx context.Context, gatewayListeners []gateway
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, listener := range gatewayListeners {
|
for _, listener := range gatewayListeners {
|
||||||
if !matchListener(listener, route.Namespace, parentRef) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
accepted := true
|
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))
|
parentStatus.Conditions = updateRouteConditionAccepted(parentStatus.Conditions, string(gatev1.RouteReasonNotAllowedByListeners))
|
||||||
accepted = false
|
accepted = false
|
||||||
}
|
}
|
||||||
hostnames, ok := findMatchingHostnames(listener.Hostname, route.Spec.Hostnames)
|
hostnames, ok := findMatchingHostnames(listener.Hostname, route.Spec.Hostnames)
|
||||||
if !ok {
|
if accepted && !ok {
|
||||||
parentStatus.Conditions = updateRouteConditionAccepted(parentStatus.Conditions, string(gatev1.RouteReasonNoMatchingListenerHostname))
|
parentStatus.Conditions = updateRouteConditionAccepted(parentStatus.Conditions, string(gatev1.RouteReasonNoMatchingListenerHostname))
|
||||||
accepted = false
|
accepted = false
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
|
|
||||||
"github.com/cenkalti/backoff/v4"
|
"github.com/cenkalti/backoff/v4"
|
||||||
"github.com/hashicorp/nomad/api"
|
"github.com/hashicorp/nomad/api"
|
||||||
|
"github.com/mitchellh/hashstructure"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
ptypes "github.com/traefik/paerser/types"
|
ptypes "github.com/traefik/paerser/types"
|
||||||
"github.com/traefik/traefik/v3/pkg/config/dynamic"
|
"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"`
|
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"`
|
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"`
|
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.
|
// SetDefaults sets the default values for the Nomad Traefik Provider Configuration.
|
||||||
|
@ -117,7 +120,7 @@ func (c *Configuration) SetDefaults() {
|
||||||
c.ExposedByDefault = true
|
c.ExposedByDefault = true
|
||||||
c.RefreshInterval = ptypes.Duration(15 * time.Second)
|
c.RefreshInterval = ptypes.Duration(15 * time.Second)
|
||||||
c.DefaultRule = defaultTemplateRule
|
c.DefaultRule = defaultTemplateRule
|
||||||
c.AllowEmptyServices = false
|
c.ThrottleDuration = ptypes.Duration(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
type EndpointConfig struct {
|
type EndpointConfig struct {
|
||||||
|
@ -139,6 +142,8 @@ type Provider struct {
|
||||||
namespace string
|
namespace string
|
||||||
client *api.Client // client for Nomad API
|
client *api.Client // client for Nomad API
|
||||||
defaultRuleTpl *template.Template // default routing rule
|
defaultRuleTpl *template.Template // default routing rule
|
||||||
|
|
||||||
|
lastConfiguration safe.Safe
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDefaults sets the default values for the Nomad Traefik Provider.
|
// 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")
|
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)
|
defaultRuleTpl, err := provider.MakeDefaultRuleTemplate(p.DefaultRule, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error while parsing default rule: %w", err)
|
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)
|
ctx, cancel := context.WithCancel(ctxLog)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// load initial configuration
|
serviceEventsChan, err := p.pollOrWatch(ctx)
|
||||||
if err := p.loadConfiguration(ctx, configurationChan); err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to load initial nomad services: %w", err)
|
return fmt.Errorf("watching Nomad events: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// issue periodic refreshes in the background
|
throttleDuration := time.Duration(p.ThrottleDuration)
|
||||||
// (Nomad does not support Watch style observations)
|
throttledChan := throttleEvents(ctx, throttleDuration, pool, serviceEventsChan)
|
||||||
ticker := time.NewTicker(time.Duration(p.RefreshInterval))
|
if throttledChan != nil {
|
||||||
defer ticker.Stop()
|
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 {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return nil
|
return nil
|
||||||
case <-ticker.C:
|
case event := <-serviceEventsChan:
|
||||||
|
conf, err = p.loadConfiguration(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("loading configuration: %w", err)
|
||||||
}
|
}
|
||||||
// load services due to refresh
|
updated, err := p.updateLastConfiguration(conf)
|
||||||
if err := p.loadConfiguration(ctx, configurationChan); err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to refresh nomad services: %w", err)
|
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) {
|
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(
|
if retryErr := backoff.RetryNotify(
|
||||||
|
@ -223,27 +263,70 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
|
||||||
return nil
|
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 items []item
|
||||||
var err error
|
var err error
|
||||||
if p.AllowEmptyServices {
|
if p.AllowEmptyServices {
|
||||||
items, err = p.getNomadServiceDataWithEmptyServices(ctx)
|
items, err = p.getNomadServiceDataWithEmptyServices(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
items, err = p.getNomadServiceData(ctx)
|
items, err = p.getNomadServiceData(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
configurationC <- dynamic.Message{
|
return p.buildConfig(ctx, items), nil
|
||||||
ProviderName: p.name,
|
}
|
||||||
Configuration: p.buildConfig(ctx, items),
|
|
||||||
|
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) {
|
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)
|
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
129
pkg/proxy/fast/builder.go
Normal 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
163
pkg/proxy/fast/connpool.go
Normal 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,
|
||||||
|
})
|
||||||
|
}
|
184
pkg/proxy/fast/connpool_test.go
Normal file
184
pkg/proxy/fast/connpool_test.go
Normal 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
195
pkg/proxy/fast/dialer.go
Normal 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
553
pkg/proxy/fast/proxy.go
Normal 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"))
|
||||||
|
}
|
311
pkg/proxy/fast/proxy_test.go
Normal file
311
pkg/proxy/fast/proxy_test.go
Normal 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
|
||||||
|
}
|
693
pkg/proxy/fast/proxy_websocket_test.go
Normal file
693
pkg/proxy/fast/proxy_websocket_test.go
Normal 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
104
pkg/proxy/fast/upgrade.go
Normal 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"))
|
||||||
|
}
|
|
@ -1,23 +1,25 @@
|
||||||
package service
|
package httputil
|
||||||
|
|
||||||
import "sync"
|
import "sync"
|
||||||
|
|
||||||
const bufferPoolSize = 32 * 1024
|
const bufferSize = 32 * 1024
|
||||||
|
|
||||||
func newBufferPool() *bufferPool {
|
|
||||||
return &bufferPool{
|
|
||||||
pool: sync.Pool{
|
|
||||||
New: func() interface{} {
|
|
||||||
return make([]byte, bufferPoolSize)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type bufferPool struct {
|
type bufferPool struct {
|
||||||
pool sync.Pool
|
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 {
|
func (b *bufferPool) Get() []byte {
|
||||||
return b.pool.Get().([]byte)
|
return b.pool.Get().([]byte)
|
||||||
}
|
}
|
54
pkg/proxy/httputil/builder.go
Normal file
54
pkg/proxy/httputil/builder.go
Normal 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
|
||||||
|
}
|
56
pkg/proxy/httputil/builder_test.go
Normal file
56
pkg/proxy/httputil/builder_test.go
Normal 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")
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package service
|
package httputil
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
@ -23,6 +23,13 @@ type wrapper struct {
|
||||||
rt http.RoundTripper
|
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) {
|
func (t *wrapper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
var span trace.Span
|
var span trace.Span
|
||||||
|
@ -42,7 +49,7 @@ func (t *wrapper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
var headers http.Header
|
var headers http.Header
|
||||||
response, err := t.rt.RoundTrip(req)
|
response, err := t.rt.RoundTrip(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
statusCode = computeStatusCode(err)
|
statusCode = ComputeStatusCode(err)
|
||||||
}
|
}
|
||||||
if response != nil {
|
if response != nil {
|
||||||
statusCode = response.StatusCode
|
statusCode = response.StatusCode
|
||||||
|
@ -96,10 +103,3 @@ func (t *wrapper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
|
|
||||||
return response, err
|
return response, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func newObservabilityRoundTripper(semConvMetricRegistry *metrics.SemConvMetricsRegistry, rt http.RoundTripper) http.RoundTripper {
|
|
||||||
return &wrapper{
|
|
||||||
semConvMetricRegistry: semConvMetricRegistry,
|
|
||||||
rt: rt,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
package service
|
package httputil
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
|
@ -1,4 +1,4 @@
|
||||||
package service
|
package httputil
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
@ -27,7 +27,7 @@ func buildSingleHostProxy(target *url.URL, passHostHeader bool, flushInterval ti
|
||||||
Transport: roundTripper,
|
Transport: roundTripper,
|
||||||
FlushInterval: flushInterval,
|
FlushInterval: flushInterval,
|
||||||
BufferPool: bufferPool,
|
BufferPool: bufferPool,
|
||||||
ErrorHandler: errorHandler,
|
ErrorHandler: ErrorHandler,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,8 +93,9 @@ func isWebSocketUpgrade(req *http.Request) bool {
|
||||||
strings.EqualFold(req.Header.Get("Upgrade"), "websocket")
|
strings.EqualFold(req.Header.Get("Upgrade"), "websocket")
|
||||||
}
|
}
|
||||||
|
|
||||||
func errorHandler(w http.ResponseWriter, req *http.Request, err error) {
|
// ErrorHandler is the http.Handler called when something goes wrong when forwarding the request.
|
||||||
statusCode := computeStatusCode(err)
|
func ErrorHandler(w http.ResponseWriter, req *http.Request, err error) {
|
||||||
|
statusCode := ComputeStatusCode(err)
|
||||||
|
|
||||||
logger := log.Ctx(req.Context())
|
logger := log.Ctx(req.Context())
|
||||||
logger.Debug().Err(err).Msgf("%d %s", statusCode, statusText(statusCode))
|
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 {
|
switch {
|
||||||
case errors.Is(err, io.EOF):
|
case errors.Is(err, io.EOF):
|
||||||
return http.StatusBadGateway
|
return http.StatusBadGateway
|
|
@ -1,4 +1,4 @@
|
||||||
package service
|
package httputil
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
@ -8,13 +8,13 @@ import (
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
gorillawebsocket "github.com/gorilla/websocket"
|
gorillawebsocket "github.com/gorilla/websocket"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/traefik/traefik/v3/pkg/testhelpers"
|
||||||
"golang.org/x/net/websocket"
|
"golang.org/x/net/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -27,6 +27,7 @@ func TestWebSocketTCPClose(t *testing.T) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer c.Close()
|
defer c.Close()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
_, _, err := c.ReadMessage()
|
_, _, err := c.ReadMessage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -71,6 +72,7 @@ func TestWebSocketPingPong(t *testing.T) {
|
||||||
ws.SetPingHandler(func(appData string) error {
|
ws.SetPingHandler(func(appData string) error {
|
||||||
err = ws.WriteMessage(gorillawebsocket.PongMessage, []byte(appData+"Pong"))
|
err = ws.WriteMessage(gorillawebsocket.PongMessage, []byte(appData+"Pong"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -97,6 +99,7 @@ func TestWebSocketPingPong(t *testing.T) {
|
||||||
if data == "PingPong" {
|
if data == "PingPong" {
|
||||||
return goodErr
|
return goodErr
|
||||||
}
|
}
|
||||||
|
|
||||||
return badErr
|
return badErr
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -104,7 +107,6 @@ func TestWebSocketPingPong(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
_, _, err = conn.ReadMessage()
|
_, _, err = conn.ReadMessage()
|
||||||
|
|
||||||
if !errors.Is(err, goodErr) {
|
if !errors.Is(err, goodErr) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
@ -114,12 +116,10 @@ func TestWebSocketEcho(t *testing.T) {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.Handle("/ws", websocket.Handler(func(conn *websocket.Conn) {
|
mux.Handle("/ws", websocket.Handler(func(conn *websocket.Conn) {
|
||||||
msg := make([]byte, 4)
|
msg := make([]byte, 4)
|
||||||
_, err := conn.Read(msg)
|
n, err := conn.Read(msg)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
fmt.Println(string(msg))
|
_, err = conn.Write(msg[:n])
|
||||||
|
|
||||||
_, err = conn.Write(msg)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = conn.Close()
|
err = conn.Close()
|
||||||
|
@ -142,7 +142,10 @@ func TestWebSocketEcho(t *testing.T) {
|
||||||
err = conn.WriteMessage(gorillawebsocket.TextMessage, []byte("OK"))
|
err = conn.WriteMessage(gorillawebsocket.TextMessage, []byte("OK"))
|
||||||
require.NoError(t, err)
|
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()
|
err = conn.Close()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -178,11 +181,10 @@ func TestWebSocketPassHost(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := make([]byte, 4)
|
msg := make([]byte, 4)
|
||||||
_, err := conn.Read(msg)
|
n, err := conn.Read(msg)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
fmt.Println(string(msg))
|
_, err = conn.Write(msg[:n])
|
||||||
_, err = conn.Write(msg)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = conn.Close()
|
err = conn.Close()
|
||||||
|
@ -207,7 +209,10 @@ func TestWebSocketPassHost(t *testing.T) {
|
||||||
err = conn.WriteMessage(gorillawebsocket.TextMessage, []byte("OK"))
|
err = conn.WriteMessage(gorillawebsocket.TextMessage, []byte("OK"))
|
||||||
require.NoError(t, err)
|
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()
|
err = conn.Close()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -216,27 +221,8 @@ func TestWebSocketPassHost(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWebSocketServerWithoutCheckOrigin(t *testing.T) {
|
func TestWebSocketServerWithoutCheckOrigin(t *testing.T) {
|
||||||
upgrader := gorillawebsocket.Upgrader{CheckOrigin: func(r *http.Request) bool {
|
upgrader := gorillawebsocket.Upgrader{CheckOrigin: func(*http.Request) bool { return true }}
|
||||||
return true
|
srv := createServer(t, upgrader, func(*http.Request) {})
|
||||||
}}
|
|
||||||
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, http.DefaultTransport)
|
proxy := createProxyWithForwarder(t, srv.URL, http.DefaultTransport)
|
||||||
defer proxy.Close()
|
defer proxy.Close()
|
||||||
|
@ -254,25 +240,7 @@ func TestWebSocketServerWithoutCheckOrigin(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWebSocketRequestWithOrigin(t *testing.T) {
|
func TestWebSocketRequestWithOrigin(t *testing.T) {
|
||||||
upgrader := gorillawebsocket.Upgrader{}
|
srv := createServer(t, gorillawebsocket.Upgrader{}, func(*http.Request) {})
|
||||||
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, http.DefaultTransport)
|
proxy := createProxyWithForwarder(t, srv.URL, http.DefaultTransport)
|
||||||
defer proxy.Close()
|
defer proxy.Close()
|
||||||
|
@ -297,26 +265,9 @@ func TestWebSocketRequestWithOrigin(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWebSocketRequestWithQueryParams(t *testing.T) {
|
func TestWebSocketRequestWithQueryParams(t *testing.T) {
|
||||||
upgrader := gorillawebsocket.Upgrader{}
|
srv := createServer(t, gorillawebsocket.Upgrader{}, func(r *http.Request) {
|
||||||
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"))
|
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)
|
proxy := createProxyWithForwarder(t, srv.URL, http.DefaultTransport)
|
||||||
defer proxy.Close()
|
defer proxy.Close()
|
||||||
|
@ -341,11 +292,19 @@ func TestWebSocketRequestWithHeadersInResponseWriter(t *testing.T) {
|
||||||
srv := httptest.NewServer(mux)
|
srv := httptest.NewServer(mux)
|
||||||
defer srv.Close()
|
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) {
|
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")
|
w.Header().Set("HEADER-KEY", "HEADER-VALUE")
|
||||||
f.ServeHTTP(w, req)
|
p.ServeHTTP(w, req)
|
||||||
}))
|
}))
|
||||||
defer proxy.Close()
|
defer proxy.Close()
|
||||||
|
|
||||||
|
@ -363,26 +322,9 @@ func TestWebSocketRequestWithHeadersInResponseWriter(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWebSocketRequestWithEncodedChar(t *testing.T) {
|
func TestWebSocketRequestWithEncodedChar(t *testing.T) {
|
||||||
upgrader := gorillawebsocket.Upgrader{}
|
srv := createServer(t, gorillawebsocket.Upgrader{}, func(r *http.Request) {
|
||||||
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())
|
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)
|
proxy := createProxyWithForwarder(t, srv.URL, http.DefaultTransport)
|
||||||
defer proxy.Close()
|
defer proxy.Close()
|
||||||
|
@ -407,15 +349,23 @@ func TestWebSocketUpgradeFailed(t *testing.T) {
|
||||||
srv := httptest.NewServer(mux)
|
srv := httptest.NewServer(mux)
|
||||||
defer srv.Close()
|
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) {
|
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
path := req.URL.Path // keep the original path
|
path := req.URL.Path // keep the original path
|
||||||
|
|
||||||
if path == "/ws" {
|
if path == "/ws" {
|
||||||
// Set new backend URL
|
// Set new backend URL
|
||||||
req.URL = parseURI(t, srv.URL)
|
req.URL = testhelpers.MustParseURL(srv.URL)
|
||||||
req.URL.Path = path
|
req.URL.Path = path
|
||||||
f.ServeHTTP(w, req)
|
p.ServeHTTP(w, req)
|
||||||
} else {
|
} else {
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
}
|
}
|
||||||
|
@ -629,27 +579,60 @@ func (w *websocketRequest) open() (*websocket.Conn, net.Conn, error) {
|
||||||
return conn, client, 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 createProxyWithForwarder(t *testing.T, uri string, transport http.RoundTripper) *httptest.Server {
|
func createProxyWithForwarder(t *testing.T, uri string, transport http.RoundTripper) *httptest.Server {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
u := parseURI(t, uri)
|
u := testhelpers.MustParseURL(uri)
|
||||||
proxy := buildSingleHostProxy(u, true, 0, transport, nil)
|
|
||||||
|
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) {
|
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
|
// Set new backend URL
|
||||||
req.URL = u
|
req.URL = u
|
||||||
req.URL.Path = path
|
req.URL.Path = path
|
||||||
|
|
||||||
proxy.ServeHTTP(w, req)
|
p.ServeHTTP(w, req)
|
||||||
}))
|
}))
|
||||||
t.Cleanup(srv.Close)
|
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
|
return srv
|
||||||
}
|
}
|
61
pkg/proxy/smart_builder.go
Normal file
61
pkg/proxy/smart_builder.go
Normal 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)
|
||||||
|
}
|
113
pkg/proxy/smart_builder_test.go
Normal file
113
pkg/proxy/smart_builder_test.go
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,10 +2,12 @@ package router
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
@ -18,7 +20,7 @@ import (
|
||||||
"github.com/traefik/traefik/v3/pkg/server/middleware"
|
"github.com/traefik/traefik/v3/pkg/server/middleware"
|
||||||
"github.com/traefik/traefik/v3/pkg/server/service"
|
"github.com/traefik/traefik/v3/pkg/server/service"
|
||||||
"github.com/traefik/traefik/v3/pkg/testhelpers"
|
"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) {
|
func TestRouterManager_Get(t *testing.T) {
|
||||||
|
@ -309,11 +311,12 @@ func TestRouterManager_Get(t *testing.T) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
roundTripperManager := service.NewRoundTripperManager(nil)
|
transportManager := service.NewTransportManager(nil)
|
||||||
roundTripperManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
|
transportManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
|
||||||
serviceManager := service.NewManager(rtConf.Services, nil, nil, roundTripperManager)
|
|
||||||
|
serviceManager := service.NewManager(rtConf.Services, nil, nil, transportManager, proxyBuilderMock{})
|
||||||
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil)
|
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil)
|
||||||
tlsManager := tls.NewManager()
|
tlsManager := traefiktls.NewManager()
|
||||||
|
|
||||||
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager)
|
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager)
|
||||||
|
|
||||||
|
@ -340,7 +343,7 @@ func TestRuntimeConfiguration(t *testing.T) {
|
||||||
serviceConfig map[string]*dynamic.Service
|
serviceConfig map[string]*dynamic.Service
|
||||||
routerConfig map[string]*dynamic.Router
|
routerConfig map[string]*dynamic.Router
|
||||||
middlewareConfig map[string]*dynamic.Middleware
|
middlewareConfig map[string]*dynamic.Middleware
|
||||||
tlsOptions map[string]tls.Options
|
tlsOptions map[string]traefiktls.Options
|
||||||
expectedError int
|
expectedError int
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
|
@ -597,7 +600,7 @@ func TestRuntimeConfiguration(t *testing.T) {
|
||||||
TLS: &dynamic.RouterTLSConfig{},
|
TLS: &dynamic.RouterTLSConfig{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
tlsOptions: map[string]tls.Options{},
|
tlsOptions: map[string]traefiktls.Options{},
|
||||||
expectedError: 1,
|
expectedError: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -624,9 +627,9 @@ func TestRuntimeConfiguration(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
tlsOptions: map[string]tls.Options{
|
tlsOptions: map[string]traefiktls.Options{
|
||||||
"broken-tlsOption": {
|
"broken-tlsOption": {
|
||||||
ClientAuth: tls.ClientAuth{
|
ClientAuth: traefiktls.ClientAuth{
|
||||||
ClientAuthType: "foobar",
|
ClientAuthType: "foobar",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -655,9 +658,9 @@ func TestRuntimeConfiguration(t *testing.T) {
|
||||||
TLS: &dynamic.RouterTLSConfig{},
|
TLS: &dynamic.RouterTLSConfig{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
tlsOptions: map[string]tls.Options{
|
tlsOptions: map[string]traefiktls.Options{
|
||||||
"default": {
|
"default": {
|
||||||
ClientAuth: tls.ClientAuth{
|
ClientAuth: traefiktls.ClientAuth{
|
||||||
ClientAuthType: "foobar",
|
ClientAuthType: "foobar",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -682,11 +685,12 @@ func TestRuntimeConfiguration(t *testing.T) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
roundTripperManager := service.NewRoundTripperManager(nil)
|
transportManager := service.NewTransportManager(nil)
|
||||||
roundTripperManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
|
transportManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
|
||||||
serviceManager := service.NewManager(rtConf.Services, nil, nil, roundTripperManager)
|
|
||||||
|
serviceManager := service.NewManager(rtConf.Services, nil, nil, transportManager, proxyBuilderMock{})
|
||||||
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil)
|
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil)
|
||||||
tlsManager := tls.NewManager()
|
tlsManager := traefiktls.NewManager()
|
||||||
tlsManager.UpdateConfigs(context.Background(), nil, test.tlsOptions, nil)
|
tlsManager.UpdateConfigs(context.Background(), nil, test.tlsOptions, nil)
|
||||||
|
|
||||||
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager)
|
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager)
|
||||||
|
@ -759,11 +763,12 @@ func TestProviderOnMiddlewares(t *testing.T) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
roundTripperManager := service.NewRoundTripperManager(nil)
|
transportManager := service.NewTransportManager(nil)
|
||||||
roundTripperManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
|
transportManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
|
||||||
serviceManager := service.NewManager(rtConf.Services, nil, nil, roundTripperManager)
|
|
||||||
|
serviceManager := service.NewManager(rtConf.Services, nil, nil, transportManager, nil)
|
||||||
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil)
|
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil)
|
||||||
tlsManager := tls.NewManager()
|
tlsManager := traefiktls.NewManager()
|
||||||
|
|
||||||
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager)
|
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)
|
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
|
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
|
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 {
|
type staticTransport struct {
|
||||||
res *http.Response
|
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)
|
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil)
|
||||||
tlsManager := tls.NewManager()
|
tlsManager := traefiktls.NewManager()
|
||||||
|
|
||||||
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager)
|
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()
|
w := httptest.NewRecorder()
|
||||||
req := testhelpers.MustNewRequest(http.MethodGet, "http://foo.bar/", nil)
|
req := testhelpers.MustNewRequest(http.MethodGet, "http://foo.bar/", nil)
|
||||||
|
|
||||||
|
@ -881,3 +894,13 @@ func BenchmarkService(b *testing.B) {
|
||||||
handler.ServeHTTP(w, req)
|
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")
|
||||||
|
}
|
||||||
|
|
|
@ -3,7 +3,9 @@ package server
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/traefik/traefik/v3/pkg/config/dynamic"
|
"github.com/traefik/traefik/v3/pkg/config/dynamic"
|
||||||
|
@ -48,9 +50,10 @@ func TestReuseService(t *testing.T) {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
roundTripperManager := service.NewRoundTripperManager(nil)
|
transportManager := service.NewTransportManager(nil)
|
||||||
roundTripperManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
|
transportManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
|
||||||
managerFactory := service.NewManagerFactory(staticConfig, nil, nil, roundTripperManager, nil)
|
|
||||||
|
managerFactory := service.NewManagerFactory(staticConfig, nil, nil, transportManager, proxyBuilderMock{}, nil)
|
||||||
tlsManager := tls.NewManager()
|
tlsManager := tls.NewManager()
|
||||||
|
|
||||||
dialerManager := tcp.NewDialerManager(nil)
|
dialerManager := tcp.NewDialerManager(nil)
|
||||||
|
@ -184,9 +187,10 @@ func TestServerResponseEmptyBackend(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
roundTripperManager := service.NewRoundTripperManager(nil)
|
transportManager := service.NewTransportManager(nil)
|
||||||
roundTripperManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
|
transportManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
|
||||||
managerFactory := service.NewManagerFactory(staticConfig, nil, nil, roundTripperManager, nil)
|
|
||||||
|
managerFactory := service.NewManagerFactory(staticConfig, nil, nil, transportManager, proxyBuilderMock{}, nil)
|
||||||
tlsManager := tls.NewManager()
|
tlsManager := tls.NewManager()
|
||||||
|
|
||||||
dialerManager := tcp.NewDialerManager(nil)
|
dialerManager := tcp.NewDialerManager(nil)
|
||||||
|
@ -228,9 +232,10 @@ func TestInternalServices(t *testing.T) {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
roundTripperManager := service.NewRoundTripperManager(nil)
|
transportManager := service.NewTransportManager(nil)
|
||||||
roundTripperManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
|
transportManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
|
||||||
managerFactory := service.NewManagerFactory(staticConfig, nil, nil, roundTripperManager, nil)
|
|
||||||
|
managerFactory := service.NewManagerFactory(staticConfig, nil, nil, transportManager, nil, nil)
|
||||||
tlsManager := tls.NewManager()
|
tlsManager := tls.NewManager()
|
||||||
|
|
||||||
dialerManager := tcp.NewDialerManager(nil)
|
dialerManager := tcp.NewDialerManager(nil)
|
||||||
|
@ -246,3 +251,13 @@ func TestInternalServices(t *testing.T) {
|
||||||
|
|
||||||
assert.Equal(t, http.StatusOK, responseRecorderOk.Result().StatusCode, "status code")
|
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")
|
||||||
|
}
|
||||||
|
|
|
@ -17,7 +17,8 @@ import (
|
||||||
type ManagerFactory struct {
|
type ManagerFactory struct {
|
||||||
observabilityMgr *middleware.ObservabilityMgr
|
observabilityMgr *middleware.ObservabilityMgr
|
||||||
|
|
||||||
roundTripperManager *RoundTripperManager
|
transportManager *TransportManager
|
||||||
|
proxyBuilder ProxyBuilder
|
||||||
|
|
||||||
api func(configuration *runtime.Configuration) http.Handler
|
api func(configuration *runtime.Configuration) http.Handler
|
||||||
restHandler http.Handler
|
restHandler http.Handler
|
||||||
|
@ -30,11 +31,12 @@ type ManagerFactory struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewManagerFactory creates a new ManagerFactory.
|
// 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{
|
factory := &ManagerFactory{
|
||||||
observabilityMgr: observabilityMgr,
|
observabilityMgr: observabilityMgr,
|
||||||
routinesPool: routinesPool,
|
routinesPool: routinesPool,
|
||||||
roundTripperManager: roundTripperManager,
|
transportManager: transportManager,
|
||||||
|
proxyBuilder: proxyBuilder,
|
||||||
acmeHTTPHandler: acmeHTTPHandler,
|
acmeHTTPHandler: acmeHTTPHandler,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,7 +75,7 @@ func NewManagerFactory(staticConfiguration static.Configuration, routinesPool *s
|
||||||
|
|
||||||
// Build creates a service manager.
|
// Build creates a service manager.
|
||||||
func (f *ManagerFactory) Build(configuration *runtime.Configuration) *InternalHandlers {
|
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
|
var apiHandler http.Handler
|
||||||
if f.api != nil {
|
if f.api != nil {
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -9,7 +9,6 @@ import (
|
||||||
"hash/fnv"
|
"hash/fnv"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -25,6 +24,7 @@ import (
|
||||||
"github.com/traefik/traefik/v3/pkg/middlewares/capture"
|
"github.com/traefik/traefik/v3/pkg/middlewares/capture"
|
||||||
metricsMiddle "github.com/traefik/traefik/v3/pkg/middlewares/metrics"
|
metricsMiddle "github.com/traefik/traefik/v3/pkg/middlewares/metrics"
|
||||||
"github.com/traefik/traefik/v3/pkg/middlewares/observability"
|
"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/safe"
|
||||||
"github.com/traefik/traefik/v3/pkg/server/cookie"
|
"github.com/traefik/traefik/v3/pkg/server/cookie"
|
||||||
"github.com/traefik/traefik/v3/pkg/server/middleware"
|
"github.com/traefik/traefik/v3/pkg/server/middleware"
|
||||||
|
@ -40,17 +40,18 @@ const (
|
||||||
defaultMaxBodySize int64 = -1
|
defaultMaxBodySize int64 = -1
|
||||||
)
|
)
|
||||||
|
|
||||||
// RoundTripperGetter is a roundtripper getter interface.
|
// ProxyBuilder builds reverse proxy handlers.
|
||||||
type RoundTripperGetter interface {
|
type ProxyBuilder interface {
|
||||||
Get(name string) (http.RoundTripper, error)
|
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.
|
// Manager The service manager.
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
routinePool *safe.Pool
|
routinePool *safe.Pool
|
||||||
observabilityMgr *middleware.ObservabilityMgr
|
observabilityMgr *middleware.ObservabilityMgr
|
||||||
bufferPool httputil.BufferPool
|
transportManager httputil.TransportManager
|
||||||
roundTripperManager RoundTripperGetter
|
proxyBuilder ProxyBuilder
|
||||||
|
|
||||||
services map[string]http.Handler
|
services map[string]http.Handler
|
||||||
configs map[string]*runtime.ServiceInfo
|
configs map[string]*runtime.ServiceInfo
|
||||||
|
@ -59,12 +60,12 @@ type Manager struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewManager creates a new Manager.
|
// 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{
|
return &Manager{
|
||||||
routinePool: routinePool,
|
routinePool: routinePool,
|
||||||
observabilityMgr: observabilityMgr,
|
observabilityMgr: observabilityMgr,
|
||||||
bufferPool: newBufferPool(),
|
transportManager: transportManager,
|
||||||
roundTripperManager: roundTripperManager,
|
proxyBuilder: proxyBuilder,
|
||||||
services: make(map[string]http.Handler),
|
services: make(map[string]http.Handler),
|
||||||
configs: configs,
|
configs: configs,
|
||||||
healthCheckers: make(map[string]*healthcheck.ServiceHealthChecker),
|
healthCheckers: make(map[string]*healthcheck.ServiceHealthChecker),
|
||||||
|
@ -298,9 +299,9 @@ func (m *Manager) getLoadBalancerServiceHandler(ctx context.Context, serviceName
|
||||||
logger.Debug().Msg("Creating load-balancer")
|
logger.Debug().Msg("Creating load-balancer")
|
||||||
|
|
||||||
// TODO: should we keep this config value as Go is now handling stream response correctly?
|
// 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 {
|
if service.ResponseForwarding != nil {
|
||||||
flushInterval = service.ResponseForwarding.FlushInterval
|
flushInterval = time.Duration(service.ResponseForwarding.FlushInterval)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(service.ServersTransport) > 0 {
|
if len(service.ServersTransport) > 0 {
|
||||||
|
@ -317,11 +318,6 @@ func (m *Manager) getLoadBalancerServiceHandler(ctx context.Context, serviceName
|
||||||
passHostHeader = *service.PassHostHeader
|
passHostHeader = *service.PassHostHeader
|
||||||
}
|
}
|
||||||
|
|
||||||
roundTripper, err := m.roundTripperManager.Get(service.ServersTransport)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
lb := wrr.New(service.Sticky, service.HealthCheck != nil)
|
lb := wrr.New(service.Sticky, service.HealthCheck != nil)
|
||||||
healthCheckTargets := make(map[string]*url.URL)
|
healthCheckTargets := make(map[string]*url.URL)
|
||||||
|
|
||||||
|
@ -341,14 +337,12 @@ func (m *Manager) getLoadBalancerServiceHandler(ctx context.Context, serviceName
|
||||||
|
|
||||||
qualifiedSvcName := provider.GetQualifiedName(ctx, serviceName)
|
qualifiedSvcName := provider.GetQualifiedName(ctx, serviceName)
|
||||||
|
|
||||||
if m.observabilityMgr.ShouldAddTracing(qualifiedSvcName) || m.observabilityMgr.ShouldAddMetrics(qualifiedSvcName) {
|
shouldObserve := m.observabilityMgr.ShouldAddTracing(qualifiedSvcName) || m.observabilityMgr.ShouldAddMetrics(qualifiedSvcName)
|
||||||
// Wrapping the roundTripper with the Tracing roundTripper,
|
proxy, err := m.proxyBuilder.Build(service.ServersTransport, target, shouldObserve, passHostHeader, flushInterval)
|
||||||
// to handle the reverseProxy client span creation.
|
if err != nil {
|
||||||
roundTripper = newObservabilityRoundTripper(m.observabilityMgr.SemConvMetricsRegistry(), roundTripper)
|
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.
|
// Prevents from enabling observability for internal resources.
|
||||||
|
|
||||||
if m.observabilityMgr.ShouldAddAccessLogs(qualifiedSvcName) {
|
if m.observabilityMgr.ShouldAddAccessLogs(qualifiedSvcName) {
|
||||||
|
@ -393,6 +387,11 @@ func (m *Manager) getLoadBalancerServiceHandler(ctx context.Context, serviceName
|
||||||
}
|
}
|
||||||
|
|
||||||
if service.HealthCheck != nil {
|
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(
|
m.healthCheckers[serviceName] = healthcheck.NewServiceHealthChecker(
|
||||||
ctx,
|
ctx,
|
||||||
m.observabilityMgr.MetricsRegistry(),
|
m.observabilityMgr.MetricsRegistry(),
|
||||||
|
|
|
@ -2,6 +2,7 @@ package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
@ -14,13 +15,14 @@ import (
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/traefik/traefik/v3/pkg/config/dynamic"
|
"github.com/traefik/traefik/v3/pkg/config/dynamic"
|
||||||
"github.com/traefik/traefik/v3/pkg/config/runtime"
|
"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/server/provider"
|
||||||
"github.com/traefik/traefik/v3/pkg/testhelpers"
|
"github.com/traefik/traefik/v3/pkg/testhelpers"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGetLoadBalancer(t *testing.T) {
|
func TestGetLoadBalancer(t *testing.T) {
|
||||||
sm := Manager{
|
sm := Manager{
|
||||||
roundTripperManager: newRtMock(),
|
transportManager: &transportManagerMock{},
|
||||||
}
|
}
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
|
@ -40,14 +42,14 @@ func TestGetLoadBalancer(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
fwd: &MockForwarder{},
|
fwd: &forwarderMock{},
|
||||||
expectError: true,
|
expectError: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "Succeeds when there are no servers",
|
desc: "Succeeds when there are no servers",
|
||||||
serviceName: "test",
|
serviceName: "test",
|
||||||
service: &dynamic.ServersLoadBalancer{},
|
service: &dynamic.ServersLoadBalancer{},
|
||||||
fwd: &MockForwarder{},
|
fwd: &forwarderMock{},
|
||||||
expectError: false,
|
expectError: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -56,7 +58,7 @@ func TestGetLoadBalancer(t *testing.T) {
|
||||||
service: &dynamic.ServersLoadBalancer{
|
service: &dynamic.ServersLoadBalancer{
|
||||||
Sticky: &dynamic.Sticky{Cookie: &dynamic.Cookie{}},
|
Sticky: &dynamic.Sticky{Cookie: &dynamic.Cookie{}},
|
||||||
},
|
},
|
||||||
fwd: &MockForwarder{},
|
fwd: &forwarderMock{},
|
||||||
expectError: false,
|
expectError: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -79,11 +81,8 @@ func TestGetLoadBalancer(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetLoadBalancerServiceHandler(t *testing.T) {
|
func TestGetLoadBalancerServiceHandler(t *testing.T) {
|
||||||
sm := NewManager(nil, nil, nil, &RoundTripperManager{
|
pb := httputil.NewProxyBuilder(&transportManagerMock{}, nil)
|
||||||
roundTrippers: map[string]http.RoundTripper{
|
sm := NewManager(nil, nil, nil, transportManagerMock{}, pb)
|
||||||
"default@internal": http.DefaultTransport,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
server1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
server1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("X-From", "first")
|
w.Header().Set("X-From", "first")
|
||||||
|
@ -139,7 +138,7 @@ func TestGetLoadBalancerServiceHandler(t *testing.T) {
|
||||||
desc: "Load balances between the two servers",
|
desc: "Load balances between the two servers",
|
||||||
serviceName: "test",
|
serviceName: "test",
|
||||||
service: &dynamic.ServersLoadBalancer{
|
service: &dynamic.ServersLoadBalancer{
|
||||||
PassHostHeader: Bool(true),
|
PassHostHeader: boolPtr(true),
|
||||||
Servers: []dynamic.Server{
|
Servers: []dynamic.Server{
|
||||||
{
|
{
|
||||||
URL: server1.URL,
|
URL: server1.URL,
|
||||||
|
@ -254,7 +253,7 @@ func TestGetLoadBalancerServiceHandler(t *testing.T) {
|
||||||
desc: "PassHost doesn't pass the host instead of the IP",
|
desc: "PassHost doesn't pass the host instead of the IP",
|
||||||
serviceName: "test",
|
serviceName: "test",
|
||||||
service: &dynamic.ServersLoadBalancer{
|
service: &dynamic.ServersLoadBalancer{
|
||||||
PassHostHeader: Bool(false),
|
PassHostHeader: boolPtr(false),
|
||||||
Sticky: &dynamic.Sticky{Cookie: &dynamic.Cookie{}},
|
Sticky: &dynamic.Sticky{Cookie: &dynamic.Cookie{}},
|
||||||
Servers: []dynamic.Server{
|
Servers: []dynamic.Server{
|
||||||
{
|
{
|
||||||
|
@ -359,11 +358,8 @@ func TestGetLoadBalancerServiceHandler(t *testing.T) {
|
||||||
|
|
||||||
// This test is an adapted version of net/http/httputil.Test1xxResponses test.
|
// This test is an adapted version of net/http/httputil.Test1xxResponses test.
|
||||||
func Test1xxResponses(t *testing.T) {
|
func Test1xxResponses(t *testing.T) {
|
||||||
sm := NewManager(nil, nil, nil, &RoundTripperManager{
|
pb := httputil.NewProxyBuilder(&transportManagerMock{}, nil)
|
||||||
roundTrippers: map[string]http.RoundTripper{
|
sm := NewManager(nil, nil, nil, &transportManagerMock{}, pb)
|
||||||
"default@internal": http.DefaultTransport,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
h := w.Header()
|
h := w.Header()
|
||||||
|
@ -499,11 +495,7 @@ func TestManager_Build(t *testing.T) {
|
||||||
t.Run(test.desc, func(t *testing.T) {
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
manager := NewManager(test.configs, nil, nil, &RoundTripperManager{
|
manager := NewManager(test.configs, nil, nil, &transportManagerMock{}, nil)
|
||||||
roundTrippers: map[string]http.RoundTripper{
|
|
||||||
"default@internal": http.DefaultTransport,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
if len(test.providerName) > 0 {
|
if len(test.providerName) > 0 {
|
||||||
|
@ -526,30 +518,30 @@ func TestMultipleTypeOnBuildHTTP(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
manager := NewManager(services, nil, nil, &RoundTripperManager{
|
manager := NewManager(services, nil, nil, &transportManagerMock{}, nil)
|
||||||
roundTrippers: map[string]http.RoundTripper{
|
|
||||||
"default@internal": http.DefaultTransport,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
_, err := manager.BuildHTTP(context.Background(), "test@file")
|
_, 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")
|
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")
|
panic("not available")
|
||||||
}
|
}
|
||||||
|
|
||||||
type rtMock struct{}
|
type transportManagerMock struct{}
|
||||||
|
|
||||||
func newRtMock() RoundTripperGetter {
|
func (t transportManagerMock) GetRoundTripper(_ string) (http.RoundTripper, error) {
|
||||||
return &rtMock{}
|
return &http.Transport{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *rtMock) Get(_ string) (http.RoundTripper, error) {
|
func (t transportManagerMock) GetTLSConfig(_ string) (*tls.Config, error) {
|
||||||
return http.DefaultTransport, nil
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t transportManagerMock) Get(_ string) (*dynamic.ServersTransport, error) {
|
||||||
|
return &dynamic.ServersTransport{}, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,15 @@ import (
|
||||||
"golang.org/x/net/http2"
|
"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) {
|
func newSmartRoundTripper(transport *http.Transport, forwardingTimeouts *dynamic.ForwardingTimeouts) (*smartRoundTripper, error) {
|
||||||
transportHTTP1 := transport.Clone()
|
transportHTTP1 := transport.Clone()
|
||||||
|
|
||||||
|
|
|
@ -22,52 +22,45 @@ import (
|
||||||
"github.com/traefik/traefik/v3/pkg/config/dynamic"
|
"github.com/traefik/traefik/v3/pkg/config/dynamic"
|
||||||
traefiktls "github.com/traefik/traefik/v3/pkg/tls"
|
traefiktls "github.com/traefik/traefik/v3/pkg/tls"
|
||||||
"github.com/traefik/traefik/v3/pkg/types"
|
"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.
|
// SpiffeX509Source allows to retrieve a x509 SVID and bundle.
|
||||||
type SpiffeX509Source interface {
|
type SpiffeX509Source interface {
|
||||||
x509svid.Source
|
x509svid.Source
|
||||||
x509bundle.Source
|
x509bundle.Source
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRoundTripperManager creates a new RoundTripperManager.
|
// TransportManager handles transports for backend communication.
|
||||||
func NewRoundTripperManager(spiffeX509Source SpiffeX509Source) *RoundTripperManager {
|
type TransportManager struct {
|
||||||
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 {
|
|
||||||
rtLock sync.RWMutex
|
rtLock sync.RWMutex
|
||||||
roundTrippers map[string]http.RoundTripper
|
roundTrippers map[string]http.RoundTripper
|
||||||
configs map[string]*dynamic.ServersTransport
|
configs map[string]*dynamic.ServersTransport
|
||||||
|
tlsConfigs map[string]*tls.Config
|
||||||
|
|
||||||
spiffeX509Source SpiffeX509Source
|
spiffeX509Source SpiffeX509Source
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update updates the roundtrippers configurations.
|
// NewTransportManager creates a new TransportManager.
|
||||||
func (r *RoundTripperManager) Update(newConfigs map[string]*dynamic.ServersTransport) {
|
func NewTransportManager(spiffeX509Source SpiffeX509Source) *TransportManager {
|
||||||
r.rtLock.Lock()
|
return &TransportManager{
|
||||||
defer r.rtLock.Unlock()
|
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]
|
newConfig, ok := newConfigs[configName]
|
||||||
if !ok {
|
if !ok {
|
||||||
delete(r.configs, configName)
|
delete(t.configs, configName)
|
||||||
delete(r.roundTrippers, configName)
|
delete(t.roundTrippers, configName)
|
||||||
|
delete(t.tlsConfigs, configName)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,50 +69,133 @@ func (r *RoundTripperManager) Update(newConfigs map[string]*dynamic.ServersTrans
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
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 {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("Could not configure HTTP Transport %s, fallback on default transport", configName)
|
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 {
|
for newConfigName, newConfig := range newConfigs {
|
||||||
if _, ok := r.configs[newConfigName]; ok {
|
if _, ok := t.configs[newConfigName]; ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
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 {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("Could not configure HTTP Transport %s, fallback on default transport", newConfigName)
|
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.
|
// GetRoundTripper gets a roundtripper corresponding to the given transport name.
|
||||||
func (r *RoundTripperManager) Get(name string) (http.RoundTripper, error) {
|
func (t *TransportManager) GetRoundTripper(name string) (http.RoundTripper, error) {
|
||||||
if len(name) == 0 {
|
if len(name) == 0 {
|
||||||
name = "default@internal"
|
name = "default@internal"
|
||||||
}
|
}
|
||||||
|
|
||||||
r.rtLock.RLock()
|
t.rtLock.RLock()
|
||||||
defer r.rtLock.RUnlock()
|
defer t.rtLock.RUnlock()
|
||||||
|
|
||||||
if rt, ok := r.roundTrippers[name]; ok {
|
if rt, ok := t.roundTrippers[name]; ok {
|
||||||
return rt, nil
|
return rt, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, fmt.Errorf("servers transport not found %s", name)
|
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.
|
// 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.
|
// 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.
|
// 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.
|
// 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 {
|
if cfg == nil {
|
||||||
return nil, errors.New("no transport configuration given")
|
return nil, errors.New("no transport configuration given")
|
||||||
}
|
}
|
||||||
|
@ -142,6 +218,7 @@ func (r *RoundTripperManager) createRoundTripper(cfg *dynamic.ServersTransport)
|
||||||
ExpectContinueTimeout: 1 * time.Second,
|
ExpectContinueTimeout: 1 * time.Second,
|
||||||
ReadBufferSize: 64 * 1024,
|
ReadBufferSize: 64 * 1024,
|
||||||
WriteBufferSize: 64 * 1024,
|
WriteBufferSize: 64 * 1024,
|
||||||
|
TLSClientConfig: tlsConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.ForwardingTimeouts != nil {
|
if cfg.ForwardingTimeouts != nil {
|
||||||
|
@ -149,41 +226,9 @@ func (r *RoundTripperManager) createRoundTripper(cfg *dynamic.ServersTransport)
|
||||||
transport.IdleConnTimeout = time.Duration(cfg.ForwardingTimeouts.IdleConnTimeout)
|
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
|
// Return directly HTTP/1.1 transport when HTTP/2 is disabled
|
||||||
if cfg.DisableHTTP2 {
|
if cfg.DisableHTTP2 {
|
||||||
return &KerberosRoundTripper{
|
return &kerberosRoundTripper{
|
||||||
OriginalRoundTripper: transport,
|
OriginalRoundTripper: transport,
|
||||||
new: func() http.RoundTripper {
|
new: func() http.RoundTripper {
|
||||||
return transport.Clone()
|
return transport.Clone()
|
||||||
|
@ -195,7 +240,7 @@ func (r *RoundTripperManager) createRoundTripper(cfg *dynamic.ServersTransport)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &KerberosRoundTripper{
|
return &kerberosRoundTripper{
|
||||||
OriginalRoundTripper: rt,
|
OriginalRoundTripper: rt,
|
||||||
new: func() http.RoundTripper {
|
new: func() http.RoundTripper {
|
||||||
return rt.Clone()
|
return rt.Clone()
|
||||||
|
@ -203,11 +248,6 @@ func (r *RoundTripperManager) createRoundTripper(cfg *dynamic.ServersTransport)
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type KerberosRoundTripper struct {
|
|
||||||
new func() http.RoundTripper
|
|
||||||
OriginalRoundTripper http.RoundTripper
|
|
||||||
}
|
|
||||||
|
|
||||||
type stickyRoundTripper struct {
|
type stickyRoundTripper struct {
|
||||||
RoundTripper http.RoundTripper
|
RoundTripper http.RoundTripper
|
||||||
}
|
}
|
||||||
|
@ -220,7 +260,12 @@ func AddTransportOnContext(ctx context.Context) context.Context {
|
||||||
return context.WithValue(ctx, transportKey, &stickyRoundTripper{})
|
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)
|
value, ok := request.Context().Value(transportKey).(*stickyRoundTripper)
|
||||||
if !ok {
|
if !ok {
|
||||||
return k.OriginalRoundTripper.RoundTrip(request)
|
return k.OriginalRoundTripper.RoundTrip(request)
|
|
@ -141,7 +141,7 @@ func TestKeepConnectionWhenSameConfiguration(t *testing.T) {
|
||||||
srv.TLS = &tls.Config{Certificates: []tls.Certificate{cert}}
|
srv.TLS = &tls.Config{Certificates: []tls.Certificate{cert}}
|
||||||
srv.StartTLS()
|
srv.StartTLS()
|
||||||
|
|
||||||
rtManager := NewRoundTripperManager(nil)
|
transportManager := NewTransportManager(nil)
|
||||||
|
|
||||||
dynamicConf := map[string]*dynamic.ServersTransport{
|
dynamicConf := map[string]*dynamic.ServersTransport{
|
||||||
"test": {
|
"test": {
|
||||||
|
@ -151,9 +151,9 @@ func TestKeepConnectionWhenSameConfiguration(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for range 10 {
|
for range 10 {
|
||||||
rtManager.Update(dynamicConf)
|
transportManager.Update(dynamicConf)
|
||||||
|
|
||||||
tr, err := rtManager.Get("test")
|
tr, err := transportManager.GetRoundTripper("test")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
client := http.Client{Transport: tr}
|
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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
client := http.Client{Transport: tr}
|
client := http.Client{Transport: tr}
|
||||||
|
@ -209,7 +209,7 @@ func TestMTLS(t *testing.T) {
|
||||||
}
|
}
|
||||||
srv.StartTLS()
|
srv.StartTLS()
|
||||||
|
|
||||||
rtManager := NewRoundTripperManager(nil)
|
transportManager := NewTransportManager(nil)
|
||||||
|
|
||||||
dynamicConf := map[string]*dynamic.ServersTransport{
|
dynamicConf := map[string]*dynamic.ServersTransport{
|
||||||
"test": {
|
"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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
client := http.Client{Transport: tr}
|
client := http.Client{Transport: tr}
|
||||||
|
@ -348,7 +348,7 @@ func TestSpiffeMTLS(t *testing.T) {
|
||||||
|
|
||||||
for _, test := range testCases {
|
for _, test := range testCases {
|
||||||
t.Run(test.desc, func(t *testing.T) {
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
rtManager := NewRoundTripperManager(test.clientSource)
|
transportManager := NewTransportManager(test.clientSource)
|
||||||
|
|
||||||
dynamicConf := map[string]*dynamic.ServersTransport{
|
dynamicConf := map[string]*dynamic.ServersTransport{
|
||||||
"test": {
|
"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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
client := http.Client{Transport: tr}
|
client := http.Client{Transport: tr}
|
||||||
|
@ -415,7 +415,7 @@ func TestDisableHTTP2(t *testing.T) {
|
||||||
srv.EnableHTTP2 = test.serverHTTP2
|
srv.EnableHTTP2 = test.serverHTTP2
|
||||||
srv.StartTLS()
|
srv.StartTLS()
|
||||||
|
|
||||||
rtManager := NewRoundTripperManager(nil)
|
transportManager := NewTransportManager(nil)
|
||||||
|
|
||||||
dynamicConf := map[string]*dynamic.ServersTransport{
|
dynamicConf := map[string]*dynamic.ServersTransport{
|
||||||
"test": {
|
"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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
client := http.Client{Transport: tr}
|
client := http.Client{Transport: tr}
|
||||||
|
@ -593,7 +593,7 @@ func TestKerberosRoundTripper(t *testing.T) {
|
||||||
|
|
||||||
origCount := 0
|
origCount := 0
|
||||||
dedicatedCount := 0
|
dedicatedCount := 0
|
||||||
rt := KerberosRoundTripper{
|
rt := kerberosRoundTripper{
|
||||||
new: func() http.RoundTripper {
|
new: func() http.RoundTripper {
|
||||||
return roundTripperFn(func(req *http.Request) (*http.Response, error) {
|
return roundTripperFn(func(req *http.Request) (*http.Response, error) {
|
||||||
dedicatedCount++
|
dedicatedCount++
|
|
@ -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"`
|
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"`
|
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"`
|
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.
|
// SetDefaults sets the default values.
|
||||||
|
@ -127,6 +128,7 @@ func (o *OTLP) SetDefaults() {
|
||||||
o.AddServicesLabels = true
|
o.AddServicesLabels = true
|
||||||
o.ExplicitBoundaries = []float64{.005, .01, .025, .05, .075, .1, .25, .5, .75, 1, 2.5, 5, 7.5, 10}
|
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.PushInterval = types.Duration(10 * time.Second)
|
||||||
|
o.ServiceName = "traefik"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Statistics provides options for monitoring request and response stats.
|
// Statistics provides options for monitoring request and response stats.
|
||||||
|
|
Loading…
Reference in a new issue