Merge branch 'alice-lg:master' into github-action-docker

This commit is contained in:
bluikko 2021-10-10 13:28:41 +07:00 committed by GitHub
commit e36a1bc5e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
61 changed files with 2654 additions and 367 deletions

View File

@ -1,6 +1,16 @@
# Changelog
## 5.0.0 (2021-10-09)
* OpenBGPD support! Thanks to the Route Server Support Foundation
for sponsoring this feature!
* Backend cleanup and restructured go codebase.
This should improve a bit working with containers.
* Fixed links to the IRR Explorer.
## 4.3.0 (2021-04-15)
* Added configurable main table

40
LICENSE
View File

@ -1,33 +1,31 @@
BSD License
BSD 3-Clause License
Copyright (c) 2016-2018, Peering GmbH / ECIX
Copyright (c) 2018-present, Matthias Hannig
Copyright (c) 2018-present, Annika Hannig
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
THE POSSIBILITY OF SUCH DAMAGE.
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -35,14 +35,11 @@ client_dev:
client_prod:
$(MAKE) -C client/ client_prod
backend_dev: client_dev
$(MAKE) -C cmd/alice-lg/
dev:
$(MAKE) -C cmd/alice-lg/ osx-dev
backend_prod: client_prod
$(MAKE) -C cmd/alice-lg/
$(MAKE) -C cmd/alice-lg/ linux
backend:
$(MAKE) -C cmd/alice-lg/ linux
alice: backend_prod
cp cmd/alice-lg/alice-lg-* bin/

View File

@ -2,9 +2,13 @@
__"No, no! The adventures first, explanations take such a dreadful time."__
_Lewis Carroll, Alice's Adventures in Wonderland & Through the Looking-Glass_
Take a look at an Alice-LG production examples at:
Take a look at Alice-LG production examples at:
- https://lg.de-cix.net/
- https://lg.ecix.net/
- https://lg.ams-ix.net
- https://lg.bcix.de/
- https://lg.megaport.com/
- https://lg.netnod.se/
- https://alice-rs.linx.net/
And checkout the API at:
- https://lg.de-cix.net/api/v1/config
@ -33,12 +37,17 @@ Major thanks to Barry O'Donovan who built the original [INEX Bird's Eye](https:/
### GoBGP
Alice-LG supports direct integration with GoBGP instances using gRPC. See the configuration section for more detail.
### OpenBGPD
Alice-LG supports OpenBGP via [`bgplgd`](https://github.com/cjeker/bgplgd)
and [`openbgpd-state-server`](https://github.com/alice-lg/openbgpd-state-server).
## Building Alice-LG from scratch
__These examples include setting up your Go environment, if you already have set that up then you can obviously skip that__
In case you have trouble with `npm` and `gulp` you can try using `yarn`.
### CentOS 7:
### CentOS:
First add the following lines at the end of your `~/.bash_profile`:
```bash
GOPATH=$HOME/go
@ -70,7 +79,7 @@ Your Alice-LG source will now be located at `~/go/src/github.com/alice-lg/alice-
## Configuration
An example configuration can be found at
[etc/alice-lg/alice.example.conf](https://github.com/alice-lg/alice-lg/blob/readme_update/etc/alice-lg/alice.example.conf).
[etc/alice-lg/alice.example.conf](https://github.com/alice-lg/alice-lg/blob/master/etc/alice-lg/alice.example.conf).
You can copy it to any of the following locations:
@ -115,6 +124,32 @@ host = rs2.example.com:50051
processing_timeout = 300
```
[OpenBGPD](https://www.openbgpd.org/) via `openbgpd-state-server`:
```ini
[source.rs-example]
name = rs-example.openbgpd-state-server
[source.rs-example.openbgpd-state-server]
api = http://rs23.example.net:29111/api
# Optional response cache time in seconds
# Default: disabled (0)
cache_ttl = 100
```
[OpenBGPD](https://www.openbgpd.org/) via `bgplgd`:
```ini
[source.rs-example]
name = rs-example.openbgpd-bgplgd
[source.rs-example.openbgpd-bgplgd]
api = http://rs23.example.net/bgplgd
# Optional response cache time in seconds
# Default: disabled (0)
cache_ttl = 100
```
## Running
Launch the server by running
@ -226,3 +261,6 @@ The development of Alice is now sponsored by
</p>
Many thanks go out to [ECIX](https://www.ecix.net), where this project originated and was backed over the last two years.
Support for **OpenBGPD** was sponsored by the [Route Server Support Foundation](https://www.rssf.nl/).

View File

@ -1 +1 @@
4.3.2
5.0.0

View File

@ -21,7 +21,7 @@ class FilterReason extends React.Component {
const cls = `reject-reason reject-reason-${community[1]}-${community[2]}`;
return (
<p key={key} className={cls}>
<a href={`http://irrexplorer.nlnog.net/search/${route.network}`}
<a href={`https://irrexplorer.nlnog.net/prefix/${route.network}`}
target="_blank" >{reason}</a>
</p>
);

View File

@ -21,7 +21,7 @@ class NoExportReason extends React.Component {
const cls = `noexport-reason noexport-reason-${community[1]}-${community[2]}`;
return (
<p key={key} className={cls}>
<a href={`http://irrexplorer.nlnog.net/search/${route.network}`}
<a href={`https://irrexplorer.nlnog.net/prefix/${route.network}`}
target="_blank" >{reason}</a>
</p>
);

View File

@ -108,11 +108,17 @@ function _sortNeighbors(neighbors, sort, order) {
return neighbors.sort(_sortOrder(cmp, order));
}
function isUpState(s) {
if (!s) { return false; }
s = s.toLowerCase();
return (s.includes("up") || s.includes("established"));
}
class RoutesLink extends React.Component {
render() {
let url = `/routeservers/${this.props.routeserverId}/protocols/${this.props.protocol}/routes`;
if (this.props.state.toLowerCase() != 'up') {
if (!isUpState(this.props.state)) {
return (<span>{this.props.children}</span>);
}
return (
@ -189,7 +195,7 @@ const ColDescription = function(props) {
protocol={neighbour.id}
state={neighbour.state}>
{neighbour.description}
{neighbour.state.toLowerCase() != "up" &&
{!isUpState(neighbour.state) &&
neighbour.last_error &&
<span className="protocol-state-error">
{neighbour.last_error}
@ -230,6 +236,12 @@ const ColPlain = function(props) {
);
}
const ColNotAvailable = function(props) {
return (
<td>-</td>
);
}
// Column:
const NeighbourColumn = function(props) {
const neighbour = props.neighbour;
@ -244,6 +256,11 @@ const NeighbourColumn = function(props) {
"Description": ColDescription,
};
// For openbgpd the value is ommitted
if (props.rsType == "openbgpd") {
widgets["routes_not_exported"] = ColNotAvailable;
}
// Get render function
let Widget = widgets[column] || ColLinked;
return (
@ -256,8 +273,11 @@ const NeighbourColumn = function(props) {
class NeighboursTableView extends React.Component {
render() {
const rs = this.props.routeserver;
if(!rs) { return null; } // We wait until we have a routeserver
const columns = this.props.neighboursColumns;
const columnsOrder = this.props.neighboursColumnsOrder;
let columnsOrder = this.props.neighboursColumnsOrder;
const sortedNeighbors = _sortNeighbors(this.props.neighbours,
this.props.sortColumn,
@ -267,6 +287,7 @@ class NeighboursTableView extends React.Component {
return (
<NeighborColumnHeader key={col}
rsId={this.props.routeserverId}
rsType={rs.type}
columns={columns} column={col}
sort={this.props.sortColumn}
order={this.props.sortOrder}
@ -278,6 +299,7 @@ class NeighboursTableView extends React.Component {
let neighbourColumns = columnsOrder.map((col) => {
return <NeighbourColumn key={col}
rsId={this.props.routeserverId}
rsType={rs.type}
column={col}
neighbour={n} />
});
@ -328,14 +350,21 @@ class NeighboursTableView extends React.Component {
}
const NeighboursTable = connect(
(state) => ({
neighboursColumns: state.config.neighbours_columns,
neighboursColumnsOrder: state.config.neighbours_columns_order,
(state, ownProps) => {
const rsId = ownProps.routeserverId;
const rs = state.routeservers.byId[rsId];
return {
routeserver: rs,
sortColumn: state.neighbors.sortColumn,
sortOrder: state.neighbors.sortOrder,
filterQuery: state.neighbors.filterQuery,
})
neighboursColumns: state.config.neighbours_columns,
neighboursColumnsOrder: state.config.neighbours_columns_order,
sortColumn: state.neighbors.sortColumn,
sortOrder: state.neighbors.sortOrder,
filterQuery: state.neighbors.filterQuery,
};
}
)(NeighboursTableView);
@ -386,25 +415,20 @@ class Protocols extends React.Component {
let neighboursDown = [];
let neighboursIdle = [];
for (let id in protocol) {
let n = protocol[id];
switch(n.state.toLowerCase()) {
case 'up':
neighboursUp.push(n);
break;
case 'down':
neighboursDown.push(n);
break;
case 'start':
neighboursIdle.push(n);
break;
default:
neighboursUp.push(n);
console.error("Couldn't classify neighbour by state:", n);
for (let n of protocol) {
let s = n.state.toLowerCase();
if (s.includes("up") || s.includes("established") ) {
neighboursUp.push(n);
} else if (s.includes("down")) {
neighboursDown.push(n);
} else if (s.includes("start") || s.includes("active")) {
neighboursIdle.push(n);
} else {
console.error("Couldn't classify neighbour by state:", n);
neighboursUp.push(n);
}
}
// Render tables
let tables = [];
if (neighboursUp.length) {
@ -437,7 +461,7 @@ export default connect(
protocols: state.routeservers.protocols,
filterQuery: state.neighbors.filterQuery,
routing: state.routing.locationBeforeTransitions,
routing: state.routing.locationBeforeTransitions,
}
}
)(Protocols);

View File

@ -68,6 +68,9 @@ const makeQueryLinkProps = function(routing, query, loadNotExported) {
* loading) and show info screen.
*/
const RoutesViewEmpty = (props) => {
const hasContent = props.routes.received.totalResults > 0 ||
props.routes.filtered.totalResults > 0 ||
props.routes.notExported.totalResults > 0;
const isLoading = props.routes.received.loading ||
props.routes.filtered.loading ||
props.routes.notExported.loading;
@ -75,24 +78,24 @@ const RoutesViewEmpty = (props) => {
if (isLoading) {
return null; // We are not a loading indicator.
}
if (!props.loadNotExported) {
return null; // There may be routes matching the query in there!
// Maybe this has something to do with a filter
if (!hasContent && props.hasQuery) {
return (
<div className="card info-result-empty">
<h4>No routes matching your query.</h4>
<p>Please check if your query is too restrictive.</p>
</div>
);
}
const hasContent = props.routes.received.totalResults > 0 ||
props.routes.filtered.totalResults > 0 ||
props.routes.notExported.totalResults > 0;
if (hasContent) {
return null; // Nothing to do then.
}
// Show info screen
return (
<div className="card info-result-empty">
<h4>No routes found matching your query.</h4>
<p>Please check if your query is too restrictive.</p>
<p className="card-body">There are <b>no routes</b> to display for this neighbor.</p>
</div>
);
}
@ -156,8 +159,7 @@ class RoutesPage extends React.Component {
const filterPlaceholder = "Filter by " +
filterableColumnsText(
this.props.routesColumns,
this.props.routesColumnsOrder
);
this.props.routesColumnsOrder);
return(
<div className={pageClass}>
@ -189,9 +191,8 @@ class RoutesPage extends React.Component {
<QuickLinks routes={this.props.routes} />
<RoutesViewEmpty routes={this.props.routes}
hasQuery={!!this.props.filterValue}
loadNotExported={this.props.loadNotExported} />
<RoutesView
type={ROUTES_FILTERED}
routeserverId={this.props.params.routeserverId}
@ -239,6 +240,7 @@ class RoutesPage extends React.Component {
export default connect(
(state, props) => {
const query = props.params.query;
const protocolId = props.params.protocolId;
const rsId = props.params.routeserverId;
const neighbors = state.routeservers.protocols[rsId];
@ -266,6 +268,11 @@ export default connect(
totalResults: state.routes.notExportedTotalResults,
apiStatus: state.routes.notExportedApiStatus
};
const totalResults = state.routes.receivedTotalResults +
state.routes.filteredTotalResults +
state.routes.notExportedTotalResults;
const anyLoading = state.routes.receivedLoading ||
state.routes.filteredLoading ||
state.routes.notExportedLoading;
@ -286,9 +293,9 @@ export default connect(
neighbor: neighbor,
filterValue: state.routes.filterValue,
routes: {
[ROUTES_RECEIVED]: received,
[ROUTES_FILTERED]: filtered,
[ROUTES_NOT_EXPORTED]: notExported
[ROUTES_RECEIVED]: received,
[ROUTES_FILTERED]: filtered,
[ROUTES_NOT_EXPORTED]: notExported
},
routesColumns: state.config.routes_columns,
routesColumnsOrder: state.config.routes_columns_order,
@ -297,8 +304,10 @@ export default connect(
loadNotExported: state.routes.loadNotExported ||
!state.config.noexport_load_on_demand,
totalResults: totalResults,
anyLoading: anyLoading,
filterQuery: state.routes.filterQuery,
filtersApplied: filtersApplied,
filtersAvailable: filtersAvailable,

View File

@ -31,7 +31,6 @@ const QuickLinks = function(props) {
// Is there anything to show?
if (!isLoading &&
!showNotExported &&
props.routes.notExported.totalResults == 0 &&
props.routes.received.totalResults == 0 &&
props.routes.filtered.totalResults == 0) {
@ -50,7 +49,8 @@ const QuickLinks = function(props) {
props.routes.received.totalResults > 0) &&
<li className="received">
<a href="#routes-received">Accepted</a></li>}
{showNotExported &&
{(!props.routes.notExported.loading &&
props.routes.notExported.totalResults > 0) &&
<li className="not-exported">
<a href="#routes-not-exported">Not Exported</a></li>}
</ul>

View File

@ -48,7 +48,7 @@ function PeerLink(props) {
const rid = neighbor.routeserver_id;
let peerUrl;
if (neighbor.state == "up") {
if (neighbor.state == "up" || neighbor.state.includes("established")) {
peerUrl = `/routeservers/${rid}/protocols/${pid}/routes`;
} else {
peerUrl = `/routeservers/${rid}#sessions-down`;

View File

@ -54,7 +54,7 @@ export const ColNetwork = function(props) {
// Special AS Path Widget
export const ColAsPath = function(props) {
const asns = _lookup(props.route, "bgp.as_path");
const baseUrl = "http://irrexplorer.nlnog.net/search/"
const baseUrl = "https://irrexplorer.nlnog.net/asn/AS"
let asnLinks = asns.map((asn, i) => {
return (<a key={`${asn}_${i}`} href={baseUrl + asn} target="_blank">{asn} </a>);
@ -91,6 +91,7 @@ export default function(props) {
"flags": ColFlags,
"bgp.as_path": ColAsPath,
"Flags": ColFlags,
"ASPath": ColAsPath,
};

View File

@ -158,6 +158,9 @@ export const RpkiIndicator = connect(
class _RejectCandidateIndicator extends React.Component {
render() {
if (!this.props.candidateCommunities) {
return null;
}
if (!isRejectCandidate(this.props.candidateCommunities, this.props.route)) {
return null;
}

View File

@ -183,6 +183,7 @@ class RoutesView extends React.Component {
}
renderLoadTrigger() {
const rs = this.props.routeserver;
const type = this.props.type;
const state = this.props.routes[type];
const name = {
@ -191,6 +192,10 @@ class RoutesView extends React.Component {
[ROUTES_NOT_EXPORTED]: "routes-not-exported",
}[type];
// We do not support this with openbgpd based route servers.
if (rs && rs.type == "openbgpd") {
return null;
}
// This is an artificial delay, to make the user wait until
// filtered and recieved routes are fetched
@ -234,7 +239,8 @@ class RoutesView extends React.Component {
}
export default connect(
(state) => {
(state, ownProps) => {
const rs = state.routeservers.byId[ownProps.routeserverId];
const received = {
routes: state.routes.received,
requested: state.routes.receivedRequested,
@ -278,6 +284,7 @@ export default connect(
state.routes.notExportedFiltersApplied
);
return({
routeserver: rs,
filterQuery: state.routes.filterQuery,
routes: {
[ROUTES_RECEIVED]: received,

View File

@ -46,6 +46,15 @@ class Details extends React.Component {
];
};
let lastReconfig = rsStatus.last_reconfig;
// We have some capabilities: openbgpd does not support
// last reboot or last reconfig
if (rs.type == "openbgpd") {
lastReboot = null;
lastReconfig = null;
}
return (
<table className="routeserver-status">
<tbody>
@ -54,10 +63,11 @@ class Details extends React.Component {
<td><i className="fa fa-clock-o"></i></td>
<td>Last Reboot: <b><Datetime value={lastReboot} /></b></td>
</tr>}
{lastReconfig &&
<tr>
<td><i className="fa fa-clock-o"></i></td>
<td>Last Reconfig: <b><Datetime value={rsStatus.last_reconfig} /></b></td>
</tr>
<td>Last Reconfig: <b><Datetime value={lastReconfig} /></b></td>
</tr>}
<tr>
<td><i className="fa fa-thumbs-up"></i></td>

View File

@ -120,8 +120,10 @@ Description = Description
routes_received = Routes Received
routes_filtered = Filtered
#
[routes_columns]
flags =
network = Network
gateway = Gateway
interface = Interface
@ -130,6 +132,7 @@ bgp.as_path = AS Path
[lookup_columns]
flags =
network = Network
gateway = Gateway
neighbour.asn = ASN
@ -193,3 +196,16 @@ servertime_ext = Mon, 02 Jan 2006 15:04:05 -0700
# Default: 300
# processing_timeout = 300
# [source.rs0-example]
# name = rs-example.openbgpd-state-server
# [source.rs0-example.openbgpd-state-server]
# api = http://165.22.27.105:29111/api
# Cache results from openbgpd for n seconds, 0 disables the cache.
# cache_ttl = 30
# routes_cache_size = 1024 # Neighbors
# [source.rs0-example-bgplgd]
# name = rs-example.bgplgd
# [source.rs0-example-bgplgd.openbgpd-bgplgd]
# api = http://165.22.27.105:29111/api

View File

@ -5,13 +5,13 @@ import (
"time"
)
// General api response
// Response is a general API response
type Response interface{}
// Details, usually the original backend response
// Details are usually the original backend response
type Details map[string]interface{}
// Error Handling
// ErrorResponse encodes an error message and code
type ErrorResponse struct {
Message string `json:"message"`
Code int `json:"code"`
@ -19,12 +19,12 @@ type ErrorResponse struct {
RouteserverId string `json:"routeserver_id"`
}
// Cache aware api response
// CacheableResponse is a cache aware API response
type CacheableResponse interface {
CacheTTL() time.Duration
}
// Config
// ConfigResponse is a response with client runtime configuration
type ConfigResponse struct {
Asn int `json:"asn"`
@ -51,6 +51,7 @@ type ConfigResponse struct {
PrefixLookupEnabled bool `json:"prefix_lookup_enabled"`
}
// Noexport options
type Noexport struct {
LoadOnDemand bool `json:"load_on_demand"`
}
@ -95,9 +96,10 @@ type StatusResponse struct {
Status Status `json:"status"`
}
// Routeservers
// A Routeserver is a datasource with attributes.
type Routeserver struct {
Id string `json:"id"`
Type string `json:"type"`
Name string `json:"name"`
Group string `json:"group"`
Blackholes []string `json:"blackholes"`

View File

@ -1,6 +1,7 @@
package api
import (
"encoding/json"
"strings"
"time"
)
@ -29,6 +30,13 @@ type Neighbour struct {
Details map[string]interface{} `json:"details"`
}
// String encodes a neighbor as json. This is
// more readable than the golang default represenation.
func (n *Neighbour) String() string {
repr, _ := json.Marshal(n)
return string(repr)
}
// Implement sorting interface for routes
func (neighbours Neighbours) Len() int {
return len(neighbours)
@ -85,6 +93,8 @@ type NeighboursLookupResults map[string]Neighbours
type NeighboursStatus []*NeighbourStatus
// NeighbourStatus contains only the neighbor state and
// uptime.
type NeighbourStatus struct {
Id string `json:"id"`
State string `json:"state"`

View File

@ -1,6 +1,7 @@
package api
import (
"encoding/json"
"time"
)
@ -21,6 +22,11 @@ type Route struct {
Details Details `json:"details"`
}
func (r *Route) String() string {
s, _ := json.Marshal(r)
return string(s)
}
// Implement Filterable interface for routes
func (self *Route) MatchSourceId(id string) bool {
return true // A route has no source info so we exclude this filter

View File

@ -0,0 +1,212 @@
# ======================================
# Alice-LG configuration example
# ======================================
[server]
# configures the built-in webserver and provides global application settings
listen_http = 127.0.0.1:7340
# enable the prefix-lookup endpoint / the global search feature
enable_prefix_lookup = true
# Try to refresh the neighbor status on every request to /neighbors
enable_neighbors_status_refresh = false
asn = 9033
# this ASN is used as a fallback value in the RPKI feature and for route
# filtering evaluation with large BGP communities
[housekeeping]
# Interval for the housekeeping routine in minutes
interval = 5
# Try to release memory via a forced GC/SCVG run on every housekeeping run
force_release_memory = true
[theme]
path = /path/to/my/alice/theme/files
# Optional:
url_base = /theme
[pagination]
# Routes tables can be paginated, which comes in handy with
# peers announcing a lot of routes. Set to 0 to disable
# pagination.
routes_filtered_page_size = 250
routes_accepted_page_size = 250
routes_not_exported_page_size = 250
[rejection_reasons]
# a pair of a large BGP community value and a string to signal the processing
# results of route filtering
9033:65666:1 = An IP Bogon was detected
9033:65666:2 = Prefix is longer than 64
9033:65666:3 = Prefix is longer than 24
9033:65666:4 = AS path contains a bogon AS
9033:65666:5 = AS path length is longer than 64
9033:65666:6 = First AS in path is not the same as the Peer AS
9033:65666:7 = ECIX prefix hijack
9033:65666:8 = Origin AS not found in IRRDB for Peer AS-SET
9033:65666:9 = Prefix not found in IRRDB for Origin AS
9033:65666:10 = Advertised nexthop address is not the same as the peer
23:42:1 = Some made up reason
#
# Optional: Define communities which might be filtered
# in the future.
[rejection_candidates]
communities = 6695:1102:14, 6695:1102:15, 23:42:46
[noexport]
load_on_demand = true # Default: false
[noexport_reasons]
# 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
9033: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
9033: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
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
9033: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
23:46:1 = Some other made up reason
[rpki]
# shows rpki validation status in the client, based on the presence of a large
# BGP community on the route
enabled = true
# Optional, falling back to defaults as defined in:
# https://www.euro-ix.net/en/forixps/large-bgp-communities/
valid = 23042:1000:1
unknown = 23042:1000:2
# not_checked = 23042:1000:3
invalid = 23042:1000:4-*
# Define other 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$1
#
# Define columns for neighbours and routes table,
# with <key> = <Table Header>
#
# and <key> := <object.path> Implicitly referencing the object,
# e.g. route.bgp.as_path -> bgp.as_path)
# |= <Widget> A widget with special rendering features,
# to which the object is applied. E.g.
# Uptime, which will be rendered as
# Uptime(neighbour).
#
# As per convention: Widgets are in Uppercase, object properties are
# in lowercase.
#
# Available Widgets for Neighbours:
#
# Uptime Displays the relative uptime of this neighbour
# Description The neighbour's description with link to routes page
#
[neighbours_columns]
address = Neighbour
asn = ASN
state = State
Uptime = Uptime
Description = Description
routes_received = Routes Received
routes_filtered = Filtered
[routes_columns]
network = Network
gateway = Gateway
interface = Interface
metric = Metric
bgp.as_path = AS Path
[lookup_columns]
network = Network
gateway = Gateway
neighbour.asn = ASN
neighbour.description = Description
bgp.as_path = AS Path
routeserver.name = RS
# Routeservers
# Birdwatcher Example
[source.rs0-example-v4]
name = rs1.example.com (IPv4)
# Optional: a group for the routeservers list
group = FRA
blackholes = 10.23.6.666, 10.23.6.665
[source.rs0-example-v4.birdwatcher]
api = http://rs1.example.com:29184/
# single_table / multi_table
type = multi_table
peer_table_prefix = T
pipe_protocol_prefix = M
# Timeout in seconds to wait for the status data (only required if enable_neighbors_status_refresh is true)
neighbors_refresh_timeout = 2
# Optional:
show_last_reboot = true
[source.rs1-example-v6]
name = rs1.example.com (IPv6)
[source.rs1-example-v6.birdwatcher]
timezone = Europe/Brussels
api = http://rs1.example.com:29186/
# single_table / multi_table
type = multi_table
peer_table_prefix = T
pipe_protocol_prefix = M
# Timeout in seconds to wait for the status data (only required if enable_neighbors_status_refresh is true)
neighbors_refresh_timeout = 2
# Optional: Examples for time format
# Please see https://golang.org/pkg/time/#pkg-constants for an
# explanation on how time parsing in go works.
servertime = 2006-01-02T15:04:05Z07:00
servertime_short = 02.01.2006
servertime_ext = Mon, 02 Jan 2006 15:04:05 -0700
# Routeservers
# GoBGP Example
[source.rs2-example]
name = rs2.example.com
group = AMS
[source.rs2-example.gobgp]
# host is the IP (or DNS name) and port for the remote GoBGP daemon
host = rs2.example.com:50051
# processing_timeout is a timeout in seconds configured per gRPC call to a given GoBGP daemon
processing_timeout = 300
type = multi_table
peer_table_prefix = T
pipe_protocol_prefix = M
neighbors_refresh_timeout = 2
[source.rs3-example.openbgpd-state-server]
name = rs-example.openbgpd-state-server
[source.rs3-example.openbgpd-state-server]
api = http://165.22.27.105:29111/api
# Cache results from openbgpd for n seconds, 0 disables the cache.
cache_ttl = 30
routes_cache_size = 1024 # Neighbors
[source.rs4-example-bgplgd]
name = rs-example.bgplgd
[source.rs4-example-bgplgd.openbgpd-bgplgd]
api = http://165.22.27.105:29111/api
cache_ttl = 30
routes_cache_size = 1024 # Neighbors

View File

@ -17,19 +17,19 @@ func apiStatusShow(_req *http.Request, _params httprouter.Params) (api.Response,
// Handle status
func apiStatus(_req *http.Request, params httprouter.Params) (api.Response, error) {
rsId, err := validateSourceID(params.ByName("id"))
rsID, err := validateSourceID(params.ByName("id"))
if err != nil {
return nil, err
}
source := AliceConfig.SourceInstanceById(rsId)
source := AliceConfig.SourceInstanceByID(rsID)
if source == nil {
return nil, SOURCE_NOT_FOUND_ERROR
}
result, err := source.Status()
if err != nil {
apiLogSourceError("status", rsId, err)
apiLogSourceError("status", rsID, err)
}
return result, err
@ -39,22 +39,22 @@ func apiStatus(_req *http.Request, params httprouter.Params) (api.Response, erro
func apiConfigShow(_req *http.Request, _params httprouter.Params) (api.Response, error) {
result := api.ConfigResponse{
Asn: AliceConfig.Server.Asn,
BgpCommunities: AliceConfig.Ui.BgpCommunities,
RejectReasons: AliceConfig.Ui.RoutesRejections.Reasons,
BgpCommunities: AliceConfig.UI.BgpCommunities,
RejectReasons: AliceConfig.UI.RoutesRejections.Reasons,
Noexport: api.Noexport{
LoadOnDemand: AliceConfig.Ui.RoutesNoexports.LoadOnDemand,
LoadOnDemand: AliceConfig.UI.RoutesNoexports.LoadOnDemand,
},
NoexportReasons: AliceConfig.Ui.RoutesNoexports.Reasons,
NoexportReasons: AliceConfig.UI.RoutesNoexports.Reasons,
RejectCandidates: api.RejectCandidates{
Communities: AliceConfig.Ui.RoutesRejectCandidates.Communities,
Communities: AliceConfig.UI.RoutesRejectCandidates.Communities,
},
Rpki: api.Rpki(AliceConfig.Ui.Rpki),
RoutesColumns: AliceConfig.Ui.RoutesColumns,
RoutesColumnsOrder: AliceConfig.Ui.RoutesColumnsOrder,
NeighboursColumns: AliceConfig.Ui.NeighboursColumns,
NeighboursColumnsOrder: AliceConfig.Ui.NeighboursColumnsOrder,
LookupColumns: AliceConfig.Ui.LookupColumns,
LookupColumnsOrder: AliceConfig.Ui.LookupColumnsOrder,
Rpki: api.Rpki(AliceConfig.UI.Rpki),
RoutesColumns: AliceConfig.UI.RoutesColumns,
RoutesColumnsOrder: AliceConfig.UI.RoutesColumnsOrder,
NeighboursColumns: AliceConfig.UI.NeighboursColumns,
NeighboursColumnsOrder: AliceConfig.UI.NeighboursColumnsOrder,
LookupColumns: AliceConfig.UI.LookupColumns,
LookupColumnsOrder: AliceConfig.UI.LookupColumnsOrder,
PrefixLookupEnabled: AliceConfig.Server.EnablePrefixLookup,
}
return result, nil

View File

@ -14,7 +14,7 @@ func apiNeighborsList(
_req *http.Request,
params httprouter.Params,
) (api.Response, error) {
rsId, err := validateSourceID(params.ByName("id"))
rsID, err := validateSourceID(params.ByName("id"))
if err != nil {
return nil, err
}
@ -23,9 +23,9 @@ func apiNeighborsList(
// Try to fetch neighbors from store, only fall back
// to RS query if store is not ready yet
sourceStatus := AliceNeighboursStore.SourceStatus(rsId)
sourceStatus := AliceNeighboursStore.SourceStatus(rsID)
if sourceStatus.State == STATE_READY {
neighbors := AliceNeighboursStore.GetNeighborsAt(rsId)
neighbors := AliceNeighboursStore.GetNeighborsAt(rsID)
// Make response
neighborsResponse = &api.NeighboursResponse{
Api: api.ApiStatus{
@ -41,14 +41,14 @@ func apiNeighborsList(
Neighbours: neighbors,
}
} else {
source := AliceConfig.SourceInstanceById(rsId)
source := AliceConfig.SourceInstanceByID(rsID)
if source == nil {
return nil, SOURCE_NOT_FOUND_ERROR
}
neighborsResponse, err = source.Neighbours()
if err != nil {
apiLogSourceError("neighbors", rsId, err)
apiLogSourceError("neighbors", rsID, err)
return nil, err
}
}

View File

@ -11,20 +11,20 @@ import (
// Handle routes
func apiRoutesList(_req *http.Request, params httprouter.Params) (api.Response, error) {
rsId, err := validateSourceID(params.ByName("id"))
rsID, err := validateSourceID(params.ByName("id"))
if err != nil {
return nil, err
}
neighborId := params.ByName("neighborId")
neighborID := params.ByName("neighborId")
source := AliceConfig.SourceInstanceById(rsId)
source := AliceConfig.SourceInstanceByID(rsID)
if source == nil {
return nil, SOURCE_NOT_FOUND_ERROR
}
result, err := source.Routes(neighborId)
result, err := source.Routes(neighborID)
if err != nil {
apiLogSourceError("routes", rsId, neighborId, err)
apiLogSourceError("routes", rsID, neighborID, err)
}
return result, err
@ -38,20 +38,20 @@ func apiRoutesListReceived(
// Measure response time
t0 := time.Now()
rsId, err := validateSourceID(params.ByName("id"))
rsID, err := validateSourceID(params.ByName("id"))
if err != nil {
return nil, err
}
neighborId := params.ByName("neighborId")
source := AliceConfig.SourceInstanceById(rsId)
neighborID := params.ByName("neighborId")
source := AliceConfig.SourceInstanceByID(rsID)
if source == nil {
return nil, SOURCE_NOT_FOUND_ERROR
}
result, err := source.RoutesReceived(neighborId)
result, err := source.RoutesReceived(neighborID)
if err != nil {
apiLogSourceError("routes_received", rsId, neighborId, err)
apiLogSourceError("routes_received", rsID, neighborID, err)
return nil, err
}
@ -80,7 +80,7 @@ func apiRoutesListReceived(
// Paginate results
page := apiQueryMustInt(req, "page", 0)
pageSize := AliceConfig.Ui.Pagination.RoutesAcceptedPageSize
pageSize := AliceConfig.UI.Pagination.RoutesAcceptedPageSize
routes, pagination := apiPaginateRoutes(routes, page, pageSize)
// Calculate query duration
@ -111,20 +111,20 @@ func apiRoutesListFiltered(
) (api.Response, error) {
t0 := time.Now()
rsId, err := validateSourceID(params.ByName("id"))
rsID, err := validateSourceID(params.ByName("id"))
if err != nil {
return nil, err
}
neighborId := params.ByName("neighborId")
source := AliceConfig.SourceInstanceById(rsId)
neighborID := params.ByName("neighborId")
source := AliceConfig.SourceInstanceByID(rsID)
if source == nil {
return nil, SOURCE_NOT_FOUND_ERROR
}
result, err := source.RoutesFiltered(neighborId)
result, err := source.RoutesFiltered(neighborID)
if err != nil {
apiLogSourceError("routes_filtered", rsId, neighborId, err)
apiLogSourceError("routes_filtered", rsID, neighborID, err)
return nil, err
}
@ -153,7 +153,7 @@ func apiRoutesListFiltered(
// Paginate results
page := apiQueryMustInt(req, "page", 0)
pageSize := AliceConfig.Ui.Pagination.RoutesFilteredPageSize
pageSize := AliceConfig.UI.Pagination.RoutesFilteredPageSize
routes, pagination := apiPaginateRoutes(routes, page, pageSize)
// Calculate query duration
@ -184,20 +184,20 @@ func apiRoutesListNotExported(
) (api.Response, error) {
t0 := time.Now()
rsId, err := validateSourceID(params.ByName("id"))
rsID, err := validateSourceID(params.ByName("id"))
if err != nil {
return nil, err
}
neighborId := params.ByName("neighborId")
source := AliceConfig.SourceInstanceById(rsId)
neighborID := params.ByName("neighborId")
source := AliceConfig.SourceInstanceByID(rsID)
if source == nil {
return nil, SOURCE_NOT_FOUND_ERROR
}
result, err := source.RoutesNotExported(neighborId)
result, err := source.RoutesNotExported(neighborID)
if err != nil {
apiLogSourceError("routes_not_exported", rsId, neighborId, err)
apiLogSourceError("routes_not_exported", rsID, neighborID, err)
return nil, err
}
@ -226,7 +226,7 @@ func apiRoutesListNotExported(
// Paginate results
page := apiQueryMustInt(req, "page", 0)
pageSize := AliceConfig.Ui.Pagination.RoutesNotExportedPageSize
pageSize := AliceConfig.UI.Pagination.RoutesNotExportedPageSize
routes, pagination := apiPaginateRoutes(routes, page, pageSize)
// Calculate query duration

View File

@ -17,7 +17,8 @@ func apiRouteserversList(_req *http.Request, _params httprouter.Params) (api.Res
sources := AliceConfig.Sources
for _, source := range sources {
routeservers = append(routeservers, api.Routeserver{
Id: source.Id,
Id: source.ID,
Type: source.Type,
Name: source.Name,
Group: source.Group,
Blackholes: source.Blackholes,

View File

@ -89,13 +89,13 @@ func apiLookupPrefixGlobal(
// Paginate results
pageImported := apiQueryMustInt(req, "page_imported", 0)
pageSizeImported := AliceConfig.Ui.Pagination.RoutesAcceptedPageSize
pageSizeImported := AliceConfig.UI.Pagination.RoutesAcceptedPageSize
routesImported, paginationImported := apiPaginateLookupRoutes(
imported, pageImported, pageSizeImported,
)
pageFiltered := apiQueryMustInt(req, "page_filtered", 0)
pageSizeFiltered := AliceConfig.Ui.Pagination.RoutesFilteredPageSize
pageSizeFiltered := AliceConfig.UI.Pagination.RoutesFilteredPageSize
routesFiltered, paginationFiltered := apiPaginateLookupRoutes(
filtered, pageFiltered, pageSizeFiltered,
)

View File

@ -12,7 +12,7 @@ func apiLogSourceError(module string, sourceID string, params ...interface{}) {
args := []string{}
// Get source configuration
source := AliceConfig.SourceById(sourceID)
source := AliceConfig.SourceByID(sourceID)
sourceName := "unknown"
if source != nil {
sourceName = source.Name

View File

@ -11,7 +11,7 @@ func TestApiLogSourceError(t *testing.T) {
conf := &Config{
Sources: []*SourceConfig{
&SourceConfig{
Id: "rs1v4",
ID: "rs1v4",
Name: "rs1.example.net (IPv4)",
},
},

View File

@ -15,7 +15,7 @@ func StartHTTPServer() {
router := httprouter.New()
// Serve static content
if err := webRegisterAssets(AliceConfig.Ui, router); err != nil {
if err := webRegisterAssets(AliceConfig.UI, router); err != nil {
log.Fatal(err)
}

View File

@ -2,7 +2,10 @@ package backend
import (
"fmt"
"strconv"
"strings"
"github.com/alice-lg/alice-lg/pkg/api"
)
/*
@ -130,3 +133,44 @@ func (c BgpCommunities) Set(community string, label string) {
slookup := lookup.(BgpCommunities)
slookup[path[len(path)-1]] = label
}
// APICommunities enumerates all bgp communities into
// a set of api.Communities.
// CAVEAT: Wildcards are substituted by 0 and ** ARE NOT ** expanded.
func (c BgpCommunities) APICommunities() api.Communities {
communities := api.Communities{}
// We could do this recursive, or assume that
// the max depth is 3.
for uVal, c1 := range c {
u, err := strconv.Atoi(uVal)
if err != nil {
u = 0
}
for vVal, c2 := range c1.(BgpCommunities) {
v, err := strconv.Atoi(vVal)
if err != nil {
v = 0
}
com2, ok := c2.(BgpCommunities)
if !ok {
// we only have labels here
communities = append(
communities, api.Community{u, v})
continue
}
for wVal := range com2 {
w, err := strconv.Atoi(wVal)
if err != nil {
w = 0
}
communities = append(
communities, api.Community{u, v, w})
continue
}
}
}
return communities
}

View File

@ -82,3 +82,11 @@ func TestWildcardLookup(t *testing.T) {
t.Error("Unexpected label for key")
}
}
func TestAPICommunities(t *testing.T) {
c := MakeWellKnownBgpCommunities()
comm := c.APICommunities()
if len(comm) != 14 {
t.Error("unexpected len(communities) = ", len(comm))
}
}

View File

@ -1,23 +1,55 @@
package backend
import (
"errors"
"fmt"
"log"
"os"
"strings"
"time"
"github.com/go-ini/ini"
"github.com/alice-lg/alice-lg/pkg/sources"
"github.com/alice-lg/alice-lg/pkg/sources/birdwatcher"
"github.com/alice-lg/alice-lg/pkg/sources/gobgp"
"github.com/alice-lg/alice-lg/pkg/sources/openbgpd"
)
var (
// ErrSourceTypeUnknown will be used if the type could
// not be identified from the section.
ErrSourceTypeUnknown = errors.New("source type unknown")
)
// Config Source Types
const (
SOURCE_UNKNOWN = 0
SOURCE_BIRDWATCHER = 1
SOURCE_GOBGP = 2
// SourceTypeBird is used for either bird 1x and 2x
// based route servers with a birdwatcher backend.
SourceTypeBird = "bird"
// SourceTypeGoBGP indicates a GoBGP based source.
SourceTypeGoBGP = "gobgp"
// SourceTypeOpenBGPD is used for an OpenBGPD source.
SourceTypeOpenBGPD = "openbgpd"
)
const (
// SourceBackendBirdwatcher is used to indicate that
// the source is using a birdwatcher interface.
SourceBackendBirdwatcher = "birdwatcher"
// SourceBackendGoBGP is used when the source is consuming
// a GoBGP daemon via grpc API.
SourceBackendGoBGP = "gobgp"
// SourceBackendOpenBGPDStateServer is used when the openbgpd
// is exported using the openbgpd-state-server.
SourceBackendOpenBGPDStateServer = "openbgpd-state-server"
// SourceBackendOpenBGPDBgplgd is used when the openbgpd
// state is exported through the bgplgd.
SourceBackendOpenBGPDBgplgd = "openbgpd-bgplgd"
)
// A ServerConfig holds the runtime configuration
@ -70,8 +102,8 @@ type RpkiConfig struct {
Invalid []string `ini:"invalid"`
}
// UiConfig holds runtime settings for the web client
type UiConfig struct {
// UIConfig holds runtime settings for the web client
type UIConfig struct {
RoutesColumns map[string]string
RoutesColumnsOrder []string
@ -108,7 +140,7 @@ type PaginationConfig struct {
// A SourceConfig is a generic source configuration
type SourceConfig struct {
Id string
ID string
Order int
Name string
Group string
@ -117,9 +149,11 @@ type SourceConfig struct {
Blackholes []string
// Source configurations
Type int
Type string
Backend string
Birdwatcher birdwatcher.Config
GoBGP gobgp.Config
OpenBGPD openbgpd.Config
// Source instance
instance sources.Source
@ -129,24 +163,24 @@ type SourceConfig struct {
type Config struct {
Server ServerConfig
Housekeeping HousekeepingConfig
Ui UiConfig
UI UIConfig
Sources []*SourceConfig
File string
}
// SourceById returns a source from the config by id
func (cfg *Config) SourceById(sourceId string) *SourceConfig {
// SourceByID returns a source from the config by id
func (cfg *Config) SourceByID(id string) *SourceConfig {
for _, sourceConfig := range cfg.Sources {
if sourceConfig.Id == sourceId {
if sourceConfig.ID == id {
return sourceConfig
}
}
return nil
}
// SourceInstanceById returns an instance by id
func (cfg *Config) SourceInstanceById(sourceId string) sources.Source {
sourceConfig := cfg.SourceById(sourceId)
// SourceInstanceByID returns an instance by id
func (cfg *Config) SourceInstanceByID(id string) sources.Source {
sourceConfig := cfg.SourceByID(id)
if sourceConfig == nil {
return nil // Nothing to do here.
}
@ -172,15 +206,36 @@ func isSourceBase(section *ini.Section) bool {
}
// Get backend configuration type
func getBackendType(section *ini.Section) int {
func sourceBackendTypeFromConfig(section *ini.Section) (string, error) {
name := section.Name()
if strings.HasSuffix(name, "birdwatcher") {
return SOURCE_BIRDWATCHER
return SourceBackendBirdwatcher, nil
} else if strings.HasSuffix(name, "gobgp") {
return SOURCE_GOBGP
return SourceBackendGoBGP, nil
} else if strings.HasSuffix(name, "openbgpd-bgplgd") {
return SourceBackendOpenBGPDBgplgd, nil
} else if strings.HasSuffix(name, "openbgpd-state-server") {
return SourceBackendOpenBGPDStateServer, nil
}
return SOURCE_UNKNOWN
return "", ErrSourceTypeUnknown
}
// sourceTypeFromBackendType will return the backend source type
// for a given backend type
func sourceTypeFromBackendType(t string) string {
switch t {
case SourceBackendBirdwatcher:
return SourceTypeBird
case SourceBackendGoBGP:
return SourceTypeGoBGP
case SourceBackendOpenBGPDStateServer:
return SourceTypeOpenBGPD
case SourceBackendOpenBGPDBgplgd:
return SourceTypeOpenBGPD
default:
return ""
}
}
// Get UI config: Routes Columns Default
@ -191,9 +246,7 @@ func getRoutesColumnsDefaults() (map[string]string, []string, error) {
"gateway": "Gateway",
"interface": "Interface",
}
order := []string{"network", "bgp.as_path", "gateway", "interface"}
return columns, order, nil
}
@ -502,8 +555,8 @@ func getPaginationConfig(config *ini.File) PaginationConfig {
}
// Get the UI configuration from the config file
func getUiConfig(config *ini.File) (UiConfig, error) {
uiConfig := UiConfig{}
func getUIConfig(config *ini.File) (UIConfig, error) {
uiConfig := UIConfig{}
// Get route columns
routesColumns, routesColumnsOrder, err := getRoutesColumns(config)
@ -553,7 +606,7 @@ func getUiConfig(config *ini.File) (UiConfig, error) {
paginationConfig := getPaginationConfig(config)
// Make config
uiConfig = UiConfig{
uiConfig = UIConfig{
RoutesColumns: routesColumns,
RoutesColumnsOrder: routesColumnsOrder,
@ -589,47 +642,49 @@ func getSources(config *ini.File) ([]*SourceConfig, error) {
}
// Derive source-id from name
sourceId := section.Name()[len("source:"):]
sourceID := section.Name()[len("source:"):]
// Try to get child configs and determine
// Source type
sourceConfigSections := section.ChildSections()
if len(sourceConfigSections) == 0 {
// This source has no configured backend
return sources, fmt.Errorf("%s has no backend configuration", section.Name())
return nil, fmt.Errorf("%s has no backend configuration", section.Name())
}
if len(sourceConfigSections) > 1 {
// The source is ambiguous
return sources, fmt.Errorf("%s has ambigous backends", section.Name())
return nil, fmt.Errorf("%s has ambigous backends", section.Name())
}
// Configure backend
backendConfig := sourceConfigSections[0]
backendType := getBackendType(backendConfig)
if backendType == SOURCE_UNKNOWN {
return sources, fmt.Errorf("%s has an unsupported backend", section.Name())
backendType, err := sourceBackendTypeFromConfig(backendConfig)
if err != nil {
return nil, fmt.Errorf("%s has an unsupported backend", section.Name())
}
sourceType := sourceTypeFromBackendType(backendType)
// Make config
sourceName := section.Key("name").MustString("Unknown Source")
sourceGroup := section.Key("group").MustString("")
sourceBlackholes := TrimmedStringList(
section.Key("blackholes").MustString(""))
config := &SourceConfig{
Id: sourceId,
srcCfg := &SourceConfig{
ID: sourceID,
Order: order,
Name: sourceName,
Group: sourceGroup,
Blackholes: sourceBlackholes,
Type: backendType,
Backend: backendType,
Type: sourceType,
}
// Set backend
switch backendType {
case SOURCE_BIRDWATCHER:
case SourceBackendBirdwatcher:
sourceType := backendConfig.Key("type").MustString("")
mainTable := backendConfig.Key("main_table").MustString("master")
peerTablePrefix := backendConfig.Key("peer_table_prefix").MustString("T")
@ -645,8 +700,8 @@ func getSources(config *ini.File) ([]*SourceConfig, error) {
"and pipe_protocol_prefix", pipeProtocolPrefix)
c := birdwatcher.Config{
Id: config.Id,
Name: config.Name,
ID: srcCfg.ID,
Name: srcCfg.Name,
Timezone: "UTC",
ServerTime: "2006-01-02T15:04:05.999999999Z07:00",
@ -660,12 +715,12 @@ func getSources(config *ini.File) ([]*SourceConfig, error) {
}
backendConfig.MapTo(&c)
config.Birdwatcher = c
srcCfg.Birdwatcher = c
case SOURCE_GOBGP:
case SourceBackendGoBGP:
c := gobgp.Config{
Id: config.Id,
Name: config.Name,
Id: srcCfg.ID,
Name: srcCfg.Name,
}
backendConfig.MapTo(&c)
@ -675,11 +730,51 @@ func getSources(config *ini.File) ([]*SourceConfig, error) {
c.ProcessingTimeout = 300
}
config.GoBGP = c
srcCfg.GoBGP = c
case SourceBackendOpenBGPDStateServer:
// Get cache TTL and reject communities from the config
cacheTTL := time.Second * time.Duration(backendConfig.Key("cache_ttl").MustInt(300))
routesCacheSize := backendConfig.Key("routes_cache_size").MustInt(1024)
rc, err := getRoutesRejections(config)
if err != nil {
return nil, err
}
rejectComms := rc.Reasons.APICommunities()
c := openbgpd.Config{
ID: srcCfg.ID,
Name: srcCfg.Name,
CacheTTL: cacheTTL,
RoutesCacheSize: routesCacheSize,
RejectCommunities: rejectComms,
}
backendConfig.MapTo(&c)
srcCfg.OpenBGPD = c
case SourceBackendOpenBGPDBgplgd:
// Get cache TTL from the config
cacheTTL := time.Second * time.Duration(backendConfig.Key("cache_ttl").MustInt(300))
routesCacheSize := backendConfig.Key("routes_cache_size").MustInt(1024)
rc, err := getRoutesRejections(config)
if err != nil {
return nil, err
}
rejectComms := rc.Reasons.APICommunities()
c := openbgpd.Config{
ID: srcCfg.ID,
Name: srcCfg.Name,
CacheTTL: cacheTTL,
RoutesCacheSize: routesCacheSize,
RejectCommunities: rejectComms,
}
backendConfig.MapTo(&c)
srcCfg.OpenBGPD = c
}
// Add to list of sources
sources = append(sources, config)
sources = append(sources, srcCfg)
order++
}
@ -728,7 +823,7 @@ func loadConfig(file string) (*Config, error) {
}
// Get UI configurations
ui, err := getUiConfig(parsedConfig)
ui, err := getUIConfig(parsedConfig)
if err != nil {
return nil, err
}
@ -736,7 +831,7 @@ func loadConfig(file string) (*Config, error) {
config := &Config{
Server: server,
Housekeeping: housekeeping,
Ui: ui,
UI: ui,
Sources: sources,
File: file,
}
@ -751,11 +846,15 @@ func (cfg *SourceConfig) getInstance() sources.Source {
}
var instance sources.Source
switch cfg.Type {
case SOURCE_BIRDWATCHER:
switch cfg.Backend {
case SourceBackendBirdwatcher:
instance = birdwatcher.NewBirdwatcher(cfg.Birdwatcher)
case SOURCE_GOBGP:
case SourceBackendGoBGP:
instance = gobgp.NewGoBGP(cfg.GoBGP)
case SourceBackendOpenBGPDStateServer:
instance = openbgpd.NewStateServerSource(&cfg.OpenBGPD)
case SourceBackendOpenBGPDBgplgd:
instance = openbgpd.NewBgplgdSource(&cfg.OpenBGPD)
}
cfg.instance = instance

View File

@ -3,8 +3,8 @@ package backend
import (
"testing"
"github.com/alice-lg/alice-lg/backend/sources/birdwatcher"
"github.com/alice-lg/alice-lg/backend/sources/gobgp"
"github.com/alice-lg/alice-lg/pkg/sources/birdwatcher"
"github.com/alice-lg/alice-lg/pkg/sources/gobgp"
)
// Test configuration loading and parsing
@ -12,25 +12,25 @@ import (
func TestLoadConfigs(t *testing.T) {
config, err := loadConfig("../etc/alice-lg/alice.example.conf")
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.Listen == "" {
t.Error("Listen string not present.")
}
if len(config.Ui.RoutesColumns) == 0 {
if len(config.UI.RoutesColumns) == 0 {
t.Error("Route columns settings missing")
}
if len(config.Ui.RoutesRejections.Reasons) == 0 {
if len(config.UI.RoutesRejections.Reasons) == 0 {
t.Error("Rejection reasons missing")
}
// Check communities
label, err := config.Ui.BgpCommunities.Lookup("1:23")
label, err := config.UI.BgpCommunities.Lookup("1:23")
if err != nil {
t.Error(err)
}
@ -43,10 +43,9 @@ func TestLoadConfigs(t *testing.T) {
// TestSourceConfig checks that the proper backend type was identified for each
// example routeserver
func TestSourceConfig(t *testing.T) {
config, err := loadConfig("../etc/alice-lg/alice.example.conf")
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)
}
// Get sources
@ -77,10 +76,9 @@ func TestSourceConfig(t *testing.T) {
}
func TestSourceConfigDefaultsOverride(t *testing.T) {
config, err := loadConfig("../etc/alice-lg/alice.example.conf")
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)
}
// Get sources
@ -117,13 +115,13 @@ func TestSourceConfigDefaultsOverride(t *testing.T) {
}
func TestRejectAndNoexportReasons(t *testing.T) {
config, err := loadConfig("../etc/alice-lg/alice.example.conf")
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)
}
// Rejection reasons
description, err := config.Ui.RoutesRejections.Reasons.Lookup("23:42:1")
description, err := config.UI.RoutesRejections.Reasons.Lookup("23:42:1")
if err != nil {
t.Error(err)
}
@ -133,7 +131,7 @@ func TestRejectAndNoexportReasons(t *testing.T) {
}
// Noexport reasons
description, err = config.Ui.RoutesNoexports.Reasons.Lookup("23:46:1")
description, err = config.UI.RoutesNoexports.Reasons.Lookup("23:46:1")
if err != nil {
t.Error(err)
}
@ -144,9 +142,9 @@ func TestRejectAndNoexportReasons(t *testing.T) {
}
func TestBlackholeParsing(t *testing.T) {
config, err := loadConfig("../etc/alice-lg/alice.example.conf")
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)
}
// Get first source
@ -163,9 +161,9 @@ func TestBlackholeParsing(t *testing.T) {
}
func TestOwnASN(t *testing.T) {
config, err := loadConfig("../etc/alice-lg/alice.example.conf")
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.Asn != 9033 {
@ -174,45 +172,43 @@ func TestOwnASN(t *testing.T) {
}
func TestRpkiConfig(t *testing.T) {
config, err := loadConfig("../etc/alice-lg/alice.example.conf")
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 len(config.Ui.Rpki.Valid) != 3 {
t.Error("Unexpected RPKI:VALID,", config.Ui.Rpki.Valid)
if len(config.UI.Rpki.Valid) != 3 {
t.Error("Unexpected RPKI:VALID,", config.UI.Rpki.Valid)
}
if len(config.Ui.Rpki.Invalid) != 4 {
t.Error("Unexpected RPKI:INVALID,", config.Ui.Rpki.Invalid)
return // We would fail hard later
if len(config.UI.Rpki.Invalid) != 4 {
t.Fatal("Unexpected RPKI:INVALID,", config.UI.Rpki.Invalid)
}
// Check fallback
if config.Ui.Rpki.NotChecked[0] != "9033" {
if config.UI.Rpki.NotChecked[0] != "9033" {
t.Error(
"Expected NotChecked to fall back to defaults, got:",
config.Ui.Rpki.NotChecked,
config.UI.Rpki.NotChecked,
)
}
// Check range postprocessing
if config.Ui.Rpki.Invalid[3] != "*" {
if config.UI.Rpki.Invalid[3] != "*" {
t.Error("Missing wildcard from config")
}
t.Log(config.Ui.Rpki)
t.Log(config.UI.Rpki)
}
func TestRejectCandidatesConfig(t *testing.T) {
config, err := loadConfig("../etc/alice-lg/alice.example.conf")
config, err := loadConfig("_testdata/alice.conf")
if err != nil {
t.Error("Could not load test config:", err)
return
t.Fatal("Could not load test config:", err)
}
t.Log(config.Ui.RoutesRejectCandidates.Communities)
t.Log(config.UI.RoutesRejectCandidates.Communities)
description, err := config.Ui.RoutesRejectCandidates.Communities.Lookup("23:42:46")
description, err := config.UI.RoutesRejectCandidates.Communities.Lookup("23:42:46")
if err != nil {
t.Error(err)
}

View File

@ -25,6 +25,7 @@ type NeighboursStore struct {
sync.RWMutex
}
// NewNeighboursStore creates a new store for neighbors
func NewNeighboursStore(config *Config) *NeighboursStore {
// Build source mapping
@ -33,13 +34,13 @@ func NewNeighboursStore(config *Config) *NeighboursStore {
statusMap := make(map[string]StoreStatus)
for _, source := range config.Sources {
sourceId := source.Id
configMap[sourceId] = source
statusMap[sourceId] = StoreStatus{
id := source.ID
configMap[id] = source
statusMap[id] = StoreStatus{
State: STATE_INIT,
}
neighboursMap[sourceId] = make(NeighboursIndex)
neighboursMap[id] = make(NeighboursIndex)
}
// Set refresh interval, default to 5 minutes when
@ -62,6 +63,7 @@ func NewNeighboursStore(config *Config) *NeighboursStore {
return store
}
// Start the store's housekeeping.
func (self *NeighboursStore) Start() {
log.Println("Starting local neighbours store")
log.Println("Neighbours Store refresh interval set to:", self.refreshInterval)
@ -121,7 +123,7 @@ func (self *NeighboursStore) update() {
if err != nil {
log.Println(
"Refreshing the neighbors store failed for:",
sourceConfig.Name, "(", sourceConfig.Id, ")",
sourceConfig.Name, "(", sourceConfig.ID, ")",
"with:", err,
"- NEXT STATE: ERROR",
)

View File

@ -33,7 +33,7 @@ func NewRoutesStore(config *Config) *RoutesStore {
configMap := make(map[string]*SourceConfig)
for _, source := range config.Sources {
id := source.Id
id := source.ID
configMap[id] = source
routesMap[id] = &api.RoutesResponse{}
@ -109,7 +109,7 @@ func (rs *RoutesStore) update() {
if err != nil {
log.Println(
"Refreshing the routes store failed for:", sourceConfig.Name,
"(", sourceConfig.Id, ")",
"(", sourceConfig.ID, ")",
"with:", err,
"- NEXT STATE: ERROR",
)
@ -207,7 +207,7 @@ func routeToLookupRoute(
) *api.LookupRoute {
// Get neighbour
neighbour := AliceNeighboursStore.GetNeighbourAt(source.Id, route.NeighbourId)
neighbour := AliceNeighboursStore.GetNeighbourAt(source.ID, route.NeighbourId)
// Make route
lookup := &api.LookupRoute{
@ -217,7 +217,7 @@ func routeToLookupRoute(
Neighbour: neighbour,
Routeserver: api.Routeserver{
Id: source.Id,
Id: source.ID,
Name: source.Name,
},

View File

@ -79,12 +79,12 @@ func makeTestRoutesStore() *RoutesStore {
configMap := map[string]*SourceConfig{
"rs1": &SourceConfig{
Id: "rs1",
ID: "rs1",
Name: "rs1.test",
Type: SOURCE_BIRDWATCHER,
Type: SourceTypeBird,
Birdwatcher: birdwatcher.Config{
Api: "http://localhost:2342",
API: "http://localhost:2342",
Timezone: "UTC",
ServerTime: "2006-01-02T15:04:05",
ServerTimeShort: "2006-01-02",

View File

@ -34,7 +34,7 @@ func webPrepareClientHTML(html string) string {
// Register assets handler and index handler
// at /static and /
func webRegisterAssets(ui UiConfig, router *httprouter.Router) error {
func webRegisterAssets(ui UIConfig, router *httprouter.Router) error {
log.Println("Preparing and installing assets")
// Prepare client html: Rewrite paths

View File

@ -5,12 +5,14 @@ import (
)
/*
Use a least recently used caching strategy:
LRUMap is a cache map which uses
a least recently used caching strategy:
Store last access in map, retrieve least recently
used key.
*/
type LRUMap map[string]time.Time
// LRU retrievs the least recently used key
func (lrumap LRUMap) LRU() string {
t := time.Now()
key := ""

View File

@ -14,11 +14,13 @@ birdwatcher, we keep a local cache. (This comes in handy
when we are paginating the results for better client performance.)
*/
// NeighborsCache implements a cache to store neighbors
type NeighborsCache struct {
response *api.NeighboursResponse
disabled bool
}
// NewNeighborsCache initializes a cache for neighbor responses.
func NewNeighborsCache(disabled bool) *NeighborsCache {
cache := &NeighborsCache{
response: nil,
@ -28,26 +30,29 @@ func NewNeighborsCache(disabled bool) *NeighborsCache {
return cache
}
func (self *NeighborsCache) Get() *api.NeighboursResponse {
if self.disabled {
// Get retrievs the neighbors response from the cache, if present,
// and makes sure the information is still up to date.
func (cache *NeighborsCache) Get() *api.NeighboursResponse {
if cache.disabled {
return nil
}
if self.response == nil {
if cache.response == nil {
return nil
}
if self.response.CacheTTL() < 0 {
if cache.response.CacheTTL() < 0 {
return nil
}
return self.response
return cache.response
}
func (self *NeighborsCache) Set(response *api.NeighboursResponse) {
if self.disabled {
// Set updates the neighbors cache with a new response retrieved
// from a backend source.
func (cache *NeighborsCache) Set(response *api.NeighboursResponse) {
if cache.disabled {
return
}
self.response = response
cache.response = response
}

View File

@ -8,7 +8,8 @@ import (
)
/*
Routes Cache:
RoutesCache stores routes responses from the backend.
Keep a kv map with neighborId <-> api.RoutesResponse
TTL is derived from the api.RoutesResponse.
@ -24,6 +25,7 @@ type RoutesCache struct {
sync.Mutex
}
// NewRoutesCache initializes a new cache for route responses.
func NewRoutesCache(disabled bool, size int) *RoutesCache {
cache := &RoutesCache{
responses: make(map[string]*api.RoutesResponse),
@ -35,15 +37,16 @@ func NewRoutesCache(disabled bool, size int) *RoutesCache {
return cache
}
func (self *RoutesCache) Get(neighborId string) *api.RoutesResponse {
if self.disabled {
// Get retrievs all routes for a given neighbor
func (cache *RoutesCache) Get(neighborID string) *api.RoutesResponse {
if cache.disabled {
return nil
}
self.Lock()
defer self.Unlock()
cache.Lock()
defer cache.Unlock()
response, ok := self.responses[neighborId]
response, ok := cache.responses[neighborID]
if !ok {
return nil
}
@ -52,43 +55,45 @@ func (self *RoutesCache) Get(neighborId string) *api.RoutesResponse {
return nil
}
self.accessedAt[neighborId] = time.Now()
cache.accessedAt[neighborID] = time.Now()
return response
}
func (self *RoutesCache) Set(neighborId string, response *api.RoutesResponse) {
if self.disabled {
// Set the routes response for a given neighbor
func (cache *RoutesCache) Set(neighborID string, response *api.RoutesResponse) {
if cache.disabled {
return
}
self.Lock()
defer self.Unlock()
cache.Lock()
defer cache.Unlock()
if len(self.responses) > self.size {
if len(cache.responses) > cache.size {
// delete LRU
lru := self.accessedAt.LRU()
delete(self.accessedAt, lru)
delete(self.responses, lru)
leastRecentNeighbor := cache.accessedAt.LRU()
delete(cache.accessedAt, leastRecentNeighbor)
delete(cache.responses, leastRecentNeighbor)
}
self.accessedAt[neighborId] = time.Now()
self.responses[neighborId] = response
cache.accessedAt[neighborID] = time.Now()
cache.responses[neighborID] = response
}
func (self *RoutesCache) Expire() int {
self.Lock()
defer self.Unlock()
// Expire will flush expired keys. (TODO: naming could be better.)
func (cache *RoutesCache) Expire() int {
cache.Lock()
defer cache.Unlock()
expiredKeys := []string{}
for key, response := range self.responses {
for key, response := range cache.responses {
if response.CacheTTL() < 0 {
expiredKeys = append(expiredKeys, key)
}
}
for _, key := range expiredKeys {
delete(self.responses, key)
delete(cache.responses, key)
}
return len(expiredKeys)

View File

@ -0,0 +1,27 @@
package decoders
// Helper for decoding json bodies from responses
import (
"encoding/json"
"io/ioutil"
"net/http"
)
// ReadJSONResponse reads a json blob from a
// http response and decodes it into a map
func ReadJSONResponse(res *http.Response) (map[string]interface{}, error) {
// Read body
defer res.Body.Close()
data, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}
// Parse JSON
payload := make(map[string]interface{})
if err := json.Unmarshal(data, &payload); err != nil {
return nil, err
}
return payload, nil
}

31
pkg/decoders/maps.go Normal file
View File

@ -0,0 +1,31 @@
package decoders
// MapGet retrievs a key from an expected map
// it falls back if the input is not a map
// or the key was not found.
func MapGet(m interface{}, key string, fallback interface{}) interface{} {
smap, ok := m.(map[string]interface{})
if !ok {
return fallback
}
val, ok := smap[key]
if !ok {
return fallback
}
return val
}
// MapGetString retrievs a key from a map and
// asserts its type is a string. Otherwise fallback
// will be returned.
func MapGetString(m interface{}, key string, fallback string) string {
val := MapGet(m, key, fallback)
return val.(string)
}
// MapGetBool will retrieve a boolean value
// for a given key.
func MapGetBool(m interface{}, key string, fallback bool) bool {
val := MapGet(m, key, fallback)
return val.(bool)
}

153
pkg/decoders/types.go Normal file
View File

@ -0,0 +1,153 @@
package decoders
// Decode interfaces into expected types
// with a fallback.
import (
"fmt"
"strconv"
"time"
)
// String asserts a string, provided a default
func String(value interface{}, fallback string) string {
sval, ok := value.(string)
if !ok {
return fallback
}
return sval
}
// StringList decodes a list of strings
func StringList(data interface{}) []string {
ldata, ok := data.([]interface{})
if !ok {
return []string{}
}
list := make([]string, 0, len(ldata))
for _, e := range ldata {
s, ok := e.(string)
if ok {
list = append(list, s)
}
}
return list
}
// IntList decodes a list of integers
func IntList(data interface{}) []int {
sdata := StringList(data)
list := make([]int, 0, len(sdata))
for _, e := range sdata {
val, err := strconv.Atoi(e)
if err == nil {
list = append(list, val)
}
}
return list
}
// IntListFromStrings decodes a list of strings
// into a list of integers.
func IntListFromStrings(strs []string) []int {
list := make([]int, 0, len(strs))
for _, s := range strs {
v, err := strconv.Atoi(s)
if err != nil {
continue // skip this
}
list = append(list, v)
}
return list
}
// Int decodes an integer value
func Int(value interface{}, fallback int) int {
fval, ok := value.(float64)
if !ok {
return fallback
}
return int(fval)
}
// IntFromString decodes an integer from a string
func IntFromString(s string, fallback int) int {
val, err := strconv.Atoi(s)
if err != nil {
return fallback
}
return val
}
// Bool decodes a boolean value
func Bool(value interface{}, fallback bool) bool {
val, ok := value.(bool)
if !ok {
return fallback
}
return val
}
// Duration decodes a time.Duration
func Duration(value interface{}, fallback time.Duration) time.Duration {
val, ok := value.(time.Duration)
if !ok {
return fallback
}
return val
}
// DurationTimeframe decodes a duration: Bgpctl encodes
// this using fmt_timeframe, whiuch outputs a format similar
// to that being understood by time.ParseDuration - however
// the time unit "w" (weeks) is not supported.
// According to https://github.com/openbgpd-portable/openbgpd-openbsd/blob/master/src/usr.sbin/bgpctl/bgpctl.c#L586-L591
// we have to parse %02lluw%01ud%02uh, %01ud%02uh%02um and %02u:%02u:%02u.
// This yields three formats:
// 01w3d01h
// 1d02h03m
// 01:02:03
func DurationTimeframe(value interface{}, fallback time.Duration) time.Duration {
var sec, min, hour, day uint
var week uint64
sval := String(value, "")
if sval == "" {
return fallback
}
n, _ := fmt.Sscanf(sval, "%02dw%01dd%02dh", &week, &day, &hour)
if n == 3 {
return time.Duration(week)*7*24*time.Hour +
time.Duration(day)*24*time.Hour +
time.Duration(hour)*time.Hour
}
n, _ = fmt.Sscanf(sval, "%01dd%02dh%02dm", &day, &hour, &min)
if n == 3 {
return time.Duration(day)*24*time.Hour +
time.Duration(hour)*time.Hour +
time.Duration(min)*time.Minute
}
n, _ = fmt.Sscanf(sval, "%02d:%02d:%02d", &hour, &min, &sec)
if n == 3 {
return time.Duration(hour)*time.Hour +
time.Duration(min)*time.Minute +
time.Duration(sec)*time.Second
}
return fallback
}
// TimeUTC returns the time expecting an UTC timestamp
func TimeUTC(value interface{}, fallback time.Time) time.Time {
sval := String(value, "")
if sval == "" {
return fallback
}
t, err := time.Parse(time.RFC3339Nano, sval)
if err != nil {
return fallback
}
return t
}

View File

@ -0,0 +1,24 @@
package decoders
import (
"testing"
"time"
)
func TestDurationTimeframe(t *testing.T) {
d := DurationTimeframe("01w1d5h", 0)
if d != 197*time.Hour {
t.Error("unexpected", d)
}
d = DurationTimeframe("5d20h05m", 0)
if d != 140*time.Hour+5*time.Minute {
t.Error("unexpected", d)
}
d = DurationTimeframe("01:02:03", 0)
if d != 1*time.Hour+2*time.Minute+3*time.Second {
t.Error("unexpected", d)
}
}

View File

@ -1,10 +1,12 @@
package birdwatcher
// Config contains all configuration attributes
// for a birdwatcher based source.
type Config struct {
Id string
ID string
Name string
Api string `ini:"api"`
API string `ini:"api"`
Timezone string `ini:"timezone"`
ServerTime string `ini:"servertime"`
ServerTimeShort string `ini:"servertime_short"`

View File

@ -11,6 +11,7 @@ import (
"time"
"github.com/alice-lg/alice-lg/pkg/api"
"github.com/alice-lg/alice-lg/pkg/decoders"
)
// Convert server time string to time
@ -136,9 +137,9 @@ func parseBirdwatcherStatus(bird ClientResponse, config Config) (api.Status, err
LastReboot: lastReboot,
LastReconfig: lastReconfig,
Backend: "bird",
Version: mustString(birdStatus["version"], "unknown"),
Message: mustString(birdStatus["message"], "unknown"),
RouterId: mustString(birdStatus["router_id"], "unknown"),
Version: decoders.String(birdStatus["version"], "unknown"),
Message: decoders.String(birdStatus["message"], "unknown"),
RouterId: decoders.String(birdStatus["router_id"], "unknown"),
}
return status, nil
@ -152,7 +153,7 @@ func parseRelativeServerTime(uptime interface{}, config Config) time.Duration {
// Parse neighbours response
func parseNeighbours(bird ClientResponse, config Config) (api.Neighbours, error) {
rsId := config.Id
rsId := config.ID
neighbours := api.Neighbours{}
protocols := bird["protocols"].(map[string]interface{})
@ -162,7 +163,7 @@ func parseNeighbours(bird ClientResponse, config Config) (api.Neighbours, error)
routes := protocol["routes"].(map[string]interface{})
uptime := parseRelativeServerTime(protocol["state_changed"], config)
lastError := mustString(protocol["last_error"], "")
lastError := decoders.String(protocol["last_error"], "")
routesReceived := float64(0)
if routes != nil {
@ -177,17 +178,17 @@ func parseNeighbours(bird ClientResponse, config Config) (api.Neighbours, error)
neighbour := &api.Neighbour{
Id: protocolId,
Address: mustString(protocol["neighbor_address"], "error"),
Asn: mustInt(protocol["neighbor_as"], 0),
Address: decoders.String(protocol["neighbor_address"], "error"),
Asn: decoders.Int(protocol["neighbor_as"], 0),
State: strings.ToLower(
mustString(protocol["state"], "unknown")),
Description: mustString(protocol["description"], "no description"),
decoders.String(protocol["state"], "unknown")),
Description: decoders.String(protocol["description"], "no description"),
RoutesReceived: mustInt(routesReceived, 0),
RoutesAccepted: mustInt(routes["imported"], 0),
RoutesFiltered: mustInt(routes["filtered"], 0),
RoutesExported: mustInt(routes["exported"], 0), //TODO protocol_exported?
RoutesPreferred: mustInt(routes["preferred"], 0),
RoutesReceived: decoders.Int(routesReceived, 0),
RoutesAccepted: decoders.Int(routes["imported"], 0),
RoutesFiltered: decoders.Int(routes["filtered"], 0),
RoutesExported: decoders.Int(routes["exported"], 0), //TODO protocol_exported?
RoutesPreferred: decoders.Int(routes["preferred"], 0),
Uptime: uptime,
LastError: lastError,
@ -218,7 +219,7 @@ func parseNeighboursShort(bird ClientResponse, config Config) (api.NeighboursSta
neighbour := &api.NeighbourStatus{
Id: protocolId,
State: mustString(protocol["state"], "unknown"),
State: decoders.String(protocol["state"], "unknown"),
Since: uptime,
}
@ -238,18 +239,18 @@ func parseRouteBgpInfo(data interface{}) api.BgpInfo {
return api.BgpInfo{}
}
asPath := mustIntList(bgpData["as_path"])
asPath := decoders.IntList(bgpData["as_path"])
communities := parseBgpCommunities(bgpData["communities"])
largeCommunities := parseBgpCommunities(bgpData["large_communities"])
extCommunities := parseExtBgpCommunities(bgpData["ext_communities"])
localPref, _ := strconv.Atoi(mustString(bgpData["local_pref"], "0"))
med, _ := strconv.Atoi(mustString(bgpData["med"], "0"))
localPref, _ := strconv.Atoi(decoders.String(bgpData["local_pref"], "0"))
med, _ := strconv.Atoi(decoders.String(bgpData["med"], "0"))
bgp := api.BgpInfo{
Origin: mustString(bgpData["origin"], "unknown"),
Origin: decoders.String(bgpData["origin"], "unknown"),
AsPath: asPath,
NextHop: mustString(bgpData["next_hop"], "unknown"),
NextHop: decoders.String(bgpData["next_hop"], "unknown"),
LocalPref: localPref,
Med: med,
Communities: communities,
@ -312,18 +313,18 @@ func parseRoutesData(birdRoutes []interface{}, config Config) api.Routes {
rdata := data.(map[string]interface{})
age := parseRelativeServerTime(rdata["age"], config)
rtype := mustStringList(rdata["type"])
rtype := decoders.StringList(rdata["type"])
bgpInfo := parseRouteBgpInfo(rdata["bgp"])
route := &api.Route{
Id: mustString(rdata["network"], "unknown"),
NeighbourId: mustString(rdata["from_protocol"], "unknown neighbour"),
Id: decoders.String(rdata["network"], "unknown"),
NeighbourId: decoders.String(rdata["from_protocol"], "unknown neighbour"),
Network: mustString(rdata["network"], "unknown net"),
Interface: mustString(rdata["interface"], "unknown interface"),
Gateway: mustString(rdata["gateway"], "unknown gateway"),
Metric: mustInt(rdata["metric"], -1),
Primary: mustBool(rdata["primary"], false),
Network: decoders.String(rdata["network"], "unknown net"),
Interface: decoders.String(rdata["interface"], "unknown interface"),
Gateway: decoders.String(rdata["gateway"], "unknown gateway"),
Metric: decoders.Int(rdata["metric"], -1),
Primary: decoders.Bool(rdata["primary"], false),
Age: age,
Type: rtype,
Bgp: bgpInfo,

View File

@ -30,7 +30,7 @@ type GenericBirdwatcher struct {
}
func NewBirdwatcher(config Config) Birdwatcher {
client := NewClient(config.Api)
client := NewClient(config.API)
// Cache settings:
// TODO: Maybe read from config file
@ -248,7 +248,7 @@ func (self *GenericBirdwatcher) NeighboursStatus() (*api.NeighboursStatusRespons
func (self *GenericBirdwatcher) LookupPrefix(prefix string) (*api.RoutesLookupResponse, error) {
// Get RS info
rs := api.Routeserver{
Id: self.config.Id,
Id: self.config.ID,
Name: self.config.Name,
}

View File

@ -7,6 +7,7 @@ import (
"strings"
"github.com/alice-lg/alice-lg/pkg/api"
"github.com/alice-lg/alice-lg/pkg/decoders"
)
type MultiTableBirdwatcher struct {
@ -238,7 +239,7 @@ func (self *MultiTableBirdwatcher) fetchRequiredRoutes(neighborId string) (*api.
importedRoutes := api.Routes{}
if len(receivedRoutes) > 0 {
peer := receivedRoutes[0].Gateway
learntFrom := mustString(receivedRoutes[0].Details["learnt_from"], peer)
learntFrom := decoders.String(receivedRoutes[0].Details["learnt_from"], peer)
filteredRoutes = self.filterRoutesByPeerOrLearntFrom(filteredRoutes, peer, learntFrom)
importedRoutes = self.filterRoutesByDuplicates(receivedRoutes, filteredRoutes)
@ -507,7 +508,7 @@ func (self *MultiTableBirdwatcher) AllRoutes() (*api.RoutesResponse, error) {
protocolsBgp := self.filterProtocolsBgp(birdProtocols)
for protocolId, protocolsData := range protocolsBgp["protocols"].(map[string]interface{}) {
peer := protocolsData.(map[string]interface{})["neighbor_address"].(string)
learntFrom := mustString(protocolsData.(map[string]interface{})["learnt_from"], peer)
learntFrom := decoders.String(protocolsData.(map[string]interface{})["learnt_from"], peer)
// Fetch filtered routes
_, filtered, err := self.fetchFilteredRoutes(protocolId)

View File

@ -5,6 +5,7 @@ import (
"sort"
"github.com/alice-lg/alice-lg/pkg/api"
"github.com/alice-lg/alice-lg/pkg/decoders"
)
type SingleTableBirdwatcher struct {
@ -119,7 +120,7 @@ func (self *SingleTableBirdwatcher) fetchRequiredRoutes(neighborId string) (*api
importedRoutes := api.Routes{}
if len(receivedRoutes) > 0 {
peer := receivedRoutes[0].Gateway
learntFrom := mustString(receivedRoutes[0].Details["learnt_from"], peer)
learntFrom := decoders.String(receivedRoutes[0].Details["learnt_from"], peer)
filteredRoutes = self.filterRoutesByPeerOrLearntFrom(filteredRoutes, peer, learntFrom)
importedRoutes = self.filterRoutesByDuplicates(receivedRoutes, filteredRoutes)

View File

@ -1,61 +0,0 @@
package birdwatcher
import (
"strconv"
)
/*
* Types helper for parser
*/
// Assert string, provide default
func mustString(value interface{}, fallback string) string {
sval, ok := value.(string)
if !ok {
return fallback
}
return sval
}
// Assert list of strings
func mustStringList(data interface{}) []string {
list := []string{}
ldata, ok := data.([]interface{})
if !ok {
return []string{}
}
for _, e := range ldata {
s, ok := e.(string)
if ok {
list = append(list, s)
}
}
return list
}
// Convert list of strings to int
func mustIntList(data interface{}) []int {
list := []int{}
sdata := mustStringList(data)
for _, e := range sdata {
val, _ := strconv.Atoi(e)
list = append(list, val)
}
return list
}
func mustInt(value interface{}, fallback int) int {
fval, ok := value.(float64)
if !ok {
return fallback
}
return int(fval)
}
func mustBool(value interface{}, fallback bool) bool {
val, ok := value.(bool)
if !ok {
return fallback
}
return val
}

View File

@ -0,0 +1,384 @@
package openbgpd
import (
"context"
"net/http"
"time"
"github.com/alice-lg/alice-lg/pkg/api"
"github.com/alice-lg/alice-lg/pkg/caches"
"github.com/alice-lg/alice-lg/pkg/decoders"
)
const (
// BgplgdSourceVersion is currently fixed at 1.0
BgplgdSourceVersion = "1.0"
)
// BgplgdSource implements a source for Alice, consuming
// the openbgp bgplgd.
type BgplgdSource struct {
// cfg is the source configuration retrieved
// from the alice config file.
cfg *Config
// Store the neighbor responses from the server here
neighborsCache *caches.NeighborsCache
// Store the routes responses from the server
// here identified by neighborID
routesCache *caches.RoutesCache
routesReceivedCache *caches.RoutesCache
routesFilteredCache *caches.RoutesCache
}
// NewBgplgdSource creates a new source instance with a configuration.
func NewBgplgdSource(cfg *Config) *BgplgdSource {
cacheDisabled := cfg.CacheTTL == 0
// Initialize caches
nc := caches.NewNeighborsCache(cacheDisabled)
rc := caches.NewRoutesCache(cacheDisabled, cfg.RoutesCacheSize)
rrc := caches.NewRoutesCache(cacheDisabled, cfg.RoutesCacheSize)
rfc := caches.NewRoutesCache(cacheDisabled, cfg.RoutesCacheSize)
return &BgplgdSource{
cfg: cfg,
neighborsCache: nc,
routesCache: rc,
routesReceivedCache: rrc,
routesFilteredCache: rfc,
}
}
// ExpireCaches ... will flush the cache.
func (src *BgplgdSource) ExpireCaches() int {
totalExpired := src.routesReceivedCache.Expire()
return totalExpired
}
// Requests
// ========
// ShowNeighborsRequest makes an all neighbors request
func (src *BgplgdSource) ShowNeighborsRequest(ctx context.Context) (*http.Request, error) {
url := src.cfg.APIURL("/neighbors")
return http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
}
// ShowNeighborsSummaryRequest builds an neighbors status request
func (src *BgplgdSource) ShowNeighborsSummaryRequest(
ctx context.Context,
) (*http.Request, error) {
url := src.cfg.APIURL("/summary")
return http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
}
// ShowNeighborRIBRequest retrives the routes accepted from the neighbor
// identified by bgp-id.
func (src *BgplgdSource) ShowNeighborRIBRequest(
ctx context.Context,
neighborID string,
) (*http.Request, error) {
url := src.cfg.APIURL("/rib?neighbor=%s", neighborID)
return http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
}
// ShowRIBRequest makes a request for retrieving all routes imported
// from all peers
func (src *BgplgdSource) ShowRIBRequest(ctx context.Context) (*http.Request, error) {
url := src.cfg.APIURL("/rib")
return http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
}
// Datasource
// ==========
// makeCacheStatus will create a new api status with cache infos
func (src *BgplgdSource) makeCacheStatus() api.ApiStatus {
return api.ApiStatus{
CacheStatus: api.CacheStatus{
CachedAt: time.Now().UTC(),
},
Version: BgplgdSourceVersion,
ResultFromCache: false,
Ttl: time.Now().UTC().Add(src.cfg.CacheTTL),
}
}
// Status returns an API status response. In our case
// this is pretty much only that the service is available.
func (src *BgplgdSource) Status() (*api.StatusResponse, error) {
// Make API request and read response. We do not cache the result.
response := &api.StatusResponse{
Api: src.makeCacheStatus(),
Status: api.Status{
Version: "openbgpd",
Message: "openbgpd up and running",
},
}
return response, nil
}
// Neighbours retrievs a full list of all neighbors
func (src *BgplgdSource) Neighbours() (*api.NeighboursResponse, error) {
// Query cache and see if we have a hit
response := src.neighborsCache.Get()
if response != nil {
response.Api.ResultFromCache = true
return response, nil
}
// Make API request and read response
req, err := src.ShowNeighborsRequest(context.Background())
if err != nil {
return nil, err
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
body, err := decoders.ReadJSONResponse(res)
if err != nil {
return nil, err
}
nb, err := decodeNeighbors(body)
if err != nil {
return nil, err
}
// Set route server id (sourceID) for all neighbors and
// calculate the filtered routes.
for _, n := range nb {
n.RouteServerId = src.cfg.ID
rejectedRes, err := src.RoutesFiltered(n.Id)
if err != nil {
return nil, err
}
rejectCount := len(rejectedRes.Filtered)
n.RoutesFiltered = rejectCount
}
response = &api.NeighboursResponse{
Api: src.makeCacheStatus(),
Neighbours: nb,
}
src.neighborsCache.Set(response)
return response, nil
}
// NeighboursStatus retrives the status summary
// for all neightbors
func (src *BgplgdSource) NeighboursStatus() (*api.NeighboursStatusResponse, error) {
// Make API request and read response
req, err := src.ShowNeighborsSummaryRequest(context.Background())
if err != nil {
return nil, err
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
// Read and decode response
body, err := decoders.ReadJSONResponse(res)
if err != nil {
return nil, err
}
nb, err := decodeNeighborsStatus(body)
if err != nil {
return nil, err
}
response := &api.NeighboursStatusResponse{
Api: src.makeCacheStatus(),
Neighbours: nb,
}
return response, nil
}
// Routes retrieves the routes for a specific neighbor
// identified by ID.
func (src *BgplgdSource) Routes(neighborID string) (*api.RoutesResponse, error) {
response := src.routesCache.Get(neighborID)
if response != nil {
response.Api.ResultFromCache = true
return response, nil
}
// Query RIB for routes received
req, err := src.ShowNeighborRIBRequest(context.Background(), neighborID)
if err != nil {
return nil, err
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
// Read and decode response
body, err := decoders.ReadJSONResponse(res)
if err != nil {
return nil, err
}
routes, err := decodeRoutes(body)
if err != nil {
return nil, err
}
// Filtered routes are marked with a large BGP community
// as defined in the reject reasons.
received := filterReceivedRoutes(src.cfg.RejectCommunities, routes)
rejected := filterRejectedRoutes(src.cfg.RejectCommunities, routes)
response = &api.RoutesResponse{
Api: src.makeCacheStatus(),
Imported: received,
NotExported: api.Routes{},
Filtered: rejected,
}
src.routesCache.Set(neighborID, response)
return response, nil
}
// RoutesReceived returns the routes exported by the neighbor.
func (src *BgplgdSource) RoutesReceived(neighborID string) (*api.RoutesResponse, error) {
response := src.routesReceivedCache.Get(neighborID)
if response != nil {
response.Api.ResultFromCache = true
return response, nil
}
// Query RIB for routes received
req, err := src.ShowNeighborRIBRequest(context.Background(), neighborID)
if err != nil {
return nil, err
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
// Read and decode response
body, err := decoders.ReadJSONResponse(res)
if err != nil {
return nil, err
}
routes, err := decodeRoutes(body)
if err != nil {
return nil, err
}
// Filtered routes are marked with a large BGP community
// as defined in the reject reasons.
received := filterReceivedRoutes(src.cfg.RejectCommunities, routes)
response = &api.RoutesResponse{
Api: src.makeCacheStatus(),
Imported: received,
NotExported: api.Routes{},
Filtered: api.Routes{},
}
src.routesReceivedCache.Set(neighborID, response)
return response, nil
}
// RoutesFiltered retrieves the routes filtered / not valid
func (src *BgplgdSource) RoutesFiltered(neighborID string) (*api.RoutesResponse, error) {
response := src.routesFilteredCache.Get(neighborID)
if response != nil {
response.Api.ResultFromCache = true
return response, nil
}
// Query RIB for routes received
req, err := src.ShowNeighborRIBRequest(context.Background(), neighborID)
if err != nil {
return nil, err
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
// Read and decode response
body, err := decoders.ReadJSONResponse(res)
if err != nil {
return nil, err
}
routes, err := decodeRoutes(body)
if err != nil {
return nil, err
}
// Filtered routes are marked with a large BGP community
// as defined in the reject reasons.
rejected := filterRejectedRoutes(src.cfg.RejectCommunities, routes)
response = &api.RoutesResponse{
Api: src.makeCacheStatus(),
Imported: api.Routes{},
NotExported: api.Routes{},
Filtered: rejected,
}
src.routesFilteredCache.Set(neighborID, response)
return response, nil
}
// RoutesNotExported retrievs the routes not exported
// from the rs for a neighbor.
func (src *BgplgdSource) RoutesNotExported(neighborID string) (*api.RoutesResponse, error) {
response := &api.RoutesResponse{
Api: src.makeCacheStatus(),
Imported: api.Routes{},
NotExported: api.Routes{},
Filtered: api.Routes{},
}
return response, nil
}
// AllRoutes retrievs the entire RIB from the source. This is never
// cached as it is processed by the store.
func (src *BgplgdSource) AllRoutes() (*api.RoutesResponse, error) {
req, err := src.ShowRIBRequest(context.Background())
if err != nil {
return nil, err
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
// Read and decode response
body, err := decoders.ReadJSONResponse(res)
if err != nil {
return nil, err
}
routes, err := decodeRoutes(body)
if err != nil {
return nil, err
}
// Filtered routes are marked with a large BGP community
// as defined in the reject reasons.
received := filterReceivedRoutes(src.cfg.RejectCommunities, routes)
rejected := filterRejectedRoutes(src.cfg.RejectCommunities, routes)
response := &api.RoutesResponse{
Api: src.makeCacheStatus(),
Imported: received,
NotExported: api.Routes{},
Filtered: rejected,
}
return response, nil
}

View File

@ -0,0 +1,32 @@
package openbgpd
import (
"fmt"
"strings"
"time"
"github.com/alice-lg/alice-lg/pkg/api"
)
// Config is a OpenBGPD source config
type Config struct {
ID string
Name string
CacheTTL time.Duration
RoutesCacheSize int
API string `ini:"api"`
RejectCommunities api.Communities
}
// APIURL creates an url from the config
func (cfg *Config) APIURL(path string, params ...interface{}) string {
u := cfg.API
if strings.HasSuffix(u, "/") {
u = u[:len(u)-1]
}
u += fmt.Sprintf(path, params...)
return u
}

View File

@ -0,0 +1,16 @@
package openbgpd
import (
"testing"
)
func TestConfigAPIURL(t *testing.T) {
cfg := &Config{
API: "http://a",
}
url := cfg.APIURL("/%d/bgpd", 42)
if url != "http://a/42/bgpd" {
t.Error("unexpected url:", url)
}
}

View File

@ -0,0 +1,239 @@
package openbgpd
import (
"fmt"
"strings"
"time"
"github.com/alice-lg/alice-lg/pkg/api"
"github.com/alice-lg/alice-lg/pkg/decoders"
)
// Decode the api status response from the openbgpd
// state server.
func decodeAPIStatus(res map[string]interface{}) api.Status {
now := time.Now().UTC()
uptime := decoders.Duration(res["server_uptime"], 0)
// This is an approximation and maybe wrong
lastReboot := now.Add(-uptime)
s := api.Status{
ServerTime: decoders.TimeUTC(res["server_time_utc"], time.Time{}),
LastReboot: lastReboot,
LastReconfig: time.Time{},
Message: "openbgpd up and running",
Version: "",
Backend: "openbgpd",
}
return s
}
// decodeNeighbor decodes a single neighbor in a
// bgpctl response.
func decodeNeighbor(n interface{}) (*api.Neighbour, error) {
nb, ok := n.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("decode neighbor failed, interface is not a map")
}
stats := decoders.MapGet(nb, "stats", map[string]interface{}{})
prefixes := decoders.MapGet(stats, "prefixes", map[string]interface{}{})
neighbor := &api.Neighbour{
Id: decoders.MapGetString(nb, "remote_addr", "invalid_id"),
Address: decoders.MapGetString(nb, "remote_addr", "invalid_address"),
Asn: decoders.IntFromString(decoders.MapGetString(nb, "remote_as", ""), -1),
State: decodeState(decoders.MapGetString(nb, "state", "unknown")),
Description: describeNeighbor(nb),
RoutesReceived: int(decoders.MapGet(prefixes, "received", -1).(float64)),
// TODO: RoutesFiltered
RoutesExported: int(decoders.MapGet(prefixes, "sent", -1).(float64)),
// TODO: RoutesPreferred
// TODO: RoutesAccepted
Uptime: decoders.DurationTimeframe(decoders.MapGet(nb, "last_updown", ""), 0),
}
return neighbor, nil
}
// describeNeighbor creates a neighbor description
func describeNeighbor(nb interface{}) string {
addr := decoders.MapGetString(nb, "remote_addr", "invalid_address")
asn := decoders.MapGetString(nb, "remote_as", "")
return fmt.Sprintf("PEER AS%s %s", asn, addr)
}
// decodeNeighbors retrievs neighbors data from
// the bgpctl response.
func decodeNeighbors(res map[string]interface{}) (api.Neighbours, error) {
nbs := decoders.MapGet(res, "neighbors", nil)
if nbs == nil {
return nil, fmt.Errorf("missing neighbors in response body")
}
neighbors, ok := nbs.([]interface{})
if !ok {
return nil, fmt.Errorf("no a list of neighbors")
}
all := make(api.Neighbours, 0, len(neighbors))
for _, nb := range neighbors {
n, err := decodeNeighbor(nb)
if err != nil {
return nil, err
}
all = append(all, n)
}
return all, nil
}
// decodeNeighborsStatus retrievs a neighbors summary
// and decodes the status.
func decodeNeighborsStatus(res map[string]interface{}) (api.NeighboursStatus, error) {
nbs := decoders.MapGet(res, "neighbors", nil)
if nbs == nil {
return nil, fmt.Errorf("missing neighbors in response body")
}
neighbors, ok := nbs.([]interface{})
if !ok {
return nil, fmt.Errorf("no a list of interfaces")
}
all := make(api.NeighboursStatus, 0, len(neighbors))
for _, nb := range neighbors {
status := decodeNeighborStatus(nb)
all = append(all, status)
}
return all, nil
}
// decodeNeighborStatus decodes a single status from a
// list of neighbor summaries.
func decodeNeighborStatus(nb interface{}) *api.NeighbourStatus {
id := decoders.MapGetString(nb, "bgpid", "undefined")
state := decodeState(decoders.MapGetString(nb, "state", "Down"))
uptime := decoders.DurationTimeframe(decoders.MapGet(nb, "last_updown", ""), 0)
return &api.NeighbourStatus{
Id: id,
State: state,
Since: uptime,
}
}
// decodeRoutes decodes a response with a rib query.
// The toplevel element is expected to be "rib".
func decodeRoutes(res interface{}) (api.Routes, error) {
r := decoders.MapGet(res, "rib", nil)
if r == nil {
// The response was a valid json but empty. So no
// routes are present.
return api.Routes{}, nil
}
rib, ok := r.([]interface{})
if !ok {
return nil, fmt.Errorf("not a list of interfaces")
}
routes := make(api.Routes, 0, len(rib))
for _, details := range rib {
route, err := decodeRoute(details.(map[string]interface{}))
if err != nil {
return nil, err
}
routes = append(routes, route)
}
return routes, nil
}
// decodeRoute decodes a single route received from the source
func decodeRoute(details map[string]interface{}) (*api.Route, error) {
prefix := decoders.MapGetString(details, "prefix", "")
origin := decoders.MapGetString(details, "origin", "")
neighbor := decoders.MapGet(details, "neighbor", nil)
neighborID := "unknown"
if neighbor != nil {
neighborID = decoders.MapGetString(neighbor, "remote_addr", neighborID)
}
trueNextHop := decoders.MapGetString(details, "true_nexthop", "")
lastUpdate := decoders.DurationTimeframe(
decoders.MapGet(details, "last_update", nil), 0)
asPath := decodeASPath(decoders.MapGetString(details, "aspath", ""))
localPref := int(decoders.MapGet(details, "localpref", 0).(float64))
// Decode BGP communities
communities := decodeCommunities(
decoders.MapGet(details, "communities", nil))
largeCommunities := decodeCommunities(
decoders.MapGet(details, "large_communities", nil))
extendedCommunities := decodeExtendedCommunities(
decoders.MapGet(details, "extended_communities", nil))
// Is preferred route
isPrimary := decoders.MapGetBool(details, "best", false)
// Make bgp info
bgpInfo := api.BgpInfo{
Origin: origin,
AsPath: asPath,
NextHop: trueNextHop,
Communities: communities,
ExtCommunities: extendedCommunities,
LargeCommunities: largeCommunities,
LocalPref: localPref,
}
r := &api.Route{
Id: prefix,
NeighbourId: neighborID,
Network: prefix,
Gateway: trueNextHop,
Bgp: bgpInfo,
Age: lastUpdate,
Type: []string{origin},
Primary: isPrimary,
Details: api.Details(details),
}
return r, nil
}
// decodeState will decode the state into a canonical form
// used by the looking glass.
func decodeState(s string) string {
s = strings.ToLower(s) // todo elaborate
return s
}
// decodeASPath decodes a space separated list of
// string encoded ASNs into a list of integers.
func decodeASPath(path string) []int {
tokens := strings.Split(path, " ")
return decoders.IntListFromStrings(tokens)
}
// decodeCommunities decodes communities into a list of
// list of ints.
func decodeCommunities(c interface{}) api.Communities {
details := decoders.StringList(c)
comms := make(api.Communities, 0, len(details))
for _, com := range details {
tokens := strings.Split(com, ":")
comms = append(comms, decoders.IntListFromStrings(tokens))
}
return comms
}
// decodeExtendedCommunities decodes extended communties
// into a list of (str, int, int).
func decodeExtendedCommunities(c interface{}) api.ExtCommunities {
details := decoders.StringList(c)
comms := make(api.ExtCommunities, 0, len(details))
for _, com := range details {
tokens := strings.SplitN(com, " ", 2)
if len(tokens) != 2 {
continue
}
nums := decoders.IntListFromStrings(
strings.SplitN(tokens[1], ":", 2))
comms = append(comms, []interface{}{tokens[0], nums[0], nums[1]})
}
return comms
}

View File

@ -0,0 +1,81 @@
package openbgpd
import (
"encoding/json"
"io/ioutil"
"path/filepath"
"testing"
)
func readTestData(filename string) map[string]interface{} {
data, _ := ioutil.ReadFile(filepath.Join("testdata", filename))
payload := make(map[string]interface{})
json.Unmarshal(data, &payload)
return payload
}
func TestDecodeAPIStatus(t *testing.T) {
res := readTestData("status.json")
s := decodeAPIStatus(res)
t.Log(s.ServerTime)
t.Log(s.LastReboot)
}
func TestDecodeNeighbors(t *testing.T) {
res := readTestData("show.neighbor.json")
n, err := decodeNeighbors(res)
if err != nil {
t.Fatal(err)
}
t.Log(n[0])
}
func TestDecodeNeighborsStatus(t *testing.T) {
res := readTestData("show.summary.json")
n, err := decodeNeighborsStatus(res)
if err != nil {
t.Fatal(err)
}
if len(n) != 3 {
t.Error("unexpected length:", len(n))
}
t.Log(*n[0])
}
func TestDecodeRoutes(t *testing.T) {
res := readTestData("rib.json")
routes, err := decodeRoutes(res)
if err != nil {
t.Fatal(err)
}
if len(routes) != 2 {
t.Error("unexpected length:", len(routes))
}
// Check first route
r := routes[0]
if r.Network != "23.42.1.0/24" {
t.Error("unexpected network:", r.Network)
}
// Community decoding
if r.Bgp.Communities[0][0] != 20119 {
t.Error("unexpected community:", r.Bgp.Communities[0])
}
if r.Bgp.Communities[0][1] != 3 {
t.Error("unexpected community:", r.Bgp.Communities[0])
}
if r.Bgp.ExtCommunities[1][0] != "rt" {
t.Error("unexpected community:", r.Bgp.ExtCommunities[0])
}
if r.Bgp.ExtCommunities[1][1] != 65000 {
t.Error("unexpected community:", r.Bgp.ExtCommunities[0])
}
if r.Bgp.ExtCommunities[1][2] != 11000 {
t.Error("unexpected community:", r.Bgp.ExtCommunities[0])
}
if r.Bgp.AsPath[0] != 1111 {
t.Error("unexpected as_path:", r.Bgp.AsPath)
}
t.Log(r.Age)
}

View File

@ -0,0 +1,45 @@
package openbgpd
import (
"github.com/alice-lg/alice-lg/pkg/api"
)
func filterReceivedRoutes(
rejectCommunities api.Communities,
routes api.Routes,
) api.Routes {
filtered := make(api.Routes, 0, len(routes))
for _, r := range routes {
received := true
for _, c := range rejectCommunities {
if r.Bgp.HasLargeCommunity(c) {
received = false
break
}
}
if received {
filtered = append(filtered, r)
}
}
return filtered
}
func filterRejectedRoutes(
rejectCommunities api.Communities,
routes api.Routes,
) api.Routes {
filtered := make(api.Routes, 0, len(routes))
for _, r := range routes {
rejected := false
for _, c := range rejectCommunities {
if r.Bgp.HasLargeCommunity(c) {
rejected = true
break
}
}
if rejected {
filtered = append(filtered, r)
}
}
return filtered
}

View File

@ -0,0 +1,95 @@
package openbgpd
import (
"testing"
"github.com/alice-lg/alice-lg/pkg/api"
)
func TestFilterReceivedRoutes(t *testing.T) {
routes := api.Routes{
&api.Route{
Id: "1.2.3.4",
Bgp: api.BgpInfo{
LargeCommunities: api.Communities{
api.Community{9999, 23, 23},
api.Community{9999, 666, 1},
},
},
},
&api.Route{
Id: "5.6.6.6",
Bgp: api.BgpInfo{
LargeCommunities: api.Communities{
api.Community{9999, 23, 23},
api.Community{9999, 5, 42},
api.Community{9999, 666, 2},
},
},
},
&api.Route{
Id: "5.6.7.8",
Bgp: api.BgpInfo{
LargeCommunities: api.Communities{
api.Community{9999, 23, 23},
api.Community{9999, 5, 42},
},
},
},
}
c := api.Communities{
api.Community{9999, 666, 1},
api.Community{9999, 666, 2},
}
filtered := filterReceivedRoutes(c, routes)
if filtered[0].Id != "5.6.7.8" {
t.Error("unexpected route:", filtered[0])
}
}
func TestFilterRejectedRoutes(t *testing.T) {
routes := api.Routes{
&api.Route{
Id: "5.6.7.8",
Bgp: api.BgpInfo{
LargeCommunities: api.Communities{
api.Community{9999, 23, 23},
api.Community{9999, 5, 42},
},
},
},
&api.Route{
Id: "1.2.3.4",
Bgp: api.BgpInfo{
LargeCommunities: api.Communities{
api.Community{9999, 23, 23},
api.Community{9999, 666, 1},
},
},
},
&api.Route{
Id: "5.6.6.6",
Bgp: api.BgpInfo{
LargeCommunities: api.Communities{
api.Community{9999, 23, 23},
api.Community{9999, 5, 42},
api.Community{9999, 666, 2},
},
},
},
}
c := api.Communities{
api.Community{9999, 666, 1},
api.Community{9999, 666, 2},
}
filtered := filterRejectedRoutes(c, routes)
if len(filtered) != 2 {
t.Error("expected two filtered routes")
}
if filtered[0].Id != "1.2.3.4" {
t.Error("unexpected route:", filtered[0])
}
}

View File

@ -0,0 +1,403 @@
package openbgpd
import (
"context"
"net/http"
"time"
"github.com/alice-lg/alice-lg/pkg/api"
"github.com/alice-lg/alice-lg/pkg/caches"
"github.com/alice-lg/alice-lg/pkg/decoders"
)
const (
// StateServerSourceVersion is currently fixed at 1.0
StateServerSourceVersion = "1.0"
)
// StateServerSource implements the OpenBGPD source for Alice.
// It is intendet to consume structured bgpctl output
// queried over HTTP using the:
//
// openbgpd-state-server
// https://github.com/alice-lg/openbgpd-state-server
//
type StateServerSource struct {
// cfg is the source configuration retrieved
// from the alice config file.
cfg *Config
// Store the neighbor responses from the server here
neighborsCache *caches.NeighborsCache
// Store the routes responses from the server
// here identified by neighborID
routesCache *caches.RoutesCache
routesReceivedCache *caches.RoutesCache
routesFilteredCache *caches.RoutesCache
}
// NewStateServerSource creates a new source instance with a
// configuration.
func NewStateServerSource(cfg *Config) *StateServerSource {
cacheDisabled := cfg.CacheTTL == 0
// Initialize caches
nc := caches.NewNeighborsCache(cacheDisabled)
rc := caches.NewRoutesCache(cacheDisabled, cfg.RoutesCacheSize)
rrc := caches.NewRoutesCache(cacheDisabled, cfg.RoutesCacheSize)
rfc := caches.NewRoutesCache(cacheDisabled, cfg.RoutesCacheSize)
return &StateServerSource{
cfg: cfg,
neighborsCache: nc,
routesCache: rc,
routesReceivedCache: rrc,
routesFilteredCache: rfc,
}
}
// ExpireCaches ... will flush the cache. Seriously this needs
// a renaming.
func (src *StateServerSource) ExpireCaches() int {
totalExpired := src.routesCache.Expire()
return totalExpired
}
// Requests
// ========
// StatusRequest makes status request from source
func (src *StateServerSource) StatusRequest(ctx context.Context) (*http.Request, error) {
url := src.cfg.APIURL("/v1/status")
return http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
}
// ShowNeighborsRequest makes an all neighbors request
func (src *StateServerSource) ShowNeighborsRequest(ctx context.Context) (*http.Request, error) {
url := src.cfg.APIURL("/v1/bgpd/show/neighbor")
return http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
}
// ShowNeighborsSummaryRequest builds an neighbors status request
func (src *StateServerSource) ShowNeighborsSummaryRequest(
ctx context.Context,
) (*http.Request, error) {
url := src.cfg.APIURL("/v1/bgpd/show/summary")
return http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
}
// ShowNeighborRIBRequest retrives the routes accepted from the neighbor
// identified by bgp-id.
func (src *StateServerSource) ShowNeighborRIBRequest(
ctx context.Context,
neighborID string,
) (*http.Request, error) {
url := src.cfg.APIURL("/v1/bgpd/show/rib/neighbor/%s/detail", neighborID)
return http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
}
// ShowRIBRequest makes a request for retrieving all routes imported
// from all peers
func (src *StateServerSource) ShowRIBRequest(ctx context.Context) (*http.Request, error) {
url := src.cfg.APIURL("/v1/bgpd/show/rib/detail")
return http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
}
// Datasource
// ==========
// makeCacheStatus will create a new api status with cache infos
func (src *StateServerSource) makeCacheStatus() api.ApiStatus {
return api.ApiStatus{
CacheStatus: api.CacheStatus{
CachedAt: time.Now().UTC(),
},
Version: StateServerSourceVersion,
ResultFromCache: false,
Ttl: time.Now().UTC().Add(src.cfg.CacheTTL),
}
}
// Status returns an API status response. In our case
// this is pretty much only that the service is available.
func (src *StateServerSource) Status() (*api.StatusResponse, error) {
// Make API request and read response. We do not cache the result.
req, err := src.StatusRequest(context.Background())
if err != nil {
return nil, err
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
body, err := decoders.ReadJSONResponse(res)
if err != nil {
return nil, err
}
status := decodeAPIStatus(body)
response := &api.StatusResponse{
Api: src.makeCacheStatus(),
Status: status,
}
return response, nil
}
// Neighbours retrievs a full list of all neighbors
func (src *StateServerSource) Neighbours() (*api.NeighboursResponse, error) {
// Query cache and see if we have a hit
response := src.neighborsCache.Get()
if response != nil {
response.Api.ResultFromCache = true
return response, nil
}
// Make API request and read response
req, err := src.ShowNeighborsRequest(context.Background())
if err != nil {
return nil, err
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
body, err := decoders.ReadJSONResponse(res)
if err != nil {
return nil, err
}
nb, err := decodeNeighbors(body)
if err != nil {
return nil, err
}
// Set route server id (sourceID) for all neighbors
for _, n := range nb {
n.RouteServerId = src.cfg.ID
rejectedRes, err := src.RoutesFiltered(n.Id)
if err != nil {
return nil, err
}
rejectCount := len(rejectedRes.Filtered)
n.RoutesFiltered = rejectCount
}
response = &api.NeighboursResponse{
Api: src.makeCacheStatus(),
Neighbours: nb,
}
src.neighborsCache.Set(response)
return response, nil
}
// NeighboursStatus retrives the status summary
// for all neightbors
func (src *StateServerSource) NeighboursStatus() (*api.NeighboursStatusResponse, error) {
// Make API request and read response
req, err := src.ShowNeighborsSummaryRequest(context.Background())
if err != nil {
return nil, err
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
// Read and decode response
body, err := decoders.ReadJSONResponse(res)
if err != nil {
return nil, err
}
nb, err := decodeNeighborsStatus(body)
if err != nil {
return nil, err
}
response := &api.NeighboursStatusResponse{
Api: src.makeCacheStatus(),
Neighbours: nb,
}
return response, nil
}
// Routes retrieves the routes for a specific neighbor
// identified by ID.
func (src *StateServerSource) Routes(neighborID string) (*api.RoutesResponse, error) {
response := src.routesCache.Get(neighborID)
if response != nil {
response.Api.ResultFromCache = true
return response, nil
}
// Query RIB for routes received
req, err := src.ShowNeighborRIBRequest(context.Background(), neighborID)
if err != nil {
return nil, err
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
// Read and decode response
body, err := decoders.ReadJSONResponse(res)
if err != nil {
return nil, err
}
routes, err := decodeRoutes(body)
if err != nil {
return nil, err
}
// Filtered routes are marked with a large BGP community
// as defined in the reject reasons.
received := filterReceivedRoutes(src.cfg.RejectCommunities, routes)
rejected := filterRejectedRoutes(src.cfg.RejectCommunities, routes)
response = &api.RoutesResponse{
Api: src.makeCacheStatus(),
Imported: received,
NotExported: api.Routes{},
Filtered: rejected,
}
src.routesCache.Set(neighborID, response)
return response, nil
}
// RoutesReceived returns the routes exported by the neighbor.
func (src *StateServerSource) RoutesReceived(neighborID string) (*api.RoutesResponse, error) {
response := src.routesReceivedCache.Get(neighborID)
if response != nil {
response.Api.ResultFromCache = true
return response, nil
}
// Query RIB for routes received
req, err := src.ShowNeighborRIBRequest(context.Background(), neighborID)
if err != nil {
return nil, err
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
// Read and decode response
body, err := decoders.ReadJSONResponse(res)
if err != nil {
return nil, err
}
routes, err := decodeRoutes(body)
if err != nil {
return nil, err
}
received := filterReceivedRoutes(src.cfg.RejectCommunities, routes)
response = &api.RoutesResponse{
Api: src.makeCacheStatus(),
Imported: received,
NotExported: api.Routes{},
Filtered: api.Routes{},
}
src.routesReceivedCache.Set(neighborID, response)
return response, nil
}
// RoutesFiltered retrieves the routes filtered / not valid
func (src *StateServerSource) RoutesFiltered(neighborID string) (*api.RoutesResponse, error) {
response := src.routesFilteredCache.Get(neighborID)
if response != nil {
response.Api.ResultFromCache = true
return response, nil
}
// Query RIB for routes received
req, err := src.ShowNeighborRIBRequest(context.Background(), neighborID)
if err != nil {
return nil, err
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
// Read and decode response
body, err := decoders.ReadJSONResponse(res)
if err != nil {
return nil, err
}
routes, err := decodeRoutes(body)
if err != nil {
return nil, err
}
rejected := filterRejectedRoutes(src.cfg.RejectCommunities, routes)
response = &api.RoutesResponse{
Api: src.makeCacheStatus(),
Imported: api.Routes{},
NotExported: api.Routes{},
Filtered: rejected,
}
src.routesFilteredCache.Set(neighborID, response)
return response, nil
}
// RoutesNotExported retrievs the routes not exported
// from the rs for a neighbor.
func (src *StateServerSource) RoutesNotExported(neighborID string) (*api.RoutesResponse, error) {
response := &api.RoutesResponse{
Api: src.makeCacheStatus(),
Imported: api.Routes{},
NotExported: api.Routes{},
Filtered: api.Routes{},
}
return response, nil
}
// AllRoutes retrievs the entire RIB from the source. This is never
// cached as it is processed by the store.
func (src *StateServerSource) AllRoutes() (*api.RoutesResponse, error) {
req, err := src.ShowRIBRequest(context.Background())
if err != nil {
return nil, err
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
// Read and decode response
body, err := decoders.ReadJSONResponse(res)
if err != nil {
return nil, err
}
routes, err := decodeRoutes(body)
if err != nil {
return nil, err
}
// Filtered routes are marked with a large BGP community
// as defined in the reject reasons.
received := filterReceivedRoutes(src.cfg.RejectCommunities, routes)
rejected := filterRejectedRoutes(src.cfg.RejectCommunities, routes)
response := &api.RoutesResponse{
Api: src.makeCacheStatus(),
Imported: received,
NotExported: api.Routes{},
Filtered: rejected,
}
return response, nil
}

55
pkg/sources/openbgpd/testdata/rib.json vendored Normal file
View File

@ -0,0 +1,55 @@
{
"rib": [
{
"prefix": "23.42.1.0/24",
"aspath": "1111",
"exit_nexthop": "200.100.25.7",
"true_nexthop": "200.100.25.7",
"neighbor": {
"remote_addr": "200.100.25.7",
"bgp_id": "200.100.25.7"
},
"valid": true,
"source": "external",
"ovs": "not-found",
"origin": "IGP",
"metric": 50,
"localpref": 100,
"weight": 0,
"last_update": "05w3d19h",
"communities": [
"20119:3"
],
"large_communities": [
"54987:1:1"
],
"extended_communities": [
"[0] 11000:0",
"rt 65000:11000"
]
},
{
"prefix": "fc29:bac0:167::/48",
"aspath": "1111",
"exit_nexthop": "2212:100:f::1:1",
"true_nexthop": "2212:100:f::1:1",
"neighbor": {
"remote_addr": "2212:100:f::1:1",
"bgp_id": "172.11.111.1"
},
"valid": true,
"source": "external",
"ovs": "valid",
"origin": "IGP",
"metric": 0,
"localpref": 100,
"weight": 0,
"last_update": "05w3d19h",
"communities": [
"13335:10167",
"13335:19000",
"13335:31000"
]
}
]
}

View File

@ -0,0 +1,224 @@
{
"neighbors": [
{
"remote_as": "123",
"group": "clients",
"remote_addr": "200.100.25.7",
"bgpid": "206.100.25.7",
"state": "Established",
"last_updown": "01w0d16h",
"config": {
"template": false,
"cloned": false,
"passive": true,
"down": false,
"multihop": false,
"max_prefix": 577,
"max_prefix_restart": 15,
"ttl_security": false,
"holdtime": 0,
"min_holdtime": 0,
"announce_capabilities": true,
"capabilities": {
"as4byte": true,
"refresh": true,
"multiprotocol": [
"IPv4 unicast"
],
"graceful_restart": {
"eor": true,
"restart": false
}
}
},
"stats": {
"last_read": "00:00:08",
"last_write": "00:00:08",
"prefixes": {
"sent": 864,
"received": 1
},
"message": {
"sent": {
"open": 1,
"notifications": 0,
"updates": 460,
"keepalives": 22152,
"route_refresh": 0,
"total": 22613
},
"received": {
"open": 1,
"notifications": 0,
"updates": 2,
"keepalives": 22158,
"route_refresh": 0,
"total": 22161
}
},
"update": {
"sent": {
"updates": 897,
"withdraws": 7,
"eor": 0
},
"received": {
"updates": 1,
"withdraws": 0,
"eor": 1
}
}
},
"session": {
"holdtime": 90,
"keepalive": 30,
"local": {
"address": "206.126.225.254",
"port": 179,
"capabilities": {
"as4byte": true,
"refresh": true,
"multiprotocol": [
"IPv4 unicast"
],
"graceful_restart": {
"eor": true,
"restart": false
}
}
},
"remote": {
"address": "206.126.225.7",
"port": 37237,
"capabilities": {
"as4byte": true,
"refresh": false,
"multiprotocol": [
"IPv4 unicast",
"IPv6 unicast",
"IPv4 vpn",
"IPv6 vpn"
]
}
},
"capabilities": {
"as4byte": true,
"refresh": false,
"multiprotocol": [
"IPv4 unicast"
]
}
}
},
{
"remote_as": "12346",
"group": "clients",
"remote_addr": "2000:1:2::3",
"bgpid": "1.2.3.4",
"state": "Established",
"last_updown": "01w0d16h",
"config": {
"template": false,
"cloned": false,
"passive": true,
"down": false,
"multihop": false,
"max_prefix": 747,
"max_prefix_restart": 15,
"ttl_security": false,
"holdtime": 0,
"min_holdtime": 0,
"announce_capabilities": true,
"capabilities": {
"as4byte": true,
"refresh": true,
"multiprotocol": [
"IPv6 unicast"
],
"graceful_restart": {
"eor": true,
"restart": false
}
}
},
"stats": {
"last_read": "00:00:08",
"last_write": "00:00:07",
"prefixes": {
"sent": 127,
"received": 0
},
"message": {
"sent": {
"open": 1,
"notifications": 0,
"updates": 133,
"keepalives": 22155,
"route_refresh": 0,
"total": 22289
},
"received": {
"open": 1,
"notifications": 0,
"updates": 1,
"keepalives": 22158,
"route_refresh": 0,
"total": 22160
}
},
"update": {
"sent": {
"updates": 131,
"withdraws": 2,
"eor": 0
},
"received": {
"updates": 0,
"withdraws": 0,
"eor": 1
}
}
},
"session": {
"holdtime": 90,
"keepalive": 30,
"local": {
"address": "fd42:1:2::3:4:5",
"port": 179,
"capabilities": {
"as4byte": true,
"refresh": true,
"multiprotocol": [
"IPv6 unicast"
],
"graceful_restart": {
"eor": true,
"restart": false
}
}
},
"remote": {
"address": "fd42:1:2::3:4:6",
"port": 43025,
"capabilities": {
"as4byte": true,
"refresh": false,
"multiprotocol": [
"IPv4 unicast",
"IPv6 unicast",
"IPv4 vpn",
"IPv6 vpn"
]
}
},
"capabilities": {
"as4byte": true,
"refresh": false,
"multiprotocol": [
"IPv6 unicast"
]
}
}
}
]
}

View File

@ -0,0 +1,28 @@
{
"neighbors": [
{
"remote_as": "12345",
"group": "clients",
"remote_addr": "200.100.50.25",
"bgpid": "200.100.50.1",
"state": "Established",
"last_updown": "5d20h05m"
},
{
"remote_as": "12345",
"group": "clients",
"remote_addr": "fd23:42:1::23:1",
"bgpid": "100.200.50.25",
"state": "Active",
"last_updown": "01w2d15h"
},
{
"remote_as": "62993",
"group": "clients",
"remote_addr": "200.100.50.25",
"bgpid": "200.100.50.1",
"state": "None",
"last_updown": "10:11:12"
}
]
}

View File

@ -0,0 +1 @@
{"service":"openbgpd-state-server","version":"HEAD","build":"df4a695","server_time_utc":"2021-03-23T06:15:44.563675318Z","server_uptime":20574589140}