You've already forked bird-lg-go
mirror of
https://github.com/xddxdd/bird-lg-go
synced 2025-10-24 04:42:12 +02:00
Compare commits
131 Commits
dependabot
...
v1.3.7.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01438edaef | ||
|
|
90f36610dc | ||
|
|
6174208d07 | ||
|
|
76174cdc08 | ||
|
|
088bb6fe5a | ||
|
|
3951eed011 | ||
|
|
91c0a8962b | ||
|
|
5f7850a903 | ||
|
|
6a78cf2e80 | ||
|
|
5b5a44bcb6 | ||
|
|
ac31862237 | ||
|
|
86129190ab | ||
|
|
ff55064a20 | ||
|
|
dbb02c04ed | ||
|
|
c2b7de2e17 | ||
|
|
c1b578e8db | ||
|
|
7b0e5689d4 | ||
|
|
3c46bda49d | ||
|
|
32e00d2ce3 | ||
|
|
a19750cdef | ||
|
|
7f1cdaa4ee | ||
|
|
2d2193041e | ||
|
|
aad8ee98d7 | ||
|
|
00b5c12787 | ||
|
|
55a1eb54fd | ||
|
|
0594edc69d | ||
|
|
38bf6aba09 | ||
|
|
d261c22235 | ||
|
|
19aa8c77c5 | ||
|
|
fe07ebb5a5 | ||
|
|
66547ebfa9 | ||
|
|
d253e4311b | ||
|
|
026498ba2f | ||
|
|
27c348a864 | ||
|
|
43b4ad93dd | ||
|
|
6176c45006 | ||
|
|
47113184f4 | ||
|
|
3c9a3e4339 | ||
|
|
8457b18d46 | ||
|
|
f8f64b03a6 | ||
|
|
cc818c1cc0 | ||
|
|
6224b43808 | ||
|
|
17e0b14243 | ||
|
|
b4c1bed9ba | ||
|
|
abb32abff3 | ||
|
|
b368c75aa3 | ||
|
|
09405cdb38 | ||
|
|
f999d47d9f | ||
|
|
005dfb1435 | ||
|
|
4bd7a6bb95 | ||
|
|
462d76a2d0 | ||
|
|
58f217578c | ||
|
|
0e95727de1 | ||
|
|
a48f1c8040 | ||
|
|
81acde3a37 | ||
|
|
7c0fe0d512 | ||
|
|
a5f4452d02 | ||
|
|
b237185ef7 | ||
|
|
e949646790 | ||
|
|
bb479d22ae | ||
|
|
d40f41b4d5 | ||
|
|
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 |
@@ -1,61 +0,0 @@
|
|||||||
version: 2.1
|
|
||||||
|
|
||||||
workflows:
|
|
||||||
docker:
|
|
||||||
jobs:
|
|
||||||
- build
|
|
||||||
- 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
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
docker:
|
|
||||||
- image: circleci/golang:1.15
|
|
||||||
working_directory: /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
|
|
||||||
command: |
|
|
||||||
sudo apt-get update && sudo apt-get install -y gpp
|
|
||||||
- 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
|
|
||||||
16
.github/workflows/auto-merge.yml
vendored
Normal file
16
.github/workflows/auto-merge.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
name: auto-merge
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
auto-merge:
|
||||||
|
name: Dependabot Auto Merge
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.actor == 'dependabot[bot]' || github.actor == 'dependabot-preview[bot]'
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: ahmadnassri/action-dependabot-auto-merge@v2
|
||||||
|
with:
|
||||||
|
target: minor
|
||||||
|
github-token: ${{ secrets.AUTOMERGE_TOKEN }}
|
||||||
127
.github/workflows/develop.yaml
vendored
Normal file
127
.github/workflows/develop.yaml
vendored
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'master'
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- 'master'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
go-test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Setup Golang
|
||||||
|
uses: actions/setup-go@v4
|
||||||
|
|
||||||
|
- name: Run frontend unit test
|
||||||
|
run: |
|
||||||
|
export GO111MODULE=on
|
||||||
|
cd frontend
|
||||||
|
go get -v -t -d ./...
|
||||||
|
go test -v ./...
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
- name: Run proxy unit test
|
||||||
|
run: |
|
||||||
|
export GO111MODULE=on
|
||||||
|
cd proxy
|
||||||
|
go get -v -t -d ./...
|
||||||
|
go test -v ./...
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
docker-test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
- name: Test whois binary in frontend image
|
||||||
|
run: |
|
||||||
|
docker build -t local/frontend frontend/
|
||||||
|
docker run --rm --net host --entrypoint whois local/frontend -I github.com || exit 1
|
||||||
|
docker run --rm --net host --entrypoint whois local/frontend -h whois.ripe.net github.com || exit 1
|
||||||
|
docker run --rm --net host --entrypoint whois local/frontend -h whois.ripe.net:43 github.com || exit 1
|
||||||
|
|
||||||
|
- name: Test traceroute binary in proxy image
|
||||||
|
run: |
|
||||||
|
docker build -t local/proxy proxy/
|
||||||
|
docker run --rm --net host --entrypoint traceroute local/proxy 127.0.0.1 || exit 1
|
||||||
|
docker run --rm --net host --entrypoint traceroute local/proxy ::1 || exit 1
|
||||||
|
|
||||||
|
- name: Test mtr binary in proxy image
|
||||||
|
run: |
|
||||||
|
docker build -t local/proxy:mtr -f proxy/Dockerfile.mtr proxy/
|
||||||
|
docker run --rm --net host --entrypoint mtr local/proxy:mtr -w -c1 -Z1 -G1 -b 127.0.0.1 || exit 1
|
||||||
|
docker run --rm --net host --entrypoint mtr local/proxy:mtr -w -c1 -Z1 -G1 -b ::1 || exit 1
|
||||||
|
|
||||||
|
docker-develop:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- go-test
|
||||||
|
- docker-test
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
steps:
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build frontend docker image
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: '{{defaultContext}}:frontend'
|
||||||
|
platforms: linux/amd64,linux/arm64,linux/386,linux/arm/v7
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
xddxdd/bird-lg-go:develop
|
||||||
|
xddxdd/bird-lg-go:develop-${{ github.sha }}
|
||||||
|
ghcr.io/xddxdd/bird-lg-go:frontend-develop
|
||||||
|
ghcr.io/xddxdd/bird-lg-go:frontend-develop-${{ github.sha }}
|
||||||
|
|
||||||
|
- name: Build proxy docker image
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: '{{defaultContext}}:proxy'
|
||||||
|
platforms: linux/amd64,linux/arm64,linux/386,linux/arm/v7
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
xddxdd/bird-lgproxy-go:develop
|
||||||
|
xddxdd/bird-lgproxy-go:develop-${{ github.sha }}
|
||||||
|
ghcr.io/xddxdd/bird-lg-go:proxy-develop
|
||||||
|
ghcr.io/xddxdd/bird-lg-go:proxy-develop-${{ github.sha }}
|
||||||
|
|
||||||
|
- name: Build proxy docker image
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: '{{defaultContext}}:proxy'
|
||||||
|
file: 'Dockerfile.mtr'
|
||||||
|
platforms: linux/amd64,linux/arm64,linux/386,linux/arm/v7
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
xddxdd/bird-lgproxy-go:develop-mtr
|
||||||
|
xddxdd/bird-lgproxy-go:develop-${{ github.sha }}-mtr
|
||||||
|
ghcr.io/xddxdd/bird-lg-go:proxy-develop-mtr
|
||||||
|
ghcr.io/xddxdd/bird-lg-go:proxy-develop-${{ github.sha }}-mtr
|
||||||
100
.github/workflows/release.yaml
vendored
Normal file
100
.github/workflows/release.yaml
vendored
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [created]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
go-release:
|
||||||
|
name: Release Go Binary
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
goos: [linux, windows, darwin]
|
||||||
|
goarch: ["386", amd64, "arm", arm64]
|
||||||
|
exclude:
|
||||||
|
- goarch: "386"
|
||||||
|
goos: darwin
|
||||||
|
- goarch: "arm"
|
||||||
|
goos: darwin
|
||||||
|
- goarch: "arm"
|
||||||
|
goos: windows
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Release frontend
|
||||||
|
uses: wangyoucao577/go-release-action@v1.40
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
goos: ${{ matrix.goos }}
|
||||||
|
goarch: ${{ matrix.goarch }}
|
||||||
|
project_path: "./frontend"
|
||||||
|
binary_name: "bird-lg-go"
|
||||||
|
|
||||||
|
- name: Release proxy
|
||||||
|
uses: wangyoucao577/go-release-action@v1.40
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
goos: ${{ matrix.goos }}
|
||||||
|
goarch: ${{ matrix.goarch }}
|
||||||
|
project_path: "./proxy"
|
||||||
|
binary_name: "bird-lgproxy-go"
|
||||||
|
|
||||||
|
docker-release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build frontend docker image
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: '{{defaultContext}}:frontend'
|
||||||
|
platforms: linux/amd64,linux/arm64,linux/386,linux/arm/v7
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
xddxdd/bird-lg-go:latest
|
||||||
|
xddxdd/bird-lg-go:${{ github.event.release.tag_name }}
|
||||||
|
ghcr.io/xddxdd/bird-lg-go:frontend
|
||||||
|
ghcr.io/xddxdd/bird-lg-go:frontend-${{ github.event.release.tag_name }}
|
||||||
|
|
||||||
|
- name: Build proxy docker image
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: '{{defaultContext}}:proxy'
|
||||||
|
platforms: linux/amd64,linux/arm64,linux/386,linux/arm/v7
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
xddxdd/bird-lgproxy-go:latest
|
||||||
|
xddxdd/bird-lgproxy-go:${{ github.event.release.tag_name }}
|
||||||
|
ghcr.io/xddxdd/bird-lg-go:proxy
|
||||||
|
ghcr.io/xddxdd/bird-lg-go:proxy-${{ github.event.release.tag_name }}
|
||||||
|
|
||||||
|
- name: Build proxy docker image
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: '{{defaultContext}}:proxy'
|
||||||
|
file: 'Dockerfile.mtr'
|
||||||
|
platforms: linux/amd64,linux/arm64,linux/386,linux/arm/v7
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
xddxdd/bird-lgproxy-go:latest-mtr
|
||||||
|
xddxdd/bird-lgproxy-go:${{ github.event.release.tag_name }}-mtr
|
||||||
|
ghcr.io/xddxdd/bird-lg-go:proxy-mtr
|
||||||
|
ghcr.io/xddxdd/bird-lg-go:proxy-${{ github.event.release.tag_name }}-mtr
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -20,7 +20,3 @@ proxy/proxy
|
|||||||
|
|
||||||
# don't include generated bindata file
|
# don't include generated bindata file
|
||||||
frontend/bindata.go
|
frontend/bindata.go
|
||||||
|
|
||||||
# don't include generated Dockerfiles
|
|
||||||
frontend/Dockerfile.*
|
|
||||||
proxy/Dockerfile.*
|
|
||||||
4
Makefile
4
Makefile
@@ -9,5 +9,5 @@ proxy:
|
|||||||
all: frontend proxy
|
all: frontend proxy
|
||||||
|
|
||||||
install:
|
install:
|
||||||
install -m 755 frontend/frontend /usr/local/bin/frontend
|
install -m 755 frontend/frontend /usr/local/bin/bird-lg-go
|
||||||
install -m 755 proxy/proxy /usr/local/bin/proxy
|
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")}
|
|
||||||
@@ -153,9 +153,11 @@ Request:
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"servers": [],
|
"servers": [
|
||||||
"type": "server_list",
|
"alpha"
|
||||||
"args": ""
|
],
|
||||||
|
"type": "bird",
|
||||||
|
"args": "show status"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -179,11 +181,9 @@ Request:
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"servers": [
|
"servers": [],
|
||||||
"alpha"
|
"type": "server_list",
|
||||||
],
|
"args": ""
|
||||||
"type": "bird",
|
|
||||||
"args": "show status"
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
||||||
33
frontend/Dockerfile
Normal file
33
frontend/Dockerfile
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
FROM golang AS step_0
|
||||||
|
ENV CGO_ENABLED=0 GO111MODULE=on
|
||||||
|
WORKDIR /root
|
||||||
|
COPY . .
|
||||||
|
RUN go build -ldflags "-w -s" -o /frontend
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
FROM alpine:edge AS step_1
|
||||||
|
|
||||||
|
WORKDIR /root
|
||||||
|
RUN apk add --no-cache build-base pkgconf perl gettext \
|
||||||
|
libidn2-dev libidn2-static libunistring-dev libunistring-static gnu-libiconv-dev
|
||||||
|
|
||||||
|
RUN wget https://github.com/rfc1036/whois/archive/refs/tags/v5.5.18.tar.gz \
|
||||||
|
-O whois-5.5.18.tar.gz
|
||||||
|
|
||||||
|
RUN tar xvf whois-5.5.18.tar.gz \
|
||||||
|
&& cd whois-5.5.18 \
|
||||||
|
&& sed -i "s/#if defined _POSIX_C_SOURCE && _POSIX_C_SOURCE >= 200112L/#if 1/g" config.h \
|
||||||
|
&& make whois -j4 \
|
||||||
|
LDFLAGS="-static" CONFIG_FILE="/etc/whois.conf" PKG_CONFIG="pkg-config --static" HAVE_ICONV=1 \
|
||||||
|
&& strip /root/whois-5.5.18/whois
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
FROM scratch AS step_2
|
||||||
|
ENV PATH=/
|
||||||
|
ENV BIRDLG_WHOIS=/whois
|
||||||
|
COPY --from=step_0 /frontend /
|
||||||
|
COPY --from=step_1 /root/whois-5.5.18/whois /
|
||||||
|
COPY --from=step_1 /etc/services /etc/services
|
||||||
|
ENTRYPOINT ["/frontend"]
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
.PHONY: all
|
.PHONY: all
|
||||||
all:
|
all:
|
||||||
go get -u github.com/kevinburke/go-bindata/...
|
|
||||||
go generate
|
|
||||||
go build -ldflags "-w -s" -o frontend
|
go build -ldflags "-w -s" -o frontend
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ func apiHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
} else {
|
} else {
|
||||||
handler := apiHandlerMap[request.Type]
|
handler := apiHandlerMap[request.Type]
|
||||||
if handler == nil {
|
if handler == nil {
|
||||||
response = apiErrorHandler(errors.New("Invalid request type"))
|
response = apiErrorHandler(errors.New("invalid request type"))
|
||||||
} else {
|
} else {
|
||||||
response = handler(request)
|
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 id="bgpmap">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/viz.js@2.1.2/viz.min.js" crossorigin="anonymous"></script>
|
<script src="/static/jsdelivr/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/lite.render.js" crossorigin="anonymous"></script>
|
||||||
<script>
|
<script>
|
||||||
var viz = new Viz();
|
var viz = new Viz();
|
||||||
viz.renderSVGElement(`{{ .Result }}`)
|
viz.renderSVGElement(atob({{ .Result }}))
|
||||||
.then(element => {
|
.then(element => {
|
||||||
document.getElementById("bgpmap").appendChild(element);
|
document.getElementById("bgpmap").appendChild(element);
|
||||||
})
|
})
|
||||||
@@ -1,18 +1,32 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en-US">
|
<html lang="en-US">
|
||||||
<head>
|
<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="Content-Type" content="text/html;charset=UTF-8">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<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="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||||
<meta name="renderer" content="webkit">
|
<meta name="renderer" content="webkit">
|
||||||
<title>{{ html .Title }}</title>
|
<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">
|
||||||
|
<style>
|
||||||
|
.navbar-nav {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.navbar form {
|
||||||
|
min-width: 400px;
|
||||||
|
}
|
||||||
|
.nav-link {
|
||||||
|
padding: 0.2rem 0.5rem !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
<meta name="robots" content="noindex, nofollow">
|
<meta name="robots" content="noindex, nofollow">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
<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">
|
<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>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
@@ -28,13 +42,24 @@
|
|||||||
{{ end }}
|
{{ end }}
|
||||||
<ul class="navbar-nav mr-auto">
|
<ul class="navbar-nav mr-auto">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
|
{{ if eq .AllServersURLCustom "all" }}
|
||||||
<a class="nav-link{{ if .AllServersLinkActive }} active{{ end }}"
|
<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>
|
</li>
|
||||||
{{ range $k, $v := .ServersEscaped }}
|
{{ $length := len .Servers }}
|
||||||
|
{{ range $k, $v := .Servers }}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
|
{{ if gt $length 1 }}
|
||||||
<a class="nav-link{{ if eq $server $v }} active{{ end }}"
|
<a class="nav-link{{ if eq $server $v }} active{{ end }}"
|
||||||
href="/{{ $option }}/{{ $v }}/{{ $target }}">{{ html (index $.ServersDisplay $k) }}</a>
|
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>
|
</li>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -48,7 +73,7 @@
|
|||||||
<option value="{{ html $k }}"{{ if eq $k $.URLOption }} selected{{end}}>{{ html $v }}</option>
|
<option value="{{ html $k }}"{{ if eq $k $.URLOption }} selected{{end}}>{{ html $v }}</option>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</select>
|
</select>
|
||||||
<input name="server" class="d-none" value="{{ html $server }}">
|
<input name="server" class="d-none" value="{{ html ($server | pathescape) }}">
|
||||||
<input name="target" class="form-control" placeholder="Target" aria-label="Target" value="{{ html $target }}">
|
<input name="target" class="form-control" placeholder="Target" aria-label="Target" value="{{ html $target }}">
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
<button class="btn btn-outline-success" type="submit">»</button>
|
<button class="btn btn-outline-success" type="submit">»</button>
|
||||||
@@ -62,8 +87,9 @@
|
|||||||
{{ .Content }}
|
{{ .Content }}
|
||||||
</div>
|
</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="/static/jsdelivr/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/bootstrap@4.5.1/dist/js/bootstrap.min.js" integrity="sha256-0IiaoZCI++9oAAvmCb5Y0r93XkuhvJpRalZLffQXLok=" crossorigin="anonymous"></script>
|
||||||
|
<script src="/static/sortTable.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function goto() {
|
function goto() {
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{{ $ServerName := urlquery .ServerName }}
|
{{ $ServerName := urlquery .ServerName }}
|
||||||
|
|
||||||
<table class="table table-striped table-bordered table-sm">
|
<table class="table table-striped table-bordered table-sm sortable">
|
||||||
<thead>
|
<thead>
|
||||||
{{ range .Header }}
|
{{ range .Header }}
|
||||||
<th scope="col">{{ html . }}</th>
|
<th scope="col">{{ html . }}</th>
|
||||||
@@ -1,122 +1,121 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"regexp"
|
||||||
"html"
|
|
||||||
"net"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getASNRepresentation(asn string) string {
|
// The protocol name for each route (e.g. "ibgp_sea02") is encoded in the form:
|
||||||
records, err := net.LookupTXT(fmt.Sprintf("AS%s.%s", asn, setting.dnsInterface))
|
//
|
||||||
if err != nil {
|
// unicast [ibgp_sea02 2021-08-27 from fd86:bad:11b7:1::1] * (100/1015) [i]
|
||||||
// DNS query failed, only use ASN as output
|
var protocolNameRe = regexp.MustCompile(`\[(.*?) .*\]`)
|
||||||
return fmt.Sprintf("AS%s", asn)
|
|
||||||
}
|
|
||||||
|
|
||||||
result := strings.Join(records, " ")
|
// Try to split the output into one chunk for each route.
|
||||||
if resultSplit := strings.Split(result, " | "); len(resultSplit) > 1 {
|
// Possible values are defined at https://gitlab.nic.cz/labs/bird/-/blob/v2.0.8/nest/rt-attr.c#L81-87
|
||||||
result = strings.Join(resultSplit[1:], "\\n")
|
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",
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("AS%s\\n%s", asn, result)
|
if preferred {
|
||||||
|
result["color"] = "red"
|
||||||
|
}
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func birdRouteToGraphviz(servers []string, responses []string, target string) string {
|
func makePointAttrs(preferred bool) RouteAttrs {
|
||||||
graph := make(map[string]string)
|
result := RouteAttrs{}
|
||||||
// Helper to add an edge
|
if preferred {
|
||||||
addEdge := func(src string, dest string, attr string) {
|
result["color"] = "red"
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
for serverID, server := range servers {
|
||||||
response := responses[serverID]
|
response := responses[serverID]
|
||||||
if len(response) == 0 {
|
if len(response) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
addPoint(server, "[color=blue,shape=box]")
|
graph.AddPoint(server, false, RouteAttrs{"color": "blue", "shape": "box"})
|
||||||
// This is the best split point I can find for bird2
|
routes := routeSplitRe.Split(response, -1)
|
||||||
routes := strings.Split(response, "\tvia ")
|
|
||||||
routeFound := false
|
|
||||||
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") {
|
for routeIndex, route := range routes {
|
||||||
if strings.HasPrefix(routeParameter, "\tBGP.next_hop: ") {
|
if routeIndex == 0 {
|
||||||
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
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connect each node on AS path
|
var via string
|
||||||
paths := strings.Split(strings.TrimSpace(routeASPath), " ")
|
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 {
|
if match := routeViaRe.FindStringSubmatch(route); len(match) >= 2 {
|
||||||
paths[pathIndex] = strings.TrimPrefix(paths[pathIndex], "(")
|
via = strings.TrimSpace(match[1])
|
||||||
paths[pathIndex] = strings.TrimSuffix(paths[pathIndex], ")")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// First step starting from originating server
|
if match := routeASPathRe.FindStringSubmatch(route); len(match) >= 2 {
|
||||||
if len(paths) > 0 {
|
pathString := strings.TrimSpace(match[1])
|
||||||
if len(routeNexthop) > 0 {
|
if len(pathString) > 0 {
|
||||||
// Edge from originating server to nexthop
|
paths = strings.Split(strings.TrimSpace(match[1]), " ")
|
||||||
addEdge(server, "Nexthop:\\n"+routeNexthop, (map[bool]string{true: "[color=red]"})[routePreferred])
|
for i := range paths {
|
||||||
// and from nexthop to AS
|
paths[i] = strings.TrimPrefix(paths[i], "(")
|
||||||
addEdge("Nexthop:\\n"+routeNexthop, getASNRepresentation(paths[0]), (map[bool]string{true: "[color=red]"})[routePreferred])
|
paths[i] = strings.TrimSuffix(paths[i], ")")
|
||||||
addPoint("Nexthop:\\n"+routeNexthop, "[shape=diamond]")
|
}
|
||||||
routeFound = true
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
} else {
|
||||||
// Edge from originating server to AS
|
src = paths[i-1]
|
||||||
addEdge(server, getASNRepresentation(paths[0]), (map[bool]string{true: "[color=red]"})[routePreferred])
|
label = ""
|
||||||
routeFound = true
|
|
||||||
}
|
}
|
||||||
|
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
|
// Last AS to destination
|
||||||
addEdge(getASNRepresentation(paths[len(paths)-1]), "Target: "+target, (map[bool]string{true: "[color=red]"})[routePreferred])
|
src := paths[len(paths)-1]
|
||||||
}
|
graph.AddEdge(src, target, "", makeEdgeAttrs(routePreferred))
|
||||||
|
|
||||||
if !routeFound {
|
|
||||||
// Cannot find a path starting from this server
|
|
||||||
addEdge(server, "Target: "+target, "[color=gray,label=\"?\"]")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Combine all graphviz commands
|
return graph
|
||||||
var result string
|
}
|
||||||
for edge, attr := range graph {
|
|
||||||
result += edge + " " + attr + ";\n"
|
func birdRouteToGraphviz(servers []string, responses []string, targetName string) string {
|
||||||
}
|
graph := birdRouteToGraph(servers, responses, targetName)
|
||||||
return "digraph {\n" + result + "}\n"
|
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"
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user