You've already forked bird-lg-go
							
							
				mirror of
				https://github.com/xddxdd/bird-lg-go
				synced 2025-10-30 09:40:51 +01:00 
			
		
		
		
	Compare commits
	
		
			91 Commits
		
	
	
		
			dependabot
			...
			v1.3.2.1
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | cc818c1cc0 | ||
|   | 6224b43808 | ||
|   | 17e0b14243 | ||
|   | b4c1bed9ba | ||
|   | abb32abff3 | ||
|   | b368c75aa3 | ||
|   | 09405cdb38 | ||
|   | f999d47d9f | ||
|   | 005dfb1435 | ||
|   | 4bd7a6bb95 | ||
|   | 462d76a2d0 | ||
|   | 58f217578c | ||
|   | 0e95727de1 | ||
|   | a48f1c8040 | ||
|   | 81acde3a37 | ||
|   | 7c0fe0d512 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | a5f4452d02 | ||
|   | b237185ef7 | ||
|   | e949646790 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | bb479d22ae | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | d40f41b4d5 | ||
|   | cdc34704b5 | ||
|   | db58bd3354 | ||
|   | a0246ccee2 | ||
|   | ccd14af0c8 | ||
|   | 594ca80f50 | ||
|   | 5625058e71 | ||
|   | 7efa3237a9 | ||
|   | 7b0c8c0556 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | ffd9165062 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 24fd5203e8 | ||
|   | 49a05767c1 | ||
|   | e7010f75f8 | ||
|   | dba2af7634 | ||
|   | 049775319b | ||
|   | 47c66b125c | ||
|   | 9e17b116f1 | ||
|   | 335ad40634 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 6ec0f2e7a6 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 4b73cf0fcb | ||
|   | 3b1d001543 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 675cb26ed1 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 556d3e50d3 | ||
|   | 06796f546e | ||
|   | d029d6684c | ||
|   | 5ce0f55f35 | ||
|   | 890ab51b07 | ||
|   | 8e4a35cc8c | ||
|   | 97f3c6088f | ||
|   | 982326a678 | ||
|   | 4b3980f6bd | ||
|   | 6f6b2bd283 | ||
|   | 892a7bee22 | ||
|   | 348295b9aa | ||
|   | 950c018b18 | ||
|   | 26efeb4996 | ||
|   | 5a5dfbc93f | ||
|   | f60a292129 | ||
|   | e7f6026854 | ||
|   | a4e0f4c193 | ||
|   | af5b653326 | ||
|   | 58847759b3 | ||
|   | 6481e7cc8d | ||
|   | 2166d73b3d | ||
|   | a64d839e2c | ||
|   | 1a3c618522 | ||
|   | fbd190628c | ||
|   | 823b639245 | ||
|   | b0c0e5442d | ||
|   | 4e4ce89418 | ||
|   | 234aadadd9 | ||
|   | bee26f421c | ||
|   | 2e0cb131ca | ||
|   | 4c248c638a | ||
|   | 3550362a4d | ||
|   | 256a80646f | ||
|   | 03c42eb1e8 | ||
|   | aea85e774c | ||
|   | 80d9351a58 | ||
|   | 5e0bc081e6 | ||
|   | 4d53d1f095 | ||
|   | 5883015294 | ||
|   | 80e66a7a81 | ||
|   | 41329da7cb | ||
|   | 8e56705205 | ||
|   | 6a8b3a0e55 | ||
|   | 83ab403706 | ||
|   | 7c7814cc7b | ||
|   | 8598060cc0 | ||
|   | bda06ddd5e | ||
|   | f404072ab8 | 
| @@ -1,61 +0,0 @@ | ||||
| version: 2.1 | ||||
|  | ||||
| workflows: | ||||
|   docker: | ||||
|     jobs: | ||||
|       - build | ||||
|       - deploy: | ||||
|           context: | ||||
|             - docker | ||||
|           requires: | ||||
|             - build | ||||
|           matrix: | ||||
|             parameters: | ||||
|               program: [frontend, proxy] | ||||
|               # latest is amd64 arch + push to default latest tag | ||||
|               image_arch: [latest, i386, arm32v7, arm64v8, ppc64le, s390x] | ||||
|           filters: | ||||
|             branches: | ||||
|               only: master | ||||
|  | ||||
| jobs: | ||||
|   build: | ||||
|     docker: | ||||
|       - image: circleci/golang:1.15 | ||||
|     working_directory: /go/src/github.com/xddxdd/bird-lg-go | ||||
|     steps: | ||||
|       - checkout | ||||
|       - run: go get -v -t -d ./... | ||||
|       - run: go get -u github.com/kevinburke/go-bindata/... | ||||
|       - run: cd frontend && go generate | ||||
|       - run: go test -v ./... | ||||
|   deploy: | ||||
|     docker: | ||||
|       - image: circleci/golang:1.15 | ||||
|     working_directory: /go/src/github.com/xddxdd/bird-lg-go | ||||
|     parameters: | ||||
|       image_arch: | ||||
|         type: string | ||||
|       program: | ||||
|         type: string | ||||
|     steps: | ||||
|       - checkout | ||||
|       - setup_remote_docker: | ||||
|           version: 19.03.13 | ||||
|       - run: | ||||
|           name: Install GPP | ||||
|           command: | | ||||
|             sudo apt-get update && sudo apt-get install -y gpp | ||||
|       - run: | ||||
|           name: Build Docker image | ||||
|           environment: | ||||
|             IMAGE_ARCH: << parameters.image_arch >> | ||||
|             PROGRAM: << parameters.program >> | ||||
|             BUILD_ID: << pipeline.number >> | ||||
|           command: | | ||||
|             make -f Makefile.docker _crossbuild | ||||
|             echo $DOCKER_PASSWORD | docker login -u $DOCKER_USERNAME --password-stdin | ||||
|             make -f Makefile.docker \ | ||||
|                  DOCKER_USERNAME=$DOCKER_USERNAME \ | ||||
|                  BUILD_ID=circleci-build$BUILD_ID \ | ||||
|                  $PROGRAM/$IMAGE_ARCH | ||||
							
								
								
									
										108
									
								
								.github/workflows/develop.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								.github/workflows/develop.yaml
									
									
									
									
										vendored
									
									
										Normal 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 }} | ||||
							
								
								
									
										83
									
								
								.github/workflows/release.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								.github/workflows/release.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,83 @@ | ||||
| on: | ||||
|   release: | ||||
|     types: [created] | ||||
|  | ||||
| jobs: | ||||
|   go-release: | ||||
|     name: Release Go Binary | ||||
|     runs-on: ubuntu-latest | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       matrix: | ||||
|         goos: [linux, windows, darwin] | ||||
|         goarch: ["386", amd64, "arm", arm64] | ||||
|         exclude: | ||||
|           - goarch: "386" | ||||
|             goos: darwin | ||||
|           - goarch: "arm" | ||||
|             goos: darwin | ||||
|           - goarch: "arm" | ||||
|             goos: windows | ||||
|     steps: | ||||
|     - name: Checkout | ||||
|       uses: actions/checkout@v3 | ||||
|  | ||||
|     - name: Release frontend | ||||
|       uses: wangyoucao577/go-release-action@v1.40 | ||||
|       with: | ||||
|         github_token: ${{ secrets.GITHUB_TOKEN }} | ||||
|         goos: ${{ matrix.goos }} | ||||
|         goarch: ${{ matrix.goarch }} | ||||
|         project_path: "./frontend" | ||||
|         binary_name: "bird-lg-go" | ||||
|  | ||||
|     - name: Release proxy | ||||
|       uses: wangyoucao577/go-release-action@v1.40 | ||||
|       with: | ||||
|         github_token: ${{ secrets.GITHUB_TOKEN }} | ||||
|         goos: ${{ matrix.goos }} | ||||
|         goarch: ${{ matrix.goarch }} | ||||
|         project_path: "./proxy" | ||||
|         binary_name: "bird-lgproxy-go" | ||||
|  | ||||
|   docker-release: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Set up QEMU | ||||
|         uses: docker/setup-qemu-action@v2 | ||||
|  | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v2 | ||||
|  | ||||
|       - name: Login to Docker Hub | ||||
|         uses: docker/login-action@v2 | ||||
|         with: | ||||
|           username: ${{ secrets.DOCKERHUB_USERNAME }} | ||||
|           password: ${{ secrets.DOCKERHUB_TOKEN }} | ||||
|  | ||||
|       - name: Login to GitHub Container Registry | ||||
|         uses: docker/login-action@v2 | ||||
|         with: | ||||
|           registry: ghcr.io | ||||
|           username: ${{ github.repository_owner }} | ||||
|           password: ${{ secrets.GITHUB_TOKEN }} | ||||
|  | ||||
|       - name: Build frontend docker image | ||||
|         uses: docker/build-push-action@v4 | ||||
|         with: | ||||
|           context: '{{defaultContext}}:frontend' | ||||
|           platforms: linux/amd64,linux/arm64,linux/386,linux/arm/v7 | ||||
|           push: true | ||||
|           tags: | | ||||
|             xddxdd/bird-lg-go:latest | ||||
|             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 | ||||
							
								
								
									
										4
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								Makefile
									
									
									
									
									
								
							| @@ -9,5 +9,5 @@ proxy: | ||||
| all: frontend proxy | ||||
|  | ||||
| install: | ||||
| 	install -m 755 frontend/frontend /usr/local/bin/frontend | ||||
| 	install -m 755 proxy/proxy /usr/local/bin/proxy | ||||
| 	install -m 755 frontend/frontend /usr/local/bin/bird-lg-go | ||||
| 	install -m 755 proxy/proxy /usr/local/bin/bird-lgproxy-go | ||||
|   | ||||
| @@ -1,77 +0,0 @@ | ||||
| # Basic definitions | ||||
| DOCKER_USERNAME := xddxdd | ||||
| ARCHITECTURES := amd64 i386 arm32v7 arm64v8 ppc64le s390x | ||||
| IMAGES := frontend proxy | ||||
|  | ||||
| # General Purpose Preprocessor config | ||||
| GPP_INCLUDE_DIR := include | ||||
| GPP_FLAGS_U := "" "" "(" "," ")" "(" ")" "\#" "" | ||||
| GPP_FLAGS_M := "\#" "\n" " " " " "\n" "(" ")" | ||||
| GPP_FLAGS_EXTRA := +c "\\\n" "" | ||||
| GPP_FLAGS := -I ${GPP_INCLUDE_DIR} --nostdinc -U ${GPP_FLAGS_U} -M ${GPP_FLAGS_M} ${GPP_FLAGS_EXTRA} | ||||
|  | ||||
| BUILD_ID ?= $(shell date +%Y%m%d%H%M) | ||||
|  | ||||
| define create-image-arch-target | ||||
| frontend/Dockerfile.$1: frontend/template.Dockerfile | ||||
| 	@gpp ${GPP_FLAGS} -D ARCH_$(shell echo $1 | tr a-z A-Z) -o frontend/Dockerfile.$1 frontend/template.Dockerfile || rm -rf frontend/Dockerfile.$1 | ||||
|  | ||||
| frontend/$1: frontend/Dockerfile.$1 | ||||
| 	@if [ -f frontend/Dockerfile.$1 ]; then \ | ||||
| 		docker build --pull --no-cache -t ${DOCKER_USERNAME}/bird-lg-go:$1-${BUILD_ID} -f frontend/Dockerfile.$1 frontend || exit 1; \ | ||||
| 		docker push ${DOCKER_USERNAME}/bird-lg-go:$1-${BUILD_ID} || exit 1; \ | ||||
| 		docker tag ${DOCKER_USERNAME}/bird-lg-go:$1-${BUILD_ID} ${DOCKER_USERNAME}/bird-lg-go:$1 || exit 1; \ | ||||
| 		docker push ${DOCKER_USERNAME}/bird-lg-go:$1 || exit 1; \ | ||||
| 	else \ | ||||
| 		echo "Dockerfile generation failed, see error above"; \ | ||||
| 		exit 1; \ | ||||
| 	fi | ||||
|  | ||||
| proxy/Dockerfile.$1: proxy/template.Dockerfile | ||||
| 	@gpp ${GPP_FLAGS} -D ARCH_$(shell echo $1 | tr a-z A-Z) -o proxy/Dockerfile.$1 proxy/template.Dockerfile || rm -rf proxy/Dockerfile.$1 | ||||
|  | ||||
| proxy/$1: proxy/Dockerfile.$1 | ||||
| 	@if [ -f proxy/Dockerfile.$1 ]; then \ | ||||
| 		docker build --pull --no-cache -t ${DOCKER_USERNAME}/bird-lgproxy-go:$1-${BUILD_ID} -f proxy/Dockerfile.$1 proxy || exit 1; \ | ||||
| 		docker push ${DOCKER_USERNAME}/bird-lgproxy-go:$1-${BUILD_ID} || exit 1; \ | ||||
| 		docker tag ${DOCKER_USERNAME}/bird-lgproxy-go:$1-${BUILD_ID} ${DOCKER_USERNAME}/bird-lgproxy-go:$1 || exit 1; \ | ||||
| 		docker push ${DOCKER_USERNAME}/bird-lgproxy-go:$1 || exit 1; \ | ||||
| 	else \ | ||||
| 		echo "Dockerfile generation failed, see error above"; \ | ||||
| 		exit 1; \ | ||||
| 	fi | ||||
|  | ||||
| endef | ||||
|  | ||||
| $(foreach arch,${ARCHITECTURES},$(eval $(call create-image-arch-target,$(arch)))) | ||||
|  | ||||
| frontend:$(foreach arch,latest ${ARCHITECTURES},frontend/${arch}) | ||||
|  | ||||
| frontend/latest: frontend/amd64 | ||||
| 	@docker tag ${DOCKER_USERNAME}/bird-lg-go:amd64-${BUILD_ID} ${DOCKER_USERNAME}/bird-lg-go:${BUILD_ID} || exit 1 | ||||
| 	@docker push ${DOCKER_USERNAME}/bird-lg-go:${BUILD_ID} || exit 1 | ||||
| 	@docker tag ${DOCKER_USERNAME}/bird-lg-go:amd64-${BUILD_ID} ${DOCKER_USERNAME}/bird-lg-go:latest || exit 1 | ||||
| 	@docker push ${DOCKER_USERNAME}/bird-lg-go:latest || exit 1 | ||||
|  | ||||
| proxy:$(foreach arch,latest ${ARCHITECTURES},proxy/${arch}) | ||||
|  | ||||
| proxy/latest: proxy/amd64 | ||||
| 	@docker tag ${DOCKER_USERNAME}/bird-lgproxy-go:amd64-${BUILD_ID} ${DOCKER_USERNAME}/bird-lgproxy-go:${BUILD_ID} || exit 1 | ||||
| 	@docker push ${DOCKER_USERNAME}/bird-lgproxy-go:${BUILD_ID} || exit 1 | ||||
| 	@docker tag ${DOCKER_USERNAME}/bird-lgproxy-go:amd64-${BUILD_ID} ${DOCKER_USERNAME}/bird-lgproxy-go:latest || exit 1 | ||||
| 	@docker push ${DOCKER_USERNAME}/bird-lgproxy-go:latest || exit 1 | ||||
|  | ||||
| .DEFAULT_GOAL := images | ||||
| .DELETE_ON_ERROR: | ||||
| .SECONDARY: | ||||
|  | ||||
| # Target to enable multiarch support | ||||
| _crossbuild: | ||||
| 	@docker run --rm --privileged multiarch/qemu-user-static --reset -p yes >/dev/null | ||||
|  | ||||
| dockerfiles: $(foreach image,${IMAGES},$(foreach arch,${ARCHITECTURES},$(image)/Dockerfile.$(arch))) | ||||
|  | ||||
| images: $(foreach image,${IMAGES},$(image)) | ||||
|  | ||||
| clean: | ||||
| 	@rm -rf */Dockerfile.{$(shell echo ${ARCHITECTURES} | sed "s/ /,/g")} | ||||
| @@ -153,9 +153,11 @@ Request: | ||||
|  | ||||
| ```json | ||||
| { | ||||
|     "servers": [], | ||||
|     "type": "server_list", | ||||
|     "args": "" | ||||
|     "servers": [ | ||||
|         "alpha" | ||||
|     ], | ||||
|     "type": "bird", | ||||
|     "args": "show status" | ||||
| } | ||||
| ``` | ||||
|  | ||||
| @@ -179,11 +181,9 @@ Request: | ||||
|  | ||||
| ```json | ||||
| { | ||||
|     "servers": [ | ||||
|         "alpha" | ||||
|     ], | ||||
|     "type": "bird", | ||||
|     "args": "show status" | ||||
|     "servers": [], | ||||
|     "type": "server_list", | ||||
|     "args": "" | ||||
| } | ||||
| ``` | ||||
|  | ||||
							
								
								
									
										22
									
								
								docs/Telegram.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								docs/Telegram.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| # Telegram Bot Webhook | ||||
|  | ||||
| The frontend can act as a Telegram Bot webhook endpoint, to add BGP route/traceroute/whois lookup functionality to your tech group. | ||||
|  | ||||
| There is no configuration necessary on the frontend, just start it up normally. | ||||
|  | ||||
| Set your Telegram Bot webhook URL to `https://your.frontend.com/telegram/alpha+beta+gamma`, where `alpha+beta+gamma` is the list of servers to be queried on Telegram commands, separated by `+`. | ||||
|  | ||||
| You may omit `alpha+beta+gamma` to use all your servers, but it is not recommended when you have lots of servers, or the message would be too long and hard to read. | ||||
|  | ||||
| ## Example of setting the webhook | ||||
|  | ||||
| ```bash | ||||
| curl "https://api.telegram.org/bot${BOT_TOKEN}/setWebhook?url=https://your.frontend.com:5000/telegram/alpha+beta+gamma" | ||||
| ``` | ||||
|  | ||||
| ## Supported commands | ||||
|  | ||||
| - `path`: Show bird's ASN path to target IP | ||||
| - `route`: Show bird's preferred route to target IP | ||||
| - `trace`: Traceroute to target IP/domain | ||||
| - `whois`: Whois query | ||||
							
								
								
									
										33
									
								
								frontend/Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								frontend/Dockerfile
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| FROM golang AS step_0 | ||||
| ENV CGO_ENABLED=0 GO111MODULE=on | ||||
| WORKDIR /root | ||||
| COPY . . | ||||
| RUN go build -ldflags "-w -s" -o /frontend | ||||
|  | ||||
| ################################################################################ | ||||
|  | ||||
| FROM alpine:edge AS step_1 | ||||
|  | ||||
| WORKDIR /root | ||||
| RUN apk add --no-cache build-base pkgconf perl gettext \ | ||||
|     libidn2-dev libidn2-static libunistring-dev libunistring-static gnu-libiconv-dev | ||||
|  | ||||
| RUN wget https://github.com/rfc1036/whois/archive/refs/tags/v5.5.18.tar.gz \ | ||||
|     -O whois-5.5.18.tar.gz | ||||
|  | ||||
| RUN tar xvf whois-5.5.18.tar.gz \ | ||||
|     && cd whois-5.5.18 \ | ||||
|     && sed -i "s/#if defined _POSIX_C_SOURCE && _POSIX_C_SOURCE >= 200112L/#if 1/g" config.h \ | ||||
|     && make whois -j4 \ | ||||
|       LDFLAGS="-static" CONFIG_FILE="/etc/whois.conf" PKG_CONFIG="pkg-config --static" HAVE_ICONV=1 \ | ||||
|     && strip /root/whois-5.5.18/whois | ||||
|  | ||||
| ################################################################################ | ||||
|  | ||||
| FROM scratch AS step_2 | ||||
| ENV PATH=/ | ||||
| ENV BIRDLG_WHOIS=/whois | ||||
| COPY --from=step_0 /frontend / | ||||
| COPY --from=step_1 /root/whois-5.5.18/whois / | ||||
| COPY --from=step_1 /etc/services /etc/services | ||||
| ENTRYPOINT ["/frontend"] | ||||
| @@ -1,5 +1,3 @@ | ||||
| .PHONY: all | ||||
| all: | ||||
| 	go get -u github.com/kevinburke/go-bindata/... | ||||
| 	go generate | ||||
| 	go build -ldflags "-w -s" -o frontend | ||||
|   | ||||
| @@ -113,7 +113,7 @@ func apiHandler(w http.ResponseWriter, r *http.Request) { | ||||
| 	} else { | ||||
| 		handler := apiHandlerMap[request.Type] | ||||
| 		if handler == nil { | ||||
| 			response = apiErrorHandler(errors.New("Invalid request type")) | ||||
| 			response = apiErrorHandler(errors.New("invalid request type")) | ||||
| 		} else { | ||||
| 			response = handler(request) | ||||
| 		} | ||||
|   | ||||
							
								
								
									
										207
									
								
								frontend/api_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										207
									
								
								frontend/api_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,207 @@ | ||||
| package main | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"net/http" | ||||
| 	"net/http/httptest" | ||||
| 	"net/url" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/jarcoal/httpmock" | ||||
| 	"github.com/magiconair/properties/assert" | ||||
| ) | ||||
|  | ||||
| func TestApiServerListHandler(t *testing.T) { | ||||
| 	setting.servers = []string{"alpha", "beta", "gamma"} | ||||
| 	response := apiServerListHandler(apiRequest{}) | ||||
|  | ||||
| 	assert.Equal(t, len(response.Result), 3) | ||||
| 	assert.Equal(t, response.Result[0].(apiGenericResultPair).Server, "alpha") | ||||
| 	assert.Equal(t, response.Result[1].(apiGenericResultPair).Server, "beta") | ||||
| 	assert.Equal(t, response.Result[2].(apiGenericResultPair).Server, "gamma") | ||||
| } | ||||
|  | ||||
| func TestApiGenericHandlerFactory(t *testing.T) { | ||||
| 	httpmock.Activate() | ||||
| 	defer httpmock.DeactivateAndReset() | ||||
|  | ||||
| 	httpResponse := httpmock.NewStringResponder(200, BirdSummaryData) | ||||
| 	httpmock.RegisterResponder("GET", "http://alpha:8000/bird?q="+url.QueryEscape("show protocols"), httpResponse) | ||||
|  | ||||
| 	setting.servers = []string{"alpha"} | ||||
| 	setting.domain = "" | ||||
| 	setting.proxyPort = 8000 | ||||
|  | ||||
| 	request := apiRequest{ | ||||
| 		Servers: setting.servers, | ||||
| 		Type:    "bird", | ||||
| 		Args:    "show protocols", | ||||
| 	} | ||||
|  | ||||
| 	handler := apiGenericHandlerFactory("bird") | ||||
| 	response := handler(request) | ||||
|  | ||||
| 	assert.Equal(t, response.Error, "") | ||||
|  | ||||
| 	result := response.Result[0].(*apiGenericResultPair) | ||||
| 	assert.Equal(t, result.Server, "alpha") | ||||
| 	assert.Equal(t, result.Data, BirdSummaryData) | ||||
| } | ||||
|  | ||||
| func TestApiSummaryHandler(t *testing.T) { | ||||
| 	httpmock.Activate() | ||||
| 	defer httpmock.DeactivateAndReset() | ||||
|  | ||||
| 	httpResponse := httpmock.NewStringResponder(200, BirdSummaryData) | ||||
| 	httpmock.RegisterResponder("GET", "http://alpha:8000/bird?q="+url.QueryEscape("show protocols"), httpResponse) | ||||
|  | ||||
| 	setting.servers = []string{"alpha"} | ||||
| 	setting.domain = "" | ||||
| 	setting.proxyPort = 8000 | ||||
|  | ||||
| 	request := apiRequest{ | ||||
| 		Servers: setting.servers, | ||||
| 		Type:    "summary", | ||||
| 		Args:    "", | ||||
| 	} | ||||
| 	response := apiSummaryHandler(request) | ||||
|  | ||||
| 	assert.Equal(t, response.Error, "") | ||||
|  | ||||
| 	summary := response.Result[0].(*apiSummaryResultPair) | ||||
| 	assert.Equal(t, summary.Server, "alpha") | ||||
| 	// Protocol list will be sorted | ||||
| 	assert.Equal(t, summary.Data[1].Name, "device1") | ||||
| 	assert.Equal(t, summary.Data[1].Proto, "Device") | ||||
| 	assert.Equal(t, summary.Data[1].Table, "---") | ||||
| 	assert.Equal(t, summary.Data[1].State, "up") | ||||
| 	assert.Equal(t, summary.Data[1].Since, "2021-08-27") | ||||
| 	assert.Equal(t, summary.Data[1].Info, "") | ||||
| } | ||||
|  | ||||
| func TestApiSummaryHandlerError(t *testing.T) { | ||||
| 	httpmock.Activate() | ||||
| 	defer httpmock.DeactivateAndReset() | ||||
|  | ||||
| 	httpResponse := httpmock.NewStringResponder(200, "Mock backend error") | ||||
| 	httpmock.RegisterResponder("GET", "http://alpha:8000/bird?q="+url.QueryEscape("show protocols"), httpResponse) | ||||
|  | ||||
| 	setting.servers = []string{"alpha"} | ||||
| 	setting.domain = "" | ||||
| 	setting.proxyPort = 8000 | ||||
|  | ||||
| 	request := apiRequest{ | ||||
| 		Servers: setting.servers, | ||||
| 		Type:    "summary", | ||||
| 		Args:    "", | ||||
| 	} | ||||
| 	response := apiSummaryHandler(request) | ||||
|  | ||||
| 	assert.Equal(t, response.Error, "Mock backend error") | ||||
| } | ||||
|  | ||||
| func TestApiWhoisHandler(t *testing.T) { | ||||
| 	expectedData := "Mock Data" | ||||
| 	server := WhoisServer{ | ||||
| 		t:             t, | ||||
| 		expectedQuery: "AS6939", | ||||
| 		response:      expectedData, | ||||
| 	} | ||||
|  | ||||
| 	server.Listen() | ||||
| 	go server.Run() | ||||
| 	defer server.Close() | ||||
|  | ||||
| 	setting.whoisServer = server.server.Addr().String() | ||||
|  | ||||
| 	request := apiRequest{ | ||||
| 		Servers: []string{}, | ||||
| 		Type:    "", | ||||
| 		Args:    "AS6939", | ||||
| 	} | ||||
| 	response := apiWhoisHandler(request) | ||||
|  | ||||
| 	assert.Equal(t, response.Error, "") | ||||
|  | ||||
| 	whoisResult := response.Result[0].(apiGenericResultPair) | ||||
| 	assert.Equal(t, whoisResult.Server, "") | ||||
| 	assert.Equal(t, whoisResult.Data, expectedData) | ||||
| } | ||||
|  | ||||
| func TestApiErrorHandler(t *testing.T) { | ||||
| 	err := errors.New("Mock Error") | ||||
| 	response := apiErrorHandler(err) | ||||
| 	assert.Equal(t, response.Error, "Mock Error") | ||||
| } | ||||
|  | ||||
| func TestApiHandler(t *testing.T) { | ||||
| 	setting.servers = []string{"alpha", "beta", "gamma"} | ||||
|  | ||||
| 	request := apiRequest{ | ||||
| 		Servers: []string{}, | ||||
| 		Type:    "server_list", | ||||
| 		Args:    "", | ||||
| 	} | ||||
| 	requestJson, err := json.Marshal(request) | ||||
| 	if err != nil { | ||||
| 		t.Error(err) | ||||
| 	} | ||||
|  | ||||
| 	r := httptest.NewRequest(http.MethodGet, "/api", bytes.NewReader(requestJson)) | ||||
| 	w := httptest.NewRecorder() | ||||
| 	apiHandler(w, r) | ||||
|  | ||||
| 	var response apiResponse | ||||
| 	err = json.Unmarshal(w.Body.Bytes(), &response) | ||||
| 	if err != nil { | ||||
| 		t.Error(err) | ||||
| 	} | ||||
|  | ||||
| 	assert.Equal(t, len(response.Result), 3) | ||||
| 	// Hard to unmarshal JSON into apiGenericResultPair objects, won't check here | ||||
| } | ||||
|  | ||||
| func TestApiHandlerBadJSON(t *testing.T) { | ||||
| 	setting.servers = []string{"alpha", "beta", "gamma"} | ||||
|  | ||||
| 	r := httptest.NewRequest(http.MethodGet, "/api", strings.NewReader("{bad json}")) | ||||
| 	w := httptest.NewRecorder() | ||||
| 	apiHandler(w, r) | ||||
|  | ||||
| 	var response apiResponse | ||||
| 	err := json.Unmarshal(w.Body.Bytes(), &response) | ||||
| 	if err != nil { | ||||
| 		t.Error(err) | ||||
| 	} | ||||
|  | ||||
| 	assert.Equal(t, len(response.Result), 0) | ||||
| } | ||||
|  | ||||
| func TestApiHandlerInvalidType(t *testing.T) { | ||||
| 	setting.servers = []string{"alpha", "beta", "gamma"} | ||||
|  | ||||
| 	request := apiRequest{ | ||||
| 		Servers: setting.servers, | ||||
| 		Type:    "invalid_type", | ||||
| 		Args:    "", | ||||
| 	} | ||||
| 	requestJson, err := json.Marshal(request) | ||||
| 	if err != nil { | ||||
| 		t.Error(err) | ||||
| 	} | ||||
|  | ||||
| 	r := httptest.NewRequest(http.MethodGet, "/api", bytes.NewReader(requestJson)) | ||||
| 	w := httptest.NewRecorder() | ||||
| 	apiHandler(w, r) | ||||
|  | ||||
| 	var response apiResponse | ||||
| 	err = json.Unmarshal(w.Body.Bytes(), &response) | ||||
| 	if err != nil { | ||||
| 		t.Error(err) | ||||
| 	} | ||||
|  | ||||
| 	assert.Equal(t, len(response.Result), 0) | ||||
| } | ||||
							
								
								
									
										83
									
								
								frontend/asn_cache.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								frontend/asn_cache.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,83 @@ | ||||
| package main | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| type ASNCache map[string]string | ||||
|  | ||||
| func (cache ASNCache) _lookup(asn string) string { | ||||
| 	// Try to get ASN representation using DNS | ||||
| 	if setting.dnsInterface != "" { | ||||
| 		records, err := net.LookupTXT(fmt.Sprintf("AS%s.%s", asn, setting.dnsInterface)) | ||||
| 		if err == nil { | ||||
| 			result := strings.Join(records, " ") | ||||
| 			if resultSplit := strings.Split(result, " | "); len(resultSplit) > 1 { | ||||
| 				result = strings.Join(resultSplit[1:], "\n") | ||||
| 			} | ||||
| 			return fmt.Sprintf("AS%s\n%s", asn, result) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Try to get ASN representation using WHOIS | ||||
| 	if setting.whoisServer != "" { | ||||
| 		if setting.bgpmapInfo == "" { | ||||
| 			setting.bgpmapInfo = "asn,as-name,ASName,descr" | ||||
| 		} | ||||
| 		records := whois(fmt.Sprintf("AS%s", asn)) | ||||
| 		if records != "" { | ||||
| 			recordsSplit := strings.Split(records, "\n") | ||||
| 			var result []string | ||||
| 			for _, title := range strings.Split(setting.bgpmapInfo, ",") { | ||||
| 				if title == "asn" { | ||||
| 					result = append(result, "AS"+asn) | ||||
| 				} | ||||
| 			} | ||||
| 			for _, title := range strings.Split(setting.bgpmapInfo, ",") { | ||||
| 				allow_multiline := false | ||||
| 				if title[0] == ':' && len(title) >= 2 { | ||||
| 					title = title[1:] | ||||
| 					allow_multiline = true | ||||
| 				} | ||||
| 				for _, line := range recordsSplit { | ||||
| 					if len(line) == 0 || line[0] == '%' || !strings.Contains(line, ":") { | ||||
| 						continue | ||||
| 					} | ||||
| 					linearr := strings.SplitN(line, ":", 2) | ||||
| 					line_title := linearr[0] | ||||
| 					content := strings.TrimSpace(linearr[1]) | ||||
| 					if line_title != title { | ||||
| 						continue | ||||
| 					} | ||||
| 					result = append(result, content) | ||||
| 					if !allow_multiline { | ||||
| 						break | ||||
| 					} | ||||
|  | ||||
| 				} | ||||
| 			} | ||||
| 			if len(result) > 0 { | ||||
| 				return strings.Join(result, "\n") | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| func (cache ASNCache) Lookup(asn string) string { | ||||
| 	cachedValue, cacheOk := cache[asn] | ||||
| 	if cacheOk { | ||||
| 		return cachedValue | ||||
| 	} | ||||
|  | ||||
| 	result := cache._lookup(asn) | ||||
| 	if len(result) == 0 { | ||||
| 		result = fmt.Sprintf("AS%s", asn) | ||||
| 	} | ||||
|  | ||||
| 	cache[asn] = result | ||||
| 	return result | ||||
| } | ||||
							
								
								
									
										52
									
								
								frontend/asn_cache_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								frontend/asn_cache_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| package main | ||||
|  | ||||
| import ( | ||||
| 	"strings" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/magiconair/properties/assert" | ||||
| ) | ||||
|  | ||||
| func TestGetASNRepresentationDNS(t *testing.T) { | ||||
| 	checkNetwork(t) | ||||
|  | ||||
| 	setting.dnsInterface = "asn.cymru.com" | ||||
| 	setting.whoisServer = "" | ||||
| 	cache := make(ASNCache) | ||||
| 	result := cache.Lookup("6939") | ||||
| 	if !strings.Contains(result, "HURRICANE") { | ||||
| 		t.Errorf("Lookup AS6939 failed, got %s", result) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestGetASNRepresentationDNSFallback(t *testing.T) { | ||||
| 	checkNetwork(t) | ||||
|  | ||||
| 	setting.dnsInterface = "invalid.example.com" | ||||
| 	setting.whoisServer = "whois.arin.net" | ||||
| 	cache := make(ASNCache) | ||||
| 	result := cache.Lookup("6939") | ||||
| 	if !strings.Contains(result, "HURRICANE") { | ||||
| 		t.Errorf("Lookup AS6939 failed, got %s", result) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestGetASNRepresentationWhois(t *testing.T) { | ||||
| 	checkNetwork(t) | ||||
|  | ||||
| 	setting.dnsInterface = "" | ||||
| 	setting.whoisServer = "whois.arin.net" | ||||
| 	cache := make(ASNCache) | ||||
| 	result := cache.Lookup("6939") | ||||
| 	if !strings.Contains(result, "HURRICANE") { | ||||
| 		t.Errorf("Lookup AS6939 failed, got %s", result) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestGetASNRepresentationFallback(t *testing.T) { | ||||
| 	setting.dnsInterface = "" | ||||
| 	setting.whoisServer = "" | ||||
| 	cache := make(ASNCache) | ||||
| 	result := cache.Lookup("6939") | ||||
| 	assert.Equal(t, result, "AS6939") | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								frontend/assets/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								frontend/assets/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 32 KiB | 
							
								
								
									
										7
									
								
								frontend/assets/static/jsdelivr/npm/bootstrap@4.5.1/dist/css/bootstrap.min.css
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								frontend/assets/static/jsdelivr/npm/bootstrap@4.5.1/dist/css/bootstrap.min.css
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										7
									
								
								frontend/assets/static/jsdelivr/npm/bootstrap@4.5.1/dist/js/bootstrap.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								frontend/assets/static/jsdelivr/npm/bootstrap@4.5.1/dist/js/bootstrap.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										2
									
								
								frontend/assets/static/jsdelivr/npm/jquery@3.5.1/dist/jquery.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								frontend/assets/static/jsdelivr/npm/jquery@3.5.1/dist/jquery.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										8
									
								
								frontend/assets/static/jsdelivr/npm/viz.js@2.1.2/viz.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								frontend/assets/static/jsdelivr/npm/viz.js@2.1.2/viz.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										73
									
								
								frontend/assets/static/sortTable.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								frontend/assets/static/sortTable.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,73 @@ | ||||
| // adapted from https://stackoverflow.com/a/57080195 | ||||
|  | ||||
| document.querySelectorAll('table.sortable') | ||||
|     .forEach((table)=> { | ||||
|         table.querySelectorAll('th') | ||||
|             .forEach((element, columnNo) => { | ||||
|                 element.addEventListener('click', event => { | ||||
|                     if(element.classList.contains('ascSorted')) { | ||||
|                         dir = -1; | ||||
|                         element.classList.remove('ascSorted'); | ||||
|                         element.classList.add('descSorted'); | ||||
|                         element.innerText = element.innerText.slice(0,-2) + " ↓"; | ||||
|                     } else if(element.classList.contains('descSorted')) { | ||||
|                         dir = 1; | ||||
|                         element.classList.remove('descSorted'); | ||||
|                         element.classList.add('ascSorted'); | ||||
|                         element.innerText = element.innerText.slice(0,-2) + " ↑"; | ||||
|                     } else { | ||||
|                         dir = 1; | ||||
|                         element.classList.add('ascSorted'); | ||||
|                         element.innerText += " ↑"; | ||||
|                     } | ||||
|                     sortTable(table, columnNo, 0, dir, 1); | ||||
|                 }); | ||||
|             }); | ||||
|     }); | ||||
|  | ||||
| function sortTable(table, priCol, secCol, priDir, secDir) { | ||||
|     const tableBody = table.querySelector('tbody'); | ||||
|     const tableData = table2data(tableBody); | ||||
|     tableData.sort((a, b) => { | ||||
|         if(a[priCol] === b[priCol]) { | ||||
|             if(a[secCol] > b[secCol]) { | ||||
|                 return secDir; | ||||
|             } else { | ||||
|                 return -secDir; | ||||
|             } | ||||
|         } else if(a[priCol] > b[priCol]) { | ||||
|             return priDir; | ||||
|         } else { | ||||
|             return -priDir; | ||||
|         } | ||||
|     }); | ||||
|     data2table(tableBody, tableData); | ||||
| } | ||||
|  | ||||
| function table2data(tableBody) { | ||||
|     const tableData = []; | ||||
|     tableBody.querySelectorAll('tr') | ||||
|         .forEach(row => { | ||||
|             const rowData = []; | ||||
|             row.querySelectorAll('td') | ||||
|                 .forEach(cell => { | ||||
|                     rowData.push(cell.innerHTML); | ||||
|                 }); | ||||
|             rowData.classList = row.classList.toString(); | ||||
|             tableData.push(rowData); | ||||
|         }); | ||||
|     return tableData; | ||||
| } | ||||
|  | ||||
| function data2table(tableBody, tableData) { | ||||
|     tableBody.querySelectorAll('tr') | ||||
|         .forEach((row, i) => { | ||||
|             const rowData = tableData[i]; | ||||
|             row.classList = rowData.classList; | ||||
|             row.querySelectorAll('td') | ||||
|                 .forEach((cell, j) => { | ||||
|                     cell.innerHTML = rowData[j]; | ||||
|                 }); | ||||
|             tableData.push(rowData); | ||||
|         }); | ||||
| } | ||||
| @@ -2,11 +2,11 @@ | ||||
| <div id="bgpmap"> | ||||
| </div> | ||||
|  | ||||
| <script src="https://cdn.jsdelivr.net/npm/viz.js@2.1.2/viz.min.js" crossorigin="anonymous"></script> | ||||
| <script src="https://cdn.jsdelivr.net/npm/viz.js@2.1.2/lite.render.js" crossorigin="anonymous"></script> | ||||
| <script src="/static/jsdelivr/npm/viz.js@2.1.2/viz.min.js" crossorigin="anonymous"></script> | ||||
| <script src="/static/jsdelivr/npm/viz.js@2.1.2/lite.render.js" crossorigin="anonymous"></script> | ||||
| <script> | ||||
|   var viz = new Viz(); | ||||
|   viz.renderSVGElement(`{{ .Result }}`) | ||||
|   viz.renderSVGElement(atob({{ .Result }})) | ||||
|   .then(element => { | ||||
|     document.getElementById("bgpmap").appendChild(element); | ||||
|   }) | ||||
| @@ -1,18 +1,19 @@ | ||||
| <!DOCTYPE html> | ||||
| <html lang="en-US"> | ||||
| <head> | ||||
| <link rel="icon" href="/favicon.ico" type="image/x-icon" /> | ||||
| <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"> | ||||
| <meta http-equiv="X-UA-Compatible" content="IE=edge"> | ||||
| <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> | ||||
| <meta name="renderer" content="webkit"> | ||||
| <title>{{ html .Title }}</title> | ||||
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.1/dist/css/bootstrap.min.css" integrity="sha256-VoFZSlmyTXsegReQCNmbXrS4hBBUl/cexZvPmPWoJsY=" crossorigin="anonymous"> | ||||
| <link rel="stylesheet" href="/static/jsdelivr/npm/bootstrap@4.5.1/dist/css/bootstrap.min.css" integrity="sha256-VoFZSlmyTXsegReQCNmbXrS4hBBUl/cexZvPmPWoJsY=" crossorigin="anonymous"> | ||||
| <meta name="robots" content="noindex, nofollow"> | ||||
| </head> | ||||
| <body> | ||||
|  | ||||
| <nav class="navbar navbar-expand-lg navbar-light bg-light"> | ||||
| 	<a class="navbar-brand" href="/">{{ .Brand }}</a> | ||||
| 	<a class="navbar-brand" href="{{ .BrandURL }}">{{ .Brand }}</a> | ||||
| 	<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> | ||||
| 		<span class="navbar-toggler-icon"></span> | ||||
| 	</button> | ||||
| @@ -28,13 +29,24 @@ | ||||
| 		{{ end }} | ||||
| 		<ul class="navbar-nav mr-auto"> | ||||
| 			<li class="nav-item"> | ||||
| 				{{ if eq .AllServersURLCustom "all" }} | ||||
| 				<a class="nav-link{{ if .AllServersLinkActive }} active{{ end }}" | ||||
| 					href="/{{ $option }}/{{ .AllServersURL }}/{{ $target }}"> All Servers </a> | ||||
| 					href="/{{ $option }}/{{ .AllServersURL }}/{{ $target }}"> {{ .AllServerTitle }} </a> | ||||
| 				{{ else }} | ||||
| 				<a class="nav-link active" | ||||
| 					href="{{ .AllServersURLCustom }}"> {{ .AllServerTitle }} </a> | ||||
| 				{{ end }} | ||||
| 			</li> | ||||
| 			{{ range $k, $v := .ServersEscaped }} | ||||
| 			{{ $length := len .Servers }}  | ||||
| 			{{ range $k, $v := .Servers }} | ||||
| 			<li class="nav-item"> | ||||
| 				{{ if gt $length 1 }} | ||||
| 				<a class="nav-link{{ if eq $server $v }} active{{ end }}" | ||||
| 					href="/{{ $option }}/{{ $v }}/{{ $target }}">{{ html (index $.ServersDisplay $k) }}</a> | ||||
| 				{{ else }} | ||||
| 				<a class="nav-link{{ if eq $server $v }} active{{ end }}" | ||||
| 					href="/">{{ html (index $.ServersDisplay $k) }}</a> | ||||
| 				{{ end }} | ||||
| 			</li> | ||||
| 			{{ end }} | ||||
| 		</ul> | ||||
| @@ -48,7 +60,7 @@ | ||||
| 					<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="server" class="d-none" value="{{ html ($server | pathescape) }}"> | ||||
| 				<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">»</button> | ||||
| @@ -62,8 +74,9 @@ | ||||
| 	{{ .Content }} | ||||
| </div> | ||||
|  | ||||
| <script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script> | ||||
| <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.1/dist/js/bootstrap.min.js" integrity="sha256-0IiaoZCI++9oAAvmCb5Y0r93XkuhvJpRalZLffQXLok=" crossorigin="anonymous"></script> | ||||
| <script src="/static/jsdelivr/npm/jquery@3.5.1/dist/jquery.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script> | ||||
| <script src="/static/jsdelivr/npm/bootstrap@4.5.1/dist/js/bootstrap.min.js" integrity="sha256-0IiaoZCI++9oAAvmCb5Y0r93XkuhvJpRalZLffQXLok=" crossorigin="anonymous"></script> | ||||
| <script src="/static/sortTable.js"></script> | ||||
|  | ||||
| <script> | ||||
| function goto() { | ||||
| @@ -1,6 +1,6 @@ | ||||
| {{ $ServerName := urlquery .ServerName }} | ||||
|  | ||||
| <table class="table table-striped table-bordered table-sm"> | ||||
| <table class="table table-striped table-bordered table-sm sortable"> | ||||
|   <thead> | ||||
| {{ range .Header }} | ||||
|     <th scope="col">{{ html . }}</th> | ||||
| @@ -1,122 +1,121 @@ | ||||
| package main | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"html" | ||||
| 	"net" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| func getASNRepresentation(asn string) string { | ||||
| 	records, err := net.LookupTXT(fmt.Sprintf("AS%s.%s", asn, setting.dnsInterface)) | ||||
| 	if err != nil { | ||||
| 		// DNS query failed, only use ASN as output | ||||
| 		return fmt.Sprintf("AS%s", asn) | ||||
| // The protocol name for each route (e.g. "ibgp_sea02") is encoded in the form: | ||||
| // | ||||
| //	unicast [ibgp_sea02 2021-08-27 from fd86:bad:11b7:1::1] * (100/1015) [i] | ||||
| var protocolNameRe = regexp.MustCompile(`\[(.*?) .*\]`) | ||||
|  | ||||
| // Try to split the output into one chunk for each route. | ||||
| // Possible values are defined at https://gitlab.nic.cz/labs/bird/-/blob/v2.0.8/nest/rt-attr.c#L81-87 | ||||
| var routeSplitRe = regexp.MustCompile("(unicast|blackhole|unreachable|prohibited)") | ||||
|  | ||||
| var routeViaRe = regexp.MustCompile(`(?m)^\t(via .*?)$`) | ||||
| var routeASPathRe = regexp.MustCompile(`(?m)^\tBGP\.as_path: (.*?)$`) | ||||
|  | ||||
| func makeEdgeAttrs(preferred bool) RouteAttrs { | ||||
| 	result := RouteAttrs{ | ||||
| 		"fontsize": "12.0", | ||||
| 	} | ||||
| 	if preferred { | ||||
| 		result["color"] = "red" | ||||
| 	} | ||||
| 	return result | ||||
| } | ||||
|  | ||||
| 	result := strings.Join(records, " ") | ||||
| 	if resultSplit := strings.Split(result, " | "); len(resultSplit) > 1 { | ||||
| 		result = strings.Join(resultSplit[1:], "\\n") | ||||
| func makePointAttrs(preferred bool) RouteAttrs { | ||||
| 	result := RouteAttrs{} | ||||
| 	if preferred { | ||||
| 		result["color"] = "red" | ||||
| 	} | ||||
| 	return fmt.Sprintf("AS%s\\n%s", asn, result) | ||||
| 	return result | ||||
| } | ||||
|  | ||||
| func birdRouteToGraphviz(servers []string, responses []string, target string) string { | ||||
| 	graph := make(map[string]string) | ||||
| 	// Helper to add an edge | ||||
| 	addEdge := func(src string, dest string, attr string) { | ||||
| 		key := "\"" + html.EscapeString(src) + "\" -> \"" + html.EscapeString(dest) + "\"" | ||||
| 		_, present := graph[key] | ||||
| 		// Do not remove edge's attributes if it's already present | ||||
| 		if present && len(attr) == 0 { | ||||
| 			return | ||||
| 		} | ||||
| 		graph[key] = attr | ||||
| 	} | ||||
| 	// Helper to set attribute for a point in graph | ||||
| 	addPoint := func(name string, attr string) { | ||||
| 		key := "\"" + html.EscapeString(name) + "\"" | ||||
| 		_, present := graph[key] | ||||
| 		// Do not remove point's attributes if it's already present | ||||
| 		if present && len(attr) == 0 { | ||||
| 			return | ||||
| 		} | ||||
| 		graph[key] = attr | ||||
| 	} | ||||
| func birdRouteToGraph(servers []string, responses []string, target string) RouteGraph { | ||||
| 	graph := makeRouteGraph() | ||||
|  | ||||
| 	graph.AddPoint(target, false, RouteAttrs{"color": "red", "shape": "diamond"}) | ||||
|  | ||||
| 	addPoint("Target: "+target, "[color=red,shape=diamond]") | ||||
| 	for serverID, server := range servers { | ||||
| 		response := responses[serverID] | ||||
| 		if len(response) == 0 { | ||||
| 			continue | ||||
| 		} | ||||
| 		addPoint(server, "[color=blue,shape=box]") | ||||
| 		// This is the best split point I can find for bird2 | ||||
| 		routes := strings.Split(response, "\tvia ") | ||||
| 		routeFound := false | ||||
| 		graph.AddPoint(server, false, RouteAttrs{"color": "blue", "shape": "box"}) | ||||
| 		routes := routeSplitRe.Split(response, -1) | ||||
|  | ||||
| 		for routeIndex, route := range routes { | ||||
| 			var routeNexthop string | ||||
| 			var routeASPath string | ||||
| 			var routePreferred bool = routeIndex > 0 && strings.Contains(routes[routeIndex-1], "*") | ||||
| 			// Have to look at previous slice to determine if route is preferred, due to bad split point selection | ||||
|  | ||||
| 			for _, routeParameter := range strings.Split(route, "\n") { | ||||
| 				if strings.HasPrefix(routeParameter, "\tBGP.next_hop: ") { | ||||
| 					routeNexthop = strings.TrimPrefix(routeParameter, "\tBGP.next_hop: ") | ||||
| 				} else if strings.HasPrefix(routeParameter, "\tBGP.as_path: ") { | ||||
| 					routeASPath = strings.TrimPrefix(routeParameter, "\tBGP.as_path: ") | ||||
| 				} | ||||
| 			} | ||||
| 			if len(routeASPath) == 0 { | ||||
| 				// Either this is not a BGP route, or the information is incomplete | ||||
| 			if routeIndex == 0 { | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			// Connect each node on AS path | ||||
| 			paths := strings.Split(strings.TrimSpace(routeASPath), " ") | ||||
| 			var via string | ||||
| 			var paths []string | ||||
| 			var routePreferred bool = strings.Contains(route, "*") | ||||
| 			// Track non-BGP routes in the output by their protocol name, but draw them altogether in one line | ||||
| 			// so that there are no conflicts in the edge label | ||||
| 			var protocolName string | ||||
|  | ||||
| 			for pathIndex := range paths { | ||||
| 				paths[pathIndex] = strings.TrimPrefix(paths[pathIndex], "(") | ||||
| 				paths[pathIndex] = strings.TrimSuffix(paths[pathIndex], ")") | ||||
| 			if match := routeViaRe.FindStringSubmatch(route); len(match) >= 2 { | ||||
| 				via = strings.TrimSpace(match[1]) | ||||
| 			} | ||||
|  | ||||
| 			// First step starting from originating server | ||||
| 			if len(paths) > 0 { | ||||
| 				if len(routeNexthop) > 0 { | ||||
| 					// Edge from originating server to nexthop | ||||
| 					addEdge(server, "Nexthop:\\n"+routeNexthop, (map[bool]string{true: "[color=red]"})[routePreferred]) | ||||
| 					// and from nexthop to AS | ||||
| 					addEdge("Nexthop:\\n"+routeNexthop, getASNRepresentation(paths[0]), (map[bool]string{true: "[color=red]"})[routePreferred]) | ||||
| 					addPoint("Nexthop:\\n"+routeNexthop, "[shape=diamond]") | ||||
| 					routeFound = true | ||||
| 			if match := routeASPathRe.FindStringSubmatch(route); len(match) >= 2 { | ||||
| 				pathString := strings.TrimSpace(match[1]) | ||||
| 				if len(pathString) > 0 { | ||||
| 					paths = strings.Split(strings.TrimSpace(match[1]), " ") | ||||
| 					for i := range paths { | ||||
| 						paths[i] = strings.TrimPrefix(paths[i], "(") | ||||
| 						paths[i] = strings.TrimSuffix(paths[i], ")") | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			if match := protocolNameRe.FindStringSubmatch(route); len(match) >= 2 { | ||||
| 				protocolName = strings.TrimSpace(match[1]) | ||||
| 				if routePreferred { | ||||
| 					protocolName = protocolName + "*" | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			if len(paths) == 0 { | ||||
| 				graph.AddEdge(server, target, strings.TrimSpace(protocolName+"\n"+via), makeEdgeAttrs(routePreferred)) | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			// Edges between AS | ||||
| 			for i := range paths { | ||||
| 				var src string | ||||
| 				var label string | ||||
| 				// Only show nexthop information on the first hop | ||||
| 				if i == 0 { | ||||
| 					src = server | ||||
| 					label = strings.TrimSpace(protocolName + "\n" + via) | ||||
| 				} else { | ||||
| 					// Edge from originating server to AS | ||||
| 					addEdge(server, getASNRepresentation(paths[0]), (map[bool]string{true: "[color=red]"})[routePreferred]) | ||||
| 					routeFound = true | ||||
| 					src = paths[i-1] | ||||
| 					label = "" | ||||
| 				} | ||||
| 				dst := paths[i] | ||||
|  | ||||
| 				graph.AddEdge(src, dst, label, makeEdgeAttrs(routePreferred)) | ||||
| 				// Only set color for next step, origin color is set to blue above | ||||
| 				graph.AddPoint(dst, true, makePointAttrs(routePreferred)) | ||||
| 			} | ||||
|  | ||||
| 			// Following steps, edges between AS | ||||
| 			for pathIndex := range paths { | ||||
| 				if pathIndex == 0 { | ||||
| 					continue | ||||
| 				} | ||||
| 				addEdge(getASNRepresentation(paths[pathIndex-1]), getASNRepresentation(paths[pathIndex]), (map[bool]string{true: "[color=red]"})[routePreferred]) | ||||
| 			} | ||||
| 			// Last AS to destination | ||||
| 			addEdge(getASNRepresentation(paths[len(paths)-1]), "Target: "+target, (map[bool]string{true: "[color=red]"})[routePreferred]) | ||||
| 		} | ||||
|  | ||||
| 		if !routeFound { | ||||
| 			// Cannot find a path starting from this server | ||||
| 			addEdge(server, "Target: "+target, "[color=gray,label=\"?\"]") | ||||
| 			src := paths[len(paths)-1] | ||||
| 			graph.AddEdge(src, target, "", makeEdgeAttrs(routePreferred)) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Combine all graphviz commands | ||||
| 	var result string | ||||
| 	for edge, attr := range graph { | ||||
| 		result += edge + " " + attr + ";\n" | ||||
| 	return graph | ||||
| } | ||||
| 	return "digraph {\n" + result + "}\n" | ||||
|  | ||||
| func birdRouteToGraphviz(servers []string, responses []string, targetName string) string { | ||||
| 	graph := birdRouteToGraph(servers, responses, targetName) | ||||
| 	return graph.ToGraphviz() | ||||
| } | ||||
|   | ||||
							
								
								
									
										173
									
								
								frontend/bgpmap_graph.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										173
									
								
								frontend/bgpmap_graph.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,173 @@ | ||||
| package main | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| type RouteAttrs map[string]string | ||||
|  | ||||
| type RoutePoint struct { | ||||
| 	performLookup bool | ||||
| 	attrs         RouteAttrs | ||||
| } | ||||
|  | ||||
| type RouteEdgeKey struct { | ||||
| 	src  string | ||||
| 	dest string | ||||
| } | ||||
|  | ||||
| type RouteEdgeValue struct { | ||||
| 	label []string | ||||
| 	attrs RouteAttrs | ||||
| } | ||||
|  | ||||
| type RouteGraph struct { | ||||
| 	points map[string]RoutePoint | ||||
| 	edges  map[RouteEdgeKey]RouteEdgeValue | ||||
| } | ||||
|  | ||||
| func makeRouteGraph() RouteGraph { | ||||
| 	return RouteGraph{ | ||||
| 		points: make(map[string]RoutePoint), | ||||
| 		edges:  make(map[RouteEdgeKey]RouteEdgeValue), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func makeRoutePoint() RoutePoint { | ||||
| 	return RoutePoint{ | ||||
| 		performLookup: false, | ||||
| 		attrs:         make(RouteAttrs), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func makeRouteEdgeValue() RouteEdgeValue { | ||||
| 	return RouteEdgeValue{ | ||||
| 		label: []string{}, | ||||
| 		attrs: make(RouteAttrs), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (graph *RouteGraph) attrsToString(attrs RouteAttrs) string { | ||||
| 	if len(attrs) == 0 { | ||||
| 		return "" | ||||
| 	} | ||||
|  | ||||
| 	result := "" | ||||
| 	isFirst := true | ||||
| 	for k, v := range attrs { | ||||
| 		if isFirst { | ||||
| 			isFirst = false | ||||
| 		} else { | ||||
| 			result += "," | ||||
| 		} | ||||
| 		result += graph.escape(k) + "=" + graph.escape(v) + "" | ||||
| 	} | ||||
|  | ||||
| 	return "[" + result + "]" | ||||
| } | ||||
|  | ||||
| func (graph *RouteGraph) escape(s string) string { | ||||
| 	result, err := json.Marshal(s) | ||||
| 	if err != nil { | ||||
| 		return err.Error() | ||||
| 	} else { | ||||
| 		return string(result) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (graph *RouteGraph) AddEdge(src string, dest string, label string, attrs RouteAttrs) { | ||||
| 	// Add edges with same src/dest separately, multiple edges with same src/dest could exist | ||||
| 	edge := RouteEdgeKey{ | ||||
| 		src:  src, | ||||
| 		dest: dest, | ||||
| 	} | ||||
|  | ||||
| 	newValue, exists := graph.edges[edge] | ||||
| 	if !exists { | ||||
| 		newValue = makeRouteEdgeValue() | ||||
| 	} | ||||
|  | ||||
| 	if len(label) != 0 { | ||||
| 		newValue.label = append(newValue.label, label) | ||||
| 	} | ||||
| 	for k, v := range attrs { | ||||
| 		newValue.attrs[k] = v | ||||
| 	} | ||||
|  | ||||
| 	graph.edges[edge] = newValue | ||||
| } | ||||
|  | ||||
| func (graph *RouteGraph) AddPoint(name string, performLookup bool, attrs RouteAttrs) { | ||||
| 	newValue, exists := graph.points[name] | ||||
| 	if !exists { | ||||
| 		newValue = makeRoutePoint() | ||||
| 	} | ||||
|  | ||||
| 	newValue.performLookup = performLookup | ||||
| 	for k, v := range attrs { | ||||
| 		newValue.attrs[k] = v | ||||
| 	} | ||||
|  | ||||
| 	graph.points[name] = newValue | ||||
| } | ||||
|  | ||||
| func (graph *RouteGraph) GetEdge(src string, dest string) *RouteEdgeValue { | ||||
| 	key := RouteEdgeKey{ | ||||
| 		src:  src, | ||||
| 		dest: dest, | ||||
| 	} | ||||
| 	value, ok := graph.edges[key] | ||||
| 	if ok { | ||||
| 		return &value | ||||
| 	} else { | ||||
| 		return nil | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (graph *RouteGraph) GetPoint(name string) *RoutePoint { | ||||
| 	value, ok := graph.points[name] | ||||
| 	if ok { | ||||
| 		return &value | ||||
| 	} else { | ||||
| 		return nil | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (graph *RouteGraph) ToGraphviz() string { | ||||
| 	var result string | ||||
|  | ||||
| 	asnCache := make(ASNCache) | ||||
|  | ||||
| 	for name, value := range graph.points { | ||||
| 		var representation string | ||||
|  | ||||
| 		if value.performLookup { | ||||
| 			representation = asnCache.Lookup(name) | ||||
| 		} else { | ||||
| 			representation = name | ||||
| 		} | ||||
|  | ||||
| 		attrsCopy := value.attrs | ||||
| 		if attrsCopy == nil { | ||||
| 			attrsCopy = make(RouteAttrs) | ||||
| 		} | ||||
| 		attrsCopy["label"] = representation | ||||
|  | ||||
| 		result += fmt.Sprintf("%s %s;\n", graph.escape(name), graph.attrsToString(value.attrs)) | ||||
| 	} | ||||
|  | ||||
| 	for key, value := range graph.edges { | ||||
| 		attrsCopy := value.attrs | ||||
| 		if attrsCopy == nil { | ||||
| 			attrsCopy = make(RouteAttrs) | ||||
| 		} | ||||
| 		if len(value.label) > 0 { | ||||
| 			attrsCopy["label"] = strings.Join(value.label, "\n") | ||||
| 		} | ||||
| 		result += fmt.Sprintf("%s -> %s %s;\n", graph.escape(key.src), graph.escape(key.dest), graph.attrsToString(attrsCopy)) | ||||
| 	} | ||||
|  | ||||
| 	return "digraph {\n" + result + "}\n" | ||||
| } | ||||
| @@ -1,58 +1,23 @@ | ||||
| package main | ||||
|  | ||||
| import ( | ||||
| 	"io/ioutil" | ||||
| 	"path" | ||||
| 	"runtime" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
| ) | ||||
|  | ||||
| func TestGetASNRepresentation(t *testing.T) { | ||||
| 	setting.dnsInterface = "asn.cymru.com" | ||||
| 	result := getASNRepresentation("6939") | ||||
| 	if !strings.Contains(result, "HURRICANE") { | ||||
| 		t.Errorf("Lookup AS6939 failed, got %s", result) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestGetASNRepresentationFallback(t *testing.T) { | ||||
| 	setting.dnsInterface = "" | ||||
| 	result := getASNRepresentation("6939") | ||||
| 	if result != "AS6939" { | ||||
| 		t.Errorf("Lookup AS6939 failed, got %s", result) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestBirdRouteToGraphviz(t *testing.T) { | ||||
| 	setting.dnsInterface = "" | ||||
|  | ||||
| 	// Don't change formatting of the following strings! | ||||
|  | ||||
| 	fakeResult := `192.168.0.1/32       unicast [alpha 2021-01-14 from 192.168.0.2] * (100) [AS12345i] | ||||
| 	via 192.168.0.2 on eth0 | ||||
| 	Type: BGP univ | ||||
| 	BGP.origin: IGP | ||||
| 	BGP.as_path: 4242422601 | ||||
| 	BGP.next_hop: 172.18.0.2` | ||||
|  | ||||
| 	expectedResult := `digraph { | ||||
| "Nexthop:\n172.18.0.2" -> "AS4242422601" [color=red]; | ||||
| "Nexthop:\n172.18.0.2" [shape=diamond]; | ||||
| "AS4242422601" -> "Target: 192.168.0.1" [color=red]; | ||||
| "Target: 192.168.0.1" [color=red,shape=diamond]; | ||||
| "alpha" [color=blue,shape=box]; | ||||
| "alpha" -> "Nexthop:\n172.18.0.2" [color=red]; | ||||
| }` | ||||
|  | ||||
| 	result := birdRouteToGraphviz([]string{ | ||||
| 		"alpha", | ||||
| 	}, []string{ | ||||
| 		fakeResult, | ||||
| 	}, "192.168.0.1") | ||||
|  | ||||
| 	for _, line := range strings.Split(result, "\n") { | ||||
| 		if !strings.Contains(expectedResult, line) { | ||||
| 			t.Errorf("Unexpected line in result: %s", line) | ||||
| 		} | ||||
| func readDataFile(t *testing.T, filename string) string { | ||||
| 	_, sourceName, _, _ := runtime.Caller(0) | ||||
| 	projectRoot := path.Join(path.Dir(sourceName), "..") | ||||
| 	dir := path.Join(projectRoot, filename) | ||||
|  | ||||
| 	data, err := ioutil.ReadFile(dir) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	return string(data) | ||||
| } | ||||
|  | ||||
| func TestBirdRouteToGraphvizXSS(t *testing.T) { | ||||
| @@ -72,3 +37,48 @@ func TestBirdRouteToGraphvizXSS(t *testing.T) { | ||||
| 		t.Errorf("XSS injection succeeded: %s", result) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestBirdRouteToGraph(t *testing.T) { | ||||
| 	setting.dnsInterface = "" | ||||
|  | ||||
| 	input := readDataFile(t, "frontend/test_data/bgpmap_case1.txt") | ||||
| 	result := birdRouteToGraph([]string{"node"}, []string{input}, "target") | ||||
|  | ||||
| 	// Source node must exist | ||||
| 	if result.GetPoint("node") == nil { | ||||
| 		t.Error("Result doesn't contain point node") | ||||
| 	} | ||||
| 	// Last hop must exist | ||||
| 	if result.GetPoint("4242423914") == nil { | ||||
| 		t.Error("Result doesn't contain point 4242423914") | ||||
| 	} | ||||
| 	// Destination must exist | ||||
| 	if result.GetPoint("target") == nil { | ||||
| 		t.Error("Result doesn't contain point target") | ||||
| 	} | ||||
|  | ||||
| 	// Verify that a few paths exist | ||||
| 	if result.GetEdge("node", "4242423914") == nil { | ||||
| 		t.Error("Result doesn't contain edge from node to 4242423914") | ||||
| 	} | ||||
| 	if result.GetEdge("node", "4242422688") == nil { | ||||
| 		t.Error("Result doesn't contain edge from node to 4242422688") | ||||
| 	} | ||||
| 	if result.GetEdge("4242422688", "4242423914") == nil { | ||||
| 		t.Error("Result doesn't contain edge from 4242422688 to 4242423914") | ||||
| 	} | ||||
| 	if result.GetEdge("4242423914", "target") == nil { | ||||
| 		t.Error("Result doesn't contain edge from 4242423914 to target") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestBirdRouteToGraphviz(t *testing.T) { | ||||
| 	setting.dnsInterface = "" | ||||
|  | ||||
| 	input := readDataFile(t, "frontend/test_data/bgpmap_case1.txt") | ||||
| 	result := birdRouteToGraphviz([]string{"node"}, []string{input}, "target") | ||||
|  | ||||
| 	if !strings.Contains(result, "digraph {") { | ||||
| 		t.Error("Response is not Graphviz data") | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -49,3 +49,53 @@ func dn42WhoisFilter(whois string) string { | ||||
| 		return commandResult | ||||
| 	} | ||||
| } | ||||
|  | ||||
| /* experimental, behavior may change */ | ||||
| func shortenWhoisFilter(whois string) string { | ||||
| 	commandResult := "" | ||||
| 	commandResultLonger := "" | ||||
| 	lines := 0 | ||||
| 	linesLonger := 0 | ||||
| 	skippedLines := 0 | ||||
| 	skippedLinesLonger := 0 | ||||
|  | ||||
| 	for _, s := range strings.Split(whois, "\n") { | ||||
| 		s = strings.TrimSpace(s) | ||||
|  | ||||
| 		shouldSkip := false | ||||
| 		shouldSkip = shouldSkip || len(s) == 0 | ||||
| 		shouldSkip = shouldSkip || len(s) > 0 && s[0] == '#' | ||||
| 		shouldSkip = shouldSkip || strings.Contains(strings.ToUpper(s), "REDACTED") | ||||
|  | ||||
| 		if shouldSkip { | ||||
| 			skippedLinesLonger++ | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		commandResultLonger += s + "\n" | ||||
| 		linesLonger++ | ||||
|  | ||||
| 		shouldSkip = shouldSkip || len(s) > 80 | ||||
| 		shouldSkip = shouldSkip || !strings.Contains(s, ":") | ||||
| 		shouldSkip = shouldSkip || strings.Index(s, ":") > 20 | ||||
|  | ||||
| 		if shouldSkip { | ||||
| 			skippedLines++ | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		commandResult += s + "\n" | ||||
| 		lines++ | ||||
| 	} | ||||
|  | ||||
| 	if lines < 5 { | ||||
| 		commandResult = commandResultLonger | ||||
| 		skippedLines = skippedLinesLonger | ||||
| 	} | ||||
|  | ||||
| 	if skippedLines > 0 { | ||||
| 		return commandResult + fmt.Sprintf("\n%d line(s) skipped.\n", skippedLines) | ||||
| 	} else { | ||||
| 		return commandResult | ||||
| 	} | ||||
| } | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user