diff --git a/cmd/k8s-operator/ingress.go b/cmd/k8s-operator/ingress.go index 4952e789f..8ea6d2f47 100644 --- a/cmd/k8s-operator/ingress.go +++ b/cmd/k8s-operator/ingress.go @@ -8,6 +8,7 @@ package main import ( "context" "fmt" + "net" "slices" "strings" "sync" @@ -364,7 +365,7 @@ func handlersForIngress(ctx context.Context, ing *networkingv1.Ingress, cl clien proto = "https+insecure://" } mak.Set(&handlers, path, &ipn.HTTPHandler{ - Proxy: proto + svc.Spec.ClusterIP + ":" + fmt.Sprint(port) + path, + Proxy: proto + net.JoinHostPort(svc.Spec.ClusterIP, fmt.Sprint(port)) + path, }) } addIngressBackend(ing.Spec.DefaultBackend, "/") diff --git a/cmd/k8s-operator/ingress_test.go b/cmd/k8s-operator/ingress_test.go index c2a1198cc..92065b43d 100644 --- a/cmd/k8s-operator/ingress_test.go +++ b/cmd/k8s-operator/ingress_test.go @@ -942,3 +942,91 @@ func TestTailscaleIngressWithHTTPRedirect(t *testing.T) { t.Errorf("incorrect status ports after removing redirect: got %v, want %v", ing.Status.LoadBalancer.Ingress[0].Ports, wantPorts) } } + +func TestTailscaleIngressIPv6(t *testing.T) { + fc := fake.NewFakeClient(ingressClass()) + zl, err := zap.NewDevelopment() + if err != nil { + t.Fatal(err) + } + + // Create a Service with an IPv6 ClusterIP + ipv6Svc := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-ipv6", + Namespace: "default", + }, + Spec: corev1.ServiceSpec{ + ClusterIP: "fda9:e575:6e22:2::25", + Ports: []corev1.ServicePort{ + { + Port: 2283, + Name: "http", + }, + }, + }, + } + mustCreate(t, fc, ipv6Svc) + + // Create an Ingress that routes to the IPv6 service + ing := &networkingv1.Ingress{ + TypeMeta: metav1.TypeMeta{Kind: "Ingress", APIVersion: "networking.k8s.io/v1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-ipv6", + Namespace: "default", + UID: "1234-UID-IPV6", + }, + Spec: networkingv1.IngressSpec{ + IngressClassName: new("tailscale"), + DefaultBackend: &networkingv1.IngressBackend{ + Service: &networkingv1.IngressServiceBackend{ + Name: "test-ipv6", + Port: networkingv1.ServiceBackendPort{ + Number: 2283, + }, + }, + }, + }, + } + mustCreate(t, fc, ing) + + ingR := &IngressReconciler{ + Client: fc, + ingressClassName: "tailscale", + ssr: &tailscaleSTSReconciler{ + Client: fc, + clients: tsclient.NewProvider(&fakeTSClient{}), + tsnetServer: &fakeTSNetServer{certDomains: []string{"test-host"}}, + defaultTags: []string{"tag:test"}, + operatorNamespace: "operator-ns", + proxyImage: "tailscale/tailscale", + }, + logger: zl.Sugar(), + } + + expectReconciled(t, ingR, "default", "test-ipv6") + + // Verify the generated serveConfig has properly bracketed IPv6 address + fullName, _ := findGenName(t, fc, "default", "test-ipv6", "ingress") + opts := configOpts{ + replicas: new(int32(1)), + stsName: "tailscale-ipv6-ingress-test-ipv6", + secretName: fullName, + namespace: "default", + parentType: "ingress", + hostname: "default-test-ipv6-ingress", + app: kubetypes.AppIngressResource, + serveConfig: &ipn.ServeConfig{ + TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}}, + Web: map[ipn.HostPort]*ipn.WebServerConfig{ + "${TS_CERT_DOMAIN}:443": {Handlers: map[string]*ipn.HTTPHandler{ + "/": {Proxy: "http://[fda9:e575:6e22:2::25]:2283/"}, + }}, + }, + }, + } + // expectedSecret hardcodes the parent-resource label to "test", so fix it for our IPv6 test + secret := expectedSecret(t, fc, opts) + secret.Labels[LabelParentName] = "test-ipv6" + expectEqual(t, fc, secret) +}