mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-08-05 09:06:58 +02:00
Merge pull request #4593 from johannwagner/feature/nat64
feat: NAT64 network source
This commit is contained in:
commit
848e309ad4
21
docs/nat64.md
Normal file
21
docs/nat64.md
Normal file
@ -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.
|
1
main.go
1
main.go
@ -173,6 +173,7 @@ func main() {
|
||||
|
||||
// Combine multiple sources into a single, deduplicated source.
|
||||
endpointsSource := source.NewDedupSource(source.NewMultiSource(sources, sourceCfg.DefaultTargets))
|
||||
endpointsSource = source.NewNAT64Source(endpointsSource, cfg.NAT64Networks)
|
||||
endpointsSource = source.NewTargetFilterSource(endpointsSource, targetFilter)
|
||||
|
||||
// RegexDomainFilter overrides DomainFilter
|
||||
|
@ -31,6 +31,7 @@ nav:
|
||||
- Advanced Topics:
|
||||
- Initial Design: docs/initial-design.md
|
||||
- TTL: docs/ttl.md
|
||||
- NAT64: docs/nat64.md
|
||||
- MultiTarget: docs/proposal/multi-target.md
|
||||
- Rate Limits: docs/rate-limits.md
|
||||
- Contributing:
|
||||
|
@ -201,6 +201,7 @@ type Config struct {
|
||||
WebhookServer bool
|
||||
TraefikDisableLegacy bool
|
||||
TraefikDisableNew bool
|
||||
NAT64Networks []string
|
||||
}
|
||||
|
||||
var defaultConfig = &Config{
|
||||
@ -349,6 +350,7 @@ var defaultConfig = &Config{
|
||||
WebhookServer: false,
|
||||
TraefikDisableLegacy: false,
|
||||
TraefikDisableNew: false,
|
||||
NAT64Networks: []string{},
|
||||
}
|
||||
|
||||
// NewConfig returns new Config object
|
||||
@ -438,6 +440,7 @@ func (cfg *Config) ParseFlags(args []string) error {
|
||||
app.Flag("exclude-target-net", "Exclude target nets (optional)").StringsVar(&cfg.ExcludeTargetNets)
|
||||
app.Flag("traefik-disable-legacy", "Disable listeners on Resources under the traefik.containo.us API Group").Default(strconv.FormatBool(defaultConfig.TraefikDisableLegacy)).BoolVar(&cfg.TraefikDisableLegacy)
|
||||
app.Flag("traefik-disable-new", "Disable listeners on Resources under the traefik.io API Group").Default(strconv.FormatBool(defaultConfig.TraefikDisableNew)).BoolVar(&cfg.TraefikDisableNew)
|
||||
app.Flag("nat64-networks", "Adding an A record for each AAAA record in NAT64-enabled networks; specify multiple times for multiple possible nets (optional)").StringsVar(&cfg.NAT64Networks)
|
||||
|
||||
// Flags related to providers
|
||||
providers := []string{"akamai", "alibabacloud", "aws", "aws-sd", "azure", "azure-dns", "azure-private-dns", "civo", "cloudflare", "coredns", "designate", "digitalocean", "dnsimple", "exoscale", "gandi", "godaddy", "google", "ibmcloud", "inmemory", "linode", "ns1", "oci", "ovh", "pdns", "pihole", "plural", "rdns", "rfc2136", "scaleway", "skydns", "tencentcloud", "transip", "ultradns", "webhook"}
|
||||
|
112
source/nat64source.go
Normal file
112
source/nat64source.go
Normal file
@ -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)
|
||||
}
|
90
source/nat64source_test.go
Normal file
90
source/nat64source_test.go
Normal file
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user