-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #4593 from johannwagner/feature/nat64
feat: NAT64 network source
- Loading branch information
Showing
6 changed files
with
228 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
Configure NAT64 DNS Records | ||
======================================= | ||
|
||
Some NAT64 configurations are entirely handled outside the Kubernetes cluster, therefore Kubernetes does not know anything about the associated IPv4 addresses. ExternalDNS should also be able to create A records for those cases. | ||
Therefore, we can configure `nat64-networks`, which **must** be a /96 network. You can also specify multiple `nat64-networks` for more complex setups. | ||
This creates an additional A record with a NAT64-translated IPv4 address for each AAAA record pointing to an IPv6 address within the given `nat64-networks`. | ||
|
||
This can be configured with the following flag passed to the operator binary. You can also pass multiple `nat64-networks` by using a comma as seperator. | ||
```sh | ||
--nat64-networks="2001:db8:96::/96" | ||
``` | ||
|
||
|
||
## Setup Example | ||
|
||
We use an external NAT64 resolver and SIIT (Stateless IP/ICMP Translation). Therefore, our nodes only have IPv6 IP adresses but can reach IPv4 addresses *and* can be reached via IPv4. | ||
Outgoing connections are a classic NAT64 setup, where all IPv6 addresses gets translated to a small pool of IPv4 addresses. | ||
Incoming connnections are mapped on a different IPv4 pool, e.g. `198.51.100.0/24`, which can get translated one-to-one to IPv6 addresses. We dedicate a `/96` network for this, for example `2001:db8:96::/96`, so `198.51.100.0/24` can translated to `2001:db8:96::c633:6400/120`. Note: `/120` IPv6 network has exactly as many IP addresses as `/24` IPv4 network. | ||
|
||
Therefore, the `/96` network can be configured as `nat64-networks`. This means, that `2001:0DB8:96::198.51.100.10` or `2001:db8:96::c633:640a` can be translated to `198.51.100.10`. | ||
Any source can point a record to an IPv6 address within the given `nat64-networks`, for example `2001:db8:96::c633:640a`. This creates by default an AAAA record and - if `nat64-networks` is configured - also an A record with `198.51.100.10` as target. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
/* | ||
Copyright 2024 The Kubernetes Authors. | ||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
http://www.apache.org/licenses/LICENSE-2.0 | ||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
package source | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"net/netip" | ||
|
||
"sigs.k8s.io/external-dns/endpoint" | ||
) | ||
|
||
// nat64Source is a Source that adds A endpoints for AAAA records including an NAT64 address. | ||
type nat64Source struct { | ||
source Source | ||
nat64Prefixes []string | ||
} | ||
|
||
// NewNAT64Source creates a new nat64Source wrapping the provided Source. | ||
func NewNAT64Source(source Source, nat64Prefixes []string) Source { | ||
return &nat64Source{source: source, nat64Prefixes: nat64Prefixes} | ||
} | ||
|
||
// Endpoints collects endpoints from its wrapped source and returns them without duplicates. | ||
func (s *nat64Source) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) { | ||
parsedNAT64Prefixes := make([]netip.Prefix, 0) | ||
for _, prefix := range s.nat64Prefixes { | ||
pPrefix, err := netip.ParsePrefix(prefix) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if pPrefix.Bits() != 96 { | ||
return nil, fmt.Errorf("NAT64 prefixes need to be /96 prefixes.") | ||
} | ||
parsedNAT64Prefixes = append(parsedNAT64Prefixes, pPrefix) | ||
} | ||
|
||
additionalEndpoints := []*endpoint.Endpoint{} | ||
|
||
endpoints, err := s.source.Endpoints(ctx) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
for _, ep := range endpoints { | ||
if ep.RecordType != endpoint.RecordTypeAAAA { | ||
continue | ||
} | ||
|
||
v4Targets := make([]string, 0) | ||
|
||
for _, target := range ep.Targets { | ||
ip, err := netip.ParseAddr(target) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
var sPrefix *netip.Prefix | ||
|
||
for _, cPrefix := range parsedNAT64Prefixes { | ||
if cPrefix.Contains(ip) { | ||
sPrefix = &cPrefix | ||
} | ||
} | ||
|
||
// If we do not have a NAT64 prefix, we skip this record. | ||
if sPrefix == nil { | ||
continue | ||
} | ||
|
||
ipBytes := ip.As16() | ||
v4AddrBytes := ipBytes[12:16] | ||
|
||
v4Addr, isOk := netip.AddrFromSlice(v4AddrBytes) | ||
if !isOk { | ||
return nil, fmt.Errorf("Could not parse %v to IPv4 address", v4AddrBytes) | ||
} | ||
|
||
v4Targets = append(v4Targets, v4Addr.String()) | ||
} | ||
|
||
if len(v4Targets) == 0 { | ||
continue | ||
} | ||
|
||
v4EP := ep.DeepCopy() | ||
v4EP.Targets = v4Targets | ||
v4EP.RecordType = endpoint.RecordTypeA | ||
|
||
additionalEndpoints = append(additionalEndpoints, v4EP) | ||
} | ||
return append(endpoints, additionalEndpoints...), nil | ||
} | ||
|
||
func (s *nat64Source) AddEventHandler(ctx context.Context, handler func()) { | ||
s.source.AddEventHandler(ctx, handler) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
/* | ||
Copyright 2024 The Kubernetes Authors. | ||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
http://www.apache.org/licenses/LICENSE-2.0 | ||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
package source | ||
|
||
import ( | ||
"context" | ||
"testing" | ||
|
||
"sigs.k8s.io/external-dns/endpoint" | ||
"sigs.k8s.io/external-dns/internal/testutils" | ||
) | ||
|
||
// Validates that dedupSource is a Source | ||
var _ Source = &nat64Source{} | ||
|
||
func TestNAT64Source(t *testing.T) { | ||
t.Run("Endpoints", testNat64Source) | ||
} | ||
|
||
// testDedupEndpoints tests that duplicates from the wrapped source are removed. | ||
func testNat64Source(t *testing.T) { | ||
for _, tc := range []struct { | ||
title string | ||
endpoints []*endpoint.Endpoint | ||
expected []*endpoint.Endpoint | ||
}{ | ||
{ | ||
"single non-nat64 ipv6 endpoint returns one ipv6 endpoint", | ||
[]*endpoint.Endpoint{ | ||
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8:1::1"}}, | ||
}, | ||
[]*endpoint.Endpoint{ | ||
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8:1::1"}}, | ||
}, | ||
}, | ||
{ | ||
"single nat64 ipv6 endpoint returns one ipv4 endpoint and one ipv6 endpoint", | ||
[]*endpoint.Endpoint{ | ||
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::192.0.2.42"}}, | ||
}, | ||
[]*endpoint.Endpoint{ | ||
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::192.0.2.42"}}, | ||
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"192.0.2.42"}}, | ||
}, | ||
}, | ||
{ | ||
"single nat64 ipv6 endpoint returns one ipv4 endpoint and one ipv6 endpoint", | ||
[]*endpoint.Endpoint{ | ||
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::c000:22a"}}, | ||
}, | ||
[]*endpoint.Endpoint{ | ||
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::c000:22a"}}, | ||
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"192.0.2.42"}}, | ||
}, | ||
}, | ||
} { | ||
t.Run(tc.title, func(t *testing.T) { | ||
mockSource := new(testutils.MockSource) | ||
mockSource.On("Endpoints").Return(tc.endpoints, nil) | ||
|
||
// Create our object under test and get the endpoints. | ||
source := NewNAT64Source(mockSource, []string{"2001:DB8::/96"}) | ||
|
||
endpoints, err := source.Endpoints(context.Background()) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
// Validate returned endpoints against desired endpoints. | ||
validateEndpoints(t, endpoints, tc.expected) | ||
|
||
// Validate that the mock source was called. | ||
mockSource.AssertExpectations(t) | ||
}) | ||
} | ||
} |