diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go index 77594cf..49b1645 100644 --- a/internal/pkg/config/config.go +++ b/internal/pkg/config/config.go @@ -18,11 +18,14 @@ package config import ( + "net" "net/url" "strconv" "strings" "time" + "github.com/networkservicemesh/sdk/pkg/networkservice/ipam/singlepointipam" + "github.com/kelseyhightower/envconfig" "github.com/pkg/errors" ) @@ -66,6 +69,86 @@ func validateConfig(cfg *Config) error { if cfg.ListenOn.Scheme != tcpSchema { return errors.New("only tcp schema is supported for this type of endpoint") } + if _, err := ParseCidr(cfg.CidrPrefix); err != nil { + return err + } + return nil +} + +// ParseCidr parses array of CIDR prefixes (and/or IP ranges) +// Valid "prefix" formats are "192.168.0.0/24", "192.168.0.1-192.168.0.100/24", +// or composite "192.168.0.1-192.168.0.10/24;192.168.0.20-192.168.0.30/24". +func ParseCidr(cIDRs []string) ([]*singlepointipam.IpamNet, error) { + if len(cIDRs) > 2 { + return nil, errors.Errorf("one IPv4 and/or one IPv6 config allowed") + } + + ipamNets := []*singlepointipam.IpamNet{} + for _, cIDR := range cIDRs { + ipamNet := singlepointipam.NewIpamNet() + composite := strings.Split(cIDR, ";") + for _, cidrStr := range composite { + if r := strings.SplitN(cidrStr, "-", 2); len(r) == 2 { + if err := parseRange(ipamNet, r); err != nil { + return nil, err + } + } else { + // IP network + if len(composite) > 1 { + return nil, errors.Errorf("%s composite subnet config is invalid", composite) + } + if err := parseNetwork(ipamNet, cidrStr); err != nil { + return nil, err + } + } + } + ipamNets = append(ipamNets, ipamNet) + } + + if err := validateNetworkFamily(ipamNets...); err != nil { + return nil, err + } + + return ipamNets, nil +} + +func parseNetwork(ipamNet *singlepointipam.IpamNet, network string) error { + network = strings.TrimSpace(network) + _, ipNet, err := net.ParseCIDR(network) + if err != nil { + return errors.Errorf("%s is invalid CIDR: %s", network, err) + } + return ipamNet.SetOrCheckNetwork(ipNet) +} + +func parseRange(ipamNet *singlepointipam.IpamNet, r []string) error { + r[0] = strings.TrimSpace(r[0]) + r[1] = strings.TrimSpace(r[1]) + firstip := net.ParseIP(r[0]) + if firstip == nil { + return errors.Errorf("%s is invalid range start", r[0]) + } + lastip, ipNet, err := net.ParseCIDR(r[1]) + if err != nil { + return errors.Errorf("%s is invalid CIDR: %s", r[1], err) + } + if !ipNet.Contains(firstip) { + return errors.Errorf("%s is invalid range start for CIDR %s", firstip, ipNet.String()) + } + if err := ipamNet.SetOrCheckNetwork(ipNet); err != nil { + return err + } + return ipamNet.AddRange(firstip, lastip) +} + +func validateNetworkFamily(ipamNets ...*singlepointipam.IpamNet) error { + if len(ipamNets) == 2 { + ip1 := ipamNets[0].Network.IP.To16() + ip2 := ipamNets[1].Network.IP.To16() + if ip1.To4() != nil && ip2.To4() != nil || ip1.To4() == nil && ip2.To4() == nil { + return errors.Errorf("one IPv4 and/or one IPv6 config allowed") + } + } return nil } diff --git a/internal/pkg/config/config_test.go b/internal/pkg/config/config_test.go index c2024e2..5b244c2 100644 --- a/internal/pkg/config/config_test.go +++ b/internal/pkg/config/config_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2021 Nordix Foundation. +// Copyright (c) 2021-2022 Nordix Foundation. // // SPDX-License-Identifier: Apache-2.0 // @@ -46,3 +46,149 @@ func TestServiceConfig_UnmarshalBinary(t *testing.T) { VLANTag: 200, }, cfg) } + +func TestCidrPrefix(t *testing.T) { + cidrs := []string{ + "192.168.0.0/16", + "dead:beaf::/64", + } + _, err := config.ParseCidr(cidrs) + require.NoError(t, err) + + cidrs = []string{ + "192.168.0.1/32", + "dead:beaf::1/128", + } + _, err = config.ParseCidr(cidrs) + require.NoError(t, err) + + cidrs = []string{ + "192.168.100.0/24", + } + _, err = config.ParseCidr(cidrs) + require.NoError(t, err) + + cidrs = []string{ + "2dea:1000::/48", + } + _, err = config.ParseCidr(cidrs) + require.NoError(t, err) + + cidrs = []string{ + "2dea:1000::/48", + } + _, err = config.ParseCidr(cidrs) + require.NoError(t, err) + + cidrs = []string{ + "192.168.0.1-192.168.0.100/24", + "fe80:1000:2000::/64", + } + _, err = config.ParseCidr(cidrs) + require.NoError(t, err) + + cidrs = []string{ + "192.168.0.1-192.168.0.100/24", + "2dea:ffff:ffff:ffff:ffff:ffff:0000:0000-2dea:ffff:ffff:ffff:ffff:ffff:abcd:0000/96", + } + _, err = config.ParseCidr(cidrs) + require.NoError(t, err) + + cidrs = []string{ + "192.168.0.0-192.168.0.0/16;192.168.1.1-192.168.1.1/16;192.168.100.100-192.168.200.200/16", + "1000::a:2-1000::a:ffff/64;1000::d:2-1000::e:ffff/64;1000::f:f:2-1000::f:f:2/64", + } + _, err = config.ParseCidr(cidrs) + require.NoError(t, err) + + cidrs = []string{ + "2000::1-2000::2000/64", + } + _, err = config.ParseCidr(cidrs) + require.NoError(t, err) +} + +func TestCidrPrefixInvalid(t *testing.T) { + cidrs := []string{ + "192.168.0.1-192.168.0.100/24", + "10.0.0.1-10.0.200/16", + "100::1-100::ffff/64", + } + _, err := config.ParseCidr(cidrs) + require.Error(t, err) + + cidrs = []string{ + "192.168.0.1-192.168.0.100/24", + "10.0.0.1-10.0.200/16", + } + _, err = config.ParseCidr(cidrs) + require.Error(t, err) + + cidrs = []string{ + "100::1-100::ffff/64", + "2dea::1-2dea::ffff/64", + } + _, err = config.ParseCidr(cidrs) + require.Error(t, err) + + cidrs = []string{ + "192.168.0.1-192.168.0.100/24;192.168.32.1-192.168.32.100/25", + } + _, err = config.ParseCidr(cidrs) + require.Error(t, err) + + cidrs = []string{ + "2dea::af0-2dea::aff/96;2dea::ef0-2dea::eff/64;2dea::cf0-2dea::cff/96", + } + _, err = config.ParseCidr(cidrs) + require.Error(t, err) + + cidrs = []string{ + "192.168.0.1-192.168.0.100/24;192.168.0.0/24", + } + _, err = config.ParseCidr(cidrs) + require.Error(t, err) + + cidrs = []string{ + "192.168.0.64/28", + "2000::/64;2000::1-2000::2000/64", + } + _, err = config.ParseCidr(cidrs) + require.Error(t, err) + + cidrs = []string{ + "192.168.0.0/28-192.168.0.2/28", + } + _, err = config.ParseCidr(cidrs) + require.Error(t, err) + + cidrs = []string{ + "2000::2000-2000::1/64", + } + _, err = config.ParseCidr(cidrs) + require.Error(t, err) + + cidrs = []string{ + "192.168.0.0-192.168.0.2/36", + } + _, err = config.ParseCidr(cidrs) + require.Error(t, err) + + cidrs = []string{ + "abba::baba/140", + } + _, err = config.ParseCidr(cidrs) + require.Error(t, err) + + cidrs = []string{ + "192.168.11.0-192.168.10.2/30", + } + _, err = config.ParseCidr(cidrs) + require.Error(t, err) + + cidrs = []string{ + "2000:b::1-2000:a::2000/64", + } + _, err = config.ParseCidr(cidrs) + require.Error(t, err) +} diff --git a/main.go b/main.go index 3c9e4c3..34321ad 100644 --- a/main.go +++ b/main.go @@ -26,7 +26,6 @@ import ( "net/url" "os" "os/signal" - "strings" "syscall" "time" @@ -155,7 +154,7 @@ func main() { log.FromContext(ctx).Infof("executing phase 3: parsing network prefixes for ipam") // ******************************************************************************** - ipamChain := getIPAMChain(ctx, cfg.CidrPrefix) + ipamChain := getIPAMChain(cfg.CidrPrefix) log.FromContext(ctx).Infof("network prefixes parsed successfully") @@ -305,15 +304,11 @@ func genPublishableURL(listenOn *url.URL, logger log.Logger) *url.URL { return listenonurl.GetPublicURL(addrs, listenOn) } -func getIPAMChain(ctx context.Context, cIDRs []string) networkservice.NetworkServiceServer { +func getIPAMChain(cIDRs []string) networkservice.NetworkServiceServer { var ipamchain []networkservice.NetworkServiceServer - for _, cidr := range cIDRs { - var parseErr error - _, ipNet, parseErr := net.ParseCIDR(strings.TrimSpace(cidr)) - if parseErr != nil { - log.FromContext(ctx).Fatalf("Could not parse CIDR %s; %+v", cidr, parseErr) - } - ipamchain = append(ipamchain, singlepointipam.NewServer(ipNet)) + ipamNets, _ := config.ParseCidr(cIDRs) + for _, ipamNet := range ipamNets { + ipamchain = append(ipamchain, singlepointipam.NewServer(ipamNet)) } return chain.NewNetworkServiceServer(ipamchain...) }