Skip to content
Closed
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ All notable changes to this project will be documented in this file.
- Activator
- Automatically backfill flex-algo node segment IDs for all activated devices when a new `TopologyInfo` account is created onchain
- Automatically backfill existing topologies' node segments when a Vpnv4 loopback interface is activated on a device
- Controller
- Add `features.yaml` config file support (`--features-config` flag); a `flex_algo.enabled` flag gates all flex-algo config generation
- When flex-algo is enabled: generate IS-IS TE admin-group assignments on link interfaces, flex-algo topology definitions, and `system-colored-tunnel-rib` as the BGP next-hop resolution source
- Stamp BGP color extcommunity on user tunnel route-maps for tenants with `include_topologies` set
- Add `node-segment ipv4 index` lines to Vpnv4 loopback config for each flex-algo, sourced from segment routing IDs backfilled by the activator
- Client
- Add `doublezero_connection_info` Prometheus metric exposing connection metadata (user_type, network, current_device, metro, tunnel_name, tunnel_src, tunnel_dst) ([#3201](https://github.com/malbeclabs/doublezero/pull/3201))
- Add `doublezero_connection_rtt_nanoseconds` and `doublezero_connection_loss_percentage` Prometheus metrics reporting RTT and packet loss to the current connected device
Expand Down
46 changes: 32 additions & 14 deletions controlplane/controller/cmd/controller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,24 +129,26 @@ func NewControllerCommand() *ControllerCommand {
c.fs.StringVar(&c.tlsKeyFile, "tls-key", "", "path to tls key file")
c.fs.BoolVar(&c.enablePprof, "enable-pprof", false, "enable pprof server")
c.fs.StringVar(&c.tlsListenPort, "tls-listen-port", "", "listening port for controller grpc server")
c.fs.StringVar(&c.featuresConfigPath, "features-config", "", "path to features.yaml config file (optional)")
return c
}

type ControllerCommand struct {
fs *flag.FlagSet
description string
listenAddr string
listenPort string
env string
programID string
rpcEndpoint string
deviceLocalASN uint64
noHardware bool
showVersion bool
tlsCertFile string
tlsKeyFile string
tlsListenPort string
enablePprof bool
fs *flag.FlagSet
description string
listenAddr string
listenPort string
env string
programID string
rpcEndpoint string
deviceLocalASN uint64
noHardware bool
showVersion bool
tlsCertFile string
tlsKeyFile string
tlsListenPort string
enablePprof bool
featuresConfigPath string
}

func (c *ControllerCommand) Fs() *flag.FlagSet {
Expand Down Expand Up @@ -249,6 +251,22 @@ func (c *ControllerCommand) Run() error {
log.Info("clickhouse disabled (CLICKHOUSE_ADDR not set)")
}

if c.featuresConfigPath != "" {
f, err := os.Open(c.featuresConfigPath)
if err != nil {
log.Error("failed to open features config", "path", c.featuresConfigPath, "error", err)
os.Exit(1)
}
featuresConfig, err := controller.LoadFeaturesConfig(f)
f.Close()
if err != nil {
log.Error("failed to parse features config", "path", c.featuresConfigPath, "error", err)
os.Exit(1)
}
options = append(options, controller.WithFeaturesConfig(featuresConfig))
log.Info("features config loaded", "path", c.featuresConfigPath, "flex_algo_enabled", featuresConfig.Features.FlexAlgo.Enabled)
}

if c.noHardware {
options = append(options, controller.WithNoHardware())
}
Expand Down
68 changes: 68 additions & 0 deletions controlplane/controller/internal/controller/features_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package controller

import (
"io"
"slices"

"gopkg.in/yaml.v3"
)

// FeaturesConfig is loaded from a YAML file at controller startup.
// It gates flex-algo topology config, link tagging, and BGP color community stamping.
type FeaturesConfig struct {
Features struct {
FlexAlgo FlexAlgoConfig `yaml:"flex_algo"`
} `yaml:"features"`
}

// FlexAlgoConfig controls IS-IS Flex-Algo and BGP color extended community behaviour.
type FlexAlgoConfig struct {
Enabled bool `yaml:"enabled"`
LinkTagging LinkTaggingConfig `yaml:"link_tagging"`
CommunityStamping CommunityStampingConfig `yaml:"community_stamping"`
}

// LinkTaggingConfig controls which links receive IS-IS TE admin-group attributes.
type LinkTaggingConfig struct {
Exclude struct {
Links []string `yaml:"links"` // link pubkeys to skip
} `yaml:"exclude"`
}

// IsExcluded returns true if the given link pubkey is in the exclude list.
func (c *LinkTaggingConfig) IsExcluded(linkPubKey string) bool {
return slices.Contains(c.Exclude.Links, linkPubKey)
}

// CommunityStampingConfig controls BGP color extended community stamping per tenant/device.
// A device is stamped if All is true, OR its pubkey is in Devices, OR the tenant's pubkey
// is in Tenants — unless the device pubkey is in Exclude.Devices (overrides all).
type CommunityStampingConfig struct {
All bool `yaml:"all"`
Tenants []string `yaml:"tenants"` // tenant pubkeys
Devices []string `yaml:"devices"` // device pubkeys
Exclude struct {
Devices []string `yaml:"devices"`
} `yaml:"exclude"`
}

// ShouldStamp returns true if BGP color communities should be stamped for the
// given (tenantPubKey, devicePubKey) pair.
func (c *CommunityStampingConfig) ShouldStamp(tenantPubKey, devicePubKey string) bool {
if slices.Contains(c.Exclude.Devices, devicePubKey) {
return false
}
if c.All {
return true
}
return slices.Contains(c.Tenants, tenantPubKey) || slices.Contains(c.Devices, devicePubKey)
}

// LoadFeaturesConfig parses a features YAML config from the given reader.
func LoadFeaturesConfig(r io.Reader) (*FeaturesConfig, error) {
var cfg FeaturesConfig
if err := yaml.NewDecoder(r).Decode(&cfg); err != nil {
return nil, err
}
return &cfg, nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package controller

import (
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestFeaturesConfigLoad(t *testing.T) {
yaml := `
features:
flex_algo:
enabled: true
link_tagging:
exclude:
links:
- ABC123pubkey
community_stamping:
all: false
tenants:
- TenantPubkey1
devices:
- DevicePubkey1
exclude:
devices:
- ExcludedDevicePubkey
`
config, err := LoadFeaturesConfig(strings.NewReader(yaml))
require.NoError(t, err)
assert.True(t, config.Features.FlexAlgo.Enabled)
assert.Len(t, config.Features.FlexAlgo.LinkTagging.Exclude.Links, 1)
assert.Equal(t, "ABC123pubkey", config.Features.FlexAlgo.LinkTagging.Exclude.Links[0])
assert.False(t, config.Features.FlexAlgo.CommunityStamping.All)
assert.Len(t, config.Features.FlexAlgo.CommunityStamping.Tenants, 1)
assert.Len(t, config.Features.FlexAlgo.CommunityStamping.Devices, 1)
assert.Len(t, config.Features.FlexAlgo.CommunityStamping.Exclude.Devices, 1)
}

func TestFeaturesConfigEmpty(t *testing.T) {
yaml := `features: {}`
config, err := LoadFeaturesConfig(strings.NewReader(yaml))
require.NoError(t, err)
assert.False(t, config.Features.FlexAlgo.Enabled)
assert.Empty(t, config.Features.FlexAlgo.LinkTagging.Exclude.Links)
}

func TestLinkTaggingIsExcluded(t *testing.T) {
cfg := LinkTaggingConfig{}
cfg.Exclude.Links = []string{"pubkey1", "pubkey2"}
assert.True(t, cfg.IsExcluded("pubkey1"))
assert.True(t, cfg.IsExcluded("pubkey2"))
assert.False(t, cfg.IsExcluded("pubkey3"))
assert.False(t, cfg.IsExcluded(""))
}

func TestShouldStamp(t *testing.T) {
cfg := CommunityStampingConfig{
All: false,
Tenants: []string{"tenant1"},
Devices: []string{"device1"},
}
cfg.Exclude.Devices = []string{"excluded_device"}

// tenant in list, device not excluded → stamp
assert.True(t, cfg.ShouldStamp("tenant1", "any_device"))
// device in list, tenant not in list → stamp
assert.True(t, cfg.ShouldStamp("other_tenant", "device1"))
// excluded device — always false regardless of tenant/device match
assert.False(t, cfg.ShouldStamp("tenant1", "excluded_device"))
// not in any list
assert.False(t, cfg.ShouldStamp("other_tenant", "other_device"))
}

func TestShouldStampAllTrue(t *testing.T) {
cfg := CommunityStampingConfig{All: true}
assert.True(t, cfg.ShouldStamp("any_tenant", "any_device"))

cfg.Exclude.Devices = []string{"excluded"}
assert.False(t, cfg.ShouldStamp("any_tenant", "excluded"))
assert.True(t, cfg.ShouldStamp("any_tenant", "other_device"))
}

func TestShouldStampExcludeOverridesAll(t *testing.T) {
cfg := CommunityStampingConfig{
All: true,
Tenants: []string{"tenant1"},
Devices: []string{"device1"},
}
cfg.Exclude.Devices = []string{"device1"}
// device1 is both in Devices and Exclude.Devices — exclude wins
assert.False(t, cfg.ShouldStamp("tenant1", "device1"))
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ interface Ethernet1/1
no isis passive
isis hello padding
isis network point-to-point
no traffic-engineering administrative-group
no traffic-engineering
!
interface Ethernet1/2
mtu 2048
Expand Down Expand Up @@ -121,6 +123,7 @@ router bgp 65342
!
address-family vpn-ipv4
neighbor 15.15.15.15 activate
no next-hop resolution ribs
!
vrf vrf1
rd 65342:1
Expand All @@ -137,8 +140,11 @@ router isis 1
!
segment-routing mpls
no shutdown
no traffic-engineering
set-overload-bit
!
no router traffic-engineering
!
ip community-list COMM-ALL_USERS permit 21682:1200
ip community-list COMM-ALL_MCAST_USERS permit 21682:1300
ip community-list COMM-TST_USERS permit 21682:10050
Expand Down
Loading
Loading