diff --git a/docs/content/reference/dynamic-configuration/file.toml b/docs/content/reference/dynamic-configuration/file.toml index 016a3f867..3f5b20af5 100644 --- a/docs/content/reference/dynamic-configuration/file.toml +++ b/docs/content/reference/dynamic-configuration/file.toml @@ -9,6 +9,7 @@ Rule = "foobar" priority = 42 [HTTP.Routers.Router0.tls] + options = "TLS0" [HTTP.Middlewares] @@ -206,6 +207,7 @@ Rule = "foobar" [TCP.Routers.TCPRouter0.tls] passthrough = true + options = "TLS1" [TCP.Services] diff --git a/docs/content/reference/dynamic-configuration/labels.yml b/docs/content/reference/dynamic-configuration/labels.yml index a2bbe146c..c7ba2981e 100644 --- a/docs/content/reference/dynamic-configuration/labels.yml +++ b/docs/content/reference/dynamic-configuration/labels.yml @@ -109,6 +109,7 @@ labels: - "traefik.HTTP.Routers.Router0.Rule=foobar" - "traefik.HTTP.Routers.Router0.Service=foobar" - "traefik.HTTP.Routers.Router0.TLS=true" +- "traefik.HTTP.Routers.Router0.TLS.options=foo" - "traefik.HTTP.Routers.Router1.EntryPoints=foobar, fiibar" - "traefik.HTTP.Routers.Router1.Middlewares=foobar, fiibar" - "traefik.HTTP.Routers.Router1.Priority=42" @@ -143,9 +144,11 @@ labels: - "traefik.TCP.Routers.Router0.EntryPoints=foobar, fiibar" - "traefik.TCP.Routers.Router0.Service=foobar" - "traefik.TCP.Routers.Router0.TLS.Passthrough=false" +- "traefik.TCP.Routers.Router0.TLS.options=bar" - "traefik.TCP.Routers.Router1.Rule=foobar" - "traefik.TCP.Routers.Router1.EntryPoints=foobar, fiibar" - "traefik.TCP.Routers.Router1.Service=foobar" - "traefik.TCP.Routers.Router1.TLS.Passthrough=false" +- "traefik.TCP.Routers.Router1.TLS.options=foobar" - "traefik.TCP.Services.Service0.LoadBalancer.server.Port=42" - "traefik.TCP.Services.Service1.LoadBalancer.server.Port=42" diff --git a/docs/content/routing/routers/index.md b/docs/content/routing/routers/index.md index 1400a3957..05be2b464 100644 --- a/docs/content/routing/routers/index.md +++ b/docs/content/routing/routers/index.md @@ -156,7 +156,9 @@ Services are the target for the router. ### TLS -When specifying a TLS section, you tell Traefik that the current router is dedicated to HTTPS requests only (and that the router should ignore HTTP (non tls) requests). +#### General + + When a TLS section is specified, it instructs Traefik that the current router is dedicated to HTTPS requests only (and that the router should ignore HTTP (non TLS) requests). Traefik will terminate the SSL connections (meaning that it will send decrypted data to the services). ??? example "Configuring the router to accept HTTPS requests only" @@ -172,8 +174,7 @@ Traefik will terminate the SSL connections (meaning that it will send decrypted !!! note "HTTPS & ACME" In the current version, with [ACME](../../https-tls/acme.md) enabled, automatic certificate generation will apply to every router declaring a TLS section. - In the near future, options will be available to enable fine-grain control of the TLS parameters. - + !!! note "Passthrough" On TCP routers, you can configure a passthrough option so that Traefik doesn't terminate the TLS connection. @@ -196,6 +197,31 @@ Traefik will terminate the SSL connections (meaning that it will send decrypted service = "service-id" ``` +#### `Options` + +The `Options` field enables fine-grained control of the TLS parameters. +It refers to a [tlsOptions](../../https-tls/overview/#configuration-options) and will be applied only if a `Host` rule is defined. + +??? example "Configuring the tls options" + + ```toml + [http.routers] + [http.routers.Router-1] + rule = "Host(`foo-domain`) && Path(`/foo-path/`)" + service = "service-id" + [http.routers.Router-1.tls] # will terminate the TLS request + options = "foo" + + + [tlsOptions] + [tlsOptions.foo] + minVersion = "VersionTLS12" + cipherSuites = [ + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_RSA_WITH_AES_256_GCM_SHA384" + ] + ``` + ## Configuring TCP Routers ### General @@ -269,8 +295,10 @@ Services are the target for the router. ### TLS -When specifying a TLS section, you tell Traefik that the current router is dedicated to TLS requests only (and that the router should ignore non-tls requests). -By default, Traefik will terminate the SSL connections (meaning that it will send decrypted data to the services), but you can tell Traefik that the request should pass through (keeping the encrypted data) and be forwarded to the service "as is". +#### General + + When a TLS section is specified, it instructs Traefik that the current router is dedicated to TLS requests only (and that the router should ignore non-TLS requests). + By default, Traefik will terminate the SSL connections (meaning that it will send decrypted data to the services), but Traefik can be configured in order to let the requests pass through (keeping the data encrypted), and be forwarded to the service "as is". ??? example "Configuring TLS Termination" @@ -296,4 +324,28 @@ By default, Traefik will terminate the SSL connections (meaning that it will sen !!! note "TLS & ACME" In the current version, with [ACME](../../https-tls/acme.md) enabled, automatic certificate generation will apply to every router declaring a TLS section. - In the near future, options will be available to enable fine-grain control of the TLS parameters. + +#### `Options` + +The `Options` field enables fine-grained control of the TLS parameters. +It refers to a [tlsOptions](../../https-tls/overview/#configuration-options) and will be applied only if a `HostSNI` rule is defined. + +??? example "Configuring the tls options" + + ```toml + [tcp.routers] + [tcp.routers.Router-1] + rule = "Host(`foo-domain`) && Path(`/foo-path/`)" + service = "service-id" + [tcp.routers.Router-1.tls] # will terminate the TLS request + options = "foo" + + + [tlsOptions] + [tlsOptions.foo] + minVersion = "VersionTLS12" + cipherSuites = [ + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_RSA_WITH_AES_256_GCM_SHA384" + ] + ``` diff --git a/integration/fixtures/https/https_tls_options.toml b/integration/fixtures/https/https_tls_options.toml new file mode 100644 index 000000000..c0072efcd --- /dev/null +++ b/integration/fixtures/https/https_tls_options.toml @@ -0,0 +1,62 @@ +[global] +checkNewVersion = false +sendAnonymousUsage = false + +[log] +level = "DEBUG" + +[entryPoints] + [entryPoints.web-secure] + address = ":4443" + +[api] + +[providers] + [providers.file] + +[http.routers] + [http.routers.router1] + Service = "service1" + Rule = "Host(`snitest.com`)" + [http.routers.router1.tls] + options = "foo" + + [http.routers.router2] + Service = "service2" + Rule = "Host(`snitest.org`)" + [http.routers.router2.tls] + options = "bar" + + [http.routers.router3] + Service = "service2" + Rule = "Host(`snitest.org`)" + [http.routers.router3.tls] + options = "unknown" + +[http.services] + [http.services.service1] + [http.services.service1.LoadBalancer] + [[http.services.service1.LoadBalancer.Servers]] + URL = "http://127.0.0.1:9010" + + [http.services.service2] + [http.services.service2.LoadBalancer] + [[http.services.service2.LoadBalancer.Servers]] + URL = "http://127.0.0.1:9020" + + +[[tls]] + [tls.certificate] + certFile = "fixtures/https/snitest.com.cert" + keyFile = "fixtures/https/snitest.com.key" + +[[tls]] + [tls.certificate] + certFile = "fixtures/https/snitest.org.cert" + keyFile = "fixtures/https/snitest.org.key" + +[tlsoptions.foo] + minversion = "VersionTLS11" + +[tlsoptions.bar] + minversion = "VersionTLS12" diff --git a/integration/fixtures/ratelimit/simple.toml b/integration/fixtures/ratelimit/simple.toml index d7508877e..cfd7a888c 100644 --- a/integration/fixtures/ratelimit/simple.toml +++ b/integration/fixtures/ratelimit/simple.toml @@ -2,12 +2,19 @@ checkNewVersion = false sendAnonymousUsage = false +[api] +entrypoint="api" + [log] level = "DEBUG" [entryPoints] [entryPoints.web] - address = ":80" + address = ":8081" + + + [entryPoints.api] + address = ":8080" [providers] [providers.file] diff --git a/integration/fixtures/tcp/catch-all-no-tls-with-https.toml b/integration/fixtures/tcp/catch-all-no-tls-with-https.toml index fbb8e133b..da1c0b62e 100644 --- a/integration/fixtures/tcp/catch-all-no-tls-with-https.toml +++ b/integration/fixtures/tcp/catch-all-no-tls-with-https.toml @@ -38,4 +38,3 @@ level = "DEBUG" [http.services.whoami.loadbalancer] [[http.services.whoami.loadbalancer.servers]] url = "http://localhost:8085" - weight=1 diff --git a/integration/fixtures/tcp/multi-tls-options.toml b/integration/fixtures/tcp/multi-tls-options.toml new file mode 100644 index 000000000..36b75fc67 --- /dev/null +++ b/integration/fixtures/tcp/multi-tls-options.toml @@ -0,0 +1,42 @@ +[global] +checkNewVersion = false +sendAnonymousUsage = false + +[log] +level = "DEBUG" + +[entryPoints] + [entryPoints.tcp] + address = ":8093" + +[api] + +[providers.file] + +[tcp] + [tcp.routers] + [tcp.routers.to-whoami-no-cert] + rule = "HostSNI(`whoami-c.test`)" + service = "whoami-no-cert" + entryPoints = [ "tcp" ] + [tcp.routers.to-whoami-no-cert.tls] + options = "foo" + + [tcp.routers.to-whoami-sni-strict] + rule = "HostSNI(`whoami-d.test`)" + service = "whoami-no-cert" + entryPoints = [ "tcp" ] + [tcp.routers.to-whoami-sni-strict.tls] + options = "bar" + + [tcp.services.whoami-no-cert] + [tcp.services.whoami-no-cert.loadbalancer] + method = "wrr" + [[tcp.services.whoami-no-cert.loadbalancer.servers]] + address = "localhost:8083" + +[tlsoptions.foo] + minversion = "VersionTLS11" + +[tlsoptions.bar] + minversion = "VersionTLS12" diff --git a/integration/https_test.go b/integration/https_test.go index 8e0118f6c..2b43ae673 100644 --- a/integration/https_test.go +++ b/integration/https_test.go @@ -111,6 +111,90 @@ func (s *HTTPSSuite) TestWithSNIConfigRoute(c *check.C) { c.Assert(err, checker.IsNil) } +// TestWithTLSOptions verifies that traefik routes the requests with the associated tls options. +func (s *HTTPSSuite) TestWithTLSOptions(c *check.C) { + cmd, display := s.traefikCmd(withConfigFile("fixtures/https/https_tls_options.toml")) + defer display(c) + err := cmd.Start() + c.Assert(err, checker.IsNil) + defer cmd.Process.Kill() + + // wait for Traefik + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`snitest.org`)")) + c.Assert(err, checker.IsNil) + + backend1 := startTestServer("9010", http.StatusNoContent) + backend2 := startTestServer("9020", http.StatusResetContent) + defer backend1.Close() + defer backend2.Close() + + err = try.GetRequest(backend1.URL, 1*time.Second, try.StatusCodeIs(http.StatusNoContent)) + c.Assert(err, checker.IsNil) + err = try.GetRequest(backend2.URL, 1*time.Second, try.StatusCodeIs(http.StatusResetContent)) + c.Assert(err, checker.IsNil) + + tr1 := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + MaxVersion: tls.VersionTLS11, + ServerName: "snitest.com", + }, + } + + tr2 := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + MaxVersion: tls.VersionTLS12, + ServerName: "snitest.org", + }, + } + + tr3 := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + MaxVersion: tls.VersionTLS11, + ServerName: "snitest.org", + }, + } + + // With valid TLS options and request + req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil) + c.Assert(err, checker.IsNil) + req.Host = tr1.TLSClientConfig.ServerName + req.Header.Set("Host", tr1.TLSClientConfig.ServerName) + req.Header.Set("Accept", "*/*") + + err = try.RequestWithTransport(req, 30*time.Second, tr1, try.HasCn(tr1.TLSClientConfig.ServerName), try.StatusCodeIs(http.StatusNoContent)) + c.Assert(err, checker.IsNil) + + // With a valid TLS version + req, err = http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil) + c.Assert(err, checker.IsNil) + req.Host = tr2.TLSClientConfig.ServerName + req.Header.Set("Host", tr2.TLSClientConfig.ServerName) + req.Header.Set("Accept", "*/*") + + err = try.RequestWithTransport(req, 3*time.Second, tr2, try.HasCn(tr2.TLSClientConfig.ServerName), try.StatusCodeIs(http.StatusResetContent)) + c.Assert(err, checker.IsNil) + + // With a bad TLS version + req, err = http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil) + c.Assert(err, checker.IsNil) + req.Host = tr3.TLSClientConfig.ServerName + req.Header.Set("Host", tr3.TLSClientConfig.ServerName) + req.Header.Set("Accept", "*/*") + client := http.Client{ + Transport: tr3, + } + _, err = client.Do(req) + c.Assert(err, checker.NotNil) + c.Assert(err.Error(), checker.Contains, "protocol version not supported") + + // with unknown tls option + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("unknown TLS options: unknown")) + c.Assert(err, checker.IsNil) +} + // TestWithSNIStrictNotMatchedRequest involves a client sending a SNI hostname of // "snitest.org", which does not match the CN of 'snitest.com.crt'. The test // verifies that traefik closes the connection. diff --git a/integration/ratelimit_test.go b/integration/ratelimit_test.go index 163744313..204221b80 100644 --- a/integration/ratelimit_test.go +++ b/integration/ratelimit_test.go @@ -28,34 +28,38 @@ func (s *RateLimitSuite) TestSimpleConfiguration(c *check.C) { }{s.ServerIP}) defer os.Remove(file) - cmd, _ := s.cmdTraefik(withConfigFile(file)) + cmd, display := s.traefikCmd(withConfigFile(file)) + defer display(c) err := cmd.Start() c.Assert(err, checker.IsNil) defer cmd.Process.Kill() - err = try.GetRequest("http://127.0.0.1:80/", 500*time.Millisecond, try.StatusCodeIs(http.StatusOK)) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("ratelimit")) c.Assert(err, checker.IsNil) - err = try.GetRequest("http://127.0.0.1:80/", 500*time.Millisecond, try.StatusCodeIs(http.StatusOK)) + + err = try.GetRequest("http://127.0.0.1:8081/", 500*time.Millisecond, try.StatusCodeIs(http.StatusOK)) c.Assert(err, checker.IsNil) - err = try.GetRequest("http://127.0.0.1:80/", 500*time.Millisecond, try.StatusCodeIs(http.StatusTooManyRequests)) + err = try.GetRequest("http://127.0.0.1:8081/", 500*time.Millisecond, try.StatusCodeIs(http.StatusOK)) + c.Assert(err, checker.IsNil) + err = try.GetRequest("http://127.0.0.1:8081/", 500*time.Millisecond, try.StatusCodeIs(http.StatusTooManyRequests)) c.Assert(err, checker.IsNil) // sleep for 4 seconds to be certain the configured time period has elapsed // then test another request and verify a 200 status code time.Sleep(4 * time.Second) - err = try.GetRequest("http://127.0.0.1:80/", 500*time.Millisecond, try.StatusCodeIs(http.StatusOK)) + err = try.GetRequest("http://127.0.0.1:8081/", 500*time.Millisecond, try.StatusCodeIs(http.StatusOK)) c.Assert(err, checker.IsNil) // continue requests at 3 second intervals to test the other rate limit time period time.Sleep(3 * time.Second) - err = try.GetRequest("http://127.0.0.1:80/", 500*time.Millisecond, try.StatusCodeIs(http.StatusOK)) + err = try.GetRequest("http://127.0.0.1:8081/", 500*time.Millisecond, try.StatusCodeIs(http.StatusOK)) c.Assert(err, checker.IsNil) time.Sleep(3 * time.Second) - err = try.GetRequest("http://127.0.0.1:80/", 500*time.Millisecond, try.StatusCodeIs(http.StatusOK)) + err = try.GetRequest("http://127.0.0.1:8081/", 500*time.Millisecond, try.StatusCodeIs(http.StatusOK)) c.Assert(err, checker.IsNil) time.Sleep(3 * time.Second) - err = try.GetRequest("http://127.0.0.1:80/", 500*time.Millisecond, try.StatusCodeIs(http.StatusTooManyRequests)) + err = try.GetRequest("http://127.0.0.1:8081/", 500*time.Millisecond, try.StatusCodeIs(http.StatusTooManyRequests)) c.Assert(err, checker.IsNil) } diff --git a/integration/tcp_test.go b/integration/tcp_test.go index 54188453a..095993166 100644 --- a/integration/tcp_test.go +++ b/integration/tcp_test.go @@ -70,6 +70,36 @@ func (s *TCPSuite) TestMixed(c *check.C) { c.Assert(err, checker.IsNil) } +func (s *TCPSuite) TestTLSOptions(c *check.C) { + file := s.adaptFile(c, "fixtures/tcp/multi-tls-options.toml", struct{}{}) + defer os.Remove(file) + + cmd, display := s.traefikCmd(withConfigFile(file)) + defer display(c) + + err := cmd.Start() + c.Assert(err, checker.IsNil) + defer cmd.Process.Kill() + + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 500*time.Millisecond, try.StatusCodeIs(http.StatusOK), try.BodyContains("HostSNI(`whoami-c.test`)")) + c.Assert(err, checker.IsNil) + + // Check that we can use a client tls version <= 1.1 with hostSNI 'whoami-c.test' + out, err := guessWhoTLSMaxVersion("127.0.0.1:8093", "whoami-c.test", true, tls.VersionTLS11) + c.Assert(err, checker.IsNil) + c.Assert(out, checker.Contains, "whoami-no-cert") + + // Check that we can use a client tls version <= 1.2 with hostSNI 'whoami-d.test' + out, err = guessWhoTLSMaxVersion("127.0.0.1:8093", "whoami-d.test", true, tls.VersionTLS12) + c.Assert(err, checker.IsNil) + c.Assert(out, checker.Contains, "whoami-no-cert") + + // Check that we cannot use a client tls version <= 1.1 with hostSNI 'whoami-d.test' + _, err = guessWhoTLSMaxVersion("127.0.0.1:8093", "whoami-d.test", true, tls.VersionTLS11) + c.Assert(err, checker.NotNil) + c.Assert(err.Error(), checker.Contains, "protocol version not supported") +} + func (s *TCPSuite) TestNonTLSFallback(c *check.C) { file := s.adaptFile(c, "fixtures/tcp/non-tls-fallback.toml", struct{}{}) defer os.Remove(file) @@ -191,11 +221,21 @@ func welcome(addr string) (string, error) { } func guessWho(addr, serverName string, tlsCall bool) (string, error) { + return guessWhoTLSMaxVersion(addr, serverName, tlsCall, 0) +} + +func guessWhoTLSMaxVersion(addr, serverName string, tlsCall bool, tlsMaxVersion uint16) (string, error) { var conn net.Conn var err error if tlsCall { - conn, err = tls.Dial("tcp", addr, &tls.Config{ServerName: serverName, InsecureSkipVerify: true}) + + conn, err = tls.Dial("tcp", addr, &tls.Config{ + ServerName: serverName, + InsecureSkipVerify: true, + MinVersion: 0, + MaxVersion: tlsMaxVersion, + }) } else { tcpAddr, err2 := net.ResolveTCPAddr("tcp", addr) if err2 != nil { diff --git a/pkg/anonymize/anonymize_doOnJSON_test.go b/pkg/anonymize/anonymize_doOnJSON_test.go index dc8da18dd..24e565d2b 100644 --- a/pkg/anonymize/anonymize_doOnJSON_test.go +++ b/pkg/anonymize/anonymize_doOnJSON_test.go @@ -58,7 +58,7 @@ func Test_doOnJSON(t *testing.T) { "DNSProvider": "", "DelayDontCheckDNS": 0, "ACMELogging": false, - "TLSOptions": null + "Options": null }, "DefaultEntryPoints": [ "https", @@ -141,7 +141,7 @@ func Test_doOnJSON(t *testing.T) { "DNSProvider": "", "DelayDontCheckDNS": 0, "ACMELogging": false, - "TLSOptions": null + "Options": null }, "DefaultEntryPoints": [ "https", diff --git a/pkg/config/dyn_config.go b/pkg/config/dyn_config.go index 1ccea24aa..b08ebd227 100644 --- a/pkg/config/dyn_config.go +++ b/pkg/config/dyn_config.go @@ -22,7 +22,9 @@ type Router struct { } // RouterTLSConfig holds the TLS configuration for a router -type RouterTLSConfig struct{} +type RouterTLSConfig struct { + Options string `json:"options,omitempty" toml:"options,omitzero"` +} // TCPRouter holds the router configuration. type TCPRouter struct { @@ -34,7 +36,8 @@ type TCPRouter struct { // RouterTCPTLSConfig holds the TLS configuration for a router type RouterTCPTLSConfig struct { - Passthrough bool `json:"passthrough" toml:"passthrough,omitzero"` + Passthrough bool `json:"passthrough" toml:"passthrough,omitzero"` + Options string `json:"options,omitempty" toml:"options,omitzero"` } // LoadBalancerService holds the LoadBalancerService configuration. diff --git a/pkg/config/label/label_test.go b/pkg/config/label/label_test.go index 78ee957ff..88672cdc5 100644 --- a/pkg/config/label/label_test.go +++ b/pkg/config/label/label_test.go @@ -162,9 +162,11 @@ func TestDecodeConfiguration(t *testing.T) { "traefik.tcp.routers.Router0.entrypoints": "foobar, fiibar", "traefik.tcp.routers.Router0.service": "foobar", "traefik.tcp.routers.Router0.tls.passthrough": "false", + "traefik.tcp.routers.Router0.tls.options": "foo", "traefik.tcp.routers.Router1.rule": "foobar", "traefik.tcp.routers.Router1.entrypoints": "foobar, fiibar", "traefik.tcp.routers.Router1.service": "foobar", + "traefik.tcp.routers.Router1.tls.options": "foo", "traefik.tcp.routers.Router1.tls.passthrough": "false", "traefik.tcp.services.Service0.loadbalancer.server.Port": "42", "traefik.tcp.services.Service1.loadbalancer.server.Port": "42", @@ -185,6 +187,7 @@ func TestDecodeConfiguration(t *testing.T) { Rule: "foobar", TLS: &config.RouterTCPTLSConfig{ Passthrough: false, + Options: "foo", }, }, "Router1": { @@ -196,6 +199,7 @@ func TestDecodeConfiguration(t *testing.T) { Rule: "foobar", TLS: &config.RouterTCPTLSConfig{ Passthrough: false, + Options: "foo", }, }, }, @@ -580,6 +584,7 @@ func TestEncodeConfiguration(t *testing.T) { Rule: "foobar", TLS: &config.RouterTCPTLSConfig{ Passthrough: false, + Options: "foo", }, }, "Router1": { @@ -591,6 +596,7 @@ func TestEncodeConfiguration(t *testing.T) { Rule: "foobar", TLS: &config.RouterTCPTLSConfig{ Passthrough: false, + Options: "foo", }, }, }, @@ -1110,10 +1116,12 @@ func TestEncodeConfiguration(t *testing.T) { "traefik.TCP.Routers.Router0.EntryPoints": "foobar, fiibar", "traefik.TCP.Routers.Router0.Service": "foobar", "traefik.TCP.Routers.Router0.TLS.Passthrough": "false", + "traefik.TCP.Routers.Router0.TLS.Options": "foo", "traefik.TCP.Routers.Router1.Rule": "foobar", "traefik.TCP.Routers.Router1.EntryPoints": "foobar, fiibar", "traefik.TCP.Routers.Router1.Service": "foobar", "traefik.TCP.Routers.Router1.TLS.Passthrough": "false", + "traefik.TCP.Routers.Router1.TLS.Options": "foo", "traefik.TCP.Services.Service0.LoadBalancer.server.Port": "42", "traefik.TCP.Services.Service1.LoadBalancer.server.Port": "42", } diff --git a/pkg/config/runtime.go b/pkg/config/runtime.go index b1d2409b6..046ce9155 100644 --- a/pkg/config/runtime.go +++ b/pkg/config/runtime.go @@ -1,6 +1,7 @@ package config import ( + "context" "sort" "strings" "sync" @@ -128,6 +129,74 @@ func (r *RuntimeConfiguration) PopulateUsedBy() { } } +func contains(entryPoints []string, entryPointName string) bool { + for _, name := range entryPoints { + if name == entryPointName { + return true + } + } + return false +} + +// GetRoutersByEntrypoints returns all the http routers by entrypoints name and routers name +func (r *RuntimeConfiguration) GetRoutersByEntrypoints(ctx context.Context, entryPoints []string, tls bool) map[string]map[string]*RouterInfo { + entryPointsRouters := make(map[string]map[string]*RouterInfo) + + for rtName, rt := range r.Routers { + if (tls && rt.TLS == nil) || (!tls && rt.TLS != nil) { + continue + } + + eps := rt.EntryPoints + if len(eps) == 0 { + eps = entryPoints + } + for _, entryPointName := range eps { + if !contains(entryPoints, entryPointName) { + log.FromContext(log.With(ctx, log.Str(log.EntryPointName, entryPointName))). + Errorf("entryPoint %q doesn't exist", entryPointName) + continue + } + + if _, ok := entryPointsRouters[entryPointName]; !ok { + entryPointsRouters[entryPointName] = make(map[string]*RouterInfo) + } + + entryPointsRouters[entryPointName][rtName] = rt + } + } + + return entryPointsRouters +} + +// GetTCPRoutersByEntrypoints returns all the tcp routers by entrypoints name and routers name +func (r *RuntimeConfiguration) GetTCPRoutersByEntrypoints(ctx context.Context, entryPoints []string) map[string]map[string]*TCPRouterInfo { + entryPointsRouters := make(map[string]map[string]*TCPRouterInfo) + + for rtName, rt := range r.TCPRouters { + eps := rt.EntryPoints + if len(eps) == 0 { + eps = entryPoints + } + + for _, entryPointName := range eps { + if !contains(entryPoints, entryPointName) { + log.FromContext(log.With(ctx, log.Str(log.EntryPointName, entryPointName))). + Errorf("entryPoint %q doesn't exist", entryPointName) + continue + } + + if _, ok := entryPointsRouters[entryPointName]; !ok { + entryPointsRouters[entryPointName] = make(map[string]*TCPRouterInfo) + } + + entryPointsRouters[entryPointName][rtName] = rt + } + } + + return entryPointsRouters +} + // RouterInfo holds information about a currently running HTTP router type RouterInfo struct { *Router // dynamic configuration diff --git a/pkg/config/runtime_test.go b/pkg/config/runtime_test.go index 11e14a7b2..66aa3174c 100644 --- a/pkg/config/runtime_test.go +++ b/pkg/config/runtime_test.go @@ -1,6 +1,7 @@ package config_test import ( + "context" "testing" "github.com/containous/traefik/pkg/config" @@ -688,3 +689,399 @@ func TestPopulateUsedby(t *testing.T) { } } + +func TestGetTCPRoutersByEntrypoints(t *testing.T) { + testCases := []struct { + desc string + conf config.Configuration + entryPoints []string + expected map[string]map[string]*config.TCPRouterInfo + }{ + { + desc: "Empty Configuration without entrypoint", + conf: config.Configuration{}, + entryPoints: []string{""}, + expected: map[string]map[string]*config.TCPRouterInfo{}, + }, + { + desc: "Empty Configuration with unknown entrypoints", + conf: config.Configuration{}, + entryPoints: []string{"foo"}, + expected: map[string]map[string]*config.TCPRouterInfo{}, + }, + { + desc: "Valid configuration with an unknown entrypoint", + conf: config.Configuration{ + HTTP: &config.HTTPConfiguration{ + Routers: map[string]*config.Router{ + "foo": { + EntryPoints: []string{"web"}, + Service: "myprovider.foo-service", + Rule: "Host(`bar.foo`)", + }, + }, + }, + TCP: &config.TCPConfiguration{ + Routers: map[string]*config.TCPRouter{ + "foo": { + EntryPoints: []string{"web"}, + Service: "myprovider.foo-service", + Rule: "HostSNI(`bar.foo`)", + }, + }, + }, + }, + entryPoints: []string{"foo"}, + expected: map[string]map[string]*config.TCPRouterInfo{}, + }, + { + desc: "Valid configuration with a known entrypoint", + conf: config.Configuration{ + HTTP: &config.HTTPConfiguration{ + Routers: map[string]*config.Router{ + "foo": { + EntryPoints: []string{"web"}, + Service: "myprovider.foo-service", + Rule: "Host(`bar.foo`)", + }, + "bar": { + EntryPoints: []string{"webs"}, + Service: "myprovider.bar-service", + Rule: "Host(`foo.bar`)", + }, + "foobar": { + EntryPoints: []string{"web", "webs"}, + Service: "myprovider.foobar-service", + Rule: "Host(`bar.foobar`)", + }, + }, + }, + TCP: &config.TCPConfiguration{ + Routers: map[string]*config.TCPRouter{ + "foo": { + EntryPoints: []string{"web"}, + Service: "myprovider.foo-service", + Rule: "HostSNI(`bar.foo`)", + }, + "bar": { + EntryPoints: []string{"webs"}, + Service: "myprovider.bar-service", + Rule: "HostSNI(`foo.bar`)", + }, + "foobar": { + EntryPoints: []string{"web", "webs"}, + Service: "myprovider.foobar-service", + Rule: "HostSNI(`bar.foobar`)", + }, + }, + }, + }, + entryPoints: []string{"web"}, + expected: map[string]map[string]*config.TCPRouterInfo{ + "web": { + "foo": { + TCPRouter: &config.TCPRouter{ + EntryPoints: []string{"web"}, + Service: "myprovider.foo-service", + Rule: "HostSNI(`bar.foo`)", + }, + }, + "foobar": { + TCPRouter: &config.TCPRouter{ + EntryPoints: []string{"web", "webs"}, + Service: "myprovider.foobar-service", + Rule: "HostSNI(`bar.foobar`)", + }, + }, + }, + }, + }, + { + desc: "Valid configuration with multiple known entrypoints", + conf: config.Configuration{ + HTTP: &config.HTTPConfiguration{ + Routers: map[string]*config.Router{ + "foo": { + EntryPoints: []string{"web"}, + Service: "myprovider.foo-service", + Rule: "Host(`bar.foo`)", + }, + "bar": { + EntryPoints: []string{"webs"}, + Service: "myprovider.bar-service", + Rule: "Host(`foo.bar`)", + }, + "foobar": { + EntryPoints: []string{"web", "webs"}, + Service: "myprovider.foobar-service", + Rule: "Host(`bar.foobar`)", + }, + }, + }, + TCP: &config.TCPConfiguration{ + Routers: map[string]*config.TCPRouter{ + "foo": { + EntryPoints: []string{"web"}, + Service: "myprovider.foo-service", + Rule: "HostSNI(`bar.foo`)", + }, + "bar": { + EntryPoints: []string{"webs"}, + Service: "myprovider.bar-service", + Rule: "HostSNI(`foo.bar`)", + }, + "foobar": { + EntryPoints: []string{"web", "webs"}, + Service: "myprovider.foobar-service", + Rule: "HostSNI(`bar.foobar`)", + }, + }, + }, + }, + entryPoints: []string{"web", "webs"}, + expected: map[string]map[string]*config.TCPRouterInfo{ + "web": { + "foo": { + TCPRouter: &config.TCPRouter{ + EntryPoints: []string{"web"}, + Service: "myprovider.foo-service", + Rule: "HostSNI(`bar.foo`)", + }, + }, + "foobar": { + TCPRouter: &config.TCPRouter{ + EntryPoints: []string{"web", "webs"}, + Service: "myprovider.foobar-service", + Rule: "HostSNI(`bar.foobar`)", + }, + }, + }, + "webs": { + "bar": { + TCPRouter: &config.TCPRouter{ + + EntryPoints: []string{"webs"}, + Service: "myprovider.bar-service", + Rule: "HostSNI(`foo.bar`)", + }, + }, + "foobar": { + TCPRouter: &config.TCPRouter{ + EntryPoints: []string{"web", "webs"}, + Service: "myprovider.foobar-service", + Rule: "HostSNI(`bar.foobar`)", + }, + }, + }, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + runtimeConfig := config.NewRuntimeConfig(test.conf) + actual := runtimeConfig.GetTCPRoutersByEntrypoints(context.Background(), test.entryPoints) + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestGetRoutersByEntrypoints(t *testing.T) { + testCases := []struct { + desc string + conf config.Configuration + entryPoints []string + expected map[string]map[string]*config.RouterInfo + }{ + { + desc: "Empty Configuration without entrypoint", + conf: config.Configuration{}, + entryPoints: []string{""}, + expected: map[string]map[string]*config.RouterInfo{}, + }, + { + desc: "Empty Configuration with unknown entrypoints", + conf: config.Configuration{}, + entryPoints: []string{"foo"}, + expected: map[string]map[string]*config.RouterInfo{}, + }, + { + desc: "Valid configuration with an unknown entrypoint", + conf: config.Configuration{ + HTTP: &config.HTTPConfiguration{ + Routers: map[string]*config.Router{ + "foo": { + EntryPoints: []string{"web"}, + Service: "myprovider.foo-service", + Rule: "Host(`bar.foo`)", + }, + }, + }, + TCP: &config.TCPConfiguration{ + Routers: map[string]*config.TCPRouter{ + "foo": { + EntryPoints: []string{"web"}, + Service: "myprovider.foo-service", + Rule: "HostSNI(`bar.foo`)", + }, + }, + }, + }, + entryPoints: []string{"foo"}, + expected: map[string]map[string]*config.RouterInfo{}, + }, + { + desc: "Valid configuration with a known entrypoint", + conf: config.Configuration{ + HTTP: &config.HTTPConfiguration{ + Routers: map[string]*config.Router{ + "foo": { + EntryPoints: []string{"web"}, + Service: "myprovider.foo-service", + Rule: "Host(`bar.foo`)", + }, + "bar": { + EntryPoints: []string{"webs"}, + Service: "myprovider.bar-service", + Rule: "Host(`foo.bar`)", + }, + "foobar": { + EntryPoints: []string{"web", "webs"}, + Service: "myprovider.foobar-service", + Rule: "Host(`bar.foobar`)", + }, + }, + }, + TCP: &config.TCPConfiguration{ + Routers: map[string]*config.TCPRouter{ + "foo": { + EntryPoints: []string{"web"}, + Service: "myprovider.foo-service", + Rule: "HostSNI(`bar.foo`)", + }, + "bar": { + EntryPoints: []string{"webs"}, + Service: "myprovider.bar-service", + Rule: "HostSNI(`foo.bar`)", + }, + "foobar": { + EntryPoints: []string{"web", "webs"}, + Service: "myprovider.foobar-service", + Rule: "HostSNI(`bar.foobar`)", + }, + }, + }, + }, + entryPoints: []string{"web"}, + expected: map[string]map[string]*config.RouterInfo{ + "web": { + "foo": { + Router: &config.Router{ + EntryPoints: []string{"web"}, + Service: "myprovider.foo-service", + Rule: "Host(`bar.foo`)", + }, + }, + "foobar": { + Router: &config.Router{ + EntryPoints: []string{"web", "webs"}, + Service: "myprovider.foobar-service", + Rule: "Host(`bar.foobar`)", + }, + }, + }, + }, + }, + { + desc: "Valid configuration with multiple known entrypoints", + conf: config.Configuration{ + HTTP: &config.HTTPConfiguration{ + Routers: map[string]*config.Router{ + "foo": { + EntryPoints: []string{"web"}, + Service: "myprovider.foo-service", + Rule: "Host(`bar.foo`)", + }, + "bar": { + EntryPoints: []string{"webs"}, + Service: "myprovider.bar-service", + Rule: "Host(`foo.bar`)", + }, + "foobar": { + EntryPoints: []string{"web", "webs"}, + Service: "myprovider.foobar-service", + Rule: "Host(`bar.foobar`)", + }, + }, + }, + TCP: &config.TCPConfiguration{ + Routers: map[string]*config.TCPRouter{ + "foo": { + EntryPoints: []string{"web"}, + Service: "myprovider.foo-service", + Rule: "HostSNI(`bar.foo`)", + }, + "bar": { + EntryPoints: []string{"webs"}, + Service: "myprovider.bar-service", + Rule: "HostSNI(`foo.bar`)", + }, + "foobar": { + EntryPoints: []string{"web", "webs"}, + Service: "myprovider.foobar-service", + Rule: "HostSNI(`bar.foobar`)", + }, + }, + }, + }, + entryPoints: []string{"web", "webs"}, + expected: map[string]map[string]*config.RouterInfo{ + "web": { + "foo": { + Router: &config.Router{ + EntryPoints: []string{"web"}, + Service: "myprovider.foo-service", + Rule: "Host(`bar.foo`)", + }, + }, + "foobar": { + Router: &config.Router{ + EntryPoints: []string{"web", "webs"}, + Service: "myprovider.foobar-service", + Rule: "Host(`bar.foobar`)", + }, + }, + }, + "webs": { + "bar": { + Router: &config.Router{ + + EntryPoints: []string{"webs"}, + Service: "myprovider.bar-service", + Rule: "Host(`foo.bar`)", + }, + }, + "foobar": { + Router: &config.Router{ + EntryPoints: []string{"web", "webs"}, + Service: "myprovider.foobar-service", + Rule: "Host(`bar.foobar`)", + }, + }, + }, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + runtimeConfig := config.NewRuntimeConfig(test.conf) + actual := runtimeConfig.GetRoutersByEntrypoints(context.Background(), test.entryPoints, false) + assert.Equal(t, test.expected, actual) + }) + } +} diff --git a/pkg/provider/docker/config_test.go b/pkg/provider/docker/config_test.go index 484aa510b..ac801157d 100644 --- a/pkg/provider/docker/config_test.go +++ b/pkg/provider/docker/config_test.go @@ -2164,7 +2164,7 @@ func Test_buildConfiguration(t *testing.T) { Name: "Test", Labels: map[string]string{ "traefik.tcp.routers.foo.rule": "HostSNI(`foo.bar`)", - "traefik.tcp.routers.foo.tls": "true", + "traefik.tcp.routers.foo.tls.options": "foo", "traefik.tcp.services.foo.loadbalancer.server.port": "8080", }, NetworkSettings: networkSettings{ @@ -2186,7 +2186,9 @@ func Test_buildConfiguration(t *testing.T) { "foo": { Service: "foo", Rule: "HostSNI(`foo.bar`)", - TLS: &config.RouterTCPTLSConfig{}, + TLS: &config.RouterTCPTLSConfig{ + Options: "foo", + }, }, }, Services: map[string]*config.TCPService{ diff --git a/pkg/provider/rancher/config_test.go b/pkg/provider/rancher/config_test.go index 3aa8b65d1..077eb42a2 100644 --- a/pkg/provider/rancher/config_test.go +++ b/pkg/provider/rancher/config_test.go @@ -580,7 +580,6 @@ func Test_buildConfiguration(t *testing.T) { Name: "Test", Labels: map[string]string{ "traefik.tcp.routers.foo.rule": "HostSNI(`foo.bar`)", - "traefik.tcp.routers.foo.tls": "true", "traefik.tcp.services.foo.loadbalancer.server.port": "8080", }, Port: "80/tcp", @@ -595,7 +594,6 @@ func Test_buildConfiguration(t *testing.T) { "foo": { Service: "foo", Rule: "HostSNI(`foo.bar`)", - TLS: &config.RouterTCPTLSConfig{}, }, }, Services: map[string]*config.TCPService{ diff --git a/pkg/server/router/router.go b/pkg/server/router/router.go index e963ec9f2..51f6aae14 100644 --- a/pkg/server/router/router.go +++ b/pkg/server/router/router.go @@ -2,7 +2,6 @@ package router import ( "context" - "fmt" "net/http" "github.com/containous/alice" @@ -23,33 +22,42 @@ const ( ) // NewManager Creates a new Manager -func NewManager(routers map[string]*config.RouterInfo, - serviceManager *service.Manager, middlewaresBuilder *middleware.Builder, modifierBuilder *responsemodifiers.Builder, +func NewManager(conf *config.RuntimeConfiguration, + serviceManager *service.Manager, + middlewaresBuilder *middleware.Builder, + modifierBuilder *responsemodifiers.Builder, ) *Manager { return &Manager{ routerHandlers: make(map[string]http.Handler), - configs: routers, serviceManager: serviceManager, middlewaresBuilder: middlewaresBuilder, modifierBuilder: modifierBuilder, + conf: conf, } } // Manager A route/router manager type Manager struct { routerHandlers map[string]http.Handler - configs map[string]*config.RouterInfo serviceManager *service.Manager middlewaresBuilder *middleware.Builder modifierBuilder *responsemodifiers.Builder + conf *config.RuntimeConfiguration +} + +func (m *Manager) getHTTPRouters(ctx context.Context, entryPoints []string, tls bool) map[string]map[string]*config.RouterInfo { + if m.conf != nil { + return m.conf.GetRoutersByEntrypoints(ctx, entryPoints, tls) + } + + return make(map[string]map[string]*config.RouterInfo) } // BuildHandlers Builds handler for all entry points func (m *Manager) BuildHandlers(rootCtx context.Context, entryPoints []string, tls bool) map[string]http.Handler { - entryPointsRouters := m.filteredRouters(rootCtx, entryPoints, tls) - entryPointHandlers := make(map[string]http.Handler) - for entryPointName, routers := range entryPointsRouters { + + for entryPointName, routers := range m.getHTTPRouters(rootCtx, entryPoints, tls) { entryPointName := entryPointName ctx := log.With(rootCtx, log.Str(log.EntryPointName, entryPointName)) @@ -75,45 +83,6 @@ func (m *Manager) BuildHandlers(rootCtx context.Context, entryPoints []string, t return entryPointHandlers } -func contains(entryPoints []string, entryPointName string) bool { - for _, name := range entryPoints { - if name == entryPointName { - return true - } - } - return false -} - -func (m *Manager) filteredRouters(ctx context.Context, entryPoints []string, tls bool) map[string]map[string]*config.RouterInfo { - entryPointsRouters := make(map[string]map[string]*config.RouterInfo) - - for rtName, rt := range m.configs { - if (tls && rt.TLS == nil) || (!tls && rt.TLS != nil) { - continue - } - - eps := rt.EntryPoints - if len(eps) == 0 { - eps = entryPoints - } - for _, entryPointName := range eps { - if !contains(entryPoints, entryPointName) { - log.FromContext(log.With(ctx, log.Str(log.EntryPointName, entryPointName))). - Errorf("entryPoint %q doesn't exist", entryPointName) - continue - } - - if _, ok := entryPointsRouters[entryPointName]; !ok { - entryPointsRouters[entryPointName] = make(map[string]*config.RouterInfo) - } - - entryPointsRouters[entryPointName][rtName] = rt - } - } - - return entryPointsRouters -} - func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string]*config.RouterInfo) (http.Handler, error) { router, err := rules.NewRouter() if err != nil { @@ -124,7 +93,7 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string ctxRouter := log.With(internal.AddProviderInContext(ctx, routerName), log.Str(log.RouterName, routerName)) logger := log.FromContext(ctxRouter) - handler, err := m.buildRouterHandler(ctxRouter, routerName) + handler, err := m.buildRouterHandler(ctxRouter, routerName, routerConfig) if err != nil { routerConfig.Err = err.Error() logger.Error(err) @@ -149,17 +118,12 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string return chain.Then(router) } -func (m *Manager) buildRouterHandler(ctx context.Context, routerName string) (http.Handler, error) { +func (m *Manager) buildRouterHandler(ctx context.Context, routerName string, routerConfig *config.RouterInfo) (http.Handler, error) { if handler, ok := m.routerHandlers[routerName]; ok { return handler, nil } - configRouter, ok := m.configs[routerName] - if !ok { - return nil, fmt.Errorf("no configuration for %s", routerName) - } - - handler, err := m.buildHTTPHandler(ctx, configRouter, routerName) + handler, err := m.buildHTTPHandler(ctx, routerConfig, routerName) if err != nil { return nil, err } diff --git a/pkg/server/router/router_test.go b/pkg/server/router/router_test.go index eb86f64d9..b9c456cdb 100644 --- a/pkg/server/router/router_test.go +++ b/pkg/server/router/router_test.go @@ -308,7 +308,7 @@ func TestRouterManager_Get(t *testing.T) { serviceManager := service.NewManager(rtConf.Services, http.DefaultTransport) middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager) responseModifierFactory := responsemodifiers.NewBuilder(rtConf.Middlewares) - routerManager := NewManager(rtConf.Routers, serviceManager, middlewaresBuilder, responseModifierFactory) + routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, responseModifierFactory) handlers := routerManager.BuildHandlers(context.Background(), test.entryPoints, false) @@ -409,7 +409,7 @@ func TestAccessLog(t *testing.T) { serviceManager := service.NewManager(rtConf.Services, http.DefaultTransport) middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager) responseModifierFactory := responsemodifiers.NewBuilder(rtConf.Middlewares) - routerManager := NewManager(rtConf.Routers, serviceManager, middlewaresBuilder, responseModifierFactory) + routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, responseModifierFactory) handlers := routerManager.BuildHandlers(context.Background(), test.entryPoints, false) @@ -695,7 +695,7 @@ func TestRuntimeConfiguration(t *testing.T) { serviceManager := service.NewManager(rtConf.Services, http.DefaultTransport) middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager) responseModifierFactory := responsemodifiers.NewBuilder(map[string]*config.MiddlewareInfo{}) - routerManager := NewManager(rtConf.Routers, serviceManager, middlewaresBuilder, responseModifierFactory) + routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, responseModifierFactory) _ = routerManager.BuildHandlers(context.Background(), entryPoints, false) @@ -769,7 +769,7 @@ func BenchmarkRouterServe(b *testing.B) { serviceManager := service.NewManager(rtConf.Services, &staticTransport{res}) middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager) responseModifierFactory := responsemodifiers.NewBuilder(rtConf.Middlewares) - routerManager := NewManager(rtConf.Routers, serviceManager, middlewaresBuilder, responseModifierFactory) + routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, responseModifierFactory) handlers := routerManager.BuildHandlers(context.Background(), entryPoints, false) diff --git a/pkg/server/router/tcp/router.go b/pkg/server/router/tcp/router.go index ba3e687f9..18299b793 100644 --- a/pkg/server/router/tcp/router.go +++ b/pkg/server/router/tcp/router.go @@ -2,7 +2,6 @@ package tcp import ( "context" - "crypto/tls" "fmt" "net/http" @@ -12,6 +11,7 @@ import ( "github.com/containous/traefik/pkg/server/internal" tcpservice "github.com/containous/traefik/pkg/server/service/tcp" "github.com/containous/traefik/pkg/tcp" + "github.com/containous/traefik/pkg/tls" ) // NewManager Creates a new Manager @@ -19,29 +19,46 @@ func NewManager(conf *config.RuntimeConfiguration, serviceManager *tcpservice.Manager, httpHandlers map[string]http.Handler, httpsHandlers map[string]http.Handler, - tlsConfig *tls.Config, + tlsManager *tls.Manager, ) *Manager { return &Manager{ - configs: conf.TCPRouters, serviceManager: serviceManager, httpHandlers: httpHandlers, httpsHandlers: httpsHandlers, - tlsConfig: tlsConfig, + tlsManager: tlsManager, + conf: conf, } } // Manager is a route/router manager type Manager struct { - configs map[string]*config.TCPRouterInfo serviceManager *tcpservice.Manager httpHandlers map[string]http.Handler httpsHandlers map[string]http.Handler - tlsConfig *tls.Config + tlsManager *tls.Manager + conf *config.RuntimeConfiguration +} + +func (m *Manager) getTCPRouters(ctx context.Context, entryPoints []string) map[string]map[string]*config.TCPRouterInfo { + if m.conf != nil { + return m.conf.GetTCPRoutersByEntrypoints(ctx, entryPoints) + } + + return make(map[string]map[string]*config.TCPRouterInfo) +} + +func (m *Manager) getHTTPRouters(ctx context.Context, entryPoints []string, tls bool) map[string]map[string]*config.RouterInfo { + if m.conf != nil { + return m.conf.GetRoutersByEntrypoints(ctx, entryPoints, tls) + } + + return make(map[string]map[string]*config.RouterInfo) } // BuildHandlers builds the handlers for the given entrypoints func (m *Manager) BuildHandlers(rootCtx context.Context, entryPoints []string) map[string]*tcp.Router { - entryPointsRouters := m.filteredRouters(rootCtx, entryPoints) + entryPointsRouters := m.getTCPRouters(rootCtx, entryPoints) + entryPointsRoutersHTTP := m.getHTTPRouters(rootCtx, entryPoints, true) entryPointHandlers := make(map[string]*tcp.Router) for _, entryPointName := range entryPoints { @@ -51,7 +68,7 @@ func (m *Manager) BuildHandlers(rootCtx context.Context, entryPoints []string) m ctx := log.With(rootCtx, log.Str(log.EntryPointName, entryPointName)) - handler, err := m.buildEntryPointHandler(ctx, routers, m.httpHandlers[entryPointName], m.httpsHandlers[entryPointName]) + handler, err := m.buildEntryPointHandler(ctx, routers, entryPointsRoutersHTTP[entryPointName], m.httpHandlers[entryPointName], m.httpsHandlers[entryPointName]) if err != nil { log.FromContext(ctx).Error(err) continue @@ -61,10 +78,50 @@ func (m *Manager) BuildHandlers(rootCtx context.Context, entryPoints []string) m return entryPointHandlers } -func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string]*config.TCPRouterInfo, handlerHTTP http.Handler, handlerHTTPS http.Handler) (*tcp.Router, error) { +func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string]*config.TCPRouterInfo, configsHTTP map[string]*config.RouterInfo, handlerHTTP http.Handler, handlerHTTPS http.Handler) (*tcp.Router, error) { router := &tcp.Router{} router.HTTPHandler(handlerHTTP) - router.HTTPSHandler(handlerHTTPS, m.tlsConfig) + + defaultTLSConf, err := m.tlsManager.Get("default", "default") + if err != nil { + return nil, err + } + + router.HTTPSHandler(handlerHTTPS, defaultTLSConf) + + for routerHTTPName, routerHTTPConfig := range configsHTTP { + if len(routerHTTPConfig.TLS.Options) == 0 || routerHTTPConfig.TLS.Options == "default" { + continue + } + + ctxRouter := log.With(internal.AddProviderInContext(ctx, routerHTTPName), log.Str(log.RouterName, routerHTTPName)) + logger := log.FromContext(ctxRouter) + + domains, err := rules.ParseDomains(routerHTTPConfig.Rule) + if err != nil { + routerErr := fmt.Errorf("invalid rule %s, error: %v", routerHTTPConfig.Rule, err) + routerHTTPConfig.Err = routerErr.Error() + logger.Debug(routerErr) + continue + } + + if len(domains) == 0 { + logger.Warnf("The 'default' TLS options will be applied instead of %q as no domain has been found in the rule", routerHTTPConfig.TLS.Options) + } + + for _, domain := range domains { + if routerHTTPConfig.TLS != nil { + tlsConf, err := m.tlsManager.Get("default", routerHTTPConfig.TLS.Options) + if err != nil { + routerHTTPConfig.Err = err.Error() + logger.Debug(err) + continue + } + + router.AddRouteHTTPTLS(domain, tlsConf) + } + } + } for routerName, routerConfig := range configs { ctxRouter := log.With(internal.AddProviderInContext(ctx, routerName), log.Str(log.RouterName, routerName)) @@ -92,7 +149,19 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string if routerConfig.TLS.Passthrough { router.AddRoute(domain, handler) } else { - router.AddRouteTLS(domain, handler, m.tlsConfig) + configName := "default" + if len(routerConfig.TLS.Options) > 0 { + configName = routerConfig.TLS.Options + } + + tlsConf, err := m.tlsManager.Get("default", configName) + if err != nil { + routerConfig.Err = err.Error() + logger.Debug(err) + continue + } + + router.AddRouteTLS(domain, handler, tlsConf) } case domain == "*": router.AddCatchAllNoTLS(handler) @@ -104,39 +173,3 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string return router, nil } - -func contains(entryPoints []string, entryPointName string) bool { - for _, name := range entryPoints { - if name == entryPointName { - return true - } - } - return false -} - -func (m *Manager) filteredRouters(ctx context.Context, entryPoints []string) map[string]map[string]*config.TCPRouterInfo { - entryPointsRouters := make(map[string]map[string]*config.TCPRouterInfo) - - for rtName, rt := range m.configs { - eps := rt.EntryPoints - if len(eps) == 0 { - eps = entryPoints - } - - for _, entryPointName := range eps { - if !contains(entryPoints, entryPointName) { - log.FromContext(log.With(ctx, log.Str(log.EntryPointName, entryPointName))). - Errorf("entryPoint %q doesn't exist", entryPointName) - continue - } - - if _, ok := entryPointsRouters[entryPointName]; !ok { - entryPointsRouters[entryPointName] = make(map[string]*config.TCPRouterInfo) - } - - entryPointsRouters[entryPointName][rtName] = rt - } - } - - return entryPointsRouters -} diff --git a/pkg/server/router/tcp/router_test.go b/pkg/server/router/tcp/router_test.go index a8b933a51..b8657b985 100644 --- a/pkg/server/router/tcp/router_test.go +++ b/pkg/server/router/tcp/router_test.go @@ -6,6 +6,7 @@ import ( "github.com/containous/traefik/pkg/config" "github.com/containous/traefik/pkg/server/service/tcp" + "github.com/containous/traefik/pkg/tls" "github.com/stretchr/testify/assert" ) @@ -42,6 +43,10 @@ func TestRuntimeConfiguration(t *testing.T) { EntryPoints: []string{"web"}, Service: "foo-service", Rule: "HostSNI(`bar.foo`)", + TLS: &config.RouterTCPTLSConfig{ + Passthrough: false, + Options: "foo", + }, }, }, "bar": { @@ -50,6 +55,10 @@ func TestRuntimeConfiguration(t *testing.T) { EntryPoints: []string{"web"}, Service: "foo-service", Rule: "HostSNI(`foo.bar`)", + TLS: &config.RouterTCPTLSConfig{ + Passthrough: false, + Options: "bar", + }, }, }, }, @@ -191,8 +200,21 @@ func TestRuntimeConfiguration(t *testing.T) { TCPRouters: test.routerConfig, } serviceManager := tcp.NewManager(conf) + tlsManager := tls.NewManager() + tlsManager.UpdateConfigs( + map[string]tls.Store{}, + map[string]tls.TLS{ + "foo": { + MinVersion: "VersionTLS12", + }, + "bar": { + MinVersion: "VersionTLS11", + }, + }, + []*tls.Configuration{}) + routerManager := NewManager(conf, serviceManager, - nil, nil, nil) + nil, nil, tlsManager) _ = routerManager.BuildHandlers(context.Background(), entryPoints) diff --git a/pkg/server/server_configuration.go b/pkg/server/server_configuration.go index a5f5bab5f..de9f2bb4c 100644 --- a/pkg/server/server_configuration.go +++ b/pkg/server/server_configuration.go @@ -2,7 +2,6 @@ package server import ( "context" - "crypto/tls" "encoding/json" "net/http" "reflect" @@ -71,20 +70,21 @@ func (s *Server) loadConfigurationTCP(configurations config.Configurations) map[ rtConf := config.NewRuntimeConfig(conf) handlersNonTLS, handlersTLS := s.createHTTPHandlers(ctx, rtConf, entryPoints) - routersTCP := s.createTCPRouters(ctx, rtConf, entryPoints, handlersNonTLS, handlersTLS, s.tlsManager.Get("default", "default")) + routersTCP := s.createTCPRouters(ctx, rtConf, entryPoints, handlersNonTLS, handlersTLS) rtConf.PopulateUsedBy() return routersTCP } // the given configuration must not be nil. its fields will get mutated. -func (s *Server) createTCPRouters(ctx context.Context, configuration *config.RuntimeConfiguration, entryPoints []string, handlers map[string]http.Handler, handlersTLS map[string]http.Handler, tlsConfig *tls.Config) map[string]*tcpCore.Router { +func (s *Server) createTCPRouters(ctx context.Context, configuration *config.RuntimeConfiguration, entryPoints []string, handlers map[string]http.Handler, handlersTLS map[string]http.Handler) map[string]*tcpCore.Router { if configuration == nil { return make(map[string]*tcpCore.Router) } serviceManager := tcp.NewManager(configuration) - routerManager := routertcp.NewManager(configuration, serviceManager, handlers, handlersTLS, tlsConfig) + + routerManager := routertcp.NewManager(configuration, serviceManager, handlers, handlersTLS, s.tlsManager) return routerManager.BuildHandlers(ctx, entryPoints) } @@ -94,7 +94,7 @@ func (s *Server) createHTTPHandlers(ctx context.Context, configuration *config.R serviceManager := service.NewManager(configuration.Services, s.defaultRoundTripper) middlewaresBuilder := middleware.NewBuilder(configuration.Middlewares, serviceManager) responseModifierFactory := responsemodifiers.NewBuilder(configuration.Middlewares) - routerManager := router.NewManager(configuration.Routers, serviceManager, middlewaresBuilder, responseModifierFactory) + routerManager := router.NewManager(configuration, serviceManager, middlewaresBuilder, responseModifierFactory) handlersNonTLS := routerManager.BuildHandlers(ctx, entryPoints, false) handlersTLS := routerManager.BuildHandlers(ctx, entryPoints, true) diff --git a/pkg/tcp/router.go b/pkg/tcp/router.go index 7e5552bf7..aeb98171e 100644 --- a/pkg/tcp/router.go +++ b/pkg/tcp/router.go @@ -14,13 +14,14 @@ import ( // Router is a TCP router type Router struct { - routingTable map[string]Handler - httpForwarder Handler - httpsForwarder Handler - httpHandler http.Handler - httpsHandler http.Handler - httpsTLSConfig *tls.Config - catchAllNoTLS Handler + routingTable map[string]Handler + httpForwarder Handler + httpsForwarder Handler + httpHandler http.Handler + httpsHandler http.Handler + httpsTLSConfig *tls.Config // default TLS config + catchAllNoTLS Handler + hostHTTPTLSConfig map[string]*tls.Config // TLS configs keyed by SNI } // ServeTCP forwards the connection to the right TCP/HTTP handler @@ -84,6 +85,15 @@ func (r *Router) AddRouteTLS(sniHost string, target Handler, config *tls.Config) }) } +// AddRouteHTTPTLS defines a handler for a given sniHost and sets the matching tlsConfig +func (r *Router) AddRouteHTTPTLS(sniHost string, config *tls.Config) { + if r.hostHTTPTLSConfig == nil { + r.hostHTTPTLSConfig = map[string]*tls.Config{} + } + log.Debugf("adding route %s with minversion %d", sniHost, config.MinVersion) + r.hostHTTPTLSConfig[sniHost] = config +} + // AddCatchAllNoTLS defines the fallback tcp handler func (r *Router) AddCatchAllNoTLS(handler Handler) { r.catchAllNoTLS = handler @@ -116,6 +126,10 @@ func (r *Router) HTTPForwarder(handler Handler) { // HTTPSForwarder sets the tcp handler that will forward the TLS connections to an http handler func (r *Router) HTTPSForwarder(handler Handler) { + for sniHost, tlsConf := range r.hostHTTPTLSConfig { + r.AddRouteTLS(sniHost, handler, tlsConf) + } + r.httpsForwarder = &TLSHandler{ Next: handler, Config: r.httpsTLSConfig, diff --git a/pkg/tls/tlsmanager.go b/pkg/tls/tlsmanager.go index d1b33a307..98dfbfc03 100644 --- a/pkg/tls/tlsmanager.go +++ b/pkg/tls/tlsmanager.go @@ -67,14 +67,19 @@ func (m *Manager) UpdateConfigs(stores map[string]Store, configs map[string]TLS, } } -// Get gets the tls configuration to use for a given store / configuration -func (m *Manager) Get(storeName string, configName string) *tls.Config { +// Get gets the TLS configuration to use for a given store / configuration +func (m *Manager) Get(storeName string, configName string) (*tls.Config, error) { m.lock.RLock() defer m.lock.RUnlock() + config, ok := m.configs[configName] + if !ok && configName != "default" { + return nil, fmt.Errorf("unknown TLS options: %s", configName) + } + store := m.getStore(storeName) - tlsConfig, err := buildTLSConfig(m.configs[configName]) + tlsConfig, err := buildTLSConfig(config) if err != nil { log.Error(err) tlsConfig = &tls.Config{} @@ -106,7 +111,7 @@ func (m *Manager) Get(storeName string, configName string) *tls.Config { log.WithoutContext().Debugf("Serving default certificate for request: %q", domainToCheck) return store.DefaultCertificate, nil } - return tlsConfig + return tlsConfig, nil } func (m *Manager) getStore(storeName string) *CertificateStore {