Skip to content

Instantly share code, notes, and snippets.

@whyrusleeping
Created July 27, 2016 18:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save whyrusleeping/3feb3102fcc3072b17a177853008c89b to your computer and use it in GitHub Desktop.
Save whyrusleeping/3feb3102fcc3072b17a177853008c89b to your computer and use it in GitHub Desktop.

Implementing a new libp2p transport

Libp2p is the modular networking stack used by ipfs. It is designed in a way that anyone should be able to make it work with little effort over any given transport. In this example, i'm going to go through how to make libp2p (and consequently ipfs) work over websockets.

To implement a transport on the go side of libp2p, all you need to do is satisfy the Transport interface in go-libp2p-transport. To do this, I'm going to use an existing websockets library from golang.org/x/net/websocket and create my own package ws-transport.

Multiaddr Handling

The first thing we're going to need is a multiaddr parser to be able to reference our websocket addresses.

Lets create a multiaddr protocol for this:

var WsProtocol = ma.Protocol{
	Code:  477,
	Name:  "ws",
	VCode: ma.CodeToVarint(477),
}

Here we select ws as the tag for websockets, and 477 as the binary code for websockets. We also need to implement a few extra things as shims so we can reuse a lot of the code already written in go-multiaddr-net.

Next, a multiaddr-net shim:

var WsCodec = &manet.NetCodec{
	NetAddrNetworks:  []string{"websocket"},
	ProtocolName:     "ws",
	ConvertMultiaddr: ConvertWebsocketMultiaddrToNetAddr,
	ParseNetAddr:     ParseWebsocketNetAddr,
}

This defines a codec to allow libp2p to translate conventional address types to multiaddr types. The two functions referenced there implement those translations, and you can check out the code here.

Now, a formatter object to parse and validate websocket multiaddrs:

var WsFmt = mafmt.And(mafmt.TCP, mafmt.Base(WsProtocol.Code))

The above statement declares a websocket address as a TCP address with a /ws/ protocol tag at the end, for example: /ip4/1.2.3.4/tcp/8005/ws

Now that we have those, we should register them with the multiaddr library in our package initialization function:

func init() {
	err := ma.AddProtocol(WsProtocol)
	if err != nil {
		log.Fatalf("error registering websocket protocol: %s", err)
	}

	manet.RegisterNetCodec(WsCodec)
}

The Transport

Now that all that nonsense is out of the way, let's get to actually implementing the Transport. To do this, we need four main types, the Transport, the Dialer, the Listener and a Conn type.

The transport is used to manage the creation of dialers and listeners. The reason we have an object for that instead of just using constructor functions is that some transports want to be able to reuse certain resources, for example TCP with reuseport, or UTP reusing the same underlying socket for dialing out. That transport struct in our case is very simple, and looks like:

type WebsocketTransport struct{}

func (t *WebsocketTransport) Matches(a ma.Multiaddr) bool {
	return WsFmt.Matches(a)
}

func (t *WebsocketTransport) Dialer(_ ma.Multiaddr, opts ...tpt.DialOpt) (tpt.Dialer, error) {
	return &dialer{}, nil
}

func (t *WebsocketTransport) Listen(a ma.Multiaddr) (tpt.Listener, error) {
	list, err := manet.Listen(a)
	if err != nil {
		return nil, err
	}

	tlist := t.wrapListener(list)

	u, err := url.Parse("ws://" + list.Addr().String())
	if err != nil {
		return nil, err
	}

	s := &ws.Server{
		Handler: tlist.handleWsConn,
		Config:  ws.Config{Origin: u},
	}

	go http.Serve(list.NetListener(), s)

	return tlist, nil
}

In this code, we define a transport object and three methods on it.

First, we have Matches. This method is used to determine if a transport can handle a given multiaddr. At first glance, this is just a way to match TCP addresses to TCP transports and so on, but it can also be used for other more interesting things. For example, you could use a different transport for TCP connections over the localhost vs TCP connections over WAN or LAN.

The final two methods are pretty self explanatory, Listen and Dialer. Listen creates a listener object to accept incoming connections from the specified multiaddr, and Dialer creates an object (with options) that can be used to create outgoing connections.

For now, we aren't supporting any special dialing options, so the implementation of Dialer is trivial. The implementation of Listen sets up a websockets server and will return connections that get made to it.

The Listener

The interface for the listener is somewhat simple as well:

type Listener interface {
    Accept() (Conn, error)
    Close() error
    Addr() net.Addr
    Multiaddr() ma.Multiaddr
}

The most complicated method here is Accept. It just needs to return the next incoming connection. Close needs to shut down the listener, and Addr and Multiaddr need to return the address (in respective formats) that this listener is listening on.

The Dialer

type Dialer interface {
    Dial(ma.Multiaddr) (Conn, error)
    Matches(ma.Multiaddr) bool
}

Even smaller, the Dialer simply exists to open connections to peers over this transport. The Matches method serves the same purpose as discussed earlier on the transport.

The Conn

The final piece of the puzzle, the Conn object is a bit more involved than the others. Its interface as described in go-libp2p-transport looks like this:

type Conn interface {
    manet.Conn
    Transport() Transport
}

This says that we need to implement all the methods of a manet.Conn plus the additional Transport method which is used to identify where a given Conn came from. You can manually implement these methods (defined here) or, if you already have an object that implements the net.Conn interface (from the go stdlib net package) you can use go-multiaddr-net's WrapNetConn method to fulfill the rest.

Plugging it in

Once you have that code down, you will have successfully written a go-libp2p transport! The next step is to actually integrate that into go-libp2p so you can use it in ipfs. This involves editing the libp2p swarm code, the baseline diff to accomplish this for websockets looks like:

diff --git a/p2p/net/swarm/swarm.go b/p2p/net/swarm/swarm.go
index 100a982..5c06a10 100644
--- a/p2p/net/swarm/swarm.go
+++ b/p2p/net/swarm/swarm.go
@@ -29,6 +29,7 @@ import (
        spdy "github.com/whyrusleeping/go-smux-spdystream"
        yamux "github.com/whyrusleeping/go-smux-yamux"
        mafilter "github.com/whyrusleeping/multiaddr-filter"
+       ws "github.com/whyrusleeping/ws-transport"
        context "golang.org/x/net/context"
 )
 
@@ -118,6 +119,7 @@ func NewSwarm(ctx context.Context, listenAddrs []ma.Multiaddr,
                transports: []transport.Transport{
                        transport.NewTCPTransport(),
                        transport.NewUtpTransport(),
+                       new(ws.WebsocketTransport),
                },
                bwc:         bwc,
                fdRateLimit: make(chan struct{}, concurrentFdDials),

You will also have to let libp2p's address parser know that your address type is okay, This is accomplished by adding the protocol path strings you want to support to the SupportedTransportStrings array in p2p/net/swarm/addr/addr.go.

diff --git a/p2p/net/swarm/addr/addr.go b/p2p/net/swarm/addr/addr.go
index d9ba872..a505097 100644
--- a/p2p/net/swarm/addr/addr.go
+++ b/p2p/net/swarm/addr/addr.go
@@ -7,6 +7,8 @@ import (
        ma "github.com/jbenet/go-multiaddr"
        manet "github.com/jbenet/go-multiaddr-net"
        context "golang.org/x/net/context"
+
+       _ "github.com/whyrusleeping/ws-transport"
 )
 
 var log = logging.Logger("github.com/ipfs/go-libp2p/p2p/net/swarm/addr")
@@ -19,6 +21,8 @@ var SupportedTransportStrings = []string{
        "/ip6/tcp",
        "/ip4/udp/utp",
        "/ip6/udp/utp",
+       "/ip4/tcp/ws",
+       "/ip6/tcp/ws",
        // "/ip4/udp/udt", disabled because the lib doesnt work on arm
        // "/ip6/udp/udt", disabled because the lib doesnt work on arm
 }

And that's it! You now have a libp2p that can communicate over websockets! If you have any questions, please hit me up on IRC, send me an email, or file an issue on the ipfs/examples repo!

By whyrusleeping

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment