87 Commits

Author SHA1 Message Date
Lan Tian
abb32abff3 general: add unit test for docker images 2023-09-08 18:45:58 -07:00
Lan Tian
b368c75aa3 frontend: fix whois client cannot get default whois port 2023-09-08 18:38:23 -07:00
Lan Tian
09405cdb38 frontend: also print whois client output on error 2023-09-08 18:22:31 -07:00
Lan Tian
f999d47d9f frontend: force enable whois client regex parser on alpine/musl 2023-09-07 19:14:04 -07:00
Lan Tian
005dfb1435 frontend: make docker image whois client try to use config file 2023-09-07 00:51:56 -07:00
Lan Tian
4bd7a6bb95 general: also release docker image to GitHub container registry 2023-09-06 21:06:10 -07:00
Lan Tian
462d76a2d0 general: reenable docker multiarch build 2023-09-06 21:02:32 -07:00
Lan Tian
58f217578c readme: add note about development version of docker image 2023-09-06 20:59:27 -07:00
Lan Tian
0e95727de1 general: reorganize GitHub Actions workflows and readd unit test 2023-09-06 20:55:45 -07:00
Lan Tian
a48f1c8040 general: move Docker image build to GitHub Actions 2023-09-06 20:48:14 -07:00
Lan Tian
81acde3a37 frontend: add whois client for more complex whois lookup 2023-09-06 20:35:30 -07:00
Lan Tian
7c0fe0d512 proxy: update traceroute version in Docker image 2023-09-06 20:33:40 -07:00
dependabot[bot]
a5f4452d02 build(deps): bump github.com/jarcoal/httpmock in /frontend (#82)
Bumps [github.com/jarcoal/httpmock](https://github.com/jarcoal/httpmock) from 1.3.0 to 1.3.1.
- [Release notes](https://github.com/jarcoal/httpmock/releases)
- [Commits](https://github.com/jarcoal/httpmock/compare/v1.3.0...v1.3.1)

---
updated-dependencies:
- dependency-name: github.com/jarcoal/httpmock
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-25 00:44:30 -07:00
Lan Tian
b237185ef7 release: v1.3.1 2023-06-18 20:14:41 -07:00
towalink
e949646790 Properly escape URL path (#81) 2023-06-10 15:14:10 -07:00
dependabot[bot]
bb479d22ae build(deps): bump github.com/spf13/viper from 1.15.0 to 1.16.0 in /proxy (#79)
Bumps [github.com/spf13/viper](https://github.com/spf13/viper) from 1.15.0 to 1.16.0.
- [Release notes](https://github.com/spf13/viper/releases)
- [Commits](https://github.com/spf13/viper/compare/v1.15.0...v1.16.0)

---
updated-dependencies:
- dependency-name: github.com/spf13/viper
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-02 00:58:12 -07:00
dependabot[bot]
d40f41b4d5 build(deps): bump github.com/spf13/viper in /frontend (#80)
Bumps [github.com/spf13/viper](https://github.com/spf13/viper) from 1.15.0 to 1.16.0.
- [Release notes](https://github.com/spf13/viper/releases)
- [Commits](https://github.com/spf13/viper/compare/v1.15.0...v1.16.0)

---
updated-dependencies:
- dependency-name: github.com/spf13/viper
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-02 00:58:06 -07:00
Lan Tian
cdc34704b5 release: v1.3.0 2023-05-14 12:37:46 -07:00
Lan Tian
db58bd3354 frontend: add a test case for lookup DNS -> WHOIS fallback 2023-05-06 00:26:40 -07:00
Lan Tian
a0246ccee2 general: add unit tests for >80% coverage
Includes a few minor fixes:
- frontend: support setting port for WHOIS server
- proxy: fix handling of very long lines
- proxy: refactor IP allowlist logic, parse allow IP list at startup
2023-05-06 00:23:28 -07:00
James Lu
ccd14af0c8 settings: treat empty environment variables as set (#77)
This allows disabling specific options like dns_interface or whois via environment variables.

ref: https://github.com/spf13/viper#working-with-environment-variables
2023-05-05 21:36:38 -07:00
Lan Tian
594ca80f50 frontend: fix whois lookup & only show bgpmap nexthop info on the first hop 2023-05-05 20:20:12 -07:00
Lan Tian
5625058e71 frontend: use ASN as bgpmap node identifier (instead of resolved whois result) 2023-05-05 19:52:30 -07:00
Lan Tian
7efa3237a9 frontend: refactor bgpmap code to fix #75 2023-05-05 01:58:05 -07:00
Lan Tian
7b0c8c0556 general: bump go version in go.mod 2023-01-26 22:01:47 -08:00
dependabot[bot]
ffd9165062 build(deps): bump github.com/spf13/viper in /frontend (#73)
Bumps [github.com/spf13/viper](https://github.com/spf13/viper) from 1.14.0 to 1.15.0.
- [Release notes](https://github.com/spf13/viper/releases)
- [Commits](https://github.com/spf13/viper/compare/v1.14.0...v1.15.0)

---
updated-dependencies:
- dependency-name: github.com/spf13/viper
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-23 10:50:42 -08:00
dependabot[bot]
24fd5203e8 build(deps): bump github.com/spf13/viper from 1.14.0 to 1.15.0 in /proxy (#74)
Bumps [github.com/spf13/viper](https://github.com/spf13/viper) from 1.14.0 to 1.15.0.
- [Release notes](https://github.com/spf13/viper/releases)
- [Commits](https://github.com/spf13/viper/compare/v1.14.0...v1.15.0)

---
updated-dependencies:
- dependency-name: github.com/spf13/viper
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-23 10:50:30 -08:00
Lan Tian
49a05767c1 ci: bump version for go-release-action 2023-01-08 01:16:23 -06:00
Lan Tian
e7010f75f8 release: v1.2.0 2023-01-06 23:05:05 -06:00
Yuhui Xu
dba2af7634 proxy: fix description for --traceroute_flags (#70) 2022-12-27 15:38:41 -06:00
Yuhui Xu
049775319b proxy: autodetect traceroute args on startup (#69) 2022-12-25 15:41:29 -06:00
Lan Tian
47c66b125c release: v1.1.1 2022-12-18 16:26:47 -06:00
Yuhui Xu
9e17b116f1 frontend: refactor bgpmap and fix node colors (#67)
* frontend: refactor bgpmap and fix node colors

* frontend: alternative way to test bgpmap
2022-12-07 16:30:19 -06:00
Lan Tian
335ad40634 release: v1.1.0 2022-11-27 17:22:21 -06:00
dependabot[bot]
6ec0f2e7a6 build(deps): bump github.com/spf13/viper from 1.13.0 to 1.14.0 in /proxy (#65)
Bumps [github.com/spf13/viper](https://github.com/spf13/viper) from 1.13.0 to 1.14.0.
- [Release notes](https://github.com/spf13/viper/releases)
- [Commits](https://github.com/spf13/viper/compare/v1.13.0...v1.14.0)

---
updated-dependencies:
- dependency-name: github.com/spf13/viper
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-07 20:26:44 -06:00
dependabot[bot]
4b73cf0fcb build(deps): bump github.com/spf13/viper in /frontend (#64)
Bumps [github.com/spf13/viper](https://github.com/spf13/viper) from 1.13.0 to 1.14.0.
- [Release notes](https://github.com/spf13/viper/releases)
- [Commits](https://github.com/spf13/viper/compare/v1.13.0...v1.14.0)

---
updated-dependencies:
- dependency-name: github.com/spf13/viper
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-07 20:26:37 -06:00
Klara Modin
3b1d001543 frontend: sortable tables in summary (#61)
Adapted from https://stackoverflow.com/a/57080195.
Additions:
1. sortTable accepts a secondary column and sorting direction. The 'Name'
   column (number 0) is always used for the secondary
2. use classes 'ascSorted' and 'descSorted' to toggle between ascending and
   descending order
3. in the table functions, use .innerHTML instead of .innnerText so the
   link to detailed protocol information does not get lost. Also
   preserve .classList
2022-09-18 15:58:20 -05:00
dependabot[bot]
675cb26ed1 build(deps): bump github.com/spf13/viper from 1.12.0 to 1.13.0 in /proxy (#63)
Bumps [github.com/spf13/viper](https://github.com/spf13/viper) from 1.12.0 to 1.13.0.
- [Release notes](https://github.com/spf13/viper/releases)
- [Commits](https://github.com/spf13/viper/compare/v1.12.0...v1.13.0)

---
updated-dependencies:
- dependency-name: github.com/spf13/viper
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-14 12:22:03 -05:00
dependabot[bot]
556d3e50d3 build(deps): bump github.com/spf13/viper in /frontend (#62)
Bumps [github.com/spf13/viper](https://github.com/spf13/viper) from 1.12.0 to 1.13.0.
- [Release notes](https://github.com/spf13/viper/releases)
- [Commits](https://github.com/spf13/viper/compare/v1.12.0...v1.13.0)

---
updated-dependencies:
- dependency-name: github.com/spf13/viper
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-14 12:21:56 -05:00
Lan Tian
06796f546e general: remove failing docker image build tasks for PRs 2022-08-25 11:42:41 -05:00
Potat0
d029d6684c Fix the wrong order of examples (#60) 2022-08-17 11:44:35 -05:00
日下部 詩
5ce0f55f35 1. support local whois. 2 add some useful bird command (#59) 2022-08-11 22:34:39 -04:00
Lan Tian
890ab51b07 release: v1.0.0 2022-08-05 22:36:43 -04:00
Lan Tian
8e4a35cc8c general: fix GOARCH for ARM 32 bit 2022-08-05 22:33:33 -04:00
Lan Tian
97f3c6088f general: add GitHub actions for releasing binaries 2022-08-05 22:29:30 -04:00
Yuhui Xu
982326a678 frontend: fix XSS (#57) (#58) 2022-08-05 21:59:18 -04:00
Tristan Heaven
4b3980f6bd Fix navbar_brand_url config (#56) 2022-07-28 09:54:39 -04:00
Yuhui Xu
6f6b2bd283 general: support reading config files (#55) 2022-07-08 23:13:10 -04:00
Yuhui Xu
892a7bee22 frontend: support listening on unix socket (#54) 2022-07-08 21:14:11 -04:00
Nicolas Lorin
348295b9aa frontend: add the abilities to customized timeout time (#51)
* main.go: add timeout setting

* lgproxy.go: use timeout setting when querying server

* README.md: add new timeout setting
2022-02-08 02:29:05 -06:00
Kioubit
950c018b18 Confirm that bird access was restricted (#49)
Co-authored-by: Kioubit <kioubit@localhost.invalid>
2022-01-18 03:03:09 -06:00
herver
26efeb4996 Add name filter feature (#48)
This uses a RE2 regexp to hide protocols which name matches the expression
2022-01-18 03:01:57 -06:00
Lan Tian
5a5dfbc93f general: add docker hub links to readme 2022-01-09 01:51:36 -06:00
Lan Tian
f60a292129 general: build docker images with correct arch label 2022-01-09 01:45:00 -06:00
James Lu
e7f6026854 Add BIRDLG_TRACEROUTE_RAW option to leave traceroute output in the default format (#47) 2022-01-09 00:14:31 -06:00
Lan Tian
a4e0f4c193 frontend: skip network related tests when unavailable
Fix #46
2022-01-09 00:10:14 -06:00
日下部 詩
af5b653326 BIRDLG_BGPMAP_INFO 選項 (#44)
* BIRDLG_BGPMAP_INFO

update the paramater

description fix for bgpmap_test

singleline and multiline

* Static file instead of jsdelivr; favicon.ico

Co-authored-by: testscript <testscript@example.com>
2021-12-20 03:35:43 -06:00
日下部 詩
58847759b3 一些小改動 (#42)
* 1. remove ":" at the start of port assignement. 2. use BIRDLG_PROXY_PORT at proxy. 3. add custom URL to brand

* goto / if only one server

* add BIRDLG_TRACEROUTE_BIN
2021-11-09 12:27:02 -06:00
Yuhui Xu
6481e7cc8d frontend: fix uninitialized buffer (#41) 2021-09-26 13:26:07 -05:00
James Lu
2166d73b3d frontend: add filtering by protocol type to summary tables (#40)
* frontend: add option to filter by protocol type

Closes #33.

* frontend: use case insensitive comparisons for protocol filter
2021-09-07 15:17:16 -05:00
Yuhui Xu
a64d839e2c frontend: limit fetched response size to 64KB (#39) 2021-09-02 20:21:28 -05:00
James Lu
1a3c618522 frontend: BGPmap improvements (#36)
* bgpmap: Compact nexthop info into an edge label

* bgpmap: parse and show non-BGP routes

* bgpmap: Misc tweaks

- Show the protocol name instead of the ASN in edge labels
- Correctly draw only the primary path if there are multiple routes to the first neighbour ASN in a path
- Use a smaller font size for edge labels

* bgpmap_test: update to match new changes

* bgpmap: Split route info on all (non-empty) rta_dest_names values
2021-08-31 09:44:14 -05:00
Yuhui Xu
fbd190628c general: add go 1.16 requirement to readme [skip ci] (#37) 2021-08-30 23:46:14 -05:00
Yuhui Xu
823b639245 frontend: also filter whois privacy redacted lines (#34) 2021-08-28 22:02:03 -05:00
Yuhui Xu
b0c0e5442d frontend: set lgproxy request timeout to 120s (#31)
lgproxy traceroutecan be really slow if dns resolve doesnt work well.
2021-08-04 00:30:38 +08:00
Yuhui Xu
4e4ce89418 frontend: set timeout longer for lgproxy requests (#30) 2021-08-03 11:46:05 +08:00
Yuhui Xu
234aadadd9 frontend: specify timeout for requests (#29) 2021-08-02 18:46:43 +08:00
Lan Tian
bee26f421c frontend: resolve asn in dns/whois/fail order & fix tests 2021-07-31 17:11:20 +08:00
Yuhui Xu
2e0cb131ca Merge pull request #27 from miegl/master
frontend: optional ASN resolution using whois
2021-07-31 17:02:25 +08:00
Josef Miegl
4c248c638a frontend: optional asn resolution using whois 2021-07-30 16:54:41 +02:00
Yuhui Xu
3550362a4d Merge pull request #26 from sesa-me/master
Refactor to use go:embed
2021-07-14 22:13:36 +08:00
Simon Marsh
256a80646f - Refactor file embedding to use Go 1.16 embed functionality.
- Remove references to previous bindata packages from build scripts and docs
2021-07-13 11:54:35 +01:00
Yuhui Xu
03c42eb1e8 Merge pull request #25 from outloudvi/patch-1
doc: add --net-specific-mode
2021-07-03 22:20:56 +08:00
Outvi V
aea85e774c doc: add --net-specific-mode 2021-07-03 12:33:49 +08:00
Yuhui Xu
80d9351a58 Merge pull request #24 from xddxdd/lantian-dev
frontend: allow long lines if result is short
2021-06-21 01:01:37 +08:00
Lan Tian
5e0bc081e6 frontend: allow long lines if result is short 2021-06-21 00:57:26 +08:00
Yuhui Xu
4d53d1f095 Merge pull request #23 from xddxdd/lantian-dev
frontend: change behavior of whois shorten mode
2021-06-21 00:49:37 +08:00
Lan Tian
5883015294 frontend: change behavior of whois shorten mode 2021-06-21 00:44:44 +08:00
Yuhui Xu
80e66a7a81 Merge pull request #22 from xddxdd/lantian-dev
frontend: add generic whois shorten mode
2021-06-20 02:24:49 +08:00
Lan Tian
41329da7cb frontend: add generic whois shorten mode 2021-06-20 02:20:18 +08:00
Yuhui Xu
8e56705205 Merge pull request #21 from xddxdd/lantian-dev
frontend: fix typo
2021-06-19 16:42:01 +08:00
Lan Tian
6a8b3a0e55 frontend: fix typo 2021-06-19 16:36:18 +08:00
Yuhui Xu
83ab403706 Merge pull request #20 from xddxdd/lantian-dev
frontend: limit telegram commands by bot name
2021-06-19 16:27:17 +08:00
Lan Tian
7c7814cc7b frontend: limit telegram commands by bot name 2021-06-19 16:23:43 +08:00
Yuhui Xu
8598060cc0 Merge pull request #19 from xddxdd/lantian-dev
Add instruction for create Dockerfile
2021-05-21 21:11:00 +08:00
Lan Tian
bda06ddd5e Add instruction for create Dockerfile 2021-05-21 21:09:50 +08:00
Yuhui Xu
f404072ab8 Merge pull request #18 from xddxdd/dependabot/add-v2-config-file
Upgrade to GitHub-native Dependabot
2021-05-01 23:21:14 +08:00
65 changed files with 7110 additions and 772 deletions

View File

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

108
.github/workflows/develop.yaml vendored Normal file
View File

@@ -0,0 +1,108 @@
on:
push:
branches:
- '**'
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 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
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 }}

82
.github/workflows/release.yaml vendored Normal file
View File

@@ -0,0 +1,82 @@
on:
release:
types: [created]
jobs:
go-release:
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:
- name: Checkout
uses: actions/checkout@v3
- name: Release frontend
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"
- name: Release proxy
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"
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
ghcr.io/xddxdd/bird-lg-go:frontend
- 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
ghcr.io/xddxdd/bird-lg-go:proxy

View File

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

View File

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

173
README.md
View File

@@ -6,43 +6,36 @@ An alternative implementation for [bird-lg](https://github.com/sileht/bird-lg) w
## Table of Contents ## Table of Contents
* [Bird-lg-go](#bird-lg-go) - [Bird-lg-go](#bird-lg-go)
* [Table of Contents](#table-of-contents) - [Table of Contents](#table-of-contents)
* [Frontend](#frontend) - [Build Instructions](#build-instructions)
* [Proxy](#proxy) - [Build Docker Images](#build-docker-images)
* [Advanced Features](#advanced-features) - [Frontend](#frontend)
* [API](#api) - [Proxy](#proxy)
* [Telegram Bot Webhook](#telegram-bot-webhook) - [Advanced Features](#advanced-features)
* [Example of setting the webhook](#example-of-setting-the-webhook) - [Display names](#display-names)
* [Supported commands](#supported-commands) - [IP addresses](#ip-addresses)
* [Credits](#credits) - [API](#api)
* [License](#license) - [Telegram Bot Webhook](#telegram-bot-webhook)
- [Credits](#credits)
Created by [gh-md-toc](https://github.com/ekalinin/github-markdown-toc) - [License](#license)
## Build Instructions ## 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 Docker Images
# Build frontend binary
cd frontend
go get -u github.com/kevinburke/go-bindata/...
go generate
go build -ldflags "-w -s" -o frontend
cd ..
# Build proxy binary Use the Dockerfiles in `frontend` and `proxy` directory.
cd proxy
go build -ldflags "-w -s" -o proxy
cd ..
```
- 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 ## Frontend
@@ -56,28 +49,49 @@ Features implemented:
- Work with both Python proxy (lgproxy.py) and Go proxy (proxy dir of this project) - Work with both Python proxy (lgproxy.py) and Go proxy (proxy dir of this project)
- Visualize AS paths as picture (bgpmap feature) - 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 | - `bird-lg.[json/yaml/etc]` in current directory
| --------- | -------------------- | ----------- | - `/etc/bird-lg/bird-lg.[json/yaml/etc]`
| --servers | BIRDLG_SERVERS | server name prefixes, separated by comma | - Commandline parameter
| --domain | BIRDLG_DOMAIN | server name domain suffixes | - Environment variables
| --listen | BIRDLG_LISTEN | address bird-lg is listening on (default ":5000") |
| --proxy-port | BIRDLG_PROXY_PORT | port bird-lgproxy is running on (default 8000) | Configuration is handled by [viper](https://github.com/spf13/viper), any config format supported by it can be used.
| --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") | | Config Key | Parameter | Environment Variable | Description |
| --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") | | 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. 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 ./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: Example: the following docker-compose.yml entry does the same as above, but by starting a Docker container:
```yaml
services: services:
bird-lg: bird-lg:
image: xddxdd/bird-lg-go # Use xddxdd/bird-lg-go:develop for the latest build from master branch
image: xddxdd/bird-lg-go:latest
container_name: bird-lg container_name: bird-lg
restart: always restart: always
environment: environment:
@@ -85,8 +99,9 @@ Example: the following docker-compose.yml entry does the same as above, but by s
- BIRDLG_DOMAIN=dn42.lantian.pub - BIRDLG_DOMAIN=dn42.lantian.pub
ports: ports:
- "5000:5000" - "5000:5000"
```
Demo: https://lg.lantian.pub Demo: <https://lg.lantian.pub>
## Proxy ## Proxy
@@ -99,32 +114,67 @@ Features implemented:
- Executing traceroute command on Linux, FreeBSD and OpenBSD - Executing traceroute command on Linux, FreeBSD and OpenBSD
- Source IP restriction - 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 | - `bird-lgproxy.[json/yaml/etc]` in current directory
| --------- | -------------------- | ----------- | - `/etc/bird-lg/bird-lgproxy.[json/yaml/etc]`
| --allowed | ALLOWED_IPS | IPs allowed to access this proxy, separated by commas. Don't set to allow all IPs. (default "") | - Commandline parameter
| --bird | BIRD_SOCKET | socket file for bird, set either in parameter or environment variable BIRD_SOCKET (default "/var/run/bird/bird.ctl") | - Environment variables
| --listen | BIRDLG_LISTEN | listen address, set either in parameter or environment variable BIRDLG_LISTEN (default ":8000") |
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: Example: start proxy with default configuration, should work "out of the box" on Debian 9 with BIRDv1:
```bash
./proxy ./proxy
```
Example: start proxy with custom bird socket location: Example: start proxy with custom bird socket location:
```bash
./proxy --bird /run/bird.ctl ./proxy --bird /run/bird.ctl
```
Example: the following docker-compose.yml entry does the same as above, but by starting a Docker container: Example: the following docker-compose.yml entry does the same as above, but by starting a Docker container:
```yaml
services:
bird-lgproxy: bird-lgproxy:
image: xddxdd/bird-lgproxy-go # Use xddxdd/bird-lgproxy-go:develop for the latest build from master branch
image: xddxdd/bird-lgproxy-go:latest
container_name: bird-lgproxy container_name: bird-lgproxy
restart: always restart: always
volumes: volumes:
- "/run/bird.ctl:/var/run/bird/bird.ctl" - "/run/bird.ctl:/var/run/bird/bird.ctl"
ports: ports:
- "192.168.0.1:8000:8000" - "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. 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 +186,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): 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 ./frontend --servers="Gigs<gigsgigscloud>,Hostdare<hostdare>" --domain=dn42.lantian.pub
```
### IP addresses ### IP addresses
@@ -144,7 +196,9 @@ You may also specify IP addresses as server names when no domain is specified. I
For example: For example:
```bash
./frontend --servers="Prod<prod.mydomain.local>,Test1<fd88:dead:beef::1>,Test2<fe80::c%wg0>" --domain= ./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. These three servers are displayed as "Prod", "Test1" and "Test2" in the user interface.
@@ -152,30 +206,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. 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 ### 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. 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. See [Telegram docs](docs/Telegram.md) for detailed information.
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
## Credits ## Credits

1
RELEASE Normal file
View File

@@ -0,0 +1 @@
v1.3.1

View File

@@ -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
View 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
View 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"]

View File

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

View File

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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

View File

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

View File

@@ -1,18 +1,19 @@
<!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">
<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 +29,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 +60,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">&raquo;</button> <button class="btn btn-outline-success" type="submit">&raquo;</button>
@@ -62,8 +74,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() {

View File

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

View File

@@ -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)
// 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, " ") func makePointAttrs(preferred bool) RouteAttrs {
if resultSplit := strings.Split(result, " | "); len(resultSplit) > 1 { result := RouteAttrs{}
result = strings.Join(resultSplit[1:], "\\n") 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 { func birdRouteToGraph(servers []string, responses []string, target string) RouteGraph {
graph := make(map[string]string) graph := makeRouteGraph()
// Helper to add an edge
addEdge := func(src string, dest string, attr string) { graph.AddPoint(target, false, RouteAttrs{"color": "red", "shape": "diamond"})
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
}
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 { for routeIndex, route := range routes {
var routeNexthop string if routeIndex == 0 {
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
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"
} }
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
View 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"
}

View File

@@ -1,58 +1,23 @@
package main package main
import ( import (
"io/ioutil"
"path"
"runtime"
"strings" "strings"
"testing" "testing"
) )
func TestGetASNRepresentation(t *testing.T) { func readDataFile(t *testing.T, filename string) string {
setting.dnsInterface = "asn.cymru.com" _, sourceName, _, _ := runtime.Caller(0)
result := getASNRepresentation("6939") projectRoot := path.Join(path.Dir(sourceName), "..")
if !strings.Contains(result, "HURRICANE") { dir := path.Join(projectRoot, filename)
t.Errorf("Lookup AS6939 failed, got %s", result)
} data, err := ioutil.ReadFile(dir)
} if err != nil {
t.Fatal(err)
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)
}
} }
return string(data)
} }
func TestBirdRouteToGraphvizXSS(t *testing.T) { func TestBirdRouteToGraphvizXSS(t *testing.T) {
@@ -72,3 +37,48 @@ func TestBirdRouteToGraphvizXSS(t *testing.T) {
t.Errorf("XSS injection succeeded: %s", result) 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")
}
}

View File

@@ -49,3 +49,53 @@ func dn42WhoisFilter(whois string) string {
return commandResult 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
}
}

View File

@@ -28,3 +28,79 @@ func TestDN42WhoisFilterUnneeded(t *testing.T) {
t.Errorf("Output doesn't match expected: %s", result) 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)
}
}

View File

@@ -1,9 +1,29 @@
module github.com/xddxdd/bird-lg-go/frontend module github.com/xddxdd/bird-lg-go/frontend
go 1.15 go 1.17
require ( require (
github.com/elazarl/go-bindata-assetfs v1.0.1 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/gorilla/handlers v1.5.1 github.com/gorilla/handlers v1.5.1
github.com/kevinburke/go-bindata v3.22.0+incompatible // indirect github.com/jarcoal/httpmock v1.3.1
github.com/magiconair/properties v1.8.7
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.16.0
)
require (
github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/spf13/afero v1.9.5 // indirect
github.com/spf13/cast v1.5.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/text v0.9.0 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,12 @@
package main package main
import ( import (
"io/ioutil" "io"
"net/http" "net/http"
"net/url" "net/url"
"strconv" "strconv"
"strings" "strings"
"time"
) )
type channelData struct { type channelData struct {
@@ -46,13 +47,20 @@ func batchRequest(servers []string, endpoint string, command string) []string {
} }
url := "http://" + hostname + ":" + strconv.Itoa(setting.proxyPort) + "/" + url.PathEscape(endpoint) + "?q=" + url.QueryEscape(command) url := "http://" + hostname + ":" + strconv.Itoa(setting.proxyPort) + "/" + url.PathEscape(endpoint) + "?q=" + url.QueryEscape(command)
go func(url string, i int) { go func(url string, i int) {
response, err := http.Get(url) client := http.Client{Timeout: time.Duration(setting.timeOut) * time.Second}
response, err := client.Get(url)
if err != nil { if err != nil {
ch <- channelData{i, "request failed: " + err.Error() + "\n"} ch <- channelData{i, "request failed: " + err.Error() + "\n"}
return return
} }
text, _ := ioutil.ReadAll(response.Body)
ch <- channelData{i, string(text)} buf := make([]byte, 65536)
n, err := io.ReadFull(response.Body, buf)
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
ch <- channelData{i, "request failed: " + err.Error()}
} else {
ch <- channelData{i, string(buf[:n])}
}
}(url, i) }(url, i)
} }
} }

163
frontend/lgproxy_test.go Normal file
View File

@@ -0,0 +1,163 @@
package main
import (
"errors"
"strings"
"testing"
"github.com/jarcoal/httpmock"
)
func TestBatchRequestIPv4(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
httpResponse := httpmock.NewStringResponder(200, "Mock Result")
httpmock.RegisterResponder("GET", "http://1.1.1.1:8000/mock?q=cmd", httpResponse)
httpmock.RegisterResponder("GET", "http://2.2.2.2:8000/mock?q=cmd", httpResponse)
httpmock.RegisterResponder("GET", "http://3.3.3.3:8000/mock?q=cmd", httpResponse)
setting.servers = []string{
"1.1.1.1",
"2.2.2.2",
"3.3.3.3",
}
setting.domain = ""
setting.proxyPort = 8000
response := batchRequest(setting.servers, "mock", "cmd")
if len(response) != 3 {
t.Error("Did not get response of all three mock servers")
}
for i := 0; i < len(response); i++ {
if response[i] != "Mock Result" {
t.Error("HTTP response mismatch")
}
}
}
func TestBatchRequestIPv6(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
httpResponse := httpmock.NewStringResponder(200, "Mock Result")
httpmock.RegisterResponder("GET", "http://[2001:db8::1]:8000/mock?q=cmd", httpResponse)
httpmock.RegisterResponder("GET", "http://[2001:db8::2]:8000/mock?q=cmd", httpResponse)
httpmock.RegisterResponder("GET", "http://[2001:db8::3]:8000/mock?q=cmd", httpResponse)
setting.servers = []string{
"2001:db8::1",
"2001:db8::2",
"2001:db8::3",
}
setting.domain = ""
setting.proxyPort = 8000
response := batchRequest(setting.servers, "mock", "cmd")
if len(response) != 3 {
t.Error("Did not get response of all three mock servers")
}
for i := 0; i < len(response); i++ {
if response[i] != "Mock Result" {
t.Error("HTTP response mismatch")
}
}
}
func TestBatchRequestEmptyResponse(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
httpResponse := httpmock.NewStringResponder(200, "")
httpmock.RegisterResponder("GET", "http://alpha:8000/mock?q=cmd", httpResponse)
httpmock.RegisterResponder("GET", "http://beta:8000/mock?q=cmd", httpResponse)
httpmock.RegisterResponder("GET", "http://gamma:8000/mock?q=cmd", httpResponse)
setting.servers = []string{
"alpha",
"beta",
"gamma",
}
setting.domain = ""
setting.proxyPort = 8000
response := batchRequest(setting.servers, "mock", "cmd")
if len(response) != 3 {
t.Error("Did not get response of all three mock servers")
}
for i := 0; i < len(response); i++ {
if !strings.Contains(response[i], "node returned empty response") {
t.Error("Did not produce error for empty response")
}
}
}
func TestBatchRequestDomainSuffix(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
httpResponse := httpmock.NewStringResponder(200, "Mock Result")
httpmock.RegisterResponder("GET", "http://alpha.suffix:8000/mock?q=cmd", httpResponse)
httpmock.RegisterResponder("GET", "http://beta.suffix:8000/mock?q=cmd", httpResponse)
httpmock.RegisterResponder("GET", "http://gamma.suffix:8000/mock?q=cmd", httpResponse)
setting.servers = []string{
"alpha",
"beta",
"gamma",
}
setting.domain = "suffix"
setting.proxyPort = 8000
response := batchRequest(setting.servers, "mock", "cmd")
if len(response) != 3 {
t.Error("Did not get response of all three mock servers")
}
for i := 0; i < len(response); i++ {
if response[i] != "Mock Result" {
t.Error("HTTP response mismatch")
}
}
}
func TestBatchRequestHTTPError(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
httpError := httpmock.NewErrorResponder(errors.New("Oops!"))
httpmock.RegisterResponder("GET", "http://alpha:8000/mock?q=cmd", httpError)
httpmock.RegisterResponder("GET", "http://beta:8000/mock?q=cmd", httpError)
httpmock.RegisterResponder("GET", "http://gamma:8000/mock?q=cmd", httpError)
setting.servers = []string{
"alpha",
"beta",
"gamma",
}
setting.domain = ""
setting.proxyPort = 8000
response := batchRequest(setting.servers, "mock", "cmd")
if len(response) != 3 {
t.Error("Did not get response of all three mock servers")
}
for i := 0; i < len(response); i++ {
if !strings.Contains(response[i], "request failed") {
t.Error("Did not produce HTTP error")
}
}
}
func TestBatchRequestInvalidServer(t *testing.T) {
setting.servers = []string{}
setting.domain = ""
setting.proxyPort = 8000
response := batchRequest([]string{"invalid"}, "mock", "cmd")
if len(response) != 1 {
t.Error("Did not get response of all mock servers")
}
if !strings.Contains(response[0], "invalid server") {
t.Error("Did not produce invalid server error")
}
}

View File

@@ -1,15 +1,11 @@
package main package main
import ( import (
"flag" "net"
"os" "os"
"strconv"
"strings" "strings"
) )
// binary data
//go:generate go-bindata -prefix bindata -o bindata.go bindata/...
type settingType struct { type settingType struct {
servers []string servers []string
serversDisplay []string serversDisplay []string
@@ -21,93 +17,40 @@ type settingType struct {
netSpecificMode string netSpecificMode string
titleBrand string titleBrand string
navBarBrand string navBarBrand string
navBarBrandURL string
navBarAllServer string
navBarAllURL string
bgpmapInfo string
telegramBotName string
protocolFilter []string
nameFilter string
timeOut int
} }
var setting settingType var setting settingType
func main() { func main() {
var settingDefault = settingType{ parseSettings()
servers: []string{""}, ImportTemplates()
proxyPort: 8000,
whoisServer: "whois.verisign-grs.com", var l net.Listener
listen: ":5000", var err error
dnsInterface: "asn.cymru.com",
titleBrand: "Bird-lg Go", if strings.HasPrefix(setting.listen, "/") {
navBarBrand: "Bird-lg Go", // Delete existing socket file, ignore errors (will fail later anyway)
os.Remove(setting.listen)
l, err = net.Listen("unix", setting.listen)
} else {
listenAddr := setting.listen
if !strings.Contains(listenAddr, ":") {
listenAddr = ":" + listenAddr
}
l, err = net.Listen("tcp", listenAddr)
} }
if env := os.Getenv("BIRDLG_SERVERS"); env != "" { if err != nil {
settingDefault.servers = strings.Split(env, ",")
}
if env := os.Getenv("BIRDLG_DOMAIN"); env != "" {
settingDefault.domain = env
}
if env := os.Getenv("BIRDLG_PROXY_PORT"); env != "" {
var err error
if settingDefault.proxyPort, err = strconv.Atoi(env); err != nil {
panic(err) panic(err)
} }
}
if env := os.Getenv("BIRDLG_WHOIS"); env != "" {
settingDefault.whoisServer = env
}
if env := os.Getenv("BIRDLG_LISTEN"); env != "" {
settingDefault.listen = env
}
if env := os.Getenv("BIRDLG_DNS_INTERFACE"); env != "" {
settingDefault.dnsInterface = env
}
if env := os.Getenv("BIRDLG_NET_SPECIFIC_MODE"); env != "" {
settingDefault.netSpecificMode = env
}
if env := os.Getenv("BIRDLG_TITLE_BRAND"); env != "" {
settingDefault.titleBrand = env
settingDefault.navBarBrand = env
}
if env := os.Getenv("BIRDLG_NAVBAR_BRAND"); env != "" {
settingDefault.navBarBrand = env
}
serversPtr := flag.String("servers", strings.Join(settingDefault.servers, ","), "server name prefixes, separated by comma") webServerStart(l)
domainPtr := flag.String("domain", settingDefault.domain, "server name domain suffixes")
proxyPortPtr := flag.Int("proxy-port", settingDefault.proxyPort, "port bird-lgproxy is running on")
whoisPtr := flag.String("whois", settingDefault.whoisServer, "whois server for queries")
listenPtr := flag.String("listen", settingDefault.listen, "address bird-lg is listening on")
dnsInterfacePtr := flag.String("dns-interface", settingDefault.dnsInterface, "dns zone to query ASN information")
netSpecificModePtr := flag.String("net-specific-mode", settingDefault.netSpecificMode, "network specific operation mode, [(none)|dn42]")
titleBrandPtr := flag.String("title-brand", settingDefault.titleBrand, "prefix of page titles in browser tabs")
navBarBrandPtr := flag.String("navbar-brand", settingDefault.navBarBrand, "brand to show in the navigation bar")
flag.Parse()
if *serversPtr == "" {
panic("no server set")
}
servers := strings.Split(*serversPtr, ",")
serversDisplay := strings.Split(*serversPtr, ",")
// Split server names of the form "DisplayName<Hostname>"
for i, server := range servers {
pos := strings.Index(server, "<")
if pos != -1 {
serversDisplay[i] = server[0:pos]
servers[i] = server[pos+1:len(server)-1]
}
}
setting = settingType{
servers,
serversDisplay,
*domainPtr,
*proxyPortPtr,
*whoisPtr,
*listenPtr,
*dnsInterfacePtr,
strings.ToLower(*netSpecificModePtr),
*titleBrandPtr,
*navBarBrandPtr,
}
ImportTemplates()
webServerStart()
} }

30
frontend/network_test.go Normal file
View File

@@ -0,0 +1,30 @@
package main
import (
"net"
"testing"
"time"
)
const (
NETWORK_UNKNOWN = 0
NETWORK_DOWN = 1
NETWORK_UP = 2
)
var networkState int = NETWORK_UNKNOWN
func checkNetwork(t *testing.T) {
if networkState == NETWORK_UNKNOWN {
conn, err := net.DialTimeout("tcp", "8.8.8.8:53", 1*time.Second)
if err != nil {
networkState = NETWORK_DOWN
} else {
networkState = NETWORK_UP
conn.Close()
}
}
if networkState == NETWORK_DOWN {
t.Skipf("Test skipped for network error")
}
}

View File

@@ -4,18 +4,25 @@ import (
"bytes" "bytes"
"errors" "errors"
"fmt" "fmt"
"html/template"
"net/http" "net/http"
"net/url"
"regexp" "regexp"
"sort" "sort"
"strings" "strings"
"text/template"
) )
// static options map // static options map
var optionsMap = map[string]string{ var optionsMap = map[string]string{
"summary": "show protocols", "summary": "show protocols",
"detail": "show protocols all", "detail": "show protocols all ...",
"route_from_protocol": "show route protocol ...",
"route_from_protocol_all": "show route protocol ... all",
"route_from_protocol_all_primary": "show route protocol ... all primary",
"route_filtered_from_protocol": "show route filtered protocol ...",
"route_filtered_from_protocol_all": "show route filtered protocol ... all",
"route_from_origin": "show route where bgp_path.last = ...",
"route_from_origin_all": "show route where bgp_path.last = ... all",
"route_from_origin_all_primary": "show route where bgp_path.last = ... all primary",
"route": "show route for ...", "route": "show route for ...",
"route_all": "show route for ... all", "route_all": "show route for ... all",
"route_bgpmap": "show route for ... (bgpmap)", "route_bgpmap": "show route for ... (bgpmap)",
@@ -38,7 +45,7 @@ var summaryStateMap = map[string]string{
} }
// render the page template // render the page template
func renderPageTemplate(w http.ResponseWriter, r *http.Request, title string, content string) { func renderPageTemplate(w http.ResponseWriter, r *http.Request, title string, content template.HTML) {
path := r.URL.Path[1:] path := r.URL.Path[1:]
split := strings.SplitN(path, "/", 3) split := strings.SplitN(path, "/", 3)
@@ -48,33 +55,30 @@ func renderPageTemplate(w http.ResponseWriter, r *http.Request, title string, co
// Use a default URL if the request URL is too short // Use a default URL if the request URL is too short
// The URL is for return to summary page // The URL is for return to summary page
if len(split) < 2 { if len(split) < 2 {
path = "summary/" + url.PathEscape(strings.Join(setting.servers, "+")) + "/" path = "summary/" + strings.Join(setting.servers, "+") + "/"
} else if len(split) == 2 { } else if len(split) == 2 {
path += "/" path += "/"
} }
split = strings.SplitN(path, "/", 3) split = strings.SplitN(path, "/", 3)
serversEscaped := make([]string, len(setting.servers))
for i, v := range setting.servers {
serversEscaped[i] = url.PathEscape(v)
}
args := TemplatePage{ args := TemplatePage{
Options: optionsMap, Options: optionsMap,
Servers: setting.servers, Servers: setting.servers,
ServersEscaped: serversEscaped,
ServersDisplay: setting.serversDisplay, ServersDisplay: setting.serversDisplay,
AllServersLinkActive: strings.ToLower(split[1]) == strings.ToLower(strings.Join(setting.servers, "+")), AllServersLinkActive: strings.EqualFold(split[1], strings.Join(setting.servers, "+")),
AllServersURL: url.PathEscape(strings.Join(setting.servers, "+")), AllServersURL: strings.Join(setting.servers, "+"),
AllServerTitle: setting.navBarAllServer,
AllServersURLCustom: setting.navBarAllURL,
IsWhois: isWhois, IsWhois: isWhois,
WhoisTarget: whoisTarget, WhoisTarget: whoisTarget,
URLOption: strings.ToLower(split[0]), URLOption: strings.ToLower(split[0]),
URLServer: url.PathEscape(strings.ToLower(split[1])), URLServer: strings.ToLower(split[1]),
URLCommand: split[2], URLCommand: split[2],
Title: setting.titleBrand + title, Title: setting.titleBrand + title,
Brand: setting.navBarBrand, Brand: setting.navBarBrand,
BrandURL: setting.navBarBrandURL,
Content: content, Content: content,
} }
@@ -88,7 +92,7 @@ func renderPageTemplate(w http.ResponseWriter, r *http.Request, title string, co
// Write the given text to http response, and add whois links for // Write the given text to http response, and add whois links for
// ASNs and IP addresses // ASNs and IP addresses
func smartFormatter(s string) string { func smartFormatter(s string) template.HTML {
var result string var result string
result += "<pre>" result += "<pre>"
s = template.HTMLEscapeString(s) s = template.HTMLEscapeString(s)
@@ -105,7 +109,7 @@ func smartFormatter(s string) string {
result += lineFormatted + "\n" result += lineFormatted + "\n"
} }
result += "</pre>" result += "</pre>"
return result return template.HTML(result)
} }
// Parse bird show protocols result // Parse bird show protocols result
@@ -130,6 +134,9 @@ func summaryParse(data string, serverName string) (TemplateSummary, error) {
args.Header = append(args.Header, col) args.Header = append(args.Header, col)
} }
// Build regexp for nameFilter
nameFilterRegexp := regexp.MustCompile(setting.nameFilter)
// sort the remaining rows // sort the remaining rows
rows := lines[1:] rows := lines[1:]
sort.Strings(rows) sort.Strings(rows)
@@ -153,9 +160,24 @@ func summaryParse(data string, serverName string) (TemplateSummary, error) {
if len(lineSplitted) >= 2 { if len(lineSplitted) >= 2 {
row.Name = strings.TrimSpace(lineSplitted[1]) row.Name = strings.TrimSpace(lineSplitted[1])
if setting.nameFilter != "" && nameFilterRegexp.MatchString(row.Name) {
continue
}
} }
if len(lineSplitted) >= 4 { if len(lineSplitted) >= 4 {
row.Proto = strings.TrimSpace(lineSplitted[3]) row.Proto = strings.TrimSpace(lineSplitted[3])
// Filter away unwanted protocol types, if setting.protocolFilter is non-empty
found := false
for _, protocol := range setting.protocolFilter {
if strings.EqualFold(row.Proto, protocol) {
found = true
break
}
}
if len(setting.protocolFilter) > 0 && !found {
continue
}
} }
if len(lineSplitted) >= 6 { if len(lineSplitted) >= 6 {
row.Table = strings.TrimSpace(lineSplitted[5]) row.Table = strings.TrimSpace(lineSplitted[5])
@@ -179,11 +201,11 @@ func summaryParse(data string, serverName string) (TemplateSummary, error) {
} }
// Output a table for the summary page // Output a table for the summary page
func summaryTable(data string, serverName string) string { func summaryTable(data string, serverName string) template.HTML {
result, err := summaryParse(data, serverName) result, err := summaryParse(data, serverName)
if err != nil { if err != nil {
return "<pre>" + template.HTMLEscapeString(err.Error()) + "</pre>" return template.HTML("<pre>" + template.HTMLEscapeString(err.Error()) + "</pre>")
} }
// render the summary template // render the summary template
@@ -194,5 +216,5 @@ func summaryTable(data string, serverName string) string {
fmt.Println("Error rendering summary:", err.Error()) fmt.Println("Error rendering summary:", err.Error())
} }
return buffer.String() return template.HTML(buffer.String())
} }

View File

@@ -1,13 +1,24 @@
package main package main
import ( import (
"html/template"
"io/ioutil" "io/ioutil"
"net/http/httptest" "net/http/httptest"
"net/url"
"strings" "strings"
"testing" "testing"
) )
const BirdSummaryData = `BIRD 2.0.8 ready.
Name Proto Table State Since Info
static1 Static master4 up 2021-08-27
static2 Static master6 up 2021-08-27
device1 Device --- up 2021-08-27
kernel1 Kernel master6 up 2021-08-27
kernel2 Kernel master4 up 2021-08-27
direct1 Direct --- up 2021-08-27
int_babel Babel --- up 2021-08-27
`
func initSettings() { func initSettings() {
setting.servers = []string{"alpha"} setting.servers = []string{"alpha"}
setting.serversDisplay = []string{"alpha"} setting.serversDisplay = []string{"alpha"}
@@ -25,7 +36,7 @@ func TestRenderPageTemplate(t *testing.T) {
r := httptest.NewRequest("GET", "/route/alpha/192.168.0.1/", nil) r := httptest.NewRequest("GET", "/route/alpha/192.168.0.1/", nil)
w := httptest.NewRecorder() w := httptest.NewRecorder()
renderPageTemplate(w, r, title, content) renderPageTemplate(w, r, title, template.HTML(content))
resultBytes, _ := ioutil.ReadAll(w.Result().Body) resultBytes, _ := ioutil.ReadAll(w.Result().Body)
result := string(resultBytes) result := string(resultBytes)
@@ -43,7 +54,27 @@ func TestRenderPageTemplateXSS(t *testing.T) {
evil := "<script>alert('evil');</script>" evil := "<script>alert('evil');</script>"
r := httptest.NewRequest("GET", "/whois/"+url.PathEscape(evil), nil) r := httptest.NewRequest("GET", "/whois/"+evil, nil)
w := httptest.NewRecorder()
// renderPageTemplate doesn't escape content, filter is done beforehand
renderPageTemplate(w, r, evil, "Test Content")
resultBytes, _ := ioutil.ReadAll(w.Result().Body)
result := string(resultBytes)
if strings.Contains(result, evil) {
t.Errorf("XSS injection succeeded: %s", result)
}
}
// https://github.com/xddxdd/bird-lg-go/issues/57
func TestRenderPageTemplateXSS_2(t *testing.T) {
initSettings()
evil := "<script>alert('evil');</script>"
r := httptest.NewRequest("GET", "/generic/dummy_server/"+evil, nil)
w := httptest.NewRecorder() w := httptest.NewRecorder()
// renderPageTemplate doesn't escape content, filter is done beforehand // renderPageTemplate doesn't escape content, filter is done beforehand
@@ -59,7 +90,7 @@ func TestRenderPageTemplateXSS(t *testing.T) {
func TestSmartFormatterXSS(t *testing.T) { func TestSmartFormatterXSS(t *testing.T) {
evil := "<script>alert('evil');</script>" evil := "<script>alert('evil');</script>"
result := smartFormatter(evil) result := string(smartFormatter(evil))
if strings.Contains(result, evil) { if strings.Contains(result, evil) {
t.Errorf("XSS injection succeeded: %s", result) t.Errorf("XSS injection succeeded: %s", result)
@@ -71,9 +102,57 @@ func TestSummaryTableXSS(t *testing.T) {
evilData := `Name Proto Table State Since Info evilData := `Name Proto Table State Since Info
` + evil + ` ` + evil + ` --- up 2021-01-04 17:21:44 ` + evil ` + evil + ` ` + evil + ` --- up 2021-01-04 17:21:44 ` + evil
result := summaryTable(evilData, evil) result := string(summaryTable(evilData, evil))
if strings.Contains(result, evil) { if strings.Contains(result, evil) {
t.Errorf("XSS injection succeeded: %s", result) t.Errorf("XSS injection succeeded: %s", result)
} }
} }
func TestSummaryTableProtocolFilter(t *testing.T) {
initSettings()
setting.protocolFilter = []string{"Static", "Direct", "Babel"}
result := string(summaryTable(BirdSummaryData, "testserver"))
expectedInclude := []string{"static1", "static2", "int_babel", "direct1"}
expectedExclude := []string{"device1", "kernel1", "kernel2"}
for _, item := range expectedInclude {
if !strings.Contains(result, item) {
t.Errorf("Did not find expected %s in summary table output", result)
}
}
for _, item := range expectedExclude {
if strings.Contains(result, item) {
t.Errorf("Found unexpected %s in summary table output", result)
}
}
t.Cleanup(func() {
setting.protocolFilter = []string{}
})
}
func TestSummaryTableNameFilter(t *testing.T) {
initSettings()
setting.nameFilter = "^static"
result := string(summaryTable(BirdSummaryData, "testserver"))
expectedInclude := []string{"device1", "kernel1", "kernel2", "direct1", "int_babel"}
expectedExclude := []string{"static1", "static2"}
for _, item := range expectedInclude {
if !strings.Contains(result, item) {
t.Errorf("Did not find expected %s in summary table output", result)
}
}
for _, item := range expectedExclude {
if strings.Contains(result, item) {
t.Errorf("Found unexpected %s in summary table output", result)
}
}
t.Cleanup(func() {
setting.nameFilter = ""
})
}

144
frontend/settings.go Normal file
View File

@@ -0,0 +1,144 @@
package main
import (
"fmt"
"strings"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)
type viperSettingType struct {
Servers string `mapstructure:"servers"`
Domain string `mapstructure:"domain"`
ProxyPort int `mapstructure:"proxy_port"`
WhoisServer string `mapstructure:"whois"`
Listen string `mapstructure:"listen"`
DNSInterface string `mapstructure:"dns_interface"`
NetSpecificMode string `mapstructure:"net_specific_mode"`
TitleBrand string `mapstructure:"title_brand"`
NavBarBrand string `mapstructure:"navbar_brand"`
NavBarBrandURL string `mapstructure:"navbar_brand_url"`
NavBarAllServer string `mapstructure:"navbar_all_servers"`
NavBarAllURL string `mapstructure:"navbar_all_url"`
BgpmapInfo string `mapstructure:"bgpmap_info"`
TelegramBotName string `mapstructure:"telegram_bot_name"`
ProtocolFilter string `mapstructure:"protocol_filter"`
NameFilter string `mapstructure:"name_filter"`
TimeOut int `mapstructure:"timeout"`
}
// Parse settings with viper, and convert to legacy setting format
func parseSettings() {
viper.AddConfigPath(".")
viper.AddConfigPath("/etc/bird-lg")
viper.SetConfigName("bird-lg")
viper.AllowEmptyEnv(true)
viper.AutomaticEnv()
viper.SetEnvPrefix("birdlg")
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_"))
pflag.String("servers", "", "server name prefixes, separated by comma")
viper.BindPFlag("servers", pflag.Lookup("servers"))
pflag.String("domain", "", "server name domain suffixes")
viper.BindPFlag("domain", pflag.Lookup("domain"))
pflag.Int("proxy-port", 8000, "port bird-lgproxy is running on")
viper.BindPFlag("proxy_port", pflag.Lookup("proxy-port"))
pflag.String("whois", "whois.verisign-grs.com", "whois server for queries")
viper.BindPFlag("whois", pflag.Lookup("whois"))
pflag.String("listen", "5000", "address or unix socket bird-lg is listening on")
viper.BindPFlag("listen", pflag.Lookup("listen"))
pflag.String("dns-interface", "asn.cymru.com", "dns zone to query ASN information")
viper.BindPFlag("dns_interface", pflag.Lookup("dns-interface"))
pflag.String("net-specific-mode", "", "network specific operation mode, [(none)|dn42]")
viper.BindPFlag("net_specific-mode", pflag.Lookup("net-specific-mode"))
pflag.String("title-brand", "Bird-lg Go", "prefix of page titles in browser tabs")
viper.BindPFlag("title_brand", pflag.Lookup("title-brand"))
pflag.String("navbar-brand", "", "brand to show in the navigation bar")
viper.BindPFlag("navbar_brand", pflag.Lookup("navbar-brand"))
pflag.String("navbar-brand-url", "/", "the url of the brand to show in the navigation bar")
viper.BindPFlag("navbar_brand_url", pflag.Lookup("navbar-brand-url"))
pflag.String("navbar-all-servers", "All Servers", "the text of \"All servers\" button in the navigation bar")
viper.BindPFlag("navbar_all_servers", pflag.Lookup("navbar-all-servers"))
pflag.String("navbar-all-url", "all", "the URL of \"All servers\" button")
viper.BindPFlag("navbar_all_url", pflag.Lookup("navbar-all-url"))
pflag.String("bgpmap-info", "asn,as-name,ASName,descr", "the infos displayed in bgpmap, separated by comma, start with \":\" means allow multiline")
viper.BindPFlag("bgpmap_info", pflag.Lookup("bgpmap-info"))
pflag.String("telegram-bot-name", "", "telegram bot name (used to filter @bot commands)")
viper.BindPFlag("telegram_bot_name", pflag.Lookup("telegram-bot-name"))
pflag.String("protocol-filter", "",
"protocol types to show in summary tables (comma separated list); defaults to all if not set")
viper.BindPFlag("protocol_filter", pflag.Lookup("protocol-filter"))
pflag.String("name-filter", "", "protocol name regex to hide in summary tables (RE2 syntax); defaults to none if not set")
viper.BindPFlag("name_filter", pflag.Lookup("name-filter"))
pflag.Int("time-out", 120, "time before request timed out, in seconds; defaults to 120 if not set")
viper.BindPFlag("timeout", pflag.Lookup("time-out"))
pflag.Parse()
if err := viper.ReadInConfig(); err != nil {
println("Warning on reading config: " + err.Error())
}
viperSettings := viperSettingType{}
if err := viper.Unmarshal(&viperSettings); err != nil {
panic(err)
}
setting.servers = strings.Split(viperSettings.Servers, ",")
setting.serversDisplay = strings.Split(viperSettings.Servers, ",")
// Split server names of the form "DisplayName<Hostname>"
for i, server := range setting.servers {
pos := strings.Index(server, "<")
if pos != -1 {
setting.serversDisplay[i] = server[0:pos]
setting.servers[i] = server[pos+1 : len(server)-1]
}
}
setting.domain = viperSettings.Domain
setting.proxyPort = viperSettings.ProxyPort
setting.whoisServer = viperSettings.WhoisServer
setting.listen = viperSettings.Listen
setting.dnsInterface = viperSettings.DNSInterface
setting.netSpecificMode = viperSettings.NetSpecificMode
setting.titleBrand = viperSettings.TitleBrand
setting.navBarBrand = viperSettings.NavBarBrand
if setting.navBarBrand == "" {
setting.navBarBrand = setting.titleBrand
}
setting.navBarBrandURL = viperSettings.NavBarBrandURL
setting.navBarAllServer = viperSettings.NavBarAllServer
setting.navBarAllURL = viperSettings.NavBarAllURL
setting.bgpmapInfo = viperSettings.BgpmapInfo
setting.telegramBotName = viperSettings.TelegramBotName
if viperSettings.ProtocolFilter != "" {
setting.protocolFilter = strings.Split(viperSettings.ProtocolFilter, ",")
} else {
setting.protocolFilter = []string{}
}
setting.nameFilter = viperSettings.NameFilter
setting.timeOut = viperSettings.TimeOut
fmt.Printf("%#v\n", setting)
}

View File

@@ -0,0 +1,8 @@
package main
import "testing"
func TestParseSettings(t *testing.T) {
parseSettings()
// Good as long as it doesn't panic
}

View File

@@ -31,7 +31,8 @@ type tgWebhookResponse struct {
func telegramIsCommand(message string, command string) bool { func telegramIsCommand(message string, command string) bool {
b := false b := false
b = b || strings.HasPrefix(message, "/"+command+"@") b = b || strings.HasPrefix(message, "/"+command+"@"+setting.telegramBotName+" ")
b = b || message == "/"+command+"@"+setting.telegramBotName
b = b || strings.HasPrefix(message, "/"+command+" ") b = b || strings.HasPrefix(message, "/"+command+" ")
b = b || message == "/"+command b = b || message == "/"+command
return b return b
@@ -104,7 +105,7 @@ func webHandlerTelegramBot(w http.ResponseWriter, r *http.Request) {
}) })
} else if telegramIsCommand(request.Message.Text, "whois") { } else if telegramIsCommand(request.Message.Text, "whois") {
if setting.netSpecificMode == "dn42" { if setting.netSpecificMode == "dn42" || setting.netSpecificMode == "dn42_generic" {
targetNumber, err := strconv.ParseUint(target, 10, 64) targetNumber, err := strconv.ParseUint(target, 10, 64)
if err == nil { if err == nil {
if targetNumber < 10000 { if targetNumber < 10000 {
@@ -118,6 +119,8 @@ func webHandlerTelegramBot(w http.ResponseWriter, r *http.Request) {
tempResult := whois(target) tempResult := whois(target)
if setting.netSpecificMode == "dn42" { if setting.netSpecificMode == "dn42" {
commandResult = dn42WhoisFilter(tempResult) commandResult = dn42WhoisFilter(tempResult)
} else if setting.netSpecificMode == "dn42_shorten" || setting.netSpecificMode == "shorten" {
commandResult = shortenWhoisFilter(tempResult)
} else { } else {
commandResult = tempResult commandResult = tempResult
} }

View File

@@ -0,0 +1,367 @@
package main
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"github.com/jarcoal/httpmock"
"github.com/magiconair/properties/assert"
)
func doTestTelegramIsCommand(t *testing.T, message string, command string, expected bool) {
result := telegramIsCommand(message, command)
assert.Equal(t, result, expected)
}
func mockTelegramCall(t *testing.T, msg string, raw bool) string {
return mockTelegramEndpointCall(t, "/telegram/", msg, raw)
}
func mockTelegramEndpointCall(t *testing.T, endpoint string, msg string, raw bool) string {
request := tgWebhookRequest{
Message: tgMessage{
MessageID: 123,
Chat: tgChat{
ID: 456,
},
Text: msg,
},
}
requestJson, err := json.Marshal(request)
if err != nil {
t.Fatal(err)
}
requestBody := bytes.NewReader(requestJson)
r := httptest.NewRequest(http.MethodGet, endpoint, requestBody)
w := httptest.NewRecorder()
webHandlerTelegramBot(w, r)
if raw {
return w.Body.String()
} else {
var response tgWebhookResponse
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
t.Error(err)
}
assert.Equal(t, response.ChatID, request.Message.Chat.ID)
assert.Equal(t, response.ReplyToMessageID, request.Message.MessageID)
return response.Text
}
}
func TestTelegramIsCommand(t *testing.T) {
setting.telegramBotName = "test_bot"
// Recognize command
doTestTelegramIsCommand(t, "/trace", "trace", true)
doTestTelegramIsCommand(t, "/trace", "trace1234", false)
doTestTelegramIsCommand(t, "/trace", "tra", false)
doTestTelegramIsCommand(t, "/trace", "abcdefg", false)
// Recognize command with parameters
doTestTelegramIsCommand(t, "/trace google.com", "trace", true)
doTestTelegramIsCommand(t, "/trace google.com", "trace1234", false)
doTestTelegramIsCommand(t, "/trace google.com", "tra", false)
doTestTelegramIsCommand(t, "/trace google.com", "abcdefg", false)
// Recognize command with bot name
doTestTelegramIsCommand(t, "/trace@test_bot", "trace", true)
doTestTelegramIsCommand(t, "/trace@test_bot", "trace1234", false)
doTestTelegramIsCommand(t, "/trace@test_bot", "tra", false)
doTestTelegramIsCommand(t, "/trace@test_bot", "abcdefg", false)
doTestTelegramIsCommand(t, "/trace@test_bot_123", "trace", false)
doTestTelegramIsCommand(t, "/trace@test_", "trace", false)
// Recognize command with bot name and parameters
doTestTelegramIsCommand(t, "/trace@test_bot google.com", "trace", true)
doTestTelegramIsCommand(t, "/trace@test_bot google.com", "trace1234", false)
doTestTelegramIsCommand(t, "/trace@test_bot google.com", "tra", false)
doTestTelegramIsCommand(t, "/trace@test_bot google.com", "abcdefg", false)
doTestTelegramIsCommand(t, "/trace@test_bot_123 google.com", "trace", false)
doTestTelegramIsCommand(t, "/trace@test google.com", "trace", false)
}
func TestTelegramBatchRequestFormatSingleServer(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
httpResponse := httpmock.NewStringResponder(200, "Mock")
httpmock.RegisterResponder("GET", "http://alpha:8000/mock?q=cmd", httpResponse)
setting.servers = []string{"alpha"}
setting.domain = ""
setting.proxyPort = 8000
result := telegramBatchRequestFormat(setting.servers, "mock", "cmd", telegramDefaultPostProcess)
expected := "Mock\n\n"
assert.Equal(t, result, expected)
}
func TestTelegramBatchRequestFormatMultipleServers(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
httpResponse := httpmock.NewStringResponder(200, "Mock")
httpmock.RegisterResponder("GET", "http://alpha:8000/mock?q=cmd", httpResponse)
httpmock.RegisterResponder("GET", "http://beta:8000/mock?q=cmd", httpResponse)
httpmock.RegisterResponder("GET", "http://gamma:8000/mock?q=cmd", httpResponse)
setting.servers = []string{
"alpha",
"beta",
"gamma",
}
setting.domain = ""
setting.proxyPort = 8000
result := telegramBatchRequestFormat(setting.servers, "mock", "cmd", telegramDefaultPostProcess)
expected := "alpha\nMock\n\nbeta\nMock\n\ngamma\nMock\n\n"
assert.Equal(t, result, expected)
}
func TestWebHandlerTelegramBotBadJSON(t *testing.T) {
requestBody := strings.NewReader("{bad json}")
r := httptest.NewRequest(http.MethodGet, "/telegram/", requestBody)
w := httptest.NewRecorder()
webHandlerTelegramBot(w, r)
response := w.Body.String()
assert.Equal(t, response, "")
}
func TestWebHandlerTelegramBotTrace(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
httpResponse := httpmock.NewStringResponder(200, "Mock Response")
httpmock.RegisterResponder("GET", "http://alpha:8000/traceroute?q=1.1.1.1", httpResponse)
setting.servers = []string{"alpha"}
setting.domain = ""
setting.proxyPort = 8000
response := mockTelegramCall(t, "/trace 1.1.1.1", false)
assert.Equal(t, response, "```\nMock Response\n```")
}
func TestWebHandlerTelegramBotTraceWithServerList(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
httpResponse := httpmock.NewStringResponder(200, "Mock Response")
httpmock.RegisterResponder("GET", "http://alpha:8000/traceroute?q=1.1.1.1", httpResponse)
setting.servers = []string{"alpha"}
setting.domain = ""
setting.proxyPort = 8000
response := mockTelegramEndpointCall(t, "/telegram/alpha", "/trace 1.1.1.1", false)
assert.Equal(t, response, "```\nMock Response\n```")
}
func TestWebHandlerTelegramBotRoute(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
httpResponse := httpmock.NewStringResponder(200, "Mock Response")
httpmock.RegisterResponder("GET", "http://alpha:8000/bird?q="+url.QueryEscape("show route for 1.1.1.1 primary"), httpResponse)
setting.servers = []string{"alpha"}
setting.domain = ""
setting.proxyPort = 8000
response := mockTelegramCall(t, "/route 1.1.1.1", false)
assert.Equal(t, response, "```\nMock Response\n```")
}
func TestWebHandlerTelegramBotPath(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
httpResponse := httpmock.NewStringResponder(200, `
BGP.as_path: 123 456
`)
httpmock.RegisterResponder("GET", "http://alpha:8000/bird?q="+url.QueryEscape("show route for 1.1.1.1 all primary"), httpResponse)
setting.servers = []string{"alpha"}
setting.domain = ""
setting.proxyPort = 8000
response := mockTelegramCall(t, "/path 1.1.1.1", false)
assert.Equal(t, response, "```\n123 456\n```")
}
func TestWebHandlerTelegramBotPathMissing(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
httpResponse := httpmock.NewStringResponder(200, "No path in this response")
httpmock.RegisterResponder("GET", "http://alpha:8000/bird?q="+url.QueryEscape("show route for 1.1.1.1 all primary"), httpResponse)
setting.servers = []string{"alpha"}
setting.domain = ""
setting.proxyPort = 8000
response := mockTelegramCall(t, "/path 1.1.1.1", false)
assert.Equal(t, response, "```\nempty result\n```")
}
func TestWebHandlerTelegramBotWhois(t *testing.T) {
server := WhoisServer{
t: t,
expectedQuery: "AS6939",
response: AS6939Response,
}
server.Listen()
go server.Run()
defer server.Close()
setting.netSpecificMode = ""
setting.whoisServer = server.server.Addr().String()
response := mockTelegramCall(t, "/whois AS6939", false)
assert.Equal(t, response, "```"+server.response+"```")
}
func TestWebHandlerTelegramBotWhoisDN42Mode(t *testing.T) {
server := WhoisServer{
t: t,
expectedQuery: "AS4242422547",
response: `
Query for AS4242422547
`,
}
server.Listen()
go server.Run()
defer server.Close()
setting.netSpecificMode = "dn42"
setting.whoisServer = server.server.Addr().String()
response := mockTelegramCall(t, "/whois 2547", false)
assert.Equal(t, response, "```"+server.response+"```")
}
func TestWebHandlerTelegramBotWhoisDN42ModeFullASN(t *testing.T) {
server := WhoisServer{
t: t,
expectedQuery: "AS4242422547",
response: `
Query for AS4242422547
`,
}
server.Listen()
go server.Run()
defer server.Close()
setting.netSpecificMode = "dn42"
setting.whoisServer = server.server.Addr().String()
response := mockTelegramCall(t, "/whois 4242422547", false)
assert.Equal(t, response, "```"+server.response+"```")
}
func TestWebHandlerTelegramBotWhoisShortenMode(t *testing.T) {
server := WhoisServer{
t: t,
expectedQuery: "AS6939",
response: `
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.
`,
}
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.`
server.Listen()
go server.Run()
defer server.Close()
setting.netSpecificMode = "shorten"
setting.whoisServer = server.server.Addr().String()
response := mockTelegramCall(t, "/whois AS6939", false)
assert.Equal(t, response, "```\n"+expectedResult+"\n```")
}
func TestWebHandlerTelegramBotHelp(t *testing.T) {
response := mockTelegramCall(t, "/help", false)
if !strings.Contains(response, "/trace") {
t.Error("Did not get help message")
}
}
func TestWebHandlerTelegramBotUnknownCommand(t *testing.T) {
response := mockTelegramCall(t, "/nonexistent", true)
assert.Equal(t, response, "")
}
func TestWebHandlerTelegramBotNotCommand(t *testing.T) {
response := mockTelegramCall(t, "random chat message", true)
assert.Equal(t, response, "")
}
func TestWebHandlerTelegramBotEmptyResponse(t *testing.T) {
server := WhoisServer{
t: t,
expectedQuery: "AS6939",
response: "",
}
server.Listen()
go server.Run()
defer server.Close()
setting.netSpecificMode = ""
setting.whoisServer = server.server.Addr().String()
response := mockTelegramCall(t, "/whois AS6939", false)
assert.Equal(t, response, "```\nempty result\n```")
}
func TestWebHandlerTelegramBotTruncateLongResponse(t *testing.T) {
server := WhoisServer{
t: t,
expectedQuery: "AS6939",
response: strings.Repeat("A", 65536),
}
server.Listen()
go server.Run()
defer server.Close()
setting.netSpecificMode = ""
setting.whoisServer = server.server.Addr().String()
response := mockTelegramCall(t, "/whois AS6939", false)
assert.Equal(t, response, "```\n"+strings.Repeat("A", 4096)+"\n```")
}

View File

@@ -1,31 +0,0 @@
FROM golang:buster AS step_0
#if defined(ARCH_AMD64)
ENV GOOS=linux GOARCH=amd64
#elif defined(ARCH_I386)
ENV GOOS=linux GOARCH=386
#elif defined(ARCH_ARM32V7)
ENV GOOS=linux GOARCH=arm
#elif defined(ARCH_ARM64V8)
ENV GOOS=linux GOARCH=arm64
#elif defined(ARCH_PPC64LE)
ENV GOOS=linux GOARCH=ppc64le
#elif defined(ARCH_S390X)
ENV GOOS=linux GOARCH=s390x
#else
#error "Architecture not set"
#endif
ENV CGO_ENABLED=0 GO111MODULE=on
WORKDIR /root
COPY . .
# go-bindata is run on the build host as part of the go generate step
RUN GOARCH=amd64 go get -u github.com/kevinburke/go-bindata/...
RUN go generate
RUN go build -ldflags "-w -s" -o /frontend
################################################################################
FROM scratch AS step_1
COPY --from=step_0 /frontend /
ENTRYPOINT ["/frontend"]

View File

@@ -1,10 +1,18 @@
package main package main
import ( import (
"embed"
"html/template"
"net/url"
"strings" "strings"
"text/template"
) )
// import templates and other assets
//go:embed assets
var assets embed.FS
const TEMPLATE_PATH = "assets/templates/"
// template argument structures // template argument structures
// page // page
@@ -12,12 +20,13 @@ type TemplatePage struct {
// Global options // Global options
Options map[string]string Options map[string]string
Servers []string Servers []string
ServersEscaped []string
ServersDisplay []string ServersDisplay []string
// Parameters related to current request // Parameters related to current request
AllServersLinkActive bool AllServersLinkActive bool
AllServerTitle string
AllServersURL string AllServersURL string
AllServersURLCustom string
// Whois specific handling (for its unique URL) // Whois specific handling (for its unique URL)
IsWhois bool IsWhois bool
@@ -30,7 +39,8 @@ type TemplatePage struct {
// Generated content to be displayed // Generated content to be displayed
Title string Title string
Brand string Brand string
Content string BrandURL string
Content template.HTML
} }
// summary // summary
@@ -64,7 +74,7 @@ type TemplateSummary struct {
// whois // whois
type TemplateWhois struct { type TemplateWhois struct {
Target string Target string
Result string Result template.HTML
} }
// bgpmap // bgpmap
@@ -78,7 +88,7 @@ type TemplateBGPmap struct {
type TemplateBird struct { type TemplateBird struct {
ServerName string ServerName string
Target string Target string
Result string Result template.HTML
} }
// global variable to hold the templates // global variable to hold the templates
@@ -95,7 +105,13 @@ var requiredTemplates = [...]string{
"bird", "bird",
} }
// import templates from bindata // define functions to be made available in templates
var funcMap = template.FuncMap{
"pathescape": url.PathEscape,
}
// import templates from embedded assets
func ImportTemplates() { func ImportTemplates() {
@@ -105,13 +121,16 @@ func ImportTemplates() {
// for each template that is needed // for each template that is needed
for _, tmpl := range requiredTemplates { for _, tmpl := range requiredTemplates {
// extract the template definition from the bindata // extract the template definition from the embedded assets
def := MustAssetString("templates/" + tmpl + ".tpl") def, err := assets.ReadFile(TEMPLATE_PATH + tmpl + ".tpl")
if err != nil {
panic("Unable to read template (" + TEMPLATE_PATH + tmpl + ": " + err.Error())
}
// and add it to the template library // and add it to the template library
template, err := template.New(tmpl).Parse(def) template, err := template.New(tmpl).Funcs(funcMap).Parse(string(def))
if err != nil { if err != nil {
panic("Unable to parse template (templates/" + tmpl + ": " + err.Error()) panic("Unable to parse template (" + TEMPLATE_PATH + tmpl + ": " + err.Error())
} }
// store in the library // store in the library

25
frontend/template_test.go Normal file
View File

@@ -0,0 +1,25 @@
package main
import (
"testing"
"github.com/magiconair/properties/assert"
)
func TestSummaryRowDataNameHasPrefix(t *testing.T) {
data := SummaryRowData{
Name: "mock",
}
assert.Equal(t, data.NameHasPrefix("m"), true)
assert.Equal(t, data.NameHasPrefix("n"), false)
}
func TestSummaryRowDataNameContains(t *testing.T) {
data := SummaryRowData{
Name: "mock",
}
assert.Equal(t, data.NameContains("oc"), true)
assert.Equal(t, data.NameContains("no"), false)
}

View File

@@ -0,0 +1,151 @@
Table master4:
172.20.0.53/32 unicast [ibgp_sjc2 2023-04-29 from fd86:bad:11b7:22::1] * (100/38) [AS4242423914i]
via 169.254.108.122 on igp-sjc2
Type: BGP univ
BGP.origin: IGP
BGP.as_path: 4242423914
BGP.next_hop: 172.20.229.122
BGP.med: 50
BGP.local_pref: 100
BGP.community: (64511,1) (64511,24) (64511,34)
BGP.large_community: (4242421080, 101, 44) (4242421080, 103, 122) (4242421080, 104, 1)
unicast [miaotony_2688 2023-04-29 from fe80::2688] (100) [AS4242423914i]
via 172.23.6.6 on dn42las-miaoton
Type: BGP univ
BGP.origin: IGP
BGP.as_path: 4242422688 4242423914
BGP.next_hop: 172.23.6.6
BGP.med: 50
BGP.local_pref: 100
BGP.community: (64511,3) (64511,24) (64511,34)
BGP.large_community: (4242421080, 104, 1) (4242421080, 101, 44) (4242421080, 103, 126)
unicast [imlonghao_1888 2023-04-17] (100) [AS4242423914i]
via fe80::1888 on dn42-imlonghao
Type: BGP univ
BGP.origin: IGP
BGP.as_path: 4242421888 4242423914
BGP.next_hop: :: fe80::1888
BGP.med: 50
BGP.local_pref: 100
BGP.community: (64511,1) (64511,24) (64511,34)
BGP.large_community: (4242421080, 104, 1) (4242421080, 101, 44) (4242421080, 103, 126)
unicast [ciplc_3021 2023-04-29 from fe80::943e] (100) [AS4242423914i]
via 172.23.33.161 on dn42-ciplc
Type: BGP univ
BGP.origin: IGP
BGP.as_path: 4242423021 4242423914
BGP.next_hop: 172.23.33.161
BGP.med: 50
BGP.local_pref: 100
BGP.community: (64511,1) (64511,24) (64511,34)
BGP.large_community: (4242421080, 104, 1) (4242421080, 101, 44) (4242421080, 103, 126)
unicast [iedon_2189 2023-04-29 from fe80::2189:ef] (100) [AS4242423914i]
via 172.23.91.114 on dn42-iedon
Type: BGP univ
BGP.origin: IGP
BGP.as_path: 4242422189 4242423914
BGP.next_hop: 172.23.91.114
BGP.med: 65
BGP.local_pref: 100
BGP.community: (64511,24) (64511,33) (64511,3)
BGP.large_community: (4242422189, 1, 4) (4242421080, 104, 1) (4242421080, 101, 44) (4242421080, 103, 126)
unicast [prevarinite_2475 2023-04-19] (100) [AS4242423914i]
via fe80::7072:6576:6172:1 on dn42-prevarinit
Type: BGP univ
BGP.origin: IGP
BGP.as_path: 4242422475 4242423192 4242423914
BGP.next_hop: :: fe80::7072:6576:6172:1
BGP.med: 50
BGP.local_pref: 100
BGP.community: (64511,1) (64511,24) (64511,34)
BGP.large_community: (4242421080, 104, 1) (4242421080, 101, 44) (4242421080, 103, 126)
unicast [lare_3035 2023-04-29] (100) [AS4242423914i]
via fe80::3035:132 on dn42-lare
Type: BGP univ
BGP.origin: IGP
BGP.as_path: 4242423035 4242423914
BGP.next_hop: :: fe80::3035:132
BGP.med: 50
BGP.local_pref: 100
BGP.community: (64511,3) (64511,34) (64511,24)
BGP.large_community: (4242421080, 104, 1) (4242421080, 101, 44) (4242421080, 103, 126)
unicast [hinata_3724 2023-04-29 from fe80::3724] (100) [AS4242423914i]
via 172.23.215.228 on dn42las-hinata
Type: BGP univ
BGP.origin: IGP
BGP.as_path: 4242423724 4201271111 4242423914
BGP.next_hop: 172.23.215.228
BGP.med: 70
BGP.local_pref: 100
BGP.community: (64511,22) (64511,1) (64511,34)
BGP.large_community: (4242421080, 104, 1) (4242421080, 101, 44) (4242421080, 103, 126)
unicast [liki4_0927 2023-04-21] (100) [AS4242423914i]
via fe80::927 on dn42-liki4
Type: BGP univ
BGP.origin: IGP
BGP.as_path: 4242420927 4242421888 4242423914
BGP.next_hop: :: fe80::927
BGP.med: 50
BGP.local_pref: 100
BGP.community: (64511,2) (64511,24) (64511,34)
BGP.large_community: (4242421080, 104, 1) (4242421080, 101, 44) (4242421080, 103, 126)
unicast [eastbound_2633 2023-04-29 from fe80::2633] (100) [AS4242423914i]
via 172.23.250.42 on dn42las-eastbnd
Type: BGP univ
BGP.origin: IGP
BGP.as_path: 4242422633 4242423914
BGP.next_hop: 172.23.250.42
BGP.med: 50
BGP.local_pref: 100
BGP.community: (64511,24) (64511,34) (64511,3)
BGP.large_community: (4242422633, 101, 44) (4242422633, 103, 36) (4242421080, 104, 1) (4242421080, 101, 44) (4242421080, 103, 126)
unicast [yura_2464 2023-04-29] (100) [AS4242423914i]
via fe80::2464 on dn42las-yura
Type: BGP univ
BGP.origin: IGP
BGP.as_path: 4242422464 4242423914
BGP.next_hop: :: fe80::2464
BGP.med: 50
BGP.local_pref: 100
BGP.community: (64511,1) (64511,24) (64511,34)
BGP.large_community: (4242422464, 2, 4242423914) (4242422464, 64511, 44) (4242422464, 64511, 1840) (4242421080, 104, 1) (4242421080, 101, 44) (4242421080, 103, 126)
unicast [ibgp_fra 2023-04-29 from fd86:bad:11b7:117::1] (100/186) [AS4242423914i]
via 169.254.108.113 on igp-chi
Type: BGP univ
BGP.origin: IGP
BGP.as_path: 4242423914
BGP.next_hop: 172.20.229.117
BGP.med: 50
BGP.local_pref: 100
BGP.community: (64511,1) (64511,24) (64511,34)
BGP.large_community: (4242421080, 101, 41) (4242421080, 103, 117) (4242421080, 104, 3)
unicast [ibgp_sgp 2023-04-29 from fd86:bad:11b7:239::1] (100/200) [AS4242423914i]
via 169.254.108.39 on igp-sgp
Type: BGP univ
BGP.origin: IGP
BGP.as_path: 4242423914
BGP.next_hop: 172.22.108.39
BGP.med: 50
BGP.local_pref: 100
BGP.community: (64511,4) (64511,24) (64511,34)
BGP.large_community: (4242421080, 101, 51) (4242421080, 103, 39) (4242421080, 104, 4)
unicast [ibgp_ymq 2023-04-30 from fd86:bad:11b7:23::1] (100/105) [AS4242423914i]
via 169.254.108.113 on igp-chi
Type: BGP univ
BGP.origin: IGP
BGP.as_path: 4242423914
BGP.next_hop: 172.20.229.123
BGP.med: 50
BGP.local_pref: 100
BGP.community: (64511,3) (64511,24) (64511,34)
BGP.large_community: (4242421080, 101, 42) (4242421080, 103, 123) (4242421080, 104, 2)
unicast [cola_3391 18:41:16.608 from fe80::3391] (100) [AS4242423914i]
via 172.22.96.65 on dn42-cola
Type: BGP univ
BGP.origin: IGP
BGP.as_path: 4242423391 4242420604 4242423914
BGP.next_hop: 172.22.96.65
BGP.med: 50
BGP.local_pref: 100
BGP.community: (64511,4) (64511,34) (64511,24)
BGP.large_community: (4242420604, 2, 50) (4242420604, 501, 4242423914) (4242420604, 502, 44) (4242420604, 504, 4) (4242421080, 104, 1) (4242421080, 101, 44) (4242421080, 103, 126)

View File

@@ -2,26 +2,40 @@ package main
import ( import (
"bytes" "bytes"
"encoding/base64"
"fmt" "fmt"
"html" "html"
"html/template"
"io/fs"
"net"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"strings" "strings"
assetfs "github.com/elazarl/go-bindata-assetfs"
"github.com/gorilla/handlers" "github.com/gorilla/handlers"
) )
var primitiveMap = map[string]string{ var primitiveMap = map[string]string{
"summary": "show protocols", "summary": "show protocols",
"detail": "show protocols all %s", "detail": "show protocols all %s",
"route_from_protocol": "show route protocol %s",
"route_from_protocol_all": "show route protocol %s all",
"route_from_protocol_primary": "show route protocol %s primary",
"route_from_protocol_all_primary": "show route protocol %s all primary",
"route_filtered_from_protocol": "show route filtered protocol %s",
"route_filtered_from_protocol_all": "show route filtered protocol %s all",
"route_from_origin": "show route where bgp_path.last = %s",
"route_from_origin_all": "show route where bgp_path.last = %s all",
"route_from_origin_primary": "show route where bgp_path.last = %s primary",
"route_from_origin_all_primary": "show route where bgp_path.last = %s all primary",
"route": "show route for %s", "route": "show route for %s",
"route_all": "show route for %s all", "route_all": "show route for %s all",
"route_where": "show route where net ~ [ %s ]", "route_where": "show route where net ~ [ %s ]",
"route_where_all": "show route where net ~ [ %s ] all", "route_where_all": "show route where net ~ [ %s ] all",
"route_generic": "show route %s", "route_generic": "show route %s",
"generic": "show %s", "generic": "show %s",
"whois": "%s",
"traceroute": "%s", "traceroute": "%s",
} }
@@ -55,7 +69,7 @@ func webHandlerWhois(w http.ResponseWriter, r *http.Request) {
renderPageTemplate( renderPageTemplate(
w, r, w, r,
" - whois "+html.EscapeString(target), " - whois "+html.EscapeString(target),
buffer.String(), template.HTML(buffer.String()),
) )
} }
@@ -88,7 +102,7 @@ func webBackendCommunicator(endpoint string, command string) func(w http.Respons
var content string var content string
for i, response := range responses { for i, response := range responses {
var result string var result template.HTML
if (endpoint == "bird") && backendCommand == "show protocols" && len(response) > 4 && strings.ToLower(response[0:4]) == "name" { if (endpoint == "bird") && backendCommand == "show protocols" && len(response) > 4 && strings.ToLower(response[0:4]) == "name" {
result = summaryTable(response, servers[i]) result = summaryTable(response, servers[i])
} else { } else {
@@ -123,7 +137,7 @@ func webBackendCommunicator(endpoint string, command string) func(w http.Respons
renderPageTemplate( renderPageTemplate(
w, r, w, r,
" - "+endpoint+" "+backendCommand, " - "+endpoint+" "+backendCommand,
content, template.HTML(content),
) )
} }
} }
@@ -153,11 +167,15 @@ func webHandlerBGPMap(endpoint string, command string) func(w http.ResponseWrite
var servers []string = strings.Split(split[1], "+") var servers []string = strings.Split(split[1], "+")
var responses []string = batchRequest(servers, endpoint, backendCommand) var responses []string = batchRequest(servers, endpoint, backendCommand)
// encode result with base64 to prevent xss
result := birdRouteToGraphviz(servers, responses, urlCommands)
result = base64.StdEncoding.EncodeToString([]byte(result))
// render the bgpmap result template // render the bgpmap result template
args := TemplateBGPmap{ args := TemplateBGPmap{
Servers: servers, Servers: servers,
Target: backendCommand, Target: backendCommand,
Result: birdRouteToGraphviz(servers, responses, urlCommands), Result: result,
} }
tmpl := TemplateLibrary["bgpmap"] tmpl := TemplateLibrary["bgpmap"]
@@ -170,26 +188,25 @@ func webHandlerBGPMap(endpoint string, command string) func(w http.ResponseWrite
renderPageTemplate( renderPageTemplate(
w, r, w, r,
" - "+html.EscapeString(endpoint+" "+backendCommand), " - "+html.EscapeString(endpoint+" "+backendCommand),
buffer.String(), template.HTML(buffer.String()),
) )
} }
} }
// set up routing paths and start webserver // set up routing paths and start webserver
func webServerStart() { func webServerStart(l net.Listener) {
// redirect main page to all server summary // redirect main page to all server summary
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/summary/"+url.PathEscape(strings.Join(setting.servers, "+")), 302) http.Redirect(w, r, "/summary/"+url.PathEscape(strings.Join(setting.servers, "+")), 302)
}) })
// serve static pages using the AssetFS and bindata // serve static pages using embedded assets from template.go
fs := http.FileServer(&assetfs.AssetFS{ subfs, err := fs.Sub(assets, "assets")
Asset: Asset, if err != nil {
AssetDir: AssetDir, panic("Webserver fs.sub failed: " + err.Error())
AssetInfo: AssetInfo, }
Prefix: "", fs := http.FileServer(http.FS(subfs))
})
http.Handle("/static/", fs) http.Handle("/static/", fs)
http.Handle("/robots.txt", fs) http.Handle("/robots.txt", fs)
@@ -198,6 +215,16 @@ func webServerStart() {
// backend routes // backend routes
http.HandleFunc("/summary/", webBackendCommunicator("bird", "summary")) http.HandleFunc("/summary/", webBackendCommunicator("bird", "summary"))
http.HandleFunc("/detail/", webBackendCommunicator("bird", "detail")) http.HandleFunc("/detail/", webBackendCommunicator("bird", "detail"))
http.HandleFunc("/route_filtered_from_protocol/", webBackendCommunicator("bird", "route_filtered_from_protocol"))
http.HandleFunc("/route_filtered_from_protocol_all/", webBackendCommunicator("bird", "route_filtered_from_protocol_all"))
http.HandleFunc("/route_from_protocol/", webBackendCommunicator("bird", "route_from_protocol"))
http.HandleFunc("/route_from_protocol_all/", webBackendCommunicator("bird", "route_from_protocol_all"))
http.HandleFunc("/route_from_protocol_primary/", webBackendCommunicator("bird", "route_from_protocol_primary"))
http.HandleFunc("/route_from_protocol_all_primary/", webBackendCommunicator("bird", "route_from_protocol_all_primary"))
http.HandleFunc("/route_from_origin/", webBackendCommunicator("bird", "route_from_origin"))
http.HandleFunc("/route_from_origin_all/", webBackendCommunicator("bird", "route_from_origin_all"))
http.HandleFunc("/route_from_origin_primary/", webBackendCommunicator("bird", "route_from_origin_primary"))
http.HandleFunc("/route_from_origin_all_primary/", webBackendCommunicator("bird", "route_from_origin_all_primary"))
http.HandleFunc("/route/", webBackendCommunicator("bird", "route")) http.HandleFunc("/route/", webBackendCommunicator("bird", "route"))
http.HandleFunc("/route_all/", webBackendCommunicator("bird", "route_all")) http.HandleFunc("/route_all/", webBackendCommunicator("bird", "route_all"))
http.HandleFunc("/route_bgpmap/", webHandlerBGPMap("bird", "route_bgpmap")) http.HandleFunc("/route_bgpmap/", webHandlerBGPMap("bird", "route_bgpmap"))
@@ -212,5 +239,5 @@ func webServerStart() {
http.HandleFunc("/telegram/", webHandlerTelegramBot) http.HandleFunc("/telegram/", webHandlerTelegramBot)
// Start HTTP server // Start HTTP server
http.ListenAndServe(setting.listen, handlers.LoggingHandler(os.Stdout, http.DefaultServeMux)) http.Serve(l, handlers.LoggingHandler(os.Stdout, http.DefaultServeMux))
} }

View File

@@ -0,0 +1,89 @@
package main
import (
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"github.com/jarcoal/httpmock"
"github.com/magiconair/properties/assert"
)
func TestServerError(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/error", nil)
w := httptest.NewRecorder()
serverError(w, r)
assert.Equal(t, w.Code, http.StatusInternalServerError)
}
func TestWebHandlerWhois(t *testing.T) {
server := WhoisServer{
t: t,
expectedQuery: "AS6939",
response: AS6939Response,
}
server.Listen()
go server.Run()
defer server.Close()
setting.netSpecificMode = ""
setting.whoisServer = server.server.Addr().String()
r := httptest.NewRequest(http.MethodGet, "/whois/AS6939", nil)
w := httptest.NewRecorder()
webHandlerWhois(w, r)
assert.Equal(t, w.Code, http.StatusOK)
if !strings.Contains(w.Body.String(), "HURRICANE") {
t.Error("Body does not contain whois result")
}
}
func TestWebBackendCommunicator(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
input := readDataFile(t, "frontend/test_data/bgpmap_case1.txt")
httpResponse := httpmock.NewStringResponder(200, input)
httpmock.RegisterResponder("GET", "http://alpha:8000/bird?q="+url.QueryEscape("show route for 1.1.1.1 all"), httpResponse)
setting.servers = []string{"alpha"}
setting.domain = ""
setting.proxyPort = 8000
setting.dnsInterface = ""
setting.whoisServer = ""
r := httptest.NewRequest(http.MethodGet, "/route_bgpmap/alpha/1.1.1.1", nil)
w := httptest.NewRecorder()
handler := webBackendCommunicator("bird", "route_all")
handler(w, r)
assert.Equal(t, w.Code, http.StatusOK)
}
func TestWebHandlerBGPMap(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
input := readDataFile(t, "frontend/test_data/bgpmap_case1.txt")
httpResponse := httpmock.NewStringResponder(200, input)
httpmock.RegisterResponder("GET", "http://alpha:8000/bird?q="+url.QueryEscape("show route for 1.1.1.1 all"), httpResponse)
setting.servers = []string{"alpha"}
setting.domain = ""
setting.proxyPort = 8000
setting.dnsInterface = ""
setting.whoisServer = ""
r := httptest.NewRequest(http.MethodGet, "/route_bgpmap/alpha/1.1.1.1", nil)
w := httptest.NewRecorder()
handler := webHandlerBGPMap("bird", "route_bgpmap")
handler(w, r)
assert.Equal(t, w.Code, http.StatusOK)
}

View File

@@ -1,22 +1,59 @@
package main package main
import ( import (
"io/ioutil" "io"
"net" "net"
"os/exec"
"strings"
"time"
"github.com/google/shlex"
) )
// Send a whois request // Send a whois request
func whois(s string) string { func whois(s string) string {
conn, err := net.Dial("tcp", setting.whoisServer+":43") if setting.whoisServer == "" {
return ""
}
if strings.HasPrefix(setting.whoisServer, "/") {
args, err := shlex.Split(setting.whoisServer)
if err != nil {
return err.Error()
}
args = append(args, s)
cmd := exec.Command(args[0], args[1:]...)
output, err := cmd.CombinedOutput()
if len(output) > 65535 {
output = output[:65535]
}
if err != nil {
return err.Error() + "\n" + string(output)
} else {
return string(output)
}
} else {
buf := make([]byte, 65536)
whoisServer := setting.whoisServer
if !strings.Contains(whoisServer, ":") {
whoisServer = whoisServer + ":43"
}
conn, err := net.DialTimeout("tcp", whoisServer, 5*time.Second)
if err != nil { if err != nil {
return err.Error() return err.Error()
} }
defer conn.Close() defer conn.Close()
conn.Write([]byte(s + "\r\n")) conn.Write([]byte(s + "\r\n"))
result, err := ioutil.ReadAll(conn)
if err != nil { n, err := io.ReadFull(conn, buf)
return err.Error() if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
return err.Error() + "\n" + string(buf[:n])
} }
return string(result) return string(buf[:n])
}
} }

128
frontend/whois_test.go Normal file
View File

@@ -0,0 +1,128 @@
package main
import (
"bufio"
"net"
"strings"
"testing"
)
type WhoisServer struct {
t *testing.T
expectedQuery string
response string
server net.Listener
}
const AS6939Response = `
ASNumber: 6939
ASName: HURRICANE
ASHandle: AS6939
RegDate: 1996-06-28
Updated: 2003-11-04
Ref: https://rdap.arin.net/registry/autnum/6939
`
func (s *WhoisServer) Listen() {
var err error
s.server, err = net.Listen("tcp", "127.0.0.1:0")
if err != nil {
s.t.Error(err)
}
}
func (s *WhoisServer) Run() {
for {
conn, err := s.server.Accept()
if err != nil {
break
}
if conn == nil {
break
}
reader := bufio.NewReader(conn)
query, err := reader.ReadBytes('\n')
if err != nil {
break
}
if strings.TrimSpace(string(query)) != s.expectedQuery {
s.t.Errorf("Query %s doesn't match expectation %s", string(query), s.expectedQuery)
}
conn.Write([]byte(s.response))
conn.Close()
}
}
func (s *WhoisServer) Close() {
if s.server == nil {
return
}
s.server.Close()
}
func TestWhois(t *testing.T) {
server := WhoisServer{
t: t,
expectedQuery: "AS6939",
response: AS6939Response,
}
server.Listen()
go server.Run()
defer server.Close()
setting.whoisServer = server.server.Addr().String()
result := whois("AS6939")
if !strings.Contains(result, "HURRICANE") {
t.Errorf("Whois AS6939 failed, got %s", result)
}
}
func TestWhoisWithoutServer(t *testing.T) {
setting.whoisServer = ""
result := whois("AS6939")
if result != "" {
t.Errorf("Whois AS6939 without server produced output, got %s", result)
}
}
func TestWhoisConnectionError(t *testing.T) {
setting.whoisServer = "127.0.0.1:0"
result := whois("AS6939")
if !strings.Contains(result, "connect: connection refused") {
t.Errorf("Whois AS6939 without server produced output, got %s", result)
}
}
func TestWhoisHostProcess(t *testing.T) {
setting.whoisServer = "/bin/sh -c \"echo Mock Result\""
result := whois("AS6939")
if result != "Mock Result\n" {
t.Errorf("Whois didn't produce expected result, got %s", result)
}
}
func TestWhoisHostProcessMalformedCommand(t *testing.T) {
setting.whoisServer = "/bin/sh -c \"mock"
result := whois("AS6939")
if result != "EOF found when expecting closing quote" {
t.Errorf("Whois didn't produce expected result, got %s", result)
}
}
func TestWhoisHostProcessError(t *testing.T) {
setting.whoisServer = "/nonexistent"
result := whois("AS6939")
if !strings.Contains(result, "no such file or directory") {
t.Errorf("Whois didn't produce expected result, got %s", result)
}
}
func TestWhoisHostProcessVeryLong(t *testing.T) {
setting.whoisServer = "/bin/sh -c \"for i in $(seq 1 131072); do printf 'A'; done\""
result := whois("AS6939")
if len(result) != 65535 {
t.Errorf("Whois result incorrectly truncated, actual len %d", len(result))
}
}

28
proxy/Dockerfile Normal file
View File

@@ -0,0 +1,28 @@
FROM golang AS step_0
ENV CGO_ENABLED=0 GO111MODULE=on
WORKDIR /root
COPY . .
RUN go build -ldflags "-w -s" -o /proxy
################################################################################
FROM alpine:edge AS step_1
WORKDIR /root
RUN apk add --no-cache build-base linux-headers
RUN wget https://sourceforge.net/projects/traceroute/files/traceroute/traceroute-2.1.3/traceroute-2.1.3.tar.gz/download \
-O traceroute-2.1.3.tar.gz
RUN tar xvf traceroute-2.1.3.tar.gz \
&& cd traceroute-2.1.3 \
&& make -j4 LDFLAGS="-static" \
&& strip /root/traceroute-2.1.3/traceroute/traceroute
################################################################################
FROM scratch AS step_2
ENV PATH=/
COPY --from=step_0 /proxy /
COPY --from=step_1 /root/traceroute-2.1.3/traceroute/traceroute /
ENTRYPOINT ["/proxy"]

View File

@@ -1,24 +1,30 @@
package main package main
import ( import (
"bytes"
"io" "io"
"net" "net"
"net/http" "net/http"
"strings"
) )
const MAX_LINE_SIZE = 1024
// Read a line from bird socket, removing preceding status number, output it. // Read a line from bird socket, removing preceding status number, output it.
// Returns if there are more lines. // Returns if there are more lines.
func birdReadln(bird io.Reader, w io.Writer) bool { func birdReadln(bird io.Reader, w io.Writer) bool {
// Read from socket byte by byte, until reaching newline character // Read from socket byte by byte, until reaching newline character
c := make([]byte, 1024, 1024) c := make([]byte, MAX_LINE_SIZE)
pos := 0 pos := 0
for { for {
if pos >= 1024 { // Leave one byte for newline character
if pos >= MAX_LINE_SIZE-1 {
break break
} }
_, err := bird.Read(c[pos : pos+1]) _, err := bird.Read(c[pos : pos+1])
if err != nil { if err != nil {
panic(err) w.Write([]byte(err.Error()))
return false
} }
if c[pos] == byte('\n') { if c[pos] == byte('\n') {
break break
@@ -27,6 +33,7 @@ func birdReadln(bird io.Reader, w io.Writer) bool {
} }
c = c[:pos+1] c = c[:pos+1]
c[pos] = '\n'
// print(string(c[:])) // print(string(c[:]))
// Remove preceding status number, different situations // Remove preceding status number, different situations
@@ -67,7 +74,13 @@ func birdHandler(httpW http.ResponseWriter, httpR *http.Request) {
birdReadln(bird, nil) birdReadln(bird, nil)
birdWriteln(bird, "restrict") birdWriteln(bird, "restrict")
birdReadln(bird, nil) var restrictedConfirmation bytes.Buffer
birdReadln(bird, &restrictedConfirmation)
if !strings.Contains(restrictedConfirmation.String(), "Access restricted") {
httpW.WriteHeader(http.StatusInternalServerError)
httpW.Write([]byte("could not verify that bird access was restricted"))
return
}
birdWriteln(bird, query) birdWriteln(bird, query)
for birdReadln(bird, httpW) { for birdReadln(bird, httpW) {
} }

213
proxy/bird_test.go Normal file
View File

@@ -0,0 +1,213 @@
package main
import (
"bufio"
"bytes"
"io/ioutil"
"net"
"net/http"
"net/http/httptest"
"net/url"
"path"
"strings"
"testing"
"github.com/magiconair/properties/assert"
)
type BirdServer struct {
t *testing.T
expectedQuery string
response string
server net.Listener
socket string
injectError string
}
func (s *BirdServer) initSocket() {
tmpDir, err := ioutil.TempDir("", "bird-lgproxy-go-mock")
if err != nil {
s.t.Fatal(err)
}
s.socket = path.Join(tmpDir, "mock.socket")
}
func (s *BirdServer) Listen() {
s.initSocket()
var err error
s.server, err = net.Listen("unix", s.socket)
if err != nil {
s.t.Error(err)
}
}
func (s *BirdServer) Run() {
for {
conn, err := s.server.Accept()
if err != nil {
break
}
if conn == nil {
break
}
reader := bufio.NewReader(conn)
conn.Write([]byte("1234 Hello from mock bird\n"))
query, err := reader.ReadBytes('\n')
if err != nil {
break
}
if strings.TrimSpace(string(query)) != "restrict" {
s.t.Errorf("Did not restrict bird permissions")
}
if s.injectError == "restriction" {
conn.Write([]byte("1234 Restriction is disabled!\n"))
} else {
conn.Write([]byte("1234 Access restricted\n"))
}
query, err = reader.ReadBytes('\n')
if err != nil {
break
}
if strings.TrimSpace(string(query)) != s.expectedQuery {
s.t.Errorf("Query %s doesn't match expectation %s", string(query), s.expectedQuery)
}
responseList := strings.Split(s.response, "\n")
for i := range responseList {
if i == len(responseList)-1 {
if s.injectError == "eof" {
conn.Write([]byte("0000 " + responseList[i]))
} else {
conn.Write([]byte("0000 " + responseList[i] + "\n"))
}
} else {
conn.Write([]byte("1234 " + responseList[i] + "\n"))
}
}
conn.Close()
}
}
func (s *BirdServer) Close() {
if s.server == nil {
return
}
s.server.Close()
}
func TestBirdReadln(t *testing.T) {
input := strings.NewReader("1234 Bird Message\n")
var output bytes.Buffer
birdReadln(input, &output)
assert.Equal(t, output.String(), "Bird Message\n")
}
func TestBirdReadlnNoPrefix(t *testing.T) {
input := strings.NewReader(" Message without prefix\n")
var output bytes.Buffer
birdReadln(input, &output)
assert.Equal(t, output.String(), "Message without prefix\n")
}
func TestBirdReadlnVeryLongLine(t *testing.T) {
input := strings.NewReader(strings.Repeat("A", 4096))
var output bytes.Buffer
birdReadln(input, &output)
assert.Equal(t, output.String(), strings.Repeat("A", 1022)+"\n")
}
func TestBirdWriteln(t *testing.T) {
var output bytes.Buffer
birdWriteln(&output, "Test command")
assert.Equal(t, output.String(), "Test command\n")
}
func TestBirdHandlerWithoutQuery(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/bird", nil)
w := httptest.NewRecorder()
birdHandler(w, r)
}
func TestBirdHandlerWithQuery(t *testing.T) {
server := BirdServer{
t: t,
expectedQuery: "show protocols",
response: "Mock Response\nSecond Line",
injectError: "",
}
server.Listen()
go server.Run()
defer server.Close()
setting.birdSocket = server.socket
r := httptest.NewRequest(http.MethodGet, "/bird?q="+url.QueryEscape(server.expectedQuery), nil)
w := httptest.NewRecorder()
birdHandler(w, r)
assert.Equal(t, w.Code, http.StatusOK)
assert.Equal(t, w.Body.String(), server.response+"\n")
}
func TestBirdHandlerWithBadSocket(t *testing.T) {
setting.birdSocket = "/nonexistent.sock"
r := httptest.NewRequest(http.MethodGet, "/bird?q="+url.QueryEscape("mock"), nil)
w := httptest.NewRecorder()
birdHandler(w, r)
assert.Equal(t, w.Code, http.StatusInternalServerError)
}
func TestBirdHandlerWithoutRestriction(t *testing.T) {
server := BirdServer{
t: t,
expectedQuery: "show protocols",
response: "Mock Response",
injectError: "restriction",
}
server.Listen()
go server.Run()
defer server.Close()
setting.birdSocket = server.socket
r := httptest.NewRequest(http.MethodGet, "/bird?q="+url.QueryEscape("mock"), nil)
w := httptest.NewRecorder()
birdHandler(w, r)
assert.Equal(t, w.Code, http.StatusInternalServerError)
}
func TestBirdHandlerEOF(t *testing.T) {
server := BirdServer{
t: t,
expectedQuery: "show protocols",
response: "Mock Response\nSecond Line",
injectError: "eof",
}
server.Listen()
go server.Run()
defer server.Close()
setting.birdSocket = server.socket
r := httptest.NewRequest(http.MethodGet, "/bird?q="+url.QueryEscape("show protocols"), nil)
w := httptest.NewRecorder()
birdHandler(w, r)
assert.Equal(t, w.Code, http.StatusOK)
assert.Equal(t, w.Body.String(), "Mock Response\nEOF")
}

View File

@@ -1,8 +1,28 @@
module github.com/xddxdd/bird-lg-go/proxy module github.com/xddxdd/bird-lg-go/proxy
go 1.15 go 1.17
require ( require (
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/gorilla/handlers v1.5.1 github.com/gorilla/handlers v1.5.1
github.com/magiconair/properties v1.8.7
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.16.0
)
require (
github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/spf13/afero v1.9.5 // indirect
github.com/spf13/cast v1.5.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/text v0.9.0 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,8 @@
package main package main
import ( import (
"flag" "fmt"
"net"
"net/http" "net/http"
"os" "os"
"strings" "strings"
@@ -20,71 +21,81 @@ func invalidHandler(httpW http.ResponseWriter, httpR *http.Request) {
httpW.Write([]byte("Invalid Request\n")) httpW.Write([]byte("Invalid Request\n"))
} }
func hasAccess(remoteAddr string) bool {
// setting.allowedIPs will always have at least one element because of how it's defined
if len(setting.allowedIPs) == 0 {
return true
}
if !strings.Contains(remoteAddr, ":") {
return false
}
// Remove port from IP and remove brackets that are around IPv6 addresses
remoteAddr = remoteAddr[0:strings.LastIndex(remoteAddr, ":")]
remoteAddr = strings.Trim(remoteAddr, "[]")
ipObject := net.ParseIP(remoteAddr)
if ipObject == nil {
return false
}
for _, allowedIP := range setting.allowedIPs {
if ipObject.Equal(allowedIP) {
return true
}
}
return false
}
// Access handler, check to see if client IP in allowed IPs, continue if it is, send to invalidHandler if not // Access handler, check to see if client IP in allowed IPs, continue if it is, send to invalidHandler if not
func accessHandler(next http.Handler) http.Handler { func accessHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(httpW http.ResponseWriter, httpR *http.Request) { return http.HandlerFunc(func(httpW http.ResponseWriter, httpR *http.Request) {
if hasAccess(httpR.RemoteAddr) {
// setting.allowedIPs will always have at least one element because of how it's defined
if setting.allowedIPs[0] == "" {
next.ServeHTTP(httpW, httpR) next.ServeHTTP(httpW, httpR)
return } else {
}
IPPort := httpR.RemoteAddr
// Remove port from IP and remove brackets that are around IPv6 addresses
requestIp := IPPort[0:strings.LastIndex(IPPort, ":")]
requestIp = strings.Replace(requestIp, "[", "", -1)
requestIp = strings.Replace(requestIp, "]", "", -1)
for _, allowedIP := range setting.allowedIPs {
if requestIp == allowedIP {
next.ServeHTTP(httpW, httpR)
return
}
}
invalidHandler(httpW, httpR) invalidHandler(httpW, httpR)
return }
}) })
} }
type settingType struct { type settingType struct {
birdSocket string birdSocket string
listen string listen string
allowedIPs []string allowedIPs []net.IP
tr_bin string
tr_flags []string
tr_raw bool
} }
var setting settingType var setting settingType
// Wrapper of tracer // Wrapper of tracer
func main() { func main() {
// Prepare default socket paths, use environment variable if possible parseSettings()
var settingDefault = settingType{ tracerouteAutodetect()
"/var/run/bird/bird.ctl",
":8000", fmt.Printf("Listening on %s...\n", setting.listen)
[]string{""},
var l net.Listener
var err error
if strings.HasPrefix(setting.listen, "/") {
// Delete existing socket file, ignore errors (will fail later anyway)
os.Remove(setting.listen)
l, err = net.Listen("unix", setting.listen)
} else {
listenAddr := setting.listen
if !strings.Contains(listenAddr, ":") {
listenAddr = ":" + listenAddr
}
l, err = net.Listen("tcp", listenAddr)
} }
if birdSocketEnv := os.Getenv("BIRD_SOCKET"); birdSocketEnv != "" { if err != nil {
settingDefault.birdSocket = birdSocketEnv panic(err)
} }
if listenEnv := os.Getenv("BIRDLG_LISTEN"); listenEnv != "" {
settingDefault.listen = listenEnv
}
if AllowedIPsEnv := os.Getenv("ALLOWED_IPS"); AllowedIPsEnv != "" {
settingDefault.allowedIPs = strings.Split(AllowedIPsEnv, ",")
}
// Allow parameters to override environment variables
birdParam := flag.String("bird", settingDefault.birdSocket, "socket file for bird, set either in parameter or environment variable BIRD_SOCKET")
listenParam := flag.String("listen", settingDefault.listen, "listen address, set either in parameter or environment variable BIRDLG_LISTEN")
AllowedIPsParam := flag.String("allowed", strings.Join(settingDefault.allowedIPs, ","), "IPs allowed to access this proxy, separated by commas. Don't set to allow all IPs.")
flag.Parse()
setting.birdSocket = *birdParam
setting.listen = *listenParam
setting.allowedIPs = strings.Split(*AllowedIPsParam, ",")
// Start HTTP server // Start HTTP server
http.HandleFunc("/", invalidHandler) http.HandleFunc("/", invalidHandler)
@@ -92,5 +103,5 @@ func main() {
http.HandleFunc("/bird6", birdHandler) http.HandleFunc("/bird6", birdHandler)
http.HandleFunc("/traceroute", tracerouteHandler) http.HandleFunc("/traceroute", tracerouteHandler)
http.HandleFunc("/traceroute6", tracerouteHandler) http.HandleFunc("/traceroute6", tracerouteHandler)
http.ListenAndServe(*listenParam, handlers.LoggingHandler(os.Stdout, accessHandler(http.DefaultServeMux))) http.Serve(l, handlers.LoggingHandler(os.Stdout, accessHandler(http.DefaultServeMux)))
} }

78
proxy/main_test.go Normal file
View File

@@ -0,0 +1,78 @@
package main
import (
"net"
"net/http"
"net/http/httptest"
"testing"
"github.com/magiconair/properties/assert"
)
func TestHasAccessNotConfigured(t *testing.T) {
setting.allowedIPs = []net.IP{}
assert.Equal(t, hasAccess("whatever"), true)
}
func TestHasAccessAllowIPv4(t *testing.T) {
setting.allowedIPs = []net.IP{net.ParseIP("1.2.3.4")}
assert.Equal(t, hasAccess("1.2.3.4:4321"), true)
}
func TestHasAccessDenyIPv4(t *testing.T) {
setting.allowedIPs = []net.IP{net.ParseIP("4.3.2.1")}
assert.Equal(t, hasAccess("1.2.3.4:4321"), false)
}
func TestHasAccessAllowIPv6(t *testing.T) {
setting.allowedIPs = []net.IP{net.ParseIP("2001:db8::1")}
assert.Equal(t, hasAccess("[2001:db8::1]:4321"), true)
}
func TestHasAccessAllowIPv6DifferentForm(t *testing.T) {
setting.allowedIPs = []net.IP{net.ParseIP("2001:0db8::1")}
assert.Equal(t, hasAccess("[2001:db8::1]:4321"), true)
}
func TestHasAccessDenyIPv6(t *testing.T) {
setting.allowedIPs = []net.IP{net.ParseIP("2001:db8::2")}
assert.Equal(t, hasAccess("[2001:db8::1]:4321"), false)
}
func TestHasAccessBadClientIP(t *testing.T) {
setting.allowedIPs = []net.IP{net.ParseIP("1.2.3.4")}
assert.Equal(t, hasAccess("not an IP"), false)
}
func TestHasAccessBadClientIPPort(t *testing.T) {
setting.allowedIPs = []net.IP{net.ParseIP("1.2.3.4")}
assert.Equal(t, hasAccess("not an IP:not a port"), false)
}
func TestAccessHandlerAllow(t *testing.T) {
baseHandler := http.NotFoundHandler()
wrappedHandler := accessHandler(baseHandler)
r := httptest.NewRequest(http.MethodGet, "/mock", nil)
r.RemoteAddr = "1.2.3.4:4321"
w := httptest.NewRecorder()
setting.allowedIPs = []net.IP{net.ParseIP("1.2.3.4")}
wrappedHandler.ServeHTTP(w, r)
assert.Equal(t, w.Code, http.StatusNotFound)
}
func TestAccessHandlerDeny(t *testing.T) {
baseHandler := http.NotFoundHandler()
wrappedHandler := accessHandler(baseHandler)
r := httptest.NewRequest(http.MethodGet, "/mock", nil)
r.RemoteAddr = "1.2.3.4:4321"
w := httptest.NewRecorder()
setting.allowedIPs = []net.IP{net.ParseIP("4.3.2.1")}
wrappedHandler.ServeHTTP(w, r)
assert.Equal(t, w.Code, http.StatusInternalServerError)
}

93
proxy/settings.go Normal file
View File

@@ -0,0 +1,93 @@
package main
import (
"fmt"
"net"
"strings"
"github.com/google/shlex"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)
type viperSettingType struct {
BirdSocket string `mapstructure:"bird_socket"`
Listen string `mapstructure:"listen"`
AllowedIPs string `mapstructure:"allowed_ips"`
TracerouteBin string `mapstructure:"traceroute_bin"`
TracerouteFlags string `mapstructure:"traceroute_flags"`
TracerouteRaw bool `mapstructure:"traceroute_raw"`
}
// Parse settings with viper, and convert to legacy setting format
func parseSettings() {
viper.AddConfigPath(".")
viper.AddConfigPath("/etc/bird-lg")
viper.SetConfigName("bird-lgproxy")
viper.AllowEmptyEnv(true)
viper.AutomaticEnv()
viper.SetEnvPrefix("birdlg")
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_"))
// Legacy environment variables without prefixes
viper.BindEnv("bird_socket", "BIRD_SOCKET")
viper.BindEnv("listen", "BIRDLG_LISTEN", "BIRDLG_PROXY_PORT")
viper.BindEnv("allowed_ips", "ALLOWED_IPS")
pflag.String("bird", "/var/run/bird/bird.ctl", "socket file for bird, set either in parameter or environment variable BIRD_SOCKET")
viper.BindPFlag("bird_socket", pflag.Lookup("bird"))
pflag.String("listen", "8000", "listen address, set either in parameter or environment variable BIRDLG_PROXY_PORT")
viper.BindPFlag("listen", pflag.Lookup("listen"))
pflag.String("allowed", "", "IPs allowed to access this proxy, separated by commas. Don't set to allow all IPs.")
viper.BindPFlag("allowed_ips", pflag.Lookup("allowed"))
pflag.String("traceroute_bin", "", "traceroute binary file, set either in parameter or environment variable BIRDLG_TRACEROUTE_BIN")
viper.BindPFlag("traceroute_bin", pflag.Lookup("traceroute_bin"))
pflag.String("traceroute_flags", "", "traceroute flags, supports multiple flags separated with space.")
viper.BindPFlag("traceroute_flags", pflag.Lookup("traceroute_flags"))
pflag.Bool("traceroute_raw", false, "whether to display traceroute outputs raw; set via parameter or environment variable BIRDLG_TRACEROUTE_RAW")
viper.BindPFlag("traceroute_raw", pflag.Lookup("traceroute_raw"))
pflag.Parse()
if err := viper.ReadInConfig(); err != nil {
println("Warning on reading config: " + err.Error())
}
viperSettings := viperSettingType{}
if err := viper.Unmarshal(&viperSettings); err != nil {
panic(err)
}
setting.birdSocket = viperSettings.BirdSocket
setting.listen = viperSettings.Listen
if viperSettings.AllowedIPs != "" {
for _, ip := range strings.Split(viperSettings.AllowedIPs, ",") {
ipObject := net.ParseIP(ip)
if ipObject == nil {
fmt.Printf("Parse IP %s failed\n", ip)
continue
}
setting.allowedIPs = append(setting.allowedIPs, ipObject)
}
} else {
setting.allowedIPs = []net.IP{}
}
var err error
setting.tr_bin = viperSettings.TracerouteBin
setting.tr_flags, err = shlex.Split(viperSettings.TracerouteFlags)
if err != nil {
panic(err)
}
setting.tr_raw = viperSettings.TracerouteRaw
fmt.Printf("%#v\n", setting)
}

8
proxy/settings_test.go Normal file
View File

@@ -0,0 +1,8 @@
package main
import "testing"
func TestParseSettings(t *testing.T) {
parseSettings()
// Good as long as it doesn't panic
}

View File

@@ -1,64 +0,0 @@
FROM golang:buster AS step_0
#if defined(ARCH_AMD64)
ENV GOOS=linux GOARCH=amd64
#elif defined(ARCH_I386)
ENV GOOS=linux GOARCH=386
#elif defined(ARCH_ARM32V7)
ENV GOOS=linux GOARCH=arm
#elif defined(ARCH_ARM64V8)
ENV GOOS=linux GOARCH=arm64
#elif defined(ARCH_PPC64LE)
ENV GOOS=linux GOARCH=ppc64le
#elif defined(ARCH_S390X)
ENV GOOS=linux GOARCH=s390x
#else
#error "Architecture not set"
#endif
ENV CGO_ENABLED=0 GO111MODULE=on
WORKDIR /root
COPY . .
RUN go build -ldflags "-w -s" -o /proxy
################################################################################
#if defined(ARCH_AMD64)
FROM amd64/debian:sid AS step_1
ENV TARGET_ARCH=x86_64
#elif defined(ARCH_I386)
FROM i386/debian:sid AS step_1
ENV TARGET_ARCH=i386
#elif defined(ARCH_ARM32V7)
FROM arm32v7/debian:sid AS step_1
ENV TARGET_ARCH=arm
#elif defined(ARCH_ARM64V8)
FROM arm64v8/debian:sid AS step_1
ENV TARGET_ARCH=aarch64
#elif defined(ARCH_PPC64LE)
FROM ppc64le/debian:sid AS step_1
ENV TARGET_ARCH=ppc64le
#elif defined(ARCH_S390X)
FROM s390x/debian:sid AS step_1
ENV TARGET_ARCH=s390
#else
#error "Architecture not set"
#endif
WORKDIR /root
RUN apt-get -qq update && DEBIAN_FRONTEND=noninteractive apt-get -qq install -y \
build-essential musl-dev musl-tools tar wget git
RUN git clone https://github.com/sabotage-linux/kernel-headers.git
RUN wget https://sourceforge.net/projects/traceroute/files/traceroute/traceroute-2.1.0/traceroute-2.1.0.tar.gz/download \
-O traceroute-2.1.0.tar.gz
RUN tar xvf traceroute-2.1.0.tar.gz \
&& cd traceroute-2.1.0 \
&& make -j4 CC=musl-gcc CFLAGS="-I/root/kernel-headers/${TARGET_ARCH}/include" LDFLAGS="-static"
################################################################################
FROM scratch AS step_2
ENV PATH=/
COPY --from=step_0 /proxy /
COPY --from=step_1 /root/traceroute-2.1.0/traceroute/traceroute /
ENTRYPOINT ["/proxy"]

View File

@@ -5,28 +5,81 @@ import (
"net/http" "net/http"
"os/exec" "os/exec"
"regexp" "regexp"
"runtime"
"strconv" "strconv"
"strings" "strings"
"github.com/google/shlex" "github.com/google/shlex"
) )
func tracerouteTryExecute(cmd []string, args [][]string) ([]byte, string) { func tracerouteArgsToString(cmd string, args []string, target []string) string {
var output []byte var cmdCombined = append([]string{cmd}, args...)
var errString = "" cmdCombined = append(cmdCombined, target...)
for i := range cmd { return strings.Join(cmdCombined, " ")
var err error }
var cmdCombined = cmd[i] + " " + strings.Join(args[i], " ")
instance := exec.Command(cmd[i], args[i]...) func tracerouteTryExecute(cmd string, args []string, target []string) ([]byte, error) {
output, err = instance.CombinedOutput() instance := exec.Command(cmd, append(args, target...)...)
output, err := instance.CombinedOutput()
if err == nil { if err == nil {
return output, "" return output, nil
} }
errString += fmt.Sprintf("+ (Try %d) %s\n%s\n\n", (i + 1), cmdCombined, output)
return output, err
} }
return nil, errString
func tracerouteDetect(cmd string, args []string) bool {
target := []string{"127.0.0.1"}
success := false
if result, err := tracerouteTryExecute(cmd, args, target); err == nil {
setting.tr_bin = cmd
setting.tr_flags = args
success = true
fmt.Printf("Traceroute autodetect success: %s\n", tracerouteArgsToString(cmd, args, target))
} else {
fmt.Printf("Traceroute autodetect fail, continuing: %s (%s)\n%s", tracerouteArgsToString(cmd, args, target), err.Error(), result)
}
return success
}
func tracerouteAutodetect() {
if setting.tr_bin != "" && setting.tr_flags != nil {
return
}
// Traceroute (custom binary)
if setting.tr_bin != "" {
if tracerouteDetect(setting.tr_bin, []string{"-q1", "-N32", "-w1"}) {
return
}
if tracerouteDetect(setting.tr_bin, []string{"-q1", "-w1"}) {
return
}
if tracerouteDetect(setting.tr_bin, []string{}) {
return
}
}
// MTR
if tracerouteDetect("mtr", []string{"-w", "-c1", "-Z1", "-G1", "-b"}) {
return
}
// Traceroute
if tracerouteDetect("traceroute", []string{"-q1", "-N32", "-w1"}) {
return
}
if tracerouteDetect("traceroute", []string{"-q1", "-w1"}) {
return
}
if tracerouteDetect("traceroute", []string{}) {
return
}
// Unsupported
setting.tr_bin = ""
setting.tr_flags = nil
println("Traceroute autodetect failed! Traceroute will be disabled")
} }
func tracerouteHandler(httpW http.ResponseWriter, httpR *http.Request) { func tracerouteHandler(httpW http.ResponseWriter, httpR *http.Request) {
@@ -44,52 +97,34 @@ func tracerouteHandler(httpW http.ResponseWriter, httpR *http.Request) {
} }
var result []byte var result []byte
var errString string
skippedCounter := 0 skippedCounter := 0
if runtime.GOOS == "freebsd" || runtime.GOOS == "netbsd" || runtime.GOOS == "openbsd" { if setting.tr_bin == "" {
result, errString = tracerouteTryExecute(
[]string{
"traceroute",
"traceroute",
},
[][]string{
append([]string{"-q1", "-w1"}, args...),
args,
},
)
} else if runtime.GOOS == "linux" {
result, errString = tracerouteTryExecute(
[]string{
"traceroute",
"traceroute",
"traceroute",
},
[][]string{
append([]string{"-q1", "-N32", "-w1"}, args...),
append([]string{"-q1", "-w1"}, args...),
args,
},
)
} else {
httpW.WriteHeader(http.StatusInternalServerError) httpW.WriteHeader(http.StatusInternalServerError)
httpW.Write([]byte("traceroute not supported on this node.\n")) httpW.Write([]byte("traceroute not supported on this node.\n"))
return return
} }
if errString != "" {
result, err = tracerouteTryExecute(setting.tr_bin, setting.tr_flags, args)
if err != nil {
httpW.WriteHeader(http.StatusInternalServerError) httpW.WriteHeader(http.StatusInternalServerError)
httpW.Write([]byte(errString)) httpW.Write([]byte(fmt.Sprintf("Error executing traceroute: %s\n\n", err.Error())))
} }
if result != nil { if result != nil {
errString = string(result) if setting.tr_raw {
errString = regexp.MustCompile(`(?m)^\s*(\d*)\s*\*\n`).ReplaceAllStringFunc(errString, func(w string) string { httpW.Write(result)
} else {
resultString := string(result)
resultString = regexp.MustCompile(`(?m)^\s*(\d*)\s*\*\n`).ReplaceAllStringFunc(resultString, func(w string) string {
skippedCounter++ skippedCounter++
return "" return ""
}) })
httpW.Write([]byte(strings.TrimSpace(errString))) httpW.Write([]byte(strings.TrimSpace(resultString)))
if skippedCounter > 0 { if skippedCounter > 0 {
httpW.Write([]byte("\n\n" + strconv.Itoa(skippedCounter) + " hops not responding.")) httpW.Write([]byte("\n\n" + strconv.Itoa(skippedCounter) + " hops not responding."))
} }
} }
} }
} }
}

168
proxy/traceroute_test.go Normal file
View File

@@ -0,0 +1,168 @@
package main
import (
"net/http"
"net/http/httptest"
"net/url"
"os"
"strings"
"testing"
"github.com/magiconair/properties/assert"
)
func TestTracerouteArgsToString(t *testing.T) {
result := tracerouteArgsToString("traceroute", []string{
"-a",
"-b",
"-c",
}, []string{
"google.com",
})
assert.Equal(t, result, "traceroute -a -b -c google.com")
}
func TestTracerouteTryExecuteSuccess(t *testing.T) {
_, err := tracerouteTryExecute("sh", []string{
"-c",
}, []string{
"true",
})
if err != nil {
t.Error(err)
}
}
func TestTracerouteTryExecuteFail(t *testing.T) {
_, err := tracerouteTryExecute("sh", []string{
"-c",
}, []string{
"false",
})
if err == nil {
t.Error("Should trigger error, not triggered")
}
}
func TestTracerouteDetectSuccess(t *testing.T) {
result := tracerouteDetect("sh", []string{
"-c",
"true",
})
assert.Equal(t, result, true)
}
func TestTracerouteDetectFail(t *testing.T) {
result := tracerouteDetect("sh", []string{
"-c",
"false",
})
assert.Equal(t, result, false)
}
func TestTracerouteAutodetect(t *testing.T) {
pathBackup := os.Getenv("PATH")
os.Setenv("PATH", "")
defer os.Setenv("PATH", pathBackup)
setting.tr_bin = ""
setting.tr_flags = []string{}
tracerouteAutodetect()
// Should not panic
}
func TestTracerouteAutodetectExisting(t *testing.T) {
setting.tr_bin = "mock"
setting.tr_flags = []string{"mock"}
tracerouteAutodetect()
assert.Equal(t, setting.tr_bin, "mock")
assert.Equal(t, setting.tr_flags, []string{"mock"})
}
func TestTracerouteAutodetectFlagsOnly(t *testing.T) {
pathBackup := os.Getenv("PATH")
os.Setenv("PATH", "")
defer os.Setenv("PATH", pathBackup)
setting.tr_bin = "mock"
setting.tr_flags = nil
tracerouteAutodetect()
// Should not panic
}
func TestTracerouteHandlerWithoutQuery(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/traceroute", nil)
w := httptest.NewRecorder()
tracerouteHandler(w, r)
assert.Equal(t, w.Code, http.StatusInternalServerError)
if !strings.Contains(w.Body.String(), "Invalid Request") {
t.Error("Did not get invalid request")
}
}
func TestTracerouteHandlerShlexError(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/traceroute?q="+url.QueryEscape("\"1.1.1.1"), nil)
w := httptest.NewRecorder()
tracerouteHandler(w, r)
assert.Equal(t, w.Code, http.StatusInternalServerError)
if !strings.Contains(w.Body.String(), "parse") {
t.Error("Did not get parsing error message")
}
}
func TestTracerouteHandlerNoTracerouteFound(t *testing.T) {
setting.tr_bin = ""
setting.tr_flags = nil
r := httptest.NewRequest(http.MethodGet, "/traceroute?q="+url.QueryEscape("1.1.1.1"), nil)
w := httptest.NewRecorder()
tracerouteHandler(w, r)
assert.Equal(t, w.Code, http.StatusInternalServerError)
if !strings.Contains(w.Body.String(), "not supported") {
t.Error("Did not get not supported error message")
}
}
func TestTracerouteHandlerExecuteError(t *testing.T) {
setting.tr_bin = "sh"
setting.tr_flags = []string{"-c", "false"}
setting.tr_raw = true
r := httptest.NewRequest(http.MethodGet, "/traceroute?q="+url.QueryEscape("1.1.1.1"), nil)
w := httptest.NewRecorder()
tracerouteHandler(w, r)
assert.Equal(t, w.Code, http.StatusInternalServerError)
if !strings.Contains(w.Body.String(), "Error executing traceroute") {
t.Error("Did not get not execute error message")
}
}
func TestTracerouteHandlerRaw(t *testing.T) {
setting.tr_bin = "sh"
setting.tr_flags = []string{"-c", "echo Mock"}
setting.tr_raw = true
r := httptest.NewRequest(http.MethodGet, "/traceroute?q="+url.QueryEscape("1.1.1.1"), nil)
w := httptest.NewRecorder()
tracerouteHandler(w, r)
assert.Equal(t, w.Code, http.StatusOK)
assert.Equal(t, w.Body.String(), "Mock\n")
}
func TestTracerouteHandlerPostprocess(t *testing.T) {
setting.tr_bin = "sh"
setting.tr_flags = []string{"-c", "echo \"first line\n 2 *\nthird line\""}
setting.tr_raw = false
r := httptest.NewRequest(http.MethodGet, "/traceroute?q="+url.QueryEscape("1.1.1.1"), nil)
w := httptest.NewRecorder()
tracerouteHandler(w, r)
assert.Equal(t, w.Code, http.StatusOK)
assert.Equal(t, w.Body.String(), "first line\nthird line\n\n1 hops not responding.")
}