├── package ├── .keep ├── stayrtr.env ├── Dockerfile.release ├── after-install-debian.sh ├── Dockerfile └── stayrtr.service ├── prefixfile ├── test.rpki.json ├── slurm.json ├── prefixfile.go ├── slurm.go └── slurm_test.go ├── stayrtr.gif ├── ossec ├── constrain.go └── constrain_openbsd.go ├── docker-compose-pkg.yml ├── cmd ├── stayrtr │ ├── test.slurm.json │ ├── smalltest.rpki.json │ ├── test.rpki.json │ └── stayrtr_test.go ├── rtrmon │ ├── index.html.tmpl │ ├── rtrmon_test.go │ └── rtrmon.go └── rtrdump │ └── rtrdump.go ├── .github └── workflows │ ├── go-tag.yml │ ├── codeql.yml │ ├── go.yml │ ├── docker-tag.yml │ └── docker.yml ├── .gitignore ├── docker-compose-integration.yml ├── CONTRIBUTING ├── go.mod ├── Dockerfile ├── LICENSE.txt ├── lib ├── structs_test.go ├── client_test.go ├── client.go ├── server_test.go ├── structs.go └── server.go ├── Makefile ├── utils └── utils.go ├── go.sum └── README.md /package/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /package/stayrtr.env: -------------------------------------------------------------------------------- 1 | STAYRTR_ARGS= -------------------------------------------------------------------------------- /prefixfile/test.rpki.json: -------------------------------------------------------------------------------- 1 | ../cmd/stayrtr/test.rpki.json -------------------------------------------------------------------------------- /stayrtr.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgp/stayrtr/HEAD/stayrtr.gif -------------------------------------------------------------------------------- /package/Dockerfile.release: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | COPY stayrtr /usr/bin/stayrtr 3 | ENTRYPOINT ["/usr/bin/stayrtr"] -------------------------------------------------------------------------------- /ossec/constrain.go: -------------------------------------------------------------------------------- 1 | // +build !openbsd 2 | 3 | package ossec 4 | 5 | func PledgePromises(promises string) error { 6 | return nil 7 | } 8 | -------------------------------------------------------------------------------- /ossec/constrain_openbsd.go: -------------------------------------------------------------------------------- 1 | package ossec 2 | 3 | import "golang.org/x/sys/unix" 4 | 5 | func PledgePromises(promises string) error { 6 | return unix.PledgePromises(promises) 7 | } 8 | -------------------------------------------------------------------------------- /package/after-install-debian.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Debian convention is to auto start software 4 | 5 | systemctl daemon-reload 6 | systemctl enable stayrtr 7 | systemctl start stayrtr -------------------------------------------------------------------------------- /package/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby 2 | 3 | RUN apt-get update && \ 4 | apt-get install -y git make rpm golang && \ 5 | gem install fpm 6 | 7 | WORKDIR /work 8 | 9 | ENTRYPOINT [ "/bin/bash" ] -------------------------------------------------------------------------------- /docker-compose-pkg.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | packager: 4 | build: package 5 | entrypoint: make 6 | command: 7 | - build-stayrtr 8 | - package-deb-stayrtr 9 | - package-rpm-stayrtr 10 | volumes: 11 | - ./:/work/ -------------------------------------------------------------------------------- /package/stayrtr.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=StayRTR 3 | After=network.target 4 | 5 | [Service] 6 | Type=simple 7 | EnvironmentFile=/etc/default/stayrtr 8 | WorkingDirectory=/usr/share/stayrtr 9 | ExecStart=/usr/bin/stayrtr $STAYRTR_ARGS 10 | 11 | [Install] 12 | WantedBy=multi-user.target 13 | -------------------------------------------------------------------------------- /cmd/stayrtr/test.slurm.json: -------------------------------------------------------------------------------- 1 | { 2 | "slurmVersion": 1, 3 | "validationOutputFilters": { 4 | "prefixFilters": [ 5 | { 6 | "asn": 65000, 7 | "prefix": "10.0.0.0/24" 8 | } 9 | ], 10 | "bgpsecFilters": [] 11 | }, 12 | "locallyAddedAssertions": { 13 | "prefixAssertions": [ 14 | { 15 | "asn": 65002, 16 | "prefix": "10.2.0.0/25", 17 | "maxPrefixLength": 26 18 | } 19 | ] 20 | } 21 | } -------------------------------------------------------------------------------- /cmd/stayrtr/smalltest.rpki.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "buildtime": "2021-07-27T18:56:02Z", 4 | "vrps": 2 5 | }, 6 | 7 | "roas": [ 8 | { 9 | "asn": 13335, 10 | "prefix": "1.0.0.0/24", 11 | "maxLength": 24, 12 | "ta": "apnic", 13 | "expires": 1627568318 14 | }, 15 | { 16 | "asn": "AS9367", 17 | "prefix": "2001:200:136::/48", 18 | "maxLength": 48, 19 | "ta": "apnic", 20 | "expires": 1627575699 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/go-tag.yml: -------------------------------------------------------------------------------- 1 | name: Tagged Go build 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | go: 10 | name: Go 11 | uses: ./.github/workflows/go.yml 12 | 13 | release: 14 | name: Release 15 | runs-on: ubuntu-latest 16 | needs: [go] 17 | steps: 18 | - name: Download artifacts 19 | uses: actions/download-artifact@v4 20 | 21 | - name: Create release 22 | uses: softprops/action-gh-release@v2 23 | with: 24 | files: dist/* 25 | fail_on_unmatched_files: true 26 | generate_release_notes: true 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | *.pyc 6 | cmd/gobgp/gobgp 7 | cmd/gobgpd/gobgpd 8 | 9 | ### OSX ### 10 | .DS_Store 11 | .AppleDouble 12 | .LSOverride 13 | Icon 14 | 15 | # Folders 16 | _obj 17 | _test 18 | vendor 19 | dist 20 | 21 | # Architecture specific extensions/prefixes 22 | *.[568vq] 23 | [568vq].out 24 | 25 | *.cgo1.go 26 | *.cgo2.c 27 | _cgo_defun.c 28 | _cgo_gotypes.go 29 | _cgo_export.* 30 | 31 | _testmain.go 32 | 33 | test/scenario_test/nosetest*.xml 34 | 35 | *.exe 36 | *.test 37 | *.prof 38 | 39 | # IDE workspace 40 | .idea 41 | .vscode 42 | 43 | output.json -------------------------------------------------------------------------------- /docker-compose-integration.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | stayrtr: 4 | build: 5 | dockerfile: Dockerfile.stayrtr 6 | context: . 7 | command: 8 | - -cache 9 | - rpki.json 10 | - -slurm 11 | - slurm.json 12 | - -verify=false 13 | - -checktime=false 14 | volumes: 15 | - ./cmd/stayrtr/test.rpki.json:/rpki.json 16 | - ./cmd/stayrtr/test.slurm.json:/slurm.json 17 | rtrdump: 18 | build: 19 | dockerfile: Dockerfile.rtrdump 20 | context: . 21 | command: 22 | - -connect 23 | - stayrtr:8282 24 | - -file 25 | - "" 26 | depends_on: 27 | - stayrtr 28 | -------------------------------------------------------------------------------- /CONTRIBUTING: -------------------------------------------------------------------------------- 1 | Stuff is addressed as follows: 2 | 3 | -PRs with details 4 | The less code that has to be read to understand, the easier it is to review. 5 | 6 | -Issues with debugging detail 7 | Debugging detail is how stuff is troubleshot. 8 | 9 | -PRs that are complicated and not explained well 10 | Brain may not be able to process immediately. Will get back. 11 | 12 | -Issues with minimal detail but somewhat obvious 13 | Cool, but this requires work. 14 | 15 | Things that will not be looked at, and instantly closed: 16 | 17 | -Typo fixes that are not project impacting somehow. Speling doent mater muhc. 18 | -Issues with no details and no troubleshooting attempted 19 | 20 | Prohibited conduct in issues/PRs: 21 | 22 | -Being rude or obnoxious 23 | -The usual list that other CoCs care about mostly 24 | -Being annoying with useless commentary 25 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: CodeQL 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | name: Build 12 | runs-on: ubuntu-latest 13 | steps: 14 | 15 | - name: Set latest stable go version 16 | uses: actions/setup-go@v5 17 | with: 18 | go-version: 'stable' 19 | 20 | - name: Check out code into the Go module directory 21 | uses: actions/checkout@v4 22 | with: 23 | fetch-depth: '0' 24 | 25 | # Initializes the CodeQL tools for scanning. 26 | - name: Initialize CodeQL 27 | uses: github/codeql-action/init@v3 28 | with: 29 | queries: +security-and-quality 30 | languages: go 31 | 32 | - name: Autobuild 33 | uses: github/codeql-action/autobuild@v3 34 | 35 | - name: Perform CodeQL Analysis 36 | uses: github/codeql-action/analyze@v3 37 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/bgp/stayrtr 2 | 3 | go 1.24.0 4 | 5 | toolchain go1.24.3 6 | 7 | require ( 8 | github.com/google/go-cmp v0.7.0 9 | github.com/prometheus/client_golang v1.21.1 10 | github.com/sirupsen/logrus v1.9.3 11 | github.com/stretchr/testify v1.10.0 12 | golang.org/x/crypto v0.45.0 13 | golang.org/x/sync v0.18.0 14 | golang.org/x/sys v0.38.0 15 | ) 16 | 17 | require ( 18 | github.com/beorn7/perks v1.0.1 // indirect 19 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 20 | github.com/davecgh/go-spew v1.1.1 // indirect 21 | github.com/klauspost/compress v1.17.11 // indirect 22 | github.com/kr/text v0.2.0 // indirect 23 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 24 | github.com/pmezard/go-difflib v1.0.0 // indirect 25 | github.com/prometheus/client_model v0.6.1 // indirect 26 | github.com/prometheus/common v0.62.0 // indirect 27 | github.com/prometheus/procfs v0.15.1 // indirect 28 | google.golang.org/protobuf v1.36.5 // indirect 29 | gopkg.in/yaml.v3 v3.0.1 // indirect 30 | ) 31 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ############### 2 | # Build stage # 3 | ############### 4 | ARG src_dir="/stayrtr" 5 | 6 | FROM golang:alpine as builder 7 | ARG src_dir 8 | 9 | RUN apk --update --no-cache add git make && \ 10 | mkdir -p ${src_dir} 11 | 12 | WORKDIR ${src_dir} 13 | COPY . . 14 | 15 | RUN SUFFIX= make build-all 16 | 17 | ################ 18 | # Keygen stage # 19 | ################ 20 | FROM alpine:latest as keygen 21 | 22 | RUN apk --update --no-cache add openssl 23 | RUN openssl ecparam -genkey -name prime256v1 -noout -outform pem > private.pem 24 | 25 | ################# 26 | # StayRTR stage # 27 | ################# 28 | FROM alpine:latest AS stayrtr 29 | 30 | RUN apk --update --no-cache add ca-certificates && \ 31 | adduser -S -D -H -h / rtr 32 | USER rtr 33 | COPY --from=builder /stayrtr/dist/stayrtr / 34 | COPY --from=keygen /private.pem /private.pem 35 | ENTRYPOINT ["./stayrtr"] 36 | 37 | ################# 38 | # RTRdump stage # 39 | ################# 40 | FROM alpine:latest AS rtrdump 41 | 42 | RUN apk --update --no-cache add ca-certificates && \ 43 | adduser -S -D -H -h / rtr 44 | USER rtr 45 | COPY --from=builder /stayrtr/dist/rtrdump / 46 | ENTRYPOINT ["./rtrdump"] 47 | 48 | ################# 49 | # RTRmon stage # 50 | ################# 51 | FROM alpine:latest AS rtrmon 52 | 53 | RUN apk --update --no-cache add ca-certificates && \ 54 | adduser -S -D -H -h / rtr 55 | USER rtr 56 | COPY --from=builder /stayrtr/dist/rtrmon / 57 | ENTRYPOINT ["./rtrmon"] 58 | -------------------------------------------------------------------------------- /cmd/rtrmon/index.html.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | rtrmon 6 | 7 | 8 |

rtrmon

9 | 13 | 14 |

usage

15 |

diff:

16 | The /{{ .OutFile }} endpoint contains four keys: 17 | 18 |
19 |     metadata-primary: configuration of the primary source
20 |     metadata-secondary: configuration of the secondary source
21 |     only-primary: objects in the primary source but not in the secondary source.
22 |     only-secondary: objects in the secondary source but not in the primary source.
23 |     
24 | 25 |

metrics:

26 | By default the Prometheus endpoint is on http://[host]{{ .Addr }}{{ .MetricsPath }}. Among others, this endpoint contains the following metrics: 27 | 28 |
29 |     rpki_vrps: Current number of VRPS and current difference between the primary and secondary.
30 |     rtr_serial: Serial of the rtr session (when applicable).
31 |     rtr_session: Session ID of the RTR session.
32 |     rtr_state: State of the rtr session (up/down).
33 |     update: Timestamp of the last update.
34 |     vrp_diff: The number of VRPs which were seen in lhs at least visibility_seconds ago not in rhs.
35 |     
36 | 37 | 38 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021-2023, Various github.com/bgp/stayrtr contributors, including Ben Cox, Job Snijders, and Ties de Kock. All rights reserved. 2 | Copyright (c) 2018, Cloudflare. All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 13 | -------------------------------------------------------------------------------- /prefixfile/slurm.json: -------------------------------------------------------------------------------- 1 | { 2 | "slurmVersion": 2, 3 | "validationOutputFilters": { 4 | "prefixFilters": [ 5 | { 6 | "prefix": "192.0.2.0/24", 7 | "comment": "All VRPs encompassed by prefix" 8 | }, 9 | { 10 | "asn": 64496, 11 | "comment": "All VRPs matching ASN" 12 | }, 13 | { 14 | "prefix": "198.51.100.0/24", 15 | "asn": 64497, 16 | "comment": "All VRPs encompassed by prefix, matching ASN" 17 | } 18 | ], 19 | "bgpsecFilters": [ 20 | { 21 | "asn": 64496, 22 | "comment": "All keys for ASN" 23 | }, 24 | { 25 | "SKI": "XC7RBWu3661vfYmhXZwtUw==", 26 | "comment": "Key matching Router SKI" 27 | }, 28 | { 29 | "asn": 64497, 30 | "SKI": "XC7RBWu3661vfYmhXZwtUw==", 31 | "comment": "Key for ASN 64497 matching Router SKI" 32 | } 33 | ] 34 | }, 35 | "locallyAddedAssertions": { 36 | "prefixAssertions": [ 37 | { 38 | "asn": 64496, 39 | "prefix": "198.51.100.0/24", 40 | "comment": "My other important route" 41 | }, 42 | { 43 | "asn": 64496, 44 | "prefix": "2001:DB8::/32", 45 | "maxPrefixLength": 48, 46 | "comment": "My other important de-aggregated routes" 47 | } 48 | ], 49 | "bgpsecAssertions": [ 50 | { 51 | "asn": 64496, 52 | "comment": "My known key for my important ASN", 53 | "SKI": "NQYXZ0PgL2fdRscxGdVDa+fhAQY=", 54 | "routerPublicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEhv5HEBGixUjKJTlenvcD1Axyi07rFdVY1KhN4vMPYy5y0Mx6zfaiEqJN27jK/l61xC36Vsaezd7eXAsZ1AEEsQ==" 55 | } 56 | ] 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | # This workflow is re-used in go-tag.yml 9 | workflow_call: 10 | 11 | jobs: 12 | build: 13 | name: Build 14 | runs-on: ubuntu-latest 15 | steps: 16 | 17 | - name: Check out code into the Go module directory 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: '0' 21 | 22 | - name: Set up go version from go.mod 23 | uses: actions/setup-go@v5 24 | with: 25 | go-version-file: 'go.mod' 26 | 27 | - id: govulncheck 28 | uses: golang/govulncheck-action@v1 29 | with: 30 | go-version-file: 'go.mod' 31 | go-package: ./... 32 | 33 | - name: Test & Vet 34 | run: make test vet 35 | 36 | - name: Build 37 | run: | 38 | GOOS=linux make build-stayrtr 39 | GOOS=linux GOARCH=arm64 make build-stayrtr 40 | GOOS=darwin make build-stayrtr 41 | GOOS=windows EXTENSION=.exe make build-stayrtr 42 | GOOS=linux make build-rtrdump 43 | GOOS=linux GOARCH=arm64 make build-rtrdump 44 | GOOS=darwin make build-rtrdump 45 | GOOS=windows EXTENSION=.exe make build-rtrdump 46 | GOOS=linux make build-rtrmon 47 | GOOS=linux GOARCH=arm64 make build-rtrmon 48 | GOOS=darwin make build-rtrmon 49 | GOOS=windows EXTENSION=.exe make build-rtrmon 50 | 51 | - name: Install fpm 52 | run: | 53 | sudo apt-get update 54 | sudo apt-get install -y rpm ruby ruby-dev 55 | sudo gem install fpm 56 | 57 | - name: Package 58 | run: | 59 | make package-deb-stayrtr package-rpm-stayrtr 60 | GOARCH=arm64 make package-deb-stayrtr package-rpm-stayrtr 61 | 62 | - name: Upload Artifact 63 | uses: actions/upload-artifact@v4 64 | with: 65 | name: dist 66 | path: dist/* 67 | retention-days: 14 68 | -------------------------------------------------------------------------------- /.github/workflows/docker-tag.yml: -------------------------------------------------------------------------------- 1 | name: Tagged docker build 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Check Out Repo 14 | uses: actions/checkout@v4 15 | 16 | - name: Get short SHA from commit hash 17 | id: tagcalc 18 | run: echo "tagname=$(git describe --tags --abbrev=0 HEAD)" >> $GITHUB_OUTPUT 19 | 20 | - name: Login to Docker Hub 21 | uses: docker/login-action@v3 22 | with: 23 | username: ${{ secrets.DOCKER_HUB_USERNAME }} 24 | password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} 25 | 26 | - name: Set up Docker Buildx 27 | id: buildx 28 | uses: docker/setup-buildx-action@v1 29 | 30 | - name: Build and push (stayrtr) 31 | id: docker_build_stayrtr 32 | uses: docker/build-push-action@v2 33 | with: 34 | context: ./ 35 | file: ./Dockerfile 36 | push: true 37 | tags: rpki/stayrtr:latest,rpki/stayrtr:${{ steps.tagcalc.outputs.tagname }} 38 | target: stayrtr 39 | 40 | - name: Image digest (stayrtr) 41 | run: echo ${{ steps.docker_build_stayrtr.outputs.digest }} 42 | 43 | - name: Build and push (rtrmon) 44 | id: docker_build_rtrmon 45 | uses: docker/build-push-action@v2 46 | with: 47 | context: ./ 48 | file: ./Dockerfile 49 | push: true 50 | tags: rpki/rtrmon:latest,rpki/rtrmon:${{ steps.tagcalc.outputs.tagname }} 51 | target: rtrmon 52 | 53 | - name: Image digest (rtrmon) 54 | run: echo ${{ steps.docker_build_rtrmon.outputs.digest }} 55 | 56 | - name: Build and push rtrdump 57 | id: docker_build_rtrdump 58 | uses: docker/build-push-action@v2 59 | with: 60 | context: ./ 61 | file: ./Dockerfile 62 | push: true 63 | tags: rpki/rtrdump:latest,rpki/rtrdump:${{ steps.tagcalc.outputs.tagname }} 64 | target: rtrdump 65 | 66 | - name: Image digest (rtrdump) 67 | run: echo ${{ steps.docker_build_rtrdump.outputs.digest }} 68 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: CI to Docker Hub 2 | on: 3 | push: 4 | branches: [master] 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Get short SHA from commit hash 11 | id: shacalc 12 | run: echo "sha8=${GITHUB_SHA:0:8}" >> $GITHUB_OUTPUT 13 | 14 | - name: Check Out Repo 15 | uses: actions/checkout@v4 16 | 17 | - name: Login to Docker Hub 18 | uses: docker/login-action@v3 19 | with: 20 | username: ${{ secrets.DOCKER_HUB_USERNAME }} 21 | password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} 22 | 23 | - name: Set up Docker Buildx 24 | id: buildx 25 | uses: docker/setup-buildx-action@v1 26 | 27 | - name: Build and push (stayrtr) 28 | id: docker_build_stayrtr 29 | uses: docker/build-push-action@v2 30 | with: 31 | context: ./ 32 | platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8,linux/ppc64le,linux/riscv64,linux/s390x 33 | file: ./Dockerfile 34 | push: true 35 | tags: rpki/stayrtr:latest,rpki/stayrtr:${{ steps.shacalc.outputs.sha8 }} 36 | target: stayrtr 37 | 38 | - name: Image digest (stayrtr) 39 | run: echo ${{ steps.docker_build_stayrtr.outputs.digest }} 40 | 41 | - name: Build and push (rtrmon) 42 | id: docker_build_rtrmon 43 | uses: docker/build-push-action@v2 44 | with: 45 | context: ./ 46 | platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8,linux/ppc64le,linux/riscv64,linux/s390x 47 | file: ./Dockerfile 48 | push: true 49 | tags: rpki/rtrmon:latest,rpki/rtrmon:${{ steps.shacalc.outputs.sha8 }} 50 | target: rtrmon 51 | 52 | - name: Image digest (rtrmon) 53 | run: echo ${{ steps.docker_build_rtrmon.outputs.digest }} 54 | 55 | - name: Build and push rtrdump 56 | id: docker_build_rtrdump 57 | uses: docker/build-push-action@v2 58 | with: 59 | context: ./ 60 | platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8,linux/ppc64le,linux/riscv64,linux/s390x 61 | file: ./Dockerfile 62 | push: true 63 | tags: rpki/rtrdump:latest,rpki/rtrdump:${{ steps.shacalc.outputs.sha8 }} 64 | target: rtrdump 65 | 66 | - name: Image digest (rtrdump) 67 | run: echo ${{ steps.docker_build_rtrdump.outputs.digest }} 68 | -------------------------------------------------------------------------------- /lib/structs_test.go: -------------------------------------------------------------------------------- 1 | package rtrlib 2 | 3 | import ( 4 | "runtime" 5 | "testing" 6 | "unsafe" 7 | 8 | "github.com/google/go-cmp/cmp" 9 | ) 10 | 11 | func TestPDUPrefixStructSize(t *testing.T) { 12 | if a := runtime.GOARCH; a != "amd64" { 13 | t.Skipf("skipping, running on %s but this test is hard-coded for amd64 architecture", a) 14 | } 15 | 16 | // This test verifies that the size of PDUIPv{4,6}Prefix and its component structures 17 | // do not change unexpectedly due to other code modifications. 18 | // 19 | // For reference, the tool structlayout can be used to examine struct sizes 20 | // and structlayout-optimize to recommend ordering of members to minimize memory utilization. 21 | // Whenever a constant is changed here, be sure to update the associated 22 | // comment with the output of the tools. 23 | // 24 | // $ go install honnef.co/go/tools/cmd/structlayout@latest 25 | // $ go install honnef.co/go/tools/cmd/structlayout-optimize@latest 26 | 27 | const ( 28 | // $ structlayout . PDUIPv4Prefix 29 | // PDUIPv4Prefix.Prefix.ip.addr.hi uint64: 0-8 (size 8, align 8) 30 | // PDUIPv4Prefix.Prefix.ip.addr.lo uint64: 8-16 (size 8, align 8) 31 | // PDUIPv4Prefix.Prefix.ip.z *internal/intern.Value: 16-24 (size 8, align 8) 32 | // PDUIPv4Prefix.Prefix.bitsPlusOne uint8: 24-25 (size 1, align 1) 33 | // padding: 25-32 (size 7, align 0) 34 | // PDUIPv4Prefix.ASN uint32: 32-36 (size 4, align 4) 35 | // PDUIPv4Prefix.Version uint8: 36-37 (size 1, align 1) 36 | // PDUIPv4Prefix.MaxLen uint8: 37-38 (size 1, align 1) 37 | // PDUIPv4Prefix.Flags uint8: 38-39 (size 1, align 1) 38 | // padding: 39-40 (size 1, align 0) 39 | // 40 | // $ structlayout . PDUIPv6Prefix 41 | // PDUIPv6Prefix.Prefix.ip.addr.hi uint64: 0-8 (size 8, align 8) 42 | // PDUIPv6Prefix.Prefix.ip.addr.lo uint64: 8-16 (size 8, align 8) 43 | // PDUIPv6Prefix.Prefix.ip.z *internal/intern.Value: 16-24 (size 8, align 8) 44 | // PDUIPv6Prefix.Prefix.bitsPlusOne uint8: 24-25 (size 1, align 1) 45 | // padding: 25-32 (size 7, align 0) 46 | // PDUIPv6Prefix.ASN uint32: 32-36 (size 4, align 4) 47 | // PDUIPv6Prefix.Version uint8: 36-37 (size 1, align 1) 48 | // PDUIPv6Prefix.MaxLen uint8: 37-38 (size 1, align 1) 49 | // PDUIPv6Prefix.Flags uint8: 38-39 (size 1, align 1) 50 | // padding: 39-40 (size 1, align 0) 51 | pduIPv4PrefixSize = 40 52 | pduIPv6PrefixSize = 40 53 | ) 54 | 55 | if diff := cmp.Diff(int(unsafe.Sizeof(PDUIPv4Prefix{})), pduIPv4PrefixSize); diff != "" { 56 | t.Fatalf("unexpected PDUIPv4Prefix struct size (-want +got):\n%s", diff) 57 | } 58 | if diff := cmp.Diff(int(unsafe.Sizeof(PDUIPv6Prefix{})), pduIPv6PrefixSize); diff != "" { 59 | t.Fatalf("unexpected PDUIPv6Prefix struct size (-want +got):\n%s", diff) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /prefixfile/prefixfile.go: -------------------------------------------------------------------------------- 1 | package prefixfile 2 | 3 | import ( 4 | "fmt" 5 | "net/netip" 6 | "strconv" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | type RPKIList struct { 12 | Metadata MetaData `json:"metadata,omitempty"` 13 | ROA []VRPJson `json:"roas"` // for historical reasons this is called 'roas', but should've been called vrps 14 | BgpSecKeys []BgpSecKeyJson `json:"bgpsec_keys,omitempty"` 15 | } 16 | 17 | type MetaData struct { 18 | Counts int `json:"vrps"` 19 | CountBgpSecKeys int `json:"bgpsec_pubkeys"` 20 | Buildtime string `json:"buildtime,omitempty"` 21 | GeneratedUnix *int64 `json:"generated,omitempty"` 22 | SessionID int `json:"sessionid,omitempty"` 23 | Serial int `json:"serial"` 24 | } 25 | 26 | type VRPJson struct { 27 | Prefix string `json:"prefix"` 28 | Length uint8 `json:"maxLength"` 29 | ASN interface{} `json:"asn"` 30 | TA string `json:"ta,omitempty"` 31 | Expires *int64 `json:"expires,omitempty"` 32 | } 33 | 34 | type BgpSecKeyJson struct { 35 | Asn uint32 `json:"asn"` 36 | Expires *int64 `json:"expires,omitempty"` 37 | Ta string `json:"ta,omitempty"` 38 | 39 | // Base64 encoded, but encoding/json handles this for us 40 | // Example: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE4FxJr0n2bux1uX1Evl+QWwZYvIadPjLuFX2mxqKuAGUhKnr7VLLDgrE++l9p5eH2kWTNVAN22FUU3db/RKpE2w== 41 | Pubkey []byte `json:"pubkey"` 42 | // Base16 encoded, we need to decode this ourself 43 | // Example: 510F485D29A29DB7B515F9C478F8ED3CB7AA7D23 44 | Ski string `json:"ski"` 45 | } 46 | 47 | func (md MetaData) GetBuildTime() time.Time { 48 | bt, err := time.Parse(time.RFC3339, md.Buildtime) 49 | if err != nil { 50 | return time.Time{} 51 | } 52 | return bt 53 | } 54 | 55 | func (vrp *VRPJson) GetASN2() (uint32, error) { 56 | switch asnc := vrp.ASN.(type) { 57 | case string: 58 | asnStr := strings.TrimLeft(asnc, "aAsS") 59 | asnInt, err := strconv.ParseUint(asnStr, 10, 32) 60 | if err != nil { 61 | return 0, fmt.Errorf("could not decode ASN string: %v", vrp.ASN) 62 | } 63 | asn := uint32(asnInt) 64 | return asn, nil 65 | case uint32: 66 | return asnc, nil 67 | case float64: 68 | return uint32(asnc), nil 69 | case int: 70 | return uint32(asnc), nil 71 | default: 72 | return 0, fmt.Errorf("could not decode ASN: %v", vrp.ASN) 73 | } 74 | } 75 | 76 | func (vrp *VRPJson) GetASN() uint32 { 77 | asn, _ := vrp.GetASN2() 78 | return asn 79 | } 80 | 81 | func (vrp *VRPJson) GetPrefix2() (netip.Prefix, error) { 82 | prefix, err := netip.ParsePrefix(vrp.Prefix) 83 | if err != nil { 84 | return netip.Prefix{}, fmt.Errorf("could not decode prefix: %v", vrp.Prefix) 85 | } 86 | if !prefix.IsValid() { 87 | return netip.Prefix{}, fmt.Errorf("prefix %s is invalid", prefix) 88 | } 89 | return prefix, nil 90 | } 91 | 92 | func (vrp *VRPJson) GetPrefix() netip.Prefix { 93 | prefix, _ := vrp.GetPrefix2() 94 | return prefix 95 | } 96 | 97 | func (vrp *VRPJson) GetMaxLen() int { 98 | return int(vrp.Length) 99 | } 100 | 101 | func (vrp *VRPJson) String() string { 102 | return fmt.Sprintf("%v/%v/%v", vrp.Prefix, vrp.Length, vrp.ASN) 103 | } 104 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | EXTENSION ?= 2 | DIST_DIR ?= dist/ 3 | GOOS ?= linux 4 | GOARCH ?= x86_64 5 | ARCH ?= $(GOARCH) 6 | BUILDINFOSDET ?= 7 | 8 | DOCKER_REPO := rpki/ 9 | STAYRTR_NAME := stayrtr 10 | STAYRTR_VERSION := $(shell git describe --always --tags $(git rev-list --tags --max-count=1)) 11 | VERSION_PKG := $(shell echo $(STAYRTR_VERSION) | sed 's/^v//g') 12 | LICENSE := BSD-3 13 | URL := https://github.com/bgp/stayrtr 14 | DESCRIPTION := StayRTR: a RPKI-to-Router server 15 | BUILDINFOS := ($(shell date +%FT%T%z)$(BUILDINFOSDET)) 16 | LDFLAGS ?= '-X main.version=$(STAYRTR_VERSION) -X main.buildinfos=$(BUILDINFOS)' 17 | 18 | RTRDUMP_NAME := rtrdump 19 | RTRMON_NAME := rtrmon 20 | 21 | SUFFIX ?= -$(STAYRTR_VERSION)-$(GOOS)-$(ARCH)$(EXTENSION) 22 | 23 | OUTPUT_STAYRTR := $(DIST_DIR)stayrtr$(SUFFIX) 24 | OUTPUT_RTRDUMP := $(DIST_DIR)rtrdump$(SUFFIX) 25 | OUTPUT_RTRMON := $(DIST_DIR)rtrmon$(SUFFIX) 26 | 27 | export CGO_ENABLED ?= 0 28 | 29 | 30 | .PHONY: build-all 31 | build-all: vet build-stayrtr build-rtrdump build-rtrmon 32 | 33 | .PHONY: vet 34 | vet: 35 | go vet cmd/stayrtr/stayrtr.go 36 | 37 | .PHONY: test 38 | test: 39 | go test -v github.com/bgp/stayrtr/lib 40 | go test -v github.com/bgp/stayrtr/prefixfile 41 | go test -v github.com/bgp/stayrtr/cmd/rtrmon 42 | go test -v github.com/bgp/stayrtr/cmd/stayrtr 43 | 44 | .PHONY: prepare 45 | prepare: 46 | mkdir -p $(DIST_DIR) 47 | 48 | .PHONY: clean 49 | clean: 50 | rm -rf $(DIST_DIR) 51 | 52 | .PHONY: build-stayrtr 53 | build-stayrtr: prepare 54 | go build -trimpath -ldflags $(LDFLAGS) -o $(OUTPUT_STAYRTR) cmd/stayrtr/stayrtr.go 55 | 56 | .PHONY: build-rtrdump 57 | build-rtrdump: 58 | go build -trimpath -ldflags $(LDFLAGS) -o $(OUTPUT_RTRDUMP) cmd/rtrdump/rtrdump.go 59 | 60 | .PHONY: build-rtrmon 61 | build-rtrmon: 62 | go build -trimpath -ldflags $(LDFLAGS) -o $(OUTPUT_RTRMON) cmd/rtrmon/rtrmon.go 63 | 64 | .PHONY: docker 65 | docker: 66 | docker build -t $(DOCKER_REPO)$(STAYRTR_NAME) --target stayrtr . 67 | docker build -t $(DOCKER_REPO)$(RTRDUMP_NAME) --target rtrdump . 68 | docker build -t $(DOCKER_REPO)$(RTRMON_NAME) --target rtrmon . 69 | 70 | .PHONY: package-deb-stayrtr 71 | package-deb-stayrtr: prepare 72 | fpm -s dir -t deb -n $(STAYRTR_NAME) -v $(VERSION_PKG) \ 73 | --description "$(DESCRIPTION)" \ 74 | --url "$(URL)" \ 75 | --architecture $(ARCH) \ 76 | --license "$(LICENSE)" \ 77 | --package $(DIST_DIR) \ 78 | --after-install package/after-install-debian.sh \ 79 | package/.keep=/usr/share/stayrtr/.keep \ 80 | $(OUTPUT_STAYRTR)=/usr/bin/stayrtr \ 81 | package/stayrtr.service=/lib/systemd/system/stayrtr.service \ 82 | package/stayrtr.env=/etc/default/stayrtr \ 83 | $(OUTPUT_RTRDUMP)=/usr/bin/rtrdump \ 84 | $(OUTPUT_RTRMON)=/usr/bin/rtrmon 85 | 86 | .PHONY: package-rpm-stayrtr 87 | package-rpm-stayrtr: prepare 88 | fpm -s dir -t rpm -n $(STAYRTR_NAME) -v $(VERSION_PKG) \ 89 | --description "$(DESCRIPTION)" \ 90 | --url "$(URL)" \ 91 | --architecture $(ARCH) \ 92 | --license "$(LICENSE)" \ 93 | --package $(DIST_DIR) \ 94 | $(OUTPUT_STAYRTR)=/usr/bin/stayrtr \ 95 | package/.keep=/usr/share/stayrtr/.keep \ 96 | package/stayrtr.service=/lib/systemd/system/stayrtr.service \ 97 | package/stayrtr.env=/etc/default/stayrtr \ 98 | $(OUTPUT_RTRDUMP)=/usr/bin/rtrdump \ 99 | $(OUTPUT_RTRMON)=/usr/bin/rtrmon -------------------------------------------------------------------------------- /lib/client_test.go: -------------------------------------------------------------------------------- 1 | package rtrlib 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/bgp/stayrtr/prefixfile" 9 | "github.com/google/go-cmp/cmp" 10 | ) 11 | 12 | var ( 13 | Serial = uint32(0) 14 | Session = uint16(0) 15 | InitSerial = false 16 | ) 17 | 18 | type TestClient struct { 19 | Data prefixfile.RPKIList 20 | 21 | InitSerial bool 22 | Serial uint32 23 | SessionID uint16 24 | } 25 | 26 | func getBasicClientConguration(version int) ClientConfiguration { 27 | return ClientConfiguration{ 28 | ProtocolVersion: uint8(version), 29 | RefreshInterval: 10, 30 | RetryInterval: 15, 31 | ExpireInterval: 20, 32 | } 33 | } 34 | 35 | func getClient() *TestClient { 36 | return &TestClient{ 37 | Data: prefixfile.RPKIList{ 38 | Metadata: prefixfile.MetaData{}, 39 | ROA: make([]prefixfile.VRPJson, 0), 40 | }, 41 | InitSerial: InitSerial, 42 | Serial: Serial, 43 | SessionID: Session, 44 | } 45 | } 46 | 47 | func (tc *TestClient) HandlePDU(cs *ClientSession, pdu PDU) {} 48 | 49 | func (tc *TestClient) ClientConnected(cs *ClientSession) {} 50 | 51 | func (tc *TestClient) ClientDisconnected(cs *ClientSession) {} 52 | 53 | func TestSendResetQuery(t *testing.T) { 54 | tests := []struct { 55 | desc string 56 | version int 57 | want PDU 58 | }{{ 59 | desc: "Reset Query, Version 0", 60 | want: &PDUResetQuery{PROTOCOL_VERSION_0}, 61 | }, { 62 | desc: "Reset Query, Version 1", 63 | version: 1, 64 | want: &PDUResetQuery{PROTOCOL_VERSION_1}, 65 | }} 66 | 67 | for _, tc := range tests { 68 | t.Run(tc.desc, func(t *testing.T) { 69 | cs := NewClientSession(getBasicClientConguration(tc.version), getClient()) 70 | cs.SendResetQuery() 71 | c := <-cs.transmits 72 | 73 | if !cmp.Equal(c, tc.want) { 74 | t.Errorf("Wanted (%+v), but got (%+v)", tc.want, c) 75 | } 76 | }) 77 | } 78 | } 79 | 80 | func TestSendSerialQuery(t *testing.T) { 81 | tests := []struct { 82 | desc string 83 | version int 84 | want PDU 85 | }{{ 86 | desc: "Serial Query PDU", 87 | version: 1, 88 | want: &PDUSerialQuery{PROTOCOL_VERSION_1, 123, 456}, 89 | }} 90 | 91 | for _, tc := range tests { 92 | t.Run(tc.desc, func(t *testing.T) { 93 | cs := NewClientSession(getBasicClientConguration(tc.version), getClient()) 94 | cs.SendSerialQuery(123, 456) 95 | c := <-cs.transmits 96 | 97 | if !cmp.Equal(c, tc.want) { 98 | t.Errorf("Wanted (%+v), but got (%+v)", tc.want, c) 99 | } 100 | }) 101 | } 102 | } 103 | 104 | func TestRouterKeyEncodeDecode(t *testing.T) { 105 | p := &PDURouterKey{ 106 | Version: 1, 107 | Flags: 1, 108 | SubjectKeyIdentifier: []byte{0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01}, 109 | ASN: 64497, 110 | SubjectPublicKeyInfo: []byte("This is not a real key"), 111 | } 112 | 113 | buf := bytes.NewBuffer(nil) 114 | p.Write(buf) 115 | 116 | outputPdu, err := Decode(buf) 117 | 118 | if err != nil { 119 | t.FailNow() 120 | } 121 | 122 | orig := fmt.Sprintf("%#v", p) 123 | decode := fmt.Sprintf("%#v", outputPdu) 124 | if orig != decode { 125 | t.Fatalf("%s\n is not\n%s", orig, decode) 126 | t.FailNow() 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /cmd/stayrtr/test.rpki.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "buildmachine": "ams-vrp-gen1.rpki-client.org", 4 | "buildtime": "2023-07-27T18:56:02Z", 5 | "elapsedtime": "301", 6 | "usertime": "208", 7 | "systemtime": "113", 8 | "roas": 87883, 9 | "failedroas": 0, 10 | "invalidroas": 1, 11 | "certificates": 26001, 12 | "failcertificates": 0, 13 | "invalidcertificates": 0, 14 | "tals": 4, 15 | "talfiles": "/etc/rpki/afrinic.tal /etc/rpki/apnic.tal /etc/rpki/lacnic.tal /etc/rpki/ripe.tal", 16 | "manifests": 26001, 17 | "failedmanifests": 4, 18 | "stalemanifests": 4, 19 | "crls": 25993, 20 | "gbrs": 2, 21 | "repositories": 25824, 22 | "vrps": 272724, 23 | "uniquevrps": 267772, 24 | "cachedir_del_files": 31, 25 | "cachedir_del_dirs": 283 26 | }, 27 | 28 | "roas": [ 29 | { "asn": 13335, "prefix": "1.0.0.0/24", "maxLength": 24, "ta": "apnic", "expires": 1827568318 }, 30 | { "asn": 38803, "prefix": "1.0.4.0/24", "maxLength": 24, "ta": "apnic", "expires": 1827559320 }, 31 | { "asn": 38803, "prefix": "1.0.4.0/22", "maxLength": 22, "ta": "apnic", "expires": 1827559320 }, 32 | { "asn": 38803, "prefix": "1.0.5.0/24", "maxLength": 24, "ta": "apnic", "expires": 1827559320 }, 33 | { "asn": 9367, "prefix": "2001:200:136::/48", "maxLength": 48, "ta": "apnic", "expires": 1827575699 }, 34 | { "asn": 24047, "prefix": "2001:200:1ba::/48", "maxLength": 48, "ta": "apnic", "expires": 1827575699 }, 35 | { "asn": 7660, "prefix": "2001:200:900::/40", "maxLength": 40, "ta": "apnic", "expires": 1827575699 }, 36 | { "asn": 4690, "prefix": "2001:200:e00::/40", "maxLength": 40, "ta": "apnic", "expires": 1827575699 }, 37 | { "asn": 1103, "prefix": "2001:610::/32", "maxLength": 48, "ta": "ripe", "expires": 1827488503 }, 38 | { "asn": 1103, "prefix": "2001:610::/29", "maxLength": 29, "ta": "ripe", "expires": 1827488503 }, 39 | { "asn": 3333, "prefix": "2001:610:240::/42", "maxLength": 42, "ta": "ripe", "expires": 1827488503 }, 40 | { "asn": 30999, "prefix": "2001:4248::/32", "maxLength": 64, "ta": "afrinic", "expires": 1827520144 }, 41 | { "asn": 6453, "prefix": "2001:42c8::/32", "maxLength": 32, "ta": "afrinic", "expires": 1827520974 }, 42 | { "asn": 33764, "prefix": "2001:42d0::/40", "maxLength": 40, "ta": "afrinic", "expires": 1827518625 }, 43 | { "asn": 33764, "prefix": "2001:42d0:1500::/40", "maxLength": 40, "ta": "afrinic", "expires": 1827518625 }, 44 | { "asn": 27808, "prefix": "2800:38::/32", "maxLength": 128, "ta": "lacnic", "expires": 1827677646 }, 45 | { "asn": 16814, "prefix": "2800:40::/32", "maxLength": 48, "ta": "lacnic", "expires": 1827665407 }, 46 | { "asn": 16814, "prefix": "2800:40::/32", "maxLength": 32, "ta": "lacnic", "expires": 1827665407 }, 47 | { "asn": 1, "prefix": "192.0.2.0/24", "maxLength": 24, "ta": "lmao", "expires": 1827665407 } 48 | ], 49 | "bgpsec_keys": [ 50 | { 51 | "asn": 15562, 52 | "pubkey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgFcjQ/g//LAQerAH2Mpp+GucoDAGBbhIqD33wNPsXxnAGb+mtZ7XQrVO9DQ6UlAShtig5+QfEKpTtFgiqfiAFQ==", 53 | "ski": "5d4250e2d81d4448d8a29efce91d29ff075ec9e2" 54 | }, 55 | { 56 | "asn": 64496, 57 | "pubkey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEhv5HEBGixUjKJTlenvcD1Axyi07rFdVY1KhN4vMPYy5y0Mx6zfaiEqJN27jK/l61xC36Vsaezd7eXAsZ1AEEsQ==", 58 | "ski": "510f485d29a29db7b515f9c478f8ed3cb7aa7d23" 59 | }, 60 | { 61 | "asn": 15562, 62 | "pubkey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE4FxJr0n2bux1uX1Evl+QWwZYvIadPjLuFX2mxqKuAGUhKnr7VLLDgrE++l9p5eH2kWTNVAN22FUU3db/RKpE2w==", 63 | "ski": "be889b55d0b737397d75c49f485b858fa98ad11f" 64 | } 65 | ], 66 | "provider_authorizations": { 67 | "ipv4": [ 68 | { 69 | "customer_asid": 15562, 70 | "providers": [2914,8283,51088,206238] 71 | }, 72 | { 73 | "customer_asid": 64496, 74 | "providers": [1299,6939,7480,32097,50058,61138] 75 | } 76 | ], 77 | "ipv6": [ 78 | { 79 | "customer_asid": 64496, 80 | "providers": [1299,6939,7480,32097,50058,61138] 81 | }, 82 | { 83 | "customer_asid": 15562, 84 | "providers": [2914,8283,51088,206238] 85 | } 86 | ] 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /cmd/stayrtr/stayrtr_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/netip" 6 | "os" 7 | "testing" 8 | "time" 9 | 10 | rtr "github.com/bgp/stayrtr/lib" 11 | "github.com/bgp/stayrtr/prefixfile" 12 | "github.com/google/go-cmp/cmp" 13 | ) 14 | 15 | func TestProcessData(t *testing.T) { 16 | var stuff []prefixfile.VRPJson 17 | NowUnix := time.Now().Unix() 18 | ExpiredTime := int64(1337) 19 | 20 | stuff = append(stuff, 21 | prefixfile.VRPJson{ 22 | Prefix: "192.168.0.0/24", 23 | Length: 24, 24 | ASN: 123, 25 | TA: "testrir", 26 | Expires: &NowUnix, 27 | }, 28 | prefixfile.VRPJson{ 29 | Prefix: "192.168.0.0/24", 30 | Length: 24, 31 | TA: "testrir", 32 | }, 33 | prefixfile.VRPJson{ 34 | Prefix: "2001:db8::/32", 35 | Length: 33, 36 | ASN: "AS123", 37 | TA: "testrir", 38 | }, 39 | prefixfile.VRPJson{ 40 | Prefix: "192.168.1.0/24", 41 | Length: 25, 42 | ASN: 123, 43 | TA: "testrir", 44 | }, 45 | // Invalid. Length is 0 46 | prefixfile.VRPJson{ 47 | Prefix: "192.168.1.0/24", 48 | Length: 0, 49 | ASN: 123, 50 | TA: "testrir", 51 | }, 52 | // Invalid. Length less than prefix length 53 | prefixfile.VRPJson{ 54 | Prefix: "192.168.1.0/24", 55 | Length: 16, 56 | ASN: 123, 57 | TA: "testrir", 58 | }, 59 | // Invalid. 129 is invalid for IPv6 60 | prefixfile.VRPJson{ 61 | Prefix: "2001:db8::/32", 62 | Length: 129, 63 | ASN: 123, 64 | TA: "testrir", 65 | }, 66 | // Invalid. 33 is invalid for IPv4 67 | prefixfile.VRPJson{ 68 | Prefix: "192.168.1.0/24", 69 | Length: 33, 70 | ASN: 123, 71 | TA: "testrir", 72 | }, 73 | // Invalid. Not a prefix 74 | prefixfile.VRPJson{ 75 | Prefix: "192.168.1.0", 76 | Length: 24, 77 | ASN: 123, 78 | TA: "testrir", 79 | }, 80 | // Invalid. Not a prefix 81 | prefixfile.VRPJson{ 82 | Prefix: "👻", 83 | Length: 24, 84 | ASN: 123, 85 | TA: "testrir", 86 | }, 87 | // Invalid. Invalid ASN string 88 | prefixfile.VRPJson{ 89 | Prefix: "192.168.1.0/22", 90 | Length: 22, 91 | ASN: "ASN123", 92 | TA: "testrir", 93 | }, 94 | // Invalid. Has expired 95 | prefixfile.VRPJson{ 96 | Prefix: "192.168.2.0/24", 97 | Length: 24, 98 | ASN: 124, 99 | TA: "testrir", 100 | Expires: &ExpiredTime, 101 | }, 102 | ) 103 | got, _, v4count, v6count := processData(stuff, nil) 104 | want := []rtr.VRP{ 105 | { 106 | Prefix: netip.MustParsePrefix("2001:db8::/32"), 107 | MaxLen: 33, 108 | ASN: 123, 109 | }, 110 | { 111 | Prefix: netip.MustParsePrefix("192.168.1.0/24"), 112 | MaxLen: 25, 113 | ASN: 123, 114 | }, 115 | { 116 | Prefix: netip.MustParsePrefix("192.168.0.0/24"), 117 | MaxLen: 24, 118 | ASN: 123, 119 | }, 120 | } 121 | if v4count != 2 || v6count != 1 { 122 | t.Errorf("Wanted v4count = 2, v6count = 1, but got %d, %d", v4count, v6count) 123 | } 124 | 125 | opts := []cmp.Option{ 126 | cmp.Comparer(func(x, y netip.Prefix) bool { 127 | return x == y 128 | }), 129 | } 130 | 131 | if !cmp.Equal(got, want, opts...) { 132 | t.Errorf("Want (%+v), Got (%+v)", want, got) 133 | } 134 | } 135 | 136 | func BenchmarkDecodeJSON(b *testing.B) { 137 | json, err := os.ReadFile("test.rpki.json") 138 | if err != nil { 139 | panic(err) 140 | } 141 | for n := 0; n < b.N; n++ { 142 | decodeJSON(json) 143 | } 144 | } 145 | 146 | func TestJson(t *testing.T) { 147 | json, err := os.ReadFile("smalltest.rpki.json") 148 | if err != nil { 149 | panic(err) 150 | } 151 | got, err := decodeJSON(json) 152 | if err != nil { 153 | t.Errorf("Unable to decode json: %v", err) 154 | } 155 | 156 | Ex1 := int64(1627568318) 157 | Ex2 := int64(1627575699) 158 | 159 | want := (&prefixfile.RPKIList{ 160 | Metadata: prefixfile.MetaData{ 161 | Counts: 2, 162 | Buildtime: "2021-07-27T18:56:02Z", 163 | }, 164 | ROA: []prefixfile.VRPJson{ 165 | {Prefix: "1.0.0.0/24", 166 | Length: 24, 167 | ASN: float64(13335), 168 | TA: "apnic", 169 | Expires: &Ex1, 170 | }, 171 | { 172 | Prefix: "2001:200:136::/48", 173 | Length: 48, 174 | ASN: "AS9367", 175 | TA: "apnic", 176 | Expires: &Ex2, 177 | }, 178 | }, 179 | }) 180 | 181 | if !cmp.Equal(got, want) { 182 | t.Errorf("Got (%v), Wanted (%v)", got, want) 183 | } 184 | 185 | } 186 | 187 | func TestNewSHA256(t *testing.T) { 188 | want := "8eddd6897b244bb4d045ff811128b50b53ed85d19a9d1b756a0a400e82b23c2f" 189 | got := fmt.Sprintf("%x", newSHA256([]byte("☘️"))) 190 | if got != want { 191 | t.Errorf("Got (%s), Wanted (%s)", got, want) 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net" 7 | "net/http" 8 | "os" 9 | "sync" 10 | "time" 11 | ) 12 | 13 | type FetchConfig struct { 14 | UserAgent string 15 | Mime string 16 | 17 | etags map[string]string 18 | lastModified map[string]time.Time 19 | conditionalRequestLock *sync.RWMutex 20 | EnableEtags bool 21 | EnableLastModified bool 22 | } 23 | 24 | func NewFetchConfig() *FetchConfig { 25 | return &FetchConfig{ 26 | etags: make(map[string]string), 27 | lastModified: make(map[string]time.Time), 28 | conditionalRequestLock: &sync.RWMutex{}, 29 | Mime: "application/json", 30 | } 31 | } 32 | 33 | type HttpNotModified struct { 34 | File string 35 | } 36 | 37 | func (e HttpNotModified) Error() string { 38 | return fmt.Sprintf("HTTP 304 Not modified for %s", e.File) 39 | } 40 | 41 | type IdenticalEtag struct { 42 | File string 43 | Etag string 44 | } 45 | 46 | func (e IdenticalEtag) Error() string { 47 | return fmt.Sprintf("File %s is identical according to Etag: %s", e.File, e.Etag) 48 | } 49 | 50 | func (c *FetchConfig) FetchFile(file string) ([]byte, int, bool, error) { 51 | var f io.Reader 52 | var err error 53 | if len(file) > 8 && (file[0:7] == "http://" || file[0:8] == "https://") { 54 | 55 | // Copying base of DefaultTransport from https://golang.org/src/net/http/transport.go 56 | // There is a proposal for a Clone of 57 | tr := &http.Transport{ 58 | Proxy: http.ProxyFromEnvironment, 59 | DialContext: (&net.Dialer{ 60 | Timeout: 30 * time.Second, 61 | KeepAlive: 30 * time.Second, 62 | DualStack: true, 63 | }).DialContext, 64 | MaxIdleConns: 100, 65 | IdleConnTimeout: 90 * time.Second, 66 | TLSHandshakeTimeout: 10 * time.Second, 67 | ExpectContinueTimeout: 1 * time.Second, 68 | ProxyConnectHeader: map[string][]string{}, 69 | } 70 | // Keep User-Agent in proxy request 71 | tr.ProxyConnectHeader.Set("User-Agent", c.UserAgent) 72 | 73 | client := &http.Client{Transport: tr} 74 | req, err := http.NewRequest("GET", file, nil) 75 | if err != nil { 76 | return nil, -1, false, err 77 | } 78 | 79 | req.Header.Set("User-Agent", c.UserAgent) 80 | if c.Mime != "" { 81 | req.Header.Set("Accept", c.Mime) 82 | } 83 | 84 | c.conditionalRequestLock.RLock() 85 | if c.EnableEtags { 86 | etag, ok := c.etags[file] 87 | if ok { 88 | req.Header.Set("If-None-Match", etag) 89 | } 90 | } 91 | if c.EnableLastModified { 92 | lastModified, ok := c.lastModified[file] 93 | if ok { 94 | req.Header.Set("If-Modified-Since", lastModified.UTC().Format(http.TimeFormat)) 95 | } 96 | } 97 | c.conditionalRequestLock.RUnlock() 98 | 99 | proxyurl, err := http.ProxyFromEnvironment(req) 100 | if err != nil { 101 | return nil, -1, false, err 102 | } 103 | proxyreq := http.ProxyURL(proxyurl) 104 | tr.Proxy = proxyreq 105 | 106 | if err != nil { 107 | return nil, -1, false, err 108 | } 109 | 110 | fhttp, err := client.Do(req) 111 | if err != nil { 112 | return nil, -1, false, err 113 | } 114 | if fhttp.Body != nil { 115 | defer fhttp.Body.Close() 116 | } 117 | defer client.CloseIdleConnections() 118 | //RefreshStatusCode.WithLabelValues(file, fmt.Sprintf("%d", fhttp.StatusCode)).Inc() 119 | 120 | if fhttp.StatusCode == 304 { 121 | //LastRefresh.WithLabelValues(file).Set(float64(s.lastts.UnixNano() / 1e9)) 122 | return nil, fhttp.StatusCode, true, HttpNotModified{ 123 | File: file, 124 | } 125 | } else if fhttp.StatusCode != 200 { 126 | c.conditionalRequestLock.Lock() 127 | delete(c.etags, file) 128 | delete(c.lastModified, file) 129 | c.conditionalRequestLock.Unlock() 130 | return nil, fhttp.StatusCode, true, fmt.Errorf("HTTP %s", fhttp.Status) 131 | } 132 | //LastRefresh.WithLabelValues(file).Set(float64(s.lastts.UnixNano() / 1e9)) 133 | 134 | f = fhttp.Body 135 | 136 | newEtag := fhttp.Header.Get("ETag") 137 | 138 | if !c.EnableEtags || newEtag == "" || newEtag != c.etags[file] { // check lock here 139 | c.conditionalRequestLock.Lock() 140 | c.etags[file] = newEtag 141 | c.conditionalRequestLock.Unlock() 142 | } else { 143 | return nil, fhttp.StatusCode, true, IdenticalEtag{ 144 | File: file, 145 | Etag: newEtag, 146 | } 147 | } 148 | 149 | if c.EnableLastModified { 150 | // Accept any valid Last-Modified values. Because of the 1s resolution, 151 | // getting the same value is not an error (c.f. the IdenticalEtag error). 152 | ifModifiedSince, err := http.ParseTime(fhttp.Header.Get("Last-Modified")) 153 | c.conditionalRequestLock.Lock() 154 | if err == nil { 155 | c.lastModified[file] = ifModifiedSince 156 | } else { 157 | delete(c.lastModified, file) 158 | } 159 | c.conditionalRequestLock.Unlock() 160 | } 161 | } else { 162 | f, err = os.Open(file) 163 | if err != nil { 164 | return nil, -1, false, err 165 | } 166 | } 167 | data, err := io.ReadAll(f) 168 | if err != nil { 169 | return nil, -1, false, err 170 | } 171 | return data, -1, false, nil 172 | } 173 | -------------------------------------------------------------------------------- /lib/client.go: -------------------------------------------------------------------------------- 1 | package rtrlib 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "io" 7 | "net" 8 | 9 | "golang.org/x/crypto/ssh" 10 | ) 11 | 12 | type RTRClientSessionEventHandler interface { 13 | //RequestCache(*ClientSession) 14 | HandlePDU(*ClientSession, PDU) 15 | ClientConnected(*ClientSession) 16 | ClientDisconnected(*ClientSession) 17 | } 18 | 19 | type ClientSession struct { 20 | version uint8 21 | 22 | connected bool 23 | tcpconn net.Conn 24 | rd io.Reader 25 | wr io.Writer 26 | 27 | transmits chan PDU 28 | quit chan bool 29 | 30 | handler RTRClientSessionEventHandler 31 | 32 | log Logger 33 | } 34 | 35 | type ClientConfiguration struct { 36 | ProtocolVersion uint8 37 | 38 | RefreshInterval uint32 39 | RetryInterval uint32 40 | ExpireInterval uint32 41 | 42 | Log Logger 43 | } 44 | 45 | func NewClientSession(configuration ClientConfiguration, handler RTRClientSessionEventHandler) *ClientSession { 46 | return &ClientSession{ 47 | version: configuration.ProtocolVersion, 48 | transmits: make(chan PDU, 256), 49 | quit: make(chan bool), 50 | log: configuration.Log, 51 | handler: handler, 52 | } 53 | } 54 | 55 | func (c *ClientSession) SendResetQuery() { 56 | pdu := &PDUResetQuery{} 57 | c.SendPDU(pdu) 58 | } 59 | 60 | func (c *ClientSession) SendSerialQuery(sessionid uint16, serial uint32) { 61 | pdu := &PDUSerialQuery{ 62 | SessionId: sessionid, 63 | SerialNumber: serial, 64 | } 65 | c.SendPDU(pdu) 66 | } 67 | 68 | func (c *ClientSession) SendPDU(pdu PDU) { 69 | pdu.SetVersion(c.version) 70 | c.SendRawPDU(pdu) 71 | } 72 | 73 | func (c *ClientSession) SendRawPDU(pdu PDU) { 74 | c.transmits <- pdu 75 | } 76 | 77 | func (c *ClientSession) sendLoop() { 78 | for c.connected { 79 | select { 80 | case pdu := <-c.transmits: 81 | if c.wr != nil { 82 | c.wr.Write(pdu.Bytes()) 83 | } 84 | case <-c.quit: 85 | return 86 | } 87 | } 88 | } 89 | 90 | func (c *ClientSession) Disconnect() { 91 | c.connected = false 92 | //log.Debugf("Disconnecting client %v", c.String()) 93 | if c.handler != nil { 94 | c.handler.ClientDisconnected(c) 95 | } 96 | select { 97 | case c.quit <- true: 98 | default: 99 | 100 | } 101 | 102 | c.tcpconn.Close() 103 | } 104 | 105 | func (c *ClientSession) StartRW(rd io.Reader, wr io.Writer) error { 106 | go c.sendLoop() 107 | if c.handler != nil { 108 | c.handler.ClientConnected(c) 109 | } 110 | for c.connected { 111 | dec, err := Decode(c.rd) 112 | if err != nil || dec == nil { 113 | if c.log != nil { 114 | c.log.Errorf("Error %v", err) 115 | } 116 | c.Disconnect() 117 | return err 118 | } 119 | if c.version == PROTOCOL_VERSION_1 && dec.GetVersion() == PROTOCOL_VERSION_0 { 120 | if c.log != nil { 121 | c.log.Infof("Downgrading to version 0") 122 | } 123 | c.version = PROTOCOL_VERSION_0 124 | } 125 | 126 | if c.handler != nil { 127 | c.handler.HandlePDU(c, dec) 128 | } 129 | } 130 | return nil 131 | } 132 | 133 | func (c *ClientSession) StartWithConn(tcpconn net.Conn) error { 134 | c.tcpconn = tcpconn 135 | c.wr = tcpconn 136 | c.rd = tcpconn 137 | c.connected = true 138 | 139 | return c.StartRW(c.tcpconn, c.tcpconn) 140 | } 141 | 142 | func (c *ClientSession) StartWithSSH(tcpconn *net.TCPConn, session *ssh.Session) error { 143 | c.tcpconn = tcpconn 144 | c.rd, _ = session.StdoutPipe() 145 | c.wr, _ = session.StdinPipe() 146 | c.connected = true 147 | 148 | return c.StartRW(c.rd, c.wr) 149 | } 150 | 151 | func (c *ClientSession) StartPlain(addr string) error { 152 | addrTCP, err := net.ResolveTCPAddr("tcp", addr) 153 | if err != nil { 154 | return err 155 | } 156 | tcpconn, err := net.DialTCP("tcp", nil, addrTCP) 157 | if err != nil { 158 | return err 159 | } 160 | return c.StartWithConn(tcpconn) 161 | } 162 | 163 | func (c *ClientSession) StartTLS(addr string, config *tls.Config) error { 164 | tcpconn, err := tls.Dial("tcp", addr, config) 165 | if err != nil { 166 | return err 167 | } 168 | return c.StartWithConn(tcpconn) 169 | } 170 | 171 | func (c *ClientSession) StartSSH(addr string, config *ssh.ClientConfig) error { 172 | addrTCP, err := net.ResolveTCPAddr("tcp", addr) 173 | if err != nil { 174 | return err 175 | } 176 | tcpconn, err := net.DialTCP("tcp", nil, addrTCP) 177 | if err != nil { 178 | return err 179 | } 180 | 181 | conn, chans, reqs, err := ssh.NewClientConn(tcpconn, addr, config) 182 | if err != nil { 183 | return err 184 | } 185 | 186 | //client, err := ssh.Dial("tcp", addr, config) 187 | client := ssh.NewClient(conn, chans, reqs) 188 | session, err := client.NewSession() 189 | if err != nil { 190 | return err 191 | } 192 | err = session.RequestSubsystem("rpki-rtr") 193 | if err != nil { 194 | return err 195 | } 196 | return c.StartWithSSH(tcpconn, session) 197 | } 198 | 199 | func (c *ClientSession) Start(addr string, connType int, configTLS *tls.Config, configSSH *ssh.ClientConfig) error { 200 | switch connType { 201 | case TYPE_TLS: 202 | return c.StartTLS(addr, configTLS) 203 | case TYPE_PLAIN: 204 | return c.StartPlain(addr) 205 | case TYPE_SSH: 206 | return c.StartSSH(addr, configSSH) 207 | default: 208 | return fmt.Errorf("unknown ClientSession type %v", connType) 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 2 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 3 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 4 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 5 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 6 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 10 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 11 | github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= 12 | github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= 13 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 14 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 15 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 16 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 17 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 18 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 19 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 20 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 21 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 22 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 23 | github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk= 24 | github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= 25 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= 26 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= 27 | github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= 28 | github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= 29 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 30 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 31 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 32 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 33 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 34 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 35 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 36 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 37 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 38 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 39 | golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= 40 | golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= 41 | golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= 42 | golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= 43 | golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= 44 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 45 | golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= 46 | golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 47 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 48 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 49 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 50 | golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= 51 | golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 52 | golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= 53 | golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= 54 | golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= 55 | google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= 56 | google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 57 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 58 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 59 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 60 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 61 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 62 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 63 | -------------------------------------------------------------------------------- /cmd/rtrmon/rtrmon_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | log "github.com/sirupsen/logrus" 8 | 9 | "github.com/bgp/stayrtr/prefixfile" 10 | ) 11 | 12 | func TestBuildNewVrpMap_expiry(t *testing.T) { 13 | stuff := testDataFile() 14 | emptyFile := &prefixfile.RPKIList{ 15 | Metadata: prefixfile.MetaData{}, 16 | ROA: []prefixfile.VRPJson{}, 17 | BgpSecKeys: []prefixfile.BgpSecKeyJson{}, 18 | } 19 | 20 | now := time.Now() 21 | log := log.WithField("client", "TestBuildNewVrpMap_expiry") 22 | 23 | res, inGracePeriod := BuildNewVrpMap(log, make(VRPMap), stuff, now) 24 | if inGracePeriod != 0 { 25 | t.Errorf("Initial build does not have objects in grace period") 26 | } 27 | 28 | _, inGracePeriodPreserved := BuildNewVrpMap(log, res, emptyFile, now.Add(time.Minute*10)) 29 | if inGracePeriodPreserved != len(res) { 30 | t.Errorf("All objects are in grace period") 31 | } 32 | 33 | // Objects are kept in grace period 34 | // 1s before grace period ends 35 | t1 := now.Add(*GracePeriod).Add(-time.Second * 1) 36 | res, inGracePeriod = BuildNewVrpMap(log, res, emptyFile, t1) 37 | 38 | assertLastSeenMatchesTimeCount(t, res, t1, 0) 39 | assertLastSeenMatchesTimeCount(t, res, now, len(stuff.ROA)) 40 | if inGracePeriod != len(stuff.ROA) { 41 | t.Errorf("All objects should be in grace period. Expected: %d, actual: %d", len(stuff.ROA), inGracePeriod) 42 | } 43 | 44 | // 1s after grace period ends, they are removed 45 | res, inGracePeriod = BuildNewVrpMap(log, res, emptyFile, now.Add(*GracePeriod).Add(time.Second*1)) 46 | if len(res) != 0 { 47 | t.Errorf("Expected no objects to be left after grace period, actual: %d", len(res)) 48 | } 49 | if inGracePeriod != 0 { 50 | t.Errorf("Expected 0 objects in grace period, actual: %d", inGracePeriod) 51 | } 52 | } 53 | 54 | func TestBuildNewVrpMap_firsSeen_lastSeen(t *testing.T) { 55 | t0 := time.Now() 56 | log := log.WithField("client", "TestBuildNewVrpMap_firstSeen_lastSeen") 57 | stuff := testDataFile() 58 | 59 | var res, _ = BuildNewVrpMap(log, make(VRPMap), stuff, t0) 60 | 61 | // All have firstSeen + lastSeen equal to t0 62 | assertFirstSeenMatchesTimeCount(t, res, t0, len(stuff.ROA)) 63 | assertLastSeenMatchesTimeCount(t, res, t0, len(stuff.ROA)) 64 | assertVisibleMatchesTimeCount(t, res, len(stuff.ROA)) 65 | 66 | // Supply same data again later 67 | t1 := t0.Add(time.Minute * 10) 68 | res, _ = BuildNewVrpMap(log, res, stuff, t1) 69 | 70 | // FirstSeen is constant, LastSeen gets updated, none removed 71 | assertFirstSeenMatchesTimeCount(t, res, t0, len(stuff.ROA)) 72 | assertLastSeenMatchesTimeCount(t, res, t1, len(stuff.ROA)) 73 | assertVisibleMatchesTimeCount(t, res, len(stuff.ROA)) 74 | 75 | // Supply one new VRP, expect one at new time, others at old time 76 | otherStuff := []prefixfile.VRPJson{ 77 | { 78 | Prefix: "2001:DB8::/32", 79 | Length: 48, 80 | ASN: 65536, 81 | TA: "testrir", 82 | }, 83 | } 84 | otherStuffFile := prefixfile.RPKIList{ 85 | Metadata: prefixfile.MetaData{}, 86 | ROA: otherStuff, 87 | BgpSecKeys: []prefixfile.BgpSecKeyJson{}, 88 | } 89 | t2 := t1.Add(time.Minute * 10) 90 | res, _ = BuildNewVrpMap(log, res, &otherStuffFile, t2) 91 | 92 | // LastSeen gets updated just for the new item 93 | assertFirstSeenMatchesTimeCount(t, res, t0, len(stuff.ROA)) 94 | assertLastSeenMatchesTimeCount(t, res, t1, len(stuff.ROA)) 95 | 96 | assertFirstSeenMatchesTimeCount(t, res, t2, len(otherStuff)) 97 | assertLastSeenMatchesTimeCount(t, res, t2, len(otherStuff)) 98 | assertVisibleMatchesTimeCount(t, res, len(otherStuff)) 99 | } 100 | 101 | func assertFirstSeenMatchesTimeCount(t *testing.T, vrps VRPMap, pit time.Time, expected int) { 102 | actual := countMatches(vrps, func(vrp *VRPJsonSimple) bool { return vrp.FirstSeen == pit.Unix() }) 103 | if actual != expected { 104 | t.Errorf("Expected %d VRPs to have FirstSeen of %v, actual: %d", expected, pit, actual) 105 | } 106 | } 107 | 108 | func assertLastSeenMatchesTimeCount(t *testing.T, vrps VRPMap, pit time.Time, expected int) { 109 | actual := countMatches(vrps, func(vrp *VRPJsonSimple) bool { return vrp.LastSeen == pit.Unix() }) 110 | if actual != expected { 111 | t.Errorf("Expected %d VRPs to have LastSeen of %v, actual: %d", expected, pit, actual) 112 | } 113 | } 114 | 115 | func assertVisibleMatchesTimeCount(t *testing.T, vrps VRPMap, expected int) { 116 | actual := countMatches(vrps, func(vrp *VRPJsonSimple) bool { return vrp.Visible }) 117 | if actual != expected { 118 | t.Errorf("Expected %d VRPs to be visible, actual: %d", expected, actual) 119 | } 120 | } 121 | 122 | type extractor func(object *VRPJsonSimple) bool 123 | 124 | func countMatches(vrps VRPMap, e extractor) int { 125 | matches := 0 126 | for _, entry := range vrps { 127 | if e(entry) { 128 | matches++ 129 | } 130 | } 131 | 132 | return matches 133 | } 134 | 135 | func testData() []prefixfile.VRPJson { 136 | var stuff []prefixfile.VRPJson 137 | stuff = append(stuff, 138 | prefixfile.VRPJson{ 139 | Prefix: "192.168.0.0/24", 140 | Length: 24, 141 | ASN: 65537, 142 | TA: "testrir", 143 | }, 144 | prefixfile.VRPJson{ 145 | Prefix: "192.168.0.0/24", 146 | Length: 24, 147 | ASN: 65536, 148 | TA: "testrir", 149 | }, 150 | prefixfile.VRPJson{ 151 | Prefix: "2001:db8::/32", 152 | Length: 33, 153 | ASN: "AS64496", 154 | TA: "testrir", 155 | }, 156 | prefixfile.VRPJson{ 157 | Prefix: "192.168.1.0/24", 158 | Length: 25, 159 | ASN: 64497, 160 | TA: "testrir", 161 | }, 162 | ) 163 | 164 | return stuff 165 | } 166 | 167 | func testDataFile() *prefixfile.RPKIList { 168 | stuff := prefixfile.RPKIList{ 169 | Metadata: prefixfile.MetaData{}, 170 | ROA: testData(), 171 | BgpSecKeys: []prefixfile.BgpSecKeyJson{}, 172 | } 173 | return &stuff 174 | } 175 | -------------------------------------------------------------------------------- /cmd/rtrdump/rtrdump.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "encoding/hex" 6 | "encoding/json" 7 | "flag" 8 | "fmt" 9 | "io" 10 | "net" 11 | "os" 12 | "strings" 13 | 14 | rtr "github.com/bgp/stayrtr/lib" 15 | "github.com/bgp/stayrtr/prefixfile" 16 | log "github.com/sirupsen/logrus" 17 | "golang.org/x/crypto/ssh" 18 | ) 19 | 20 | const ( 21 | ENV_SSH_PASSWORD = "RTR_SSH_PASSWORD" 22 | ENV_SSH_KEY = "RTR_SSH_KEY" 23 | 24 | METHOD_NONE = iota 25 | METHOD_PASSWORD 26 | METHOD_KEY 27 | ) 28 | 29 | var ( 30 | AppVersion = "RTRdump " + rtr.APP_VERSION 31 | 32 | Connect = flag.String("connect", "127.0.0.1:8282", "Connection address") 33 | OutFile = flag.String("file", "output.json", "Output file") 34 | 35 | InitSerial = flag.Bool("serial", false, "Send serial query instead of reset") 36 | Serial = flag.Int("serial.value", 0, "Serial number") 37 | Session = flag.Int("session.id", 0, "Session ID") 38 | 39 | FlagVersion = flag.Int("rtr.version", 1, "What RTR version you want to use, Version 1 is RFC8210") 40 | 41 | ConnType = flag.String("type", "plain", "Type of connection: plain, tls or ssh") 42 | ValidateCert = flag.Bool("tls.validate", true, "Validate TLS") 43 | 44 | ValidateSSH = flag.Bool("ssh.validate", false, "Validate SSH key") 45 | SSHServerKey = flag.String("ssh.validate.key", "", "SSH server key SHA256 to validate") 46 | SSHAuth = flag.String("ssh.method", "none", "Select SSH method (none, password or key)") 47 | SSHAuthUser = flag.String("ssh.auth.user", "rpki", "SSH user") 48 | SSHAuthPassword = flag.String("ssh.auth.password", "", fmt.Sprintf("SSH password (if blank, will use envvar %v)", ENV_SSH_PASSWORD)) 49 | SSHAuthKey = flag.String("ssh.auth.key", "id_rsa", fmt.Sprintf("SSH key file (if blank, will use envvar %v)", ENV_SSH_KEY)) 50 | 51 | RefreshInterval = flag.Int("refresh", 600, "Refresh interval in seconds") 52 | 53 | LogLevel = flag.String("loglevel", "info", "Log level") 54 | LogDataPDU = flag.Bool("datapdu", false, "Log data PDU") 55 | Version = flag.Bool("version", false, "Print version") 56 | 57 | typeToId = map[string]int{ 58 | "plain": rtr.TYPE_PLAIN, 59 | "tls": rtr.TYPE_TLS, 60 | "ssh": rtr.TYPE_SSH, 61 | } 62 | authToId = map[string]int{ 63 | "none": METHOD_NONE, 64 | "password": METHOD_PASSWORD, 65 | "key": METHOD_KEY, 66 | } 67 | ) 68 | 69 | type Client struct { 70 | Data prefixfile.RPKIList 71 | 72 | InitSerial bool 73 | Serial uint32 74 | SessionID uint16 75 | } 76 | 77 | func (c *Client) HandlePDU(cs *rtr.ClientSession, pdu rtr.PDU) { 78 | switch pdu := pdu.(type) { 79 | case *rtr.PDUIPv4Prefix: 80 | rj := prefixfile.VRPJson{ 81 | Prefix: pdu.Prefix.String(), 82 | ASN: uint32(pdu.ASN), 83 | Length: pdu.MaxLen, 84 | } 85 | c.Data.ROA = append(c.Data.ROA, rj) 86 | c.Data.Metadata.Counts++ 87 | 88 | if *LogDataPDU { 89 | log.Debugf("Received: %v", pdu) 90 | } 91 | case *rtr.PDUIPv6Prefix: 92 | rj := prefixfile.VRPJson{ 93 | Prefix: pdu.Prefix.String(), 94 | ASN: uint32(pdu.ASN), 95 | Length: pdu.MaxLen, 96 | } 97 | c.Data.ROA = append(c.Data.ROA, rj) 98 | c.Data.Metadata.Counts++ 99 | 100 | if *LogDataPDU { 101 | log.Debugf("Received: %v", pdu) 102 | } 103 | case *rtr.PDURouterKey: 104 | skiHex := hex.EncodeToString(pdu.SubjectKeyIdentifier) 105 | rj := prefixfile.BgpSecKeyJson{ 106 | Asn: uint32(pdu.ASN), 107 | Pubkey: pdu.SubjectPublicKeyInfo, 108 | Ski: skiHex, 109 | } 110 | c.Data.BgpSecKeys = append(c.Data.BgpSecKeys, rj) 111 | 112 | if *LogDataPDU { 113 | log.Debugf("Received: %v", pdu) 114 | } 115 | case *rtr.PDUEndOfData: 116 | c.Data.Metadata.SessionID = int(pdu.SessionId) 117 | c.Data.Metadata.Serial = int(pdu.SerialNumber) 118 | cs.Disconnect() 119 | log.Debugf("Received: %v", pdu) 120 | case *rtr.PDUCacheResponse: 121 | log.Debugf("Received: %v", pdu) 122 | default: 123 | log.Debugf("Received: %v", pdu) 124 | cs.Disconnect() 125 | } 126 | } 127 | 128 | func (c *Client) ClientConnected(cs *rtr.ClientSession) { 129 | if c.InitSerial { 130 | cs.SendSerialQuery(c.SessionID, c.Serial) 131 | } else { 132 | cs.SendResetQuery() 133 | } 134 | } 135 | 136 | func (c *Client) ClientDisconnected(cs *rtr.ClientSession) { 137 | 138 | } 139 | 140 | func main() { 141 | flag.Parse() 142 | if flag.NArg() > 0 { 143 | fmt.Printf("%s: illegal positional argument(s) provided (\"%s\") - did you mean to provide a flag?\n", os.Args[0], strings.Join(flag.Args(), " ")) 144 | os.Exit(2) 145 | } 146 | if *Version { 147 | fmt.Println(AppVersion) 148 | os.Exit(0) 149 | } 150 | 151 | targetVersion := rtr.PROTOCOL_VERSION_0 152 | if *FlagVersion > 1 { 153 | log.Fatalf("Invalid RTR Version provided, the highest version this release supports is 1") 154 | } 155 | if *FlagVersion == 1 { 156 | targetVersion = rtr.PROTOCOL_VERSION_1 157 | } 158 | 159 | lvl, _ := log.ParseLevel(*LogLevel) 160 | log.SetLevel(lvl) 161 | 162 | cc := rtr.ClientConfiguration{ 163 | ProtocolVersion: uint8(targetVersion), 164 | Log: log.StandardLogger(), 165 | } 166 | 167 | client := &Client{ 168 | Data: prefixfile.RPKIList{ 169 | Metadata: prefixfile.MetaData{}, 170 | ROA: make([]prefixfile.VRPJson, 0), 171 | }, 172 | InitSerial: *InitSerial, 173 | Serial: uint32(*Serial), 174 | SessionID: uint16(*Session), 175 | } 176 | 177 | clientSession := rtr.NewClientSession(cc, client) 178 | 179 | configTLS := &tls.Config{ 180 | InsecureSkipVerify: !*ValidateCert, 181 | } 182 | configSSH := &ssh.ClientConfig{ 183 | Auth: make([]ssh.AuthMethod, 0), 184 | User: *SSHAuthUser, 185 | HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { 186 | serverKeyHash := ssh.FingerprintSHA256(key) 187 | if *ValidateSSH { 188 | if serverKeyHash != fmt.Sprintf("SHA256:%v", *SSHServerKey) { 189 | return fmt.Errorf("server key hash %v is different than expected key hash SHA256:%v", serverKeyHash, *SSHServerKey) 190 | } 191 | } 192 | log.Infof("Connected to server %v via ssh. Fingerprint: %v", remote.String(), serverKeyHash) 193 | return nil 194 | }, 195 | } 196 | if authType, ok := authToId[*SSHAuth]; ok { 197 | if authType == METHOD_PASSWORD { 198 | password := *SSHAuthPassword 199 | if password == "" { 200 | password = os.Getenv(ENV_SSH_PASSWORD) 201 | } 202 | configSSH.Auth = append(configSSH.Auth, ssh.Password(password)) 203 | } else if authType == METHOD_KEY { 204 | var keyBytes []byte 205 | var err error 206 | if *SSHAuthKey == "" { 207 | keyBytesStr := os.Getenv(ENV_SSH_KEY) 208 | keyBytes = []byte(keyBytesStr) 209 | } else { 210 | keyBytes, err = os.ReadFile(*SSHAuthKey) 211 | if err != nil { 212 | log.Fatal(err) 213 | } 214 | } 215 | signer, err := ssh.ParsePrivateKey(keyBytes) 216 | if err != nil { 217 | log.Fatal(err) 218 | } 219 | configSSH.Auth = append(configSSH.Auth, ssh.PublicKeys(signer)) 220 | } 221 | } else { 222 | log.Fatalf("Auth type %v unknown", *SSHAuth) 223 | } 224 | 225 | log.Infof("Connecting with %v to %v", *ConnType, *Connect) 226 | err := clientSession.Start(*Connect, typeToId[*ConnType], configTLS, configSSH) 227 | if err != nil { 228 | log.Fatal(err) 229 | } 230 | 231 | var f io.Writer 232 | if *OutFile != "" { 233 | ff, err := os.Create(*OutFile) 234 | if err != nil { 235 | log.Fatal(err) 236 | } 237 | defer ff.Close() 238 | f = ff 239 | } else { 240 | f = os.Stdout 241 | } 242 | 243 | enc := json.NewEncoder(f) 244 | err = enc.Encode(client.Data) 245 | if err != nil { 246 | log.Fatal(err) 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /prefixfile/slurm.go: -------------------------------------------------------------------------------- 1 | // rfc8416 2 | 3 | package prefixfile 4 | 5 | import ( 6 | "bytes" 7 | "encoding/hex" 8 | "encoding/json" 9 | "io" 10 | "net" 11 | "net/netip" 12 | ) 13 | 14 | type SlurmPrefixFilter struct { 15 | Prefix string 16 | ASN *uint32 `json:"asn,omitempty"` 17 | Comment string 18 | } 19 | 20 | type SlurmBGPsecFilter struct { 21 | ASN *uint32 `json:"asn,omitempty"` 22 | SKI []byte `json:"SKI,omitempty"` 23 | Comment string `json:"comment"` 24 | } 25 | 26 | func (pf *SlurmPrefixFilter) GetASN() (uint32, bool) { 27 | if pf.ASN == nil { 28 | return 0, true 29 | } else { 30 | return *pf.ASN, false 31 | } 32 | } 33 | 34 | func (pf *SlurmPrefixFilter) GetPrefix() netip.Prefix { 35 | prefix, _ := netip.ParsePrefix(pf.Prefix) 36 | return prefix 37 | } 38 | 39 | type SlurmValidationOutputFilters struct { 40 | PrefixFilters []SlurmPrefixFilter 41 | BgpsecFilters []SlurmBGPsecFilter 42 | } 43 | 44 | type SlurmPrefixAssertion struct { 45 | Prefix string 46 | ASN uint32 47 | MaxPrefixLength int 48 | Comment string 49 | } 50 | 51 | type SlurmBGPsecAssertion struct { 52 | SKI []byte `json:"SKI"` 53 | ASN uint32 `json:"asn"` 54 | Comment string `json:"comment"` 55 | RouterPublicKey []byte `json:"routerPublicKey"` 56 | } 57 | 58 | func (pa *SlurmPrefixAssertion) GetASN() uint32 { 59 | return pa.ASN 60 | } 61 | 62 | func (pa *SlurmPrefixAssertion) GetPrefix() *net.IPNet { 63 | _, prefix, _ := net.ParseCIDR(pa.Prefix) 64 | return prefix 65 | } 66 | 67 | func (pa *SlurmPrefixAssertion) GetMaxLen() int { 68 | return pa.MaxPrefixLength 69 | } 70 | 71 | type SlurmLocallyAddedAssertions struct { 72 | PrefixAssertions []SlurmPrefixAssertion 73 | BgpsecAssertions []SlurmBGPsecAssertion 74 | } 75 | 76 | type SlurmConfig struct { 77 | SlurmVersion int 78 | ValidationOutputFilters SlurmValidationOutputFilters 79 | LocallyAddedAssertions SlurmLocallyAddedAssertions 80 | } 81 | 82 | func DecodeJSONSlurm(buf io.Reader) (*SlurmConfig, error) { 83 | slurm := &SlurmConfig{} 84 | dec := json.NewDecoder(buf) 85 | dec.UseNumber() 86 | err := dec.Decode(slurm) 87 | if err != nil { 88 | return nil, err 89 | } 90 | return slurm, nil 91 | } 92 | 93 | func (s *SlurmValidationOutputFilters) FilterOnVRPs(vrps []VRPJson) (added, removed []VRPJson) { 94 | added = make([]VRPJson, 0) 95 | removed = make([]VRPJson, 0) 96 | if s.PrefixFilters == nil || len(s.PrefixFilters) == 0 { 97 | return vrps, removed 98 | } 99 | for _, vrp := range vrps { 100 | rPrefix := vrp.GetPrefix() 101 | 102 | var wasRemoved bool 103 | for _, filter := range s.PrefixFilters { 104 | fPrefix := filter.GetPrefix() 105 | fASN, fASNEmpty := filter.GetASN() 106 | match := true 107 | if match && fPrefix.IsValid() && rPrefix.IsValid() { 108 | 109 | if !(fPrefix.Overlaps(rPrefix) && 110 | fPrefix.Bits() <= rPrefix.Bits()) { 111 | match = false 112 | } 113 | } 114 | if match && !fASNEmpty { 115 | if vrp.GetASN() != fASN { 116 | match = false 117 | } 118 | } 119 | if match { 120 | removed = append(removed, vrp) 121 | wasRemoved = true 122 | break 123 | } 124 | } 125 | 126 | if !wasRemoved { 127 | added = append(added, vrp) 128 | } 129 | } 130 | return added, removed 131 | } 132 | 133 | func (s *SlurmValidationOutputFilters) FilterOnBRKs(brks []BgpSecKeyJson) (added, removed []BgpSecKeyJson) { 134 | added = make([]BgpSecKeyJson, 0) 135 | removed = make([]BgpSecKeyJson, 0) 136 | if s.BgpsecFilters == nil || len(s.BgpsecFilters) == 0 { 137 | return brks, removed 138 | } 139 | for _, brk := range brks { 140 | var skiCache []byte 141 | var wasRemoved bool 142 | for _, filter := range s.BgpsecFilters { 143 | if filter.ASN != nil { 144 | if brk.Asn == *filter.ASN { 145 | if len(filter.SKI) != 0 { 146 | // We need to compare the SKIs then 147 | if skiCache == nil { // We have not yet decoded the ski hex 148 | var err error 149 | skiCache, err = hex.DecodeString(brk.Ski) 150 | if err != nil { 151 | // Ski could not be decoded, so we can't filter 152 | continue 153 | } 154 | } 155 | if bytes.Equal(filter.SKI, skiCache) { 156 | removed = append(removed, brk) 157 | wasRemoved = true 158 | break 159 | } 160 | } else { 161 | // Only a ASN match was needed 162 | removed = append(removed, brk) 163 | wasRemoved = true 164 | break 165 | } 166 | } 167 | } 168 | 169 | if len(filter.SKI) != 0 && filter.ASN == nil { 170 | // We need to compare just the SKIs then 171 | if skiCache == nil { // We have not yet decoded the ski hex 172 | var err error 173 | skiCache, err = hex.DecodeString(brk.Ski) 174 | if err != nil { 175 | // Ski could not be decoded, so we can't filter 176 | continue 177 | } 178 | } 179 | if bytes.Equal(filter.SKI, skiCache) { 180 | removed = append(removed, brk) 181 | wasRemoved = true 182 | break 183 | } 184 | } 185 | } 186 | 187 | if !wasRemoved { 188 | added = append(added, brk) 189 | } 190 | } 191 | return added, removed 192 | } 193 | 194 | func (s *SlurmLocallyAddedAssertions) AssertVRPs() []VRPJson { 195 | vrps := make([]VRPJson, 0) 196 | if s.PrefixAssertions == nil || len(s.PrefixAssertions) == 0 { 197 | return vrps 198 | } 199 | for _, assertion := range s.PrefixAssertions { 200 | prefix := assertion.GetPrefix() 201 | if prefix == nil { 202 | continue 203 | } 204 | size, _ := prefix.Mask.Size() 205 | maxLength := assertion.MaxPrefixLength 206 | if assertion.MaxPrefixLength <= size { 207 | maxLength = size 208 | } 209 | vrps = append(vrps, VRPJson{ 210 | ASN: uint32(assertion.ASN), 211 | Prefix: assertion.Prefix, 212 | Length: uint8(maxLength), 213 | TA: assertion.Comment, 214 | }) 215 | } 216 | return vrps 217 | } 218 | 219 | func (s *SlurmLocallyAddedAssertions) AssertBRKs() []BgpSecKeyJson { 220 | brks := make([]BgpSecKeyJson, 0) 221 | 222 | if s.BgpsecAssertions == nil || len(s.BgpsecAssertions) == 0 { 223 | return brks 224 | } 225 | for _, assertion := range s.BgpsecAssertions { 226 | hexSki := hex.EncodeToString(assertion.SKI) 227 | brk := BgpSecKeyJson{ 228 | Asn: assertion.ASN, 229 | Pubkey: assertion.RouterPublicKey, 230 | Ski: hexSki, 231 | } 232 | brks = append(brks, brk) 233 | } 234 | return brks 235 | } 236 | 237 | func (s *SlurmConfig) GetAssertions() (vrps []VRPJson, BRKs []BgpSecKeyJson) { 238 | vrps = s.LocallyAddedAssertions.AssertVRPs() 239 | BRKs = s.LocallyAddedAssertions.AssertBRKs() 240 | return 241 | } 242 | 243 | func (s *SlurmConfig) FilterAssert(vrps []VRPJson, BRKs []BgpSecKeyJson, log Logger) ( 244 | ovrps []VRPJson, oBRKs []BgpSecKeyJson) { 245 | // 246 | filteredVRPs, removedVRPs := s.ValidationOutputFilters.FilterOnVRPs(vrps) 247 | filteredBRKs, removedBRKs := s.ValidationOutputFilters.FilterOnBRKs(BRKs) 248 | 249 | assertVRPs, assertBRKs := s.GetAssertions() 250 | 251 | ovrps = append(filteredVRPs, assertVRPs...) 252 | oBRKs = append(filteredBRKs, assertBRKs...) 253 | 254 | if log != nil { 255 | if len(s.ValidationOutputFilters.PrefixFilters) != 0 { 256 | log.Infof("Slurm VRP filtering: %v kept, %v removed, %v asserted", len(filteredVRPs), len(removedVRPs), len(ovrps)) 257 | } 258 | 259 | if len(s.ValidationOutputFilters.BgpsecFilters) != 0 { 260 | log.Infof("Slurm Router Key filtering: %v kept, %v removed, %v asserted", len(filteredBRKs), len(removedBRKs), len(oBRKs)) 261 | } 262 | } 263 | return 264 | } 265 | 266 | type Logger interface { 267 | Debugf(string, ...interface{}) 268 | Printf(string, ...interface{}) 269 | Warnf(string, ...interface{}) 270 | Errorf(string, ...interface{}) 271 | Infof(string, ...interface{}) 272 | } 273 | -------------------------------------------------------------------------------- /lib/server_test.go: -------------------------------------------------------------------------------- 1 | package rtrlib 2 | 3 | import ( 4 | "fmt" 5 | "net/netip" 6 | "runtime" 7 | "testing" 8 | "unsafe" 9 | 10 | "github.com/google/go-cmp/cmp" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func GenerateVrps(size uint32, offset uint32) []SendableData { 15 | vrps := make([]SendableData, size) 16 | for i := uint32(0); i < size; i++ { 17 | ipFinal := i+offset 18 | vrps[i] = &VRP{ 19 | Prefix: netip.MustParsePrefix(fmt.Sprintf("fd00::%04x:%04x/128", ipFinal >> 16, ipFinal & 0xffff)), 20 | MaxLen: 128, 21 | ASN: 64496, 22 | } 23 | } 24 | return vrps 25 | } 26 | 27 | func BaseBench(base int, multiplier int) { 28 | benchSize1 := base * multiplier 29 | newVrps := GenerateVrps(uint32(benchSize1), uint32(0)) 30 | benchSize2 := base 31 | prevVrps := GenerateVrps(uint32(benchSize2), uint32(benchSize1-benchSize2/2)) 32 | ComputeDiff(newVrps, prevVrps, false) 33 | } 34 | 35 | func BenchmarkComputeDiff1000x10(b *testing.B) { 36 | BaseBench(1000, 10) 37 | } 38 | 39 | func BenchmarkComputeDiff10000x10(b *testing.B) { 40 | BaseBench(10000, 10) 41 | } 42 | 43 | func BenchmarkComputeDiff100000x1(b *testing.B) { 44 | BaseBench(100000, 1) 45 | } 46 | 47 | func TestComputeDiff(t *testing.T) { 48 | newVrps := []VRP{ 49 | { 50 | Prefix: netip.MustParsePrefix("fd00::3/128"), 51 | MaxLen: 128, 52 | ASN: 65003, 53 | }, 54 | { 55 | Prefix: netip.MustParsePrefix("fd00::2/128"), 56 | MaxLen: 128, 57 | ASN: 65002, 58 | }, 59 | } 60 | prevVrps := []VRP{ 61 | { 62 | Prefix: netip.MustParsePrefix("fd00::1/128"), 63 | MaxLen: 128, 64 | ASN: 65001, 65 | }, 66 | { 67 | Prefix: netip.MustParsePrefix("fd00::2/128"), 68 | MaxLen: 128, 69 | ASN: 65002, 70 | }, 71 | } 72 | 73 | newVrpsSD, prevVrpsAsSD := make([]SendableData, 0), make([]SendableData, 0) 74 | for _, v := range newVrps { 75 | newVrpsSD = append(newVrpsSD, v.Copy()) 76 | } 77 | for _, v := range prevVrps { 78 | prevVrpsAsSD = append(prevVrpsAsSD, v.Copy()) 79 | } 80 | 81 | added, removed, unchanged := ComputeDiff(newVrpsSD, prevVrpsAsSD, true) 82 | assert.Len(t, added, 1) 83 | assert.Len(t, removed, 1) 84 | assert.Len(t, unchanged, 1) 85 | assert.Equal(t, added[0].(*VRP).ASN, uint32(65003)) 86 | assert.Equal(t, removed[0].(*VRP).ASN, uint32(65001)) 87 | assert.Equal(t, unchanged[0].(*VRP).ASN, uint32(65002)) 88 | } 89 | 90 | func TestApplyDiff(t *testing.T) { 91 | diff := []VRP{ 92 | { 93 | Prefix: netip.MustParsePrefix("fd00::3/128"), 94 | MaxLen: 128, 95 | ASN: 65003, 96 | Flags: FLAG_ADDED, 97 | }, 98 | { 99 | Prefix: netip.MustParsePrefix("fd00::2/128"), 100 | MaxLen: 128, 101 | ASN: 65002, 102 | Flags: FLAG_REMOVED, 103 | }, 104 | { 105 | Prefix: netip.MustParsePrefix("fd00::4/128"), 106 | MaxLen: 128, 107 | ASN: 65004, 108 | Flags: FLAG_REMOVED, 109 | }, 110 | { 111 | Prefix: netip.MustParsePrefix("fd00::6/128"), 112 | MaxLen: 128, 113 | ASN: 65006, 114 | Flags: FLAG_REMOVED, 115 | }, 116 | { 117 | Prefix: netip.MustParsePrefix("fd00::7/128"), 118 | MaxLen: 128, 119 | ASN: 65007, 120 | Flags: FLAG_ADDED, 121 | }, 122 | } 123 | prevVrps := []VRP{ 124 | { 125 | Prefix: netip.MustParsePrefix("fd00::1/128"), 126 | MaxLen: 128, 127 | ASN: 65001, 128 | Flags: FLAG_ADDED, 129 | }, 130 | { 131 | Prefix: netip.MustParsePrefix("fd00::2/128"), 132 | MaxLen: 128, 133 | ASN: 65002, 134 | Flags: FLAG_ADDED, 135 | }, 136 | { 137 | Prefix: netip.MustParsePrefix("fd00::5/128"), 138 | MaxLen: 128, 139 | ASN: 65005, 140 | Flags: FLAG_REMOVED, 141 | }, 142 | { 143 | Prefix: netip.MustParsePrefix("fd00::6/128"), 144 | MaxLen: 128, 145 | ASN: 65006, 146 | Flags: FLAG_REMOVED, 147 | }, 148 | { 149 | Prefix: netip.MustParsePrefix("fd00::7/128"), 150 | MaxLen: 128, 151 | ASN: 65007, 152 | Flags: FLAG_REMOVED, 153 | }, 154 | } 155 | diffSD, prevVrpsAsSD := make([]SendableData, 0), make([]SendableData, 0) 156 | for _, v := range diff { 157 | diffSD = append(diffSD, v.Copy()) 158 | } 159 | for _, v := range prevVrps { 160 | prevVrpsAsSD = append(prevVrpsAsSD, v.Copy()) 161 | } 162 | 163 | vrps := ApplyDiff(diffSD, prevVrpsAsSD) 164 | 165 | assert.Len(t, vrps, 6) 166 | assert.Equal(t, vrps[0].(*VRP).ASN, uint32(65001)) 167 | assert.Equal(t, vrps[0].(*VRP).GetFlag(), uint8(FLAG_ADDED)) 168 | assert.Equal(t, vrps[1].(*VRP).ASN, uint32(65005)) 169 | assert.Equal(t, vrps[1].(*VRP).GetFlag(), uint8(FLAG_REMOVED)) 170 | assert.Equal(t, vrps[2].(*VRP).ASN, uint32(65003)) 171 | assert.Equal(t, vrps[2].(*VRP).GetFlag(), uint8(FLAG_ADDED)) 172 | assert.Equal(t, vrps[3].(*VRP).ASN, uint32(65004)) 173 | assert.Equal(t, vrps[3].(*VRP).GetFlag(), uint8(FLAG_REMOVED)) 174 | assert.Equal(t, vrps[4].(*VRP).ASN, uint32(65006)) 175 | assert.Equal(t, vrps[4].(*VRP).GetFlag(), uint8(FLAG_REMOVED)) 176 | assert.Equal(t, vrps[5].(*VRP).ASN, uint32(65007)) 177 | assert.Equal(t, vrps[5].(*VRP).GetFlag(), uint8(FLAG_ADDED)) 178 | } 179 | 180 | func TestComputeDiffBGPSEC(t *testing.T) { 181 | newVrps := []BgpsecKey{ 182 | { 183 | ASN: 65003, 184 | Pubkey: []byte("hurr"), 185 | Ski: []byte("durr"), 186 | }, 187 | { 188 | Pubkey: []byte("abc"), 189 | Ski: []byte("dce"), 190 | ASN: 65002, 191 | }, 192 | } 193 | prevVrps := []BgpsecKey{ 194 | { 195 | Pubkey: []byte("murr"), 196 | Ski: []byte("durr"), 197 | ASN: 65001, 198 | }, 199 | { 200 | Pubkey: []byte("abc"), 201 | Ski: []byte("dce"), 202 | ASN: 65002, 203 | }, 204 | } 205 | 206 | newVrpsSD, prevVrpsAsSD := make([]SendableData, 0), make([]SendableData, 0) 207 | for _, v := range newVrps { 208 | newVrpsSD = append(newVrpsSD, v.Copy()) 209 | } 210 | for _, v := range prevVrps { 211 | prevVrpsAsSD = append(prevVrpsAsSD, v.Copy()) 212 | } 213 | 214 | added, removed, unchanged := ComputeDiff(newVrpsSD, prevVrpsAsSD, true) 215 | assert.Len(t, added, 1) 216 | assert.Len(t, removed, 1) 217 | assert.Len(t, unchanged, 1) 218 | assert.Equal(t, added[0].(*BgpsecKey).ASN, uint32(65003)) 219 | assert.Equal(t, removed[0].(*BgpsecKey).ASN, uint32(65001)) 220 | assert.Equal(t, unchanged[0].(*BgpsecKey).ASN, uint32(65002)) 221 | } 222 | 223 | func TestVRPStructSize(t *testing.T) { 224 | if a := runtime.GOARCH; a != "amd64" { 225 | t.Skipf("skipping, running on %s but this test is hard-coded for amd64 architecture", a) 226 | } 227 | 228 | // This test verifies that the size of a VRP and its component structures 229 | // do not change unexpectedly due to other code modifications. 230 | // 231 | // For reference, the tool structlayout can be used to examine struct sizes 232 | // and structlayout-optimize to recommend ordering of members to minimize memory utilization. 233 | // Whenever a constant is changed here, be sure to update the associated 234 | // comment with the output of the tools. 235 | // 236 | // $ go install honnef.co/go/tools/cmd/structlayout@latest 237 | // $ go install honnef.co/go/tools/cmd/structlayout-optimize@latest 238 | 239 | const ( 240 | // $ structlayout -json . VRP 241 | // VRP.Prefix.ip.addr.hi uint64: 0-8 (size 8, align 8) 242 | // VRP.Prefix.ip.addr.lo uint64: 8-16 (size 8, align 8) 243 | // VRP.Prefix.ip.z *internal/intern.Value: 16-24 (size 8, align 8) 244 | // VRP.Prefix.bitsPlusOne uint8: 24-25 (size 1, align 1) 245 | // padding: 25-32 (size 7, align 0) 246 | // VRP.ASN uint32: 32-36 (size 4, align 4) 247 | // VRP.MaxLen uint8: 36-37 (size 1, align 1) 248 | // VRP.Flags uint8: 37-38 (size 1, align 1) 249 | // padding: 38-40 (size 2, align 0) 250 | // 251 | // NOTE: we could actually reduce this to 32 if netip.Prefix 252 | // were properly aligned. Ex: 253 | // $ structlayout -json . VRP | structlayout-optimize 254 | // VRP.Prefix struct: 0-24 (size 24, align 8) 255 | // VRP.ASN uint32: 24-28 (size 4, align 4) 256 | // VRP.MaxLen uint8: 28-29 (size 1, align 1) 257 | // VRP.Flags uint8: 29-30 (size 1, align 1) 258 | // padding: 30-32 (size 2, align 0) 259 | // 260 | vrpSize = 40 261 | ) 262 | 263 | if diff := cmp.Diff(int(unsafe.Sizeof(VRP{})), vrpSize); diff != "" { 264 | t.Fatalf("unexpected VRPs struct size (-want +got):\n%s", diff) 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /prefixfile/slurm_test.go: -------------------------------------------------------------------------------- 1 | package prefixfile 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | "os" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestDecodeJSONSlurm(t *testing.T) { 14 | json, err := os.Open("slurm.json") 15 | if err != nil { 16 | panic(err) 17 | } 18 | decoded, err := DecodeJSONSlurm(json) 19 | if err != nil { 20 | t.Errorf("Unable to decode json: %v", err) 21 | } 22 | assert.Nil(t, err) 23 | asn, _ := decoded.ValidationOutputFilters.PrefixFilters[1].GetASN() 24 | _, asnEmpty := decoded.ValidationOutputFilters.PrefixFilters[0].GetASN() 25 | assert.Equal(t, uint32(64496), asn) 26 | assert.True(t, asnEmpty) 27 | assert.Equal(t, "192.0.2.0/24", decoded.ValidationOutputFilters.PrefixFilters[0].Prefix) 28 | } 29 | 30 | func TestFilterOnVRPs(t *testing.T) { 31 | vrps := []VRPJson{ 32 | { 33 | ASN: uint32(65001), 34 | Prefix: "192.168.0.0/25", 35 | Length: 25, 36 | }, 37 | { 38 | ASN: uint32(65002), 39 | Prefix: "192.168.1.0/24", 40 | Length: 24, 41 | }, 42 | { 43 | ASN: uint32(65003), 44 | Prefix: "192.168.2.0/24", 45 | Length: 24, 46 | }, 47 | { 48 | ASN: uint32(65004), 49 | Prefix: "10.0.0.0/24", 50 | Length: 24, 51 | }, 52 | { 53 | ASN: uint32(65005), 54 | Prefix: "10.1.0.0/24", 55 | Length: 16, // this VRP is broken, maxlength can't be smaller than plen 56 | }, 57 | } 58 | 59 | asA, asB := uint32(65001), uint32(65002) 60 | slurm := SlurmValidationOutputFilters{ 61 | PrefixFilters: []SlurmPrefixFilter{ 62 | { 63 | Prefix: "10.0.0.0/8", 64 | }, 65 | { 66 | ASN: &asA, 67 | Prefix: "192.168.0.0/24", 68 | }, 69 | { 70 | ASN: &asB, 71 | }, 72 | }, 73 | } 74 | added, removed := slurm.FilterOnVRPs(vrps) 75 | assert.Len(t, added, 1) 76 | assert.Len(t, removed, 4) 77 | assert.Equal(t, uint32(65001), removed[0].GetASN()) 78 | assert.Equal(t, uint32(65005), removed[3].GetASN()) 79 | } 80 | 81 | func TestAssertVRPs(t *testing.T) { 82 | slurm := SlurmLocallyAddedAssertions{ 83 | PrefixAssertions: []SlurmPrefixAssertion{ 84 | { 85 | ASN: uint32(65001), 86 | Prefix: "10.0.0.0/8", 87 | Comment: "Hello", 88 | }, 89 | { 90 | ASN: uint32(65001), 91 | Prefix: "192.168.0.0/24", 92 | }, 93 | { 94 | ASN: uint32(65003), 95 | Prefix: "192.168.0.0/25", 96 | MaxPrefixLength: 26, 97 | }, 98 | }, 99 | } 100 | vrps := slurm.AssertVRPs() 101 | assert.Len(t, vrps, 3) 102 | } 103 | 104 | func TestFilterOnBSKs(t *testing.T) { 105 | vrps := []BgpSecKeyJson{ 106 | { 107 | Asn: 65001, 108 | Pubkey: []byte{ 109 | 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 110 | 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, 0x04, 0x80, 0x57, 0x23, 0x43, 0xf8, 111 | 0x3f, 0xfc, 0xb0, 0x10, 0x7a, 0xb0, 0x07, 0xd8, 0xca, 0x69, 0xf8, 0x6b, 0x9c, 0xa0, 0x30, 0x06, 112 | 0x05, 0xb8, 0x48, 0xa8, 0x3d, 0xf7, 0xc0, 0xd3, 0xec, 0x5f, 0x19, 0xc0, 0x19, 0xbf, 0xa6, 0xb5, 113 | 0x9e, 0xd7, 0x42, 0xb5, 0x4e, 0xf4, 0x34, 0x3a, 0x52, 0x50, 0x12, 0x86, 0xd8, 0xa0, 0xe7, 0xe4, 114 | 0x1f, 0x10, 0xaa, 0x53, 0xb4, 0x58, 0x22, 0xa9, 0xf8, 0x80, 0x15, 115 | }, 116 | Ski: "5d4250e2d81d4448d8a29efce91d29ff075ec9e2", 117 | }, 118 | { 119 | Asn: 65003, 120 | Pubkey: []byte{ 121 | 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 122 | 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, 0x04, 0xe0, 0x5c, 0x49, 0xaf, 0x49, 123 | 0xf6, 0x6e, 0xec, 0x75, 0xb9, 0x7d, 0x44, 0xbe, 0x5f, 0x90, 0x5b, 0x06, 0x58, 0xbc, 0x86, 0x9d, 124 | 0x3e, 0x32, 0xee, 0x15, 0x7d, 0xa6, 0xc6, 0xa2, 0xae, 0x00, 0x65, 0x21, 0x2a, 0x7a, 0xfb, 0x54, 125 | 0xb2, 0xc3, 0x82, 0xb1, 0x3e, 0xfa, 0x5f, 0x69, 0xe5, 0xe1, 0xf6, 0x91, 0x64, 0xcd, 0x54, 0x03, 126 | 0x76, 0xd8, 0x55, 0x14, 0xdd, 0xd6, 0xff, 0x44, 0xaa, 0x44, 0xdb, 127 | }, 128 | Ski: "be889b55d0b737397d75c49f485b858fa98ad11f", 129 | }, 130 | { 131 | Asn: 65002, 132 | Pubkey: []byte{ 133 | 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 134 | 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, 0x04, 0x86, 0xfe, 0x47, 0x10, 0x11, 135 | 0xa2, 0xc5, 0x48, 0xca, 0x25, 0x39, 0x5e, 0x9e, 0xf7, 0x03, 0xd4, 0x0c, 0x72, 0x8b, 0x4e, 0xeb, 136 | 0x15, 0xd5, 0x58, 0xd4, 0xa8, 0x4d, 0xe2, 0xf3, 0x0f, 0x63, 0x2e, 0x72, 0xd0, 0xcc, 0x7a, 0xcd, 137 | 0xf6, 0xa2, 0x12, 0xa2, 0x4d, 0xdb, 0xb8, 0xca, 0xfe, 0x5e, 0xb5, 0xc4, 0x2d, 0xfa, 0x56, 0xc6, 138 | 0x9e, 0xcd, 0xde, 0xde, 0x5c, 0x0b, 0x19, 0xd4, 0x01, 0x04, 0xb1, 139 | }, 140 | Ski: "510f485d29a29db7b515f9c478f8ed3cb7aa7d23", 141 | }, 142 | { 143 | Asn: 65004, 144 | Pubkey: []byte{ 145 | 0x30, 0x59, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 146 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 147 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 148 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 149 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 150 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 151 | }, 152 | Ski: "111b485d29a29db7b515f9c471e1ed3cb7bb7dee", 153 | }, 154 | { 155 | Asn: 65005, 156 | Pubkey: []byte{ 157 | 0x30, 0x59, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 158 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 159 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 160 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 161 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 162 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 163 | }, 164 | Ski: "111b485d29a29db7b515f9c471e1ed3cb7bb7dee", 165 | }, 166 | } 167 | 168 | asA, asB := uint32(65001), uint32(65005) 169 | slurm := SlurmValidationOutputFilters{ 170 | BgpsecFilters: []SlurmBGPsecFilter{ 171 | { 172 | SKI: []byte{0xbe, 0x88, 0x9b, 0x55, 0xd0, 0xb7, 0x37, 0x39, 0x7d, 0x75, 0xc4, 0x9f, 0x48, 0x5b, 0x85, 0x8f, 0xa9, 0x8a, 0xd1, 0x1f}, 173 | }, 174 | { 175 | ASN: &asA, 176 | }, 177 | { 178 | SKI: []byte{0x11, 0x1b, 0x48, 0x5d, 0x29, 0xa2, 0x9d, 0xb7, 0xb5, 0x15, 0xf9, 0xc4, 0x71, 0xe1, 0xed, 0x3c, 0xb7, 0xbb, 0x7d, 0xee}, 179 | ASN: &asB, 180 | }, 181 | }, 182 | } 183 | added, removed := slurm.FilterOnBRKs(vrps) 184 | assert.Len(t, added, 2) 185 | assert.Len(t, removed, 3) 186 | assert.Equal(t, "5d4250e2d81d4448d8a29efce91d29ff075ec9e2", removed[0].Ski) 187 | assert.Equal(t, "be889b55d0b737397d75c49f485b858fa98ad11f", removed[1].Ski) 188 | assert.Equal(t, "111b485d29a29db7b515f9c471e1ed3cb7bb7dee", removed[2].Ski) 189 | assert.Equal(t, uint32(65005), removed[2].Asn) 190 | } 191 | 192 | func TestSlurmEndToEnd(t *testing.T) { 193 | slurmfd, err := os.Open("slurm.json") 194 | if err != nil { 195 | panic(err) 196 | } 197 | config, err := DecodeJSONSlurm(slurmfd) 198 | if err != nil { 199 | t.Errorf("Unable to decode json: %v", err) 200 | } 201 | 202 | rpkifd, err := os.Open("test.rpki.json") 203 | if err != nil { 204 | panic(err) 205 | } 206 | rpkidata, err := io.ReadAll(rpkifd) 207 | if err != nil { 208 | panic(err) 209 | } 210 | vrplist, err := decodeJSON(rpkidata) 211 | if err != nil { 212 | panic(err) 213 | } 214 | 215 | finalVRP, finalBgpsec := config.FilterAssert(vrplist.ROA, vrplist.BgpSecKeys, nil) 216 | 217 | foundAssertVRP := false 218 | for _, vrps := range finalVRP { 219 | if vrps.Prefix == "192.0.2.0/24" { 220 | t.Fatalf("Found filtered VRP") 221 | } 222 | 223 | if vrps.Prefix == "198.51.100.0/24" { 224 | foundAssertVRP = true 225 | } 226 | } 227 | if !foundAssertVRP { 228 | t.Fatalf("Did not find asserted VRP") 229 | } 230 | 231 | foundAssertBRK := false 232 | for _, brks := range finalBgpsec { 233 | if brks.Ski == "510f485d29a29db7b515f9c478f8ed3cb7aa7d23" { 234 | t.FailNow() 235 | } 236 | if brks.Ski == "3506176743e02f67dd46c73119d5436be7e10106" { 237 | foundAssertBRK = true 238 | } 239 | } 240 | if !foundAssertBRK { 241 | t.Fatalf("Did not find asserted BR") 242 | } 243 | } 244 | 245 | func decodeJSON(data []byte) (*RPKIList, error) { 246 | buf := bytes.NewBuffer(data) 247 | dec := json.NewDecoder(buf) 248 | 249 | var vrplistjson RPKIList 250 | err := dec.Decode(&vrplistjson) 251 | return &vrplistjson, err 252 | } 253 | -------------------------------------------------------------------------------- /lib/structs.go: -------------------------------------------------------------------------------- 1 | package rtrlib 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/hex" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "net/netip" 11 | ) 12 | 13 | type Logger interface { 14 | Debugf(string, ...interface{}) 15 | Printf(string, ...interface{}) 16 | Warnf(string, ...interface{}) 17 | Errorf(string, ...interface{}) 18 | Infof(string, ...interface{}) 19 | } 20 | 21 | const ( 22 | APP_VERSION = "0.6.2" 23 | 24 | // We use the size of the largest sensible PDU. 25 | // 26 | // We ignore the theoretically unbounded length of SKIs for router keys. 27 | // RPs should validate that this has the correct length. 28 | // 29 | messageMaxSize = 262168 30 | 31 | PROTOCOL_VERSION_0 = 0 32 | PROTOCOL_VERSION_1 = 1 33 | 34 | PDU_ID_SERIAL_NOTIFY = 0 35 | PDU_ID_SERIAL_QUERY = 1 36 | PDU_ID_RESET_QUERY = 2 37 | PDU_ID_CACHE_RESPONSE = 3 38 | PDU_ID_IPV4_PREFIX = 4 39 | PDU_ID_IPV6_PREFIX = 6 40 | PDU_ID_END_OF_DATA = 7 41 | PDU_ID_CACHE_RESET = 8 42 | PDU_ID_ROUTER_KEY = 9 43 | PDU_ID_ERROR_REPORT = 10 44 | 45 | FLAG_ADDED = 1 46 | FLAG_REMOVED = 0 47 | 48 | PDU_ERROR_CORRUPTDATA = 0 49 | PDU_ERROR_INTERNALERR = 1 50 | PDU_ERROR_NODATA = 2 51 | PDU_ERROR_INVALIDREQUEST = 3 52 | PDU_ERROR_BADPROTOVERSION = 4 53 | PDU_ERROR_BADPDUTYPE = 5 54 | PDU_ERROR_WITHDRAWUNKNOWN = 6 55 | PDU_ERROR_DUPANNOUNCE = 7 56 | 57 | AFI_IPv4 = uint8(0) 58 | AFI_IPv6 = uint8(1) 59 | 60 | TYPE_UNKNOWN = iota 61 | TYPE_PLAIN 62 | TYPE_TLS 63 | TYPE_SSH 64 | ) 65 | 66 | type PDU interface { 67 | Bytes() []byte 68 | Write(io.Writer) 69 | String() string 70 | SetVersion(uint8) 71 | GetVersion() uint8 72 | GetType() uint8 73 | } 74 | 75 | func TypeToString(t uint8) string { 76 | switch t { 77 | case PDU_ID_SERIAL_NOTIFY: 78 | return "Serial Notify" 79 | case PDU_ID_SERIAL_QUERY: 80 | return "Serial Query" 81 | case PDU_ID_RESET_QUERY: 82 | return "Reset Query" 83 | case PDU_ID_CACHE_RESPONSE: 84 | return "Cache Response" 85 | case PDU_ID_IPV4_PREFIX: 86 | return "IPv4 Prefix" 87 | case PDU_ID_IPV6_PREFIX: 88 | return "IPv6 Prefix" 89 | case PDU_ID_END_OF_DATA: 90 | return "End of Data" 91 | case PDU_ID_CACHE_RESET: 92 | return "Cache Reset" 93 | case PDU_ID_ROUTER_KEY: 94 | return "Router Key" 95 | case PDU_ID_ERROR_REPORT: 96 | return "Error Report" 97 | default: 98 | return fmt.Sprintf("Unknown type %d", t) 99 | } 100 | } 101 | 102 | func IsCorrectPDUVersion(pdu PDU, version uint8) bool { 103 | if version > 1 { 104 | return false 105 | } 106 | switch pdu.(type) { 107 | case *PDURouterKey: 108 | if version == 0 { 109 | return false 110 | } 111 | } 112 | return true 113 | } 114 | 115 | type PDUSerialNotify struct { 116 | Version uint8 117 | SessionId uint16 118 | SerialNumber uint32 119 | } 120 | 121 | func (pdu *PDUSerialNotify) String() string { 122 | return fmt.Sprintf("PDU Serial Notify v%d (session: %d): serial: %d", pdu.Version, pdu.SessionId, pdu.SerialNumber) 123 | } 124 | 125 | func (pdu *PDUSerialNotify) Bytes() []byte { 126 | b := bytes.NewBuffer([]byte{}) 127 | pdu.Write(b) 128 | return b.Bytes() 129 | } 130 | 131 | func (pdu *PDUSerialNotify) SetVersion(version uint8) { 132 | pdu.Version = version 133 | } 134 | 135 | func (pdu *PDUSerialNotify) GetVersion() uint8 { 136 | return pdu.Version 137 | } 138 | 139 | func (pdu *PDUSerialNotify) GetType() uint8 { 140 | return PDU_ID_SERIAL_NOTIFY 141 | } 142 | 143 | func (pdu *PDUSerialNotify) Write(wr io.Writer) { 144 | binary.Write(wr, binary.BigEndian, uint8(pdu.Version)) 145 | binary.Write(wr, binary.BigEndian, uint8(PDU_ID_SERIAL_NOTIFY)) 146 | binary.Write(wr, binary.BigEndian, pdu.SessionId) 147 | binary.Write(wr, binary.BigEndian, uint32(12)) 148 | binary.Write(wr, binary.BigEndian, uint32(pdu.SerialNumber)) 149 | } 150 | 151 | type PDUSerialQuery struct { 152 | Version uint8 153 | SessionId uint16 154 | SerialNumber uint32 155 | } 156 | 157 | func (pdu *PDUSerialQuery) String() string { 158 | return fmt.Sprintf("PDU Serial Query v%d (session: %d): serial: %d", pdu.Version, pdu.SessionId, pdu.SerialNumber) 159 | } 160 | 161 | func (pdu *PDUSerialQuery) Bytes() []byte { 162 | b := bytes.NewBuffer([]byte{}) 163 | pdu.Write(b) 164 | return b.Bytes() 165 | } 166 | 167 | func (pdu *PDUSerialQuery) SetVersion(version uint8) { 168 | pdu.Version = version 169 | } 170 | 171 | func (pdu *PDUSerialQuery) GetVersion() uint8 { 172 | return pdu.Version 173 | } 174 | 175 | func (pdu *PDUSerialQuery) GetType() uint8 { 176 | return PDU_ID_SERIAL_QUERY 177 | } 178 | 179 | func (pdu *PDUSerialQuery) Write(wr io.Writer) { 180 | binary.Write(wr, binary.BigEndian, uint8(pdu.Version)) 181 | binary.Write(wr, binary.BigEndian, uint8(PDU_ID_SERIAL_QUERY)) 182 | binary.Write(wr, binary.BigEndian, pdu.SessionId) 183 | binary.Write(wr, binary.BigEndian, uint32(12)) 184 | binary.Write(wr, binary.BigEndian, uint32(pdu.SerialNumber)) 185 | } 186 | 187 | type PDUResetQuery struct { 188 | Version uint8 189 | } 190 | 191 | func (pdu *PDUResetQuery) String() string { 192 | return fmt.Sprintf("PDU Reset Query v%d", pdu.Version) 193 | } 194 | 195 | func (pdu *PDUResetQuery) Bytes() []byte { 196 | b := bytes.NewBuffer([]byte{}) 197 | pdu.Write(b) 198 | return b.Bytes() 199 | } 200 | 201 | func (pdu *PDUResetQuery) SetVersion(version uint8) { 202 | pdu.Version = version 203 | } 204 | 205 | func (pdu *PDUResetQuery) GetVersion() uint8 { 206 | return pdu.Version 207 | } 208 | 209 | func (pdu *PDUResetQuery) GetType() uint8 { 210 | return PDU_ID_RESET_QUERY 211 | } 212 | 213 | func (pdu *PDUResetQuery) Write(wr io.Writer) { 214 | binary.Write(wr, binary.BigEndian, uint8(pdu.Version)) 215 | binary.Write(wr, binary.BigEndian, uint8(PDU_ID_RESET_QUERY)) 216 | binary.Write(wr, binary.BigEndian, uint16(0)) 217 | binary.Write(wr, binary.BigEndian, uint32(8)) 218 | } 219 | 220 | type PDUCacheResponse struct { 221 | Version uint8 222 | SessionId uint16 223 | } 224 | 225 | func (pdu *PDUCacheResponse) String() string { 226 | return fmt.Sprintf("PDU Cache Response v%d (session: %d)", pdu.Version, pdu.SessionId) 227 | } 228 | 229 | func (pdu *PDUCacheResponse) Bytes() []byte { 230 | b := bytes.NewBuffer([]byte{}) 231 | pdu.Write(b) 232 | return b.Bytes() 233 | } 234 | 235 | func (pdu *PDUCacheResponse) SetVersion(version uint8) { 236 | pdu.Version = version 237 | } 238 | 239 | func (pdu *PDUCacheResponse) GetVersion() uint8 { 240 | return pdu.Version 241 | } 242 | 243 | func (pdu *PDUCacheResponse) GetType() uint8 { 244 | return PDU_ID_CACHE_RESPONSE 245 | } 246 | 247 | func (pdu *PDUCacheResponse) Write(wr io.Writer) { 248 | binary.Write(wr, binary.BigEndian, uint8(pdu.Version)) 249 | binary.Write(wr, binary.BigEndian, uint8(PDU_ID_CACHE_RESPONSE)) 250 | binary.Write(wr, binary.BigEndian, pdu.SessionId) 251 | binary.Write(wr, binary.BigEndian, uint32(8)) 252 | } 253 | 254 | type PDUIPv4Prefix struct { 255 | Prefix netip.Prefix 256 | ASN uint32 257 | Version uint8 258 | MaxLen uint8 259 | Flags uint8 260 | } 261 | 262 | func (pdu *PDUIPv4Prefix) String() string { 263 | return fmt.Sprintf("PDU IPv4 Prefix v%d %s(->/%d), origin: AS%d, flags: %d", pdu.Version, pdu.Prefix.String(), pdu.MaxLen, pdu.ASN, pdu.Flags) 264 | } 265 | 266 | func (pdu *PDUIPv4Prefix) Bytes() []byte { 267 | b := bytes.NewBuffer([]byte{}) 268 | pdu.Write(b) 269 | return b.Bytes() 270 | } 271 | 272 | func (pdu *PDUIPv4Prefix) SetVersion(version uint8) { 273 | pdu.Version = version 274 | } 275 | 276 | func (pdu *PDUIPv4Prefix) GetVersion() uint8 { 277 | return pdu.Version 278 | } 279 | 280 | func (pdu *PDUIPv4Prefix) GetType() uint8 { 281 | return PDU_ID_IPV4_PREFIX 282 | } 283 | 284 | func (pdu *PDUIPv4Prefix) Write(wr io.Writer) { 285 | binary.Write(wr, binary.BigEndian, uint8(pdu.Version)) 286 | binary.Write(wr, binary.BigEndian, uint8(PDU_ID_IPV4_PREFIX)) 287 | binary.Write(wr, binary.BigEndian, uint16(0)) 288 | binary.Write(wr, binary.BigEndian, uint32(20)) 289 | binary.Write(wr, binary.BigEndian, pdu.Flags) 290 | binary.Write(wr, binary.BigEndian, uint8(pdu.Prefix.Bits())) 291 | binary.Write(wr, binary.BigEndian, pdu.MaxLen) 292 | binary.Write(wr, binary.BigEndian, uint8(0)) 293 | binary.Write(wr, binary.BigEndian, pdu.Prefix.Addr().As4()) 294 | binary.Write(wr, binary.BigEndian, pdu.ASN) 295 | } 296 | 297 | type PDUIPv6Prefix struct { 298 | Prefix netip.Prefix 299 | ASN uint32 300 | Version uint8 301 | MaxLen uint8 302 | Flags uint8 303 | } 304 | 305 | func (pdu *PDUIPv6Prefix) String() string { 306 | return fmt.Sprintf("PDU IPv6 Prefix v%d %s(->/%d), origin: AS%d, flags: %d", pdu.Version, pdu.Prefix.String(), pdu.MaxLen, pdu.ASN, pdu.Flags) 307 | } 308 | 309 | func (pdu *PDUIPv6Prefix) Bytes() []byte { 310 | b := bytes.NewBuffer([]byte{}) 311 | pdu.Write(b) 312 | return b.Bytes() 313 | } 314 | 315 | func (pdu *PDUIPv6Prefix) SetVersion(version uint8) { 316 | pdu.Version = version 317 | } 318 | 319 | func (pdu *PDUIPv6Prefix) GetVersion() uint8 { 320 | return pdu.Version 321 | } 322 | 323 | func (pdu *PDUIPv6Prefix) GetType() uint8 { 324 | return PDU_ID_IPV6_PREFIX 325 | } 326 | 327 | func (pdu *PDUIPv6Prefix) Write(wr io.Writer) { 328 | binary.Write(wr, binary.BigEndian, uint8(pdu.Version)) 329 | binary.Write(wr, binary.BigEndian, uint8(PDU_ID_IPV6_PREFIX)) 330 | binary.Write(wr, binary.BigEndian, uint16(0)) 331 | binary.Write(wr, binary.BigEndian, uint32(32)) 332 | binary.Write(wr, binary.BigEndian, pdu.Flags) 333 | binary.Write(wr, binary.BigEndian, uint8(pdu.Prefix.Bits())) 334 | binary.Write(wr, binary.BigEndian, pdu.MaxLen) 335 | binary.Write(wr, binary.BigEndian, uint8(0)) 336 | binary.Write(wr, binary.BigEndian, pdu.Prefix.Addr().As16()) 337 | binary.Write(wr, binary.BigEndian, pdu.ASN) 338 | } 339 | 340 | type PDUEndOfData struct { 341 | Version uint8 342 | SessionId uint16 343 | SerialNumber uint32 344 | 345 | RefreshInterval uint32 346 | RetryInterval uint32 347 | ExpireInterval uint32 348 | } 349 | 350 | func (pdu *PDUEndOfData) String() string { 351 | return fmt.Sprintf("PDU End of Data v%d (session: %d): serial: %d, refresh: %d, retry: %d, expire: %d", 352 | pdu.Version, pdu.SessionId, pdu.SerialNumber, pdu.RefreshInterval, pdu.RetryInterval, pdu.ExpireInterval) 353 | } 354 | 355 | func (pdu *PDUEndOfData) Bytes() []byte { 356 | b := bytes.NewBuffer([]byte{}) 357 | pdu.Write(b) 358 | return b.Bytes() 359 | } 360 | 361 | func (pdu *PDUEndOfData) SetVersion(version uint8) { 362 | pdu.Version = version 363 | } 364 | 365 | func (pdu *PDUEndOfData) GetVersion() uint8 { 366 | return pdu.Version 367 | } 368 | 369 | func (pdu *PDUEndOfData) GetType() uint8 { 370 | return PDU_ID_END_OF_DATA 371 | } 372 | 373 | func (pdu *PDUEndOfData) Write(wr io.Writer) { 374 | binary.Write(wr, binary.BigEndian, uint8(pdu.Version)) 375 | binary.Write(wr, binary.BigEndian, uint8(PDU_ID_END_OF_DATA)) 376 | binary.Write(wr, binary.BigEndian, pdu.SessionId) 377 | 378 | if pdu.Version == PROTOCOL_VERSION_0 { 379 | binary.Write(wr, binary.BigEndian, uint32(12)) 380 | binary.Write(wr, binary.BigEndian, pdu.SerialNumber) 381 | } else { 382 | binary.Write(wr, binary.BigEndian, uint32(24)) 383 | binary.Write(wr, binary.BigEndian, pdu.SerialNumber) 384 | binary.Write(wr, binary.BigEndian, pdu.RefreshInterval) 385 | binary.Write(wr, binary.BigEndian, pdu.RetryInterval) 386 | binary.Write(wr, binary.BigEndian, pdu.ExpireInterval) 387 | } 388 | } 389 | 390 | type PDUCacheReset struct { 391 | Version uint8 392 | } 393 | 394 | func (pdu *PDUCacheReset) String() string { 395 | return fmt.Sprintf("PDU Cache Reset v%d", pdu.Version) 396 | } 397 | 398 | func (pdu *PDUCacheReset) Bytes() []byte { 399 | b := bytes.NewBuffer([]byte{}) 400 | pdu.Write(b) 401 | return b.Bytes() 402 | } 403 | 404 | func (pdu *PDUCacheReset) SetVersion(version uint8) { 405 | pdu.Version = version 406 | } 407 | 408 | func (pdu *PDUCacheReset) GetVersion() uint8 { 409 | return pdu.Version 410 | } 411 | 412 | func (pdu *PDUCacheReset) GetType() uint8 { 413 | return PDU_ID_CACHE_RESET 414 | } 415 | 416 | func (pdu *PDUCacheReset) Write(wr io.Writer) { 417 | binary.Write(wr, binary.BigEndian, uint8(pdu.Version)) 418 | binary.Write(wr, binary.BigEndian, uint8(PDU_ID_CACHE_RESET)) 419 | binary.Write(wr, binary.BigEndian, uint16(0)) 420 | binary.Write(wr, binary.BigEndian, uint32(8)) 421 | } 422 | 423 | type PDURouterKey struct { 424 | Version uint8 425 | Flags uint8 426 | SubjectKeyIdentifier []byte 427 | ASN uint32 428 | SubjectPublicKeyInfo []byte 429 | } 430 | 431 | func (pdu *PDURouterKey) String() string { 432 | return "PDU Router Key" 433 | } 434 | 435 | func (pdu *PDURouterKey) Bytes() []byte { 436 | b := bytes.NewBuffer([]byte{}) 437 | pdu.Write(b) 438 | return b.Bytes() 439 | } 440 | 441 | func (pdu *PDURouterKey) SetVersion(version uint8) { 442 | pdu.Version = version 443 | } 444 | 445 | func (pdu *PDURouterKey) GetVersion() uint8 { 446 | return pdu.Version 447 | } 448 | 449 | func (pdu *PDURouterKey) GetType() uint8 { 450 | return PDU_ID_ROUTER_KEY 451 | } 452 | 453 | func (pdu *PDURouterKey) Write(wr io.Writer) { 454 | if len(pdu.SubjectKeyIdentifier) != 20 { 455 | return 456 | } 457 | 458 | binary.Write(wr, binary.BigEndian, uint8(pdu.Version)) 459 | binary.Write(wr, binary.BigEndian, uint8(PDU_ID_ROUTER_KEY)) 460 | binary.Write(wr, binary.BigEndian, uint8(pdu.Flags)) 461 | binary.Write(wr, binary.BigEndian, uint8(0)) 462 | binary.Write(wr, binary.BigEndian, uint32(32+len(pdu.SubjectPublicKeyInfo))) 463 | wr.Write(pdu.SubjectKeyIdentifier) 464 | binary.Write(wr, binary.BigEndian, pdu.ASN) 465 | wr.Write(pdu.SubjectPublicKeyInfo) 466 | } 467 | 468 | type PDUErrorReport struct { 469 | Version uint8 470 | ErrorCode uint16 471 | PDUCopy []byte 472 | ErrorMsg string 473 | } 474 | 475 | func (pdu *PDUErrorReport) String() string { 476 | return fmt.Sprintf("PDU Error report v%d (error code: %d): bytes PDU copy (%d): %s. Message: %s", pdu.Version, pdu.ErrorCode, len(pdu.PDUCopy), hex.EncodeToString(pdu.PDUCopy), pdu.ErrorMsg) 477 | } 478 | 479 | func (pdu *PDUErrorReport) Bytes() []byte { 480 | b := bytes.NewBuffer([]byte{}) 481 | pdu.Write(b) 482 | return b.Bytes() 483 | } 484 | 485 | func (pdu *PDUErrorReport) SetVersion(version uint8) { 486 | pdu.Version = version 487 | } 488 | 489 | func (pdu *PDUErrorReport) GetVersion() uint8 { 490 | return pdu.Version 491 | } 492 | 493 | func (pdu *PDUErrorReport) GetType() uint8 { 494 | return PDU_ID_ERROR_REPORT 495 | } 496 | 497 | func (pdu *PDUErrorReport) Write(wr io.Writer) { 498 | nonnull := (pdu.ErrorMsg != "") 499 | addlen := 0 500 | if nonnull { 501 | addlen = 1 502 | } 503 | 504 | binary.Write(wr, binary.BigEndian, uint8(pdu.Version)) 505 | binary.Write(wr, binary.BigEndian, uint8(PDU_ID_ERROR_REPORT)) 506 | binary.Write(wr, binary.BigEndian, pdu.ErrorCode) 507 | binary.Write(wr, binary.BigEndian, uint32(12+len(pdu.PDUCopy)+4+len(pdu.ErrorMsg)+addlen)) 508 | binary.Write(wr, binary.BigEndian, uint32(len(pdu.PDUCopy))) 509 | binary.Write(wr, binary.BigEndian, pdu.PDUCopy) 510 | binary.Write(wr, binary.BigEndian, uint32(len(pdu.ErrorMsg)+addlen)) 511 | if nonnull { 512 | binary.Write(wr, binary.BigEndian, []byte(pdu.ErrorMsg)) 513 | binary.Write(wr, binary.BigEndian, uint8(0)) 514 | // Some clients require null-terminated strings 515 | } 516 | } 517 | 518 | func DecodeBytes(b []byte) (PDU, error) { 519 | buf := bytes.NewBuffer(b) 520 | return Decode(buf) 521 | } 522 | 523 | func Decode(rdr io.Reader) (PDU, error) { 524 | if rdr == nil { 525 | return nil, errors.New("reader for decoding is nil") 526 | } 527 | var pver uint8 528 | var pduType uint8 529 | var sessionId uint16 530 | var length uint32 531 | err := binary.Read(rdr, binary.BigEndian, &pver) 532 | if err != nil { 533 | return nil, err 534 | } 535 | err = binary.Read(rdr, binary.BigEndian, &pduType) 536 | if err != nil { 537 | return nil, err 538 | } 539 | err = binary.Read(rdr, binary.BigEndian, &sessionId) 540 | if err != nil { 541 | return nil, err 542 | } 543 | err = binary.Read(rdr, binary.BigEndian, &length) 544 | if err != nil { 545 | return nil, err 546 | } 547 | 548 | if length < 8 { 549 | return nil, fmt.Errorf("wrong length: %d < 8", length) 550 | } 551 | if length > messageMaxSize { 552 | return nil, fmt.Errorf("wrong length: %d > %d", length, messageMaxSize) 553 | } 554 | toread := make([]byte, length-8) 555 | err = binary.Read(rdr, binary.BigEndian, toread) 556 | if err != nil { 557 | return nil, err 558 | } 559 | 560 | switch pduType { 561 | case PDU_ID_SERIAL_NOTIFY: 562 | if len(toread) != 4 { 563 | return nil, fmt.Errorf("wrong length for Serial Notify PDU: %d != 4", len(toread)) 564 | } 565 | serial := binary.BigEndian.Uint32(toread) 566 | return &PDUSerialNotify{ 567 | Version: pver, 568 | SessionId: sessionId, 569 | SerialNumber: serial, 570 | }, nil 571 | case PDU_ID_SERIAL_QUERY: 572 | if len(toread) != 4 { 573 | return nil, fmt.Errorf("wrong length for Serial Query PDU: %d != 4", len(toread)) 574 | } 575 | serial := binary.BigEndian.Uint32(toread) 576 | return &PDUSerialQuery{ 577 | Version: pver, 578 | SessionId: sessionId, 579 | SerialNumber: serial, 580 | }, nil 581 | case PDU_ID_RESET_QUERY: 582 | if len(toread) != 0 { 583 | return nil, fmt.Errorf("wrong length for Reset Query PDU: %d != 0", len(toread)) 584 | } 585 | return &PDUResetQuery{ 586 | Version: pver, 587 | }, nil 588 | case PDU_ID_CACHE_RESPONSE: 589 | if len(toread) != 0 { 590 | return nil, fmt.Errorf("wrong length for Cache Response PDU: %d != 0", len(toread)) 591 | } 592 | return &PDUCacheResponse{ 593 | Version: pver, 594 | SessionId: sessionId, 595 | }, nil 596 | case PDU_ID_IPV4_PREFIX: 597 | if len(toread) != 12 { 598 | return nil, fmt.Errorf("wrong length for IPv4 Prefix PDU: %d != 12", len(toread)) 599 | } 600 | prefixLen := int(toread[1]) 601 | ip := toread[4:8] 602 | addr, ok := netip.AddrFromSlice(ip) 603 | if !ok { 604 | return nil, fmt.Errorf("ip slice length is not 4 or 16: %+v", addr) 605 | } 606 | asn := binary.BigEndian.Uint32(toread[8:]) 607 | return &PDUIPv4Prefix{ 608 | Version: pver, 609 | Flags: uint8(toread[0]), 610 | MaxLen: uint8(toread[2]), 611 | ASN: asn, 612 | Prefix: netip.PrefixFrom(addr, prefixLen), 613 | }, nil 614 | case PDU_ID_IPV6_PREFIX: 615 | if len(toread) != 24 { 616 | return nil, fmt.Errorf("wrong length for IPv6 Prefix PDU: %d != 24", len(toread)) 617 | } 618 | prefixLen := int(toread[1]) 619 | ip := toread[4:20] 620 | addr, ok := netip.AddrFromSlice(ip) 621 | if !ok { 622 | return nil, fmt.Errorf("ip slice length is not 4 or 16: %+v", addr) 623 | } 624 | asn := binary.BigEndian.Uint32(toread[20:]) 625 | return &PDUIPv6Prefix{ 626 | Version: pver, 627 | Flags: uint8(toread[0]), 628 | MaxLen: uint8(toread[2]), 629 | ASN: asn, 630 | Prefix: netip.PrefixFrom(addr, prefixLen), 631 | }, nil 632 | case PDU_ID_END_OF_DATA: 633 | if len(toread) != 4 && len(toread) != 16 { 634 | return nil, fmt.Errorf("wrong length for End of Data PDU: %d != 4 or != 16", len(toread)) 635 | } 636 | 637 | var serial uint32 638 | var refreshInterval uint32 639 | var retryInterval uint32 640 | var expireInterval uint32 641 | if len(toread) == 4 { 642 | serial = binary.BigEndian.Uint32(toread) 643 | } else if len(toread) == 16 { 644 | serial = binary.BigEndian.Uint32(toread[0:4]) 645 | refreshInterval = binary.BigEndian.Uint32(toread[4:8]) 646 | retryInterval = binary.BigEndian.Uint32(toread[8:12]) 647 | expireInterval = binary.BigEndian.Uint32(toread[12:16]) 648 | } 649 | 650 | return &PDUEndOfData{ 651 | Version: pver, 652 | SessionId: sessionId, 653 | SerialNumber: serial, 654 | RefreshInterval: refreshInterval, 655 | RetryInterval: retryInterval, 656 | ExpireInterval: expireInterval, 657 | }, nil 658 | case PDU_ID_CACHE_RESET: 659 | if len(toread) != 0 { 660 | return nil, fmt.Errorf("wrong length for Cache Reset PDU: %d != 0", len(toread)) 661 | } 662 | return &PDUCacheReset{ 663 | Version: pver, 664 | }, nil 665 | case PDU_ID_ROUTER_KEY: 666 | if len(toread) < 28 { 667 | return nil, fmt.Errorf("wrong length for Router Key PDU: %d < 28", len(toread)) 668 | } 669 | asn := binary.BigEndian.Uint32(toread[20:24]) 670 | spki := toread[24:] 671 | ski := make([]byte, 20) 672 | copy(ski[:], toread[0:20]) 673 | return &PDURouterKey{ 674 | Version: pver, 675 | SubjectKeyIdentifier: ski, 676 | // Router Key uses a rarely used spot that is also used by the SessionID, So we we will just bitshift 677 | Flags: uint8(sessionId >> 8), 678 | ASN: asn, 679 | SubjectPublicKeyInfo: spki, 680 | }, nil 681 | case PDU_ID_ERROR_REPORT: 682 | if len(toread) < 8 { 683 | return nil, fmt.Errorf("wrong length for Error Report PDU: %d < 8", len(toread)) 684 | } 685 | lenPdu := binary.BigEndian.Uint32(toread[0:4]) 686 | if len(toread) < int(lenPdu)+8 { 687 | return nil, fmt.Errorf("wrong length for Error Report PDU: %d < %d", len(toread), lenPdu+4) 688 | } 689 | errPdu := toread[4 : lenPdu+4] 690 | lenErrText := binary.BigEndian.Uint32(toread[lenPdu+4 : lenPdu+8]) 691 | // int casting for each value is needed here to prevent an uint32 overflow that could result in 692 | // upper bound being lower than lower bound causing a crash 693 | if len(toread) < int(lenPdu)+8+int(lenErrText) { 694 | return nil, fmt.Errorf("wrong length for Error Report PDU: %d < %d", len(toread), lenPdu+8+lenErrText) 695 | } 696 | errMsg := string(toread[lenPdu+8 : lenPdu+8+lenErrText]) 697 | return &PDUErrorReport{ 698 | Version: pver, 699 | ErrorCode: sessionId, 700 | PDUCopy: errPdu, 701 | ErrorMsg: errMsg, 702 | }, nil 703 | default: 704 | return nil, errors.New("could not decode packet") 705 | } 706 | } 707 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # StayRTR 2 | 3 | ![animated stayrtr logo](stayrtr.gif) 4 | 5 | [![Docker Pulls](https://img.shields.io/docker/pulls/rpki/stayrtr.svg)](https://hub.docker.com/r/rpki/stayrtr) 6 | 7 | 8 | Packaging status 9 | 10 | 11 | StayRTR is an open-source implementation of RPKI-to-Router protocol (RFC 6810, RFC 8210); based on GoRTR using the [the Go Programming Language](http://golang.org/). 12 | 13 | * `/lib` contains a library to create your own server and client. 14 | * `/prefixfile` contains the structure of a JSON export file and signing capabilities. 15 | * `/cmd/stayrtr/stayrtr.go` is a simple implementation that fetches a list and offers it to a router. 16 | * `/cmd/rtrdump/rtrdump.go` allows copying the PDUs sent by a RTR server as a JSON file. 17 | * `/cmd/rtrmon/rtrmon.go` compare and monitor two RTR servers (using RTR and/or JSON), outputs diff and Prometheus metrics. 18 | 19 | ## Disclaimer 20 | 21 | _This software comes with no warranty._ 22 | 23 | ## Sponsors 24 | 25 | The StayRTR project was built on contributions of money and time. 26 | Special thanks for support to the Route Server Support Foundation [RSSF](https://www.rssf.nl), [Internet Society](https://www.internetsociety.org/) and [PCCW Global](https://www.pccwglobal.com/). 27 | 28 | ## Features of the server 29 | 30 | * Dissemination of validated ROA and BGPsec payloads 31 | * Refreshes a JSON list of prefixes 32 | * Automatic expiration of outdated information (when using JSON produced by [rpki-client](https://www.rpki-client.org)) 33 | * Prometheus metrics 34 | * TLS 35 | * SSH 36 | 37 | ## Features of the extractor 38 | 39 | * Generate a list of prefixes received via RTR into a JSON file 40 | * Lightweight 41 | * TLS 42 | * SSH 43 | 44 | ## Features of the API 45 | 46 | * Protocol v0 of [RFC6810](https://tools.ietf.org/html/rfc6810) 47 | * Protocol v1 of [RFC8210](https://tools.ietf.org/html/rfc8210) 48 | * Event-driven API 49 | * TLS 50 | * SSH 51 | 52 | ## To start developing 53 | 54 | You need a working [Go environment](https://golang.org/doc/install) (1.24 or newer). 55 | This project also uses [Go Modules](https://github.com/golang/go/wiki/Modules). 56 | 57 | ```bash 58 | $ git clone git@github.com:bgp/stayrtr.git && cd stayrtr 59 | $ go build cmd/stayrtr/stayrtr.go 60 | ``` 61 | 62 | ## With Docker 63 | 64 | If you do not want to use Docker, please go to the next section. 65 | 66 | If you have **Docker**, you can start StayRTR with `docker run -ti -p 8082:8082 rpki/stayrtr` someday when it has been built. 67 | 68 | You can now use any CLI attributes as long as they are after the image name: 69 | 70 | ```bash 71 | $ docker run -ti -p 8083:8083 rpki/stayrtr -bind :8083 72 | ``` 73 | 74 | If you want to build your own image of StayRTR: 75 | 76 | ```bash 77 | $ docker build -t mystayrtr -f Dockerfile.stayrtr.prod . 78 | $ docker run -ti mystayrtr -h 79 | ``` 80 | 81 | It will download the code from GitHub and compile it with Go and also generate an ECDSA key for SSH. 82 | 83 | Please note: if you plan to use SSH with the default container (`rpki/stayrtr`), 84 | replace the key `private.pem` since it is a testing key that has been published. 85 | An example is given below: 86 | 87 | ```bash 88 | $ docker run -ti -v $PWD/mynewkey.pem:/private.pem rpki/stayrtr -ssh.bind :8083 89 | ``` 90 | 91 | ## Install it 92 | 93 | There are a few solutions to install it. 94 | 95 | Go can directly fetch it from the source 96 | 97 | ```bash 98 | $ go get github.com/bgp/stayrtr/cmd/stayrtr 99 | ``` 100 | 101 | You can use the Makefile (by default it will be compiled for Linux, add `GOOS=darwin` for Mac) 102 | 103 | ```bash 104 | $ make build-stayrtr 105 | ``` 106 | 107 | The compiled file will be in `/dist`. 108 | 109 | Or you can use a tarball file from the [Releases page](https://github.com/bgp/stayrtr/releases): 110 | 111 | ## Run it 112 | 113 | Once you have a binary: 114 | 115 | ```bash 116 | $ ./stayrtr -tls.bind 127.0.0.1:8282 117 | ``` 118 | 119 | ## Package it 120 | 121 | If you want to package it (deb/rpm), you can use the pre-built docker-compose file. 122 | 123 | ```bash 124 | $ docker-compose -f docker-compose-pkg.yml up 125 | ``` 126 | 127 | You can find both files in the `dist/` directory. 128 | 129 | ### Usage with a proxy 130 | 131 | This was tested with a basic Squid proxy. The `User-Agent` header is passed 132 | in the CONNECT. 133 | 134 | You have to export the following two variables in order for StayRTR to use the proxy. 135 | 136 | ``` 137 | export HTTP_PROXY=schema://host:port 138 | export HTTPS_PROXY=schema://host:port 139 | ``` 140 | 141 | ### With SSL 142 | 143 | You can run StayRTR and listen for TLS connections only (just pass `-bind ""`). 144 | 145 | First, you will have to create a SSL certificate. 146 | 147 | ```bash 148 | $ openssl ecparam -genkey -name prime256v1 -noout -outform pem > private.pem 149 | $ openssl req -new -x509 -key private.pem -out server.pem 150 | ``` 151 | 152 | Then, you have to run 153 | 154 | ```bash 155 | $ ./stayrtr -tls.bind :8282 -tls.key private.pem -tls.cert server.pem 156 | ``` 157 | 158 | ### With SSH 159 | 160 | You can run StayRTR and listen for SSH connections only (just pass `-bind ""`). 161 | 162 | You will have to create an ECDSA key. You can use the following command: 163 | 164 | ```bash 165 | $ openssl ecparam -genkey -name prime256v1 -noout -outform pem > private.pem 166 | ``` 167 | 168 | Then you can start: 169 | 170 | ```bash 171 | $ ./stayrtr -ssh.bind :8282 -ssh.key private.pem -bind "" 172 | ``` 173 | 174 | By default, there is no authentication. 175 | 176 | You can use password and key authentication: 177 | 178 | For example, to configure user **rpki** and password **rpki**: 179 | 180 | ```bash 181 | $ ./stayrtr -ssh.bind :8282 -ssh.key private.pem -ssh.method.password=true -ssh.auth.user rpki -ssh.auth.password rpki -bind "" 182 | ``` 183 | 184 | And to configure a bypass for every SSH key: 185 | 186 | ```bash 187 | $ ./stayrtr -ssh.bind :8282 -ssh.key private.pem -ssh.method.key=true -ssh.auth.key.bypass=true -bind "" 188 | ``` 189 | 190 | ## Configure filters and overrides (SLURM) 191 | 192 | StayRTR supports SLURM configuration files ([RFC8416](https://tools.ietf.org/html/rfc8416)). 193 | 194 | Create a json file (`slurm.json`): 195 | 196 | ``` 197 | { 198 | "slurmVersion": 1, 199 | "validationOutputFilters": { 200 | "prefixFilters": [ 201 | { 202 | "prefix": "10.0.0.0/8", 203 | "comment": "Everything inside will be removed" 204 | }, 205 | { 206 | "asn": 65001, 207 | }, 208 | { 209 | "asn": 65002, 210 | "prefix": "192.168.0.0/24", 211 | }, 212 | ], 213 | "bgpsecFilters": [] 214 | }, 215 | "locallyAddedAssertions": { 216 | "prefixAssertions": [ 217 | { 218 | "asn": 65001, 219 | "prefix": "2001:db8::/32", 220 | "maxPrefixLength": 48, 221 | "comment": "Manual add" 222 | } 223 | ], 224 | "bgpsecAssertions": [ 225 | ] 226 | } 227 | } 228 | ``` 229 | 230 | When starting StayRTR, add the `-slurm ./slurm.json` argument. 231 | 232 | The log should display something similar to the following: 233 | 234 | ``` 235 | INFO[0001] Slurm filtering: 112214 kept, 159 removed, 1 asserted 236 | INFO[0002] New update (112215 uniques, 112215 total prefixes). 237 | ``` 238 | 239 | For instance, if the original JSON fetched contains the VRP: `10.0.0.0/24-24 AS65001`, 240 | it will be removed. 241 | 242 | The JSON exported by StayRTR will contain the overrides and the file can be signed again. 243 | Others StayRTR can be configured to fetch the VRPs from the filtering StayRTR: 244 | the operator manages one SLURM file on a leader StayRTR. 245 | 246 | ## Debug the content 247 | 248 | You can check the content provided over RTR with rtrdump tool 249 | 250 | ```bash 251 | $ ./rtrdump -connect 127.0.0.1:8282 -file debug.json 252 | ``` 253 | 254 | You can also fetch the re-generated JSON from the `-export.path` endpoint (default: `http://localhost:9847/rpki.json`) 255 | 256 | ## Monitoring rtr and JSON endpoints 257 | 258 | With `rtrmon` you can monitor the difference between rtr and/or JSON endpoints. 259 | You can use this to, for example, track that your StayRTR instance is still in 260 | sync with your RP instance. Or to track that multiple RP instances are in sync. 261 | 262 | If your CA software has an endpoint that exposes objects in the standard JSON 263 | format, you can even make sure that the objects that your CA software should 264 | generate actually are visible to RPs, to monitor the full cycle. 265 | 266 | ``` 267 | $ ./rtrmon \ 268 | -primary.host tcp://rtr.rpki.cloudflare.com:8282 \ 269 | -secondary.host https://console.rpki-client.org/rpki.json \ 270 | -secondary.refresh 30s \ 271 | -primary.refresh 30s 272 | ``` 273 | 274 | rtrmon has two endpoints: 275 | * `/metrics`: for prometheus metrics 276 | * `/diff.json` (default, can be overridden by the `-file` flag): for a JSON file containing the difference between sources 277 | 278 | ### diff 279 | 280 | The `diff.json` endpoint contains four keys. 281 | 282 | * `metadata-primary`: configuration of the primary source 283 | * `metadata-secondary`: configuration of the secondary source 284 | * `only-primary`: objects in the primary source but not in the secondary source. 285 | * `only-secondary`: objects in the secondary source but not in the primary source. 286 | 287 | ### Metrics 288 | By default the Prometheus endpoint is on `http://[host]:9866/metrics`. 289 | Among others, this endpoint contains the following metrics: 290 | 291 | * `rpki_vrps`: Current number of VRPS and current difference between the primary and secondary. 292 | * `rtr_serial`: Serial of the rtr session (when applicable). 293 | * `rtr_session`: Session ID of the RTR session. 294 | * `rtr_state`: State of the rtr session (up/down). 295 | * `update`: Timestamp of the last update. 296 | * `vrp_diff`: The number of VRPs which were seen in `lhs` at least `visibility_seconds` ago not in `rhs`. 297 | 298 | Using these metrics you can visualise or alert on, for example: 299 | 300 | * Unexpected behaviour 301 | * Did the number of VRPs drop more than 10% compared to the 24h average? 302 | * Liveliness 303 | * Is the RTR serial increasing? 304 | * Is rtrmon still getting updates? 305 | * Convergence 306 | * Do both my RP instances see the same objects eventually? 307 | * Are objects first visible in the JSON `difference` (e.g. 1706) seconds ago visible in RTR? 308 | 309 | When the objects are not converging, the `diff.json` endpoint may help while investigating the issues. 310 | 311 | ### Data sources 312 | 313 | Use your own validator, as long as the JSON source follows the following schema: 314 | 315 | ``` 316 | { 317 | "roas": [ 318 | { 319 | "prefix": "10.0.0.0/24", 320 | "maxLength": 24, 321 | "asn": 65001 322 | }, 323 | ... 324 | ] 325 | } 326 | ``` 327 | 328 | * **Third-party JSON formatted VRP exports:** 329 | * [console.rpki-client.org](https://console.rpki-client.org/rpki.json) (default, based on OpenBSD's `rpki-client`) 330 | * [NTT](https://rpki.gin.ntt.net/api/export.json) (based on OpenBSD's `rpki-client`) 331 | 332 | By default, the session ID will be randomly generated. The serial will start at zero. 333 | 334 | Make sure the refresh rate of StayRTR is more frequent than the refresh rate of the JSON. 335 | 336 | ## Configurations 337 | 338 | ### Compatibility matrix 339 | 340 | A simple comparison between software and devices. 341 | Implementations on versions may vary. 342 | 343 | | Device/software | Plaintext | TLS | SSH | Notes | 344 | | --------------- | --------- | --- | --- | ----------------- | 345 | | RTRdump | Yes | Yes | Yes | | 346 | | RTRlib | Yes | No | Yes | Only SSH key | 347 | | Juniper | Yes | No | No | | 348 | | Cisco | Yes | No | Yes | Only SSH password | 349 | | Nokia | Yes | No | No | | 350 | | Arista | Yes | No | No | | 351 | | FRRouting | Yes | No | Yes | Only SSH key | 352 | | Bird2 | Yes | No | Yes | Only SSH key | 353 | | Quagga | Yes | No | No | | 354 | | OpenBGPD | Yes | No | No | | 355 | 356 | ### Configure on Juniper 357 | 358 | Configure a session to the RTR server (assuming it runs on `192.168.1.100:8282`) 359 | 360 | ``` 361 | louis@router> show configuration routing-options validation 362 | group TEST-RPKI { 363 | session 192.168.1.100 { 364 | port 8282; 365 | } 366 | } 367 | ``` 368 | 369 | Add policies to validate or invalidate prefixes 370 | 371 | ``` 372 | louis@router> show configuration policy-options policy-statement STATEMENT-EXAMPLE 373 | term RPKI-TEST-VAL { 374 | from { 375 | protocol bgp; 376 | validation-database valid; 377 | } 378 | then { 379 | validation-state valid; 380 | next term; 381 | } 382 | } 383 | term RPKI-TEST-INV { 384 | from { 385 | protocol bgp; 386 | validation-database invalid; 387 | } 388 | then { 389 | validation-state invalid; 390 | reject; 391 | } 392 | } 393 | ``` 394 | 395 | Display status of the session to the RTR server. 396 | 397 | ``` 398 | louis@router> show validation session 192.168.1.100 detail 399 | Session 192.168.1.100, State: up, Session index: 1 400 | Group: TEST-RPKI, Preference: 100 401 | Port: 8282 402 | Refresh time: 300s 403 | Hold time: 600s 404 | Record Life time: 3600s 405 | Serial (Full Update): 1 406 | Serial (Incremental Update): 1 407 | Session flaps: 2 408 | Session uptime: 00:25:07 409 | Last PDU received: 00:04:50 410 | IPv4 prefix count: 46478 411 | IPv6 prefix count: 8216 412 | ``` 413 | 414 | Show content of the database (list the PDUs) 415 | 416 | ``` 417 | louis@router> show validation database brief 418 | RV database for instance master 419 | 420 | Prefix Origin-AS Session State Mismatch 421 | 1.0.0.0/24-24 13335 192.168.1.100 valid 422 | 1.1.1.0/24-24 13335 192.168.1.100 valid 423 | ``` 424 | 425 | ### Configure on Cisco 426 | 427 | You may want to use the option to do SSH-based connection. 428 | 429 | On Cisco, you can have only one RTR server per IP. 430 | 431 | To configure a session for `192.168.1.100:8282`: 432 | Replace `65001` by the configured ASN: 433 | 434 | ``` 435 | router bgp 65001 436 | rpki server 192.168.1.100 437 | transport tcp port 8282 438 | ! 439 | ! 440 | ``` 441 | 442 | For an SSH session, you will also have to configure 443 | `router bgp 65001 rpki server 192.168.1.100 password xxx` 444 | where `xxx` is the password. 445 | Some experimentations showed you have to configure 446 | the username/password first, otherwise it will not accept the port. 447 | 448 | ``` 449 | router bgp 65001 450 | rpki server 192.168.1.100 451 | username rpki 452 | transport ssh port 8282 453 | ! 454 | ! 455 | ssh client tcp-window-scale 14 456 | ssh timeout 120 457 | ``` 458 | 459 | The last two SSH statements solved an issue causing the 460 | connection to break before receiving all the PDUs (TCP window full problem). 461 | 462 | To visualize the state of the session: 463 | 464 | ``` 465 | RP/0/RP0/CPU0:ios#sh bgp rpki server 192.168.1.100 466 | 467 | RPKI Cache-Server 192.168.1.100 468 | Transport: SSH port 8282 469 | Connect state: ESTAB 470 | Conn attempts: 1 471 | Total byte RX: 1726892 472 | Total byte TX: 452 473 | Last reset 474 | Timest: Apr 05 01:19:32 (04:26:58 ago) 475 | Reason: protocol error 476 | SSH information 477 | Username: rpki 478 | Password: ***** 479 | SSH PID: 18576 480 | RPKI-RTR protocol information 481 | Serial number: 15 482 | Cache nonce: 0x0 483 | Protocol state: DATA_END 484 | Refresh time: 600 seconds 485 | Response time: 30 seconds 486 | Purge time: 60 seconds 487 | Protocol exchange 488 | VRPs announced: 67358 IPv4 11754 IPv6 489 | VRPs withdrawn: 80 IPv4 34 IPv6 490 | Error Reports : 0 sent 0 rcvd 491 | Last protocol error 492 | Reason: response timeout 493 | Detail: response timeout while in DATA_START state 494 | ``` 495 | 496 | To visualize the accepted PDUs: 497 | 498 | ``` 499 | RP/0/RP0/CPU0:ios#sh bgp rpki table 500 | 501 | Network Maxlen Origin-AS Server 502 | 1.0.0.0/24 24 13335 192.168.1.100 503 | 1.1.1.0/24 24 13335 192.168.1.100 504 | ``` 505 | 506 | ### Configure on Arista 507 | ``` 508 | router bgp 509 | rpki cache 510 | host [vrf ] [port <1-65535>] # default port is 323 511 | local-interface 512 | preference <1-10> # the lower the value, the more preferred 513 | # default is 5 514 | refresh-interval <1-86400 seconds> # default is 3600 515 | expire-interval <600-172800 seconds> # default is 7200 516 | retry-interval <1-7200 seconds> # default is 600 517 | ``` 518 | If multiple caches are configured, the preference controls the priority. 519 | Caches which are more preferred will be connected to first, if they are not reachable then connections will be attempted to less preferred caches. 520 | If caches have the same preference value, they will all be connected to and the VRPs that are synced from them will be merged together. 521 | 522 | To visualize the state of the session: 523 | 524 | ``` 525 | show bgp rpki cache [] 526 | show bgp rpki cache counters [errors] 527 | show bgp rpki roa summary 528 | ``` 529 | 530 | To visualize the accepted PDUs: 531 | 532 | ``` 533 | show bgp rpki roa (ipv4|ipv6) [prefix] 534 | ``` 535 | 536 | ### Configure on Nokia SR OS 537 | 538 | Configure a session to the RTR server (assuming it runs on `192.168.1.100:8282`): 539 | 540 | ``` 541 | [ex:/configure router "Base" origin-validation] 542 | A:grhankin@br1-nyc# info 543 | rpki-session 192.168.1.100 { 544 | admin-state enable 545 | port 8282 546 | } 547 | ``` 548 | 549 | Add policies to validate or invalidate prefixes with an optional step of adding communities: 550 | 551 | ``` 552 | [ex:/configure policy-options] 553 | A:grhankin@er2-nyc# info 554 | community "VRP_INVALID_COMM" { 555 | member "ext:4300:2" { } 556 | } 557 | community "VRP_NOT_FOUND_COMM" { 558 | member "ext:4300:1" { } 559 | } 560 | community "VRP_VALID_COMM" { 561 | member "ext:4300:0" { } 562 | } 563 | policy-statement "ORIGIN_POLICY" { 564 | entry 10 { 565 | from { 566 | origin-validation-state invalid 567 | } 568 | action { 569 | action-type reject 570 | community { 571 | add ["VRP_INVALID_COMM"] 572 | } 573 | } 574 | } 575 | entry 20 { 576 | from { 577 | origin-validation-state not-found 578 | } 579 | action { 580 | action-type accept 581 | local-preference 100 582 | community { 583 | add ["VRP_NOT_FOUND_COMM"] 584 | } 585 | } 586 | } 587 | entry 30 { 588 | from { 589 | origin-validation-state valid 590 | } 591 | action { 592 | action-type accept 593 | local-preference 110 594 | community { 595 | add ["VRP_VALID_COMM"] 596 | } 597 | } 598 | } 599 | } 600 | ``` 601 | Display status of the session to the RTR server: 602 | 603 | ``` 604 | [/] 605 | A:grhankin@br1-nyc# show router origin-validation rpki-session detail 606 | 607 | =============================================================================== 608 | RPKI Session Information 609 | =============================================================================== 610 | IP Address : 192.168.1.100 611 | ------------------------------------------------------------------------------- 612 | Port : 8282 Oper State : established 613 | Uptime : 0d 15:27:54 Flaps : 38 614 | Active IPv4 Records: 324319 Active IPv6 Records: 67880 615 | Admin State : Up Local Address : n/a 616 | Hold Time : 600 Refresh Time : 300 617 | Stale Route Time : 3600 Connect Retry : 120 618 | Serial ID : 411 Session ID : 15502 619 | =============================================================================== 620 | No. of Sessions : 1 621 | =============================================================================== 622 | ``` 623 | 624 | Show content of the database: 625 | 626 | ``` 627 | [/] 628 | A:grhankin@br1-nyc# show router origin-validation database summary 629 | =============================================================================== 630 | Static and Dynamic VRP Database Summary 631 | =============================================================================== 632 | Source IPv4 Entries IPv6 Entries 633 | Description 634 | ------------------------------------------------------------------------------- 635 | 192.168.1.100 [B] 324319 67880 636 | Static 0 0 637 | =============================================================================== 638 | ``` 639 | 640 | ``` 641 | [/] 642 | A:grhankin@br1-nyc# show router origin-validation database origin-as 38016 643 | =============================================================================== 644 | Static and Dynamic VRP Database Entries 645 | =============================================================================== 646 | Prefix Range [Flags] Origin AS 647 | Session IP [Flags] 648 | ------------------------------------------------------------------------------- 649 | 124.252.0.0/16-16 [Dynamic] 38016 650 | 192.168.1.100 [B] 651 | 124.252.255.0/24-24 [Dynamic] 38016 652 | 192.168.1.100 [B] 653 | 135.92.55.0/24-24 [Dynamic] 38016 654 | 192.168.1.100 [B] 655 | 2406:c800::/32-32 [Dynamic] 38016 656 | 192.168.1.100 [B] 657 | 2406:c800:a1ca::/48-48 [Dynamic] 38016 658 | 192.168.1.100 [B] 659 | 2406:c800:e000::/48-48 [Dynamic] 38016 660 | 192.168.1.100 [B] 661 | ------------------------------------------------------------------------------- 662 | No. of VRP Database Entries: 6 663 | ------------------------------------------------------------------------------- 664 | Flags: B = Base instance session 665 | M = Management instance session 666 | Static-V = Static-Valid; Static-I = Static-Invalid 667 | =============================================================================== 668 | ``` 669 | 670 | ## License 671 | 672 | Licensed under the BSD 3 License. 673 | -------------------------------------------------------------------------------- /lib/server.go: -------------------------------------------------------------------------------- 1 | package rtrlib 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/tls" 7 | "encoding/json" 8 | "fmt" 9 | "io" 10 | "math" 11 | "math/rand" 12 | "net" 13 | "net/http" 14 | "net/netip" 15 | "sync" 16 | 17 | "golang.org/x/crypto/ssh" 18 | "golang.org/x/sync/errgroup" 19 | ) 20 | 21 | func GenerateSessionId() uint16 { 22 | return uint16(rand.Intn(math.MaxUint16 + 1)) 23 | } 24 | 25 | type RTRServerEventHandler interface { 26 | ClientConnected(*Client) 27 | ClientDisconnected(*Client) 28 | HandlePDU(*Client, PDU) 29 | } 30 | 31 | type RTREventHandler interface { 32 | RequestCache(*Client) 33 | RequestNewVersion(*Client, uint16, uint32) 34 | } 35 | 36 | // This is a general interface for things like a VRP or BGPsec Router key 37 | // Be sure to have all of these as pointers, or SetFlag() cannot work! 38 | type SendableData interface { 39 | Copy() SendableData 40 | Equals(SendableData) bool 41 | HashKey() string 42 | String() string 43 | Type() string 44 | SetFlag(uint8) 45 | GetFlag() uint8 46 | } 47 | 48 | // This handles things like ROAs, BGPsec Router keys info etc 49 | type SendableDataManager interface { 50 | GetCurrentSerial() (uint32, bool) 51 | GetSessionId(uint8) uint16 52 | GetCurrentSDs() ([]SendableData, bool) 53 | GetSDsSerialDiff(uint32) ([]SendableData, bool) 54 | } 55 | 56 | type DefaultRTREventHandler struct { 57 | sdManager SendableDataManager 58 | Log Logger 59 | } 60 | 61 | func (e *DefaultRTREventHandler) SetSDManager(m SendableDataManager) { 62 | e.sdManager = m 63 | } 64 | 65 | func (e *DefaultRTREventHandler) RequestCache(c *Client) { 66 | if e.Log != nil { 67 | e.Log.Debugf("%v > Request Cache", c) 68 | } 69 | sessionId := e.sdManager.GetSessionId(c.GetVersion()) 70 | serial, valid := e.sdManager.GetCurrentSerial() 71 | if !valid { 72 | c.SendNoDataError() 73 | if e.Log != nil { 74 | e.Log.Debugf("%v < No data", c) 75 | } 76 | } else { 77 | data, exists := e.sdManager.GetCurrentSDs() 78 | if !exists { 79 | c.SendInternalError() 80 | if e.Log != nil { 81 | e.Log.Debugf("%v < Internal error requesting cache (does not exists)", c) 82 | } 83 | } else { 84 | c.SendSDs(sessionId, serial, data) 85 | if e.Log != nil { 86 | e.Log.Debugf("%v < Sent cache (current serial %d, session: %d)", c, serial, sessionId) 87 | } 88 | } 89 | } 90 | } 91 | 92 | func (e *DefaultRTREventHandler) RequestNewVersion(c *Client, sessionId uint16, serialNumber uint32) { 93 | if e.Log != nil { 94 | e.Log.Debugf("%v > Request New Version", c) 95 | } 96 | serverSessionId := e.sdManager.GetSessionId(c.GetVersion()) 97 | if sessionId != serverSessionId { 98 | c.SendCorruptData() 99 | if e.Log != nil { 100 | e.Log.Debugf("%v < Invalid request (client asked for session %d but server is at %d)", c, sessionId, serverSessionId) 101 | } 102 | c.Disconnect() 103 | return 104 | } 105 | serial, valid := e.sdManager.GetCurrentSerial() 106 | if !valid { 107 | c.SendNoDataError() 108 | if e.Log != nil { 109 | e.Log.Debugf("%v < No data", c) 110 | } 111 | } else { 112 | data, exists := e.sdManager.GetSDsSerialDiff(serialNumber) 113 | if !exists { 114 | c.SendCacheReset() 115 | if e.Log != nil { 116 | e.Log.Debugf("%v < Sent cache reset", c) 117 | } 118 | } else { 119 | c.SendSDs(sessionId, serial, data) 120 | if e.Log != nil { 121 | e.Log.Debugf("%v < Sent cache (current serial %d, session from client: %d)", c, serial, sessionId) 122 | } 123 | } 124 | } 125 | } 126 | 127 | type Server struct { 128 | baseVersion uint8 129 | clientlock *sync.RWMutex 130 | clients []*Client 131 | sessId []uint16 132 | connected int 133 | maxconn int 134 | 135 | sshconfig *ssh.ServerConfig 136 | 137 | handler RTRServerEventHandler 138 | simpleHandler RTREventHandler 139 | enforceVersion bool 140 | disableBGPSec bool 141 | enableNODELAY bool 142 | 143 | sdlock *sync.RWMutex 144 | sdListDiff [][]SendableData 145 | sdCurrent []SendableData 146 | sdCurrentSerial uint32 147 | keepDiff int 148 | 149 | pduRefreshInterval uint32 150 | pduRetryInterval uint32 151 | pduExpireInterval uint32 152 | 153 | log Logger 154 | logverbose bool 155 | } 156 | 157 | type ServerConfiguration struct { 158 | MaxConn int 159 | ProtocolVersion uint8 160 | EnforceVersion bool 161 | KeepDifference int 162 | 163 | SessId int 164 | 165 | DisableBGPSec bool 166 | EnableNODELAY bool 167 | 168 | RefreshInterval uint32 169 | RetryInterval uint32 170 | ExpireInterval uint32 171 | 172 | Log Logger 173 | LogVerbose bool 174 | } 175 | 176 | func NewServer(configuration ServerConfiguration, handler RTRServerEventHandler, simpleHandler RTREventHandler) *Server { 177 | sessids := make([]uint16, 0, int(configuration.ProtocolVersion) + 1) 178 | s := GenerateSessionId() 179 | for i := 0; i <= int(configuration.ProtocolVersion); i++ { 180 | sessids = append(sessids, s + uint16(100 * i)) 181 | } 182 | 183 | refreshInterval := uint32(3600) 184 | if configuration.RefreshInterval != 0 { 185 | refreshInterval = configuration.RefreshInterval 186 | } 187 | retryInterval := uint32(600) 188 | if configuration.RetryInterval != 0 { 189 | retryInterval = configuration.RetryInterval 190 | } 191 | expireInterval := uint32(7200) 192 | if configuration.ExpireInterval != 0 { 193 | expireInterval = configuration.ExpireInterval 194 | } 195 | 196 | return &Server{ 197 | sdlock: &sync.RWMutex{}, 198 | sdListDiff: make([][]SendableData, 0), 199 | sdCurrent: make([]SendableData, 0), 200 | keepDiff: configuration.KeepDifference, 201 | 202 | clientlock: &sync.RWMutex{}, 203 | clients: make([]*Client, 0), 204 | sessId: sessids, 205 | maxconn: configuration.MaxConn, 206 | baseVersion: configuration.ProtocolVersion, 207 | 208 | enforceVersion: configuration.EnforceVersion, 209 | disableBGPSec: configuration.DisableBGPSec, 210 | 211 | pduRefreshInterval: refreshInterval, 212 | pduRetryInterval: retryInterval, 213 | pduExpireInterval: expireInterval, 214 | 215 | handler: handler, 216 | simpleHandler: simpleHandler, 217 | 218 | log: configuration.Log, 219 | logverbose: configuration.LogVerbose, 220 | } 221 | } 222 | 223 | func ConvertSDListToMap(SDs []SendableData) map[string]uint8 { 224 | sdMap := make(map[string]uint8, len(SDs)) 225 | for _, v := range SDs { 226 | sdMap[v.HashKey()] = v.GetFlag() 227 | } 228 | return sdMap 229 | } 230 | 231 | func ComputeDiff(newSDs, prevSDs []SendableData, populateUnchanged bool) (added, removed, unchanged []SendableData) { 232 | added = make([]SendableData, 0) 233 | removed = make([]SendableData, 0) 234 | unchanged = make([]SendableData, 0) 235 | 236 | newSDsMap := ConvertSDListToMap(newSDs) 237 | prevSDsMap := ConvertSDListToMap(prevSDs) 238 | 239 | for _, item := range newSDs { 240 | _, exists := prevSDsMap[item.HashKey()] 241 | if !exists { 242 | rcopy := item.Copy() 243 | rcopy.SetFlag(FLAG_ADDED) 244 | added = append(added, rcopy) 245 | } 246 | } 247 | for _, item := range prevSDs { 248 | _, exists := newSDsMap[item.HashKey()] 249 | if !exists { 250 | rcopy := item.Copy() 251 | rcopy.SetFlag(FLAG_REMOVED) 252 | removed = append(removed, rcopy) 253 | } else if populateUnchanged { 254 | rcopy := item.Copy() 255 | unchanged = append(unchanged, rcopy) 256 | } 257 | } 258 | 259 | return added, removed, unchanged 260 | } 261 | 262 | func ApplyDiff(diff, prevSDs []SendableData) []SendableData { 263 | newSDs := make([]SendableData, 0) 264 | diffMap := ConvertSDListToMap(diff) 265 | prevSDsMap := ConvertSDListToMap(prevSDs) 266 | 267 | for _, item := range prevSDs { 268 | _, exists := diffMap[item.HashKey()] 269 | if !exists { 270 | rcopy := item.Copy() 271 | newSDs = append(newSDs, rcopy) 272 | } 273 | } 274 | for _, item := range diff { 275 | if item.GetFlag() == FLAG_ADDED { 276 | rcopy := item.Copy() 277 | newSDs = append(newSDs, rcopy) 278 | } else if item.GetFlag() == FLAG_REMOVED { 279 | citem, exists := prevSDsMap[item.HashKey()] 280 | if !exists { 281 | rcopy := item.Copy() 282 | newSDs = append(newSDs, rcopy) 283 | } else { 284 | if citem == FLAG_REMOVED { 285 | rcopy := item.Copy() 286 | newSDs = append(newSDs, rcopy) 287 | } 288 | } 289 | } 290 | 291 | } 292 | return newSDs 293 | } 294 | 295 | func (s *Server) GetSessionId(version uint8) uint16 { 296 | return s.sessId[version] 297 | } 298 | 299 | func (s *Server) GetCurrentSDs() ([]SendableData, bool) { 300 | s.sdlock.RLock() 301 | sd := s.sdCurrent 302 | s.sdlock.RUnlock() 303 | return sd, true 304 | } 305 | 306 | func (s *Server) GetSDsSerialDiff(serial uint32) ([]SendableData, bool) { 307 | s.sdlock.RLock() 308 | sd, ok := s.getSDsSerialDiff(serial) 309 | s.sdlock.RUnlock() 310 | return sd, ok 311 | } 312 | 313 | func (s *Server) getSDsSerialDiff(serial uint32) ([]SendableData, bool) { 314 | if serial == s.sdCurrentSerial { 315 | return []SendableData{}, true 316 | } 317 | if serial > s.sdCurrentSerial { 318 | return nil, false 319 | } 320 | diff := int(s.sdCurrentSerial - serial) 321 | if diff > len(s.sdListDiff) { 322 | return nil, false 323 | } 324 | 325 | sd := s.sdListDiff[len(s.sdListDiff) - diff] 326 | return sd, true 327 | } 328 | 329 | func (s *Server) GetCurrentSerial() (uint32, bool) { 330 | s.sdlock.RLock() 331 | serial, valid := s.getCurrentSerial() 332 | s.sdlock.RUnlock() 333 | return serial, valid 334 | } 335 | 336 | func (s *Server) getCurrentSerial() (uint32, bool) { 337 | return s.sdCurrentSerial, len(s.sdCurrent) > 0 338 | } 339 | 340 | func (s *Server) generateSerial() uint32 { 341 | newserial := s.sdCurrentSerial 342 | if len(s.sdCurrent) > 0 { 343 | newserial++ 344 | } 345 | return newserial 346 | } 347 | 348 | func (s *Server) CountSDs() int { 349 | s.sdlock.RLock() 350 | defer s.sdlock.RUnlock() 351 | 352 | return len(s.sdCurrent) 353 | } 354 | 355 | func (s *Server) AddData(new []SendableData) bool { 356 | s.sdlock.RLock() 357 | 358 | added, removed, _ := ComputeDiff(new, s.sdCurrent, false) 359 | if s.log != nil && s.logverbose { 360 | s.log.Debugf("Computed diff: added (%v), removed (%v)", added, removed) 361 | } else if s.log != nil { 362 | s.log.Debugf("Computed diff: added (%d), removed (%d)", len(added), len(removed)) 363 | } 364 | curDiff := append(added, removed...) 365 | s.sdlock.RUnlock() 366 | 367 | if len(curDiff) == 0 { 368 | return false 369 | } else { 370 | s.AddSDsDiff(curDiff) 371 | return true 372 | } 373 | } 374 | 375 | func (s *Server) AddSDsDiff(diff []SendableData) { 376 | s.sdlock.RLock() 377 | nextDiff := make([][]SendableData, len(s.sdListDiff) + 1) 378 | for i, prevSDs := range s.sdListDiff { 379 | nextDiff[i] = ApplyDiff(diff, prevSDs) 380 | } 381 | newSDCurrent := ApplyDiff(diff, s.sdCurrent) 382 | s.sdlock.RUnlock() 383 | 384 | s.sdlock.Lock() 385 | defer s.sdlock.Unlock() 386 | newserial := s.generateSerial() 387 | 388 | nextDiff = append(nextDiff, diff) 389 | if s.keepDiff > 0 && len(nextDiff) > s.keepDiff { 390 | nextDiff = nextDiff[len(nextDiff) - s.keepDiff:] 391 | } 392 | 393 | s.sdListDiff = nextDiff 394 | s.sdCurrent = newSDCurrent 395 | s.sdCurrentSerial = newserial 396 | } 397 | 398 | func (s *Server) SetBaseVersion(version uint8) { 399 | s.baseVersion = version 400 | } 401 | 402 | func (s *Server) SetVersionEnforced(adapt bool) { 403 | s.enforceVersion = adapt 404 | } 405 | 406 | func (s *Server) SetMaxConnections(maxconn int) { 407 | if s.connected > maxconn { 408 | todisconnect := s.connected - maxconn 409 | clients := s.GetClientList() 410 | if s.log != nil { 411 | s.log.Debugf("Too many clients connected, disconnecting first %v", todisconnect) 412 | } 413 | for i := 0; i < todisconnect; i++ { 414 | if len(clients) > i { 415 | clients[i].Disconnect() 416 | } 417 | } 418 | } 419 | s.maxconn = maxconn 420 | } 421 | 422 | func (s *Server) GetMaxConnections() int { 423 | return s.maxconn 424 | } 425 | 426 | func (s *Server) GetClientRemoteAddrs(w http.ResponseWriter, r *http.Request) { 427 | clients := s.GetClientList() 428 | out := make([]string, len(clients)) 429 | for i, c := range clients { 430 | out[i] = c.GetRemoteAddress().String() 431 | } 432 | json.NewEncoder(w).Encode(out) 433 | } 434 | 435 | func (s *Server) ClientConnected(c *Client) { 436 | s.clientlock.Lock() 437 | s.clients = append(s.clients, c) 438 | s.connected++ 439 | s.clientlock.Unlock() 440 | 441 | if s.handler != nil { 442 | s.handler.ClientConnected(c) 443 | } 444 | } 445 | 446 | func (s *Server) ClientDisconnected(c *Client) { 447 | s.clientlock.Lock() 448 | tmpclients := make([]*Client, 0) 449 | for _, cc := range s.clients { 450 | if cc != c { 451 | tmpclients = append(tmpclients, cc) 452 | } 453 | } 454 | s.clients = tmpclients 455 | s.connected-- 456 | s.clientlock.Unlock() 457 | 458 | if s.handler != nil { 459 | s.handler.ClientDisconnected(c) 460 | } 461 | } 462 | 463 | func (s *Server) HandlePDU(c *Client, pdu PDU) { 464 | if s.enforceVersion && c.GetVersion() != s.baseVersion { 465 | // Enforce a single version 466 | if s.log != nil { 467 | s.log.Debugf("Client %v uses version %v and server is using %v", c.String(), c.GetVersion(), s.baseVersion) 468 | } 469 | c.SendWrongVersionError() 470 | c.Disconnect() 471 | } 472 | if c.GetVersion() > s.baseVersion { 473 | // Downgrade 474 | c.SetVersion(s.baseVersion) 475 | } 476 | 477 | if s.handler != nil { 478 | s.handler.HandlePDU(c, pdu) 479 | } 480 | } 481 | 482 | func (s *Server) RequestCache(c *Client) { 483 | if s.simpleHandler != nil { 484 | s.simpleHandler.RequestCache(c) 485 | } 486 | } 487 | 488 | func (s *Server) RequestNewVersion(c *Client, sessionId uint16, serial uint32) { 489 | if s.simpleHandler != nil { 490 | s.simpleHandler.RequestNewVersion(c, sessionId, serial) 491 | } 492 | } 493 | 494 | func (s *Server) Start(bind string) error { 495 | tcplist, err := net.Listen("tcp", bind) 496 | if err != nil { 497 | return err 498 | } 499 | return s.loopTCP(tcplist, "tcp", s.acceptClientTCP) 500 | } 501 | 502 | func (s *Server) acceptClientTCP(tcpconn net.Conn) error { 503 | if !s.enableNODELAY { 504 | tc, ok := tcpconn.(*net.TCPConn) 505 | if ok { 506 | tc.SetNoDelay(false) 507 | } 508 | } 509 | 510 | client := ClientFromConn(tcpconn, s, s) 511 | client.log = s.log 512 | if s.enforceVersion { 513 | client.SetVersion(s.baseVersion) 514 | } 515 | client.SetIntervals(s.pduRefreshInterval, s.pduRetryInterval, s.pduExpireInterval) 516 | if s.disableBGPSec { 517 | client.DisableBGPsec() 518 | } 519 | go client.Start() 520 | return nil 521 | } 522 | 523 | func (s *Server) acceptClientSSH(tcpconn net.Conn) error { 524 | _, chans, reqs, err := ssh.NewServerConn(tcpconn, s.sshconfig) 525 | if err != nil { 526 | return err 527 | } 528 | 529 | go func() { 530 | s.connected++ 531 | cont := true 532 | for cont { 533 | select { 534 | case req := <-reqs: 535 | if req != nil && req.WantReply { 536 | req.Reply(false, nil) 537 | } else if req == nil { 538 | cont = false 539 | break 540 | } 541 | case newChannel := <-chans: 542 | if newChannel != nil && newChannel.ChannelType() != "session" { 543 | newChannel.Reject(ssh.UnknownChannelType, "unknown channel type") 544 | continue 545 | } else if newChannel == nil { 546 | cont = false 547 | break 548 | } 549 | channel, requests, err := newChannel.Accept() 550 | if err != nil { 551 | if s.log != nil { 552 | s.log.Errorf("Could not accept channel: %v", err) 553 | } 554 | cont = false 555 | break 556 | } 557 | for req := range requests { 558 | if req != nil && req.Type == "subsystem" && bytes.Equal(req.Payload, []byte{0, 0, 0, 8, 114, 112, 107, 105, 45, 114, 116, 114}) { 559 | err := req.Reply(true, nil) 560 | if err != nil { 561 | if s.log != nil { 562 | s.log.Errorf("Could not accept channel: %v", err) 563 | } 564 | cont = false 565 | break 566 | } 567 | client := ClientFromConnSSH(tcpconn, channel, s, s) 568 | client.log = s.log 569 | if s.enforceVersion { 570 | client.SetVersion(s.baseVersion) 571 | } 572 | client.SetIntervals(s.pduRefreshInterval, s.pduRetryInterval, s.pduExpireInterval) 573 | client.Start() 574 | } else { 575 | cont = false 576 | break 577 | } 578 | 579 | } 580 | } 581 | } 582 | s.connected-- 583 | tcpconn.Close() 584 | }() 585 | return nil 586 | } 587 | 588 | type ClientCallback func(net.Conn) error 589 | 590 | func (s *Server) loopTCP(tcplist net.Listener, logEnv string, clientCallback ClientCallback) error { 591 | for { 592 | tcpconn, err := tcplist.Accept() 593 | if err != nil { 594 | if s.log != nil { 595 | s.log.Errorf("Failed to accept %s connection: %s", logEnv, err) 596 | } 597 | continue 598 | } 599 | 600 | if s.maxconn > 0 && s.connected >= s.maxconn { 601 | if s.log != nil { 602 | s.log.Warnf("Could not accept %s connection from %v (not enough slots available: %d)", logEnv, tcpconn.RemoteAddr(), s.maxconn) 603 | } 604 | tcpconn.Close() 605 | } else { 606 | if s.log != nil { 607 | s.log.Infof("Accepted %s connection from %v (%d/%d)", logEnv, tcpconn.RemoteAddr(), s.connected+1, s.maxconn) 608 | } 609 | if clientCallback != nil { 610 | err := clientCallback(tcpconn) 611 | if err != nil && s.log != nil { 612 | s.log.Errorf("Error with %s client %v: %v", logEnv, tcpconn.RemoteAddr(), err) 613 | } 614 | } 615 | } 616 | } 617 | } 618 | 619 | func (s *Server) StartSSH(bind string, config *ssh.ServerConfig) error { 620 | tcplist, err := net.Listen("tcp", bind) 621 | if err != nil { 622 | return err 623 | } 624 | s.sshconfig = config 625 | return s.loopTCP(tcplist, "ssh", s.acceptClientSSH) 626 | } 627 | 628 | func (s *Server) StartTLS(bind string, config *tls.Config) error { 629 | tcplist, err := tls.Listen("tcp", bind, config) 630 | if err != nil { 631 | return err 632 | } 633 | return s.loopTCP(tcplist, "tls", s.acceptClientTCP) 634 | } 635 | 636 | func (s *Server) GetClientList() []*Client { 637 | s.clientlock.RLock() 638 | list := make([]*Client, len(s.clients)) 639 | copy(list, s.clients) 640 | s.clientlock.RUnlock() 641 | return list 642 | } 643 | 644 | func (s *Server) NotifyClientsLatest() { 645 | serial, _ := s.GetCurrentSerial() 646 | clients := s.GetClientList() 647 | for _, c := range clients { 648 | c.Notify(s.GetSessionId(c.GetVersion()), serial) 649 | } 650 | } 651 | 652 | func ClientFromConn(tcpconn net.Conn, handler RTRServerEventHandler, simpleHandler RTREventHandler) *Client { 653 | return &Client{ 654 | tcpconn: tcpconn, 655 | rd: tcpconn, 656 | wr: tcpconn, 657 | handler: handler, 658 | simpleHandler: simpleHandler, 659 | transmits: make(chan PDU, 256), 660 | } 661 | } 662 | 663 | func ClientFromConnSSH(tcpconn net.Conn, channel ssh.Channel, handler RTRServerEventHandler, simpleHandler RTREventHandler) *Client { 664 | client := ClientFromConn(tcpconn, handler, simpleHandler) 665 | client.rd = channel 666 | client.wr = channel 667 | return client 668 | } 669 | 670 | type Client struct { 671 | version uint8 672 | versionset bool 673 | tcpconn net.Conn 674 | rd io.Reader 675 | wr io.Writer 676 | handler RTRServerEventHandler 677 | simpleHandler RTREventHandler 678 | curserial uint32 679 | 680 | transmits chan PDU 681 | cancel context.CancelFunc 682 | 683 | enforceVersion bool 684 | disableVersionCheck bool 685 | 686 | refreshInterval uint32 687 | retryInterval uint32 688 | expireInterval uint32 689 | 690 | dontSendBGPsecKeys bool 691 | 692 | log Logger 693 | } 694 | 695 | func (c *Client) String() string { 696 | return fmt.Sprintf("%v (v%v) / Serial: %v", c.tcpconn.RemoteAddr(), c.version, c.curserial) 697 | } 698 | 699 | func (c *Client) GetRemoteAddress() net.Addr { 700 | return c.tcpconn.RemoteAddr() 701 | } 702 | 703 | func (c *Client) GetLocalAddress() net.Addr { 704 | return c.tcpconn.LocalAddr() 705 | } 706 | 707 | func (c *Client) GetVersion() uint8 { 708 | return c.version 709 | } 710 | 711 | func (c *Client) DisableBGPsec() { 712 | c.dontSendBGPsecKeys = true 713 | } 714 | 715 | func (c *Client) SetIntervals(refreshInterval uint32, retryInterval uint32, expireInterval uint32) { 716 | c.refreshInterval = refreshInterval 717 | c.retryInterval = retryInterval 718 | c.expireInterval = expireInterval 719 | } 720 | 721 | func (c *Client) SetVersion(newversion uint8) { 722 | c.versionset = true 723 | c.version = newversion 724 | } 725 | 726 | func (c *Client) SetDisableVersionCheck(disableCheck bool) { 727 | c.disableVersionCheck = disableCheck 728 | } 729 | 730 | func (c *Client) checkVersion(newversion uint8) error { 731 | if (!c.versionset || newversion == c.version) && (newversion == PROTOCOL_VERSION_1 || newversion == PROTOCOL_VERSION_0) { 732 | c.SetVersion(newversion) 733 | } else { 734 | if c.log != nil { 735 | c.log.Debugf("%v: has bad version (received: v%v, current: v%v) error", c.String(), newversion, c.version) 736 | } 737 | c.SendWrongVersionError() 738 | c.Disconnect() 739 | return fmt.Errorf("%v: has bad version (received: v%v, current: v%v)", c.String(), newversion, c.version) 740 | } 741 | return nil 742 | } 743 | 744 | func (c *Client) passSimpleHandler(pdu PDU) { 745 | if c.simpleHandler != nil { 746 | switch pduConv := pdu.(type) { 747 | case *PDUSerialQuery: 748 | c.simpleHandler.RequestNewVersion(c, pduConv.SessionId, pduConv.SerialNumber) 749 | case *PDUResetQuery: 750 | c.simpleHandler.RequestCache(c) 751 | default: 752 | // not a proper client packet 753 | } 754 | } 755 | } 756 | 757 | func (c *Client) sendLoop(ctx context.Context) error { 758 | for { 759 | select { 760 | case pdu := <-c.transmits: 761 | c.wr.Write(pdu.Bytes()) 762 | case <-ctx.Done(): 763 | return ctx.Err() 764 | } 765 | } 766 | } 767 | 768 | func (c *Client) readLoop(ctx context.Context) error { 769 | buf := make([]byte, 8000) 770 | for { 771 | select { 772 | case <-ctx.Done(): 773 | return ctx.Err() 774 | default: 775 | length, err := c.rd.Read(buf) 776 | if err != nil || length == 0 { 777 | if c.log != nil { 778 | c.log.Debugf("Error %v", err) 779 | } 780 | c.Disconnect() 781 | return err 782 | } 783 | 784 | pkt := buf[0:length] 785 | dec, err := DecodeBytes(pkt) 786 | if err != nil || dec == nil { 787 | if c.log != nil { 788 | c.log.Errorf("Error %v", err) 789 | } 790 | c.Disconnect() 791 | return err 792 | } 793 | if !c.disableVersionCheck { 794 | if err := c.checkVersion(dec.GetVersion()); err != nil { 795 | // checkVersion returns an error if it issued a disconnect 796 | return err 797 | } 798 | } 799 | if c.log != nil { 800 | c.log.Debugf("%v: Received %v", c.String(), dec) 801 | } 802 | 803 | if c.enforceVersion { 804 | if !IsCorrectPDUVersion(dec, c.version) { 805 | if c.log != nil { 806 | c.log.Debugf("Bad version error") 807 | } 808 | c.SendWrongVersionError() 809 | c.Disconnect() 810 | return fmt.Errorf("%s: bad version error", c.String()) 811 | } 812 | } 813 | 814 | switch pduconv := dec.(type) { 815 | case *PDUSerialQuery: 816 | c.curserial = pduconv.SerialNumber 817 | } 818 | 819 | if c.handler != nil { 820 | c.handler.HandlePDU(c, dec) 821 | } 822 | 823 | c.passSimpleHandler(dec) 824 | } 825 | } 826 | } 827 | 828 | 829 | func (c *Client) Start() { 830 | defer c.tcpconn.Close() 831 | 832 | if c.handler != nil { 833 | c.handler.ClientConnected(c) 834 | } 835 | 836 | ctx, cancel := context.WithCancel(context.TODO()) 837 | c.cancel = cancel 838 | eg, ctx := errgroup.WithContext(ctx) 839 | 840 | eg.Go(func() error { 841 | return c.sendLoop(ctx) 842 | }) 843 | 844 | eg.Go(func() error { 845 | return c.readLoop(ctx) 846 | }) 847 | 848 | eg.Wait() 849 | } 850 | 851 | func (c *Client) Notify(sessionId uint16, serialNumber uint32) { 852 | pdu := &PDUSerialNotify{ 853 | SessionId: sessionId, 854 | SerialNumber: serialNumber, 855 | } 856 | c.SendPDU(pdu) 857 | } 858 | 859 | type VRP struct { 860 | Prefix netip.Prefix 861 | ASN uint32 862 | MaxLen uint8 863 | Flags uint8 864 | } 865 | 866 | func (r *VRP) Type() string { 867 | return "VRP" 868 | } 869 | 870 | func (r *VRP) String() string { 871 | return fmt.Sprintf("VRP %v -> /%v, AS%v, Flags: %v", r.Prefix.String(), r.MaxLen, r.ASN, r.Flags) 872 | } 873 | 874 | func (vrp *VRP) HashKey() string { 875 | return fmt.Sprintf("%v-%v-%v", vrp.Prefix.String(), vrp.MaxLen, vrp.ASN) 876 | } 877 | 878 | func (r1 *VRP) Equals(r2 SendableData) bool { 879 | if r1.Type() != r2.Type() { 880 | return false 881 | } 882 | 883 | r2True := r2.(*VRP) 884 | 885 | return r1.MaxLen == r2True.MaxLen && r1.ASN == r2True.ASN && r1.Prefix == r2True.Prefix 886 | } 887 | 888 | func (r1 *VRP) Copy() SendableData { 889 | return &VRP{ 890 | Prefix: r1.Prefix, 891 | ASN: r1.ASN, 892 | MaxLen: r1.MaxLen, 893 | Flags: r1.Flags} 894 | } 895 | 896 | func (r1 *VRP) SetFlag(f uint8) { 897 | r1.Flags = f 898 | } 899 | 900 | func (r1 *VRP) GetFlag() uint8 { 901 | return r1.Flags 902 | } 903 | 904 | type BgpsecKey struct { 905 | ASN uint32 906 | Pubkey []byte 907 | Ski []byte 908 | Flags uint8 909 | } 910 | 911 | func (brk *BgpsecKey) Type() string { 912 | return "BGPsecKey" 913 | } 914 | 915 | func (brk *BgpsecKey) String() string { 916 | return fmt.Sprintf("BGPsec AS%v -> %x, Flags: %v", brk.ASN, brk.Ski, brk.Flags) 917 | } 918 | 919 | func (brk *BgpsecKey) HashKey() string { 920 | return fmt.Sprintf("%v-%x-%x", brk.ASN, brk.Ski, brk.Pubkey) 921 | } 922 | 923 | func (r1 *BgpsecKey) Equals(r2 SendableData) bool { 924 | if r1.Type() != r2.Type() { 925 | return false 926 | } 927 | 928 | r2True := r2.(*BgpsecKey) 929 | return r1.ASN == r2True.ASN && bytes.Equal(r1.Pubkey, r2True.Pubkey) && bytes.Equal(r1.Ski, r2True.Ski) 930 | } 931 | 932 | func (brk *BgpsecKey) Copy() SendableData { 933 | cop := BgpsecKey{ 934 | ASN: brk.ASN, 935 | Pubkey: make([]byte, len(brk.Pubkey)), 936 | Ski: make([]byte, len(brk.Ski)), 937 | Flags: brk.Flags, 938 | } 939 | copy(cop.Pubkey, brk.Pubkey) 940 | copy(cop.Ski, brk.Ski) 941 | return &cop 942 | } 943 | 944 | func (brk *BgpsecKey) SetFlag(f uint8) { 945 | brk.Flags = f 946 | } 947 | 948 | func (brk *BgpsecKey) GetFlag() uint8 { 949 | return brk.Flags 950 | } 951 | 952 | func (c *Client) SendSDs(sessionId uint16, serialNumber uint32, data []SendableData) { 953 | pduBegin := &PDUCacheResponse{ 954 | SessionId: sessionId, 955 | } 956 | c.SendPDU(pduBegin) 957 | for _, item := range data { 958 | c.SendData(item.Copy()) 959 | } 960 | pduEnd := &PDUEndOfData{ 961 | SessionId: sessionId, 962 | SerialNumber: serialNumber, 963 | 964 | RefreshInterval: c.refreshInterval, 965 | RetryInterval: c.retryInterval, 966 | ExpireInterval: c.expireInterval, 967 | } 968 | c.SendPDU(pduEnd) 969 | } 970 | 971 | func (c *Client) SendCacheReset() { 972 | pdu := &PDUCacheReset{} 973 | c.SendPDU(pdu) 974 | } 975 | 976 | func (c *Client) SendInternalError() { 977 | pdu := &PDUErrorReport{ 978 | ErrorCode: PDU_ERROR_INTERNALERR, 979 | ErrorMsg: "Unknown internal error", 980 | } 981 | c.SendPDU(pdu) 982 | } 983 | 984 | func (c *Client) SendNoDataError() { 985 | pdu := &PDUErrorReport{ 986 | ErrorCode: PDU_ERROR_NODATA, 987 | ErrorMsg: "No data available", 988 | } 989 | c.SendPDU(pdu) 990 | } 991 | 992 | func (c *Client) SendCorruptData() { 993 | pdu := &PDUErrorReport{ 994 | ErrorCode: PDU_ERROR_CORRUPTDATA, 995 | ErrorMsg: "Session ID mismatch: client is desynchronized", 996 | } 997 | c.SendPDU(pdu) 998 | } 999 | 1000 | func (c *Client) SendWrongVersionError() { 1001 | pdu := &PDUErrorReport{ 1002 | ErrorCode: PDU_ERROR_BADPROTOVERSION, 1003 | ErrorMsg: "Bad protocol version", 1004 | } 1005 | c.SendPDU(pdu) 1006 | } 1007 | 1008 | // Converts a SendableData to a PDU and sends it to the client 1009 | func (c *Client) SendData(sd SendableData) { 1010 | switch t := sd.(type) { 1011 | case *VRP: 1012 | 1013 | if t.Prefix.Addr().Is6() { 1014 | pdu := &PDUIPv6Prefix{ 1015 | Flags: t.Flags, 1016 | MaxLen: t.MaxLen, 1017 | ASN: t.ASN, 1018 | Prefix: t.Prefix, 1019 | } 1020 | c.SendPDU(pdu) 1021 | } else if t.Prefix.Addr().Is4() { 1022 | pdu := &PDUIPv4Prefix{ 1023 | Flags: t.Flags, 1024 | MaxLen: t.MaxLen, 1025 | ASN: t.ASN, 1026 | Prefix: t.Prefix, 1027 | } 1028 | c.SendPDU(pdu) 1029 | } 1030 | case *BgpsecKey: 1031 | if c.version == 0 || c.dontSendBGPsecKeys { 1032 | return 1033 | } 1034 | 1035 | pdu := &PDURouterKey{ 1036 | Version: c.version, 1037 | Flags: t.Flags, 1038 | SubjectKeyIdentifier: t.Ski, 1039 | ASN: t.ASN, 1040 | SubjectPublicKeyInfo: t.Pubkey, 1041 | } 1042 | c.SendPDU(pdu) 1043 | } 1044 | } 1045 | 1046 | func (c *Client) SendRawPDU(pdu PDU) { 1047 | c.transmits <- pdu 1048 | } 1049 | 1050 | func (c *Client) SendPDU(pdu PDU) { 1051 | pdu.SetVersion(c.version) 1052 | c.SendRawPDU(pdu) 1053 | } 1054 | 1055 | func (c *Client) Disconnect() { 1056 | if c.log != nil { 1057 | c.log.Infof("Disconnecting client %v", c.String()) 1058 | } 1059 | if c.handler != nil { 1060 | c.handler.ClientDisconnected(c) 1061 | } 1062 | c.cancel() 1063 | } 1064 | -------------------------------------------------------------------------------- /cmd/rtrmon/rtrmon.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | _ "embed" 7 | "encoding/hex" 8 | "encoding/json" 9 | "errors" 10 | "flag" 11 | "fmt" 12 | "html/template" 13 | "io" 14 | "net" 15 | "net/http" 16 | "net/url" 17 | "os" 18 | "sort" 19 | "strconv" 20 | "strings" 21 | "sync" 22 | "time" 23 | 24 | rtr "github.com/bgp/stayrtr/lib" 25 | "github.com/bgp/stayrtr/prefixfile" 26 | "github.com/bgp/stayrtr/utils" 27 | "github.com/prometheus/client_golang/prometheus" 28 | "github.com/prometheus/client_golang/prometheus/promhttp" 29 | log "github.com/sirupsen/logrus" 30 | "golang.org/x/crypto/ssh" 31 | ) 32 | 33 | const ( 34 | ENV_SSH_PASSWORD = "RTR_SSH_PASSWORD" 35 | ENV_SSH_KEY = "RTR_SSH_KEY" 36 | 37 | METHOD_NONE = iota 38 | METHOD_PASSWORD 39 | METHOD_KEY 40 | ) 41 | 42 | type thresholds []int64 43 | 44 | var ( 45 | AppVersion = "RTRmon " + rtr.APP_VERSION 46 | //go:embed index.html.tmpl 47 | IndexTemplate string 48 | 49 | OneOff = flag.Bool("oneoff", false, "dump as json and exits") 50 | Addr = flag.String("addr", ":9866", "Server address") 51 | MetricsPath = flag.String("metrics", "/metrics", "Metrics path") 52 | OutFile = flag.String("file", "diff.json", "Diff file (or URL path without /)") 53 | 54 | UserAgent = flag.String("useragent", fmt.Sprintf("StayRTR-%v (+https://github.com/bgp/stayrtr)", AppVersion), "User-Agent header") 55 | DisableConditionalRequests = flag.Bool("disable.conditional.requests", false, "Disable conditional requests (using If-None-Match/If-Modified-Since headers)") 56 | GracePeriod = flag.Duration("grace.period", 1*time.Hour, "Grace period during which objects removed from a source are not considered for the diff") 57 | 58 | PrimaryHost = flag.String("primary.host", "tcp://rtr.rpki.cloudflare.com:8282", "primary server") 59 | PrimaryValidateCert = flag.Bool("primary.tls.validate", true, "Validate TLS") 60 | PrimaryValidateSSH = flag.Bool("primary.ssh.validate", false, "Validate SSH key") 61 | PrimarySSHServerKey = flag.String("primary.ssh.validate.key", "", "SSH server key SHA256 to validate") 62 | PrimarySSHAuth = flag.String("primary.ssh.method", "none", "Select SSH method (none, password or key)") 63 | PrimarySSHAuthUser = flag.String("primary.ssh.auth.user", "rpki", "SSH user") 64 | PrimarySSHAuthPassword = flag.String("primary.ssh.auth.password", "", fmt.Sprintf("SSH password (if blank, will use envvar %s_1)", ENV_SSH_PASSWORD)) 65 | PrimarySSHAuthKey = flag.String("primary.ssh.auth.key", "id_rsa", fmt.Sprintf("SSH key file (if blank, will use envvar %s_1)", ENV_SSH_KEY)) 66 | PrimaryRefresh = flag.Duration("primary.refresh", time.Second*600, "Refresh interval") 67 | PrimaryRTRBreak = flag.Bool("primary.rtr.break", false, "Break RTR session at each interval") 68 | 69 | SecondaryHost = flag.String("secondary.host", "https://rpki.cloudflare.com/rpki.json", "secondary server") 70 | SecondaryValidateCert = flag.Bool("secondary.tls.validate", true, "Validate TLS") 71 | SecondaryValidateSSH = flag.Bool("secondary.ssh.validate", false, "Validate SSH key") 72 | SecondarySSHServerKey = flag.String("secondary.ssh.validate.key", "", "SSH server key SHA256 to validate") 73 | SecondarySSHAuth = flag.String("secondary.ssh.method", "none", "Select SSH method (none, password or key)") 74 | SecondarySSHAuthUser = flag.String("secondary.ssh.auth.user", "rpki", "SSH user") 75 | SecondarySSHAuthPassword = flag.String("secondary.ssh.auth.password", "", fmt.Sprintf("SSH password (if blank, will use envvar %s_2)", ENV_SSH_PASSWORD)) 76 | SecondarySSHAuthKey = flag.String("secondary.ssh.auth.key", "id_rsa", fmt.Sprintf("SSH key file (if blank, will use envvar %s_2)", ENV_SSH_KEY)) 77 | SecondaryRefresh = flag.Duration("secondary.refresh", time.Second*600, "Refresh interval") 78 | SecondaryRTRBreak = flag.Bool("secondary.rtr.break", false, "Break RTR session at each interval") 79 | 80 | LogLevel = flag.String("loglevel", "info", "Log level") 81 | Version = flag.Bool("version", false, "Print version") 82 | 83 | typeToId = map[string]int{ 84 | "tcp": rtr.TYPE_PLAIN, 85 | "tls": rtr.TYPE_TLS, 86 | "ssh": rtr.TYPE_SSH, 87 | } 88 | authToId = map[string]int{ 89 | "none": METHOD_NONE, 90 | "password": METHOD_PASSWORD, 91 | "key": METHOD_KEY, 92 | } 93 | 94 | VRPCount = prometheus.NewGaugeVec( 95 | prometheus.GaugeOpts{ 96 | Name: "rpki_vrps", 97 | Help: "Total number of current VRPS in primary/secondary and current difference between primary and secondary.", 98 | }, 99 | []string{"server", "url", "type"}, 100 | ) 101 | VRPDifferenceForDuration = prometheus.NewGaugeVec( 102 | prometheus.GaugeOpts{ 103 | Name: "vrp_diff", 104 | Help: "Number of VRPS in [lhs_url] that are not in [rhs_url] that were first seen [visibility_seconds] ago in lhs.", 105 | }, 106 | []string{"lhs_url", "rhs_url", "visibility_seconds"}, 107 | ) 108 | VRPInGracePeriod = prometheus.NewGaugeVec( 109 | prometheus.GaugeOpts{ 110 | Name: "rpki_grace_period_vrps", 111 | Help: "Number of unique VRPS in grace period by url.", 112 | }, 113 | []string{"url"}, 114 | ) 115 | RTRState = prometheus.NewGaugeVec( 116 | prometheus.GaugeOpts{ 117 | Name: "rtr_state", 118 | Help: "State of the RTR session (up/down).", 119 | }, 120 | []string{"server", "url"}, 121 | ) 122 | RTRSerial = prometheus.NewGaugeVec( 123 | prometheus.GaugeOpts{ 124 | Name: "rtr_serial", 125 | Help: "Serial of the RTR session.", 126 | }, 127 | []string{"server", "url"}, 128 | ) 129 | RTRSession = prometheus.NewGaugeVec( 130 | prometheus.GaugeOpts{ 131 | Name: "rtr_session", 132 | Help: "ID of the RTR session.", 133 | }, 134 | []string{"server", "url"}, 135 | ) 136 | LastUpdate = prometheus.NewGaugeVec( 137 | prometheus.GaugeOpts{ 138 | Name: "update", 139 | Help: "Timestamp of last update.", 140 | }, 141 | []string{"server", "url"}, 142 | ) 143 | 144 | idToInfo = map[int]string{ 145 | 0: "unknown", 146 | 1: "primary", 147 | 2: "secondary", 148 | } 149 | 150 | visibilityThresholds = thresholds{0, 56, 256, 596, 851, 1024, 1706, 3411} 151 | ) 152 | 153 | func init() { 154 | prometheus.MustRegister(VRPCount) 155 | prometheus.MustRegister(VRPDifferenceForDuration) 156 | prometheus.MustRegister(VRPInGracePeriod) 157 | prometheus.MustRegister(RTRState) 158 | prometheus.MustRegister(RTRSerial) 159 | prometheus.MustRegister(RTRSession) 160 | prometheus.MustRegister(LastUpdate) 161 | 162 | flag.Var(&visibilityThresholds, "visibility.thresholds", "comma-separated list of visibility thresholds to override the default") 163 | } 164 | 165 | // String formats an array of thresholds as a comma separated string. 166 | func (t *thresholds) String() string { 167 | res := []byte("") 168 | for idx, tr := range *t { 169 | res = strconv.AppendInt(res, tr, 10) 170 | 171 | if idx < len(*t)-1 { 172 | res = append(res, ","...) 173 | } 174 | } 175 | return string(res) 176 | } 177 | 178 | func (t *thresholds) Set(value string) error { 179 | // Setting overrides current values 180 | if len(*t) > 0 { 181 | *t = thresholds{} 182 | } 183 | 184 | for _, tr := range strings.Split(value, ",") { 185 | threshold, err := strconv.ParseInt(tr, 10, 32) 186 | 187 | if err != nil { 188 | return err 189 | } 190 | 191 | *t = append(*t, threshold) 192 | } 193 | 194 | // Sort the breaks in ascending order. 195 | sort.Slice(*t, func(i, j int) bool { return (*t)[i] < (*t)[j] }) 196 | 197 | return nil 198 | } 199 | 200 | func decodeJSON(data []byte) (*prefixfile.RPKIList, error) { 201 | buf := bytes.NewBuffer(data) 202 | dec := json.NewDecoder(buf) 203 | 204 | var vrplistjson prefixfile.RPKIList 205 | err := dec.Decode(&vrplistjson) 206 | return &vrplistjson, err 207 | } 208 | 209 | type Client struct { 210 | ValidateSSH bool 211 | ValidateCert bool 212 | SSHAuthUser string 213 | SSHServerKey string 214 | SSHAuthPassword string 215 | BreakRTR bool 216 | authType int 217 | keyBytes []byte 218 | 219 | serial uint32 220 | sessionID uint16 221 | 222 | FetchConfig *utils.FetchConfig 223 | 224 | Path string 225 | RefreshInterval time.Duration 226 | 227 | qrtr chan bool 228 | 229 | lastUpdate time.Time 230 | 231 | compLock *sync.RWMutex 232 | vrps VRPMap 233 | compRtrLock *sync.RWMutex 234 | vrpsRtr VRPMap 235 | 236 | unlock chan bool 237 | ch chan int 238 | id int 239 | 240 | rtrRefresh uint32 241 | rtrRetry uint32 242 | rtrExpire uint32 243 | } 244 | 245 | func NewClient() *Client { 246 | return &Client{ 247 | compLock: &sync.RWMutex{}, 248 | vrps: make(VRPMap), 249 | compRtrLock: &sync.RWMutex{}, 250 | vrpsRtr: make(VRPMap), 251 | } 252 | } 253 | 254 | func (c *Client) Start(id int, ch chan int) { 255 | c.ch = ch 256 | c.id = id 257 | 258 | pathUrl, err := url.Parse(c.Path) 259 | if err != nil { 260 | log.Fatal(err) 261 | } 262 | 263 | connType := pathUrl.Scheme 264 | rtrAddr := pathUrl.Host 265 | 266 | bypass := true 267 | for { 268 | 269 | if !bypass { 270 | <-time.After(c.RefreshInterval) 271 | } 272 | bypass = false 273 | 274 | if connType == "ssh" || connType == "tcp" || connType == "tls" { 275 | 276 | cc := rtr.ClientConfiguration{ 277 | ProtocolVersion: rtr.PROTOCOL_VERSION_1, 278 | Log: log.StandardLogger(), 279 | } 280 | 281 | clientSession := rtr.NewClientSession(cc, c) 282 | 283 | configTLS := &tls.Config{ 284 | InsecureSkipVerify: !c.ValidateCert, 285 | } 286 | configSSH := &ssh.ClientConfig{ 287 | Auth: make([]ssh.AuthMethod, 0), 288 | User: c.SSHAuthUser, 289 | HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { 290 | serverKeyHash := ssh.FingerprintSHA256(key) 291 | if c.ValidateSSH { 292 | if serverKeyHash != fmt.Sprintf("SHA256:%v", c.SSHServerKey) { 293 | return fmt.Errorf("server key hash %v is different than expected key hash SHA256:%v", serverKeyHash, c.SSHServerKey) 294 | } 295 | } 296 | log.Infof("%d: Connected to server %v via ssh. Fingerprint: %v", id, remote.String(), serverKeyHash) 297 | return nil 298 | }, 299 | } 300 | if c.authType == METHOD_PASSWORD { 301 | password := c.SSHAuthPassword 302 | configSSH.Auth = append(configSSH.Auth, ssh.Password(password)) 303 | } else if c.authType == METHOD_KEY { 304 | signer, err := ssh.ParsePrivateKey(c.keyBytes) 305 | if err != nil { 306 | log.Fatal(err) 307 | } 308 | configSSH.Auth = append(configSSH.Auth, ssh.PublicKeys(signer)) 309 | } 310 | 311 | log.Infof("%d: Connecting with %v to %v", id, connType, rtrAddr) 312 | 313 | c.qrtr = make(chan bool) 314 | c.unlock = make(chan bool) 315 | if !c.BreakRTR { 316 | go c.continuousRTR(clientSession) 317 | } 318 | 319 | err := clientSession.Start(rtrAddr, typeToId[connType], configTLS, configSSH) 320 | if err != nil { 321 | log.Fatal(err) 322 | } 323 | 324 | <-c.qrtr 325 | log.Infof("%d: Quitting RTR session", id) 326 | } else { 327 | log.Infof("%d: Fetching %s", c.id, c.Path) 328 | data, statusCode, _, err := c.FetchConfig.FetchFile(c.Path) 329 | if err != nil && statusCode != 304 { 330 | log.Error(err) 331 | continue 332 | } 333 | 334 | var updatedVrpMap VRPMap 335 | var inGracePeriod int 336 | tCurrentUpdate := time.Now().UTC() 337 | if statusCode == 304 { 338 | updatedVrpMap, inGracePeriod = UpdateCurrentVrpMap(log.WithField("client", c.id), c.vrps, tCurrentUpdate) 339 | } else { 340 | log.Debug(data) 341 | decoded, err := decodeJSON(data) 342 | if err != nil { 343 | log.Error(err) 344 | continue 345 | } 346 | 347 | updatedVrpMap, inGracePeriod = BuildNewVrpMap(log.WithField("client", c.id), c.vrps, decoded, tCurrentUpdate) 348 | } 349 | 350 | VRPInGracePeriod.With(prometheus.Labels{"url": c.Path}).Set(float64(inGracePeriod)) 351 | 352 | c.compLock.Lock() 353 | c.vrps = updatedVrpMap 354 | c.lastUpdate = tCurrentUpdate 355 | c.compLock.Unlock() 356 | if ch != nil { 357 | ch <- id 358 | } 359 | } 360 | 361 | } 362 | 363 | } 364 | 365 | // Build the new vrpMap 366 | // The result: 367 | // * contains all the VRPs in newVRPs 368 | // * keeps the firstSeen value for VRPs already in the old map 369 | // * keeps elements around for GracePeriod after they are not in the input. 370 | func BuildNewVrpMap(log *log.Entry, currentVrps VRPMap, pfxFile *prefixfile.RPKIList, now time.Time) (VRPMap, int) { 371 | var newVrps = pfxFile.ROA 372 | tCurrentUpdate := now.Unix() 373 | res := make(VRPMap) 374 | 375 | for _, vrp := range newVrps { 376 | asn, err := vrp.GetASN2() 377 | if err != nil { 378 | log.Errorf("exploration error for %v asn: %v", vrp, err) 379 | continue 380 | } 381 | prefix, err := vrp.GetPrefix2() 382 | if err != nil { 383 | log.Errorf("exploration error for %v prefix: %v", vrp, err) 384 | continue 385 | } 386 | 387 | maxlen := vrp.GetMaxLen() 388 | key := fmt.Sprintf("%s-%d-%d", prefix.String(), maxlen, asn) 389 | 390 | firstSeen := tCurrentUpdate 391 | currentEntry, ok := currentVrps[key] 392 | if ok && currentEntry.Visible { 393 | // VRP is still visible, so keep the existing `FirstSeen`. 394 | firstSeen = currentEntry.FirstSeen 395 | } 396 | 397 | res[key] = &VRPJsonSimple{ 398 | Prefix: prefix.String(), 399 | ASN: asn, 400 | Length: uint8(maxlen), 401 | FirstSeen: firstSeen, 402 | LastSeen: tCurrentUpdate, 403 | Visible: true, 404 | } 405 | } 406 | 407 | for _, bgpsecKey := range pfxFile.BgpSecKeys { 408 | key := fmt.Sprintf("%s-%d-%s", bgpsecKey.Ski, bgpsecKey.Asn, bgpsecKey.Pubkey) 409 | 410 | firstSeen := tCurrentUpdate 411 | currentEntry, ok := currentVrps[key] 412 | if ok && currentEntry.Visible { 413 | // VRP is still visible, so keep the existing `FirstSeen`. 414 | firstSeen = currentEntry.FirstSeen 415 | } 416 | 417 | copyOf := bgpsecKey 418 | 419 | res[key] = &VRPJsonSimple{ 420 | ASN: bgpsecKey.Asn, 421 | FirstSeen: firstSeen, 422 | LastSeen: tCurrentUpdate, 423 | Visible: true, 424 | BGPSecData: ©Of, 425 | } 426 | } 427 | 428 | // Copy objects that are within the grace period to the new map 429 | gracePeriodEnds := tCurrentUpdate - int64(GracePeriod.Seconds()) 430 | inGracePeriod := 0 431 | for k, entry := range currentVrps { 432 | if _, ok := res[k]; !ok { // no longer present 433 | if entry.LastSeen >= gracePeriodEnds { 434 | entry.Visible = false 435 | res[k] = entry 436 | inGracePeriod++ 437 | } 438 | } 439 | } 440 | 441 | return res, inGracePeriod 442 | } 443 | 444 | func UpdateCurrentVrpMap(log *log.Entry, currentVrps VRPMap, now time.Time) (VRPMap, int) { 445 | tCurrentUpdate := now.Unix() 446 | gracePeriodEnds := tCurrentUpdate - int64(GracePeriod.Seconds()) 447 | inGracePeriod := 0 448 | res := make(VRPMap) 449 | 450 | for key, vrp := range currentVrps { 451 | if !vrp.Visible && vrp.LastSeen < gracePeriodEnds { 452 | continue 453 | } 454 | 455 | copy := *vrp 456 | if copy.Visible { 457 | copy.LastSeen = tCurrentUpdate 458 | } else { 459 | inGracePeriod++ 460 | } 461 | res[key] = © 462 | } 463 | 464 | return res, inGracePeriod 465 | } 466 | 467 | func (c *Client) HandlePDU(cs *rtr.ClientSession, pdu rtr.PDU) { 468 | switch pdu := pdu.(type) { 469 | case *rtr.PDUIPv4Prefix: 470 | vrp := VRPJsonSimple{ 471 | Prefix: pdu.Prefix.String(), 472 | ASN: pdu.ASN, 473 | Length: pdu.MaxLen, 474 | FirstSeen: time.Now().Unix(), 475 | Visible: true, 476 | } 477 | 478 | key := fmt.Sprintf("%s-%d-%d", pdu.Prefix.String(), pdu.MaxLen, pdu.ASN) 479 | c.compRtrLock.Lock() 480 | 481 | if pdu.Flags == rtr.FLAG_ADDED { 482 | c.vrpsRtr[key] = &vrp 483 | } else { 484 | delete(c.vrpsRtr, key) 485 | } 486 | 487 | c.compRtrLock.Unlock() 488 | case *rtr.PDUIPv6Prefix: 489 | vrp := VRPJsonSimple{ 490 | Prefix: pdu.Prefix.String(), 491 | ASN: pdu.ASN, 492 | Length: pdu.MaxLen, 493 | FirstSeen: time.Now().Unix(), 494 | Visible: true, 495 | } 496 | 497 | key := fmt.Sprintf("%s-%d-%d", pdu.Prefix.String(), pdu.MaxLen, pdu.ASN) 498 | c.compRtrLock.Lock() 499 | 500 | if pdu.Flags == rtr.FLAG_ADDED { 501 | c.vrpsRtr[key] = &vrp 502 | } else { 503 | delete(c.vrpsRtr, key) 504 | } 505 | 506 | c.compRtrLock.Unlock() 507 | case *rtr.PDURouterKey: 508 | vrp := VRPJsonSimple{ 509 | ASN: pdu.ASN, 510 | FirstSeen: time.Now().Unix(), 511 | Visible: true, 512 | BGPSecData: &prefixfile.BgpSecKeyJson{ 513 | Asn: pdu.ASN, 514 | Pubkey: pdu.SubjectPublicKeyInfo, 515 | Ski: hex.EncodeToString(pdu.SubjectKeyIdentifier), 516 | }, 517 | } 518 | 519 | key := fmt.Sprintf("%s-%d-%s", vrp.BGPSecData.Ski, pdu.ASN, pdu.SubjectPublicKeyInfo) 520 | c.compRtrLock.Lock() 521 | 522 | if pdu.Flags == rtr.FLAG_ADDED { 523 | c.vrpsRtr[key] = &vrp 524 | } else { 525 | delete(c.vrpsRtr, key) 526 | } 527 | 528 | c.compRtrLock.Unlock() 529 | case *rtr.PDUEndOfData: 530 | log.Infof("%d: Received: %v", c.id, pdu) 531 | 532 | c.compRtrLock.Lock() 533 | c.serial = pdu.SerialNumber 534 | tmpVrpMap := make(VRPMap, len(c.vrpsRtr)) 535 | for key, vrp := range c.vrpsRtr { 536 | tmpVrpMap[key] = vrp 537 | } 538 | c.compRtrLock.Unlock() 539 | 540 | c.compLock.Lock() 541 | c.vrps = tmpVrpMap 542 | 543 | c.rtrRefresh = pdu.RefreshInterval 544 | c.rtrRetry = pdu.RetryInterval 545 | c.rtrExpire = pdu.ExpireInterval 546 | c.lastUpdate = time.Now().UTC() 547 | c.compLock.Unlock() 548 | 549 | if c.ch != nil { 550 | c.ch <- c.id 551 | } 552 | 553 | if c.BreakRTR { 554 | cs.Disconnect() 555 | } 556 | case *rtr.PDUCacheResponse: 557 | log.Infof("%d: Received: %v", c.id, pdu) 558 | c.sessionID = pdu.SessionId 559 | case *rtr.PDUCacheReset: 560 | log.Infof("%d: Received: %v", c.id, pdu) 561 | case *rtr.PDUSerialNotify: 562 | log.Infof("%d: Received: %v", c.id, pdu) 563 | default: 564 | log.Infof("%d: Received: %v", c.id, pdu) 565 | cs.Disconnect() 566 | } 567 | } 568 | 569 | func (c *Client) ClientConnected(cs *rtr.ClientSession) { 570 | close(c.unlock) 571 | cs.SendResetQuery() 572 | 573 | RTRState.With( 574 | prometheus.Labels{ 575 | "server": idToInfo[c.id], 576 | "url": c.Path, 577 | }).Set(float64(1)) 578 | } 579 | 580 | func (c *Client) ClientDisconnected(cs *rtr.ClientSession) { 581 | log.Warnf("%d: RTR client disconnected", c.id) 582 | select { 583 | case <-c.qrtr: 584 | default: 585 | close(c.qrtr) 586 | } 587 | 588 | RTRState.With( 589 | prometheus.Labels{ 590 | "server": idToInfo[c.id], 591 | "url": c.Path, 592 | }).Set(float64(0)) 593 | } 594 | 595 | func (c *Client) continuousRTR(cs *rtr.ClientSession) { 596 | log.Debugf("%d: RTR routine started", c.id) 597 | var stop bool 598 | 599 | select { 600 | case <-c.unlock: 601 | case <-c.qrtr: 602 | stop = true 603 | } 604 | 605 | for !stop { 606 | select { 607 | case <-c.qrtr: 608 | stop = true 609 | case <-time.After(c.RefreshInterval): 610 | cs.SendSerialQuery(c.sessionID, c.serial) 611 | } 612 | } 613 | } 614 | 615 | func (c *Client) GetData() (VRPMap, *diffMetadata) { 616 | c.compLock.RLock() 617 | defer c.compLock.RUnlock() 618 | vrps := c.vrps 619 | 620 | md := &diffMetadata{ 621 | URL: c.Path, 622 | Serial: c.serial, 623 | SessionID: c.sessionID, 624 | Count: len(vrps), 625 | 626 | RTRRefresh: c.rtrRefresh, 627 | RTRRetry: c.rtrRetry, 628 | RTRExpire: c.rtrExpire, 629 | 630 | LastFetch: c.lastUpdate.UnixNano() / 1e9, 631 | } 632 | 633 | return vrps, md 634 | } 635 | 636 | type Comparator struct { 637 | PrimaryClient, SecondaryClient *Client 638 | 639 | q chan bool 640 | comp chan int 641 | 642 | OneOff bool 643 | 644 | diffLock *sync.RWMutex 645 | onlyIn1, onlyIn2 VRPMap 646 | md1 *diffMetadata 647 | md2 *diffMetadata 648 | } 649 | 650 | func NewComparator(c1, c2 *Client) *Comparator { 651 | return &Comparator{ 652 | PrimaryClient: c1, 653 | SecondaryClient: c2, 654 | 655 | q: make(chan bool), 656 | comp: make(chan int), 657 | 658 | diffLock: &sync.RWMutex{}, 659 | } 660 | } 661 | 662 | func isDefinitelyVisible(vrp *VRPJsonSimple, thresholdTimestamp int64) bool { 663 | return vrp != nil && vrp.Visible && vrp.FirstSeen <= thresholdTimestamp 664 | } 665 | 666 | func isDefinitelyNotVisisble(vrp *VRPJsonSimple, thresholdTimestamp int64) bool { 667 | return vrp == nil || (!vrp.Visible && vrp.LastSeen <= thresholdTimestamp) 668 | } 669 | 670 | func countUnmatched(vrps VRPMap, others VRPMap, thresholdTimestamp int64) float64 { 671 | count := 0 672 | 673 | for key, vrp := range vrps { 674 | other := others[key] 675 | if isDefinitelyVisible(vrp, thresholdTimestamp) { 676 | // VRP is definitely visible, count if other side is definitely not visible 677 | if isDefinitelyNotVisisble(other, thresholdTimestamp) { 678 | count++ 679 | } 680 | } else if isDefinitelyNotVisisble(vrp, thresholdTimestamp) { 681 | // VRP is definitely not visible, count if other side is definitely visible 682 | if isDefinitelyVisible(other, thresholdTimestamp) { 683 | count++ 684 | } 685 | } 686 | } 687 | 688 | return float64(count) 689 | } 690 | 691 | func Diff(a, b VRPMap) VRPMap { 692 | onlyInA := make(VRPMap) 693 | for key, vrpA := range a { 694 | if vrpA.Visible { 695 | if vrpB, ok := b[key]; !ok || !vrpB.Visible { 696 | onlyInA[key] = vrpA 697 | } 698 | } 699 | } 700 | return onlyInA 701 | } 702 | 703 | func VRPArray(a VRPMap) []*VRPJsonSimple { 704 | result := make([]*VRPJsonSimple, 0) 705 | for _, vrp := range a { 706 | result = append(result, vrp) 707 | } 708 | return result 709 | } 710 | 711 | type diffMetadata struct { 712 | LastFetch int64 `json:"last-fetch"` 713 | URL string `json:"url"` 714 | Serial uint32 `json:"serial"` 715 | SessionID uint16 `json:"session-id"` 716 | Count int `json:"count"` 717 | 718 | RTRRefresh uint32 `json:"rtr-refresh"` 719 | RTRRetry uint32 `json:"rtr-retry"` 720 | RTRExpire uint32 `json:"rtr-expire"` 721 | } 722 | 723 | type VRPJsonSimple struct { 724 | ASN uint32 `json:"asn"` 725 | Length uint8 `json:"max-length"` 726 | Prefix string `json:"prefix"` 727 | FirstSeen int64 `json:"first-seen"` 728 | LastSeen int64 `json:"last-seen"` 729 | Visible bool `json:"visible"` 730 | BGPSecData *prefixfile.BgpSecKeyJson `json:"bgpsec,omitempty"` 731 | } 732 | type VRPMap map[string]*VRPJsonSimple 733 | 734 | type diffExport struct { 735 | MetadataPrimary *diffMetadata `json:"metadata-primary"` 736 | MetadataSecondary *diffMetadata `json:"metadata-secondary"` 737 | OnlyInPrimary []*VRPJsonSimple `json:"only-primary"` 738 | OnlyInSecondary []*VRPJsonSimple `json:"only-secondary"` 739 | } 740 | 741 | func (c *Comparator) ServeDiff(wr http.ResponseWriter, req *http.Request) { 742 | enc := json.NewEncoder(wr) 743 | 744 | c.diffLock.RLock() 745 | d1 := c.onlyIn1 746 | d2 := c.onlyIn2 747 | 748 | md1 := c.md1 749 | md2 := c.md2 750 | c.diffLock.RUnlock() 751 | export := diffExport{ 752 | MetadataPrimary: md1, 753 | MetadataSecondary: md2, 754 | OnlyInPrimary: VRPArray(d1), 755 | OnlyInSecondary: VRPArray(d2), 756 | } 757 | 758 | wr.Header().Add("content-type", "application/json") 759 | 760 | enc.Encode(export) 761 | } 762 | 763 | func (c *Comparator) Compare() { 764 | var donePrimary, doneSecondary bool 765 | var stop bool 766 | startedAt := time.Now().Unix() 767 | for !stop { 768 | select { 769 | case <-c.q: 770 | stop = true 771 | continue 772 | case id := <-c.comp: 773 | log.Infof("Worker %d finished: comparison", id) 774 | 775 | vrps1, md1 := c.PrimaryClient.GetData() 776 | vrps2, md2 := c.SecondaryClient.GetData() 777 | 778 | onlyIn1 := Diff(vrps1, vrps2) 779 | onlyIn2 := Diff(vrps2, vrps1) 780 | 781 | c.diffLock.Lock() 782 | c.onlyIn1 = onlyIn1 783 | c.onlyIn2 = onlyIn2 784 | 785 | c.md1 = md1 786 | c.md2 = md2 787 | 788 | VRPCount.With( 789 | prometheus.Labels{ 790 | "server": "primary", 791 | "url": md1.URL, 792 | "type": "total", 793 | }).Set(float64(len(vrps1))) 794 | 795 | VRPCount.With( 796 | prometheus.Labels{ 797 | "server": "primary", 798 | "url": md1.URL, 799 | "type": "diff", 800 | }).Set(float64(len(onlyIn1))) 801 | 802 | VRPCount.With( 803 | prometheus.Labels{ 804 | "server": "secondary", 805 | "url": md2.URL, 806 | "type": "total", 807 | }).Set(float64(len(vrps2))) 808 | 809 | VRPCount.With( 810 | prometheus.Labels{ 811 | "server": "secondary", 812 | "url": md2.URL, 813 | "type": "diff", 814 | }).Set(float64(len(onlyIn2))) 815 | 816 | for _, visibleFor := range visibilityThresholds { 817 | thresholdTimestamp := time.Now().Unix() - visibleFor 818 | // Prevent differences with value 0 appearing if the process has not 819 | // been running long enough for them to exist. 820 | if thresholdTimestamp >= startedAt { 821 | VRPDifferenceForDuration.With( 822 | prometheus.Labels{ 823 | "lhs_url": md1.URL, 824 | "rhs_url": md2.URL, 825 | "visibility_seconds": strconv.FormatInt(visibleFor, 10), 826 | }).Set(countUnmatched(onlyIn1, vrps2, thresholdTimestamp)) 827 | 828 | VRPDifferenceForDuration.With( 829 | prometheus.Labels{ 830 | "lhs_url": md2.URL, 831 | "rhs_url": md1.URL, 832 | "visibility_seconds": strconv.FormatInt(visibleFor, 10), 833 | }).Set(countUnmatched(onlyIn2, vrps1, thresholdTimestamp)) 834 | } 835 | } 836 | 837 | RTRSerial.With( 838 | prometheus.Labels{ 839 | "server": "primary", 840 | "url": md1.URL, 841 | }).Set(float64(md1.Serial)) 842 | 843 | RTRSerial.With( 844 | prometheus.Labels{ 845 | "server": "secondary", 846 | "url": md2.URL, 847 | }).Set(float64(md2.Serial)) 848 | 849 | RTRSession.With( 850 | prometheus.Labels{ 851 | "server": "primary", 852 | "url": md1.URL, 853 | }).Set(float64(md1.SessionID)) 854 | 855 | RTRSession.With( 856 | prometheus.Labels{ 857 | "server": "secondary", 858 | "url": md2.URL, 859 | }).Set(float64(md2.SessionID)) 860 | 861 | c.diffLock.Unlock() 862 | 863 | if id == 1 { 864 | donePrimary = true 865 | 866 | LastUpdate.With( 867 | prometheus.Labels{ 868 | "server": "primary", 869 | "url": md1.URL, 870 | }).Set(float64(md1.LastFetch)) 871 | 872 | } else if id == 2 { 873 | doneSecondary = true 874 | 875 | LastUpdate.With( 876 | prometheus.Labels{ 877 | "server": "secondary", 878 | "url": md2.URL, 879 | }).Set(float64(md2.LastFetch)) 880 | } 881 | 882 | if c.OneOff && donePrimary && doneSecondary { 883 | // save file (one-off) 884 | stop = true 885 | } 886 | 887 | } 888 | } 889 | } 890 | 891 | func (c *Comparator) Start() error { 892 | if c.PrimaryClient == nil || c.SecondaryClient == nil { 893 | return errors.New("must have two clients") 894 | } 895 | 896 | wg := &sync.WaitGroup{} 897 | wg.Add(2) 898 | go func() { 899 | defer wg.Done() 900 | c.PrimaryClient.Start(1, c.comp) 901 | }() 902 | go func() { 903 | defer wg.Done() 904 | c.SecondaryClient.Start(2, c.comp) 905 | }() 906 | 907 | go c.Compare() 908 | 909 | wg.Wait() 910 | close(c.q) 911 | return nil 912 | } 913 | 914 | func main() { 915 | flag.Parse() 916 | if flag.NArg() > 0 { 917 | fmt.Printf("%s: illegal positional argument(s) provided (\"%s\") - did you mean to provide a flag?\n", os.Args[0], strings.Join(flag.Args(), " ")) 918 | os.Exit(2) 919 | } 920 | if *Version { 921 | fmt.Println(AppVersion) 922 | os.Exit(0) 923 | } 924 | 925 | lvl, _ := log.ParseLevel(*LogLevel) 926 | log.SetLevel(lvl) 927 | 928 | highestVisibilityThreshold := time.Second * time.Duration(visibilityThresholds[len(visibilityThresholds)-1]) 929 | if highestVisibilityThreshold > *GracePeriod { 930 | log.Warnf("Highest visibility threshold %v greater than grace period %v, adjusting grace period", highestVisibilityThreshold, GracePeriod) 931 | *GracePeriod = highestVisibilityThreshold 932 | } 933 | 934 | fc := utils.NewFetchConfig() 935 | fc.EnableEtags = !*DisableConditionalRequests 936 | fc.EnableLastModified = !*DisableConditionalRequests 937 | fc.UserAgent = *UserAgent 938 | 939 | c1 := NewClient() 940 | var ok bool 941 | c1.authType, ok = authToId[*PrimarySSHAuth] 942 | if !ok { 943 | log.Fatalf("Auth type %v unknown", *PrimarySSHAuth) 944 | } 945 | 946 | c1.SSHAuthUser = *PrimarySSHAuthUser 947 | c1.SSHAuthPassword = *PrimarySSHAuthPassword 948 | c1.Path = *PrimaryHost 949 | c1.RefreshInterval = *PrimaryRefresh 950 | c1.FetchConfig = fc 951 | c1.BreakRTR = *PrimaryRTRBreak 952 | 953 | if c1.SSHAuthPassword == "" { 954 | c1.SSHAuthPassword = os.Getenv(fmt.Sprintf("%s_1", ENV_SSH_PASSWORD)) 955 | } 956 | 957 | if c1.authType == METHOD_KEY { 958 | var keyBytes []byte 959 | var err error 960 | if *PrimarySSHAuthKey == "" { 961 | keyBytesStr := os.Getenv(fmt.Sprintf("%s_1", ENV_SSH_KEY)) 962 | keyBytes = []byte(keyBytesStr) 963 | } else { 964 | keyBytes, err = os.ReadFile(*PrimarySSHAuthKey) 965 | if err != nil { 966 | log.Fatal(err) 967 | } 968 | } 969 | c1.keyBytes = keyBytes 970 | } 971 | 972 | c2 := NewClient() 973 | c2.authType, ok = authToId[*SecondarySSHAuth] 974 | if !ok { 975 | log.Fatalf("Auth type %v unknown", *SecondarySSHAuth) 976 | } 977 | 978 | c2.SSHAuthUser = *SecondarySSHAuthUser 979 | c2.SSHAuthPassword = *SecondarySSHAuthPassword 980 | c2.Path = *SecondaryHost 981 | c2.RefreshInterval = *SecondaryRefresh 982 | c2.FetchConfig = fc 983 | c2.BreakRTR = *SecondaryRTRBreak 984 | 985 | if method, ok := authToId[*SecondarySSHAuth]; ok && method == METHOD_KEY { 986 | c2.SSHAuthPassword = os.Getenv(fmt.Sprintf("%s_2", ENV_SSH_PASSWORD)) 987 | } 988 | 989 | if c2.authType == METHOD_KEY { 990 | var keyBytes []byte 991 | var err error 992 | if *SecondarySSHAuthKey == "" { 993 | keyBytesStr := os.Getenv(fmt.Sprintf("%s_2", ENV_SSH_KEY)) 994 | keyBytes = []byte(keyBytesStr) 995 | } else { 996 | keyBytes, err = os.ReadFile(*SecondarySSHAuthKey) 997 | if err != nil { 998 | log.Fatal(err) 999 | } 1000 | } 1001 | c2.keyBytes = keyBytes 1002 | } 1003 | 1004 | cmp := NewComparator(c1, c2) 1005 | 1006 | go func() { 1007 | http.HandleFunc(fmt.Sprintf("/%s", *OutFile), cmp.ServeDiff) 1008 | http.Handle(*MetricsPath, promhttp.Handler()) 1009 | http.HandleFunc("/", ServeIndex) 1010 | 1011 | log.Fatal(http.ListenAndServe(*Addr, nil)) 1012 | }() 1013 | 1014 | log.Fatal(cmp.Start()) 1015 | 1016 | } 1017 | 1018 | type IndexTemplateVars struct { 1019 | MetricsPath string 1020 | OutFile string 1021 | Addr string 1022 | } 1023 | 1024 | func ServeIndex(wr http.ResponseWriter, req *http.Request) { 1025 | tmpl, err := template.New("index").Parse(IndexTemplate) 1026 | if err == nil { 1027 | err = tmpl.Execute(wr, IndexTemplateVars{*MetricsPath, *OutFile, *Addr}) 1028 | } 1029 | 1030 | if err != nil { 1031 | io.WriteString(wr, IndexTemplate) 1032 | } 1033 | } 1034 | --------------------------------------------------------------------------------