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 all commits
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ This repo contains 'nse-remote-vlan' an NSE application for Network Service Mesh
* `NSM_MAX_TOKEN_LIFETIME` - maximum lifetime of tokens (default: "24h")
* `NSM_REGISTRY_CLIENT_POLICIES` - paths to files and directories that contain registry client policies (default: "etc/nsm/opa/common/.*.rego,etc/nsm/opa/registry/.*.rego,etc/nsm/opa/client/.*.rego")
* `NSM_CIDR_PREFIX` - CIDR Prefix to assign IPs (IPv4 and/or IPv6) from (default: "169.254.0.0/16")
* `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 (default: "true")
* `NSM_LISTEN_ON` - tcp:// url to be listen on. It will be used as public to register NSM (default: "tcp://:5003")
* `NSM_OPEN_TELEMETRY_ENDPOINT` - OpenTelemetry Collector Endpoint (default: "otel-collector.observability.svc.cluster.local:4317")
Expand Down
102 changes: 101 additions & 1 deletion internal/pkg/config/config.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

// Copyright (c) 2023 Cisco and/or its affiliates.
//
// Copyright (c) 2021-2024 Nordix Foundation.
Expand All @@ -20,11 +21,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 @@ -46,7 +50,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 cidr.Groups `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" split_words:"true"`
Expand All @@ -72,6 +76,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)
}
}
21 changes: 20 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,16 @@ func main() {
tlsServerConfig.MinVersion = tls.VersionTLS12

// ********************************************************************************
logger.Infof("executing phase 3: create network service endpoint")
log.FromContext(ctx).Infof("executing phase 3: parsing network prefixes for ipam")
// ********************************************************************************

ipamChain := getIPAMChain(cfg.CidrPrefix)

log.FromContext(ctx).Infof("network prefixes parsed successfully")

// ********************************************************************************
logger.Infof("executing phase 4: create network service endpoint")

// ********************************************************************************
responderEndpoint := endpoint.NewServer(ctx,
spiffejwt.TokenGeneratorFunc(source, cfg.MaxTokenLifetime),
Expand Down Expand Up @@ -307,3 +316,13 @@ func genPublishableURL(listenOn *url.URL, logger log.Logger) *url.URL {
}
return listenonurl.GetPublicURL(addrs, listenOn)
}

func getIPAMChain(cIDRs []string) networkservice.NetworkServiceServer {
var ipamchain []networkservice.NetworkServiceServer
ipamNets, _ := config.ParseCidr(cIDRs)
for _, ipamNet := range ipamNets {
ipamchain = append(ipamchain, singlepointipam.NewServer(ipamNet))
}
return chain.NewNetworkServiceServer(ipamchain...)
}

Loading