Merge branch 'feature/blackhole-communities' into develop

This commit is contained in:
Annika Hannig 2022-11-24 17:59:18 +01:00
commit b47769817e
10 changed files with 281 additions and 38 deletions

View File

@ -95,6 +95,11 @@ load_on_demand = true # Default: false
23:46:1 = Some other made up reason
[blackhole_communities]
65535:666
12345:1105-1189:*
12345:1111:10-90
rt:1234:4200000000-4200010000
[rpki]
# shows rpki validation status in the client, based on the presence of a large

View File

@ -173,3 +173,33 @@ func (c BGPCommunityMap) Communities() Communities {
}
return communities
}
// BGPCommunity types: Standard, Extended and Large
const (
BGPCommunityTypeStd = iota
BGPCommunityTypeExt
BGPCommunityTypeLarge
)
// BGPCommunityRange is a list of tuples with the start and end
// of the range defining a community.
type BGPCommunityRange []interface{}
// Type classifies the BGP Ranged BGP Community into: std, large, ext
func (c BGPCommunityRange) Type() int {
if len(c) == 2 {
return BGPCommunityTypeStd
}
if _, ok := c[0].([]string); ok {
return BGPCommunityTypeExt
}
return BGPCommunityTypeLarge
}
// A BGPCommunitiesSet is a set of communities, large and extended.
// The communities are described as ranges.
type BGPCommunitiesSet struct {
Standard []BGPCommunityRange `json:"standard"`
Extended []BGPCommunityRange `json:"extended"`
Large []BGPCommunityRange `json:"large"`
}

View File

@ -38,7 +38,8 @@ type ConfigResponse struct {
Rpki Rpki `json:"rpki"`
BGPCommunities map[string]interface{} `json:"bgp_communities"`
BGPCommunities map[string]interface{} `json:"bgp_communities"`
BGPBlackholeCommunities BGPCommunitiesSet `json:"bgp_blackhole_communities"`
NeighborsColumns map[string]string `json:"neighbors_columns"`
NeighborsColumnsOrder []string `json:"neighbors_columns_order"`

View File

@ -0,0 +1,121 @@
package config
import (
"fmt"
"log"
"strconv"
"strings"
"github.com/alice-lg/alice-lg/pkg/api"
"github.com/alice-lg/alice-lg/pkg/decoders"
)
// ErrInvalidCommunity creates an invalid community error
func ErrInvalidCommunity(s string) error {
return fmt.Errorf("invalid community: %s", s)
}
// Helper parse communities from a section body
func parseAndMergeCommunities(
communities api.BGPCommunityMap, body string,
) api.BGPCommunityMap {
// Parse and merge communities
lines := strings.Split(body, "\n")
for _, line := range lines {
kv := strings.SplitN(line, "=", 2)
if len(kv) != 2 {
log.Println("Skipping malformed BGP community:", line)
continue
}
community := strings.TrimSpace(kv[0])
label := strings.TrimSpace(kv[1])
communities.Set(community, label)
}
return communities
}
// Parse a communities set with ranged communities
func parseRangeCommunitiesSet(body string) (*api.BGPCommunitiesSet, error) {
comms := []api.BGPCommunityRange{}
large := []api.BGPCommunityRange{}
ext := []api.BGPCommunityRange{}
lines := strings.Split(body, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue // Empty
}
if strings.HasPrefix(line, "#") {
continue // Comment
}
comm, err := parseRangeCommunity(line)
if err != nil {
return nil, err
}
switch comm.Type() {
case api.BGPCommunityTypeStd:
comms = append(comms, comm)
case api.BGPCommunityTypeLarge:
large = append(large, comm)
case api.BGPCommunityTypeExt:
ext = append(ext, comm)
}
}
set := &api.BGPCommunitiesSet{
Standard: comms,
Large: large,
Extended: ext,
}
return set, nil
}
func parseRangeCommunity(s string) (api.BGPCommunityRange, error) {
tokens := strings.Split(s, ":")
if len(tokens) < 2 {
return nil, ErrInvalidCommunity(s)
}
// Extract ranges and make uniform structure
parts := make([][]string, 0, len(tokens))
for _, t := range tokens {
values := strings.SplitN(t, "-", 2)
if len(values) == 0 {
return nil, ErrInvalidCommunity(s)
}
if len(values) == 1 {
parts = append(parts, []string{values[0], values[0]})
} else {
parts = append(parts, []string{values[0], values[1]})
}
}
if len(parts) <= 1 {
return nil, ErrInvalidCommunity(s)
}
// Check if this might be an ext community
isExt := false
if _, err := strconv.Atoi(parts[0][0]); err != nil {
isExt = true // At least it looks like...
}
if isExt && len(parts) != 3 {
return nil, ErrInvalidCommunity(s)
}
if isExt {
return api.BGPCommunityRange{
[]string{parts[0][0], parts[0][0]},
decoders.IntListFromStrings(parts[1]),
decoders.IntListFromStrings(parts[2]),
}, nil
}
comm := api.BGPCommunityRange{}
for _, p := range parts {
comm = append(comm, decoders.IntListFromStrings(p))
}
return comm, nil
}

View File

@ -146,8 +146,9 @@ type UIConfig struct {
RoutesNoexports NoexportsConfig
RoutesRejectCandidates RejectCandidatesConfig
BGPCommunities api.BGPCommunityMap
Rpki RpkiConfig
BGPCommunities api.BGPCommunityMap
BGPBlackholeCommunities api.BGPCommunitiesSet
Rpki RpkiConfig
Theme ThemeConfig
@ -387,28 +388,6 @@ func getLookupColumns(config *ini.File) (
return columns, order, nil
}
// Helper parse communities from a section body
func parseAndMergeCommunities(
communities api.BGPCommunityMap, body string,
) api.BGPCommunityMap {
// Parse and merge communities
lines := strings.Split(body, "\n")
for _, line := range lines {
kv := strings.SplitN(line, "=", 2)
if len(kv) != 2 {
log.Println("Skipping malformed BGP community:", line)
continue
}
community := strings.TrimSpace(kv[0])
label := strings.TrimSpace(kv[1])
communities.Set(community, label)
}
return communities
}
// Get UI config: BGP Communities
func getBGPCommunityMap(config *ini.File) api.BGPCommunityMap {
// Load defaults
@ -459,6 +438,25 @@ func getRoutesNoexports(config *ini.File) (NoexportsConfig, error) {
return noexportsConfig, nil
}
// Get UI config: blackhole communities
func getBlackholeCommunities(config *ini.File) (api.BGPCommunitiesSet, error) {
section := config.Section("blackhole_communities")
defaultBlackholes := api.BGPCommunitiesSet{
Standard: []api.BGPCommunityRange{
{[]interface{}{65535, 65535}, []interface{}{666, 666}},
},
}
if section == nil {
return defaultBlackholes, nil
}
set, err := parseRangeCommunitiesSet(section.Body())
if err != nil {
return defaultBlackholes, err
}
set.Standard = append(set.Standard, defaultBlackholes.Standard...)
return *set, nil
}
// Get UI config: Reject candidates
func getRejectCandidatesConfig(config *ini.File) (RejectCandidatesConfig, error) {
candidateCommunities := config.Section(
@ -618,6 +616,12 @@ func getUIConfig(config *ini.File) (UIConfig, error) {
return uiConfig, err
}
// Blackhole communities
blackholeCommunities, err := getBlackholeCommunities(config)
if err != nil {
return uiConfig, err
}
// Theme configuration: Theming is optional, if no settings
// are found, it will be ignored
themeConfig := getThemeConfig(config)
@ -640,8 +644,9 @@ func getUIConfig(config *ini.File) (UIConfig, error) {
RoutesNoexports: noexports,
RoutesRejectCandidates: rejectCandidates,
BGPCommunities: getBGPCommunityMap(config),
Rpki: rpki,
BGPBlackholeCommunities: blackholeCommunities,
BGPCommunities: getBGPCommunityMap(config),
Rpki: rpki,
Theme: themeConfig,
@ -830,6 +835,7 @@ func LoadConfig(file string) (*Config, error) {
parsedConfig, err := ini.LoadSources(ini.LoadOptions{
UnparseableSections: []string{
"bgp_communities",
"blackhole_communities",
"rejection_reasons",
"noexport_reasons",
},

View File

@ -227,7 +227,7 @@ func TestRejectCandidatesConfig(t *testing.T) {
func TestDefaultHTTPTimeout(t *testing.T) {
config, err := LoadConfig("testdata/alice.conf")
if err != nil {
t.Error("Could not load test config:", err)
t.Fatal("Could not load test config:", err)
}
if config.Server.HTTPTimeout != DefaultHTTPTimeout {
@ -249,3 +249,19 @@ func TestPostgresStoreConfig(t *testing.T) {
}
t.Log(config.Postgres)
}
func TestGetBlackholeCommunities(t *testing.T) {
config, _ := LoadConfig("testdata/alice.conf")
comms := config.UI.BGPBlackholeCommunities
if comms.Standard[0][0].([]int)[0] != 1337 {
t.Error("unexpected community:", comms.Standard[0])
}
if len(comms.Extended) != 1 {
t.Error("unexpected communities:", comms.Extended)
}
if len(comms.Large) != 1 {
t.Error("unexpected communities:", comms.Large)
}
t.Log(comms)
}

View File

@ -88,6 +88,10 @@ load_on_demand = true # Default: false
23:46:1 = Some other made up reason
[blackhole_communities]
1337:666
rt:1324:4200000000-4200010000
2342:65530-65535:665-667
[rpki]
# shows rpki validation status in the client, based on the presence of a large
@ -128,6 +132,7 @@ invalid = 23042:1000:4-*
# Uptime Displays the relative uptime of this neighbour
# Description The neighbour's description with link to routes page
#
#
[neighbours_columns]
address = Neighbour

View File

@ -51,9 +51,10 @@ func (s *Server) apiConfigShow(
_params httprouter.Params,
) (response, error) {
result := api.ConfigResponse{
Asn: s.cfg.Server.Asn,
BGPCommunities: s.cfg.UI.BGPCommunities,
RejectReasons: s.cfg.UI.RoutesRejections.Reasons,
Asn: s.cfg.Server.Asn,
BGPCommunities: s.cfg.UI.BGPCommunities,
BGPBlackholeCommunities: s.cfg.UI.BGPBlackholeCommunities,
RejectReasons: s.cfg.UI.RoutesRejections.Reasons,
Noexport: api.Noexport{
LoadOnDemand: s.cfg.UI.RoutesNoexports.LoadOnDemand,
},

View File

@ -6,26 +6,53 @@ import { faCircle }
import { useRouteServer }
from 'app/context/route-servers';
import { matchCommunityRange
, useBlackholeCommunities
}
from 'app/context/bgp';
const BlackholeIndicator = ({route}) => {
const routeServer = useRouteServer(); // blackholes are store per RS
const blackholeCommunities = useBlackholeCommunities();
const blackholes = routeServer?.blackholes || [];
const communities = route?.bgp?.communities || [];
const nextHop = route?.bgp?.next_hop;
const routeStandard = route?.bgp?.communities || [];
const routeExtended = route?.bgp?.ext_communities || [];
const routeLarge = route?.bgp?.large_communities || [];
// Check if next hop is a known blackhole
let isBlackhole = blackholes.includes(nextHop);
// Check if BGP community 65535:666 is set
for (const c of communities) {
if (c[0] === 65535 && c[1] === 666) {
isBlackhole = true;
break;
// Check standard communities
for (const c of blackholeCommunities.standard) {
for (const r of routeStandard) {
if (matchCommunityRange(r, c)) {
isBlackhole = true;
break;
}
}
}
// Check large communities
for (const c of blackholeCommunities.large) {
for (const r of routeLarge) {
if (matchCommunityRange(r, c)) {
isBlackhole = true;
break;
}
}
}
// Check extended
for (const c of blackholeCommunities.extended) {
for (const r of routeExtended) {
if (matchCommunityRange(r, c)) {
isBlackhole = true;
break;
}
}
}
if (isBlackhole) {
return(
<span className="route-prefix-flag blackhole-route is-blackhole-route">

View File

@ -114,3 +114,34 @@ export const useReadableCommunity = (community) => {
community, getLabel,
]);
}
/**
* Get blackhole communities from config
*/
export const useBlackholeCommunities = () => {
let config = useConfig();
return config.bgp_blackhole_communities;
}
/**
* Match community ranges. When doing so, make sure
* you compare the right communities. e.g. comparing
* a large and an extended community will lead to unexpected
* results.
*/
export const matchCommunityRange = (community, range) => {
if (community.length !== range.length) {
return false;
}
for (let i in community) {
let c = community[i];
let rs = range[i][0];
let re = range[i][1];
if ((c < rs) || (c > re)) {
return false;
}
}
return true;
}