Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Config parameter CidrPrefix accepts range of IPs #111

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ This repo contains 'nse-remote-vlan' an NSE application for Network Service Mesh
* NSM_NAME Name of the endpoint
* NSM_CONNECT_TO An URL of registry service to connect to
* NSM_MAX_TOKEN_LIFETIME Maximum lifetime of tokens
* NSM_CIDR_PREFIX CIDR Prefix to assign IPs from
* NSM_IPV6_PREFIX Ipv6 Prefix for dual-stack
* NSM_CIDR_PREFIX CIDR Prefix or IP range(s) to assign IPs from (IPv4 and/or IPv6)
* NSM_REGISTER_SERVICE if true then registers network service on startup
* NSM_LISTEN_ON tcp:// url to be listen on. It will be used as public to register NSM
* NSM_SERVICES List of supported services
Expand Down
103 changes: 101 additions & 2 deletions internal/pkg/config/config.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2021-2022 Nordix Foundation.
// Copyright (c) 2021-2023 Nordix Foundation.
//
// SPDX-License-Identifier: Apache-2.0
//
Expand All @@ -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"
)
Expand All @@ -42,7 +45,7 @@ type Config struct {
ConnectTo url.URL `default:"nsm-registry-svc:5002" desc:"url of registry service to connect to" split_words:"true"`
MaxTokenLifetime time.Duration `default:"24h" desc:"maximum lifetime of tokens" split_words:"true"`
RegistryClientPolicies []string `default:"etc/nsm/opa/common/.*.rego,etc/nsm/opa/registry/.*.rego,etc/nsm/opa/client/.*.rego" desc:"paths to files and directories that contain registry client policies" split_words:"true"`
CidrPrefix []string `default:"169.254.0.0/16" desc:"CIDR Prefix to assign IPs (IPv4 and/or IPv6) from" split_words:"true"`
CidrPrefix []string `default:"169.254.0.0/16" desc:"CIDR Prefix or IP range to assign IPs (IPv4 and/or IPv6) from" split_words:"true"`
RegisterService bool `default:"true" desc:"if true then registers network service on startup" split_words:"true"`
ListenOn url.URL `default:"tcp://:5003" desc:"tcp:// url to be listen on. It will be used as public to register NSM" split_words:"true"`
OpenTelemetryEndpoint string `default:"otel-collector.observability.svc.cluster.local:4317" desc:"OpenTelemetry Collector Endpoint"`
Expand All @@ -67,6 +70,102 @@ 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 setNetwork(ipamNet *singlepointipam.IpamNet, ipNet *net.IPNet) error {
if ipamNet.Network == nil {
ipamNet.Network = ipNet
} else {
if !ipamNet.Network.IP.Equal(ipNet.IP) {
return errors.Errorf("network mismatch: %v != %v", ipamNet.Network, ipNet)
}
ones, bits := ipNet.Mask.Size()
iones, ibits := ipamNet.Network.Mask.Size()
if ones != iones || bits != ibits {
return errors.Errorf("network mask mismatch: %v != %v", ipamNet.Network.Mask, ipNet.Mask)
}
}
return 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 setNetwork(ipamNet, 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 := setNetwork(ipamNet, 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
}

Expand Down
136 changes: 135 additions & 1 deletion internal/pkg/config/config_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2021 Nordix Foundation.
// Copyright (c) 2021-2023 Nordix Foundation.
//
// SPDX-License-Identifier: Apache-2.0
//
Expand All @@ -24,6 +24,126 @@ import (
"github.com/networkservicemesh/cmd-nse-remote-vlan/internal/pkg/config"
)

var TestCidrPrefixTests = []struct {
cidrs []string
}{
{
[]string{
"192.168.0.0/16",
"dead:beaf::/64",
},
},
{
[]string{
"192.168.0.1/32",
"dead:beaf::1/128",
},
},
{
[]string{
"192.168.100.0/24",
},
},
{
[]string{
"192.168.0.1-192.168.0.100/24",
"fe80:1000:2000::/64",
},
},
{
[]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",
},
},
{
[]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",
},
},
{
[]string{
"2000::1-2000::2000/64",
},
},
}

var TestCidrPrefixInvalidTests = []struct {
cidrs []string
}{
{
[]string{
"192.168.0.1-192.168.0.100/24",
"10.0.0.1-10.0.200/16",
"100::1-100::ffff/64",
},
},
{
[]string{
"192.168.0.1-192.168.0.100/24",
"10.0.0.1-10.0.200/16",
},
},
{
[]string{
"100::1-100::ffff/64",
"2dea::1-2dea::ffff/64",
},
},
{
[]string{
"192.168.0.1-192.168.0.100/24;192.168.32.1-192.168.32.100/25",
},
},
{
[]string{
"2dea::af0-2dea::aff/96;2dea::ef0-2dea::eff/64;2dea::cf0-2dea::cff/96",
},
},
{
[]string{
"192.168.0.1-192.168.0.100/24;192.168.0.0/24",
},
},
{
[]string{
"192.168.0.64/28",
"2000::/64;2000::1-2000::2000/64",
},
},
{
[]string{
"192.168.0.0/28-192.168.0.2/28",
},
},
{
[]string{
"2000::2000-2000::1/64",
},
},
{
[]string{
"192.168.0.0-192.168.0.2/36",
},
},
{
[]string{
"abba::baba/140",
},
},
{
[]string{
"192.168.11.0-192.168.10.2/30",
},
},
{
[]string{
"2000:b::1-2000:a::2000/64",
},
},
}

func TestServiceConfig_UnmarshalBinary(t *testing.T) {
cfg := new(config.ServiceConfig)

Expand All @@ -46,3 +166,17 @@ func TestServiceConfig_UnmarshalBinary(t *testing.T) {
VLANTag: 200,
}, cfg)
}

func TestCidrPrefix(t *testing.T) {
for _, ct := range TestCidrPrefixTests {
_, err := config.ParseCidr(ct.cidrs)
require.NoError(t, err, ct.cidrs)
}
}

func TestCidrPrefixInvalid(t *testing.T) {
for _, ct := range TestCidrPrefixInvalidTests {
_, err := config.ParseCidr(ct.cidrs)
require.Error(t, err, ct.cidrs)
}
}
15 changes: 5 additions & 10 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import (
"net/url"
"os"
"os/signal"
"strings"
"syscall"
"time"

Expand Down Expand Up @@ -157,7 +156,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")

Expand Down Expand Up @@ -315,15 +314,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...)
}