Merge branch 'feature/human-readable-bgp-communities' into develop
This commit is contained in:
commit
3443e05e6d
@ -138,6 +138,7 @@ func apiConfigShow(_req *http.Request, _params httprouter.Params) (api.Response,
|
||||
NoexportId: AliceConfig.Ui.RoutesNoexports.NoexportId,
|
||||
LoadOnDemand: AliceConfig.Ui.RoutesNoexports.LoadOnDemand,
|
||||
},
|
||||
BgpCommunities: AliceConfig.Ui.BgpCommunities,
|
||||
NoexportReasons: SerializeReasons(
|
||||
AliceConfig.Ui.RoutesNoexports.Reasons),
|
||||
RoutesColumns: AliceConfig.Ui.RoutesColumns,
|
||||
|
@ -31,6 +31,8 @@ type ConfigResponse struct {
|
||||
Noexport Noexport `json:"noexport"`
|
||||
NoexportReasons map[string]string `json:"noexport_reasons"`
|
||||
|
||||
BgpCommunities map[string]interface{} `json:"bgp_communities"`
|
||||
|
||||
NeighboursColumns map[string]string `json:"neighbours_columns"`
|
||||
NeighboursColumnsOrder []string `json:"neighbours_columns_order"`
|
||||
|
||||
|
124
backend/bgp_communities.go
Normal file
124
backend/bgp_communities.go
Normal file
@ -0,0 +1,124 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
/*
|
||||
Implement BGP Communities Lookup Base
|
||||
|
||||
We initialize the dictionary with well known communities and
|
||||
store the representation as a string with : as delimiter.
|
||||
|
||||
From: https://www.iana.org/assignments/bgp-well-known-communities/bgp-well-known-communities.xhtml
|
||||
|
||||
0x00000000-0x0000FFFF Reserved [RFC1997]
|
||||
0x00010000-0xFFFEFFFF Reserved for Private Use [RFC1997]
|
||||
|
||||
0xFFFF0000 GRACEFUL_SHUTDOWN [RFC8326]
|
||||
0xFFFF0001 ACCEPT_OWN [RFC7611]
|
||||
0xFFFF0002 ROUTE_FILTER_TRANSLATED_v4 [draft-l3vpn-legacy-rtc]
|
||||
0xFFFF0003 ROUTE_FILTER_v4 [draft-l3vpn-legacy-rtc]
|
||||
0xFFFF0004 ROUTE_FILTER_TRANSLATED_v6 [draft-l3vpn-legacy-rtc]
|
||||
0xFFFF0005 ROUTE_FILTER_v6 [draft-l3vpn-legacy-rtc]
|
||||
0xFFFF0006 LLGR_STALE [draft-uttaro-idr-bgp-persistence]
|
||||
0xFFFF0007 NO_LLGR [draft-uttaro-idr-bgp-persistence]
|
||||
0xFFFF0008 accept-own-nexthop [draft-agrewal-idr-accept-own-nexthop]
|
||||
|
||||
0xFFFF0009-0xFFFF0299 Unassigned
|
||||
|
||||
0xFFFF029A BLACKHOLE [RFC7999]
|
||||
|
||||
0xFFFF029B-0xFFFFFF00 Unassigned
|
||||
|
||||
0xFFFFFF01 NO_EXPORT [RFC1997]
|
||||
0xFFFFFF02 NO_ADVERTISE [RFC1997]
|
||||
0xFFFFFF03 NO_EXPORT_SUBCONFED [RFC1997]
|
||||
0xFFFFFF04 NOPEER [RFC3765]
|
||||
0xFFFFFF05-0xFFFFFFFF Unassigned
|
||||
*/
|
||||
|
||||
type BgpCommunities map[string]interface{}
|
||||
|
||||
func MakeWellKnownBgpCommunities() BgpCommunities {
|
||||
c := BgpCommunities{
|
||||
"65535": BgpCommunities{
|
||||
"0": "graceful shutdown",
|
||||
"1": "accept own",
|
||||
"2": "route filter translated v4",
|
||||
"3": "route filter v4",
|
||||
"4": "route filter translated v6",
|
||||
"5": "route filter v6",
|
||||
"6": "llgr stale",
|
||||
"7": "no llgr",
|
||||
"8": "accept-own-nexthop",
|
||||
|
||||
"666": "blackhole",
|
||||
|
||||
"1048321": "no export",
|
||||
"1048322": "no advertise",
|
||||
"1048323": "no export subconfed",
|
||||
"1048324": "nopeer",
|
||||
},
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (self BgpCommunities) Lookup(community string) (string, error) {
|
||||
path := strings.Split(community, ":")
|
||||
var lookup interface{} // This is all much too dynamic...
|
||||
lookup = self
|
||||
|
||||
for _, key := range path {
|
||||
clookup, ok := lookup.(BgpCommunities)
|
||||
if !ok {
|
||||
// This happens if path.len > depth
|
||||
return "", fmt.Errorf("community not found")
|
||||
}
|
||||
|
||||
res, ok := clookup[key]
|
||||
if !ok {
|
||||
// Try to fall back to wildcard key
|
||||
res, ok = clookup["*"]
|
||||
if !ok {
|
||||
break // we did everything we could.
|
||||
}
|
||||
}
|
||||
|
||||
lookup = res
|
||||
}
|
||||
|
||||
label, ok := lookup.(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("community not found")
|
||||
}
|
||||
|
||||
return label, nil
|
||||
}
|
||||
|
||||
func (self BgpCommunities) Set(community string, label string) {
|
||||
path := strings.Split(community, ":")
|
||||
var lookup interface{} // Again, this is all much too dynamic...
|
||||
lookup = self
|
||||
|
||||
for _, key := range path[:len(path)-1] {
|
||||
clookup, ok := lookup.(BgpCommunities)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
|
||||
res, ok := clookup[key]
|
||||
if !ok {
|
||||
// The key does not exist, create it!
|
||||
clookup[key] = BgpCommunities{}
|
||||
res = clookup[key]
|
||||
}
|
||||
|
||||
lookup = res
|
||||
}
|
||||
|
||||
slookup := lookup.(BgpCommunities)
|
||||
slookup[path[len(path)-1]] = label
|
||||
}
|
84
backend/bgp_communities_test.go
Normal file
84
backend/bgp_communities_test.go
Normal file
@ -0,0 +1,84 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCommunityLookup(t *testing.T) {
|
||||
|
||||
c := MakeWellKnownBgpCommunities()
|
||||
|
||||
label, err := c.Lookup("65535:666")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if label != "blackhole" {
|
||||
t.Error("Label should have been: blackhole, got:", label)
|
||||
}
|
||||
|
||||
// Okay now try some fails
|
||||
label, err = c.Lookup("65535")
|
||||
if err == nil {
|
||||
t.Error("Expected error!")
|
||||
}
|
||||
|
||||
label, err = c.Lookup("65535:23:42")
|
||||
if err == nil {
|
||||
t.Error("Expected not found error!")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetCommunity(t *testing.T) {
|
||||
c := MakeWellKnownBgpCommunities()
|
||||
|
||||
c.Set("2342:10", "foo")
|
||||
c.Set("2342:42:23", "bar")
|
||||
|
||||
// Simple lookup
|
||||
label, err := c.Lookup("2342:10")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if label != "foo" {
|
||||
t.Error("Expected foo for 2342:10, got:", label)
|
||||
}
|
||||
|
||||
label, err = c.Lookup("2342:42:23")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if label != "bar" {
|
||||
t.Error("Expected bar for 2342:42:23, got:", label)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWildcardLookup(t *testing.T) {
|
||||
c := MakeWellKnownBgpCommunities()
|
||||
|
||||
c.Set("2342:*", "foobar $0")
|
||||
c.Set("42:*:1", "baz")
|
||||
|
||||
// This should work
|
||||
label, err := c.Lookup("2342:23")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if label != "foobar $0" {
|
||||
t.Error("Did not get expected label.")
|
||||
}
|
||||
|
||||
// This however not
|
||||
label, err = c.Lookup("2342:23:666")
|
||||
if err == nil {
|
||||
t.Error("Lookup should have failed, got label:", label)
|
||||
}
|
||||
|
||||
// This should again work
|
||||
label, err = c.Lookup("42:123:1")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if label != "baz" {
|
||||
t.Error("Unexpected label for key")
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -51,6 +52,7 @@ type UiConfig struct {
|
||||
|
||||
RoutesRejections RejectionsConfig
|
||||
RoutesNoexports NoexportsConfig
|
||||
BgpCommunities BgpCommunities
|
||||
|
||||
Theme ThemeConfig
|
||||
|
||||
@ -297,6 +299,32 @@ func getRoutesNoexports(config *ini.File) (NoexportsConfig, error) {
|
||||
return noexportsConfig, nil
|
||||
}
|
||||
|
||||
// Get UI config: Bgp Communities
|
||||
func getBgpCommunities(config *ini.File) BgpCommunities {
|
||||
// Load defaults
|
||||
communities := MakeWellKnownBgpCommunities()
|
||||
communitiesConfig := config.Section("bgp_communities")
|
||||
if communitiesConfig == nil {
|
||||
return communities // nothing else to do here, go with the default
|
||||
}
|
||||
|
||||
// Parse and merge communities
|
||||
lines := strings.Split(communitiesConfig.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: Theme settings
|
||||
func getThemeConfig(config *ini.File) ThemeConfig {
|
||||
baseConfig := config.Section("theme")
|
||||
@ -376,6 +404,7 @@ func getUiConfig(config *ini.File) (UiConfig, error) {
|
||||
|
||||
RoutesRejections: rejections,
|
||||
RoutesNoexports: noexports,
|
||||
BgpCommunities: getBgpCommunities(config),
|
||||
|
||||
Theme: themeConfig,
|
||||
|
||||
@ -463,7 +492,11 @@ func loadConfig(file string) (*Config, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parsedConfig, err := ini.LooseLoad(file)
|
||||
// Load configuration, but handle bgp communities section
|
||||
// with our own parser
|
||||
parsedConfig, err := ini.LoadSources(ini.LoadOptions{
|
||||
UnparseableSections: []string{"bgp_communities"},
|
||||
}, file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -25,6 +25,16 @@ func TestLoadConfigs(t *testing.T) {
|
||||
if len(config.Ui.RoutesRejections.Reasons) == 0 {
|
||||
t.Error("Rejection reasons missing")
|
||||
}
|
||||
|
||||
// Check communities
|
||||
label, err := config.Ui.BgpCommunities.Lookup("1:23")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if label != "some tag" {
|
||||
t.Error("expcted to find example community 1:23 with 'some tag'",
|
||||
"but got:", label)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSourceConfigDefaultsOverride(t *testing.T) {
|
||||
@ -58,5 +68,4 @@ func TestSourceConfigDefaultsOverride(t *testing.T) {
|
||||
if rs2.Birdwatcher.Timezone != "Europe/Brussels" {
|
||||
t.Error("Expected 'Europe/Brussels', got", rs2.Birdwatcher.Timezone)
|
||||
}
|
||||
|
||||
}
|
||||
|
15
client/assets/scss/components/communities.scss
Normal file
15
client/assets/scss/components/communities.scss
Normal file
@ -0,0 +1,15 @@
|
||||
|
||||
// BGP Communities Labels
|
||||
|
||||
|
||||
.label-bgp-community {
|
||||
margin-right: 5px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.label-bgp-unknown {
|
||||
color: #333;
|
||||
background-color: #ddd;
|
||||
}
|
||||
|
||||
|
@ -11,4 +11,5 @@
|
||||
@import 'components/error';
|
||||
@import 'components/lookup';
|
||||
@import 'components/pagination';
|
||||
@import 'components/communities';
|
||||
|
||||
|
89
client/components/bgp-communities/label.jsx
Normal file
89
client/components/bgp-communities/label.jsx
Normal file
@ -0,0 +1,89 @@
|
||||
|
||||
import React from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
|
||||
|
||||
function _lookupCommunity(communities, community) {
|
||||
let lookup = communities;
|
||||
for (let c of community) {
|
||||
if (typeof(lookup) !== "object") {
|
||||
return null;
|
||||
}
|
||||
let res = lookup[c];
|
||||
if (!res) {
|
||||
// Try the wildcard
|
||||
if (lookup["*"]) {
|
||||
res = lookup["*"]
|
||||
} else {
|
||||
return null; // We did everything we could
|
||||
}
|
||||
}
|
||||
lookup = res;
|
||||
}
|
||||
return lookup;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Expand variables in string:
|
||||
* "Test AS$0 rejects $2"
|
||||
* will expand with [23, 42, 123] to
|
||||
* "Test AS23 rejects 123"
|
||||
*/
|
||||
function _expandVars(str, vars) {
|
||||
if (!str) {
|
||||
return str; // We don't have to do anything.
|
||||
}
|
||||
|
||||
var res = str;
|
||||
vars.map((v, i) => {
|
||||
res = res.replace(`$${i}`, v);
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/*
|
||||
* Make style tags
|
||||
* Derive classes from community parts.
|
||||
*/
|
||||
function _makeStyleTags(community) {
|
||||
return community.map((part, i) => {
|
||||
return `label-bgp-community-${i}-${part}`;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
class Label extends React.Component {
|
||||
render() {
|
||||
|
||||
// Lookup communities
|
||||
const readableCommunityLabel = _lookupCommunity(this.props.communities, this.props.community);
|
||||
const readableCommunity = _expandVars(readableCommunityLabel, this.props.community);
|
||||
const key = this.props.community.join(":");
|
||||
|
||||
let cls = 'label label-bgp-community ';
|
||||
if (!readableCommunity) {
|
||||
cls += "label-bgp-unknown";
|
||||
// Default label
|
||||
return (
|
||||
<span className={cls}>{key}</span>
|
||||
);
|
||||
}
|
||||
|
||||
// Apply style
|
||||
cls += "label-info ";
|
||||
|
||||
const styleTags = _makeStyleTags(this.props.community);
|
||||
cls += styleTags.join(" ");
|
||||
|
||||
return (<span className={cls}>{readableCommunity} ({key})</span>);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
(state) => ({
|
||||
communities: state.config.bgp_communities,
|
||||
})
|
||||
)(Label);
|
||||
|
@ -10,7 +10,8 @@ const initialState = {
|
||||
prefix_lookup_enabled: false,
|
||||
content: {},
|
||||
noexport_load_on_demand: true, // we have to assume this
|
||||
// otherwise fetch will start.
|
||||
// otherwise fetch will start.
|
||||
bgp_communities: {},
|
||||
};
|
||||
|
||||
|
||||
@ -29,6 +30,7 @@ export default function reducer(state = initialState, action) {
|
||||
|
||||
prefix_lookup_enabled: action.payload.prefix_lookup_enabled,
|
||||
|
||||
bgp_communities: action.payload.bgp_communities,
|
||||
noexport_load_on_demand: action.payload.noexport.load_on_demand
|
||||
});
|
||||
}
|
||||
|
@ -9,6 +9,8 @@ import {connect} from 'react-redux'
|
||||
|
||||
import Modal, {Header, Body, Footer} from 'components/modals/modal'
|
||||
|
||||
import BgpCommunitiyLabel from 'components/bgp-communities/label'
|
||||
|
||||
import {hideBgpAttributesModal}
|
||||
from './bgp-attributes-modal-actions'
|
||||
|
||||
@ -27,15 +29,8 @@ class BgpAttributesModal extends React.Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
let communities = [];
|
||||
if (attrs.bgp.communities) {
|
||||
communities = attrs.bgp.communities.map((c) => c.join(':'));
|
||||
}
|
||||
|
||||
let large_communities = [];
|
||||
if (attrs.bgp.large_communities) {
|
||||
large_communities = attrs.bgp.large_communities.map((c) => c.join(':'));
|
||||
}
|
||||
const communities = attrs.bgp.communities;
|
||||
const large_communities = attrs.bgp.large_communities;
|
||||
|
||||
return (
|
||||
<Modal className="bgp-attributes-modal"
|
||||
@ -67,14 +62,19 @@ class BgpAttributesModal extends React.Component {
|
||||
<tr>
|
||||
<th>AS Path:</th><td>{attrs.bgp.as_path.join(' ')}</td>
|
||||
</tr>}
|
||||
<tr>
|
||||
<th>Communities:</th>
|
||||
<td>{communities.join(' ')}</td>
|
||||
</tr>
|
||||
{communities.length > 0 &&
|
||||
<tr>
|
||||
<th>Communities:</th>
|
||||
<td>
|
||||
{communities.map((c) => <BgpCommunitiyLabel community={c} key={c} />)}
|
||||
</td>
|
||||
</tr>}
|
||||
{large_communities.length > 0 &&
|
||||
<tr>
|
||||
<th>Large Communities:</th>
|
||||
<td>{large_communities.join(' ')}</td>
|
||||
<td>
|
||||
{large_communities.map((c) => <BgpCommunitiyLabel community={c} key={c} />)}
|
||||
</td>
|
||||
</tr>}
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -45,6 +45,13 @@ load_on_demand = true # Default: false
|
||||
6 = The Sender has set (peerRTTHigherDeny:ms) and the targets RTT ms >= then the ms in the community
|
||||
7 = The Sender has set (peerRTTLowerDeny:ms) and the targets RTT ms <= then the ms in the community
|
||||
|
||||
# Define Known Bgp Communities
|
||||
[bgp_communities]
|
||||
1:23 = some tag
|
||||
9033:65666:1 = ip bogon detected
|
||||
# Wildcards are supported aswell:
|
||||
0:* = do not redistribute to AS$0
|
||||
|
||||
#
|
||||
# Define columns for neighbours and routes table,
|
||||
# with <key> = <Table Header>
|
||||
|
Loading…
x
Reference in New Issue
Block a user