From 02c9a1e0c3d7d98a8fdd3fd4cc91895f5a6a1445 Mon Sep 17 00:00:00 2001 From: y-kzm Date: Wed, 12 Jun 2024 14:11:52 +0900 Subject: [PATCH] Add pref64 option support and config & daemon test --- README.md | 1 + advertiser.go | 7 +++ config.go | 31 ++++++++++ config_test.go | 161 +++++++++++++++++++++++++++++++++++++++++++++++++ daemon_test.go | 19 ++++++ 5 files changed, 219 insertions(+) diff --git a/README.md b/README.md index 7d54515..fc983e5 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ declarative configuration interface on top of it. - Prefix discovery with Prefix Information option - DNS configuration discovery with RDNSS/DNSSL option - Route advertisement with Route Information option +- NAT64 prefix discovery with PREF64 option ## Installation diff --git a/advertiser.go b/advertiser.go index b70f4e1..840a516 100644 --- a/advertiser.go +++ b/advertiser.go @@ -125,6 +125,13 @@ func (s *advertiser) createOptions(config *InterfaceConfig, deviceState *deviceS }) } + for _, nat64prefix := range config.NAT64Prefixes { + options = append(options, &ndp.PREF64{ + Lifetime: time.Second * time.Duration(*nat64prefix.LifetimeSeconds), + Prefix: netip.MustParsePrefix(nat64prefix.Prefix), + }) + } + return options } diff --git a/config.go b/config.go index bc004eb..61eaef8 100644 --- a/config.go +++ b/config.go @@ -95,6 +95,9 @@ type InterfaceConfig struct { // DNSSL-specific configuration parameters. DNSSLs []*DNSSLConfig `yaml:"dnssls" json:"dnssls" validate:"dive,required" default:"[]"` + + // NAT64 prefix-specific configuration parameters. + NAT64Prefixes []*NAT64PrefixConfig `yaml:"nat64prefixes" json:"nat64prefixes" validate:"dive,required" default:"[]"` } // PrefixConfig represents the prefix-specific configuration parameters @@ -158,6 +161,19 @@ type DNSSLConfig struct { DomainNames []string `yaml:"domainNames" json:"domainNames" validate:"required,unique,min=1,dive,domain"` } +// NAT64PrefixConfig represents the NAT64 prefix-specific configuration parameters +type NAT64PrefixConfig struct { + // Required: NAT64 prefix. Must be a valid IPv6 prefix. + // Can only be one of /32, /40, /48, /56, /64, or /96. + Prefix string `yaml:"prefix" json:"prefix" validate:"required,cidrv6,invalid_prefix_len"` + + // Required: The valid lifetime of the NAT64 prefix in seconds. Must be >= 0 + // and <= 65528. If set to 0, it indicates that the prefix should not be used anymore. + // Should not be shorter than Router Lifetime. This lifetime is encoded + // in units of 8-seconds increments as ScaledLifetime. + LifetimeSeconds *int `yaml:"lifetimeSeconds" json:"lifetimeSeconds" validate:"required,gte=0,lte=65528" default:"65528"` +} + // ValidationErrors is a type alias for the validator.ValidationErrors type ValidationErrors = validator.ValidationErrors @@ -217,6 +233,21 @@ func (c *Config) defaultAndValidate() error { return domainRegexp.Match([]byte(dom)) }) + // Adhoc custom validator which validates the prefix length must + // be one of /32, /40, /48, /56, /64, or /96. + validate.RegisterValidation("invalid_prefix_len", func(fl validator.FieldLevel) bool { + p := netip.MustParsePrefix(fl.Field().String()) + validPrefixLengths := map[int]bool{ + 32: true, + 40: true, + 48: true, + 56: true, + 64: true, + 96: true, + } + return validPrefixLengths[p.Bits()] + }) + if err := validate.Struct(c); err != nil { if _, ok := err.(*validator.InvalidValidationError); ok { panic("BUG (Please report 🙏): Invalid validation: " + err.Error()) diff --git a/config_test.go b/config_test.go index 46720dd..a58a142 100644 --- a/config_test.go +++ b/config_test.go @@ -1039,6 +1039,167 @@ func TestConfigValidation(t *testing.T) { errorField: "DomainNames[0]", errorTag: "domain", }, + + // NAT64PrefixConfig + { + name: "Nil NAT64PrefixConfig", + config: &Config{ + Interfaces: []*InterfaceConfig{ + { + Name: "net0", + RAIntervalMilliseconds: 1000, + NAT64Prefixes: nil, + }, + }, + }, + expectError: false, + }, + { + name: "Empty NAT64PrefixConfig", + config: &Config{ + Interfaces: []*InterfaceConfig{ + { + Name: "net0", + RAIntervalMilliseconds: 1000, + NAT64Prefixes: []*NAT64PrefixConfig{}, + }, + }, + }, + expectError: false, + }, + { + name: "Nil NAT64PrefixConfig Element", + config: &Config{ + Interfaces: []*InterfaceConfig{ + { + Name: "net0", + RAIntervalMilliseconds: 1000, + NAT64Prefixes: []*NAT64PrefixConfig{nil}, + }, + }, + }, + expectError: true, + errorField: "Prefix", + errorTag: "required", + }, + { + name: "No NAT64Prefix", + config: &Config{ + Interfaces: []*InterfaceConfig{ + { + Name: "net0", + RAIntervalMilliseconds: 1000, + NAT64Prefixes: []*NAT64PrefixConfig{ + { + LifetimeSeconds: ptr.To(1800), + }, + }, + }, + }, + }, + expectError: true, + errorField: "Prefix", + errorTag: "required", + }, + { + name: "Multiple NAT64PrefixConfig", + config: &Config{ + Interfaces: []*InterfaceConfig{ + { + Name: "net0", + RAIntervalMilliseconds: 1000, + NAT64Prefixes: []*NAT64PrefixConfig{ + { + Prefix: "fc64:ff9b::/96", + LifetimeSeconds: ptr.To(1800), + }, + { + Prefix: "fd64:ff9b::/96", + LifetimeSeconds: ptr.To(1800), + }, + }, + }, + }, + }, + expectError: false, + }, + { + name: "Invalid NAT64Prefix length", + config: &Config{ + Interfaces: []*InterfaceConfig{ + { + Name: "net0", + RAIntervalMilliseconds: 1000, + NAT64Prefixes: []*NAT64PrefixConfig{ + { + Prefix: "64:ff9b::/104", + }, + }, + }, + }, + }, + expectError: true, + errorField: "Prefix", + errorTag: "invalid_prefix_len", + }, + { + name: "LifetimeSeconds = 65528", + config: &Config{ + Interfaces: []*InterfaceConfig{ + { + Name: "net0", + RAIntervalMilliseconds: 1000, + NAT64Prefixes: []*NAT64PrefixConfig{ + { + Prefix: "64:ff9b::/96", + LifetimeSeconds: ptr.To(65528), + }, + }, + }, + }, + }, + expectError: false, + }, + { + name: "LifetimeSeconds < 0", + config: &Config{ + Interfaces: []*InterfaceConfig{ + { + Name: "net0", + RAIntervalMilliseconds: 1000, + NAT64Prefixes: []*NAT64PrefixConfig{ + { + Prefix: "64:ff9b::/96", + LifetimeSeconds: ptr.To(-1), + }, + }, + }, + }, + }, + expectError: true, + errorField: "LifetimeSeconds", + errorTag: "gte", + }, + { + name: "LifetimeSeconds > 65528", + config: &Config{ + Interfaces: []*InterfaceConfig{ + { + Name: "net0", + RAIntervalMilliseconds: 1000, + NAT64Prefixes: []*NAT64PrefixConfig{ + { + Prefix: "64:ff9b::/96", + LifetimeSeconds: ptr.To(65529), + }, + }, + }, + }, + }, + expectError: true, + errorField: "LifetimeSeconds", + errorTag: "lte", + }, } for _, tt := range tests { diff --git a/daemon_test.go b/daemon_test.go index 3eb9106..223e0e7 100644 --- a/daemon_test.go +++ b/daemon_test.go @@ -97,6 +97,12 @@ func TestDaemonHappyPath(t *testing.T) { DomainNames: []string{"example.com", "foo.example.com"}, }, }, + NAT64Prefixes: []*NAT64PrefixConfig{ + { + Prefix: "64:ff9b::/96", + LifetimeSeconds: ptr.To(1800), + }, + }, }, { Name: "net1", @@ -250,6 +256,19 @@ func TestDaemonHappyPath(t *testing.T) { require.NotNil(t, dnsslOptions, "DNSSL option is not advertised") require.Equal(t, time.Second*400, dnsslOptions.Lifetime) require.ElementsMatch(t, []string{"example.com", "foo.example.com"}, dnsslOptions.DomainNames) + + // Find and check NAT64Prefix options + nat64prefixOptions := map[netip.Prefix]*ndp.PREF64{} + for _, option := range ra.msg.Options { + if opt, ok := option.(*ndp.PREF64); ok { + nat64prefixOptions[opt.Prefix] = opt + } + } + nat64prefix := netip.MustParsePrefix("64:ff9b::/96") + require.Contains(t, nat64prefixOptions, nat64prefix) + nat64prefixInfo := nat64prefixOptions[nat64prefix] + require.Equal(t, int(96), nat64prefixInfo.Prefix.Bits()) + require.Equal(t, time.Second*1800, nat64prefixInfo.Lifetime) }) t.Run("Ensure the status is running and the result is ordered by name", func(t *testing.T) {