Merge branch 'feature/human-readable-bgp-communities' into develop

This commit is contained in:
Matthias Hannig 2018-09-07 11:40:15 +02:00
commit 3443e05e6d
12 changed files with 384 additions and 17 deletions

View File

@ -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,

View File

@ -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
View 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
}

View 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")
}
}

View File

@ -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
}

View File

@ -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)
}
}

View 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;
}

View File

@ -11,4 +11,5 @@
@import 'components/error';
@import 'components/lookup';
@import 'components/pagination';
@import 'components/communities';

View 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);

View File

@ -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
});
}

View File

@ -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>

View File

@ -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>