diff --git a/README.md b/README.md index 6a570e934..bb09fae86 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ _(But if you'd rather configure some of your routes manually, Traefik supports t - Provides HTTPS to your microservices by leveraging [Let's Encrypt](https://letsencrypt.org) (wildcard certificates support) - Circuit breakers, retry - See the magic through its clean web UI -- Websocket, HTTP/2, gRPC ready +- WebSocket, HTTP/2, gRPC ready - Provides metrics (Rest, Prometheus, Datadog, Statsd, InfluxDB 2.X) - Keeps access logs (JSON, CLF) - Fast diff --git a/SECURITY.md b/SECURITY.md index 7b5c4b953..c9a2670f6 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,7 +1,7 @@ # Security Policy You can join our security mailing list to be aware of the latest announcements from our security team. -You can subscribe sending a mail to security+subscribe@traefik.io or on [the online viewer](https://groups.google.com/a/traefik.io/forum/#!forum/security). +You can subscribe by sending an email to security+subscribe@traefik.io or on [the online viewer](https://groups.google.com/a/traefik.io/forum/#!forum/security). Reported vulnerabilities can be found on [cve.mitre.org](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=traefik). diff --git a/docs/content/contributing/building-testing.md b/docs/content/contributing/building-testing.md index 20d05740a..cd71b9482 100644 --- a/docs/content/contributing/building-testing.md +++ b/docs/content/contributing/building-testing.md @@ -92,7 +92,7 @@ For development purposes, you can specify which tests to run by using (only work Create `tailscale.secret` file in `integration` directory. - This file need to contains a [Tailscale auth key](https://tailscale.com/kb/1085/auth-keys) + This file needs to contain a [Tailscale auth key](https://tailscale.com/kb/1085/auth-keys) (an ephemeral, but reusable, one is recommended). Add this section to your tailscale ACLs to auto-approve the routes for the diff --git a/docs/content/contributing/documentation.md b/docs/content/contributing/documentation.md index 5a2974ff8..a57e4c695 100644 --- a/docs/content/contributing/documentation.md +++ b/docs/content/contributing/documentation.md @@ -15,13 +15,13 @@ Let's see how. ### General -This [documentation](../../ "Link to the official Traefik documentation") is built with [MkDocs](https://mkdocs.org/ "Link to website of MkDocs"). +This [documentation](../../ "Link to the official Traefik documentation") is built with [MkDocs](https://mkdocs.org/ "Link to the website of MkDocs"). ### Method 1: `Docker` and `make` Please make sure you have the following requirements installed: -- [Docker](https://www.docker.com/ "Link to website of Docker") +- [Docker](https://www.docker.com/ "Link to the website of Docker") You can build the documentation and test it locally (with live reloading), using the `docs-serve` target: @@ -51,7 +51,7 @@ $ make docs-build Please make sure you have the following requirements installed: -- [Python](https://www.python.org/ "Link to website of Python") +- [Python](https://www.python.org/ "Link to the website of Python") - [pip](https://pypi.org/project/pip/ "Link to the website of pip on PyPI") ```bash diff --git a/docs/content/contributing/maintainers-guidelines.md b/docs/content/contributing/maintainers-guidelines.md index 7c229917e..4fa13a0f1 100644 --- a/docs/content/contributing/maintainers-guidelines.md +++ b/docs/content/contributing/maintainers-guidelines.md @@ -32,7 +32,7 @@ The contributor should also meet one or several of the following requirements: including those of other maintainers and contributors. - The contributor is active on Traefik Community forums - or other technical forums/boards such as K8S slack, Reddit, StackOverflow, hacker news. + or other technical forums/boards, such as K8S Slack, Reddit, StackOverflow, and Hacker News. Any existing active maintainer can create an issue to discuss promoting a contributor to maintainer. Other maintainers can vote on the issue, and if the quorum is reached, the contributor is promoted to maintainer. diff --git a/docs/content/contributing/submitting-pull-requests.md b/docs/content/contributing/submitting-pull-requests.md index 7611023d2..d46e282c9 100644 --- a/docs/content/contributing/submitting-pull-requests.md +++ b/docs/content/contributing/submitting-pull-requests.md @@ -17,7 +17,7 @@ or the list of [confirmed bugs](https://github.com/traefik/traefik/labels/kind%2 ## How We Prioritize -We wish we could review every pull request right away, but because it's a time consuming operation, it's not always possible. +We wish we could review every pull request right away, but because it's a time-consuming operation, it's not always possible. The PRs we are able to handle the fastest are: @@ -130,7 +130,7 @@ This label can be used when: Traefik Proxy is made by the community for the community, as such the goal is to engage the community to make Traefik the best reverse proxy available. Part of this goal is maintaining a lean codebase and ensuring code velocity. -unfortunately, this means that sometimes we will not be able to merge a pull request. +Unfortunately, this means that sometimes we will not be able to merge a pull request. Because we respect the work you did, you will always be told why we are closing your pull request. If you do not agree with our decision, do not worry; closed pull requests are effortless to recreate, diff --git a/docs/content/contributing/submitting-security-issues.md b/docs/content/contributing/submitting-security-issues.md index 08fbb79a2..981c7fe4c 100644 --- a/docs/content/contributing/submitting-security-issues.md +++ b/docs/content/contributing/submitting-security-issues.md @@ -8,7 +8,7 @@ description: "Security is a key part of Traefik Proxy. Read the technical docume ## Security Advisories We strongly advise you to join our mailing list to be aware of the latest announcements from our security team. -You can subscribe sending a mail to security+subscribe@traefik.io or on [the online viewer](https://groups.google.com/a/traefik.io/forum/#!forum/security). +You can subscribe by sending an email to security+subscribe@traefik.io or on [the online viewer](https://groups.google.com/a/traefik.io/forum/#!forum/security). ## CVE diff --git a/docs/content/migration/v2.md b/docs/content/migration/v2.md index d5c58271c..6a870936a 100644 --- a/docs/content/migration/v2.md +++ b/docs/content/migration/v2.md @@ -658,4 +658,4 @@ Please check out the [entrypoint forwarded headers connection option configurati ### X-Forwarded-Prefix In `v2.11.14`, the `X-Forwarded-Prefix` header is now handled like the other `X-Forwarded-*` headers: Traefik removes it when it's sent from an untrusted source. -Please refer to the Forwarded headers [documentation](https://doc.traefik.io/traefik/routing/entrypoints/#forwarded-headers) for more details. +Please refer to the Forwarded headers [documentation](../routing/entrypoints.md#forwarded-headers) for more details. diff --git a/docs/content/observability/access-logs.md b/docs/content/observability/access-logs.md index bb664dc51..8a24603fa 100644 --- a/docs/content/observability/access-logs.md +++ b/docs/content/observability/access-logs.md @@ -79,6 +79,20 @@ If the given format is unsupported, the default (CLF) is used instead. - [] " " "" "" "" "" ms ``` +```yaml tab="File (YAML)" +accessLog: + format: "json" +``` + +```toml tab="File (TOML)" +[accessLog] + format = "json" +``` + +```bash tab="CLI" +--accesslog.format=json +``` + ### `bufferingSize` To write the logs in an asynchronous fashion, specify a `bufferingSize` option. diff --git a/docs/content/operations/api.md b/docs/content/operations/api.md index 7c8248c2e..2829f3ffe 100644 --- a/docs/content/operations/api.md +++ b/docs/content/operations/api.md @@ -70,7 +70,7 @@ And then define a routing configuration on Traefik itself with the ### `insecure` -Enable the API in `insecure` mode, which means that the API will be available directly on the entryPoint named `traefik`. +Enable the API in `insecure` mode, which means that the API will be available directly on the entryPoint named `traefik`, on path `/api`. !!! info If the entryPoint named `traefik` is not configured, it will be automatically created on port 8080. diff --git a/docs/content/operations/dashboard.md b/docs/content/operations/dashboard.md index 170d5ebe4..c2b3c21e9 100644 --- a/docs/content/operations/dashboard.md +++ b/docs/content/operations/dashboard.md @@ -37,32 +37,15 @@ Start by enabling the dashboard by using the following option from [Traefik's AP on the [static configuration](../getting-started/configuration-overview.md#the-static-configuration): ```yaml tab="File (YAML)" -api: - # Dashboard - # - # Optional - # Default: true - # - dashboard: true +api: {} ``` ```toml tab="File (TOML)" [api] - # Dashboard - # - # Optional - # Default: true - # - dashboard = true ``` ```bash tab="CLI" -# Dashboard -# -# Optional -# Default: true -# ---api.dashboard=true +--api=true ``` Then define a routing configuration on Traefik itself, @@ -106,27 +89,47 @@ rule = "Host(`traefik.example.com`) && (PathPrefix(`/api`) || PathPrefix(`/dashb ## Insecure Mode -This mode is not recommended because it does not allow the use of security features. +When _insecure_ mode is enabled, one can access the dashboard on the `traefik` port (default: `8080`) of the Traefik instance, +at the following URL: `http://:8080/dashboard/` (trailing slash is mandatory). -To enable the "insecure mode", use the following options from [Traefik's API](./api.md#insecure): +This mode is **not** recommended because it does not allow security features. +For example, it is not possible to add an authentication middleware with this mode. + +It should be used for testing purpose **only**. + +To enable the _insecure_ mode, use the following options from [Traefik's API](./api.md#insecure): ```yaml tab="File (YAML)" api: - dashboard: true insecure: true ``` ```toml tab="File (TOML)" [api] - dashboard = true insecure = true ``` ```bash tab="CLI" ---api.dashboard=true --api.insecure=true +--api.insecure=true ``` -You can now access the dashboard on the port `8080` of the Traefik instance, -at the following URL: `http://:8080/dashboard/` (trailing slash is mandatory). +## Disable The Dashboard + +By default, the dashboard is enabled when the API is enabled. +If necessary, the dashboard can be disabled by using the following option. + +```yaml tab="File (YAML)" +api: + dashboard: false +``` + +```toml tab="File (TOML)" +[api] + dashboard = false +``` + +```bash tab="CLI" +--api.dashboard=false +``` {!traefik-for-business-applications.md!} diff --git a/docs/content/providers/consul-catalog.md b/docs/content/providers/consul-catalog.md index cdec6ae86..5b3b97e65 100644 --- a/docs/content/providers/consul-catalog.md +++ b/docs/content/providers/consul-catalog.md @@ -525,7 +525,7 @@ providers: ``` ```bash tab="CLI" ---providers.consulcatalog.defaultRule=Host(`{{ .Name }}.{{ index .Labels \"customLabel\"}}`) +--providers.consulcatalog.defaultRule='Host(`{{ .Name }}.{{ index .Labels "customLabel"}}`)' # ... ``` diff --git a/docs/content/providers/docker.md b/docs/content/providers/docker.md index d2ffed6c5..05811656d 100644 --- a/docs/content/providers/docker.md +++ b/docs/content/providers/docker.md @@ -455,7 +455,7 @@ providers: ``` ```bash tab="CLI" ---providers.docker.defaultRule=Host(`{{ .Name }}.{{ index .Labels \"customLabel\"}}`) +--providers.docker.defaultRule='Host(`{{ .Name }}.{{ index .Labels "customLabel"}}`)' # ... ``` diff --git a/docs/content/providers/ecs.md b/docs/content/providers/ecs.md index d85c6aae3..5c5c2f08c 100644 --- a/docs/content/providers/ecs.md +++ b/docs/content/providers/ecs.md @@ -283,7 +283,7 @@ providers: ``` ```bash tab="CLI" ---providers.ecs.defaultRule=Host(`{{ .Name }}.{{ index .Labels \"customLabel\"}}`) +--providers.ecs.defaultRule='Host(`{{ .Name }}.{{ index .Labels "customLabel"}}`)' # ... ``` diff --git a/docs/content/providers/nomad.md b/docs/content/providers/nomad.md index 0bd1ca706..ed55aa799 100644 --- a/docs/content/providers/nomad.md +++ b/docs/content/providers/nomad.md @@ -432,7 +432,7 @@ providers: ``` ```bash tab="CLI" ---providers.nomad.defaultRule="Host(`{{ .Name }}.{{ index .Labels \"customLabel\"}}`)" +--providers.nomad.defaultRule='Host(`{{ .Name }}.{{ index .Labels "customLabel"}}`)' # ... ``` diff --git a/docs/content/providers/swarm.md b/docs/content/providers/swarm.md index 4808c7e29..746f44513 100644 --- a/docs/content/providers/swarm.md +++ b/docs/content/providers/swarm.md @@ -503,7 +503,7 @@ providers: ``` ```bash tab="CLI" ---providers.swarm.defaultRule=Host(`{{ .Name }}.{{ index .Labels \"customLabel\"}}`) +--providers.swarm.defaultRule='Host(`{{ .Name }}.{{ index .Labels "customLabel"}}`)' # ... ``` diff --git a/pkg/server/server_entrypoint_tcp.go b/pkg/server/server_entrypoint_tcp.go index 094332ea3..0a69e8adf 100644 --- a/pkg/server/server_entrypoint_tcp.go +++ b/pkg/server/server_entrypoint_tcp.go @@ -625,17 +625,17 @@ func createHTTPServer(ctx context.Context, ln net.Listener, configuration *stati handler = contenttype.DisableAutoDetection(handler) + debugConnection := os.Getenv(debugConnectionEnv) != "" + if debugConnection || (configuration.Transport != nil && (configuration.Transport.KeepAliveMaxTime > 0 || configuration.Transport.KeepAliveMaxRequests > 0)) { + handler = newKeepAliveMiddleware(handler, configuration.Transport.KeepAliveMaxRequests, configuration.Transport.KeepAliveMaxTime) + } + if withH2c { handler = h2c.NewHandler(handler, &http2.Server{ MaxConcurrentStreams: uint32(configuration.HTTP2.MaxConcurrentStreams), }) } - debugConnection := os.Getenv(debugConnectionEnv) != "" - if debugConnection || (configuration.Transport != nil && (configuration.Transport.KeepAliveMaxTime > 0 || configuration.Transport.KeepAliveMaxRequests > 0)) { - handler = newKeepAliveMiddleware(handler, configuration.Transport.KeepAliveMaxRequests, configuration.Transport.KeepAliveMaxTime) - } - serverHTTP := &http.Server{ Handler: handler, ErrorLog: stdlog.New(logs.NoLevel(log.Logger, zerolog.DebugLevel), "", 0), diff --git a/pkg/server/server_entrypoint_tcp_test.go b/pkg/server/server_entrypoint_tcp_test.go index c300c45e8..65ca0d83b 100644 --- a/pkg/server/server_entrypoint_tcp_test.go +++ b/pkg/server/server_entrypoint_tcp_test.go @@ -3,6 +3,7 @@ package server import ( "bufio" "context" + "crypto/tls" "errors" "io" "net" @@ -17,6 +18,7 @@ import ( "github.com/traefik/traefik/v3/pkg/config/static" tcprouter "github.com/traefik/traefik/v3/pkg/server/router/tcp" "github.com/traefik/traefik/v3/pkg/tcp" + "golang.org/x/net/http2" ) func TestShutdownHijacked(t *testing.T) { @@ -330,3 +332,53 @@ func TestKeepAliveMaxTime(t *testing.T) { err = resp.Body.Close() require.NoError(t, err) } + +func TestKeepAliveH2c(t *testing.T) { + epConfig := &static.EntryPointsTransport{} + epConfig.SetDefaults() + epConfig.KeepAliveMaxRequests = 1 + + entryPoint, err := NewTCPEntryPoint(context.Background(), "", &static.EntryPoint{ + Address: ":0", + Transport: epConfig, + ForwardedHeaders: &static.ForwardedHeaders{}, + HTTP2: &static.HTTP2Config{}, + }, nil, nil) + require.NoError(t, err) + + router, err := tcprouter.NewRouter() + require.NoError(t, err) + + router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(http.StatusOK) + })) + + conn, err := startEntrypoint(entryPoint, router) + require.NoError(t, err) + + http2Transport := &http2.Transport{ + AllowHTTP: true, + DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) { + return conn, nil + }, + } + + client := &http.Client{Transport: http2Transport} + + resp, err := client.Get("http://" + entryPoint.listener.Addr().String()) + require.NoError(t, err) + require.False(t, resp.Close) + err = resp.Body.Close() + require.NoError(t, err) + + _, err = client.Get("http://" + entryPoint.listener.Addr().String()) + require.Error(t, err) + // Unlike HTTP/1, where we can directly check `resp.Close`, HTTP/2 uses a different + // mechanism: it sends a GOAWAY frame when the connection is closing. + // We can only check the error type. The error received should be poll.ErrClosed from + // the `internal/poll` package, but we cannot directly reference the error type due to + // package restrictions. Since this error message ("use of closed network connection") + // is distinct and specific, we rely on its consistency, assuming it is stable and unlikely + // to change. + require.Contains(t, err.Error(), "use of closed network connection") +} diff --git a/pkg/server/service/internalhandler.go b/pkg/server/service/internalhandler.go index e5c3ab26d..4ec26c930 100644 --- a/pkg/server/service/internalhandler.go +++ b/pkg/server/service/internalhandler.go @@ -8,11 +8,6 @@ import ( "strings" ) -type serviceManager interface { - BuildHTTP(rootCtx context.Context, serviceName string) (http.Handler, error) - LaunchHealthCheck(ctx context.Context) -} - // InternalHandlers is the internal HTTP handlers builder. type InternalHandlers struct { api http.Handler @@ -21,26 +16,24 @@ type InternalHandlers struct { prometheus http.Handler ping http.Handler acmeHTTP http.Handler - serviceManager } // NewInternalHandlers creates a new InternalHandlers. -func NewInternalHandlers(next serviceManager, apiHandler, rest, metricsHandler, pingHandler, dashboard, acmeHTTP http.Handler) *InternalHandlers { +func NewInternalHandlers(apiHandler, rest, metricsHandler, pingHandler, dashboard, acmeHTTP http.Handler) *InternalHandlers { return &InternalHandlers{ - api: apiHandler, - dashboard: dashboard, - rest: rest, - prometheus: metricsHandler, - ping: pingHandler, - acmeHTTP: acmeHTTP, - serviceManager: next, + api: apiHandler, + dashboard: dashboard, + rest: rest, + prometheus: metricsHandler, + ping: pingHandler, + acmeHTTP: acmeHTTP, } } // BuildHTTP builds an HTTP handler. func (m *InternalHandlers) BuildHTTP(rootCtx context.Context, serviceName string) (http.Handler, error) { if !strings.HasSuffix(serviceName, "@internal") { - return m.serviceManager.BuildHTTP(rootCtx, serviceName) + return nil, nil } internalHandler, err := m.get(serviceName) diff --git a/pkg/server/service/managerfactory.go b/pkg/server/service/managerfactory.go index 43e2189b3..ce1fa8a27 100644 --- a/pkg/server/service/managerfactory.go +++ b/pkg/server/service/managerfactory.go @@ -74,13 +74,12 @@ func NewManagerFactory(staticConfiguration static.Configuration, routinesPool *s } // Build creates a service manager. -func (f *ManagerFactory) Build(configuration *runtime.Configuration) *InternalHandlers { - svcManager := NewManager(configuration.Services, f.observabilityMgr, f.routinesPool, f.transportManager, f.proxyBuilder) - +func (f *ManagerFactory) Build(configuration *runtime.Configuration) *Manager { var apiHandler http.Handler if f.api != nil { apiHandler = f.api(configuration) } - return NewInternalHandlers(svcManager, apiHandler, f.restHandler, f.metricsHandler, f.pingHandler, f.dashboardHandler, f.acmeHTTPHandler) + internalHandlers := NewInternalHandlers(apiHandler, f.restHandler, f.metricsHandler, f.pingHandler, f.dashboardHandler, f.acmeHTTPHandler) + return NewManager(configuration.Services, f.observabilityMgr, f.routinesPool, f.transportManager, f.proxyBuilder, internalHandlers) } diff --git a/pkg/server/service/service.go b/pkg/server/service/service.go index 349c03179..1c7d01642 100644 --- a/pkg/server/service/service.go +++ b/pkg/server/service/service.go @@ -46,12 +46,18 @@ type ProxyBuilder interface { Update(configs map[string]*dynamic.ServersTransport) } +// ServiceBuilder is a Service builder. +type ServiceBuilder interface { + BuildHTTP(rootCtx context.Context, serviceName string) (http.Handler, error) +} + // Manager The service manager. type Manager struct { routinePool *safe.Pool observabilityMgr *middleware.ObservabilityMgr transportManager httputil.TransportManager proxyBuilder ProxyBuilder + serviceBuilders []ServiceBuilder services map[string]http.Handler configs map[string]*runtime.ServiceInfo @@ -60,12 +66,13 @@ type Manager struct { } // NewManager creates a new Manager. -func NewManager(configs map[string]*runtime.ServiceInfo, observabilityMgr *middleware.ObservabilityMgr, routinePool *safe.Pool, transportManager httputil.TransportManager, proxyBuilder ProxyBuilder) *Manager { +func NewManager(configs map[string]*runtime.ServiceInfo, observabilityMgr *middleware.ObservabilityMgr, routinePool *safe.Pool, transportManager httputil.TransportManager, proxyBuilder ProxyBuilder, serviceBuilders ...ServiceBuilder) *Manager { return &Manager{ routinePool: routinePool, observabilityMgr: observabilityMgr, transportManager: transportManager, proxyBuilder: proxyBuilder, + serviceBuilders: serviceBuilders, services: make(map[string]http.Handler), configs: configs, healthCheckers: make(map[string]*healthcheck.ServiceHealthChecker), @@ -85,6 +92,18 @@ func (m *Manager) BuildHTTP(rootCtx context.Context, serviceName string) (http.H return handler, nil } + // Must be before we get configs to handle services without config. + for _, builder := range m.serviceBuilders { + handler, err := builder.BuildHTTP(rootCtx, serviceName) + if err != nil { + return nil, err + } + if handler != nil { + m.services[serviceName] = handler + return handler, nil + } + } + conf, ok := m.configs[serviceName] if !ok { return nil, fmt.Errorf("the service %q does not exist", serviceName) diff --git a/pkg/server/service/service_test.go b/pkg/server/service/service_test.go index 2d5e46a76..9628db7ff 100644 --- a/pkg/server/service/service_test.go +++ b/pkg/server/service/service_test.go @@ -450,6 +450,48 @@ func Test1xxResponses(t *testing.T) { } } +type serviceBuilderFunc func(ctx context.Context, serviceName string) (http.Handler, error) + +func (s serviceBuilderFunc) BuildHTTP(ctx context.Context, serviceName string) (http.Handler, error) { + return s(ctx, serviceName) +} + +type internalHandler struct{} + +func (internalHandler) ServeHTTP(_ http.ResponseWriter, _ *http.Request) {} + +func TestManager_ServiceBuilders(t *testing.T) { + var internalHandler internalHandler + + manager := NewManager(map[string]*runtime.ServiceInfo{ + "test@test": { + Service: &dynamic.Service{ + LoadBalancer: &dynamic.ServersLoadBalancer{}, + }, + }, + }, nil, nil, &TransportManager{ + roundTrippers: map[string]http.RoundTripper{ + "default@internal": http.DefaultTransport, + }, + }, nil, serviceBuilderFunc(func(rootCtx context.Context, serviceName string) (http.Handler, error) { + if strings.HasSuffix(serviceName, "@internal") { + return internalHandler, nil + } + return nil, nil + })) + + h, err := manager.BuildHTTP(context.Background(), "test@internal") + require.NoError(t, err) + assert.Equal(t, internalHandler, h) + + h, err = manager.BuildHTTP(context.Background(), "test@test") + require.NoError(t, err) + assert.NotNil(t, h) + + _, err = manager.BuildHTTP(context.Background(), "wrong@test") + assert.Error(t, err) +} + func TestManager_Build(t *testing.T) { testCases := []struct { desc string