Repository: kubernetes-sigs/external-dns (language: go)

Issue:
'Nodes' source assumes all discovered IPs are IPv4
**What would you like to be added**:

The `node.go` source only knows how to make `A` endpoints: https://github.com/kubernetes-sigs/external-dns/blob/master/source/node.go#L129

The source should be extended to recognize IPv6 addresses and add them as `AAAA` endpoints instead of `A`.

I looked at providing a refactor but struggled to handle the code as-written, it wants to only think about one type of endpoint. This is the basic gist of the tests that should pass:

```
		{
			"node with both IPv4 and IPv6 addresses results in both A and AAAA records",
			"",
			"",
			"node1",
			[]v1.NodeAddress{{Type: v1.NodeInternalIP, Address: "2.3.4.5"}, {Type: v1.NodeInternalIP, Address: "2::5"}},
			map[string]string{},
			map[string]string{},
			[]*endpoint.Endpoint{
				{RecordType: "A", DNSName: "node1", Targets: endpoint.Targets{"2.3.4.5"}},
				{RecordType: "AAAA", DNSName: "node1", Targets: endpoint.Targets{"2::5"}},
			},
			false,
		},
		{
			"node with only IPv6 addresses results in only AAAA records",
			"",
			"",
			"node1",
			[]v1.NodeAddress{{Type: v1.NodeInternalIP, Address: "1::1"}, {Type: v1.NodeInternalIP, Address: "2::5"}},
			map[string]string{},
			map[string]string{},
			[]*endpoint.Endpoint{
				{RecordType: "AAAA", DNSName: "node1", Targets: endpoint.Targets{"1::1", "2::5"}},
			},
			false,
		},
```

**Why is this needed**:

I have geographically diverse nodes and external-dns's nodes source made for a pretty sweet Kubernetes-native dynamic DNS solution. Unfortunately I found a limit when I added a dual-stack node. (Even more important because the relevant ISP rotates the customer's IPv6 prefix every few days)

```
time="2020-11-28T21:08:34Z" level=info msg="Change zone: myzone batch #0"
time="2020-11-28T21:08:34Z" level=info msg="Del records: chicago.my.zone. A [8.8.8.8] 300"
time="2020-11-28T21:08:34Z" level=info msg="Del records: chicago.my.zone. TXT [\"heritage=external-dns,external-dns/owner=nodes-usc1f\"] 300"
time="2020-11-28T21:08:34Z" level=info msg="Add records: chicago.my.zone. A [8.8.8.8 2001::1] 300"
time="2020-11-28T21:08:34Z" level=info msg="Add records: chicago.my.zone. TXT [\"heritage=external-dns,external-dns/owner=nodes-usc1f\"] 300"
time="2020-11-28T21:08:34Z" level=error msg="googleapi: Error 400: Invalid value for 'entity.change.additions[0].rrdata[1]': '2001::1', invalid"
```


Discussion / hints:
Well, I wrote my intended sloppy patch on node.go (linked in above activity) and it satisfies my test cases but apparently external-dns has larger issues with IPv6 preparedness:

1. AAAA just isn't planned by default... https://github.com/kubernetes-sigs/external-dns/blob/master/plan/plan.go#L245
1. When planning multiple endpoints for one dns name; the planner just takes the "less" endpoint and rolls with it

So I suppose this is somewhat blocked on overall AAAA support in external-dns. I tried also patching AAAA into the plannable record passlist and now IPv6-only nodes ~~work correctly~~ keep re-adding the AAAA over and over, while dual-stack ones simply pick one family to use.

Also, related, another source that hardcodes A: https://github.com/kubernetes-sigs/external-dns/issues/1812
Issues go stale after 90d of inactivity.
Mark the issue as fresh with `/remove-lifecycle stale`.
Stale issues rot after an additional 30d of inactivity and eventually close.

If this issue is safe to close now please do so with `/close`.

Send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).
/lifecycle stale
/remove-lifecycle stale
Issues go stale after 90d of inactivity.
Mark the issue as fresh with `/remove-lifecycle stale`.
Stale issues rot after an additional 30d of inactivity and eventually close.

If this issue is safe to close now please do so with `/close`.

Send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).
/lifecycle stale
/lifecycle frozen

this is part of the IPv6 efforts and should therefore not be autoclosed.
Could this get some more traction as the IPv6 support in Kubernetes is moving to stable in the next release?
Is there any news on this feature?

Fix patch:
diff --git a/docs/tutorials/nodes.md b/docs/tutorials/nodes.md
index 46f21da5d8..b99a2f9ca3 100644
--- a/docs/tutorials/nodes.md
+++ b/docs/tutorials/nodes.md
@@ -3,8 +3,9 @@
 This tutorial describes how to configure ExternalDNS to use the cluster nodes as source.
 Using nodes (`--source=node`) as source is possible to synchronize a DNS zone with the nodes of a cluster.
 
-The node source adds an `A` record per each node `externalIP` (if not found, node's `internalIP` is used).
-The TTL record can be set with the `external-dns.alpha.kubernetes.io/ttl` node annotation.
+The node source adds an `A` record per each node `externalIP` (if not found, any IPv4 `internalIP` is used instead).
+It also adds an `AAAA` record per each node IPv6 `internalIP`.
+The TTL of the records can be set with the `external-dns.alpha.kubernetes.io/ttl` node annotation.
 
 ## Manifest (for cluster without RBAC enabled)
 
diff --git a/source/compatibility.go b/source/compatibility.go
index bc6e19abf9..1953b76ca9 100644
--- a/source/compatibility.go
+++ b/source/compatibility.go
@@ -157,11 +157,13 @@ func legacyEndpointsFromDNSControllerNodePortService(svc *v1.Service, sc *servic
 				continue
 			}
 			for _, address := range node.Status.Addresses {
-				if address.Type == v1.NodeExternalIP && isExternal {
-					endpoints = append(endpoints, endpoint.NewEndpoint(hostname, endpoint.RecordTypeA, address.Address))
+				recordType := suitableType(address.Address)
+				// IPv6 addresses are labeled as NodeInternalIP despite being usable externally as well.
+				if isExternal && (address.Type == v1.NodeExternalIP || (address.Type == v1.NodeInternalIP && recordType == endpoint.RecordTypeAAAA)) {
+					endpoints = append(endpoints, endpoint.NewEndpoint(hostname, recordType, address.Address))
 				}
-				if address.Type == v1.NodeInternalIP && isInternal {
-					endpoints = append(endpoints, endpoint.NewEndpoint(hostname, endpoint.RecordTypeA, address.Address))
+				if isInternal && address.Type == v1.NodeInternalIP {
+					endpoints = append(endpoints, endpoint.NewEndpoint(hostname, recordType, address.Address))
 				}
 			}
 		}
diff --git a/source/node.go b/source/node.go
index b0e672d732..5e287e9a00 100644
--- a/source/node.go
+++ b/source/node.go
@@ -88,7 +88,7 @@ func (ns *nodeSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, erro
 		return nil, err
 	}
 
-	endpoints := map[string]*endpoint.Endpoint{}
+	endpoints := map[endpointKey]*endpoint.Endpoint{}
 
 	// create endpoints for all nodes
 	for _, node := range nodes {
@@ -109,8 +109,7 @@ func (ns *nodeSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, erro
 
 		// create new endpoint with the information we already have
 		ep := &endpoint.Endpoint{
-			RecordType: "A", // hardcoded DNS record type
-			RecordTTL:  ttl,
+			RecordTTL: ttl,
 		}
 
 		if ns.fqdnTemplate != nil {
@@ -134,14 +133,19 @@ func (ns *nodeSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, erro
 			return nil, fmt.Errorf("failed to get node address from %s: %s", node.Name, err.Error())
 		}
 
-		ep.Targets = endpoint.Targets(addrs)
 		ep.Labels = endpoint.NewLabels()
-
-		log.Debugf("adding endpoint %s", ep)
-		if _, ok := endpoints[ep.DNSName]; ok {
-			endpoints[ep.DNSName].Targets = append(endpoints[ep.DNSName].Targets, ep.Targets...)
-		} else {
-			endpoints[ep.DNSName] = ep
+		for _, addr := range addrs {
+			log.Debugf("adding endpoint %s target %s", ep, addr)
+			key := endpointKey{
+				dnsName:    ep.DNSName,
+				recordType: suitableType(addr),
+			}
+			if _, ok := endpoints[key]; !ok {
+				epCopy := *ep
+				epCopy.RecordType = key.recordType
+				endpoints[key] = &epCopy
+			}
+			endpoints[key].Targets = append(endpoints[key].Targets, addr)
 		}
 	}
 
@@ -163,13 +167,18 @@ func (ns *nodeSource) nodeAddresses(node *v1.Node) ([]string, error) {
 		v1.NodeExternalIP: {},
 		v1.NodeInternalIP: {},
 	}
+	var ipv6Addresses []string
 
 	for _, addr := range node.Status.Addresses {
 		addresses[addr.Type] = append(addresses[addr.Type], addr.Address)
+		// IPv6 addresses are labeled as NodeInternalIP despite being usable externally as well.
+		if addr.Type == v1.NodeInternalIP && suitableType(addr.Address) == endpoint.RecordTypeAAAA {
+			ipv6Addresses = append(ipv6Addresses, addr.Address)
+		}
 	}
 
 	if len(addresses[v1.NodeExternalIP]) > 0 {
-		return addresses[v1.NodeExternalIP], nil
+		return append(addresses[v1.NodeExternalIP], ipv6Addresses...), nil
 	}
 
 	if len(addresses[v1.NodeInternalIP]) > 0 {
diff --git a/source/pod.go b/source/pod.go
index 36e6ffe50e..123468539f 100644
--- a/source/pod.go
+++ b/source/pod.go
@@ -82,7 +82,7 @@ func (ps *podSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error
 		return nil, err
 	}
 
-	domains := make(map[string][]string)
+	endpointMap := make(map[endpointKey][]string)
 	for _, pod := range pods {
 		if !pod.Spec.HostNetwork {
 			log.Debugf("skipping pod %s. hostNetwork=false", pod.Name)
@@ -90,50 +90,51 @@ func (ps *podSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error
 		}
 
 		if domain, ok := pod.Annotations[internalHostnameAnnotationKey]; ok {
-			if _, ok := domains[domain]; !ok {
-				domains[domain] = []string{}
-			}
-			domains[domain] = append(domains[domain], pod.Status.PodIP)
+			addToEndpointMap(endpointMap, domain, suitableType(pod.Status.PodIP), pod.Status.PodIP)
 		}
 
 		if domain, ok := pod.Annotations[hostnameAnnotationKey]; ok {
-			if _, ok := domains[domain]; !ok {
-				domains[domain] = []string{}
-			}
-
 			node, _ := ps.nodeInformer.Lister().Get(pod.Spec.NodeName)
 			for _, address := range node.Status.Addresses {
-				if address.Type == corev1.NodeExternalIP {
-					domains[domain] = append(domains[domain], address.Address)
+				recordType := suitableType(address.Address)
+				// IPv6 addresses are labeled as NodeInternalIP despite being usable externally as well.
+				if address.Type == corev1.NodeExternalIP || (address.Type == corev1.NodeInternalIP && recordType == endpoint.RecordTypeAAAA) {
+					addToEndpointMap(endpointMap, domain, recordType, address.Address)
 				}
 			}
 		}
 
 		if ps.compatibility == "kops-dns-controller" {
 			if domain, ok := pod.Annotations[kopsDNSControllerInternalHostnameAnnotationKey]; ok {
-				if _, ok := domains[domain]; !ok {
-					domains[domain] = []string{}
-				}
-				domains[domain] = append(domains[domain], pod.Status.PodIP)
+				addToEndpointMap(endpointMap, domain, suitableType(pod.Status.PodIP), pod.Status.PodIP)
 			}
 
 			if domain, ok := pod.Annotations[kopsDNSControllerHostnameAnnotationKey]; ok {
-				if _, ok := domains[domain]; !ok {
-					domains[domain] = []string{}
-				}
-
 				node, _ := ps.nodeInformer.Lister().Get(pod.Spec.NodeName)
 				for _, address := range node.Status.Addresses {
-					if address.Type == corev1.NodeExternalIP {
-						domains[domain] = append(domains[domain], address.Address)
+					recordType := suitableType(address.Address)
+					// IPv6 addresses are labeled as NodeInternalIP despite being usable externally as well.
+					if address.Type == corev1.NodeExternalIP || (address.Type == corev1.NodeInternalIP && recordType == endpoint.RecordTypeAAAA) {
+						addToEndpointMap(endpointMap, domain, recordType, address.Address)
 					}
 				}
 			}
 		}
 	}
 	endpoints := []*endpoint.Endpoint{}
-	for domain, targets := range domains {
-		endpoints = append(endpoints, endpoint.NewEndpoint(domain, endpoint.RecordTypeA, targets...))
+	for key, targets := range endpointMap {
+		endpoints = append(endpoints, endpoint.NewEndpoint(key.dnsName, key.recordType, targets...))
 	}
 	return endpoints, nil
 }
+
+func addToEndpointMap(endpointMap map[endpointKey][]string, domain string, recordType string, address string) {
+	key := endpointKey{
+		dnsName:    domain,
+		recordType: recordType,
+	}
+	if _, ok := endpointMap[key]; !ok {
+		endpointMap[key] = []string{}
+	}
+	endpointMap[key] = append(endpointMap[key], address)
+}
diff --git a/source/service.go b/source/service.go
index 925c327f24..58270cdcc3 100644
--- a/source/service.go
+++ b/source/service.go
@@ -216,7 +216,10 @@ func (sc *serviceSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, e
 		})
 		// Use stable sort to not disrupt the order of services
 		sort.SliceStable(endpoints, func(i, j int) bool {
-			return endpoints[i].DNSName < endpoints[j].DNSName
+			if endpoints[i].DNSName != endpoints[j].DNSName {
+				return endpoints[i].DNSName < endpoints[j].DNSName
+			}
+			return endpoints[i].RecordType < endpoints[j].RecordType
 		})
 		mergedEndpoints := []*endpoint.Endpoint{}
 		mergedEndpoints = append(mergedEndpoints, endpoints[0])
@@ -308,8 +311,8 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri
 							return endpoints
 						}
 						for _, address := range node.Status.Addresses {
-							if address.Type == v1.NodeExternalIP {
-								targets = endpoint.Targets{address.Address}
+							if address.Type == v1.NodeExternalIP || (address.Type == v1.NodeInternalIP && suitableType(address.Address) == endpoint.RecordTypeAAAA) {
+								targets = append(targets, address.Address)
 								log.Debugf("Generating matching endpoint %s with NodeExternalIP %s", headlessDomain, address.Address)
 							}
 						}
@@ -499,7 +502,7 @@ func (sc *serviceSource) generateEndpoints(svc *v1.Service, hostname string, pro
 			log.Errorf("Unable to extract targets from service %s/%s error: %v", svc.Namespace, svc.Name, err)
 			return endpoints
 		}
-		endpoints = append(endpoints, sc.extractNodePortEndpoints(svc, targets, hostname, ttl)...)
+		endpoints = append(endpoints, sc.extractNodePortEndpoints(svc, hostname, ttl)...)
 	case v1.ServiceTypeExternalName:
 		targets = append(targets, extractServiceExternalName(svc)...)
 	}
@@ -587,6 +590,7 @@ func (sc *serviceSource) extractNodePortTargets(svc *v1.Service) (endpoint.Targe
 	var (
 		internalIPs endpoint.Targets
 		externalIPs endpoint.Targets
+		ipv6IPs     endpoint.Targets
 		nodes       []*v1.Node
 		err         error
 	)
@@ -634,24 +638,27 @@ func (sc *serviceSource) extractNodePortTargets(svc *v1.Service) (endpoint.Targe
 				externalIPs = append(externalIPs, address.Address)
 			case v1.NodeInternalIP:
 				internalIPs = append(internalIPs, address.Address)
+				if suitableType(address.Address) == endpoint.RecordTypeAAAA {
+					ipv6IPs = append(ipv6IPs, address.Address)
+				}
 			}
 		}
 	}
 
 	access := getAccessFromAnnotations(svc.Annotations)
 	if access == "public" {
-		return externalIPs, nil
+		return append(externalIPs, ipv6IPs...), nil
 	}
 	if access == "private" {
 		return internalIPs, nil
 	}
 	if len(externalIPs) > 0 {
-		return externalIPs, nil
+		return append(externalIPs, ipv6IPs...), nil
 	}
 	return internalIPs, nil
 }
 
-func (sc *serviceSource) extractNodePortEndpoints(svc *v1.Service, nodeTargets endpoint.Targets, hostname string, ttl endpoint.TTL) []*endpoint.Endpoint {
+func (sc *serviceSource) extractNodePortEndpoints(svc *v1.Service, hostname string, ttl endpoint.TTL) []*endpoint.Endpoint {
 	var endpoints []*endpoint.Endpoint
 
 	for _, port := range svc.Spec.Ports {
diff --git a/source/source.go b/source/source.go
index 8573772c37..91b83bb4fc 100644
--- a/source/source.go
+++ b/source/source.go
@@ -86,6 +86,12 @@ type Source interface {
 	AddEventHandler(context.Context, func())
 }
 
+// endpointKey is the type of a map key for separating endpoints or targets.
+type endpointKey struct {
+	dnsName    string
+	recordType string
+}
+
 func getTTLFromAnnotations(annotations map[string]string) (endpoint.TTL, error) {
 	ttlNotConfigured := endpoint.TTL(0)
 	ttlAnnotation, exists := annotations[ttlAnnotationKey]


Test patch:
diff --git a/source/node_test.go b/source/node_test.go
index 901c1baa11..885d9f54e1 100644
--- a/source/node_test.go
+++ b/source/node_test.go
@@ -127,6 +127,19 @@ func testNodeSourceEndpoints(t *testing.T) {
 			},
 			false,
 		},
+		{
+			"ipv6 node with fqdn returns one endpoint",
+			"",
+			"",
+			"node1.example.org",
+			[]v1.NodeAddress{{Type: v1.NodeInternalIP, Address: "2001:DB8::8"}},
+			map[string]string{},
+			map[string]string{},
+			[]*endpoint.Endpoint{
+				{RecordType: "AAAA", DNSName: "node1.example.org", Targets: endpoint.Targets{"2001:DB8::8"}},
+			},
+			false,
+		},
 		{
 			"node with fqdn template returns endpoint with expanded hostname",
 			"",
@@ -166,6 +179,20 @@ func testNodeSourceEndpoints(t *testing.T) {
 			},
 			false,
 		},
+		{
+			"node with fqdn template returns two endpoints with dual-stack IP addresses and expanded hostname",
+			"",
+			"{{.Name}}.example.org",
+			"node1",
+			[]v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}, {Type: v1.NodeInternalIP, Address: "2001:DB8::8"}},
+			map[string]string{},
+			map[string]string{},
+			[]*endpoint.Endpoint{
+				{RecordType: "A", DNSName: "node1.example.org", Targets: endpoint.Targets{"1.2.3.4"}},
+				{RecordType: "AAAA", DNSName: "node1.example.org", Targets: endpoint.Targets{"2001:DB8::8"}},
+			},
+			false,
+		},
 		{
 			"node with both external and internal IP returns an endpoint with external IP",
 			"",
@@ -179,6 +206,20 @@ func testNodeSourceEndpoints(t *testing.T) {
 			},
 			false,
 		},
+		{
+			"node with both external, internal, and IPv6 IP returns endpoints with external IPs",
+			"",
+			"",
+			"node1",
+			[]v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}, {Type: v1.NodeInternalIP, Address: "2.3.4.5"}, {Type: v1.NodeInternalIP, Address: "2001:DB8::8"}},
+			map[string]string{},
+			map[string]string{},
+			[]*endpoint.Endpoint{
+				{RecordType: "A", DNSName: "node1", Targets: endpoint.Targets{"1.2.3.4"}},
+				{RecordType: "AAAA", DNSName: "node1", Targets: endpoint.Targets{"2001:DB8::8"}},
+			},
+			false,
+		},
 		{
 			"node with only internal IP returns an endpoint with internal IP",
 			"",
@@ -192,6 +233,20 @@ func testNodeSourceEndpoints(t *testing.T) {
 			},
 			false,
 		},
+		{
+			"node with only internal IPs returns endpoints with internal IPs",
+			"",
+			"",
+			"node1",
+			[]v1.NodeAddress{{Type: v1.NodeInternalIP, Address: "2.3.4.5"}, {Type: v1.NodeInternalIP, Address: "2001:DB8::8"}},
+			map[string]string{},
+			map[string]string{},
+			[]*endpoint.Endpoint{
+				{RecordType: "A", DNSName: "node1", Targets: endpoint.Targets{"2.3.4.5"}},
+				{RecordType: "AAAA", DNSName: "node1", Targets: endpoint.Targets{"2001:DB8::8"}},
+			},
+			false,
+		},
 		{
 			"node with neither external nor internal IP returns no endpoints",
 			"",
@@ -318,7 +373,7 @@ func testNodeSourceEndpoints(t *testing.T) {
 			false,
 		},
 		{
-			"node with nil Lables returns valid endpoint",
+			"node with nil Labels returns valid endpoint",
 			"",
 			"",
 			"node1",
diff --git a/source/pod_test.go b/source/pod_test.go
index c138aaf5fd..5a57aedc26 100644
--- a/source/pod_test.go
+++ b/source/pod_test.go
@@ -41,7 +41,7 @@ func TestPodSource(t *testing.T) {
 		pods            []*corev1.Pod
 	}{
 		{
-			"create records based on pod's external and internal IPs",
+			"create IPv4 records based on pod's external and internal IPs",
 			"",
 			"",
 			[]*endpoint.Endpoint{
@@ -111,7 +111,7 @@ func TestPodSource(t *testing.T) {
 			},
 		},
 		{
-			"create records based on pod's external and internal IPs using DNS Controller annotations",
+			"create IPv4 records based on pod's external and internal IPs using DNS Controller annotations",
 			"",
 			"kops-dns-controller",
 			[]*endpoint.Endpoint{
@@ -180,12 +180,149 @@ func TestPodSource(t *testing.T) {
 				},
 			},
 		},
+		{
+			"create IPv6 records based on pod's external and internal IPs",
+			"",
+			"",
+			[]*endpoint.Endpoint{
+				{DNSName: "a.foo.example.org", Targets: endpoint.Targets{"2001:DB8::1", "2001:DB8::2"}, RecordType: endpoint.RecordTypeAAAA},
+				{DNSName: "internal.a.foo.example.org", Targets: endpoint.Targets{"2001:DB8::1", "2001:DB8::2"}, RecordType: endpoint.RecordTypeAAAA},
+			},
+			false,
+			[]*corev1.Node{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "my-node1",
+					},
+					Status: corev1.NodeStatus{
+						Addresses: []corev1.NodeAddress{
+							{Type: corev1.NodeInternalIP, Address: "2001:DB8::1"},
+						},
+					},
+				},
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "my-node2",
+					},
+					Status: corev1.NodeStatus{
+						Addresses: []corev1.NodeAddress{
+							{Type: corev1.NodeInternalIP, Address: "2001:DB8::2"},
+						},
+					},
+				},
+			},
+			[]*corev1.Pod{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "my-pod1",
+						Namespace: "kube-system",
+						Annotations: map[string]string{
+							internalHostnameAnnotationKey: "internal.a.foo.example.org",
+							hostnameAnnotationKey:         "a.foo.example.org",
+						},
+					},
+					Spec: corev1.PodSpec{
+						HostNetwork: true,
+						NodeName:    "my-node1",
+					},
+					Status: corev1.PodStatus{
+						PodIP: "2001:DB8::1",
+					},
+				},
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "my-pod2",
+						Namespace: "kube-system",
+						Annotations: map[string]string{
+							internalHostnameAnnotationKey: "internal.a.foo.example.org",
+							hostnameAnnotationKey:         "a.foo.example.org",
+						},
+					},
+					Spec: corev1.PodSpec{
+						HostNetwork: true,
+						NodeName:    "my-node2",
+					},
+					Status: corev1.PodStatus{
+						PodIP: "2001:DB8::2",
+					},
+				},
+			},
+		},
+		{
+			"create IPv6 records based on pod's external and internal IPs using DNS Controller annotations",
+			"",
+			"kops-dns-controller",
+			[]*endpoint.Endpoint{
+				{DNSName: "a.foo.example.org", Targets: endpoint.Targets{"2001:DB8::1", "2001:DB8::2"}, RecordType: endpoint.RecordTypeAAAA},
+				{DNSName: "internal.a.foo.example.org", Targets: endpoint.Targets{"2001:DB8::1", "2001:DB8::2"}, RecordType: endpoint.RecordTypeAAAA},
+			},
+			false,
+			[]*corev1.Node{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "my-node1",
+					},
+					Status: corev1.NodeStatus{
+						Addresses: []corev1.NodeAddress{
+							{Type: corev1.NodeInternalIP, Address: "2001:DB8::1"},
+						},
+					},
+				},
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "my-node2",
+					},
+					Status: corev1.NodeStatus{
+						Addresses: []corev1.NodeAddress{
+							{Type: corev1.NodeInternalIP, Address: "2001:DB8::2"},
+						},
+					},
+				},
+			},
+			[]*corev1.Pod{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "my-pod1",
+						Namespace: "kube-system",
+						Annotations: map[string]string{
+							kopsDNSControllerInternalHostnameAnnotationKey: "internal.a.foo.example.org",
+							kopsDNSControllerHostnameAnnotationKey:         "a.foo.example.org",
+						},
+					},
+					Spec: corev1.PodSpec{
+						HostNetwork: true,
+						NodeName:    "my-node1",
+					},
+					Status: corev1.PodStatus{
+						PodIP: "2001:DB8::1",
+					},
+				},
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "my-pod2",
+						Namespace: "kube-system",
+						Annotations: map[string]string{
+							kopsDNSControllerInternalHostnameAnnotationKey: "internal.a.foo.example.org",
+							kopsDNSControllerHostnameAnnotationKey:         "a.foo.example.org",
+						},
+					},
+					Spec: corev1.PodSpec{
+						HostNetwork: true,
+						NodeName:    "my-node2",
+					},
+					Status: corev1.PodStatus{
+						PodIP: "2001:DB8::2",
+					},
+				},
+			},
+		},
 		{
 			"create multiple records",
 			"",
 			"",
 			[]*endpoint.Endpoint{
 				{DNSName: "a.foo.example.org", Targets: endpoint.Targets{"54.10.11.1"}, RecordType: endpoint.RecordTypeA},
+				{DNSName: "a.foo.example.org", Targets: endpoint.Targets{"2001:DB8::1"}, RecordType: endpoint.RecordTypeAAAA},
 				{DNSName: "b.foo.example.org", Targets: endpoint.Targets{"54.10.11.2"}, RecordType: endpoint.RecordTypeA},
 			},
 			false,
@@ -197,6 +334,7 @@ func TestPodSource(t *testing.T) {
 					Status: corev1.NodeStatus{
 						Addresses: []corev1.NodeAddress{
 							{Type: corev1.NodeExternalIP, Address: "54.10.11.1"},
+							{Type: corev1.NodeInternalIP, Address: "2001:DB8::1"},
 							{Type: corev1.NodeInternalIP, Address: "10.0.1.1"},
 						},
 					},
diff --git a/source/service_test.go b/source/service_test.go
index 98b400fca3..45737adb7c 100644
--- a/source/service_test.go
+++ b/source/service_test.go
@@ -1518,6 +1518,7 @@ func TestServiceSourceNodePortServices(t *testing.T) {
 			expected: []*endpoint.Endpoint{
 				{DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV},
 				{DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.1", "54.10.11.2"}, RecordType: endpoint.RecordTypeA},
+				{DNSName: "foo.example.org", Targets: endpoint.Targets{"2001:DB8::1", "2001:DB8::2"}, RecordType: endpoint.RecordTypeAAAA},
 			},
 			nodes: []*v1.Node{{
 				ObjectMeta: metav1.ObjectMeta{
@@ -1527,6 +1528,7 @@ func TestServiceSourceNodePortServices(t *testing.T) {
 					Addresses: []v1.NodeAddress{
 						{Type: v1.NodeExternalIP, Address: "54.10.11.1"},
 						{Type: v1.NodeInternalIP, Address: "10.0.1.1"},
+						{Type: v1.NodeInternalIP, Address: "2001:DB8::1"},
 					},
 				},
 			}, {
@@ -1537,6 +1539,7 @@ func TestServiceSourceNodePortServices(t *testing.T) {
 					Addresses: []v1.NodeAddress{
 						{Type: v1.NodeExternalIP, Address: "54.10.11.2"},
 						{Type: v1.NodeInternalIP, Address: "10.0.1.2"},
+						{Type: v1.NodeInternalIP, Address: "2001:DB8::2"},
 					},
 				},
 			}},
@@ -1559,6 +1562,7 @@ func TestServiceSourceNodePortServices(t *testing.T) {
 					Addresses: []v1.NodeAddress{
 						{Type: v1.NodeExternalIP, Address: "54.10.11.1"},
 						{Type: v1.NodeInternalIP, Address: "10.0.1.1"},
+						{Type: v1.NodeInternalIP, Address: "2001:DB8::1"},
 					},
 				},
 			}, {
@@ -1569,6 +1573,7 @@ func TestServiceSourceNodePortServices(t *testing.T) {
 					Addresses: []v1.NodeAddress{
 						{Type: v1.NodeExternalIP, Address: "54.10.11.2"},
 						{Type: v1.NodeInternalIP, Address: "10.0.1.2"},
+						{Type: v1.NodeInternalIP, Address: "2001:DB8::2"},
 					},
 				},
 			}},
@@ -1584,6 +1589,7 @@ func TestServiceSourceNodePortServices(t *testing.T) {
 			expected: []*endpoint.Endpoint{
 				{DNSName: "_foo._tcp.foo.bar.example.com", Targets: endpoint.Targets{"0 50 30192 foo.bar.example.com"}, RecordType: endpoint.RecordTypeSRV},
 				{DNSName: "foo.bar.example.com", Targets: endpoint.Targets{"54.10.11.1", "54.10.11.2"}, RecordType: endpoint.RecordTypeA},
+				{DNSName: "foo.bar.example.com", Targets: endpoint.Targets{"2001:DB8::1", "2001:DB8::2"}, RecordType: endpoint.RecordTypeAAAA},
 			},
 			nodes: []*v1.Node{{
 				ObjectMeta: metav1.ObjectMeta{
@@ -1593,6 +1599,7 @@ func TestServiceSourceNodePortServices(t *testing.T) {
 					Addresses: []v1.NodeAddress{
 						{Type: v1.NodeExternalIP, Address: "54.10.11.1"},
 						{Type: v1.NodeInternalIP, Address: "10.0.1.1"},
+						{Type: v1.NodeInternalIP, Address: "2001:DB8::1"},
 					},
 				},
 			}, {
@@ -1603,6 +1610,7 @@ func TestServiceSourceNodePortServices(t *testing.T) {
 					Addresses: []v1.NodeAddress{
 						{Type: v1.NodeExternalIP, Address: "54.10.11.2"},
 						{Type: v1.NodeInternalIP, Address: "10.0.1.2"},
+						{Type: v1.NodeInternalIP, Address: "2001:DB8::2"},
 					},
 				},
 			}},
@@ -1619,6 +1627,7 @@ func TestServiceSourceNodePortServices(t *testing.T) {
 			expected: []*endpoint.Endpoint{
 				{DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV},
 				{DNSName: "foo.example.org", Targets: endpoint.Targets{"10.0.1.1", "10.0.1.2"}, RecordType: endpoint.RecordTypeA},
+				{DNSName: "foo.example.org", Targets: endpoint.Targets{"2001:DB8::1", "2001:DB8::2"}, RecordType: endpoint.RecordTypeAAAA},
 			},
 			nodes: []*v1.Node{{
 				ObjectMeta: metav1.ObjectMeta{
@@ -1627,6 +1636,7 @@ func TestServiceSourceNodePortServices(t *testing.T) {
 				Status: v1.NodeStatus{
 					Addresses: []v1.NodeAddress{
 						{Type: v1.NodeInternalIP, Address: "10.0.1.1"},
+						{Type: v1.NodeInternalIP, Address: "2001:DB8::1"},
 					},
 				},
 			}, {
@@ -1636,6 +1646,7 @@ func TestServiceSourceNodePortServices(t *testing.T) {
 				Status: v1.NodeStatus{
 					Addresses: []v1.NodeAddress{
 						{Type: v1.NodeInternalIP, Address: "10.0.1.2"},
+						{Type: v1.NodeInternalIP, Address: "2001:DB8::2"},
 					},
 				},
 			}},
@@ -1652,6 +1663,7 @@ func TestServiceSourceNodePortServices(t *testing.T) {
 			expected: []*endpoint.Endpoint{
 				{DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV},
 				{DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.2"}, RecordType: endpoint.RecordTypeA},
+				{DNSName: "foo.example.org", Targets: endpoint.Targets{"2001:DB8::2"}, RecordType: endpoint.RecordTypeAAAA},
 			},
 			nodes: []*v1.Node{{
 				ObjectMeta: metav1.ObjectMeta{
@@ -1661,6 +1673,7 @@ func TestServiceSourceNodePortServices(t *testing.T) {
 					Addresses: []v1.NodeAddress{
 						{Type: v1.NodeExternalIP, Address: "54.10.11.1"},
 						{Type: v1.NodeInternalIP, Address: "10.0.1.1"},
+						{Type: v1.NodeInternalIP, Address: "2001:DB8::1"},
 					},
 				},
 			}, {
@@ -1671,6 +1684,7 @@ func TestServiceSourceNodePortServices(t *testing.T) {
 					Addresses: []v1.NodeAddress{
 						{Type: v1.NodeExternalIP, Address: "54.10.11.2"},
 						{Type: v1.NodeInternalIP, Address: "10.0.1.2"},
+						{Type: v1.NodeInternalIP, Address: "2001:DB8::2"},
 					},
 				},
 			}},
@@ -1691,6 +1705,7 @@ func TestServiceSourceNodePortServices(t *testing.T) {
 			expected: []*endpoint.Endpoint{
 				{DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV},
 				{DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.2"}, RecordType: endpoint.RecordTypeA},
+				{DNSName: "foo.example.org", Targets: endpoint.Targets{"2001:DB8::2"}, RecordType: endpoint.RecordTypeAAAA},
 			},
 			nodes: []*v1.Node{{
 				ObjectMeta: metav1.ObjectMeta{
@@ -1700,6 +1715,7 @@ func TestServiceSourceNodePortServices(t *testing.T) {
 					Addresses: []v1.NodeAddress{
 						{Type: v1.NodeExternalIP, Address: "54.10.11.1"},
 						{Type: v1.NodeInternalIP, Address: "10.0.1.1"},
+						{Type: v1.NodeInternalIP, Address: "2001:DB8::1"},
 					},
 				},
 			}, {
@@ -1710,6 +1726,7 @@ func TestServiceSourceNodePortServices(t *testing.T) {
 					Addresses: []v1.NodeAddress{
 						{Type: v1.NodeExternalIP, Address: "54.10.11.2"},
 						{Type: v1.NodeInternalIP, Address: "10.0.1.2"},
+						{Type: v1.NodeInternalIP, Address: "2001:DB8::2"},
 					},
 				},
 			}},
@@ -1731,6 +1748,7 @@ func TestServiceSourceNodePortServices(t *testing.T) {
 			expected: []*endpoint.Endpoint{
 				{DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV},
 				{DNSName: "foo.example.org", Targets: endpoint.Targets{"10.0.1.1", "10.0.1.2"}, RecordType: endpoint.RecordTypeA},
+				{DNSName: "foo.example.org", Targets: endpoint.Targets{"2001:DB8::1", "2001:DB8::2"}, RecordType: endpoint.RecordTypeAAAA},
 			},
 			nodes: []*v1.Node{{
 				ObjectMeta: metav1.ObjectMeta{
@@ -1740,6 +1758,7 @@ func TestServiceSourceNodePortServices(t *testing.T) {
 					Addresses: []v1.NodeAddress{
 						{Type: v1.NodeExternalIP, Address: "54.10.11.1"},
 						{Type: v1.NodeInternalIP, Address: "10.0.1.1"},
+						{Type: v1.NodeInternalIP, Address: "2001:DB8::1"},
 					},
 				},
 			}, {
@@ -1750,6 +1769,7 @@ func TestServiceSourceNodePortServices(t *testing.T) {
 					Addresses: []v1.NodeAddress{
 						{Type: v1.NodeExternalIP, Address: "54.10.11.2"},
 						{Type: v1.NodeInternalIP, Address: "10.0.1.2"},
+						{Type: v1.NodeInternalIP, Address: "2001:DB8::2"},
 					},
 				},
 			}},
@@ -1768,6 +1788,7 @@ func TestServiceSourceNodePortServices(t *testing.T) {
 			expected: []*endpoint.Endpoint{
 				{DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV},
 				{DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.1", "54.10.11.2"}, RecordType: endpoint.RecordTypeA},
+				{DNSName: "foo.example.org", Targets: endpoint.Targets{"2001:DB8::1", "2001:DB8::2"}, RecordType: endpoint.RecordTypeAAAA},
 			},
 			nodes: []*v1.Node{{
 				ObjectMeta: metav1.ObjectMeta{
@@ -1777,6 +1798,7 @@ func TestServiceSourceNodePortServices(t *testing.T) {
 					Addresses: []v1.NodeAddress{
 						{Type: v1.NodeExternalIP, Address: "54.10.11.1"},
 						{Type: v1.NodeInternalIP, Address: "10.0.1.1"},
+						{Type: v1.NodeInternalIP, Address: "2001:DB8::1"},
 					},
 				},
 			}, {
@@ -1787,6 +1809,7 @@ func TestServiceSourceNodePortServices(t *testing.T) {
 					Addresses: []v1.NodeAddress{
 						{Type: v1.NodeExternalIP, Address: "54.10.11.2"},
 						{Type: v1.NodeInternalIP, Address: "10.0.1.2"},
+						{Type: v1.NodeInternalIP, Address: "2001:DB8::2"},
 					},
 				},
 			}},
@@ -1804,7 +1827,9 @@ func TestServiceSourceNodePortServices(t *testing.T) {
 			},
 			expected: []*endpoint.Endpoint{
 				{DNSName: "internal.foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"10.0.1.1"}},
+				{DNSName: "internal.foo.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:DB8::1"}},
 				{DNSName: "internal.bar.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"10.0.1.1"}},
+				{DNSName: "internal.bar.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:DB8::1"}},
 			},
 			nodes: []*v1.Node{{
 				ObjectMeta: metav1.ObjectMeta{
@@ -1817,6 +1842,7 @@ func TestServiceSourceNodePortServices(t *testing.T) {
 					Addresses: []v1.NodeAddress{
 						{Type: v1.NodeExternalIP, Address: "54.10.11.1"},
 						{Type: v1.NodeInternalIP, Address: "10.0.1.1"},
+						{Type: v1.NodeInternalIP, Address: "2001:DB8::1"},
 					},
 				},
 			}, {
@@ -1830,6 +1856,7 @@ func TestServiceSourceNodePortServices(t *testing.T) {
 					Addresses: []v1.NodeAddress{
 						{Type: v1.NodeExternalIP, Address: "54.10.11.2"},
 						{Type: v1.NodeInternalIP, Address: "10.0.1.2"},
+						{Type: v1.NodeInternalIP, Address: "2001:DB8::2"},
 					},
 				},
 			}},
@@ -1846,7 +1873,9 @@ func TestServiceSourceNodePortServices(t *testing.T) {
 			},
 			expected: []*endpoint.Endpoint{
 				{DNSName: "internal.foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"10.0.1.1", "10.0.1.2"}},
+				{DNSName: "internal.foo.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:DB8::1", "2001:DB8::2"}},
 				{DNSName: "internal.bar.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"10.0.1.1", "10.0.1.2"}},
+				{DNSName: "internal.bar.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:DB8::1", "2001:DB8::2"}},
 			},
 			nodes: []*v1.Node{{
 				ObjectMeta: metav1.ObjectMeta{
@@ -1859,6 +1888,7 @@ func TestServiceSourceNodePortServices(t *testing.T) {
 					Addresses: []v1.NodeAddress{
 						{Type: v1.NodeExternalIP, Address: "54.10.11.1"},
 						{Type: v1.NodeInternalIP, Address: "10.0.1.1"},
+						{Type: v1.NodeInternalIP, Address: "2001:DB8::1"},
 					},
 				},
 			}, {
@@ -1872,6 +1902,7 @@ func TestServiceSourceNodePortServices(t *testing.T) {
 					Addresses: []v1.NodeAddress{
 						{Type: v1.NodeExternalIP, Address: "54.10.11.2"},
 						{Type: v1.NodeInternalIP, Address: "10.0.1.2"},
+						{Type: v1.NodeInternalIP, Address: "2001:DB8::2"},
 					},
 				},
 			}},
@@ -1888,7 +1919,9 @@ func TestServiceSourceNodePortServices(t *testing.T) {
 			},
 			expected: []*endpoint.Endpoint{
 				{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"54.10.11.1", "54.10.11.2"}},
+				{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:DB8::1", "2001:DB8::2"}},
 				{DNSName: "bar.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"54.10.11.1", "54.10.11.2"}},
+				{DNSName: "bar.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:DB8::1", "2001:DB8::2"}},
 			},
 			nodes: []*v1.Node{{
 				ObjectMeta: metav1.ObjectMeta{
@@ -1901,6 +1934,7 @@ func TestServiceSourceNodePortServices(t *testing.T) {
 					Addresses: []v1.NodeAddress{
 						{Type: v1.NodeExternalIP, Address: "54.10.11.1"},
 						{Type: v1.NodeInternalIP, Address: "10.0.1.1"},
+						{Type: v1.NodeInternalIP, Address: "2001:DB8::1"},
 					},
 				},
 			}, {
@@ -1914,6 +1948,7 @@ func TestServiceSourceNodePortServices(t *testing.T) {
 					Addresses: []v1.NodeAddress{
 						{Type: v1.NodeExternalIP, Address: "54.10.11.2"},
 						{Type: v1.NodeInternalIP, Address: "10.0.1.2"},
+						{Type: v1.NodeInternalIP, Address: "2001:DB8::2"},
 					},
 				},
 			}},
@@ -1942,6 +1977,7 @@ func TestServiceSourceNodePortServices(t *testing.T) {
 					Addresses: []v1.NodeAddress{
 						{Type: v1.NodeExternalIP, Address: "54.10.11.1"},
 						{Type: v1.NodeInternalIP, Address: "10.0.1.1"},
+						{Type: v1.NodeInternalIP, Address: "2001:DB8::1"},
 					},
 				},
 			}, {
@@ -1955,6 +1991,7 @@ func TestServiceSourceNodePortServices(t *testing.T) {
 					Addresses: []v1.NodeAddress{
 						{Type: v1.NodeExternalIP, Address: "54.10.11.2"},
 						{Type: v1.NodeInternalIP, Address: "10.0.1.2"},
+						{Type: v1.NodeInternalIP, Address: "2001:DB8::2"},
 					},
 				},
 			}},
diff --git a/source/shared_test.go b/source/shared_test.go
index 9cdc58d22a..11828dbe2e 100644
--- a/source/shared_test.go
+++ b/source/shared_test.go
@@ -29,11 +29,14 @@ func sortEndpoints(endpoints []*endpoint.Endpoint) {
 		sort.Strings([]string(ep.Targets))
 	}
 	sort.Slice(endpoints, func(i, k int) bool {
-		// Sort by DNSName and Targets
+		// Sort by DNSName, RecordType, and Targets
 		ei, ek := endpoints[i], endpoints[k]
 		if ei.DNSName != ek.DNSName {
 			return ei.DNSName < ek.DNSName
 		}
+		if ei.RecordType != ek.RecordType {
+			return ei.RecordType < ek.RecordType
+		}
 		// Targets are sorted ahead of time.
 		for j, ti := range ei.Targets {
 			if j >= len(ek.Targets) {