Introduction
Bifrost is a peer-to-peer communications engine with pluggable transports:
- Cross-platform: supports web browsers, servers, desktop, mobile, ...
- Efficient: multiplex many simultaneous streams over a single Link.
- Encryption: identify, authenticate, and encrypt each Link between peers.
- Flexible: use multiple transports, protocols, simultaneously.
- Meshing: supports multi-hop routing to a desired target peer w/ circuits.
- PubSub: publish/subscribe channels with pluggable implementations.
- Robust: uses Quic for lossless links over lossy transports.
Bifrost uses ControllerBus controllers and directives to send any protocol
over any transport with extensive and flexible configuration.
Overview
Bifrost is designed around the following core concepts:
- Peer: a routable process or device with a keypair.
- Transport: a protocol which can create Links with other peers.
- Link: a connection between two peers over a Transport.
- Stream: channel of data between two Peer with a protocol type.
- RPC: request/reply and bidirectional streaming remote calls.
- PubSub: at-least-once delivery of messages to named topics.
- Signaling: exchanging messages between peers via a relay server.
Integrates with networking, pubsub, and RPC libraries like libp2p, starpc,
nats, and pion webrtc.
The network simulator, testbed, and in-proc transport can be used to write
end-to-end tests as Go unit tests. The mock transports use identical code to the
real transports, and appear the same to the application code.
The http packages provide a http server and a mechanism for attaching http
handlers to the controller bus and using them to serve requests. There is also
an implementation of attaching and looking up http clients on the bus.
EntityGraph exposes the internal state representation of Bifrost to
visualizers and instrumentation via a graph-based inter-connected entity model.
Configuring each component as an independent controller makes it easy to adapt
application code to different operating environments and protocols.
Examples
Bifrost can be used as either a Go library or a command-line / daemon.
The examples directory contains yaml files to configure the
daemon, as well as "toys" which are self-contained Go program examples.
To install the CLI and daemon:
# Clone the repo and install.
git clone https://github.com/aperturerobotics/bifrost
cd ./bifrost/cmd/bifrost
go install -v
# Alternatively:
# Note: this currently fails on go >= 1.16 due to replace directives.
# See: https://github.com/golang/go/issues/44840
GO111MODULE=on go install -v github.com/aperturerobotics/bifrost/cmd/bifrost@master
Access help by adding the "-h" tag or running "bifrost help."
As a basic example, launch the daemon:
bifrost daemon \
--write-config \
--hold-open-links \
--pubsub nats \
--api-listen :5110 \
--udp-listen :5112
Check out the http forwarding example for a complete demo.
Daemon CLI
The Bifrost daemon is configured with a YAML ConfigSet and/or via the API.
NAME:
bifrost daemon - run a bifrost daemon
OPTIONS:
--hold-open-links if set, hold open links without an inactivity timeout [$BIFROST_HOLD_OPEN_LINKS]
--websocket-listen value if set, will listen on address for websocket connections, ex :5111 [$BIFROST_WS_LISTEN]
--udp-listen value if set, will listen on address for udp connections, ex :5112 [$BIFROST_UDP_LISTEN]
--establish-peers value if set, request establish links to list of peer ids [$BIFROST_ESTABLISH_PEERS]
--udp-peers value list of peer-id@address known UDP peers [$BIFROST_UDP_PEERS]
--websocket-peers value list of peer-id@address known WebSocket peers [$BIFROST_WS_PEERS]
--pubsub value if set, will configure pubsub from options: [floodsub, nats] [$BIFROST_PUBSUB]
--config value, -c value path to configuration yaml file (default: "bifrost_daemon.yaml") [$BIFROST_CONFIG]
--write-config write the daemon config file on startup [$BIFROST_WRITE_CONFIG]
--node-priv value path to node private key, will be generated if doesn't exist (default: "bifrost_daemon.pem") [$BIFROST_NODE_PRIV]
--api-listen value if set, will listen on address for API connections, ex :5110 (default: none) [$BIFROST_API_LISTEN]
--prof-listen value if set, debug profiler will be hosted on the port, ex :8080 [$BIFROST_PROF_LISTEN]
These CLI flags are provided for convenience to quickly configure a daemon, and
the resulting config can be written to a file with --write-config
for further
adjustments to be made. Note, however, that additional controllers are available
which are not yet exposed via these flags.
Client CLI
Most Bifrost functionality is exposed on the client CLI and RPC API:
- Mount a peer by loading a private key into the daemon.
- Forward incoming streams with a protocol ID to a multiaddress
- Proxy incoming connections to a listener to a remote peer
- Open a stream with a remote peer and a given protocol ID
- Accept a stream for a local peer with a given protocol ID
The client CLI has the following help output:
bifrost client command [command options] [arguments...]
COMMANDS:
local-peers returns local peer info
identify Private key will be loaded with a peer controller
subscribe Subscribe to a pubsub channel with a private key or mounted peer and publish base64 w/ newline delim from stdin.
forward Protocol ID will be forwarded to the target multiaddress
accept Single incoming stream with Protocol ID will be accepted
dial Single outgoing stream with Protocol ID will be dialed
listen Listen on the multiaddress and forward the connection to a remote stream.
controller-bus, cbus ControllerBus system sub-commands.
With the above operations, all stream handling and interaction with the network
is exposed to the API and command line. Some examples:
# Note: you can edit bifrost_daemon.yaml to change settings.
# Once the daemon configuration exists, you can now just run:
bifrost daemon
# While the command is executing, the private key will be attached.
bifrost client identify --peer-priv priv-key.pem
# While the command is executing, a forwarding controller will be running.
# Protocol ID will be forwarded to the target multiaddress
# Handles HandleMountedStream directives by contacting the target.
# HTTP can be easily proxied through an encrypted stream this way.
bifrost client forward \
--peer-id <agent-id> \
--protocol-id /x/myproto \
--target /ip4/127.0.0.1/tcp/8000
# While the command is executing, a proxying controller will be running.
# Protocol ID will be proxied from the listen multiaddress to the target peer.
# Calls OpenStream to build a stream from <source-peer-id> to <target-peer-id>.
# HTTP can be easily proxied through an encrypted stream this way.
bifrost client listen \
--peer-id <target-peer-id> \
--from-peer-id <source-peer-id> \
--protocol-id /x/myproto \
--listen /ip4/127.0.0.1/tcp/8001
# Wait for a stream to be opened to the mounted peer with the protocol ID /x/myproto
# Standard output is the incoming data stream, standard input is the outgoing data stream.
# Standard error is used for logging.
bifrost client accept \
--local-peer-id <peer-id> \
--protocol-id /x/myproto
# Establish a stream.
# Standard output is the incoming data stream, standard input is the outgoing data stream.
# Standard error is used for logging.
bifrost client dial \
--peer-id <target-peer-id> \
--local-peer-id <local-peer-id> \
--protocol-id /x/myproto
YAML Configuration
An example of a ConfigSet in YAML format for the daemon: bifrost_daemon.yaml
:
# Start a UDP listener on port 5112.
my-udp:
id: bifrost/udp
config:
listenAddr: :5112
# Use the floodsub driver for PubSub.
pubsub:
id: bifrost/floodsub
config: {}
Example: forward HTTP traffic between peers
The following is a basic example of using the CLI to forward encrypted traffic
between a local port and a remote peer port, similar to SSH port forwarding:
# note the peer id in the logs
./bifrost daemon \
--write-config \
--udp-listen :5000 \
--node-priv daemon_node_priv_2.pem \
--websocket-listen ":5111" \
--prof-listen ":6201"
# forward incoming connections to port 8000
# example: "python3 -m http.server 8080"
./bifrost client forward \
--protocol-id "test/protocol" \
--target /ip4/127.0.0.1/tcp/8000
# replace PEER-ID-HERE with the peer ID from the first daemon.
# start a second daemon (in a new shell).
./bifrost daemon \
--udp-listen :5001 \
--udp-peers "PEER-ID-HERE@127.0.0.1:5000" \
--api-listen ":5112"
# tell it to listen on port 8082 and forward to the other peer.
# try browsing to http://localhost:8082
./bifrost client --dial-addr 127.0.0.1:5112 listen \
--peer-id "PEER-ID-HERE" \
--protocol-id test/protocol \
--listen /ip4/127.0.0.1/tcp/8002
This example shows how to run two daemons with information on how to contact
each other, and then "tell" the second daemon to listen on port 8002 and forward
any incoming connections to the remote peer with the given peer ID.
When someone connects to port 8002 the EstablishLinkWithPeer directive is added
and the UDP transport opens the connection with the peer (on-demand.) The stream
is then negotiated. The remote daemon uses HandleMountedStream which is handled
by the "forwarding" controller, which forwards the stream to localhost at 8000.
Simulator and End-to-end Testing
// g is the graph of peers and LANs.
g := graph.NewGraph()
// Add two peers to the graph.
p0 := addPeer(t, g)
p1 := addPeer(t, g)
// Connect them together with a LAN.
lan1 := graph.AddLAN(g)
lan1.AddPeer(g, p0)
lan1.AddPeer(g, p1)
// Run the simulator
sim := initSimulator(t, ctx, le, g)
// Test connecting p0 and p1 together!
simulate.TestConnectivity(ctx, p0, p1)
The simulator enables writing end-to-end tests of running controllers on peers
and validating that everything works with the full Bifrost stack in the loop. It
uses in-memory pipes to simulate local-area-network connections between peers.
Try the example end-to-end test yourself:
go test -v -run Basic github.com/aperturerobotics/bifrost/sim/tests/bifrost
You will see the debug and quic logging followed by the success message:
INFO successful connectivity test: p0 <-> [lan1] <-> p1
Concepts
Transports and Links
A Link is a packet stream between two Peer. Links are created by Transports,
which are associated with a local private keypair.
Transports are responsible for handshaking their identity and providing stream
multiplexing, encryption, and ordering. The Bifrost codebase contains common
implementations for packet-based and stream-based transports, based primarily on
the quic-go implementation of the Quic UDP protocol.
The HandleMountedStream directive contains incoming protocol and peer info. The
appropriate controller for the protocol responds to the directive and handles
the incoming stream. This decouples the transport layers from the protocols.
PubSub
A PubSub is a controller that supports topic-based at-least-once delivery.
Nats and floodsub are currently supported as PubSub protocols.
Developing
If using Go only, you don't need yarn
or Node.JS
.
Bifrost uses Protobuf for message encoding.
You can re-generate the protobufs after changing any .proto
file:
# stage the .proto file so yarn gen sees it
git add .
# install deps
yarn
# generate the protobufs
yarn gen
To run the test suite:
# Go tests only
go test ./...
# All tests
yarn test
# Lint
yarn lint
Developing on MacOS
On MacOS, some homebrew packages are required for yarn gen
:
brew install bash make coreutils gnu-sed findutils protobuf
brew link --overwrite protobuf
Add to your .bashrc or .zshrc:
export PATH="/opt/homebrew/opt/coreutils/libexec/gnubin:$PATH"
export PATH="/opt/homebrew/opt/gnu-sed/libexec/gnubin:$PATH"
export PATH="/opt/homebrew/opt/findutils/libexec/gnubin:$PATH"
export PATH="/opt/homebrew/opt/make/libexec/gnubin:$PATH"
Support
Please open a GitHub issue with any questions / issues.
... or feel free to reach out on Matrix Chat or Discord.
License
Apache 2.0