Merge branch 'develop' of ssh://github.com/alice-lg/alice-lg into develop
This commit is contained in:
commit
cd8fdfa610
@ -14,9 +14,9 @@ enable_prefix_lookup = true
|
|||||||
# Try to refresh the neighbor status on every request to /neighbors
|
# Try to refresh the neighbor status on every request to /neighbors
|
||||||
enable_neighbors_status_refresh = false
|
enable_neighbors_status_refresh = false
|
||||||
|
|
||||||
# this ASN is used as a fallback value in the RPKI feature and for route
|
# This default ASN is used as a fallback value in the RPKI feature.
|
||||||
# filtering evaluation with large BGP communities
|
# Setting it is optional.
|
||||||
asn = 9033
|
asn = 9999
|
||||||
|
|
||||||
# Use an alternative store backend. The default is `memory`.
|
# Use an alternative store backend. The default is `memory`.
|
||||||
# store_backend = postgres
|
# store_backend = postgres
|
||||||
@ -36,9 +36,9 @@ neighbors_store_refresh_interval = 5
|
|||||||
# Add a delay to the stream parser in order to reduce
|
# Add a delay to the stream parser in order to reduce
|
||||||
# CPU load while ingesting routes. Route refreshs will take
|
# CPU load while ingesting routes. Route refreshs will take
|
||||||
# a bit longer. The value is in nanoseconds.
|
# a bit longer. The value is in nanoseconds.
|
||||||
# A value of 30000 will keep the cpu load at roughly 60% and
|
# A value of 10000 will keep the cpu load at roughly 70% and
|
||||||
# parsing a master4 table will take about 2.5 instead of 1.25 minutes.
|
# parsing a master4 table will take about 2.5 instead of 1.25 minutes.
|
||||||
stream_parser_throttle = 30000
|
stream_parser_throttle = 10000
|
||||||
|
|
||||||
# [postgres]
|
# [postgres]
|
||||||
# url = "postgres://postgres:postgres@localhost:5432/alice"
|
# url = "postgres://postgres:postgres@localhost:5432/alice"
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
@ -80,7 +81,7 @@ type ServerConfig struct {
|
|||||||
RoutesStoreRefreshInterval int `ini:"routes_store_refresh_interval"`
|
RoutesStoreRefreshInterval int `ini:"routes_store_refresh_interval"`
|
||||||
RoutesStoreRefreshParallelism int `ini:"routes_store_refresh_parallelism"`
|
RoutesStoreRefreshParallelism int `ini:"routes_store_refresh_parallelism"`
|
||||||
StoreBackend string `ini:"store_backend"`
|
StoreBackend string `ini:"store_backend"`
|
||||||
Asn int `ini:"asn"`
|
DefaultAsn int `ini:"asn"`
|
||||||
EnableNeighborsStatusRefresh bool `ini:"enable_neighbors_status_refresh"`
|
EnableNeighborsStatusRefresh bool `ini:"enable_neighbors_status_refresh"`
|
||||||
StreamParserThrottle int `ini:"stream_parser_throttle"`
|
StreamParserThrottle int `ini:"stream_parser_throttle"`
|
||||||
}
|
}
|
||||||
@ -125,11 +126,11 @@ type RejectCandidatesConfig struct {
|
|||||||
// validation state.
|
// validation state.
|
||||||
type RpkiConfig struct {
|
type RpkiConfig struct {
|
||||||
// Define communities
|
// Define communities
|
||||||
Enabled bool `ini:"enabled"`
|
Enabled bool `ini:"enabled"`
|
||||||
Valid []string `ini:"valid"`
|
Valid [][]string `ini:"valid"`
|
||||||
Unknown []string `ini:"unknown"`
|
Unknown [][]string `ini:"unknown"`
|
||||||
NotChecked []string `ini:"not_checked"`
|
NotChecked [][]string `ini:"not_checked"`
|
||||||
Invalid []string `ini:"invalid"`
|
Invalid [][]string `ini:"invalid"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UIConfig holds runtime settings for the web client
|
// UIConfig holds runtime settings for the web client
|
||||||
@ -485,51 +486,88 @@ func getRpkiConfig(config *ini.File) (RpkiConfig, error) {
|
|||||||
// Defaults taken from:
|
// Defaults taken from:
|
||||||
// https://www.euro-ix.net/en/forixps/large-bgp-communities/
|
// https://www.euro-ix.net/en/forixps/large-bgp-communities/
|
||||||
section := config.Section("rpki")
|
section := config.Section("rpki")
|
||||||
|
lines := strings.Split(section.Body(), "\n")
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
l := strings.TrimSpace(line)
|
||||||
|
if !strings.Contains(l, "=") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts := strings.SplitN(l, "=", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return rpki, fmt.Errorf("invalid rpki config line: %s", line)
|
||||||
|
}
|
||||||
|
key := strings.TrimSpace(parts[0])
|
||||||
|
value := strings.Split(strings.TrimSpace(parts[1]), ":")
|
||||||
|
|
||||||
|
if key == "enabled" {
|
||||||
|
rpki.Enabled = parts[1] == "true"
|
||||||
|
} else if key == "valid" {
|
||||||
|
rpki.Valid = append(rpki.Valid, value)
|
||||||
|
} else if key == "not_checked" {
|
||||||
|
rpki.NotChecked = append(rpki.NotChecked, value)
|
||||||
|
} else if key == "invalid" {
|
||||||
|
rpki.Invalid = append(rpki.Invalid, value)
|
||||||
|
} else if key == "unknown" {
|
||||||
|
rpki.Unknown = append(rpki.Unknown, value)
|
||||||
|
} else {
|
||||||
|
return rpki, fmt.Errorf("invalid rpki config line: %s", line)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
if err := section.MapTo(&rpki); err != nil {
|
if err := section.MapTo(&rpki); err != nil {
|
||||||
return rpki, err
|
return rpki, err
|
||||||
}
|
}
|
||||||
|
hasDefaultASN := true
|
||||||
fallbackAsn, err := getOwnASN(config)
|
asn, err := getDefaultASN(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(
|
hasDefaultASN = false
|
||||||
"Own ASN is not configured.",
|
|
||||||
"This might lead to unexpected behaviour with BGP large communities",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
ownAsn := fmt.Sprintf("%d", fallbackAsn)
|
|
||||||
|
|
||||||
// Fill in defaults or postprocess config value
|
// Fill in defaults or postprocess config value
|
||||||
if len(rpki.Valid) == 0 {
|
if len(rpki.Valid) == 0 && !hasDefaultASN && rpki.Enabled {
|
||||||
rpki.Valid = []string{ownAsn, "1000", "1"}
|
return rpki, fmt.Errorf(
|
||||||
} else {
|
"rpki.valid must be set if no server.asn is configured")
|
||||||
rpki.Valid = strings.SplitN(rpki.Valid[0], ":", 3)
|
}
|
||||||
|
if len(rpki.Valid) == 0 && rpki.Enabled {
|
||||||
|
log.Printf("Using default rpki.valid: %s:1000:1\n", asn)
|
||||||
|
rpki.Valid = [][]string{{asn, "1000", "1"}}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(rpki.Unknown) == 0 {
|
if len(rpki.Unknown) == 0 && !hasDefaultASN && rpki.Enabled {
|
||||||
rpki.Unknown = []string{ownAsn, "1000", "2"}
|
return rpki, fmt.Errorf(
|
||||||
} else {
|
"rpki.unknown must be set if no server.asn is configured")
|
||||||
rpki.Unknown = strings.SplitN(rpki.Unknown[0], ":", 3)
|
}
|
||||||
|
if len(rpki.Unknown) == 0 && rpki.Enabled {
|
||||||
|
log.Printf("Using default rpki.unknown: %s:1000:2\n", asn)
|
||||||
|
rpki.Unknown = [][]string{{asn, "1000", "2"}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(rpki.NotChecked) == 0 && !hasDefaultASN && rpki.Enabled {
|
||||||
|
return rpki, fmt.Errorf(
|
||||||
|
"rpki.not_checked must be set if no server.asn is set")
|
||||||
|
}
|
||||||
if len(rpki.NotChecked) == 0 {
|
if len(rpki.NotChecked) == 0 {
|
||||||
rpki.NotChecked = []string{ownAsn, "1000", "3"}
|
log.Printf("Using default rpki.not_checked: %s:1000:3\n", asn)
|
||||||
} else {
|
rpki.NotChecked = [][]string{{asn, "1000", "3"}}
|
||||||
rpki.NotChecked = strings.SplitN(rpki.NotChecked[0], ":", 3)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// As the euro-ix document states, this can be a range.
|
// As the euro-ix document states, this can be a range.
|
||||||
if len(rpki.Invalid) == 0 {
|
for i, com := range rpki.Invalid {
|
||||||
rpki.Invalid = []string{ownAsn, "1000", "4", "*"}
|
if len(com) != 3 {
|
||||||
} else {
|
return rpki, fmt.Errorf("Invalid rpki.invalid config: %v", com)
|
||||||
// Preprocess
|
|
||||||
rpki.Invalid = strings.SplitN(rpki.Invalid[0], ":", 3)
|
|
||||||
if len(rpki.Invalid) != 3 {
|
|
||||||
// This is wrong, we should have three parts (RS):1000:[range]
|
|
||||||
return rpki, fmt.Errorf(
|
|
||||||
"unexpected rpki.Invalid configuration: %v", rpki.Invalid)
|
|
||||||
}
|
}
|
||||||
tokens := strings.Split(rpki.Invalid[2], "-")
|
tokens := strings.Split(com[2], "-")
|
||||||
rpki.Invalid = append([]string{rpki.Invalid[0], rpki.Invalid[1]}, tokens...)
|
rpki.Invalid[i] = append([]string{com[0], com[1]}, tokens...)
|
||||||
|
}
|
||||||
|
if len(rpki.Invalid) == 0 && !hasDefaultASN && rpki.Enabled {
|
||||||
|
return rpki, fmt.Errorf(
|
||||||
|
"rpki.invalid must be set if no server.asn is configured")
|
||||||
|
}
|
||||||
|
if len(rpki.Invalid) == 0 && rpki.Enabled {
|
||||||
|
log.Printf("Using default rpki.invalid: %s:1000:4-*\n", asn)
|
||||||
|
rpki.Invalid = [][]string{{asn, "1000", "4", "*"}}
|
||||||
}
|
}
|
||||||
|
|
||||||
return rpki, nil
|
return rpki, nil
|
||||||
@ -538,12 +576,12 @@ func getRpkiConfig(config *ini.File) (RpkiConfig, error) {
|
|||||||
// Helper: Get own ASN from ini
|
// Helper: Get own ASN from ini
|
||||||
// This is now easy, since we enforce an ASN in
|
// This is now easy, since we enforce an ASN in
|
||||||
// the [server] section.
|
// the [server] section.
|
||||||
func getOwnASN(config *ini.File) (int, error) {
|
func getDefaultASN(config *ini.File) (string, error) {
|
||||||
server := config.Section("server")
|
server := config.Section("server")
|
||||||
asn := server.Key("asn").MustInt(-1)
|
asn := server.Key("asn").MustString("")
|
||||||
|
|
||||||
if asn == -1 {
|
if asn == "" {
|
||||||
return 0, fmt.Errorf("could not get own ASN from config")
|
return "", fmt.Errorf("could not get default ASN from config")
|
||||||
}
|
}
|
||||||
|
|
||||||
return asn, nil
|
return asn, nil
|
||||||
@ -822,15 +860,52 @@ func getSources(config *ini.File) ([]*SourceConfig, error) {
|
|||||||
return sources, nil
|
return sources, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// preprocessConfig parses the variables in the config
|
||||||
|
// and applies it to the rest of the config.
|
||||||
|
func preprocessConfig(data []byte) []byte {
|
||||||
|
lines := bytes.Split(data, []byte("\n"))
|
||||||
|
config := make([][]byte, 0, len(lines))
|
||||||
|
|
||||||
|
expMap := ExpandMap{}
|
||||||
|
for _, line := range lines {
|
||||||
|
l := strings.TrimSpace(string(line))
|
||||||
|
if strings.HasPrefix(l, "$") {
|
||||||
|
expMap.AddExpr(l[1:])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
config = append(config, line)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now apply to config
|
||||||
|
configLines := []string{}
|
||||||
|
for _, line := range config {
|
||||||
|
l := string(line)
|
||||||
|
exp, err := expMap.Expand(l)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Error expanding expression in config:", l, err)
|
||||||
|
}
|
||||||
|
for _, e := range exp {
|
||||||
|
configLines = append(configLines, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []byte(strings.Join(configLines, "\n"))
|
||||||
|
}
|
||||||
|
|
||||||
// LoadConfig reads a configuration from a file.
|
// LoadConfig reads a configuration from a file.
|
||||||
func LoadConfig(file string) (*Config, error) {
|
func LoadConfig(file string) (*Config, error) {
|
||||||
|
|
||||||
// Try to get config file, fallback to alternatives
|
// Try to get config file, fallback to alternatives
|
||||||
file, err := getConfigFile(file)
|
file, err := getConfigFile(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read the config file and preprocess it
|
||||||
|
configData, err := os.ReadFile(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
configData = preprocessConfig(configData)
|
||||||
|
|
||||||
// Load configuration, but handle bgp communities section
|
// Load configuration, but handle bgp communities section
|
||||||
// with our own parser
|
// with our own parser
|
||||||
parsedConfig, err := ini.LoadSources(ini.LoadOptions{
|
parsedConfig, err := ini.LoadSources(ini.LoadOptions{
|
||||||
@ -839,8 +914,9 @@ func LoadConfig(file string) (*Config, error) {
|
|||||||
"blackhole_communities",
|
"blackhole_communities",
|
||||||
"rejection_reasons",
|
"rejection_reasons",
|
||||||
"noexport_reasons",
|
"noexport_reasons",
|
||||||
|
"rpki",
|
||||||
},
|
},
|
||||||
}, file)
|
}, configData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -167,32 +167,21 @@ func TestBlackholeParsing(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOwnASN(t *testing.T) {
|
|
||||||
config, err := LoadConfig("testdata/alice.conf")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Could not load test config:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.Server.Asn != 9033 {
|
|
||||||
t.Error("Expected a set server asn")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRpkiConfig(t *testing.T) {
|
func TestRpkiConfig(t *testing.T) {
|
||||||
config, err := LoadConfig("testdata/alice.conf")
|
config, err := LoadConfig("testdata/alice.conf")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal("Could not load test config:", err)
|
t.Fatal("Could not load test config:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(config.UI.Rpki.Valid) != 3 {
|
if len(config.UI.Rpki.Valid[0]) != 3 {
|
||||||
t.Error("Unexpected RPKI:VALID,", config.UI.Rpki.Valid)
|
t.Error("Unexpected RPKI:VALID,", config.UI.Rpki.Valid)
|
||||||
}
|
}
|
||||||
if len(config.UI.Rpki.Invalid) != 4 {
|
if len(config.UI.Rpki.Invalid[0]) != 4 {
|
||||||
t.Fatal("Unexpected RPKI:INVALID,", config.UI.Rpki.Invalid)
|
t.Fatal("Unexpected RPKI:INVALID,", config.UI.Rpki.Invalid)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check fallback
|
// Check fallback
|
||||||
if config.UI.Rpki.NotChecked[0] != "9033" {
|
if config.UI.Rpki.NotChecked[0][0] != "9999" {
|
||||||
t.Error(
|
t.Error(
|
||||||
"Expected NotChecked to fall back to defaults, got:",
|
"Expected NotChecked to fall back to defaults, got:",
|
||||||
config.UI.Rpki.NotChecked,
|
config.UI.Rpki.NotChecked,
|
||||||
@ -200,7 +189,7 @@ func TestRpkiConfig(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check range postprocessing
|
// Check range postprocessing
|
||||||
if config.UI.Rpki.Invalid[3] != "*" {
|
if config.UI.Rpki.Invalid[0][3] != "*" {
|
||||||
t.Error("Missing wildcard from config")
|
t.Error("Missing wildcard from config")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
187
pkg/config/expand.go
Normal file
187
pkg/config/expand.go
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Compile input pattern regex
|
||||||
|
var (
|
||||||
|
expandMatchPlaceholder = regexp.MustCompile(`(?U:{.*}+?)`)
|
||||||
|
expandMatchWildcardShorthard = regexp.MustCompile(`(?U:{{.*\*}}+?)`)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Extract all matches from the input string.
|
||||||
|
// The pattern to find is {INPUT}. The input string
|
||||||
|
// itself can contain new matches.
|
||||||
|
func expandFindPlaceholders(s string) []string {
|
||||||
|
|
||||||
|
// Find all matches
|
||||||
|
results := expandMatchPlaceholder.FindAllString(s, -1)
|
||||||
|
if len(results) == 0 {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
matches := []string{}
|
||||||
|
for _, result := range results {
|
||||||
|
key := expandGetKey(result)
|
||||||
|
subP := expandFindPlaceholders(key)
|
||||||
|
matches = append(matches, result)
|
||||||
|
matches = append(matches, subP...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return matches
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the key from the placeholder
|
||||||
|
func expandGetKey(s string) string {
|
||||||
|
// Strip the enclosing curly braces
|
||||||
|
s = strings.TrimPrefix(s, "{")
|
||||||
|
s = strings.TrimSuffix(s, "}")
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpandMap holds the current state of variables
|
||||||
|
type ExpandMap map[string]string
|
||||||
|
|
||||||
|
// Retrieve a set of matching variables, by iterating variables.
|
||||||
|
// Whenever a key matches the wildcard, the prefix is removed.
|
||||||
|
// Example:
|
||||||
|
// pattern = "AS*", key = "AS2342", value = "2342"
|
||||||
|
func (e ExpandMap) matchWildcard(pattern string) []string {
|
||||||
|
matches := []string{}
|
||||||
|
|
||||||
|
// Strip the wildcard from the pattern.
|
||||||
|
pattern = strings.TrimSuffix(pattern, "*")
|
||||||
|
|
||||||
|
// Iterate variables and add match to result set
|
||||||
|
for k, _ := range e {
|
||||||
|
if strings.HasPrefix(k, pattern) {
|
||||||
|
key := strings.TrimPrefix(k, pattern)
|
||||||
|
matches = append(matches, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matches
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all substitutions for a given key.
|
||||||
|
// This method will return an error, if a placeholder
|
||||||
|
// does not match.
|
||||||
|
func (e ExpandMap) getSubstitutions(key string) []string {
|
||||||
|
// Check if the placeholder is a wildcard
|
||||||
|
if strings.HasSuffix(key, "*") {
|
||||||
|
return e.matchWildcard(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the placeholder is direct match
|
||||||
|
if val, ok := e[key]; ok {
|
||||||
|
return []string{val}
|
||||||
|
}
|
||||||
|
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get placeholder level. This is the number of opening
|
||||||
|
// curly braces in the placeholder.
|
||||||
|
func expandGetLevel(s string) int {
|
||||||
|
level := 0
|
||||||
|
for _, c := range s {
|
||||||
|
if c == '{' {
|
||||||
|
level++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return level
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preprocess input string and resolve syntactic sugar.
|
||||||
|
// Replace {{VAR}} with {VAR{VAR}} to make it easier
|
||||||
|
// to access the wildcard value.
|
||||||
|
func expandPreprocess(s string) string {
|
||||||
|
// Find all access shorthands and replace them
|
||||||
|
// with the full syntax
|
||||||
|
results := expandMatchWildcardShorthard.FindAllString(s, -1)
|
||||||
|
for _, match := range results {
|
||||||
|
// Wildcard {{KEY*}} -> KEY
|
||||||
|
key := match[2 : len(match)-3]
|
||||||
|
expr := fmt.Sprintf("{%s{%s*}}", key, key)
|
||||||
|
s = strings.Replace(s, match, expr, -1)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expand variables by recursive substitution and expansion
|
||||||
|
func (e ExpandMap) Expand(s string) ([]string, error) {
|
||||||
|
// Preprocess syntactic sugar: replace {{VAR}}
|
||||||
|
// with {VAR{VAR}}
|
||||||
|
s = expandPreprocess(s)
|
||||||
|
|
||||||
|
// Find all placeholders and substitute them
|
||||||
|
placeholders := expandFindPlaceholders(s)
|
||||||
|
if len(placeholders) == 0 {
|
||||||
|
return []string{s}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find substitutions for each placeholder
|
||||||
|
substitutions := map[string][]string{}
|
||||||
|
for _, p := range placeholders {
|
||||||
|
key := expandGetKey(p)
|
||||||
|
subs := e.getSubstitutions(key)
|
||||||
|
if len(subs) == 0 {
|
||||||
|
level := expandGetLevel(p)
|
||||||
|
if level == 1 {
|
||||||
|
err := fmt.Errorf("No substitution for %s in '%s'", p, s)
|
||||||
|
return []string{}, err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
substitutions[p] = subs
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply substitutions
|
||||||
|
subsRes := []string{s}
|
||||||
|
for p, subs := range substitutions {
|
||||||
|
subsExp := []string{}
|
||||||
|
for _, s := range subsRes {
|
||||||
|
for _, sub := range subs {
|
||||||
|
res := strings.Replace(s, p, sub, -1)
|
||||||
|
subsExp = append(subsExp, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
subsRes = subsExp
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expand recursively
|
||||||
|
results := []string{}
|
||||||
|
for _, s := range subsRes {
|
||||||
|
res, err := e.Expand(s)
|
||||||
|
if err != nil {
|
||||||
|
return []string{}, err
|
||||||
|
}
|
||||||
|
results = append(results, res...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a new variable to the map. Key and value are
|
||||||
|
// expanded.
|
||||||
|
func (e ExpandMap) AddExpr(expr string) error {
|
||||||
|
// Expand expression
|
||||||
|
res, err := e.Expand(expr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, exp := range res {
|
||||||
|
// Split key and value
|
||||||
|
parts := strings.SplitN(exp, "=", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return fmt.Errorf("Invalid expression '%s'", expr)
|
||||||
|
}
|
||||||
|
key := strings.TrimSpace(parts[0])
|
||||||
|
val := strings.TrimSpace(parts[1])
|
||||||
|
e[key] = val
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
122
pkg/config/expand_test.go
Normal file
122
pkg/config/expand_test.go
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Text variable pattern matching
|
||||||
|
func TestExpandMatch(t *testing.T) {
|
||||||
|
exp := ExpandMap{
|
||||||
|
"AS2342": "",
|
||||||
|
"AS1111": "",
|
||||||
|
"FOOBAR": "foo",
|
||||||
|
}
|
||||||
|
|
||||||
|
matches := exp.matchWildcard("AS*")
|
||||||
|
if len(matches) != 2 {
|
||||||
|
t.Errorf("Expected 2 matches, got %d", len(matches))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, m := range matches {
|
||||||
|
t.Log("Match wildcard:", m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test variable expansion / substitution
|
||||||
|
func TestFindPlaceholders(t *testing.T) {
|
||||||
|
s := "{FOO} BAR {AS{AS*}}"
|
||||||
|
placeholders := expandFindPlaceholders(s)
|
||||||
|
if len(placeholders) != 3 {
|
||||||
|
t.Errorf("Expected 3 placeholders, got %d", len(placeholders))
|
||||||
|
}
|
||||||
|
t.Log(placeholders)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test variable expansion / substitution
|
||||||
|
func TestExpand(t *testing.T) {
|
||||||
|
s := "{FOO} BAR {AS{AS*}} AS {AS*}"
|
||||||
|
exp := ExpandMap{
|
||||||
|
"AS2342": "AS2342",
|
||||||
|
"AS1111": "AS1111",
|
||||||
|
"FOO": "foo",
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err := exp.Expand(s)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
t.Log(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExpandErr(t *testing.T) {
|
||||||
|
s := "{FOO} BAR {AS{AS*}} AS {AS*} {UNKNOWN}"
|
||||||
|
exp := ExpandMap{
|
||||||
|
"AS2342": "AS2342",
|
||||||
|
"AS1111": "AS1111",
|
||||||
|
"FOO": "foo",
|
||||||
|
"FN": "fn",
|
||||||
|
"FA": "fa",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := exp.Expand(s)
|
||||||
|
t.Log(err)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExpandPreprocess(t *testing.T) {
|
||||||
|
s := "FOO {FOO} {{AS*}} {F*} {{F*}} {X{X*}}"
|
||||||
|
expect := "FOO {FOO} {AS{AS*}} {F*} {F{F*}} {X{X*}}"
|
||||||
|
s = expandPreprocess(s)
|
||||||
|
if s != expect {
|
||||||
|
t.Errorf("Expected '%s', got '%s'", expect, s)
|
||||||
|
}
|
||||||
|
t.Log(s)
|
||||||
|
|
||||||
|
s = "TEST {{FN}}"
|
||||||
|
s = expandPreprocess(s)
|
||||||
|
t.Log(s)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExpandAddExpr(t *testing.T) {
|
||||||
|
e := ExpandMap{
|
||||||
|
"FOO": "foo23",
|
||||||
|
"BAR": "bar42",
|
||||||
|
"bar42": "BAM",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := e.AddExpr("FOOBAR = {FOO}{BAR}{{BAR}}"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Log(e)
|
||||||
|
|
||||||
|
if e["FOOBAR"] != "foo23bar42BAM" {
|
||||||
|
t.Error("Expected 'foo23bar42BAM', got", e["FOOBAR"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExpandBgpCommunities(t *testing.T) {
|
||||||
|
e := ExpandMap{
|
||||||
|
"ASRS01": "6695",
|
||||||
|
"ASRS02": "4617",
|
||||||
|
"SW1001": "edge01.fra2",
|
||||||
|
"SW1002": "edge01.fra6",
|
||||||
|
"SW2038": "edge01.nyc1",
|
||||||
|
"RDCTL911": "Redistribute",
|
||||||
|
"RDCTL922": "Do not redistribute",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some large communities:
|
||||||
|
expr := "{{AS*}}:{RDCTL*}:{SW*} = {{RDCTL*}} to {{SW*}}"
|
||||||
|
exp, err := e.Expand(expr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
expected := 2 * 3 * 2
|
||||||
|
if len(exp) != expected {
|
||||||
|
t.Errorf("Expected %d results, got %d", expected, len(exp))
|
||||||
|
}
|
||||||
|
t.Log(exp)
|
||||||
|
}
|
52
pkg/config/testdata/alice.conf
vendored
52
pkg/config/testdata/alice.conf
vendored
@ -2,6 +2,13 @@
|
|||||||
# Alice-LG configuration example
|
# Alice-LG configuration example
|
||||||
# ======================================
|
# ======================================
|
||||||
|
|
||||||
|
$ASN01 = 1111
|
||||||
|
$ASN02 = 2222
|
||||||
|
|
||||||
|
$SW1001 = switch01.dc01
|
||||||
|
$SW1002 = switch02.dc01
|
||||||
|
$SW2023 = switch23.dc02
|
||||||
|
|
||||||
[server]
|
[server]
|
||||||
# configures the built-in webserver and provides global application settings
|
# configures the built-in webserver and provides global application settings
|
||||||
listen_http = 127.0.0.1:7340
|
listen_http = 127.0.0.1:7340
|
||||||
@ -9,7 +16,6 @@ listen_http = 127.0.0.1:7340
|
|||||||
enable_prefix_lookup = true
|
enable_prefix_lookup = true
|
||||||
# Try to refresh the neighbor status on every request to /neighbors
|
# Try to refresh the neighbor status on every request to /neighbors
|
||||||
enable_neighbors_status_refresh = false
|
enable_neighbors_status_refresh = false
|
||||||
asn = 9033
|
|
||||||
# this ASN is used as a fallback value in the RPKI feature and for route
|
# this ASN is used as a fallback value in the RPKI feature and for route
|
||||||
# filtering evaluation with large BGP communities
|
# filtering evaluation with large BGP communities
|
||||||
|
|
||||||
@ -29,6 +35,8 @@ stream_parser_throttle = 2342
|
|||||||
|
|
||||||
store_backend = postgres
|
store_backend = postgres
|
||||||
|
|
||||||
|
asn = 9999
|
||||||
|
|
||||||
[postgres]
|
[postgres]
|
||||||
url = "postgres://postgres:postgres@localhost:5432/alice"
|
url = "postgres://postgres:postgres@localhost:5432/alice"
|
||||||
min_connections = 10
|
min_connections = 10
|
||||||
@ -55,16 +63,16 @@ routes_not_exported_page_size = 250
|
|||||||
[rejection_reasons]
|
[rejection_reasons]
|
||||||
# a pair of a large BGP community value and a string to signal the processing
|
# a pair of a large BGP community value and a string to signal the processing
|
||||||
# results of route filtering
|
# results of route filtering
|
||||||
9033:65666:1 = An IP Bogon was detected
|
{{ASN*}}:65666:1 = An IP Bogon was detected
|
||||||
9033:65666:2 = Prefix is longer than 64
|
{{ASN*}}:65666:2 = Prefix is longer than 64
|
||||||
9033:65666:3 = Prefix is longer than 24
|
{{ASN*}}:65666:3 = Prefix is longer than 24
|
||||||
9033:65666:4 = AS path contains a bogon AS
|
{{ASN*}}:65666:4 = AS path contains a bogon AS
|
||||||
9033:65666:5 = AS path length is longer than 64
|
{{ASN*}}:65666:5 = AS path length is longer than 64
|
||||||
9033:65666:6 = First AS in path is not the same as the Peer AS
|
{{ASN*}}:65666:6 = First AS in path is not the same as the Peer AS
|
||||||
9033:65666:7 = ECIX prefix hijack
|
{{ASN*}}:65666:7 = ECIX prefix hijack
|
||||||
9033:65666:8 = Origin AS not found in IRRDB for Peer AS-SET
|
{{ASN*}}:65666:8 = Origin AS not found in IRRDB for Peer AS-SET
|
||||||
9033:65666:9 = Prefix not found in IRRDB for Origin AS
|
{{ASN*}}:65666:9 = Prefix not found in IRRDB for Origin AS
|
||||||
9033:65666:10 = Advertised nexthop address is not the same as the peer
|
{{ASN*}}:65666:10 = Advertised nexthop address is not the same as the peer
|
||||||
|
|
||||||
23:42:1 = Some made up reason
|
23:42:1 = Some made up reason
|
||||||
|
|
||||||
@ -80,13 +88,13 @@ load_on_demand = true # Default: false
|
|||||||
[noexport_reasons]
|
[noexport_reasons]
|
||||||
# a pair of a large BGP community value and a string to signal the processing
|
# a pair of a large BGP community value and a string to signal the processing
|
||||||
# results of route distribution and the distribution policy applied to a route
|
# results of route distribution and the distribution policy applied to a route
|
||||||
9033:65667:1 = The target peer policy is Fairly-open and the sender ASN is an exception
|
{{ASN*}}:65667:1 = The target peer policy is Fairly-open and the sender ASN is an exception
|
||||||
9033:65667:2 = The target peer policy is Selective and the sender ASN is no exception
|
{{ASN*}}:65667:2 = The target peer policy is Selective and the sender ASN is no exception
|
||||||
9033:65667:3 = The target peer policy is set to restrictive
|
{{ASN*}}:65667:3 = The target peer policy is set to restrictive
|
||||||
9033:65667:4 = The sender has specifically refused export to the target peer, either through sending 65000:AS, or through the portal
|
{{ASN*}}:65667:4 = The sender has specifically refused export to the target peer, either through sending 65000:AS, or through the portal
|
||||||
9033:65667:5 = The sender has refused export to all peers and the target is no exception, either through sending 65000:0, or through the portal
|
{{ASN*}}:65667:5 = The sender has refused export to all peers and the target is no exception, either through sending 65000:0, or through the portal
|
||||||
9033:65667:6 = The Sender has set (peerRTTHigherDeny:ms) and the targets RTT ms >= then the ms in the community
|
{{ASN*}}:65667:6 = The Sender has set (peerRTTHigherDeny:ms) and the targets RTT ms >= then the ms in the community
|
||||||
9033:65667:7 = The Sender has set (peerRTTLowerDeny:ms) and the targets RTT ms <= then the ms in the community
|
{{ASN*}}:65667:7 = The Sender has set (peerRTTLowerDeny:ms) and the targets RTT ms <= then the ms in the community
|
||||||
|
|
||||||
23:46:1 = Some other made up reason
|
23:46:1 = Some other made up reason
|
||||||
|
|
||||||
@ -102,10 +110,10 @@ enabled = true
|
|||||||
|
|
||||||
# Optional, falling back to defaults as defined in:
|
# Optional, falling back to defaults as defined in:
|
||||||
# https://www.euro-ix.net/en/forixps/large-bgp-communities/
|
# https://www.euro-ix.net/en/forixps/large-bgp-communities/
|
||||||
valid = 23042:1000:1
|
valid = {{ASN*}}:1000:1
|
||||||
unknown = 23042:1000:2
|
unknown = {{ASN*}}:1000:2
|
||||||
# not_checked = 23042:1000:3
|
# not_checked = 23042:1000:3
|
||||||
invalid = 23042:1000:4-*
|
invalid = {{ASN*}}:1000:4-*
|
||||||
|
|
||||||
|
|
||||||
# Define other known bgp communities
|
# Define other known bgp communities
|
||||||
@ -115,6 +123,8 @@ invalid = 23042:1000:4-*
|
|||||||
# Wildcards are supported aswell:
|
# Wildcards are supported aswell:
|
||||||
0:* = do not redistribute to AS$1
|
0:* = do not redistribute to AS$1
|
||||||
|
|
||||||
|
{{ASN*}}:911:{SW*} = Redistribute to {{SW*}}
|
||||||
|
|
||||||
#
|
#
|
||||||
# Define columns for neighbours and routes table,
|
# Define columns for neighbours and routes table,
|
||||||
# with <key> = <Table Header>
|
# with <key> = <Table Header>
|
||||||
|
@ -27,11 +27,16 @@ const RpkiIndicator = ({route}) => {
|
|||||||
const rpkiInvalid = rpki.invalid;
|
const rpkiInvalid = rpki.invalid;
|
||||||
|
|
||||||
const communities = route?.bgp?.large_communities || [];
|
const communities = route?.bgp?.large_communities || [];
|
||||||
|
|
||||||
|
const matchCommunity = (com, coms) =>
|
||||||
|
coms.some((match) =>
|
||||||
|
(com[0].toFixed() === match[0] &&
|
||||||
|
com[1].toFixed() === match[1] &&
|
||||||
|
com[2].toFixed() === match[2]));
|
||||||
|
|
||||||
for (const com of communities) {
|
for (const com of communities) {
|
||||||
// RPKI VALID
|
// RPKI VALID
|
||||||
if (com[0].toFixed() === rpkiValid[0] &&
|
if (matchCommunity(com, rpkiValid)) {
|
||||||
com[1].toFixed() === rpkiValid[1] &&
|
|
||||||
com[2].toFixed() === rpkiValid[2]) {
|
|
||||||
return (
|
return (
|
||||||
<span className="route-prefix-flag rpki-route rpki-valid">
|
<span className="route-prefix-flag rpki-route rpki-valid">
|
||||||
<FlagIcon icon={faCircleCheck} tooltip="RPKI Valid" />
|
<FlagIcon icon={faCircleCheck} tooltip="RPKI Valid" />
|
||||||
@ -40,9 +45,7 @@ const RpkiIndicator = ({route}) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RPKI UNKNOWN
|
// RPKI UNKNOWN
|
||||||
if (com[0].toFixed() === rpkiUnknown[0] &&
|
if (matchCommunity(com, rpkiUnknown)) {
|
||||||
com[1].toFixed() === rpkiUnknown[1] &&
|
|
||||||
com[2].toFixed() === rpkiUnknown[2]) {
|
|
||||||
return (
|
return (
|
||||||
<span className="route-prefix-flag rpki-route rpki-unknown">
|
<span className="route-prefix-flag rpki-route rpki-unknown">
|
||||||
<FlagIcon icon={faCircleQuestion} tooltip="RPKI Unknown" />
|
<FlagIcon icon={faCircleQuestion} tooltip="RPKI Unknown" />
|
||||||
@ -51,9 +54,7 @@ const RpkiIndicator = ({route}) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RPKI NOT CHECKED
|
// RPKI NOT CHECKED
|
||||||
if (com[0].toFixed() === rpkiNotChecked[0] &&
|
if (matchCommunity(com, rpkiNotChecked)) {
|
||||||
com[1].toFixed() === rpkiNotChecked[1] &&
|
|
||||||
com[2].toFixed() === rpkiNotChecked[2]) {
|
|
||||||
return (
|
return (
|
||||||
<span className="route-prefix-flag rpki-route rpki-not-checked">
|
<span className="route-prefix-flag rpki-route rpki-not-checked">
|
||||||
<FlagIcon icon={faCircle} tooltip="RPKI Not Checked" />
|
<FlagIcon icon={faCircle} tooltip="RPKI Not Checked" />
|
||||||
@ -65,20 +66,23 @@ const RpkiIndicator = ({route}) => {
|
|||||||
// Depending on the configration this can either be a
|
// Depending on the configration this can either be a
|
||||||
// single flag or a range with a given reason.
|
// single flag or a range with a given reason.
|
||||||
let rpkiInvalidReason = 0;
|
let rpkiInvalidReason = 0;
|
||||||
if (com[0].toFixed() === rpkiInvalid[0] &&
|
for (const invalid of rpkiInvalid) {
|
||||||
com[1].toFixed() === rpkiInvalid[1]) {
|
if (com[0].toFixed() === invalid[0] &&
|
||||||
|
com[1].toFixed() === invalid[1]) {
|
||||||
|
|
||||||
// This needs to be considered invalid, now try to detect why
|
// This needs to be considered invalid, now try to detect why
|
||||||
if (rpkiInvalid.length > 3 && rpkiInvalid[3] === "*") {
|
if (invalid.length > 3 && invalid[3] === "*") {
|
||||||
// Check if token falls within range
|
// Check if token falls within range
|
||||||
const start = parseInt(rpkiInvalid[2], 10);
|
const start = parseInt(invalid[2], 10);
|
||||||
if (com[2] >= start) {
|
if (com[2] >= start) {
|
||||||
rpkiInvalidReason = com[2];
|
rpkiInvalidReason = com[2];
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (com[2].toFixed() === rpkiInvalid[2]) {
|
if (com[2].toFixed() === invalid[2]) {
|
||||||
rpkiInvalidReason = 1;
|
rpkiInvalidReason = 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
break; // We found a match, stop searching
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,10 +10,10 @@ import RpkiIndicator
|
|||||||
const config = {
|
const config = {
|
||||||
rpki: {
|
rpki: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
valid: ["1234", "1111", "1"],
|
valid: [["1234", "1111", "1"]],
|
||||||
unknown: ["1234", "1111", "0"],
|
unknown: [["1234", "1111", "0"]],
|
||||||
not_checked: ["1234", "1111", "10"],
|
not_checked: [["1234", "1111", "10"]],
|
||||||
invalid: ["1234", "1111", "100"],
|
invalid: [["1234", "1111", "100"]],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user