You've already forked bird-lg-go
mirror of
https://github.com/xddxdd/bird-lg-go
synced 2025-11-11 21:57:34 +01:00
Compare commits
70 Commits
dependabot
...
v1.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cdc34704b5 | ||
|
|
db58bd3354 | ||
|
|
a0246ccee2 | ||
|
|
ccd14af0c8 | ||
|
|
594ca80f50 | ||
|
|
5625058e71 | ||
|
|
7efa3237a9 | ||
|
|
7b0c8c0556 | ||
|
|
ffd9165062 | ||
|
|
24fd5203e8 | ||
|
|
49a05767c1 | ||
|
|
e7010f75f8 | ||
|
|
dba2af7634 | ||
|
|
049775319b | ||
|
|
47c66b125c | ||
|
|
9e17b116f1 | ||
|
|
335ad40634 | ||
|
|
6ec0f2e7a6 | ||
|
|
4b73cf0fcb | ||
|
|
3b1d001543 | ||
|
|
675cb26ed1 | ||
|
|
556d3e50d3 | ||
|
|
06796f546e | ||
|
|
d029d6684c | ||
|
|
5ce0f55f35 | ||
|
|
890ab51b07 | ||
|
|
8e4a35cc8c | ||
|
|
97f3c6088f | ||
|
|
982326a678 | ||
|
|
4b3980f6bd | ||
|
|
6f6b2bd283 | ||
|
|
892a7bee22 | ||
|
|
348295b9aa | ||
|
|
950c018b18 | ||
|
|
26efeb4996 | ||
|
|
5a5dfbc93f | ||
|
|
f60a292129 | ||
|
|
e7f6026854 | ||
|
|
a4e0f4c193 | ||
|
|
af5b653326 | ||
|
|
58847759b3 | ||
|
|
6481e7cc8d | ||
|
|
2166d73b3d | ||
|
|
a64d839e2c | ||
|
|
1a3c618522 | ||
|
|
fbd190628c | ||
|
|
823b639245 | ||
|
|
b0c0e5442d | ||
|
|
4e4ce89418 | ||
|
|
234aadadd9 | ||
|
|
bee26f421c | ||
|
|
2e0cb131ca | ||
|
|
4c248c638a | ||
|
|
3550362a4d | ||
|
|
256a80646f | ||
|
|
03c42eb1e8 | ||
|
|
aea85e774c | ||
|
|
80d9351a58 | ||
|
|
5e0bc081e6 | ||
|
|
4d53d1f095 | ||
|
|
5883015294 | ||
|
|
80e66a7a81 | ||
|
|
41329da7cb | ||
|
|
8e56705205 | ||
|
|
6a8b3a0e55 | ||
|
|
83ab403706 | ||
|
|
7c7814cc7b | ||
|
|
8598060cc0 | ||
|
|
bda06ddd5e | ||
|
|
f404072ab8 |
@@ -4,16 +4,19 @@ workflows:
|
||||
docker:
|
||||
jobs:
|
||||
- build
|
||||
- deploy:
|
||||
- docker-frontend-deploy:
|
||||
context:
|
||||
- docker
|
||||
requires:
|
||||
- build
|
||||
filters:
|
||||
branches:
|
||||
only: master
|
||||
- docker-proxy-deploy:
|
||||
context:
|
||||
- docker
|
||||
requires:
|
||||
- build
|
||||
matrix:
|
||||
parameters:
|
||||
program: [frontend, proxy]
|
||||
# latest is amd64 arch + push to default latest tag
|
||||
image_arch: [latest, i386, arm32v7, arm64v8, ppc64le, s390x]
|
||||
filters:
|
||||
branches:
|
||||
only: master
|
||||
@@ -21,41 +24,104 @@ workflows:
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: circleci/golang:1.15
|
||||
working_directory: /go/src/github.com/xddxdd/bird-lg-go
|
||||
- image: cimg/go:1.17
|
||||
working_directory: /home/circleci/go/src/github.com/xddxdd/bird-lg-go
|
||||
steps:
|
||||
- checkout
|
||||
- run: go get -v -t -d ./...
|
||||
- run: go get -u github.com/kevinburke/go-bindata/...
|
||||
- run: cd frontend && go generate
|
||||
- run: go test -v ./...
|
||||
deploy:
|
||||
docker:
|
||||
- image: circleci/golang:1.15
|
||||
working_directory: /go/src/github.com/xddxdd/bird-lg-go
|
||||
parameters:
|
||||
image_arch:
|
||||
type: string
|
||||
program:
|
||||
type: string
|
||||
steps:
|
||||
- checkout
|
||||
- setup_remote_docker:
|
||||
version: 19.03.13
|
||||
- run:
|
||||
name: Install GPP
|
||||
name: Test frontend
|
||||
command: |
|
||||
sudo apt-get update && sudo apt-get install -y gpp
|
||||
export GO111MODULE=on
|
||||
cd frontend
|
||||
go get -v -t -d ./...
|
||||
go test -v ./...
|
||||
- run:
|
||||
name: Test proxy
|
||||
command: |
|
||||
export GO111MODULE=on
|
||||
cd proxy
|
||||
go get -v -t -d ./...
|
||||
go test -v ./...
|
||||
|
||||
docker-frontend-deploy:
|
||||
machine:
|
||||
image: ubuntu-2004:202111-02
|
||||
environment:
|
||||
BUILDX_PLATFORMS: linux/amd64,linux/arm64,linux/386,linux/arm/v7
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Install buildx
|
||||
command: |
|
||||
BUILDX_BINARY_URL="https://github.com/docker/buildx/releases/download/v0.7.1/buildx-v0.7.1.linux-amd64"
|
||||
|
||||
curl --output docker-buildx \
|
||||
--silent --show-error --location --fail --retry 3 \
|
||||
"$BUILDX_BINARY_URL"
|
||||
|
||||
mkdir -p ~/.docker/cli-plugins
|
||||
|
||||
mv docker-buildx ~/.docker/cli-plugins/
|
||||
chmod a+x ~/.docker/cli-plugins/docker-buildx
|
||||
|
||||
docker buildx install
|
||||
# Run binfmt
|
||||
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
|
||||
- run:
|
||||
name: Build Docker image
|
||||
environment:
|
||||
IMAGE_ARCH: << parameters.image_arch >>
|
||||
PROGRAM: << parameters.program >>
|
||||
BUILD_ID: << pipeline.number >>
|
||||
command: |
|
||||
make -f Makefile.docker _crossbuild
|
||||
echo $DOCKER_PASSWORD | docker login -u $DOCKER_USERNAME --password-stdin
|
||||
make -f Makefile.docker \
|
||||
DOCKER_USERNAME=$DOCKER_USERNAME \
|
||||
BUILD_ID=circleci-build$BUILD_ID \
|
||||
$PROGRAM/$IMAGE_ARCH
|
||||
docker buildx create --name mybuilder --use
|
||||
docker buildx build \
|
||||
--platform $BUILDX_PLATFORMS \
|
||||
-t $DOCKER_USERNAME/bird-lg-go:circleci-build$BUILD_ID \
|
||||
--progress plain \
|
||||
--push frontend
|
||||
docker buildx build \
|
||||
--platform $BUILDX_PLATFORMS \
|
||||
-t $DOCKER_USERNAME/bird-lg-go:latest \
|
||||
--progress plain \
|
||||
--push frontend
|
||||
|
||||
docker-proxy-deploy:
|
||||
machine:
|
||||
image: ubuntu-2004:202111-02
|
||||
environment:
|
||||
BUILDX_PLATFORMS: linux/amd64,linux/arm64,linux/386,linux/arm/v7
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Install buildx
|
||||
command: |
|
||||
BUILDX_BINARY_URL="https://github.com/docker/buildx/releases/download/v0.7.1/buildx-v0.7.1.linux-amd64"
|
||||
|
||||
curl --output docker-buildx \
|
||||
--silent --show-error --location --fail --retry 3 \
|
||||
"$BUILDX_BINARY_URL"
|
||||
|
||||
mkdir -p ~/.docker/cli-plugins
|
||||
|
||||
mv docker-buildx ~/.docker/cli-plugins/
|
||||
chmod a+x ~/.docker/cli-plugins/docker-buildx
|
||||
|
||||
docker buildx install
|
||||
# Run binfmt
|
||||
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
|
||||
- run:
|
||||
name: Build Docker image
|
||||
environment:
|
||||
BUILD_ID: << pipeline.number >>
|
||||
command: |
|
||||
echo $DOCKER_PASSWORD | docker login -u $DOCKER_USERNAME --password-stdin
|
||||
docker buildx create --name mybuilder --use
|
||||
docker buildx build \
|
||||
--platform $BUILDX_PLATFORMS \
|
||||
-t $DOCKER_USERNAME/bird-lgproxy-go:circleci-build$BUILD_ID \
|
||||
--push proxy
|
||||
docker buildx build \
|
||||
--platform $BUILDX_PLATFORMS \
|
||||
-t $DOCKER_USERNAME/bird-lgproxy-go:latest \
|
||||
--progress plain \
|
||||
--push proxy
|
||||
|
||||
35
.github/workflows/release.yaml
vendored
Normal file
35
.github/workflows/release.yaml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
releases-matrix:
|
||||
name: Release Go Binary
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
goos: [linux, windows, darwin]
|
||||
goarch: ["386", amd64, "arm", arm64]
|
||||
exclude:
|
||||
- goarch: "386"
|
||||
goos: darwin
|
||||
- goarch: "arm"
|
||||
goos: darwin
|
||||
- goarch: "arm"
|
||||
goos: windows
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: wangyoucao577/go-release-action@v1.34
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
goos: ${{ matrix.goos }}
|
||||
goarch: ${{ matrix.goarch }}
|
||||
project_path: "./frontend"
|
||||
binary_name: "bird-lg-go"
|
||||
- uses: wangyoucao577/go-release-action@v1.34
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
goos: ${{ matrix.goos }}
|
||||
goarch: ${{ matrix.goarch }}
|
||||
project_path: "./proxy"
|
||||
binary_name: "bird-lgproxy-go"
|
||||
4
Makefile
4
Makefile
@@ -9,5 +9,5 @@ proxy:
|
||||
all: frontend proxy
|
||||
|
||||
install:
|
||||
install -m 755 frontend/frontend /usr/local/bin/frontend
|
||||
install -m 755 proxy/proxy /usr/local/bin/proxy
|
||||
install -m 755 frontend/frontend /usr/local/bin/bird-lg-go
|
||||
install -m 755 proxy/proxy /usr/local/bin/bird-lgproxy-go
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
# Basic definitions
|
||||
DOCKER_USERNAME := xddxdd
|
||||
ARCHITECTURES := amd64 i386 arm32v7 arm64v8 ppc64le s390x
|
||||
IMAGES := frontend proxy
|
||||
|
||||
# General Purpose Preprocessor config
|
||||
GPP_INCLUDE_DIR := include
|
||||
GPP_FLAGS_U := "" "" "(" "," ")" "(" ")" "\#" ""
|
||||
GPP_FLAGS_M := "\#" "\n" " " " " "\n" "(" ")"
|
||||
GPP_FLAGS_EXTRA := +c "\\\n" ""
|
||||
GPP_FLAGS := -I ${GPP_INCLUDE_DIR} --nostdinc -U ${GPP_FLAGS_U} -M ${GPP_FLAGS_M} ${GPP_FLAGS_EXTRA}
|
||||
|
||||
BUILD_ID ?= $(shell date +%Y%m%d%H%M)
|
||||
|
||||
define create-image-arch-target
|
||||
frontend/Dockerfile.$1: frontend/template.Dockerfile
|
||||
@gpp ${GPP_FLAGS} -D ARCH_$(shell echo $1 | tr a-z A-Z) -o frontend/Dockerfile.$1 frontend/template.Dockerfile || rm -rf frontend/Dockerfile.$1
|
||||
|
||||
frontend/$1: frontend/Dockerfile.$1
|
||||
@if [ -f frontend/Dockerfile.$1 ]; then \
|
||||
docker build --pull --no-cache -t ${DOCKER_USERNAME}/bird-lg-go:$1-${BUILD_ID} -f frontend/Dockerfile.$1 frontend || exit 1; \
|
||||
docker push ${DOCKER_USERNAME}/bird-lg-go:$1-${BUILD_ID} || exit 1; \
|
||||
docker tag ${DOCKER_USERNAME}/bird-lg-go:$1-${BUILD_ID} ${DOCKER_USERNAME}/bird-lg-go:$1 || exit 1; \
|
||||
docker push ${DOCKER_USERNAME}/bird-lg-go:$1 || exit 1; \
|
||||
else \
|
||||
echo "Dockerfile generation failed, see error above"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
proxy/Dockerfile.$1: proxy/template.Dockerfile
|
||||
@gpp ${GPP_FLAGS} -D ARCH_$(shell echo $1 | tr a-z A-Z) -o proxy/Dockerfile.$1 proxy/template.Dockerfile || rm -rf proxy/Dockerfile.$1
|
||||
|
||||
proxy/$1: proxy/Dockerfile.$1
|
||||
@if [ -f proxy/Dockerfile.$1 ]; then \
|
||||
docker build --pull --no-cache -t ${DOCKER_USERNAME}/bird-lgproxy-go:$1-${BUILD_ID} -f proxy/Dockerfile.$1 proxy || exit 1; \
|
||||
docker push ${DOCKER_USERNAME}/bird-lgproxy-go:$1-${BUILD_ID} || exit 1; \
|
||||
docker tag ${DOCKER_USERNAME}/bird-lgproxy-go:$1-${BUILD_ID} ${DOCKER_USERNAME}/bird-lgproxy-go:$1 || exit 1; \
|
||||
docker push ${DOCKER_USERNAME}/bird-lgproxy-go:$1 || exit 1; \
|
||||
else \
|
||||
echo "Dockerfile generation failed, see error above"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
endef
|
||||
|
||||
$(foreach arch,${ARCHITECTURES},$(eval $(call create-image-arch-target,$(arch))))
|
||||
|
||||
frontend:$(foreach arch,latest ${ARCHITECTURES},frontend/${arch})
|
||||
|
||||
frontend/latest: frontend/amd64
|
||||
@docker tag ${DOCKER_USERNAME}/bird-lg-go:amd64-${BUILD_ID} ${DOCKER_USERNAME}/bird-lg-go:${BUILD_ID} || exit 1
|
||||
@docker push ${DOCKER_USERNAME}/bird-lg-go:${BUILD_ID} || exit 1
|
||||
@docker tag ${DOCKER_USERNAME}/bird-lg-go:amd64-${BUILD_ID} ${DOCKER_USERNAME}/bird-lg-go:latest || exit 1
|
||||
@docker push ${DOCKER_USERNAME}/bird-lg-go:latest || exit 1
|
||||
|
||||
proxy:$(foreach arch,latest ${ARCHITECTURES},proxy/${arch})
|
||||
|
||||
proxy/latest: proxy/amd64
|
||||
@docker tag ${DOCKER_USERNAME}/bird-lgproxy-go:amd64-${BUILD_ID} ${DOCKER_USERNAME}/bird-lgproxy-go:${BUILD_ID} || exit 1
|
||||
@docker push ${DOCKER_USERNAME}/bird-lgproxy-go:${BUILD_ID} || exit 1
|
||||
@docker tag ${DOCKER_USERNAME}/bird-lgproxy-go:amd64-${BUILD_ID} ${DOCKER_USERNAME}/bird-lgproxy-go:latest || exit 1
|
||||
@docker push ${DOCKER_USERNAME}/bird-lgproxy-go:latest || exit 1
|
||||
|
||||
.DEFAULT_GOAL := images
|
||||
.DELETE_ON_ERROR:
|
||||
.SECONDARY:
|
||||
|
||||
# Target to enable multiarch support
|
||||
_crossbuild:
|
||||
@docker run --rm --privileged multiarch/qemu-user-static --reset -p yes >/dev/null
|
||||
|
||||
dockerfiles: $(foreach image,${IMAGES},$(foreach arch,${ARCHITECTURES},$(image)/Dockerfile.$(arch)))
|
||||
|
||||
images: $(foreach image,${IMAGES},$(image))
|
||||
|
||||
clean:
|
||||
@rm -rf */Dockerfile.{$(shell echo ${ARCHITECTURES} | sed "s/ /,/g")}
|
||||
167
README.md
167
README.md
@@ -6,43 +6,36 @@ An alternative implementation for [bird-lg](https://github.com/sileht/bird-lg) w
|
||||
|
||||
## Table of Contents
|
||||
|
||||
* [Bird-lg-go](#bird-lg-go)
|
||||
* [Table of Contents](#table-of-contents)
|
||||
* [Frontend](#frontend)
|
||||
* [Proxy](#proxy)
|
||||
* [Advanced Features](#advanced-features)
|
||||
* [API](#api)
|
||||
* [Telegram Bot Webhook](#telegram-bot-webhook)
|
||||
* [Example of setting the webhook](#example-of-setting-the-webhook)
|
||||
* [Supported commands](#supported-commands)
|
||||
* [Credits](#credits)
|
||||
* [License](#license)
|
||||
|
||||
Created by [gh-md-toc](https://github.com/ekalinin/github-markdown-toc)
|
||||
- [Bird-lg-go](#bird-lg-go)
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [Build Instructions](#build-instructions)
|
||||
- [Build Docker Images](#build-docker-images)
|
||||
- [Frontend](#frontend)
|
||||
- [Proxy](#proxy)
|
||||
- [Advanced Features](#advanced-features)
|
||||
- [Display names](#display-names)
|
||||
- [IP addresses](#ip-addresses)
|
||||
- [API](#api)
|
||||
- [Telegram Bot Webhook](#telegram-bot-webhook)
|
||||
- [Credits](#credits)
|
||||
- [License](#license)
|
||||
|
||||
## Build Instructions
|
||||
|
||||
Run `make` to build binaries for both the frontend and the proxy. You need to have Go installed on your machine.
|
||||
You need to have **Go 1.16 or newer** installed on your machine.
|
||||
|
||||
Optionally run `make install` to install them to `/usr/local/bin`.
|
||||
Run `make` to build binaries for both the frontend and the proxy.
|
||||
|
||||
Or, you can manually do the building steps:
|
||||
Optionally run `make install` to install them to `/usr/local/bin` (`bird-lg-go` and `bird-lgproxy-go`).
|
||||
|
||||
```bash
|
||||
# Build frontend binary
|
||||
cd frontend
|
||||
go get -u github.com/kevinburke/go-bindata/...
|
||||
go generate
|
||||
go build -ldflags "-w -s" -o frontend
|
||||
cd ..
|
||||
### Build Docker Images
|
||||
|
||||
# Build proxy binary
|
||||
cd proxy
|
||||
go build -ldflags "-w -s" -o proxy
|
||||
cd ..
|
||||
```
|
||||
Use the Dockerfiles in `frontend` and `proxy` directory.
|
||||
|
||||
- If you get `undefined: MustAssetString`, you need to uninstall an older version of go-bindata from your machine: see [#11](https://github.com/xddxdd/bird-lg-go/issues/11)
|
||||
Ready-to-use images are available at:
|
||||
|
||||
- Frontend: <https://hub.docker.com/r/xddxdd/bird-lg-go>
|
||||
- Proxy: <https://hub.docker.com/r/xddxdd/bird-lgproxy-go>
|
||||
|
||||
## Frontend
|
||||
|
||||
@@ -56,25 +49,45 @@ Features implemented:
|
||||
- Work with both Python proxy (lgproxy.py) and Go proxy (proxy dir of this project)
|
||||
- Visualize AS paths as picture (bgpmap feature)
|
||||
|
||||
Usage: all configuration is done via commandline parameters or environment variables, no config file.
|
||||
Configuration can be set in:
|
||||
|
||||
| Parameter | Environment Variable | Description |
|
||||
| --------- | -------------------- | ----------- |
|
||||
| --servers | BIRDLG_SERVERS | server name prefixes, separated by comma |
|
||||
| --domain | BIRDLG_DOMAIN | server name domain suffixes |
|
||||
| --listen | BIRDLG_LISTEN | address bird-lg is listening on (default ":5000") |
|
||||
| --proxy-port | BIRDLG_PROXY_PORT | port bird-lgproxy is running on (default 8000) |
|
||||
| --whois | BIRDLG_WHOIS | whois server for queries (default "whois.verisign-grs.com") |
|
||||
| --dns-interface | BIRDLG_DNS_INTERFACE | dns zone to query ASN information (default "asn.cymru.com") |
|
||||
| --title-brand | BIRDLG_TITLE_BRAND | prefix of page titles in browser tabs (default "Bird-lg Go") |
|
||||
| --navbar-brand | BIRDLG_NAVBAR_BRAND | brand to show in the navigation bar (default "Bird-lg Go") |
|
||||
- `bird-lg.[json/yaml/etc]` in current directory
|
||||
- `/etc/bird-lg/bird-lg.[json/yaml/etc]`
|
||||
- Commandline parameter
|
||||
- Environment variables
|
||||
|
||||
Configuration is handled by [viper](https://github.com/spf13/viper), any config format supported by it can be used.
|
||||
|
||||
| Config Key | Parameter | Environment Variable | Description |
|
||||
| ---------- | --------- | -------------------- | ----------- |
|
||||
| servers | --servers | BIRDLG_SERVERS | server name prefixes, separated by comma |
|
||||
| domain | --domain | BIRDLG_DOMAIN | server name domain suffixes |
|
||||
| listen | --listen | BIRDLG_LISTEN | address bird-lg is listening on (default "5000") |
|
||||
| proxy_port | --proxy-port | BIRDLG_PROXY_PORT | port bird-lgproxy is running on (default 8000) |
|
||||
| whois | --whois | BIRDLG_WHOIS | whois server for queries (default "whois.verisign-grs.com"). Start with "/" to spacify local whois binary("/usr/local/whois"). |
|
||||
| dns_interface | --dns-interface | BIRDLG_DNS_INTERFACE | dns zone to query ASN information (default "asn.cymru.com") |
|
||||
| bgpmap_info | --bgpmap-info | BIRDLG_BGPMAP_INFO | the infos displayed in bgpmap, separated by comma, start with `:` means allow multiline (default "asn,as-name,ASName,descr") |
|
||||
| title_brand | --title-brand | BIRDLG_TITLE_BRAND | prefix of page titles in browser tabs (default "Bird-lg Go") |
|
||||
| navbar_brand | --navbar-brand | BIRDLG_NAVBAR_BRAND | brand to show in the navigation bar (default "Bird-lg Go") |
|
||||
| navbar_brand_url | --navbar-brand-url | BIRDLG_NAVBAR_BRAND_URL | the url of the brand to show in the navigation bar (default "/") |
|
||||
| navbar_all_servers | --navbar-all-servers | BIRDLG_NAVBAR_ALL_SERVERS | the text of "All servers" button in the navigation bar (default "ALL Servers") |
|
||||
| navbar_all_url | --navbar-all-url | BIRDLG_NAVBAR_ALL_URL | the URL of "All servers" button (default "all") |
|
||||
| net_specific_mode | --net-specific-mode | BIRDLG_NET_SPECIFIC_MODE | apply network-specific changes for some networks, use "dn42" for BIRD in dn42 network |
|
||||
| protocol_filter | --protocol-filter | BIRDLG_PROTOCOL_FILTER | protocol types to show in summary tables (comma separated list); defaults to all if not set |
|
||||
| name_filter | --name-filter | BIRDLG_NAME_FILTER | protocol names to hide in summary tables (RE2 syntax); defaults to none if not set |
|
||||
| timeout | --time-out | BIRDLG_TIMEOUT | time before request timed out, in seconds; defaults to 120 if not set |
|
||||
|
||||
### Examples
|
||||
|
||||
Example: the following command starts the frontend with 2 BIRD nodes, with domain name "gigsgigscloud.dn42.lantian.pub" and "hostdare.dn42.lantian.pub", and proxies are running on port 8000 on both nodes.
|
||||
|
||||
```bash
|
||||
./frontend --servers=gigsgigscloud,hostdare --domain=dn42.lantian.pub --proxy-port=8000
|
||||
```
|
||||
|
||||
Example: the following docker-compose.yml entry does the same as above, but by starting a Docker container:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
bird-lg:
|
||||
image: xddxdd/bird-lg-go
|
||||
@@ -85,8 +98,9 @@ Example: the following docker-compose.yml entry does the same as above, but by s
|
||||
- BIRDLG_DOMAIN=dn42.lantian.pub
|
||||
ports:
|
||||
- "5000:5000"
|
||||
```
|
||||
|
||||
Demo: https://lg.lantian.pub
|
||||
Demo: <https://lg.lantian.pub>
|
||||
|
||||
## Proxy
|
||||
|
||||
@@ -99,24 +113,57 @@ Features implemented:
|
||||
- Executing traceroute command on Linux, FreeBSD and OpenBSD
|
||||
- Source IP restriction
|
||||
|
||||
Usage: all configuration is done via commandline parameters or environment variables, no config file.
|
||||
Configuration can be set in:
|
||||
|
||||
| Parameter | Environment Variable | Description |
|
||||
| --------- | -------------------- | ----------- |
|
||||
| --allowed | ALLOWED_IPS | IPs allowed to access this proxy, separated by commas. Don't set to allow all IPs. (default "") |
|
||||
| --bird | BIRD_SOCKET | socket file for bird, set either in parameter or environment variable BIRD_SOCKET (default "/var/run/bird/bird.ctl") |
|
||||
| --listen | BIRDLG_LISTEN | listen address, set either in parameter or environment variable BIRDLG_LISTEN (default ":8000") |
|
||||
- `bird-lgproxy.[json/yaml/etc]` in current directory
|
||||
- `/etc/bird-lg/bird-lgproxy.[json/yaml/etc]`
|
||||
- Commandline parameter
|
||||
- Environment variables
|
||||
|
||||
Configuration is handled by [viper](https://github.com/spf13/viper), any config format supported by it can be used.
|
||||
|
||||
| Config Key | Parameter | Environment Variable | Description |
|
||||
| ---------- | --------- | -------------------- | ----------- |
|
||||
| allowed_ips | --allowed | ALLOWED_IPS | IPs allowed to access this proxy, separated by commas. Don't set to allow all IPs. (default "") |
|
||||
| bird_socket | --bird | BIRD_SOCKET | socket file for bird, set either in parameter or environment variable BIRD_SOCKET (default "/var/run/bird/bird.ctl") |
|
||||
| listen | --listen | BIRDLG_PROXY_PORT | listen address, set either in parameter or environment variable BIRDLG_PROXY_PORT(default "8000") |
|
||||
| traceroute_bin | --traceroute_bin | BIRDLG_TRACEROUTE_BIN | traceroute binary file, set either in parameter or environment variable BIRDLG_TRACEROUTE_BIN |
|
||||
| traceroute_flags | --traceroute_flags | BIRDLG_TRACEROUTE_FLAGS | traceroute flags, supports multiple flags separated with space. |
|
||||
| traceroute_raw | --traceroute_raw | BIRDLG_TRACEROUTE_RAW | whether to display traceroute outputs raw (default false) |
|
||||
|
||||
### Traceroute Binary Autodetection
|
||||
|
||||
If `traceroute_bin` or `traceroute_flags` is not set, then on startup, the proxy will try to `traceroute 127.0.0.1` with different traceroute binaries and arguments, in order to use the most optimized setting available, while maintaining compatibility with multiple variants of traceroute binaries.
|
||||
|
||||
Traceroute binaries will be autodetected in the following order:
|
||||
|
||||
1. If `traceroute_bin` is set:
|
||||
1. `[traceroute_bin] -q1 -N32 -w1 127.0.0.1` (Corresponds to Traceroute on Debian)
|
||||
2. `[traceroute_bin] -q1 -w1 127.0.0.1` (Corresponds to Traceroute on FreeBSD)
|
||||
3. `[traceroute_bin] 127.0.0.1` (Corresponds to Busybox Traceroute)
|
||||
2. `mtr -w -c1 -Z1 -G1 -b 127.0.0.1` (MTR)
|
||||
3. `traceroute -q1 -N32 -w1 127.0.0.1` (Corresponds to Traceroute on Debian)
|
||||
4. `traceroute -q1 -w1 127.0.0.1` (Corresponds to Traceroute on FreeBSD)
|
||||
5. `traceroute 127.0.0.1` (Corresponds to Busybox Traceroute)
|
||||
|
||||
### Examples
|
||||
|
||||
Example: start proxy with default configuration, should work "out of the box" on Debian 9 with BIRDv1:
|
||||
|
||||
```bash
|
||||
./proxy
|
||||
```
|
||||
|
||||
Example: start proxy with custom bird socket location:
|
||||
|
||||
```bash
|
||||
./proxy --bird /run/bird.ctl
|
||||
```
|
||||
|
||||
Example: the following docker-compose.yml entry does the same as above, but by starting a Docker container:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
bird-lgproxy:
|
||||
image: xddxdd/bird-lgproxy-go
|
||||
container_name: bird-lgproxy
|
||||
@@ -125,6 +172,7 @@ Example: the following docker-compose.yml entry does the same as above, but by s
|
||||
- "/run/bird.ctl:/var/run/bird/bird.ctl"
|
||||
ports:
|
||||
- "192.168.0.1:8000:8000"
|
||||
```
|
||||
|
||||
You can use source IP restriction to increase security. You should also bind the proxy to a specific interface and use an external firewall/iptables for added security.
|
||||
|
||||
@@ -136,7 +184,9 @@ The server parameter is composed of server name prefixes, separated by comma. It
|
||||
|
||||
For instance, the two servers from the basic example can be displayed as "Gigs" and "Hostdare" using the following syntax (as known from email addresses):
|
||||
|
||||
```bash
|
||||
./frontend --servers="Gigs<gigsgigscloud>,Hostdare<hostdare>" --domain=dn42.lantian.pub
|
||||
```
|
||||
|
||||
### IP addresses
|
||||
|
||||
@@ -144,7 +194,9 @@ You may also specify IP addresses as server names when no domain is specified. I
|
||||
|
||||
For example:
|
||||
|
||||
```bash
|
||||
./frontend --servers="Prod<prod.mydomain.local>,Test1<fd88:dead:beef::1>,Test2<fe80::c%wg0>" --domain=
|
||||
```
|
||||
|
||||
These three servers are displayed as "Prod", "Test1" and "Test2" in the user interface.
|
||||
|
||||
@@ -152,30 +204,13 @@ These three servers are displayed as "Prod", "Test1" and "Test2" in the user int
|
||||
|
||||
The frontend provides an API for running BIRD/traceroute/whois queries.
|
||||
|
||||
See [API docs](API.md) for detailed information.
|
||||
See [API docs](docs/API.md) for detailed information.
|
||||
|
||||
### Telegram Bot Webhook
|
||||
|
||||
The frontend can act as a Telegram Bot webhook endpoint, to add BGP route/traceroute/whois lookup functionality to your tech group.
|
||||
|
||||
There is no configuration necessary on the frontend, just start it up normally.
|
||||
|
||||
Set your Telegram Bot webhook URL to `https://your.frontend.com/telegram/alpha+beta+gamma`, where `alpha+beta+gamma` is the list of servers to be queried on Telegram commands, separated by `+`.
|
||||
|
||||
You may omit `alpha+beta+gamma` to use all your servers, but it is not recommended when you have lots of servers, or the message would be too long and hard to read.
|
||||
|
||||
#### Example of setting the webhook
|
||||
|
||||
```bash
|
||||
curl "https://api.telegram.org/bot${BOT_TOKEN}/setWebhook?url=https://your.frontend.com:5000/telegram/alpha+beta+gamma"
|
||||
```
|
||||
|
||||
#### Supported commands
|
||||
|
||||
- `path`: Show bird's ASN path to target IP
|
||||
- `route`: Show bird's preferred route to target IP
|
||||
- `trace`: Traceroute to target IP/domain
|
||||
- `whois`: Whois query
|
||||
See [Telegram docs](docs/Telegram.md) for detailed information.
|
||||
|
||||
## Credits
|
||||
|
||||
|
||||
@@ -153,9 +153,11 @@ Request:
|
||||
|
||||
```json
|
||||
{
|
||||
"servers": [],
|
||||
"type": "server_list",
|
||||
"args": ""
|
||||
"servers": [
|
||||
"alpha"
|
||||
],
|
||||
"type": "bird",
|
||||
"args": "show status"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -179,11 +181,9 @@ Request:
|
||||
|
||||
```json
|
||||
{
|
||||
"servers": [
|
||||
"alpha"
|
||||
],
|
||||
"type": "bird",
|
||||
"args": "show status"
|
||||
"servers": [],
|
||||
"type": "server_list",
|
||||
"args": ""
|
||||
}
|
||||
```
|
||||
|
||||
22
docs/Telegram.md
Normal file
22
docs/Telegram.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Telegram Bot Webhook
|
||||
|
||||
The frontend can act as a Telegram Bot webhook endpoint, to add BGP route/traceroute/whois lookup functionality to your tech group.
|
||||
|
||||
There is no configuration necessary on the frontend, just start it up normally.
|
||||
|
||||
Set your Telegram Bot webhook URL to `https://your.frontend.com/telegram/alpha+beta+gamma`, where `alpha+beta+gamma` is the list of servers to be queried on Telegram commands, separated by `+`.
|
||||
|
||||
You may omit `alpha+beta+gamma` to use all your servers, but it is not recommended when you have lots of servers, or the message would be too long and hard to read.
|
||||
|
||||
## Example of setting the webhook
|
||||
|
||||
```bash
|
||||
curl "https://api.telegram.org/bot${BOT_TOKEN}/setWebhook?url=https://your.frontend.com:5000/telegram/alpha+beta+gamma"
|
||||
```
|
||||
|
||||
## Supported commands
|
||||
|
||||
- `path`: Show bird's ASN path to target IP
|
||||
- `route`: Show bird's preferred route to target IP
|
||||
- `trace`: Traceroute to target IP/domain
|
||||
- `whois`: Whois query
|
||||
11
frontend/Dockerfile
Normal file
11
frontend/Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
||||
FROM golang:buster AS step_0
|
||||
ENV CGO_ENABLED=0 GO111MODULE=on
|
||||
WORKDIR /root
|
||||
COPY . .
|
||||
RUN go build -ldflags "-w -s" -o /frontend
|
||||
|
||||
################################################################################
|
||||
|
||||
FROM scratch AS step_1
|
||||
COPY --from=step_0 /frontend /
|
||||
ENTRYPOINT ["/frontend"]
|
||||
@@ -1,5 +1,3 @@
|
||||
.PHONY: all
|
||||
all:
|
||||
go get -u github.com/kevinburke/go-bindata/...
|
||||
go generate
|
||||
go build -ldflags "-w -s" -o frontend
|
||||
|
||||
@@ -113,7 +113,7 @@ func apiHandler(w http.ResponseWriter, r *http.Request) {
|
||||
} else {
|
||||
handler := apiHandlerMap[request.Type]
|
||||
if handler == nil {
|
||||
response = apiErrorHandler(errors.New("Invalid request type"))
|
||||
response = apiErrorHandler(errors.New("invalid request type"))
|
||||
} else {
|
||||
response = handler(request)
|
||||
}
|
||||
|
||||
207
frontend/api_test.go
Normal file
207
frontend/api_test.go
Normal file
@@ -0,0 +1,207 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/jarcoal/httpmock"
|
||||
"github.com/magiconair/properties/assert"
|
||||
)
|
||||
|
||||
func TestApiServerListHandler(t *testing.T) {
|
||||
setting.servers = []string{"alpha", "beta", "gamma"}
|
||||
response := apiServerListHandler(apiRequest{})
|
||||
|
||||
assert.Equal(t, len(response.Result), 3)
|
||||
assert.Equal(t, response.Result[0].(apiGenericResultPair).Server, "alpha")
|
||||
assert.Equal(t, response.Result[1].(apiGenericResultPair).Server, "beta")
|
||||
assert.Equal(t, response.Result[2].(apiGenericResultPair).Server, "gamma")
|
||||
}
|
||||
|
||||
func TestApiGenericHandlerFactory(t *testing.T) {
|
||||
httpmock.Activate()
|
||||
defer httpmock.DeactivateAndReset()
|
||||
|
||||
httpResponse := httpmock.NewStringResponder(200, BirdSummaryData)
|
||||
httpmock.RegisterResponder("GET", "http://alpha:8000/bird?q="+url.QueryEscape("show protocols"), httpResponse)
|
||||
|
||||
setting.servers = []string{"alpha"}
|
||||
setting.domain = ""
|
||||
setting.proxyPort = 8000
|
||||
|
||||
request := apiRequest{
|
||||
Servers: setting.servers,
|
||||
Type: "bird",
|
||||
Args: "show protocols",
|
||||
}
|
||||
|
||||
handler := apiGenericHandlerFactory("bird")
|
||||
response := handler(request)
|
||||
|
||||
assert.Equal(t, response.Error, "")
|
||||
|
||||
result := response.Result[0].(*apiGenericResultPair)
|
||||
assert.Equal(t, result.Server, "alpha")
|
||||
assert.Equal(t, result.Data, BirdSummaryData)
|
||||
}
|
||||
|
||||
func TestApiSummaryHandler(t *testing.T) {
|
||||
httpmock.Activate()
|
||||
defer httpmock.DeactivateAndReset()
|
||||
|
||||
httpResponse := httpmock.NewStringResponder(200, BirdSummaryData)
|
||||
httpmock.RegisterResponder("GET", "http://alpha:8000/bird?q="+url.QueryEscape("show protocols"), httpResponse)
|
||||
|
||||
setting.servers = []string{"alpha"}
|
||||
setting.domain = ""
|
||||
setting.proxyPort = 8000
|
||||
|
||||
request := apiRequest{
|
||||
Servers: setting.servers,
|
||||
Type: "summary",
|
||||
Args: "",
|
||||
}
|
||||
response := apiSummaryHandler(request)
|
||||
|
||||
assert.Equal(t, response.Error, "")
|
||||
|
||||
summary := response.Result[0].(*apiSummaryResultPair)
|
||||
assert.Equal(t, summary.Server, "alpha")
|
||||
// Protocol list will be sorted
|
||||
assert.Equal(t, summary.Data[1].Name, "device1")
|
||||
assert.Equal(t, summary.Data[1].Proto, "Device")
|
||||
assert.Equal(t, summary.Data[1].Table, "---")
|
||||
assert.Equal(t, summary.Data[1].State, "up")
|
||||
assert.Equal(t, summary.Data[1].Since, "2021-08-27")
|
||||
assert.Equal(t, summary.Data[1].Info, "")
|
||||
}
|
||||
|
||||
func TestApiSummaryHandlerError(t *testing.T) {
|
||||
httpmock.Activate()
|
||||
defer httpmock.DeactivateAndReset()
|
||||
|
||||
httpResponse := httpmock.NewStringResponder(200, "Mock backend error")
|
||||
httpmock.RegisterResponder("GET", "http://alpha:8000/bird?q="+url.QueryEscape("show protocols"), httpResponse)
|
||||
|
||||
setting.servers = []string{"alpha"}
|
||||
setting.domain = ""
|
||||
setting.proxyPort = 8000
|
||||
|
||||
request := apiRequest{
|
||||
Servers: setting.servers,
|
||||
Type: "summary",
|
||||
Args: "",
|
||||
}
|
||||
response := apiSummaryHandler(request)
|
||||
|
||||
assert.Equal(t, response.Error, "Mock backend error")
|
||||
}
|
||||
|
||||
func TestApiWhoisHandler(t *testing.T) {
|
||||
expectedData := "Mock Data"
|
||||
server := WhoisServer{
|
||||
t: t,
|
||||
expectedQuery: "AS6939",
|
||||
response: expectedData,
|
||||
}
|
||||
|
||||
server.Listen()
|
||||
go server.Run()
|
||||
defer server.Close()
|
||||
|
||||
setting.whoisServer = server.server.Addr().String()
|
||||
|
||||
request := apiRequest{
|
||||
Servers: []string{},
|
||||
Type: "",
|
||||
Args: "AS6939",
|
||||
}
|
||||
response := apiWhoisHandler(request)
|
||||
|
||||
assert.Equal(t, response.Error, "")
|
||||
|
||||
whoisResult := response.Result[0].(apiGenericResultPair)
|
||||
assert.Equal(t, whoisResult.Server, "")
|
||||
assert.Equal(t, whoisResult.Data, expectedData)
|
||||
}
|
||||
|
||||
func TestApiErrorHandler(t *testing.T) {
|
||||
err := errors.New("Mock Error")
|
||||
response := apiErrorHandler(err)
|
||||
assert.Equal(t, response.Error, "Mock Error")
|
||||
}
|
||||
|
||||
func TestApiHandler(t *testing.T) {
|
||||
setting.servers = []string{"alpha", "beta", "gamma"}
|
||||
|
||||
request := apiRequest{
|
||||
Servers: []string{},
|
||||
Type: "server_list",
|
||||
Args: "",
|
||||
}
|
||||
requestJson, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
r := httptest.NewRequest(http.MethodGet, "/api", bytes.NewReader(requestJson))
|
||||
w := httptest.NewRecorder()
|
||||
apiHandler(w, r)
|
||||
|
||||
var response apiResponse
|
||||
err = json.Unmarshal(w.Body.Bytes(), &response)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, len(response.Result), 3)
|
||||
// Hard to unmarshal JSON into apiGenericResultPair objects, won't check here
|
||||
}
|
||||
|
||||
func TestApiHandlerBadJSON(t *testing.T) {
|
||||
setting.servers = []string{"alpha", "beta", "gamma"}
|
||||
|
||||
r := httptest.NewRequest(http.MethodGet, "/api", strings.NewReader("{bad json}"))
|
||||
w := httptest.NewRecorder()
|
||||
apiHandler(w, r)
|
||||
|
||||
var response apiResponse
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, len(response.Result), 0)
|
||||
}
|
||||
|
||||
func TestApiHandlerInvalidType(t *testing.T) {
|
||||
setting.servers = []string{"alpha", "beta", "gamma"}
|
||||
|
||||
request := apiRequest{
|
||||
Servers: setting.servers,
|
||||
Type: "invalid_type",
|
||||
Args: "",
|
||||
}
|
||||
requestJson, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
r := httptest.NewRequest(http.MethodGet, "/api", bytes.NewReader(requestJson))
|
||||
w := httptest.NewRecorder()
|
||||
apiHandler(w, r)
|
||||
|
||||
var response apiResponse
|
||||
err = json.Unmarshal(w.Body.Bytes(), &response)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, len(response.Result), 0)
|
||||
}
|
||||
83
frontend/asn_cache.go
Normal file
83
frontend/asn_cache.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ASNCache map[string]string
|
||||
|
||||
func (cache ASNCache) _lookup(asn string) string {
|
||||
// Try to get ASN representation using DNS
|
||||
if setting.dnsInterface != "" {
|
||||
records, err := net.LookupTXT(fmt.Sprintf("AS%s.%s", asn, setting.dnsInterface))
|
||||
if err == nil {
|
||||
result := strings.Join(records, " ")
|
||||
if resultSplit := strings.Split(result, " | "); len(resultSplit) > 1 {
|
||||
result = strings.Join(resultSplit[1:], "\n")
|
||||
}
|
||||
return fmt.Sprintf("AS%s\n%s", asn, result)
|
||||
}
|
||||
}
|
||||
|
||||
// Try to get ASN representation using WHOIS
|
||||
if setting.whoisServer != "" {
|
||||
if setting.bgpmapInfo == "" {
|
||||
setting.bgpmapInfo = "asn,as-name,ASName,descr"
|
||||
}
|
||||
records := whois(fmt.Sprintf("AS%s", asn))
|
||||
if records != "" {
|
||||
recordsSplit := strings.Split(records, "\n")
|
||||
var result []string
|
||||
for _, title := range strings.Split(setting.bgpmapInfo, ",") {
|
||||
if title == "asn" {
|
||||
result = append(result, "AS"+asn)
|
||||
}
|
||||
}
|
||||
for _, title := range strings.Split(setting.bgpmapInfo, ",") {
|
||||
allow_multiline := false
|
||||
if title[0] == ':' && len(title) >= 2 {
|
||||
title = title[1:]
|
||||
allow_multiline = true
|
||||
}
|
||||
for _, line := range recordsSplit {
|
||||
if len(line) == 0 || line[0] == '%' || !strings.Contains(line, ":") {
|
||||
continue
|
||||
}
|
||||
linearr := strings.SplitN(line, ":", 2)
|
||||
line_title := linearr[0]
|
||||
content := strings.TrimSpace(linearr[1])
|
||||
if line_title != title {
|
||||
continue
|
||||
}
|
||||
result = append(result, content)
|
||||
if !allow_multiline {
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
if len(result) > 0 {
|
||||
return strings.Join(result, "\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (cache ASNCache) Lookup(asn string) string {
|
||||
cachedValue, cacheOk := cache[asn]
|
||||
if cacheOk {
|
||||
return cachedValue
|
||||
}
|
||||
|
||||
result := cache._lookup(asn)
|
||||
if len(result) == 0 {
|
||||
result = fmt.Sprintf("AS%s", asn)
|
||||
}
|
||||
|
||||
cache[asn] = result
|
||||
return result
|
||||
}
|
||||
52
frontend/asn_cache_test.go
Normal file
52
frontend/asn_cache_test.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/magiconair/properties/assert"
|
||||
)
|
||||
|
||||
func TestGetASNRepresentationDNS(t *testing.T) {
|
||||
checkNetwork(t)
|
||||
|
||||
setting.dnsInterface = "asn.cymru.com"
|
||||
setting.whoisServer = ""
|
||||
cache := make(ASNCache)
|
||||
result := cache.Lookup("6939")
|
||||
if !strings.Contains(result, "HURRICANE") {
|
||||
t.Errorf("Lookup AS6939 failed, got %s", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetASNRepresentationDNSFallback(t *testing.T) {
|
||||
checkNetwork(t)
|
||||
|
||||
setting.dnsInterface = "invalid.example.com"
|
||||
setting.whoisServer = "whois.arin.net"
|
||||
cache := make(ASNCache)
|
||||
result := cache.Lookup("6939")
|
||||
if !strings.Contains(result, "HURRICANE") {
|
||||
t.Errorf("Lookup AS6939 failed, got %s", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetASNRepresentationWhois(t *testing.T) {
|
||||
checkNetwork(t)
|
||||
|
||||
setting.dnsInterface = ""
|
||||
setting.whoisServer = "whois.arin.net"
|
||||
cache := make(ASNCache)
|
||||
result := cache.Lookup("6939")
|
||||
if !strings.Contains(result, "HURRICANE") {
|
||||
t.Errorf("Lookup AS6939 failed, got %s", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetASNRepresentationFallback(t *testing.T) {
|
||||
setting.dnsInterface = ""
|
||||
setting.whoisServer = ""
|
||||
cache := make(ASNCache)
|
||||
result := cache.Lookup("6939")
|
||||
assert.Equal(t, result, "AS6939")
|
||||
}
|
||||
BIN
frontend/assets/favicon.ico
Normal file
BIN
frontend/assets/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
7
frontend/assets/static/jsdelivr/npm/bootstrap@4.5.1/dist/css/bootstrap.min.css
vendored
Normal file
7
frontend/assets/static/jsdelivr/npm/bootstrap@4.5.1/dist/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
7
frontend/assets/static/jsdelivr/npm/bootstrap@4.5.1/dist/js/bootstrap.min.js
vendored
Normal file
7
frontend/assets/static/jsdelivr/npm/bootstrap@4.5.1/dist/js/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
frontend/assets/static/jsdelivr/npm/jquery@3.5.1/dist/jquery.min.js
vendored
Normal file
2
frontend/assets/static/jsdelivr/npm/jquery@3.5.1/dist/jquery.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
8
frontend/assets/static/jsdelivr/npm/viz.js@2.1.2/viz.min.js
vendored
Normal file
8
frontend/assets/static/jsdelivr/npm/viz.js@2.1.2/viz.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
73
frontend/assets/static/sortTable.js
Normal file
73
frontend/assets/static/sortTable.js
Normal file
@@ -0,0 +1,73 @@
|
||||
// adapted from https://stackoverflow.com/a/57080195
|
||||
|
||||
document.querySelectorAll('table.sortable')
|
||||
.forEach((table)=> {
|
||||
table.querySelectorAll('th')
|
||||
.forEach((element, columnNo) => {
|
||||
element.addEventListener('click', event => {
|
||||
if(element.classList.contains('ascSorted')) {
|
||||
dir = -1;
|
||||
element.classList.remove('ascSorted');
|
||||
element.classList.add('descSorted');
|
||||
element.innerText = element.innerText.slice(0,-2) + " ↓";
|
||||
} else if(element.classList.contains('descSorted')) {
|
||||
dir = 1;
|
||||
element.classList.remove('descSorted');
|
||||
element.classList.add('ascSorted');
|
||||
element.innerText = element.innerText.slice(0,-2) + " ↑";
|
||||
} else {
|
||||
dir = 1;
|
||||
element.classList.add('ascSorted');
|
||||
element.innerText += " ↑";
|
||||
}
|
||||
sortTable(table, columnNo, 0, dir, 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function sortTable(table, priCol, secCol, priDir, secDir) {
|
||||
const tableBody = table.querySelector('tbody');
|
||||
const tableData = table2data(tableBody);
|
||||
tableData.sort((a, b) => {
|
||||
if(a[priCol] === b[priCol]) {
|
||||
if(a[secCol] > b[secCol]) {
|
||||
return secDir;
|
||||
} else {
|
||||
return -secDir;
|
||||
}
|
||||
} else if(a[priCol] > b[priCol]) {
|
||||
return priDir;
|
||||
} else {
|
||||
return -priDir;
|
||||
}
|
||||
});
|
||||
data2table(tableBody, tableData);
|
||||
}
|
||||
|
||||
function table2data(tableBody) {
|
||||
const tableData = [];
|
||||
tableBody.querySelectorAll('tr')
|
||||
.forEach(row => {
|
||||
const rowData = [];
|
||||
row.querySelectorAll('td')
|
||||
.forEach(cell => {
|
||||
rowData.push(cell.innerHTML);
|
||||
});
|
||||
rowData.classList = row.classList.toString();
|
||||
tableData.push(rowData);
|
||||
});
|
||||
return tableData;
|
||||
}
|
||||
|
||||
function data2table(tableBody, tableData) {
|
||||
tableBody.querySelectorAll('tr')
|
||||
.forEach((row, i) => {
|
||||
const rowData = tableData[i];
|
||||
row.classList = rowData.classList;
|
||||
row.querySelectorAll('td')
|
||||
.forEach((cell, j) => {
|
||||
cell.innerHTML = rowData[j];
|
||||
});
|
||||
tableData.push(rowData);
|
||||
});
|
||||
}
|
||||
@@ -2,11 +2,11 @@
|
||||
<div id="bgpmap">
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/viz.js@2.1.2/viz.min.js" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/viz.js@2.1.2/lite.render.js" crossorigin="anonymous"></script>
|
||||
<script src="/static/jsdelivr/npm/viz.js@2.1.2/viz.min.js" crossorigin="anonymous"></script>
|
||||
<script src="/static/jsdelivr/npm/viz.js@2.1.2/lite.render.js" crossorigin="anonymous"></script>
|
||||
<script>
|
||||
var viz = new Viz();
|
||||
viz.renderSVGElement(`{{ .Result }}`)
|
||||
viz.renderSVGElement(atob({{ .Result }}))
|
||||
.then(element => {
|
||||
document.getElementById("bgpmap").appendChild(element);
|
||||
})
|
||||
@@ -1,18 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<link rel="icon" href="/favicon.ico" type="image/x-icon" />
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<meta name="renderer" content="webkit">
|
||||
<title>{{ html .Title }}</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.1/dist/css/bootstrap.min.css" integrity="sha256-VoFZSlmyTXsegReQCNmbXrS4hBBUl/cexZvPmPWoJsY=" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="/static/jsdelivr/npm/bootstrap@4.5.1/dist/css/bootstrap.min.css" integrity="sha256-VoFZSlmyTXsegReQCNmbXrS4hBBUl/cexZvPmPWoJsY=" crossorigin="anonymous">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
||||
<a class="navbar-brand" href="/">{{ .Brand }}</a>
|
||||
<a class="navbar-brand" href="{{ .BrandURL }}">{{ .Brand }}</a>
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
@@ -28,13 +29,24 @@
|
||||
{{ end }}
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item">
|
||||
{{ if eq .AllServersURLCustom "all" }}
|
||||
<a class="nav-link{{ if .AllServersLinkActive }} active{{ end }}"
|
||||
href="/{{ $option }}/{{ .AllServersURL }}/{{ $target }}"> All Servers </a>
|
||||
href="/{{ $option }}/{{ .AllServersURL }}/{{ $target }}"> {{ .AllServerTitle }} </a>
|
||||
{{ else }}
|
||||
<a class="nav-link active"
|
||||
href="{{ .AllServersURLCustom }}"> {{ .AllServerTitle }} </a>
|
||||
{{ end }}
|
||||
</li>
|
||||
{{ range $k, $v := .ServersEscaped }}
|
||||
{{ $length := len .Servers }}
|
||||
{{ range $k, $v := .Servers }}
|
||||
<li class="nav-item">
|
||||
{{ if gt $length 1 }}
|
||||
<a class="nav-link{{ if eq $server $v }} active{{ end }}"
|
||||
href="/{{ $option }}/{{ $v }}/{{ $target }}">{{ html (index $.ServersDisplay $k) }}</a>
|
||||
{{ else }}
|
||||
<a class="nav-link{{ if eq $server $v }} active{{ end }}"
|
||||
href="/">{{ html (index $.ServersDisplay $k) }}</a>
|
||||
{{ end }}
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
@@ -62,8 +74,9 @@
|
||||
{{ .Content }}
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.1/dist/js/bootstrap.min.js" integrity="sha256-0IiaoZCI++9oAAvmCb5Y0r93XkuhvJpRalZLffQXLok=" crossorigin="anonymous"></script>
|
||||
<script src="/static/jsdelivr/npm/jquery@3.5.1/dist/jquery.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
|
||||
<script src="/static/jsdelivr/npm/bootstrap@4.5.1/dist/js/bootstrap.min.js" integrity="sha256-0IiaoZCI++9oAAvmCb5Y0r93XkuhvJpRalZLffQXLok=" crossorigin="anonymous"></script>
|
||||
<script src="/static/sortTable.js"></script>
|
||||
|
||||
<script>
|
||||
function goto() {
|
||||
@@ -1,6 +1,6 @@
|
||||
{{ $ServerName := urlquery .ServerName }}
|
||||
|
||||
<table class="table table-striped table-bordered table-sm">
|
||||
<table class="table table-striped table-bordered table-sm sortable">
|
||||
<thead>
|
||||
{{ range .Header }}
|
||||
<th scope="col">{{ html . }}</th>
|
||||
@@ -1,122 +1,121 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
"net"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func getASNRepresentation(asn string) string {
|
||||
records, err := net.LookupTXT(fmt.Sprintf("AS%s.%s", asn, setting.dnsInterface))
|
||||
if err != nil {
|
||||
// DNS query failed, only use ASN as output
|
||||
return fmt.Sprintf("AS%s", asn)
|
||||
// The protocol name for each route (e.g. "ibgp_sea02") is encoded in the form:
|
||||
//
|
||||
// unicast [ibgp_sea02 2021-08-27 from fd86:bad:11b7:1::1] * (100/1015) [i]
|
||||
var protocolNameRe = regexp.MustCompile(`\[(.*?) .*\]`)
|
||||
|
||||
// Try to split the output into one chunk for each route.
|
||||
// Possible values are defined at https://gitlab.nic.cz/labs/bird/-/blob/v2.0.8/nest/rt-attr.c#L81-87
|
||||
var routeSplitRe = regexp.MustCompile("(unicast|blackhole|unreachable|prohibited)")
|
||||
|
||||
var routeViaRe = regexp.MustCompile(`(?m)^\t(via .*?)$`)
|
||||
var routeASPathRe = regexp.MustCompile(`(?m)^\tBGP\.as_path: (.*?)$`)
|
||||
|
||||
func makeEdgeAttrs(preferred bool) RouteAttrs {
|
||||
result := RouteAttrs{
|
||||
"fontsize": "12.0",
|
||||
}
|
||||
if preferred {
|
||||
result["color"] = "red"
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
result := strings.Join(records, " ")
|
||||
if resultSplit := strings.Split(result, " | "); len(resultSplit) > 1 {
|
||||
result = strings.Join(resultSplit[1:], "\\n")
|
||||
func makePointAttrs(preferred bool) RouteAttrs {
|
||||
result := RouteAttrs{}
|
||||
if preferred {
|
||||
result["color"] = "red"
|
||||
}
|
||||
return fmt.Sprintf("AS%s\\n%s", asn, result)
|
||||
return result
|
||||
}
|
||||
|
||||
func birdRouteToGraphviz(servers []string, responses []string, target string) string {
|
||||
graph := make(map[string]string)
|
||||
// Helper to add an edge
|
||||
addEdge := func(src string, dest string, attr string) {
|
||||
key := "\"" + html.EscapeString(src) + "\" -> \"" + html.EscapeString(dest) + "\""
|
||||
_, present := graph[key]
|
||||
// Do not remove edge's attributes if it's already present
|
||||
if present && len(attr) == 0 {
|
||||
return
|
||||
}
|
||||
graph[key] = attr
|
||||
}
|
||||
// Helper to set attribute for a point in graph
|
||||
addPoint := func(name string, attr string) {
|
||||
key := "\"" + html.EscapeString(name) + "\""
|
||||
_, present := graph[key]
|
||||
// Do not remove point's attributes if it's already present
|
||||
if present && len(attr) == 0 {
|
||||
return
|
||||
}
|
||||
graph[key] = attr
|
||||
}
|
||||
func birdRouteToGraph(servers []string, responses []string, target string) RouteGraph {
|
||||
graph := makeRouteGraph()
|
||||
|
||||
graph.AddPoint(target, false, RouteAttrs{"color": "red", "shape": "diamond"})
|
||||
|
||||
addPoint("Target: "+target, "[color=red,shape=diamond]")
|
||||
for serverID, server := range servers {
|
||||
response := responses[serverID]
|
||||
if len(response) == 0 {
|
||||
continue
|
||||
}
|
||||
addPoint(server, "[color=blue,shape=box]")
|
||||
// This is the best split point I can find for bird2
|
||||
routes := strings.Split(response, "\tvia ")
|
||||
routeFound := false
|
||||
graph.AddPoint(server, false, RouteAttrs{"color": "blue", "shape": "box"})
|
||||
routes := routeSplitRe.Split(response, -1)
|
||||
|
||||
for routeIndex, route := range routes {
|
||||
var routeNexthop string
|
||||
var routeASPath string
|
||||
var routePreferred bool = routeIndex > 0 && strings.Contains(routes[routeIndex-1], "*")
|
||||
// Have to look at previous slice to determine if route is preferred, due to bad split point selection
|
||||
|
||||
for _, routeParameter := range strings.Split(route, "\n") {
|
||||
if strings.HasPrefix(routeParameter, "\tBGP.next_hop: ") {
|
||||
routeNexthop = strings.TrimPrefix(routeParameter, "\tBGP.next_hop: ")
|
||||
} else if strings.HasPrefix(routeParameter, "\tBGP.as_path: ") {
|
||||
routeASPath = strings.TrimPrefix(routeParameter, "\tBGP.as_path: ")
|
||||
}
|
||||
}
|
||||
if len(routeASPath) == 0 {
|
||||
// Either this is not a BGP route, or the information is incomplete
|
||||
if routeIndex == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Connect each node on AS path
|
||||
paths := strings.Split(strings.TrimSpace(routeASPath), " ")
|
||||
var via string
|
||||
var paths []string
|
||||
var routePreferred bool = strings.Contains(route, "*")
|
||||
// Track non-BGP routes in the output by their protocol name, but draw them altogether in one line
|
||||
// so that there are no conflicts in the edge label
|
||||
var protocolName string
|
||||
|
||||
for pathIndex := range paths {
|
||||
paths[pathIndex] = strings.TrimPrefix(paths[pathIndex], "(")
|
||||
paths[pathIndex] = strings.TrimSuffix(paths[pathIndex], ")")
|
||||
if match := routeViaRe.FindStringSubmatch(route); len(match) >= 2 {
|
||||
via = strings.TrimSpace(match[1])
|
||||
}
|
||||
|
||||
// First step starting from originating server
|
||||
if len(paths) > 0 {
|
||||
if len(routeNexthop) > 0 {
|
||||
// Edge from originating server to nexthop
|
||||
addEdge(server, "Nexthop:\\n"+routeNexthop, (map[bool]string{true: "[color=red]"})[routePreferred])
|
||||
// and from nexthop to AS
|
||||
addEdge("Nexthop:\\n"+routeNexthop, getASNRepresentation(paths[0]), (map[bool]string{true: "[color=red]"})[routePreferred])
|
||||
addPoint("Nexthop:\\n"+routeNexthop, "[shape=diamond]")
|
||||
routeFound = true
|
||||
if match := routeASPathRe.FindStringSubmatch(route); len(match) >= 2 {
|
||||
pathString := strings.TrimSpace(match[1])
|
||||
if len(pathString) > 0 {
|
||||
paths = strings.Split(strings.TrimSpace(match[1]), " ")
|
||||
for i := range paths {
|
||||
paths[i] = strings.TrimPrefix(paths[i], "(")
|
||||
paths[i] = strings.TrimSuffix(paths[i], ")")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if match := protocolNameRe.FindStringSubmatch(route); len(match) >= 2 {
|
||||
protocolName = strings.TrimSpace(match[1])
|
||||
if routePreferred {
|
||||
protocolName = protocolName + "*"
|
||||
}
|
||||
}
|
||||
|
||||
if len(paths) == 0 {
|
||||
graph.AddEdge(server, target, strings.TrimSpace(protocolName+"\n"+via), makeEdgeAttrs(routePreferred))
|
||||
continue
|
||||
}
|
||||
|
||||
// Edges between AS
|
||||
for i := range paths {
|
||||
var src string
|
||||
var label string
|
||||
// Only show nexthop information on the first hop
|
||||
if i == 0 {
|
||||
src = server
|
||||
label = strings.TrimSpace(protocolName + "\n" + via)
|
||||
} else {
|
||||
// Edge from originating server to AS
|
||||
addEdge(server, getASNRepresentation(paths[0]), (map[bool]string{true: "[color=red]"})[routePreferred])
|
||||
routeFound = true
|
||||
src = paths[i-1]
|
||||
label = ""
|
||||
}
|
||||
dst := paths[i]
|
||||
|
||||
graph.AddEdge(src, dst, label, makeEdgeAttrs(routePreferred))
|
||||
// Only set color for next step, origin color is set to blue above
|
||||
graph.AddPoint(dst, true, makePointAttrs(routePreferred))
|
||||
}
|
||||
|
||||
// Following steps, edges between AS
|
||||
for pathIndex := range paths {
|
||||
if pathIndex == 0 {
|
||||
continue
|
||||
}
|
||||
addEdge(getASNRepresentation(paths[pathIndex-1]), getASNRepresentation(paths[pathIndex]), (map[bool]string{true: "[color=red]"})[routePreferred])
|
||||
}
|
||||
// Last AS to destination
|
||||
addEdge(getASNRepresentation(paths[len(paths)-1]), "Target: "+target, (map[bool]string{true: "[color=red]"})[routePreferred])
|
||||
}
|
||||
|
||||
if !routeFound {
|
||||
// Cannot find a path starting from this server
|
||||
addEdge(server, "Target: "+target, "[color=gray,label=\"?\"]")
|
||||
src := paths[len(paths)-1]
|
||||
graph.AddEdge(src, target, "", makeEdgeAttrs(routePreferred))
|
||||
}
|
||||
}
|
||||
|
||||
// Combine all graphviz commands
|
||||
var result string
|
||||
for edge, attr := range graph {
|
||||
result += edge + " " + attr + ";\n"
|
||||
return graph
|
||||
}
|
||||
return "digraph {\n" + result + "}\n"
|
||||
|
||||
func birdRouteToGraphviz(servers []string, responses []string, targetName string) string {
|
||||
graph := birdRouteToGraph(servers, responses, targetName)
|
||||
return graph.ToGraphviz()
|
||||
}
|
||||
|
||||
173
frontend/bgpmap_graph.go
Normal file
173
frontend/bgpmap_graph.go
Normal file
@@ -0,0 +1,173 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type RouteAttrs map[string]string
|
||||
|
||||
type RoutePoint struct {
|
||||
performLookup bool
|
||||
attrs RouteAttrs
|
||||
}
|
||||
|
||||
type RouteEdgeKey struct {
|
||||
src string
|
||||
dest string
|
||||
}
|
||||
|
||||
type RouteEdgeValue struct {
|
||||
label []string
|
||||
attrs RouteAttrs
|
||||
}
|
||||
|
||||
type RouteGraph struct {
|
||||
points map[string]RoutePoint
|
||||
edges map[RouteEdgeKey]RouteEdgeValue
|
||||
}
|
||||
|
||||
func makeRouteGraph() RouteGraph {
|
||||
return RouteGraph{
|
||||
points: make(map[string]RoutePoint),
|
||||
edges: make(map[RouteEdgeKey]RouteEdgeValue),
|
||||
}
|
||||
}
|
||||
|
||||
func makeRoutePoint() RoutePoint {
|
||||
return RoutePoint{
|
||||
performLookup: false,
|
||||
attrs: make(RouteAttrs),
|
||||
}
|
||||
}
|
||||
|
||||
func makeRouteEdgeValue() RouteEdgeValue {
|
||||
return RouteEdgeValue{
|
||||
label: []string{},
|
||||
attrs: make(RouteAttrs),
|
||||
}
|
||||
}
|
||||
|
||||
func (graph *RouteGraph) attrsToString(attrs RouteAttrs) string {
|
||||
if len(attrs) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
result := ""
|
||||
isFirst := true
|
||||
for k, v := range attrs {
|
||||
if isFirst {
|
||||
isFirst = false
|
||||
} else {
|
||||
result += ","
|
||||
}
|
||||
result += graph.escape(k) + "=" + graph.escape(v) + ""
|
||||
}
|
||||
|
||||
return "[" + result + "]"
|
||||
}
|
||||
|
||||
func (graph *RouteGraph) escape(s string) string {
|
||||
result, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
} else {
|
||||
return string(result)
|
||||
}
|
||||
}
|
||||
|
||||
func (graph *RouteGraph) AddEdge(src string, dest string, label string, attrs RouteAttrs) {
|
||||
// Add edges with same src/dest separately, multiple edges with same src/dest could exist
|
||||
edge := RouteEdgeKey{
|
||||
src: src,
|
||||
dest: dest,
|
||||
}
|
||||
|
||||
newValue, exists := graph.edges[edge]
|
||||
if !exists {
|
||||
newValue = makeRouteEdgeValue()
|
||||
}
|
||||
|
||||
if len(label) != 0 {
|
||||
newValue.label = append(newValue.label, label)
|
||||
}
|
||||
for k, v := range attrs {
|
||||
newValue.attrs[k] = v
|
||||
}
|
||||
|
||||
graph.edges[edge] = newValue
|
||||
}
|
||||
|
||||
func (graph *RouteGraph) AddPoint(name string, performLookup bool, attrs RouteAttrs) {
|
||||
newValue, exists := graph.points[name]
|
||||
if !exists {
|
||||
newValue = makeRoutePoint()
|
||||
}
|
||||
|
||||
newValue.performLookup = performLookup
|
||||
for k, v := range attrs {
|
||||
newValue.attrs[k] = v
|
||||
}
|
||||
|
||||
graph.points[name] = newValue
|
||||
}
|
||||
|
||||
func (graph *RouteGraph) GetEdge(src string, dest string) *RouteEdgeValue {
|
||||
key := RouteEdgeKey{
|
||||
src: src,
|
||||
dest: dest,
|
||||
}
|
||||
value, ok := graph.edges[key]
|
||||
if ok {
|
||||
return &value
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (graph *RouteGraph) GetPoint(name string) *RoutePoint {
|
||||
value, ok := graph.points[name]
|
||||
if ok {
|
||||
return &value
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (graph *RouteGraph) ToGraphviz() string {
|
||||
var result string
|
||||
|
||||
asnCache := make(ASNCache)
|
||||
|
||||
for name, value := range graph.points {
|
||||
var representation string
|
||||
|
||||
if value.performLookup {
|
||||
representation = asnCache.Lookup(name)
|
||||
} else {
|
||||
representation = name
|
||||
}
|
||||
|
||||
attrsCopy := value.attrs
|
||||
if attrsCopy == nil {
|
||||
attrsCopy = make(RouteAttrs)
|
||||
}
|
||||
attrsCopy["label"] = representation
|
||||
|
||||
result += fmt.Sprintf("%s %s;\n", graph.escape(name), graph.attrsToString(value.attrs))
|
||||
}
|
||||
|
||||
for key, value := range graph.edges {
|
||||
attrsCopy := value.attrs
|
||||
if attrsCopy == nil {
|
||||
attrsCopy = make(RouteAttrs)
|
||||
}
|
||||
if len(value.label) > 0 {
|
||||
attrsCopy["label"] = strings.Join(value.label, "\n")
|
||||
}
|
||||
result += fmt.Sprintf("%s -> %s %s;\n", graph.escape(key.src), graph.escape(key.dest), graph.attrsToString(attrsCopy))
|
||||
}
|
||||
|
||||
return "digraph {\n" + result + "}\n"
|
||||
}
|
||||
@@ -1,58 +1,23 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"path"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetASNRepresentation(t *testing.T) {
|
||||
setting.dnsInterface = "asn.cymru.com"
|
||||
result := getASNRepresentation("6939")
|
||||
if !strings.Contains(result, "HURRICANE") {
|
||||
t.Errorf("Lookup AS6939 failed, got %s", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetASNRepresentationFallback(t *testing.T) {
|
||||
setting.dnsInterface = ""
|
||||
result := getASNRepresentation("6939")
|
||||
if result != "AS6939" {
|
||||
t.Errorf("Lookup AS6939 failed, got %s", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBirdRouteToGraphviz(t *testing.T) {
|
||||
setting.dnsInterface = ""
|
||||
|
||||
// Don't change formatting of the following strings!
|
||||
|
||||
fakeResult := `192.168.0.1/32 unicast [alpha 2021-01-14 from 192.168.0.2] * (100) [AS12345i]
|
||||
via 192.168.0.2 on eth0
|
||||
Type: BGP univ
|
||||
BGP.origin: IGP
|
||||
BGP.as_path: 4242422601
|
||||
BGP.next_hop: 172.18.0.2`
|
||||
|
||||
expectedResult := `digraph {
|
||||
"Nexthop:\n172.18.0.2" -> "AS4242422601" [color=red];
|
||||
"Nexthop:\n172.18.0.2" [shape=diamond];
|
||||
"AS4242422601" -> "Target: 192.168.0.1" [color=red];
|
||||
"Target: 192.168.0.1" [color=red,shape=diamond];
|
||||
"alpha" [color=blue,shape=box];
|
||||
"alpha" -> "Nexthop:\n172.18.0.2" [color=red];
|
||||
}`
|
||||
|
||||
result := birdRouteToGraphviz([]string{
|
||||
"alpha",
|
||||
}, []string{
|
||||
fakeResult,
|
||||
}, "192.168.0.1")
|
||||
|
||||
for _, line := range strings.Split(result, "\n") {
|
||||
if !strings.Contains(expectedResult, line) {
|
||||
t.Errorf("Unexpected line in result: %s", line)
|
||||
}
|
||||
func readDataFile(t *testing.T, filename string) string {
|
||||
_, sourceName, _, _ := runtime.Caller(0)
|
||||
projectRoot := path.Join(path.Dir(sourceName), "..")
|
||||
dir := path.Join(projectRoot, filename)
|
||||
|
||||
data, err := ioutil.ReadFile(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func TestBirdRouteToGraphvizXSS(t *testing.T) {
|
||||
@@ -72,3 +37,48 @@ func TestBirdRouteToGraphvizXSS(t *testing.T) {
|
||||
t.Errorf("XSS injection succeeded: %s", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBirdRouteToGraph(t *testing.T) {
|
||||
setting.dnsInterface = ""
|
||||
|
||||
input := readDataFile(t, "frontend/test_data/bgpmap_case1.txt")
|
||||
result := birdRouteToGraph([]string{"node"}, []string{input}, "target")
|
||||
|
||||
// Source node must exist
|
||||
if result.GetPoint("node") == nil {
|
||||
t.Error("Result doesn't contain point node")
|
||||
}
|
||||
// Last hop must exist
|
||||
if result.GetPoint("4242423914") == nil {
|
||||
t.Error("Result doesn't contain point 4242423914")
|
||||
}
|
||||
// Destination must exist
|
||||
if result.GetPoint("target") == nil {
|
||||
t.Error("Result doesn't contain point target")
|
||||
}
|
||||
|
||||
// Verify that a few paths exist
|
||||
if result.GetEdge("node", "4242423914") == nil {
|
||||
t.Error("Result doesn't contain edge from node to 4242423914")
|
||||
}
|
||||
if result.GetEdge("node", "4242422688") == nil {
|
||||
t.Error("Result doesn't contain edge from node to 4242422688")
|
||||
}
|
||||
if result.GetEdge("4242422688", "4242423914") == nil {
|
||||
t.Error("Result doesn't contain edge from 4242422688 to 4242423914")
|
||||
}
|
||||
if result.GetEdge("4242423914", "target") == nil {
|
||||
t.Error("Result doesn't contain edge from 4242423914 to target")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBirdRouteToGraphviz(t *testing.T) {
|
||||
setting.dnsInterface = ""
|
||||
|
||||
input := readDataFile(t, "frontend/test_data/bgpmap_case1.txt")
|
||||
result := birdRouteToGraphviz([]string{"node"}, []string{input}, "target")
|
||||
|
||||
if !strings.Contains(result, "digraph {") {
|
||||
t.Error("Response is not Graphviz data")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,3 +49,53 @@ func dn42WhoisFilter(whois string) string {
|
||||
return commandResult
|
||||
}
|
||||
}
|
||||
|
||||
/* experimental, behavior may change */
|
||||
func shortenWhoisFilter(whois string) string {
|
||||
commandResult := ""
|
||||
commandResultLonger := ""
|
||||
lines := 0
|
||||
linesLonger := 0
|
||||
skippedLines := 0
|
||||
skippedLinesLonger := 0
|
||||
|
||||
for _, s := range strings.Split(whois, "\n") {
|
||||
s = strings.TrimSpace(s)
|
||||
|
||||
shouldSkip := false
|
||||
shouldSkip = shouldSkip || len(s) == 0
|
||||
shouldSkip = shouldSkip || len(s) > 0 && s[0] == '#'
|
||||
shouldSkip = shouldSkip || strings.Contains(strings.ToUpper(s), "REDACTED")
|
||||
|
||||
if shouldSkip {
|
||||
skippedLinesLonger++
|
||||
continue
|
||||
}
|
||||
|
||||
commandResultLonger += s + "\n"
|
||||
linesLonger++
|
||||
|
||||
shouldSkip = shouldSkip || len(s) > 80
|
||||
shouldSkip = shouldSkip || !strings.Contains(s, ":")
|
||||
shouldSkip = shouldSkip || strings.Index(s, ":") > 20
|
||||
|
||||
if shouldSkip {
|
||||
skippedLines++
|
||||
continue
|
||||
}
|
||||
|
||||
commandResult += s + "\n"
|
||||
lines++
|
||||
}
|
||||
|
||||
if lines < 5 {
|
||||
commandResult = commandResultLonger
|
||||
skippedLines = skippedLinesLonger
|
||||
}
|
||||
|
||||
if skippedLines > 0 {
|
||||
return commandResult + fmt.Sprintf("\n%d line(s) skipped.\n", skippedLines)
|
||||
} else {
|
||||
return commandResult
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,3 +28,79 @@ func TestDN42WhoisFilterUnneeded(t *testing.T) {
|
||||
t.Errorf("Output doesn't match expected: %s", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShortenWhoisFilterShorterMode(t *testing.T) {
|
||||
input := `
|
||||
Information line that will be removed
|
||||
|
||||
# Comment that will be removed
|
||||
Name: Redacted for privacy
|
||||
Descr: This is a vvvvvvvvvvvvvvvvvvvvvvveeeeeeeeeeeeeeeeeeeerrrrrrrrrrrrrrrrrrrrrrrryyyyyyyyyyyyyyyyyyy long line that will be skipped.
|
||||
Looooooooooooooooooooooong key: this line will be skipped.
|
||||
|
||||
Preserved1: this line isn't removed.
|
||||
Preserved2: this line isn't removed.
|
||||
Preserved3: this line isn't removed.
|
||||
Preserved4: this line isn't removed.
|
||||
Preserved5: this line isn't removed.
|
||||
|
||||
`
|
||||
|
||||
result := shortenWhoisFilter(input)
|
||||
|
||||
expectedResult := `Preserved1: this line isn't removed.
|
||||
Preserved2: this line isn't removed.
|
||||
Preserved3: this line isn't removed.
|
||||
Preserved4: this line isn't removed.
|
||||
Preserved5: this line isn't removed.
|
||||
|
||||
3 line(s) skipped.
|
||||
`
|
||||
|
||||
if result != expectedResult {
|
||||
t.Errorf("Output doesn't match expected: %s", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShortenWhoisFilterLongerMode(t *testing.T) {
|
||||
input := `
|
||||
Information line that will be removed
|
||||
|
||||
# Comment that will be removed
|
||||
Name: Redacted for privacy
|
||||
Descr: This is a vvvvvvvvvvvvvvvvvvvvvvveeeeeeeeeeeeeeeeeeeerrrrrrrrrrrrrrrrrrrrrrrryyyyyyyyyyyyyyyyyyy long line that will be skipped.
|
||||
Looooooooooooooooooooooong key: this line will be skipped.
|
||||
|
||||
Preserved1: this line isn't removed.
|
||||
|
||||
`
|
||||
|
||||
result := shortenWhoisFilter(input)
|
||||
|
||||
expectedResult := `Information line that will be removed
|
||||
Descr: This is a vvvvvvvvvvvvvvvvvvvvvvveeeeeeeeeeeeeeeeeeeerrrrrrrrrrrrrrrrrrrrrrrryyyyyyyyyyyyyyyyyyy long line that will be skipped.
|
||||
Looooooooooooooooooooooong key: this line will be skipped.
|
||||
Preserved1: this line isn't removed.
|
||||
|
||||
7 line(s) skipped.
|
||||
`
|
||||
|
||||
if result != expectedResult {
|
||||
t.Errorf("Output doesn't match expected: %s", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShortenWhoisFilterSkipNothing(t *testing.T) {
|
||||
input := `Preserved1: this line isn't removed.
|
||||
Preserved2: this line isn't removed.
|
||||
Preserved3: this line isn't removed.
|
||||
Preserved4: this line isn't removed.
|
||||
Preserved5: this line isn't removed.
|
||||
`
|
||||
|
||||
result := shortenWhoisFilter(input)
|
||||
|
||||
if result != input {
|
||||
t.Errorf("Output doesn't match expected: %s", result)
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user