├── 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 |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 |
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 |  4 | 5 | [](https://hub.docker.com/r/rpki/stayrtr) 6 | 7 | 8 |