Repository: libp2p/go-libp2p
Pull request #1174 (2021-09-02 19:17:58)

move the circuit v2 code here
As discussed in https://github.com/libp2p/go-libp2p-circuit/pull/138, the circuit v2 implementation should live in go-libp2p, not in a separate package.

Commit history extracted from the original repo by running:
```bash
echo "github.com/libp2p/go-libp2p-circuit/v2==>github.com/libp2p/go-libp2p/p2p/host/circuitv2" >> replace.txt
git filter-repo --force --path v2/ --path-rename v2:p2p/host/circuitv2 --replace-text replace.txt
```
and then running
```bash
git rebase --committer-date-is-author-date origin/master --exec "go mod tidy"
```
as I couldn't figure out how to have `git filter-repo` do that in one step without writing a Python script.



Patch:
diff --git a/examples/go.sum b/examples/go.sum
index fa6e8eeb..d5f816ce 100644
--- a/examples/go.sum
+++ b/examples/go.sum
@@ -429,8 +429,9 @@ github.com/libp2p/go-flow-metrics v0.0.1/go.mod h1:Iv1GH0sG8DtYN3SVJ2eG221wMiNpZ
 github.com/libp2p/go-flow-metrics v0.0.2/go.mod h1:HeoSNUrOJVK1jEpDqVEiUOIXqhbnS27omG0uWU5slZs=
 github.com/libp2p/go-flow-metrics v0.0.3 h1:8tAs/hSdNvUiLgtlSy3mxwxWP4I9y/jlkPFT7epKdeM=
 github.com/libp2p/go-flow-metrics v0.0.3/go.mod h1:HeoSNUrOJVK1jEpDqVEiUOIXqhbnS27omG0uWU5slZs=
-github.com/libp2p/go-libp2p-asn-util v0.0.0-20200825225859-85005c6cf052 h1:BM7aaOF7RpmNn9+9g6uTjGJ0cTzWr5j9i9IKeun2M8U=
 github.com/libp2p/go-libp2p-asn-util v0.0.0-20200825225859-85005c6cf052/go.mod h1:nRMRTab+kZuk0LnKZpxhOVH/ndsdr2Nr//Zltc/vwgo=
+github.com/libp2p/go-libp2p-asn-util v0.0.0-20210818120414-1f382a4aa43a h1:6yEuCOY31elgeJ2KA2JiREZjIznvH6lOWCdHRuhgEgc=
+github.com/libp2p/go-libp2p-asn-util v0.0.0-20210818120414-1f382a4aa43a/go.mod h1:wu+AnM9Ii2KgO5jMmS1rz9dvzTdj8BXqsPR9HR0XB7I=
 github.com/libp2p/go-libp2p-autonat v0.4.2 h1:YMp7StMi2dof+baaxkbxaizXjY1RPvU71CXfxExzcUU=
 github.com/libp2p/go-libp2p-autonat v0.4.2/go.mod h1:YxaJlpr81FhdOv3W3BTconZPfhaYivRdf53g+S2wobk=
 github.com/libp2p/go-libp2p-blankhost v0.2.0 h1:3EsGAi0CBGcZ33GwRuXEYJLLPoVWyXJ1bcJzAJjINkk=
diff --git a/examples/ipfs-camp-2019/go.sum b/examples/ipfs-camp-2019/go.sum
index 7453b4e4..8f1f154f 100644
--- a/examples/ipfs-camp-2019/go.sum
+++ b/examples/ipfs-camp-2019/go.sum
@@ -429,8 +429,9 @@ github.com/libp2p/go-flow-metrics v0.0.1/go.mod h1:Iv1GH0sG8DtYN3SVJ2eG221wMiNpZ
 github.com/libp2p/go-flow-metrics v0.0.2/go.mod h1:HeoSNUrOJVK1jEpDqVEiUOIXqhbnS27omG0uWU5slZs=
 github.com/libp2p/go-flow-metrics v0.0.3 h1:8tAs/hSdNvUiLgtlSy3mxwxWP4I9y/jlkPFT7epKdeM=
 github.com/libp2p/go-flow-metrics v0.0.3/go.mod h1:HeoSNUrOJVK1jEpDqVEiUOIXqhbnS27omG0uWU5slZs=
-github.com/libp2p/go-libp2p-asn-util v0.0.0-20200825225859-85005c6cf052 h1:BM7aaOF7RpmNn9+9g6uTjGJ0cTzWr5j9i9IKeun2M8U=
 github.com/libp2p/go-libp2p-asn-util v0.0.0-20200825225859-85005c6cf052/go.mod h1:nRMRTab+kZuk0LnKZpxhOVH/ndsdr2Nr//Zltc/vwgo=
+github.com/libp2p/go-libp2p-asn-util v0.0.0-20210818120414-1f382a4aa43a h1:6yEuCOY31elgeJ2KA2JiREZjIznvH6lOWCdHRuhgEgc=
+github.com/libp2p/go-libp2p-asn-util v0.0.0-20210818120414-1f382a4aa43a/go.mod h1:wu+AnM9Ii2KgO5jMmS1rz9dvzTdj8BXqsPR9HR0XB7I=
 github.com/libp2p/go-libp2p-autonat v0.4.2 h1:YMp7StMi2dof+baaxkbxaizXjY1RPvU71CXfxExzcUU=
 github.com/libp2p/go-libp2p-autonat v0.4.2/go.mod h1:YxaJlpr81FhdOv3W3BTconZPfhaYivRdf53g+S2wobk=
 github.com/libp2p/go-libp2p-blankhost v0.2.0 h1:3EsGAi0CBGcZ33GwRuXEYJLLPoVWyXJ1bcJzAJjINkk=
diff --git a/examples/pubsub/chat/go.sum b/examples/pubsub/chat/go.sum
index 3256e236..8d3dedde 100644
--- a/examples/pubsub/chat/go.sum
+++ b/examples/pubsub/chat/go.sum
@@ -401,6 +401,7 @@ github.com/libp2p/go-addr-util v0.1.0/go.mod h1:6I3ZYuFr2O/9D+SoyM0zEw0EF3YkldtT
 github.com/libp2p/go-buffer-pool v0.0.1/go.mod h1:xtyIz9PMobb13WaxR6Zo1Pd1zXJKYg0a8KiIvDp3TzQ=
 github.com/libp2p/go-buffer-pool v0.0.2 h1:QNK2iAFa8gjAe1SPz6mHSMuCcjs+X1wlHzeOSqcmlfs=
 github.com/libp2p/go-buffer-pool v0.0.2/go.mod h1:MvaB6xw5vOrDl8rYZGLFdKAuk/hRoRZd1Vi32+RXyFM=
+github.com/libp2p/go-cidranger v1.1.0/go.mod h1:KWZTfSr+r9qEo9OkI9/SIEeAtw+NNoU0dXIXt15Okic=
 github.com/libp2p/go-conn-security-multistream v0.2.0/go.mod h1:hZN4MjlNetKD3Rq5Jb/P5ohUnFLNzEAR4DLSzpn2QLU=
 github.com/libp2p/go-conn-security-multistream v0.2.1 h1:ft6/POSK7F+vl/2qzegnHDaXFU0iWB4yVTYrioC6Zy0=
 github.com/libp2p/go-conn-security-multistream v0.2.1/go.mod h1:cR1d8gA0Hr59Fj6NhaTpFhJZrjSYuNmhpT2r25zYR70=
@@ -409,6 +410,7 @@ github.com/libp2p/go-eventbus v0.2.1/go.mod h1:jc2S4SoEVPP48H9Wpzm5aiGwUCBMfGhVh
 github.com/libp2p/go-flow-metrics v0.0.1/go.mod h1:Iv1GH0sG8DtYN3SVJ2eG221wMiNpZxBdp967ls1g+k8=
 github.com/libp2p/go-flow-metrics v0.0.3 h1:8tAs/hSdNvUiLgtlSy3mxwxWP4I9y/jlkPFT7epKdeM=
 github.com/libp2p/go-flow-metrics v0.0.3/go.mod h1:HeoSNUrOJVK1jEpDqVEiUOIXqhbnS27omG0uWU5slZs=
+github.com/libp2p/go-libp2p-asn-util v0.0.0-20210818120414-1f382a4aa43a/go.mod h1:wu+AnM9Ii2KgO5jMmS1rz9dvzTdj8BXqsPR9HR0XB7I=
 github.com/libp2p/go-libp2p-autonat v0.4.2 h1:YMp7StMi2dof+baaxkbxaizXjY1RPvU71CXfxExzcUU=
 github.com/libp2p/go-libp2p-autonat v0.4.2/go.mod h1:YxaJlpr81FhdOv3W3BTconZPfhaYivRdf53g+S2wobk=
 github.com/libp2p/go-libp2p-blankhost v0.2.0 h1:3EsGAi0CBGcZ33GwRuXEYJLLPoVWyXJ1bcJzAJjINkk=
diff --git a/go.mod b/go.mod
index e47ddf66..1440a611 100644
--- a/go.mod
+++ b/go.mod
@@ -12,14 +12,17 @@ require (
 	github.com/ipfs/go-datastore v0.4.6
 	github.com/ipfs/go-detect-race v0.0.1
 	github.com/ipfs/go-ipfs-util v0.0.2
+	github.com/ipfs/go-log v1.0.5
 	github.com/ipfs/go-log/v2 v2.3.0
 	github.com/jbenet/go-cienv v0.1.0
 	github.com/jbenet/goprocess v0.1.4
 	github.com/klauspost/cpuid/v2 v2.0.9 // indirect
 	github.com/koron/go-ssdp v0.0.2 // indirect
 	github.com/libp2p/go-addr-util v0.1.0
+	github.com/libp2p/go-buffer-pool v0.0.2
 	github.com/libp2p/go-conn-security-multistream v0.2.1
 	github.com/libp2p/go-eventbus v0.2.1
+	github.com/libp2p/go-libp2p-asn-util v0.0.0-20210818120414-1f382a4aa43a
 	github.com/libp2p/go-libp2p-autonat v0.4.2
 	github.com/libp2p/go-libp2p-blankhost v0.2.0
 	github.com/libp2p/go-libp2p-circuit v0.4.0
@@ -45,6 +48,7 @@ require (
 	github.com/multiformats/go-multiaddr v0.4.0
 	github.com/multiformats/go-multiaddr-dns v0.3.1
 	github.com/multiformats/go-multistream v0.2.2
+	github.com/multiformats/go-varint v0.0.6
 	github.com/prometheus/common v0.30.0 // indirect
 	github.com/prometheus/procfs v0.7.3 // indirect
 	github.com/stretchr/testify v1.7.0
diff --git a/go.sum b/go.sum
index 4451e23c..fac8839f 100644
--- a/go.sum
+++ b/go.sum
@@ -418,6 +418,8 @@ github.com/libp2p/go-addr-util v0.1.0/go.mod h1:6I3ZYuFr2O/9D+SoyM0zEw0EF3YkldtT
 github.com/libp2p/go-buffer-pool v0.0.1/go.mod h1:xtyIz9PMobb13WaxR6Zo1Pd1zXJKYg0a8KiIvDp3TzQ=
 github.com/libp2p/go-buffer-pool v0.0.2 h1:QNK2iAFa8gjAe1SPz6mHSMuCcjs+X1wlHzeOSqcmlfs=
 github.com/libp2p/go-buffer-pool v0.0.2/go.mod h1:MvaB6xw5vOrDl8rYZGLFdKAuk/hRoRZd1Vi32+RXyFM=
+github.com/libp2p/go-cidranger v1.1.0 h1:ewPN8EZ0dd1LSnrtuwd4709PXVcITVeuwbag38yPW7c=
+github.com/libp2p/go-cidranger v1.1.0/go.mod h1:KWZTfSr+r9qEo9OkI9/SIEeAtw+NNoU0dXIXt15Okic=
 github.com/libp2p/go-conn-security-multistream v0.1.0/go.mod h1:aw6eD7LOsHEX7+2hJkDxw1MteijaVcI+/eP2/x3J1xc=
 github.com/libp2p/go-conn-security-multistream v0.2.0/go.mod h1:hZN4MjlNetKD3Rq5Jb/P5ohUnFLNzEAR4DLSzpn2QLU=
 github.com/libp2p/go-conn-security-multistream v0.2.1 h1:ft6/POSK7F+vl/2qzegnHDaXFU0iWB4yVTYrioC6Zy0=
@@ -432,6 +434,8 @@ github.com/libp2p/go-libp2p v0.6.1/go.mod h1:CTFnWXogryAHjXAKEbOf1OWY+VeAP3lDMZk
 github.com/libp2p/go-libp2p v0.7.0/go.mod h1:hZJf8txWeCduQRDC/WSqBGMxaTHCOYHt2xSU1ivxn0k=
 github.com/libp2p/go-libp2p v0.7.4/go.mod h1:oXsBlTLF1q7pxr+9w6lqzS1ILpyHsaBPniVO7zIHGMw=
 github.com/libp2p/go-libp2p v0.8.1/go.mod h1:QRNH9pwdbEBpx5DTJYg+qxcVaDMAz3Ee/qDKwXujH5o=
+github.com/libp2p/go-libp2p-asn-util v0.0.0-20210818120414-1f382a4aa43a h1:6yEuCOY31elgeJ2KA2JiREZjIznvH6lOWCdHRuhgEgc=
+github.com/libp2p/go-libp2p-asn-util v0.0.0-20210818120414-1f382a4aa43a/go.mod h1:wu+AnM9Ii2KgO5jMmS1rz9dvzTdj8BXqsPR9HR0XB7I=
 github.com/libp2p/go-libp2p-autonat v0.1.1/go.mod h1:OXqkeGOY2xJVWKAGV2inNF5aKN/djNA3fdpCWloIudE=
 github.com/libp2p/go-libp2p-autonat v0.2.0/go.mod h1:DX+9teU4pEEoZUqR1PiMlqliONQdNbfzE1C718tcViI=
 github.com/libp2p/go-libp2p-autonat v0.2.1/go.mod h1:MWtAhV5Ko1l6QBsHQNSuM6b1sRkXrpk0/LqCr+vCVxI=
diff --git a/p2p/host/circuitv2/client/client.go b/p2p/host/circuitv2/client/client.go
new file mode 100644
index 00000000..60d7e758
--- /dev/null
+++ b/p2p/host/circuitv2/client/client.go
@@ -0,0 +1,66 @@
+package client
+
+import (
+	"context"
+	"sync"
+
+	"github.com/libp2p/go-libp2p/p2p/host/circuitv2/proto"
+
+	"github.com/libp2p/go-libp2p-core/host"
+	"github.com/libp2p/go-libp2p-core/peer"
+
+	logging "github.com/ipfs/go-log"
+	tptu "github.com/libp2p/go-libp2p-transport-upgrader"
+)
+
+var log = logging.Logger("p2p-circuit")
+
+// Client implements the client-side of the p2p-circuit/v2 protocol:
+// - it implements dialing through v2 relays
+// - it listens for incoming connections through v2 relays.
+//
+// For backwards compatibility with v1 relays and older nodes, the client will
+// also accept relay connections through v1 relays and fallback dial peers using p2p-circuit/v1.
+// This allows us to use the v2 code as drop in replacement for v1 in a host without breaking
+// existing code and interoperability with older nodes.
+type Client struct {
+	ctx      context.Context
+	host     host.Host
+	upgrader *tptu.Upgrader
+
+	incoming chan accept
+
+	mx          sync.Mutex
+	activeDials map[peer.ID]*completion
+	hopCount    map[peer.ID]int
+}
+
+type accept struct {
+	conn          *Conn
+	writeResponse func() error
+}
+
+type completion struct {
+	ch    chan struct{}
+	relay peer.ID
+	err   error
+}
+
+// New constructs a new p2p-circuit/v2 client, attached to the given host and using the given
+// upgrader to perform connection upgrades.
+func New(ctx context.Context, h host.Host, upgrader *tptu.Upgrader) (*Client, error) {
+	return &Client{
+		ctx:         ctx,
+		host:        h,
+		upgrader:    upgrader,
+		incoming:    make(chan accept),
+		activeDials: make(map[peer.ID]*completion),
+		hopCount:    make(map[peer.ID]int),
+	}, nil
+}
+
+// Start registers the circuit (client) protocol stream handlers
+func (c *Client) Start() {
+	c.host.SetStreamHandler(proto.ProtoIDv1, c.handleStreamV1)
+	c.host.SetStreamHandler(proto.ProtoIDv2Stop, c.handleStreamV2)
+}
diff --git a/p2p/host/circuitv2/client/conn.go b/p2p/host/circuitv2/client/conn.go
new file mode 100644
index 00000000..dc770768
--- /dev/null
+++ b/p2p/host/circuitv2/client/conn.go
@@ -0,0 +1,145 @@
+package client
+
+import (
+	"fmt"
+	"net"
+	"time"
+
+	"github.com/libp2p/go-libp2p-core/network"
+	"github.com/libp2p/go-libp2p-core/peer"
+
+	ma "github.com/multiformats/go-multiaddr"
+	manet "github.com/multiformats/go-multiaddr/net"
+)
+
+// HopTagWeight is the connection manager weight for connections carrying relay hop streams
+var HopTagWeight = 5
+
+type statLimitDuration struct{}
+type statLimitData struct{}
+
+var (
+	StatLimitDuration = statLimitDuration{}
+	StatLimitData     = statLimitData{}
+)
+
+type Conn struct {
+	stream network.Stream
+	remote peer.AddrInfo
+	stat   network.Stat
+
+	client *Client
+}
+
+type NetAddr struct {
+	Relay  string
+	Remote string
+}
+
+var _ net.Addr = (*NetAddr)(nil)
+
+func (n *NetAddr) Network() string {
+	return "libp2p-circuit-relay"
+}
+
+func (n *NetAddr) String() string {
+	return fmt.Sprintf("relay[%s-%s]", n.Remote, n.Relay)
+}
+
+// Conn interface
+var _ manet.Conn = (*Conn)(nil)
+
+func (c *Conn) Close() error {
+	c.untagHop()
+	return c.stream.Reset()
+}
+
+func (c *Conn) Read(buf []byte) (int, error) {
+	return c.stream.Read(buf)
+}
+
+func (c *Conn) Write(buf []byte) (int, error) {
+	return c.stream.Write(buf)
+}
+
+func (c *Conn) SetDeadline(t time.Time) error {
+	return c.stream.SetDeadline(t)
+}
+
+func (c *Conn) SetReadDeadline(t time.Time) error {
+	return c.stream.SetReadDeadline(t)
+}
+
+func (c *Conn) SetWriteDeadline(t time.Time) error {
+	return c.stream.SetWriteDeadline(t)
+}
+
+// TODO: is it okay to cast c.Conn().RemotePeer() into a multiaddr? might be "user input"
+func (c *Conn) RemoteMultiaddr() ma.Multiaddr {
+	// TODO: We should be able to do this directly without converting to/from a string.
+	relayAddr, err := ma.NewComponent(
+		ma.ProtocolWithCode(ma.P_P2P).Name,
+		c.stream.Conn().RemotePeer().Pretty(),
+	)
+	if err != nil {
+		panic(err)
+	}
+	return ma.Join(c.stream.Conn().RemoteMultiaddr(), relayAddr, circuitAddr)
+}
+
+func (c *Conn) LocalMultiaddr() ma.Multiaddr {
+	return c.stream.Conn().LocalMultiaddr()
+}
+
+func (c *Conn) LocalAddr() net.Addr {
+	na, err := manet.ToNetAddr(c.stream.Conn().LocalMultiaddr())
+	if err != nil {
+		log.Error("failed to convert local multiaddr to net addr:", err)
+		return nil
+	}
+	return na
+}
+
+func (c *Conn) RemoteAddr() net.Addr {
+	return &NetAddr{
+		Relay:  c.stream.Conn().RemotePeer().Pretty(),
+		Remote: c.remote.ID.Pretty(),
+	}
+}
+
+// ConnStat interface
+var _ network.ConnStat = (*Conn)(nil)
+
+func (c *Conn) Stat() network.Stat {
+	return c.stat
+}
+
+// tagHop tags the underlying relay connection so that it can be (somewhat) protected from the
+// connection manager as it is an important connection that proxies other connections.
+// This is handled here so that the user code doesnt need to bother with this and avoid
+// clown shoes situations where a high value peer connection is behind a relayed connection and it is
+// implicitly because the connection manager closed the underlying relay connection.
+func (c *Conn) tagHop() {
+	c.client.mx.Lock()
+	defer c.client.mx.Unlock()
+
+	p := c.stream.Conn().RemotePeer()
+	c.client.hopCount[p]++
+	if c.client.hopCount[p] == 1 {
+		c.client.host.ConnManager().TagPeer(p, "relay-hop-stream", HopTagWeight)
+	}
+}
+
+// untagHop removes the relay-hop-stream tag if necessary; it is invoked when a relayed connection
+// is closed.
+func (c *Conn) untagHop() {
+	c.client.mx.Lock()
+	defer c.client.mx.Unlock()
+
+	p := c.stream.Conn().RemotePeer()
+	c.client.hopCount[p]--
+	if c.client.hopCount[p] == 0 {
+		c.client.host.ConnManager().UntagPeer(p, "relay-hop-stream")
+		delete(c.client.hopCount, p)
+	}
+}
diff --git a/p2p/host/circuitv2/client/dial.go b/p2p/host/circuitv2/client/dial.go
new file mode 100644
index 00000000..535521fd
--- /dev/null
+++ b/p2p/host/circuitv2/client/dial.go
@@ -0,0 +1,239 @@
+package client
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	pbv1 "github.com/libp2p/go-libp2p-circuit/pb"
+	pbv2 "github.com/libp2p/go-libp2p/p2p/host/circuitv2/pb"
+	"github.com/libp2p/go-libp2p/p2p/host/circuitv2/proto"
+	"github.com/libp2p/go-libp2p/p2p/host/circuitv2/util"
+
+	"github.com/libp2p/go-libp2p-core/network"
+	"github.com/libp2p/go-libp2p-core/peer"
+	"github.com/libp2p/go-libp2p-core/peerstore"
+
+	ma "github.com/multiformats/go-multiaddr"
+)
+
+const maxMessageSize = 4096
+
+var DialTimeout = time.Minute
+var DialRelayTimeout = 5 * time.Second
+
+// relay protocol errors; used for signalling deduplication
+type relayError struct {
+	err string
+}
+
+func (e relayError) Error() string {
+	return e.err
+}
+
+func newRelayError(t string, args ...interface{}) error {
+	return relayError{err: fmt.Sprintf(t, args...)}
+}
+
+func isRelayError(err error) bool {
+	_, ok := err.(relayError)
+	return ok
+}
+
+// dialer
+func (c *Client) dial(ctx context.Context, a ma.Multiaddr, p peer.ID) (*Conn, error) {
+	// split /a/p2p-circuit/b into (/a, /p2p-circuit/b)
+	relayaddr, destaddr := ma.SplitFunc(a, func(c ma.Component) bool {
+		return c.Protocol().Code == ma.P_CIRCUIT
+	})
+
+	// If the address contained no /p2p-circuit part, the second part is nil.
+	if destaddr == nil {
+		return nil, fmt.Errorf("%s is not a relay address", a)
+	}
+
+	if relayaddr == nil {
+		return nil, fmt.Errorf("can't dial a p2p-circuit without specifying a relay: %s", a)
+	}
+
+	dinfo := peer.AddrInfo{ID: p}
+
+	// Strip the /p2p-circuit prefix from the destaddr so that we can pass the destination address
+	// (if present) for active relays
+	_, destaddr = ma.SplitFirst(destaddr)
+	if destaddr != nil {
+		dinfo.Addrs = append(dinfo.Addrs, destaddr)
+	}
+
+	rinfo, err := peer.AddrInfoFromP2pAddr(relayaddr)
+	if err != nil {
+		return nil, fmt.Errorf("error parsing relay multiaddr '%s': %w", relayaddr, err)
+	}
+
+	// deduplicate active relay dials to the same peer
+retry:
+	c.mx.Lock()
+	dedup, active := c.activeDials[p]
+	if !active {
+		dedup = &completion{ch: make(chan struct{}), relay: rinfo.ID}
+		c.activeDials[p] = dedup
+	}
+	c.mx.Unlock()
+
+	if active {
+		select {
+		case <-dedup.ch:
+			if dedup.err != nil {
+				if dedup.relay != rinfo.ID {
+					// different relay, retry
+					goto retry
+				}
+
+				if !isRelayError(dedup.err) {
+					// not a relay protocol error, retry
+					goto retry
+				}
+
+				// don't try the same relay if it failed to connect with a protocol error
+				return nil, fmt.Errorf("concurrent active dial through the same relay failed with a protocol error")
+			}
+
+			return nil, fmt.Errorf("concurrent active dial succeeded")
+
+		case <-ctx.Done():
+			return nil, ctx.Err()
+		}
+	}
+
+	conn, err := c.dialPeer(ctx, *rinfo, dinfo)
+
+	c.mx.Lock()
+	dedup.err = err
+	close(dedup.ch)
+	delete(c.activeDials, p)
+	c.mx.Unlock()
+
+	return conn, err
+}
+
+func (c *Client) dialPeer(ctx context.Context, relay, dest peer.AddrInfo) (*Conn, error) {
+	log.Debugf("dialing peer %s through relay %s", dest.ID, relay.ID)
+
+	if len(relay.Addrs) > 0 {
+		c.host.Peerstore().AddAddrs(relay.ID, relay.Addrs, peerstore.TempAddrTTL)
+	}
+
+	dialCtx, cancel := context.WithTimeout(ctx, DialRelayTimeout)
+	defer cancel()
+	s, err := c.host.NewStream(dialCtx, relay.ID, proto.ProtoIDv2Hop, proto.ProtoIDv1)
+	if err != nil {
+		return nil, fmt.Errorf("error opening hop stream to relay: %w", err)
+	}
+
+	switch s.Protocol() {
+	case proto.ProtoIDv2Hop:
+		return c.connectV2(s, dest)
+
+	case proto.ProtoIDv1:
+		return c.connectV1(s, dest)
+
+	default:
+		s.Reset()
+		return nil, fmt.Errorf("unexpected stream protocol: %s", s.Protocol())
+	}
+}
+
+func (c *Client) connectV2(s network.Stream, dest peer.AddrInfo) (*Conn, error) {
+	rd := util.NewDelimitedReader(s, maxMessageSize)
+	wr := util.NewDelimitedWriter(s)
+	defer rd.Close()
+
+	var msg pbv2.HopMessage
+
+	msg.Type = pbv2.HopMessage_CONNECT.Enum()
+	msg.Peer = util.PeerInfoToPeerV2(dest)
+
+	s.SetDeadline(time.Now().Add(DialTimeout))
+
+	err := wr.WriteMsg(&msg)
+	if err != nil {
+		s.Reset()
+		return nil, err
+	}
+
+	msg.Reset()
+
+	err = rd.ReadMsg(&msg)
+	if err != nil {
+		s.Reset()
+		return nil, err
+	}
+
+	s.SetDeadline(time.Time{})
+
+	if msg.GetType() != pbv2.HopMessage_STATUS {
+		s.Reset()
+		return nil, newRelayError("unexpected relay response; not a status message (%d)", msg.GetType())
+	}
+
+	status := msg.GetStatus()
+	if status != pbv2.Status_OK {
+		s.Reset()
+		return nil, newRelayError("error opening relay circuit: %s (%d)", pbv2.Status_name[int32(status)], status)
+	}
+
+	// check for a limit provided by the relay; if the limit is not nil, then this is a limited
+	// relay connection and we mark the connection as transient.
+	var stat network.Stat
+	if limit := msg.GetLimit(); limit != nil {
+		stat.Transient = true
+		stat.Extra = make(map[interface{}]interface{})
+		stat.Extra[StatLimitDuration] = time.Duration(limit.GetDuration()) * time.Second
+		stat.Extra[StatLimitData] = limit.GetData()
+	}
+
+	return &Conn{stream: s, remote: dest, stat: stat, client: c}, nil
+}
+
+func (c *Client) connectV1(s network.Stream, dest peer.AddrInfo) (*Conn, error) {
+	rd := util.NewDelimitedReader(s, maxMessageSize)
+	wr := util.NewDelimitedWriter(s)
+	defer rd.Close()
+
+	var msg pbv1.CircuitRelay
+
+	msg.Type = pbv1.CircuitRelay_HOP.Enum()
+	msg.SrcPeer = util.PeerInfoToPeerV1(c.host.Peerstore().PeerInfo(c.host.ID()))
+	msg.DstPeer = util.PeerInfoToPeerV1(dest)
+
+	s.SetDeadline(time.Now().Add(DialTimeout))
+
+	err := wr.WriteMsg(&msg)
+	if err != nil {
+		s.Reset()
+		return nil, err
+	}
+
+	msg.Reset()
+
+	err = rd.ReadMsg(&msg)
+	if err != nil {
+		s.Reset()
+		return nil, err
+	}
+
+	s.SetDeadline(time.Time{})
+
+	if msg.GetType() != pbv1.CircuitRelay_STATUS {
+		s.Reset()
+		return nil, newRelayError("unexpected relay response; not a status message (%d)", msg.GetType())
+	}
+
+	status := msg.GetCode()
+	if status != pbv1.CircuitRelay_SUCCESS {
+		s.Reset()
+		return nil, newRelayError("error opening relay circuit: %s (%d)", pbv1.CircuitRelay_Status_name[int32(status)], status)
+	}
+
+	return &Conn{stream: s, remote: dest, client: c}, nil
+}
diff --git a/p2p/host/circuitv2/client/handlers.go b/p2p/host/circuitv2/client/handlers.go
new file mode 100644
index 00000000..78704846
--- /dev/null
+++ b/p2p/host/circuitv2/client/handlers.go
@@ -0,0 +1,172 @@
+package client
+
+import (
+	"time"
+
+	pbv1 "github.com/libp2p/go-libp2p-circuit/pb"
+	pbv2 "github.com/libp2p/go-libp2p/p2p/host/circuitv2/pb"
+	"github.com/libp2p/go-libp2p/p2p/host/circuitv2/util"
+
+	"github.com/libp2p/go-libp2p-core/network"
+)
+
+var (
+	StreamTimeout = 1 * time.Minute
+	AcceptTimeout = 10 * time.Second
+)
+
+func (c *Client) handleStreamV2(s network.Stream) {
+	log.Debugf("new relay/v2 stream from: %s", s.Conn().RemotePeer())
+
+	s.SetReadDeadline(time.Now().Add(StreamTimeout))
+
+	rd := util.NewDelimitedReader(s, maxMessageSize)
+	defer rd.Close()
+
+	writeResponse := func(status pbv2.Status) error {
+		wr := util.NewDelimitedWriter(s)
+
+		var msg pbv2.StopMessage
+		msg.Type = pbv2.StopMessage_STATUS.Enum()
+		msg.Status = status.Enum()
+
+		return wr.WriteMsg(&msg)
+	}
+
+	handleError := func(status pbv2.Status) {
+		log.Debugf("protocol error: %s (%d)", pbv2.Status_name[int32(status)], status)
+		err := writeResponse(status)
+		if err != nil {
+			s.Reset()
+			log.Debugf("error writing circuit response: %s", err.Error())
+		} else {
+			s.Close()
+		}
+	}
+
+	var msg pbv2.StopMessage
+
+	err := rd.ReadMsg(&msg)
+	if err != nil {
+		handleError(pbv2.Status_MALFORMED_MESSAGE)
+		return
+	}
+	// reset stream deadline as message has been read
+	s.SetReadDeadline(time.Time{})
+
+	if msg.GetType() != pbv2.StopMessage_CONNECT {
+		handleError(pbv2.Status_UNEXPECTED_MESSAGE)
+		return
+	}
+
+	src, err := util.PeerToPeerInfoV2(msg.GetPeer())
+	if err != nil {
+		handleError(pbv2.Status_MALFORMED_MESSAGE)
+		return
+	}
+
+	// check for a limit provided by the relay; if the limit is not nil, then this is a limited
+	// relay connection and we mark the connection as transient.
+	var stat network.Stat
+	if limit := msg.GetLimit(); limit != nil {
+		stat.Transient = true
+		stat.Extra = make(map[interface{}]interface{})
+		stat.Extra[StatLimitDuration] = time.Duration(limit.GetDuration()) * time.Second
+		stat.Extra[StatLimitData] = limit.GetData()
+	}
+
+	log.Debugf("incoming relay connection from: %s", src.ID)
+
+	select {
+	case c.incoming <- accept{
+		conn: &Conn{stream: s, remote: src, stat: stat, client: c},
+		writeResponse: func() error {
+			return writeResponse(pbv2.Status_OK)
+		},
+	}:
+	case <-time.After(AcceptTimeout):
+		handleError(pbv2.Status_CONNECTION_FAILED)
+	}
+}
+
+func (c *Client) handleStreamV1(s network.Stream) {
+	log.Debugf("new relay/v1 stream from: %s", s.Conn().RemotePeer())
+
+	s.SetReadDeadline(time.Now().Add(StreamTimeout))
+
+	rd := util.NewDelimitedReader(s, maxMessageSize)
+	defer rd.Close()
+
+	writeResponse := func(status pbv1.CircuitRelay_Status) error {
+		wr := util.NewDelimitedWriter(s)
+
+		var msg pbv1.CircuitRelay
+		msg.Type = pbv1.CircuitRelay_STATUS.Enum()
+		msg.Code = status.Enum()
+
+		return wr.WriteMsg(&msg)
+	}
+
+	handleError := func(status pbv1.CircuitRelay_Status) {
+		log.Debugf("protocol error: %s (%d)", pbv1.CircuitRelay_Status_name[int32(status)], status)
+		err := writeResponse(status)
+		if err != nil {
+			s.Reset()
+			log.Debugf("error writing circuit response: %s", err.Error())
+		} else {
+			s.Close()
+		}
+	}
+
+	var msg pbv1.CircuitRelay
+
+	err := rd.ReadMsg(&msg)
+	if err != nil {
+		handleError(pbv1.CircuitRelay_MALFORMED_MESSAGE)
+		return
+	}
+	// reset stream deadline as message has been read
+	s.SetReadDeadline(time.Time{})
+
+	switch msg.GetType() {
+	case pbv1.CircuitRelay_STOP:
+
+	case pbv1.CircuitRelay_HOP:
+		handleError(pbv1.CircuitRelay_HOP_CANT_SPEAK_RELAY)
+		return
+
+	case pbv1.CircuitRelay_CAN_HOP:
+		handleError(pbv1.CircuitRelay_HOP_CANT_SPEAK_RELAY)
+		return
+
+	default:
+		log.Debugf("unexpected relay handshake: %d", msg.GetType())
+		handleError(pbv1.CircuitRelay_MALFORMED_MESSAGE)
+		return
+	}
+
+	src, err := util.PeerToPeerInfoV1(msg.GetSrcPeer())
+	if err != nil {
+		handleError(pbv1.CircuitRelay_STOP_SRC_MULTIADDR_INVALID)
+		return
+	}
+
+	dst, err := util.PeerToPeerInfoV1(msg.GetDstPeer())
+	if err != nil || dst.ID != c.host.ID() {
+		handleError(pbv1.CircuitRelay_STOP_DST_MULTIADDR_INVALID)
+		return
+	}
+
+	log.Debugf("incoming relay connection from: %s", src.ID)
+
+	select {
+	case c.incoming <- accept{
+		conn: &Conn{stream: s, remote: src, client: c},
+		writeResponse: func() error {
+			return writeResponse(pbv1.CircuitRelay_SUCCESS)
+		},
+	}:
+	case <-time.After(AcceptTimeout):
+		handleError(pbv1.CircuitRelay_STOP_RELAY_REFUSED)
+	}
+}
diff --git a/p2p/host/circuitv2/client/listen.go b/p2p/host/circuitv2/client/listen.go
new file mode 100644
index 00000000..fcfb9fa6
--- /dev/null
+++ b/p2p/host/circuitv2/client/listen.go
@@ -0,0 +1,54 @@
+package client
+
+import (
+	"net"
+
+	ma "github.com/multiformats/go-multiaddr"
+	manet "github.com/multiformats/go-multiaddr/net"
+)
+
+var _ manet.Listener = (*Listener)(nil)
+
+type Listener Client
+
+func (c *Client) Listener() *Listener {
+	return (*Listener)(c)
+}
+
+func (l *Listener) Accept() (manet.Conn, error) {
+	for {
+		select {
+		case evt := <-l.incoming:
+			err := evt.writeResponse()
+			if err != nil {
+				log.Debugf("error writing relay response: %s", err.Error())
+				evt.conn.stream.Reset()
+				continue
+			}
+
+			log.Debugf("accepted relay connection from %s through %s", evt.conn.remote.ID, evt.conn.RemoteMultiaddr())
+
+			evt.conn.tagHop()
+			return evt.conn, nil
+
+		case <-l.ctx.Done():
+			return nil, l.ctx.Err()
+		}
+	}
+}
+
+func (l *Listener) Addr() net.Addr {
+	return &NetAddr{
+		Relay:  "any",
+		Remote: "any",
+	}
+}
+
+func (l *Listener) Multiaddr() ma.Multiaddr {
+	return circuitAddr
+}
+
+func (l *Listener) Close() error {
+	// noop for now
+	return nil
+}
diff --git a/p2p/host/circuitv2/client/reservation.go b/p2p/host/circuitv2/client/reservation.go
new file mode 100644
index 00000000..479a1476
--- /dev/null
+++ b/p2p/host/circuitv2/client/reservation.go
@@ -0,0 +1,122 @@
+package client
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	pbv2 "github.com/libp2p/go-libp2p/p2p/host/circuitv2/pb"
+	"github.com/libp2p/go-libp2p/p2p/host/circuitv2/proto"
+	"github.com/libp2p/go-libp2p/p2p/host/circuitv2/util"
+
+	"github.com/libp2p/go-libp2p-core/host"
+	"github.com/libp2p/go-libp2p-core/peer"
+	"github.com/libp2p/go-libp2p-core/peerstore"
+	"github.com/libp2p/go-libp2p-core/record"
+
+	ma "github.com/multiformats/go-multiaddr"
+)
+
+var ReserveTimeout = time.Minute
+
+// Reservation is a struct carrying information about a relay/v2 slot reservation.
+type Reservation struct {
+	// Expiration is the expiration time of the reservation
+	Expiration time.Time
+	// Addrs contains the vouched public addresses of the reserving peer, which can be
+	// announced to the network
+	Addrs []ma.Multiaddr
+
+	// LimitDuration is the time limit for which the relay will keep a relayed connection
+	// open. If 0, there is no limit.
+	LimitDuration time.Duration
+	// LimitData is the number of bytes that the relay will relay in each direction before
+	// resetting a relayed connection.
+	LimitData uint64
+
+	// Voucher is a signed reservation voucher provided by the relay
+	Voucher *proto.ReservationVoucher
+}
+
+// Reserve reserves a slot in a relay and returns the reservation information.
+// Clients must reserve slots in order for the relay to relay connections to them.
+func Reserve(ctx context.Context, h host.Host, ai peer.AddrInfo) (*Reservation, error) {
+	if len(ai.Addrs) > 0 {
+		h.Peerstore().AddAddrs(ai.ID, ai.Addrs, peerstore.TempAddrTTL)
+	}
+
+	s, err := h.NewStream(ctx, ai.ID, proto.ProtoIDv2Hop)
+	if err != nil {
+		return nil, err
+	}
+	defer s.Close()
+
+	rd := util.NewDelimitedReader(s, maxMessageSize)
+	wr := util.NewDelimitedWriter(s)
+	defer rd.Close()
+
+	var msg pbv2.HopMessage
+	msg.Type = pbv2.HopMessage_RESERVE.Enum()
+
+	s.SetDeadline(time.Now().Add(ReserveTimeout))
+
+	if err := wr.WriteMsg(&msg); err != nil {
+		s.Reset()
+		return nil, fmt.Errorf("error writing reservation message: %w", err)
+	}
+
+	msg.Reset()
+
+	if err := rd.ReadMsg(&msg); err != nil {
+		s.Reset()
+		return nil, fmt.Errorf("error reading reservation response message: %w", err)
+	}
+
+	if msg.GetType() != pbv2.HopMessage_STATUS {
+		return nil, fmt.Errorf("unexpected relay response: not a status message (%d)", msg.GetType())
+	}
+
+	if status := msg.GetStatus(); status != pbv2.Status_OK {
+		return nil, fmt.Errorf("reservation failed: %s (%d)", pbv2.Status_name[int32(status)], status)
+	}
+
+	rsvp := msg.GetReservation()
+	if rsvp == nil {
+		return nil, fmt.Errorf("missing reservation info")
+	}
+
+	result := &Reservation{}
+	result.Expiration = time.Unix(int64(rsvp.GetExpire()), 0)
+
+	for _, ab := range rsvp.GetAddrs() {
+		a, err := ma.NewMultiaddrBytes(ab)
+		if err != nil {
+			log.Warnf("ignoring unparsable relay address: %s", err)
+			continue
+		}
+		result.Addrs = append(result.Addrs, a)
+	}
+
+	voucherBytes := rsvp.GetVoucher()
+	if voucherBytes != nil {
+		_, rec, err := record.ConsumeEnvelope(voucherBytes, proto.RecordDomain)
+		if err != nil {
+			return nil, fmt.Errorf("error consuming voucher envelope: %w", err)
+		}
+
+		voucher, ok := rec.(*proto.ReservationVoucher)
+		if !ok {
+			return nil, fmt.Errorf("unexpected voucher record type: %+T", rec)
+		}
+
+		result.Voucher = voucher
+	}
+
+	limit := msg.GetLimit()
+	if limit != nil {
+		result.LimitDuration = time.Duration(limit.GetDuration()) * time.Second
+		result.LimitData = limit.GetData()
+	}
+
+	return result, nil
+}
diff --git a/p2p/host/circuitv2/client/transport.go b/p2p/host/circuitv2/client/transport.go
new file mode 100644
index 00000000..406188cf
--- /dev/null
+++ b/p2p/host/circuitv2/client/transport.go
@@ -0,0 +1,80 @@
+package client
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/libp2p/go-libp2p-core/host"
+	"github.com/libp2p/go-libp2p-core/peer"
+	"github.com/libp2p/go-libp2p-core/transport"
+
+	tptu "github.com/libp2p/go-libp2p-transport-upgrader"
+	ma "github.com/multiformats/go-multiaddr"
+)
+
+var circuitProtocol = ma.ProtocolWithCode(ma.P_CIRCUIT)
+var circuitAddr = ma.Cast(circuitProtocol.VCode)
+
+// AddTransport constructs a new p2p-circuit/v2 client and adds it as a transport to the
+// host network
+func AddTransport(ctx context.Context, h host.Host, upgrader *tptu.Upgrader) error {
+	n, ok := h.Network().(transport.TransportNetwork)
+	if !ok {
+		return fmt.Errorf("%v is not a transport network", h.Network())
+	}
+
+	c, err := New(ctx, h, upgrader)
+	if err != nil {
+		return fmt.Errorf("error constructing circuit client: %w", err)
+	}
+
+	err = n.AddTransport(c)
+	if err != nil {
+		return fmt.Errorf("error adding circuit transport: %w", err)
+	}
+
+	err = n.Listen(circuitAddr)
+	if err != nil {
+		return fmt.Errorf("error listening to circuit addr: %w", err)
+	}
+
+	c.Start()
+
+	return nil
+}
+
+// Transport interface
+var _ transport.Transport = (*Client)(nil)
+
+func (c *Client) Dial(ctx context.Context, a ma.Multiaddr, p peer.ID) (transport.CapableConn, error) {
+	conn, err := c.dial(ctx, a, p)
+	if err != nil {
+		return nil, err
+	}
+
+	conn.tagHop()
+
+	return c.upgrader.UpgradeOutbound(ctx, c, conn, p)
+}
+
+func (c *Client) CanDial(addr ma.Multiaddr) bool {
+	_, err := addr.ValueForProtocol(ma.P_CIRCUIT)
+	return err == nil
+}
+
+func (c *Client) Listen(addr ma.Multiaddr) (transport.Listener, error) {
+	// TODO connect to the relay and reserve slot if specified
+	if _, err := addr.ValueForProtocol(ma.P_CIRCUIT); err != nil {
+		return nil, err
+	}
+
+	return c.upgrader.UpgradeListener(c, c.Listener()), nil
+}
+
+func (c *Client) Protocols() []int {
+	return []int{ma.P_CIRCUIT}
+}
+
+func (c *Client) Proxy() bool {
+	return true
+}
diff --git a/p2p/host/circuitv2/pb/Makefile b/p2p/host/circuitv2/pb/Makefile
new file mode 100644
index 00000000..c360a6fb
--- /dev/null
+++ b/p2p/host/circuitv2/pb/Makefile
@@ -0,0 +1,11 @@
+PB = $(wildcard *.proto)
+GO = $(PB:.proto=.pb.go)
+
+all: $(GO)
+
+%.pb.go: %.proto
+		protoc  --gogofast_out=. $<
+
+clean:
+		rm -f *.pb.go
+		rm -f *.go
diff --git a/p2p/host/circuitv2/pb/circuit.pb.go b/p2p/host/circuitv2/pb/circuit.pb.go
new file mode 100644
index 00000000..f908d0d7
--- /dev/null
+++ b/p2p/host/circuitv2/pb/circuit.pb.go
@@ -0,0 +1,1766 @@
+// Code generated by protoc-gen-gogo. DO NOT EDIT.
+// source: circuit.proto
+
+package circuit_pb
+
+import (
+	fmt "fmt"
+	github_com_gogo_protobuf_proto "github.com/gogo/protobuf/proto"
+	proto "github.com/gogo/protobuf/proto"
+	io "io"
+	math "math"
+	math_bits "math/bits"
+)
+
+// Reference imports to suppress errors if they are not otherwise used.
+var _ = proto.Marshal
+var _ = fmt.Errorf
+var _ = math.Inf
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the proto package it is being compiled against.
+// A compilation error at this line likely means your copy of the
+// proto package needs to be updated.
+const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package
+
+type Status int32
+
+const (
+	Status_OK                      Status = 100
+	Status_RESERVATION_REFUSED     Status = 200
+	Status_RESOURCE_LIMIT_EXCEEDED Status = 201
+	Status_PERMISSION_DENIED       Status = 202
+	Status_CONNECTION_FAILED       Status = 203
+	Status_NO_RESERVATION          Status = 204
+	Status_MALFORMED_MESSAGE       Status = 400
+	Status_UNEXPECTED_MESSAGE      Status = 401
+)
+
+var Status_name = map[int32]string{
+	100: "OK",
+	200: "RESERVATION_REFUSED",
+	201: "RESOURCE_LIMIT_EXCEEDED",
+	202: "PERMISSION_DENIED",
+	203: "CONNECTION_FAILED",
+	204: "NO_RESERVATION",
+	400: "MALFORMED_MESSAGE",
+	401: "UNEXPECTED_MESSAGE",
+}
+
+var Status_value = map[string]int32{
+	"OK":                      100,
+	"RESERVATION_REFUSED":     200,
+	"RESOURCE_LIMIT_EXCEEDED": 201,
+	"PERMISSION_DENIED":       202,
+	"CONNECTION_FAILED":       203,
+	"NO_RESERVATION":          204,
+	"MALFORMED_MESSAGE":       400,
+	"UNEXPECTED_MESSAGE":      401,
+}
+
+func (x Status) Enum() *Status {
+	p := new(Status)
+	*p = x
+	return p
+}
+
+func (x Status) String() string {
+	return proto.EnumName(Status_name, int32(x))
+}
+
+func (x *Status) UnmarshalJSON(data []byte) error {
+	value, err := proto.UnmarshalJSONEnum(Status_value, data, "Status")
+	if err != nil {
+		return err
+	}
+	*x = Status(value)
+	return nil
+}
+
+func (Status) EnumDescriptor() ([]byte, []int) {
+	return fileDescriptor_ed01bbc211f15e47, []int{0}
+}
+
+type HopMessage_Type int32
+
+const (
+	HopMessage_RESERVE HopMessage_Type = 0
+	HopMessage_CONNECT HopMessage_Type = 1
+	HopMessage_STATUS  HopMessage_Type = 2
+)
+
+var HopMessage_Type_name = map[int32]string{
+	0: "RESERVE",
+	1: "CONNECT",
+	2: "STATUS",
+}
+
+var HopMessage_Type_value = map[string]int32{
+	"RESERVE": 0,
+	"CONNECT": 1,
+	"STATUS":  2,
+}
+
+func (x HopMessage_Type) Enum() *HopMessage_Type {
+	p := new(HopMessage_Type)
+	*p = x
+	return p
+}
+
+func (x HopMessage_Type) String() string {
+	return proto.EnumName(HopMessage_Type_name, int32(x))
+}
+
+func (x *HopMessage_Type) UnmarshalJSON(data []byte) error {
+	value, err := proto.UnmarshalJSONEnum(HopMessage_Type_value, data, "HopMessage_Type")
+	if err != nil {
+		return err
+	}
+	*x = HopMessage_Type(value)
+	return nil
+}
+
+func (HopMessage_Type) EnumDescriptor() ([]byte, []int) {
+	return fileDescriptor_ed01bbc211f15e47, []int{0, 0}
+}
+
+type StopMessage_Type int32
+
+const (
+	StopMessage_CONNECT StopMessage_Type = 0
+	StopMessage_STATUS  StopMessage_Type = 1
+)
+
+var StopMessage_Type_name = map[int32]string{
+	0: "CONNECT",
+	1: "STATUS",
+}
+
+var StopMessage_Type_value = map[string]int32{
+	"CONNECT": 0,
+	"STATUS":  1,
+}
+
+func (x StopMessage_Type) Enum() *StopMessage_Type {
+	p := new(StopMessage_Type)
+	*p = x
+	return p
+}
+
+func (x StopMessage_Type) String() string {
+	return proto.EnumName(StopMessage_Type_name, int32(x))
+}
+
+func (x *StopMessage_Type) UnmarshalJSON(data []byte) error {
+	value, err := proto.UnmarshalJSONEnum(StopMessage_Type_value, data, "StopMessage_Type")
+	if err != nil {
+		return err
+	}
+	*x = StopMessage_Type(value)
+	return nil
+}
+
+func (StopMessage_Type) EnumDescriptor() ([]byte, []int) {
+	return fileDescriptor_ed01bbc211f15e47, []int{1, 0}
+}
+
+type HopMessage struct {
+	Type                 *HopMessage_Type `protobuf:"varint,1,req,name=type,enum=circuit.pb.HopMessage_Type" json:"type,omitempty"`
+	Peer                 *Peer            `protobuf:"bytes,2,opt,name=peer" json:"peer,omitempty"`
+	Reservation          *Reservation     `protobuf:"bytes,3,opt,name=reservation" json:"reservation,omitempty"`
+	Limit                *Limit           `protobuf:"bytes,4,opt,name=limit" json:"limit,omitempty"`
+	Status               *Status          `protobuf:"varint,5,opt,name=status,enum=circuit.pb.Status" json:"status,omitempty"`
+	XXX_NoUnkeyedLiteral struct{}         `json:"-"`
+	XXX_unrecognized     []byte           `json:"-"`
+	XXX_sizecache        int32            `json:"-"`
+}
+
+func (m *HopMessage) Reset()         { *m = HopMessage{} }
+func (m *HopMessage) String() string { return proto.CompactTextString(m) }
+func (*HopMessage) ProtoMessage()    {}
+func (*HopMessage) Descriptor() ([]byte, []int) {
+	return fileDescriptor_ed01bbc211f15e47, []int{0}
+}
+func (m *HopMessage) XXX_Unmarshal(b []byte) error {
+	return m.Unmarshal(b)
+}
+func (m *HopMessage) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	if deterministic {
+		return xxx_messageInfo_HopMessage.Marshal(b, m, deterministic)
+	} else {
+		b = b[:cap(b)]
+		n, err := m.MarshalToSizedBuffer(b)
+		if err != nil {
+			return nil, err
+		}
+		return b[:n], nil
+	}
+}
+func (m *HopMessage) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_HopMessage.Merge(m, src)
+}
+func (m *HopMessage) XXX_Size() int {
+	return m.Size()
+}
+func (m *HopMessage) XXX_DiscardUnknown() {
+	xxx_messageInfo_HopMessage.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_HopMessage proto.InternalMessageInfo
+
+func (m *HopMessage) GetType() HopMessage_Type {
+	if m != nil && m.Type != nil {
+		return *m.Type
+	}
+	return HopMessage_RESERVE
+}
+
+func (m *HopMessage) GetPeer() *Peer {
+	if m != nil {
+		return m.Peer
+	}
+	return nil
+}
+
+func (m *HopMessage) GetReservation() *Reservation {
+	if m != nil {
+		return m.Reservation
+	}
+	return nil
+}
+
+func (m *HopMessage) GetLimit() *Limit {
+	if m != nil {
+		return m.Limit
+	}
+	return nil
+}
+
+func (m *HopMessage) GetStatus() Status {
+	if m != nil && m.Status != nil {
+		return *m.Status
+	}
+	return Status_OK
+}
+
+type StopMessage struct {
+	Type                 *StopMessage_Type `protobuf:"varint,1,req,name=type,enum=circuit.pb.StopMessage_Type" json:"type,omitempty"`
+	Peer                 *Peer             `protobuf:"bytes,2,opt,name=peer" json:"peer,omitempty"`
+	Limit                *Limit            `protobuf:"bytes,3,opt,name=limit" json:"limit,omitempty"`
+	Status               *Status           `protobuf:"varint,4,opt,name=status,enum=circuit.pb.Status" json:"status,omitempty"`
+	XXX_NoUnkeyedLiteral struct{}          `json:"-"`
+	XXX_unrecognized     []byte            `json:"-"`
+	XXX_sizecache        int32             `json:"-"`
+}
+
+func (m *StopMessage) Reset()         { *m = StopMessage{} }
+func (m *StopMessage) String() string { return proto.CompactTextString(m) }
+func (*StopMessage) ProtoMessage()    {}
+func (*StopMessage) Descriptor() ([]byte, []int) {
+	return fileDescriptor_ed01bbc211f15e47, []int{1}
+}
+func (m *StopMessage) XXX_Unmarshal(b []byte) error {
+	return m.Unmarshal(b)
+}
+func (m *StopMessage) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	if deterministic {
+		return xxx_messageInfo_StopMessage.Marshal(b, m, deterministic)
+	} else {
+		b = b[:cap(b)]
+		n, err := m.MarshalToSizedBuffer(b)
+		if err != nil {
+			return nil, err
+		}
+		return b[:n], nil
+	}
+}
+func (m *StopMessage) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_StopMessage.Merge(m, src)
+}
+func (m *StopMessage) XXX_Size() int {
+	return m.Size()
+}
+func (m *StopMessage) XXX_DiscardUnknown() {
+	xxx_messageInfo_StopMessage.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_StopMessage proto.InternalMessageInfo
+
+func (m *StopMessage) GetType() StopMessage_Type {
+	if m != nil && m.Type != nil {
+		return *m.Type
+	}
+	return StopMessage_CONNECT
+}
+
+func (m *StopMessage) GetPeer() *Peer {
+	if m != nil {
+		return m.Peer
+	}
+	return nil
+}
+
+func (m *StopMessage) GetLimit() *Limit {
+	if m != nil {
+		return m.Limit
+	}
+	return nil
+}
+
+func (m *StopMessage) GetStatus() Status {
+	if m != nil && m.Status != nil {
+		return *m.Status
+	}
+	return Status_OK
+}
+
+type Peer struct {
+	Id                   []byte   `protobuf:"bytes,1,req,name=id" json:"id,omitempty"`
+	Addrs                [][]byte `protobuf:"bytes,2,rep,name=addrs" json:"addrs,omitempty"`
+	XXX_NoUnkeyedLiteral struct{} `json:"-"`
+	XXX_unrecognized     []byte   `json:"-"`
+	XXX_sizecache        int32    `json:"-"`
+}
+
+func (m *Peer) Reset()         { *m = Peer{} }
+func (m *Peer) String() string { return proto.CompactTextString(m) }
+func (*Peer) ProtoMessage()    {}
+func (*Peer) Descriptor() ([]byte, []int) {
+	return fileDescriptor_ed01bbc211f15e47, []int{2}
+}
+func (m *Peer) XXX_Unmarshal(b []byte) error {
+	return m.Unmarshal(b)
+}
+func (m *Peer) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	if deterministic {
+		return xxx_messageInfo_Peer.Marshal(b, m, deterministic)
+	} else {
+		b = b[:cap(b)]
+		n, err := m.MarshalToSizedBuffer(b)
+		if err != nil {
+			return nil, err
+		}
+		return b[:n], nil
+	}
+}
+func (m *Peer) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_Peer.Merge(m, src)
+}
+func (m *Peer) XXX_Size() int {
+	return m.Size()
+}
+func (m *Peer) XXX_DiscardUnknown() {
+	xxx_messageInfo_Peer.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_Peer proto.InternalMessageInfo
+
+func (m *Peer) GetId() []byte {
+	if m != nil {
+		return m.Id
+	}
+	return nil
+}
+
+func (m *Peer) GetAddrs() [][]byte {
+	if m != nil {
+		return m.Addrs
+	}
+	return nil
+}
+
+type Reservation struct {
+	Expire               *uint64  `protobuf:"varint,1,opt,name=expire" json:"expire,omitempty"`
+	Addrs                [][]byte `protobuf:"bytes,2,rep,name=addrs" json:"addrs,omitempty"`
+	Voucher              []byte   `protobuf:"bytes,3,opt,name=voucher" json:"voucher,omitempty"`
+	XXX_NoUnkeyedLiteral struct{} `json:"-"`
+	XXX_unrecognized     []byte   `json:"-"`
+	XXX_sizecache        int32    `json:"-"`
+}
+
+func (m *Reservation) Reset()         { *m = Reservation{} }
+func (m *Reservation) String() string { return proto.CompactTextString(m) }
+func (*Reservation) ProtoMessage()    {}
+func (*Reservation) Descriptor() ([]byte, []int) {
+	return fileDescriptor_ed01bbc211f15e47, []int{3}
+}
+func (m *Reservation) XXX_Unmarshal(b []byte) error {
+	return m.Unmarshal(b)
+}
+func (m *Reservation) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	if deterministic {
+		return xxx_messageInfo_Reservation.Marshal(b, m, deterministic)
+	} else {
+		b = b[:cap(b)]
+		n, err := m.MarshalToSizedBuffer(b)
+		if err != nil {
+			return nil, err
+		}
+		return b[:n], nil
+	}
+}
+func (m *Reservation) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_Reservation.Merge(m, src)
+}
+func (m *Reservation) XXX_Size() int {
+	return m.Size()
+}
+func (m *Reservation) XXX_DiscardUnknown() {
+	xxx_messageInfo_Reservation.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_Reservation proto.InternalMessageInfo
+
+func (m *Reservation) GetExpire() uint64 {
+	if m != nil && m.Expire != nil {
+		return *m.Expire
+	}
+	return 0
+}
+
+func (m *Reservation) GetAddrs() [][]byte {
+	if m != nil {
+		return m.Addrs
+	}
+	return nil
+}
+
+func (m *Reservation) GetVoucher() []byte {
+	if m != nil {
+		return m.Voucher
+	}
+	return nil
+}
+
+type Limit struct {
+	Duration             *uint32  `protobuf:"varint,1,opt,name=duration" json:"duration,omitempty"`
+	Data                 *uint64  `protobuf:"varint,2,opt,name=data" json:"data,omitempty"`
+	XXX_NoUnkeyedLiteral struct{} `json:"-"`
+	XXX_unrecognized     []byte   `json:"-"`
+	XXX_sizecache        int32    `json:"-"`
+}
+
+func (m *Limit) Reset()         { *m = Limit{} }
+func (m *Limit) String() string { return proto.CompactTextString(m) }
+func (*Limit) ProtoMessage()    {}
+func (*Limit) Descriptor() ([]byte, []int) {
+	return fileDescriptor_ed01bbc211f15e47, []int{4}
+}
+func (m *Limit) XXX_Unmarshal(b []byte) error {
+	return m.Unmarshal(b)
+}
+func (m *Limit) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	if deterministic {
+		return xxx_messageInfo_Limit.Marshal(b, m, deterministic)
+	} else {
+		b = b[:cap(b)]
+		n, err := m.MarshalToSizedBuffer(b)
+		if err != nil {
+			return nil, err
+		}
+		return b[:n], nil
+	}
+}
+func (m *Limit) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_Limit.Merge(m, src)
+}
+func (m *Limit) XXX_Size() int {
+	return m.Size()
+}
+func (m *Limit) XXX_DiscardUnknown() {
+	xxx_messageInfo_Limit.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_Limit proto.InternalMessageInfo
+
+func (m *Limit) GetDuration() uint32 {
+	if m != nil && m.Duration != nil {
+		return *m.Duration
+	}
+	return 0
+}
+
+func (m *Limit) GetData() uint64 {
+	if m != nil && m.Data != nil {
+		return *m.Data
+	}
+	return 0
+}
+
+func init() {
+	proto.RegisterEnum("circuit.pb.Status", Status_name, Status_value)
+	proto.RegisterEnum("circuit.pb.HopMessage_Type", HopMessage_Type_name, HopMessage_Type_value)
+	proto.RegisterEnum("circuit.pb.StopMessage_Type", StopMessage_Type_name, StopMessage_Type_value)
+	proto.RegisterType((*HopMessage)(nil), "circuit.pb.HopMessage")
+	proto.RegisterType((*StopMessage)(nil), "circuit.pb.StopMessage")
+	proto.RegisterType((*Peer)(nil), "circuit.pb.Peer")
+	proto.RegisterType((*Reservation)(nil), "circuit.pb.Reservation")
+	proto.RegisterType((*Limit)(nil), "circuit.pb.Limit")
+}
+
+func init() { proto.RegisterFile("circuit.proto", fileDescriptor_ed01bbc211f15e47) }
+
+var fileDescriptor_ed01bbc211f15e47 = []byte{
+	// 516 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x92, 0xcf, 0x8a, 0xd3, 0x50,
+	0x14, 0xc6, 0xe7, 0xa6, 0x69, 0x47, 0x4e, 0x3b, 0xe5, 0xce, 0x19, 0x99, 0x09, 0x3a, 0x8c, 0xa5,
+	0x08, 0x96, 0x41, 0xaa, 0x74, 0x23, 0x2e, 0x6b, 0x73, 0xaa, 0xc1, 0x26, 0x29, 0xf7, 0xa6, 0x32,
+	0xbb, 0x12, 0x9b, 0x8b, 0x06, 0xd4, 0x86, 0x24, 0x1d, 0x9c, 0xb7, 0xd0, 0x47, 0xf0, 0x45, 0x5c,
+	0x8f, 0x7f, 0x16, 0xee, 0xdd, 0x48, 0x9f, 0x44, 0x72, 0xdb, 0x69, 0x33, 0x20, 0x28, 0xb8, 0xcb,
+	0x77, 0xbf, 0xef, 0xe4, 0xe6, 0xf7, 0x9d, 0xc0, 0xde, 0x2c, 0x4e, 0x67, 0x8b, 0x38, 0xef, 0x26,
+	0xe9, 0x3c, 0x9f, 0x23, 0x6c, 0xe4, 0xcb, 0xf6, 0x27, 0x03, 0xe0, 0xd9, 0x3c, 0x71, 0x55, 0x96,
+	0x85, 0xaf, 0x14, 0x3e, 0x00, 0x33, 0xbf, 0x48, 0x94, 0xc5, 0x5a, 0x46, 0xa7, 0xd9, 0xbb, 0xdd,
+	0xdd, 0x26, 0xbb, 0xdb, 0x54, 0x37, 0xb8, 0x48, 0x94, 0xd0, 0x41, 0xbc, 0x0b, 0x66, 0xa2, 0x54,
+	0x6a, 0x19, 0x2d, 0xd6, 0xa9, 0xf7, 0x78, 0x79, 0x60, 0xac, 0x54, 0x2a, 0xb4, 0x8b, 0x8f, 0xa1,
+	0x9e, 0xaa, 0x4c, 0xa5, 0xe7, 0x61, 0x1e, 0xcf, 0xdf, 0x59, 0x15, 0x1d, 0x3e, 0x2a, 0x87, 0xc5,
+	0xd6, 0x16, 0xe5, 0x2c, 0xde, 0x83, 0xea, 0x9b, 0xf8, 0x6d, 0x9c, 0x5b, 0xa6, 0x1e, 0xda, 0x2f,
+	0x0f, 0x8d, 0x0a, 0x43, 0xac, 0x7c, 0x3c, 0x85, 0x5a, 0x96, 0x87, 0xf9, 0x22, 0xb3, 0xaa, 0x2d,
+	0xd6, 0x69, 0xf6, 0xb0, 0x9c, 0x94, 0xda, 0x11, 0xeb, 0x44, 0xfb, 0x3e, 0x98, 0x05, 0x03, 0xd6,
+	0x61, 0x57, 0x90, 0x24, 0xf1, 0x82, 0xf8, 0x4e, 0x21, 0x06, 0xbe, 0xe7, 0xd1, 0x20, 0xe0, 0x0c,
+	0x01, 0x6a, 0x32, 0xe8, 0x07, 0x13, 0xc9, 0x8d, 0xf6, 0x4f, 0x06, 0x75, 0x99, 0x6f, 0x4b, 0x7a,
+	0x78, 0xad, 0xa4, 0xe3, 0xeb, 0xf7, 0xfc, 0x47, 0x4b, 0x1b, 0xd4, 0xca, 0x3f, 0xa3, 0x9a, 0x7f,
+	0x45, 0xbd, 0xb3, 0x45, 0xbd, 0xa2, 0xdb, 0x29, 0xd1, 0xb1, 0xa2, 0x8b, 0xe2, 0x1b, 0xb0, 0x09,
+	0x46, 0x1c, 0x69, 0xa6, 0x86, 0x30, 0xe2, 0x08, 0x6f, 0x42, 0x35, 0x8c, 0xa2, 0x34, 0xb3, 0x8c,
+	0x56, 0xa5, 0xd3, 0x10, 0x2b, 0xd1, 0x9e, 0x40, 0xbd, 0xb4, 0x2a, 0x3c, 0x84, 0x9a, 0x7a, 0x9f,
+	0xc4, 0x69, 0x51, 0x06, 0xeb, 0x98, 0x62, 0xad, 0xfe, 0x3c, 0x8c, 0x16, 0xec, 0x9e, 0xcf, 0x17,
+	0xb3, 0xd7, 0x2a, 0xd5, 0x88, 0x0d, 0x71, 0x25, 0xdb, 0x8f, 0xa0, 0xaa, 0x09, 0xf1, 0x16, 0xdc,
+	0x88, 0x16, 0xe9, 0xea, 0x37, 0x29, 0x5e, 0xb9, 0x27, 0x36, 0x1a, 0x11, 0xcc, 0x28, 0xcc, 0x43,
+	0xdd, 0xa2, 0x29, 0xf4, 0xf3, 0xe9, 0x67, 0x06, 0xb5, 0x15, 0x31, 0xd6, 0xc0, 0xf0, 0x9f, 0xf3,
+	0x08, 0x2d, 0x38, 0x58, 0x2d, 0xb5, 0x1f, 0x38, 0xbe, 0x37, 0x15, 0x34, 0x9c, 0x48, 0xb2, 0xf9,
+	0x25, 0xc3, 0x63, 0x38, 0x12, 0x24, 0xfd, 0x89, 0x18, 0xd0, 0x74, 0xe4, 0xb8, 0x4e, 0x30, 0xa5,
+	0xb3, 0x01, 0x91, 0x4d, 0x36, 0xff, 0xc2, 0xf0, 0x10, 0xf6, 0xc7, 0x24, 0x5c, 0x47, 0xca, 0x62,
+	0xcc, 0x26, 0xcf, 0x21, 0x9b, 0x7f, 0xd5, 0xe7, 0xeb, 0xe6, 0x8a, 0xf3, 0x61, 0xdf, 0x19, 0x91,
+	0xcd, 0xbf, 0x31, 0x3c, 0x80, 0xa6, 0xe7, 0x4f, 0x4b, 0x57, 0xf1, 0xef, 0x3a, 0xec, 0xf6, 0x47,
+	0x43, 0x5f, 0xb8, 0x64, 0x4f, 0x5d, 0x92, 0xb2, 0xff, 0x94, 0xf8, 0x87, 0x0a, 0x1e, 0x01, 0x4e,
+	0x3c, 0x3a, 0x1b, 0xd3, 0x20, 0x28, 0x19, 0x1f, 0x2b, 0x4f, 0x1a, 0x97, 0xcb, 0x13, 0xf6, 0x63,
+	0x79, 0xc2, 0x7e, 0x2d, 0x4f, 0xd8, 0xef, 0x00, 0x00, 0x00, 0xff, 0xff, 0xa7, 0x24, 0x77, 0x69,
+	0xaa, 0x03, 0x00, 0x00,
+}
+
+func (m *HopMessage) Marshal() (dAtA []byte, err error) {
+	size := m.Size()
+	dAtA = make([]byte, size)
+	n, err := m.MarshalToSizedBuffer(dAtA[:size])
+	if err != nil {
+		return nil, err
+	}
+	return dAtA[:n], nil
+}
+
+func (m *HopMessage) MarshalTo(dAtA []byte) (int, error) {
+	size := m.Size()
+	return m.MarshalToSizedBuffer(dAtA[:size])
+}
+
+func (m *HopMessage) MarshalToSizedBuffer(dAtA []byte) (int, error) {
+	i := len(dAtA)
+	_ = i
+	var l int
+	_ = l
+	if m.XXX_unrecognized != nil {
+		i -= len(m.XXX_unrecognized)
+		copy(dAtA[i:], m.XXX_unrecognized)
+	}
+	if m.Status != nil {
+		i = encodeVarintCircuit(dAtA, i, uint64(*m.Status))
+		i--
+		dAtA[i] = 0x28
+	}
+	if m.Limit != nil {
+		{
+			size, err := m.Limit.MarshalToSizedBuffer(dAtA[:i])
+			if err != nil {
+				return 0, err
+			}
+			i -= size
+			i = encodeVarintCircuit(dAtA, i, uint64(size))
+		}
+		i--
+		dAtA[i] = 0x22
+	}
+	if m.Reservation != nil {
+		{
+			size, err := m.Reservation.MarshalToSizedBuffer(dAtA[:i])
+			if err != nil {
+				return 0, err
+			}
+			i -= size
+			i = encodeVarintCircuit(dAtA, i, uint64(size))
+		}
+		i--
+		dAtA[i] = 0x1a
+	}
+	if m.Peer != nil {
+		{
+			size, err := m.Peer.MarshalToSizedBuffer(dAtA[:i])
+			if err != nil {
+				return 0, err
+			}
+			i -= size
+			i = encodeVarintCircuit(dAtA, i, uint64(size))
+		}
+		i--
+		dAtA[i] = 0x12
+	}
+	if m.Type == nil {
+		return 0, github_com_gogo_protobuf_proto.NewRequiredNotSetError("type")
+	} else {
+		i = encodeVarintCircuit(dAtA, i, uint64(*m.Type))
+		i--
+		dAtA[i] = 0x8
+	}
+	return len(dAtA) - i, nil
+}
+
+func (m *StopMessage) Marshal() (dAtA []byte, err error) {
+	size := m.Size()
+	dAtA = make([]byte, size)
+	n, err := m.MarshalToSizedBuffer(dAtA[:size])
+	if err != nil {
+		return nil, err
+	}
+	return dAtA[:n], nil
+}
+
+func (m *StopMessage) MarshalTo(dAtA []byte) (int, error) {
+	size := m.Size()
+	return m.MarshalToSizedBuffer(dAtA[:size])
+}
+
+func (m *StopMessage) MarshalToSizedBuffer(dAtA []byte) (int, error) {
+	i := len(dAtA)
+	_ = i
+	var l int
+	_ = l
+	if m.XXX_unrecognized != nil {
+		i -= len(m.XXX_unrecognized)
+		copy(dAtA[i:], m.XXX_unrecognized)
+	}
+	if m.Status != nil {
+		i = encodeVarintCircuit(dAtA, i, uint64(*m.Status))
+		i--
+		dAtA[i] = 0x20
+	}
+	if m.Limit != nil {
+		{
+			size, err := m.Limit.MarshalToSizedBuffer(dAtA[:i])
+			if err != nil {
+				return 0, err
+			}
+			i -= size
+			i = encodeVarintCircuit(dAtA, i, uint64(size))
+		}
+		i--
+		dAtA[i] = 0x1a
+	}
+	if m.Peer != nil {
+		{
+			size, err := m.Peer.MarshalToSizedBuffer(dAtA[:i])
+			if err != nil {
+				return 0, err
+			}
+			i -= size
+			i = encodeVarintCircuit(dAtA, i, uint64(size))
+		}
+		i--
+		dAtA[i] = 0x12
+	}
+	if m.Type == nil {
+		return 0, github_com_gogo_protobuf_proto.NewRequiredNotSetError("type")
+	} else {
+		i = encodeVarintCircuit(dAtA, i, uint64(*m.Type))
+		i--
+		dAtA[i] = 0x8
+	}
+	return len(dAtA) - i, nil
+}
+
+func (m *Peer) Marshal() (dAtA []byte, err error) {
+	size := m.Size()
+	dAtA = make([]byte, size)
+	n, err := m.MarshalToSizedBuffer(dAtA[:size])
+	if err != nil {
+		return nil, err
+	}
+	return dAtA[:n], nil
+}
+
+func (m *Peer) MarshalTo(dAtA []byte) (int, error) {
+	size := m.Size()
+	return m.MarshalToSizedBuffer(dAtA[:size])
+}
+
+func (m *Peer) MarshalToSizedBuffer(dAtA []byte) (int, error) {
+	i := len(dAtA)
+	_ = i
+	var l int
+	_ = l
+	if m.XXX_unrecognized != nil {
+		i -= len(m.XXX_unrecognized)
+		copy(dAtA[i:], m.XXX_unrecognized)
+	}
+	if len(m.Addrs) > 0 {
+		for iNdEx := len(m.Addrs) - 1; iNdEx >= 0; iNdEx-- {
+			i -= len(m.Addrs[iNdEx])
+			copy(dAtA[i:], m.Addrs[iNdEx])
+			i = encodeVarintCircuit(dAtA, i, uint64(len(m.Addrs[iNdEx])))
+			i--
+			dAtA[i] = 0x12
+		}
+	}
+	if m.Id == nil {
+		return 0, github_com_gogo_protobuf_proto.NewRequiredNotSetError("id")
+	} else {
+		i -= len(m.Id)
+		copy(dAtA[i:], m.Id)
+		i = encodeVarintCircuit(dAtA, i, uint64(len(m.Id)))
+		i--
+		dAtA[i] = 0xa
+	}
+	return len(dAtA) - i, nil
+}
+
+func (m *Reservation) Marshal() (dAtA []byte, err error) {
+	size := m.Size()
+	dAtA = make([]byte, size)
+	n, err := m.MarshalToSizedBuffer(dAtA[:size])
+	if err != nil {
+		return nil, err
+	}
+	return dAtA[:n], nil
+}
+
+func (m *Reservation) MarshalTo(dAtA []byte) (int, error) {
+	size := m.Size()
+	return m.MarshalToSizedBuffer(dAtA[:size])
+}
+
+func (m *Reservation) MarshalToSizedBuffer(dAtA []byte) (int, error) {
+	i := len(dAtA)
+	_ = i
+	var l int
+	_ = l
+	if m.XXX_unrecognized != nil {
+		i -= len(m.XXX_unrecognized)
+		copy(dAtA[i:], m.XXX_unrecognized)
+	}
+	if m.Voucher != nil {
+		i -= len(m.Voucher)
+		copy(dAtA[i:], m.Voucher)
+		i = encodeVarintCircuit(dAtA, i, uint64(len(m.Voucher)))
+		i--
+		dAtA[i] = 0x1a
+	}
+	if len(m.Addrs) > 0 {
+		for iNdEx := len(m.Addrs) - 1; iNdEx >= 0; iNdEx-- {
+			i -= len(m.Addrs[iNdEx])
+			copy(dAtA[i:], m.Addrs[iNdEx])
+			i = encodeVarintCircuit(dAtA, i, uint64(len(m.Addrs[iNdEx])))
+			i--
+			dAtA[i] = 0x12
+		}
+	}
+	if m.Expire != nil {
+		i = encodeVarintCircuit(dAtA, i, uint64(*m.Expire))
+		i--
+		dAtA[i] = 0x8
+	}
+	return len(dAtA) - i, nil
+}
+
+func (m *Limit) Marshal() (dAtA []byte, err error) {
+	size := m.Size()
+	dAtA = make([]byte, size)
+	n, err := m.MarshalToSizedBuffer(dAtA[:size])
+	if err != nil {
+		return nil, err
+	}
+	return dAtA[:n], nil
+}
+
+func (m *Limit) MarshalTo(dAtA []byte) (int, error) {
+	size := m.Size()
+	return m.MarshalToSizedBuffer(dAtA[:size])
+}
+
+func (m *Limit) MarshalToSizedBuffer(dAtA []byte) (int, error) {
+	i := len(dAtA)
+	_ = i
+	var l int
+	_ = l
+	if m.XXX_unrecognized != nil {
+		i -= len(m.XXX_unrecognized)
+		copy(dAtA[i:], m.XXX_unrecognized)
+	}
+	if m.Data != nil {
+		i = encodeVarintCircuit(dAtA, i, uint64(*m.Data))
+		i--
+		dAtA[i] = 0x10
+	}
+	if m.Duration != nil {
+		i = encodeVarintCircuit(dAtA, i, uint64(*m.Duration))
+		i--
+		dAtA[i] = 0x8
+	}
+	return len(dAtA) - i, nil
+}
+
+func encodeVarintCircuit(dAtA []byte, offset int, v uint64) int {
+	offset -= sovCircuit(v)
+	base := offset
+	for v >= 1<<7 {
+		dAtA[offset] = uint8(v&0x7f | 0x80)
+		v >>= 7
+		offset++
+	}
+	dAtA[offset] = uint8(v)
+	return base
+}
+func (m *HopMessage) Size() (n int) {
+	if m == nil {
+		return 0
+	}
+	var l int
+	_ = l
+	if m.Type != nil {
+		n += 1 + sovCircuit(uint64(*m.Type))
+	}
+	if m.Peer != nil {
+		l = m.Peer.Size()
+		n += 1 + l + sovCircuit(uint64(l))
+	}
+	if m.Reservation != nil {
+		l = m.Reservation.Size()
+		n += 1 + l + sovCircuit(uint64(l))
+	}
+	if m.Limit != nil {
+		l = m.Limit.Size()
+		n += 1 + l + sovCircuit(uint64(l))
+	}
+	if m.Status != nil {
+		n += 1 + sovCircuit(uint64(*m.Status))
+	}
+	if m.XXX_unrecognized != nil {
+		n += len(m.XXX_unrecognized)
+	}
+	return n
+}
+
+func (m *StopMessage) Size() (n int) {
+	if m == nil {
+		return 0
+	}
+	var l int
+	_ = l
+	if m.Type != nil {
+		n += 1 + sovCircuit(uint64(*m.Type))
+	}
+	if m.Peer != nil {
+		l = m.Peer.Size()
+		n += 1 + l + sovCircuit(uint64(l))
+	}
+	if m.Limit != nil {
+		l = m.Limit.Size()
+		n += 1 + l + sovCircuit(uint64(l))
+	}
+	if m.Status != nil {
+		n += 1 + sovCircuit(uint64(*m.Status))
+	}
+	if m.XXX_unrecognized != nil {
+		n += len(m.XXX_unrecognized)
+	}
+	return n
+}
+
+func (m *Peer) Size() (n int) {
+	if m == nil {
+		return 0
+	}
+	var l int
+	_ = l
+	if m.Id != nil {
+		l = len(m.Id)
+		n += 1 + l + sovCircuit(uint64(l))
+	}
+	if len(m.Addrs) > 0 {
+		for _, b := range m.Addrs {
+			l = len(b)
+			n += 1 + l + sovCircuit(uint64(l))
+		}
+	}
+	if m.XXX_unrecognized != nil {
+		n += len(m.XXX_unrecognized)
+	}
+	return n
+}
+
+func (m *Reservation) Size() (n int) {
+	if m == nil {
+		return 0
+	}
+	var l int
+	_ = l
+	if m.Expire != nil {
+		n += 1 + sovCircuit(uint64(*m.Expire))
+	}
+	if len(m.Addrs) > 0 {
+		for _, b := range m.Addrs {
+			l = len(b)
+			n += 1 + l + sovCircuit(uint64(l))
+		}
+	}
+	if m.Voucher != nil {
+		l = len(m.Voucher)
+		n += 1 + l + sovCircuit(uint64(l))
+	}
+	if m.XXX_unrecognized != nil {
+		n += len(m.XXX_unrecognized)
+	}
+	return n
+}
+
+func (m *Limit) Size() (n int) {
+	if m == nil {
+		return 0
+	}
+	var l int
+	_ = l
+	if m.Duration != nil {
+		n += 1 + sovCircuit(uint64(*m.Duration))
+	}
+	if m.Data != nil {
+		n += 1 + sovCircuit(uint64(*m.Data))
+	}
+	if m.XXX_unrecognized != nil {
+		n += len(m.XXX_unrecognized)
+	}
+	return n
+}
+
+func sovCircuit(x uint64) (n int) {
+	return (math_bits.Len64(x|1) + 6) / 7
+}
+func sozCircuit(x uint64) (n int) {
+	return sovCircuit(uint64((x << 1) ^ uint64((int64(x) >> 63))))
+}
+func (m *HopMessage) Unmarshal(dAtA []byte) error {
+	var hasFields [1]uint64
+	l := len(dAtA)
+	iNdEx := 0
+	for iNdEx < l {
+		preIndex := iNdEx
+		var wire uint64
+		for shift := uint(0); ; shift += 7 {
+			if shift >= 64 {
+				return ErrIntOverflowCircuit
+			}
+			if iNdEx >= l {
+				return io.ErrUnexpectedEOF
+			}
+			b := dAtA[iNdEx]
+			iNdEx++
+			wire |= uint64(b&0x7F) << shift
+			if b < 0x80 {
+				break
+			}
+		}
+		fieldNum := int32(wire >> 3)
+		wireType := int(wire & 0x7)
+		if wireType == 4 {
+			return fmt.Errorf("proto: HopMessage: wiretype end group for non-group")
+		}
+		if fieldNum <= 0 {
+			return fmt.Errorf("proto: HopMessage: illegal tag %d (wire type %d)", fieldNum, wire)
+		}
+		switch fieldNum {
+		case 1:
+			if wireType != 0 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Type", wireType)
+			}
+			var v HopMessage_Type
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowCircuit
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				v |= HopMessage_Type(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			m.Type = &v
+			hasFields[0] |= uint64(0x00000001)
+		case 2:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Peer", wireType)
+			}
+			var msglen int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowCircuit
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				msglen |= int(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if msglen < 0 {
+				return ErrInvalidLengthCircuit
+			}
+			postIndex := iNdEx + msglen
+			if postIndex < 0 {
+				return ErrInvalidLengthCircuit
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			if m.Peer == nil {
+				m.Peer = &Peer{}
+			}
+			if err := m.Peer.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
+				return err
+			}
+			iNdEx = postIndex
+		case 3:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Reservation", wireType)
+			}
+			var msglen int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowCircuit
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				msglen |= int(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if msglen < 0 {
+				return ErrInvalidLengthCircuit
+			}
+			postIndex := iNdEx + msglen
+			if postIndex < 0 {
+				return ErrInvalidLengthCircuit
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			if m.Reservation == nil {
+				m.Reservation = &Reservation{}
+			}
+			if err := m.Reservation.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
+				return err
+			}
+			iNdEx = postIndex
+		case 4:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Limit", wireType)
+			}
+			var msglen int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowCircuit
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				msglen |= int(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if msglen < 0 {
+				return ErrInvalidLengthCircuit
+			}
+			postIndex := iNdEx + msglen
+			if postIndex < 0 {
+				return ErrInvalidLengthCircuit
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			if m.Limit == nil {
+				m.Limit = &Limit{}
+			}
+			if err := m.Limit.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
+				return err
+			}
+			iNdEx = postIndex
+		case 5:
+			if wireType != 0 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Status", wireType)
+			}
+			var v Status
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowCircuit
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				v |= Status(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			m.Status = &v
+		default:
+			iNdEx = preIndex
+			skippy, err := skipCircuit(dAtA[iNdEx:])
+			if err != nil {
+				return err
+			}
+			if skippy < 0 {
+				return ErrInvalidLengthCircuit
+			}
+			if (iNdEx + skippy) < 0 {
+				return ErrInvalidLengthCircuit
+			}
+			if (iNdEx + skippy) > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...)
+			iNdEx += skippy
+		}
+	}
+	if hasFields[0]&uint64(0x00000001) == 0 {
+		return github_com_gogo_protobuf_proto.NewRequiredNotSetError("type")
+	}
+
+	if iNdEx > l {
+		return io.ErrUnexpectedEOF
+	}
+	return nil
+}
+func (m *StopMessage) Unmarshal(dAtA []byte) error {
+	var hasFields [1]uint64
+	l := len(dAtA)
+	iNdEx := 0
+	for iNdEx < l {
+		preIndex := iNdEx
+		var wire uint64
+		for shift := uint(0); ; shift += 7 {
+			if shift >= 64 {
+				return ErrIntOverflowCircuit
+			}
+			if iNdEx >= l {
+				return io.ErrUnexpectedEOF
+			}
+			b := dAtA[iNdEx]
+			iNdEx++
+			wire |= uint64(b&0x7F) << shift
+			if b < 0x80 {
+				break
+			}
+		}
+		fieldNum := int32(wire >> 3)
+		wireType := int(wire & 0x7)
+		if wireType == 4 {
+			return fmt.Errorf("proto: StopMessage: wiretype end group for non-group")
+		}
+		if fieldNum <= 0 {
+			return fmt.Errorf("proto: StopMessage: illegal tag %d (wire type %d)", fieldNum, wire)
+		}
+		switch fieldNum {
+		case 1:
+			if wireType != 0 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Type", wireType)
+			}
+			var v StopMessage_Type
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowCircuit
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				v |= StopMessage_Type(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			m.Type = &v
+			hasFields[0] |= uint64(0x00000001)
+		case 2:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Peer", wireType)
+			}
+			var msglen int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowCircuit
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				msglen |= int(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if msglen < 0 {
+				return ErrInvalidLengthCircuit
+			}
+			postIndex := iNdEx + msglen
+			if postIndex < 0 {
+				return ErrInvalidLengthCircuit
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			if m.Peer == nil {
+				m.Peer = &Peer{}
+			}
+			if err := m.Peer.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
+				return err
+			}
+			iNdEx = postIndex
+		case 3:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Limit", wireType)
+			}
+			var msglen int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowCircuit
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				msglen |= int(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if msglen < 0 {
+				return ErrInvalidLengthCircuit
+			}
+			postIndex := iNdEx + msglen
+			if postIndex < 0 {
+				return ErrInvalidLengthCircuit
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			if m.Limit == nil {
+				m.Limit = &Limit{}
+			}
+			if err := m.Limit.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
+				return err
+			}
+			iNdEx = postIndex
+		case 4:
+			if wireType != 0 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Status", wireType)
+			}
+			var v Status
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowCircuit
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				v |= Status(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			m.Status = &v
+		default:
+			iNdEx = preIndex
+			skippy, err := skipCircuit(dAtA[iNdEx:])
+			if err != nil {
+				return err
+			}
+			if skippy < 0 {
+				return ErrInvalidLengthCircuit
+			}
+			if (iNdEx + skippy) < 0 {
+				return ErrInvalidLengthCircuit
+			}
+			if (iNdEx + skippy) > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...)
+			iNdEx += skippy
+		}
+	}
+	if hasFields[0]&uint64(0x00000001) == 0 {
+		return github_com_gogo_protobuf_proto.NewRequiredNotSetError("type")
+	}
+
+	if iNdEx > l {
+		return io.ErrUnexpectedEOF
+	}
+	return nil
+}
+func (m *Peer) Unmarshal(dAtA []byte) error {
+	var hasFields [1]uint64
+	l := len(dAtA)
+	iNdEx := 0
+	for iNdEx < l {
+		preIndex := iNdEx
+		var wire uint64
+		for shift := uint(0); ; shift += 7 {
+			if shift >= 64 {
+				return ErrIntOverflowCircuit
+			}
+			if iNdEx >= l {
+				return io.ErrUnexpectedEOF
+			}
+			b := dAtA[iNdEx]
+			iNdEx++
+			wire |= uint64(b&0x7F) << shift
+			if b < 0x80 {
+				break
+			}
+		}
+		fieldNum := int32(wire >> 3)
+		wireType := int(wire & 0x7)
+		if wireType == 4 {
+			return fmt.Errorf("proto: Peer: wiretype end group for non-group")
+		}
+		if fieldNum <= 0 {
+			return fmt.Errorf("proto: Peer: illegal tag %d (wire type %d)", fieldNum, wire)
+		}
+		switch fieldNum {
+		case 1:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Id", wireType)
+			}
+			var byteLen int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowCircuit
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				byteLen |= int(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if byteLen < 0 {
+				return ErrInvalidLengthCircuit
+			}
+			postIndex := iNdEx + byteLen
+			if postIndex < 0 {
+				return ErrInvalidLengthCircuit
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.Id = append(m.Id[:0], dAtA[iNdEx:postIndex]...)
+			if m.Id == nil {
+				m.Id = []byte{}
+			}
+			iNdEx = postIndex
+			hasFields[0] |= uint64(0x00000001)
+		case 2:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Addrs", wireType)
+			}
+			var byteLen int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowCircuit
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				byteLen |= int(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if byteLen < 0 {
+				return ErrInvalidLengthCircuit
+			}
+			postIndex := iNdEx + byteLen
+			if postIndex < 0 {
+				return ErrInvalidLengthCircuit
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.Addrs = append(m.Addrs, make([]byte, postIndex-iNdEx))
+			copy(m.Addrs[len(m.Addrs)-1], dAtA[iNdEx:postIndex])
+			iNdEx = postIndex
+		default:
+			iNdEx = preIndex
+			skippy, err := skipCircuit(dAtA[iNdEx:])
+			if err != nil {
+				return err
+			}
+			if skippy < 0 {
+				return ErrInvalidLengthCircuit
+			}
+			if (iNdEx + skippy) < 0 {
+				return ErrInvalidLengthCircuit
+			}
+			if (iNdEx + skippy) > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...)
+			iNdEx += skippy
+		}
+	}
+	if hasFields[0]&uint64(0x00000001) == 0 {
+		return github_com_gogo_protobuf_proto.NewRequiredNotSetError("id")
+	}
+
+	if iNdEx > l {
+		return io.ErrUnexpectedEOF
+	}
+	return nil
+}
+func (m *Reservation) Unmarshal(dAtA []byte) error {
+	l := len(dAtA)
+	iNdEx := 0
+	for iNdEx < l {
+		preIndex := iNdEx
+		var wire uint64
+		for shift := uint(0); ; shift += 7 {
+			if shift >= 64 {
+				return ErrIntOverflowCircuit
+			}
+			if iNdEx >= l {
+				return io.ErrUnexpectedEOF
+			}
+			b := dAtA[iNdEx]
+			iNdEx++
+			wire |= uint64(b&0x7F) << shift
+			if b < 0x80 {
+				break
+			}
+		}
+		fieldNum := int32(wire >> 3)
+		wireType := int(wire & 0x7)
+		if wireType == 4 {
+			return fmt.Errorf("proto: Reservation: wiretype end group for non-group")
+		}
+		if fieldNum <= 0 {
+			return fmt.Errorf("proto: Reservation: illegal tag %d (wire type %d)", fieldNum, wire)
+		}
+		switch fieldNum {
+		case 1:
+			if wireType != 0 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Expire", wireType)
+			}
+			var v uint64
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowCircuit
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				v |= uint64(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			m.Expire = &v
+		case 2:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Addrs", wireType)
+			}
+			var byteLen int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowCircuit
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				byteLen |= int(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if byteLen < 0 {
+				return ErrInvalidLengthCircuit
+			}
+			postIndex := iNdEx + byteLen
+			if postIndex < 0 {
+				return ErrInvalidLengthCircuit
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.Addrs = append(m.Addrs, make([]byte, postIndex-iNdEx))
+			copy(m.Addrs[len(m.Addrs)-1], dAtA[iNdEx:postIndex])
+			iNdEx = postIndex
+		case 3:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Voucher", wireType)
+			}
+			var byteLen int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowCircuit
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				byteLen |= int(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if byteLen < 0 {
+				return ErrInvalidLengthCircuit
+			}
+			postIndex := iNdEx + byteLen
+			if postIndex < 0 {
+				return ErrInvalidLengthCircuit
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.Voucher = append(m.Voucher[:0], dAtA[iNdEx:postIndex]...)
+			if m.Voucher == nil {
+				m.Voucher = []byte{}
+			}
+			iNdEx = postIndex
+		default:
+			iNdEx = preIndex
+			skippy, err := skipCircuit(dAtA[iNdEx:])
+			if err != nil {
+				return err
+			}
+			if skippy < 0 {
+				return ErrInvalidLengthCircuit
+			}
+			if (iNdEx + skippy) < 0 {
+				return ErrInvalidLengthCircuit
+			}
+			if (iNdEx + skippy) > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...)
+			iNdEx += skippy
+		}
+	}
+
+	if iNdEx > l {
+		return io.ErrUnexpectedEOF
+	}
+	return nil
+}
+func (m *Limit) Unmarshal(dAtA []byte) error {
+	l := len(dAtA)
+	iNdEx := 0
+	for iNdEx < l {
+		preIndex := iNdEx
+		var wire uint64
+		for shift := uint(0); ; shift += 7 {
+			if shift >= 64 {
+				return ErrIntOverflowCircuit
+			}
+			if iNdEx >= l {
+				return io.ErrUnexpectedEOF
+			}
+			b := dAtA[iNdEx]
+			iNdEx++
+			wire |= uint64(b&0x7F) << shift
+			if b < 0x80 {
+				break
+			}
+		}
+		fieldNum := int32(wire >> 3)
+		wireType := int(wire & 0x7)
+		if wireType == 4 {
+			return fmt.Errorf("proto: Limit: wiretype end group for non-group")
+		}
+		if fieldNum <= 0 {
+			return fmt.Errorf("proto: Limit: illegal tag %d (wire type %d)", fieldNum, wire)
+		}
+		switch fieldNum {
+		case 1:
+			if wireType != 0 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Duration", wireType)
+			}
+			var v uint32
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowCircuit
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				v |= uint32(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			m.Duration = &v
+		case 2:
+			if wireType != 0 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Data", wireType)
+			}
+			var v uint64
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowCircuit
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				v |= uint64(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			m.Data = &v
+		default:
+			iNdEx = preIndex
+			skippy, err := skipCircuit(dAtA[iNdEx:])
+			if err != nil {
+				return err
+			}
+			if skippy < 0 {
+				return ErrInvalidLengthCircuit
+			}
+			if (iNdEx + skippy) < 0 {
+				return ErrInvalidLengthCircuit
+			}
+			if (iNdEx + skippy) > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...)
+			iNdEx += skippy
+		}
+	}
+
+	if iNdEx > l {
+		return io.ErrUnexpectedEOF
+	}
+	return nil
+}
+func skipCircuit(dAtA []byte) (n int, err error) {
+	l := len(dAtA)
+	iNdEx := 0
+	depth := 0
+	for iNdEx < l {
+		var wire uint64
+		for shift := uint(0); ; shift += 7 {
+			if shift >= 64 {
+				return 0, ErrIntOverflowCircuit
+			}
+			if iNdEx >= l {
+				return 0, io.ErrUnexpectedEOF
+			}
+			b := dAtA[iNdEx]
+			iNdEx++
+			wire |= (uint64(b) & 0x7F) << shift
+			if b < 0x80 {
+				break
+			}
+		}
+		wireType := int(wire & 0x7)
+		switch wireType {
+		case 0:
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return 0, ErrIntOverflowCircuit
+				}
+				if iNdEx >= l {
+					return 0, io.ErrUnexpectedEOF
+				}
+				iNdEx++
+				if dAtA[iNdEx-1] < 0x80 {
+					break
+				}
+			}
+		case 1:
+			iNdEx += 8
+		case 2:
+			var length int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return 0, ErrIntOverflowCircuit
+				}
+				if iNdEx >= l {
+					return 0, io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				length |= (int(b) & 0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if length < 0 {
+				return 0, ErrInvalidLengthCircuit
+			}
+			iNdEx += length
+		case 3:
+			depth++
+		case 4:
+			if depth == 0 {
+				return 0, ErrUnexpectedEndOfGroupCircuit
+			}
+			depth--
+		case 5:
+			iNdEx += 4
+		default:
+			return 0, fmt.Errorf("proto: illegal wireType %d", wireType)
+		}
+		if iNdEx < 0 {
+			return 0, ErrInvalidLengthCircuit
+		}
+		if depth == 0 {
+			return iNdEx, nil
+		}
+	}
+	return 0, io.ErrUnexpectedEOF
+}
+
+var (
+	ErrInvalidLengthCircuit        = fmt.Errorf("proto: negative length found during unmarshaling")
+	ErrIntOverflowCircuit          = fmt.Errorf("proto: integer overflow")
+	ErrUnexpectedEndOfGroupCircuit = fmt.Errorf("proto: unexpected end of group")
+)
diff --git a/p2p/host/circuitv2/pb/circuit.proto b/p2p/host/circuitv2/pb/circuit.proto
new file mode 100644
index 00000000..b73555cb
--- /dev/null
+++ b/p2p/host/circuitv2/pb/circuit.proto
@@ -0,0 +1,60 @@
+syntax = "proto2";
+
+package circuit.pb;
+
+message HopMessage {
+  enum Type {
+    RESERVE = 0;
+    CONNECT = 1;
+    STATUS = 2;
+  }
+
+  required Type type = 1;
+
+  optional Peer peer = 2;
+  optional Reservation reservation = 3;
+  optional Limit limit = 4;
+
+  optional Status status = 5;
+}
+
+message StopMessage {
+  enum Type {
+    CONNECT = 0;
+    STATUS = 1;
+  }
+
+  required Type type = 1;
+
+  optional Peer peer = 2;
+  optional Limit limit = 3;
+
+  optional Status status = 4;
+}
+
+message Peer {
+  required bytes id = 1;
+  repeated bytes addrs = 2;
+}
+
+message Reservation {
+  optional uint64 expire = 1; // Unix expiration time (UTC)
+  repeated bytes addrs = 2;   // relay addrs for reserving peer
+  optional bytes voucher = 3; // reservation voucher
+}
+
+message Limit {
+  optional uint32 duration = 1; // seconds
+  optional uint64 data = 2;     // bytes
+}
+
+enum Status {
+  OK                      = 100;
+  RESERVATION_REFUSED     = 200;
+  RESOURCE_LIMIT_EXCEEDED = 201;
+  PERMISSION_DENIED       = 202;
+  CONNECTION_FAILED       = 203;
+  NO_RESERVATION          = 204;
+  MALFORMED_MESSAGE       = 400;
+  UNEXPECTED_MESSAGE      = 401;
+}
diff --git a/p2p/host/circuitv2/pb/voucher.pb.go b/p2p/host/circuitv2/pb/voucher.pb.go
new file mode 100644
index 00000000..6fed0082
--- /dev/null
+++ b/p2p/host/circuitv2/pb/voucher.pb.go
@@ -0,0 +1,438 @@
+// Code generated by protoc-gen-gogo. DO NOT EDIT.
+// source: voucher.proto
+
+package circuit_pb
+
+import (
+	fmt "fmt"
+	github_com_gogo_protobuf_proto "github.com/gogo/protobuf/proto"
+	proto "github.com/gogo/protobuf/proto"
+	io "io"
+	math "math"
+	math_bits "math/bits"
+)
+
+// Reference imports to suppress errors if they are not otherwise used.
+var _ = proto.Marshal
+var _ = fmt.Errorf
+var _ = math.Inf
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the proto package it is being compiled against.
+// A compilation error at this line likely means your copy of the
+// proto package needs to be updated.
+const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package
+
+type ReservationVoucher struct {
+	Relay                []byte   `protobuf:"bytes,1,req,name=relay" json:"relay,omitempty"`
+	Peer                 []byte   `protobuf:"bytes,2,req,name=peer" json:"peer,omitempty"`
+	Expiration           *uint64  `protobuf:"varint,3,req,name=expiration" json:"expiration,omitempty"`
+	XXX_NoUnkeyedLiteral struct{} `json:"-"`
+	XXX_unrecognized     []byte   `json:"-"`
+	XXX_sizecache        int32    `json:"-"`
+}
+
+func (m *ReservationVoucher) Reset()         { *m = ReservationVoucher{} }
+func (m *ReservationVoucher) String() string { return proto.CompactTextString(m) }
+func (*ReservationVoucher) ProtoMessage()    {}
+func (*ReservationVoucher) Descriptor() ([]byte, []int) {
+	return fileDescriptor_a22a9b0d3335ba25, []int{0}
+}
+func (m *ReservationVoucher) XXX_Unmarshal(b []byte) error {
+	return m.Unmarshal(b)
+}
+func (m *ReservationVoucher) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	if deterministic {
+		return xxx_messageInfo_ReservationVoucher.Marshal(b, m, deterministic)
+	} else {
+		b = b[:cap(b)]
+		n, err := m.MarshalToSizedBuffer(b)
+		if err != nil {
+			return nil, err
+		}
+		return b[:n], nil
+	}
+}
+func (m *ReservationVoucher) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_ReservationVoucher.Merge(m, src)
+}
+func (m *ReservationVoucher) XXX_Size() int {
+	return m.Size()
+}
+func (m *ReservationVoucher) XXX_DiscardUnknown() {
+	xxx_messageInfo_ReservationVoucher.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_ReservationVoucher proto.InternalMessageInfo
+
+func (m *ReservationVoucher) GetRelay() []byte {
+	if m != nil {
+		return m.Relay
+	}
+	return nil
+}
+
+func (m *ReservationVoucher) GetPeer() []byte {
+	if m != nil {
+		return m.Peer
+	}
+	return nil
+}
+
+func (m *ReservationVoucher) GetExpiration() uint64 {
+	if m != nil && m.Expiration != nil {
+		return *m.Expiration
+	}
+	return 0
+}
+
+func init() {
+	proto.RegisterType((*ReservationVoucher)(nil), "circuit.pb.ReservationVoucher")
+}
+
+func init() { proto.RegisterFile("voucher.proto", fileDescriptor_a22a9b0d3335ba25) }
+
+var fileDescriptor_a22a9b0d3335ba25 = []byte{
+	// 135 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x2d, 0xcb, 0x2f, 0x4d,
+	0xce, 0x48, 0x2d, 0xd2, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x4a, 0xce, 0x2c, 0x4a, 0x2e,
+	0xcd, 0x2c, 0xd1, 0x2b, 0x48, 0x52, 0x8a, 0xe3, 0x12, 0x0a, 0x4a, 0x2d, 0x4e, 0x2d, 0x2a, 0x4b,
+	0x2c, 0xc9, 0xcc, 0xcf, 0x0b, 0x83, 0xa8, 0x13, 0x12, 0xe1, 0x62, 0x2d, 0x4a, 0xcd, 0x49, 0xac,
+	0x94, 0x60, 0x54, 0x60, 0xd2, 0xe0, 0x09, 0x82, 0x70, 0x84, 0x84, 0xb8, 0x58, 0x0a, 0x52, 0x53,
+	0x8b, 0x24, 0x98, 0xc0, 0x82, 0x60, 0xb6, 0x90, 0x1c, 0x17, 0x57, 0x6a, 0x45, 0x41, 0x66, 0x11,
+	0x58, 0xbb, 0x04, 0xb3, 0x02, 0x93, 0x06, 0x4b, 0x10, 0x92, 0x88, 0x13, 0xcf, 0x89, 0x47, 0x72,
+	0x8c, 0x17, 0x1e, 0xc9, 0x31, 0x3e, 0x78, 0x24, 0xc7, 0x08, 0x08, 0x00, 0x00, 0xff, 0xff, 0xc0,
+	0x81, 0x3a, 0xee, 0x89, 0x00, 0x00, 0x00,
+}
+
+func (m *ReservationVoucher) Marshal() (dAtA []byte, err error) {
+	size := m.Size()
+	dAtA = make([]byte, size)
+	n, err := m.MarshalToSizedBuffer(dAtA[:size])
+	if err != nil {
+		return nil, err
+	}
+	return dAtA[:n], nil
+}
+
+func (m *ReservationVoucher) MarshalTo(dAtA []byte) (int, error) {
+	size := m.Size()
+	return m.MarshalToSizedBuffer(dAtA[:size])
+}
+
+func (m *ReservationVoucher) MarshalToSizedBuffer(dAtA []byte) (int, error) {
+	i := len(dAtA)
+	_ = i
+	var l int
+	_ = l
+	if m.XXX_unrecognized != nil {
+		i -= len(m.XXX_unrecognized)
+		copy(dAtA[i:], m.XXX_unrecognized)
+	}
+	if m.Expiration == nil {
+		return 0, github_com_gogo_protobuf_proto.NewRequiredNotSetError("expiration")
+	} else {
+		i = encodeVarintVoucher(dAtA, i, uint64(*m.Expiration))
+		i--
+		dAtA[i] = 0x18
+	}
+	if m.Peer == nil {
+		return 0, github_com_gogo_protobuf_proto.NewRequiredNotSetError("peer")
+	} else {
+		i -= len(m.Peer)
+		copy(dAtA[i:], m.Peer)
+		i = encodeVarintVoucher(dAtA, i, uint64(len(m.Peer)))
+		i--
+		dAtA[i] = 0x12
+	}
+	if m.Relay == nil {
+		return 0, github_com_gogo_protobuf_proto.NewRequiredNotSetError("relay")
+	} else {
+		i -= len(m.Relay)
+		copy(dAtA[i:], m.Relay)
+		i = encodeVarintVoucher(dAtA, i, uint64(len(m.Relay)))
+		i--
+		dAtA[i] = 0xa
+	}
+	return len(dAtA) - i, nil
+}
+
+func encodeVarintVoucher(dAtA []byte, offset int, v uint64) int {
+	offset -= sovVoucher(v)
+	base := offset
+	for v >= 1<<7 {
+		dAtA[offset] = uint8(v&0x7f | 0x80)
+		v >>= 7
+		offset++
+	}
+	dAtA[offset] = uint8(v)
+	return base
+}
+func (m *ReservationVoucher) Size() (n int) {
+	if m == nil {
+		return 0
+	}
+	var l int
+	_ = l
+	if m.Relay != nil {
+		l = len(m.Relay)
+		n += 1 + l + sovVoucher(uint64(l))
+	}
+	if m.Peer != nil {
+		l = len(m.Peer)
+		n += 1 + l + sovVoucher(uint64(l))
+	}
+	if m.Expiration != nil {
+		n += 1 + sovVoucher(uint64(*m.Expiration))
+	}
+	if m.XXX_unrecognized != nil {
+		n += len(m.XXX_unrecognized)
+	}
+	return n
+}
+
+func sovVoucher(x uint64) (n int) {
+	return (math_bits.Len64(x|1) + 6) / 7
+}
+func sozVoucher(x uint64) (n int) {
+	return sovVoucher(uint64((x << 1) ^ uint64((int64(x) >> 63))))
+}
+func (m *ReservationVoucher) Unmarshal(dAtA []byte) error {
+	var hasFields [1]uint64
+	l := len(dAtA)
+	iNdEx := 0
+	for iNdEx < l {
+		preIndex := iNdEx
+		var wire uint64
+		for shift := uint(0); ; shift += 7 {
+			if shift >= 64 {
+				return ErrIntOverflowVoucher
+			}
+			if iNdEx >= l {
+				return io.ErrUnexpectedEOF
+			}
+			b := dAtA[iNdEx]
+			iNdEx++
+			wire |= uint64(b&0x7F) << shift
+			if b < 0x80 {
+				break
+			}
+		}
+		fieldNum := int32(wire >> 3)
+		wireType := int(wire & 0x7)
+		if wireType == 4 {
+			return fmt.Errorf("proto: ReservationVoucher: wiretype end group for non-group")
+		}
+		if fieldNum <= 0 {
+			return fmt.Errorf("proto: ReservationVoucher: illegal tag %d (wire type %d)", fieldNum, wire)
+		}
+		switch fieldNum {
+		case 1:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Relay", wireType)
+			}
+			var byteLen int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowVoucher
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				byteLen |= int(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if byteLen < 0 {
+				return ErrInvalidLengthVoucher
+			}
+			postIndex := iNdEx + byteLen
+			if postIndex < 0 {
+				return ErrInvalidLengthVoucher
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.Relay = append(m.Relay[:0], dAtA[iNdEx:postIndex]...)
+			if m.Relay == nil {
+				m.Relay = []byte{}
+			}
+			iNdEx = postIndex
+			hasFields[0] |= uint64(0x00000001)
+		case 2:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Peer", wireType)
+			}
+			var byteLen int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowVoucher
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				byteLen |= int(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if byteLen < 0 {
+				return ErrInvalidLengthVoucher
+			}
+			postIndex := iNdEx + byteLen
+			if postIndex < 0 {
+				return ErrInvalidLengthVoucher
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.Peer = append(m.Peer[:0], dAtA[iNdEx:postIndex]...)
+			if m.Peer == nil {
+				m.Peer = []byte{}
+			}
+			iNdEx = postIndex
+			hasFields[0] |= uint64(0x00000002)
+		case 3:
+			if wireType != 0 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Expiration", wireType)
+			}
+			var v uint64
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowVoucher
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				v |= uint64(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			m.Expiration = &v
+			hasFields[0] |= uint64(0x00000004)
+		default:
+			iNdEx = preIndex
+			skippy, err := skipVoucher(dAtA[iNdEx:])
+			if err != nil {
+				return err
+			}
+			if skippy < 0 {
+				return ErrInvalidLengthVoucher
+			}
+			if (iNdEx + skippy) < 0 {
+				return ErrInvalidLengthVoucher
+			}
+			if (iNdEx + skippy) > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...)
+			iNdEx += skippy
+		}
+	}
+	if hasFields[0]&uint64(0x00000001) == 0 {
+		return github_com_gogo_protobuf_proto.NewRequiredNotSetError("relay")
+	}
+	if hasFields[0]&uint64(0x00000002) == 0 {
+		return github_com_gogo_protobuf_proto.NewRequiredNotSetError("peer")
+	}
+	if hasFields[0]&uint64(0x00000004) == 0 {
+		return github_com_gogo_protobuf_proto.NewRequiredNotSetError("expiration")
+	}
+
+	if iNdEx > l {
+		return io.ErrUnexpectedEOF
+	}
+	return nil
+}
+func skipVoucher(dAtA []byte) (n int, err error) {
+	l := len(dAtA)
+	iNdEx := 0
+	depth := 0
+	for iNdEx < l {
+		var wire uint64
+		for shift := uint(0); ; shift += 7 {
+			if shift >= 64 {
+				return 0, ErrIntOverflowVoucher
+			}
+			if iNdEx >= l {
+				return 0, io.ErrUnexpectedEOF
+			}
+			b := dAtA[iNdEx]
+			iNdEx++
+			wire |= (uint64(b) & 0x7F) << shift
+			if b < 0x80 {
+				break
+			}
+		}
+		wireType := int(wire & 0x7)
+		switch wireType {
+		case 0:
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return 0, ErrIntOverflowVoucher
+				}
+				if iNdEx >= l {
+					return 0, io.ErrUnexpectedEOF
+				}
+				iNdEx++
+				if dAtA[iNdEx-1] < 0x80 {
+					break
+				}
+			}
+		case 1:
+			iNdEx += 8
+		case 2:
+			var length int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return 0, ErrIntOverflowVoucher
+				}
+				if iNdEx >= l {
+					return 0, io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				length |= (int(b) & 0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if length < 0 {
+				return 0, ErrInvalidLengthVoucher
+			}
+			iNdEx += length
+		case 3:
+			depth++
+		case 4:
+			if depth == 0 {
+				return 0, ErrUnexpectedEndOfGroupVoucher
+			}
+			depth--
+		case 5:
+			iNdEx += 4
+		default:
+			return 0, fmt.Errorf("proto: illegal wireType %d", wireType)
+		}
+		if iNdEx < 0 {
+			return 0, ErrInvalidLengthVoucher
+		}
+		if depth == 0 {
+			return iNdEx, nil
+		}
+	}
+	return 0, io.ErrUnexpectedEOF
+}
+
+var (
+	ErrInvalidLengthVoucher        = fmt.Errorf("proto: negative length found during unmarshaling")
+	ErrIntOverflowVoucher          = fmt.Errorf("proto: integer overflow")
+	ErrUnexpectedEndOfGroupVoucher = fmt.Errorf("proto: unexpected end of group")
+)
diff --git a/p2p/host/circuitv2/pb/voucher.proto b/p2p/host/circuitv2/pb/voucher.proto
new file mode 100644
index 00000000..08644025
--- /dev/null
+++ b/p2p/host/circuitv2/pb/voucher.proto
@@ -0,0 +1,9 @@
+syntax = "proto2";
+
+package circuit.pb;
+
+message ReservationVoucher {
+  required bytes relay = 1;
+  required bytes peer = 2;
+  required uint64 expiration = 3;
+}
diff --git a/p2p/host/circuitv2/proto/protocol.go b/p2p/host/circuitv2/proto/protocol.go
new file mode 100644
index 00000000..d27fc509
--- /dev/null
+++ b/p2p/host/circuitv2/proto/protocol.go
@@ -0,0 +1,7 @@
+package proto
+
+const (
+	ProtoIDv1     = "/libp2p/circuit/relay/0.1.0"
+	ProtoIDv2Hop  = "/libp2p/circuit/relay/0.2.0/hop"
+	ProtoIDv2Stop = "/libp2p/circuit/relay/0.2.0/stop"
+)
diff --git a/p2p/host/circuitv2/proto/voucher.go b/p2p/host/circuitv2/proto/voucher.go
new file mode 100644
index 00000000..6b4c24c8
--- /dev/null
+++ b/p2p/host/circuitv2/proto/voucher.go
@@ -0,0 +1,72 @@
+package proto
+
+import (
+	"time"
+
+	pbv2 "github.com/libp2p/go-libp2p/p2p/host/circuitv2/pb"
+
+	"github.com/libp2p/go-libp2p-core/peer"
+	"github.com/libp2p/go-libp2p-core/record"
+)
+
+const RecordDomain = "libp2p-relay-rsvp"
+
+// TODO: register in multicodec table in https://github.com/multiformats/multicodec
+var RecordCodec = []byte{0x03, 0x02}
+
+func init() {
+	record.RegisterType(&ReservationVoucher{})
+}
+
+type ReservationVoucher struct {
+	// Relay is the ID of the peer providing relay service
+	Relay peer.ID
+	// Peer is the ID of the peer receiving relay service through Relay
+	Peer peer.ID
+	// Expiration is the expiration time of the reservation
+	Expiration time.Time
+}
+
+var _ record.Record = (*ReservationVoucher)(nil)
+
+func (rv *ReservationVoucher) Domain() string {
+	return RecordDomain
+}
+
+func (rv *ReservationVoucher) Codec() []byte {
+	return RecordCodec
+}
+
+func (rv *ReservationVoucher) MarshalRecord() ([]byte, error) {
+	relay := []byte(rv.Relay)
+	peer := []byte(rv.Peer)
+	expiration := uint64(rv.Expiration.Unix())
+	pbrv := &pbv2.ReservationVoucher{
+		Relay:      relay,
+		Peer:       peer,
+		Expiration: &expiration,
+	}
+
+	return pbrv.Marshal()
+}
+
+func (rv *ReservationVoucher) UnmarshalRecord(blob []byte) error {
+	pbrv := pbv2.ReservationVoucher{}
+	err := pbrv.Unmarshal(blob)
+	if err != nil {
+		return err
+	}
+
+	rv.Relay, err = peer.IDFromBytes(pbrv.GetRelay())
+	if err != nil {
+		return err
+	}
+
+	rv.Peer, err = peer.IDFromBytes(pbrv.GetPeer())
+	if err != nil {
+		return err
+	}
+
+	rv.Expiration = time.Unix(int64(pbrv.GetExpiration()), 0)
+	return nil
+}
diff --git a/p2p/host/circuitv2/relay/acl.go b/p2p/host/circuitv2/relay/acl.go
new file mode 100644
index 00000000..05010513
--- /dev/null
+++ b/p2p/host/circuitv2/relay/acl.go
@@ -0,0 +1,17 @@
+package relay
+
+import (
+	"github.com/libp2p/go-libp2p-core/peer"
+
+	ma "github.com/multiformats/go-multiaddr"
+)
+
+// ACLFilter is an Access Control mechanism for relayed connect.
+type ACLFilter interface {
+	// AllowReserve returns true if a reservation from a peer with the given peer ID and multiaddr
+	// is allowed.
+	AllowReserve(p peer.ID, a ma.Multiaddr) bool
+	// AllowConnect returns true if a source peer, with a given multiaddr is allowed to connect
+	// to a destination peer.
+	AllowConnect(src peer.ID, srcAddr ma.Multiaddr, dest peer.ID) bool
+}
diff --git a/p2p/host/circuitv2/relay/constraints.go b/p2p/host/circuitv2/relay/constraints.go
new file mode 100644
index 00000000..1b0087ec
--- /dev/null
+++ b/p2p/host/circuitv2/relay/constraints.go
@@ -0,0 +1,124 @@
+package relay
+
+import (
+	"errors"
+	"sync"
+	"time"
+
+	asnutil "github.com/libp2p/go-libp2p-asn-util"
+	"github.com/libp2p/go-libp2p-core/peer"
+	ma "github.com/multiformats/go-multiaddr"
+	manet "github.com/multiformats/go-multiaddr/net"
+)
+
+var validity = 30 * time.Minute
+
+var (
+	errTooManyReservations        = errors.New("too many reservations")
+	errTooManyReservationsForPeer = errors.New("too many reservations for peer")
+	errTooManyReservationsForIP   = errors.New("too many peers for IP address")
+	errTooManyReservationsForASN  = errors.New("too many peers for ASN")
+)
+
+// constraints implements various reservation constraints
+type constraints struct {
+	rc *Resources
+
+	mutex sync.Mutex
+	total []time.Time
+	peers map[peer.ID][]time.Time
+	ips   map[string][]time.Time
+	asns  map[string][]time.Time
+}
+
+// newConstraints creates a new constraints object.
+// The methods are *not* thread-safe; an external lock must be held if synchronization
+// is required.
+func newConstraints(rc *Resources) *constraints {
+	return &constraints{
+		rc:    rc,
+		peers: make(map[peer.ID][]time.Time),
+		ips:   make(map[string][]time.Time),
+		asns:  make(map[string][]time.Time),
+	}
+}
+
+// AddReservation adds a reservation for a given peer with a given multiaddr.
+// If adding this reservation violates IP constraints, an error is returned.
+func (c *constraints) AddReservation(p peer.ID, a ma.Multiaddr) error {
+	c.mutex.Lock()
+	defer c.mutex.Unlock()
+
+	now := time.Now()
+	c.cleanup(now)
+
+	if len(c.total) >= c.rc.MaxReservations {
+		return errTooManyReservations
+	}
+
+	ip, err := manet.ToIP(a)
+	if err != nil {
+		return errors.New("no IP address associated with peer")
+	}
+
+	peerReservations := c.peers[p]
+	if len(peerReservations) >= c.rc.MaxReservationsPerPeer {
+		return errTooManyReservationsForPeer
+	}
+
+	ipReservations := c.ips[ip.String()]
+	if len(ipReservations) >= c.rc.MaxReservationsPerIP {
+		return errTooManyReservationsForIP
+	}
+
+	var asnReservations []time.Time
+	var asn string
+	if ip.To4() == nil {
+		asn, _ = asnutil.Store.AsnForIPv6(ip)
+		if asn != "" {
+			asnReservations = c.asns[asn]
+			if len(asnReservations) >= c.rc.MaxReservationsPerASN {
+				return errTooManyReservationsForASN
+			}
+		}
+	}
+
+	expiry := now.Add(validity)
+	c.total = append(c.total, expiry)
+
+	peerReservations = append(peerReservations, expiry)
+	c.peers[p] = peerReservations
+
+	ipReservations = append(ipReservations, expiry)
+	c.ips[ip.String()] = ipReservations
+
+	if asn != "" {
+		asnReservations = append(asnReservations, expiry)
+		c.asns[asn] = asnReservations
+	}
+	return nil
+}
+
+func (c *constraints) cleanupList(l []time.Time, now time.Time) []time.Time {
+	var index int
+	for i, t := range l {
+		if t.After(now) {
+			break
+		}
+		index = i + 1
+	}
+	return l[index:]
+}
+
+func (c *constraints) cleanup(now time.Time) {
+	c.total = c.cleanupList(c.total, now)
+	for k, peerReservations := range c.peers {
+		c.peers[k] = c.cleanupList(peerReservations, now)
+	}
+	for k, ipReservations := range c.ips {
+		c.ips[k] = c.cleanupList(ipReservations, now)
+	}
+	for k, asnReservations := range c.asns {
+		c.asns[k] = c.cleanupList(asnReservations, now)
+	}
+}
diff --git a/p2p/host/circuitv2/relay/options.go b/p2p/host/circuitv2/relay/options.go
new file mode 100644
index 00000000..34641152
--- /dev/null
+++ b/p2p/host/circuitv2/relay/options.go
@@ -0,0 +1,27 @@
+package relay
+
+type Option func(*Relay) error
+
+// WithResources is a Relay option that sets specific relay resources for the relay.
+func WithResources(rc Resources) Option {
+	return func(r *Relay) error {
+		r.rc = rc
+		return nil
+	}
+}
+
+// WithLimit is a Relay option that sets only the relayed connection limits for the relay.
+func WithLimit(limit *RelayLimit) Option {
+	return func(r *Relay) error {
+		r.rc.Limit = limit
+		return nil
+	}
+}
+
+// WithACL is a Relay option that supplies an ACLFilter for access control.
+func WithACL(acl ACLFilter) Option {
+	return func(r *Relay) error {
+		r.acl = acl
+		return nil
+	}
+}
diff --git a/p2p/host/circuitv2/relay/relay.go b/p2p/host/circuitv2/relay/relay.go
new file mode 100644
index 00000000..8ddddd17
--- /dev/null
+++ b/p2p/host/circuitv2/relay/relay.go
@@ -0,0 +1,510 @@
+package relay
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"sync"
+	"sync/atomic"
+	"time"
+
+	pbv2 "github.com/libp2p/go-libp2p/p2p/host/circuitv2/pb"
+	"github.com/libp2p/go-libp2p/p2p/host/circuitv2/proto"
+	"github.com/libp2p/go-libp2p/p2p/host/circuitv2/util"
+
+	"github.com/libp2p/go-libp2p-core/host"
+	"github.com/libp2p/go-libp2p-core/network"
+	"github.com/libp2p/go-libp2p-core/peer"
+	"github.com/libp2p/go-libp2p-core/record"
+
+	logging "github.com/ipfs/go-log"
+	pool "github.com/libp2p/go-buffer-pool"
+	ma "github.com/multiformats/go-multiaddr"
+	manet "github.com/multiformats/go-multiaddr/net"
+)
+
+const (
+	ReservationTagWeight = 10
+
+	StreamTimeout    = time.Minute
+	ConnectTimeout   = 30 * time.Second
+	HandshakeTimeout = time.Minute
+
+	maxMessageSize = 4096
+)
+
+var log = logging.Logger("relay")
+
+// Relay is the (limited) relay service object.
+type Relay struct {
+	closed uint32
+	ctx    context.Context
+	cancel func()
+
+	host        host.Host
+	rc          Resources
+	acl         ACLFilter
+	constraints *constraints
+
+	mx    sync.Mutex
+	rsvp  map[peer.ID]time.Time
+	conns map[peer.ID]int
+
+	selfAddr ma.Multiaddr
+}
+
+// New constructs a new limited relay that can provide relay services in the given host.
+func New(h host.Host, opts ...Option) (*Relay, error) {
+	ctx, cancel := context.WithCancel(context.Background())
+
+	r := &Relay{
+		ctx:    ctx,
+		cancel: cancel,
+		host:   h,
+		rc:     DefaultResources(),
+		acl:    nil,
+		rsvp:   make(map[peer.ID]time.Time),
+		conns:  make(map[peer.ID]int),
+	}
+
+	for _, opt := range opts {
+		err := opt(r)
+		if err != nil {
+			return nil, fmt.Errorf("error applying relay option: %w", err)
+		}
+	}
+
+	r.constraints = newConstraints(&r.rc)
+	r.selfAddr = ma.StringCast(fmt.Sprintf("/p2p/%s", h.ID()))
+
+	h.SetStreamHandler(proto.ProtoIDv2Hop, r.handleStream)
+	h.Network().Notify(
+		&network.NotifyBundle{
+			DisconnectedF: r.disconnected,
+		})
+	go r.background()
+
+	return r, nil
+}
+
+func (r *Relay) Close() error {
+	if atomic.CompareAndSwapUint32(&r.closed, 0, 1) {
+		r.host.RemoveStreamHandler(proto.ProtoIDv2Hop)
+		r.cancel()
+		r.mx.Lock()
+		for p := range r.rsvp {
+			r.host.ConnManager().UntagPeer(p, "relay-reservation")
+		}
+		r.mx.Unlock()
+	}
+	return nil
+}
+
+func (r *Relay) handleStream(s network.Stream) {
+	s.SetReadDeadline(time.Now().Add(StreamTimeout))
+
+	log.Infof("new relay stream from: %s", s.Conn().RemotePeer())
+
+	rd := util.NewDelimitedReader(s, maxMessageSize)
+	defer rd.Close()
+
+	var msg pbv2.HopMessage
+
+	err := rd.ReadMsg(&msg)
+	if err != nil {
+		r.handleError(s, pbv2.Status_MALFORMED_MESSAGE)
+		return
+	}
+	// reset stream deadline as message has been read
+	s.SetReadDeadline(time.Time{})
+
+	switch msg.GetType() {
+	case pbv2.HopMessage_RESERVE:
+		r.handleReserve(s)
+
+	case pbv2.HopMessage_CONNECT:
+		r.handleConnect(s, &msg)
+
+	default:
+		r.handleError(s, pbv2.Status_MALFORMED_MESSAGE)
+	}
+}
+
+func (r *Relay) handleReserve(s network.Stream) {
+	defer s.Close()
+
+	p := s.Conn().RemotePeer()
+	a := s.Conn().RemoteMultiaddr()
+
+	if util.IsRelayAddr(a) {
+		log.Debugf("refusing relay reservation for %s; reservation attempt over relay connection")
+		r.handleError(s, pbv2.Status_PERMISSION_DENIED)
+		return
+	}
+
+	if r.acl != nil && !r.acl.AllowReserve(p, a) {
+		log.Debugf("refusing relay reservation for %s; permission denied", p)
+		r.handleError(s, pbv2.Status_PERMISSION_DENIED)
+		return
+	}
+
+	r.mx.Lock()
+	now := time.Now()
+
+	_, exists := r.rsvp[p]
+	if !exists {
+		if err := r.constraints.AddReservation(p, a); err != nil {
+			r.mx.Unlock()
+			log.Debugf("refusing relay reservation for %s; IP constraint violation: %s", p, err)
+			r.handleError(s, pbv2.Status_RESERVATION_REFUSED)
+			return
+		}
+	}
+
+	expire := now.Add(r.rc.ReservationTTL)
+	r.rsvp[p] = expire
+	r.host.ConnManager().TagPeer(p, "relay-reservation", ReservationTagWeight)
+	r.mx.Unlock()
+
+	log.Debugf("reserving relay slot for %s", p)
+
+	// Delivery of the reservation might fail for a number of reasons.
+	// For example, the stream might be reset or the connection might be closed before the reservation is received.
+	// In that case, the reservation will just be garbage collected later.
+	if err := r.writeResponse(s, pbv2.Status_OK, r.makeReservationMsg(p, expire), r.makeLimitMsg(p)); err != nil {
+		log.Debugf("error writing reservation response; retracting reservation for %s", p)
+		s.Reset()
+	}
+}
+
+func (r *Relay) handleConnect(s network.Stream, msg *pbv2.HopMessage) {
+	src := s.Conn().RemotePeer()
+	a := s.Conn().RemoteMultiaddr()
+
+	if util.IsRelayAddr(a) {
+		log.Debugf("refusing connection from %s; connection attempt over relay connection")
+		r.handleError(s, pbv2.Status_PERMISSION_DENIED)
+		return
+	}
+
+	dest, err := util.PeerToPeerInfoV2(msg.GetPeer())
+	if err != nil {
+		r.handleError(s, pbv2.Status_MALFORMED_MESSAGE)
+		return
+	}
+
+	if r.acl != nil && !r.acl.AllowConnect(src, s.Conn().RemoteMultiaddr(), dest.ID) {
+		log.Debugf("refusing connection from %s to %s; permission denied", src, dest.ID)
+		r.handleError(s, pbv2.Status_PERMISSION_DENIED)
+		return
+	}
+
+	r.mx.Lock()
+	_, rsvp := r.rsvp[dest.ID]
+	if !rsvp {
+		r.mx.Unlock()
+		log.Debugf("refusing connection from %s to %s; no reservation", src, dest.ID)
+		r.handleError(s, pbv2.Status_NO_RESERVATION)
+		return
+	}
+
+	srcConns := r.conns[src]
+	if srcConns >= r.rc.MaxCircuits {
+		r.mx.Unlock()
+		log.Debugf("refusing connection from %s to %s; too many connections from %s", src, dest.ID, src)
+		r.handleError(s, pbv2.Status_RESOURCE_LIMIT_EXCEEDED)
+		return
+	}
+	r.conns[src]++
+
+	destConns := r.conns[dest.ID]
+	if destConns >= r.rc.MaxCircuits {
+		r.conns[src]--
+		r.mx.Unlock()
+		log.Debugf("refusing connection from %s to %s; too many connecitons to %s", src, dest.ID, dest.ID)
+		r.handleError(s, pbv2.Status_RESOURCE_LIMIT_EXCEEDED)
+		return
+	}
+	r.conns[dest.ID]++
+	r.mx.Unlock()
+
+	cleanup := func() {
+		r.mx.Lock()
+		r.conns[src]--
+		r.conns[dest.ID]--
+		r.mx.Unlock()
+	}
+
+	ctx, cancel := context.WithTimeout(r.ctx, ConnectTimeout)
+	defer cancel()
+
+	ctx = network.WithNoDial(ctx, "relay connect")
+
+	bs, err := r.host.NewStream(ctx, dest.ID, proto.ProtoIDv2Stop)
+	if err != nil {
+		log.Debugf("error opening relay stream to %s: %s", dest.ID, err)
+		cleanup()
+		r.handleError(s, pbv2.Status_CONNECTION_FAILED)
+		return
+	}
+
+	// handshake
+	rd := util.NewDelimitedReader(bs, maxMessageSize)
+	wr := util.NewDelimitedWriter(bs)
+	defer rd.Close()
+
+	var stopmsg pbv2.StopMessage
+	stopmsg.Type = pbv2.StopMessage_CONNECT.Enum()
+	stopmsg.Peer = util.PeerInfoToPeerV2(peer.AddrInfo{ID: src})
+	stopmsg.Limit = r.makeLimitMsg(dest.ID)
+
+	bs.SetDeadline(time.Now().Add(HandshakeTimeout))
+
+	err = wr.WriteMsg(&stopmsg)
+	if err != nil {
+		log.Debugf("error writing stop handshake")
+		bs.Reset()
+		cleanup()
+		r.handleError(s, pbv2.Status_CONNECTION_FAILED)
+		return
+	}
+
+	stopmsg.Reset()
+
+	err = rd.ReadMsg(&stopmsg)
+	if err != nil {
+		log.Debugf("error reading stop response: %s", err.Error())
+		bs.Reset()
+		cleanup()
+		r.handleError(s, pbv2.Status_CONNECTION_FAILED)
+		return
+	}
+
+	if t := stopmsg.GetType(); t != pbv2.StopMessage_STATUS {
+		log.Debugf("unexpected stop response; not a status message (%d)", t)
+		bs.Reset()
+		cleanup()
+		r.handleError(s, pbv2.Status_CONNECTION_FAILED)
+		return
+	}
+
+	if status := stopmsg.GetStatus(); status != pbv2.Status_OK {
+		log.Debugf("relay stop failure: %d", status)
+		bs.Reset()
+		cleanup()
+		r.handleError(s, pbv2.Status_CONNECTION_FAILED)
+		return
+	}
+
+	var response pbv2.HopMessage
+	response.Type = pbv2.HopMessage_STATUS.Enum()
+	response.Status = pbv2.Status_OK.Enum()
+	response.Limit = r.makeLimitMsg(dest.ID)
+
+	wr = util.NewDelimitedWriter(s)
+	err = wr.WriteMsg(&response)
+	if err != nil {
+		log.Debugf("error writing relay response: %s", err)
+		bs.Reset()
+		s.Reset()
+		cleanup()
+		return
+	}
+
+	// reset deadline
+	bs.SetDeadline(time.Time{})
+
+	log.Infof("relaying connection from %s to %s", src, dest.ID)
+
+	goroutines := new(int32)
+	*goroutines = 2
+
+	done := func() {
+		if atomic.AddInt32(goroutines, -1) == 0 {
+			s.Close()
+			bs.Close()
+			cleanup()
+		}
+	}
+
+	if r.rc.Limit != nil {
+		deadline := time.Now().Add(r.rc.Limit.Duration)
+		s.SetDeadline(deadline)
+		bs.SetDeadline(deadline)
+		go r.relayLimited(s, bs, src, dest.ID, r.rc.Limit.Data, done)
+		go r.relayLimited(bs, s, dest.ID, src, r.rc.Limit.Data, done)
+	} else {
+		go r.relayUnlimited(s, bs, src, dest.ID, done)
+		go r.relayUnlimited(bs, s, dest.ID, src, done)
+	}
+}
+
+func (r *Relay) relayLimited(src, dest network.Stream, srcID, destID peer.ID, limit int64, done func()) {
+	defer done()
+
+	buf := pool.Get(r.rc.BufferSize)
+	defer pool.Put(buf)
+
+	limitedSrc := io.LimitReader(src, limit)
+
+	count, err := io.CopyBuffer(dest, limitedSrc, buf)
+	if err != nil {
+		log.Debugf("relay copy error: %s", err)
+		// Reset both.
+		src.Reset()
+		dest.Reset()
+	} else {
+		// propagate the close
+		dest.CloseWrite()
+		if count == limit {
+			// we've reached the limit, discard further input
+			src.CloseRead()
+		}
+	}
+
+	log.Debugf("relayed %d bytes from %s to %s", count, srcID, destID)
+}
+
+func (r *Relay) relayUnlimited(src, dest network.Stream, srcID, destID peer.ID, done func()) {
+	defer done()
+
+	buf := pool.Get(r.rc.BufferSize)
+	defer pool.Put(buf)
+
+	count, err := io.CopyBuffer(dest, src, buf)
+	if err != nil {
+		log.Debugf("relay copy error: %s", err)
+		// Reset both.
+		src.Reset()
+		dest.Reset()
+	} else {
+		// propagate the close
+		dest.CloseWrite()
+	}
+
+	log.Debugf("relayed %d bytes from %s to %s", count, srcID, destID)
+}
+
+func (r *Relay) handleError(s network.Stream, status pbv2.Status) {
+	log.Debugf("relay error: %s (%d)", pbv2.Status_name[int32(status)], status)
+	err := r.writeResponse(s, status, nil, nil)
+	if err != nil {
+		s.Reset()
+		log.Debugf("error writing relay response: %s", err.Error())
+	} else {
+		s.Close()
+	}
+}
+
+func (r *Relay) writeResponse(s network.Stream, status pbv2.Status, rsvp *pbv2.Reservation, limit *pbv2.Limit) error {
+	wr := util.NewDelimitedWriter(s)
+
+	var msg pbv2.HopMessage
+	msg.Type = pbv2.HopMessage_STATUS.Enum()
+	msg.Status = status.Enum()
+	msg.Reservation = rsvp
+	msg.Limit = limit
+
+	return wr.WriteMsg(&msg)
+}
+
+func (r *Relay) makeReservationMsg(p peer.ID, expire time.Time) *pbv2.Reservation {
+	expireUnix := uint64(expire.Unix())
+
+	var addrBytes [][]byte
+	for _, addr := range r.host.Addrs() {
+		if !manet.IsPublicAddr(addr) {
+			continue
+		}
+
+		addr = addr.Encapsulate(r.selfAddr)
+		addrBytes = append(addrBytes, addr.Bytes())
+	}
+
+	rsvp := &pbv2.Reservation{
+		Expire: &expireUnix,
+		Addrs:  addrBytes,
+	}
+
+	voucher := &proto.ReservationVoucher{
+		Relay:      r.host.ID(),
+		Peer:       p,
+		Expiration: expire,
+	}
+
+	envelope, err := record.Seal(voucher, r.host.Peerstore().PrivKey(r.host.ID()))
+	if err != nil {
+		log.Errorf("error sealing voucher for %s: %s", p, err)
+		return rsvp
+	}
+
+	blob, err := envelope.Marshal()
+	if err != nil {
+		log.Errorf("error marshalling voucher for %s: %s", p, err)
+		return rsvp
+	}
+
+	rsvp.Voucher = blob
+
+	return rsvp
+}
+
+func (r *Relay) makeLimitMsg(p peer.ID) *pbv2.Limit {
+	if r.rc.Limit == nil {
+		return nil
+	}
+
+	duration := uint32(r.rc.Limit.Duration / time.Second)
+	data := uint64(r.rc.Limit.Data)
+
+	return &pbv2.Limit{
+		Duration: &duration,
+		Data:     &data,
+	}
+}
+
+func (r *Relay) background() {
+	ticker := time.NewTicker(time.Minute)
+	defer ticker.Stop()
+
+	for {
+		select {
+		case <-ticker.C:
+			r.gc()
+		case <-r.ctx.Done():
+			return
+		}
+	}
+}
+
+func (r *Relay) gc() {
+	r.mx.Lock()
+	defer r.mx.Unlock()
+
+	now := time.Now()
+
+	for p, expire := range r.rsvp {
+		if expire.Before(now) {
+			delete(r.rsvp, p)
+			r.host.ConnManager().UntagPeer(p, "relay-reservation")
+		}
+	}
+
+	for p, count := range r.conns {
+		if count == 0 {
+			delete(r.conns, p)
+		}
+	}
+}
+
+func (r *Relay) disconnected(n network.Network, c network.Conn) {
+	p := c.RemotePeer()
+	if n.Connectedness(p) == network.Connected {
+		return
+	}
+
+	r.mx.Lock()
+	defer r.mx.Unlock()
+
+	delete(r.rsvp, p)
+}
diff --git a/p2p/host/circuitv2/relay/resources.go b/p2p/host/circuitv2/relay/resources.go
new file mode 100644
index 00000000..0ae01698
--- /dev/null
+++ b/p2p/host/circuitv2/relay/resources.go
@@ -0,0 +1,66 @@
+package relay
+
+import (
+	"time"
+)
+
+// Resources are the resource limits associated with the relay service.
+type Resources struct {
+	// Limit is the (optional) relayed connection limits.
+	Limit *RelayLimit
+
+	// ReservationTTL is the duration of a new (or refreshed reservation).
+	// Defaults to 1hr.
+	ReservationTTL time.Duration
+
+	// MaxReservations is the maximum number of active relay slots; defaults to 128.
+	MaxReservations int
+	// MaxCircuits is the maximum number of open relay connections for each peer; defaults to 16.
+	MaxCircuits int
+	// BufferSize is the size of the relayed connection buffers; defaults to 2048.
+	BufferSize int
+
+	// MaxReservationsPerPeer is the maximum number of reservations originating from the same
+	// peer; default is 4.
+	MaxReservationsPerPeer int
+	// MaxReservationsPerIP is the maximum number of reservations originating from the same
+	// IP address; default is 8.
+	MaxReservationsPerIP int
+	// MaxReservationsPerASN is the maximum number of reservations origination from the same
+	// ASN; default is 32
+	MaxReservationsPerASN int
+}
+
+// RelayLimit are the per relayed connection resource limits.
+type RelayLimit struct {
+	// Duration is the time limit before resetting a relayed connection; defaults to 2min.
+	Duration time.Duration
+	// Data is the limit of data relayed (on each direction) before resetting the connection.
+	// Defaults to 128KB
+	Data int64
+}
+
+// DefaultResources returns a Resources object with the default filled in.
+func DefaultResources() Resources {
+	return Resources{
+		Limit: DefaultLimit(),
+
+		ReservationTTL: time.Hour,
+
+		MaxReservations: 128,
+		MaxCircuits:     16,
+		BufferSize:      2048,
+
+		MaxReservationsPerPeer: 4,
+		MaxReservationsPerIP:   8,
+		MaxReservationsPerASN:  32,
+	}
+}
+
+// DefaultLimit returns a RelayLimit object with the defaults filled in.
+func DefaultLimit() *RelayLimit {
+	return &RelayLimit{
+		Duration: 2 * time.Minute,
+		Data:     1 << 17, // 128K
+	}
+}
diff --git a/p2p/host/circuitv2/util/io.go b/p2p/host/circuitv2/util/io.go
new file mode 100644
index 00000000..911bad19
--- /dev/null
+++ b/p2p/host/circuitv2/util/io.go
@@ -0,0 +1,67 @@
+package util
+
+import (
+	"errors"
+	"io"
+
+	pool "github.com/libp2p/go-buffer-pool"
+	"github.com/libp2p/go-msgio/protoio"
+
+	"github.com/gogo/protobuf/proto"
+	"github.com/multiformats/go-varint"
+)
+
+type DelimitedReader struct {
+	r   io.Reader
+	buf []byte
+}
+
+// The gogo protobuf NewDelimitedReader is buffered, which may eat up stream data.
+// So we need to implement a compatible delimited reader that reads unbuffered.
+// There is a slowdown from unbuffered reading: when reading the message
+// it can take multiple single byte Reads to read the length and another Read
+// to read the message payload.
+// However, this is not critical performance degradation as
+// - the reader is utilized to read one (dialer, stop) or two messages (hop) during
+//   the handshake, so it's a drop in the water for the connection lifetime.
+// - messages are small (max 4k) and the length fits in a couple of bytes,
+//   so overall we have at most three reads per message.
+func NewDelimitedReader(r io.Reader, maxSize int) *DelimitedReader {
+	return &DelimitedReader{r: r, buf: pool.Get(maxSize)}
+}
+
+func (d *DelimitedReader) Close() {
+	if d.buf != nil {
+		pool.Put(d.buf)
+		d.buf = nil
+	}
+}
+
+func (d *DelimitedReader) ReadByte() (byte, error) {
+	buf := d.buf[:1]
+	_, err := d.r.Read(buf)
+	return buf[0], err
+}
+
+func (d *DelimitedReader) ReadMsg(msg proto.Message) error {
+	mlen, err := varint.ReadUvarint(d)
+	if err != nil {
+		return err
+	}
+
+	if uint64(len(d.buf)) < mlen {
+		return errors.New("message too large")
+	}
+
+	buf := d.buf[:mlen]
+	_, err = io.ReadFull(d.r, buf)
+	if err != nil {
+		return err
+	}
+
+	return proto.Unmarshal(buf, msg)
+}
+
+func NewDelimitedWriter(w io.Writer) protoio.WriteCloser {
+	return protoio.NewDelimitedWriter(w)
+}
diff --git a/p2p/host/circuitv2/util/ma.go b/p2p/host/circuitv2/util/ma.go
new file mode 100644
index 00000000..0dd820e2
--- /dev/null
+++ b/p2p/host/circuitv2/util/ma.go
@@ -0,0 +1,10 @@
+package util
+
+import (
+	ma "github.com/multiformats/go-multiaddr"
+)
+
+func IsRelayAddr(a ma.Multiaddr) bool {
+	_, err := a.ValueForProtocol(ma.P_CIRCUIT)
+	return err == nil
+}
diff --git a/p2p/host/circuitv2/util/pbconv.go b/p2p/host/circuitv2/util/pbconv.go
new file mode 100644
index 00000000..63074acc
--- /dev/null
+++ b/p2p/host/circuitv2/util/pbconv.go
@@ -0,0 +1,97 @@
+package util
+
+import (
+	"errors"
+
+	pbv1 "github.com/libp2p/go-libp2p-circuit/pb"
+	pbv2 "github.com/libp2p/go-libp2p/p2p/host/circuitv2/pb"
+
+	"github.com/libp2p/go-libp2p-core/peer"
+
+	ma "github.com/multiformats/go-multiaddr"
+)
+
+func PeerToPeerInfoV1(p *pbv1.CircuitRelay_Peer) (peer.AddrInfo, error) {
+	if p == nil {
+		return peer.AddrInfo{}, errors.New("nil peer")
+	}
+
+	id, err := peer.IDFromBytes(p.Id)
+	if err != nil {
+		return peer.AddrInfo{}, err
+	}
+
+	var addrs []ma.Multiaddr
+	if len(p.Addrs) > 0 {
+		addrs = make([]ma.Multiaddr, 0, len(p.Addrs))
+	}
+
+	for _, addrBytes := range p.Addrs {
+		a, err := ma.NewMultiaddrBytes(addrBytes)
+		if err == nil {
+			addrs = append(addrs, a)
+		}
+	}
+
+	return peer.AddrInfo{ID: id, Addrs: addrs}, nil
+}
+
+func PeerInfoToPeerV1(pi peer.AddrInfo) *pbv1.CircuitRelay_Peer {
+	var addrs [][]byte
+	if len(pi.Addrs) > 0 {
+		addrs = make([][]byte, 0, len(pi.Addrs))
+	}
+
+	for _, addr := range pi.Addrs {
+		addrs = append(addrs, addr.Bytes())
+	}
+
+	p := new(pbv1.CircuitRelay_Peer)
+	p.Id = []byte(pi.ID)
+	p.Addrs = addrs
+
+	return p
+}
+
+func PeerToPeerInfoV2(p *pbv2.Peer) (peer.AddrInfo, error) {
+	if p == nil {
+		return peer.AddrInfo{}, errors.New("nil peer")
+	}
+
+	id, err := peer.IDFromBytes(p.Id)
+	if err != nil {
+		return peer.AddrInfo{}, err
+	}
+
+	var addrs []ma.Multiaddr
+	if len(p.Addrs) > 0 {
+		addrs = make([]ma.Multiaddr, 0, len(p.Addrs))
+	}
+
+	for _, addrBytes := range p.Addrs {
+		a, err := ma.NewMultiaddrBytes(addrBytes)
+		if err == nil {
+			addrs = append(addrs, a)
+		}
+	}
+
+	return peer.AddrInfo{ID: id, Addrs: addrs}, nil
+}
+
+func PeerInfoToPeerV2(pi peer.AddrInfo) *pbv2.Peer {
+	var addrs [][]byte
+
+	if len(pi.Addrs) > 0 {
+		addrs = make([][]byte, 0, len(pi.Addrs))
+	}
+
+	for _, addr := range pi.Addrs {
+		addrs = append(addrs, addr.Bytes())
+	}
+
+	p := new(pbv2.Peer)
+	p.Id = []byte(pi.ID)
+	p.Addrs = addrs
+
+	return p
+}


Test patch:
diff --git a/p2p/host/circuitv2/proto/voucher_test.go b/p2p/host/circuitv2/proto/voucher_test.go
new file mode 100644
index 00000000..148bd77a
--- /dev/null
+++ b/p2p/host/circuitv2/proto/voucher_test.go
@@ -0,0 +1,68 @@
+package proto
+
+import (
+	"testing"
+	"time"
+
+	"github.com/libp2p/go-libp2p-core/crypto"
+	"github.com/libp2p/go-libp2p-core/peer"
+	"github.com/libp2p/go-libp2p-core/record"
+)
+
+func TestReservationVoucher(t *testing.T) {
+	relayPrivk, relayPubk, err := crypto.GenerateKeyPair(crypto.Ed25519, 0)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	_, peerPubk, err := crypto.GenerateKeyPair(crypto.Ed25519, 0)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	relayID, err := peer.IDFromPublicKey(relayPubk)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	peerID, err := peer.IDFromPublicKey(peerPubk)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	rsvp := &ReservationVoucher{
+		Relay:      relayID,
+		Peer:       peerID,
+		Expiration: time.Now().Add(time.Hour),
+	}
+
+	envelope, err := record.Seal(rsvp, relayPrivk)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	blob, err := envelope.Marshal()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	_, rec, err := record.ConsumeEnvelope(blob, RecordDomain)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	rsvp2, ok := rec.(*ReservationVoucher)
+	if !ok {
+		t.Fatalf("invalid record type %+T", rec)
+	}
+
+	if rsvp.Relay != rsvp2.Relay {
+		t.Fatal("relay IDs don't match")
+	}
+	if rsvp.Peer != rsvp2.Peer {
+		t.Fatal("peer IDs don't match")
+	}
+	if rsvp.Expiration.Unix() != rsvp2.Expiration.Unix() {
+		t.Fatal("expirations don't match")
+	}
+}
diff --git a/p2p/host/circuitv2/relay/constraints_test.go b/p2p/host/circuitv2/relay/constraints_test.go
new file mode 100644
index 00000000..0eaaa680
--- /dev/null
+++ b/p2p/host/circuitv2/relay/constraints_test.go
@@ -0,0 +1,142 @@
+package relay
+
+import (
+	"crypto/rand"
+	"fmt"
+	"math"
+	"net"
+	"testing"
+	"time"
+
+	"github.com/libp2p/go-libp2p-core/test"
+	ma "github.com/multiformats/go-multiaddr"
+)
+
+func randomIPv4Addr(t *testing.T) ma.Multiaddr {
+	t.Helper()
+	b := make([]byte, 4)
+	rand.Read(b)
+	addr, err := ma.NewMultiaddr(fmt.Sprintf("/ip4/%s/tcp/1234", net.IP(b)))
+	if err != nil {
+		t.Fatal(err)
+	}
+	return addr
+}
+
+func TestConstraints(t *testing.T) {
+	infResources := func() *Resources {
+		return &Resources{
+			MaxReservations:        math.MaxInt32,
+			MaxReservationsPerPeer: math.MaxInt32,
+			MaxReservationsPerIP:   math.MaxInt32,
+			MaxReservationsPerASN:  math.MaxInt32,
+		}
+	}
+	const limit = 7
+
+	t.Run("total reservations", func(t *testing.T) {
+		res := infResources()
+		res.MaxReservations = limit
+		c := newConstraints(res)
+		for i := 0; i < limit; i++ {
+			if err := c.AddReservation(test.RandPeerIDFatal(t), randomIPv4Addr(t)); err != nil {
+				t.Fatal(err)
+			}
+		}
+		if err := c.AddReservation(test.RandPeerIDFatal(t), randomIPv4Addr(t)); err != errTooManyReservations {
+			t.Fatalf("expected to run into total reservation limit, got %v", err)
+		}
+	})
+
+	t.Run("reservations per peer", func(t *testing.T) {
+		p := test.RandPeerIDFatal(t)
+		res := infResources()
+		res.MaxReservationsPerPeer = limit
+		c := newConstraints(res)
+		for i := 0; i < limit; i++ {
+			if err := c.AddReservation(p, randomIPv4Addr(t)); err != nil {
+				t.Fatal(err)
+			}
+		}
+		if err := c.AddReservation(p, randomIPv4Addr(t)); err != errTooManyReservationsForPeer {
+			t.Fatalf("expected to run into total reservation limit, got %v", err)
+		}
+		if err := c.AddReservation(test.RandPeerIDFatal(t), randomIPv4Addr(t)); err != nil {
+			t.Fatalf("expected reservation for different peer to be possible, got %v", err)
+		}
+	})
+
+	t.Run("reservations per IP", func(t *testing.T) {
+		ip := randomIPv4Addr(t)
+		res := infResources()
+		res.MaxReservationsPerIP = limit
+		c := newConstraints(res)
+		for i := 0; i < limit; i++ {
+			if err := c.AddReservation(test.RandPeerIDFatal(t), ip); err != nil {
+				t.Fatal(err)
+			}
+		}
+		if err := c.AddReservation(test.RandPeerIDFatal(t), ip); err != errTooManyReservationsForIP {
+			t.Fatalf("expected to run into total reservation limit, got %v", err)
+		}
+		if err := c.AddReservation(test.RandPeerIDFatal(t), randomIPv4Addr(t)); err != nil {
+			t.Fatalf("expected reservation for different IP to be possible, got %v", err)
+		}
+	})
+
+	t.Run("reservations per ASN", func(t *testing.T) {
+		getAddr := func(t *testing.T, ip net.IP) ma.Multiaddr {
+			t.Helper()
+			addr, err := ma.NewMultiaddr(fmt.Sprintf("/ip6/%s/tcp/1234", ip))
+			if err != nil {
+				t.Fatal(err)
+			}
+			return addr
+		}
+
+		res := infResources()
+		res.MaxReservationsPerASN = limit
+		c := newConstraints(res)
+		const ipv6Prefix = "2a03:2880:f003:c07:face:b00c::"
+		for i := 0; i < limit; i++ {
+			addr := getAddr(t, net.ParseIP(fmt.Sprintf("%s%d", ipv6Prefix, i+1)))
+			if err := c.AddReservation(test.RandPeerIDFatal(t), addr); err != nil {
+				t.Fatal(err)
+			}
+		}
+		if err := c.AddReservation(test.RandPeerIDFatal(t), getAddr(t, net.ParseIP(fmt.Sprintf("%s%d", ipv6Prefix, 42)))); err != errTooManyReservationsForASN {
+			t.Fatalf("expected to run into total reservation limit, got %v", err)
+		}
+		if err := c.AddReservation(test.RandPeerIDFatal(t), randomIPv4Addr(t)); err != nil {
+			t.Fatalf("expected reservation for different IP to be possible, got %v", err)
+		}
+	})
+}
+
+func TestConstraintsCleanup(t *testing.T) {
+	origValidity := validity
+	defer func() { validity = origValidity }()
+	validity = 500 * time.Millisecond
+
+	const limit = 7
+	res := &Resources{
+		MaxReservations:        limit,
+		MaxReservationsPerPeer: math.MaxInt32,
+		MaxReservationsPerIP:   math.MaxInt32,
+		MaxReservationsPerASN:  math.MaxInt32,
+	}
+	c := newConstraints(res)
+	for i := 0; i < limit; i++ {
+		if err := c.AddReservation(test.RandPeerIDFatal(t), randomIPv4Addr(t)); err != nil {
+			t.Fatal(err)
+		}
+	}
+	if err := c.AddReservation(test.RandPeerIDFatal(t), randomIPv4Addr(t)); err != errTooManyReservations {
+		t.Fatalf("expected to run into total reservation limit, got %v", err)
+	}
+
+	time.Sleep(validity + time.Millisecond)
+	if err := c.AddReservation(test.RandPeerIDFatal(t), randomIPv4Addr(t)); err != nil {
+		t.Fatalf("expected old reservations to have been garbage collected, %v", err)
+	}
+}
diff --git a/p2p/host/circuitv2/test/compat_test.go b/p2p/host/circuitv2/test/compat_test.go
new file mode 100644
index 00000000..bcecba02
--- /dev/null
+++ b/p2p/host/circuitv2/test/compat_test.go
@@ -0,0 +1,177 @@
+package test
+
+import (
+	"bytes"
+	"context"
+	"fmt"
+	"io"
+	"testing"
+
+	v1 "github.com/libp2p/go-libp2p-circuit"
+
+	"github.com/libp2p/go-libp2p-core/host"
+	"github.com/libp2p/go-libp2p-core/network"
+	"github.com/libp2p/go-libp2p-core/peer"
+
+	tptu "github.com/libp2p/go-libp2p-transport-upgrader"
+	ma "github.com/multiformats/go-multiaddr"
+)
+
+func addTransportV1(t *testing.T, ctx context.Context, h host.Host, upgrader *tptu.Upgrader) {
+	err := v1.AddRelayTransport(ctx, h, upgrader)
+	if err != nil {
+		t.Fatal(err)
+	}
+}
+
+func TestRelayCompatV2DialV1(t *testing.T) {
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	hosts, upgraders := getNetHosts(t, ctx, 3)
+	addTransportV1(t, ctx, hosts[0], upgraders[0])
+	addTransport(t, ctx, hosts[2], upgraders[2])
+
+	rch := make(chan []byte, 1)
+	hosts[0].SetStreamHandler("test", func(s network.Stream) {
+		defer s.Close()
+		defer close(rch)
+
+		buf := make([]byte, 1024)
+		nread := 0
+		for nread < len(buf) {
+			n, err := s.Read(buf[nread:])
+			nread += n
+			if err != nil {
+				if err == io.EOF {
+					break
+				}
+				t.Fatal(err)
+			}
+		}
+
+		rch <- buf[:nread]
+	})
+
+	_, err := v1.NewRelay(ctx, hosts[1], upgraders[1], v1.OptHop)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	connect(t, hosts[0], hosts[1])
+	connect(t, hosts[1], hosts[2])
+
+	raddr, err := ma.NewMultiaddr(fmt.Sprintf("/p2p/%s/p2p-circuit/p2p/%s", hosts[1].ID(), hosts[0].ID()))
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	err = hosts[2].Connect(ctx, peer.AddrInfo{ID: hosts[0].ID(), Addrs: []ma.Multiaddr{raddr}})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	conns := hosts[2].Network().ConnsToPeer(hosts[0].ID())
+	if len(conns) != 1 {
+		t.Fatalf("expected 1 connection, but got %d", len(conns))
+	}
+	if conns[0].Stat().Transient {
+		t.Fatal("expected non transient connection")
+	}
+
+	s, err := hosts[2].NewStream(ctx, hosts[0].ID(), "test")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	msg := []byte("relay works!")
+	nwritten, err := s.Write(msg)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if nwritten != len(msg) {
+		t.Fatalf("expected to write %d bytes, but wrote %d instead", len(msg), nwritten)
+	}
+	s.CloseWrite()
+
+	got := <-rch
+	if !bytes.Equal(msg, got) {
+		t.Fatalf("Wrong echo; expected %s but got %s", string(msg), string(got))
+	}
+}
+
+func TestRelayCompatV1DialV2(t *testing.T) {
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	hosts, upgraders := getNetHosts(t, ctx, 3)
+	addTransport(t, ctx, hosts[0], upgraders[0])
+	addTransportV1(t, ctx, hosts[2], upgraders[2])
+
+	rch := make(chan []byte, 1)
+	hosts[0].SetStreamHandler("test", func(s network.Stream) {
+		defer s.Close()
+		defer close(rch)
+
+		buf := make([]byte, 1024)
+		nread := 0
+		for nread < len(buf) {
+			n, err := s.Read(buf[nread:])
+			nread += n
+			if err != nil {
+				if err == io.EOF {
+					break
+				}
+				t.Fatal(err)
+			}
+		}
+
+		rch <- buf[:nread]
+	})
+
+	_, err := v1.NewRelay(ctx, hosts[1], upgraders[1], v1.OptHop)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	connect(t, hosts[0], hosts[1])
+	connect(t, hosts[1], hosts[2])
+
+	raddr, err := ma.NewMultiaddr(fmt.Sprintf("/p2p/%s/p2p-circuit/p2p/%s", hosts[1].ID(), hosts[0].ID()))
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	err = hosts[2].Connect(ctx, peer.AddrInfo{ID: hosts[0].ID(), Addrs: []ma.Multiaddr{raddr}})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	conns := hosts[2].Network().ConnsToPeer(hosts[0].ID())
+	if len(conns) != 1 {
+		t.Fatalf("expected 1 connection, but got %d", len(conns))
+	}
+	if conns[0].Stat().Transient {
+		t.Fatal("expected non transient connection")
+	}
+
+	s, err := hosts[2].NewStream(ctx, hosts[0].ID(), "test")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	msg := []byte("relay works!")
+	nwritten, err := s.Write(msg)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if nwritten != len(msg) {
+		t.Fatalf("expected to write %d bytes, but wrote %d instead", len(msg), nwritten)
+	}
+	s.CloseWrite()
+
+	got := <-rch
+	if !bytes.Equal(msg, got) {
+		t.Fatalf("Wrong echo; expected %s but got %s", string(msg), string(got))
+	}
+}
diff --git a/p2p/host/circuitv2/test/e2e_test.go b/p2p/host/circuitv2/test/e2e_test.go
new file mode 100644
index 00000000..b822f913
--- /dev/null
+++ b/p2p/host/circuitv2/test/e2e_test.go
@@ -0,0 +1,361 @@
+package test
+
+import (
+	"bytes"
+	"context"
+	"fmt"
+	"io"
+	"math/rand"
+	"testing"
+	"time"
+
+	"github.com/libp2p/go-libp2p/p2p/host/circuitv2/client"
+	"github.com/libp2p/go-libp2p/p2p/host/circuitv2/relay"
+
+	"github.com/libp2p/go-libp2p-core/crypto"
+	"github.com/libp2p/go-libp2p-core/host"
+	"github.com/libp2p/go-libp2p-core/mux"
+	"github.com/libp2p/go-libp2p-core/network"
+	"github.com/libp2p/go-libp2p-core/peer"
+
+	logging "github.com/ipfs/go-log"
+	bhost "github.com/libp2p/go-libp2p-blankhost"
+	metrics "github.com/libp2p/go-libp2p-core/metrics"
+	pstoremem "github.com/libp2p/go-libp2p-peerstore/pstoremem"
+	swarm "github.com/libp2p/go-libp2p-swarm"
+	swarmt "github.com/libp2p/go-libp2p-swarm/testing"
+	tptu "github.com/libp2p/go-libp2p-transport-upgrader"
+	tcp "github.com/libp2p/go-tcp-transport"
+	ma "github.com/multiformats/go-multiaddr"
+)
+
+func init() {
+	// TODO temporary for debugging purposes; to be removed for merge.
+	logging.SetLogLevel("relay", "DEBUG")
+	logging.SetLogLevel("p2p-circuit", "DEBUG")
+}
+
+func getNetHosts(t *testing.T, ctx context.Context, n int) (hosts []host.Host, upgraders []*tptu.Upgrader) {
+	for i := 0; i < n; i++ {
+		privk, pubk, err := crypto.GenerateKeyPair(crypto.Ed25519, 0)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		p, err := peer.IDFromPublicKey(pubk)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		ps := pstoremem.NewPeerstore()
+		err = ps.AddPrivKey(p, privk)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		bwr := metrics.NewBandwidthCounter()
+		netw := swarm.NewSwarm(ctx, p, ps, bwr)
+
+		upgrader := swarmt.GenUpgrader(netw)
+		upgraders = append(upgraders, upgrader)
+
+		err = netw.AddTransport(tcp.NewTCPTransport(upgrader))
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		err = netw.Listen(ma.StringCast("/ip4/127.0.0.1/tcp/0"))
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		h := bhost.NewBlankHost(netw)
+
+		hosts = append(hosts, h)
+	}
+
+	return hosts, upgraders
+}
+
+func connect(t *testing.T, a, b host.Host) {
+	pi := peer.AddrInfo{ID: a.ID(), Addrs: a.Addrs()}
+	err := b.Connect(context.Background(), pi)
+	if err != nil {
+		t.Fatal(err)
+	}
+}
+
+func addTransport(t *testing.T, ctx context.Context, h host.Host, upgrader *tptu.Upgrader) {
+	err := client.AddTransport(ctx, h, upgrader)
+	if err != nil {
+		t.Fatal(err)
+	}
+}
+
+func TestBasicRelay(t *testing.T) {
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	hosts, upgraders := getNetHosts(t, ctx, 3)
+	addTransport(t, ctx, hosts[0], upgraders[0])
+	addTransport(t, ctx, hosts[2], upgraders[2])
+
+	rch := make(chan []byte, 1)
+	hosts[0].SetStreamHandler("test", func(s network.Stream) {
+		defer s.Close()
+		defer close(rch)
+
+		buf := make([]byte, 1024)
+		nread := 0
+		for nread < len(buf) {
+			n, err := s.Read(buf[nread:])
+			nread += n
+			if err != nil {
+				if err == io.EOF {
+					break
+				}
+				t.Fatal(err)
+			}
+		}
+
+		rch <- buf[:nread]
+	})
+
+	r, err := relay.New(hosts[1])
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer r.Close()
+
+	connect(t, hosts[0], hosts[1])
+	connect(t, hosts[1], hosts[2])
+
+	rinfo := hosts[1].Peerstore().PeerInfo(hosts[1].ID())
+	rsvp, err := client.Reserve(ctx, hosts[0], rinfo)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if rsvp.Voucher == nil {
+		t.Fatal("no reservation voucher")
+	}
+
+	raddr, err := ma.NewMultiaddr(fmt.Sprintf("/p2p/%s/p2p-circuit/p2p/%s", hosts[1].ID(), hosts[0].ID()))
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	err = hosts[2].Connect(ctx, peer.AddrInfo{ID: hosts[0].ID(), Addrs: []ma.Multiaddr{raddr}})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	conns := hosts[2].Network().ConnsToPeer(hosts[0].ID())
+	if len(conns) != 1 {
+		t.Fatalf("expected 1 connection, but got %d", len(conns))
+	}
+	if !conns[0].Stat().Transient {
+		t.Fatal("expected transient connection")
+	}
+
+	s, err := hosts[2].NewStream(network.WithUseTransient(ctx, "test"), hosts[0].ID(), "test")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	msg := []byte("relay works!")
+	nwritten, err := s.Write(msg)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if nwritten != len(msg) {
+		t.Fatalf("expected to write %d bytes, but wrote %d instead", len(msg), nwritten)
+	}
+	s.CloseWrite()
+
+	got := <-rch
+	if !bytes.Equal(msg, got) {
+		t.Fatalf("Wrong echo; expected %s but got %s", string(msg), string(got))
+	}
+}
+
+func TestRelayLimitTime(t *testing.T) {
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	hosts, upgraders := getNetHosts(t, ctx, 3)
+	addTransport(t, ctx, hosts[0], upgraders[0])
+	addTransport(t, ctx, hosts[2], upgraders[2])
+
+	rch := make(chan error, 1)
+	hosts[0].SetStreamHandler("test", func(s network.Stream) {
+		defer s.Close()
+		defer close(rch)
+
+		buf := make([]byte, 1024)
+		_, err := s.Read(buf)
+		rch <- err
+	})
+
+	rc := relay.DefaultResources()
+	rc.Limit.Duration = time.Second
+
+	r, err := relay.New(hosts[1], relay.WithResources(rc))
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer r.Close()
+
+	connect(t, hosts[0], hosts[1])
+	connect(t, hosts[1], hosts[2])
+
+	rinfo := hosts[1].Peerstore().PeerInfo(hosts[1].ID())
+	_, err = client.Reserve(ctx, hosts[0], rinfo)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	raddr, err := ma.NewMultiaddr(fmt.Sprintf("/p2p/%s/p2p-circuit/p2p/%s", hosts[1].ID(), hosts[0].ID()))
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	err = hosts[2].Connect(ctx, peer.AddrInfo{ID: hosts[0].ID(), Addrs: []ma.Multiaddr{raddr}})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	conns := hosts[2].Network().ConnsToPeer(hosts[0].ID())
+	if len(conns) != 1 {
+		t.Fatalf("expected 1 connection, but got %d", len(conns))
+	}
+	if !conns[0].Stat().Transient {
+		t.Fatal("expected transient connection")
+	}
+
+	s, err := hosts[2].NewStream(network.WithUseTransient(ctx, "test"), hosts[0].ID(), "test")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	time.Sleep(2 * time.Second)
+	n, err := s.Write([]byte("should be closed"))
+	if n > 0 {
+		t.Fatalf("expected to write 0 bytes, wrote %d", n)
+	}
+	if err != mux.ErrReset {
+		t.Fatalf("expected reset, but got %s", err)
+	}
+
+	err = <-rch
+	if err != mux.ErrReset {
+		t.Fatalf("expected reset, but got %s", err)
+	}
+}
+
+func TestRelayLimitData(t *testing.T) {
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	hosts, upgraders := getNetHosts(t, ctx, 3)
+	addTransport(t, ctx, hosts[0], upgraders[0])
+	addTransport(t, ctx, hosts[2], upgraders[2])
+
+	rch := make(chan int, 1)
+	hosts[0].SetStreamHandler("test", func(s network.Stream) {
+		defer s.Close()
+		defer close(rch)
+
+		buf := make([]byte, 1024)
+		for i := 0; i < 3; i++ {
+			n, err := s.Read(buf)
+			if err != nil {
+				t.Fatal(err)
+			}
+			rch <- n
+		}
+
+		n, err := s.Read(buf)
+		if err != mux.ErrReset {
+			t.Fatalf("expected reset but got %s", err)
+		}
+		rch <- n
+	})
+
+	rc := relay.DefaultResources()
+	rc.Limit.Duration = time.Second
+	rc.Limit.Data = 4096
+
+	r, err := relay.New(hosts[1], relay.WithResources(rc))
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer r.Close()
+
+	connect(t, hosts[0], hosts[1])
+	connect(t, hosts[1], hosts[2])
+
+	rinfo := hosts[1].Peerstore().PeerInfo(hosts[1].ID())
+	_, err = client.Reserve(ctx, hosts[0], rinfo)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	raddr, err := ma.NewMultiaddr(fmt.Sprintf("/p2p/%s/p2p-circuit/p2p/%s", hosts[1].ID(), hosts[0].ID()))
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	err = hosts[2].Connect(ctx, peer.AddrInfo{ID: hosts[0].ID(), Addrs: []ma.Multiaddr{raddr}})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	conns := hosts[2].Network().ConnsToPeer(hosts[0].ID())
+	if len(conns) != 1 {
+		t.Fatalf("expected 1 connection, but got %d", len(conns))
+	}
+	if !conns[0].Stat().Transient {
+		t.Fatal("expected transient connection")
+	}
+
+	s, err := hosts[2].NewStream(network.WithUseTransient(ctx, "test"), hosts[0].ID(), "test")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	buf := make([]byte, 1024)
+	for i := 0; i < 3; i++ {
+		_, err = rand.Read(buf)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		n, err := s.Write(buf)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if n != len(buf) {
+			t.Fatalf("expected to write %d bytes but wrote %d", len(buf), n)
+		}
+
+		n = <-rch
+		if n != len(buf) {
+			t.Fatalf("expected to read %d bytes but read %d", len(buf), n)
+		}
+	}
+
+	buf = make([]byte, 4096)
+	_, err = rand.Read(buf)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	s.Write(buf)
+
+	n := <-rch
+	if n != 0 {
+		t.Fatalf("expected to read 0 bytes but read %d", n)
+	}
+
+}
diff --git a/p2p/host/circuitv2/test/empty.go b/p2p/host/circuitv2/test/empty.go
new file mode 100644
index 00000000..56e54040
--- /dev/null
+++ b/p2p/host/circuitv2/test/empty.go
@@ -0,0 +1,1 @@
+package test