diff --git a/pkg/provider/kubernetes/crd/fixtures/services.yml b/pkg/provider/kubernetes/crd/fixtures/services.yml index 72b2e1b16..50d6d65c6 100644 --- a/pkg/provider/kubernetes/crd/fixtures/services.yml +++ b/pkg/provider/kubernetes/crd/fixtures/services.yml @@ -119,6 +119,35 @@ subsets: - name: websecure2 port: 8443 +--- +apiVersion: v1 +kind: Service +metadata: + name: whoami-ipv6 + namespace: default + +spec: + ports: + - name: web + port: 8080 + selector: + app: traefiklabs + task: whoami-ipv6 + +--- +kind: Endpoints +apiVersion: v1 +metadata: + name: whoami-ipv6 + namespace: default + +subsets: + - addresses: + - ip: "2001:db8:85a3:8d3:1319:8a2e:370:7348" + ports: + - name: web + port: 8080 + --- apiVersion: v1 kind: Service @@ -157,5 +186,16 @@ spec: protocol: TCP port: 443 - - +--- +apiVersion: v1 +kind: Service +metadata: + name: external-svc-with-ipv6 + namespace: default +spec: + externalName: "2001:db8:85a3:8d3:1319:8a2e:370:7347" + type: ExternalName + ports: + - name: http + protocol: TCP + port: 8080 diff --git a/pkg/provider/kubernetes/crd/fixtures/tcp/services.yml b/pkg/provider/kubernetes/crd/fixtures/tcp/services.yml index 7aabcb9be..32a854c8a 100644 --- a/pkg/provider/kubernetes/crd/fixtures/tcp/services.yml +++ b/pkg/provider/kubernetes/crd/fixtures/tcp/services.yml @@ -132,6 +132,36 @@ subsets: - name: myapp4 port: 8084 +--- +apiVersion: v1 +kind: Service +metadata: + name: whoamitcp-ipv6 + namespace: default + +spec: + ports: + - name: myapp-ipv6 + port: 8080 + selector: + app: traefiklabs + task: whoamitcp-ipv6 + +--- +kind: Endpoints +apiVersion: v1 +metadata: + name: whoamitcp-ipv6 + namespace: default + +subsets: + - addresses: + - ip: "fd00:10:244:0:1::3" + - ip: "2001:db8:85a3:8d3:1319:8a2e:370:7348" + ports: + - name: myapp-ipv6 + port: 8080 + --- apiVersion: v1 kind: Service @@ -167,4 +197,14 @@ spec: type: ExternalName ports: - name: http - protocol: TCP \ No newline at end of file + protocol: TCP + +--- +apiVersion: v1 +kind: Service +metadata: + name: external.service.with.ipv6 + namespace: default +spec: + externalName: "fe80::200:5aee:feaa:20a2" + type: ExternalName diff --git a/pkg/provider/kubernetes/crd/fixtures/tcp/with_ipv6.yml b/pkg/provider/kubernetes/crd/fixtures/tcp/with_ipv6.yml new file mode 100644 index 000000000..777915c18 --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/tcp/with_ipv6.yml @@ -0,0 +1,17 @@ +apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRouteTCP +metadata: + name: test.route + namespace: default + +spec: + entryPoints: + - foo + + routes: + - match: HostSNI(`*`) + services: + - name: whoamitcp-ipv6 + port: 8080 + - name: external.service.with.ipv6 + port: 8080 diff --git a/pkg/provider/kubernetes/crd/fixtures/udp/services.yml b/pkg/provider/kubernetes/crd/fixtures/udp/services.yml index c1a589cb6..f1c0abe18 100644 --- a/pkg/provider/kubernetes/crd/fixtures/udp/services.yml +++ b/pkg/provider/kubernetes/crd/fixtures/udp/services.yml @@ -101,3 +101,32 @@ subsets: ports: - name: myapp4 port: 8084 + +--- +apiVersion: v1 +kind: Service +metadata: + name: whoamiudp-ipv6 + namespace: default + +spec: + ports: + - name: myapp-ipv6 + port: 8080 + selector: + app: traefiklabs + task: whoamiudp-ipv6 + +--- +kind: Endpoints +apiVersion: v1 +metadata: + name: whoamiudp-ipv6 + namespace: default + +subsets: + - addresses: + - ip: "fd00:10:244:0:1::3" + ports: + - name: myapp-ipv6 + port: 8080 diff --git a/pkg/provider/kubernetes/crd/fixtures/udp/with_ipv6.yml b/pkg/provider/kubernetes/crd/fixtures/udp/with_ipv6.yml new file mode 100644 index 000000000..2acc8bdc7 --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/udp/with_ipv6.yml @@ -0,0 +1,14 @@ +apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRouteUDP +metadata: + name: test.route + namespace: default + +spec: + entryPoints: + - foo + + routes: + - services: + - name: whoamiudp-ipv6 + port: 8080 diff --git a/pkg/provider/kubernetes/crd/fixtures/with_ipv6.yml b/pkg/provider/kubernetes/crd/fixtures/with_ipv6.yml new file mode 100644 index 000000000..23af879ac --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/with_ipv6.yml @@ -0,0 +1,18 @@ +apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRoute +metadata: + name: test.route + namespace: default + +spec: + entryPoints: + - foo + + routes: + - match: Host(`foo.com`) && PathPrefix(`/bar`) + kind: Rule + services: + - name: whoami-ipv6 + port: 8080 + - name: external-svc-with-ipv6 + port: 8080 diff --git a/pkg/provider/kubernetes/crd/kubernetes_http.go b/pkg/provider/kubernetes/crd/kubernetes_http.go index 9a28bbf43..1a40c561c 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_http.go +++ b/pkg/provider/kubernetes/crd/kubernetes_http.go @@ -4,6 +4,8 @@ import ( "context" "errors" "fmt" + "net" + "strconv" "strings" "github.com/traefik/traefik/v2/pkg/config/dynamic" @@ -301,8 +303,10 @@ func (c configBuilder) loadServers(fallbackNamespace string, svc v1alpha1.LoadBa return nil, err } + hostPort := net.JoinHostPort(service.Spec.ExternalName, strconv.Itoa(int(svcPort.Port))) + return append(servers, dynamic.Server{ - URL: fmt.Sprintf("%s://%s:%d", protocol, service.Spec.ExternalName, svcPort.Port), + URL: fmt.Sprintf("%s://%s", protocol, hostPort), }), nil } @@ -336,8 +340,10 @@ func (c configBuilder) loadServers(fallbackNamespace string, svc v1alpha1.LoadBa } for _, addr := range subset.Addresses { + hostPort := net.JoinHostPort(addr.IP, strconv.Itoa(int(port))) + servers = append(servers, dynamic.Server{ - URL: fmt.Sprintf("%s://%s:%d", protocol, addr.IP, port), + URL: fmt.Sprintf("%s://%s", protocol, hostPort), }) } } diff --git a/pkg/provider/kubernetes/crd/kubernetes_tcp.go b/pkg/provider/kubernetes/crd/kubernetes_tcp.go index 49cff4e60..abf29bb8e 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_tcp.go +++ b/pkg/provider/kubernetes/crd/kubernetes_tcp.go @@ -4,6 +4,8 @@ import ( "context" "errors" "fmt" + "net" + "strconv" "strings" "github.com/traefik/traefik/v2/pkg/config/dynamic" @@ -165,7 +167,7 @@ func loadTCPServers(client Client, namespace string, svc v1alpha1.ServiceTCP) ([ var servers []dynamic.TCPServer if service.Spec.Type == corev1.ServiceTypeExternalName { servers = append(servers, dynamic.TCPServer{ - Address: fmt.Sprintf("%s:%d", service.Spec.ExternalName, svcPort.Port), + Address: net.JoinHostPort(service.Spec.ExternalName, strconv.Itoa(int(svcPort.Port))), }) } else { endpoints, endpointsExists, endpointsErr := client.GetEndpoints(namespace, svc.Name) @@ -196,7 +198,7 @@ func loadTCPServers(client Client, namespace string, svc v1alpha1.ServiceTCP) ([ for _, addr := range subset.Addresses { servers = append(servers, dynamic.TCPServer{ - Address: fmt.Sprintf("%s:%d", addr.IP, port), + Address: net.JoinHostPort(addr.IP, strconv.Itoa(int(port))), }) } } diff --git a/pkg/provider/kubernetes/crd/kubernetes_test.go b/pkg/provider/kubernetes/crd/kubernetes_test.go index 73ed3588f..b866f20cb 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_test.go +++ b/pkg/provider/kubernetes/crd/kubernetes_test.go @@ -987,6 +987,128 @@ func TestLoadIngressRouteTCPs(t *testing.T) { TLS: &dynamic.TLSConfiguration{}, }, }, + { + desc: "Ingress Route with IPv6 backends", + paths: []string{ + "services.yml", "with_ipv6.yml", + "tcp/services.yml", "tcp/with_ipv6.yml", + "udp/services.yml", "udp/with_ipv6.yml", + }, + expected: &dynamic.Configuration{ + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{ + "default-test.route-0": { + EntryPoints: []string{"foo"}, + Service: "default-test.route-0", + }, + }, + Services: map[string]*dynamic.UDPService{ + "default-test.route-0": { + LoadBalancer: &dynamic.UDPServersLoadBalancer{ + Servers: []dynamic.UDPServer{ + { + Address: "[fd00:10:244:0:1::3]:8080", + }, + }, + }, + }, + }, + }, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{ + "default-test.route-673acf455cb2dab0b43a": { + EntryPoints: []string{"foo"}, + Service: "default-test.route-673acf455cb2dab0b43a", + Rule: "HostSNI(`*`)", + }, + }, + Services: map[string]*dynamic.TCPService{ + "default-test.route-673acf455cb2dab0b43a": { + Weighted: &dynamic.TCPWeightedRoundRobin{ + Services: []dynamic.TCPWRRService{ + { + Name: "default-test.route-673acf455cb2dab0b43a-whoamitcp-ipv6-8080", + Weight: func(i int) *int { return &i }(1), + }, + { + Name: "default-test.route-673acf455cb2dab0b43a-external.service.with.ipv6-8080", + Weight: func(i int) *int { return &i }(1), + }, + }, + }, + }, + "default-test.route-673acf455cb2dab0b43a-whoamitcp-ipv6-8080": { + LoadBalancer: &dynamic.TCPServersLoadBalancer{ + Servers: []dynamic.TCPServer{ + { + Address: "[fd00:10:244:0:1::3]:8080", + }, + { + Address: "[2001:db8:85a3:8d3:1319:8a2e:370:7348]:8080", + }, + }, + }, + }, + "default-test.route-673acf455cb2dab0b43a-external.service.with.ipv6-8080": { + LoadBalancer: &dynamic.TCPServersLoadBalancer{ + Servers: []dynamic.TCPServer{ + { + Address: "[fe80::200:5aee:feaa:20a2]:8080", + }, + }, + }, + }, + }, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "default-test-route-6b204d94623b3df4370c": { + EntryPoints: []string{"foo"}, + Service: "default-test-route-6b204d94623b3df4370c", + Rule: "Host(`foo.com`) && PathPrefix(`/bar`)", + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "default-whoami-ipv6-8080": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://[2001:db8:85a3:8d3:1319:8a2e:370:7348]:8080", + }, + }, + PassHostHeader: func(i bool) *bool { return &i }(true), + }, + }, + "default-external-svc-with-ipv6-8080": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://[2001:db8:85a3:8d3:1319:8a2e:370:7347]:8080", + }, + }, + PassHostHeader: func(i bool) *bool { return &i }(true), + }, + }, + "default-test-route-6b204d94623b3df4370c": { + Weighted: &dynamic.WeightedRoundRobin{ + Services: []dynamic.WRRService{ + { + Name: "default-whoami-ipv6-8080", + Weight: func(i int) *int { return &i }(1), + }, + { + Name: "default-external-svc-with-ipv6-8080", + Weight: func(i int) *int { return &i }(1), + }, + }, + }, + }, + }, + }, + TLS: &dynamic.TLSConfiguration{}, + }, + }, } for _, test := range testCases { diff --git a/pkg/provider/kubernetes/crd/kubernetes_udp.go b/pkg/provider/kubernetes/crd/kubernetes_udp.go index 18d4f4b65..2d02c8b5f 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_udp.go +++ b/pkg/provider/kubernetes/crd/kubernetes_udp.go @@ -4,6 +4,8 @@ import ( "context" "errors" "fmt" + "net" + "strconv" "github.com/traefik/traefik/v2/pkg/config/dynamic" "github.com/traefik/traefik/v2/pkg/log" @@ -121,7 +123,7 @@ func loadUDPServers(client Client, namespace string, svc v1alpha1.ServiceUDP) ([ var servers []dynamic.UDPServer if service.Spec.Type == corev1.ServiceTypeExternalName { servers = append(servers, dynamic.UDPServer{ - Address: fmt.Sprintf("%s:%d", service.Spec.ExternalName, portSpec.Port), + Address: net.JoinHostPort(service.Spec.ExternalName, strconv.Itoa(int(portSpec.Port))), }) } else { endpoints, endpointsExists, endpointsErr := client.GetEndpoints(namespace, svc.Name) @@ -152,7 +154,7 @@ func loadUDPServers(client Client, namespace string, svc v1alpha1.ServiceUDP) ([ for _, addr := range subset.Addresses { servers = append(servers, dynamic.UDPServer{ - Address: fmt.Sprintf("%s:%d", addr.IP, port), + Address: net.JoinHostPort(addr.IP, strconv.Itoa(int(port))), }) } } diff --git a/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-IPv6-endpoints_endpoint.yml b/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-IPv6-endpoints_endpoint.yml new file mode 100644 index 000000000..e70dd1a13 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-IPv6-endpoints_endpoint.yml @@ -0,0 +1,12 @@ +kind: Endpoints +apiVersion: v1 +metadata: + name: service-bar + namespace: testing + +subsets: +- addresses: + - ip: "2001:0db8:3c4d:0015:0000:0000:1a2f:1a2b" + ports: + - name: http + port: 8080 diff --git a/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-IPv6-endpoints_ingress.yml b/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-IPv6-endpoints_ingress.yml new file mode 100644 index 000000000..a12baef74 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-IPv6-endpoints_ingress.yml @@ -0,0 +1,18 @@ +kind: Ingress +apiVersion: networking.k8s.io/v1beta1 +metadata: + name: example.com + namespace: testing + +spec: + rules: + - http: + paths: + - path: /bar + backend: + serviceName: service-bar + servicePort: 8080 + - path: /foo + backend: + serviceName: service-foo + servicePort: 8080 diff --git a/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-IPv6-endpoints_service.yml b/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-IPv6-endpoints_service.yml new file mode 100644 index 000000000..9b8bfdb8b --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-IPv6-endpoints_service.yml @@ -0,0 +1,26 @@ +kind: Service +apiVersion: v1 +metadata: + name: service-bar + namespace: testing + +spec: + ports: + - name: http + port: 8080 + clusterIp: "fc00:f853:ccd:e793::1" + type: ClusterIP + +--- +kind: Service +apiVersion: v1 +metadata: + name: service-foo + namespace: testing + +spec: + ports: + - name: http + port: 8080 + type: ExternalName + externalName: "2001:0db8:3c4d:0015:0000:0000:1a2f:2a3b" diff --git a/pkg/provider/kubernetes/ingress/kubernetes.go b/pkg/provider/kubernetes/ingress/kubernetes.go index f5944edb1..8f06b214b 100644 --- a/pkg/provider/kubernetes/ingress/kubernetes.go +++ b/pkg/provider/kubernetes/ingress/kubernetes.go @@ -5,8 +5,10 @@ import ( "errors" "fmt" "math" + "net" "os" "sort" + "strconv" "strings" "time" @@ -479,9 +481,10 @@ func loadService(client Client, namespace string, backend networkingv1beta1.Ingr if service.Spec.Type == corev1.ServiceTypeExternalName { protocol := getProtocol(portSpec, portSpec.Name, svcConfig) + hostPort := net.JoinHostPort(service.Spec.ExternalName, strconv.Itoa(int(portSpec.Port))) svc.LoadBalancer.Servers = []dynamic.Server{ - {URL: fmt.Sprintf("%s://%s:%d", protocol, service.Spec.ExternalName, portSpec.Port)}, + {URL: fmt.Sprintf("%s://%s", protocol, hostPort)}, } return svc, nil @@ -516,8 +519,10 @@ func loadService(client Client, namespace string, backend networkingv1beta1.Ingr protocol := getProtocol(portSpec, portName, svcConfig) for _, addr := range subset.Addresses { + hostPort := net.JoinHostPort(addr.IP, strconv.Itoa(int(port))) + svc.LoadBalancer.Servers = append(svc.LoadBalancer.Servers, dynamic.Server{ - URL: fmt.Sprintf("%s://%s:%d", protocol, addr.IP, port), + URL: fmt.Sprintf("%s://%s", protocol, hostPort), }) } } diff --git a/pkg/provider/kubernetes/ingress/kubernetes_test.go b/pkg/provider/kubernetes/ingress/kubernetes_test.go index 253b62f47..ee9c98347 100644 --- a/pkg/provider/kubernetes/ingress/kubernetes_test.go +++ b/pkg/provider/kubernetes/ingress/kubernetes_test.go @@ -661,6 +661,47 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { }, }, }, + { + desc: "Ingress with IPv6 endpoints", + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{}, + HTTP: &dynamic.HTTPConfiguration{ + Middlewares: map[string]*dynamic.Middleware{}, + Routers: map[string]*dynamic.Router{ + "example-com-testing-bar": { + Rule: "PathPrefix(`/bar`)", + Service: "testing-service-bar-8080", + }, + "example-com-testing-foo": { + Rule: "PathPrefix(`/foo`)", + Service: "testing-service-foo-8080", + }, + }, + Services: map[string]*dynamic.Service{ + "testing-service-bar-8080": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://[2001:0db8:3c4d:0015:0000:0000:1a2f:1a2b]:8080", + }, + }, + PassHostHeader: Bool(true), + }, + }, + "testing-service-foo-8080": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://[2001:0db8:3c4d:0015:0000:0000:1a2f:2a3b]:8080", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + }, + }, + }, { desc: "TLS support", expected: &dynamic.Configuration{