From 115d42e0f06fcb37211eba2ce82997307a6dfea7 Mon Sep 17 00:00:00 2001 From: mpl Date: Tue, 11 Feb 2020 01:26:04 +0100 Subject: [PATCH] UDP support Co-authored-by: Julien Salleyron --- cmd/healthcheck/healthcheck.go | 2 +- cmd/traefik/traefik.go | 16 +- .../dynamic-configuration/docker-labels.yml | 8 +- .../reference/dynamic-configuration/file.toml | 28 + .../reference/dynamic-configuration/file.yaml | 25 + .../reference/dynamic-configuration/kv-ref.md | 236 ++++---- .../marathon-labels.json | 8 +- docs/content/routing/entrypoints.md | 70 ++- docs/content/routing/routers/index.md | 163 +++++- docs/content/routing/services/index.md | 136 +++++ integration/fixtures/router_errors.toml | 27 + integration/fixtures/service_errors.toml | 34 ++ integration/fixtures/udp/wrr.toml | 53 ++ integration/integration_test.go | 1 + integration/resources/compose/udp.yml | 14 + integration/simple_test.go | 77 +++ integration/udp_test.go | 107 ++++ pkg/api/handler.go | 9 + pkg/api/handler_udp.go | 164 ++++++ pkg/api/handler_udp_test.go | 537 ++++++++++++++++++ pkg/api/testdata/udprouter-bar.json | 12 + pkg/api/testdata/udprouters-empty.json | 1 + .../testdata/udprouters-filtered-search.json | 14 + .../testdata/udprouters-filtered-status.json | 14 + pkg/api/testdata/udprouters-page2.json | 14 + pkg/api/testdata/udprouters.json | 38 ++ pkg/api/testdata/udpservice-bar.json | 17 + pkg/api/testdata/udpservices-empty.json | 1 + .../testdata/udpservices-filtered-search.json | 18 + .../testdata/udpservices-filtered-status.json | 19 + pkg/api/testdata/udpservices-page2.json | 18 + pkg/api/testdata/udpservices.json | 51 ++ pkg/config/dynamic/config.go | 1 + pkg/config/dynamic/udp_config.go | 82 +++ pkg/config/runtime/runtime.go | 48 +- pkg/config/runtime/runtime_udp.go | 114 ++++ pkg/config/runtime/runtime_udp_test.go | 201 +++++++ pkg/config/static/entrypoints.go | 36 +- pkg/config/static/entrypoints_test.go | 67 +++ pkg/provider/configuration.go | 4 + pkg/provider/consulcatalog/config_test.go | 164 ++++++ pkg/provider/docker/config_test.go | 180 ++++++ pkg/provider/file/file.go | 24 + pkg/provider/file/file_test.go | 23 +- .../file/fixtures/toml/simple_file_02.toml | 65 ++- .../file/fixtures/yaml/simple_file_02.yml | 53 -- pkg/provider/marathon/config_test.go | 184 +++++- pkg/provider/rancher/config_test.go | 68 +++ pkg/server/aggregator.go | 13 + pkg/server/configurationwatcher_test.go | 8 + pkg/server/provider/provider_test.go | 2 +- pkg/server/router/udp/router.go | 87 +++ pkg/server/router/udp/router_test.go | 144 +++++ pkg/server/routerfactory.go | 91 +++ ...rfactory_test.go => routerfactory_test.go} | 12 +- pkg/server/server.go | 6 +- pkg/server/server_entrypoint_tcp.go | 18 +- pkg/server/server_entrypoint_tcp_test.go | 2 +- pkg/server/server_entrypoint_udp.go | 135 +++++ pkg/server/server_entrypoint_udp_test.go | 123 ++++ pkg/server/service/tcp/service.go | 2 +- pkg/server/service/tcp/service_test.go | 2 +- pkg/server/service/udp/service.go | 81 +++ pkg/server/service/udp/service_test.go | 201 +++++++ pkg/server/tcprouterfactory.go | 70 --- pkg/udp/conn.go | 265 +++++++++ pkg/udp/conn_test.go | 270 +++++++++ pkg/udp/handler.go | 14 + pkg/udp/proxy.go | 56 ++ pkg/udp/proxy_test.go | 55 ++ pkg/udp/switcher.go | 26 + pkg/udp/wrr_load_balancer.go | 122 ++++ 72 files changed, 4730 insertions(+), 321 deletions(-) create mode 100644 integration/fixtures/udp/wrr.toml create mode 100644 integration/resources/compose/udp.yml create mode 100644 integration/udp_test.go create mode 100644 pkg/api/handler_udp.go create mode 100644 pkg/api/handler_udp_test.go create mode 100644 pkg/api/testdata/udprouter-bar.json create mode 100644 pkg/api/testdata/udprouters-empty.json create mode 100644 pkg/api/testdata/udprouters-filtered-search.json create mode 100644 pkg/api/testdata/udprouters-filtered-status.json create mode 100644 pkg/api/testdata/udprouters-page2.json create mode 100644 pkg/api/testdata/udprouters.json create mode 100644 pkg/api/testdata/udpservice-bar.json create mode 100644 pkg/api/testdata/udpservices-empty.json create mode 100644 pkg/api/testdata/udpservices-filtered-search.json create mode 100644 pkg/api/testdata/udpservices-filtered-status.json create mode 100644 pkg/api/testdata/udpservices-page2.json create mode 100644 pkg/api/testdata/udpservices.json create mode 100644 pkg/config/dynamic/udp_config.go create mode 100644 pkg/config/runtime/runtime_udp.go create mode 100644 pkg/config/runtime/runtime_udp_test.go create mode 100644 pkg/config/static/entrypoints_test.go delete mode 100644 pkg/provider/file/fixtures/yaml/simple_file_02.yml create mode 100644 pkg/server/router/udp/router.go create mode 100644 pkg/server/router/udp/router_test.go create mode 100644 pkg/server/routerfactory.go rename pkg/server/{tcprouterfactory_test.go => routerfactory_test.go} (91%) create mode 100644 pkg/server/server_entrypoint_udp.go create mode 100644 pkg/server/server_entrypoint_udp_test.go create mode 100644 pkg/server/service/udp/service.go create mode 100644 pkg/server/service/udp/service_test.go delete mode 100644 pkg/server/tcprouterfactory.go create mode 100644 pkg/udp/conn.go create mode 100644 pkg/udp/conn_test.go create mode 100644 pkg/udp/handler.go create mode 100644 pkg/udp/proxy.go create mode 100644 pkg/udp/proxy_test.go create mode 100644 pkg/udp/switcher.go create mode 100644 pkg/udp/wrr_load_balancer.go diff --git a/cmd/healthcheck/healthcheck.go b/cmd/healthcheck/healthcheck.go index 0a922b2a0..d67341039 100644 --- a/cmd/healthcheck/healthcheck.go +++ b/cmd/healthcheck/healthcheck.go @@ -75,5 +75,5 @@ func Do(staticConfiguration static.Configuration) (*http.Response, error) { path := "/" - return client.Head(protocol + "://" + pingEntryPoint.Address + path + "ping") + return client.Head(protocol + "://" + pingEntryPoint.GetAddress() + path + "ping") } diff --git a/cmd/traefik/traefik.go b/cmd/traefik/traefik.go index bc7a3ae7c..8302c4a06 100644 --- a/cmd/traefik/traefik.go +++ b/cmd/traefik/traefik.go @@ -177,6 +177,11 @@ func setupServer(staticConfiguration *static.Configuration) (*server.Server, err return nil, err } + serverEntryPointsUDP, err := server.NewUDPEntryPoints(staticConfiguration.EntryPoints) + if err != nil { + return nil, err + } + ctx := context.Background() routinesPool := safe.NewPool(ctx) @@ -184,7 +189,7 @@ func setupServer(staticConfiguration *static.Configuration) (*server.Server, err accessLog := setupAccessLog(staticConfiguration.AccessLog) chainBuilder := middleware.NewChainBuilder(*staticConfiguration, metricsRegistry, accessLog) managerFactory := service.NewManagerFactory(*staticConfiguration, routinesPool, metricsRegistry) - tcpRouterFactory := server.NewTCPRouterFactory(*staticConfiguration, managerFactory, tlsManager, chainBuilder) + routerFactory := server.NewRouterFactory(*staticConfiguration, managerFactory, tlsManager, chainBuilder) watcher := server.NewConfigurationWatcher(routinesPool, providerAggregator, time.Duration(staticConfiguration.Providers.ProvidersThrottleDuration)) @@ -198,7 +203,7 @@ func setupServer(staticConfiguration *static.Configuration) (*server.Server, err metricsRegistry.LastConfigReloadSuccessGauge().Set(float64(time.Now().Unix())) }) - watcher.AddListener(switchRouter(tcpRouterFactory, acmeProviders, serverEntryPointsTCP)) + watcher.AddListener(switchRouter(routerFactory, acmeProviders, serverEntryPointsTCP, serverEntryPointsUDP)) watcher.AddListener(func(conf dynamic.Configuration) { if metricsRegistry.IsEpEnabled() || metricsRegistry.IsSvcEnabled() { @@ -229,12 +234,12 @@ func setupServer(staticConfiguration *static.Configuration) (*server.Server, err } }) - return server.NewServer(routinesPool, serverEntryPointsTCP, watcher, chainBuilder, accessLog), nil + return server.NewServer(routinesPool, serverEntryPointsTCP, serverEntryPointsUDP, watcher, chainBuilder, accessLog), nil } -func switchRouter(tcpRouterFactory *server.TCPRouterFactory, acmeProviders []*acme.Provider, serverEntryPointsTCP server.TCPEntryPoints) func(conf dynamic.Configuration) { +func switchRouter(routerFactory *server.RouterFactory, acmeProviders []*acme.Provider, serverEntryPointsTCP server.TCPEntryPoints, serverEntryPointsUDP server.UDPEntryPoints) func(conf dynamic.Configuration) { return func(conf dynamic.Configuration) { - routers := tcpRouterFactory.CreateTCPRouters(conf) + routers, udpRouters := routerFactory.CreateRouters(conf) for entryPointName, rt := range routers { for _, p := range acmeProviders { if p != nil && p.HTTPChallenge != nil && p.HTTPChallenge.EntryPoint == entryPointName { @@ -244,6 +249,7 @@ func switchRouter(tcpRouterFactory *server.TCPRouterFactory, acmeProviders []*ac } } serverEntryPointsTCP.Switch(routers) + serverEntryPointsUDP.Switch(udpRouters) } } diff --git a/docs/content/reference/dynamic-configuration/docker-labels.yml b/docs/content/reference/dynamic-configuration/docker-labels.yml index e52e8fce2..dc3fd18f0 100644 --- a/docs/content/reference/dynamic-configuration/docker-labels.yml +++ b/docs/content/reference/dynamic-configuration/docker-labels.yml @@ -81,6 +81,7 @@ - "traefik.http.middlewares.middleware13.passtlsclientcert.info.notafter=true" - "traefik.http.middlewares.middleware13.passtlsclientcert.info.notbefore=true" - "traefik.http.middlewares.middleware13.passtlsclientcert.info.sans=true" +- "traefik.http.middlewares.middleware13.passtlsclientcert.info.serialnumber=true" - "traefik.http.middlewares.middleware13.passtlsclientcert.info.subject.commonname=true" - "traefik.http.middlewares.middleware13.passtlsclientcert.info.subject.country=true" - "traefik.http.middlewares.middleware13.passtlsclientcert.info.subject.domaincomponent=true" @@ -90,8 +91,8 @@ - "traefik.http.middlewares.middleware13.passtlsclientcert.info.subject.serialnumber=true" - "traefik.http.middlewares.middleware13.passtlsclientcert.pem=true" - "traefik.http.middlewares.middleware14.ratelimit.average=42" -- "traefik.http.middlewares.middleware14.ratelimit.period=42" - "traefik.http.middlewares.middleware14.ratelimit.burst=42" +- "traefik.http.middlewares.middleware14.ratelimit.period=42" - "traefik.http.middlewares.middleware14.ratelimit.sourcecriterion.ipstrategy.depth=42" - "traefik.http.middlewares.middleware14.ratelimit.sourcecriterion.ipstrategy.excludedips=foobar, foobar" - "traefik.http.middlewares.middleware14.ratelimit.sourcecriterion.requestheadername=foobar" @@ -173,3 +174,8 @@ - "traefik.tcp.routers.tcprouter1.tls.passthrough=true" - "traefik.tcp.services.tcpservice01.loadbalancer.terminationdelay=42" - "traefik.tcp.services.tcpservice01.loadbalancer.server.port=foobar" +- "traefik.udp.routers.udprouter0.entrypoints=foobar, foobar" +- "traefik.udp.routers.udprouter0.service=foobar" +- "traefik.udp.routers.udprouter1.entrypoints=foobar, foobar" +- "traefik.udp.routers.udprouter1.service=foobar" +- "traefik.udp.services.udpservice01.loadbalancer.server.port=foobar" diff --git a/docs/content/reference/dynamic-configuration/file.toml b/docs/content/reference/dynamic-configuration/file.toml index ba51a2e4a..9ba59ad5a 100644 --- a/docs/content/reference/dynamic-configuration/file.toml +++ b/docs/content/reference/dynamic-configuration/file.toml @@ -311,6 +311,34 @@ name = "foobar" weight = 42 +[udp] + [udp.routers] + [udp.routers.UDPRouter0] + entryPoints = ["foobar", "foobar"] + service = "foobar" + [udp.routers.UDPRouter1] + entryPoints = ["foobar", "foobar"] + service = "foobar" + [udp.services] + [udp.services.UDPService01] + [udp.services.UDPService01.loadBalancer] + + [[udp.services.UDPService01.loadBalancer.servers]] + address = "foobar" + + [[udp.services.UDPService01.loadBalancer.servers]] + address = "foobar" + [udp.services.UDPService02] + [udp.services.UDPService02.weighted] + + [[udp.services.UDPService02.weighted.services]] + name = "foobar" + weight = 42 + + [[udp.services.UDPService02.weighted.services]] + name = "foobar" + weight = 42 + [tls] [[tls.certificates]] diff --git a/docs/content/reference/dynamic-configuration/file.yaml b/docs/content/reference/dynamic-configuration/file.yaml index c991dea66..50ca3898e 100644 --- a/docs/content/reference/dynamic-configuration/file.yaml +++ b/docs/content/reference/dynamic-configuration/file.yaml @@ -344,6 +344,31 @@ tcp: weight: 42 - name: foobar weight: 42 +udp: + routers: + UDPRouter0: + entryPoints: + - foobar + - foobar + service: foobar + UDPRouter1: + entryPoints: + - foobar + - foobar + service: foobar + services: + UDPService01: + loadBalancer: + servers: + - address: foobar + - address: foobar + UDPService02: + weighted: + services: + - name: foobar + weight: 42 + - name: foobar + weight: 42 tls: certificates: - certFile: foobar diff --git a/docs/content/reference/dynamic-configuration/kv-ref.md b/docs/content/reference/dynamic-configuration/kv-ref.md index 4a23fef87..c661f2e4f 100644 --- a/docs/content/reference/dynamic-configuration/kv-ref.md +++ b/docs/content/reference/dynamic-configuration/kv-ref.md @@ -13,117 +13,119 @@ | `traefik/http/middlewares/Middleware03/chain/middlewares/0` | `foobar` | | `traefik/http/middlewares/Middleware03/chain/middlewares/1` | `foobar` | | `traefik/http/middlewares/Middleware04/circuitBreaker/expression` | `foobar` | -| `traefik/http/middlewares/Middleware05/compress` | `` | -| `traefik/http/middlewares/Middleware06/digestAuth/headerField` | `foobar` | -| `traefik/http/middlewares/Middleware06/digestAuth/realm` | `foobar` | -| `traefik/http/middlewares/Middleware06/digestAuth/removeHeader` | `true` | -| `traefik/http/middlewares/Middleware06/digestAuth/users/0` | `foobar` | -| `traefik/http/middlewares/Middleware06/digestAuth/users/1` | `foobar` | -| `traefik/http/middlewares/Middleware06/digestAuth/usersFile` | `foobar` | -| `traefik/http/middlewares/Middleware07/errors/query` | `foobar` | -| `traefik/http/middlewares/Middleware07/errors/service` | `foobar` | -| `traefik/http/middlewares/Middleware07/errors/status/0` | `foobar` | -| `traefik/http/middlewares/Middleware07/errors/status/1` | `foobar` | -| `traefik/http/middlewares/Middleware08/forwardAuth/address` | `foobar` | -| `traefik/http/middlewares/Middleware08/forwardAuth/authResponseHeaders/0` | `foobar` | -| `traefik/http/middlewares/Middleware08/forwardAuth/authResponseHeaders/1` | `foobar` | -| `traefik/http/middlewares/Middleware08/forwardAuth/tls/ca` | `foobar` | -| `traefik/http/middlewares/Middleware08/forwardAuth/tls/caOptional` | `true` | -| `traefik/http/middlewares/Middleware08/forwardAuth/tls/cert` | `foobar` | -| `traefik/http/middlewares/Middleware08/forwardAuth/tls/insecureSkipVerify` | `true` | -| `traefik/http/middlewares/Middleware08/forwardAuth/tls/key` | `foobar` | -| `traefik/http/middlewares/Middleware08/forwardAuth/trustForwardHeader` | `true` | -| `traefik/http/middlewares/Middleware09/headers/accessControlAllowCredentials` | `true` | -| `traefik/http/middlewares/Middleware09/headers/accessControlAllowHeaders/0` | `foobar` | -| `traefik/http/middlewares/Middleware09/headers/accessControlAllowHeaders/1` | `foobar` | -| `traefik/http/middlewares/Middleware09/headers/accessControlAllowMethods/0` | `foobar` | -| `traefik/http/middlewares/Middleware09/headers/accessControlAllowMethods/1` | `foobar` | -| `traefik/http/middlewares/Middleware09/headers/accessControlAllowOrigin` | `foobar` | -| `traefik/http/middlewares/Middleware09/headers/accessControlExposeHeaders/0` | `foobar` | -| `traefik/http/middlewares/Middleware09/headers/accessControlExposeHeaders/1` | `foobar` | -| `traefik/http/middlewares/Middleware09/headers/accessControlMaxAge` | `42` | -| `traefik/http/middlewares/Middleware09/headers/addVaryHeader` | `true` | -| `traefik/http/middlewares/Middleware09/headers/allowedHosts/0` | `foobar` | -| `traefik/http/middlewares/Middleware09/headers/allowedHosts/1` | `foobar` | -| `traefik/http/middlewares/Middleware09/headers/browserXssFilter` | `true` | -| `traefik/http/middlewares/Middleware09/headers/contentSecurityPolicy` | `foobar` | -| `traefik/http/middlewares/Middleware09/headers/contentTypeNosniff` | `true` | -| `traefik/http/middlewares/Middleware09/headers/customBrowserXSSValue` | `foobar` | -| `traefik/http/middlewares/Middleware09/headers/customFrameOptionsValue` | `foobar` | -| `traefik/http/middlewares/Middleware09/headers/customRequestHeaders/name0` | `foobar` | -| `traefik/http/middlewares/Middleware09/headers/customRequestHeaders/name1` | `foobar` | -| `traefik/http/middlewares/Middleware09/headers/customResponseHeaders/name0` | `foobar` | -| `traefik/http/middlewares/Middleware09/headers/customResponseHeaders/name1` | `foobar` | -| `traefik/http/middlewares/Middleware09/headers/featurePolicy` | `foobar` | -| `traefik/http/middlewares/Middleware09/headers/forceSTSHeader` | `true` | -| `traefik/http/middlewares/Middleware09/headers/frameDeny` | `true` | -| `traefik/http/middlewares/Middleware09/headers/hostsProxyHeaders/0` | `foobar` | -| `traefik/http/middlewares/Middleware09/headers/hostsProxyHeaders/1` | `foobar` | -| `traefik/http/middlewares/Middleware09/headers/isDevelopment` | `true` | -| `traefik/http/middlewares/Middleware09/headers/publicKey` | `foobar` | -| `traefik/http/middlewares/Middleware09/headers/referrerPolicy` | `foobar` | -| `traefik/http/middlewares/Middleware09/headers/sslForceHost` | `true` | -| `traefik/http/middlewares/Middleware09/headers/sslHost` | `foobar` | -| `traefik/http/middlewares/Middleware09/headers/sslProxyHeaders/name0` | `foobar` | -| `traefik/http/middlewares/Middleware09/headers/sslProxyHeaders/name1` | `foobar` | -| `traefik/http/middlewares/Middleware09/headers/sslRedirect` | `true` | -| `traefik/http/middlewares/Middleware09/headers/sslTemporaryRedirect` | `true` | -| `traefik/http/middlewares/Middleware09/headers/stsIncludeSubdomains` | `true` | -| `traefik/http/middlewares/Middleware09/headers/stsPreload` | `true` | -| `traefik/http/middlewares/Middleware09/headers/stsSeconds` | `42` | -| `traefik/http/middlewares/Middleware10/ipWhiteList/ipStrategy/depth` | `42` | -| `traefik/http/middlewares/Middleware10/ipWhiteList/ipStrategy/excludedIPs/0` | `foobar` | -| `traefik/http/middlewares/Middleware10/ipWhiteList/ipStrategy/excludedIPs/1` | `foobar` | -| `traefik/http/middlewares/Middleware10/ipWhiteList/sourceRange/0` | `foobar` | -| `traefik/http/middlewares/Middleware10/ipWhiteList/sourceRange/1` | `foobar` | -| `traefik/http/middlewares/Middleware11/inFlightReq/amount` | `42` | -| `traefik/http/middlewares/Middleware11/inFlightReq/sourceCriterion/ipStrategy/depth` | `42` | -| `traefik/http/middlewares/Middleware11/inFlightReq/sourceCriterion/ipStrategy/excludedIPs/0` | `foobar` | -| `traefik/http/middlewares/Middleware11/inFlightReq/sourceCriterion/ipStrategy/excludedIPs/1` | `foobar` | -| `traefik/http/middlewares/Middleware11/inFlightReq/sourceCriterion/requestHeaderName` | `foobar` | -| `traefik/http/middlewares/Middleware11/inFlightReq/sourceCriterion/requestHost` | `true` | -| `traefik/http/middlewares/Middleware12/passTLSClientCert/info/issuer/commonName` | `true` | -| `traefik/http/middlewares/Middleware12/passTLSClientCert/info/issuer/country` | `true` | -| `traefik/http/middlewares/Middleware12/passTLSClientCert/info/issuer/domainComponent` | `true` | -| `traefik/http/middlewares/Middleware12/passTLSClientCert/info/issuer/locality` | `true` | -| `traefik/http/middlewares/Middleware12/passTLSClientCert/info/issuer/organization` | `true` | -| `traefik/http/middlewares/Middleware12/passTLSClientCert/info/issuer/province` | `true` | -| `traefik/http/middlewares/Middleware12/passTLSClientCert/info/issuer/serialNumber` | `true` | -| `traefik/http/middlewares/Middleware12/passTLSClientCert/info/notAfter` | `true` | -| `traefik/http/middlewares/Middleware12/passTLSClientCert/info/notBefore` | `true` | -| `traefik/http/middlewares/Middleware12/passTLSClientCert/info/sans` | `true` | -| `traefik/http/middlewares/Middleware12/passTLSClientCert/info/serialNumber` | `true` | -| `traefik/http/middlewares/Middleware12/passTLSClientCert/info/subject/commonName` | `true` | -| `traefik/http/middlewares/Middleware12/passTLSClientCert/info/subject/country` | `true` | -| `traefik/http/middlewares/Middleware12/passTLSClientCert/info/subject/domainComponent` | `true` | -| `traefik/http/middlewares/Middleware12/passTLSClientCert/info/subject/locality` | `true` | -| `traefik/http/middlewares/Middleware12/passTLSClientCert/info/subject/organization` | `true` | -| `traefik/http/middlewares/Middleware12/passTLSClientCert/info/subject/province` | `true` | -| `traefik/http/middlewares/Middleware12/passTLSClientCert/info/subject/serialNumber` | `true` | -| `traefik/http/middlewares/Middleware12/passTLSClientCert/pem` | `true` | -| `traefik/http/middlewares/Middleware13/rateLimit/average` | `42` | -| `traefik/http/middlewares/Middleware13/rateLimit/period` | `42` | -| `traefik/http/middlewares/Middleware13/rateLimit/burst` | `42` | -| `traefik/http/middlewares/Middleware13/rateLimit/sourceCriterion/ipStrategy/depth` | `42` | -| `traefik/http/middlewares/Middleware13/rateLimit/sourceCriterion/ipStrategy/excludedIPs/0` | `foobar` | -| `traefik/http/middlewares/Middleware13/rateLimit/sourceCriterion/ipStrategy/excludedIPs/1` | `foobar` | -| `traefik/http/middlewares/Middleware13/rateLimit/sourceCriterion/requestHeaderName` | `foobar` | -| `traefik/http/middlewares/Middleware13/rateLimit/sourceCriterion/requestHost` | `true` | -| `traefik/http/middlewares/Middleware14/redirectRegex/permanent` | `true` | -| `traefik/http/middlewares/Middleware14/redirectRegex/regex` | `foobar` | -| `traefik/http/middlewares/Middleware14/redirectRegex/replacement` | `foobar` | -| `traefik/http/middlewares/Middleware15/redirectScheme/permanent` | `true` | -| `traefik/http/middlewares/Middleware15/redirectScheme/port` | `foobar` | -| `traefik/http/middlewares/Middleware15/redirectScheme/scheme` | `foobar` | -| `traefik/http/middlewares/Middleware16/replacePath/path` | `foobar` | -| `traefik/http/middlewares/Middleware17/replacePathRegex/regex` | `foobar` | -| `traefik/http/middlewares/Middleware17/replacePathRegex/replacement` | `foobar` | -| `traefik/http/middlewares/Middleware18/retry/attempts` | `42` | -| `traefik/http/middlewares/Middleware19/stripPrefix/forceSlash` | `true` | -| `traefik/http/middlewares/Middleware19/stripPrefix/prefixes/0` | `foobar` | -| `traefik/http/middlewares/Middleware19/stripPrefix/prefixes/1` | `foobar` | -| `traefik/http/middlewares/Middleware20/stripPrefixRegex/regex/0` | `foobar` | -| `traefik/http/middlewares/Middleware20/stripPrefixRegex/regex/1` | `foobar` | +| `traefik/http/middlewares/Middleware05/compress/excludedContentTypes/0` | `foobar` | +| `traefik/http/middlewares/Middleware05/compress/excludedContentTypes/1` | `foobar` | +| `traefik/http/middlewares/Middleware06/contentType/autoDetect` | `true` | +| `traefik/http/middlewares/Middleware07/digestAuth/headerField` | `foobar` | +| `traefik/http/middlewares/Middleware07/digestAuth/realm` | `foobar` | +| `traefik/http/middlewares/Middleware07/digestAuth/removeHeader` | `true` | +| `traefik/http/middlewares/Middleware07/digestAuth/users/0` | `foobar` | +| `traefik/http/middlewares/Middleware07/digestAuth/users/1` | `foobar` | +| `traefik/http/middlewares/Middleware07/digestAuth/usersFile` | `foobar` | +| `traefik/http/middlewares/Middleware08/errors/query` | `foobar` | +| `traefik/http/middlewares/Middleware08/errors/service` | `foobar` | +| `traefik/http/middlewares/Middleware08/errors/status/0` | `foobar` | +| `traefik/http/middlewares/Middleware08/errors/status/1` | `foobar` | +| `traefik/http/middlewares/Middleware09/forwardAuth/address` | `foobar` | +| `traefik/http/middlewares/Middleware09/forwardAuth/authResponseHeaders/0` | `foobar` | +| `traefik/http/middlewares/Middleware09/forwardAuth/authResponseHeaders/1` | `foobar` | +| `traefik/http/middlewares/Middleware09/forwardAuth/tls/ca` | `foobar` | +| `traefik/http/middlewares/Middleware09/forwardAuth/tls/caOptional` | `true` | +| `traefik/http/middlewares/Middleware09/forwardAuth/tls/cert` | `foobar` | +| `traefik/http/middlewares/Middleware09/forwardAuth/tls/insecureSkipVerify` | `true` | +| `traefik/http/middlewares/Middleware09/forwardAuth/tls/key` | `foobar` | +| `traefik/http/middlewares/Middleware09/forwardAuth/trustForwardHeader` | `true` | +| `traefik/http/middlewares/Middleware10/headers/accessControlAllowCredentials` | `true` | +| `traefik/http/middlewares/Middleware10/headers/accessControlAllowHeaders/0` | `foobar` | +| `traefik/http/middlewares/Middleware10/headers/accessControlAllowHeaders/1` | `foobar` | +| `traefik/http/middlewares/Middleware10/headers/accessControlAllowMethods/0` | `foobar` | +| `traefik/http/middlewares/Middleware10/headers/accessControlAllowMethods/1` | `foobar` | +| `traefik/http/middlewares/Middleware10/headers/accessControlAllowOrigin` | `foobar` | +| `traefik/http/middlewares/Middleware10/headers/accessControlExposeHeaders/0` | `foobar` | +| `traefik/http/middlewares/Middleware10/headers/accessControlExposeHeaders/1` | `foobar` | +| `traefik/http/middlewares/Middleware10/headers/accessControlMaxAge` | `42` | +| `traefik/http/middlewares/Middleware10/headers/addVaryHeader` | `true` | +| `traefik/http/middlewares/Middleware10/headers/allowedHosts/0` | `foobar` | +| `traefik/http/middlewares/Middleware10/headers/allowedHosts/1` | `foobar` | +| `traefik/http/middlewares/Middleware10/headers/browserXssFilter` | `true` | +| `traefik/http/middlewares/Middleware10/headers/contentSecurityPolicy` | `foobar` | +| `traefik/http/middlewares/Middleware10/headers/contentTypeNosniff` | `true` | +| `traefik/http/middlewares/Middleware10/headers/customBrowserXSSValue` | `foobar` | +| `traefik/http/middlewares/Middleware10/headers/customFrameOptionsValue` | `foobar` | +| `traefik/http/middlewares/Middleware10/headers/customRequestHeaders/name0` | `foobar` | +| `traefik/http/middlewares/Middleware10/headers/customRequestHeaders/name1` | `foobar` | +| `traefik/http/middlewares/Middleware10/headers/customResponseHeaders/name0` | `foobar` | +| `traefik/http/middlewares/Middleware10/headers/customResponseHeaders/name1` | `foobar` | +| `traefik/http/middlewares/Middleware10/headers/featurePolicy` | `foobar` | +| `traefik/http/middlewares/Middleware10/headers/forceSTSHeader` | `true` | +| `traefik/http/middlewares/Middleware10/headers/frameDeny` | `true` | +| `traefik/http/middlewares/Middleware10/headers/hostsProxyHeaders/0` | `foobar` | +| `traefik/http/middlewares/Middleware10/headers/hostsProxyHeaders/1` | `foobar` | +| `traefik/http/middlewares/Middleware10/headers/isDevelopment` | `true` | +| `traefik/http/middlewares/Middleware10/headers/publicKey` | `foobar` | +| `traefik/http/middlewares/Middleware10/headers/referrerPolicy` | `foobar` | +| `traefik/http/middlewares/Middleware10/headers/sslForceHost` | `true` | +| `traefik/http/middlewares/Middleware10/headers/sslHost` | `foobar` | +| `traefik/http/middlewares/Middleware10/headers/sslProxyHeaders/name0` | `foobar` | +| `traefik/http/middlewares/Middleware10/headers/sslProxyHeaders/name1` | `foobar` | +| `traefik/http/middlewares/Middleware10/headers/sslRedirect` | `true` | +| `traefik/http/middlewares/Middleware10/headers/sslTemporaryRedirect` | `true` | +| `traefik/http/middlewares/Middleware10/headers/stsIncludeSubdomains` | `true` | +| `traefik/http/middlewares/Middleware10/headers/stsPreload` | `true` | +| `traefik/http/middlewares/Middleware10/headers/stsSeconds` | `42` | +| `traefik/http/middlewares/Middleware11/ipWhiteList/ipStrategy/depth` | `42` | +| `traefik/http/middlewares/Middleware11/ipWhiteList/ipStrategy/excludedIPs/0` | `foobar` | +| `traefik/http/middlewares/Middleware11/ipWhiteList/ipStrategy/excludedIPs/1` | `foobar` | +| `traefik/http/middlewares/Middleware11/ipWhiteList/sourceRange/0` | `foobar` | +| `traefik/http/middlewares/Middleware11/ipWhiteList/sourceRange/1` | `foobar` | +| `traefik/http/middlewares/Middleware12/inFlightReq/amount` | `42` | +| `traefik/http/middlewares/Middleware12/inFlightReq/sourceCriterion/ipStrategy/depth` | `42` | +| `traefik/http/middlewares/Middleware12/inFlightReq/sourceCriterion/ipStrategy/excludedIPs/0` | `foobar` | +| `traefik/http/middlewares/Middleware12/inFlightReq/sourceCriterion/ipStrategy/excludedIPs/1` | `foobar` | +| `traefik/http/middlewares/Middleware12/inFlightReq/sourceCriterion/requestHeaderName` | `foobar` | +| `traefik/http/middlewares/Middleware12/inFlightReq/sourceCriterion/requestHost` | `true` | +| `traefik/http/middlewares/Middleware13/passTLSClientCert/info/issuer/commonName` | `true` | +| `traefik/http/middlewares/Middleware13/passTLSClientCert/info/issuer/country` | `true` | +| `traefik/http/middlewares/Middleware13/passTLSClientCert/info/issuer/domainComponent` | `true` | +| `traefik/http/middlewares/Middleware13/passTLSClientCert/info/issuer/locality` | `true` | +| `traefik/http/middlewares/Middleware13/passTLSClientCert/info/issuer/organization` | `true` | +| `traefik/http/middlewares/Middleware13/passTLSClientCert/info/issuer/province` | `true` | +| `traefik/http/middlewares/Middleware13/passTLSClientCert/info/issuer/serialNumber` | `true` | +| `traefik/http/middlewares/Middleware13/passTLSClientCert/info/notAfter` | `true` | +| `traefik/http/middlewares/Middleware13/passTLSClientCert/info/notBefore` | `true` | +| `traefik/http/middlewares/Middleware13/passTLSClientCert/info/sans` | `true` | +| `traefik/http/middlewares/Middleware13/passTLSClientCert/info/serialNumber` | `true` | +| `traefik/http/middlewares/Middleware13/passTLSClientCert/info/subject/commonName` | `true` | +| `traefik/http/middlewares/Middleware13/passTLSClientCert/info/subject/country` | `true` | +| `traefik/http/middlewares/Middleware13/passTLSClientCert/info/subject/domainComponent` | `true` | +| `traefik/http/middlewares/Middleware13/passTLSClientCert/info/subject/locality` | `true` | +| `traefik/http/middlewares/Middleware13/passTLSClientCert/info/subject/organization` | `true` | +| `traefik/http/middlewares/Middleware13/passTLSClientCert/info/subject/province` | `true` | +| `traefik/http/middlewares/Middleware13/passTLSClientCert/info/subject/serialNumber` | `true` | +| `traefik/http/middlewares/Middleware13/passTLSClientCert/pem` | `true` | +| `traefik/http/middlewares/Middleware14/rateLimit/average` | `42` | +| `traefik/http/middlewares/Middleware14/rateLimit/burst` | `42` | +| `traefik/http/middlewares/Middleware14/rateLimit/period` | `42` | +| `traefik/http/middlewares/Middleware14/rateLimit/sourceCriterion/ipStrategy/depth` | `42` | +| `traefik/http/middlewares/Middleware14/rateLimit/sourceCriterion/ipStrategy/excludedIPs/0` | `foobar` | +| `traefik/http/middlewares/Middleware14/rateLimit/sourceCriterion/ipStrategy/excludedIPs/1` | `foobar` | +| `traefik/http/middlewares/Middleware14/rateLimit/sourceCriterion/requestHeaderName` | `foobar` | +| `traefik/http/middlewares/Middleware14/rateLimit/sourceCriterion/requestHost` | `true` | +| `traefik/http/middlewares/Middleware15/redirectRegex/permanent` | `true` | +| `traefik/http/middlewares/Middleware15/redirectRegex/regex` | `foobar` | +| `traefik/http/middlewares/Middleware15/redirectRegex/replacement` | `foobar` | +| `traefik/http/middlewares/Middleware16/redirectScheme/permanent` | `true` | +| `traefik/http/middlewares/Middleware16/redirectScheme/port` | `foobar` | +| `traefik/http/middlewares/Middleware16/redirectScheme/scheme` | `foobar` | +| `traefik/http/middlewares/Middleware17/replacePath/path` | `foobar` | +| `traefik/http/middlewares/Middleware18/replacePathRegex/regex` | `foobar` | +| `traefik/http/middlewares/Middleware18/replacePathRegex/replacement` | `foobar` | +| `traefik/http/middlewares/Middleware19/retry/attempts` | `42` | +| `traefik/http/middlewares/Middleware20/stripPrefix/forceSlash` | `true` | +| `traefik/http/middlewares/Middleware20/stripPrefix/prefixes/0` | `foobar` | +| `traefik/http/middlewares/Middleware20/stripPrefix/prefixes/1` | `foobar` | +| `traefik/http/middlewares/Middleware21/stripPrefixRegex/regex/0` | `foobar` | +| `traefik/http/middlewares/Middleware21/stripPrefixRegex/regex/1` | `foobar` | | `traefik/http/routers/Router0/entryPoints/0` | `foobar` | | `traefik/http/routers/Router0/entryPoints/1` | `foobar` | | `traefik/http/routers/Router0/middlewares/0` | `foobar` | @@ -246,3 +248,15 @@ | `traefik/tls/stores/Store0/defaultCertificate/keyFile` | `foobar` | | `traefik/tls/stores/Store1/defaultCertificate/certFile` | `foobar` | | `traefik/tls/stores/Store1/defaultCertificate/keyFile` | `foobar` | +| `traefik/udp/routers/UDPRouter0/entryPoints/0` | `foobar` | +| `traefik/udp/routers/UDPRouter0/entryPoints/1` | `foobar` | +| `traefik/udp/routers/UDPRouter0/service` | `foobar` | +| `traefik/udp/routers/UDPRouter1/entryPoints/0` | `foobar` | +| `traefik/udp/routers/UDPRouter1/entryPoints/1` | `foobar` | +| `traefik/udp/routers/UDPRouter1/service` | `foobar` | +| `traefik/udp/services/UDPService01/loadBalancer/servers/0/address` | `foobar` | +| `traefik/udp/services/UDPService01/loadBalancer/servers/1/address` | `foobar` | +| `traefik/udp/services/UDPService02/weighted/services/0/name` | `foobar` | +| `traefik/udp/services/UDPService02/weighted/services/0/weight` | `42` | +| `traefik/udp/services/UDPService02/weighted/services/1/name` | `foobar` | +| `traefik/udp/services/UDPService02/weighted/services/1/weight` | `42` | diff --git a/docs/content/reference/dynamic-configuration/marathon-labels.json b/docs/content/reference/dynamic-configuration/marathon-labels.json index 5a2212366..ca11e5a1d 100644 --- a/docs/content/reference/dynamic-configuration/marathon-labels.json +++ b/docs/content/reference/dynamic-configuration/marathon-labels.json @@ -81,6 +81,7 @@ "traefik.http.middlewares.middleware13.passtlsclientcert.info.notafter": "true", "traefik.http.middlewares.middleware13.passtlsclientcert.info.notbefore": "true", "traefik.http.middlewares.middleware13.passtlsclientcert.info.sans": "true", +"traefik.http.middlewares.middleware13.passtlsclientcert.info.serialnumber": "true", "traefik.http.middlewares.middleware13.passtlsclientcert.info.subject.commonname": "true", "traefik.http.middlewares.middleware13.passtlsclientcert.info.subject.country": "true", "traefik.http.middlewares.middleware13.passtlsclientcert.info.subject.domaincomponent": "true", @@ -90,8 +91,8 @@ "traefik.http.middlewares.middleware13.passtlsclientcert.info.subject.serialnumber": "true", "traefik.http.middlewares.middleware13.passtlsclientcert.pem": "true", "traefik.http.middlewares.middleware14.ratelimit.average": "42", -"traefik.http.middlewares.middleware13.ratelimit.period": "42", "traefik.http.middlewares.middleware14.ratelimit.burst": "42", +"traefik.http.middlewares.middleware14.ratelimit.period": "42", "traefik.http.middlewares.middleware14.ratelimit.sourcecriterion.ipstrategy.depth": "42", "traefik.http.middlewares.middleware14.ratelimit.sourcecriterion.ipstrategy.excludedips": "foobar, foobar", "traefik.http.middlewares.middleware14.ratelimit.sourcecriterion.requestheadername": "foobar", @@ -168,3 +169,8 @@ "traefik.tcp.routers.tcprouter1.tls.passthrough": "true", "traefik.tcp.services.tcpservice01.loadbalancer.terminationdelay": "42", "traefik.tcp.services.tcpservice01.loadbalancer.server.port": "foobar", +"traefik.udp.routers.udprouter0.entrypoints": "foobar, foobar", +"traefik.udp.routers.udprouter0.service": "foobar", +"traefik.udp.routers.udprouter1.entrypoints": "foobar, foobar", +"traefik.udp.routers.udprouter1.service": "foobar", +"traefik.udp.services.udpservice01.loadbalancer.server.port": "foobar", diff --git a/docs/content/routing/entrypoints.md b/docs/content/routing/entrypoints.md index 5dc32cd8c..7eeef3c80 100644 --- a/docs/content/routing/entrypoints.md +++ b/docs/content/routing/entrypoints.md @@ -6,7 +6,8 @@ Opening Connections for Incoming Requests ![entryPoints](../assets/img/entrypoints.png) EntryPoints are the network entry points into Traefik. -They define the port which will receive the requests (whether HTTP or TCP). +They define the port which will receive the packets, +and whether to listen for TCP or UDP. ## Configuration Examples @@ -64,6 +65,27 @@ They define the port which will receive the requests (whether HTTP or TCP). - Two entrypoints are defined: one called `web`, and the other called `websecure`. - `web` listens on port `80`, and `websecure` on port `443`. +??? example "UDP on port 1704" + + ```toml tab="File (TOML)" + ## Static configuration + [entryPoints] + [entryPoints.streaming] + address = ":1704/udp" + ``` + + ```yaml tab="File (YAML)" + ## Static configuration + entryPoints: + streaming: + address: ":1704/udp" + ``` + + ```bash tab="CLI" + ## Static configuration + --entryPoints.streaming.address=:1704/udp + ``` + ## Configuration ### General @@ -77,7 +99,7 @@ You can define them using a toml file, CLI arguments, or a key-value store. ## Static configuration [entryPoints] [entryPoints.name] - address = ":8888" + address = ":8888" # same as ":8888/tcp" [entryPoints.name.transport] [entryPoints.name.transport.lifeCycle] requestAcceptGraceTimeout = 42 @@ -98,7 +120,7 @@ You can define them using a toml file, CLI arguments, or a key-value store. ## Static configuration entryPoints: name: - address: ":8888" + address: ":8888" # same as ":8888/tcp" transport: lifeCycle: requestAcceptGraceTimeout: 42 @@ -121,7 +143,7 @@ You can define them using a toml file, CLI arguments, or a key-value store. ```bash tab="CLI" ## Static configuration - --entryPoints.name.address=:8888 + --entryPoints.name.address=:8888 # same as :8888/tcp --entryPoints.name.transport.lifeCycle.requestAcceptGraceTimeout=42 --entryPoints.name.transport.lifeCycle.graceTimeOut=42 --entryPoints.name.transport.respondingTimeouts.readTimeout=42 @@ -133,6 +155,45 @@ You can define them using a toml file, CLI arguments, or a key-value store. --entryPoints.name.forwardedHeaders.trustedIPs=127.0.0.1,192.168.0.1 ``` +### Address + +The address defines the port, and optionally the hostname, on which to listen for incoming connections and packets. +It also defines the protocol to use (TCP or UDP). +If no protocol is specified, the default is TCP. +The format is: + +```bash +[host]:port[/tcp|/udp] +``` + +If both TCP and UDP are wanted for the same port, two entryPoints definitions are needed, such as in the example below. + +??? example "Both TCP and UDP on port 3179" + + ```toml tab="File (TOML)" + ## Static configuration + [entryPoints] + [entryPoints.tcpep] + address = ":3179" + [entryPoints.udpep] + address = ":3179/udp" + ``` + + ```yaml tab="File (YAML)" + ## Static configuration + entryPoints: + tcpep: + address: ":3179" + udpep: + address: ":3179/udp" + ``` + + ```bash tab="CLI" + ## Static configuration + --entryPoints.tcpep.address=:3179 + --entryPoints.udpep.address=:3179/udp + ``` + ### Forwarded Headers You can configure Traefik to trust the forwarded headers information (`X-Forwarded-*`). @@ -202,6 +263,7 @@ You can configure Traefik to trust the forwarded headers information (`X-Forward #### `respondingTimeouts` `respondingTimeouts` are timeouts for incoming requests to the Traefik instance. +Setting them has no effect for UDP entryPoints. ??? info "`transport.respondingTimeouts.readTimeout`" diff --git a/docs/content/routing/routers/index.md b/docs/content/routing/routers/index.md index 523eb97af..a33989340 100644 --- a/docs/content/routing/routers/index.md +++ b/docs/content/routing/routers/index.md @@ -6,7 +6,8 @@ Connecting Requests to Services ![routers](../../assets/img/routers.png) A router is in charge of connecting incoming requests to the services that can handle them. -In the process, routers may use pieces of [middleware](../../middlewares/overview.md) to update the request, or act before forwarding the request to the service. +In the process, routers may use pieces of [middleware](../../middlewares/overview.md) to update the request, +or act before forwarding the request to the service. ## Configuration Example @@ -792,9 +793,11 @@ Services are the target for the router. #### 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). +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". +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" @@ -946,3 +949,157 @@ tcp: sans: - "*.snitest.com" ``` + +## Configuring UDP Routers + +!!! warning "The character `@` is not allowed in the router name" + +### General + +Similarly to TCP, as UDP is the transport layer, there is no concept of a request, +so there is no notion of an URL path prefix to match an incoming UDP packet with. +Furthermore, as there is no good TLS support at the moment for multiple hosts, +there is no Host SNI notion to match against either. +Therefore, there is no criterion that could be used as a rule to match incoming packets in order to route them. +So UDP "routers" at this time are pretty much only load-balancers in one form or another. + +!!! important "Sessions and timeout" + + Even though UDP is connectionless (and because of that), + the implementation of an UDP router in Traefik relies on what we (and a couple of other implementations) call a `session`. + It basically means that some state is kept about an ongoing communication between a client and a backend, + notably so that the proxy knows where to forward a response packet from a backend. + As expected, a `timeout` is associated to each of these sessions, + so that they get cleaned out if they go through a period of inactivity longer than a given duration (that is hardcoded to 3 seconds for now). + Making this timeout configurable will be considered later if we get more usage feedback on this matter. + +### EntryPoints + +If not specified, UDP routers will accept packets from all defined (UDP) entry points. +If one wants to limit the router scope to a set of entry points, one should set the entry points option. + +??? example "Listens to Every Entry Point" + + **Dynamic Configuration** + + ```toml tab="File (TOML)" + ## Dynamic configuration + + [udp.routers] + [udp.routers.Router-1] + # By default, routers listen to all UDP entrypoints, + # i.e. "other", and "streaming". + service = "service-1" + ``` + + ```yaml tab="File (YAML)" + ## Dynamic configuration + + udp: + routers: + Router-1: + # By default, routers listen to all UDP entrypoints + # i.e. "other", and "streaming". + service: "service-1" + ``` + + **Static Configuration** + + ```toml tab="File (TOML)" + ## Static configuration + + [entryPoints] + # not used by UDP routers + [entryPoints.web] + address = ":80" + # used by UDP routers + [entryPoints.other] + address = ":9090/udp" + [entryPoints.streaming] + address = ":9191/udp" + ``` + + ```yaml tab="File (YAML)" + ## Static configuration + + entryPoints: + # not used by UDP routers + web: + address: ":80" + # used by UDP routers + other: + address: ":9090/udp" + streaming: + address: ":9191/udp" + ``` + + ```bash tab="CLI" + ## Static configuration + --entrypoints.web.address=":80" + --entrypoints.other.address=":9090/udp" + --entrypoints.streaming.address=":9191/udp" + ``` + +??? example "Listens to Specific Entry Points" + + **Dynamic Configuration** + + ```toml tab="File (TOML)" + ## Dynamic configuration + [udp.routers] + [udp.routers.Router-1] + # does not listen on "other" entry point + entryPoints = ["streaming"] + service = "service-1" + ``` + + ```yaml tab="File (YAML)" + ## Dynamic configuration + udp: + routers: + Router-1: + # does not listen on "other" entry point + entryPoints: + - "streaming" + service: "service-1" + ``` + + **Static Configuration** + + ```toml tab="File (TOML)" + ## Static configuration + + [entryPoints] + [entryPoints.web] + address = ":80" + [entryPoints.other] + address = ":9090/udp" + [entryPoints.streaming] + address = ":9191/udp" + ``` + + ```yaml tab="File (YAML)" + ## Static configuration + + entryPoints: + web: + address: ":80" + other: + address: ":9090/udp" + streaming: + address: ":9191/udp" + ``` + + ```bash tab="CLI" + ## Static configuration + --entrypoints.web.address=":80" + --entrypoints.other.address=":9090/udp" + --entrypoints.streaming.address=":9191/udp" + ``` + +### Services + +There must be one (and only one) UDP [service](../services/index.md) referenced per UDP router. +Services are the target for the router. + +!!! important "UDP routers can only target UDP services (and not HTTP or TCP services)." diff --git a/docs/content/routing/services/index.md b/docs/content/routing/services/index.md index eff981637..281df7aae 100644 --- a/docs/content/routing/services/index.md +++ b/docs/content/routing/services/index.md @@ -55,6 +55,28 @@ The `Services` are responsible for configuring how to reach the actual services - address: ":" ``` +??? example "Declaring a UDP Service with Two Servers -- Using the [File Provider](../../providers/file.md)" + + ```toml tab="TOML" + ## Dynamic configuration + [udp.services] + [udp.services.my-service.loadBalancer] + [[udp.services.my-service.loadBalancer.servers]] + address = ":" + [[udp.services.my-service.loadBalancer.servers]] + address = ":" + ``` + + ```yaml tab="YAML" + udp: + services: + my-service: + loadBalancer: + servers: + - address: ":" + - address: ":" + ``` + ## Configuring HTTP Services ### Servers Load Balancer @@ -635,3 +657,117 @@ tcp: servers: - address: "xxx.xxx.xxx.xxx:8080" ``` + +## Configuring UDP Services + +### General + +Each of the fields of the service section represents a kind of service. +Which means, that for each specified service, one of the fields, and only one, +has to be enabled to define what kind of service is created. +Currently, the two available kinds are `LoadBalancer`, and `Weighted`. + +### Servers Load Balancer + +The servers load balancer is in charge of balancing the requests between the servers of the same service. + +??? example "Declaring a Service with Two Servers -- Using the [File Provider](../../providers/file.md)" + + ```toml tab="TOML" + ## Dynamic configuration + [udp.services] + [udp.services.my-service.loadBalancer] + [[udp.services.my-service.loadBalancer.servers]] + address = "xx.xx.xx.xx:xx" + [[udp.services.my-service.loadBalancer.servers]] + address = "xx.xx.xx.xx:xx" + ``` + + ```yaml tab="YAML" + ## Dynamic configuration + udp: + services: + my-service: + loadBalancer: + servers: + - address: "xx.xx.xx.xx:xx" + - address: "xx.xx.xx.xx:xx" + ``` + +#### Servers + +The Servers field defines all the servers that are part of this load-balancing group, +i.e. each address (IP:Port) on which an instance of the service's program is deployed. + +??? example "A Service with One Server -- Using the [File Provider](../../providers/file.md)" + + ```toml tab="TOML" + ## Dynamic configuration + [udp.services] + [udp.services.my-service.loadBalancer] + [[udp.services.my-service.loadBalancer.servers]] + address = "xx.xx.xx.xx:xx" + ``` + + ```yaml tab="YAML" + ## Dynamic configuration + udp: + services: + my-service: + loadBalancer: + servers: + - address: "xx.xx.xx.xx:xx" + ``` + +### Weighted Round Robin + +The Weighted Round Robin (alias `WRR`) load-balancer of services is in charge of balancing the requests between multiple services based on provided weights. + +This strategy is only available to load balance between [services](./index.md) and not between [servers](./index.md#servers). + +This strategy can only be defined with [File](../../providers/file.md). + +```toml tab="TOML" +## Dynamic configuration +[udp.services] + [udp.services.app] + [[udp.services.app.weighted.services]] + name = "appv1" + weight = 3 + [[udp.services.app.weighted.services]] + name = "appv2" + weight = 1 + + [udp.services.appv1] + [udp.services.appv1.loadBalancer] + [[udp.services.appv1.loadBalancer.servers]] + address = "private-ip-server-1:8080/" + + [udp.services.appv2] + [udp.services.appv2.loadBalancer] + [[udp.services.appv2.loadBalancer.servers]] + address = "private-ip-server-2:8080/" +``` + +```yaml tab="YAML" +## Dynamic configuration +udp: + services: + app: + weighted: + services: + - name: appv1 + weight: 3 + - name: appv2 + weight: 1 + + appv1: + loadBalancer: + servers: + - address: "xxx.xxx.xxx.xxx:8080" + + appv2: + loadBalancer: + servers: + - address: "xxx.xxx.xxx.xxx:8080" +``` diff --git a/integration/fixtures/router_errors.toml b/integration/fixtures/router_errors.toml index 014c5bc51..c46c25f6e 100644 --- a/integration/fixtures/router_errors.toml +++ b/integration/fixtures/router_errors.toml @@ -49,3 +49,30 @@ [tls.options.baz] minversion = "VersionTLS11" + +[tcp.routers] + [tcp.routers.router3] + entrypoints=["unknown-entrypoint"] + service = "service1" + rule = "HostSNI(`mydomain.com`)" + [tcp.routers.router4] + entrypoints=["websecure"] + service = "service1" + rule = "Host(`mydomain.com`)" + +[tcp.services] + [tcp.services.service1] + [tcp.services.service1.loadBalancer] + [[tcp.services.service1.loadBalancer.servers]] + address = "127.0.0.1:9010" + +[udp.routers] + [udp.routers.router3] + entrypoints=["unknown-entrypoint"] + service = "service1" + +[udp.services] + [udp.services.service1] + [udp.services.service1.loadBalancer] + [[udp.services.service1.loadBalancer.servers]] + address = "127.0.0.1:9010" diff --git a/integration/fixtures/service_errors.toml b/integration/fixtures/service_errors.toml index 77b8d80b6..b135ea374 100644 --- a/integration/fixtures/service_errors.toml +++ b/integration/fixtures/service_errors.toml @@ -8,6 +8,8 @@ [entryPoints] [entryPoints.websecure] address = ":4443" + [entryPoints.udp] + address = ":4443/udp" [api] insecure = true @@ -33,3 +35,35 @@ [http.services.service2.loadBalancer] [[http.services.service2.loadBalancer.servers]] url = "http://127.0.0.1:9010" + +[tcp.routers] + [tcp.routers.router4] + service = "service1" + rule = "HostSNI(`snitest.net`)" + + [tcp.routers.router5] + service = "service2" + rule = "HostSNI(`snitest.com`)" + +[tcp.services] + [tcp.services.service1] + + [tcp.services.service2] + [tcp.services.service2.loadBalancer] + [[tcp.services.service2.loadBalancer.servers]] + address = "127.0.0.1:9010" + +[udp.routers] + [udp.routers.router4] + service = "service1" + + [udp.routers.router5] + service = "service2" + +[udp.services] + [udp.services.service1] + + [udp.services.service2] + [udp.services.service2.loadBalancer] + [[udp.services.service2.loadBalancer.servers]] + address = "127.0.0.1:9010" diff --git a/integration/fixtures/udp/wrr.toml b/integration/fixtures/udp/wrr.toml new file mode 100644 index 000000000..463f2deba --- /dev/null +++ b/integration/fixtures/udp/wrr.toml @@ -0,0 +1,53 @@ +[global] + checkNewVersion = false + sendAnonymousUsage = false + +[log] + level = "DEBUG" + +[entryPoints] + [entryPoints.udp] + address = ":8093/udp" + [entryPoints.web] + address = ":8093" + +[api] + insecure = true + +[providers.file] + filename = "{{ .SelfFilename }}" + +## dynamic configuration ## +[udp] + [udp.routers] + [udp.routers.to-whoami-a] + service = "whoami" + entryPoints = [ "udp" ] + + [[udp.services.whoami.weighted.services]] + name="whoami-a" + weight=3 + [[udp.services.whoami.weighted.services]] + name="whoami-b" + weight=1 + + [udp.services.whoami-a.loadBalancer] + [[udp.services.whoami-a.loadBalancer.servers]] + address = "{{ .WhoamiAIP}}:8080" + [[udp.services.whoami-a.loadBalancer.servers]] + address = "{{ .WhoamiCIP}}:8080" + + [udp.services.whoami-b.loadBalancer] + [[udp.services.whoami-b.loadBalancer.servers]] + address = "{{ .WhoamiBIP}}:8080" + +[http] + [http.routers] + [http.routers.to-whoami-d] + service = "whoami" + entryPoints = [ "web" ] + rule = "PathPrefix(`/who`)" + + [http.services.whoami.loadBalancer] + [[http.services.whoami.loadBalancer.servers]] + url = "http://{{ .WhoamiDIP}}" diff --git a/integration/integration_test.go b/integration/integration_test.go index 054498c7b..65158cb65 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -60,6 +60,7 @@ func Test(t *testing.T) { check.Suite(&TimeoutSuite{}) check.Suite(&TLSClientHeadersSuite{}) check.Suite(&TracingSuite{}) + check.Suite(&UDPSuite{}) check.Suite(&WebsocketSuite{}) check.Suite(&ZookeeperSuite{}) } diff --git a/integration/resources/compose/udp.yml b/integration/resources/compose/udp.yml new file mode 100644 index 000000000..d0370bd51 --- /dev/null +++ b/integration/resources/compose/udp.yml @@ -0,0 +1,14 @@ +whoami-a: + image: containous/whoamiudp:dev + command: -name whoami-a + +whoami-b: + image: containous/whoamiudp:dev + command: -name whoami-b + +whoami-c: + image: containous/whoamiudp:dev + command: -name whoami-c + +whoami-d: + image: containous/whoami diff --git a/integration/simple_test.go b/integration/simple_test.go index cc09aabec..5144f7136 100644 --- a/integration/simple_test.go +++ b/integration/simple_test.go @@ -549,6 +549,83 @@ func (s *SimpleSuite) TestServiceConfigErrors(c *check.C) { c.Assert(err, checker.IsNil) } +func (s *SimpleSuite) TestTCPRouterConfigErrors(c *check.C) { + file := s.adaptFile(c, "fixtures/router_errors.toml", struct{}{}) + defer os.Remove(file) + + cmd, output := s.traefikCmd(withConfigFile(file)) + defer output(c) + + err := cmd.Start() + c.Assert(err, checker.IsNil) + defer cmd.Process.Kill() + + // router3 has an error because it uses an unknown entrypoint + err = try.GetRequest("http://127.0.0.1:8080/api/tcp/routers/router3@file", 1000*time.Millisecond, try.BodyContains(`entryPoint \"unknown-entrypoint\" doesn't exist`, "no valid entryPoint for this router")) + c.Assert(err, checker.IsNil) + + // router4 has an unsupported Rule + err = try.GetRequest("http://127.0.0.1:8080/api/tcp/routers/router4@file", 1000*time.Millisecond, try.BodyContains("unknown rule Host(`mydomain.com`)")) + c.Assert(err, checker.IsNil) +} + +func (s *SimpleSuite) TestTCPServiceConfigErrors(c *check.C) { + file := s.adaptFile(c, "fixtures/service_errors.toml", struct{}{}) + defer os.Remove(file) + + cmd, output := s.traefikCmd(withConfigFile(file)) + defer output(c) + + err := cmd.Start() + c.Assert(err, checker.IsNil) + defer cmd.Process.Kill() + + err = try.GetRequest("http://127.0.0.1:8080/api/tcp/services", 1000*time.Millisecond, try.BodyContains(`["the service \"service1@file\" does not have any type defined"]`)) + c.Assert(err, checker.IsNil) + + err = try.GetRequest("http://127.0.0.1:8080/api/tcp/services/service1@file", 1000*time.Millisecond, try.BodyContains(`"status":"disabled"`)) + c.Assert(err, checker.IsNil) + + err = try.GetRequest("http://127.0.0.1:8080/api/tcp/services/service2@file", 1000*time.Millisecond, try.BodyContains(`"status":"enabled"`)) + c.Assert(err, checker.IsNil) +} + +func (s *SimpleSuite) TestUDPRouterConfigErrors(c *check.C) { + file := s.adaptFile(c, "fixtures/router_errors.toml", struct{}{}) + defer os.Remove(file) + + cmd, output := s.traefikCmd(withConfigFile(file)) + defer output(c) + + err := cmd.Start() + c.Assert(err, checker.IsNil) + defer cmd.Process.Kill() + + err = try.GetRequest("http://127.0.0.1:8080/api/udp/routers/router3@file", 1000*time.Millisecond, try.BodyContains(`entryPoint \"unknown-entrypoint\" doesn't exist`, "no valid entryPoint for this router")) + c.Assert(err, checker.IsNil) +} + +func (s *SimpleSuite) TestUDPServiceConfigErrors(c *check.C) { + file := s.adaptFile(c, "fixtures/service_errors.toml", struct{}{}) + defer os.Remove(file) + + cmd, output := s.traefikCmd(withConfigFile(file)) + defer output(c) + + err := cmd.Start() + c.Assert(err, checker.IsNil) + defer cmd.Process.Kill() + + err = try.GetRequest("http://127.0.0.1:8080/api/udp/services", 1000*time.Millisecond, try.BodyContains(`["the udp service \"service1@file\" does not have any type defined"]`)) + c.Assert(err, checker.IsNil) + + err = try.GetRequest("http://127.0.0.1:8080/api/udp/services/service1@file", 1000*time.Millisecond, try.BodyContains(`"status":"disabled"`)) + c.Assert(err, checker.IsNil) + + err = try.GetRequest("http://127.0.0.1:8080/api/udp/services/service2@file", 1000*time.Millisecond, try.BodyContains(`"status":"enabled"`)) + c.Assert(err, checker.IsNil) +} + func (s *SimpleSuite) TestWRR(c *check.C) { s.createComposeProject(c, "base") s.composeProject.Start(c) diff --git a/integration/udp_test.go b/integration/udp_test.go new file mode 100644 index 000000000..ec9214e55 --- /dev/null +++ b/integration/udp_test.go @@ -0,0 +1,107 @@ +package integration + +import ( + "net" + "net/http" + "os" + "strings" + "time" + + "github.com/containous/traefik/v2/integration/try" + "github.com/go-check/check" + checker "github.com/vdemeester/shakers" +) + +type UDPSuite struct{ BaseSuite } + +func (s *UDPSuite) SetUpSuite(c *check.C) { + s.createComposeProject(c, "udp") + s.composeProject.Start(c) +} + +func guessWhoUDP(addr string) (string, error) { + var conn net.Conn + var err error + + udpAddr, err2 := net.ResolveUDPAddr("udp", addr) + if err2 != nil { + return "", err2 + } + + conn, err = net.DialUDP("udp", nil, udpAddr) + if err != nil { + return "", err + } + + _, err = conn.Write([]byte("WHO")) + if err != nil { + return "", err + } + + out := make([]byte, 2048) + n, err := conn.Read(out) + if err != nil { + return "", err + } + return string(out[:n]), nil +} + +func (s *UDPSuite) TestWRR(c *check.C) { + whoamiAIP := s.composeProject.Container(c, "whoami-a").NetworkSettings.IPAddress + whoamiBIP := s.composeProject.Container(c, "whoami-b").NetworkSettings.IPAddress + whoamiCIP := s.composeProject.Container(c, "whoami-c").NetworkSettings.IPAddress + whoamiDIP := s.composeProject.Container(c, "whoami-d").NetworkSettings.IPAddress + + file := s.adaptFile(c, "fixtures/udp/wrr.toml", struct { + WhoamiAIP string + WhoamiBIP string + WhoamiCIP string + WhoamiDIP string + }{ + WhoamiAIP: whoamiAIP, + WhoamiBIP: whoamiBIP, + WhoamiCIP: whoamiCIP, + WhoamiDIP: whoamiDIP, + }) + 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", 5*time.Second, try.StatusCodeIs(http.StatusOK), try.BodyContains("whoami-a")) + c.Assert(err, checker.IsNil) + + err = try.GetRequest("http://127.0.0.1:8093/who", 5*time.Second, try.StatusCodeIs(http.StatusOK)) + c.Assert(err, checker.IsNil) + + stop := make(chan struct{}) + go func() { + call := map[string]int{} + for i := 0; i < 4; i++ { + out, err := guessWhoUDP("127.0.0.1:8093") + c.Assert(err, checker.IsNil) + switch { + case strings.Contains(out, "whoami-a"): + call["whoami-a"]++ + case strings.Contains(out, "whoami-b"): + call["whoami-b"]++ + case strings.Contains(out, "whoami-c"): + call["whoami-c"]++ + default: + call["unknown"]++ + } + } + c.Assert(call, checker.DeepEquals, map[string]int{"whoami-a": 2, "whoami-b": 1, "whoami-c": 1}) + close(stop) + }() + + select { + case <-stop: + case <-time.Tick(time.Second * 5): + c.Error("Timeout") + } +} diff --git a/pkg/api/handler.go b/pkg/api/handler.go index d2251fcd8..f62419ea9 100644 --- a/pkg/api/handler.go +++ b/pkg/api/handler.go @@ -40,6 +40,8 @@ type RunTimeRepresentation struct { Services map[string]*serviceInfoRepresentation `json:"services,omitempty"` TCPRouters map[string]*runtime.TCPRouterInfo `json:"tcpRouters,omitempty"` TCPServices map[string]*runtime.TCPServiceInfo `json:"tcpServices,omitempty"` + UDPRouters map[string]*runtime.UDPRouterInfo `json:"udpRouters,omitempty"` + UDPServices map[string]*runtime.UDPServiceInfo `json:"udpServices,omitempty"` } // Handler serves the configuration and status of Traefik on API endpoints. @@ -105,6 +107,11 @@ func (h Handler) createRouter() *mux.Router { router.Methods(http.MethodGet).Path("/api/tcp/services").HandlerFunc(h.getTCPServices) router.Methods(http.MethodGet).Path("/api/tcp/services/{serviceID}").HandlerFunc(h.getTCPService) + router.Methods(http.MethodGet).Path("/api/udp/routers").HandlerFunc(h.getUDPRouters) + router.Methods(http.MethodGet).Path("/api/udp/routers/{routerID}").HandlerFunc(h.getUDPRouter) + router.Methods(http.MethodGet).Path("/api/udp/services").HandlerFunc(h.getUDPServices) + router.Methods(http.MethodGet).Path("/api/udp/services/{serviceID}").HandlerFunc(h.getUDPService) + version.Handler{}.Append(router) if h.dashboard { @@ -129,6 +136,8 @@ func (h Handler) getRuntimeConfiguration(rw http.ResponseWriter, request *http.R Services: siRepr, TCPRouters: h.runtimeConfiguration.TCPRouters, TCPServices: h.runtimeConfiguration.TCPServices, + UDPRouters: h.runtimeConfiguration.UDPRouters, + UDPServices: h.runtimeConfiguration.UDPServices, } rw.Header().Set("Content-Type", "application/json") diff --git a/pkg/api/handler_udp.go b/pkg/api/handler_udp.go new file mode 100644 index 000000000..492b01212 --- /dev/null +++ b/pkg/api/handler_udp.go @@ -0,0 +1,164 @@ +package api + +import ( + "encoding/json" + "fmt" + "net/http" + "sort" + "strconv" + "strings" + + "github.com/containous/traefik/v2/pkg/config/runtime" + "github.com/containous/traefik/v2/pkg/log" + "github.com/gorilla/mux" +) + +type udpRouterRepresentation struct { + *runtime.UDPRouterInfo + Name string `json:"name,omitempty"` + Provider string `json:"provider,omitempty"` +} + +func newUDPRouterRepresentation(name string, rt *runtime.UDPRouterInfo) udpRouterRepresentation { + return udpRouterRepresentation{ + UDPRouterInfo: rt, + Name: name, + Provider: getProviderName(name), + } +} + +type udpServiceRepresentation struct { + *runtime.UDPServiceInfo + Name string `json:"name,omitempty"` + Provider string `json:"provider,omitempty"` + Type string `json:"type,omitempty"` +} + +func newUDPServiceRepresentation(name string, si *runtime.UDPServiceInfo) udpServiceRepresentation { + return udpServiceRepresentation{ + UDPServiceInfo: si, + Name: name, + Provider: getProviderName(name), + Type: strings.ToLower(extractType(si.UDPService)), + } +} + +func (h Handler) getUDPRouters(rw http.ResponseWriter, request *http.Request) { + results := make([]udpRouterRepresentation, 0, len(h.runtimeConfiguration.UDPRouters)) + + criterion := newSearchCriterion(request.URL.Query()) + + for name, rt := range h.runtimeConfiguration.UDPRouters { + if keepUDPRouter(name, rt, criterion) { + results = append(results, newUDPRouterRepresentation(name, rt)) + } + } + + sort.Slice(results, func(i, j int) bool { + return results[i].Name < results[j].Name + }) + + rw.Header().Set("Content-Type", "application/json") + + pageInfo, err := pagination(request, len(results)) + if err != nil { + writeError(rw, err.Error(), http.StatusBadRequest) + return + } + + rw.Header().Set(nextPageHeader, strconv.Itoa(pageInfo.nextPage)) + + err = json.NewEncoder(rw).Encode(results[pageInfo.startIndex:pageInfo.endIndex]) + if err != nil { + log.FromContext(request.Context()).Error(err) + writeError(rw, err.Error(), http.StatusInternalServerError) + } +} + +func (h Handler) getUDPRouter(rw http.ResponseWriter, request *http.Request) { + routerID := mux.Vars(request)["routerID"] + + rw.Header().Set("Content-Type", "application/json") + + router, ok := h.runtimeConfiguration.UDPRouters[routerID] + if !ok { + writeError(rw, fmt.Sprintf("router not found: %s", routerID), http.StatusNotFound) + return + } + + result := newUDPRouterRepresentation(routerID, router) + + err := json.NewEncoder(rw).Encode(result) + if err != nil { + log.FromContext(request.Context()).Error(err) + writeError(rw, err.Error(), http.StatusInternalServerError) + } +} + +func (h Handler) getUDPServices(rw http.ResponseWriter, request *http.Request) { + results := make([]udpServiceRepresentation, 0, len(h.runtimeConfiguration.UDPServices)) + + criterion := newSearchCriterion(request.URL.Query()) + + for name, si := range h.runtimeConfiguration.UDPServices { + if keepUDPService(name, si, criterion) { + results = append(results, newUDPServiceRepresentation(name, si)) + } + } + + sort.Slice(results, func(i, j int) bool { + return results[i].Name < results[j].Name + }) + + rw.Header().Set("Content-Type", "application/json") + + pageInfo, err := pagination(request, len(results)) + if err != nil { + writeError(rw, err.Error(), http.StatusBadRequest) + return + } + + rw.Header().Set(nextPageHeader, strconv.Itoa(pageInfo.nextPage)) + + err = json.NewEncoder(rw).Encode(results[pageInfo.startIndex:pageInfo.endIndex]) + if err != nil { + log.FromContext(request.Context()).Error(err) + writeError(rw, err.Error(), http.StatusInternalServerError) + } +} + +func (h Handler) getUDPService(rw http.ResponseWriter, request *http.Request) { + serviceID := mux.Vars(request)["serviceID"] + + rw.Header().Set("Content-Type", "application/json") + + service, ok := h.runtimeConfiguration.UDPServices[serviceID] + if !ok { + writeError(rw, fmt.Sprintf("service not found: %s", serviceID), http.StatusNotFound) + return + } + + result := newUDPServiceRepresentation(serviceID, service) + + err := json.NewEncoder(rw).Encode(result) + if err != nil { + log.FromContext(request.Context()).Error(err) + writeError(rw, err.Error(), http.StatusInternalServerError) + } +} + +func keepUDPRouter(name string, item *runtime.UDPRouterInfo, criterion *searchCriterion) bool { + if criterion == nil { + return true + } + + return criterion.withStatus(item.Status) && criterion.searchIn(name) +} + +func keepUDPService(name string, item *runtime.UDPServiceInfo, criterion *searchCriterion) bool { + if criterion == nil { + return true + } + + return criterion.withStatus(item.Status) && criterion.searchIn(name) +} diff --git a/pkg/api/handler_udp_test.go b/pkg/api/handler_udp_test.go new file mode 100644 index 000000000..a716300cc --- /dev/null +++ b/pkg/api/handler_udp_test.go @@ -0,0 +1,537 @@ +package api + +import ( + "context" + "encoding/json" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + + "github.com/containous/traefik/v2/pkg/config/dynamic" + "github.com/containous/traefik/v2/pkg/config/runtime" + "github.com/containous/traefik/v2/pkg/config/static" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestHandler_UDP(t *testing.T) { + type expected struct { + statusCode int + nextPage string + jsonFile string + } + + testCases := []struct { + desc string + path string + conf runtime.Configuration + expected expected + }{ + { + desc: "all UDP routers, but no config", + path: "/api/udp/routers", + conf: runtime.Configuration{}, + expected: expected{ + statusCode: http.StatusOK, + nextPage: "1", + jsonFile: "testdata/udprouters-empty.json", + }, + }, + { + desc: "all UDP routers", + path: "/api/udp/routers", + conf: runtime.Configuration{ + UDPRouters: map[string]*runtime.UDPRouterInfo{ + "test@myprovider": { + UDPRouter: &dynamic.UDPRouter{ + EntryPoints: []string{"web"}, + Service: "foo-service@myprovider", + }, + Status: runtime.StatusEnabled, + }, + "bar@myprovider": { + UDPRouter: &dynamic.UDPRouter{ + EntryPoints: []string{"web"}, + Service: "foo-service@myprovider", + }, + Status: runtime.StatusWarning, + }, + "foo@myprovider": { + UDPRouter: &dynamic.UDPRouter{ + EntryPoints: []string{"web"}, + Service: "foo-service@myprovider", + }, + Status: runtime.StatusDisabled, + }, + }, + }, + expected: expected{ + statusCode: http.StatusOK, + nextPage: "1", + jsonFile: "testdata/udprouters.json", + }, + }, + { + desc: "all UDP routers, pagination, 1 res per page, want page 2", + path: "/api/udp/routers?page=2&per_page=1", + conf: runtime.Configuration{ + UDPRouters: map[string]*runtime.UDPRouterInfo{ + "bar@myprovider": { + UDPRouter: &dynamic.UDPRouter{ + EntryPoints: []string{"web"}, + Service: "foo-service@myprovider", + }, + }, + "baz@myprovider": { + UDPRouter: &dynamic.UDPRouter{ + EntryPoints: []string{"web"}, + Service: "foo-service@myprovider", + }, + }, + "test@myprovider": { + UDPRouter: &dynamic.UDPRouter{ + EntryPoints: []string{"web"}, + Service: "foo-service@myprovider", + }, + }, + }, + }, + expected: expected{ + statusCode: http.StatusOK, + nextPage: "3", + jsonFile: "testdata/udprouters-page2.json", + }, + }, + { + desc: "UDP routers filtered by status", + path: "/api/udp/routers?status=enabled", + conf: runtime.Configuration{ + UDPRouters: map[string]*runtime.UDPRouterInfo{ + "test@myprovider": { + UDPRouter: &dynamic.UDPRouter{ + EntryPoints: []string{"web"}, + Service: "foo-service@myprovider", + }, + Status: runtime.StatusEnabled, + }, + "bar@myprovider": { + UDPRouter: &dynamic.UDPRouter{ + EntryPoints: []string{"web"}, + Service: "foo-service@myprovider", + }, + Status: runtime.StatusWarning, + }, + "foo@myprovider": { + UDPRouter: &dynamic.UDPRouter{ + EntryPoints: []string{"web"}, + Service: "foo-service@myprovider", + }, + Status: runtime.StatusDisabled, + }, + }, + }, + expected: expected{ + statusCode: http.StatusOK, + nextPage: "1", + jsonFile: "testdata/udprouters-filtered-status.json", + }, + }, + { + desc: "UDP routers filtered by search", + path: "/api/udp/routers?search=bar@my", + conf: runtime.Configuration{ + UDPRouters: map[string]*runtime.UDPRouterInfo{ + "test@myprovider": { + UDPRouter: &dynamic.UDPRouter{ + EntryPoints: []string{"web"}, + Service: "foo-service@myprovider", + }, + Status: runtime.StatusEnabled, + }, + "bar@myprovider": { + UDPRouter: &dynamic.UDPRouter{ + EntryPoints: []string{"web"}, + Service: "foo-service@myprovider", + }, + Status: runtime.StatusWarning, + }, + "foo@myprovider": { + UDPRouter: &dynamic.UDPRouter{ + EntryPoints: []string{"web"}, + Service: "foo-service@myprovider", + }, + Status: runtime.StatusDisabled, + }, + }, + }, + expected: expected{ + statusCode: http.StatusOK, + nextPage: "1", + jsonFile: "testdata/udprouters-filtered-search.json", + }, + }, + { + desc: "one UDP router by id", + path: "/api/udp/routers/bar@myprovider", + conf: runtime.Configuration{ + UDPRouters: map[string]*runtime.UDPRouterInfo{ + "bar@myprovider": { + UDPRouter: &dynamic.UDPRouter{ + EntryPoints: []string{"web"}, + Service: "foo-service@myprovider", + }, + }, + }, + }, + expected: expected{ + statusCode: http.StatusOK, + jsonFile: "testdata/udprouter-bar.json", + }, + }, + { + desc: "one UDP router by id, that does not exist", + path: "/api/udp/routers/foo@myprovider", + conf: runtime.Configuration{ + UDPRouters: map[string]*runtime.UDPRouterInfo{ + "bar@myprovider": { + UDPRouter: &dynamic.UDPRouter{ + EntryPoints: []string{"web"}, + Service: "foo-service@myprovider", + }, + }, + }, + }, + expected: expected{ + statusCode: http.StatusNotFound, + }, + }, + { + desc: "one UDP router by id, but no config", + path: "/api/udp/routers/bar@myprovider", + conf: runtime.Configuration{}, + expected: expected{ + statusCode: http.StatusNotFound, + }, + }, + { + desc: "all udp services, but no config", + path: "/api/udp/services", + conf: runtime.Configuration{}, + expected: expected{ + statusCode: http.StatusOK, + nextPage: "1", + jsonFile: "testdata/udpservices-empty.json", + }, + }, + { + desc: "all udp services", + path: "/api/udp/services", + conf: runtime.Configuration{ + UDPServices: map[string]*runtime.UDPServiceInfo{ + "bar@myprovider": { + UDPService: &dynamic.UDPService{ + LoadBalancer: &dynamic.UDPServersLoadBalancer{ + Servers: []dynamic.UDPServer{ + { + Address: "127.0.0.1:2345", + }, + }, + }, + }, + UsedBy: []string{"foo@myprovider", "test@myprovider"}, + Status: runtime.StatusEnabled, + }, + "baz@myprovider": { + UDPService: &dynamic.UDPService{ + LoadBalancer: &dynamic.UDPServersLoadBalancer{ + Servers: []dynamic.UDPServer{ + { + Address: "127.0.0.2:2345", + }, + }, + }, + }, + UsedBy: []string{"foo@myprovider"}, + Status: runtime.StatusWarning, + }, + "foz@myprovider": { + UDPService: &dynamic.UDPService{ + LoadBalancer: &dynamic.UDPServersLoadBalancer{ + Servers: []dynamic.UDPServer{ + { + Address: "127.0.0.2:2345", + }, + }, + }, + }, + UsedBy: []string{"foo@myprovider"}, + Status: runtime.StatusDisabled, + }, + }, + }, + expected: expected{ + statusCode: http.StatusOK, + nextPage: "1", + jsonFile: "testdata/udpservices.json", + }, + }, + { + desc: "udp services filtered by status", + path: "/api/udp/services?status=enabled", + conf: runtime.Configuration{ + UDPServices: map[string]*runtime.UDPServiceInfo{ + "bar@myprovider": { + UDPService: &dynamic.UDPService{ + LoadBalancer: &dynamic.UDPServersLoadBalancer{ + Servers: []dynamic.UDPServer{ + { + Address: "127.0.0.1:2345", + }, + }, + }, + }, + UsedBy: []string{"foo@myprovider", "test@myprovider"}, + Status: runtime.StatusEnabled, + }, + "baz@myprovider": { + UDPService: &dynamic.UDPService{ + LoadBalancer: &dynamic.UDPServersLoadBalancer{ + Servers: []dynamic.UDPServer{ + { + Address: "127.0.0.2:2345", + }, + }, + }, + }, + UsedBy: []string{"foo@myprovider"}, + Status: runtime.StatusWarning, + }, + "foz@myprovider": { + UDPService: &dynamic.UDPService{ + LoadBalancer: &dynamic.UDPServersLoadBalancer{ + Servers: []dynamic.UDPServer{ + { + Address: "127.0.0.2:2345", + }, + }, + }, + }, + UsedBy: []string{"foo@myprovider"}, + Status: runtime.StatusDisabled, + }, + }, + }, + expected: expected{ + statusCode: http.StatusOK, + nextPage: "1", + jsonFile: "testdata/udpservices-filtered-status.json", + }, + }, + { + desc: "udp services filtered by search", + path: "/api/udp/services?search=baz@my", + conf: runtime.Configuration{ + UDPServices: map[string]*runtime.UDPServiceInfo{ + "bar@myprovider": { + UDPService: &dynamic.UDPService{ + LoadBalancer: &dynamic.UDPServersLoadBalancer{ + Servers: []dynamic.UDPServer{ + { + Address: "127.0.0.1:2345", + }, + }, + }, + }, + UsedBy: []string{"foo@myprovider", "test@myprovider"}, + Status: runtime.StatusEnabled, + }, + "baz@myprovider": { + UDPService: &dynamic.UDPService{ + LoadBalancer: &dynamic.UDPServersLoadBalancer{ + Servers: []dynamic.UDPServer{ + { + Address: "127.0.0.2:2345", + }, + }, + }, + }, + UsedBy: []string{"foo@myprovider"}, + Status: runtime.StatusWarning, + }, + "foz@myprovider": { + UDPService: &dynamic.UDPService{ + LoadBalancer: &dynamic.UDPServersLoadBalancer{ + Servers: []dynamic.UDPServer{ + { + Address: "127.0.0.2:2345", + }, + }, + }, + }, + UsedBy: []string{"foo@myprovider"}, + Status: runtime.StatusDisabled, + }, + }, + }, + expected: expected{ + statusCode: http.StatusOK, + nextPage: "1", + jsonFile: "testdata/udpservices-filtered-search.json", + }, + }, + { + desc: "all udp services, 1 res per page, want page 2", + path: "/api/udp/services?page=2&per_page=1", + conf: runtime.Configuration{ + UDPServices: map[string]*runtime.UDPServiceInfo{ + "bar@myprovider": { + UDPService: &dynamic.UDPService{ + LoadBalancer: &dynamic.UDPServersLoadBalancer{ + Servers: []dynamic.UDPServer{ + { + Address: "127.0.0.1:2345", + }, + }, + }, + }, + UsedBy: []string{"foo@myprovider", "test@myprovider"}, + }, + "baz@myprovider": { + UDPService: &dynamic.UDPService{ + LoadBalancer: &dynamic.UDPServersLoadBalancer{ + Servers: []dynamic.UDPServer{ + { + Address: "127.0.0.2:2345", + }, + }, + }, + }, + UsedBy: []string{"foo@myprovider"}, + }, + "test@myprovider": { + UDPService: &dynamic.UDPService{ + LoadBalancer: &dynamic.UDPServersLoadBalancer{ + Servers: []dynamic.UDPServer{ + { + Address: "127.0.0.3:2345", + }, + }, + }, + }, + }, + }, + }, + expected: expected{ + statusCode: http.StatusOK, + nextPage: "3", + jsonFile: "testdata/udpservices-page2.json", + }, + }, + { + desc: "one udp service by id", + path: "/api/udp/services/bar@myprovider", + conf: runtime.Configuration{ + UDPServices: map[string]*runtime.UDPServiceInfo{ + "bar@myprovider": { + UDPService: &dynamic.UDPService{ + LoadBalancer: &dynamic.UDPServersLoadBalancer{ + Servers: []dynamic.UDPServer{ + { + Address: "127.0.0.1:2345", + }, + }, + }, + }, + UsedBy: []string{"foo@myprovider", "test@myprovider"}, + }, + }, + }, + expected: expected{ + statusCode: http.StatusOK, + jsonFile: "testdata/udpservice-bar.json", + }, + }, + { + desc: "one udp service by id, that does not exist", + path: "/api/udp/services/nono@myprovider", + conf: runtime.Configuration{ + UDPServices: map[string]*runtime.UDPServiceInfo{ + "bar@myprovider": { + UDPService: &dynamic.UDPService{ + LoadBalancer: &dynamic.UDPServersLoadBalancer{ + Servers: []dynamic.UDPServer{ + { + Address: "127.0.0.1:2345", + }, + }, + }, + }, + UsedBy: []string{"foo@myprovider", "test@myprovider"}, + }, + }, + }, + expected: expected{ + statusCode: http.StatusNotFound, + }, + }, + { + desc: "one udp service by id, but no config", + path: "/api/udp/services/foo@myprovider", + conf: runtime.Configuration{}, + expected: expected{ + statusCode: http.StatusNotFound, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + rtConf := &test.conf + // To lazily initialize the Statuses. + rtConf.PopulateUsedBy() + rtConf.GetUDPRoutersByEntryPoints(context.Background(), []string{"web"}) + + handler := New(static.Configuration{API: &static.API{}, Global: &static.Global{}}, rtConf) + server := httptest.NewServer(handler.createRouter()) + + resp, err := http.DefaultClient.Get(server.URL + test.path) + require.NoError(t, err) + + assert.Equal(t, test.expected.nextPage, resp.Header.Get(nextPageHeader)) + + require.Equal(t, test.expected.statusCode, resp.StatusCode) + + if test.expected.jsonFile == "" { + return + } + + assert.Equal(t, resp.Header.Get("Content-Type"), "application/json") + + contents, err := ioutil.ReadAll(resp.Body) + require.NoError(t, err) + + err = resp.Body.Close() + require.NoError(t, err) + + if *updateExpected { + var results interface{} + err := json.Unmarshal(contents, &results) + require.NoError(t, err) + + newJSON, err := json.MarshalIndent(results, "", "\t") + require.NoError(t, err) + + err = ioutil.WriteFile(test.expected.jsonFile, newJSON, 0644) + require.NoError(t, err) + } + + data, err := ioutil.ReadFile(test.expected.jsonFile) + require.NoError(t, err) + assert.JSONEq(t, string(data), string(contents)) + }) + } +} diff --git a/pkg/api/testdata/udprouter-bar.json b/pkg/api/testdata/udprouter-bar.json new file mode 100644 index 000000000..67bdc755a --- /dev/null +++ b/pkg/api/testdata/udprouter-bar.json @@ -0,0 +1,12 @@ +{ + "entryPoints": [ + "web" + ], + "name": "bar@myprovider", + "provider": "myprovider", + "service": "foo-service@myprovider", + "status": "enabled", + "using": [ + "web" + ] +} \ No newline at end of file diff --git a/pkg/api/testdata/udprouters-empty.json b/pkg/api/testdata/udprouters-empty.json new file mode 100644 index 000000000..0637a088a --- /dev/null +++ b/pkg/api/testdata/udprouters-empty.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/pkg/api/testdata/udprouters-filtered-search.json b/pkg/api/testdata/udprouters-filtered-search.json new file mode 100644 index 000000000..4b4b86eb9 --- /dev/null +++ b/pkg/api/testdata/udprouters-filtered-search.json @@ -0,0 +1,14 @@ +[ + { + "entryPoints": [ + "web" + ], + "name": "bar@myprovider", + "provider": "myprovider", + "service": "foo-service@myprovider", + "status": "warning", + "using": [ + "web" + ] + } +] \ No newline at end of file diff --git a/pkg/api/testdata/udprouters-filtered-status.json b/pkg/api/testdata/udprouters-filtered-status.json new file mode 100644 index 000000000..2f55f4e6f --- /dev/null +++ b/pkg/api/testdata/udprouters-filtered-status.json @@ -0,0 +1,14 @@ +[ + { + "entryPoints": [ + "web" + ], + "name": "test@myprovider", + "provider": "myprovider", + "service": "foo-service@myprovider", + "status": "enabled", + "using": [ + "web" + ] + } +] \ No newline at end of file diff --git a/pkg/api/testdata/udprouters-page2.json b/pkg/api/testdata/udprouters-page2.json new file mode 100644 index 000000000..c16f683ad --- /dev/null +++ b/pkg/api/testdata/udprouters-page2.json @@ -0,0 +1,14 @@ +[ + { + "entryPoints": [ + "web" + ], + "name": "baz@myprovider", + "provider": "myprovider", + "service": "foo-service@myprovider", + "status": "enabled", + "using": [ + "web" + ] + } +] \ No newline at end of file diff --git a/pkg/api/testdata/udprouters.json b/pkg/api/testdata/udprouters.json new file mode 100644 index 000000000..51c33dd63 --- /dev/null +++ b/pkg/api/testdata/udprouters.json @@ -0,0 +1,38 @@ +[ + { + "entryPoints": [ + "web" + ], + "name": "bar@myprovider", + "provider": "myprovider", + "service": "foo-service@myprovider", + "status": "warning", + "using": [ + "web" + ] + }, + { + "entryPoints": [ + "web" + ], + "name": "foo@myprovider", + "provider": "myprovider", + "service": "foo-service@myprovider", + "status": "disabled", + "using": [ + "web" + ] + }, + { + "entryPoints": [ + "web" + ], + "name": "test@myprovider", + "provider": "myprovider", + "service": "foo-service@myprovider", + "status": "enabled", + "using": [ + "web" + ] + } +] \ No newline at end of file diff --git a/pkg/api/testdata/udpservice-bar.json b/pkg/api/testdata/udpservice-bar.json new file mode 100644 index 000000000..ade480a92 --- /dev/null +++ b/pkg/api/testdata/udpservice-bar.json @@ -0,0 +1,17 @@ +{ + "loadBalancer": { + "servers": [ + { + "address": "127.0.0.1:2345" + } + ] + }, + "name": "bar@myprovider", + "provider": "myprovider", + "status": "enabled", + "type": "loadbalancer", + "usedBy": [ + "foo@myprovider", + "test@myprovider" + ] +} \ No newline at end of file diff --git a/pkg/api/testdata/udpservices-empty.json b/pkg/api/testdata/udpservices-empty.json new file mode 100644 index 000000000..0637a088a --- /dev/null +++ b/pkg/api/testdata/udpservices-empty.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/pkg/api/testdata/udpservices-filtered-search.json b/pkg/api/testdata/udpservices-filtered-search.json new file mode 100644 index 000000000..130d5eace --- /dev/null +++ b/pkg/api/testdata/udpservices-filtered-search.json @@ -0,0 +1,18 @@ +[ + { + "loadBalancer": { + "servers": [ + { + "address": "127.0.0.2:2345" + } + ] + }, + "name": "baz@myprovider", + "provider": "myprovider", + "status": "warning", + "type": "loadbalancer", + "usedBy": [ + "foo@myprovider" + ] + } +] \ No newline at end of file diff --git a/pkg/api/testdata/udpservices-filtered-status.json b/pkg/api/testdata/udpservices-filtered-status.json new file mode 100644 index 000000000..03ec085a0 --- /dev/null +++ b/pkg/api/testdata/udpservices-filtered-status.json @@ -0,0 +1,19 @@ +[ + { + "loadBalancer": { + "servers": [ + { + "address": "127.0.0.1:2345" + } + ] + }, + "name": "bar@myprovider", + "provider": "myprovider", + "status": "enabled", + "type": "loadbalancer", + "usedBy": [ + "foo@myprovider", + "test@myprovider" + ] + } +] \ No newline at end of file diff --git a/pkg/api/testdata/udpservices-page2.json b/pkg/api/testdata/udpservices-page2.json new file mode 100644 index 000000000..414e0f37d --- /dev/null +++ b/pkg/api/testdata/udpservices-page2.json @@ -0,0 +1,18 @@ +[ + { + "loadBalancer": { + "servers": [ + { + "address": "127.0.0.2:2345" + } + ] + }, + "name": "baz@myprovider", + "provider": "myprovider", + "status": "enabled", + "type": "loadbalancer", + "usedBy": [ + "foo@myprovider" + ] + } +] \ No newline at end of file diff --git a/pkg/api/testdata/udpservices.json b/pkg/api/testdata/udpservices.json new file mode 100644 index 000000000..c3f9f7ea6 --- /dev/null +++ b/pkg/api/testdata/udpservices.json @@ -0,0 +1,51 @@ +[ + { + "loadBalancer": { + "servers": [ + { + "address": "127.0.0.1:2345" + } + ] + }, + "name": "bar@myprovider", + "provider": "myprovider", + "status": "enabled", + "type": "loadbalancer", + "usedBy": [ + "foo@myprovider", + "test@myprovider" + ] + }, + { + "loadBalancer": { + "servers": [ + { + "address": "127.0.0.2:2345" + } + ] + }, + "name": "baz@myprovider", + "provider": "myprovider", + "status": "warning", + "type": "loadbalancer", + "usedBy": [ + "foo@myprovider" + ] + }, + { + "loadBalancer": { + "servers": [ + { + "address": "127.0.0.2:2345" + } + ] + }, + "name": "foz@myprovider", + "provider": "myprovider", + "status": "disabled", + "type": "loadbalancer", + "usedBy": [ + "foo@myprovider" + ] + } +] \ No newline at end of file diff --git a/pkg/config/dynamic/config.go b/pkg/config/dynamic/config.go index d41af3c46..e9749166a 100644 --- a/pkg/config/dynamic/config.go +++ b/pkg/config/dynamic/config.go @@ -23,6 +23,7 @@ type Configurations map[string]*Configuration type Configuration struct { HTTP *HTTPConfiguration `json:"http,omitempty" toml:"http,omitempty" yaml:"http,omitempty"` TCP *TCPConfiguration `json:"tcp,omitempty" toml:"tcp,omitempty" yaml:"tcp,omitempty"` + UDP *UDPConfiguration `json:"udp,omitempty" toml:"udp,omitempty" yaml:"udp,omitempty"` TLS *TLSConfiguration `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty"` } diff --git a/pkg/config/dynamic/udp_config.go b/pkg/config/dynamic/udp_config.go new file mode 100644 index 000000000..c890445a5 --- /dev/null +++ b/pkg/config/dynamic/udp_config.go @@ -0,0 +1,82 @@ +package dynamic + +import ( + "reflect" +) + +// +k8s:deepcopy-gen=true + +// UDPConfiguration contains all the UDP configuration parameters. +type UDPConfiguration struct { + Routers map[string]*UDPRouter `json:"routers,omitempty" toml:"routers,omitempty" yaml:"routers,omitempty"` + Services map[string]*UDPService `json:"services,omitempty" toml:"services,omitempty" yaml:"services,omitempty"` +} + +// +k8s:deepcopy-gen=true + +// UDPService defines the configuration for a UDP service. All fields are mutually exclusive. +type UDPService struct { + LoadBalancer *UDPServersLoadBalancer `json:"loadBalancer,omitempty" toml:"loadBalancer,omitempty" yaml:"loadBalancer,omitempty"` + Weighted *UDPWeightedRoundRobin `json:"weighted,omitempty" toml:"weighted,omitempty" yaml:"weighted,omitempty" label:"-"` +} + +// +k8s:deepcopy-gen=true + +// UDPWeightedRoundRobin is a weighted round robin UDP load-balancer of services. +type UDPWeightedRoundRobin struct { + Services []UDPWRRService `json:"services,omitempty" toml:"services,omitempty" yaml:"services,omitempty"` +} + +// +k8s:deepcopy-gen=true + +// UDPWRRService is a reference to a UDP service load-balanced with weighted round robin. +type UDPWRRService struct { + Name string `json:"name,omitempty" toml:"name,omitempty" yaml:"name,omitempty"` + Weight *int `json:"weight,omitempty" toml:"weight,omitempty" yaml:"weight,omitempty"` +} + +// SetDefaults sets the default values for a UDPWRRService. +func (w *UDPWRRService) SetDefaults() { + defaultWeight := 1 + w.Weight = &defaultWeight +} + +// +k8s:deepcopy-gen=true + +// UDPRouter defines the configuration for an UDP router. +type UDPRouter struct { + EntryPoints []string `json:"entryPoints,omitempty" toml:"entryPoints,omitempty" yaml:"entryPoints,omitempty"` + Service string `json:"service,omitempty" toml:"service,omitempty" yaml:"service,omitempty"` +} + +// +k8s:deepcopy-gen=true + +// UDPServersLoadBalancer defines the configuration for a load-balancer of UDP servers. +type UDPServersLoadBalancer struct { + Servers []UDPServer `json:"servers,omitempty" toml:"servers,omitempty" yaml:"servers,omitempty" label-slice-as-struct:"server"` +} + +// Mergeable reports whether the given load-balancer can be merged with the receiver. +func (l *UDPServersLoadBalancer) Mergeable(loadBalancer *UDPServersLoadBalancer) bool { + savedServers := l.Servers + defer func() { + l.Servers = savedServers + }() + l.Servers = nil + + savedServersLB := loadBalancer.Servers + defer func() { + loadBalancer.Servers = savedServersLB + }() + loadBalancer.Servers = nil + + return reflect.DeepEqual(l, loadBalancer) +} + +// +k8s:deepcopy-gen=true + +// UDPServer defines a UDP server configuration. +type UDPServer struct { + Address string `json:"address,omitempty" toml:"address,omitempty" yaml:"address,omitempty" label:"-"` + Port string `toml:"-" json:"-" yaml:"-"` +} diff --git a/pkg/config/runtime/runtime.go b/pkg/config/runtime/runtime.go index b59e768b8..aa64d7475 100644 --- a/pkg/config/runtime/runtime.go +++ b/pkg/config/runtime/runtime.go @@ -22,11 +22,13 @@ type Configuration struct { Services map[string]*ServiceInfo `json:"services,omitempty"` TCPRouters map[string]*TCPRouterInfo `json:"tcpRouters,omitempty"` TCPServices map[string]*TCPServiceInfo `json:"tcpServices,omitempty"` + UDPRouters map[string]*UDPRouterInfo `json:"udpRouters,omitempty"` + UDPServices map[string]*UDPServiceInfo `json:"updServices,omitempty"` } // NewConfig returns a Configuration initialized with the given conf. It never returns nil. func NewConfig(conf dynamic.Configuration) *Configuration { - if conf.HTTP == nil && conf.TCP == nil { + if conf.HTTP == nil && conf.TCP == nil && conf.UDP == nil { return &Configuration{} } @@ -74,6 +76,22 @@ func NewConfig(conf dynamic.Configuration) *Configuration { } } + if conf.UDP != nil { + if len(conf.UDP.Routers) > 0 { + runtimeConfig.UDPRouters = make(map[string]*UDPRouterInfo, len(conf.UDP.Routers)) + for k, v := range conf.UDP.Routers { + runtimeConfig.UDPRouters[k] = &UDPRouterInfo{UDPRouter: v, Status: StatusEnabled} + } + } + + if len(conf.UDP.Services) > 0 { + runtimeConfig.UDPServices = make(map[string]*UDPServiceInfo, len(conf.UDP.Services)) + for k, v := range conf.UDP.Services { + runtimeConfig.UDPServices[k] = &UDPServiceInfo{UDPService: v, Status: StatusEnabled} + } + } + } + return runtimeConfig } @@ -158,6 +176,34 @@ func (c *Configuration) PopulateUsedBy() { sort.Strings(c.TCPServices[k].UsedBy) } + + for routerName, routerInfo := range c.UDPRouters { + // lazily initialize Status in case caller forgot to do it + if routerInfo.Status == "" { + routerInfo.Status = StatusEnabled + } + + providerName := getProviderName(routerName) + if providerName == "" { + logger.WithField(log.RouterName, routerName).Error("udp router name is not fully qualified") + continue + } + + serviceName := getQualifiedName(providerName, routerInfo.UDPRouter.Service) + if _, ok := c.UDPServices[serviceName]; !ok { + continue + } + c.UDPServices[serviceName].UsedBy = append(c.UDPServices[serviceName].UsedBy, routerName) + } + + for k, serviceInfo := range c.UDPServices { + // lazily initialize Status in case caller forgot to do it + if serviceInfo.Status == "" { + serviceInfo.Status = StatusEnabled + } + + sort.Strings(c.UDPServices[k].UsedBy) + } } func contains(entryPoints []string, entryPointName string) bool { diff --git a/pkg/config/runtime/runtime_udp.go b/pkg/config/runtime/runtime_udp.go new file mode 100644 index 000000000..bee8fed2d --- /dev/null +++ b/pkg/config/runtime/runtime_udp.go @@ -0,0 +1,114 @@ +package runtime + +import ( + "context" + "fmt" + + "github.com/containous/traefik/v2/pkg/config/dynamic" + "github.com/containous/traefik/v2/pkg/log" +) + +// GetUDPRoutersByEntryPoints returns all the UDP routers by entry points name and routers name. +func (c *Configuration) GetUDPRoutersByEntryPoints(ctx context.Context, entryPoints []string) map[string]map[string]*UDPRouterInfo { + entryPointsRouters := make(map[string]map[string]*UDPRouterInfo) + + for rtName, rt := range c.UDPRouters { + logger := log.FromContext(log.With(ctx, log.Str(log.RouterName, rtName))) + + eps := rt.EntryPoints + if len(eps) == 0 { + logger.Debugf("No entryPoint defined for this router, using the default one(s) instead: %+v", entryPoints) + eps = entryPoints + } + + entryPointsCount := 0 + for _, entryPointName := range eps { + if !contains(entryPoints, entryPointName) { + rt.AddError(fmt.Errorf("entryPoint %q doesn't exist", entryPointName), false) + logger.WithField(log.EntryPointName, entryPointName). + Errorf("entryPoint %q doesn't exist", entryPointName) + continue + } + + if _, ok := entryPointsRouters[entryPointName]; !ok { + entryPointsRouters[entryPointName] = make(map[string]*UDPRouterInfo) + } + + entryPointsCount++ + rt.Using = append(rt.Using, entryPointName) + + entryPointsRouters[entryPointName][rtName] = rt + } + + if entryPointsCount == 0 { + rt.AddError(fmt.Errorf("no valid entryPoint for this router"), true) + logger.Error("no valid entryPoint for this router") + } + } + + return entryPointsRouters +} + +// UDPRouterInfo holds information about a currently running UDP router. +type UDPRouterInfo struct { + *dynamic.UDPRouter // dynamic configuration + Err []string `json:"error,omitempty"` // initialization error + // Status reports whether the router is disabled, in a warning state, or all good (enabled). + // If not in "enabled" state, the reason for it should be in the list of Err. + // It is the caller's responsibility to set the initial status. + Status string `json:"status,omitempty"` + Using []string `json:"using,omitempty"` // Effective entry points used by that router. +} + +// AddError adds err to r.Err, if it does not already exist. +// If critical is set, r is marked as disabled. +func (r *UDPRouterInfo) AddError(err error, critical bool) { + for _, value := range r.Err { + if value == err.Error() { + return + } + } + + r.Err = append(r.Err, err.Error()) + if critical { + r.Status = StatusDisabled + return + } + + // only set it to "warning" if not already in a worse state + if r.Status != StatusDisabled { + r.Status = StatusWarning + } +} + +// UDPServiceInfo holds information about a currently running UDP service. +type UDPServiceInfo struct { + *dynamic.UDPService // dynamic configuration + Err []string `json:"error,omitempty"` // initialization error + // Status reports whether the service is disabled, in a warning state, or all good (enabled). + // If not in "enabled" state, the reason for it should be in the list of Err. + // It is the caller's responsibility to set the initial status. + Status string `json:"status,omitempty"` + UsedBy []string `json:"usedBy,omitempty"` // list of routers using that service +} + +// AddError adds err to s.Err, if it does not already exist. +// If critical is set, s is marked as disabled. +func (s *UDPServiceInfo) AddError(err error, critical bool) { + for _, value := range s.Err { + if value == err.Error() { + return + } + } + + s.Err = append(s.Err, err.Error()) + if critical { + s.Status = StatusDisabled + return + } + + // only set it to "warning" if not already in a worse state + if s.Status != StatusDisabled { + s.Status = StatusWarning + } +} diff --git a/pkg/config/runtime/runtime_udp_test.go b/pkg/config/runtime/runtime_udp_test.go new file mode 100644 index 000000000..2585dcec3 --- /dev/null +++ b/pkg/config/runtime/runtime_udp_test.go @@ -0,0 +1,201 @@ +package runtime + +import ( + "context" + "testing" + + "github.com/containous/traefik/v2/pkg/config/dynamic" + "github.com/stretchr/testify/assert" +) + +func TestGetUDPRoutersByEntryPoints(t *testing.T) { + testCases := []struct { + desc string + conf dynamic.Configuration + entryPoints []string + expected map[string]map[string]*UDPRouterInfo + }{ + { + desc: "Empty Configuration without entrypoint", + conf: dynamic.Configuration{}, + entryPoints: []string{""}, + expected: map[string]map[string]*UDPRouterInfo{}, + }, + { + desc: "Empty Configuration with unknown entrypoints", + conf: dynamic.Configuration{}, + entryPoints: []string{"foo"}, + expected: map[string]map[string]*UDPRouterInfo{}, + }, + { + desc: "Valid configuration with an unknown entrypoint", + conf: dynamic.Configuration{ + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "foo": { + EntryPoints: []string{"web"}, + Service: "foo-service@myprovider", + Rule: "Host(`bar.foo`)", + }, + }, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{ + "foo": { + EntryPoints: []string{"web"}, + Service: "foo-service@myprovider", + }, + }, + }, + }, + entryPoints: []string{"foo"}, + expected: map[string]map[string]*UDPRouterInfo{}, + }, + { + desc: "Valid configuration with a known entrypoint", + conf: dynamic.Configuration{ + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "foo": { + EntryPoints: []string{"web"}, + Service: "foo-service@myprovider", + }, + "bar": { + EntryPoints: []string{"webs"}, + Service: "bar-service@myprovider", + }, + "foobar": { + EntryPoints: []string{"web", "webs"}, + Service: "foobar-service@myprovider", + }, + }, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{ + "foo": { + EntryPoints: []string{"web"}, + Service: "foo-service@myprovider", + }, + "bar": { + EntryPoints: []string{"webs"}, + Service: "bar-service@myprovider", + }, + "foobar": { + EntryPoints: []string{"web", "webs"}, + Service: "foobar-service@myprovider", + }, + }, + }, + }, + entryPoints: []string{"web"}, + expected: map[string]map[string]*UDPRouterInfo{ + "web": { + "foo": { + UDPRouter: &dynamic.UDPRouter{ + EntryPoints: []string{"web"}, + Service: "foo-service@myprovider", + }, + Status: "enabled", + Using: []string{"web"}, + }, + "foobar": { + UDPRouter: &dynamic.UDPRouter{ + EntryPoints: []string{"web", "webs"}, + Service: "foobar-service@myprovider", + }, + Status: "warning", + Err: []string{`entryPoint "webs" doesn't exist`}, + Using: []string{"web"}, + }, + }, + }, + }, + { + desc: "Valid configuration with multiple known entrypoints", + conf: dynamic.Configuration{ + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "foo": { + EntryPoints: []string{"web"}, + Service: "foo-service@myprovider", + }, + "bar": { + EntryPoints: []string{"webs"}, + Service: "bar-service@myprovider", + }, + "foobar": { + EntryPoints: []string{"web", "webs"}, + Service: "foobar-service@myprovider", + }, + }, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{ + "foo": { + EntryPoints: []string{"web"}, + Service: "foo-service@myprovider", + }, + "bar": { + EntryPoints: []string{"webs"}, + Service: "bar-service@myprovider", + }, + "foobar": { + EntryPoints: []string{"web", "webs"}, + Service: "foobar-service@myprovider", + }, + }, + }, + }, + entryPoints: []string{"web", "webs"}, + expected: map[string]map[string]*UDPRouterInfo{ + "web": { + "foo": { + UDPRouter: &dynamic.UDPRouter{ + EntryPoints: []string{"web"}, + Service: "foo-service@myprovider", + }, + Status: "enabled", + Using: []string{"web"}, + }, + "foobar": { + UDPRouter: &dynamic.UDPRouter{ + EntryPoints: []string{"web", "webs"}, + Service: "foobar-service@myprovider", + }, + Status: "enabled", + Using: []string{"web", "webs"}, + }, + }, + "webs": { + "bar": { + UDPRouter: &dynamic.UDPRouter{ + + EntryPoints: []string{"webs"}, + Service: "bar-service@myprovider", + }, + Status: "enabled", + Using: []string{"webs"}, + }, + "foobar": { + UDPRouter: &dynamic.UDPRouter{ + EntryPoints: []string{"web", "webs"}, + Service: "foobar-service@myprovider", + }, + Status: "enabled", + Using: []string{"web", "webs"}, + }, + }, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + runtimeConfig := NewConfig(test.conf) + actual := runtimeConfig.GetUDPRoutersByEntryPoints(context.Background(), test.entryPoints) + assert.Equal(t, test.expected, actual) + }) + } +} diff --git a/pkg/config/static/entrypoints.go b/pkg/config/static/entrypoints.go index ff3088ad3..bbfd22e21 100644 --- a/pkg/config/static/entrypoints.go +++ b/pkg/config/static/entrypoints.go @@ -1,5 +1,10 @@ package static +import ( + "fmt" + "strings" +) + // EntryPoint holds the entry point configuration. type EntryPoint struct { Address string `description:"Entry point address." json:"address,omitempty" toml:"address,omitempty" yaml:"address,omitempty"` @@ -8,11 +13,34 @@ type EntryPoint struct { ForwardedHeaders *ForwardedHeaders `description:"Trust client forwarding headers." json:"forwardedHeaders,omitempty" toml:"forwardedHeaders,omitempty" yaml:"forwardedHeaders,omitempty"` } +// GetAddress strips any potential protocol part of the address field of the +// entry point, in order to return the actual address. +func (ep EntryPoint) GetAddress() string { + splitN := strings.SplitN(ep.Address, "/", 2) + return splitN[0] +} + +// GetProtocol returns the protocol part of the address field of the entry point. +// If none is specified, it defaults to "tcp". +func (ep EntryPoint) GetProtocol() (string, error) { + splitN := strings.SplitN(ep.Address, "/", 2) + if len(splitN) < 2 { + return "tcp", nil + } + + protocol := strings.ToLower(splitN[1]) + if protocol == "tcp" || protocol == "udp" { + return protocol, nil + } + + return "", fmt.Errorf("invalid protocol: %s", splitN[1]) +} + // SetDefaults sets the default values. -func (e *EntryPoint) SetDefaults() { - e.Transport = &EntryPointsTransport{} - e.Transport.SetDefaults() - e.ForwardedHeaders = &ForwardedHeaders{} +func (ep *EntryPoint) SetDefaults() { + ep.Transport = &EntryPointsTransport{} + ep.Transport.SetDefaults() + ep.ForwardedHeaders = &ForwardedHeaders{} } // ForwardedHeaders Trust client forwarding headers. diff --git a/pkg/config/static/entrypoints_test.go b/pkg/config/static/entrypoints_test.go new file mode 100644 index 000000000..866076508 --- /dev/null +++ b/pkg/config/static/entrypoints_test.go @@ -0,0 +1,67 @@ +package static + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestEntryPointProtocol(t *testing.T) { + tests := []struct { + name string + address string + expectedAddress string + expectedProtocol string + expectedError bool + }{ + { + name: "Without protocol", + address: "127.0.0.1:8080", + expectedAddress: "127.0.0.1:8080", + expectedProtocol: "tcp", + expectedError: false, + }, + { + name: "With TCP protocol in upper case", + address: "127.0.0.1:8080/TCP", + expectedAddress: "127.0.0.1:8080", + expectedProtocol: "tcp", + expectedError: false, + }, + { + name: "With UDP protocol in upper case", + address: "127.0.0.1:8080/UDP", + expectedAddress: "127.0.0.1:8080", + expectedProtocol: "udp", + expectedError: false, + }, + { + name: "With UDP protocol in weird case", + address: "127.0.0.1:8080/uDp", + expectedAddress: "127.0.0.1:8080", + expectedProtocol: "udp", + expectedError: false, + }, + + { + name: "With invalid protocol", + address: "127.0.0.1:8080/toto/tata", + expectedError: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ep := EntryPoint{ + Address: tt.address, + } + protocol, err := ep.GetProtocol() + if tt.expectedError { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Equal(t, tt.expectedProtocol, protocol) + require.Equal(t, tt.expectedAddress, ep.GetAddress()) + }) + } +} diff --git a/pkg/provider/configuration.go b/pkg/provider/configuration.go index 7146e9d3e..86ad8f449 100644 --- a/pkg/provider/configuration.go +++ b/pkg/provider/configuration.go @@ -28,6 +28,10 @@ func Merge(ctx context.Context, configurations map[string]*dynamic.Configuration Routers: make(map[string]*dynamic.TCPRouter), Services: make(map[string]*dynamic.TCPService), }, + UDP: &dynamic.UDPConfiguration{ + Routers: make(map[string]*dynamic.UDPRouter), + Services: make(map[string]*dynamic.UDPService), + }, } servicesToDelete := map[string]struct{}{} diff --git a/pkg/provider/consulcatalog/config_test.go b/pkg/provider/consulcatalog/config_test.go index d4259aa58..fce1bd88f 100644 --- a/pkg/provider/consulcatalog/config_test.go +++ b/pkg/provider/consulcatalog/config_test.go @@ -40,6 +40,10 @@ func TestDefaultRule(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Test": { @@ -84,6 +88,10 @@ func TestDefaultRule(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Test": { @@ -126,6 +134,10 @@ func TestDefaultRule(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -163,6 +175,10 @@ func TestDefaultRule(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -200,6 +216,10 @@ func TestDefaultRule(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Test": { @@ -276,6 +296,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Test": { @@ -326,6 +350,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Test": { @@ -390,6 +418,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Test": { @@ -443,6 +475,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Test": { @@ -493,6 +529,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Test": { @@ -538,6 +578,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Test": { @@ -582,6 +626,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Router1": { @@ -624,6 +672,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ @@ -667,6 +719,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Router1": { @@ -711,6 +767,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -767,6 +827,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Test": { @@ -818,6 +882,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Test": { @@ -859,6 +927,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Test": { @@ -904,6 +976,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Test": { @@ -964,6 +1040,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Test": { @@ -1027,6 +1107,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Test": { @@ -1094,6 +1178,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Test": { @@ -1153,6 +1241,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -1215,6 +1307,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -1270,6 +1366,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Router1": { @@ -1315,6 +1415,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Test": { @@ -1358,6 +1462,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Test": { @@ -1401,6 +1509,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -1446,6 +1558,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -1471,6 +1587,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -1496,6 +1616,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -1521,6 +1645,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -1548,6 +1676,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -1575,6 +1707,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Test": { @@ -1618,6 +1754,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Test": { @@ -1688,6 +1828,10 @@ func Test_buildConfiguration(t *testing.T) { }, }, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -1725,6 +1869,10 @@ func Test_buildConfiguration(t *testing.T) { }, }, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -1772,6 +1920,10 @@ func Test_buildConfiguration(t *testing.T) { }, }, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -1834,6 +1986,10 @@ func Test_buildConfiguration(t *testing.T) { }, }, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Test": { @@ -1890,6 +2046,10 @@ func Test_buildConfiguration(t *testing.T) { }, }, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -1928,6 +2088,10 @@ func Test_buildConfiguration(t *testing.T) { }, }, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, diff --git a/pkg/provider/docker/config_test.go b/pkg/provider/docker/config_test.go index e999c54c1..beeabd9b2 100644 --- a/pkg/provider/docker/config_test.go +++ b/pkg/provider/docker/config_test.go @@ -49,6 +49,10 @@ func TestDefaultRule(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Test": { @@ -98,6 +102,10 @@ func TestDefaultRule(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Test": { @@ -149,6 +157,10 @@ func TestDefaultRule(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Test": { @@ -198,6 +210,10 @@ func TestDefaultRule(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -242,6 +258,10 @@ func TestDefaultRule(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -286,6 +306,10 @@ func TestDefaultRule(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Test": { @@ -372,6 +396,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -406,6 +434,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -438,6 +470,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Test": { @@ -502,6 +538,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Test": { @@ -582,6 +622,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Test": { @@ -635,6 +679,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Test": { @@ -687,6 +735,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Router1": { @@ -737,6 +789,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ @@ -788,6 +844,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Router1": { @@ -840,6 +900,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -896,6 +960,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Router1": { @@ -966,6 +1034,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Test": { @@ -1044,6 +1116,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Test": { @@ -1103,6 +1179,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Test": { @@ -1156,6 +1236,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Test": { @@ -1235,6 +1319,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Test": { @@ -1317,6 +1405,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Test": { @@ -1409,6 +1501,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Test": { @@ -1485,6 +1581,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -1572,6 +1672,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -1643,6 +1747,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Router1": { @@ -1714,6 +1822,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -1769,6 +1881,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Test": { @@ -1820,6 +1936,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Test": { @@ -1871,6 +1991,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -1922,6 +2046,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -1954,6 +2082,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -1988,6 +2120,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -2021,6 +2157,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -2056,6 +2196,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -2091,6 +2235,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Test": { @@ -2142,6 +2290,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Test": { @@ -2220,6 +2372,10 @@ func Test_buildConfiguration(t *testing.T) { }, }, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -2265,6 +2421,10 @@ func Test_buildConfiguration(t *testing.T) { }, }, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -2320,6 +2480,10 @@ func Test_buildConfiguration(t *testing.T) { }, }, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -2399,6 +2563,10 @@ func Test_buildConfiguration(t *testing.T) { }, }, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Test": { @@ -2463,6 +2631,10 @@ func Test_buildConfiguration(t *testing.T) { }, }, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -2509,6 +2681,10 @@ func Test_buildConfiguration(t *testing.T) { }, }, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -2551,6 +2727,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Test": { diff --git a/pkg/provider/file/file.go b/pkg/provider/file/file.go index 3902ad12c..bc4a4e0c5 100644 --- a/pkg/provider/file/file.go +++ b/pkg/provider/file/file.go @@ -219,6 +219,10 @@ func (p *Provider) loadFileConfigFromDirectory(ctx context.Context, directory st Stores: make(map[string]tls.Store), Options: make(map[string]tls.Options), }, + UDP: &dynamic.UDPConfiguration{ + Routers: make(map[string]*dynamic.UDPRouter), + Services: make(map[string]*dynamic.UDPService), + }, } } @@ -288,6 +292,22 @@ func (p *Provider) loadFileConfigFromDirectory(ctx context.Context, directory st } } + for name, conf := range c.UDP.Routers { + if _, exists := configuration.UDP.Routers[name]; exists { + logger.WithField(log.RouterName, name).Warn("UDP router already configured, skipping") + } else { + configuration.UDP.Routers[name] = conf + } + } + + for name, conf := range c.UDP.Services { + if _, exists := configuration.UDP.Services[name]; exists { + logger.WithField(log.ServiceName, name).Warn("UDP service already configured, skipping") + } else { + configuration.UDP.Services[name] = conf + } + } + for _, conf := range c.TLS.Certificates { if _, exists := configTLSMaps[conf]; exists { logger.Warnf("TLS configuration %v already configured, skipping", conf) @@ -392,6 +412,10 @@ func (p *Provider) decodeConfiguration(filePath string, content string) (*dynami Stores: make(map[string]tls.Store), Options: make(map[string]tls.Options), }, + UDP: &dynamic.UDPConfiguration{ + Routers: make(map[string]*dynamic.UDPRouter), + Services: make(map[string]*dynamic.UDPService), + }, } switch strings.ToLower(filepath.Ext(filePath)) { diff --git a/pkg/provider/file/file_test.go b/pkg/provider/file/file_test.go index be401823d..b89007c1d 100644 --- a/pkg/provider/file/file_test.go +++ b/pkg/provider/file/file_test.go @@ -91,8 +91,10 @@ func TestProvideWithoutWatch(t *testing.T) { select { case conf := <-configChan: require.NotNil(t, conf.Configuration.HTTP) - assert.Len(t, conf.Configuration.HTTP.Services, test.expectedNumService) - assert.Len(t, conf.Configuration.HTTP.Routers, test.expectedNumRouter) + numServices := len(conf.Configuration.HTTP.Services) + len(conf.Configuration.TCP.Services) + len(conf.Configuration.UDP.Services) + numRouters := len(conf.Configuration.HTTP.Routers) + len(conf.Configuration.TCP.Routers) + len(conf.Configuration.UDP.Routers) + assert.Equal(t, numServices, test.expectedNumService) + assert.Equal(t, numRouters, test.expectedNumRouter) require.NotNil(t, conf.Configuration.TLS) assert.Len(t, conf.Configuration.TLS.Certificates, test.expectedNumTLSConf) assert.Len(t, conf.Configuration.TLS.Options, test.expectedNumTLSOptions) @@ -119,8 +121,10 @@ func TestProvideWithWatch(t *testing.T) { select { case conf := <-configChan: require.NotNil(t, conf.Configuration.HTTP) - assert.Len(t, conf.Configuration.HTTP.Services, 0) - assert.Len(t, conf.Configuration.HTTP.Routers, 0) + numServices := len(conf.Configuration.HTTP.Services) + len(conf.Configuration.TCP.Services) + len(conf.Configuration.UDP.Services) + numRouters := len(conf.Configuration.HTTP.Routers) + len(conf.Configuration.TCP.Routers) + len(conf.Configuration.UDP.Routers) + assert.Equal(t, numServices, 0) + assert.Equal(t, numRouters, 0) require.NotNil(t, conf.Configuration.TLS) assert.Len(t, conf.Configuration.TLS.Certificates, 0) case <-timeout: @@ -145,8 +149,8 @@ func TestProvideWithWatch(t *testing.T) { select { case conf := <-configChan: numUpdates++ - numServices = len(conf.Configuration.HTTP.Services) - numRouters = len(conf.Configuration.HTTP.Routers) + numServices = len(conf.Configuration.HTTP.Services) + len(conf.Configuration.TCP.Services) + len(conf.Configuration.UDP.Services) + numRouters = len(conf.Configuration.HTTP.Routers) + len(conf.Configuration.TCP.Routers) + len(conf.Configuration.UDP.Routers) numTLSConfs = len(conf.Configuration.TLS.Certificates) t.Logf("received update #%d: services %d/%d, routers %d/%d, TLS configs %d/%d", numUpdates, numServices, test.expectedNumService, numRouters, test.expectedNumRouter, numTLSConfs, test.expectedNumTLSConf) @@ -170,6 +174,13 @@ func getTestCases() []ProvideTestCase { expectedNumService: 6, expectedNumTLSConf: 5, }, + { + desc: "simple file with tcp and udp", + filePath: "./fixtures/toml/simple_file_02.toml", + expectedNumRouter: 5, + expectedNumService: 8, + expectedNumTLSConf: 5, + }, { desc: "simple file yaml", filePath: "./fixtures/yaml/simple_file_01.yml", diff --git a/pkg/provider/file/fixtures/toml/simple_file_02.toml b/pkg/provider/file/fixtures/toml/simple_file_02.toml index 947d3d64d..b7ef74001 100644 --- a/pkg/provider/file/fixtures/toml/simple_file_02.toml +++ b/pkg/provider/file/fixtures/toml/simple_file_02.toml @@ -9,9 +9,6 @@ [http.routers."router3"] service = "application-3" - [http.routers."router4"] - service = "application-4" - [http.services] [http.services.application-1.loadBalancer] @@ -19,8 +16,8 @@ url = "http://172.17.0.1:80" [http.services.application-2.loadBalancer] - [[http.services.application-2.loadBalancer.servers]] - url = "http://172.17.0.2:80" + [[http.services.application-2.loadBalancer.servers]] + url = "http://172.17.0.2:80" [http.services.application-3.loadBalancer] [[http.services.application-3.loadBalancer.servers]] @@ -38,28 +35,46 @@ [[http.services.application-6.loadBalancer.servers]] url = "http://172.17.0.6:80" - [http.services.application-7.loadBalancer] - [[http.services.application-7.loadBalancer.servers]] - url = "http://172.17.0.7:80" - - [http.services.application-8.loadBalancer] - [[http.services.application-8.loadBalancer.servers]] - url = "http://172.17.0.8:80" - [tls] -[[tls.certificates]] - certFile = "integration/fixtures/https/snitest1.com.cert" - keyFile = "integration/fixtures/https/snitest1.com.key" + [[tls.certificates]] + certFile = "integration/fixtures/https/snitest1.com.cert" + keyFile = "integration/fixtures/https/snitest1.com.key" -[[tls.certificates]] - certFile = "integration/fixtures/https/snitest2.com.cert" - keyFile = "integration/fixtures/https/snitest2.com.key" + [[tls.certificates]] + certFile = "integration/fixtures/https/snitest2.com.cert" + keyFile = "integration/fixtures/https/snitest2.com.key" -[[tls.certificates]] - certFile = "integration/fixtures/https/snitest3.com.cert" - keyFile = "integration/fixtures/https/snitest3.com.key" + [[tls.certificates]] + certFile = "integration/fixtures/https/snitest3.com.cert" + keyFile = "integration/fixtures/https/snitest3.com.key" -[[tls.certificates]] - certFile = "integration/fixtures/https/snitest4.com.cert" - keyFile = "integration/fixtures/https/snitest4.com.key" + [[tls.certificates]] + certFile = "integration/fixtures/https/snitest4.com.cert" + keyFile = "integration/fixtures/https/snitest4.com.key" + + [[tls.certificates]] + certFile = "integration/fixtures/https/snitest5.com.cert" + keyFile = "integration/fixtures/https/snitest5.com.key" + +[tcp.routers] + + [tcp.routers."routertcp1"] + service = "applicationtcp-1" + +[tcp.services] + + [tcp.services.applicationtcp-1.loadBalancer] + [[tcp.services.applicationtcp-1.loadBalancer.servers]] + url = "http://172.17.0.9:80" + +[udp.routers] + + [udp.routers."routerudp1"] + service = "applicationudp-1" + +[udp.services] + + [udp.services.applicationudp-1.loadBalancer] + [[udp.services.applicationudp-1.loadBalancer.servers]] + url = "http://172.17.0.10:80" diff --git a/pkg/provider/file/fixtures/yaml/simple_file_02.yml b/pkg/provider/file/fixtures/yaml/simple_file_02.yml deleted file mode 100644 index db147efd4..000000000 --- a/pkg/provider/file/fixtures/yaml/simple_file_02.yml +++ /dev/null @@ -1,53 +0,0 @@ -http: - routers: - router1: - service: application-1 - router2: - service: application-2 - router3: - service: application-3 - router4: - service: application-4 - services: - application-1: - loadBalancer: - servers: - - url: 'http://172.17.0.1:80' - application-2: - loadBalancer: - servers: - - url: 'http://172.17.0.2:80' - application-3: - loadBalancer: - servers: - - url: 'http://172.17.0.3:80' - application-4: - loadBalancer: - servers: - - url: 'http://172.17.0.4:80' - application-5: - loadBalancer: - servers: - - url: 'http://172.17.0.5:80' - application-6: - loadBalancer: - servers: - - url: 'http://172.17.0.6:80' - application-7: - loadBalancer: - servers: - - url: 'http://172.17.0.7:80' - application-8: - loadBalancer: - servers: - - url: 'http://172.17.0.8:80' -tls: - certificates: - - certFile: integration/fixtures/https/snitest1.com.cert - keyFile: integration/fixtures/https/snitest1.com.key - - certFile: integration/fixtures/https/snitest2.com.cert - keyFile: integration/fixtures/https/snitest2.com.key - - certFile: integration/fixtures/https/snitest3.com.cert - keyFile: integration/fixtures/https/snitest3.com.key - - certFile: integration/fixtures/https/snitest4.com.cert - keyFile: integration/fixtures/https/snitest4.com.key diff --git a/pkg/provider/marathon/config_test.go b/pkg/provider/marathon/config_test.go index e0da93e6b..f6d93635b 100644 --- a/pkg/provider/marathon/config_test.go +++ b/pkg/provider/marathon/config_test.go @@ -50,6 +50,10 @@ func TestBuildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "app": { @@ -84,6 +88,10 @@ func TestBuildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -104,6 +112,10 @@ func TestBuildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "app": { @@ -140,6 +152,10 @@ func TestBuildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "app": { @@ -194,6 +210,10 @@ func TestBuildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Router1": { @@ -243,6 +263,10 @@ func TestBuildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Router1": { @@ -290,6 +314,10 @@ func TestBuildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "foo": { @@ -336,6 +364,10 @@ func TestBuildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "app": { @@ -376,6 +408,10 @@ func TestBuildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "app": { @@ -413,6 +449,10 @@ func TestBuildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Router1": { @@ -450,6 +490,10 @@ func TestBuildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ @@ -488,6 +532,10 @@ func TestBuildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Router1": { @@ -527,6 +575,10 @@ func TestBuildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -575,6 +627,10 @@ func TestBuildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "app": { @@ -611,6 +667,10 @@ func TestBuildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "app": { @@ -677,6 +737,10 @@ func TestBuildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "app": { @@ -734,6 +798,10 @@ func TestBuildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -784,6 +852,10 @@ func TestBuildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Router1": { @@ -830,6 +902,10 @@ func TestBuildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -872,6 +948,10 @@ func TestBuildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "app": { @@ -910,6 +990,10 @@ func TestBuildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "app": { @@ -948,6 +1032,10 @@ func TestBuildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -989,6 +1077,10 @@ func TestBuildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -1010,26 +1102,9 @@ func TestBuildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, - HTTP: &dynamic.HTTPConfiguration{ - Routers: map[string]*dynamic.Router{}, - Middlewares: map[string]*dynamic.Middleware{}, - Services: map[string]*dynamic.Service{}, - }, - }, - }, - { - desc: "one app with traefik.enable=false", - applications: withApplications( - application( - appID("/app"), - appPorts(80, 81), - withTasks(localhostTask()), - withLabel("traefik.enable", "false"), - )), - expected: &dynamic.Configuration{ - TCP: &dynamic.TCPConfiguration{ - Routers: map[string]*dynamic.TCPRouter{}, - Services: map[string]*dynamic.TCPService{}, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, @@ -1052,6 +1127,35 @@ func TestBuildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + }, + }, + }, + { + desc: "one app with traefik.enable=false", + applications: withApplications( + application( + appID("/app"), + appPorts(80, 81), + withTasks(localhostTask()), + withLabel("traefik.enable", "false"), + )), + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Services: map[string]*dynamic.TCPService{}, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -1074,6 +1178,10 @@ func TestBuildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -1096,6 +1204,10 @@ func TestBuildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -1118,6 +1230,10 @@ func TestBuildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "app": { @@ -1156,6 +1272,10 @@ func TestBuildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "app": { @@ -1193,6 +1313,10 @@ func TestBuildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "a_b_app": { @@ -1248,6 +1372,10 @@ func TestBuildConfiguration(t *testing.T) { }, }, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -1280,6 +1408,10 @@ func TestBuildConfiguration(t *testing.T) { }, }, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -1320,6 +1452,10 @@ func TestBuildConfiguration(t *testing.T) { }, }, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -1361,6 +1497,10 @@ func TestBuildConfiguration(t *testing.T) { }, }, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -1402,6 +1542,10 @@ func TestBuildConfiguration(t *testing.T) { }, }, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "app": { diff --git a/pkg/provider/rancher/config_test.go b/pkg/provider/rancher/config_test.go index aa20e87f0..c1b3f0a84 100644 --- a/pkg/provider/rancher/config_test.go +++ b/pkg/provider/rancher/config_test.go @@ -36,6 +36,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Test": { @@ -84,6 +88,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Test1": { @@ -146,6 +154,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Test1": { @@ -207,6 +219,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Router1": { @@ -246,6 +262,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -269,6 +289,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -295,6 +319,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Router1": { @@ -338,6 +366,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -365,6 +397,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Test": { @@ -408,6 +444,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Test": { @@ -460,6 +500,10 @@ func Test_buildConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Services: map[string]*dynamic.TCPService{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Test": { @@ -520,6 +564,10 @@ func Test_buildConfiguration(t *testing.T) { }, }, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -557,6 +605,10 @@ func Test_buildConfiguration(t *testing.T) { }, }, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -600,6 +652,10 @@ func Test_buildConfiguration(t *testing.T) { }, }, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -649,6 +705,10 @@ func Test_buildConfiguration(t *testing.T) { }, }, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "Test": { @@ -705,6 +765,10 @@ func Test_buildConfiguration(t *testing.T) { }, }, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, @@ -743,6 +807,10 @@ func Test_buildConfiguration(t *testing.T) { }, }, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, Middlewares: map[string]*dynamic.Middleware{}, diff --git a/pkg/server/aggregator.go b/pkg/server/aggregator.go index f6d745dc8..41142d6be 100644 --- a/pkg/server/aggregator.go +++ b/pkg/server/aggregator.go @@ -18,6 +18,10 @@ func mergeConfiguration(configurations dynamic.Configurations) dynamic.Configura Routers: make(map[string]*dynamic.TCPRouter), Services: make(map[string]*dynamic.TCPService), }, + UDP: &dynamic.UDPConfiguration{ + Routers: make(map[string]*dynamic.UDPRouter), + Services: make(map[string]*dynamic.UDPService), + }, TLS: &dynamic.TLSConfiguration{ Stores: make(map[string]tls.Store), Options: make(map[string]tls.Options), @@ -47,6 +51,15 @@ func mergeConfiguration(configurations dynamic.Configurations) dynamic.Configura } } + if configuration.UDP != nil { + for routerName, router := range configuration.UDP.Routers { + conf.UDP.Routers[provider.MakeQualifiedName(pvd, routerName)] = router + } + for serviceName, service := range configuration.UDP.Services { + conf.UDP.Services[provider.MakeQualifiedName(pvd, serviceName)] = service + } + } + if configuration.TLS != nil { conf.TLS.Certificates = append(conf.TLS.Certificates, configuration.TLS.Certificates...) diff --git a/pkg/server/configurationwatcher_test.go b/pkg/server/configurationwatcher_test.go index 1e502c615..954fcfdd1 100644 --- a/pkg/server/configurationwatcher_test.go +++ b/pkg/server/configurationwatcher_test.go @@ -79,6 +79,10 @@ func TestNewConfigurationWatcher(t *testing.T) { }, Stores: map[string]tls.Store{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, } assert.Equal(t, expected, conf) @@ -222,6 +226,10 @@ func TestListenProvidersPublishesConfigForEachProvider(t *testing.T) { }, Stores: map[string]tls.Store{}, }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, } assert.Equal(t, expected, publishedProviderConfig) diff --git a/pkg/server/provider/provider_test.go b/pkg/server/provider/provider_test.go index b6673e055..dc229e532 100644 --- a/pkg/server/provider/provider_test.go +++ b/pkg/server/provider/provider_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestAddProviderInContext(t *testing.T) { +func TestAddInContext(t *testing.T) { testCases := []struct { desc string ctx context.Context diff --git a/pkg/server/router/udp/router.go b/pkg/server/router/udp/router.go new file mode 100644 index 000000000..873279956 --- /dev/null +++ b/pkg/server/router/udp/router.go @@ -0,0 +1,87 @@ +package udp + +import ( + "context" + "errors" + + "github.com/containous/traefik/v2/pkg/config/runtime" + "github.com/containous/traefik/v2/pkg/log" + "github.com/containous/traefik/v2/pkg/server/provider" + udpservice "github.com/containous/traefik/v2/pkg/server/service/udp" + "github.com/containous/traefik/v2/pkg/udp" +) + +// NewManager Creates a new Manager +func NewManager(conf *runtime.Configuration, + serviceManager *udpservice.Manager, +) *Manager { + return &Manager{ + serviceManager: serviceManager, + conf: conf, + } +} + +// Manager is a route/router manager +type Manager struct { + serviceManager *udpservice.Manager + conf *runtime.Configuration +} + +func (m *Manager) getUDPRouters(ctx context.Context, entryPoints []string) map[string]map[string]*runtime.UDPRouterInfo { + if m.conf != nil { + return m.conf.GetUDPRoutersByEntryPoints(ctx, entryPoints) + } + + return make(map[string]map[string]*runtime.UDPRouterInfo) +} + +// BuildHandlers builds the handlers for the given entrypoints +func (m *Manager) BuildHandlers(rootCtx context.Context, entryPoints []string) map[string]udp.Handler { + entryPointsRouters := m.getUDPRouters(rootCtx, entryPoints) + + entryPointHandlers := make(map[string]udp.Handler) + for _, entryPointName := range entryPoints { + entryPointName := entryPointName + + routers := entryPointsRouters[entryPointName] + + ctx := log.With(rootCtx, log.Str(log.EntryPointName, entryPointName)) + + handler, err := m.buildEntryPointHandler(ctx, routers) + if err != nil { + log.FromContext(ctx).Error(err) + continue + } + entryPointHandlers[entryPointName] = handler + } + return entryPointHandlers +} + +func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string]*runtime.UDPRouterInfo) (udp.Handler, error) { + logger := log.FromContext(ctx) + + if len(configs) > 1 { + logger.Warn("Warning: config has more than one udp router for a given entrypoint") + } + for routerName, routerConfig := range configs { + ctxRouter := log.With(provider.AddInContext(ctx, routerName), log.Str(log.RouterName, routerName)) + logger := log.FromContext(ctxRouter) + + if routerConfig.Service == "" { + err := errors.New("the service is missing on the udp router") + routerConfig.AddError(err, true) + logger.Error(err) + continue + } + + handler, err := m.serviceManager.BuildUDP(ctxRouter, routerConfig.Service) + if err != nil { + routerConfig.AddError(err, true) + logger.Error(err) + continue + } + return handler, nil + } + + return nil, nil +} diff --git a/pkg/server/router/udp/router_test.go b/pkg/server/router/udp/router_test.go new file mode 100644 index 000000000..fb49aa6ef --- /dev/null +++ b/pkg/server/router/udp/router_test.go @@ -0,0 +1,144 @@ +package udp + +import ( + "context" + "testing" + + "github.com/containous/traefik/v2/pkg/config/dynamic" + "github.com/containous/traefik/v2/pkg/config/runtime" + "github.com/containous/traefik/v2/pkg/server/service/udp" + "github.com/stretchr/testify/assert" +) + +func TestRuntimeConfiguration(t *testing.T) { + testCases := []struct { + desc string + serviceConfig map[string]*runtime.UDPServiceInfo + routerConfig map[string]*runtime.UDPRouterInfo + expectedError int + }{ + { + desc: "No error", + serviceConfig: map[string]*runtime.UDPServiceInfo{ + "foo-service": { + UDPService: &dynamic.UDPService{ + LoadBalancer: &dynamic.UDPServersLoadBalancer{ + Servers: []dynamic.UDPServer{ + { + Port: "8085", + Address: "127.0.0.1:8085", + }, + { + Address: "127.0.0.1:8086", + Port: "8086", + }, + }, + }, + }, + }, + }, + routerConfig: map[string]*runtime.UDPRouterInfo{ + "foo": { + UDPRouter: &dynamic.UDPRouter{ + EntryPoints: []string{"web"}, + Service: "foo-service", + }, + }, + "bar": { + UDPRouter: &dynamic.UDPRouter{ + + EntryPoints: []string{"web"}, + Service: "foo-service", + }, + }, + }, + expectedError: 0, + }, + { + desc: "Router with unknown service", + serviceConfig: map[string]*runtime.UDPServiceInfo{ + "foo-service": { + UDPService: &dynamic.UDPService{ + LoadBalancer: &dynamic.UDPServersLoadBalancer{ + Servers: []dynamic.UDPServer{ + { + Address: "127.0.0.1:80", + }, + }, + }, + }, + }, + }, + routerConfig: map[string]*runtime.UDPRouterInfo{ + "foo": { + UDPRouter: &dynamic.UDPRouter{ + EntryPoints: []string{"web"}, + Service: "wrong-service", + }, + }, + "bar": { + UDPRouter: &dynamic.UDPRouter{ + + EntryPoints: []string{"web"}, + Service: "foo-service", + }, + }, + }, + expectedError: 1, + }, + { + desc: "Router with broken service", + serviceConfig: map[string]*runtime.UDPServiceInfo{ + "foo-service": { + UDPService: &dynamic.UDPService{ + LoadBalancer: nil, + }, + }, + }, + routerConfig: map[string]*runtime.UDPRouterInfo{ + "bar": { + UDPRouter: &dynamic.UDPRouter{ + EntryPoints: []string{"web"}, + Service: "foo-service", + }, + }, + }, + expectedError: 2, + }, + } + + for _, test := range testCases { + test := test + + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + entryPoints := []string{"web"} + + conf := &runtime.Configuration{ + UDPServices: test.serviceConfig, + UDPRouters: test.routerConfig, + } + serviceManager := udp.NewManager(conf) + routerManager := NewManager(conf, serviceManager) + + _ = routerManager.BuildHandlers(context.Background(), entryPoints) + + // even though conf was passed by argument to the manager builders above, + // it's ok to use it as the result we check, because everything worth checking + // can be accessed by pointers in it. + var allErrors int + for _, v := range conf.UDPServices { + if v.Err != nil { + allErrors++ + } + } + for _, v := range conf.UDPRouters { + if len(v.Err) > 0 { + allErrors++ + } + } + assert.Equal(t, test.expectedError, allErrors) + }) + } +} diff --git a/pkg/server/routerfactory.go b/pkg/server/routerfactory.go new file mode 100644 index 000000000..3ef3715c0 --- /dev/null +++ b/pkg/server/routerfactory.go @@ -0,0 +1,91 @@ +package server + +import ( + "context" + + "github.com/containous/traefik/v2/pkg/config/dynamic" + "github.com/containous/traefik/v2/pkg/config/runtime" + "github.com/containous/traefik/v2/pkg/config/static" + "github.com/containous/traefik/v2/pkg/log" + "github.com/containous/traefik/v2/pkg/responsemodifiers" + "github.com/containous/traefik/v2/pkg/server/middleware" + "github.com/containous/traefik/v2/pkg/server/router" + routertcp "github.com/containous/traefik/v2/pkg/server/router/tcp" + routerudp "github.com/containous/traefik/v2/pkg/server/router/udp" + "github.com/containous/traefik/v2/pkg/server/service" + "github.com/containous/traefik/v2/pkg/server/service/tcp" + "github.com/containous/traefik/v2/pkg/server/service/udp" + tcpCore "github.com/containous/traefik/v2/pkg/tcp" + "github.com/containous/traefik/v2/pkg/tls" + udpCore "github.com/containous/traefik/v2/pkg/udp" +) + +// RouterFactory the factory of TCP/UDP routers. +type RouterFactory struct { + entryPointsTCP []string + entryPointsUDP []string + + managerFactory *service.ManagerFactory + + chainBuilder *middleware.ChainBuilder + tlsManager *tls.Manager +} + +// NewRouterFactory creates a new RouterFactory +func NewRouterFactory(staticConfiguration static.Configuration, managerFactory *service.ManagerFactory, tlsManager *tls.Manager, chainBuilder *middleware.ChainBuilder) *RouterFactory { + var entryPointsTCP, entryPointsUDP []string + for name, cfg := range staticConfiguration.EntryPoints { + protocol, err := cfg.GetProtocol() + if err != nil { + // Should never happen because Traefik should not start if protocol is invalid. + log.WithoutContext().Errorf("Invalid protocol: %v", err) + } + + if protocol == "udp" { + entryPointsUDP = append(entryPointsUDP, name) + } else { + entryPointsTCP = append(entryPointsTCP, name) + } + } + + return &RouterFactory{ + entryPointsTCP: entryPointsTCP, + entryPointsUDP: entryPointsUDP, + managerFactory: managerFactory, + tlsManager: tlsManager, + chainBuilder: chainBuilder, + } +} + +// CreateRouters creates new TCPRouters and UDPRouters +func (f *RouterFactory) CreateRouters(conf dynamic.Configuration) (map[string]*tcpCore.Router, map[string]udpCore.Handler) { + ctx := context.Background() + + rtConf := runtime.NewConfig(conf) + + // HTTP + serviceManager := f.managerFactory.Build(rtConf) + + middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager) + responseModifierFactory := responsemodifiers.NewBuilder(rtConf.Middlewares) + + routerManager := router.NewManager(rtConf, serviceManager, middlewaresBuilder, responseModifierFactory, f.chainBuilder) + + handlersNonTLS := routerManager.BuildHandlers(ctx, f.entryPointsTCP, false) + handlersTLS := routerManager.BuildHandlers(ctx, f.entryPointsTCP, true) + + // TCP + svcTCPManager := tcp.NewManager(rtConf) + + rtTCPManager := routertcp.NewManager(rtConf, svcTCPManager, handlersNonTLS, handlersTLS, f.tlsManager) + routersTCP := rtTCPManager.BuildHandlers(ctx, f.entryPointsTCP) + + // UDP + svcUDPManager := udp.NewManager(rtConf) + rtUDPManager := routerudp.NewManager(rtConf, svcUDPManager) + routersUDP := rtUDPManager.BuildHandlers(ctx, f.entryPointsUDP) + + rtConf.PopulateUsedBy() + + return routersTCP, routersUDP +} diff --git a/pkg/server/tcprouterfactory_test.go b/pkg/server/routerfactory_test.go similarity index 91% rename from pkg/server/tcprouterfactory_test.go rename to pkg/server/routerfactory_test.go index 115fa84f7..acabdd94a 100644 --- a/pkg/server/tcprouterfactory_test.go +++ b/pkg/server/routerfactory_test.go @@ -49,9 +49,9 @@ func TestReuseService(t *testing.T) { managerFactory := service.NewManagerFactory(staticConfig, nil, metrics.NewVoidRegistry()) tlsManager := tls.NewManager() - factory := NewTCPRouterFactory(staticConfig, managerFactory, tlsManager, middleware.NewChainBuilder(staticConfig, metrics.NewVoidRegistry(), nil)) + factory := NewRouterFactory(staticConfig, managerFactory, tlsManager, middleware.NewChainBuilder(staticConfig, metrics.NewVoidRegistry(), nil)) - entryPointsHandlers := factory.CreateTCPRouters(dynamic.Configuration{HTTP: dynamicConfigs}) + entryPointsHandlers, _ := factory.CreateRouters(dynamic.Configuration{HTTP: dynamicConfigs}) // Test that the /ok path returns a status 200. responseRecorderOk := &httptest.ResponseRecorder{} @@ -183,9 +183,9 @@ func TestServerResponseEmptyBackend(t *testing.T) { managerFactory := service.NewManagerFactory(staticConfig, nil, metrics.NewVoidRegistry()) tlsManager := tls.NewManager() - factory := NewTCPRouterFactory(staticConfig, managerFactory, tlsManager, middleware.NewChainBuilder(staticConfig, metrics.NewVoidRegistry(), nil)) + factory := NewRouterFactory(staticConfig, managerFactory, tlsManager, middleware.NewChainBuilder(staticConfig, metrics.NewVoidRegistry(), nil)) - entryPointsHandlers := factory.CreateTCPRouters(dynamic.Configuration{HTTP: test.config(testServer.URL)}) + entryPointsHandlers, _ := factory.CreateRouters(dynamic.Configuration{HTTP: test.config(testServer.URL)}) responseRecorder := &httptest.ResponseRecorder{} request := httptest.NewRequest(http.MethodGet, testServer.URL+requestPath, nil) @@ -221,9 +221,9 @@ func TestInternalServices(t *testing.T) { managerFactory := service.NewManagerFactory(staticConfig, nil, metrics.NewVoidRegistry()) tlsManager := tls.NewManager() - factory := NewTCPRouterFactory(staticConfig, managerFactory, tlsManager, middleware.NewChainBuilder(staticConfig, metrics.NewVoidRegistry(), nil)) + factory := NewRouterFactory(staticConfig, managerFactory, tlsManager, middleware.NewChainBuilder(staticConfig, metrics.NewVoidRegistry(), nil)) - entryPointsHandlers := factory.CreateTCPRouters(dynamic.Configuration{HTTP: dynamicConfigs}) + entryPointsHandlers, _ := factory.CreateRouters(dynamic.Configuration{HTTP: dynamicConfigs}) // Test that the /ok path returns a status 200. responseRecorderOk := &httptest.ResponseRecorder{} diff --git a/pkg/server/server.go b/pkg/server/server.go index 99950a62d..2378f79ca 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -17,6 +17,7 @@ import ( type Server struct { watcher *ConfigurationWatcher tcpEntryPoints TCPEntryPoints + udpEntryPoints UDPEntryPoints chainBuilder *middleware.ChainBuilder accessLoggerMiddleware *accesslog.Handler @@ -28,7 +29,7 @@ type Server struct { } // NewServer returns an initialized Server. -func NewServer(routinesPool *safe.Pool, entryPoints TCPEntryPoints, watcher *ConfigurationWatcher, +func NewServer(routinesPool *safe.Pool, entryPoints TCPEntryPoints, entryPointsUDP UDPEntryPoints, watcher *ConfigurationWatcher, chainBuilder *middleware.ChainBuilder, accessLoggerMiddleware *accesslog.Handler) *Server { srv := &Server{ watcher: watcher, @@ -38,6 +39,7 @@ func NewServer(routinesPool *safe.Pool, entryPoints TCPEntryPoints, watcher *Con signals: make(chan os.Signal, 1), stopChan: make(chan bool, 1), routinesPool: routinesPool, + udpEntryPoints: entryPointsUDP, } srv.configureSignals() @@ -56,6 +58,7 @@ func (s *Server) Start(ctx context.Context) { }() s.tcpEntryPoints.Start() + s.udpEntryPoints.Start() s.watcher.Start() s.routinesPool.GoCtx(s.listenSignals) @@ -71,6 +74,7 @@ func (s *Server) Stop() { defer log.WithoutContext().Info("Server stopped") s.tcpEntryPoints.Stop() + s.udpEntryPoints.Stop() s.stopChan <- true } diff --git a/pkg/server/server_entrypoint_tcp.go b/pkg/server/server_entrypoint_tcp.go index af0104eb8..6100396f9 100644 --- a/pkg/server/server_entrypoint_tcp.go +++ b/pkg/server/server_entrypoint_tcp.go @@ -55,9 +55,17 @@ type TCPEntryPoints map[string]*TCPEntryPoint func NewTCPEntryPoints(entryPointsConfig static.EntryPoints) (TCPEntryPoints, error) { serverEntryPointsTCP := make(TCPEntryPoints) for entryPointName, config := range entryPointsConfig { + protocol, err := config.GetProtocol() + if err != nil { + return nil, fmt.Errorf("error while building entryPoint %s: %v", entryPointName, err) + } + + if protocol != "tcp" { + continue + } + ctx := log.With(context.Background(), log.Str(log.EntryPointName, entryPointName)) - var err error serverEntryPointsTCP[entryPointName], err = NewTCPEntryPoint(ctx, config) if err != nil { return nil, fmt.Errorf("error while building entryPoint %s: %v", entryPointName, err) @@ -70,7 +78,7 @@ func NewTCPEntryPoints(entryPointsConfig static.EntryPoints) (TCPEntryPoints, er func (eps TCPEntryPoints) Start() { for entryPointName, serverEntryPoint := range eps { ctx := log.With(context.Background(), log.Str(log.EntryPointName, entryPointName)) - go serverEntryPoint.StartTCP(ctx) + go serverEntryPoint.Start(ctx) } } @@ -149,8 +157,8 @@ func NewTCPEntryPoint(ctx context.Context, configuration *static.EntryPoint) (*T }, nil } -// StartTCP starts the TCP server. -func (e *TCPEntryPoint) StartTCP(ctx context.Context) { +// Start starts the TCP server. +func (e *TCPEntryPoint) Start(ctx context.Context) { logger := log.FromContext(ctx) logger.Debugf("Start TCP Server") @@ -370,7 +378,7 @@ func buildProxyProtocolListener(ctx context.Context, entryPoint *static.EntryPoi } func buildListener(ctx context.Context, entryPoint *static.EntryPoint) (net.Listener, error) { - listener, err := net.Listen("tcp", entryPoint.Address) + listener, err := net.Listen("tcp", entryPoint.GetAddress()) if err != nil { return nil, fmt.Errorf("error opening listener: %v", err) diff --git a/pkg/server/server_entrypoint_tcp_test.go b/pkg/server/server_entrypoint_tcp_test.go index 6a0f75c08..8da063281 100644 --- a/pkg/server/server_entrypoint_tcp_test.go +++ b/pkg/server/server_entrypoint_tcp_test.go @@ -125,7 +125,7 @@ func testShutdown(t *testing.T, router *tcp.Router) { } func startEntrypoint(entryPoint *TCPEntryPoint, router *tcp.Router) (net.Conn, error) { - go entryPoint.StartTCP(context.Background()) + go entryPoint.Start(context.Background()) entryPoint.SwitchRouter(router) diff --git a/pkg/server/server_entrypoint_udp.go b/pkg/server/server_entrypoint_udp.go new file mode 100644 index 000000000..7181cc3aa --- /dev/null +++ b/pkg/server/server_entrypoint_udp.go @@ -0,0 +1,135 @@ +package server + +import ( + "context" + "fmt" + "net" + "sync" + "time" + + "github.com/containous/traefik/v2/pkg/config/static" + "github.com/containous/traefik/v2/pkg/log" + "github.com/containous/traefik/v2/pkg/udp" +) + +// UDPEntryPoints maps UDP entry points by their names. +type UDPEntryPoints map[string]*UDPEntryPoint + +// NewUDPEntryPoints returns all the UDP entry points, keyed by name. +func NewUDPEntryPoints(cfg static.EntryPoints) (UDPEntryPoints, error) { + entryPoints := make(UDPEntryPoints) + for entryPointName, entryPoint := range cfg { + protocol, err := entryPoint.GetProtocol() + if err != nil { + return nil, fmt.Errorf("error while building entryPoint %s: %v", entryPointName, err) + } + + if protocol != "udp" { + continue + } + + ep, err := NewUDPEntryPoint(entryPoint) + if err != nil { + return nil, fmt.Errorf("error while building entryPoint %s: %v", entryPointName, err) + } + entryPoints[entryPointName] = ep + } + return entryPoints, nil +} + +// Start commences the listening for all the entry points. +func (eps UDPEntryPoints) Start() { + for entryPointName, ep := range eps { + ctx := log.With(context.Background(), log.Str(log.EntryPointName, entryPointName)) + go ep.Start(ctx) + } +} + +// Stop makes all the entry points stop listening, and release associated resources. +func (eps UDPEntryPoints) Stop() { + var wg sync.WaitGroup + + for epn, ep := range eps { + wg.Add(1) + + go func(entryPointName string, entryPoint *UDPEntryPoint) { + defer wg.Done() + + ctx := log.With(context.Background(), log.Str(log.EntryPointName, entryPointName)) + entryPoint.Shutdown(ctx) + + log.FromContext(ctx).Debugf("Entry point %s closed", entryPointName) + }(epn, ep) + } + + wg.Wait() +} + +// Switch swaps out all the given handlers in their associated entrypoints. +func (eps UDPEntryPoints) Switch(handlers map[string]udp.Handler) { + for epName, handler := range handlers { + if ep, ok := eps[epName]; ok { + ep.Switch(handler) + continue + } + log.WithoutContext().Errorf("EntryPoint %q does not exist", epName) + } +} + +// UDPEntryPoint is an entry point where we listen for UDP packets. +type UDPEntryPoint struct { + listener *udp.Listener + switcher *udp.HandlerSwitcher + transportConfiguration *static.EntryPointsTransport +} + +// NewUDPEntryPoint returns a UDP entry point. +func NewUDPEntryPoint(cfg *static.EntryPoint) (*UDPEntryPoint, error) { + addr, err := net.ResolveUDPAddr("udp", cfg.GetAddress()) + if err != nil { + return nil, err + } + listener, err := udp.Listen("udp", addr) + if err != nil { + return nil, err + } + + return &UDPEntryPoint{listener: listener, switcher: &udp.HandlerSwitcher{}, transportConfiguration: cfg.Transport}, nil +} + +// Start commences the listening for ep. +func (ep *UDPEntryPoint) Start(ctx context.Context) { + log.FromContext(ctx).Debug("Start UDP Server") + for { + conn, err := ep.listener.Accept() + if err != nil { + // Only errClosedListener can happen that's why we return + return + } + + go ep.switcher.ServeUDP(conn) + } +} + +// Shutdown closes ep's listener. It eventually closes all "sessions" and +// releases associated resources, but only after it has waited for a graceTimeout, +// if any was configured. +func (ep *UDPEntryPoint) Shutdown(ctx context.Context) { + logger := log.FromContext(ctx) + + reqAcceptGraceTimeOut := time.Duration(ep.transportConfiguration.LifeCycle.RequestAcceptGraceTimeout) + if reqAcceptGraceTimeOut > 0 { + logger.Infof("Waiting %s for incoming requests to cease", reqAcceptGraceTimeOut) + time.Sleep(reqAcceptGraceTimeOut) + } + + graceTimeOut := time.Duration(ep.transportConfiguration.LifeCycle.GraceTimeOut) + if err := ep.listener.Shutdown(graceTimeOut); err != nil { + logger.Error(err) + } +} + +// Switch replaces ep's handler with the one given as argument. +func (ep *UDPEntryPoint) Switch(handler udp.Handler) { + ep.switcher.Switch(handler) +} diff --git a/pkg/server/server_entrypoint_udp_test.go b/pkg/server/server_entrypoint_udp_test.go new file mode 100644 index 000000000..06861bc8d --- /dev/null +++ b/pkg/server/server_entrypoint_udp_test.go @@ -0,0 +1,123 @@ +package server + +import ( + "context" + "io" + "net" + "testing" + "time" + + "github.com/containous/traefik/v2/pkg/config/static" + "github.com/containous/traefik/v2/pkg/types" + "github.com/containous/traefik/v2/pkg/udp" + "github.com/stretchr/testify/require" +) + +func TestShutdownUDPConn(t *testing.T) { + entryPoint, err := NewUDPEntryPoint(&static.EntryPoint{ + Address: ":0", + Transport: &static.EntryPointsTransport{ + LifeCycle: &static.LifeCycle{ + GraceTimeOut: types.Duration(5 * time.Second), + }, + }, + }) + require.NoError(t, err) + + go entryPoint.Start(context.Background()) + entryPoint.Switch(udp.HandlerFunc(func(conn *udp.Conn) { + for { + b := make([]byte, 1024*1024) + n, err := conn.Read(b) + require.NoError(t, err) + // We control the termination, otherwise we would block on the Read above, until + // conn is closed by a timeout. Which means we would get an error, and even though + // we are in a goroutine and the current test might be over, go test would still + // yell at us if this happens while other tests are still running. + if string(b[:n]) == "CLOSE" { + return + } + _, err = conn.Write(b[:n]) + require.NoError(t, err) + } + })) + + conn, err := net.Dial("udp", entryPoint.listener.Addr().String()) + require.NoError(t, err) + + // Start sending packets, to create a "session" with the server. + requireEcho(t, "TEST", conn, time.Second) + + doneChan := make(chan struct{}) + go func() { + entryPoint.Shutdown(context.Background()) + close(doneChan) + }() + + // Make sure that our session is still live even after the shutdown. + requireEcho(t, "TEST2", conn, time.Second) + + // And make sure that on the other hand, opening new sessions is not possible anymore. + conn2, err := net.Dial("udp", entryPoint.listener.Addr().String()) + require.NoError(t, err) + + _, err = conn2.Write([]byte("TEST")) + // Packet is accepted, but dropped + require.NoError(t, err) + + // Make sure that our session is yet again still live. This is specifically to + // make sure we don't create a regression in listener's readLoop, i.e. that we only + // terminate the listener's readLoop goroutine by closing its pConn. + requireEcho(t, "TEST3", conn, time.Second) + + done := make(chan bool) + go func() { + defer close(done) + b := make([]byte, 1024*1024) + n, err := conn2.Read(b) + require.Error(t, err) + require.Equal(t, 0, n) + }() + + conn2.Close() + + select { + case <-done: + case <-time.Tick(time.Second): + t.Fatal("Timeout") + } + + _, err = conn.Write([]byte("CLOSE")) + require.NoError(t, err) + + select { + case <-doneChan: + case <-time.Tick(time.Second * 5): + // In case we introduce a regression that would make the test wait forever. + t.Fatal("Timeout during shutdown") + } +} + +// requireEcho tests that the conn session is live and functional, by writing +// data through it, and expecting the same data as a response when reading on it. +// It fatals if the read blocks longer than timeout, which is useful to detect +// regressions that would make a test wait forever. +func requireEcho(t *testing.T, data string, conn io.ReadWriter, timeout time.Duration) { + _, err := conn.Write([]byte(data)) + require.NoError(t, err) + + doneChan := make(chan struct{}) + go func() { + b := make([]byte, 1024*1024) + n, err := conn.Read(b) + require.NoError(t, err) + require.Equal(t, data, string(b[:n])) + close(doneChan) + }() + + select { + case <-doneChan: + case <-time.Tick(timeout): + t.Fatalf("Timeout during echo for: %s", data) + } +} diff --git a/pkg/server/service/tcp/service.go b/pkg/server/service/tcp/service.go index 7fcdd3cb0..7bb4e8d10 100644 --- a/pkg/server/service/tcp/service.go +++ b/pkg/server/service/tcp/service.go @@ -81,7 +81,7 @@ func (m *Manager) BuildTCP(rootCtx context.Context, serviceName string) (tcp.Han } return loadBalancer, nil default: - err := fmt.Errorf("the service %q doesn't have any TCP load balancer", serviceQualifiedName) + err := fmt.Errorf("the service %q does not have any type defined", serviceQualifiedName) conf.AddError(err, true) return nil, err } diff --git a/pkg/server/service/tcp/service_test.go b/pkg/server/service/tcp/service_test.go index fd9b35194..be5008d3d 100644 --- a/pkg/server/service/tcp/service_test.go +++ b/pkg/server/service/tcp/service_test.go @@ -33,7 +33,7 @@ func TestManager_BuildTCP(t *testing.T) { TCPService: &dynamic.TCPService{}, }, }, - expectedError: `the service "test" doesn't have any TCP load balancer`, + expectedError: `the service "test" does not have any type defined`, }, { desc: "no such host, server is skipped, error is logged", diff --git a/pkg/server/service/udp/service.go b/pkg/server/service/udp/service.go new file mode 100644 index 000000000..9ed896552 --- /dev/null +++ b/pkg/server/service/udp/service.go @@ -0,0 +1,81 @@ +package udp + +import ( + "context" + "errors" + "fmt" + "net" + + "github.com/containous/traefik/v2/pkg/config/runtime" + "github.com/containous/traefik/v2/pkg/log" + "github.com/containous/traefik/v2/pkg/server/provider" + "github.com/containous/traefik/v2/pkg/udp" +) + +// Manager handles UDP services creation. +type Manager struct { + configs map[string]*runtime.UDPServiceInfo +} + +// NewManager creates a new manager +func NewManager(conf *runtime.Configuration) *Manager { + return &Manager{ + configs: conf.UDPServices, + } +} + +// BuildUDP creates the UDP handler for the given service name. +func (m *Manager) BuildUDP(rootCtx context.Context, serviceName string) (udp.Handler, error) { + serviceQualifiedName := provider.GetQualifiedName(rootCtx, serviceName) + ctx := provider.AddInContext(rootCtx, serviceQualifiedName) + ctx = log.With(ctx, log.Str(log.ServiceName, serviceName)) + + conf, ok := m.configs[serviceQualifiedName] + if !ok { + return nil, fmt.Errorf("the udp service %q does not exist", serviceQualifiedName) + } + + if conf.LoadBalancer != nil && conf.Weighted != nil { + err := errors.New("cannot create service: multi-types service not supported, consider declaring two different pieces of service instead") + conf.AddError(err, true) + return nil, err + } + + logger := log.FromContext(ctx) + switch { + case conf.LoadBalancer != nil: + loadBalancer := udp.NewWRRLoadBalancer() + + for name, server := range conf.LoadBalancer.Servers { + if _, _, err := net.SplitHostPort(server.Address); err != nil { + logger.Errorf("In udp service %q: %v", serviceQualifiedName, err) + continue + } + + handler, err := udp.NewProxy(server.Address) + if err != nil { + logger.Errorf("In udp service %q server %q: %v", serviceQualifiedName, server.Address, err) + continue + } + + loadBalancer.AddServer(handler) + logger.WithField(log.ServerName, name).Debugf("Creating UDP server %d at %s", name, server.Address) + } + return loadBalancer, nil + case conf.Weighted != nil: + loadBalancer := udp.NewWRRLoadBalancer() + for _, service := range conf.Weighted.Services { + handler, err := m.BuildUDP(rootCtx, service.Name) + if err != nil { + logger.Errorf("In udp service %q: %v", serviceQualifiedName, err) + return nil, err + } + loadBalancer.AddWeightedServer(handler, service.Weight) + } + return loadBalancer, nil + default: + err := fmt.Errorf("the udp service %q does not have any type defined", serviceQualifiedName) + conf.AddError(err, true) + return nil, err + } +} diff --git a/pkg/server/service/udp/service_test.go b/pkg/server/service/udp/service_test.go new file mode 100644 index 000000000..5308ad114 --- /dev/null +++ b/pkg/server/service/udp/service_test.go @@ -0,0 +1,201 @@ +package udp + +import ( + "context" + "testing" + + "github.com/containous/traefik/v2/pkg/config/dynamic" + "github.com/containous/traefik/v2/pkg/config/runtime" + "github.com/containous/traefik/v2/pkg/server/provider" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestManager_BuildUDP(t *testing.T) { + testCases := []struct { + desc string + serviceName string + configs map[string]*runtime.UDPServiceInfo + providerName string + expectedError string + }{ + { + desc: "without configuration", + serviceName: "test", + configs: nil, + expectedError: `the udp service "test" does not exist`, + }, + { + desc: "missing lb configuration", + serviceName: "test", + configs: map[string]*runtime.UDPServiceInfo{ + "test": { + UDPService: &dynamic.UDPService{}, + }, + }, + expectedError: `the udp service "test" does not have any type defined`, + }, + { + desc: "no such host, server is skipped, error is logged", + serviceName: "test", + configs: map[string]*runtime.UDPServiceInfo{ + "test": { + UDPService: &dynamic.UDPService{ + LoadBalancer: &dynamic.UDPServersLoadBalancer{ + Servers: []dynamic.UDPServer{ + {Address: "test:31"}, + }, + }, + }, + }, + }, + }, + { + desc: "invalid IP address, server is skipped, error is logged", + serviceName: "test", + configs: map[string]*runtime.UDPServiceInfo{ + "test": { + UDPService: &dynamic.UDPService{ + LoadBalancer: &dynamic.UDPServersLoadBalancer{ + Servers: []dynamic.UDPServer{ + {Address: "foobar"}, + }, + }, + }, + }, + }, + }, + { + desc: "Simple service name", + serviceName: "serviceName", + configs: map[string]*runtime.UDPServiceInfo{ + "serviceName": { + UDPService: &dynamic.UDPService{ + LoadBalancer: &dynamic.UDPServersLoadBalancer{}, + }, + }, + }, + }, + { + desc: "Service name with provider", + serviceName: "serviceName@provider-1", + configs: map[string]*runtime.UDPServiceInfo{ + "serviceName@provider-1": { + UDPService: &dynamic.UDPService{ + LoadBalancer: &dynamic.UDPServersLoadBalancer{}, + }, + }, + }, + }, + { + desc: "Service name with provider in context", + serviceName: "serviceName", + configs: map[string]*runtime.UDPServiceInfo{ + "serviceName@provider-1": { + UDPService: &dynamic.UDPService{ + LoadBalancer: &dynamic.UDPServersLoadBalancer{}, + }, + }, + }, + providerName: "provider-1", + }, + { + desc: "Server with correct host:port as address", + serviceName: "serviceName", + configs: map[string]*runtime.UDPServiceInfo{ + "serviceName@provider-1": { + UDPService: &dynamic.UDPService{ + LoadBalancer: &dynamic.UDPServersLoadBalancer{ + Servers: []dynamic.UDPServer{ + { + Address: "foobar.com:80", + }, + }, + }, + }, + }, + }, + providerName: "provider-1", + }, + { + desc: "Server with correct ip:port as address", + serviceName: "serviceName", + configs: map[string]*runtime.UDPServiceInfo{ + "serviceName@provider-1": { + UDPService: &dynamic.UDPService{ + LoadBalancer: &dynamic.UDPServersLoadBalancer{ + Servers: []dynamic.UDPServer{ + { + Address: "192.168.0.12:80", + }, + }, + }, + }, + }, + }, + providerName: "provider-1", + }, + { + desc: "missing port in address with hostname, server is skipped, error is logged", + serviceName: "serviceName", + configs: map[string]*runtime.UDPServiceInfo{ + "serviceName@provider-1": { + UDPService: &dynamic.UDPService{ + LoadBalancer: &dynamic.UDPServersLoadBalancer{ + Servers: []dynamic.UDPServer{ + { + Address: "foobar.com", + }, + }, + }, + }, + }, + }, + providerName: "provider-1", + }, + { + desc: "missing port in address with ip, server is skipped, error is logged", + serviceName: "serviceName", + configs: map[string]*runtime.UDPServiceInfo{ + "serviceName@provider-1": { + UDPService: &dynamic.UDPService{ + LoadBalancer: &dynamic.UDPServersLoadBalancer{ + Servers: []dynamic.UDPServer{ + { + Address: "192.168.0.12", + }, + }, + }, + }, + }, + }, + providerName: "provider-1", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + manager := NewManager(&runtime.Configuration{ + UDPServices: test.configs, + }) + + ctx := context.Background() + if len(test.providerName) > 0 { + ctx = provider.AddInContext(ctx, "foobar@"+test.providerName) + } + + handler, err := manager.BuildUDP(ctx, test.serviceName) + + if test.expectedError != "" { + assert.EqualError(t, err, test.expectedError) + require.Nil(t, handler) + } else { + assert.Nil(t, err) + require.NotNil(t, handler) + } + }) + } +} diff --git a/pkg/server/tcprouterfactory.go b/pkg/server/tcprouterfactory.go deleted file mode 100644 index d42d36d9e..000000000 --- a/pkg/server/tcprouterfactory.go +++ /dev/null @@ -1,70 +0,0 @@ -package server - -import ( - "context" - - "github.com/containous/traefik/v2/pkg/config/dynamic" - "github.com/containous/traefik/v2/pkg/config/runtime" - "github.com/containous/traefik/v2/pkg/config/static" - "github.com/containous/traefik/v2/pkg/responsemodifiers" - "github.com/containous/traefik/v2/pkg/server/middleware" - "github.com/containous/traefik/v2/pkg/server/router" - routertcp "github.com/containous/traefik/v2/pkg/server/router/tcp" - "github.com/containous/traefik/v2/pkg/server/service" - "github.com/containous/traefik/v2/pkg/server/service/tcp" - tcpCore "github.com/containous/traefik/v2/pkg/tcp" - "github.com/containous/traefik/v2/pkg/tls" -) - -// TCPRouterFactory the factory of TCP routers. -type TCPRouterFactory struct { - entryPoints []string - - managerFactory *service.ManagerFactory - - chainBuilder *middleware.ChainBuilder - tlsManager *tls.Manager -} - -// NewTCPRouterFactory creates a new TCPRouterFactory -func NewTCPRouterFactory(staticConfiguration static.Configuration, managerFactory *service.ManagerFactory, tlsManager *tls.Manager, chainBuilder *middleware.ChainBuilder) *TCPRouterFactory { - var entryPoints []string - for name := range staticConfiguration.EntryPoints { - entryPoints = append(entryPoints, name) - } - - return &TCPRouterFactory{ - entryPoints: entryPoints, - managerFactory: managerFactory, - tlsManager: tlsManager, - chainBuilder: chainBuilder, - } -} - -// CreateTCPRouters creates new TCPRouters -func (f *TCPRouterFactory) CreateTCPRouters(conf dynamic.Configuration) map[string]*tcpCore.Router { - ctx := context.Background() - - rtConf := runtime.NewConfig(conf) - - // HTTP - serviceManager := f.managerFactory.Build(rtConf) - - middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager) - responseModifierFactory := responsemodifiers.NewBuilder(rtConf.Middlewares) - - routerManager := router.NewManager(rtConf, serviceManager, middlewaresBuilder, responseModifierFactory, f.chainBuilder) - - handlersNonTLS := routerManager.BuildHandlers(ctx, f.entryPoints, false) - handlersTLS := routerManager.BuildHandlers(ctx, f.entryPoints, true) - - // TCP - svcTCPManager := tcp.NewManager(rtConf) - - rtTCPManager := routertcp.NewManager(rtConf, svcTCPManager, handlersNonTLS, handlersTLS, f.tlsManager) - routersTCP := rtTCPManager.BuildHandlers(ctx, f.entryPoints) - - rtConf.PopulateUsedBy() - - return routersTCP -} diff --git a/pkg/udp/conn.go b/pkg/udp/conn.go new file mode 100644 index 000000000..4b2f0adcc --- /dev/null +++ b/pkg/udp/conn.go @@ -0,0 +1,265 @@ +package udp + +import ( + "errors" + "io" + "net" + "sync" + "time" +) + +const receiveMTU = 8192 + +const closeRetryInterval = 500 * time.Millisecond + +// connTimeout determines how long to wait on an idle session, +// before releasing all resources related to that session. +const connTimeout = time.Second * 3 + +var errClosedListener = errors.New("udp: listener closed") + +// Listener augments a session-oriented Listener over a UDP PacketConn. +type Listener struct { + pConn *net.UDPConn + + mu sync.RWMutex + conns map[string]*Conn + // accepting signifies whether the listener is still accepting new sessions. + // It also serves as a sentinel for Shutdown to be idempotent. + accepting bool + + acceptCh chan *Conn // no need for a Once, already indirectly guarded by accepting. +} + +// Listen creates a new listener. +func Listen(network string, laddr *net.UDPAddr) (*Listener, error) { + conn, err := net.ListenUDP(network, laddr) + if err != nil { + return nil, err + } + + l := &Listener{ + pConn: conn, + acceptCh: make(chan *Conn), + conns: make(map[string]*Conn), + accepting: true, + } + + go l.readLoop() + + return l, nil +} + +// Accept waits for and returns the next connection to the listener. +func (l *Listener) Accept() (*Conn, error) { + c := <-l.acceptCh + if c == nil { + // l.acceptCh got closed + return nil, errClosedListener + } + return c, nil +} + +// Addr returns the listener's network address. +func (l *Listener) Addr() net.Addr { + return l.pConn.LocalAddr() +} + +// Close closes the listener. +// It is like Shutdown with a zero graceTimeout. +func (l *Listener) Close() error { + return l.Shutdown(0) +} + +// close should not be called more than once. +func (l *Listener) close() error { + l.mu.Lock() + defer l.mu.Unlock() + err := l.pConn.Close() + for k, v := range l.conns { + v.close() + delete(l.conns, k) + } + close(l.acceptCh) + return err +} + +// Shutdown closes the listener. +// It immediately stops accepting new sessions, +// and it waits for all existing sessions to terminate, +// and a maximum of graceTimeout. +// Then it forces close any session left. +func (l *Listener) Shutdown(graceTimeout time.Duration) error { + l.mu.Lock() + if !l.accepting { + l.mu.Unlock() + return nil + } + l.accepting = false + l.mu.Unlock() + + retryInterval := closeRetryInterval + if retryInterval > graceTimeout { + retryInterval = graceTimeout + } + start := time.Now() + end := start.Add(graceTimeout) + for { + if time.Now().After(end) { + break + } + + l.mu.RLock() + if len(l.conns) == 0 { + l.mu.RUnlock() + break + } + l.mu.RUnlock() + + time.Sleep(retryInterval) + } + return l.close() +} + +// readLoop receives all packets from all remotes. +// If a packet comes from a remote that is already known to us (i.e. a "session"), +// we find that session, and otherwise we create a new one. +// We then send the data the session's readLoop. +func (l *Listener) readLoop() { + buf := make([]byte, receiveMTU) + + for { + n, raddr, err := l.pConn.ReadFrom(buf) + if err != nil { + return + } + conn, err := l.getConn(raddr) + if err != nil { + continue + } + select { + case conn.receiveCh <- buf[:n]: + case <-conn.doneCh: + continue + } + } +} + +// getConn returns the ongoing session with raddr if it exists, or creates a new +// one otherwise. +func (l *Listener) getConn(raddr net.Addr) (*Conn, error) { + l.mu.Lock() + defer l.mu.Unlock() + + conn, ok := l.conns[raddr.String()] + if ok { + return conn, nil + } + + if !l.accepting { + return nil, errClosedListener + } + conn = l.newConn(raddr) + l.conns[raddr.String()] = conn + l.acceptCh <- conn + go conn.readLoop() + + return conn, nil +} + +func (l *Listener) newConn(rAddr net.Addr) *Conn { + return &Conn{ + listener: l, + rAddr: rAddr, + receiveCh: make(chan []byte), + readCh: make(chan []byte), + sizeCh: make(chan int), + doneCh: make(chan struct{}), + timer: time.NewTimer(connTimeout), + } +} + +// Conn represents an on-going session with a client, over UDP packets. +type Conn struct { + listener *Listener + rAddr net.Addr + + receiveCh chan []byte // to receive the data from the listener's readLoop + readCh chan []byte // to receive the buffer into which we should Read + sizeCh chan int // to synchronize with the end of a Read + msgs [][]byte // to store data from listener, to be consumed by Reads + + timer *time.Timer // for timeouts + doneOnce sync.Once + doneCh chan struct{} +} + +// readLoop waits for data to come from the listener's readLoop. +// It then waits for a Read operation to be ready to consume said data, +// that is to say it waits on readCh to receive the slice of bytes that the Read operation wants to read onto. +// The Read operation receives the signal that the data has been written to the slice of bytes through the sizeCh. +func (c *Conn) readLoop() { + for { + if len(c.msgs) == 0 { + select { + case msg := <-c.receiveCh: + c.msgs = append(c.msgs, msg) + case <-c.timer.C: + c.Close() + return + } + } + + select { + case cBuf := <-c.readCh: + msg := c.msgs[0] + c.msgs = c.msgs[1:] + n := copy(cBuf, msg) + c.sizeCh <- n + case msg := <-c.receiveCh: + c.msgs = append(c.msgs, msg) + case <-c.timer.C: + c.Close() + return + } + } +} + +// Read implements io.Reader for a Conn. +func (c *Conn) Read(p []byte) (int, error) { + select { + case c.readCh <- p: + n := <-c.sizeCh + c.timer.Reset(connTimeout) + return n, nil + case <-c.doneCh: + return 0, io.EOF + } +} + +// Write implements io.Writer for a Conn. +func (c *Conn) Write(p []byte) (n int, err error) { + l := c.listener + if l == nil { + return 0, io.EOF + } + + c.timer.Reset(connTimeout) + return l.pConn.WriteTo(p, c.rAddr) +} + +func (c *Conn) close() { + c.doneOnce.Do(func() { + close(c.doneCh) + }) +} + +// Close releases resources related to the Conn. +func (c *Conn) Close() error { + c.close() + + c.listener.mu.Lock() + defer c.listener.mu.Unlock() + delete(c.listener.conns, c.rAddr.String()) + return nil +} diff --git a/pkg/udp/conn_test.go b/pkg/udp/conn_test.go new file mode 100644 index 000000000..50abfecb7 --- /dev/null +++ b/pkg/udp/conn_test.go @@ -0,0 +1,270 @@ +package udp + +import ( + "io" + "net" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestListenNotBlocking(t *testing.T) { + addr, err := net.ResolveUDPAddr("udp", ":0") + + require.NoError(t, err) + + ln, err := Listen("udp", addr) + require.NoError(t, err) + defer func() { + err := ln.Close() + require.NoError(t, err) + }() + + go func() { + for { + conn, err := ln.Accept() + if err == errClosedListener { + return + } + require.NoError(t, err) + + go func() { + b := make([]byte, 2048) + n, err := conn.Read(b) + require.NoError(t, err) + _, err = conn.Write(b[:n]) + require.NoError(t, err) + + n, err = conn.Read(b) + require.NoError(t, err) + _, err = conn.Write(b[:n]) + require.NoError(t, err) + + // This should not block second call + time.Sleep(time.Second * 10) + }() + } + }() + + udpConn, err := net.Dial("udp", ln.Addr().String()) + require.NoError(t, err) + + _, err = udpConn.Write([]byte("TEST")) + require.NoError(t, err) + + b := make([]byte, 2048) + n, err := udpConn.Read(b) + require.NoError(t, err) + require.Equal(t, "TEST", string(b[:n])) + + _, err = udpConn.Write([]byte("TEST2")) + require.NoError(t, err) + + n, err = udpConn.Read(b) + require.NoError(t, err) + require.Equal(t, "TEST2", string(b[:n])) + + _, err = udpConn.Write([]byte("TEST")) + require.NoError(t, err) + + done := make(chan struct{}) + go func() { + udpConn2, err := net.Dial("udp", ln.Addr().String()) + require.NoError(t, err) + + _, err = udpConn2.Write([]byte("TEST")) + require.NoError(t, err) + + n, err = udpConn2.Read(b) + require.NoError(t, err) + + assert.Equal(t, "TEST", string(b[:n])) + + _, err = udpConn2.Write([]byte("TEST2")) + require.NoError(t, err) + + n, err = udpConn2.Read(b) + require.NoError(t, err) + + assert.Equal(t, "TEST2", string(b[:n])) + + close(done) + }() + + select { + case <-time.Tick(time.Second): + t.Error("Timeout") + case <-done: + } +} + +func TestTimeoutWithRead(t *testing.T) { + testTimeout(t, true) +} + +func TestTimeoutWithoutRead(t *testing.T) { + testTimeout(t, false) +} + +func testTimeout(t *testing.T, withRead bool) { + addr, err := net.ResolveUDPAddr("udp", ":0") + require.NoError(t, err) + + ln, err := Listen("udp", addr) + require.NoError(t, err) + defer func() { + err := ln.Close() + require.NoError(t, err) + }() + + go func() { + for { + conn, err := ln.Accept() + if err == errClosedListener { + return + } + require.NoError(t, err) + + if withRead { + buf := make([]byte, 1024) + _, err = conn.Read(buf) + + require.NoError(t, err) + } + } + }() + + for i := 0; i < 10; i++ { + udpConn2, err := net.Dial("udp", ln.Addr().String()) + require.NoError(t, err) + + _, err = udpConn2.Write([]byte("TEST")) + require.NoError(t, err) + } + + time.Sleep(10 * time.Millisecond) + + assert.Equal(t, 10, len(ln.conns)) + + time.Sleep(3 * time.Second) + assert.Equal(t, 0, len(ln.conns)) +} + +func TestShutdown(t *testing.T) { + addr, err := net.ResolveUDPAddr("udp", ":0") + require.NoError(t, err) + + l, err := Listen("udp", addr) + require.NoError(t, err) + + go func() { + for { + conn, err := l.Accept() + if err != nil { + return + } + + go func() { + conn := conn + for { + b := make([]byte, 1024*1024) + n, err := conn.Read(b) + require.NoError(t, err) + // We control the termination, + // otherwise we would block on the Read above, + // until conn is closed by a timeout. + // Which means we would get an error, + // and even though we are in a goroutine and the current test might be over, + // go test would still yell at us if this happens while other tests are still running. + if string(b[:n]) == "CLOSE" { + return + } + _, err = conn.Write(b[:n]) + require.NoError(t, err) + } + }() + } + }() + + conn, err := net.Dial("udp", l.Addr().String()) + require.NoError(t, err) + + // Start sending packets, to create a "session" with the server. + requireEcho(t, "TEST", conn, time.Second) + + doneChan := make(chan struct{}) + go func() { + err := l.Shutdown(5 * time.Second) + require.NoError(t, err) + close(doneChan) + }() + + // Make sure that our session is still live even after the shutdown. + requireEcho(t, "TEST2", conn, time.Second) + + // And make sure that on the other hand, opening new sessions is not possible anymore. + conn2, err := net.Dial("udp", l.Addr().String()) + require.NoError(t, err) + + _, err = conn2.Write([]byte("TEST")) + // Packet is accepted, but dropped + require.NoError(t, err) + + // Make sure that our session is yet again still live. + // This is specifically to make sure we don't create a regression in listener's readLoop, + // i.e. that we only terminate the listener's readLoop goroutine by closing its pConn. + requireEcho(t, "TEST3", conn, time.Second) + + done := make(chan bool) + go func() { + defer close(done) + b := make([]byte, 1024*1024) + n, err := conn2.Read(b) + require.Error(t, err) + assert.Equal(t, 0, n) + }() + + conn2.Close() + + select { + case <-done: + case <-time.Tick(time.Second): + t.Fatal("Timeout") + } + + _, err = conn.Write([]byte("CLOSE")) + require.NoError(t, err) + + select { + case <-doneChan: + case <-time.Tick(time.Second * 5): + // In case we introduce a regression that would make the test wait forever. + t.Fatal("Timeout during shutdown") + } +} + +// requireEcho tests that the conn session is live and functional, +// by writing data through it, and expecting the same data as a response when reading on it. +// It fatals if the read blocks longer than timeout, +// which is useful to detect regressions that would make a test wait forever. +func requireEcho(t *testing.T, data string, conn io.ReadWriter, timeout time.Duration) { + _, err := conn.Write([]byte(data)) + require.NoError(t, err) + + doneChan := make(chan struct{}) + go func() { + b := make([]byte, 1024*1024) + n, err := conn.Read(b) + require.NoError(t, err) + assert.Equal(t, data, string(b[:n])) + close(doneChan) + }() + + select { + case <-doneChan: + case <-time.Tick(timeout): + t.Fatalf("Timeout during echo for: %s", data) + } +} diff --git a/pkg/udp/handler.go b/pkg/udp/handler.go new file mode 100644 index 000000000..6c47523e4 --- /dev/null +++ b/pkg/udp/handler.go @@ -0,0 +1,14 @@ +package udp + +// Handler is the UDP counterpart of the usual HTTP handler. +type Handler interface { + ServeUDP(conn *Conn) +} + +// The HandlerFunc type is an adapter to allow the use of ordinary functions as handlers. +type HandlerFunc func(conn *Conn) + +// ServeUDP implements the Handler interface for UDP. +func (f HandlerFunc) ServeUDP(conn *Conn) { + f(conn) +} diff --git a/pkg/udp/proxy.go b/pkg/udp/proxy.go new file mode 100644 index 000000000..520244ee1 --- /dev/null +++ b/pkg/udp/proxy.go @@ -0,0 +1,56 @@ +package udp + +import ( + "io" + "net" + + "github.com/containous/traefik/v2/pkg/log" +) + +// Proxy is a reverse-proxy implementation of the Handler interface. +type Proxy struct { + // TODO: maybe optimize by pre-resolving it at proxy creation time + target string +} + +// NewProxy creates a new Proxy +func NewProxy(address string) (*Proxy, error) { + return &Proxy{target: address}, nil +} + +// ServeUDP implements the Handler interface. +func (p *Proxy) ServeUDP(conn *Conn) { + log.Debugf("Handling connection from %s", conn.rAddr) + + // needed because of e.g. server.trackedConnection + defer conn.Close() + + connBackend, err := net.Dial("udp", p.target) + if err != nil { + log.Errorf("Error while connecting to backend: %v", err) + return + } + + // maybe not needed, but just in case + defer connBackend.Close() + + errChan := make(chan error) + go p.connCopy(conn, connBackend, errChan) + go p.connCopy(connBackend, conn, errChan) + + err = <-errChan + if err != nil { + log.WithoutContext().Errorf("Error while serving UDP: %v", err) + } + + <-errChan +} + +func (p Proxy) connCopy(dst io.WriteCloser, src io.Reader, errCh chan error) { + _, err := io.Copy(dst, src) + errCh <- err + + if err := dst.Close(); err != nil { + log.WithoutContext().Debugf("Error while terminating connection: %v", err) + } +} diff --git a/pkg/udp/proxy_test.go b/pkg/udp/proxy_test.go new file mode 100644 index 000000000..509f9aabd --- /dev/null +++ b/pkg/udp/proxy_test.go @@ -0,0 +1,55 @@ +package udp + +import ( + "net" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUDPProxy(t *testing.T) { + backendAddr := ":8081" + go newServer(t, ":8081", HandlerFunc(func(conn *Conn) { + for { + b := make([]byte, 1024*1024) + n, err := conn.Read(b) + require.NoError(t, err) + _, err = conn.Write(b[:n]) + require.NoError(t, err) + } + })) + + proxy, err := NewProxy(backendAddr) + require.NoError(t, err) + + proxyAddr := ":8080" + go newServer(t, proxyAddr, proxy) + + time.Sleep(time.Second) + udpConn, err := net.Dial("udp", proxyAddr) + require.NoError(t, err) + + _, err = udpConn.Write([]byte("DATAWRITE")) + require.NoError(t, err) + + b := make([]byte, 1024*1024) + n, err := udpConn.Read(b) + require.NoError(t, err) + assert.Equal(t, "DATAWRITE", string(b[:n])) +} + +func newServer(t *testing.T, addr string, handler Handler) { + addrL, err := net.ResolveUDPAddr("udp", addr) + require.NoError(t, err) + + listener, err := Listen("udp", addrL) + require.NoError(t, err) + + for { + conn, err := listener.Accept() + require.NoError(t, err) + go handler.ServeUDP(conn) + } +} diff --git a/pkg/udp/switcher.go b/pkg/udp/switcher.go new file mode 100644 index 000000000..c9ca13a37 --- /dev/null +++ b/pkg/udp/switcher.go @@ -0,0 +1,26 @@ +package udp + +import ( + "github.com/containous/traefik/v2/pkg/safe" +) + +// HandlerSwitcher is a switcher implementation of the Handler interface. +type HandlerSwitcher struct { + handler safe.Safe +} + +// ServeUDP implements the Handler interface. +func (s *HandlerSwitcher) ServeUDP(conn *Conn) { + handler := s.handler.Get() + h, ok := handler.(Handler) + if ok { + h.ServeUDP(conn) + } else { + conn.Close() + } +} + +// Switch replaces s handler with the given handler. +func (s *HandlerSwitcher) Switch(handler Handler) { + s.handler.Set(handler) +} diff --git a/pkg/udp/wrr_load_balancer.go b/pkg/udp/wrr_load_balancer.go new file mode 100644 index 000000000..f858c39b8 --- /dev/null +++ b/pkg/udp/wrr_load_balancer.go @@ -0,0 +1,122 @@ +package udp + +import ( + "fmt" + "sync" + + "github.com/containous/traefik/v2/pkg/log" +) + +type server struct { + Handler + weight int +} + +// WRRLoadBalancer is a naive RoundRobin load balancer for UDP services +type WRRLoadBalancer struct { + servers []server + lock sync.RWMutex + currentWeight int + index int +} + +// NewWRRLoadBalancer creates a new WRRLoadBalancer +func NewWRRLoadBalancer() *WRRLoadBalancer { + return &WRRLoadBalancer{ + index: -1, + } +} + +// ServeUDP forwards the connection to the right service +func (b *WRRLoadBalancer) ServeUDP(conn *Conn) { + if len(b.servers) == 0 { + log.WithoutContext().Error("no available server") + return + } + + next, err := b.next() + if err != nil { + log.WithoutContext().Errorf("Error during load balancing: %v", err) + conn.Close() + } + next.ServeUDP(conn) +} + +// AddServer appends a handler to the existing list +func (b *WRRLoadBalancer) AddServer(serverHandler Handler) { + w := 1 + b.AddWeightedServer(serverHandler, &w) +} + +// AddWeightedServer appends a handler to the existing list with a weight +func (b *WRRLoadBalancer) AddWeightedServer(serverHandler Handler, weight *int) { + w := 1 + if weight != nil { + w = *weight + } + b.servers = append(b.servers, server{Handler: serverHandler, weight: w}) +} + +func (b *WRRLoadBalancer) maxWeight() int { + max := -1 + for _, s := range b.servers { + if s.weight > max { + max = s.weight + } + } + return max +} + +func (b *WRRLoadBalancer) weightGcd() int { + divisor := -1 + for _, s := range b.servers { + if divisor == -1 { + divisor = s.weight + } else { + divisor = gcd(divisor, s.weight) + } + } + return divisor +} + +func gcd(a, b int) int { + for b != 0 { + a, b = b, a%b + } + return a +} + +func (b *WRRLoadBalancer) next() (Handler, error) { + b.lock.Lock() + defer b.lock.Unlock() + + if len(b.servers) == 0 { + return nil, fmt.Errorf("no servers in the pool") + } + + // The algorithm below may look messy, + // but is actually very simple it calculates the GCD and subtracts it on every iteration, + // what interleaves servers and allows us not to build an iterator every time we readjust weights. + + // GCD across all enabled servers + gcd := b.weightGcd() + // Maximum weight across all enabled servers + max := b.maxWeight() + + for { + b.index = (b.index + 1) % len(b.servers) + if b.index == 0 { + b.currentWeight -= gcd + if b.currentWeight <= 0 { + b.currentWeight = max + if b.currentWeight == 0 { + return nil, fmt.Errorf("all servers have 0 weight") + } + } + } + srv := b.servers[b.index] + if srv.weight >= b.currentWeight { + return srv, nil + } + } +}