Skip to content

Commit

Permalink
Merge pull request #512 from dpasiukevich/ipv6_ptr
Browse files Browse the repository at this point in the history
Add support for IPv6 PTR records
  • Loading branch information
k8s-ci-robot authored Jul 27, 2022
2 parents 4d3f36d + e889983 commit 1fb1456
Show file tree
Hide file tree
Showing 4 changed files with 233 additions and 23 deletions.
6 changes: 3 additions & 3 deletions pkg/dns/dns.go
Original file line number Diff line number Diff line change
Expand Up @@ -824,9 +824,9 @@ func (kd *KubeDNS) ReverseRecord(name string) (*skymsg.Service, error) {
klog.V(3).Infof("Query for ReverseRecord %q", name)

// if portalIP is not a valid IP, the reverseRecordMap lookup will fail
portalIP, ok := util.ExtractIP(name)
if !ok {
return nil, fmt.Errorf("does not support reverse lookup for %s", name)
portalIP, err := util.ExtractIP(name)
if err != nil {
return nil, fmt.Errorf("failed to extract ip for record %q: %w", name, err)
}

kd.cacheLock.RLock()
Expand Down
51 changes: 39 additions & 12 deletions pkg/dns/dns_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ func TestUnnamedSinglePortService(t *testing.T) {
// Delete the service
kd.removeService(s)
assertNoDNSForClusterIP(t, kd, s)
assertNoReverseRecord(t, kd, s)
assertNoReverseRecord(t, tt.name, kd, s)
}
}

Expand Down Expand Up @@ -1115,19 +1115,46 @@ func assertDNSForClusterIP(t *testing.T, testCase string, kd *KubeDNS, s *v1.Ser
}

func assertReverseRecord(t *testing.T, testCase string, kd *KubeDNS, s *v1.Service) {
segments := util.ReverseArray(strings.Split(s.Spec.ClusterIP, "."))
reverseLookup := fmt.Sprintf("%s%s", strings.Join(segments, "."), util.ArpaSuffix)
reverseRecord, err := kd.ReverseRecord(reverseLookup)
require.NoError(t, err, testCase)
assert.Equal(t, getServiceFQDN(kd.domain, s), reverseRecord.Host, testCase)
for _, ip := range util.GetClusterIPs(s) {
reverseLookup, err := makePTRRecord(ip)
require.NoError(t, err, testCase)
reverseRecord, err := kd.ReverseRecord(reverseLookup)
require.NoError(t, err, testCase)
assert.Equal(t, getServiceFQDN(kd.domain, s), reverseRecord.Host, testCase)
}
}

func assertNoReverseRecord(t *testing.T, kd *KubeDNS, s *v1.Service) {
segments := util.ReverseArray(strings.Split(s.Spec.ClusterIP, "."))
reverseLookup := fmt.Sprintf("%s%s", strings.Join(segments, "."), util.ArpaSuffix)
reverseRecord, err := kd.ReverseRecord(reverseLookup)
require.Error(t, err)
require.Nil(t, reverseRecord)
func assertNoReverseRecord(t *testing.T, testCase string, kd *KubeDNS, s *v1.Service) {
for _, ip := range util.GetClusterIPs(s) {
reverseLookup, err := makePTRRecord(ip)
require.NoError(t, err, testCase)
reverseRecord, err := kd.ReverseRecord(reverseLookup)
require.Error(t, err)
require.Nil(t, reverseRecord)
}
}

// 10.47.32.22 -> 22.32.47.10.in-addr.arpa.
// 4321:0:1:2:3:4:567:89ab -> b.a.9.8.7.6.5.0.4.0.0.0.3.0.0.0.2.0.0.0.1.0.0.0.0.0.0.0.1.2.3.4.ip6.arpa.
func makePTRRecord(ip string) (string, error) {
if net.ParseIP(ip).To4() != nil {
segments := util.ReverseArray(strings.Split(ip, "."))
return fmt.Sprintf("%s%s", strings.Join(segments, "."), util.ArpaSuffix), nil
}

const ipv6nibbleCount = 32

if ipv6 := net.ParseIP(ip).To16(); ipv6 != nil {
b := make([]string, 0, ipv6nibbleCount)
for i := 0; i < len(ipv6); i += 2 {
for _, c := range fmt.Sprintf("%04x", int64(ipv6[i])<<8|int64(ipv6[i+1])) {
b = append(b, string(c))
}
}
return fmt.Sprintf("%s%s", strings.Join(util.ReverseArray(b), "."), util.ArpaSuffixV6), nil
}

return "", fmt.Errorf("incorrect ip adress: %q", ip)
}

func getEquivalentQueries(serviceFQDN, namespace string) []string {
Expand Down
81 changes: 73 additions & 8 deletions pkg/dns/util/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import (
const (
// ArpaSuffix is the standard suffix for PTR IP reverse lookups.
ArpaSuffix = ".in-addr.arpa."
// ArpaSuffixV6 is the suffix for PTR IPv6 reverse lookups.
ArpaSuffixV6 = ".ip6.arpa."
// defaultPriority used for service records
defaultPriority = 10
// defaultWeight used for service records
Expand All @@ -41,15 +43,64 @@ const (

// ExtractIP turns a standard PTR reverse record lookup name
// into an IP address
func ExtractIP(reverseName string) (string, bool) {
if !strings.HasSuffix(reverseName, ArpaSuffix) {
return "", false
// Returns "", error if the reverseName is not a valid PTR lookup name
func ExtractIP(reverseName string) (string, error) {
if strings.HasSuffix(reverseName, ArpaSuffix) {
ip, err := extractIPv4(strings.TrimSuffix(reverseName, ArpaSuffix))
if err != nil {
return "", fmt.Errorf("incorrect PTR IPv4 %q: %w", reverseName, err)
}
return ip, nil
}
search := strings.TrimSuffix(reverseName, ArpaSuffix)

if strings.HasSuffix(reverseName, ArpaSuffixV6) {
ip, err := extractIPv6(strings.TrimSuffix(reverseName, ArpaSuffixV6))
if err != nil {
return "", fmt.Errorf("incorrect PTR IPv6 %q: %w", reverseName, err)
}
return ip, nil
}

return "", fmt.Errorf("incorrect PTR: %q", reverseName)
}

// extractIPv4 turns a standard PTR reverse record lookup name
// into an IP address
func extractIPv4(reverseName string) (string, error) {
// reverse the segments and then combine them
segments := ReverseArray(strings.Split(search, "."))
return strings.Join(segments, "."), true
segments := ReverseArray(strings.Split(reverseName, "."))

ip := net.ParseIP(strings.Join(segments, ".")).To4()
if ip == nil {
return "", fmt.Errorf("failed to parse IPv4 reverse name: %q", reverseName)
}
return ip.String(), nil
}

// extractIPv6 turns a IPv6 PTR reverse record lookup name
// into an IPv6 address according to RFC3596
// b.a.9.8.7.6.5.0.4.0.0.0.3.0.0.0.2.0.0.0.1.0.0.0.0.0.0.0.1.2.3.4.ip6.arpa.
// is reversed to 4321:0:1:2:3:4:567:89ab
func extractIPv6(reverseName string) (string, error) {
segments := ReverseArray(strings.Split(reverseName, "."))

// IPv6nibbleCount is the expected number of nibbles in IPv6 PTR record as defined in rfc3596
const ipv6nibbleCount = 32

if len(segments) != ipv6nibbleCount {
return "", fmt.Errorf("incorrect number of segments in IPv6 PTR: %v", len(segments))
}

var slice6 []string
for i := 0; i < len(segments); i += 4 {
slice6 = append(slice6, strings.Join(segments[i:i+4], ""))
}

ip := net.ParseIP(strings.Join(slice6, ":")).To16()
if ip == nil {
return "", fmt.Errorf("failed to parse IPv6 segments: %v", slice6)
}
return ip.String(), nil
}

// ReverseArray reverses an array.
Expand Down Expand Up @@ -119,8 +170,22 @@ func IsServiceIPSet(service *corev1.Service) bool {

// GetClusterIPs returns IPs set for the service
func GetClusterIPs(service *corev1.Service) []string {
clusterIPs := []string{service.Spec.ClusterIP}
if len(service.Spec.ClusterIPs) > 0 {
return service.Spec.ClusterIPs
clusterIPs = service.Spec.ClusterIPs
}

// Same IPv6 could be represented differently (as from rfc5952):
// 2001:db8:0:0:aaaa::1
// 2001:db8::aaaa:0:0:1
// 2001:db8:0::aaaa:0:0:1
// net.ParseIP(ip).String() output is used as a normalization form
// for all cases above it returns 2001:db8::aaaa:0:0:1
// without the normalization there could be mismatches in key lookups e.g. for PTR
normalized := make([]string, 0, len(clusterIPs))
for _, ip := range clusterIPs {
normalized = append(normalized, net.ParseIP(ip).String())
}
return []string{service.Spec.ClusterIP}

return normalized
}
118 changes: 118 additions & 0 deletions pkg/dns/util/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ package util

import (
"testing"

"github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1"
)

func TestValidateNameserverIpAndPort(t *testing.T) {
Expand Down Expand Up @@ -46,3 +49,118 @@ func TestValidateNameserverIpAndPort(t *testing.T) {
}
}
}

func TestExtractIP(t *testing.T) {
for _, tc := range []struct {
testName string
ptr string
wantIP string
wantErr bool
errMsg string
}{
{
testName: "valid IPv4 ptr",
ptr: "255.2.0.192.in-addr.arpa.",
wantIP: "192.0.2.255",
wantErr: false,
},
{
testName: "valid IPv6 ptr",
ptr: "b.a.9.8.7.6.5.0.4.0.0.0.3.0.0.0.2.0.0.0.0.0.0.0.0.0.0.0.1.2.3.4.ip6.arpa.",
wantIP: "4321::2:3:4:567:89ab",
wantErr: false,
},
{
testName: "valid IPv6 ptr has :0: instead of ::",
ptr: "b.a.9.8.7.6.5.0.4.0.0.0.3.0.0.0.2.0.0.0.1.0.0.0.0.0.0.0.1.2.3.4.ip6.arpa.",
wantIP: "4321:0:1:2:3:4:567:89ab",
wantErr: false,
},
{
testName: "empty ptr",
wantErr: true,
errMsg: "incorrect PTR: \"\"",
},
{
testName: "IPv4 ptr with incorrect suffix",
ptr: "255.2.0.192.ip6.arpa.",
wantErr: true,
errMsg: "incorrect PTR IPv6 \"255.2.0.192.ip6.arpa.\": incorrect number of segments in IPv6 PTR: 4",
},
{
testName: "incorrect IPv4 ptr",
ptr: "255.2.0.322.in-addr.arpa.",
wantErr: true,
errMsg: "incorrect PTR IPv4 \"255.2.0.322.in-addr.arpa.\": failed to parse IPv4 reverse name: \"255.2.0.322\"",
},
{
testName: "IPv6 ptr with incorrect suffix",
ptr: "b.a.9.8.7.6.5.0.4.0.0.0.3.0.0.0.2.0.0.0.0.0.0.0.0.0.0.0.1.2.3.4.in-addr.arpa",
wantErr: true,
errMsg: "incorrect PTR: \"b.a.9.8.7.6.5.0.4.0.0.0.3.0.0.0.2.0.0.0.0.0.0.0.0.0.0.0.1.2.3.4.in-addr.arpa\"",
},
{
testName: "large number of nibbles in ipv6 ptr",
ptr: "a.b.a.9.8.7.6.5.0.4.0.0.0.3.0.0.0.2.0.0.0.1.0.0.0.0.0.0.0.1.2.3.4.ip6.arpa.",
wantErr: true,
errMsg: "incorrect PTR IPv6 \"a.b.a.9.8.7.6.5.0.4.0.0.0.3.0.0.0.2.0.0.0.1.0.0.0.0.0.0.0.1.2.3.4.ip6.arpa.\": incorrect number of segments in IPv6 PTR: 33",
},
{
testName: "unexpected char",
ptr: "z.a.9.8.7.6.5.0.4.0.0.0.3.0.0.0.2.0.0.0.1.0.0.0.0.0.0.0.1.2.3.4.ip6.arpa.",
wantErr: true,
errMsg: "incorrect PTR IPv6 \"z.a.9.8.7.6.5.0.4.0.0.0.3.0.0.0.2.0.0.0.1.0.0.0.0.0.0.0.1.2.3.4.ip6.arpa.\": failed to parse IPv6 segments: [4321 0000 0001 0002 0003 0004 0567 89az]",
},
{
testName: "custom text",
ptr: "custom text",
wantErr: true,
errMsg: "incorrect PTR: \"custom text\"",
},
} {
ip, err := ExtractIP(tc.ptr)
if tc.wantErr {
assert.Error(t, err, "Test %q", tc.testName)
assert.Equalf(t, tc.errMsg, err.Error(), "Test %q", tc.testName)
} else {
assert.NoError(t, err)
assert.Equalf(t, tc.wantIP, ip, "Test %q", tc.testName)
}
}
}

func TestGetClusterIPs(t *testing.T) {
for _, tc := range []struct {
service *v1.Service
wantIPs []string
}{
{
service: &v1.Service{
Spec: v1.ServiceSpec{
ClusterIP: "2001:db8:0:0:aaaa::1",
ClusterIPs: []string{"2001:db8:0:0:aaaa::1"},
},
},
wantIPs: []string{"2001:db8::aaaa:0:0:1"},
},
{
service: &v1.Service{
Spec: v1.ServiceSpec{
ClusterIP: "2001:db8::aaaa:0:0:1",
},
},
wantIPs: []string{"2001:db8::aaaa:0:0:1"},
},
{
service: &v1.Service{
Spec: v1.ServiceSpec{
ClusterIP: "2001:db8:0::aaaa:0:0:1",
ClusterIPs: []string{"2001:db8:0::aaaa:0:0:1", "255.255.255.0"},
},
},
wantIPs: []string{"2001:db8::aaaa:0:0:1", "255.255.255.0"},
},
} {
assert.ElementsMatch(t, tc.wantIPs, GetClusterIPs(tc.service))
}
}

0 comments on commit 1fb1456

Please sign in to comment.