1
mirror of https://github.com/xddxdd/bird-lg-go synced 2025-10-17 22:42:12 +02:00

51 Commits

Author SHA1 Message Date
dependabot-preview[bot]
fa827502cf Upgrade to GitHub-native Dependabot 2021-04-29 22:35:16 +00:00
Yuhui Xu
794125a96f Merge pull request #17 from towalink/master
Allow specifying display names for servers
2021-04-18 19:37:22 +08:00
Henri
de9d9101b1 Fix test initialization so that tests succeed after previous commit 2021-04-14 09:17:44 +02:00
Henri
056ef3769e Allow specifying display names for servers 2021-04-13 21:58:50 +02:00
towalink
974e809deb Merge pull request #1 from xddxdd/master
Merge pull request #15 from towalink/master
2021-04-10 08:36:39 +02:00
Yuhui Xu
6e19b5ae64 Merge pull request #15 from towalink/master
Increase consistency of path escaping;  support IPv6 addresses
2021-04-10 10:25:45 +08:00
Henri
874089117b Increase consistency of path escaping and support IPv6 addresses instead of hostnames 2021-04-06 21:43:58 +02:00
Yuhui Xu
f77a8a28fe Merge pull request #14 from xddxdd/lantian-dev
Fix link in README
2021-04-01 22:57:56 +08:00
Lan Tian
9e8a845658 general: fix link in README 2021-04-01 22:56:13 +08:00
Yuhui Xu
fd3e7b8379 Merge pull request #13 from xddxdd/lantian-dev
Add build notes from #11
2021-04-01 22:54:41 +08:00
Lan Tian
8765189deb general: add build notes from #11 2021-04-01 22:47:08 +08:00
Yuhui Xu
492942cce1 Merge pull request #12 from AluisioASG/domainless
frontend: make domain optional
2021-04-01 22:43:13 +08:00
Aluísio Augusto Silva Gonçalves
f81a5308ae frontend: make domain optional
Making the domain optional allows usage of bare hostnames for
the servers (e.g. when they're statically configured) and even
IP addresses if someone is so inclined (although presentation
might suffer in this case).
2021-03-31 16:44:44 -03:00
Lan Tian
5b5a09ccbd frontend: clamp telegram api response to 4096 chars 2021-03-31 22:43:46 +08:00
Lan Tian
dc4d7e6532 general: update makefiles for project 2021-03-15 00:49:13 +08:00
Lan Tian
28a7d2a53f general: fix incorrect name for pushed docker images 2021-03-06 21:41:44 +08:00
Lan Tian
4413f1032f general: try to fix multiarch build 2021-03-06 21:21:42 +08:00
Lan Tian
007b66e036 Revert "general: remove build for all arch but amd64"
This reverts commit 1c3d9ec594.
2021-03-06 20:52:04 +08:00
Lan Tian
3f612d2e76 proxy: fix plain traceroute not executed 2021-02-28 23:09:11 +08:00
Lan Tian
f49f8bac5e proxy: support arbitraty traceroute arguments 2021-02-27 16:42:42 +08:00
Lan Tian
f6ddc5761b frontend: remove unnecessary URL escapes 2021-02-27 15:24:21 +08:00
Lan Tian
1c3d9ec594 general: remove build for all arch but amd64 2021-01-18 23:13:25 +08:00
Lan Tian
6cc0c617b4 frontend: add API for server list 2021-01-17 16:16:41 +08:00
Lan Tian
e2cc580da3 frontend: add CORS header to API 2021-01-17 12:45:20 +08:00
Lan Tian
472cec74b0 general: update README.md 2021-01-17 12:37:22 +08:00
Lan Tian
da2c3d9aed frontend: add API 2021-01-17 12:35:29 +08:00
Lan Tian
aa76bc3de7 ci: try fix test error 2021-01-17 02:39:17 +08:00
Lan Tian
a984095282 frontend: add tests against XSS 2021-01-17 02:21:23 +08:00
Lan Tian
1baf325149 frontend: move redirect logic to HTML 2021-01-17 01:33:14 +08:00
Lan Tian
72946e1113 frontend: filter output to prevent XSS 2021-01-17 01:14:49 +08:00
Lan Tian
90e5012840 proxy: filter input to prevent XSS 2021-01-15 01:22:39 +08:00
Lan Tian
8d5eb56199 Fix building Docker image for arm and i386 2021-01-15 01:00:54 +08:00
Lan Tian
8d0618fed9 Update CircleCI config 2021-01-15 00:41:38 +08:00
Yuhui Xu
f8ea511d44 Merge pull request #8 from sesa-me/burble.dn42-templates
Add static file bundling and HTML templating
2021-01-14 23:01:43 +08:00
Yuhui Xu
b99eb60c30 Merge pull request #9 from xddxdd/circleci-project-setup
Circleci project setup
2021-01-14 00:02:18 +08:00
Yuhui Xu
9f934ca53c Updated config.yml 2021-01-13 23:58:44 +08:00
Yuhui Xu
ee7cc1675b Add .circleci/config.yml 2021-01-13 23:37:32 +08:00
Simon Marsh
f4b6955343 Add utility functions for filtering results and rename templates 2021-01-12 10:21:03 +00:00
Simon Marsh
78ce724171 Fix bindata build step and parameterize docker build 2021-01-12 10:21:02 +00:00
Simon Marsh
6179c688be - Use bindata to package static file content in to the frontend binary
- Add golang templates to move HTML rendering out of the go code where possible
- Add an endpoint for serving static files
- Add URL escaping for servers and targets
2021-01-11 15:00:05 +00:00
Lan Tian
8d0e210572 Fix #7 2021-01-11 22:24:15 +08:00
Lan Tian
26c51176e4 proxy: add back ipv6 endpoints for compatibility with original project 2020-11-20 21:58:44 +08:00
Yuhui Xu
5cf2ac57b8 Merge pull request #6 from petabyteboy/master
preserve leading spaces
2020-11-19 23:22:00 +08:00
Milan Pässler
75bc63ffa7 preserve leading spaces 2020-11-18 19:27:46 +01:00
Lan Tian
438c6a1f82 general: fix travis multiarch 2020-11-09 01:37:09 +08:00
Lan Tian
b98d783739 general: use stable debian to build docker image 2020-11-09 01:23:58 +08:00
Lan Tian
5000ad1bbf general: add s390x & ppc64le 2020-11-07 20:24:37 +08:00
Lan Tian
538699ccd2 proxy: scratch-based docker image 2020-11-07 20:20:42 +08:00
Lan Tian
9e77de6b46 frontend: scratch-based docker image 2020-11-07 20:06:07 +08:00
Lan Tian
c15942cc32 proxy: fix regex formatting error 2020-10-30 23:33:56 +08:00
Lan Tian
3bcfc3d36c Remove BIRDv1 support 2020-10-30 23:10:03 +08:00
44 changed files with 1535 additions and 608 deletions

61
.circleci/config.yml Normal file
View File

@@ -0,0 +1,61 @@
version: 2.1
workflows:
docker:
jobs:
- build
- deploy:
context:
- docker
requires:
- build
matrix:
parameters:
program: [frontend, proxy]
# latest is amd64 arch + push to default latest tag
image_arch: [latest, i386, arm32v7, arm64v8, ppc64le, s390x]
filters:
branches:
only: master
jobs:
build:
docker:
- image: circleci/golang:1.15
working_directory: /go/src/github.com/xddxdd/bird-lg-go
steps:
- checkout
- run: go get -v -t -d ./...
- run: go get -u github.com/kevinburke/go-bindata/...
- run: cd frontend && go generate
- run: go test -v ./...
deploy:
docker:
- image: circleci/golang:1.15
working_directory: /go/src/github.com/xddxdd/bird-lg-go
parameters:
image_arch:
type: string
program:
type: string
steps:
- checkout
- setup_remote_docker:
version: 19.03.13
- run:
name: Install GPP
command: |
sudo apt-get update && sudo apt-get install -y gpp
- run:
name: Build Docker image
environment:
IMAGE_ARCH: << parameters.image_arch >>
PROGRAM: << parameters.program >>
BUILD_ID: << pipeline.number >>
command: |
make -f Makefile.docker _crossbuild
echo $DOCKER_PASSWORD | docker login -u $DOCKER_USERNAME --password-stdin
make -f Makefile.docker \
DOCKER_USERNAME=$DOCKER_USERNAME \
BUILD_ID=circleci-build$BUILD_ID \
$PROGRAM/$IMAGE_ARCH

16
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,16 @@
version: 2
updates:
- package-ecosystem: gomod
directory: "/frontend"
schedule:
interval: daily
time: "08:00"
timezone: Asia/Shanghai
open-pull-requests-limit: 10
- package-ecosystem: gomod
directory: "/proxy"
schedule:
interval: daily
time: "08:00"
timezone: Asia/Shanghai
open-pull-requests-limit: 10

9
.gitignore vendored
View File

@@ -16,4 +16,11 @@
.DS_Store
frontend/frontend
proxy/proxy
proxy/proxy
# don't include generated bindata file
frontend/bindata.go
# don't include generated Dockerfiles
frontend/Dockerfile.*
proxy/Dockerfile.*

View File

@@ -1,35 +0,0 @@
language: minimal
os: linux
dist: focal
services:
- docker
env:
- PROGRAM=frontend IMAGE_NAME=bird-lg-go IMAGE_ARCH=i386
- PROGRAM=frontend IMAGE_NAME=bird-lg-go IMAGE_ARCH=amd64
- PROGRAM=frontend IMAGE_NAME=bird-lg-go IMAGE_ARCH=arm32v7
- PROGRAM=frontend IMAGE_NAME=bird-lg-go IMAGE_ARCH=arm64v8
- PROGRAM=proxy IMAGE_NAME=bird-lgproxy-go IMAGE_ARCH=i386
- PROGRAM=proxy IMAGE_NAME=bird-lgproxy-go IMAGE_ARCH=amd64
- PROGRAM=proxy IMAGE_NAME=bird-lgproxy-go IMAGE_ARCH=arm32v7
- PROGRAM=proxy IMAGE_NAME=bird-lgproxy-go IMAGE_ARCH=arm64v8
install:
- docker run --rm --privileged multiarch/qemu-user-static:register --reset
- echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
script:
- |
# Build image
docker build \
-t $DOCKER_USERNAME/$IMAGE_NAME:$IMAGE_ARCH \
-f $PROGRAM/Dockerfile.$IMAGE_ARCH \
$PROGRAM
# Tag image :{arch} and :{arch}-build{build number}
docker tag $DOCKER_USERNAME/$IMAGE_NAME:$IMAGE_ARCH $DOCKER_USERNAME/$IMAGE_NAME:$IMAGE_ARCH-build$TRAVIS_BUILD_NUMBER
if [ "$IMAGE_ARCH" = "amd64" ]; then
# Tag as latest for amd64 images
docker tag $DOCKER_USERNAME/$IMAGE_NAME:$IMAGE_ARCH $DOCKER_USERNAME/$IMAGE_NAME:latest
docker tag $DOCKER_USERNAME/$IMAGE_NAME:$IMAGE_ARCH $DOCKER_USERNAME/$IMAGE_NAME:build$TRAVIS_BUILD_NUMBER
fi
- docker push $DOCKER_USERNAME/$IMAGE_NAME

202
API.md Normal file
View File

@@ -0,0 +1,202 @@
# Bird-lg-go API documentation
The frontend provides an API for running BIRD/traceroute/whois queries.
API Endpoint: `https://your.frontend.com/api/` (the last slash must not be omitted!)
Requests are sent as POSTS with JSON bodies.
## Table of Contents
* [Bird-lg-go API documentation](#bird-lg-go-api-documentation)
* [Table of Contents](#table-of-contents)
* [Request fields](#request-fields)
* [Example request of type bird](#example-request-of-type-bird)
* [Example request of type server_list](#example-request-of-type-server_list)
* [Response fields (when type is summary)](#response-fields-when-type-is-summary)
* [Fields for apiSummaryResultPair](#fields-for-apisummaryresultpair)
* [Fields for SummaryRowData](#fields-for-summaryrowdata)
* [Example response](#example-response)
* [Response fields (when type is bird, traceroute, whois or server_list)](#response-fields-when-type-is-bird-traceroute-whois-or-server_list)
* [Fields for apiGenericResultPair](#fields-for-apigenericresultpair)
* [Example response of type bird](#example-response-of-type-bird)
* [Example response of type server_list](#example-response-of-type-server_list)
Created by [gh-md-toc](https://github.com/ekalinin/github-markdown-toc)
## Request fields
| Name | Type | Value |
| ---- | ---- | -------- |
| `servers` | array of `string` | List of servers to be queried |
| `type` | `string` | Can be `summary`, `bird`, `traceroute`, `whois` or `server_list` |
| `args` | `string` | Arguments to be passed, see below |
Argument examples for each type:
- `summary`: `args` is ignored. Recommended to set to empty string.
- `bird`: `args` is the command to be passed to bird, e.g. `show route for 8.8.8.8`
- `traceroute`: `args` is the traceroute target, e.g. `8.8.8.8` or `google.com`
- `whois`: `args` is the whois target, e.g. `8.8.8.8` or `google.com`
- `server_list`: `args` is ignored. In addition, `servers` is also ignored.
### Example request of type `bird`
```json
{
"servers": [
"alpha"
],
"type": "bird",
"args": "show route for 8.8.8.8"
}
```
### Example request of type `server_list`
```json
{
"servers": [],
"type": "server_list",
"args": ""
}
```
## Response fields (when `type` is `summary`)
| Name | Type | Value |
| ---- | ---- | -------- |
| `error` | `string` | Error message when something is wrong. Empty when everything is good |
| `result` | array of `apiSummaryResultPair` | See below |
### Fields for `apiSummaryResultPair`
| Name | Type | Value |
| ---- | ---- | -------- |
| `server` | `string` | Name of the server |
| `data` | array of `SummaryRowData` | Summaries of the server, see below |
### Fields for `SummaryRowData`
All fields below is 1:1 correspondent to the output of `birdc show protocols`.
| Name | Type |
| ---- | ---- |
| `name` | `string` |
| `proto` | `string` |
| `table` | `string` |
| `state` | `string` |
| `since` | `string` |
| `info` | `string` |
### Example response
Request:
```json
{
"servers": [
"alpha"
],
"type": "summary",
"args": ""
}
```
Response:
```json
{
"error": "",
"result": [
{
"server": "alpha",
"data": [
{
"name": "bgp1",
"proto": "BGP",
"table": "---",
"state": "start",
"since": "2021-01-15 22:40:01",
"info": "Active Socket: Operation timed out"
},
{
"name": "bgp2",
"proto": "BGP",
"table": "---",
"state": "start",
"since": "2021-01-03 08:15:48",
"info": "Established"
}
]
}
]
}
```
## Response fields (when `type` is `bird`, `traceroute`, `whois` or `server_list`)
| Name | Type | Value |
| ---- | ---- | -------- |
| `error` | `string` | Error message, empty when everything is good |
| `result` | array of `apiGenericResultPair` | See below |
### Fields for `apiGenericResultPair`
| Name | Type | Value |
| ---- | ---- | -------- |
| `server` | `string` | Name of the server; is empty when type is `whois` |
| `data` | `string` | Result from the server; is empty when type is `server_list` |
### Example response of type `bird`
Request:
```json
{
"servers": [],
"type": "server_list",
"args": ""
}
```
Response:
```json
{
"error": "",
"result": [
{
"server": "alpha",
"data": "BIRD v2.0.7-137-g61dae32b\nRouter ID is 1.2.3.4\nCurrent server time is 2021-01-17 04:21:14.792\nLast reboot on 2021-01-03 08:15:48.494\nLast reconfiguration on 2021-01-17 00:49:10.573\nDaemon is up and running\n"
}
]
}
```
### Example response of type `server_list`
Request:
```json
{
"servers": [
"alpha"
],
"type": "bird",
"args": "show status"
}
```
Response:
```json
{
"error": "",
"result": [
{
"server": "gigsgigscloud",
"data": ""
}
]
}
```

13
Makefile Normal file
View File

@@ -0,0 +1,13 @@
frontend:
$(MAKE) -C frontend all
proxy:
$(MAKE) -C proxy all
.DEFAULT_GOAL := all
.PHONY: all frontend proxy
all: frontend proxy
install:
install -m 755 frontend/frontend /usr/local/bin/frontend
install -m 755 proxy/proxy /usr/local/bin/proxy

77
Makefile.docker Normal file
View File

@@ -0,0 +1,77 @@
# 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")}

138
README.md
View File

@@ -1,10 +1,50 @@
Bird-lg-go
==========
# Bird-lg-go
An alternative implementation for [bird-lg](https://github.com/sileht/bird-lg) written in Go. Both frontend and backend (proxy) are implemented, and can work with either the original Python implementation or the Go implementation.
Frontend
--------
> The code on master branch no longer support BIRDv1. Branch "bird1" is the last version that supports BIRDv1.
## Table of Contents
* [Bird-lg-go](#bird-lg-go)
* [Table of Contents](#table-of-contents)
* [Frontend](#frontend)
* [Proxy](#proxy)
* [Advanced Features](#advanced-features)
* [API](#api)
* [Telegram Bot Webhook](#telegram-bot-webhook)
* [Example of setting the webhook](#example-of-setting-the-webhook)
* [Supported commands](#supported-commands)
* [Credits](#credits)
* [License](#license)
Created by [gh-md-toc](https://github.com/ekalinin/github-markdown-toc)
## Build Instructions
Run `make` to build binaries for both the frontend and the proxy. You need to have Go installed on your machine.
Optionally run `make install` to install them to `/usr/local/bin`.
Or, you can manually do the building steps:
```bash
# Build frontend binary
cd frontend
go get -u github.com/kevinburke/go-bindata/...
go generate
go build -ldflags "-w -s" -o frontend
cd ..
# Build proxy binary
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)
## Frontend
The frontend directory contains the code for the web frontend, where users see BGP states, do traceroutes and whois, etc. It's a replacement for "lg.py" in original bird-lg project.
@@ -18,11 +58,16 @@ Features implemented:
Usage: all configuration is done via commandline parameters or environment variables, no config file.
- --servers / BIRDLG_SERVERS: server name prefixes, separated by comma
- --domain / BIRDLG_DOMAIN: server name domain suffixes
- --listen / BIRDLG_LISTEN: address bird-lg is listening on (default ":5000")
- --proxy-port / BIRDLG_PROXY_PORT: port bird-lgproxy is running on (default 8000)
- --whois / BIRDLG_WHOIS: whois server for queries (default "whois.verisign-grs.com")
| Parameter | Environment Variable | Description |
| --------- | -------------------- | ----------- |
| --servers | BIRDLG_SERVERS | server name prefixes, separated by comma |
| --domain | BIRDLG_DOMAIN | server name domain suffixes |
| --listen | BIRDLG_LISTEN | address bird-lg is listening on (default ":5000") |
| --proxy-port | BIRDLG_PROXY_PORT | port bird-lgproxy is running on (default 8000) |
| --whois | BIRDLG_WHOIS | whois server for queries (default "whois.verisign-grs.com") |
| --dns-interface | BIRDLG_DNS_INTERFACE | dns zone to query ASN information (default "asn.cymru.com") |
| --title-brand | BIRDLG_TITLE_BRAND | prefix of page titles in browser tabs (default "Bird-lg Go") |
| --navbar-brand | BIRDLG_NAVBAR_BRAND | brand to show in the navigation bar (default "Bird-lg Go") |
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.
@@ -43,25 +88,24 @@ Example: the following docker-compose.yml entry does the same as above, but by s
Demo: https://lg.lantian.pub
Proxy
-----
## Proxy
The proxy directory contains the code for the "proxy" for bird commands and traceroutes. It's a replacement for "lgproxy.py" in original bird-lg project.
Features implemented:
- Sending queries to BIRD and BIRD6
- If you are using BIRDv2, simply point both `--bird` and `--bird6` to the only socket file of BIRDv2
- Sending queries to BIRD
- Sending "restrict" command to BIRD to prevent unauthorized changes
- Executing traceroute command on Linux, FreeBSD and OpenBSD
- Source IP restriction
Usage: all configuration is done via commandline parameters or environment variables, no config file.
- --allowed / ALLOWED_IPS: IPs allowed to access this proxy, separated by commas. Don't set to allow all IPs. (default "")
- --bird / BIRD_SOCKET: socket file for bird, set either in parameter or environment variable BIRD_SOCKET (default "/var/run/bird/bird.ctl")
- --bird6 / BIRD6_SOCKET: socket file for bird6, set either in parameter or environment variable BIRD6_SOCKET (default "/var/run/bird/bird6.ctl")
- --listen / BIRDLG_LISTEN: listen address, set either in parameter or environment variable BIRDLG_LISTEN (default ":8000")
| Parameter | Environment Variable | Description |
| --------- | -------------------- | ----------- |
| --allowed | ALLOWED_IPS | IPs allowed to access this proxy, separated by commas. Don't set to allow all IPs. (default "") |
| --bird | BIRD_SOCKET | socket file for bird, set either in parameter or environment variable BIRD_SOCKET (default "/var/run/bird/bird.ctl") |
| --listen | BIRDLG_LISTEN | listen address, set either in parameter or environment variable BIRDLG_LISTEN (default ":8000") |
Example: start proxy with default configuration, should work "out of the box" on Debian 9 with BIRDv1:
@@ -69,7 +113,7 @@ Example: start proxy with default configuration, should work "out of the box" on
Example: start proxy with custom bird socket location:
./proxy --bird /run/bird.ctl --bird6 /run/bird6.ctl
./proxy --bird /run/bird.ctl
Example: the following docker-compose.yml entry does the same as above, but by starting a Docker container:
@@ -79,20 +123,66 @@ Example: the following docker-compose.yml entry does the same as above, but by s
restart: always
volumes:
- "/run/bird.ctl:/var/run/bird/bird.ctl"
- "/run/bird6.ctl:/var/run/bird/bird6.ctl"
ports:
- "192.168.0.1:8000:8000"
You can use source IP restriction to increase security. You should also bind the proxy to a specific interface and use an external firewall/iptables for added security.
Credits
-------
## Advanced Features
### Display names
The server parameter is composed of server name prefixes, separated by comma. It also supports an extended syntax: It allows to define display names for the user interface that are different from the actual server names.
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):
./frontend --servers="Gigs<gigsgigscloud>,Hostdare<hostdare>" --domain=dn42.lantian.pub
### IP addresses
You may also specify IP addresses as server names when no domain is specified. IPv6 link local addresses are supported, too.
For example:
./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.
### API
The frontend provides an API for running BIRD/traceroute/whois queries.
See [API docs](API.md) for detailed information.
### Telegram Bot Webhook
The frontend can act as a Telegram Bot webhook endpoint, to add BGP route/traceroute/whois lookup functionality to your tech group.
There is no configuration necessary on the frontend, just start it up normally.
Set your Telegram Bot webhook URL to `https://your.frontend.com/telegram/alpha+beta+gamma`, where `alpha+beta+gamma` is the list of servers to be queried on Telegram commands, separated by `+`.
You may omit `alpha+beta+gamma` to use all your servers, but it is not recommended when you have lots of servers, or the message would be too long and hard to read.
#### Example of setting the webhook
```bash
curl "https://api.telegram.org/bot${BOT_TOKEN}/setWebhook?url=https://your.frontend.com:5000/telegram/alpha+beta+gamma"
```
#### Supported commands
- `path`: Show bird's ASN path to target IP
- `route`: Show bird's preferred route to target IP
- `trace`: Traceroute to target IP/domain
- `whois`: Whois query
## Credits
- Everyone who contributed to this project (see Contributors section on the right)
- Mehdi Abaakouk for creating [the original bird-lg project](https://github.com/sileht/bird-lg)
- [Bootstrap](https://getbootstrap.com/) as web UI framework
License
-------
## License
GPL 3.0

View File

@@ -1,13 +0,0 @@
FROM amd64/debian:buster
LABEL Lan Tian "lantian@lantian.pub"
ENV GOOS=linux GOARCH=amd64
WORKDIR /root
COPY . .
RUN apt-get -qq update && DEBIAN_FRONTEND=noninteractive apt-get -qq install -y golang git \
&& cd /root && go get github.com/gorilla/handlers && go build -o /frontend \
&& cd / && rm -rf /root/* \
&& apt-get -qq purge -y golang git \
&& apt-get -qq autoremove --purge -y && apt-get clean && rm -rf /var/lib/apt/lists
ENTRYPOINT ["/frontend"]

View File

@@ -1,13 +0,0 @@
FROM multiarch/debian-debootstrap:armhf-buster
LABEL Lan Tian "lantian@lantian.pub"
ENV GOOS=linux GOARCH=arm
WORKDIR /root
COPY . .
RUN apt-get -qq update && DEBIAN_FRONTEND=noninteractive apt-get -qq install -y golang git \
&& cd /root && go get github.com/gorilla/handlers && go build -o /frontend \
&& cd / && rm -rf /root/* \
&& apt-get -qq purge -y golang git \
&& apt-get -qq autoremove --purge -y && apt-get clean && rm -rf /var/lib/apt/lists
ENTRYPOINT ["/frontend"]

View File

@@ -1,13 +0,0 @@
FROM multiarch/debian-debootstrap:arm64-buster
LABEL Lan Tian "lantian@lantian.pub"
ENV GOOS=linux GOARCH=arm64
WORKDIR /root
COPY . .
RUN apt-get -qq update && DEBIAN_FRONTEND=noninteractive apt-get -qq install -y golang git \
&& cd /root && go get github.com/gorilla/handlers && go build -o /frontend \
&& cd / && rm -rf /root/* \
&& apt-get -qq purge -y golang git \
&& apt-get -qq autoremove --purge -y && apt-get clean && rm -rf /var/lib/apt/lists
ENTRYPOINT ["/frontend"]

View File

@@ -1,13 +0,0 @@
FROM i386/debian:buster
LABEL Lan Tian "lantian@lantian.pub"
ENV GOOS=linux GOARCH=386
WORKDIR /root
COPY . .
RUN apt-get -qq update && DEBIAN_FRONTEND=noninteractive apt-get -qq install -y golang git \
&& cd /root && go get github.com/gorilla/handlers && go build -o /frontend \
&& cd / && rm -rf /root/* \
&& apt-get -qq purge -y golang git \
&& apt-get -qq autoremove --purge -y && apt-get clean && rm -rf /var/lib/apt/lists
ENTRYPOINT ["/frontend"]

5
frontend/Makefile Normal file
View File

@@ -0,0 +1,5 @@
.PHONY: all
all:
go get -u github.com/kevinburke/go-bindata/...
go generate
go build -ldflags "-w -s" -o frontend

130
frontend/api.go Normal file
View File

@@ -0,0 +1,130 @@
package main
import (
"encoding/json"
"errors"
"net/http"
)
type apiRequest struct {
Servers []string `json:"servers"`
Type string `json:"type"`
Args string `json:"args"`
}
type apiGenericResultPair struct {
Server string `json:"server"`
Data string `json:"data"`
}
type apiSummaryResultPair struct {
Server string `json:"server"`
Data []SummaryRowData `json:"data"`
}
type apiResponse struct {
Error string `json:"error"`
Result []interface{} `json:"result"`
}
var apiHandlerMap = map[string](func(request apiRequest) apiResponse){
"summary": apiSummaryHandler,
"bird": apiGenericHandlerFactory("bird"),
"traceroute": apiGenericHandlerFactory("traceroute"),
"whois": apiWhoisHandler,
"server_list": apiServerListHandler,
}
func apiGenericHandlerFactory(endpoint string) func(request apiRequest) apiResponse {
return func(request apiRequest) apiResponse {
results := batchRequest(request.Servers, endpoint, request.Args)
var response apiResponse
for i, result := range results {
response.Result = append(response.Result, &apiGenericResultPair{
Server: request.Servers[i],
Data: result,
})
}
return response
}
}
func apiServerListHandler(request apiRequest) apiResponse {
var response apiResponse
for _, server := range setting.servers {
response.Result = append(response.Result, apiGenericResultPair{
Server: server,
})
}
return response
}
func apiSummaryHandler(request apiRequest) apiResponse {
results := batchRequest(request.Servers, "bird", "show protocols")
var response apiResponse
for i, result := range results {
parsedSummary, err := summaryParse(result, request.Servers[i])
if err != nil {
return apiResponse{
Error: err.Error(),
}
}
response.Result = append(response.Result, &apiSummaryResultPair{
Server: request.Servers[i],
Data: parsedSummary.Rows,
})
}
return response
}
func apiWhoisHandler(request apiRequest) apiResponse {
return apiResponse{
Error: "",
Result: []interface{}{
apiGenericResultPair{
Server: "",
Data: whois(request.Args),
},
},
}
}
func apiErrorHandler(err error) apiResponse {
return apiResponse{
Error: err.Error(),
}
}
func apiHandler(w http.ResponseWriter, r *http.Request) {
var request apiRequest
var response apiResponse
err := json.NewDecoder(r.Body).Decode(&request)
if err != nil {
response = apiResponse{
Error: err.Error(),
}
} else {
handler := apiHandlerMap[request.Type]
if handler == nil {
response = apiErrorHandler(errors.New("Invalid request type"))
} else {
response = handler(request)
}
}
w.Header().Add("Content-Type", "application/json")
w.Header().Add("Access-Control-Allow-Origin", "*")
bytes, err := json.Marshal(response)
if err != nil {
println(err.Error())
return
}
w.Write(bytes)
}

View File

@@ -2,6 +2,7 @@ package main
import (
"fmt"
"html"
"net"
"strings"
)
@@ -24,7 +25,7 @@ func birdRouteToGraphviz(servers []string, responses []string, target string) st
graph := make(map[string]string)
// Helper to add an edge
addEdge := func(src string, dest string, attr string) {
key := "\"" + src + "\" -> \"" + dest + "\""
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 {
@@ -34,7 +35,7 @@ func birdRouteToGraphviz(servers []string, responses []string, target string) st
}
// Helper to set attribute for a point in graph
addPoint := func(name string, attr string) {
key := "\"" + name + "\""
key := "\"" + html.EscapeString(name) + "\""
_, present := graph[key]
// Do not remove point's attributes if it's already present
if present && len(attr) == 0 {

74
frontend/bgpmap_test.go Normal file
View File

@@ -0,0 +1,74 @@
package main
import (
"strings"
"testing"
)
func TestGetASNRepresentation(t *testing.T) {
setting.dnsInterface = "asn.cymru.com"
result := getASNRepresentation("6939")
if !strings.Contains(result, "HURRICANE") {
t.Errorf("Lookup AS6939 failed, got %s", result)
}
}
func TestGetASNRepresentationFallback(t *testing.T) {
setting.dnsInterface = ""
result := getASNRepresentation("6939")
if result != "AS6939" {
t.Errorf("Lookup AS6939 failed, got %s", result)
}
}
func TestBirdRouteToGraphviz(t *testing.T) {
setting.dnsInterface = ""
// Don't change formatting of the following strings!
fakeResult := `192.168.0.1/32 unicast [alpha 2021-01-14 from 192.168.0.2] * (100) [AS12345i]
via 192.168.0.2 on eth0
Type: BGP univ
BGP.origin: IGP
BGP.as_path: 4242422601
BGP.next_hop: 172.18.0.2`
expectedResult := `digraph {
"Nexthop:\n172.18.0.2" -> "AS4242422601" [color=red];
"Nexthop:\n172.18.0.2" [shape=diamond];
"AS4242422601" -> "Target: 192.168.0.1" [color=red];
"Target: 192.168.0.1" [color=red,shape=diamond];
"alpha" [color=blue,shape=box];
"alpha" -> "Nexthop:\n172.18.0.2" [color=red];
}`
result := birdRouteToGraphviz([]string{
"alpha",
}, []string{
fakeResult,
}, "192.168.0.1")
for _, line := range strings.Split(result, "\n") {
if !strings.Contains(expectedResult, line) {
t.Errorf("Unexpected line in result: %s", line)
}
}
}
func TestBirdRouteToGraphvizXSS(t *testing.T) {
setting.dnsInterface = ""
// Don't change formatting of the following strings!
fakeResult := `<script>alert("evil!")</script>`
result := birdRouteToGraphviz([]string{
"alpha",
}, []string{
fakeResult,
}, fakeResult)
if strings.Contains(result, "<script>") {
t.Errorf("XSS injection succeeded: %s", result)
}
}

View File

@@ -0,0 +1,2 @@
User-agent: *
Disallow: /

View File

@@ -0,0 +1,16 @@
<h2>BGPmap: {{ html .Target }}</h2>
<div id="bgpmap">
</div>
<script src="https://cdn.jsdelivr.net/npm/viz.js@2.1.2/viz.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/viz.js@2.1.2/lite.render.js" crossorigin="anonymous"></script>
<script>
var viz = new Viz();
viz.renderSVGElement(`{{ .Result }}`)
.then(element => {
document.getElementById("bgpmap").appendChild(element);
})
.catch(error => {
document.getElementById("bgpmap").innerHTML = "<pre>"+error+"</pre>"
});
</script>

View File

@@ -0,0 +1,2 @@
<h2>{{ html .ServerName }}: {{ html .Target }}</h2>
{{ .Result }}

View File

@@ -0,0 +1,87 @@
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<meta name="renderer" content="webkit">
<title>{{ html .Title }}</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.1/dist/css/bootstrap.min.css" integrity="sha256-VoFZSlmyTXsegReQCNmbXrS4hBBUl/cexZvPmPWoJsY=" crossorigin="anonymous">
<meta name="robots" content="noindex, nofollow">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="/">{{ .Brand }}</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
{{ $option := .URLOption }}
{{ $server := .URLServer }}
{{ $target := .URLCommand }}
{{ if .IsWhois }}
{{ $option = "summary" }}
{{ $server = .AllServersURL }}
{{ $target = "" }}
{{ end }}
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link{{ if .AllServersLinkActive }} active{{ end }}"
href="/{{ $option }}/{{ .AllServersURL }}/{{ $target }}"> All Servers </a>
</li>
{{ range $k, $v := .ServersEscaped }}
<li class="nav-item">
<a class="nav-link{{ if eq $server $v }} active{{ end }}"
href="/{{ $option }}/{{ $v }}/{{ $target }}">{{ html (index $.ServersDisplay $k) }}</a>
</li>
{{ end }}
</ul>
{{ if .IsWhois }}
{{ $target = .WhoisTarget }}
{{ end }}
<form name="goto" class="form-inline" action="javascript:goto();">
<div class="input-group">
<select name="action" class="form-control">
{{ range $k, $v := .Options }}
<option value="{{ html $k }}"{{ if eq $k $.URLOption }} selected{{end}}>{{ html $v }}</option>
{{ end }}
</select>
<input name="server" class="d-none" value="{{ html $server }}">
<input name="target" class="form-control" placeholder="Target" aria-label="Target" value="{{ html $target }}">
<div class="input-group-append">
<button class="btn btn-outline-success" type="submit">&raquo;</button>
</div>
</div>
</form>
</div>
</nav>
<div class="container">
{{ .Content }}
</div>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.1/dist/js/bootstrap.min.js" integrity="sha256-0IiaoZCI++9oAAvmCb5Y0r93XkuhvJpRalZLffQXLok=" crossorigin="anonymous"></script>
<script>
function goto() {
let action = $('[name="action"]').val();
let server = $('[name="server"]').val();
let target = $('[name="target"]').val();
let url = "";
if (action == "whois") {
url = "/" + action + "/" + target;
} else if (action == "summary") {
url = "/" + action + "/" + server + "/";
} else {
url = "/" + action + "/" + server + "/" + target;
}
window.location.href = url;
}
</script>
</body>
</html>

View File

@@ -0,0 +1,21 @@
{{ $ServerName := urlquery .ServerName }}
<table class="table table-striped table-bordered table-sm">
<thead>
{{ range .Header }}
<th scope="col">{{ html . }}</th>
{{ end }}
</thead>
<tbody>
{{ range .Rows }}
<tr class="table-{{ .MappedState }}">
<td><a href="/detail/{{ $ServerName }}/{{ urlquery .Name }}">{{ html .Name }}</a></td>
<td>{{ html .Proto }}</td>
<td>{{ html .Table }}</td>
<td>{{ html .State }}</td>
<td>{{ html .Since }}</td>
<td>{{ html .Info }}</td>
</tr>
{{ end }}
</tbody>
</table>

View File

@@ -0,0 +1,2 @@
<h2>whois {{ html .Target }}</h2>
{{ .Result }}

30
frontend/dn42_test.go Normal file
View File

@@ -0,0 +1,30 @@
package main
import (
"testing"
)
func TestDN42WhoisFilter(t *testing.T) {
input := "name: Testing\ndescr: Description"
result := dn42WhoisFilter(input)
expectedResult := `name: Testing
1 line(s) skipped.
`
if result != expectedResult {
t.Errorf("Output doesn't match expected: %s", result)
}
}
func TestDN42WhoisFilterUnneeded(t *testing.T) {
input := "name: Testing\nwhatever: Description"
result := dn42WhoisFilter(input)
if result != input+"\n" {
t.Errorf("Output doesn't match expected: %s", result)
}
}

9
frontend/go.mod Normal file
View File

@@ -0,0 +1,9 @@
module github.com/xddxdd/bird-lg-go/frontend
go 1.15
require (
github.com/elazarl/go-bindata-assetfs v1.0.1
github.com/gorilla/handlers v1.5.1
github.com/kevinburke/go-bindata v3.22.0+incompatible // indirect
)

8
frontend/go.sum Normal file
View File

@@ -0,0 +1,8 @@
github.com/elazarl/go-bindata-assetfs v1.0.1 h1:m0kkaHRKEu7tUIUFVwhGGGYClXvyl4RE03qmvRTNfbw=
github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
github.com/kevinburke/go-bindata v3.22.0+incompatible h1:/JmqEhIWQ7GRScV0WjX/0tqBrC5D21ALg0H0U/KZ/ts=
github.com/kevinburke/go-bindata v3.22.0+incompatible/go.mod h1:/pEEZ72flUW2p0yi30bslSp9YqD9pysLxunQDdb2CPM=

View File

@@ -5,6 +5,7 @@ import (
"net/http"
"net/url"
"strconv"
"strings"
)
type channelData struct {
@@ -35,7 +36,15 @@ func batchRequest(servers []string, endpoint string, command string) []string {
}(i)
} else {
// Compose URL and send the request
url := "http://" + server + "." + setting.domain + ":" + strconv.Itoa(setting.proxyPort) + "/" + url.PathEscape(endpoint) + "?q=" + url.QueryEscape(command)
hostname := server
hostname = url.PathEscape(hostname)
if strings.Contains(hostname, ":") {
hostname = "[" + hostname + "]"
}
if setting.domain != "" {
hostname += "." + setting.domain
}
url := "http://" + hostname + ":" + strconv.Itoa(setting.proxyPort) + "/" + url.PathEscape(endpoint) + "?q=" + url.QueryEscape(command)
go func(url string, i int) {
response, err := http.Get(url)
if err != nil {

View File

@@ -7,8 +7,12 @@ import (
"strings"
)
// binary data
//go:generate go-bindata -prefix bindata -o bindata.go bindata/...
type settingType struct {
servers []string
serversDisplay []string
domain string
proxyPort int
whoisServer string
@@ -77,12 +81,23 @@ func main() {
if *serversPtr == "" {
panic("no server set")
} else if *domainPtr == "" {
panic("no base domain 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{
strings.Split(*serversPtr, ","),
servers,
serversDisplay,
*domainPtr,
*proxyPortPtr,
*whoisPtr,
@@ -93,5 +108,6 @@ func main() {
*navBarBrandPtr,
}
ImportTemplates()
webServerStart()
}

File diff suppressed because it is too large Load Diff

79
frontend/render_test.go Normal file
View File

@@ -0,0 +1,79 @@
package main
import (
"io/ioutil"
"net/http/httptest"
"net/url"
"strings"
"testing"
)
func initSettings() {
setting.servers = []string{"alpha"}
setting.serversDisplay = []string{"alpha"}
setting.titleBrand = "Bird-lg Go"
setting.navBarBrand = "Bird-lg Go"
ImportTemplates()
}
func TestRenderPageTemplate(t *testing.T) {
initSettings()
title := "Test Title"
content := "Test Content"
r := httptest.NewRequest("GET", "/route/alpha/192.168.0.1/", nil)
w := httptest.NewRecorder()
renderPageTemplate(w, r, title, content)
resultBytes, _ := ioutil.ReadAll(w.Result().Body)
result := string(resultBytes)
if !strings.Contains(result, title) {
t.Error("Title not found in output")
}
if !strings.Contains(result, content) {
t.Error("Content not found in output")
}
}
func TestRenderPageTemplateXSS(t *testing.T) {
initSettings()
evil := "<script>alert('evil');</script>"
r := httptest.NewRequest("GET", "/whois/"+url.PathEscape(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)
}
}
func TestSmartFormatterXSS(t *testing.T) {
evil := "<script>alert('evil');</script>"
result := smartFormatter(evil)
if strings.Contains(result, evil) {
t.Errorf("XSS injection succeeded: %s", result)
}
}
func TestSummaryTableXSS(t *testing.T) {
evil := "<script>alert('evil');</script>"
evilData := `Name Proto Table State Since Info
` + evil + ` ` + evil + ` --- up 2021-01-04 17:21:44 ` + evil
result := summaryTable(evilData, evil)
if strings.Contains(result, evil) {
t.Errorf("XSS injection succeeded: %s", result)
}
}

View File

@@ -87,17 +87,13 @@ func webHandlerTelegramBot(w http.ResponseWriter, r *http.Request) {
commandResult := ""
// - traceroute
if telegramIsCommand(request.Message.Text, "trace") || telegramIsCommand(request.Message.Text, "trace4") {
if telegramIsCommand(request.Message.Text, "trace") {
commandResult = telegramBatchRequestFormat(servers, "traceroute", target, telegramDefaultPostProcess)
} else if telegramIsCommand(request.Message.Text, "trace6") {
commandResult = telegramBatchRequestFormat(servers, "traceroute6", target, telegramDefaultPostProcess)
} else if telegramIsCommand(request.Message.Text, "route") || telegramIsCommand(request.Message.Text, "route4") {
} else if telegramIsCommand(request.Message.Text, "route") {
commandResult = telegramBatchRequestFormat(servers, "bird", "show route for "+target+" primary", telegramDefaultPostProcess)
} else if telegramIsCommand(request.Message.Text, "route6") {
commandResult = telegramBatchRequestFormat(servers, "bird6", "show route for "+target+" primary", telegramDefaultPostProcess)
} else if telegramIsCommand(request.Message.Text, "path") || telegramIsCommand(request.Message.Text, "path4") {
} else if telegramIsCommand(request.Message.Text, "path") {
commandResult = telegramBatchRequestFormat(servers, "bird", "show route for "+target+" all primary", func(result string) string {
for _, s := range strings.Split(result, "\n") {
if strings.Contains(s, "BGP.as_path: ") {
@@ -106,15 +102,6 @@ func webHandlerTelegramBot(w http.ResponseWriter, r *http.Request) {
}
return ""
})
} else if telegramIsCommand(request.Message.Text, "path6") {
commandResult = telegramBatchRequestFormat(servers, "bird6", "show route for "+target+" all primary", func(result string) string {
for _, s := range strings.Split(result, "\n") {
if strings.Contains(s, "BGP.as_path: ") {
return strings.TrimSpace(strings.Split(s, ":")[1])
}
}
return ""
})
} else if telegramIsCommand(request.Message.Text, "whois") {
if setting.netSpecificMode == "dn42" {
@@ -137,9 +124,9 @@ func webHandlerTelegramBot(w http.ResponseWriter, r *http.Request) {
} else if telegramIsCommand(request.Message.Text, "help") {
commandResult = `
/[path|path6] <IP>
/[route|route6] <IP>
/[trace|trace6] <IP>
/path <IP>
/route <IP>
/trace <IP>
/whois <Target>
`
} else {
@@ -151,6 +138,10 @@ func webHandlerTelegramBot(w http.ResponseWriter, r *http.Request) {
commandResult = "empty result"
}
if len(commandResult) > 4096 {
commandResult = commandResult[0:4096]
}
// Create a JSON response
w.Header().Add("Content-Type", "application/json")
response := &tgWebhookResponse{

View File

@@ -0,0 +1,31 @@
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,13 +1,19 @@
package main
import (
"strings"
"text/template"
)
type tmplArguments struct {
// template argument structures
// page
type TemplatePage struct {
// Global options
Options map[string]string
Servers []string
ServersEscaped []string
ServersDisplay []string
// Parameters related to current request
AllServersLinkActive bool
@@ -17,7 +23,6 @@ type tmplArguments struct {
IsWhois bool
WhoisTarget string
URLProto string
URLOption string
URLServer string
URLCommand string
@@ -28,70 +33,89 @@ type tmplArguments struct {
Content string
}
var tmpl = template.Must(template.New("tmpl").Parse(`
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<meta name="renderer" content="webkit">
<title>{{ .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">
<meta name="robots" content="noindex, nofollow">
</head>
<body>
// summary
type SummaryRowData struct {
Name string `json:"name"`
Proto string `json:"proto"`
Table string `json:"table"`
State string `json:"state"`
MappedState string `json:"-"`
Since string `json:"since"`
Info string `json:"info"`
}
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="/">{{ .Brand }}</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
// utility functions to allow filtering of results in the template
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav mr-auto">
<li class="nav-item"><a class="nav-link{{ if eq "ipv4" .URLProto }} active{{ end }}" href="/ipv4/{{ .URLOption }}/{{ .URLServer }}/{{ .URLCommand }}"> IPv4 </a></li>
<li class="nav-item"><a class="nav-link{{ if eq "ipv6" .URLProto }} active{{ end }}" href="/ipv6/{{ .URLOption }}/{{ .URLServer }}/{{ .URLCommand }}"> IPv6 </a></li>
<span class="navbar-text">|</span>
<li class="nav-item">
<a class="nav-link{{ if .AllServersLinkActive }} active{{ end }}" href="/{{ .URLProto }}/{{ .URLOption }}/{{ .AllServersURL }}/{{ .URLCommand }}"> All Servers </a>
</li>
{{ range $k, $v := .Servers }}
<li class="nav-item">
<a class="nav-link{{ if eq $.URLServer $v }} active{{ end }}" href="/{{ $.URLProto }}/{{ $.URLOption }}/{{ $v }}/{{ $.URLCommand }}">{{ $v }}</a>
</li>
{{ end }}
</ul>
{{ $option := .URLOption }}
{{ $target := .URLCommand }}
{{ if .IsWhois }}
{{ $option = "whois" }}
{{ $target = .WhoisTarget }}
{{ end }}
<form class="form-inline" action="/redir" method="GET">
<div class="input-group">
<select name="action" class="form-control">
{{ range $k, $v := .Options }}
<option value="{{ $k }}"{{ if eq $k $option }} selected{{end}}>{{ $v }}</option>
{{ end }}
</select>
<input name="proto" class="d-none" value="{{ .URLProto }}">
<input name="server" class="d-none" value="{{ .URLServer }}">
<input name="target" class="form-control" placeholder="Target" aria-label="Target" value="{{ $target }}">
<div class="input-group-append">
<button class="btn btn-outline-success" type="submit">&raquo;</button>
</div>
</div>
</form>
</div>
</nav>
func (r SummaryRowData) NameHasPrefix(prefix string) bool {
return strings.HasPrefix(r.Name, prefix)
}
<div class="container">
{{ .Content }}
</div>
func (r SummaryRowData) NameContains(prefix string) bool {
return strings.Contains(r.Name, prefix)
}
<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.1/dist/js/bootstrap.min.js" integrity="sha256-0IiaoZCI++9oAAvmCb5Y0r93XkuhvJpRalZLffQXLok=" crossorigin="anonymous"></script>
</body>
</html>
`))
type TemplateSummary struct {
ServerName string
Raw string
Header []string
Rows []SummaryRowData
}
// whois
type TemplateWhois struct {
Target string
Result string
}
// bgpmap
type TemplateBGPmap struct {
Servers []string
Target string
Result string
}
// bird
type TemplateBird struct {
ServerName string
Target string
Result string
}
// global variable to hold the templates
var TemplateLibrary map[string]*template.Template
// list of required templates
var requiredTemplates = [...]string{
"page",
"summary",
"whois",
"bgpmap",
"bird",
}
// import templates from bindata
func ImportTemplates() {
// create a new (blank) initial template
TemplateLibrary = make(map[string]*template.Template)
// for each template that is needed
for _, tmpl := range requiredTemplates {
// extract the template definition from the bindata
def := MustAssetString("templates/" + tmpl + ".tpl")
// and add it to the template library
template, err := template.New(tmpl).Parse(def)
if err != nil {
panic("Unable to parse template (templates/" + tmpl + ": " + err.Error())
}
// store in the library
TemplateLibrary[tmpl] = template
}
}

Some files were not shown because too many files have changed in this diff Show More