Fast, modular, and flexible peer to peer library in Go
Legion is an easy to use, fast, and bare-bones peer to peer library designed to leave most of the network characteristics up to the user through a simple yet powerful framework system. It was written because here at Gladius we needed a peer to peer overlay for our applications, and existing solutions were not a perfect fit for our use case.
We would also like to link to the Perlin Network's Noise, as their initial API inspired a lot of our design philosophy and you should totally check it out.
- User defined messages, allowing you to build your own cryptography and message validation system
- Powerful framework system with easy to use event context
- Single TCP connection opened to each peer, where messages are sent over multiplexed streams
- User defined logging via a logging interface
Messages are extremely customizable and simple with only enough information to allow Legion to discern the sender and message type. Legion has no cryptography by default and doesn't enforce any specific message body requirements. We also allow a user to set your own message validator through a framework to check an incoming message, which means you can add in your own cryptography, compression, or really any other criteria you want.
This should be considered a quick start guide, there are more examples in the examples folder and in the Gladius Network Gateway
Here we create a legion object with a default config, wait until it's listening, and add a peer.
func main(){
// Build a basic config
conf := legion.SimpleConfig("localhost", 7947)
// Build a new legion object from the config with no framework
l := legion.New(conf, nil)
// Listen in a new goroutine
go l.Listen()
// Wait until the network is listening
l.Started()
// Dial a peer and add it to the non-messagable peers
err := l.AddPeer(utils.LegionAddressFromString("localhost:7946"))
if err != nil {
panic(err)
}
// Make that peer sendable by promoting it
err := l.AddPeer(utils.LegionAddressFromString("localhost:7946"))
if err != nil {
panic(err)
}
// Block forever
select {}
}
There are several ways to send messages to peers in the network:
func main(){
// ... build our legion object
// Dial a peer and add it to the non-messagable peers
err := l.AddPeer(utils.LegionAddressFromString("localhost:7946"))
if err != nil {
panic(err)
}
// Will send to all promoted peers
l.Broadcast(l.NewMessage(config.BindAddress,"ping", []byte("ping"))
// Send to a specific peer
l.Broadcast(l.NewMessage(config.BindAddress,"ping", []byte("ping"),
utils.LegionAddressFromString("localhost:7946"))
// Broadcast to a random 5 promoted peers
l.BroadcastRandom(l.NewMessage(config.BindAddress,"ping", []byte("ping"), 5)
// Block forever
select {}
}
The internal logger is a generic type that can be overridden by the user as long as your logger meets the requirements below:
// GenericLogger is the logger interface that legion uses, you can plug in
// your own logger as long as your logger implements this interface.
type GenericLogger interface {
// Base log types
Debug() GenericContext
Info() GenericContext
Warn() GenericContext
Error() GenericContext
// Add context like logger.With(NewContext().Field("test", "val"))
With(ctx GenericContext) GenericLogger
}
// GenericContext provides a way to add fields to a log event
type GenericContext interface {
Field(key string, val interface{}) GenericContext
// Actually log the built up log line with the message
Log(msg string)
}
You can register a new logger by calling
logger.SetLogger(YourLogger)
By default we use zerolog, you can see our implemented logger here. If you want to edit the underlying zerolog instance, you can call:
l := logger.GetLogger() // Get the wrapper
zerologger := l.(logger.ZeroLogger).Logger // Get the actual Zerolog instance (can change things like the formatting, output location, etc)
A framework is any struct that implements the framework interface:
type Framework interface {
// Set anything up you want with Legion when the Listen method is called.
// Should block until the framework is ready to accept messages.
Configure(*Legion) error
// Called before any message is passed to plugins
ValidateMessage(*MessageContext) bool
// Methods to interact with legion
NewMessage(*MessageContext)
PeerAdded(*PeerContext)
PeerDisconnect(*PeerContext)
Startup(*NetworkContext)
Close(*NetworkContext)
}
If you don't need all of these methods, you can use our handy GenericFramework as an anonymous field in your struct, like this:
type MyFramework struct {
network.GenericFramework
specialData string
}
func (f *MyFramework) NewMessage(ctx *MessageContext) {
fmt.Println(mspecialData)
}
by doing this, you only need to implement the methods you need and still conform to the interface.
Legion includes a Kademlia like DHT framework built on top of Ethereum addresses, you can use this for discovery if you'd like:
// Create a new framework with a default address validator and a private key.
f := ethpool.New(func(common.Address) bool { return true }, privKey)
// Register it with legion
l := legion.New(conf, f)
// Connect to a peer
l.AddPeer(utils.LegionAddressFromString("localhost:6000"))
// Bootstrap with the remote peer
f.Bootstrap()