├── .eslintrc.cjs ├── .github ├── renovate.json └── workflows │ ├── codeql-analysis.yml │ ├── dependency-review.yml │ ├── release.yml │ └── tests.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yaml ├── .ignore ├── .vscode └── launch.json ├── LICENSE ├── Makefile ├── README.md ├── cli ├── client.go ├── client_accept.go ├── client_dial.go ├── client_forwarding.go ├── client_identify.go ├── client_listen.go ├── client_nodeinfo.go ├── client_subscribe.go ├── daemonflags.go ├── dialeraddrs.go ├── pubsub-nats.go ├── pubsub.go └── util │ ├── cli.go │ └── util.go ├── cmd └── bifrost │ ├── .gitignore │ ├── bifrost_daemon.yaml │ ├── cmd_client.go │ ├── cmd_daemon.go │ ├── cmd_util.go │ └── main.go ├── core ├── core.go └── test │ └── core.go ├── daemon ├── api │ ├── api.go │ ├── api.pb.go │ ├── api.pb.ts │ ├── api.proto │ ├── api_accept.go │ ├── api_identify.go │ ├── api_peer_info.go │ ├── api_pubsub_subscribe.go │ ├── api_stream_dial.go │ ├── bifrost_api.go │ └── controller │ │ ├── config.go │ │ ├── controller.go │ │ ├── controller.pb.go │ │ ├── controller.pb.ts │ │ ├── controller.proto │ │ └── factory.go ├── daemon.go └── prof │ └── prof.go ├── deps.go ├── doc ├── WEB_TESTS.md └── img │ └── bifrost-logo.png ├── entitygraph ├── config.go ├── config.pb.go ├── config.pb.ts ├── config.proto ├── controller.go ├── establish_link.go ├── factory.go ├── get_peer.go ├── link_entity.go ├── lookup_transport.go ├── peer_entity.go ├── reporter.go ├── transport_assoc_entity.go └── transport_entity.go ├── examples ├── http-forwarding │ ├── README.md │ ├── node-1.yaml │ └── node-2.yaml ├── nats-host │ ├── .gitignore │ ├── README.md │ ├── main.go │ ├── nats-single │ │ ├── .gitignore │ │ └── main.go │ ├── peer-0.pem │ ├── peer-1.pem │ └── peer-2.pem ├── priv │ ├── node-1.pem │ ├── node-2.pem │ └── node-3.pem ├── serial-forwarding │ └── serial-forwarding.org ├── udp-link │ ├── .gitignore │ ├── README.md │ └── main.go ├── webrtc-forwarding │ ├── README.md │ ├── node-1.yaml │ ├── node-2.yaml │ └── server │ │ ├── .gitignore │ │ ├── README.md │ │ └── main.go └── websocket-browser-link │ ├── README.md │ ├── browser │ ├── .gitignore │ ├── build.bash │ ├── index.go │ ├── index.html │ ├── index_nojs.go │ └── serve.bash │ ├── common │ ├── common.go │ └── common_js.go │ ├── screenshot.png │ └── server │ ├── .gitignore │ └── server.go ├── go.mod ├── go.sum ├── hash ├── hash.go ├── hash.pb.go ├── hash.pb.ts ├── hash.proto └── hash_test.go ├── http ├── bus-handler.go ├── bus-http-handler.go ├── bus-http-handler_test.go ├── dir-lookup-http-handler.go ├── dir-lookup-http-handler_test.go ├── http-handler-builder.go ├── http-handler-controller.go ├── http-handler-controller_test.go ├── http-handler.go ├── http-handler_test.go ├── listener │ ├── config.go │ ├── config.pb.go │ ├── config.pb.ts │ ├── config.proto │ ├── factory.go │ └── listener.go ├── mock-handler_test.go └── res-lookup-http-handler.go ├── keypem ├── errors.go ├── keyfile │ └── keyfile.go └── keypem.go ├── link ├── establish-link-ex.go ├── establish-link.go ├── establish │ ├── config.go │ ├── config.pb.go │ ├── config.pb.ts │ ├── config.proto │ ├── controller.go │ └── factory.go ├── handle-mounted-stream.go ├── hold-open │ ├── config.go │ ├── config.pb.go │ ├── config.pb.ts │ ├── config.proto │ ├── controller.go │ ├── controller_establish_link.go │ ├── establish_link.go │ └── factory.go ├── link.go ├── mounted-stream.go ├── open-stream-ex.go ├── open-stream-with-link-ex.go ├── open-stream-with-link.go └── open-stream.go ├── package.json ├── peer ├── api │ ├── api.go │ ├── api.pb.go │ ├── api.pb.ts │ ├── api.proto │ ├── api_srpc.pb.go │ └── api_srpc.pb.ts ├── controller │ ├── config.go │ ├── config.pb.go │ ├── config.pb.ts │ ├── config.proto │ ├── controller.go │ ├── controller_test.go │ └── factory.go ├── derive.go ├── derive_test.go ├── directive.go ├── directive_ex.go ├── encrypt-curve25519.go ├── encrypt-rsa.go ├── encrypt.go ├── encrypt_test.go ├── errors.go ├── id.go ├── mock_test.go ├── peer.go ├── peer.pb.go ├── peer.pb.ts ├── peer.proto ├── peer_addr.go ├── resolver.go ├── signature.go ├── signed-msg.go ├── signed-msg_test.go └── ssh │ ├── pubkey.go │ ├── signer.go │ └── signer_test.go ├── protocol ├── errors.go └── id.go ├── pubsub ├── api │ ├── api.go │ ├── api.pb.go │ ├── api.pb.ts │ ├── api.proto │ ├── api_srpc.pb.go │ └── api_srpc.pb.ts ├── controller │ ├── channel-sub.go │ ├── controller.go │ ├── establish-link.go │ ├── mounted-stream.go │ ├── stream.go │ └── tracked-link.go ├── dir-build-channel-subscription.go ├── floodsub │ ├── config.go │ ├── controller │ │ ├── config.go │ │ ├── config.pb.go │ │ ├── config.pb.ts │ │ ├── config.proto │ │ ├── controller.go │ │ └── factory.go │ ├── floodsub.go │ ├── floodsub.pb.go │ ├── floodsub.pb.ts │ ├── floodsub.proto │ ├── stream.go │ ├── sub-handler.go │ └── sub.go ├── nats │ ├── README.md │ ├── auth-client.go │ ├── auth-router.go │ ├── client.go │ ├── config.go │ ├── controller │ │ ├── config.go │ │ ├── config.pb.go │ │ ├── config.pb.ts │ │ ├── config.proto │ │ ├── controller.go │ │ └── factory.go │ ├── keys.go │ ├── local-dialer.go │ ├── nats.go │ ├── nats.pb.go │ ├── nats.pb.ts │ ├── nats.proto │ ├── nats_stream.go │ ├── subscription.go │ └── subscription_handler.go ├── pubsub.go ├── relay │ ├── config.go │ ├── config.pb.go │ ├── config.pb.ts │ ├── config.proto │ ├── controller.go │ └── factory.go └── util │ └── pubmessage │ ├── errors.go │ ├── inner.go │ ├── message.go │ ├── pubmessage.go │ ├── pubmessage.pb.go │ ├── pubmessage.pb.ts │ └── pubmessage.proto ├── router ├── directive.go └── router.go ├── rpc ├── access │ ├── access.go │ ├── access.pb.go │ ├── access.pb.ts │ ├── access.proto │ ├── access_srpc.pb.go │ ├── access_srpc.pb.ts │ ├── access_test.go │ ├── client-controller.go │ ├── client-resolver.go │ ├── proxy-invoker.go │ └── server.go ├── bus-client.go ├── client-controller.go ├── errors.go ├── invoker-controller.go ├── invoker.go ├── lookup-rpc-client.go ├── lookup-rpc-service.go ├── rpc-service-builder.go ├── rpc-service-controller.go └── rpc-service-controller_test.go ├── signaling ├── dir-handle-signal-peer.go ├── dir-signal-peer.go ├── echo │ ├── config.go │ ├── echo.go │ ├── echo.pb.go │ ├── echo.pb.ts │ ├── echo.proto │ ├── resolver.go │ └── stream_handler.go ├── errors.go ├── rpc │ ├── client │ │ ├── client.go │ │ ├── config.go │ │ ├── config.pb.go │ │ ├── config.pb.ts │ │ ├── config.proto │ │ ├── controller.go │ │ ├── factory.go │ │ ├── res-signal-peer.go │ │ ├── session-tracker.go │ │ └── session.go │ ├── errors.go │ ├── server │ │ ├── config.go │ │ ├── controller.go │ │ ├── factory.go │ │ ├── listen.go │ │ ├── peer.go │ │ ├── server.go │ │ ├── server.pb.go │ │ ├── server.pb.ts │ │ ├── server.proto │ │ └── session.go │ ├── signaling.go │ ├── signaling.pb.go │ ├── signaling.pb.ts │ ├── signaling.proto │ ├── signaling_srpc.pb.go │ ├── signaling_srpc.pb.ts │ └── signaling_test.go └── signaling.go ├── sim ├── graph │ ├── doc.go │ ├── graph.go │ ├── graph_test.go │ ├── lan.go │ └── peer.go ├── simulate │ ├── assertions.go │ ├── option.go │ ├── peer.go │ ├── simulate.go │ └── simulate_test.go └── tests │ ├── bifrost │ ├── basic_test.go │ ├── bifrost.go │ ├── bifrost_test.go │ ├── pubsub_floodsub_test.go │ ├── pubsub_nats_test.go │ └── route_relay_test.go │ ├── common.go │ └── common_test.go ├── stream ├── api │ ├── accept │ │ ├── accept.go │ │ ├── accept.pb.go │ │ ├── accept.pb.ts │ │ ├── accept.proto │ │ ├── config.go │ │ ├── factory.go │ │ └── stream_handler.go │ ├── accept_client.go │ ├── accept_server.go │ ├── api.pb.go │ ├── api.pb.ts │ ├── api.proto │ ├── api_srpc.pb.go │ ├── api_srpc.pb.ts │ ├── dial │ │ ├── config.go │ │ ├── dial.go │ │ ├── dial.pb.go │ │ ├── dial.pb.ts │ │ └── dial.proto │ ├── dial_client.go │ ├── dial_server.go │ └── rpc │ │ ├── rpc.go │ │ ├── rpc.pb.go │ │ ├── rpc.pb.ts │ │ ├── rpc.proto │ │ └── rpc_conn.go ├── echo │ ├── config.go │ ├── echo.go │ ├── echo.pb.go │ ├── echo.pb.ts │ ├── echo.proto │ ├── factory.go │ ├── resolver.go │ └── stream_handler.go ├── forwarding │ ├── config.go │ ├── dial_resolver.go │ ├── factory.go │ ├── forwarding.go │ ├── forwarding.pb.go │ ├── forwarding.pb.ts │ ├── forwarding.proto │ └── stream_handler.go ├── listening │ ├── config.go │ ├── factory.go │ ├── listening.go │ ├── listening.pb.go │ ├── listening.pb.ts │ └── listening.proto ├── netconn │ └── netconn.go ├── packet │ └── packet.go ├── relay │ ├── config.go │ ├── factory.go │ ├── relay.go │ ├── relay.pb.go │ ├── relay.pb.ts │ ├── relay.proto │ ├── stream_handler.go │ └── stream_resolver.go ├── srpc │ ├── client │ │ ├── client.go │ │ ├── client.pb.go │ │ ├── client.pb.ts │ │ ├── client.proto │ │ ├── config.go │ │ └── controller │ │ │ ├── config.go │ │ │ ├── config.pb.go │ │ │ ├── config.pb.ts │ │ │ ├── config.proto │ │ │ ├── controller.go │ │ │ └── factory.go │ ├── server │ │ ├── config.go │ │ ├── resolver.go │ │ ├── server.go │ │ ├── server.pb.go │ │ ├── server.pb.ts │ │ └── server.proto │ ├── srpc.go │ └── srpc_test.go └── stream.go ├── testbed └── testbed.go ├── tptaddr ├── controller │ ├── config.go │ ├── config.pb.go │ ├── config.pb.ts │ ├── config.proto │ ├── controller.go │ ├── factory.go │ └── res-establish-link.go ├── dial-tpt-addr.go ├── errors.go ├── lookup-tpt-addr.go ├── static │ ├── config.go │ ├── controller.go │ ├── factory.go │ ├── static.pb.go │ ├── static.pb.ts │ └── static.proto ├── tptaddr.go └── tptaddr_test.go ├── transport ├── common │ ├── conn │ │ ├── conn.go │ │ ├── conn.pb.go │ │ ├── conn.pb.ts │ │ ├── conn.proto │ │ └── link.go │ ├── dialer │ │ ├── dialer.go │ │ ├── dialer.pb.go │ │ ├── dialer.pb.ts │ │ ├── dialer.proto │ │ ├── errors.go │ │ ├── opts.go │ │ └── transport.go │ ├── pconn │ │ ├── link.go │ │ ├── pconn.go │ │ ├── pconn.pb.go │ │ ├── pconn.pb.ts │ │ └── pconn.proto │ └── quic │ │ ├── config.go │ │ ├── dialer.go │ │ ├── errors.go │ │ ├── link.go │ │ ├── quic.go │ │ ├── quic.pb.go │ │ ├── quic.pb.ts │ │ ├── quic.proto │ │ ├── session.go │ │ ├── util.go │ │ └── uuid.go ├── config.go ├── controller │ ├── controller.go │ ├── controller.pb.go │ ├── controller.pb.ts │ ├── controller.proto │ ├── dial-tpt-addr.go │ ├── establish-header.go │ ├── establish-header_test.go │ ├── establish-link.go │ ├── link-dialer.go │ ├── link.go │ ├── lookup-transport.go │ ├── mounted-stream.go │ ├── open-stream-via-link.go │ └── open-stream.go ├── dir-lookup-transport.go ├── inproc │ ├── addr.go │ ├── config.go │ ├── factory.go │ ├── inproc.go │ ├── inproc.pb.go │ ├── inproc.pb.ts │ ├── inproc.proto │ ├── inproc_test.go │ └── pconn.go ├── transport.go ├── udp │ ├── config.go │ ├── factory.go │ ├── link.go │ ├── udp.go │ ├── udp.pb.go │ ├── udp.pb.ts │ └── udp.proto ├── webrtc │ ├── config.go │ ├── factory.go │ ├── handler.go │ ├── link.go │ ├── session.go │ ├── signal.go │ ├── uuid.go │ ├── webrtc-config.go │ ├── webrtc.go │ ├── webrtc.pb.go │ ├── webrtc.pb.ts │ ├── webrtc.proto │ └── webrtc_test.go └── websocket │ ├── config.go │ ├── factory.go │ ├── http │ ├── config.go │ ├── factory.go │ ├── http.go │ ├── http.pb.go │ ├── http.pb.ts │ └── http.proto │ ├── link.go │ ├── packet-conn.go │ ├── websocket.go │ ├── websocket.pb.go │ ├── websocket.pb.ts │ └── websocket.proto ├── tsconfig.json ├── util ├── confparse │ ├── duration.go │ ├── keys.go │ ├── keys_test.go │ ├── peer_id.go │ ├── pem.go │ ├── pem_test.go │ ├── protocol_id.go │ ├── regexp.go │ ├── regexp_test.go │ ├── timestamp.go │ ├── timestamp_test.go │ ├── urls.go │ └── urls_test.go ├── deadsync │ └── deadsync.go ├── extra25519 │ ├── extra25519.go │ └── lo25519.go ├── labels │ └── labels.go ├── logconn │ └── log-conn.go ├── logrw │ ├── logr.go │ ├── logrc.go │ ├── logw.go │ └── logwc.go ├── randstring │ ├── random-id.go │ ├── randstring.go │ └── randstring_test.go ├── rwc │ ├── conn-addr.go │ ├── conn.go │ ├── conn_test.go │ ├── packet-conn.go │ ├── packet-conn_test.go │ ├── rwc-overlay.go │ ├── rwc-packet-conn.go │ └── rwc.go ├── saddr │ └── saddr.go └── scrc │ └── scrc.go └── yarn.lock /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | plugins: ['@typescript-eslint', 'unused-imports'], 5 | extends: [ 6 | 'eslint:recommended', 7 | 'plugin:@typescript-eslint/recommended', 8 | 'plugin:react-hooks/recommended', 9 | 'prettier', 10 | ], 11 | parserOptions: { 12 | project: './tsconfig.json', 13 | }, 14 | rules: { 15 | '@typescript-eslint/explicit-module-boundary-types': 'off', 16 | '@typescript-eslint/no-non-null-assertion': 'off', 17 | }, 18 | ignorePatterns: [ 19 | "node_modules", 20 | "dist", 21 | "coverage", 22 | "bundle", 23 | "runtime", 24 | "vendor", 25 | ".eslintrc.js", 26 | "wasm_exec.js" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | ":semanticPrefixFixDepsChoreOthers", 5 | ":ignoreModulesAndTests", 6 | "group:all", 7 | "workarounds:all" 8 | ], 9 | "branchConcurrentLimit": 0, 10 | "packageRules": [ 11 | { 12 | "matchManagers": ["gomod"], 13 | "matchDepTypes": ["replace"], 14 | "enabled": false 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | # Dependency Review Action 2 | # 3 | # This Action will scan dependency manifest files that change as part of a Pull Request, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging. 4 | # 5 | # Source repository: https://github.com/actions/dependency-review-action 6 | # Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement 7 | name: 'Dependency Review' 8 | on: [pull_request] 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | dependency-review: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: 'Checkout Repository' 18 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 19 | - name: 'Dependency Review' 20 | uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1 21 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | goreleaser: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | go: ['1.24'] 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 20 | with: 21 | fetch-depth: 0 22 | 23 | - name: Setup Go ${{ matrix.go }} 24 | uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 25 | with: 26 | go-version: ${{ matrix.go }} 27 | 28 | - name: Release 29 | run: make release 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /coverage 3 | /build 4 | /.log 5 | /.snowpack 6 | 7 | # misc 8 | .DS_Store 9 | .env.local 10 | .env.development.local 11 | .env.test.local 12 | .env.production.local 13 | 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | 18 | .#* 19 | /dist 20 | .*.swp 21 | .vs/ 22 | .vscode/ 23 | !.vscode/launch.json 24 | 25 | vendor/ 26 | debug.test* 27 | __debug* 28 | .aider* 29 | 30 | dist/ 31 | /.tools/ 32 | .env 33 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json 2 | # vim: set ts=2 sw=2 tw=0 fo=cnqoj 3 | 4 | version: 2 5 | 6 | before: 7 | hooks: 8 | - go mod tidy 9 | 10 | builds: 11 | - id: "bifrost" 12 | main: ./cmd/bifrost 13 | binary: bifrost 14 | env: 15 | - CGO_ENABLED=0 16 | flags: 17 | - -trimpath 18 | ldflags: 19 | - -s -w -X main.version={{.Version}} 20 | goos: 21 | - linux 22 | - windows 23 | - darwin 24 | goarch: 25 | - amd64 26 | - arm64 27 | 28 | archives: 29 | - formats: ["tar.gz"] 30 | # this name template makes the OS and Arch compatible with the results of `uname`. 31 | name_template: >- 32 | {{ .ProjectName }}_ 33 | {{- title .Os }}_ 34 | {{- if eq .Arch "amd64" }}x86_64 35 | {{- else }}{{ .Arch }}{{ end }} 36 | {{- if .Arm }}v{{ .Arm }}{{ end }} 37 | # use zip for windows archives 38 | format_overrides: 39 | - goos: windows 40 | formats: ['zip'] 41 | 42 | changelog: 43 | sort: asc 44 | filters: 45 | exclude: 46 | - "^docs:" 47 | - "^test:" 48 | - "^chore:" 49 | - "^.github:" 50 | - "^hack:" 51 | -------------------------------------------------------------------------------- /.ignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | vendor/ 3 | *.pb.go 4 | *.pb.ts 5 | go.sum 6 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch Daemon", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "auto", 12 | "program": "${workspaceRoot}/cmd/bifrost", 13 | "env": {}, 14 | "args": ["daemon", "--udp-listen", ":5112"] 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # https://github.com/aperturerobotics/template 2 | PROJECT_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) 3 | SHELL:=bash 4 | MAKEFLAGS += --no-print-directory 5 | 6 | GO_VENDOR_DIR := ./vendor 7 | COMMON_DIR := $(GO_VENDOR_DIR)/github.com/aperturerobotics/common 8 | 9 | COMMON_MAKEFILE := $(COMMON_DIR)/Makefile 10 | TOOLS_DIR := .tools 11 | TOOLS_MAKEFILE := .tools/Makefile 12 | 13 | export GO111MODULE=on 14 | undefine GOARCH 15 | undefine GOOS 16 | 17 | .PHONY: $(MAKECMDGOALS) 18 | 19 | all: 20 | 21 | $(COMMON_MAKEFILE): vendor 22 | @if [ ! -f $(COMMON_MAKEFILE) ]; then \ 23 | echo "Please add github.com/aperturerobotics/common to your go.mod."; \ 24 | exit 1; \ 25 | fi 26 | 27 | $(TOOLS_MAKEFILE): $(COMMON_MAKEFILE) 28 | @$(MAKE) -C $(COMMON_DIR) TOOLS_DIR="$(TOOLS_DIR)" PROJECT_DIR="$(PROJECT_DIR)" tools 29 | 30 | $(MAKECMDGOALS): $(TOOLS_MAKEFILE) 31 | @$(MAKE) -C $(TOOLS_DIR) TOOLS_DIR="$(TOOLS_DIR)" PROJECT_DIR="$(PROJECT_DIR)" $@ 32 | 33 | %: $(TOOLS_MAKEFILE) 34 | @$(MAKE) -C $(TOOLS_DIR) TOOLS_DIR="$(TOOLS_DIR)" PROJECT_DIR="$(PROJECT_DIR)" $@ 35 | 36 | vendor: 37 | go mod vendor 38 | -------------------------------------------------------------------------------- /cli/client_accept.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "os" 5 | 6 | stream_api "github.com/aperturerobotics/bifrost/stream/api" 7 | stream_api_rpc "github.com/aperturerobotics/bifrost/stream/api/rpc" 8 | "github.com/aperturerobotics/bifrost/util/rwc" 9 | "github.com/aperturerobotics/cli" 10 | ) 11 | 12 | // RunAccept runs the accept command. 13 | func (a *ClientArgs) RunAccept(*cli.Context) error { 14 | ctx := a.GetContext() 15 | c, err := a.BuildClient() 16 | if err != nil { 17 | return err 18 | } 19 | 20 | client, err := c.AcceptStream(ctx) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | if len(a.RemotePeerIdsCsv) != 0 { 26 | a.AcceptConf.RemotePeerIds = a.ParseRemotePeerIdsCsv() 27 | } 28 | err = client.Send(&stream_api.AcceptStreamRequest{ 29 | Config: &a.AcceptConf, 30 | }) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | rpcClient := stream_api.NewAcceptStreamClientRPC(client) 36 | return stream_api_rpc.AttachRPCToStream( 37 | rpcClient, 38 | rwc.NewReadWriteCloser(os.Stdin, os.Stdout), 39 | nil, 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /cli/client_dial.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "os" 5 | 6 | stream_api "github.com/aperturerobotics/bifrost/stream/api" 7 | stream_api_rpc "github.com/aperturerobotics/bifrost/stream/api/rpc" 8 | "github.com/aperturerobotics/bifrost/util/rwc" 9 | "github.com/aperturerobotics/cli" 10 | ) 11 | 12 | // RunDial runs the dial command. 13 | func (a *ClientArgs) RunDial(*cli.Context) error { 14 | ctx := a.GetContext() 15 | c, err := a.BuildClient() 16 | if err != nil { 17 | return err 18 | } 19 | 20 | client, err := c.DialStream(ctx) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | if len(a.RemotePeerIdsCsv) != 0 { 26 | a.AcceptConf.RemotePeerIds = a.ParseRemotePeerIdsCsv() 27 | } 28 | err = client.Send(&stream_api.DialStreamRequest{ 29 | Config: &a.DialConf, 30 | }) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | rpcClient := stream_api.NewDialStreamClientRPC(client) 36 | return stream_api_rpc.AttachRPCToStream( 37 | rpcClient, 38 | rwc.NewReadWriteCloser(os.Stdin, os.Stdout), 39 | nil, 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /cli/client_forwarding.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "os" 5 | 6 | stream_api "github.com/aperturerobotics/bifrost/stream/api" 7 | "github.com/aperturerobotics/cli" 8 | ) 9 | 10 | // RunForwarding runs the forwarding command. 11 | func (a *ClientArgs) RunForwarding(_ *cli.Context) error { 12 | ctx := a.GetContext() 13 | c, err := a.BuildClient() 14 | if err != nil { 15 | return err 16 | } 17 | 18 | req, err := c.ForwardStreams(ctx, &stream_api.ForwardStreamsRequest{ 19 | ForwardingConfig: &a.ForwardingConf, 20 | }) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | for { 26 | resp, err := req.Recv() 27 | if err != nil { 28 | return err 29 | } 30 | 31 | os.Stdout.WriteString(resp.GetControllerStatus().String()) 32 | os.Stdout.WriteString("\n") 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /cli/client_identify.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "os" 5 | 6 | peer_api "github.com/aperturerobotics/bifrost/peer/api" 7 | "github.com/aperturerobotics/cli" 8 | ) 9 | 10 | // RunIdentifyController runs an identify controller. 11 | func (a *ClientArgs) RunIdentifyController(_ *cli.Context) error { 12 | c, err := a.BuildClient() 13 | if err != nil { 14 | return err 15 | } 16 | 17 | dat, _, err := a.LoadOrGenerateIdentifyKey() 18 | if err != nil { 19 | return err 20 | } 21 | a.IdentifyConf.PrivKey = string(dat) 22 | if err := a.IdentifyConf.Validate(); err != nil { 23 | return err 24 | } 25 | 26 | req, err := c.Identify(a.GetContext(), &peer_api.IdentifyRequest{ 27 | Config: &a.IdentifyConf, 28 | }) 29 | if err != nil { 30 | return err 31 | } 32 | 33 | for { 34 | resp, err := req.Recv() 35 | if err != nil { 36 | return err 37 | } 38 | 39 | os.Stdout.WriteString(resp.GetControllerStatus().String()) 40 | os.Stdout.WriteString("\n") 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /cli/client_listen.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "os" 5 | 6 | stream_api "github.com/aperturerobotics/bifrost/stream/api" 7 | "github.com/aperturerobotics/cli" 8 | ) 9 | 10 | // RunListen runs the listen command. 11 | func (a *ClientArgs) RunListen(*cli.Context) error { 12 | ctx := a.GetContext() 13 | c, err := a.BuildClient() 14 | if err != nil { 15 | return err 16 | } 17 | req, err := c.ListenStreams(ctx, &stream_api.ListenStreamsRequest{ 18 | ListeningConfig: &a.ListeningConf, 19 | }) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | for { 25 | resp, err := req.Recv() 26 | if err != nil { 27 | return err 28 | } 29 | 30 | os.Stdout.WriteString(resp.GetControllerStatus().String()) 31 | os.Stdout.WriteString("\n") 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /cli/client_nodeinfo.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | 7 | peer_api "github.com/aperturerobotics/bifrost/peer/api" 8 | "github.com/aperturerobotics/cli" 9 | ) 10 | 11 | // RunPeerInfo runs the peer information command. 12 | func (a *ClientArgs) RunPeerInfo(_ *cli.Context) error { 13 | ctx := a.GetContext() 14 | c, err := a.BuildClient() 15 | if err != nil { 16 | return err 17 | } 18 | 19 | ni, err := c.GetPeerInfo(ctx, &peer_api.GetPeerInfoRequest{}) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | dat, err := json.MarshalIndent(ni, "", "\t") 25 | if err != nil { 26 | return err 27 | } 28 | os.Stdout.WriteString(string(dat)) 29 | os.Stdout.WriteString("\n") 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /cli/dialeraddrs.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/aperturerobotics/bifrost/peer" 7 | "github.com/aperturerobotics/bifrost/transport/common/dialer" 8 | "github.com/aperturerobotics/bifrost/util/confparse" 9 | "github.com/aperturerobotics/cli" 10 | ) 11 | 12 | // parseDialerAddrs parses a dialer map from a string slice 13 | func parseDialerAddrs(ss cli.StringSlice) (map[string]*dialer.DialerOpts, error) { 14 | m := make(map[string]*dialer.DialerOpts) 15 | for _, s := range ss.Value() { 16 | pair := strings.Split(s, "@") 17 | if len(pair) < 2 { 18 | continue 19 | } 20 | pid, err := confparse.ParsePeerID(strings.TrimSpace(pair[0])) 21 | if err != nil { 22 | return nil, err 23 | } 24 | if pid == peer.ID("") { 25 | continue 26 | } 27 | m[pid.String()] = &dialer.DialerOpts{ 28 | Address: strings.TrimSpace(pair[1]), 29 | } 30 | } 31 | return m, nil 32 | } 33 | -------------------------------------------------------------------------------- /cli/pubsub-nats.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package cli 5 | 6 | import ( 7 | "github.com/aperturerobotics/bifrost/pubsub/nats" 8 | nats_controller "github.com/aperturerobotics/bifrost/pubsub/nats/controller" 9 | "github.com/aperturerobotics/controllerbus/bus" 10 | "github.com/aperturerobotics/controllerbus/config" 11 | "github.com/aperturerobotics/controllerbus/controller" 12 | ) 13 | 14 | func init() { 15 | pubsubFactories = append(pubsubFactories, func(b bus.Bus) controller.Factory { 16 | return nats_controller.NewFactory(b) 17 | }) 18 | pubsubProviders["nats"] = func(args *DaemonArgs) (config.Config, error) { 19 | return &nats_controller.Config{ 20 | PeerId: "any", 21 | NatsConfig: &nats.Config{ 22 | ClusterName: "bifrost", 23 | }, 24 | }, nil 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /cli/pubsub.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "sort" 5 | "strings" 6 | 7 | "github.com/aperturerobotics/bifrost/pubsub/floodsub" 8 | floodsub_controller "github.com/aperturerobotics/bifrost/pubsub/floodsub/controller" 9 | "github.com/aperturerobotics/controllerbus/bus" 10 | "github.com/aperturerobotics/controllerbus/config" 11 | "github.com/aperturerobotics/controllerbus/controller" 12 | ) 13 | 14 | // pubsubFactories contains the static compiled-in pubsub factories 15 | var pubsubFactories [](func(b bus.Bus) controller.Factory) 16 | 17 | // pubsubProviders contains the static compiled-in pubsub provider presets 18 | var pubsubProviders = map[string](func(args *DaemonArgs) (config.Config, error)){ 19 | "floodsub": func(args *DaemonArgs) (config.Config, error) { 20 | return &floodsub_controller.Config{ 21 | FloodsubConfig: &floodsub.Config{}, 22 | }, nil 23 | }, 24 | } 25 | 26 | // buildPubsubUsage returns the pubsub usage string 27 | func buildPubsubUsage() string { 28 | var strb strings.Builder 29 | _, _ = strb.WriteString("if set, will configure pubsub from options: [") 30 | keys := make([]string, 0, len(pubsubProviders)) 31 | for k := range pubsubProviders { 32 | keys = append(keys, k) 33 | } 34 | sort.Strings(keys) 35 | for i, k := range keys { 36 | strb.WriteString(k) 37 | if i != len(keys)-1 { 38 | strb.WriteString(", ") 39 | } 40 | } 41 | strb.WriteString("]") 42 | return strb.String() 43 | } 44 | -------------------------------------------------------------------------------- /cmd/bifrost/.gitignore: -------------------------------------------------------------------------------- 1 | bifrost 2 | *.pem 3 | bifrost.exe 4 | debug 5 | testrig* -------------------------------------------------------------------------------- /cmd/bifrost/bifrost_daemon.yaml: -------------------------------------------------------------------------------- 1 | hold-open: 2 | id: bifrost/link/hold-open 3 | config: {} 4 | 5 | pubsub: 6 | id: bifrost/floodsub 7 | config: {} 8 | 9 | udp: 10 | id: bifrost/udp 11 | config: 12 | listenAddr: :5112 13 | -------------------------------------------------------------------------------- /cmd/bifrost/cmd_client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | bcli "github.com/aperturerobotics/bifrost/cli" 5 | "github.com/aperturerobotics/cli" 6 | ) 7 | 8 | // cliArgs are the client arguments 9 | var cliArgs bcli.ClientArgs 10 | 11 | func init() { 12 | clientCommands := (&cliArgs).BuildCommands() 13 | clientFlags := (&cliArgs).BuildFlags() 14 | cbusCmd := (&cliArgs.CbusConf).BuildControllerBusCommand() 15 | cbusCmd.Before = func(_ *cli.Context) error { 16 | client, err := (&cliArgs).BuildClient() 17 | if err != nil { 18 | return err 19 | } 20 | (&cliArgs.CbusConf).SetClient(client) 21 | return nil 22 | } 23 | clientCommands = append(clientCommands, cbusCmd) 24 | commands = append( 25 | commands, 26 | &cli.Command{ 27 | Name: "client", 28 | Usage: "client sub-commands", 29 | Subcommands: clientCommands, 30 | Flags: clientFlags, 31 | }, 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /cmd/bifrost/cmd_util.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | util "github.com/aperturerobotics/bifrost/cli/util" 5 | "github.com/aperturerobotics/cli" 6 | ) 7 | 8 | var utilArgs util.UtilArgs 9 | 10 | func init() { 11 | utilCommands := (&utilArgs).BuildCommands() 12 | commands = append( 13 | commands, 14 | &cli.Command{ 15 | Name: "util", 16 | Usage: "utility sub-commands", 17 | Subcommands: utilCommands, 18 | Flags: (&utilArgs).BuildFlags(), 19 | }, 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /cmd/bifrost/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/aperturerobotics/cli" 8 | ) 9 | 10 | // Commands are the CLI commands 11 | var commands []*cli.Command 12 | 13 | var version string 14 | 15 | func main() { 16 | app := cli.NewApp() 17 | app.Name = "bifrost" 18 | app.HideVersion = true 19 | app.Usage = "command-line node and tools for bifrost" 20 | app.Commands = commands 21 | 22 | // Metadata 23 | app.Description = "Check out the docs and examples:\n\nhttps://github.com/aperturerobotics/bifrost" 24 | app.Copyright = "Apache License, Version 2.0." 25 | 26 | // Hide version if unset 27 | if version == "" { 28 | app.HideVersion = true 29 | } else { 30 | app.Version = version 31 | } 32 | 33 | if err := app.Run(os.Args); err != nil { 34 | fmt.Println(err.Error()) 35 | os.Exit(1) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /core/test/core.go: -------------------------------------------------------------------------------- 1 | package core_test 2 | 3 | import ( 4 | "context" 5 | 6 | nctr "github.com/aperturerobotics/bifrost/peer/controller" 7 | "github.com/aperturerobotics/controllerbus/bus" 8 | "github.com/aperturerobotics/controllerbus/controller/resolver/static" 9 | cbc "github.com/aperturerobotics/controllerbus/core" 10 | egc "github.com/aperturerobotics/entitygraph/controller" 11 | "github.com/sirupsen/logrus" 12 | ) 13 | 14 | // NewTestingBus constructs a minimal in-memory Bifrost bus stack. 15 | func NewTestingBus( 16 | ctx context.Context, 17 | le *logrus.Entry, 18 | opts ...cbc.Option, 19 | ) (bus.Bus, *static.Resolver, error) { 20 | b, sr, err := cbc.NewCoreBus(ctx, le, opts...) 21 | if err != nil { 22 | return nil, nil, err 23 | } 24 | 25 | sr.AddFactory(nctr.NewFactory(b)) 26 | sr.AddFactory(egc.NewFactory(b)) 27 | 28 | return b, sr, nil 29 | } 30 | -------------------------------------------------------------------------------- /daemon/api/api.pb.ts: -------------------------------------------------------------------------------- 1 | // @generated by protoc-gen-es-lite unknown with parameter "target=ts,ts_nocheck=false" 2 | // @generated from file github.com/aperturerobotics/bifrost/daemon/api/api.proto (package bifrost.api, syntax proto3) 3 | /* eslint-disable */ 4 | 5 | import { Config as Config$1 } from '@go/github.com/aperturerobotics/controllerbus/bus/api/api.pb.js' 6 | import type { MessageType, PartialFieldInfo } from '@aptre/protobuf-es-lite' 7 | import { createMessageType } from '@aptre/protobuf-es-lite' 8 | 9 | export const protobufPackage = 'bifrost.api' 10 | 11 | /** 12 | * Config configures the API. 13 | * 14 | * @generated from message bifrost.api.Config 15 | */ 16 | export interface Config { 17 | /** 18 | * BusConfig configures the bus api. 19 | * 20 | * @generated from field: bus.api.Config bus_config = 1; 21 | */ 22 | busConfig?: Config$1 23 | } 24 | 25 | // Config contains the message type declaration for Config. 26 | export const Config: MessageType = createMessageType({ 27 | typeName: 'bifrost.api.Config', 28 | fields: [ 29 | { no: 1, name: 'bus_config', kind: 'message', T: () => Config$1 }, 30 | ] as readonly PartialFieldInfo[], 31 | packedByDefault: true, 32 | }) 33 | -------------------------------------------------------------------------------- /daemon/api/api.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package bifrost.api; 3 | 4 | import "github.com/aperturerobotics/controllerbus/bus/api/api.proto"; 5 | 6 | // Config configures the API. 7 | message Config { 8 | // BusConfig configures the bus api. 9 | .bus.api.Config bus_config = 1; 10 | } 11 | 12 | /* API implements: 13 | bus_api "github.com/aperturerobotics/controllerbus/bus/api" 14 | peer_api "github.com/aperturerobotics/bifrost/peer/api" 15 | pubsub_api "github.com/aperturerobotics/bifrost/pubsub/api" 16 | stream_api "github.com/aperturerobotics/bifrost/stream/api" 17 | */ -------------------------------------------------------------------------------- /daemon/api/api_accept.go: -------------------------------------------------------------------------------- 1 | package bifrost_api 2 | 3 | import ( 4 | stream_api "github.com/aperturerobotics/bifrost/stream/api" 5 | stream_api_accept "github.com/aperturerobotics/bifrost/stream/api/accept" 6 | "github.com/aperturerobotics/controllerbus/controller/loader" 7 | "github.com/aperturerobotics/controllerbus/controller/resolver" 8 | ) 9 | 10 | // AcceptStream accepts an incoming stream. 11 | // Stream data is sent over the request / response streams. 12 | func (a *API) AcceptStream(serv stream_api.SRPCStreamService_AcceptStreamStream) error { 13 | ctx := serv.Context() 14 | msg, err := serv.Recv() 15 | if err != nil { 16 | return err 17 | } 18 | 19 | conf := msg.GetConfig() 20 | if err := conf.Validate(); err != nil { 21 | return err 22 | } 23 | 24 | dir := resolver.NewLoadControllerWithConfig(conf) 25 | 26 | // wait until it's ready 27 | val, _, valRef, err := loader.WaitExecControllerRunning(ctx, a.bus, dir, nil) 28 | if err != nil { 29 | return err 30 | } 31 | defer valRef.Release() 32 | 33 | ctrl := val.(*stream_api_accept.Controller) 34 | return ctrl.AttachRPC(stream_api.NewAcceptServerRPC(serv)) 35 | } 36 | -------------------------------------------------------------------------------- /daemon/api/api_identify.go: -------------------------------------------------------------------------------- 1 | package bifrost_api 2 | 3 | import ( 4 | "context" 5 | 6 | peer_api "github.com/aperturerobotics/bifrost/peer/api" 7 | controller_exec "github.com/aperturerobotics/controllerbus/controller/exec" 8 | ) 9 | 10 | // Identify loads and manages a private key identity. 11 | func (a *API) Identify( 12 | req *peer_api.IdentifyRequest, 13 | serv peer_api.SRPCPeerService_IdentifyStream, 14 | ) error { 15 | ctx := serv.Context() 16 | conf := req.GetConfig() 17 | if err := conf.Validate(); err != nil { 18 | return err 19 | } 20 | 21 | reqCtx, reqCtxCancel := context.WithCancel(ctx) 22 | defer reqCtxCancel() 23 | 24 | return controller_exec.ExecuteController( 25 | reqCtx, 26 | a.bus, 27 | conf, 28 | func(status controller_exec.ControllerStatus) { 29 | _ = serv.Send(&peer_api.IdentifyResponse{ 30 | ControllerStatus: status, 31 | }) 32 | }, 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /daemon/api/api_peer_info.go: -------------------------------------------------------------------------------- 1 | package bifrost_api 2 | 3 | import ( 4 | "context" 5 | "slices" 6 | "strings" 7 | 8 | "github.com/aperturerobotics/bifrost/peer" 9 | peer_api "github.com/aperturerobotics/bifrost/peer/api" 10 | "github.com/aperturerobotics/controllerbus/bus" 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | // GetPeerInfo returns the peer information 15 | func (a *API) GetPeerInfo( 16 | ctx context.Context, 17 | req *peer_api.GetPeerInfoRequest, 18 | ) (*peer_api.GetPeerInfoResponse, error) { 19 | var peerID peer.ID 20 | if peerIDStr := req.GetPeerId(); peerIDStr != "" { 21 | var err error 22 | peerID, err = peer.IDB58Decode(peerIDStr) 23 | if err != nil { 24 | return nil, errors.Wrap(err, "decode peer id constraint") 25 | } 26 | } 27 | 28 | vals, _, ref, err := bus.ExecCollectValues[peer.GetPeerValue](ctx, a.bus, peer.NewGetPeer(peerID), false, nil) 29 | if err != nil { 30 | return nil, err 31 | } 32 | ref.Release() 33 | 34 | resp := &peer_api.GetPeerInfoResponse{} 35 | for _, val := range vals { 36 | resp.LocalPeers = append(resp.LocalPeers, peer_api.NewPeerInfo(val)) 37 | } 38 | 39 | slices.SortFunc(resp.LocalPeers, func(a, b *peer_api.PeerInfo) int { 40 | return strings.Compare(a.GetPeerId(), b.GetPeerId()) 41 | }) 42 | resp.LocalPeers = slices.CompactFunc(resp.LocalPeers, func(a, b *peer_api.PeerInfo) bool { 43 | return a.GetPeerId() == b.GetPeerId() 44 | }) 45 | 46 | return resp, nil 47 | } 48 | -------------------------------------------------------------------------------- /daemon/api/api_stream_dial.go: -------------------------------------------------------------------------------- 1 | package bifrost_api 2 | 3 | import ( 4 | stream_api "github.com/aperturerobotics/bifrost/stream/api" 5 | stream_api_dial "github.com/aperturerobotics/bifrost/stream/api/dial" 6 | ) 7 | 8 | // DialStream dials a outgoing stream. 9 | // Stream data is sent over the request / response streams. 10 | func (a *API) DialStream(serv stream_api.SRPCStreamService_DialStreamStream) error { 11 | ctx := serv.Context() 12 | msg, err := serv.Recv() 13 | if err != nil { 14 | return err 15 | } 16 | 17 | conf := msg.GetConfig() 18 | if err := conf.Validate(); err != nil { 19 | return err 20 | } 21 | 22 | return stream_api_dial.ProcessRPC( 23 | ctx, 24 | a.bus, 25 | conf, 26 | stream_api.NewDialServerRPC(serv), 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /daemon/api/controller/config.go: -------------------------------------------------------------------------------- 1 | package bifrost_api_controller 2 | 3 | import ( 4 | "github.com/aperturerobotics/controllerbus/config" 5 | ) 6 | 7 | // ConfigID is the string used to identify this config object. 8 | const ConfigID = ControllerID 9 | 10 | // Validate validates the configuration. 11 | // This is a cursory validation to see if the values "look correct." 12 | func (c *Config) Validate() error { return nil } 13 | 14 | // GetConfigID returns the unique string for this configuration type. 15 | // This string is stored with the encoded config. 16 | func (c *Config) GetConfigID() string { 17 | return ConfigID 18 | } 19 | 20 | // EqualsConfig checks if the other config is equal. 21 | func (c *Config) EqualsConfig(c2 config.Config) bool { 22 | return config.EqualsConfig[*Config](c, c2) 23 | } 24 | 25 | // _ is a type assertion 26 | var _ config.Config = ((*Config)(nil)) 27 | -------------------------------------------------------------------------------- /daemon/api/controller/controller.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package bifrost.api.controller; 3 | 4 | import "github.com/aperturerobotics/bifrost/daemon/api/api.proto"; 5 | import "github.com/aperturerobotics/controllerbus/bus/api/api.proto"; 6 | 7 | // Config configures the API. 8 | message Config { 9 | // ListenAddr is the address to listen on for connections. 10 | string listen_addr = 1; 11 | // ApiConfig are api config options. 12 | bifrost.api.Config api_config = 2; 13 | // DisableBusApi disables the bus api. 14 | bool disable_bus_api = 3; 15 | // BusApiConfig are controller-bus bus api config options. 16 | // BusApiConfig are options for controller bus api. 17 | bus.api.Config bus_api_config = 4; 18 | } -------------------------------------------------------------------------------- /daemon/prof/prof.go: -------------------------------------------------------------------------------- 1 | package daemon_prof 2 | 3 | import ( 4 | "net/http" 5 | "net/http/pprof" 6 | "runtime" 7 | "time" 8 | 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | func ListenProf(le *logrus.Entry, profListen string) error { 13 | runtime.SetBlockProfileRate(1) 14 | runtime.SetMutexProfileFraction(1) 15 | le.Debugf("profiling listener running: %s", profListen) 16 | mux := http.NewServeMux() 17 | 18 | // Register pprof handlers 19 | mux.HandleFunc("/debug/pprof/", pprof.Index) 20 | mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) 21 | mux.HandleFunc("/debug/pprof/profile", pprof.Profile) 22 | mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) 23 | mux.HandleFunc("/debug/pprof/trace", pprof.Trace) 24 | 25 | // Manually add support for paths linked to by index page at /debug/pprof/ 26 | mux.Handle("/debug/pprof/goroutine", pprof.Handler("goroutine")) 27 | mux.Handle("/debug/pprof/heap", pprof.Handler("heap")) 28 | mux.Handle("/debug/pprof/threadcreate", pprof.Handler("threadcreate")) 29 | mux.Handle("/debug/pprof/block", pprof.Handler("block")) 30 | 31 | server := &http.Server{Addr: profListen, Handler: mux, ReadHeaderTimeout: time.Second * 10} 32 | err := server.ListenAndServe() 33 | le.WithError(err).Warn("profiling listener exited") 34 | return err 35 | } 36 | -------------------------------------------------------------------------------- /deps.go: -------------------------------------------------------------------------------- 1 | //go:build deps_only 2 | // +build deps_only 3 | 4 | package bifrost 5 | 6 | import ( 7 | // _ imports common with the Makefile and tools 8 | _ "github.com/aperturerobotics/common" 9 | ) 10 | -------------------------------------------------------------------------------- /doc/img/bifrost-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aperturerobotics/bifrost/4281209dc3fb99b1e23d794afb49d163a974affa/doc/img/bifrost-logo.png -------------------------------------------------------------------------------- /entitygraph/config.go: -------------------------------------------------------------------------------- 1 | package bifrost_entitygraph 2 | 3 | import ( 4 | "github.com/aperturerobotics/controllerbus/config" 5 | ) 6 | 7 | // ConfigID is the identifier for the config type. 8 | const ConfigID = ControllerID 9 | 10 | // GetConfigID returns the config identifier. 11 | func (c *Config) GetConfigID() string { 12 | return ConfigID 13 | } 14 | 15 | // EqualsConfig checks equality between two configs. 16 | func (c *Config) EqualsConfig(c2 config.Config) bool { 17 | return config.EqualsConfig[*Config](c, c2) 18 | } 19 | 20 | // Validate validates the configuration. 21 | func (c *Config) Validate() error { 22 | return nil 23 | } 24 | -------------------------------------------------------------------------------- /entitygraph/config.pb.ts: -------------------------------------------------------------------------------- 1 | // @generated by protoc-gen-es-lite unknown with parameter "target=ts,ts_nocheck=false" 2 | // @generated from file github.com/aperturerobotics/bifrost/entitygraph/config.proto (package bifrost.entitygraph, syntax proto3) 3 | /* eslint-disable */ 4 | 5 | import type { MessageType, PartialFieldInfo } from '@aptre/protobuf-es-lite' 6 | import { createMessageType } from '@aptre/protobuf-es-lite' 7 | 8 | export const protobufPackage = 'bifrost.entitygraph' 9 | 10 | /** 11 | * Config is the config object for the entitygraph repoter. 12 | * 13 | * @generated from message bifrost.entitygraph.Config 14 | */ 15 | export interface Config {} 16 | 17 | // Config contains the message type declaration for Config. 18 | export const Config: MessageType = createMessageType({ 19 | typeName: 'bifrost.entitygraph.Config', 20 | fields: [] as readonly PartialFieldInfo[], 21 | packedByDefault: true, 22 | }) 23 | -------------------------------------------------------------------------------- /entitygraph/config.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package bifrost.entitygraph; 3 | 4 | // Config is the config object for the entitygraph repoter. 5 | message Config { 6 | } -------------------------------------------------------------------------------- /entitygraph/controller.go: -------------------------------------------------------------------------------- 1 | package bifrost_entitygraph 2 | 3 | import ( 4 | "github.com/aperturerobotics/controllerbus/controller" 5 | "github.com/aperturerobotics/entitygraph/reporter" 6 | "github.com/blang/semver/v4" 7 | ) 8 | 9 | // Version is the version of the controller implementation. 10 | var Version = semver.MustParse("0.0.1") 11 | 12 | // ControllerID is the ID of the controller. 13 | const ControllerID = "bifrost/entitygraph/reporter" 14 | 15 | // Controller manages exposing Bifrost resources to the Entity Graph. 16 | // It handles CollectEntityGraph directives. 17 | type Controller = reporter.Controller 18 | 19 | // GetControllerInfo returns information about the controller. 20 | func GetControllerInfo() *controller.Info { 21 | return controller.NewInfo( 22 | ControllerID, 23 | Version, 24 | "bifrost entitygraph reporter controller ", 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /entitygraph/peer_entity.go: -------------------------------------------------------------------------------- 1 | package bifrost_entitygraph 2 | 3 | import ( 4 | "github.com/aperturerobotics/bifrost/peer" 5 | "github.com/aperturerobotics/entitygraph/entity" 6 | ) 7 | 8 | // PeerEntityTypeName is the entitygraph type name for a Bifrost peer 9 | const PeerEntityTypeName = "bifrost/peer" 10 | 11 | // PeerEntity is a entity implementation backed by a node. 12 | type PeerEntity struct { 13 | entityID, entityTypeName string 14 | } 15 | 16 | // NewPeerEntityRef constructs a new entity ref to a node. 17 | func NewPeerEntityRef(peerID peer.ID) entity.Ref { 18 | return entity.NewEntityRefWithID(peerID.String(), PeerEntityTypeName) 19 | } 20 | 21 | // NewPeerEntity constructs a new PeerEntity. 22 | func NewPeerEntity(peerID peer.ID) *PeerEntity { 23 | nodRef := NewPeerEntityRef(peerID) 24 | nodID := nodRef.GetEntityRefId() 25 | return &PeerEntity{ 26 | entityID: nodID, 27 | entityTypeName: PeerEntityTypeName, 28 | } 29 | } 30 | 31 | // GetEntityID returns the entity identifier. 32 | func (e *PeerEntity) GetEntityID() string { 33 | return e.entityID 34 | } 35 | 36 | // GetEntityTypeName returns the entity type name. 37 | func (e *PeerEntity) GetEntityTypeName() string { 38 | return e.entityTypeName 39 | } 40 | 41 | var _ entity.Entity = ((*PeerEntity)(nil)) 42 | -------------------------------------------------------------------------------- /entitygraph/transport_assoc_entity.go: -------------------------------------------------------------------------------- 1 | package bifrost_entitygraph 2 | 3 | import ( 4 | "github.com/aperturerobotics/entitygraph/entity" 5 | el "github.com/aperturerobotics/entitygraph/link" 6 | ) 7 | 8 | // TransportAssocEntityTypeName is the entitygraph type name for a Bifrost transport 9 | const TransportAssocEntityTypeName = "bifrost/transport/assoc" 10 | 11 | // TransportAssocEntity is a entity implementation backed by a transport. 12 | type TransportAssocEntity struct { 13 | entityID string 14 | edgeFrom, edgeTo entity.Ref 15 | } 16 | 17 | // GetEntityID returns the entity identifier. 18 | func (l *TransportAssocEntity) GetEntityID() string { 19 | return l.entityID 20 | } 21 | 22 | // GetEntityTypeName returns the entity type name. 23 | func (l *TransportAssocEntity) GetEntityTypeName() string { 24 | return TransportAssocEntityTypeName 25 | } 26 | 27 | // GetEdgeFrom returns the reference to the entity this link starts at. 28 | func (l *TransportAssocEntity) GetEdgeFrom() entity.Ref { 29 | return l.edgeFrom 30 | } 31 | 32 | // GetEdgeTo returns the reference to the entity this link ends at. 33 | func (l *TransportAssocEntity) GetEdgeTo() entity.Ref { 34 | return l.edgeTo 35 | } 36 | 37 | // _ is a type assertion 38 | var _ entity.Entity = ((*TransportAssocEntity)(nil)) 39 | 40 | // _ is a type assertion 41 | var _ el.Link = ((*TransportAssocEntity)(nil)) 42 | -------------------------------------------------------------------------------- /examples/http-forwarding/node-1.yaml: -------------------------------------------------------------------------------- 1 | udp: 2 | id: bifrost/udp 3 | config: 4 | listenAddr: :50042 5 | 6 | forwarding: 7 | id: bifrost/stream/forwarding 8 | config: 9 | protocolId: my/http/forwarding 10 | targetMultiaddr: "/ip4/127.0.0.1/tcp/8080" 11 | -------------------------------------------------------------------------------- /examples/http-forwarding/node-2.yaml: -------------------------------------------------------------------------------- 1 | udp: 2 | id: bifrost/udp 3 | config: 4 | dialers: 5 | 12D3KooWC9dBAEoTHbEXq2aaTeFit7QVdvPcb6Yf76oGQZ6dGf8N: 6 | address: 127.0.0.1:50042 7 | listenAddr: :50043 8 | 9 | listening: 10 | id: bifrost/stream/listening 11 | config: 12 | remotePeerId: 12D3KooWC9dBAEoTHbEXq2aaTeFit7QVdvPcb6Yf76oGQZ6dGf8N 13 | protocolId: my/http/forwarding 14 | listenMultiaddr: "/ip4/127.0.0.1/tcp/8084" 15 | 16 | -------------------------------------------------------------------------------- /examples/nats-host/.gitignore: -------------------------------------------------------------------------------- 1 | /nats-host 2 | /priv-key.pem 3 | vendor/ 4 | -------------------------------------------------------------------------------- /examples/nats-host/README.md: -------------------------------------------------------------------------------- 1 | # NATS Demo 2 | 3 | These examples run multiple NATS instances within a single process to 4 | demonstrate how it works with Bifrost. The peers speak Quic to each other and 5 | send the NATS traffic over reliable streams. 6 | 7 | The nats-single directory runs a single NATS instance as a test. 8 | 9 | ## Running 10 | 11 | You can run these with "go run ./". 12 | -------------------------------------------------------------------------------- /examples/nats-host/nats-single/.gitignore: -------------------------------------------------------------------------------- 1 | nats-single -------------------------------------------------------------------------------- /examples/nats-host/peer-0.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN LIBP2P PRIVATE KEY----- 2 | CAESQL+W6ee0rWJUNGii2pj0UJP5DFtQXYADWrqafwJs51WOl1SQzjnp+/pkJDGM 3 | c03jcFLLE1Sqcd5sNEexaW1EAfE= 4 | -----END LIBP2P PRIVATE KEY----- 5 | -------------------------------------------------------------------------------- /examples/nats-host/peer-1.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN LIBP2P PRIVATE KEY----- 2 | CAESQBrXR4mGsI+QHdwDczd0dQdkdX/cOxMOsWczuGBYrT1uGub7kvhLYn5tlTzc 3 | Bfazz/1TmH/7TSkWAJZL3af9XaM= 4 | -----END LIBP2P PRIVATE KEY----- 5 | -------------------------------------------------------------------------------- /examples/nats-host/peer-2.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN LIBP2P PRIVATE KEY----- 2 | CAESQLHy18bkCTLIY+yLqnWyQ8WPO6YIDhvalAoAIOm9jA2Ev2lQUdfJMCM8wNvy 3 | sXvASbANTYAgIQwF81ZpBzklV6k= 4 | -----END LIBP2P PRIVATE KEY----- 5 | -------------------------------------------------------------------------------- /examples/priv/node-1.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN LIBP2P PRIVATE KEY----- 2 | CAESYBW7VXisNrTll8ZIbzZ+nxgOrKMF1OgeKvrK7GD6w7sWIqZgvOGPO+Gr6uSi 3 | 1hJqpgpinyQrh85DejhgO6KPmDsipmC84Y874avq5KLWEmqmCmKfJCuHzkN6OGA7 4 | oo+YOw== 5 | -----END LIBP2P PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /examples/priv/node-2.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN LIBP2P PRIVATE KEY----- 2 | CAESYMX/EjrAyBJGuhgkEU1Z/HW+BpG4iWYg4Td7HagNYTtJhxrVzJxCBACR+K0s 3 | NYsFYwy9Rk/vprgr14pDN1zSkO6HGtXMnEIEAJH4rSw1iwVjDL1GT++muCvXikM3 4 | XNKQ7g== 5 | -----END LIBP2P PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /examples/priv/node-3.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN LIBP2P PRIVATE KEY----- 2 | CAESQDsDD4OwrX+XgYpDiPUpcjgBPiREOxrV1nLEz7G98VRaUIYDBsAL/Uzx0VAW 3 | eA5BMw+No1K9Y46lSRPchGSb71k= 4 | -----END LIBP2P PRIVATE KEY----- 5 | -------------------------------------------------------------------------------- /examples/udp-link/.gitignore: -------------------------------------------------------------------------------- 1 | udptoy 2 | udp-link -------------------------------------------------------------------------------- /examples/udp-link/README.md: -------------------------------------------------------------------------------- 1 | # UDP Link 2 | 3 | This is a basic self-contained example of a UDP link between two peers. 4 | 5 | Run the example: 6 | 7 | ``` 8 | go run -v ./ 9 | ``` 10 | 11 | Two peers will start and establish a Link with each other via UDP. 12 | -------------------------------------------------------------------------------- /examples/webrtc-forwarding/README.md: -------------------------------------------------------------------------------- 1 | # HTTP over WebRTC Example 2 | 3 | Install Bifrost: 4 | 5 | ``` 6 | go install -v github.com/aperturerobotics/bifrost/cmd/bifrost 7 | ``` 8 | 9 | Start the destination service that we will proxy connections to: 10 | 11 | ``` 12 | python3 -m http.server 8080 13 | ``` 14 | 15 | Start the signaling server: 16 | 17 | ``` 18 | cd ./server 19 | go run -v ./ 20 | ``` 21 | 22 | Start the first peer, which forwards incoming streams to localhost:8080: 23 | 24 | ``` 25 | bifrost daemon --node-priv ../priv/node-1.pem -c node-1.yaml 26 | ``` 27 | 28 | Start the second peer, which listens on :8084 and forwards incoming traffic to 29 | the other peer via. Bifrost WebRTC: 30 | 31 | ``` 32 | bifrost daemon --node-priv ../priv/node-2.pem -c node-2.yaml 33 | ``` 34 | 35 | Access the forwarded HTTP service via the proxy: 36 | 37 | ``` 38 | curl localhost:8084 39 | ``` 40 | 41 | Or browse to http://localhost:8084 in a web browser. 42 | 43 | When opening a connection to the second node at port 8084, Bifrost will open a 44 | WebRTC Link with the other peer on-demand using the demo signaling server and 45 | STUN servers. It will then proxy the traffic to the destination service. Bifrost 46 | will close the Link after a short inactivity period. 47 | -------------------------------------------------------------------------------- /examples/webrtc-forwarding/node-1.yaml: -------------------------------------------------------------------------------- 1 | forwarding: 2 | id: bifrost/stream/forwarding 3 | config: 4 | protocolId: my/http/forwarding 5 | targetMultiaddr: "/ip4/127.0.0.1/tcp/8080" 6 | 7 | signaling-websocket: 8 | id: bifrost/websocket 9 | config: 10 | dialers: 11 | 12D3KooWFEhJfWBV6ZZcpzqAAuHBYbzF2rsb4nkwXddtjJseu3mi: 12 | address: ws://127.0.0.1:2253/bifrost-ws 13 | 14 | signaling: 15 | id: bifrost/signaling/rpc/client 16 | config: 17 | signalingId: webrtc 18 | protocolId: webrtc/signaling 19 | client: 20 | serverPeerIds: 21 | - 12D3KooWFEhJfWBV6ZZcpzqAAuHBYbzF2rsb4nkwXddtjJseu3mi 22 | 23 | webrtc: 24 | id: bifrost/webrtc 25 | config: 26 | signalingId: webrtc 27 | webRtc: 28 | iceServers: 29 | - urls: 30 | - stun:stun.l.google.com:19302 31 | - stun:stun.stunprotocol.org:3478 32 | allPeers: true 33 | blockPeers: 34 | - 12D3KooWFEhJfWBV6ZZcpzqAAuHBYbzF2rsb4nkwXddtjJseu3mi 35 | verbose: true 36 | -------------------------------------------------------------------------------- /examples/webrtc-forwarding/node-2.yaml: -------------------------------------------------------------------------------- 1 | listening: 2 | id: bifrost/stream/listening 3 | config: 4 | remotePeerId: 12D3KooWC9dBAEoTHbEXq2aaTeFit7QVdvPcb6Yf76oGQZ6dGf8N 5 | protocolId: my/http/forwarding 6 | listenMultiaddr: "/ip4/127.0.0.1/tcp/8084" 7 | 8 | signaling-websocket: 9 | id: bifrost/websocket 10 | config: 11 | dialers: 12 | 12D3KooWFEhJfWBV6ZZcpzqAAuHBYbzF2rsb4nkwXddtjJseu3mi: 13 | address: ws://127.0.0.1:2253/bifrost-ws 14 | 15 | signaling: 16 | id: bifrost/signaling/rpc/client 17 | config: 18 | signalingId: webrtc 19 | protocolId: webrtc/signaling 20 | client: 21 | serverPeerIds: 22 | - 12D3KooWFEhJfWBV6ZZcpzqAAuHBYbzF2rsb4nkwXddtjJseu3mi 23 | 24 | webrtc: 25 | id: bifrost/webrtc 26 | config: 27 | signalingId: webrtc 28 | webRtc: 29 | iceServers: 30 | - urls: 31 | - stun:stun.l.google.com:19302 32 | - stun:stun.stunprotocol.org:3478 33 | allPeers: true 34 | blockPeers: 35 | - 12D3KooWFEhJfWBV6ZZcpzqAAuHBYbzF2rsb4nkwXddtjJseu3mi 36 | verbose: true 37 | -------------------------------------------------------------------------------- /examples/webrtc-forwarding/server/.gitignore: -------------------------------------------------------------------------------- 1 | server 2 | server.pem -------------------------------------------------------------------------------- /examples/webrtc-forwarding/server/README.md: -------------------------------------------------------------------------------- 1 | # Signaling Server 2 | 3 | This is a basic implementation of a WebRTC signaling server. 4 | 5 | It listens on port :2253 for incoming WebSocket connections. 6 | 7 | Peers can then signal other peers via the signaling service. 8 | 9 | ``` 10 | NAME: 11 | webrtc-signaling-server - Hosts a WebSocket server and a signaling service 12 | 13 | USAGE: 14 | webrtc-signaling-server [global options] 15 | 16 | GLOBAL OPTIONS: 17 | --listen value address to listen on (default: ":2253") [$LISTEN] 18 | --http value http path to listen on (default: "/bifrost-ws") [$HTTP_PATH] 19 | ``` 20 | -------------------------------------------------------------------------------- /examples/websocket-browser-link/README.md: -------------------------------------------------------------------------------- 1 | # Quic over WebSocket 2 | 3 | ![Screenshot](./screenshot.png) 4 | 5 | This demo shows how Bifrost communicates between a browser tab and a native 6 | process via **Quic over WebSocket**. 7 | 8 | Quic features like stream multiplexing, mTLS, congestion control work correctly. 9 | 10 | ## Running the server 11 | 12 | Start the server with go run: 13 | 14 | ```sh 15 | cd ./server 16 | go run -v ./ 17 | ``` 18 | 19 | The server listens on port 2015 for WebSocket connections. 20 | 21 | ## Running the client 22 | 23 | Run a static web-server to listen on port 8000: 24 | 25 | ```sh 26 | cd ./browser 27 | ./build.bash 28 | python -m http.server 29 | ``` 30 | 31 | Browse to http://localhost:8000 in your web browser. 32 | 33 | Open the developer console, network tab, WebSockets list. 34 | 35 | Click the "run" button to start the demo. 36 | 37 | You will see a WebSocket open with binary messages generated by Quic. 38 | -------------------------------------------------------------------------------- /examples/websocket-browser-link/browser/.gitignore: -------------------------------------------------------------------------------- 1 | example.wasm 2 | wasm_exec.js -------------------------------------------------------------------------------- /examples/websocket-browser-link/browser/build.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cp $(go env GOROOT)/misc/wasm/wasm_exec.js ./wasm_exec.js 4 | GOOS=js GOARCH=wasm go build -o example.wasm -v ./ 5 | -------------------------------------------------------------------------------- /examples/websocket-browser-link/browser/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Example 6 | 7 | 8 | 9 | 10 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/websocket-browser-link/browser/index_nojs.go: -------------------------------------------------------------------------------- 1 | //go:build !js 2 | // +build !js 3 | 4 | package main 5 | 6 | func main() {} 7 | -------------------------------------------------------------------------------- /examples/websocket-browser-link/browser/serve.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eo pipefail 3 | 4 | # cd to project root 5 | if [ ! -d ./hack ]; then 6 | if [ -d ../../../hack ]; then 7 | cd ../../../ 8 | else 9 | cd $(git rev-parse --show-toplevel) 10 | fi 11 | fi 12 | 13 | # serve the example 14 | echo "Listening on port :8090" 15 | make serve-example 16 | -------------------------------------------------------------------------------- /examples/websocket-browser-link/common/common_js.go: -------------------------------------------------------------------------------- 1 | //go:build js 2 | // +build js 3 | 4 | package common 5 | 6 | import ( 7 | "github.com/sirupsen/logrus" 8 | ) 9 | 10 | func init() { 11 | log.Formatter = &logrus.TextFormatter{ 12 | DisableColors: true, 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/websocket-browser-link/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aperturerobotics/bifrost/4281209dc3fb99b1e23d794afb49d163a974affa/examples/websocket-browser-link/screenshot.png -------------------------------------------------------------------------------- /examples/websocket-browser-link/server/.gitignore: -------------------------------------------------------------------------------- 1 | server -------------------------------------------------------------------------------- /hash/hash.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package hash; 3 | 4 | // HashType identifies the hash type in use. 5 | enum HashType { 6 | // HashType_UNKNOWN is an unknown hash type. 7 | HashType_UNKNOWN = 0; 8 | // HashType_SHA256 is the sha256 hash type. 9 | HashType_SHA256 = 1; 10 | // HashType_SHA1 is the sha1 hash type. 11 | // NOTE: Do not use SHA1 unless you absolutely have to for backwards compat! (Git) 12 | HashType_SHA1 = 2; 13 | // HashType_BLAKE3 is the blake3 hash type. 14 | // Uses a 32-byte digest size. 15 | HashType_BLAKE3 = 3; 16 | } 17 | 18 | // Hash is a hash of a binary blob. 19 | message Hash { 20 | // HashType is the hash type in use. 21 | HashType hash_type = 1; 22 | // Hash is the hash value. 23 | bytes hash = 2; 24 | } 25 | -------------------------------------------------------------------------------- /hash/hash_test.go: -------------------------------------------------------------------------------- 1 | package hash 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | // TestVerifyData tests verifying some data with each hash type. 10 | func TestVerifyData(t *testing.T) { 11 | data := []byte("hello world") 12 | for _, ht := range SupportedHashTypes { 13 | h, err := Sum(ht, data) 14 | werr := func(e error) error { 15 | return errors.Wrapf(e, "hash_type[%v]", ht) 16 | } 17 | if err != nil { 18 | t.Fatal(werr(err)) 19 | } 20 | if _, err := h.VerifyData(data); err != nil { 21 | t.Fatal(werr(err)) 22 | } 23 | t.Logf("OK: %s", ht.String()) 24 | } 25 | } 26 | 27 | // TestJSON tests marshal and unmarshal hash from json. 28 | func TestJSON(t *testing.T) { 29 | h, err := Sum(HashType_HashType_SHA256, []byte("hello world")) 30 | if err != nil { 31 | t.Fatal(err.Error()) 32 | } 33 | jdata, err := h.MarshalJSON() 34 | if err != nil { 35 | t.Fatal(err.Error()) 36 | } 37 | t.Log(string(jdata)) 38 | outHash, err := UnmarshalHashJSON(jdata) 39 | if err != nil { 40 | t.Fatal(err.Error()) 41 | } 42 | if !outHash.EqualVT(h) { 43 | t.Fail() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /http/bus-handler.go: -------------------------------------------------------------------------------- 1 | package bifrost_http 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/aperturerobotics/controllerbus/bus" 7 | ) 8 | 9 | // BusHandler implements http.Handler by calling LookupHTTPHandler. 10 | type BusHandler struct { 11 | // b is the bus to use for lookups 12 | b bus.Bus 13 | // clientID is the client id to use for lookups 14 | clientID string 15 | // notFoundIfIdle indicates to return 404 not found if the lookup is idle 16 | notFoundIfIdle bool 17 | } 18 | 19 | // NewBusHandler constructs a new bus-backed HTTP handler. 20 | func NewBusHandler(b bus.Bus, clientID string, notFoundIfIdle bool) *BusHandler { 21 | return &BusHandler{ 22 | b: b, 23 | clientID: clientID, 24 | notFoundIfIdle: notFoundIfIdle, 25 | } 26 | } 27 | 28 | // ServeHTTP serves the http request. 29 | func (h *BusHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { 30 | ctx := req.Context() 31 | handler, _, handlerRef, err := ExLookupFirstHTTPHandler( 32 | ctx, 33 | h.b, 34 | req.Method, 35 | req.URL, 36 | "", 37 | h.notFoundIfIdle, 38 | nil, 39 | ) 40 | if handlerRef != nil { 41 | defer handlerRef.Release() 42 | } 43 | if err != nil { 44 | rw.WriteHeader(500) 45 | _, _ = rw.Write([]byte(err.Error())) 46 | return 47 | } 48 | if handlerRef == nil { 49 | rw.WriteHeader(404) 50 | _, _ = rw.Write([]byte("404 not found")) 51 | return 52 | } 53 | 54 | handler.ServeHTTP(rw, req) 55 | } 56 | 57 | // _ is a type assertion 58 | var _ http.Handler = ((*BusHandler)(nil)) 59 | -------------------------------------------------------------------------------- /http/bus-http-handler.go: -------------------------------------------------------------------------------- 1 | package bifrost_http 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/url" 7 | 8 | "github.com/aperturerobotics/controllerbus/bus" 9 | ) 10 | 11 | // NewBusHTTPHandlerBuilder constructs a HTTPHandlerBuilder which looks up the handler on the bus. 12 | // 13 | // baseMethod can be empty to allow any 14 | func NewBusHTTPHandlerBuilder(b bus.Bus, baseMethod string, baseURL *url.URL, clientID string, notFoundIfIdle bool) HTTPHandlerBuilder { 15 | return func(ctx context.Context, released func()) (http.Handler, func(), error) { 16 | handler, _, handlerRef, err := ExLookupFirstHTTPHandler( 17 | ctx, 18 | b, 19 | baseMethod, 20 | baseURL, 21 | clientID, 22 | notFoundIfIdle, 23 | released, 24 | ) 25 | if err != nil { 26 | return nil, nil, err 27 | } 28 | if handlerRef == nil { 29 | return nil, nil, nil 30 | } 31 | return handler, handlerRef.Release, nil 32 | } 33 | } 34 | 35 | // NewBusHTTPHandler constructs a HTTPHandler which looks up the HTTP 36 | // handler on the bus when at least one request is active. 37 | // 38 | // baseMethod can be empty to allow any 39 | // baseURL is the URL to use for the client lookup. 40 | func NewBusHTTPHandler( 41 | ctx context.Context, 42 | b bus.Bus, 43 | baseMethod string, 44 | baseURL *url.URL, 45 | clientID string, 46 | notFoundIfIdle bool, 47 | ) *HTTPHandler { 48 | return NewHTTPHandler( 49 | ctx, 50 | NewBusHTTPHandlerBuilder( 51 | b, 52 | baseMethod, 53 | baseURL, 54 | clientID, 55 | notFoundIfIdle, 56 | )) 57 | } 58 | -------------------------------------------------------------------------------- /http/bus-http-handler_test.go: -------------------------------------------------------------------------------- 1 | package bifrost_http 2 | 3 | import ( 4 | "context" 5 | "net/url" 6 | "testing" 7 | 8 | "github.com/aperturerobotics/bifrost/testbed" 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | func TestHTTPBusHTTPHandler(t *testing.T) { 13 | ctx := context.Background() 14 | log := logrus.New() 15 | log.SetLevel(logrus.DebugLevel) 16 | le := logrus.NewEntry(log) 17 | 18 | tb, err := testbed.NewTestbed(ctx, le, testbed.TestbedOpts{ 19 | NoEcho: true, 20 | NoPeer: true, 21 | }) 22 | if err != nil { 23 | t.Fatal(err.Error()) 24 | } 25 | defer startMockHandler(t, tb)() 26 | 27 | // start the on-demand handler 28 | u, err := url.Parse("/foo") 29 | if err != nil { 30 | t.Fatal(err.Error()) 31 | } 32 | rc := NewBusHTTPHandler(ctx, tb.Bus, "", u, "test-client", false) 33 | 34 | // perform a request 35 | checkMockRequest(t, rc) 36 | } 37 | -------------------------------------------------------------------------------- /http/http-handler-builder.go: -------------------------------------------------------------------------------- 1 | package bifrost_http 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "github.com/aperturerobotics/util/refcount" 8 | ) 9 | 10 | // HTTPHandlerBuilder builds a HTTP Handle. 11 | // 12 | // returns the http handler and an optional release function 13 | // can return nil to indicate not found. 14 | type HTTPHandlerBuilder = refcount.RefCountResolver[http.Handler] 15 | 16 | // NewHTTPHandlerBuilder creates a new HTTPHandlerBuilder with a static handler. 17 | func NewHTTPHandlerBuilder(handler http.Handler) HTTPHandlerBuilder { 18 | return func(ctx context.Context, released func()) (http.Handler, func(), error) { 19 | if handler == nil { 20 | return nil, nil, nil 21 | } 22 | return handler, nil, nil 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /http/http-handler-controller_test.go: -------------------------------------------------------------------------------- 1 | package bifrost_http 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/aperturerobotics/bifrost/testbed" 8 | "github.com/sirupsen/logrus" 9 | ) 10 | 11 | func TestHTTPHandlerController(t *testing.T) { 12 | ctx := context.Background() 13 | log := logrus.New() 14 | log.SetLevel(logrus.DebugLevel) 15 | le := logrus.NewEntry(log) 16 | tb, err := testbed.NewTestbed(ctx, le, testbed.TestbedOpts{ 17 | NoEcho: true, 18 | NoPeer: true, 19 | }) 20 | if err != nil { 21 | t.Fatal(err.Error()) 22 | } 23 | defer startMockHandler(t, tb)() 24 | 25 | // start the http server 26 | // note: we set not found if idle because AddController waits to return 27 | // until the controller is added. therefore the resolution should never go 28 | // idle before returning a value. 29 | busHandler := NewBusHandler(tb.Bus, "test-client", true) 30 | 31 | // perform a request 32 | checkMockRequest(t, busHandler) 33 | } 34 | -------------------------------------------------------------------------------- /http/http-handler_test.go: -------------------------------------------------------------------------------- 1 | package bifrost_http 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/aperturerobotics/bifrost/testbed" 8 | "github.com/sirupsen/logrus" 9 | ) 10 | 11 | func TestHTTPHandler(t *testing.T) { 12 | ctx := context.Background() 13 | log := logrus.New() 14 | log.SetLevel(logrus.DebugLevel) 15 | le := logrus.NewEntry(log) 16 | tb, err := testbed.NewTestbed(ctx, le, testbed.TestbedOpts{ 17 | NoEcho: true, 18 | NoPeer: true, 19 | }) 20 | if err != nil { 21 | t.Fatal(err.Error()) 22 | } 23 | defer startMockHandler(t, tb)() 24 | 25 | busHandler := NewBusHandler(tb.Bus, "test-client", true) 26 | handler := NewHTTPHandler(ctx, NewHTTPHandlerBuilder(busHandler)) 27 | 28 | // perform a request 29 | checkMockRequest(t, handler) 30 | } 31 | -------------------------------------------------------------------------------- /http/listener/config.go: -------------------------------------------------------------------------------- 1 | package bifrost_http_listener 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/aperturerobotics/controllerbus/config" 7 | ) 8 | 9 | // ControllerID is the controller ID. 10 | const ControllerID = "bifrost/http/listener" 11 | 12 | // ConfigID is the string used to identify this config object. 13 | const ConfigID = ControllerID 14 | 15 | // NewConfig constructs a new listener config. 16 | func NewConfig(addr, clientID string) *Config { 17 | return &Config{ 18 | Addr: addr, 19 | ClientId: clientID, 20 | } 21 | } 22 | 23 | // Validate validates the configuration. 24 | func (c *Config) Validate() error { 25 | if c.GetCertFile() != "" && c.GetKeyFile() == "" { 26 | return errors.New("key_file: cannot be empty if cert_file is set") 27 | } 28 | if c.GetCertFile() == "" && c.GetKeyFile() != "" { 29 | return errors.New("cert_file: cannot be empty if key_file is set") 30 | } 31 | return nil 32 | } 33 | 34 | // GetConfigID returns the unique string for this configuration type. 35 | func (c *Config) GetConfigID() string { 36 | return ConfigID 37 | } 38 | 39 | // EqualsConfig checks if the config is equal to another. 40 | func (c *Config) EqualsConfig(other config.Config) bool { 41 | ot, ok := other.(*Config) 42 | if !ok { 43 | return false 44 | } 45 | 46 | return ot.EqualVT(c) 47 | } 48 | 49 | var _ config.Config = ((*Config)(nil)) 50 | -------------------------------------------------------------------------------- /http/listener/config.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package bifrost.http.listener; 3 | 4 | // Config configures a http server that listens on a port. 5 | // 6 | // Handles incoming requests with LookupHTTPHandler. 7 | message Config { 8 | // Addr is the address to listen. 9 | // 10 | // Example: 0.0.0.0:8080 11 | string addr = 1; 12 | // ClientId is the client id to set on LookupHTTPHandler. 13 | string client_id = 2; 14 | 15 | // CertFile is the path to the certificate file to use for https. 16 | // Can be unset to use HTTP. 17 | string cert_file = 3; 18 | // KeyFile is the path to the key file to use for https. 19 | // Cannot be unset if cert_file is set. 20 | // Otherwise can be unset. 21 | string key_file = 4; 22 | // Wait indicates to wait for LookupHTTPHandler even if it becomes idle. 23 | // If false: returns 404 not found if LookupHTTPHandler becomes idle. 24 | bool wait = 5; 25 | } 26 | -------------------------------------------------------------------------------- /http/res-lookup-http-handler.go: -------------------------------------------------------------------------------- 1 | package bifrost_http 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "github.com/aperturerobotics/controllerbus/directive" 8 | ) 9 | 10 | // LookupHTTPHandlerResolver resolves LookupHTTPHandler with a handler. 11 | type LookupHTTPHandlerResolver struct { 12 | handler http.Handler 13 | } 14 | 15 | // NewLookupHTTPHandlerResolver constructs a new resolver. 16 | func NewLookupHTTPHandlerResolver(handler http.Handler) *LookupHTTPHandlerResolver { 17 | return &LookupHTTPHandlerResolver{handler: handler} 18 | } 19 | 20 | // Resolve resolves the values, emitting them to the handler. 21 | func (r *LookupHTTPHandlerResolver) Resolve(ctx context.Context, handler directive.ResolverHandler) error { 22 | if hh := r.handler; hh != nil { 23 | _, _ = handler.AddValue(hh) 24 | } 25 | return nil 26 | } 27 | 28 | // _ is a type assertion 29 | var _ directive.Resolver = ((*LookupHTTPHandlerResolver)(nil)) 30 | -------------------------------------------------------------------------------- /keypem/errors.go: -------------------------------------------------------------------------------- 1 | package keypem 2 | 3 | import "errors" 4 | 5 | // ErrUnexpectedPemType is returned for an unexpected pem type. 6 | var ErrUnexpectedPemType = errors.New("keypem: unexpected pem type") 7 | -------------------------------------------------------------------------------- /keypem/keyfile/keyfile.go: -------------------------------------------------------------------------------- 1 | package keyfile 2 | 3 | import ( 4 | "crypto/rand" 5 | "os" 6 | 7 | "github.com/aperturerobotics/bifrost/keypem" 8 | crypto "github.com/libp2p/go-libp2p/core/crypto" 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | // OpenOrWritePrivKey opens or generates a private key at a path. 13 | // Uses PEM format and ed25519 keys. 14 | // May return a private key + an error. 15 | func OpenOrWritePrivKey(le *logrus.Entry, privKeyPath string) (crypto.PrivKey, error) { 16 | var privKey crypto.PrivKey 17 | var err error 18 | if _, err := os.Stat(privKeyPath); err != nil { 19 | if os.IsNotExist(err) { 20 | if le != nil { 21 | le.Debug("generating priv key") 22 | } 23 | privKey, _, err = crypto.GenerateEd25519Key(rand.Reader) 24 | if err != nil { 25 | return privKey, err 26 | } 27 | dat, err := keypem.MarshalPrivKeyPem(privKey) 28 | if err != nil { 29 | return privKey, err 30 | } 31 | if err := os.WriteFile(privKeyPath, dat, 0o600); err != nil { 32 | return privKey, err 33 | } 34 | if le != nil { 35 | le.Debug("wrote private key") 36 | } 37 | } 38 | } else { 39 | dat, err := os.ReadFile(privKeyPath) 40 | if err != nil { 41 | return privKey, err 42 | } 43 | privKey, err = keypem.ParsePrivKeyPem(dat) 44 | if err != nil { 45 | return privKey, err 46 | } 47 | } 48 | return privKey, err 49 | } 50 | -------------------------------------------------------------------------------- /link/establish-link-ex.go: -------------------------------------------------------------------------------- 1 | package link 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/aperturerobotics/bifrost/peer" 7 | "github.com/aperturerobotics/controllerbus/bus" 8 | ) 9 | 10 | // EstablishLinkWithPeerEx executes a EstablishLinkWithPeer directive. 11 | // Returns a release function. 12 | // if returnIfIdle: returns nil, nil, nil if not found (idle directive) 13 | func EstablishLinkWithPeerEx( 14 | ctx context.Context, 15 | b bus.Bus, 16 | localPeerID, remotePeerID peer.ID, 17 | returnIfIdle bool, 18 | ) (Link, func(), error) { 19 | estl, _, ref, err := bus.ExecWaitValue[EstablishLinkWithPeerValue]( 20 | ctx, 21 | b, 22 | NewEstablishLinkWithPeer( 23 | localPeerID, remotePeerID, 24 | ), 25 | bus.ReturnIfIdle(returnIfIdle), 26 | nil, 27 | nil, 28 | ) 29 | if err != nil { 30 | return nil, nil, err 31 | } 32 | if estl == nil { 33 | ref.Release() 34 | return nil, nil, nil 35 | } 36 | 37 | return estl, ref.Release, nil 38 | } 39 | -------------------------------------------------------------------------------- /link/establish/config.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package link.establish.controller; 3 | 4 | // Config is the link establish controller config. 5 | // The establish controller attempts to establish links with configured peers. 6 | message Config { 7 | // PeerIds is the list of peer IDs to attempt to establish links to. 8 | repeated string peer_ids = 1; 9 | // SrcPeerId is the source peer id to establish links from. 10 | // Can be empty. 11 | string src_peer_id = 2; 12 | } -------------------------------------------------------------------------------- /link/hold-open/config.go: -------------------------------------------------------------------------------- 1 | package link_holdopen_controller 2 | 3 | import ( 4 | "github.com/aperturerobotics/controllerbus/config" 5 | ) 6 | 7 | // ConfigID is the identifier for the config type. 8 | const ConfigID = ControllerID 9 | 10 | // GetConfigID returns the config identifier. 11 | func (c *Config) GetConfigID() string { 12 | return ConfigID 13 | } 14 | 15 | // EqualsConfig checks equality between two configs. 16 | func (c *Config) EqualsConfig(c2 config.Config) bool { 17 | return config.EqualsConfig[*Config](c, c2) 18 | } 19 | 20 | // Validate validates the configuration. 21 | func (c *Config) Validate() error { return nil } 22 | -------------------------------------------------------------------------------- /link/hold-open/config.pb.ts: -------------------------------------------------------------------------------- 1 | // @generated by protoc-gen-es-lite unknown with parameter "target=ts,ts_nocheck=false" 2 | // @generated from file github.com/aperturerobotics/bifrost/link/hold-open/config.proto (package link.holdopen.controller, syntax proto3) 3 | /* eslint-disable */ 4 | 5 | import type { MessageType, PartialFieldInfo } from '@aptre/protobuf-es-lite' 6 | import { createMessageType } from '@aptre/protobuf-es-lite' 7 | 8 | export const protobufPackage = 'link.holdopen.controller' 9 | 10 | /** 11 | * Config is the hold-open controller config. 12 | * 13 | * TODO: limit to specific transport ID, etc. 14 | * 15 | * @generated from message link.holdopen.controller.Config 16 | */ 17 | export interface Config {} 18 | 19 | // Config contains the message type declaration for Config. 20 | export const Config: MessageType = createMessageType({ 21 | typeName: 'link.holdopen.controller.Config', 22 | fields: [] as readonly PartialFieldInfo[], 23 | packedByDefault: true, 24 | }) 25 | -------------------------------------------------------------------------------- /link/hold-open/config.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package link.holdopen.controller; 3 | 4 | // Config is the hold-open controller config. 5 | message Config { 6 | // TODO: limit to specific transport ID, etc. 7 | } -------------------------------------------------------------------------------- /link/hold-open/controller_establish_link.go: -------------------------------------------------------------------------------- 1 | package link_holdopen_controller 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/aperturerobotics/bifrost/link" 7 | "github.com/aperturerobotics/controllerbus/directive" 8 | ) 9 | 10 | // handleEstablishLink handles an EstablishLink directive. 11 | func (c *Controller) handleEstablishLink( 12 | ctx context.Context, 13 | di directive.Instance, 14 | d link.EstablishLinkWithPeer, 15 | ) { 16 | handler := newEstablishLinkHandler(c, c.le, di, d.EstablishLinkTargetPeerId()) 17 | ref := di.AddReference(handler, true) 18 | if ref == nil { 19 | return 20 | } 21 | handler.ref = ref 22 | c.cleanupRefs = append(c.cleanupRefs, ref) 23 | } 24 | -------------------------------------------------------------------------------- /link/link.go: -------------------------------------------------------------------------------- 1 | package link 2 | 3 | import ( 4 | "github.com/aperturerobotics/bifrost/peer" 5 | "github.com/aperturerobotics/bifrost/stream" 6 | ) 7 | 8 | // Link represents a one-hop connection between two peers. 9 | type Link interface { 10 | // GetUUID returns the host-unique ID. 11 | // This should be repeatable between re-constructions of the same link. 12 | GetUUID() uint64 13 | // GetTransportUUID returns the unique ID of the transport. 14 | GetTransportUUID() uint64 15 | // OpenStream opens a stream on the link, with the given parameters. 16 | OpenStream(opts stream.OpenOpts) (stream.Stream, error) 17 | // AcceptStream accepts a stream from the link. 18 | // Terminates when the link closes. 19 | AcceptStream() (stream.Stream, stream.OpenOpts, error) 20 | // GetRemotePeer returns the identity of the remote peer. 21 | GetRemotePeer() peer.ID 22 | // GetLocalPeer returns the identity of the local peer. 23 | GetLocalPeer() peer.ID 24 | // GetRemoteTransportUUID returns the reported remote transport UUID. 25 | // This should be negotiated in the handshake. 26 | GetRemoteTransportUUID() uint64 27 | // Close closes the link. 28 | // Any blocked ReadFrom or WriteTo operations will be unblocked and return errors. 29 | // The link should call the HandleLinkLost callback exactly once. 30 | // Close may be called many times. 31 | Close() error 32 | } 33 | -------------------------------------------------------------------------------- /link/open-stream-ex.go: -------------------------------------------------------------------------------- 1 | package link 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/aperturerobotics/bifrost/peer" 7 | "github.com/aperturerobotics/bifrost/protocol" 8 | "github.com/aperturerobotics/bifrost/stream" 9 | "github.com/aperturerobotics/controllerbus/bus" 10 | ) 11 | 12 | // OpenStreamWithPeerEx executes a OpenStreamWithPeer directive. 13 | // Returns a release function for the links used for the stream. 14 | func OpenStreamWithPeerEx( 15 | ctx context.Context, 16 | b bus.Bus, 17 | protocolID protocol.ID, 18 | localPeerID, remotePeerID peer.ID, 19 | transportID uint64, 20 | openOpts stream.OpenOpts, 21 | ) (MountedStream, func(), error) { 22 | _, estLinkRef, err := b.AddDirective( 23 | NewEstablishLinkWithPeer(localPeerID, remotePeerID), 24 | nil, 25 | ) 26 | if err != nil { 27 | return nil, func() {}, err 28 | } 29 | 30 | mstrm, _, ref, err := bus.ExecWaitValue[MountedStream]( 31 | ctx, 32 | b, 33 | NewOpenStreamWithPeer( 34 | protocolID, 35 | localPeerID, remotePeerID, 36 | transportID, 37 | openOpts, 38 | ), 39 | nil, 40 | nil, 41 | nil, 42 | ) 43 | if err != nil { 44 | estLinkRef.Release() 45 | return nil, func() {}, err 46 | } 47 | ref.Release() 48 | 49 | return mstrm, estLinkRef.Release, nil 50 | } 51 | -------------------------------------------------------------------------------- /link/open-stream-with-link-ex.go: -------------------------------------------------------------------------------- 1 | package link 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/aperturerobotics/bifrost/peer" 7 | "github.com/aperturerobotics/bifrost/protocol" 8 | "github.com/aperturerobotics/bifrost/stream" 9 | "github.com/aperturerobotics/controllerbus/bus" 10 | ) 11 | 12 | // OpenStreamViaLinkEx executes a OpenStreamViaLink directive. 13 | func OpenStreamViaLinkEx( 14 | ctx context.Context, 15 | b bus.Bus, 16 | remotePeerID peer.ID, 17 | protocolID protocol.ID, 18 | linkUUID uint64, 19 | transportID uint64, 20 | openOpts stream.OpenOpts, 21 | ) (MountedStream, error) { 22 | mstrm, _, ref, err := bus.ExecWaitValue[MountedStream]( 23 | ctx, 24 | b, 25 | NewOpenStreamViaLink( 26 | linkUUID, 27 | protocolID, 28 | openOpts, 29 | transportID, 30 | ), 31 | nil, 32 | nil, 33 | nil, 34 | ) 35 | if ref != nil { 36 | ref.Release() 37 | } 38 | return mstrm, err 39 | } 40 | -------------------------------------------------------------------------------- /peer/api/api.go: -------------------------------------------------------------------------------- 1 | package peer_api 2 | 3 | import ( 4 | "github.com/aperturerobotics/bifrost/peer" 5 | ) 6 | 7 | // NewPeerInfo builds peer info from a peer. 8 | func NewPeerInfo(p peer.Peer) *PeerInfo { 9 | pi := &PeerInfo{} 10 | pi.PeerId = peer.IDB58Encode(p.GetPeerID()) 11 | return pi 12 | } 13 | -------------------------------------------------------------------------------- /peer/api/api.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package peer.api; 3 | 4 | import "github.com/aperturerobotics/bifrost/peer/controller/config.proto"; 5 | import "github.com/aperturerobotics/controllerbus/controller/exec/exec.proto"; 6 | 7 | // IdentifyRequest is a request to load an identity. 8 | message IdentifyRequest { 9 | // Config is the request to configure the peer controller. 10 | .peer.controller.Config config = 1; 11 | } 12 | 13 | // IdentifyResponse is a response to an identify request. 14 | message IdentifyResponse { 15 | // ControllerStatus is the status of the peer controller. 16 | .controller.exec.ControllerStatus controller_status = 1; 17 | } 18 | 19 | // GetPeerInfoRequest is the request type for GetPeerInfo. 20 | message GetPeerInfoRequest { 21 | // PeerId restricts the response to a specific peer ID. 22 | string peer_id = 1; 23 | } 24 | 25 | // PeerInfo is basic information about a peer. 26 | message PeerInfo { 27 | // PeerId is the b58 peer ID. 28 | string peer_id = 1; 29 | } 30 | 31 | // GetPeerInfoResponse is the response type for GetPeerInfo. 32 | message GetPeerInfoResponse { 33 | // LocalPeers is the set of peers loaded. 34 | repeated PeerInfo local_peers = 1; 35 | } 36 | 37 | // PeerService implements a bifrost peer service. 38 | service PeerService { 39 | // Identify loads and manages a private key identity. 40 | rpc Identify(IdentifyRequest) returns (stream IdentifyResponse) {} 41 | // GetPeerInfo returns information about attached peers. 42 | rpc GetPeerInfo(GetPeerInfoRequest) returns (GetPeerInfoResponse) {} 43 | } -------------------------------------------------------------------------------- /peer/controller/config.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package peer.controller; 3 | 4 | // Config is the peer controller config. 5 | message Config { 6 | // PrivKey is the peer private key in either b58 or PEM format. 7 | // See confparse.MarshalPrivateKey. 8 | // If not set, the peer private key will be unavailable. 9 | string priv_key = 1; 10 | // PubKey is the peer public key. 11 | // Ignored if priv_key is set. 12 | string pub_key = 2; 13 | // PeerId is the peer identifier. 14 | // Ignored if priv_key or pub_key are set. 15 | // The peer ID should contain the public key. 16 | string peer_id = 3; 17 | } 18 | -------------------------------------------------------------------------------- /peer/controller/controller_test.go: -------------------------------------------------------------------------------- 1 | package peer_controller 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/aperturerobotics/bifrost/peer" 8 | "github.com/aperturerobotics/controllerbus/controller" 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | func TestPrivKeyIntegrity(t *testing.T) { 13 | log := logrus.New() 14 | log.SetLevel(logrus.DebugLevel) 15 | le := logrus.NewEntry(log) 16 | 17 | npeer, err := peer.NewPeer(nil) 18 | if err != nil { 19 | t.Fatal(err.Error()) 20 | } 21 | 22 | ctx := context.Background() 23 | privKey, err := npeer.GetPrivKey(ctx) 24 | if err != nil { 25 | t.Fatal(err.Error()) 26 | } 27 | 28 | privKeyID := npeer.GetPeerID() 29 | peerControllerConf, err := NewConfigWithPrivKey(privKey) 30 | if err != nil { 31 | t.Fatal(err.Error()) 32 | } 33 | 34 | f := NewFactory(nil) 35 | ctrl, err := f.Construct(ctx, peerControllerConf, controller.ConstructOpts{Logger: le}) 36 | if err != nil { 37 | t.Fatal(err.Error()) 38 | } 39 | cctrl := ctrl.(*Controller) 40 | if privKeyID.String() != cctrl.GetPeerID().String() { 41 | t.Fatalf("priv key id mismatch: %s != %s", privKeyID.String(), cctrl.GetPeerID().String()) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /peer/derive_test.go: -------------------------------------------------------------------------------- 1 | package peer 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "testing" 7 | 8 | b58 "github.com/mr-tron/base58/base58" 9 | ) 10 | 11 | // TestDerive tests deriving a context-specific key from crypto keys. 12 | func TestDerive(t *testing.T) { 13 | keys := BuildMockKeys(t) 14 | for ki, key := range keys { 15 | var secret [32]byte 16 | salt := []byte("peer/derive test salt") 17 | cryptoCtx := fmt.Sprintf("bifrost/peer/derive_test keys[%d]", ki) 18 | err := DeriveKey(cryptoCtx, salt, key, secret[:]) 19 | if err != nil { 20 | t.Fatal(err.Error()) 21 | } 22 | t.Logf("keys[%d]: derived key: %s", ki, b58.Encode(secret[:])) 23 | 24 | // derive private key 25 | derivPriv, _, err := DeriveEd25519Key(cryptoCtx+" ed25519", salt, key) 26 | if err == nil && derivPriv == nil { 27 | err = errors.New("derived empty private key") 28 | } 29 | if err != nil { 30 | t.Fatal(err.Error()) 31 | } 32 | derivPrivID, err := IDFromPrivateKey(derivPriv) 33 | if err != nil { 34 | t.Fatal(err.Error()) 35 | } 36 | t.Logf("keys[%d]: derived private key: %s", ki, derivPrivID.String()) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /peer/directive_ex.go: -------------------------------------------------------------------------------- 1 | package peer 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/aperturerobotics/controllerbus/bus" 7 | "github.com/aperturerobotics/controllerbus/directive" 8 | ) 9 | 10 | // GetPeerWithID gets a peer. 11 | // If peer ID is empty, selects any peer. 12 | // valDisposeCallback is called when the value is no longer valid. 13 | // valDisposeCallback can be nil. 14 | func GetPeerWithID( 15 | ctx context.Context, 16 | b bus.Bus, 17 | peerIDConstraint ID, 18 | returnIfIdle bool, 19 | valDisposeCallback func(), 20 | ) (Peer, directive.Instance, directive.Reference, error) { 21 | var idleCb bus.ExecIdleCallback 22 | if returnIfIdle { 23 | idleCb = bus.ReturnWhenIdle() 24 | } 25 | return bus.ExecWaitValue[Peer]( 26 | ctx, 27 | b, 28 | NewGetPeer(peerIDConstraint), 29 | idleCb, 30 | valDisposeCallback, 31 | nil, 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /peer/encrypt_test.go: -------------------------------------------------------------------------------- 1 | package peer 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | // TestEncrypt tests encrypt/decrypt with multiple key types 9 | func TestEncrypt(t *testing.T) { 10 | keys := BuildMockKeys(t) 11 | for ki, privKey := range keys { 12 | pubKey := privKey.GetPublic() 13 | 14 | peerID, err := IDFromPublicKey(pubKey) 15 | if err != nil { 16 | t.Fatal(err.Error()) 17 | } 18 | 19 | // super-secret message 20 | msg := "Hello to " + peerID.String() + "!" 21 | context := "bifrost/peer/encrypt_test super-duper-secret" 22 | dat, err := EncryptToPubKey(pubKey, context, []byte(msg)) 23 | if err != nil { 24 | t.Fatal(err.Error()) 25 | } 26 | t.Logf( 27 | "keys[%d]: encrypted: len %d -> %d", 28 | ki, 29 | len(msg), 30 | len(dat), 31 | ) 32 | 33 | dec, err := DecryptWithPrivKey(privKey, context, dat) 34 | if err != nil { 35 | t.Fatal(err.Error()) 36 | } 37 | if !bytes.Equal(dec, []byte(msg)) { 38 | t.Fatalf("keys[%d]: data did not match: %v != expected %v", ki, dec, []byte(msg)) 39 | } 40 | t.Logf( 41 | "keys[%d]: decrypted correctly: len %d", 42 | ki, 43 | len(dec), 44 | ) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /peer/errors.go: -------------------------------------------------------------------------------- 1 | package peer 2 | 3 | import "errors" 4 | 5 | var ( 6 | // ErrEmptyPeerID is returned if the peer id cannot be empty. 7 | ErrEmptyPeerID = errors.New("peer id cannot be empty") 8 | // ErrEmptyBody is returned if the message body was empty. 9 | ErrEmptyBody = errors.New("message body cannot be empty") 10 | // ErrSignatureInvalid is returned for an invalid signature. 11 | ErrSignatureInvalid = errors.New("message signature invalid") 12 | // ErrShortMessage is returned if a message is too short. 13 | ErrShortMessage = errors.New("message too short") 14 | // ErrNoPrivKey is returned if the private key is not available. 15 | ErrNoPrivKey = errors.New("private key not available for peer") 16 | // ErrInvalidEd25519PubKeyForCurve25519 is returned if a public key cannot be used for curve25519. 17 | ErrInvalidEd25519PubKeyForCurve25519 = errors.New("invalid ed25519 public key for curve25519") 18 | ) 19 | -------------------------------------------------------------------------------- /peer/id.go: -------------------------------------------------------------------------------- 1 | package peer 2 | 3 | import ( 4 | ic "github.com/libp2p/go-libp2p/core/crypto" 5 | ip "github.com/libp2p/go-libp2p/core/peer" 6 | b58 "github.com/mr-tron/base58/base58" 7 | ) 8 | 9 | // ID is a peer identifier. 10 | type ID = ip.ID 11 | 12 | // IDFromBytes cast a string to ID type, and validate 13 | // the id to make sure it is a multihash. 14 | func IDFromBytes(b []byte) (ID, error) { 15 | return ip.IDFromBytes(b) 16 | } 17 | 18 | // IDB58Decode returns a b58-decoded Peer ID. 19 | func IDB58Decode(s string) (ID, error) { 20 | return ip.Decode(s) 21 | } 22 | 23 | // IDB58Encode returns b58-encoded string 24 | func IDB58Encode(id ID) string { 25 | return b58.Encode([]byte(id)) 26 | } 27 | 28 | // IDFromPublicKey returns the Peer ID corresponding to pk 29 | func IDFromPublicKey(pk ic.PubKey) (ID, error) { 30 | return ip.IDFromPublicKey(pk) 31 | } 32 | 33 | // IDFromPrivateKey returns the Peer ID corresponding to sk 34 | func IDFromPrivateKey(sk ic.PrivKey) (ID, error) { 35 | return ip.IDFromPrivateKey(sk) 36 | } 37 | 38 | // IDsToString converts a slice of IDs to strings. 39 | func IDsToString(ids []ID) []string { 40 | out := make([]string, len(ids)) 41 | for i := range ids { 42 | out[i] = ids[i].String() 43 | } 44 | return out 45 | } 46 | -------------------------------------------------------------------------------- /peer/mock_test.go: -------------------------------------------------------------------------------- 1 | package peer 2 | 3 | import ( 4 | "crypto/rand" 5 | "testing" 6 | 7 | "github.com/libp2p/go-libp2p/core/crypto" 8 | ) 9 | 10 | // BuildMockKeys builds the set of mock keys that are expected to work. 11 | func BuildMockKeys(t *testing.T) []crypto.PrivKey { 12 | keys := []crypto.PrivKey{} 13 | 14 | edPriv, _, err := crypto.GenerateEd25519Key(rand.Reader) 15 | if err != nil { 16 | t.Fatal(err.Error()) 17 | } 18 | keys = append(keys, edPriv) 19 | 20 | /* 21 | rPriv, _, err := crypto.GenerateRSAKeyPair(2048, rand.Reader) 22 | if err != nil { 23 | t.Fatal(err.Error()) 24 | } 25 | keys = append(keys, rPriv) 26 | */ 27 | 28 | return keys 29 | } 30 | -------------------------------------------------------------------------------- /peer/peer.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package peer; 3 | 4 | import "github.com/aperturerobotics/bifrost/hash/hash.proto"; 5 | 6 | // Signature contains a signature by a peer. 7 | message Signature { 8 | // PubKey is the public key of the peer. 9 | // May be empty if the public key is to be inferred from context. 10 | bytes pub_key = 1; 11 | // HashType is the hash type used to hash the data. 12 | // The signature is then of the hash bytes (usually 32). 13 | .hash.HashType hash_type = 2; 14 | // SigData contains the signature data. 15 | // The format is defined by the key type. 16 | bytes sig_data = 3; 17 | } 18 | 19 | // SignedMsg is a message from a peer with a signature. 20 | message SignedMsg { 21 | // FromPeerId is the peer identifier of the sender. 22 | string from_peer_id = 1; 23 | // Signature is the sender signature. 24 | // Should not contain PubKey, which is inferred from peer id. 25 | peer.Signature signature = 2; 26 | // Data is the signed data. 27 | bytes data = 3; 28 | } 29 | -------------------------------------------------------------------------------- /peer/peer_addr.go: -------------------------------------------------------------------------------- 1 | package peer 2 | 3 | import ( 4 | "net" 5 | ) 6 | 7 | // NetAddr matches net.Addr with a peer ID 8 | type NetAddr struct { 9 | pid ID 10 | } 11 | 12 | // NewNetAddr constructs a new net.Addr from a peer ID. 13 | func NewNetAddr(pid ID) net.Addr { 14 | return &NetAddr{pid: pid} 15 | } 16 | 17 | // Network is the name of the network (for example, "tcp", "udp") 18 | func (a *NetAddr) Network() string { 19 | return "bifrost" 20 | } 21 | 22 | // String form of address (for example, "192.0.2.1:25", "[2001:db8::1]:80") 23 | func (a *NetAddr) String() string { 24 | return a.pid.String() 25 | } 26 | 27 | // _ is a type assertion 28 | var _ net.Addr = ((*NetAddr)(nil)) 29 | -------------------------------------------------------------------------------- /peer/resolver.go: -------------------------------------------------------------------------------- 1 | package peer 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/aperturerobotics/controllerbus/directive" 7 | ) 8 | 9 | // GetPeerResolver resolves the GetPeer directive 10 | type GetPeerResolver struct { 11 | directive GetPeer 12 | peer Peer 13 | } 14 | 15 | // NewGetPeerResolver constructs a new GetPeer resolver 16 | func NewGetPeerResolver( 17 | directive GetPeer, 18 | peer Peer, 19 | ) *GetPeerResolver { 20 | peerID := directive.GetPeerIDConstraint() 21 | if len(peerID) != 0 { 22 | npID := peer.GetPeerID() 23 | if npID != peerID { 24 | return nil 25 | } 26 | } 27 | 28 | return &GetPeerResolver{ 29 | directive: directive, 30 | peer: peer, 31 | } 32 | } 33 | 34 | // Resolve resolves the values. 35 | func (c *GetPeerResolver) Resolve(ctx context.Context, valHandler directive.ResolverHandler) error { 36 | var val Peer = c.peer //nolint:staticcheck 37 | _, _ = valHandler.AddValue(val) 38 | return nil 39 | } 40 | 41 | // _ is a type assertion 42 | var _ directive.Resolver = ((*GetPeerResolver)(nil)) 43 | -------------------------------------------------------------------------------- /peer/signed-msg_test.go: -------------------------------------------------------------------------------- 1 | package peer 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "testing" 7 | 8 | "github.com/aperturerobotics/bifrost/hash" 9 | ) 10 | 11 | // TestSignedMsg tests signing a message. 12 | func TestSignedMsg(t *testing.T) { 13 | p, err := NewPeer(nil) 14 | if err != nil { 15 | t.Fatal(err.Error()) 16 | } 17 | ctx := context.Background() 18 | exPeerID := p.GetPeerID() 19 | privKey, err := p.GetPrivKey(ctx) 20 | if err != nil { 21 | t.Fatal(err.Error()) 22 | } 23 | 24 | encContext := "bifrost/peer TestSignedMsg" 25 | msg := "hello world from signed message test" 26 | smsg, err := NewSignedMsg(encContext, privKey, hash.RecommendedHashType, []byte(msg)) 27 | if err == nil { 28 | _, _, err = smsg.ExtractAndVerify(encContext) 29 | } 30 | if err != nil { 31 | t.Fatal(err.Error()) 32 | } 33 | 34 | _, peerID, err := smsg.ExtractAndVerify(encContext) 35 | if err != nil { 36 | t.Fatal(err.Error()) 37 | } 38 | if peerID != exPeerID { 39 | t.Fatalf("peer id mismatch: %s != %s", exPeerID.String(), peerID.String()) 40 | } 41 | 42 | if !bytes.Equal(smsg.Data, []byte(msg)) { 43 | t.FailNow() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /peer/ssh/pubkey.go: -------------------------------------------------------------------------------- 1 | package peer_ssh 2 | 3 | import ( 4 | "github.com/libp2p/go-libp2p/core/crypto" 5 | "golang.org/x/crypto/ssh" 6 | ) 7 | 8 | // NewPublicKey converts a public key into a ssh public key. 9 | // Returns ErrKeyUnsupported if the key type is unsupported. 10 | func NewPublicKey(pubKey crypto.PubKey) (ssh.PublicKey, error) { 11 | stdKey, err := crypto.PubKeyToStdKey(pubKey) 12 | if err != nil { 13 | return nil, err 14 | } 15 | return ssh.NewPublicKey(stdKey) 16 | } 17 | -------------------------------------------------------------------------------- /peer/ssh/signer.go: -------------------------------------------------------------------------------- 1 | package peer_ssh 2 | 3 | import ( 4 | "github.com/libp2p/go-libp2p/core/crypto" 5 | "golang.org/x/crypto/ssh" 6 | ) 7 | 8 | // NewSigner constructs a new signer from a private key 9 | func NewSigner(privKey crypto.PrivKey) (ssh.Signer, error) { 10 | cryptoPriv, err := crypto.PrivKeyToStdKey(privKey) 11 | if err != nil { 12 | return nil, err 13 | } 14 | return ssh.NewSignerFromKey(cryptoPriv) 15 | } 16 | -------------------------------------------------------------------------------- /peer/ssh/signer_test.go: -------------------------------------------------------------------------------- 1 | package peer_ssh 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/aperturerobotics/bifrost/peer" 8 | ) 9 | 10 | // TestSshKeys tests generating a key and converting to a ssh signer and pub key. 11 | func TestSshKeys(t *testing.T) { 12 | // generate peer 13 | p, err := peer.NewPeer(nil) 14 | if err != nil { 15 | t.Fatal(err.Error()) 16 | } 17 | 18 | // create signer 19 | ctx := context.Background() 20 | privKey, err := p.GetPrivKey(ctx) 21 | if err != nil { 22 | t.Fatal(err.Error()) 23 | } 24 | _, err = NewSigner(privKey) 25 | if err != nil { 26 | t.Fatal(err.Error()) 27 | } 28 | 29 | // create pub key 30 | _, err = NewPublicKey(p.GetPubKey()) 31 | if err != nil { 32 | t.Fatal(err.Error()) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /protocol/errors.go: -------------------------------------------------------------------------------- 1 | package protocol 2 | 3 | import "errors" 4 | 5 | var ( 6 | // ErrEmptyProtocolID is returned if the protocol id was empty. 7 | ErrEmptyProtocolID = errors.New("protocol id cannot be empty") 8 | // ErrInvalidProtocolID is returned if the protocol id was invalid. 9 | ErrInvalidProtocolID = errors.New("protocol id was invalid") 10 | ) 11 | -------------------------------------------------------------------------------- /protocol/id.go: -------------------------------------------------------------------------------- 1 | package protocol 2 | 3 | import "unicode/utf8" 4 | 5 | // ID is a protocol identifier. 6 | type ID string 7 | 8 | func (i ID) Validate() error { 9 | if i == "" { 10 | return ErrEmptyProtocolID 11 | } 12 | 13 | if !utf8.ValidString(string(i)) { 14 | return ErrInvalidProtocolID 15 | } 16 | 17 | // TODO: additional constraints on the protocol id 18 | return nil 19 | } 20 | 21 | // String returns the ID as a string. 22 | func (i ID) String() string { 23 | return string(i) 24 | } 25 | 26 | // IDsToString converts a slice of IDs to strings. 27 | func IDsToString(ids []ID) []string { 28 | out := make([]string, len(ids)) 29 | for i := range ids { 30 | out[i] = ids[i].String() 31 | } 32 | return out 33 | } 34 | -------------------------------------------------------------------------------- /pubsub/api/api.go: -------------------------------------------------------------------------------- 1 | package pubsub_api 2 | -------------------------------------------------------------------------------- /pubsub/controller/stream.go: -------------------------------------------------------------------------------- 1 | package pubsub_controller 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/aperturerobotics/bifrost/link" 7 | "github.com/aperturerobotics/bifrost/pubsub" 8 | ) 9 | 10 | // streamHandler handles HandleMountedStream directives 11 | type streamHandler struct { 12 | // c is the controller 13 | c *Controller 14 | // ps is the pubsub 15 | ps pubsub.PubSub 16 | } 17 | 18 | // newStreamHandler builds a new stream handler 19 | func newStreamHandler( 20 | c *Controller, 21 | ps pubsub.PubSub, 22 | ) *streamHandler { 23 | return &streamHandler{c: c, ps: ps} 24 | } 25 | 26 | // HandleMountedStream handles an incoming mounted stream. 27 | // Any returned error indicates the stream should be closed. 28 | // This function should return as soon as possible, and start 29 | // additional goroutines to manage the lifecycle of the stream. 30 | func (s *streamHandler) HandleMountedStream(ctx context.Context, ms link.MountedStream) error { 31 | s.c.le.WithField("protocol-id", ms.GetProtocolID()). 32 | Info("pubsub stream opened (by them)") 33 | s.ps.AddPeerStream(pubsub.NewPeerLinkTuple(ms.GetLink()), false, ms) 34 | return nil 35 | } 36 | 37 | // _ is a type assertion 38 | var _ link.MountedStreamHandler = ((*streamHandler)(nil)) 39 | -------------------------------------------------------------------------------- /pubsub/controller/tracked-link.go: -------------------------------------------------------------------------------- 1 | package pubsub_controller 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/aperturerobotics/bifrost/link" 7 | "github.com/aperturerobotics/bifrost/pubsub" 8 | "github.com/aperturerobotics/bifrost/stream" 9 | "github.com/aperturerobotics/controllerbus/bus" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | type trackedLink struct { 14 | c *Controller 15 | tpl pubsub.PeerLinkTuple 16 | lnk link.Link 17 | le *logrus.Entry 18 | ctxCancel context.CancelFunc 19 | } 20 | 21 | // trackLink executes tracking the link. 22 | func (t *trackedLink) trackLink(ctx context.Context) error { 23 | // decide which side opens stream using whoever's peer id is greater 24 | // this is deterministic enough, as long as everyone uses the same 25 | // String() implementation. 26 | if t.lnk.GetLocalPeer().String() > t.lnk.GetRemotePeer().String() { 27 | return nil 28 | } 29 | t.le.Debug("link tracking starting") 30 | ps, err := t.c.GetPubSub(ctx) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | mtStrm, _, avRef, err := bus.ExecWaitValue[link.OpenStreamViaLinkValue]( 36 | ctx, 37 | t.c.bus, 38 | link.NewOpenStreamViaLink( 39 | t.lnk.GetUUID(), 40 | t.c.protocolID, 41 | stream.OpenOpts{}, 42 | t.lnk.GetTransportUUID(), 43 | ), 44 | nil, 45 | nil, 46 | nil, 47 | ) 48 | if err != nil { 49 | return err 50 | } 51 | defer avRef.Release() 52 | 53 | t.le.WithField("protocol-id", mtStrm.GetProtocolID()). 54 | Info("pubsub stream opened (by us)") 55 | ps.AddPeerStream(t.tpl, true, mtStrm) 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /pubsub/floodsub/config.go: -------------------------------------------------------------------------------- 1 | package floodsub 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | var ( 8 | HeartbeatInitialDelay = 100 * time.Millisecond 9 | HeartbeatInterval = 1 * time.Second 10 | SubFanoutTTL = 60 * time.Second 11 | ) 12 | 13 | // Validate validates the configuration. 14 | func (c *Config) Validate() error { return nil } 15 | -------------------------------------------------------------------------------- /pubsub/floodsub/controller/config.go: -------------------------------------------------------------------------------- 1 | package floodsub_controller 2 | 3 | import ( 4 | "github.com/aperturerobotics/controllerbus/config" 5 | ) 6 | 7 | // ConfigID is the string used to identify this config object. 8 | const ConfigID = ControllerID 9 | 10 | // Validate validates the configuration. 11 | // This is a cursory validation to see if the values "look correct." 12 | func (c *Config) Validate() error { 13 | if err := c.GetFloodsubConfig().Validate(); err != nil { 14 | return err 15 | } 16 | return nil 17 | } 18 | 19 | // GetConfigID returns the unique string for this configuration type. 20 | // This string is stored with the encoded config. 21 | func (c *Config) GetConfigID() string { 22 | return ConfigID 23 | } 24 | 25 | // EqualsConfig checks if the other config is equal. 26 | func (c *Config) EqualsConfig(other config.Config) bool { 27 | return config.EqualsConfig[*Config](c, other) 28 | } 29 | 30 | // _ is a type assertion 31 | var _ config.Config = ((*Config)(nil)) 32 | -------------------------------------------------------------------------------- /pubsub/floodsub/controller/config.pb.ts: -------------------------------------------------------------------------------- 1 | // @generated by protoc-gen-es-lite unknown with parameter "target=ts,ts_nocheck=false" 2 | // @generated from file github.com/aperturerobotics/bifrost/pubsub/floodsub/controller/config.proto (package floodsub.controller, syntax proto3) 3 | /* eslint-disable */ 4 | 5 | import { Config as Config$1 } from '../floodsub.pb.js' 6 | import type { MessageType, PartialFieldInfo } from '@aptre/protobuf-es-lite' 7 | import { createMessageType } from '@aptre/protobuf-es-lite' 8 | 9 | export const protobufPackage = 'floodsub.controller' 10 | 11 | /** 12 | * Config is the floodsub controller config. 13 | * 14 | * @generated from message floodsub.controller.Config 15 | */ 16 | export interface Config { 17 | /** 18 | * FloodsubConfig are pubsub provider specific configuration variables. 19 | * 20 | * @generated from field: floodsub.Config floodsub_config = 1; 21 | */ 22 | floodsubConfig?: Config$1 23 | } 24 | 25 | // Config contains the message type declaration for Config. 26 | export const Config: MessageType = createMessageType({ 27 | typeName: 'floodsub.controller.Config', 28 | fields: [ 29 | { no: 1, name: 'floodsub_config', kind: 'message', T: () => Config$1 }, 30 | ] as readonly PartialFieldInfo[], 31 | packedByDefault: true, 32 | }) 33 | -------------------------------------------------------------------------------- /pubsub/floodsub/controller/config.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package floodsub.controller; 3 | 4 | import "github.com/aperturerobotics/bifrost/pubsub/floodsub/floodsub.proto"; 5 | 6 | // Config is the floodsub controller config. 7 | message Config { 8 | // FloodsubConfig are pubsub provider specific configuration variables. 9 | floodsub.Config floodsub_config = 1; 10 | } -------------------------------------------------------------------------------- /pubsub/floodsub/controller/controller.go: -------------------------------------------------------------------------------- 1 | package floodsub_controller 2 | 3 | import ( 4 | pubsub_controller "github.com/aperturerobotics/bifrost/pubsub/controller" 5 | "github.com/blang/semver/v4" 6 | ) 7 | 8 | // Version is the version of the controller implementation. 9 | var Version = semver.MustParse("0.0.1") 10 | 11 | // ControllerID is the ID of the controller. 12 | const ControllerID = "bifrost/floodsub" 13 | 14 | // Controller implements the FloodSub controller. 15 | type Controller = pubsub_controller.Controller 16 | -------------------------------------------------------------------------------- /pubsub/floodsub/floodsub.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package floodsub; 3 | 4 | import "github.com/aperturerobotics/bifrost/peer/peer.proto"; 5 | import "github.com/aperturerobotics/bifrost/hash/hash.proto"; 6 | 7 | // Config configures the floodsub router. 8 | message Config { 9 | // PublishHashType is the hash type to use when signing published messages. 10 | // Defaults to sha256 11 | .hash.HashType publish_hash_type = 1; 12 | } 13 | 14 | // Packet is the floodsub packet. 15 | message Packet { 16 | // Subscriptions contains any new subscription changes. 17 | repeated SubscriptionOpts subscriptions = 1; 18 | // Publish contains messages we are publishing. 19 | repeated .peer.SignedMsg publish = 2; 20 | } 21 | 22 | // SubscriptionOpts are subscription options. 23 | message SubscriptionOpts { 24 | // Subscribe indicates if we are subscribing to this channel ID. 25 | bool subscribe = 1; 26 | // ChannelId is the channel to subscribe to. 27 | string channel_id = 2; 28 | } 29 | 30 | -------------------------------------------------------------------------------- /pubsub/floodsub/sub-handler.go: -------------------------------------------------------------------------------- 1 | package floodsub 2 | 3 | import ( 4 | "github.com/aperturerobotics/bifrost/pubsub" 5 | ) 6 | 7 | // subscriptionHandler contains a handler added with AddHandler 8 | type subscriptionHandler struct { 9 | cb func(m pubsub.Message) 10 | } 11 | -------------------------------------------------------------------------------- /pubsub/nats/auth-client.go: -------------------------------------------------------------------------------- 1 | package nats 2 | 3 | import ( 4 | nats_server "github.com/nats-io/nats-server/v2/server" 5 | ) 6 | 7 | // clientAuth authenticates client connections. 8 | type clientAuth struct{} 9 | 10 | func newClientAuth() *clientAuth { 11 | return &clientAuth{} 12 | } 13 | 14 | // Check if a client is authorized to connect 15 | func (a *clientAuth) Check(c nats_server.ClientAuthentication) bool { 16 | // TODO: always allow 17 | return true 18 | } 19 | 20 | // _ is a type assertion 21 | var _ nats_server.Authentication = ((*clientAuth)(nil)) 22 | -------------------------------------------------------------------------------- /pubsub/nats/auth-router.go: -------------------------------------------------------------------------------- 1 | package nats 2 | 3 | import ( 4 | nats_server "github.com/nats-io/nats-server/v2/server" 5 | ) 6 | 7 | // routerAuth authenticates client connections. 8 | type routerAuth struct{} 9 | 10 | func newRouterAuth() *routerAuth { 11 | return &routerAuth{} 12 | } 13 | 14 | // Check if a router is authorized to connect 15 | func (a *routerAuth) Check(c nats_server.ClientAuthentication) bool { 16 | // TODO: always allow 17 | return true 18 | } 19 | 20 | // _ is a type assertion 21 | var _ nats_server.Authentication = ((*routerAuth)(nil)) 22 | -------------------------------------------------------------------------------- /pubsub/nats/client.go: -------------------------------------------------------------------------------- 1 | package nats 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/aperturerobotics/bifrost/peer" 7 | "github.com/nats-io/nats.go" 8 | ) 9 | 10 | // natsClient contains a nats client with extra data 11 | type natsClient struct { 12 | *nats.Conn 13 | npeerID peer.ID 14 | npeerIDString string 15 | subscriptionRefCount int 16 | } 17 | 18 | // newNatsClient builds a new nats client. 19 | func newNatsClient(npeerID peer.ID, nc *nats.Conn) *natsClient { 20 | return &natsClient{npeerID: npeerID, npeerIDString: npeerID.String(), Conn: nc} 21 | } 22 | 23 | // addRef adds a reference to the nats client. 24 | func (n *natsClient) addRef() func() { 25 | n.subscriptionRefCount++ 26 | var so sync.Once 27 | return func() { 28 | so.Do(func() { 29 | if n.subscriptionRefCount > 0 { 30 | n.subscriptionRefCount-- 31 | } 32 | }) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /pubsub/nats/controller/config.go: -------------------------------------------------------------------------------- 1 | package nats_controller 2 | 3 | import ( 4 | "github.com/aperturerobotics/bifrost/peer" 5 | "github.com/aperturerobotics/bifrost/util/confparse" 6 | "github.com/aperturerobotics/controllerbus/config" 7 | ) 8 | 9 | // ConfigID is the string used to identify this config object. 10 | const ConfigID = ControllerID 11 | 12 | // Validate validates the configuration. 13 | // This is a cursory validation to see if the values "look correct." 14 | func (c *Config) Validate() error { 15 | if err := c.GetNatsConfig().Validate(); err != nil { 16 | return err 17 | } 18 | return nil 19 | } 20 | 21 | // GetConfigID returns the unique string for this configuration type. 22 | // This string is stored with the encoded config. 23 | func (c *Config) GetConfigID() string { 24 | return ConfigID 25 | } 26 | 27 | // EqualsConfig checks if the other config is equal. 28 | func (c *Config) EqualsConfig(c2 config.Config) bool { 29 | return config.EqualsConfig[*Config](c, c2) 30 | } 31 | 32 | // ParsePeerID parses the peer ID if it is not empty. 33 | func (c *Config) ParsePeerID() (peer.ID, error) { 34 | if c.GetPeerId() == "any" { 35 | return peer.ID("any"), nil 36 | } 37 | return confparse.ParsePeerID(c.GetPeerId()) 38 | } 39 | 40 | // _ is a type assertion 41 | var _ config.Config = ((*Config)(nil)) 42 | -------------------------------------------------------------------------------- /pubsub/nats/controller/config.pb.ts: -------------------------------------------------------------------------------- 1 | // @generated by protoc-gen-es-lite unknown with parameter "target=ts,ts_nocheck=false" 2 | // @generated from file github.com/aperturerobotics/bifrost/pubsub/nats/controller/config.proto (package nats.controller, syntax proto3) 3 | /* eslint-disable */ 4 | 5 | import { Config as Config$1 } from '../nats.pb.js' 6 | import type { MessageType, PartialFieldInfo } from '@aptre/protobuf-es-lite' 7 | import { createMessageType, ScalarType } from '@aptre/protobuf-es-lite' 8 | 9 | export const protobufPackage = 'nats.controller' 10 | 11 | /** 12 | * Config is the nats controller config. 13 | * 14 | * @generated from message nats.controller.Config 15 | */ 16 | export interface Config { 17 | /** 18 | * PeerID sets the peer ID to attach the server to. 19 | * Must be set. 20 | * If set to special value: "any" - binds to any peer. 21 | * 22 | * @generated from field: string peer_id = 1; 23 | */ 24 | peerId?: string 25 | /** 26 | * NatsConfig configures nats. 27 | * 28 | * @generated from field: nats.Config nats_config = 2; 29 | */ 30 | natsConfig?: Config$1 31 | } 32 | 33 | // Config contains the message type declaration for Config. 34 | export const Config: MessageType = createMessageType({ 35 | typeName: 'nats.controller.Config', 36 | fields: [ 37 | { no: 1, name: 'peer_id', kind: 'scalar', T: ScalarType.STRING }, 38 | { no: 2, name: 'nats_config', kind: 'message', T: () => Config$1 }, 39 | ] as readonly PartialFieldInfo[], 40 | packedByDefault: true, 41 | }) 42 | -------------------------------------------------------------------------------- /pubsub/nats/controller/config.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package nats.controller; 3 | 4 | import "github.com/aperturerobotics/bifrost/pubsub/nats/nats.proto"; 5 | 6 | // Config is the nats controller config. 7 | message Config { 8 | // PeerID sets the peer ID to attach the server to. 9 | // Must be set. 10 | // If set to special value: "any" - binds to any peer. 11 | string peer_id = 1; 12 | // NatsConfig configures nats. 13 | nats.Config nats_config = 2; 14 | } -------------------------------------------------------------------------------- /pubsub/nats/controller/controller.go: -------------------------------------------------------------------------------- 1 | package nats_controller 2 | 3 | import ( 4 | pubsub_controller "github.com/aperturerobotics/bifrost/pubsub/controller" 5 | "github.com/blang/semver/v4" 6 | ) 7 | 8 | // Version is the version of the controller implementation. 9 | var Version = semver.MustParse("0.0.1") 10 | 11 | // ControllerID is the ID of the controller. 12 | const ControllerID = "bifrost/nats" 13 | 14 | // Controller implements the Nats router controller. 15 | type Controller = pubsub_controller.Controller 16 | -------------------------------------------------------------------------------- /pubsub/nats/local-dialer.go: -------------------------------------------------------------------------------- 1 | package nats 2 | 3 | import ( 4 | "net" 5 | 6 | nats_client "github.com/nats-io/nats.go" 7 | "github.com/nats-io/nkeys" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | const localNatsAddress = "local:4222" 12 | 13 | // localNatsDialer dials the local nats server in-process. 14 | type localNatsDialer struct { 15 | n *Nats 16 | kp nkeys.KeyPair 17 | } 18 | 19 | // newLocalNatsDialer constructs a new localNatsDialer. 20 | func newLocalNatsDialer(n *Nats, keyPair nkeys.KeyPair) *localNatsDialer { 21 | return &localNatsDialer{n: n, kp: keyPair} 22 | } 23 | 24 | // Dial dials the local server. 25 | func (d *localNatsDialer) Dial(network, address string) (net.Conn, error) { 26 | if address != localNatsAddress { 27 | return nil, errors.Errorf("unexpected address for local nats dialer: %s", address) 28 | } 29 | 30 | var pubKey string 31 | var err error 32 | if d.kp != nil { 33 | pubKey, err = d.kp.PublicKey() 34 | if err != nil { 35 | return nil, err 36 | } 37 | } 38 | 39 | p1, p2 := net.Pipe() 40 | go func() { 41 | _ = d.n.natsServer.HandleClientConnection(p2, pubKey) 42 | }() 43 | return p1, nil 44 | } 45 | 46 | // _ is a type assertion 47 | var _ nats_client.CustomDialer = ((*localNatsDialer)(nil)) 48 | -------------------------------------------------------------------------------- /pubsub/nats/nats.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package nats; 3 | 4 | import "github.com/aperturerobotics/bifrost/hash/hash.proto"; 5 | 6 | // NatsConnType indicates the type of nats conn a stream represents. 7 | enum NatsConnType { 8 | // NatsConnType_UNKNOWN is the unknown type. 9 | NatsConnType_UNKNOWN = 0; 10 | // NatsConnType_CLIENT is the client connection type. 11 | NatsConnType_CLIENT = 1; 12 | // NatsConnType_ROUTER is the router-router connection type. 13 | NatsConnType_ROUTER = 2; 14 | // TODO other types 15 | } 16 | 17 | // Config configures the nats router, hosting a nats.io routing node. 18 | // This uses nats 2.0 accounts - an Account maps to a Peer. 19 | message Config { 20 | // ClusterName is the cluster ID string to use. 21 | // This must be the same on all nodes. 22 | // If unset, uses the protocol ID. 23 | string cluster_name = 1; 24 | // PublishHashType is the hash type to use when signing published messages. 25 | // Defaults to sha256 26 | hash.HashType publish_hash_type = 2; 27 | 28 | // LogDebug turns on extended debugging logging. 29 | bool log_debug = 3; 30 | // LogTrace turns on tracing logging. 31 | // implies log_debug. 32 | bool log_trace = 4; 33 | // LogTraceVrebose turns on verbose tracing logging. 34 | // Implies log_trace and log_debug. 35 | bool log_trace_verbose = 5; 36 | } 37 | -------------------------------------------------------------------------------- /pubsub/nats/subscription_handler.go: -------------------------------------------------------------------------------- 1 | package nats 2 | 3 | import ( 4 | "github.com/aperturerobotics/bifrost/pubsub" 5 | ) 6 | 7 | // subscriptionHandler contains a handler added with AddHandler 8 | type subscriptionHandler struct { 9 | cb func(m pubsub.Message) 10 | } 11 | -------------------------------------------------------------------------------- /pubsub/relay/config.go: -------------------------------------------------------------------------------- 1 | package pubsub_relay 2 | 3 | import ( 4 | "github.com/aperturerobotics/bifrost/peer" 5 | "github.com/aperturerobotics/bifrost/util/confparse" 6 | "github.com/aperturerobotics/controllerbus/config" 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | // ConfigID is the identifier for the config type. 11 | const ConfigID = ControllerID 12 | 13 | // GetConfigID returns the config identifier. 14 | func (c *Config) GetConfigID() string { 15 | return ConfigID 16 | } 17 | 18 | // EqualsConfig checks equality between two configs. 19 | func (c *Config) EqualsConfig(c2 config.Config) bool { 20 | return config.EqualsConfig[*Config](c, c2) 21 | } 22 | 23 | // Validate validates the configuration. 24 | func (c *Config) Validate() error { 25 | if len(c.GetTopicIds()) == 0 { 26 | return errors.New("at least one topic id required") 27 | } 28 | peerID, err := c.ParsePeerID() 29 | if err != nil { 30 | return err 31 | } 32 | if peerID == "" { 33 | return errors.New("peer id must be specified") 34 | } 35 | 36 | return nil 37 | } 38 | 39 | // ParsePeerID parses the peer ID if it is not empty. 40 | func (c *Config) ParsePeerID() (peer.ID, error) { 41 | return confparse.ParsePeerID(c.GetPeerId()) 42 | } 43 | -------------------------------------------------------------------------------- /pubsub/relay/config.pb.ts: -------------------------------------------------------------------------------- 1 | // @generated by protoc-gen-es-lite unknown with parameter "target=ts,ts_nocheck=false" 2 | // @generated from file github.com/aperturerobotics/bifrost/pubsub/relay/config.proto (package pubsub.relay, syntax proto3) 3 | /* eslint-disable */ 4 | 5 | import type { MessageType, PartialFieldInfo } from '@aptre/protobuf-es-lite' 6 | import { createMessageType, ScalarType } from '@aptre/protobuf-es-lite' 7 | 8 | export const protobufPackage = 'pubsub.relay' 9 | 10 | /** 11 | * Config is the pubsub relay configuration. 12 | * The relay controller subscribes to a pubsub topic to ensure the peer relays messages. 13 | * 14 | * @generated from message pubsub.relay.Config 15 | */ 16 | export interface Config { 17 | /** 18 | * PeerId is the peer ID to look up and use private key for. 19 | * 20 | * @generated from field: string peer_id = 1; 21 | */ 22 | peerId?: string 23 | /** 24 | * TopicIds are the list of topic IDs to subscribe to. 25 | * 26 | * @generated from field: repeated string topic_ids = 2; 27 | */ 28 | topicIds?: string[] 29 | } 30 | 31 | // Config contains the message type declaration for Config. 32 | export const Config: MessageType = createMessageType({ 33 | typeName: 'pubsub.relay.Config', 34 | fields: [ 35 | { no: 1, name: 'peer_id', kind: 'scalar', T: ScalarType.STRING }, 36 | { 37 | no: 2, 38 | name: 'topic_ids', 39 | kind: 'scalar', 40 | T: ScalarType.STRING, 41 | repeated: true, 42 | }, 43 | ] as readonly PartialFieldInfo[], 44 | packedByDefault: true, 45 | }) 46 | -------------------------------------------------------------------------------- /pubsub/relay/config.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package pubsub.relay; 3 | 4 | // Config is the pubsub relay configuration. 5 | // The relay controller subscribes to a pubsub topic to ensure the peer relays messages. 6 | message Config { 7 | // PeerId is the peer ID to look up and use private key for. 8 | string peer_id = 1; 9 | // TopicIds are the list of topic IDs to subscribe to. 10 | repeated string topic_ids = 2; 11 | } -------------------------------------------------------------------------------- /pubsub/util/pubmessage/errors.go: -------------------------------------------------------------------------------- 1 | package pubmessage 2 | 3 | import "errors" 4 | 5 | // ErrInvalidChannelID is returned for an invalid inner channel ID. 6 | var ErrInvalidChannelID = errors.New("invalid or empty channel id on pubmessage") 7 | -------------------------------------------------------------------------------- /pubsub/util/pubmessage/inner.go: -------------------------------------------------------------------------------- 1 | package pubmessage 2 | 3 | // Validate checks the inner data. 4 | func (i *PubMessageInner) Validate() error { 5 | if i.GetChannel() == "" { 6 | return ErrInvalidChannelID 7 | } 8 | // allow empty 9 | if ts := i.GetTimestamp(); ts.GetSeconds() != 0 { 10 | if err := ts.CheckValid(); err != nil { 11 | return err 12 | } 13 | } 14 | return nil 15 | } 16 | -------------------------------------------------------------------------------- /pubsub/util/pubmessage/message.go: -------------------------------------------------------------------------------- 1 | package pubmessage 2 | 3 | import ( 4 | "github.com/aperturerobotics/bifrost/peer" 5 | "github.com/aperturerobotics/bifrost/pubsub" 6 | ) 7 | 8 | // Message fulfills the Message type with a PubMessageInner. 9 | type Message struct { 10 | pktInner *PubMessageInner 11 | peerID peer.ID 12 | } 13 | 14 | // NewMessage constructs a new Message object 15 | func NewMessage(peerID peer.ID, pktInner *PubMessageInner) *Message { 16 | return &Message{ 17 | peerID: peerID, 18 | pktInner: pktInner, 19 | } 20 | } 21 | 22 | // GetFrom returns the peer ID of the sender. 23 | func (m *Message) GetFrom() peer.ID { 24 | return m.peerID 25 | } 26 | 27 | // GetAuthenticated indicates if the signature is valid. 28 | func (m *Message) GetAuthenticated() bool { 29 | return true 30 | } 31 | 32 | // GetData returns the Message data. 33 | func (m *Message) GetData() []byte { 34 | return m.pktInner.GetData() 35 | } 36 | 37 | // _ is a type assertion 38 | var _ pubsub.Message = ((*Message)(nil)) 39 | -------------------------------------------------------------------------------- /pubsub/util/pubmessage/pubmessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package pubmessage; 3 | 4 | import "google/protobuf/timestamp.proto"; 5 | 6 | // PubMessageInner is the signed inner portion of the message. 7 | message PubMessageInner { 8 | // Data is the message data. 9 | bytes data = 1; 10 | // Channel is the channel. 11 | string channel = 2; 12 | // Timestamp is the message timestamp. 13 | .google.protobuf.Timestamp timestamp = 3; 14 | } 15 | -------------------------------------------------------------------------------- /router/router.go: -------------------------------------------------------------------------------- 1 | package router 2 | -------------------------------------------------------------------------------- /rpc/access/access.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package bifrost.rpc.access; 3 | 4 | import "github.com/aperturerobotics/starpc/rpcstream/rpcstream.proto"; 5 | 6 | // AccessRpcService exposes services with LookupRpcService via RPC. 7 | service AccessRpcService { 8 | // LookupRpcService checks if a RPC service exists with the given info. 9 | // Usually translates to accessing the LookupRpcService directive. 10 | // If the service was not found (directive is idle) returns empty. 11 | rpc LookupRpcService(LookupRpcServiceRequest) returns (stream LookupRpcServiceResponse); 12 | // CallRpcService forwards an RPC call to the service with the component ID. 13 | // Component ID: json encoded LookupRpcServiceRequest. 14 | rpc CallRpcService(stream .rpcstream.RpcStreamPacket) returns (stream .rpcstream.RpcStreamPacket); 15 | } 16 | 17 | // LookupRpcServiceRequest is a request to lookup an rpc service. 18 | message LookupRpcServiceRequest { 19 | // ServiceId is the service identifier. 20 | string service_id = 1; 21 | // ServerId is the identifier of the server requesting the service. 22 | // Can be empty. 23 | string server_id = 2; 24 | } 25 | 26 | // LookupRpcServiceResponse is a response to LookupRpcService 27 | message LookupRpcServiceResponse { 28 | // Idle indicates the directive is now idle. 29 | bool idle = 1; 30 | // Exists indicates we found the service on the remote. 31 | bool exists = 2; 32 | // Removed indicates the value no longer exists. 33 | bool removed = 3; 34 | } 35 | -------------------------------------------------------------------------------- /rpc/errors.go: -------------------------------------------------------------------------------- 1 | package bifrost_rpc 2 | 3 | import "errors" 4 | 5 | // ErrServiceClientUnavailable is returned if no clients are available for a service. 6 | var ErrServiceClientUnavailable = errors.New("no client available for that service") 7 | -------------------------------------------------------------------------------- /rpc/invoker.go: -------------------------------------------------------------------------------- 1 | package bifrost_rpc 2 | 3 | import ( 4 | "github.com/aperturerobotics/controllerbus/bus" 5 | "github.com/aperturerobotics/starpc/srpc" 6 | ) 7 | 8 | // Invoker implements the RPC invoker with a directive. 9 | type Invoker struct { 10 | // b is the bus 11 | b bus.Bus 12 | // serverID is the server identifier. 13 | // can be empty 14 | serverID string 15 | // wait waits for the rpc service to become available. 16 | wait bool 17 | } 18 | 19 | // NewInvoker constructs a new rpc method invoker. 20 | // serverID can be empty, will be used for directives. 21 | // if wait is set, waits for the rpc service to become available. 22 | // otherwise, returns "unimplemented" if the service is unavailable. 23 | func NewInvoker(b bus.Bus, serverID string, wait bool) *Invoker { 24 | return &Invoker{ 25 | b: b, 26 | serverID: serverID, 27 | wait: wait, 28 | } 29 | } 30 | 31 | // InvokeMethod invokes the method matching the service & method ID. 32 | // Returns false, nil if not found. 33 | // If service string is empty, ignore it. 34 | func (i *Invoker) InvokeMethod(serviceID, methodID string, strm srpc.Stream) (bool, error) { 35 | ctx := strm.Context() 36 | 37 | invokers, _, invokerRef, err := ExLookupRpcService(ctx, i.b, serviceID, i.serverID, i.wait, nil) 38 | if err != nil || invokerRef == nil { 39 | return false, err 40 | } 41 | defer invokerRef.Release() 42 | 43 | var sl srpc.InvokerSlice = invokers 44 | return sl.InvokeMethod(serviceID, methodID, strm) 45 | } 46 | 47 | // _ is a type assertion 48 | var _ srpc.Invoker = ((*Invoker)(nil)) 49 | -------------------------------------------------------------------------------- /rpc/rpc-service-builder.go: -------------------------------------------------------------------------------- 1 | package bifrost_rpc 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/aperturerobotics/starpc/srpc" 7 | "github.com/aperturerobotics/util/refcount" 8 | ) 9 | 10 | // RpcServiceBuilder builds a rpc service invoker. 11 | // 12 | // returns the srpc invoker and an optional release function 13 | // can return nil to indicate not found. 14 | type RpcServiceBuilder = refcount.RefCountResolver[srpc.Invoker] 15 | 16 | // NewRpcServiceBuilder creates a new RpcServiceBuilder with a static invoker. 17 | func NewRpcServiceBuilder(handler srpc.Invoker) RpcServiceBuilder { 18 | return func(ctx context.Context, released func()) (srpc.Invoker, func(), error) { 19 | if handler == nil { 20 | return nil, nil, nil 21 | } 22 | return handler, nil, nil 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /signaling/echo/config.go: -------------------------------------------------------------------------------- 1 | package signaling_echo 2 | 3 | import ( 4 | "github.com/aperturerobotics/bifrost/signaling" 5 | "github.com/aperturerobotics/controllerbus/config" 6 | ) 7 | 8 | // ConfigID is the string used to identify this config object. 9 | const ConfigID = ControllerID 10 | 11 | // Validate validates the configuration. 12 | // This is a cursory validation to see if the values "look correct." 13 | func (c *Config) Validate() error { 14 | if c.GetSignalingId() == "" { 15 | return signaling.ErrEmptySignalingID 16 | } 17 | 18 | return nil 19 | } 20 | 21 | // GetConfigID returns the unique string for this configuration type. 22 | // This string is stored with the encoded config. 23 | func (c *Config) GetConfigID() string { 24 | return ConfigID 25 | } 26 | 27 | // EqualsConfig checks if the config is equal to another. 28 | func (c *Config) EqualsConfig(c2 config.Config) bool { 29 | return config.EqualsConfig[*Config](c, c2) 30 | } 31 | 32 | var _ config.Config = ((*Config)(nil)) 33 | -------------------------------------------------------------------------------- /signaling/echo/echo.pb.ts: -------------------------------------------------------------------------------- 1 | // @generated by protoc-gen-es-lite unknown with parameter "target=ts,ts_nocheck=false" 2 | // @generated from file github.com/aperturerobotics/bifrost/signaling/echo/echo.proto (package signaling.echo, syntax proto3) 3 | /* eslint-disable */ 4 | 5 | import type { MessageType, PartialFieldInfo } from '@aptre/protobuf-es-lite' 6 | import { createMessageType, ScalarType } from '@aptre/protobuf-es-lite' 7 | 8 | export const protobufPackage = 'signaling.echo' 9 | 10 | /** 11 | * Config configures the echo controller. 12 | * 13 | * @generated from message signaling.echo.Config 14 | */ 15 | export interface Config { 16 | /** 17 | * SignalingId is the incoming signaling ID to handle and echo messages. 18 | * Cannot be empty. 19 | * 20 | * @generated from field: string signaling_id = 1; 21 | */ 22 | signalingId?: string 23 | } 24 | 25 | // Config contains the message type declaration for Config. 26 | export const Config: MessageType = createMessageType({ 27 | typeName: 'signaling.echo.Config', 28 | fields: [ 29 | { no: 1, name: 'signaling_id', kind: 'scalar', T: ScalarType.STRING }, 30 | ] as readonly PartialFieldInfo[], 31 | packedByDefault: true, 32 | }) 33 | -------------------------------------------------------------------------------- /signaling/echo/echo.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package signaling.echo; 3 | 4 | // Config configures the echo controller. 5 | message Config { 6 | // SignalingId is the incoming signaling ID to handle and echo messages. 7 | // Cannot be empty. 8 | string signaling_id = 1; 9 | } 10 | -------------------------------------------------------------------------------- /signaling/errors.go: -------------------------------------------------------------------------------- 1 | package signaling 2 | 3 | import "errors" 4 | 5 | // ErrEmptySignalingID is returned if the signaling id cannot be empty. 6 | var ErrEmptySignalingID = errors.New("signaling id cannot be empty") 7 | -------------------------------------------------------------------------------- /signaling/rpc/client/config.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package signaling.rpc.client; 3 | 4 | import "github.com/aperturerobotics/bifrost/stream/srpc/client/client.proto"; 5 | import "github.com/aperturerobotics/util/backoff/backoff.proto"; 6 | 7 | // Config configures a client for the Signaling SRPC service. 8 | message Config { 9 | // SignalingId is the signaling channel ID. 10 | // Filters which SignalPeer directives will be handled. 11 | string signaling_id = 1; 12 | // PeerId is the local peer id to use for the client. 13 | // Can be empty to use any local peer. 14 | string peer_id = 2; 15 | // Client contains srpc.client configuration for the signaling RPC client. 16 | // The local peer ID is overridden with the peer ID of the looked-up peer. 17 | .stream.srpc.client.Config client = 3; 18 | // ProtocolId overrides the default protocol id for the signaling client. 19 | // Default: bifrost/signaling 20 | string protocol_id = 4; 21 | // ServiceId overrides the default service id for the signaling client. 22 | // Default: signaling.rpc.Signaling 23 | string service_id = 5; 24 | // Backoff is the backoff config for connecting to the service. 25 | // If unset, defaults to reasonable defaults. 26 | .backoff.Backoff backoff = 6; 27 | // DisableListen disables listening for incoming sessions. 28 | // If set, we will only call out, not accept incoming sessions. 29 | // If false, client will emit HandleSignalPeer directives for incoming sessions. 30 | bool disable_listen = 7; 31 | } 32 | -------------------------------------------------------------------------------- /signaling/rpc/client/session.go: -------------------------------------------------------------------------------- 1 | package signaling_rpc_client 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/aperturerobotics/bifrost/signaling" 7 | "github.com/libp2p/go-libp2p/core/peer" 8 | ) 9 | 10 | // Session implements signaling.Session. 11 | type Session struct { 12 | ref *ClientPeerRef 13 | } 14 | 15 | // NewSessionWithRef wraps a ClientPeerRef into a signaling.Session. 16 | func NewSessionWithRef(ref *ClientPeerRef) *Session { 17 | return &Session{ref: ref} 18 | } 19 | 20 | // GetLocalPeerID returns the local peer ID. 21 | func (s *Session) GetLocalPeerID() peer.ID { 22 | return s.ref.GetLocalPeerID() 23 | } 24 | 25 | // GetRemotePeerID returns the remote peer ID. 26 | func (s *Session) GetRemotePeerID() peer.ID { 27 | return s.ref.GetRemotePeerID() 28 | } 29 | 30 | // Send transmits a message to the remote peer. 31 | // Blocks until the context is canceled OR the message is acked. 32 | func (s *Session) Send(ctx context.Context, msg []byte) error { 33 | _, err := s.ref.Send(ctx, msg) 34 | return err 35 | } 36 | 37 | // Recv waits for and acknowledges an incoming message from the remote peer. 38 | func (s *Session) Recv(ctx context.Context) ([]byte, error) { 39 | msg, err := s.ref.Recv(ctx) 40 | if err != nil { 41 | return nil, err 42 | } 43 | return msg.GetSignedMsg().GetData(), nil 44 | } 45 | 46 | // _ is a type assertion 47 | var _ signaling.SignalPeerSession = ((*Session)(nil)) 48 | -------------------------------------------------------------------------------- /signaling/rpc/errors.go: -------------------------------------------------------------------------------- 1 | package signaling_rpc 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrUserpedSession = errors.New("signaling: session can only be called once per peer") 7 | ErrUserpedListen = errors.New("signaling: listen can only be called once per peer") 8 | ErrUnexpectedSessionMsg = errors.New("signaling: unexpected session message") 9 | ) 10 | -------------------------------------------------------------------------------- /signaling/rpc/server/config.go: -------------------------------------------------------------------------------- 1 | package signaling_rpc_server 2 | 3 | import "github.com/aperturerobotics/controllerbus/config" 4 | 5 | // ConfigID is the string used to identify this config object. 6 | const ConfigID = ControllerID 7 | 8 | // Validate validates the configuration. 9 | // This is a cursory validation to see if the values "look correct." 10 | func (c *Config) Validate() error { 11 | if err := c.GetServer().Validate(); err != nil { 12 | return err 13 | } 14 | return nil 15 | } 16 | 17 | // GetConfigID returns the unique string for this configuration type. 18 | // This string is stored with the encoded config. 19 | func (c *Config) GetConfigID() string { 20 | return ConfigID 21 | } 22 | 23 | // EqualsConfig checks if the other config is equal. 24 | func (c *Config) EqualsConfig(other config.Config) bool { 25 | return config.EqualsConfig[*Config](c, other) 26 | } 27 | 28 | // GetDebugVals returns the directive arguments as key/value pairs. 29 | // This should be something like param1="test", param2="test". 30 | // This is not necessarily unique, and is primarily intended for display. 31 | func (c *Config) GetDebugVals() config.DebugValues { 32 | return c.GetServer().GetDebugVals() 33 | } 34 | 35 | // _ is a type assertion 36 | var _ config.Debuggable = ((*Config)(nil)) 37 | -------------------------------------------------------------------------------- /signaling/rpc/server/peer.go: -------------------------------------------------------------------------------- 1 | package signaling_rpc_server 2 | 3 | // serverPeerTracker tracks a peer on the server. 4 | type serverPeerTracker struct { 5 | // wait is a channel that is closed when below changes. 6 | wait chan struct{} 7 | // listening indicates the listen rpc is attached 8 | listening bool 9 | // listenNonce is the nonce of the current listen session 10 | listenNonce uint64 11 | // wantPeers is the list of peers that want a session w/ this peer. 12 | // peer ids encoded as string 13 | wantPeers map[string]struct{} 14 | } 15 | 16 | // getWaitCh returns the wait channel. 17 | func (p *serverPeerTracker) getWaitCh() <-chan struct{} { 18 | wait := p.wait 19 | if wait == nil { 20 | wait = make(chan struct{}) 21 | p.wait = wait 22 | } 23 | return wait 24 | } 25 | 26 | // broadcast closes the wait channel if any 27 | func (p *serverPeerTracker) broadcast() { 28 | if p.wait != nil { 29 | close(p.wait) 30 | p.wait = nil 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /signaling/rpc/server/server.pb.ts: -------------------------------------------------------------------------------- 1 | // @generated by protoc-gen-es-lite unknown with parameter "target=ts,ts_nocheck=false" 2 | // @generated from file github.com/aperturerobotics/bifrost/signaling/rpc/server/server.proto (package signaling.rpc.server, syntax proto3) 3 | /* eslint-disable */ 4 | 5 | import { Config as Config$1 } from '../../../stream/srpc/server/server.pb.js' 6 | import type { MessageType, PartialFieldInfo } from '@aptre/protobuf-es-lite' 7 | import { createMessageType } from '@aptre/protobuf-es-lite' 8 | 9 | export const protobufPackage = 'signaling.rpc.server' 10 | 11 | /** 12 | * Config is the configuration for the Signaling RPC server. 13 | * 14 | * @generated from message signaling.rpc.server.Config 15 | */ 16 | export interface Config { 17 | /** 18 | * Server configures the peer ids and protocol ids to listen on. 19 | * 20 | * @generated from field: stream.srpc.server.Config server = 1; 21 | */ 22 | server?: Config$1 23 | } 24 | 25 | // Config contains the message type declaration for Config. 26 | export const Config: MessageType = createMessageType({ 27 | typeName: 'signaling.rpc.server.Config', 28 | fields: [ 29 | { no: 1, name: 'server', kind: 'message', T: () => Config$1 }, 30 | ] as readonly PartialFieldInfo[], 31 | packedByDefault: true, 32 | }) 33 | -------------------------------------------------------------------------------- /signaling/rpc/server/server.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package signaling.rpc.server; 3 | 4 | import "github.com/aperturerobotics/bifrost/stream/srpc/server/server.proto"; 5 | 6 | // Config is the configuration for the Signaling RPC server. 7 | message Config { 8 | // Server configures the peer ids and protocol ids to listen on. 9 | .stream.srpc.server.Config server = 1; 10 | } 11 | -------------------------------------------------------------------------------- /signaling/signaling.go: -------------------------------------------------------------------------------- 1 | package signaling 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/aperturerobotics/bifrost/peer" 7 | ) 8 | 9 | // SignalPeerSession is a handle to a Signaling session with a remote peer. 10 | type SignalPeerSession interface { 11 | // GetLocalPeerID returns the local peer ID. 12 | GetLocalPeerID() peer.ID 13 | // GetRemotePeerID returns the remote peer ID. 14 | GetRemotePeerID() peer.ID 15 | 16 | // Send transmits a message to the remote peer. 17 | // Blocks until the context is canceled OR the message is acked. 18 | Send(ctx context.Context, msg []byte) error 19 | // Recv waits for and acknowledges an incoming message from the remote peer. 20 | Recv(ctx context.Context) ([]byte, error) 21 | } 22 | -------------------------------------------------------------------------------- /sim/graph/doc.go: -------------------------------------------------------------------------------- 1 | // Package graph and sub-packages contain a virtual representation of a graph of 2 | // connected networks and nodes. Utilities to simulate these virtual graphs 3 | // exist in other packages in this repo. 4 | package graph 5 | -------------------------------------------------------------------------------- /sim/graph/lan.go: -------------------------------------------------------------------------------- 1 | package graph 2 | 3 | // LAN connects together machines and networks with implied many-to-many links. 4 | // This simulates a wireless network, for example. 5 | type LAN struct { 6 | Node 7 | } 8 | 9 | // AddLAN adds a lan to the network graph. 10 | func AddLAN(g *Graph) *LAN { 11 | l := &LAN{Node: g.BuildNode()} 12 | g.AddNode(l) 13 | return l 14 | } 15 | 16 | // AddPeer adds a peer to the LAN. 17 | func (l *LAN) AddPeer(g *Graph, p *Peer) Edge { 18 | e := g.BuildEdge(l, p) 19 | g.AddEdge(e) 20 | return e 21 | } 22 | 23 | // AddConnectionToLAN adds a connection to another lan. 24 | func (l *LAN) AddConnectionToLAN(g *Graph, ol *LAN) Edge { 25 | e := g.BuildEdge(l, ol) 26 | g.AddEdge(e) 27 | return e 28 | } 29 | 30 | // GetAssociatedPeers returns all peers directly linked to the lan. 31 | func (l *LAN) GetAssociatedPeers(g *Graph) []*Peer { 32 | var peers []*Peer 33 | nodes := g.From(l) 34 | for nodes.Next() { 35 | n := nodes.Node() 36 | np, isPeer := n.(*Peer) 37 | if isPeer { 38 | peers = append(peers, np) 39 | } 40 | } 41 | return peers 42 | } 43 | -------------------------------------------------------------------------------- /sim/simulate/option.go: -------------------------------------------------------------------------------- 1 | package simulate 2 | 3 | // SimulatorOption is an option passed to NewSimulator. 4 | type SimulatorOption func(s *Simulator) error 5 | 6 | // WithVerbose enables verbose logging. 7 | func WithVerbose() SimulatorOption { 8 | return func(s *Simulator) error { 9 | s.verbose = true 10 | return nil 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /sim/tests/bifrost/basic_test.go: -------------------------------------------------------------------------------- 1 | package bifrost 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/aperturerobotics/bifrost/sim/graph" 8 | "github.com/aperturerobotics/bifrost/sim/simulate" 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | // TestBasic tests a simple connection between two peers on a LAN. 13 | func TestBasic(t *testing.T) { 14 | ctx, ctxCancel := context.WithCancel(context.Background()) 15 | defer ctxCancel() 16 | 17 | log := logrus.New() 18 | log.SetLevel(logrus.DebugLevel) 19 | le := logrus.NewEntry(log) 20 | 21 | g := graph.NewGraph() 22 | 23 | descrip := `p0 <-> [lan1] <-> p1` 24 | 25 | p0 := addPeer(t, g) 26 | p1 := addPeer(t, g) 27 | 28 | lan1 := graph.AddLAN(g) 29 | lan1.AddPeer(g, p0) 30 | lan1.AddPeer(g, p1) 31 | 32 | sim := initSimulator( 33 | t, 34 | ctx, 35 | le, 36 | g, 37 | simulate.WithVerbose(), 38 | ) 39 | 40 | le.Infof("attempting to dial %v", descrip) 41 | if err := simulate.TestConnectivity( 42 | ctx, 43 | sim.GetPeerByID(p0.GetPeerID()), 44 | sim.GetPeerByID(p1.GetPeerID()), 45 | ); err != nil { 46 | t.Fatal(err.Error()) 47 | } 48 | 49 | le.Infof("successful connectivity test: %v", descrip) 50 | // <-ctx.Done() 51 | } 52 | -------------------------------------------------------------------------------- /sim/tests/bifrost/bifrost.go: -------------------------------------------------------------------------------- 1 | package bifrost 2 | -------------------------------------------------------------------------------- /sim/tests/bifrost/bifrost_test.go: -------------------------------------------------------------------------------- 1 | package bifrost 2 | 3 | import "github.com/aperturerobotics/bifrost/sim/tests" 4 | 5 | var ( 6 | addPeer = tests.AddPeer 7 | initSimulator = tests.InitSimulator 8 | ) 9 | -------------------------------------------------------------------------------- /sim/tests/bifrost/route_relay_test.go: -------------------------------------------------------------------------------- 1 | //go:build bifrost_relay 2 | // +build bifrost_relay 3 | 4 | package bifrost 5 | 6 | import ( 7 | "context" 8 | "testing" 9 | 10 | "github.com/aperturerobotics/bifrost/sim/graph" 11 | "github.com/aperturerobotics/bifrost/sim/simulate" 12 | "github.com/sirupsen/logrus" 13 | ) 14 | 15 | // TestRouteRelay tests a simple one-hop relay routing scenario. 16 | // TODO: implement circuit/ to pass this test 17 | func TestRouteRelay(t *testing.T) { 18 | ctx, ctxCancel := context.WithCancel(context.Background()) 19 | defer ctxCancel() 20 | 21 | log := logrus.New() 22 | log.SetLevel(logrus.DebugLevel) 23 | le := logrus.NewEntry(log) 24 | 25 | g := graph.NewGraph() 26 | 27 | descrip := `p0 <-> [lan1] <-> p1 <-> [lan2] <-> p2` 28 | 29 | p0 := addPeer(t, g) 30 | p1 := addPeer(t, g) 31 | p2 := addPeer(t, g) 32 | 33 | lan1 := graph.AddLAN(g) 34 | lan2 := graph.AddLAN(g) 35 | 36 | lan1.AddPeer(g, p0) 37 | lan1.AddPeer(g, p1) 38 | lan2.AddPeer(g, p1) 39 | lan2.AddPeer(g, p2) 40 | 41 | sim := initSimulator(t, ctx, le, g) 42 | 43 | le.Infof("attempting to dial %v", descrip) 44 | if err := simulate.TestConnectivity( 45 | ctx, 46 | sim.GetPeerByID(p0.GetPeerID()), 47 | sim.GetPeerByID(p2.GetPeerID()), 48 | ); err != nil { 49 | t.Fatal(err.Error()) 50 | } 51 | 52 | le.Info("tests successful") 53 | _ = sim 54 | } 55 | -------------------------------------------------------------------------------- /sim/tests/common.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/aperturerobotics/bifrost/sim/graph" 8 | "github.com/aperturerobotics/bifrost/sim/simulate" 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | func AddPeer(t *testing.T, g *graph.Graph) *graph.Peer { 13 | ctx := context.Background() 14 | p, err := graph.GenerateAddPeer(ctx, g) 15 | if err != nil { 16 | t.Fatal(err.Error()) 17 | } 18 | return p 19 | } 20 | 21 | func InitSimulator( 22 | t *testing.T, 23 | ctx context.Context, 24 | le *logrus.Entry, 25 | g *graph.Graph, 26 | opts ...simulate.SimulatorOption, 27 | ) *simulate.Simulator { 28 | sim, err := simulate.NewSimulator(ctx, le, g, opts...) 29 | if err != nil { 30 | t.Fatal(err.Error()) 31 | } 32 | le.Info("simulator startup complete") 33 | return sim 34 | } 35 | -------------------------------------------------------------------------------- /sim/tests/common_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/aperturerobotics/bifrost/sim/graph" 8 | "github.com/sirupsen/logrus" 9 | ) 10 | 11 | var ( 12 | addPeer = AddPeer 13 | initSimulator = InitSimulator 14 | ) 15 | 16 | func TestInitSimulator(t *testing.T) { 17 | ctx, ctxCancel := context.WithCancel(context.Background()) 18 | defer ctxCancel() 19 | log := logrus.New() 20 | log.SetLevel(logrus.DebugLevel) 21 | le := logrus.NewEntry(log) 22 | 23 | g := graph.NewGraph() 24 | addPeer(t, g) 25 | sim := initSimulator(t, ctx, le, g) 26 | _ = sim 27 | } 28 | -------------------------------------------------------------------------------- /stream/api/accept/accept.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package stream.api.accept; 3 | 4 | // Config configures the accept controller. 5 | message Config { 6 | // LocalPeerId is the peer ID to accept incoming connections with. 7 | // Can be empty to accept any peer. 8 | string local_peer_id = 1; 9 | // RemotePeerIds are peer IDs to accept incoming connections from. 10 | // Can be empty to accept any remote peer IDs. 11 | repeated string remote_peer_ids = 2; 12 | // ProtocolId is the protocol ID to accept. 13 | string protocol_id = 3; 14 | // TransportId constrains the transport ID to accept from. 15 | // Can be empty. 16 | uint64 transport_id = 4; 17 | } 18 | -------------------------------------------------------------------------------- /stream/api/accept_client.go: -------------------------------------------------------------------------------- 1 | package stream_api 2 | 3 | import ( 4 | stream_api_rpc "github.com/aperturerobotics/bifrost/stream/api/rpc" 5 | ) 6 | 7 | // AcceptStreamClientRPC fulfills the RPC on the client side. 8 | type AcceptStreamClientRPC struct { 9 | SRPCStreamService_AcceptStreamClient 10 | } 11 | 12 | // NewAcceptStreamClientRPC builds a new AcceptStreamClient. 13 | func NewAcceptStreamClientRPC( 14 | client SRPCStreamService_AcceptStreamClient, 15 | ) stream_api_rpc.RPC { 16 | return &AcceptStreamClientRPC{ 17 | SRPCStreamService_AcceptStreamClient: client, 18 | } 19 | } 20 | 21 | // Send sends a packet. 22 | func (r *AcceptStreamClientRPC) Send(resp *stream_api_rpc.Data) error { 23 | return r.SRPCStreamService_AcceptStreamClient.Send( 24 | &AcceptStreamRequest{ 25 | Data: resp, 26 | }, 27 | ) 28 | } 29 | 30 | // Recv receives a packet. 31 | func (r *AcceptStreamClientRPC) Recv() (*stream_api_rpc.Data, error) { 32 | msg, err := r.SRPCStreamService_AcceptStreamClient.Recv() 33 | return msg.GetData(), err 34 | } 35 | 36 | // _ is a type assertion 37 | var _ stream_api_rpc.RPC = ((*AcceptStreamClientRPC)(nil)) 38 | -------------------------------------------------------------------------------- /stream/api/accept_server.go: -------------------------------------------------------------------------------- 1 | package stream_api 2 | 3 | import stream_api_rpc "github.com/aperturerobotics/bifrost/stream/api/rpc" 4 | 5 | // AcceptServerRPC fulfills rpc accept streams on the server. 6 | type AcceptServerRPC struct { 7 | SRPCStreamService_AcceptStreamStream 8 | } 9 | 10 | // NewAcceptServerRPC constructs a new AcceptServerRPC. 11 | func NewAcceptServerRPC( 12 | serv SRPCStreamService_AcceptStreamStream, 13 | ) stream_api_rpc.RPC { 14 | return &AcceptServerRPC{ 15 | SRPCStreamService_AcceptStreamStream: serv, 16 | } 17 | } 18 | 19 | // Send sends a packet. 20 | func (r *AcceptServerRPC) Send(resp *stream_api_rpc.Data) error { 21 | return r.SRPCStreamService_AcceptStreamStream.Send( 22 | &AcceptStreamResponse{ 23 | Data: resp, 24 | }, 25 | ) 26 | } 27 | 28 | // Recv receives a packet. 29 | func (r *AcceptServerRPC) Recv() (*stream_api_rpc.Data, error) { 30 | msg, err := r.SRPCStreamService_AcceptStreamStream.Recv() 31 | return msg.GetData(), err 32 | } 33 | 34 | // _ is a type assertion 35 | var _ stream_api_rpc.RPC = ((*AcceptServerRPC)(nil)) 36 | -------------------------------------------------------------------------------- /stream/api/dial/config.go: -------------------------------------------------------------------------------- 1 | package stream_api_dial 2 | 3 | import ( 4 | "github.com/aperturerobotics/bifrost/peer" 5 | "github.com/aperturerobotics/bifrost/protocol" 6 | "github.com/aperturerobotics/bifrost/util/confparse" 7 | ) 8 | 9 | // Validate validates the configuration. 10 | // This is a cursory validation to see if the values "look correct." 11 | func (c *Config) Validate() error { 12 | if c.GetPeerId() == "" { 13 | return peer.ErrEmptyPeerID 14 | } 15 | 16 | if _, err := c.ParseLocalPeerID(); err != nil { 17 | return err 18 | } 19 | 20 | pid := protocol.ID(c.GetProtocolId()) 21 | if err := pid.Validate(); err != nil { 22 | return err 23 | } 24 | 25 | return nil 26 | } 27 | 28 | // ParseLocalPeerID parses the local peer ID constraint. 29 | // may be empty. 30 | func (c *Config) ParseLocalPeerID() (peer.ID, error) { 31 | return confparse.ParsePeerID(c.GetLocalPeerId()) 32 | } 33 | 34 | // ParsePeerID parses the target peer ID constraint. 35 | func (c *Config) ParsePeerID() (peer.ID, error) { 36 | return confparse.ParsePeerID(c.GetPeerId()) 37 | } 38 | -------------------------------------------------------------------------------- /stream/api/dial/dial.go: -------------------------------------------------------------------------------- 1 | package stream_api_dial 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/aperturerobotics/bifrost/link" 7 | "github.com/aperturerobotics/bifrost/protocol" 8 | "github.com/aperturerobotics/bifrost/stream" 9 | stream_api "github.com/aperturerobotics/bifrost/stream/api/rpc" 10 | "github.com/aperturerobotics/controllerbus/bus" 11 | ) 12 | 13 | // ProcessRPC processes an RPC by dialing the desired target. 14 | func ProcessRPC( 15 | ctx context.Context, 16 | b bus.Bus, 17 | conf *Config, 18 | rpc stream_api.RPC, 19 | ) error { 20 | if err := conf.Validate(); err != nil { 21 | return err 22 | } 23 | 24 | localPeerID, err := conf.ParseLocalPeerID() 25 | if err != nil { 26 | return err 27 | } 28 | 29 | remotePeerID, err := conf.ParsePeerID() 30 | if err != nil { 31 | return err 32 | } 33 | 34 | // Dial the target. 35 | if err := rpc.Send(&stream_api.Data{ 36 | State: stream_api.StreamState_StreamState_ESTABLISHING, 37 | }); err != nil { 38 | return err 39 | } 40 | strm, rel, err := link.OpenStreamWithPeerEx( 41 | ctx, 42 | b, 43 | protocol.ID(conf.GetProtocolId()), 44 | localPeerID, remotePeerID, 45 | conf.GetTransportId(), 46 | stream.OpenOpts{}, 47 | ) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | defer rel() 53 | defer strm.GetStream().Close() 54 | 55 | if err := rpc.Send(&stream_api.Data{ 56 | State: stream_api.StreamState_StreamState_ESTABLISHED, 57 | }); err != nil { 58 | return err 59 | } 60 | return stream_api.AttachRPCToStream(rpc, strm.GetStream(), nil) 61 | } 62 | -------------------------------------------------------------------------------- /stream/api/dial/dial.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package stream.api.dial; 3 | 4 | // Config configures the dial controller. 5 | message Config { 6 | // PeerId is the remote peer ID to dial. 7 | string peer_id = 1; 8 | // LocalPeerId is the peer ID to dial with. 9 | // Can be empty to accept any loaded peer. 10 | string local_peer_id = 2; 11 | // ProtocolId is the protocol ID to dial with. 12 | string protocol_id = 3; 13 | // TransportId constrains the transport ID to dial with. 14 | // Can be empty. 15 | uint64 transport_id = 4; 16 | } 17 | -------------------------------------------------------------------------------- /stream/api/dial_client.go: -------------------------------------------------------------------------------- 1 | package stream_api 2 | 3 | import stream_api_rpc "github.com/aperturerobotics/bifrost/stream/api/rpc" 4 | 5 | // DialStreamClientRPC fulfills stream RPC on the client side. 6 | type DialStreamClientRPC struct { 7 | SRPCStreamService_DialStreamClient 8 | } 9 | 10 | // NewDialStreamClientRPC builds a new DialStreamClientRPC. 11 | func NewDialStreamClientRPC( 12 | client SRPCStreamService_DialStreamClient, 13 | ) stream_api_rpc.RPC { 14 | return &DialStreamClientRPC{ 15 | SRPCStreamService_DialStreamClient: client, 16 | } 17 | } 18 | 19 | // Send sends a packet. 20 | func (r *DialStreamClientRPC) Send(resp *stream_api_rpc.Data) error { 21 | return r.SRPCStreamService_DialStreamClient.Send( 22 | &DialStreamRequest{ 23 | Data: resp, 24 | }, 25 | ) 26 | } 27 | 28 | // Recv receives a packet. 29 | func (r *DialStreamClientRPC) Recv() (*stream_api_rpc.Data, error) { 30 | msg, err := r.SRPCStreamService_DialStreamClient.Recv() 31 | return msg.GetData(), err 32 | } 33 | 34 | // _ is a type assertion 35 | var _ stream_api_rpc.RPC = ((*DialStreamClientRPC)(nil)) 36 | -------------------------------------------------------------------------------- /stream/api/dial_server.go: -------------------------------------------------------------------------------- 1 | package stream_api 2 | 3 | import stream_api_rpc "github.com/aperturerobotics/bifrost/stream/api/rpc" 4 | 5 | // DialServerRPC fulfills the RPC on the server side. 6 | type DialServerRPC struct { 7 | SRPCStreamService_DialStreamStream 8 | } 9 | 10 | // NewDialServerRPC builds a new DialServerRPC. 11 | func NewDialServerRPC( 12 | serv SRPCStreamService_DialStreamStream, 13 | ) stream_api_rpc.RPC { 14 | return &DialServerRPC{ 15 | SRPCStreamService_DialStreamStream: serv, 16 | } 17 | } 18 | 19 | // Send sends a packet. 20 | func (r *DialServerRPC) Send(resp *stream_api_rpc.Data) error { 21 | return r.SRPCStreamService_DialStreamStream.Send( 22 | &DialStreamResponse{ 23 | Data: resp, 24 | }, 25 | ) 26 | } 27 | 28 | // Recv receives a packet. 29 | func (r *DialServerRPC) Recv() (*stream_api_rpc.Data, error) { 30 | msg, err := r.SRPCStreamService_DialStreamStream.Recv() 31 | return msg.GetData(), err 32 | } 33 | 34 | // _ is a type assertion 35 | var _ stream_api_rpc.RPC = ((*DialServerRPC)(nil)) 36 | -------------------------------------------------------------------------------- /stream/api/rpc/rpc.go: -------------------------------------------------------------------------------- 1 | package stream_api_rpc 2 | 3 | import ( 4 | "context" 5 | "io" 6 | ) 7 | 8 | // RPC matches the request/response interface. 9 | type RPC interface { 10 | // Context returns the context. 11 | Context() context.Context 12 | // Send sends a packet. 13 | Send(*Data) error 14 | // Recv receives a packet. 15 | Recv() (*Data, error) 16 | } 17 | 18 | // AttachRPCToStream attaches a RPC to a stream. 19 | func AttachRPCToStream( 20 | rpc RPC, 21 | s io.ReadWriteCloser, 22 | stateCb func(state StreamState), 23 | ) error { 24 | // Read pump 25 | errCh := make(chan error, 3) 26 | go func() { 27 | defer s.Close() 28 | buf := make([]byte, 1500) 29 | d := &Data{} 30 | for { 31 | n, err := s.Read(buf) 32 | if err != nil { 33 | errCh <- err 34 | return 35 | } 36 | 37 | d.Data = buf[:n] 38 | err = rpc.Send(d) 39 | if err != nil { 40 | errCh <- err 41 | return 42 | } 43 | } 44 | }() 45 | 46 | // Write pump 47 | go func() { 48 | defer s.Close() 49 | for { 50 | msg, err := rpc.Recv() 51 | if err != nil { 52 | errCh <- err 53 | return 54 | } 55 | 56 | if st := msg.GetState(); st != StreamState_StreamState_NONE { 57 | if stateCb != nil { 58 | stateCb(st) 59 | } 60 | 61 | continue 62 | } 63 | 64 | _, err = s.Write(msg.GetData()) 65 | if err != nil { 66 | errCh <- err 67 | return 68 | } 69 | } 70 | }() 71 | 72 | // Return when any errors. 73 | return <-errCh 74 | } 75 | -------------------------------------------------------------------------------- /stream/api/rpc/rpc.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package stream.api.rpc; 3 | 4 | // StreamState is state for the stream related calls. 5 | enum StreamState { 6 | // StreamState_NONE indicates nothing about the state 7 | StreamState_NONE = 0; 8 | // StreamState_ESTABLISHING indicates the stream is connecting. 9 | StreamState_ESTABLISHING = 1; 10 | // StreamState_ESTABLISHED indicates the stream is established. 11 | StreamState_ESTABLISHED = 2; 12 | } 13 | 14 | // Data is a data packet. 15 | message Data { 16 | // State indicates stream state in-band. 17 | // Data is packet data from the remote. 18 | bytes data = 1; 19 | // State indicates the stream state. 20 | StreamState state = 2; 21 | } 22 | -------------------------------------------------------------------------------- /stream/echo/config.go: -------------------------------------------------------------------------------- 1 | package stream_echo 2 | 3 | import ( 4 | "github.com/aperturerobotics/bifrost/peer" 5 | "github.com/aperturerobotics/bifrost/protocol" 6 | "github.com/aperturerobotics/bifrost/util/confparse" 7 | "github.com/aperturerobotics/controllerbus/config" 8 | ) 9 | 10 | // ConfigID is the string used to identify this config object. 11 | const ConfigID = ControllerID 12 | 13 | // Validate validates the configuration. 14 | // This is a cursory validation to see if the values "look correct." 15 | func (c *Config) Validate() error { 16 | if _, err := c.ParsePeerID(); err != nil { 17 | return err 18 | } 19 | 20 | pid := protocol.ID(c.GetProtocolId()) 21 | if len(pid) != 0 { 22 | if err := pid.Validate(); err != nil { 23 | return err 24 | } 25 | } 26 | 27 | return nil 28 | } 29 | 30 | // ParsePeerID parses the peer ID. 31 | // may return nil. 32 | func (c *Config) ParsePeerID() (peer.ID, error) { 33 | return confparse.ParsePeerID(c.GetPeerId()) 34 | } 35 | 36 | // GetConfigID returns the unique string for this configuration type. 37 | // This string is stored with the encoded config. 38 | func (c *Config) GetConfigID() string { 39 | return ConfigID 40 | } 41 | 42 | // EqualsConfig checks if the config is equal to another. 43 | func (c *Config) EqualsConfig(c2 config.Config) bool { 44 | return config.EqualsConfig[*Config](c, c2) 45 | } 46 | 47 | var _ config.Config = ((*Config)(nil)) 48 | -------------------------------------------------------------------------------- /stream/echo/echo.pb.ts: -------------------------------------------------------------------------------- 1 | // @generated by protoc-gen-es-lite unknown with parameter "target=ts,ts_nocheck=false" 2 | // @generated from file github.com/aperturerobotics/bifrost/stream/echo/echo.proto (package stream.echo, syntax proto3) 3 | /* eslint-disable */ 4 | 5 | import type { MessageType, PartialFieldInfo } from '@aptre/protobuf-es-lite' 6 | import { createMessageType, ScalarType } from '@aptre/protobuf-es-lite' 7 | 8 | export const protobufPackage = 'stream.echo' 9 | 10 | /** 11 | * Config configures the echo controller. 12 | * 13 | * @generated from message stream.echo.Config 14 | */ 15 | export interface Config { 16 | /** 17 | * PeerId is the peer ID to echo for. 18 | * Can be empty. 19 | * 20 | * @generated from field: string peer_id = 1; 21 | */ 22 | peerId?: string 23 | /** 24 | * ProtocolId is the protocol ID to echo on. 25 | * 26 | * @generated from field: string protocol_id = 2; 27 | */ 28 | protocolId?: string 29 | } 30 | 31 | // Config contains the message type declaration for Config. 32 | export const Config: MessageType = createMessageType({ 33 | typeName: 'stream.echo.Config', 34 | fields: [ 35 | { no: 1, name: 'peer_id', kind: 'scalar', T: ScalarType.STRING }, 36 | { no: 2, name: 'protocol_id', kind: 'scalar', T: ScalarType.STRING }, 37 | ] as readonly PartialFieldInfo[], 38 | packedByDefault: true, 39 | }) 40 | -------------------------------------------------------------------------------- /stream/echo/echo.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package stream.echo; 3 | 4 | // Config configures the echo controller. 5 | message Config { 6 | // PeerId is the peer ID to echo for. 7 | // Can be empty. 8 | string peer_id = 1; 9 | // ProtocolId is the protocol ID to echo on. 10 | string protocol_id = 2; 11 | } -------------------------------------------------------------------------------- /stream/echo/resolver.go: -------------------------------------------------------------------------------- 1 | package stream_echo 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/aperturerobotics/bifrost/link" 7 | "github.com/aperturerobotics/controllerbus/bus" 8 | "github.com/aperturerobotics/controllerbus/directive" 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | // EchoResolver resolves HandleMountedStream by echoing data. 13 | type EchoResolver struct { 14 | // le is the logger 15 | le *logrus.Entry 16 | // bus is the controller bus 17 | bus bus.Bus 18 | } 19 | 20 | // NewEchoResolver constructs a new mounted stream echo resolver. 21 | func NewEchoResolver(le *logrus.Entry, bus bus.Bus) (*EchoResolver, error) { 22 | return &EchoResolver{le: le, bus: bus}, nil 23 | } 24 | 25 | // Resolve resolves the values, emitting them to the handler. 26 | func (r *EchoResolver) Resolve(ctx context.Context, handler directive.ResolverHandler) error { 27 | h, err := NewMountedStreamHandler(r.le, r.bus) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | handler.AddValue(link.MountedStreamHandler(h)) 33 | return nil 34 | } 35 | 36 | // _ is a type assertion 37 | var _ directive.Resolver = ((*EchoResolver)(nil)) 38 | -------------------------------------------------------------------------------- /stream/forwarding/dial_resolver.go: -------------------------------------------------------------------------------- 1 | package stream_forwarding 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/aperturerobotics/bifrost/link" 7 | "github.com/aperturerobotics/controllerbus/bus" 8 | "github.com/aperturerobotics/controllerbus/directive" 9 | ma "github.com/multiformats/go-multiaddr" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | // DialResolver resolves HandleMountedStream by dialing a multiaddr. 14 | type DialResolver struct { 15 | // le is the logger 16 | le *logrus.Entry 17 | // ma is the multiaddress to dial 18 | ma ma.Multiaddr 19 | // bus is the controller bus 20 | bus bus.Bus 21 | } 22 | 23 | // NewDialResolver constructs a new dial resolver. 24 | func NewDialResolver(le *logrus.Entry, bus bus.Bus, ma ma.Multiaddr) (*DialResolver, error) { 25 | return &DialResolver{le: le, ma: ma, bus: bus}, nil 26 | } 27 | 28 | // Resolve resolves the values, emitting them to the handler. 29 | func (r *DialResolver) Resolve(ctx context.Context, handler directive.ResolverHandler) error { 30 | h, err := NewMountedStreamHandler(r.le, r.bus, r.ma) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | handler.AddValue(link.MountedStreamHandler(h)) 36 | return nil 37 | } 38 | 39 | // _ is a type assertion 40 | var _ directive.Resolver = ((*DialResolver)(nil)) 41 | -------------------------------------------------------------------------------- /stream/forwarding/forwarding.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package stream.forwarding; 3 | 4 | // Config configures the forwarding controller. 5 | message Config { 6 | // PeerId is the peer ID to listen for incoming streams. 7 | // Can be empty to accept any. 8 | string peer_id = 1; 9 | // ProtocolId is the protocol ID to filter incoming streams. 10 | // Cannot be empty. 11 | string protocol_id = 2; 12 | // TargetMultiaddr is the target multiaddress to dial. 13 | string target_multiaddr = 3; 14 | } 15 | -------------------------------------------------------------------------------- /stream/listening/listening.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package stream.listening; 3 | 4 | // Config configures the listening controller. 5 | message Config { 6 | // LocalPeerId is the peer ID to forward incoming connections with. 7 | // Can be empty. 8 | string local_peer_id = 1; 9 | // RemotePeerId is the peer ID to forward incoming connections to. 10 | string remote_peer_id = 2; 11 | // ProtocolId is the protocol ID to assign to incoming connections. 12 | string protocol_id = 3; 13 | // ListenMultiaddr is the listening multiaddress. 14 | string listen_multiaddr = 4; 15 | // TransportId sets a transport ID constraint. 16 | // Can be empty. 17 | uint64 transport_id = 5; 18 | } 19 | -------------------------------------------------------------------------------- /stream/netconn/netconn.go: -------------------------------------------------------------------------------- 1 | package stream_netconn 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/aperturerobotics/bifrost/link" 7 | "github.com/aperturerobotics/bifrost/peer" 8 | "github.com/aperturerobotics/bifrost/stream" 9 | ) 10 | 11 | // NetConn wraps a Bifrost stream to be compatible with net.Conn. 12 | type NetConn struct { 13 | stream.Stream 14 | 15 | localPeerID peer.ID 16 | remotePeerID peer.ID 17 | } 18 | 19 | // NewNetConn constructs a net.Conn from a stream. 20 | func NewNetConn(strm link.MountedStream) net.Conn { 21 | return &NetConn{ 22 | Stream: strm.GetStream(), 23 | remotePeerID: strm.GetPeerID(), 24 | localPeerID: strm.GetLink().GetLocalPeer(), 25 | } 26 | } 27 | 28 | // LocalAddr returns the local network address. 29 | func (n *NetConn) LocalAddr() net.Addr { 30 | return peer.NewNetAddr(n.localPeerID) 31 | } 32 | 33 | // RemoteAddr returns the remote network address. 34 | func (n *NetConn) RemoteAddr() net.Addr { 35 | return peer.NewNetAddr(n.remotePeerID) 36 | } 37 | 38 | // _ is a type assertion 39 | var _ net.Conn = ((*NetConn)(nil)) 40 | -------------------------------------------------------------------------------- /stream/relay/relay.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package stream.relay; 3 | 4 | // Config configures the relay controller. 5 | message Config { 6 | // PeerId is the peer ID to listen for incoming streams. 7 | // Can be empty to accept any. 8 | string peer_id = 1; 9 | // ProtocolId is the protocol ID to filter incoming streams. 10 | // Cannot be empty. 11 | string protocol_id = 2; 12 | // TargetPeerId is the destination peer ID to relay to. 13 | // Cannot be empty. 14 | string target_peer_id = 3; 15 | // TargetProtocolId is the destination protocol ID to relay to. 16 | // If unset, uses protocol_id. 17 | string target_protocol_id = 4; 18 | } 19 | -------------------------------------------------------------------------------- /stream/srpc/client/client.go: -------------------------------------------------------------------------------- 1 | package stream_srpc_client 2 | 3 | import ( 4 | "github.com/aperturerobotics/bifrost/protocol" 5 | stream_srpc "github.com/aperturerobotics/bifrost/stream/srpc" 6 | "github.com/aperturerobotics/controllerbus/bus" 7 | "github.com/aperturerobotics/starpc/srpc" 8 | "github.com/pkg/errors" 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | // Client is a common srpc client implementation. 13 | type Client = srpc.Client 14 | 15 | // NewClient constructs a new client. 16 | func NewClient(le *logrus.Entry, b bus.Bus, c *Config, protocolID protocol.ID) (Client, error) { 17 | srcPeer, err := c.ParseSrcPeerId() 18 | if err != nil { 19 | return nil, errors.Wrap(err, "src_peer_id") 20 | } 21 | 22 | serverPeerIDs, err := c.ParseServerPeerIds() 23 | if err != nil { 24 | return nil, errors.Wrap(err, "src_peer_id") 25 | } 26 | 27 | timeoutDur, err := c.ParseTimeoutDur() 28 | if err != nil { 29 | return nil, errors.Wrap(err, "timeout_dur") 30 | } 31 | 32 | if err := protocolID.Validate(); err != nil { 33 | return nil, err 34 | } 35 | 36 | openStreamFn := stream_srpc.NewMultiOpenStreamFunc( 37 | b, 38 | le, 39 | protocolID, 40 | srcPeer, serverPeerIDs, 41 | c.GetTransportId(), 42 | timeoutDur, 43 | ) 44 | 45 | return srpc.NewClient(openStreamFn), nil 46 | } 47 | -------------------------------------------------------------------------------- /stream/srpc/client/client.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package stream.srpc.client; 3 | 4 | import "github.com/aperturerobotics/util/backoff/backoff.proto"; 5 | 6 | // Config configures a client for a srpc service. 7 | message Config { 8 | // ServerPeerIds are the static list of peer IDs to contact. 9 | repeated string server_peer_ids = 1; 10 | // PerServerBackoff is the server peer error backoff configuration. 11 | // Can be empty. 12 | .backoff.Backoff per_server_backoff = 2; 13 | // SrcPeerId is the source peer id to contact from. 14 | // Can be empty. 15 | string src_peer_id = 3; 16 | // TransportId restricts which transport we can dial out from. 17 | uint64 transport_id = 4; 18 | // TimeoutDur sets the per-server establish timeout. 19 | // If unset, no timeout. 20 | // Example: 15s 21 | string timeout_dur = 5; 22 | } 23 | -------------------------------------------------------------------------------- /stream/srpc/client/config.go: -------------------------------------------------------------------------------- 1 | package stream_srpc_client 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/aperturerobotics/bifrost/peer" 7 | "github.com/aperturerobotics/bifrost/util/confparse" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | // Validate validates the config. 12 | func (c *Config) Validate() error { 13 | if _, err := c.ParseSrcPeerId(); err != nil { 14 | return errors.Wrap(err, "src_peer_id") 15 | } 16 | if _, err := c.ParseServerPeerIds(); err != nil { 17 | return errors.Wrap(err, "server_peer_ids") 18 | } 19 | if _, err := c.ParseTimeoutDur(); err != nil { 20 | return errors.Wrap(err, "timeout_dur") 21 | } 22 | return nil 23 | } 24 | 25 | // ParseSrcPeerId parses the source peer id, if set. 26 | func (c *Config) ParseSrcPeerId() (peer.ID, error) { 27 | return confparse.ParsePeerID(c.GetSrcPeerId()) 28 | } 29 | 30 | // ParseServerPeerIds parses the destination peer ids 31 | func (c *Config) ParseServerPeerIds() ([]peer.ID, error) { 32 | return confparse.ParsePeerIDs(c.GetServerPeerIds(), false) 33 | } 34 | 35 | // ParseTimeoutDur parses the timeout duration. 36 | // returns zero if empty 37 | func (c *Config) ParseTimeoutDur() (time.Duration, error) { 38 | durStr := c.GetTimeoutDur() 39 | if durStr == "" { 40 | return 0, nil 41 | } 42 | return time.ParseDuration(durStr) 43 | } 44 | -------------------------------------------------------------------------------- /stream/srpc/client/controller/config.go: -------------------------------------------------------------------------------- 1 | package stream_srpc_client_controller 2 | 3 | import ( 4 | "github.com/aperturerobotics/bifrost/protocol" 5 | "github.com/aperturerobotics/controllerbus/config" 6 | ) 7 | 8 | // ConfigID is the config identifier. 9 | const ConfigID = ControllerID 10 | 11 | // GetConfigID returns the unique string for this configuration type. 12 | func (c *Config) GetConfigID() string { 13 | return ConfigID 14 | } 15 | 16 | // EqualsConfig checks if the config is equal to another. 17 | func (c *Config) EqualsConfig(other config.Config) bool { 18 | ot, ok := other.(*Config) 19 | if !ok { 20 | return false 21 | } 22 | return c.EqualVT(ot) 23 | } 24 | 25 | // Validate checks the config. 26 | func (c *Config) Validate() error { 27 | var pid protocol.ID = protocol.ID(c.GetProtocolId()) //nolint:staticcheck 28 | if err := pid.Validate(); err != nil { 29 | return protocol.ErrEmptyProtocolID 30 | } 31 | if err := c.GetClient().Validate(); err != nil { 32 | return err 33 | } 34 | return nil 35 | } 36 | 37 | // _ is a type assertion 38 | var _ config.Config = ((*Config)(nil)) 39 | -------------------------------------------------------------------------------- /stream/srpc/client/controller/config.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package stream.srpc.client.controller; 3 | 4 | import "github.com/aperturerobotics/bifrost/stream/srpc/client/client.proto"; 5 | 6 | // Config configures mounting a bifrost srpc RPC client to a bus. 7 | // Resolves the LookupRpcClient directive. 8 | message Config { 9 | // Client contains srpc.client configuration for the RPC client. 10 | .stream.srpc.client.Config client = 1; 11 | // ProtocolId is the protocol ID to use to contact the remote RPC service. 12 | // Must be set. 13 | string protocol_id = 2; 14 | // ServiceIdPrefixes are the service ID prefixes to match. 15 | // The prefix will be stripped from the service id before being passed to the client. 16 | // This is used like: LookupRpcClient -> my/service. 17 | // 18 | // If empty slice or empty string: matches all LookupRpcClient calls ignoring service ID. 19 | // Optional. 20 | repeated string service_id_prefixes = 3; 21 | } 22 | -------------------------------------------------------------------------------- /stream/srpc/server/resolver.go: -------------------------------------------------------------------------------- 1 | package stream_srpc_server 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/aperturerobotics/bifrost/link" 7 | "github.com/aperturerobotics/controllerbus/directive" 8 | ) 9 | 10 | // mountedStreamResolver resolves HandleMountedStream. 11 | type mountedStreamResolver struct { 12 | c *Server 13 | } 14 | 15 | func newMountedStreamResolver(c *Server) *mountedStreamResolver { 16 | return &mountedStreamResolver{c: c} 17 | } 18 | 19 | // Resolve resolves the values, emitting them to the handler. 20 | // The resolver may be canceled and restarted multiple times. 21 | // Any fatal error resolving the value is returned. 22 | // The resolver will not be retried after returning an error. 23 | // Values will be maintained from the previous call. 24 | func (r *mountedStreamResolver) Resolve( 25 | ctx context.Context, 26 | handler directive.ResolverHandler, 27 | ) error { 28 | var val link.MountedStreamHandler = r.c 29 | _, _ = handler.AddValue(val) 30 | return nil 31 | } 32 | 33 | // _ is a type assertion 34 | var _ directive.Resolver = ((*mountedStreamResolver)(nil)) 35 | -------------------------------------------------------------------------------- /stream/srpc/server/server.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package stream.srpc.server; 3 | 4 | // Config configures the server for the srpc service. 5 | message Config { 6 | // PeerIds are the list of peer IDs to listen on. 7 | // If empty, allows any incoming peer id w/ the protocol id(s). 8 | repeated string peer_ids = 1; 9 | // ProtocolIds is the list of protocol ids to listen on. 10 | // If empty, no incoming streams will be accepted. 11 | repeated string protocol_ids = 2; 12 | // DisableEstablishLink disables adding an EstablishLink directive for each incoming peer. 13 | bool disable_establish_link = 3; 14 | } 15 | -------------------------------------------------------------------------------- /stream/stream.go: -------------------------------------------------------------------------------- 1 | package stream 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // OpenOpts are optional arguments when opening a stream. 8 | type OpenOpts struct{} 9 | 10 | // Stream is a stream-based data channel between two peers over a link. 11 | type Stream interface { 12 | // Read data from the stream. 13 | Read(b []byte) (n int, err error) 14 | // Write data to the stream. 15 | Write(b []byte) (n int, err error) 16 | // SetReadDeadline sets the read deadline as defined by 17 | // A zero time value disables the deadline. 18 | SetReadDeadline(t time.Time) error 19 | // SetWriteDeadline sets the write deadline as defined by 20 | // A zero time value disables the deadline. 21 | SetWriteDeadline(t time.Time) error 22 | // SetDeadline sets both read and write deadlines as defined by 23 | // A zero time value disables the deadlines. 24 | SetDeadline(t time.Time) error 25 | // Close closes the stream. 26 | Close() error 27 | } 28 | -------------------------------------------------------------------------------- /tptaddr/controller/config.go: -------------------------------------------------------------------------------- 1 | package tptaddr_controller 2 | 3 | import ( 4 | "github.com/aperturerobotics/controllerbus/config" 5 | ) 6 | 7 | // ConfigID is the string used to identify this config object. 8 | const ConfigID = ControllerID 9 | 10 | // Validate validates the configuration. 11 | func (c *Config) Validate() error { 12 | return nil 13 | } 14 | 15 | // GetConfigID returns the unique string for this configuration type. 16 | func (c *Config) GetConfigID() string { 17 | return ConfigID 18 | } 19 | 20 | // EqualsConfig checks if the config is equal to another. 21 | func (c *Config) EqualsConfig(c2 config.Config) bool { 22 | return config.EqualsConfig[*Config](c, c2) 23 | } 24 | 25 | var _ config.Config = ((*Config)(nil)) 26 | -------------------------------------------------------------------------------- /tptaddr/controller/config.pb.ts: -------------------------------------------------------------------------------- 1 | // @generated by protoc-gen-es-lite unknown with parameter "target=ts,ts_nocheck=false" 2 | // @generated from file github.com/aperturerobotics/bifrost/tptaddr/controller/config.proto (package tptaddr.controller, syntax proto3) 3 | /* eslint-disable */ 4 | 5 | import { Backoff } from '@go/github.com/aperturerobotics/util/backoff/backoff.pb.js' 6 | import type { MessageType, PartialFieldInfo } from '@aptre/protobuf-es-lite' 7 | import { createMessageType } from '@aptre/protobuf-es-lite' 8 | 9 | export const protobufPackage = 'tptaddr.controller' 10 | 11 | /** 12 | * Config configures the tptaddr dialer controller. 13 | * 14 | * Handles EstablishLinkWithPeer directives by creating LookupTptAddr and 15 | * DialTptAddr directives. 16 | * 17 | * @generated from message tptaddr.controller.Config 18 | */ 19 | export interface Config { 20 | /** 21 | * DialBackoff is the dial backoff configuration. 22 | * If unset, defaults to reasonable defaults. 23 | * 24 | * @generated from field: backoff.Backoff dial_backoff = 1; 25 | */ 26 | dialBackoff?: Backoff 27 | } 28 | 29 | // Config contains the message type declaration for Config. 30 | export const Config: MessageType = createMessageType({ 31 | typeName: 'tptaddr.controller.Config', 32 | fields: [ 33 | { no: 1, name: 'dial_backoff', kind: 'message', T: () => Backoff }, 34 | ] as readonly PartialFieldInfo[], 35 | packedByDefault: true, 36 | }) 37 | -------------------------------------------------------------------------------- /tptaddr/controller/config.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package tptaddr.controller; 3 | 4 | import "github.com/aperturerobotics/util/backoff/backoff.proto"; 5 | 6 | // Config configures the tptaddr dialer controller. 7 | // 8 | // Handles EstablishLinkWithPeer directives by creating LookupTptAddr and 9 | // DialTptAddr directives. 10 | message Config { 11 | // DialBackoff is the dial backoff configuration. 12 | // If unset, defaults to reasonable defaults. 13 | .backoff.Backoff dial_backoff = 1; 14 | } 15 | -------------------------------------------------------------------------------- /tptaddr/errors.go: -------------------------------------------------------------------------------- 1 | package tptaddr 2 | 3 | import "errors" 4 | 5 | // ErrInvalidTptAddr is returned if the tptaddr was invalid. 6 | var ErrInvalidTptAddr = errors.New("invalid transport address: expected transport-type-id|addr") 7 | -------------------------------------------------------------------------------- /tptaddr/static/config.go: -------------------------------------------------------------------------------- 1 | package tptaddr_static 2 | 3 | import ( 4 | "github.com/aperturerobotics/controllerbus/config" 5 | "github.com/pkg/errors" 6 | ) 7 | 8 | // ConfigID is the string used to identify this config object. 9 | const ConfigID = ControllerID 10 | 11 | // Validate validates the configuration. 12 | func (c *Config) Validate() error { 13 | _, errs := ParsePeerAddressMap(c.GetAddresses()) 14 | if len(errs) != 0 { 15 | return errors.Wrap(errs[0], "addresses") 16 | } 17 | 18 | return nil 19 | } 20 | 21 | // GetConfigID returns the unique string for this configuration type. 22 | func (c *Config) GetConfigID() string { 23 | return ConfigID 24 | } 25 | 26 | // EqualsConfig checks if the config is equal to another. 27 | func (c *Config) EqualsConfig(other config.Config) bool { 28 | ot, ok := other.(*Config) 29 | if !ok { 30 | return false 31 | } 32 | 33 | return ot.EqualVT(c) 34 | } 35 | 36 | var _ config.Config = ((*Config)(nil)) 37 | -------------------------------------------------------------------------------- /tptaddr/static/factory.go: -------------------------------------------------------------------------------- 1 | package tptaddr_static 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/aperturerobotics/controllerbus/bus" 7 | "github.com/aperturerobotics/controllerbus/config" 8 | "github.com/aperturerobotics/controllerbus/controller" 9 | "github.com/blang/semver/v4" 10 | ) 11 | 12 | // Factory constructs the controller. 13 | type Factory struct { 14 | // bus is the controller bus 15 | bus bus.Bus 16 | } 17 | 18 | // NewFactory builds a factory. 19 | func NewFactory(bus bus.Bus) *Factory { 20 | return &Factory{bus: bus} 21 | } 22 | 23 | // GetConfigID returns the configuration ID for the controller. 24 | func (t *Factory) GetConfigID() string { 25 | return ConfigID 26 | } 27 | 28 | // GetControllerID returns the unique ID for the controller. 29 | func (t *Factory) GetControllerID() string { 30 | return ControllerID 31 | } 32 | 33 | // ConstructConfig constructs an instance of the controller configuration. 34 | func (t *Factory) ConstructConfig() config.Config { 35 | return &Config{} 36 | } 37 | 38 | // Construct constructs the associated controller given configuration. 39 | func (t *Factory) Construct( 40 | ctx context.Context, 41 | conf config.Config, 42 | opts controller.ConstructOpts, 43 | ) (controller.Controller, error) { 44 | cc := conf.(*Config) 45 | 46 | // Construct the controller. 47 | return NewController(cc) 48 | } 49 | 50 | // GetVersion returns the version of this controller. 51 | func (t *Factory) GetVersion() semver.Version { 52 | return Version 53 | } 54 | 55 | // _ is a type assertion 56 | var _ controller.Factory = ((*Factory)(nil)) 57 | -------------------------------------------------------------------------------- /tptaddr/static/static.pb.ts: -------------------------------------------------------------------------------- 1 | // @generated by protoc-gen-es-lite unknown with parameter "target=ts,ts_nocheck=false" 2 | // @generated from file github.com/aperturerobotics/bifrost/tptaddr/static/static.proto (package tptaddr.static, syntax proto3) 3 | /* eslint-disable */ 4 | 5 | import type { MessageType, PartialFieldInfo } from '@aptre/protobuf-es-lite' 6 | import { createMessageType, ScalarType } from '@aptre/protobuf-es-lite' 7 | 8 | export const protobufPackage = 'tptaddr.static' 9 | 10 | /** 11 | * Config configures the static controller. 12 | * 13 | * Handles LookupTptAddr directives with a static list of addresses. 14 | * 15 | * @generated from message tptaddr.static.Config 16 | */ 17 | export interface Config { 18 | /** 19 | * Addresses is the mapping of peer id to address list. 20 | * 21 | * Format: {peer-id}|{transport-id}|{address} 22 | * Anything after the second | is treated as part of the address. 23 | * 24 | * @generated from field: repeated string addresses = 1; 25 | */ 26 | addresses?: string[] 27 | } 28 | 29 | // Config contains the message type declaration for Config. 30 | export const Config: MessageType = createMessageType({ 31 | typeName: 'tptaddr.static.Config', 32 | fields: [ 33 | { 34 | no: 1, 35 | name: 'addresses', 36 | kind: 'scalar', 37 | T: ScalarType.STRING, 38 | repeated: true, 39 | }, 40 | ] as readonly PartialFieldInfo[], 41 | packedByDefault: true, 42 | }) 43 | -------------------------------------------------------------------------------- /tptaddr/static/static.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package tptaddr.static; 3 | 4 | // Config configures the static controller. 5 | // 6 | // Handles LookupTptAddr directives with a static list of addresses. 7 | message Config { 8 | // Addresses is the mapping of peer id to address list. 9 | // 10 | // Format: {peer-id}|{transport-id}|{address} 11 | // Anything after the second | is treated as part of the address. 12 | repeated string addresses = 1; 13 | } 14 | -------------------------------------------------------------------------------- /tptaddr/tptaddr.go: -------------------------------------------------------------------------------- 1 | package tptaddr 2 | 3 | import "strings" 4 | 5 | // TptAddrDelimiter is the delimiter for tpt addr sections. 6 | const TptAddrDelimiter = '|' 7 | 8 | // ParseTptAddr parses a transport address string. 9 | func ParseTptAddr(tptAddr string) (transportID, addr string, err error) { 10 | var found bool 11 | transportID, addr, found = strings.Cut(tptAddr, string([]rune{TptAddrDelimiter})) 12 | if !found || len(transportID) == 0 || len(addr) == 0 { 13 | return "", "", ErrInvalidTptAddr 14 | } 15 | return 16 | } 17 | -------------------------------------------------------------------------------- /transport/common/conn/conn.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package conn; 3 | 4 | import "github.com/aperturerobotics/bifrost/transport/common/quic/quic.proto"; 5 | 6 | // Opts are extra options for the conn. 7 | message Opts { 8 | // Quic are the quic protocol options. 9 | .transport.quic.Opts quic = 1; 10 | // Verbose turns on verbose debug logging. 11 | bool verbose = 2; 12 | // Mtu sets the maximum size for a single packet. 13 | // Defaults to 65000. 14 | uint32 mtu = 3; 15 | // BufSize is the number of packets to buffer. 16 | // 17 | // Total memory cap is mtu * bufSize. 18 | // Defaults to 10. 19 | uint32 buf_size = 4; 20 | } 21 | -------------------------------------------------------------------------------- /transport/common/conn/link.go: -------------------------------------------------------------------------------- 1 | package conn 2 | 3 | import transport_quic "github.com/aperturerobotics/bifrost/transport/common/quic" 4 | 5 | // Link is a Quic-based connection/link backed by a Conn. 6 | type Link = transport_quic.Link 7 | -------------------------------------------------------------------------------- /transport/common/dialer/dialer.pb.ts: -------------------------------------------------------------------------------- 1 | // @generated by protoc-gen-es-lite unknown with parameter "target=ts,ts_nocheck=false" 2 | // @generated from file github.com/aperturerobotics/bifrost/transport/common/dialer/dialer.proto (package dialer, syntax proto3) 3 | /* eslint-disable */ 4 | 5 | import { Backoff } from '@go/github.com/aperturerobotics/util/backoff/backoff.pb.js' 6 | import type { MessageType, PartialFieldInfo } from '@aptre/protobuf-es-lite' 7 | import { createMessageType, ScalarType } from '@aptre/protobuf-es-lite' 8 | 9 | export const protobufPackage = 'dialer' 10 | 11 | /** 12 | * DialerOpts contains options relating to dialing a statically configured peer. 13 | * 14 | * @generated from message dialer.DialerOpts 15 | */ 16 | export interface DialerOpts { 17 | /** 18 | * Address is the address of the peer, in the format expected by the transport. 19 | * 20 | * @generated from field: string address = 1; 21 | */ 22 | address?: string 23 | /** 24 | * Backoff is the dialing backoff configuration. 25 | * Can be empty. 26 | * 27 | * @generated from field: backoff.Backoff backoff = 2; 28 | */ 29 | backoff?: Backoff 30 | } 31 | 32 | // DialerOpts contains the message type declaration for DialerOpts. 33 | export const DialerOpts: MessageType = createMessageType({ 34 | typeName: 'dialer.DialerOpts', 35 | fields: [ 36 | { no: 1, name: 'address', kind: 'scalar', T: ScalarType.STRING }, 37 | { no: 2, name: 'backoff', kind: 'message', T: () => Backoff }, 38 | ] as readonly PartialFieldInfo[], 39 | packedByDefault: true, 40 | }) 41 | -------------------------------------------------------------------------------- /transport/common/dialer/dialer.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package dialer; 3 | 4 | import "github.com/aperturerobotics/util/backoff/backoff.proto"; 5 | 6 | // DialerOpts contains options relating to dialing a statically configured peer. 7 | message DialerOpts { 8 | // Address is the address of the peer, in the format expected by the transport. 9 | string address = 1; 10 | // Backoff is the dialing backoff configuration. 11 | // Can be empty. 12 | .backoff.Backoff backoff = 2; 13 | } 14 | 15 | -------------------------------------------------------------------------------- /transport/common/dialer/errors.go: -------------------------------------------------------------------------------- 1 | package dialer 2 | 3 | import "errors" 4 | 5 | var ( 6 | // ErrNotTransportDialer is returned if a Transport does not implement TransportDialer. 7 | ErrNotTransportDialer = errors.New("transport does not implement a dialer") 8 | // ErrEmptyAddress is returned if the address field was empty. 9 | ErrEmptyAddress = errors.New("dialer opts address cannot be empty") 10 | ) 11 | -------------------------------------------------------------------------------- /transport/common/dialer/opts.go: -------------------------------------------------------------------------------- 1 | package dialer 2 | 3 | // Validate validates the dialer options. 4 | func (o *DialerOpts) Validate() error { 5 | if o.GetAddress() == "" { 6 | return ErrEmptyAddress 7 | } 8 | return nil 9 | } 10 | -------------------------------------------------------------------------------- /transport/common/dialer/transport.go: -------------------------------------------------------------------------------- 1 | package dialer 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/aperturerobotics/bifrost/link" 7 | "github.com/aperturerobotics/bifrost/peer" 8 | ) 9 | 10 | // TransportDialer is a transport that supports dialing string-serialized remote 11 | // addresses. The Transport controller will call Dial if provided an address for 12 | // the transport, and directed to connect to the peer. 13 | type TransportDialer interface { 14 | // MatchTransportType checks if the given transport type ID matches this transport. 15 | // If returns true, the transport controller will call DialPeer with that tptaddr. 16 | // E.x.: "udp-quic" or "ws" 17 | MatchTransportType(transportType string) bool 18 | 19 | // GetPeerDialer returns the dialing information for a peer. 20 | // Called when resolving EstablishLink. 21 | // Return nil, nil to indicate not found or unavailable. 22 | GetPeerDialer( 23 | ctx context.Context, 24 | peerID peer.ID, 25 | ) (*DialerOpts, error) 26 | 27 | // DialPeer dials a peer given an address. The yielded link should be 28 | // emitted to the transport handler. DialPeer should return nil if the link 29 | // was established. DialPeer will then not be called again for the same peer 30 | // ID and address tuple until the yielded link is lost. 31 | // 32 | // ctx will be canceled once DialPeer returns. use ctx from Execute for long-lived routines. 33 | // 34 | // Returns fatal and error. 35 | DialPeer( 36 | ctx context.Context, 37 | peerID peer.ID, 38 | addr string, 39 | ) (lnk link.Link, fatal bool, err error) 40 | } 41 | -------------------------------------------------------------------------------- /transport/common/pconn/link.go: -------------------------------------------------------------------------------- 1 | package pconn 2 | 3 | import transport_quic "github.com/aperturerobotics/bifrost/transport/common/quic" 4 | 5 | // Link is a Quic-based connection/link backed by a packet connection. 6 | type Link = transport_quic.Link 7 | -------------------------------------------------------------------------------- /transport/common/pconn/pconn.pb.ts: -------------------------------------------------------------------------------- 1 | // @generated by protoc-gen-es-lite unknown with parameter "target=ts,ts_nocheck=false" 2 | // @generated from file github.com/aperturerobotics/bifrost/transport/common/pconn/pconn.proto (package pconn, syntax proto3) 3 | /* eslint-disable */ 4 | 5 | import { Opts as Opts$1 } from '../quic/quic.pb.js' 6 | import type { MessageType, PartialFieldInfo } from '@aptre/protobuf-es-lite' 7 | import { createMessageType, ScalarType } from '@aptre/protobuf-es-lite' 8 | 9 | export const protobufPackage = 'pconn' 10 | 11 | /** 12 | * Opts are extra options for the packet conn. 13 | * 14 | * @generated from message pconn.Opts 15 | */ 16 | export interface Opts { 17 | /** 18 | * Quic are the quic protocol options. 19 | * 20 | * @generated from field: transport.quic.Opts quic = 1; 21 | */ 22 | quic?: Opts$1 23 | /** 24 | * Verbose turns on verbose debug logging. 25 | * 26 | * @generated from field: bool verbose = 2; 27 | */ 28 | verbose?: boolean 29 | } 30 | 31 | // Opts contains the message type declaration for Opts. 32 | export const Opts: MessageType = createMessageType({ 33 | typeName: 'pconn.Opts', 34 | fields: [ 35 | { no: 1, name: 'quic', kind: 'message', T: () => Opts$1 }, 36 | { no: 2, name: 'verbose', kind: 'scalar', T: ScalarType.BOOL }, 37 | ] as readonly PartialFieldInfo[], 38 | packedByDefault: true, 39 | }) 40 | -------------------------------------------------------------------------------- /transport/common/pconn/pconn.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package pconn; 3 | 4 | import "github.com/aperturerobotics/bifrost/transport/common/quic/quic.proto"; 5 | 6 | // Opts are extra options for the packet conn. 7 | message Opts { 8 | // Quic are the quic protocol options. 9 | .transport.quic.Opts quic = 1; 10 | // Verbose turns on verbose debug logging. 11 | bool verbose = 2; 12 | } 13 | -------------------------------------------------------------------------------- /transport/common/quic/errors.go: -------------------------------------------------------------------------------- 1 | package transport_quic 2 | 3 | import "errors" 4 | 5 | var ( 6 | // ErrDialUnimplemented is returned if dialing the peer is unimplemented. 7 | ErrDialUnimplemented = errors.New("dial peer not implemented") 8 | // ErrRemoteUnspecified is returned if the remote addr is unspecified. 9 | ErrRemoteUnspecified = errors.New("peer id and/or remote addr must be specified") 10 | ) 11 | -------------------------------------------------------------------------------- /transport/common/quic/quic.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package transport.quic; 3 | 4 | message Opts { 5 | // MaxIdleTimeoutDur is the duration of idle after which conn is closed. 6 | // 7 | // Defaults to 10s. 8 | string max_idle_timeout_dur = 1; 9 | // MaxIncomingStreams is the maximum number of concurrent bidirectional 10 | // streams that a peer is allowed to open. 11 | // 12 | // If unset or negative, defaults to 100000. 13 | int32 max_incoming_streams = 2; 14 | // DisableKeepAlive disables the keep alive packets. 15 | // 16 | bool disable_keep_alive = 3; 17 | // KeepAliveDur is the duration between keep-alive pings. 18 | // 19 | // If disable_keep_alive is set, this value is ignored. 20 | // If unset, sets keep-alive to half of MaxIdleTimeout. 21 | string keep_alive_dur = 7; 22 | // DisableDatagrams disables the unreliable datagrams feature. 23 | // Both peers must support it for it to be enabled, regardless of this flag. 24 | bool disable_datagrams = 4; 25 | // DisablePathMtuDiscovery disables sending packets to discover max packet size. 26 | bool disable_path_mtu_discovery = 5; 27 | // Verbose indicates to use verbose logging. 28 | // Note: this is VERY verbose, logs every packet sent. 29 | bool verbose = 6; 30 | } 31 | -------------------------------------------------------------------------------- /transport/common/quic/util.go: -------------------------------------------------------------------------------- 1 | package transport_quic 2 | 3 | import ( 4 | "github.com/aperturerobotics/bifrost/peer" 5 | "github.com/pkg/errors" 6 | ) 7 | 8 | // CheckAlreadyConnected checks if a address and peer id is already connected. 9 | func CheckAlreadyConnected(t *Transport, addr string, peerID peer.ID) (bool, error) { 10 | lnk, ok := t.LookupLinkWithAddr(addr) 11 | if !ok { 12 | return false, nil 13 | } 14 | lnkPeer := lnk.GetRemotePeer().String() 15 | desiredPeer := peerID.String() 16 | if lnkPeer != desiredPeer { 17 | return false, errors.Errorf( 18 | "already connected to %s with different peer id: %s != requested %s", 19 | addr, 20 | lnkPeer, 21 | desiredPeer, 22 | ) 23 | } 24 | return true, nil 25 | } 26 | -------------------------------------------------------------------------------- /transport/common/quic/uuid.go: -------------------------------------------------------------------------------- 1 | package transport_quic 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/aperturerobotics/bifrost/peer" 7 | "github.com/aperturerobotics/bifrost/util/scrc" 8 | ) 9 | 10 | // NewTransportUUID builds the UUID for a transport with a local address and peer id 11 | func NewTransportUUID(localAddr string, peerID peer.ID) uint64 { 12 | return scrc.Crc64( 13 | []byte("bifrost/quic/"), 14 | []byte(localAddr), 15 | []byte("/"), 16 | []byte(peerID.String()), 17 | ) 18 | } 19 | 20 | // NewLinkUUID builds the UUID for a link 21 | func NewLinkUUID(localAddr, remoteAddr net.Addr, peerID peer.ID) uint64 { 22 | return scrc.Crc64( 23 | []byte("quic"), 24 | []byte(localAddr.String()), 25 | []byte(remoteAddr.String()), 26 | []byte(peerID), 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /transport/config.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import "github.com/aperturerobotics/controllerbus/config" 4 | 5 | // Config contains common parameters all transport configs must have. 6 | type Config interface { 7 | // Config indicates this is a controllerbus config. 8 | config.Config 9 | // Debuggable indicates this config is debuggable. 10 | config.Debuggable 11 | // GetTransportPeerId returns the node peer ID constraint. 12 | GetTransportPeerId() string 13 | // SetTransportPeerId sets the node peer ID field. 14 | SetTransportPeerId(peerID string) 15 | } 16 | -------------------------------------------------------------------------------- /transport/controller/controller.pb.ts: -------------------------------------------------------------------------------- 1 | // @generated by protoc-gen-es-lite unknown with parameter "target=ts,ts_nocheck=false" 2 | // @generated from file github.com/aperturerobotics/bifrost/transport/controller/controller.proto (package transport.controller, syntax proto3) 3 | /* eslint-disable */ 4 | 5 | import type { MessageType, PartialFieldInfo } from '@aptre/protobuf-es-lite' 6 | import { createMessageType, ScalarType } from '@aptre/protobuf-es-lite' 7 | 8 | export const protobufPackage = 'transport.controller' 9 | 10 | /** 11 | * StreamEstablish is the first message sent by the initiator of a stream. 12 | * Prefixed by a uint32 length. 13 | * Max size: 100kb 14 | * 15 | * @generated from message transport.controller.StreamEstablish 16 | */ 17 | export interface StreamEstablish { 18 | /** 19 | * ProtocolID is the protocol identifier string for the stream. 20 | * 21 | * @generated from field: string protocol_id = 1; 22 | */ 23 | protocolId?: string 24 | } 25 | 26 | // StreamEstablish contains the message type declaration for StreamEstablish. 27 | export const StreamEstablish: MessageType = createMessageType({ 28 | typeName: 'transport.controller.StreamEstablish', 29 | fields: [ 30 | { no: 1, name: 'protocol_id', kind: 'scalar', T: ScalarType.STRING }, 31 | ] as readonly PartialFieldInfo[], 32 | packedByDefault: true, 33 | }) 34 | -------------------------------------------------------------------------------- /transport/controller/controller.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package transport.controller; 3 | 4 | // StreamEstablish is the first message sent by the initiator of a stream. 5 | // Prefixed by a uint32 length. 6 | // Max size: 100kb 7 | message StreamEstablish { 8 | // ProtocolID is the protocol identifier string for the stream. 9 | string protocol_id = 1; 10 | } -------------------------------------------------------------------------------- /transport/controller/establish-header_test.go: -------------------------------------------------------------------------------- 1 | package transport_controller 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | // TestReadStreamEstablishHeader tests reading a stream establish header. 9 | func TestReadStreamEstablishHeader(t *testing.T) { 10 | buf := &bytes.Buffer{} 11 | obj := &StreamEstablish{ProtocolId: "testing"} 12 | if _, err := writeStreamEstablishHeader(buf, obj); err != nil { 13 | t.Fatal(err.Error()) 14 | } 15 | readEst, err := readStreamEstablishHeader(buf) 16 | if err != nil { 17 | t.Fatal(err.Error()) 18 | } 19 | if readEst.ProtocolId != obj.ProtocolId { 20 | t.Fatalf("object decoded incorrectly: %s != %s", readEst.ProtocolId, obj.ProtocolId) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /transport/inproc/addr.go: -------------------------------------------------------------------------------- 1 | package inproc 2 | 3 | import ( 4 | "net" 5 | "strings" 6 | 7 | "github.com/aperturerobotics/bifrost/peer" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | var scheme = "inproc://" 12 | 13 | type Addr struct { 14 | peerID peer.ID 15 | str string 16 | } 17 | 18 | // NewAddr builds a new Addr 19 | func NewAddr(peerID peer.ID) *Addr { 20 | return &Addr{ 21 | peerID: peerID, 22 | str: scheme + peerID.String(), 23 | } 24 | } 25 | 26 | // ParseAddr parses an address. 27 | func ParseAddr(addr string) (net.Addr, error) { 28 | if !strings.HasPrefix(addr, scheme) { 29 | return nil, errors.Errorf("expected inproc prefix: %s", addr) 30 | } 31 | pid, err := peer.IDB58Decode(addr[len(scheme):]) 32 | if err != nil { 33 | return nil, err 34 | } 35 | return NewAddr(pid), nil 36 | } 37 | 38 | func (a *Addr) Network() string { 39 | return "inproc" 40 | } 41 | 42 | func (a *Addr) String() string { 43 | return a.str 44 | } 45 | 46 | // _ is a type assertion 47 | var _ net.Addr = ((*Addr)(nil)) 48 | -------------------------------------------------------------------------------- /transport/inproc/inproc.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package inproc; 3 | 4 | import "github.com/aperturerobotics/bifrost/transport/common/dialer/dialer.proto"; 5 | import "github.com/aperturerobotics/bifrost/transport/common/pconn/pconn.proto"; 6 | 7 | // Config is the configuration for the inproc testing transport. 8 | message Config { 9 | // TransportPeerID sets the peer ID to attach the transport to. 10 | // If unset, attaches to any running peer with a private key. 11 | string transport_peer_id = 1; 12 | // PacketOpts are options to set on the packet connection. 13 | pconn.Opts packet_opts = 2; 14 | // Dialers maps peer IDs to dialers. 15 | map dialers = 3; 16 | } 17 | 18 | -------------------------------------------------------------------------------- /transport/transport.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/aperturerobotics/bifrost/link" 7 | "github.com/aperturerobotics/bifrost/peer" 8 | "github.com/aperturerobotics/controllerbus/controller" 9 | ) 10 | 11 | // Transport is similar to a NIC, yielding links to remote peers. 12 | type Transport interface { 13 | // Execute executes the transport as configured, returning any fatal error. 14 | Execute(ctx context.Context) error 15 | 16 | // GetUUID returns a host-unique ID for this transport. 17 | GetUUID() uint64 18 | // GetPeerID returns the peer ID. 19 | GetPeerID() peer.ID 20 | 21 | // Close closes the transport, returning any errors closing. 22 | Close() error 23 | } 24 | 25 | // TransportHandler manages a Transport and receives event callbacks. 26 | // This is typically fulfilled by the transport controller. 27 | type TransportHandler interface { 28 | // HandleLinkEstablished is called when a link is established. 29 | HandleLinkEstablished(lnk link.Link) 30 | // HandleLinkLost is called when a link is lost. 31 | HandleLinkLost(lnk link.Link) 32 | } 33 | 34 | // Controller is a transport controller. 35 | type Controller interface { 36 | // Controller is the controllerbus controller interface. 37 | controller.Controller 38 | 39 | // GetTransport returns the controlled transport. 40 | // This may wait for the controller to be ready. 41 | GetTransport(ctx context.Context) (Transport, error) 42 | } 43 | -------------------------------------------------------------------------------- /transport/udp/link.go: -------------------------------------------------------------------------------- 1 | package udp 2 | 3 | import ( 4 | "github.com/aperturerobotics/bifrost/transport/common/pconn" 5 | ) 6 | 7 | // Link represents a UDP-based connection/link. 8 | type Link = pconn.Link 9 | -------------------------------------------------------------------------------- /transport/udp/udp.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package udp; 3 | 4 | import "github.com/aperturerobotics/bifrost/transport/common/pconn/pconn.proto"; 5 | import "github.com/aperturerobotics/bifrost/transport/common/dialer/dialer.proto"; 6 | 7 | // Config is the configuration for the udp transport. 8 | message Config { 9 | // TransportPeerID sets the peer ID to attach the transport to. 10 | // If unset, attaches to any running peer with a private key. 11 | string transport_peer_id = 1; 12 | // ListenAddr contains the address to listen on. 13 | // Has no effect in the browser. 14 | string listen_addr = 2; 15 | // PacketOpts are options to set on the packet connection. 16 | pconn.Opts packet_opts = 4; 17 | // Dialers maps peer IDs to dialers. 18 | map dialers = 5; 19 | } -------------------------------------------------------------------------------- /transport/webrtc/link.go: -------------------------------------------------------------------------------- 1 | package webrtc 2 | 3 | import ( 4 | "github.com/aperturerobotics/bifrost/transport/common/conn" 5 | ) 6 | 7 | // Link represents a WebSocket-based connection/link. 8 | type Link = conn.Link 9 | -------------------------------------------------------------------------------- /transport/webrtc/uuid.go: -------------------------------------------------------------------------------- 1 | package webrtc 2 | 3 | import ( 4 | "github.com/aperturerobotics/bifrost/peer" 5 | "github.com/aperturerobotics/bifrost/util/scrc" 6 | ) 7 | 8 | // NewTransportUUID builds the UUID for a transport with a local address and peer id 9 | func NewTransportUUID(transportType string, peerID peer.ID) uint64 { 10 | return scrc.Crc64( 11 | []byte(ControllerID), 12 | []byte("/"), 13 | []byte(transportType), 14 | []byte("/"), 15 | []byte(peerID.String()), 16 | ) 17 | } 18 | 19 | // NewLinkUUID builds the UUID for a link 20 | func NewLinkUUID(transportType, localPeerID, remotePeerID peer.ID) uint64 { 21 | return scrc.Crc64( 22 | []byte(ControllerID), 23 | []byte("/"), 24 | []byte(transportType), 25 | []byte("/"), 26 | []byte(localPeerID.String()), 27 | []byte("/"), 28 | []byte(remotePeerID.String()), 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /transport/websocket/http/http.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package websocket.http; 3 | 4 | import "github.com/aperturerobotics/bifrost/transport/common/quic/quic.proto"; 5 | import "github.com/aperturerobotics/bifrost/transport/common/dialer/dialer.proto"; 6 | 7 | // Config is the configuration for the Websocket HTTP handler transport. 8 | // 9 | // Listen for incoming connections with bifrost/http/listener 10 | // This controller resolves LookupHTTPHandler directives filtering by ServeMux patterns. 11 | // Example: ["GET example.com/my/ws", "GET /other/path"] 12 | message Config { 13 | // TransportPeerID sets the peer ID to attach the transport to. 14 | // If unset, attaches to any running peer with a private key. 15 | string transport_peer_id = 1; 16 | // HttpPatterns is the list of patterns to listen on. 17 | // Example: ["GET example.com/my/ws", "GET /other/path"] 18 | repeated string http_patterns = 2; 19 | // PeerHttpPatterns is the list of patterns to serve the peer ID on. 20 | // Example: ["GET example.com/my/ws/peer-id", "GET /other/path/peer-id"] 21 | repeated string peer_http_patterns = 3; 22 | // Quic contains the quic protocol options. 23 | // 24 | // The WebSocket transport always disables FEC and several other UDP-centric 25 | // features which are unnecessary due to the "reliable" nature of WebSockets. 26 | .transport.quic.Opts quic = 4; 27 | // Dialers maps peer IDs to dialers. 28 | map dialers = 5; 29 | } 30 | -------------------------------------------------------------------------------- /transport/websocket/link.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | import ( 4 | "github.com/aperturerobotics/bifrost/transport/common/conn" 5 | ) 6 | 7 | // Link represents a WebSocket-based connection/link. 8 | type Link = conn.Link 9 | -------------------------------------------------------------------------------- /transport/websocket/websocket.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package websocket; 3 | 4 | import "github.com/aperturerobotics/bifrost/transport/common/quic/quic.proto"; 5 | import "github.com/aperturerobotics/bifrost/transport/common/dialer/dialer.proto"; 6 | 7 | // Config is the configuration for the Websocket transport. 8 | // 9 | // Quic is used for mTLS mutual authentication over the Websocket, as well as 10 | // congestion control, stream multiplexing, and other performance features. 11 | message Config { 12 | // TransportPeerID sets the peer ID to attach the transport to. 13 | // If unset, attaches to any running peer with a private key. 14 | string transport_peer_id = 1; 15 | // ListenAddr contains the address to listen on. 16 | // Has no effect in the browser. 17 | string listen_addr = 2; 18 | // Quic contains the quic protocol options. 19 | // 20 | // The WebSocket transport always disables FEC and several other UDP-centric 21 | // features which are unnecessary due to the "reliable" nature of WebSockets. 22 | .transport.quic.Opts quic = 3; 23 | // Dialers maps peer IDs to dialers. 24 | map dialers = 4; 25 | // HttpPath is the http path to expose the websocket. 26 | // If unset, ignores the incoming request path. 27 | string http_path = 5; 28 | // DisableServePeerId disables serving the peer id. 29 | // If this is unset the peer ID is available at http_path+"/peer" 30 | // If http_path is unset the peer ID is available at /peer 31 | bool disable_serve_peer_id = 6; 32 | } 33 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "jsx": "react", 7 | "baseUrl": "./", 8 | "paths": { 9 | "@go/*": ["vendor/*"] 10 | }, 11 | "allowSyntheticDefaultImports": true, 12 | "declaration": true, 13 | "esModuleInterop": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "importsNotUsedAsValues": "remove", 16 | "noEmit": true, 17 | "resolveJsonModule": true, 18 | "skipLibCheck": true, 19 | "strict": true, 20 | "lib": ["webworker", "dom"] 21 | }, 22 | "exclude": ["node_modules", "vendor", "dist"], 23 | "ts-node": { 24 | "esm": true, 25 | "experimentalSpecifierResolution": true 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /util/confparse/duration.go: -------------------------------------------------------------------------------- 1 | package confparse 2 | 3 | import "time" 4 | 5 | // ParseDuration parses a duration or returns 0, nil if empty. 6 | func ParseDuration(dur string) (time.Duration, error) { 7 | if dur == "" { 8 | return 0, nil 9 | } 10 | return time.ParseDuration(dur) 11 | } 12 | 13 | // MarshalDuration marshals a duration to a string. 14 | func MarshalDuration(dur time.Duration, ignoreEmpty bool) string { 15 | if dur == 0 && !ignoreEmpty { 16 | return "" 17 | } 18 | return dur.String() 19 | } 20 | -------------------------------------------------------------------------------- /util/confparse/pem_test.go: -------------------------------------------------------------------------------- 1 | package confparse 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aperturerobotics/bifrost/keypem" 7 | "github.com/libp2p/go-libp2p/core/crypto" 8 | ) 9 | 10 | // TestParseKeysPEM tests parsing public and private key pems. 11 | func TestParseKeysPEM(t *testing.T) { 12 | testKeyTypes(t, testParseKeysPEM) 13 | } 14 | 15 | func testParseKeysPEM(t *testing.T, keyPriv crypto.PrivKey, keyPub crypto.PubKey) { 16 | privPEM, err := keypem.MarshalPrivKeyPem(keyPriv) 17 | if err != nil { 18 | t.Fatal(err.Error()) 19 | } 20 | t.Logf("private key pem: %s", string(privPEM)) 21 | 22 | pubPEM, err := keypem.MarshalPubKeyPem(keyPub) 23 | if err != nil { 24 | t.Fatal(err.Error()) 25 | } 26 | t.Logf("public key pem: %s", string(pubPEM)) 27 | 28 | // parse 29 | privOut, err := ParsePrivateKey(string(privPEM)) 30 | if err != nil { 31 | t.Fatal(err.Error()) 32 | } 33 | if !privOut.Equals(keyPriv) { 34 | t.Fail() 35 | } 36 | 37 | // parse 38 | pubOut, err := ParsePublicKey(string(pubPEM)) 39 | if err != nil { 40 | t.Fatal(err.Error()) 41 | } 42 | if !pubOut.Equals(keyPub) { 43 | t.Fail() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /util/confparse/regexp.go: -------------------------------------------------------------------------------- 1 | package confparse 2 | 3 | import "regexp" 4 | 5 | // ParseRegexp parses a regular expression. 6 | // If the field is empty, returns nil, nil. 7 | func ParseRegexp(re string) (*regexp.Regexp, error) { 8 | if re == "" { 9 | return nil, nil 10 | } 11 | return regexp.Compile(re) 12 | } 13 | -------------------------------------------------------------------------------- /util/confparse/regexp_test.go: -------------------------------------------------------------------------------- 1 | package confparse 2 | 3 | import "testing" 4 | 5 | // TestParseRegexp tests parsing with the regexp package. 6 | func TestParseRegexp(t *testing.T) { 7 | re, err := ParseRegexp("testing .*") 8 | if err != nil { 9 | t.Fatal(err.Error()) 10 | } 11 | if re == nil { 12 | t.Fail() 13 | } 14 | if !re.MatchString("testing 1234") { 15 | t.Fail() 16 | } 17 | if re.MatchString("foo bar") { 18 | t.Fail() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /util/confparse/timestamp.go: -------------------------------------------------------------------------------- 1 | package confparse 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | 7 | "github.com/aperturerobotics/protobuf-go-lite/types/known/timestamppb" 8 | ) 9 | 10 | // ParseTimestamp parses a timestamp string. 11 | // 12 | // The string can be either a unix time milliseconds or RFC3339 timestamp. 13 | // Returns nil, nil if empty. 14 | func ParseTimestamp(timestampStr string) (*timestamppb.Timestamp, error) { 15 | if timestampStr == "" { 16 | return nil, nil 17 | } 18 | ts := ×tamppb.Timestamp{} 19 | jdat := []byte(strconv.Quote(timestampStr)) 20 | if err := ts.UnmarshalJSON(jdat); err != nil { 21 | ts.Reset() 22 | if err := ts.UnmarshalJSON([]byte(timestampStr)); err != nil { 23 | return nil, err 24 | } 25 | } 26 | return ts, nil 27 | } 28 | 29 | // MarshalTimestamp marshals a timestamp to a RFC3339 string. 30 | // This format is also supported by proto3. 31 | func MarshalTimestamp(ts *timestamppb.Timestamp) string { 32 | if ts == nil { 33 | return "" 34 | } 35 | return ts.AsTime().Format(time.RFC3339) 36 | } 37 | -------------------------------------------------------------------------------- /util/confparse/timestamp_test.go: -------------------------------------------------------------------------------- 1 | package confparse 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aperturerobotics/protobuf-go-lite/types/known/timestamppb" 7 | ) 8 | 9 | func TestParseTimestamp(t *testing.T) { 10 | tests := []struct { 11 | name string 12 | input string 13 | expected *timestamppb.Timestamp 14 | expectError bool 15 | }{ 16 | { 17 | name: "EmptyString", 18 | input: "", 19 | expected: nil, 20 | expectError: false, 21 | }, 22 | { 23 | name: "ValidUnixMilliseconds", 24 | input: "1629048153000", 25 | expected: ×tamppb.Timestamp{ 26 | Seconds: 1629048153, 27 | }, 28 | expectError: false, 29 | }, 30 | { 31 | name: "ValidRFC3339", 32 | input: "2021-08-15T15:49:13Z", 33 | expected: ×tamppb.Timestamp{ 34 | Seconds: 1629042553, 35 | }, 36 | expectError: false, 37 | }, 38 | { 39 | name: "InvalidInput", 40 | input: "invalid", 41 | expected: nil, 42 | expectError: true, 43 | }, 44 | } 45 | 46 | for _, tt := range tests { 47 | t.Run(tt.name, func(t *testing.T) { 48 | actual, err := ParseTimestamp(tt.input) 49 | if tt.expectError { 50 | if err == nil { 51 | t.FailNow() 52 | } 53 | } else { 54 | if err != nil { 55 | t.Fatal(err.Error()) 56 | } 57 | } 58 | if !tt.expected.EqualVT(actual) { 59 | t.Fatalf("expected %q got %q", tt.expected, actual) 60 | } 61 | }) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /util/confparse/urls.go: -------------------------------------------------------------------------------- 1 | package confparse 2 | 3 | import ( 4 | "net/url" 5 | 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | // ParseURL parses the url from a string. 10 | // If there is no URL specified, returns nil, nil. 11 | func ParseURL(uri string) (*url.URL, error) { 12 | if uri == "" { 13 | return nil, nil 14 | } 15 | return url.Parse(uri) 16 | } 17 | 18 | // ValidateURL checks if a URL is set and valid. 19 | func ValidateURL(uri string, allowEmpty bool) error { 20 | url, err := ParseURL(uri) 21 | if err == nil && (url == nil && !allowEmpty) { 22 | err = errors.New("url cannot be empty") 23 | } 24 | return err 25 | } 26 | 27 | // ParseURLs parses a list of urls. 28 | // 29 | // Removes any empty values. 30 | func ParseURLs(urlStrs []string, allowEmpty bool) ([]*url.URL, error) { 31 | urls := make([]*url.URL, 0, len(urlStrs)) 32 | for i, urlStr := range urlStrs { 33 | v, err := ParseURL(urlStr) 34 | if v == nil && err == nil && !allowEmpty { 35 | err = errors.Wrapf(errors.New("empty url"), "urls[%d]", i) 36 | } 37 | if err != nil { 38 | return nil, errors.Wrapf(err, "urls[%d]", i) 39 | } 40 | if v != nil { 41 | urls = append(urls, v) 42 | } 43 | } 44 | return urls, nil 45 | } 46 | -------------------------------------------------------------------------------- /util/confparse/urls_test.go: -------------------------------------------------------------------------------- 1 | package confparse 2 | 3 | import ( 4 | "net/url" 5 | "testing" 6 | ) 7 | 8 | // TestURLs tests parsing URLs. 9 | func TestURLs(t *testing.T) { 10 | fatal := func(u *url.URL, err error) { 11 | if err != nil { 12 | t.Fatal(err.Error()) 13 | } 14 | if u == nil { 15 | t.Fail() 16 | } 17 | } 18 | 19 | fatal(ParseURL("https://test.com")) 20 | fatal(ParseURL("http://www.google.com")) 21 | 22 | u, err := ParseURL("") 23 | if err != nil { 24 | t.Fatal(err.Error()) 25 | } 26 | if u != nil { 27 | t.Fail() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /util/deadsync/deadsync.go: -------------------------------------------------------------------------------- 1 | package sync 2 | 3 | import ( 4 | ssync "sync" 5 | 6 | "github.com/sasha-s/go-deadlock" 7 | ) 8 | 9 | // Pool is the sync.Pool. 10 | type Pool = ssync.Pool 11 | 12 | // Mutex is the deadlock mutex. 13 | type Mutex = deadlock.Mutex 14 | 15 | // RWMutex is the deadlock mutex. 16 | type RWMutex = deadlock.RWMutex 17 | -------------------------------------------------------------------------------- /util/extra25519/extra25519.go: -------------------------------------------------------------------------------- 1 | package extra25519 2 | 3 | import ( 4 | "crypto/sha512" 5 | 6 | "filippo.io/edwards25519" 7 | "golang.org/x/crypto/ed25519" 8 | ) 9 | 10 | // PrivateKeyToCurve25519 converts an ed25519 private key into a corresponding 11 | // curve25519 private key such that the resulting curve25519 public key will 12 | // equal the result from PublicKeyToCurve25519. 13 | // 14 | // returns a 64-byte curve25519 scalar 15 | func PrivateKeyToCurve25519(privateKey ed25519.PrivateKey) []byte { 16 | h := sha512.New() 17 | h.Write(privateKey[:32]) 18 | digest := h.Sum(nil) 19 | 20 | digest[0] &= 248 21 | digest[31] &= 127 22 | digest[31] |= 64 23 | 24 | return digest 25 | } 26 | 27 | // PublicKeyToCurve25519 converts an Ed25519 public key into the curve25519 28 | // public key that would be generated from the same private key. 29 | // 30 | // returns a 32-byte curve25519 point and true if valid. 31 | // returns nil, false if not valid 32 | // some ed25519 public keys cannot / should not be converted to curve25519. 33 | func PublicKeyToCurve25519(edBytes ed25519.PublicKey) ([]byte, bool) { 34 | if IsEdLowOrder(edBytes) { 35 | return nil, false 36 | } 37 | 38 | edPoint, err := (&edwards25519.Point{}).SetBytes(edBytes) 39 | if err != nil { 40 | return nil, false 41 | } 42 | 43 | return edPoint.BytesMontgomery(), true 44 | } 45 | -------------------------------------------------------------------------------- /util/logrw/logr.go: -------------------------------------------------------------------------------- 1 | package logrw 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/sirupsen/logrus" 7 | ) 8 | 9 | // LogReader wraps a io.Reader with a logger. 10 | type LogReader struct { 11 | le *logrus.Entry 12 | underlying io.Reader 13 | } 14 | 15 | func NewLogReader(le *logrus.Entry, underlying io.Reader) *LogReader { 16 | return &LogReader{ 17 | le: le, 18 | underlying: underlying, 19 | } 20 | } 21 | 22 | // Read reads data from the connection. 23 | // Read can be made to time out and return an error after a fixed 24 | // time limit; see SetDeadline and SetReadDeadline. 25 | func (c *LogReader) Read(b []byte) (n int, err error) { 26 | n, err = c.underlying.Read(b) 27 | if err != nil && !(err == io.EOF && n != 0) { //nolint:staticcheck 28 | c.le.Warnf("read(...) => error %v", err.Error()) 29 | } else { 30 | c.le.Debugf("read(...) => %v", b[:n]) 31 | } 32 | return 33 | } 34 | 35 | // _ is a type assertion 36 | var _ io.Reader = ((*LogReader)(nil)) 37 | -------------------------------------------------------------------------------- /util/logrw/logrc.go: -------------------------------------------------------------------------------- 1 | package logrw 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/sirupsen/logrus" 7 | ) 8 | 9 | // LogReadCloser wraps a io.ReaderCloser with a logger. 10 | type LogReadCloser struct { 11 | *LogReader 12 | underlying io.Closer 13 | } 14 | 15 | func NewLogReadCloser(le *logrus.Entry, underlying io.ReadCloser) *LogReadCloser { 16 | return &LogReadCloser{ 17 | LogReader: NewLogReader(le, underlying), 18 | underlying: underlying, 19 | } 20 | } 21 | 22 | func (c *LogReadCloser) Close() error { 23 | err := c.underlying.Close() 24 | if err != nil { 25 | c.le.Warnf("close() => error %v", err.Error()) 26 | } else { 27 | c.le.Debug("close()") 28 | } 29 | return err 30 | } 31 | 32 | // _ is a type assertion 33 | var _ io.ReadCloser = ((*LogReadCloser)(nil)) 34 | -------------------------------------------------------------------------------- /util/logrw/logw.go: -------------------------------------------------------------------------------- 1 | package logrw 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/sirupsen/logrus" 7 | ) 8 | 9 | // LogWriter wraps a io.Writer with a logger. 10 | type LogWriter struct { 11 | le *logrus.Entry 12 | underlying io.Writer 13 | } 14 | 15 | func NewLogWriter(le *logrus.Entry, underlying io.Writer) *LogWriter { 16 | return &LogWriter{ 17 | le: le, 18 | underlying: underlying, 19 | } 20 | } 21 | 22 | // Write writes data to the connection. 23 | // Write can be made to time out and return an error after a fixed 24 | // time limit; see SetDeadline and SetWriteDeadline. 25 | func (c *LogWriter) Write(b []byte) (n int, err error) { 26 | c.le.Debugf("write(%d): %v", len(b), b) 27 | n, err = c.underlying.Write(b) 28 | if err != nil { 29 | c.le.Warnf("write(%d) => errored %v", len(b), err.Error()) 30 | } 31 | return 32 | } 33 | 34 | // _ is a type assertion 35 | var _ io.Writer = ((*LogWriter)(nil)) 36 | -------------------------------------------------------------------------------- /util/logrw/logwc.go: -------------------------------------------------------------------------------- 1 | package logrw 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/sirupsen/logrus" 7 | ) 8 | 9 | // LogWriteCloser wraps a io.WriterCloser with a logger. 10 | type LogWriteCloser struct { 11 | *LogWriter 12 | underlying io.Closer 13 | } 14 | 15 | func NewLogWriteCloser(le *logrus.Entry, underlying io.WriteCloser) *LogWriteCloser { 16 | return &LogWriteCloser{ 17 | LogWriter: NewLogWriter(le, underlying), 18 | underlying: underlying, 19 | } 20 | } 21 | 22 | func (c *LogWriteCloser) Close() error { 23 | err := c.underlying.Close() 24 | if err != nil { 25 | c.le.Warnf("close() => error %v", err.Error()) 26 | } else { 27 | c.le.Debug("close()") 28 | } 29 | return err 30 | } 31 | 32 | // _ is a type assertion 33 | var _ io.WriteCloser = ((*LogWriteCloser)(nil)) 34 | -------------------------------------------------------------------------------- /util/randstring/random-id.go: -------------------------------------------------------------------------------- 1 | package randstring 2 | 3 | import "strings" 4 | 5 | // RandomIdentifier generates a random string identifier. 6 | func RandomIdentifier(idLen int) string { 7 | if idLen == 0 { 8 | idLen = 8 9 | } 10 | return strings.ToLower(RandString(nil, idLen)) 11 | } 12 | -------------------------------------------------------------------------------- /util/randstring/randstring.go: -------------------------------------------------------------------------------- 1 | package randstring 2 | 3 | import ( 4 | "crypto/rand" 5 | mrand "math/rand/v2" 6 | "strings" 7 | ) 8 | 9 | const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 10 | const ( 11 | letterIdxBits = 6 // 6 bits to represent a letter index 12 | letterIdxMask = 1<= 0; { 31 | if remain == 0 { 32 | cache, remain = src.Int64(), letterIdxMax 33 | } 34 | if idx := int(cache & letterIdxMask); idx < len(letterBytes) { 35 | sb.WriteByte(letterBytes[idx]) 36 | i-- 37 | } 38 | cache >>= letterIdxBits 39 | remain-- 40 | } 41 | 42 | return sb.String() 43 | } 44 | -------------------------------------------------------------------------------- /util/randstring/randstring_test.go: -------------------------------------------------------------------------------- 1 | package randstring 2 | 3 | import ( 4 | "math/rand/v2" 5 | "slices" 6 | "testing" 7 | 8 | "github.com/aperturerobotics/util/prng" 9 | ) 10 | 11 | func TestRandString(t *testing.T) { 12 | rnd := rand.New(prng.BuildSeededRand([]byte("testing randstring"))) //nolint:gosec 13 | strs := make([]string, 10) 14 | for i := range strs { 15 | strs[i] = RandString(rnd, 8) 16 | } 17 | expected := []string{"EaTsEKPw", "nvvjMxQe", "JcnevYoi", "qhjIFzMl", "nIhGmfAT", "WCMJUZhe", "WAqmjYbL", "CgYZxfxR", "KphaTnzC", "iZDnLFYn"} 18 | if !slices.Equal(strs, expected) { 19 | t.Logf("expected: %#v", expected) 20 | t.Logf("actual: %#v", strs) 21 | t.FailNow() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /util/rwc/conn_test.go: -------------------------------------------------------------------------------- 1 | package rwc 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "testing" 7 | 8 | "github.com/aperturerobotics/bifrost/peer" 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | // TestConn tests the conn. 13 | func TestConn(t *testing.T) { 14 | ctx := context.Background() 15 | 16 | peer1, err := peer.NewPeer(nil) 17 | if err != nil { 18 | t.Fatal(err.Error()) 19 | } 20 | 21 | peer2, err := peer.NewPeer(nil) 22 | if err != nil { 23 | t.Fatal(err.Error()) 24 | } 25 | 26 | a1, a2 := peer.NewNetAddr(peer1.GetPeerID()), peer.NewNetAddr(peer2.GetPeerID()) 27 | c1, c2 := net.Pipe() 28 | 29 | pc1 := NewConn(ctx, c1, a1, a2, 10) 30 | pc2 := NewConn(ctx, c2, a2, a1, 10) 31 | 32 | data := []byte("testing 1234") 33 | n, err := pc1.Write(data) 34 | if err == nil && n != len(data) { 35 | err = errors.Errorf("expected to write %d but wrote %d", len(data), n) 36 | } 37 | if err != nil { 38 | t.Fatal(err.Error()) 39 | } 40 | 41 | outData := make([]byte, len(data)*2) 42 | on, err := pc2.Read(outData) 43 | if err != nil { 44 | t.Fatal(err.Error()) 45 | } 46 | outData = outData[:on] 47 | if on != len(data) { 48 | t.Fatalf( 49 | "length incorrect received %v != %v data: %v", 50 | on, 51 | len(data), 52 | string(outData), 53 | ) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /util/rwc/rwc.go: -------------------------------------------------------------------------------- 1 | package rwc 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | // ReadWriteCloser implements ReadWriteCloser with a Reader and Writer. 8 | type ReadWriteCloser struct { 9 | reader io.ReadCloser 10 | writer io.WriteCloser 11 | } 12 | 13 | // NewReadWriteCloser builds a new ReadWriteCloser. 14 | func NewReadWriteCloser( 15 | reader io.ReadCloser, 16 | writer io.WriteCloser, 17 | ) io.ReadWriteCloser { 18 | return &ReadWriteCloser{reader: reader, writer: writer} 19 | } 20 | 21 | // Read implements io.Reader 22 | func (r *ReadWriteCloser) Read(p []byte) (n int, err error) { 23 | return r.reader.Read(p) 24 | } 25 | 26 | // Write implements io.Writer 27 | func (r *ReadWriteCloser) Write(p []byte) (n int, err error) { 28 | return r.writer.Write(p) 29 | } 30 | 31 | // Close closes both streams. 32 | func (r *ReadWriteCloser) Close() error { 33 | er1 := r.reader.Close() 34 | er2 := r.writer.Close() 35 | if er1 != nil { 36 | return er1 37 | } 38 | if er2 != nil { 39 | return er2 40 | } 41 | 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /util/saddr/saddr.go: -------------------------------------------------------------------------------- 1 | package saddr 2 | 3 | import "net" 4 | 5 | // StringAddr is a net.Addr backed by a string. 6 | type StringAddr struct { 7 | net string 8 | addr string 9 | } 10 | 11 | // NewStringAddr constructs a new net.Addr from strings. 12 | func NewStringAddr(net, addr string) *StringAddr { 13 | return &StringAddr{ 14 | net: net, 15 | addr: addr, 16 | } 17 | } 18 | 19 | func (s *StringAddr) Network() string { 20 | return s.net 21 | } 22 | 23 | func (s *StringAddr) String() string { 24 | return s.addr 25 | } 26 | 27 | // _ is a type assertion 28 | var _ net.Addr = ((*StringAddr)(nil)) 29 | -------------------------------------------------------------------------------- /util/scrc/scrc.go: -------------------------------------------------------------------------------- 1 | package scrc 2 | 3 | import ( 4 | "hash" 5 | "hash/crc64" 6 | "sync" 7 | ) 8 | 9 | var ( 10 | h64Mtx sync.Mutex 11 | h64 hash.Hash64 12 | ) 13 | 14 | func init() { 15 | h64 = crc64.New(crc64.MakeTable(crc64.ECMA)) 16 | } 17 | 18 | // Crc64 computes the crc64 of some data. 19 | func Crc64(ds ...[]byte) uint64 { 20 | h64Mtx.Lock() 21 | defer h64Mtx.Unlock() 22 | defer h64.Reset() 23 | 24 | for _, d := range ds { 25 | _, _ = h64.Write(d) 26 | } 27 | 28 | return h64.Sum64() 29 | } 30 | --------------------------------------------------------------------------------