diff --git a/.gitignore b/.gitignore
index 608e9d0d..500d68ca 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,7 @@
+# Develop tools
+/.vscode/
+/.idea/
+
 # Binaries for programs and plugins
 *.exe
 *.exe~
diff --git a/README.zh-cn.md b/README.zh-cn.md
index b5599821..b770c8bd 100644
--- a/README.zh-cn.md
+++ b/README.zh-cn.md
@@ -32,5 +32,5 @@ Go Micro把分布式系统的各种细节抽象出来。下面是它的主要特
 
 ## 快速上手
 
-更多关于架构、安装的资料可以查看[文档](https://micro.mu/docs/go-micro_cn.html)。
+更多关于架构、安装的资料可以查看[文档](https://micro.mu/docs/cn/)。
 
diff --git a/client/grpc/codec.go b/client/grpc/codec.go
index e7891eef..c792abbd 100644
--- a/client/grpc/codec.go
+++ b/client/grpc/codec.go
@@ -30,6 +30,7 @@ var (
 		"application/proto":        protoCodec{},
 		"application/protobuf":     protoCodec{},
 		"application/octet-stream": protoCodec{},
+		"application/grpc":         protoCodec{},
 		"application/grpc+json":    jsonCodec{},
 		"application/grpc+proto":   protoCodec{},
 		"application/grpc+bytes":   bytesCodec{},
diff --git a/client/rpc_client.go b/client/rpc_client.go
index 754e4329..2fe51934 100644
--- a/client/rpc_client.go
+++ b/client/rpc_client.go
@@ -96,7 +96,15 @@ func (r *rpcClient) call(ctx context.Context, node *registry.Node, req Request,
 		}
 	}
 
-	c, err := r.pool.Get(address, transport.WithTimeout(opts.DialTimeout))
+	dOpts := []transport.DialOption{
+		transport.WithStream(),
+	}
+
+	if opts.DialTimeout >= 0 {
+		dOpts = append(dOpts, transport.WithTimeout(opts.DialTimeout))
+	}
+
+	c, err := r.pool.Get(address, dOpts...)
 	if err != nil {
 		return errors.InternalServerError("go.micro.client", "connection error: %v", err)
 	}
diff --git a/config/source/consul/README.md b/config/source/consul/README.md
index 2ba45e5b..28006fc4 100644
--- a/config/source/consul/README.md
+++ b/config/source/consul/README.md
@@ -44,6 +44,6 @@ Load the source into config
 // Create new config
 conf := config.NewConfig()
 
-// Load file source
+// Load consul source
 conf.Load(consulSource)
 ```
diff --git a/config/source/env/README.md b/config/source/env/README.md
index 25bfa07a..61a18f8d 100644
--- a/config/source/env/README.md
+++ b/config/source/env/README.md
@@ -91,6 +91,6 @@ Load the source into config
 // Create new config
 conf := config.NewConfig()
 
-// Load file source
+// Load env source
 conf.Load(src)
 ```
diff --git a/config/source/flag/README.md b/config/source/flag/README.md
index fb78bae2..79e5cb54 100644
--- a/config/source/flag/README.md
+++ b/config/source/flag/README.md
@@ -42,6 +42,6 @@ Load the source into config
 // Create new config
 conf := config.NewConfig()
 
-// Load file source
+// Load flag source
 conf.Load(flagSource)
 ```
diff --git a/config/source/memory/README.md b/config/source/memory/README.md
index 7f1a4008..adef980b 100644
--- a/config/source/memory/README.md
+++ b/config/source/memory/README.md
@@ -39,6 +39,6 @@ Load the source into config
 // Create new config
 conf := config.NewConfig()
 
-// Load file source
+// Load memory source
 conf.Load(memorySource)
 ```
diff --git a/network/default.go b/network/default.go
new file mode 100644
index 00000000..23c924f6
--- /dev/null
+++ b/network/default.go
@@ -0,0 +1,1005 @@
+package network
+
+import (
+	"container/list"
+	"errors"
+	"sync"
+	"time"
+
+	"github.com/golang/protobuf/proto"
+	"github.com/micro/go-micro/client"
+	rtr "github.com/micro/go-micro/client/selector/router"
+	pbNet "github.com/micro/go-micro/network/proto"
+	"github.com/micro/go-micro/proxy"
+	"github.com/micro/go-micro/router"
+	pbRtr "github.com/micro/go-micro/router/proto"
+	"github.com/micro/go-micro/server"
+	"github.com/micro/go-micro/transport"
+	"github.com/micro/go-micro/tunnel"
+	tun "github.com/micro/go-micro/tunnel/transport"
+	"github.com/micro/go-micro/util/log"
+)
+
+var (
+	// NetworkChannel is the name of the tunnel channel for passing network messages
+	NetworkChannel = "network"
+	// ControlChannel is the name of the tunnel channel for passing control message
+	ControlChannel = "control"
+	// DefaultLink is default network link
+	DefaultLink = "network"
+)
+
+var (
+	// ErrMsgUnknown is returned when unknown message is attempted to send or receive
+	ErrMsgUnknown = errors.New("unknown message")
+	// ErrClientNotFound is returned when client for tunnel channel could not be found
+	ErrClientNotFound = errors.New("client not found")
+)
+
+// node is network node
+type node struct {
+	sync.RWMutex
+	// id is node id
+	id string
+	// address is node address
+	address string
+	// neighbours maps the node neighbourhood
+	neighbours map[string]*node
+	// network returns the node network
+	network Network
+	// lastSeen stores the time the node has been seen last time
+	lastSeen time.Time
+}
+
+// Id is node ide
+func (n *node) Id() string {
+	return n.id
+}
+
+// Address returns node address
+func (n *node) Address() string {
+	return n.address
+}
+
+// Network returns node network
+func (n *node) Network() Network {
+	return n.network
+}
+
+// Neighbourhood returns node neighbourhood
+func (n *node) Neighbourhood() []Node {
+	var nodes []Node
+	n.RLock()
+	for _, neighbourNode := range n.neighbours {
+		// make a copy of the node
+		n := &node{
+			id:      neighbourNode.id,
+			address: neighbourNode.address,
+			network: neighbourNode.network,
+		}
+		// NOTE: we do not care about neighbour's neighbours
+		nodes = append(nodes, n)
+	}
+	n.RUnlock()
+
+	return nodes
+}
+
+// network implements Network interface
+type network struct {
+	// node is network node
+	*node
+	// options configure the network
+	options Options
+	// rtr is network router
+	router.Router
+	// prx is network proxy
+	proxy.Proxy
+	// tun is network tunnel
+	tunnel.Tunnel
+	// server is network server
+	server server.Server
+	// client is network client
+	client client.Client
+
+	// tunClient is a map of tunnel clients keyed over tunnel channel names
+	tunClient map[string]transport.Client
+
+	sync.RWMutex
+	// connected marks the network as connected
+	connected bool
+	// closed closes the network
+	closed chan bool
+}
+
+// newNetwork returns a new network node
+func newNetwork(opts ...Option) Network {
+	options := DefaultOptions()
+
+	for _, o := range opts {
+		o(&options)
+	}
+
+	// init tunnel address to the network bind address
+	options.Tunnel.Init(
+		tunnel.Address(options.Address),
+		tunnel.Nodes(options.Nodes...),
+	)
+
+	// init router Id to the network id
+	options.Router.Init(
+		router.Id(options.Id),
+	)
+
+	// create tunnel client with tunnel transport
+	tunTransport := tun.NewTransport(
+		tun.WithTunnel(options.Tunnel),
+	)
+
+	// server is network server
+	server := server.NewServer(
+		server.Id(options.Id),
+		server.Address(options.Address),
+		server.Name(options.Name),
+		server.Transport(tunTransport),
+	)
+
+	// client is network client
+	client := client.NewClient(
+		client.Transport(tunTransport),
+		client.Selector(
+			rtr.NewSelector(
+				rtr.WithRouter(options.Router),
+			),
+		),
+	)
+
+	network := &network{
+		node: &node{
+			id:         options.Id,
+			address:    options.Address,
+			neighbours: make(map[string]*node),
+		},
+		options:   options,
+		Router:    options.Router,
+		Proxy:     options.Proxy,
+		Tunnel:    options.Tunnel,
+		server:    server,
+		client:    client,
+		tunClient: make(map[string]transport.Client),
+	}
+
+	network.node.network = network
+
+	return network
+}
+
+// Options returns network options
+func (n *network) Options() Options {
+	n.Lock()
+	options := n.options
+	n.Unlock()
+
+	return options
+}
+
+// Name returns network name
+func (n *network) Name() string {
+	return n.options.Name
+}
+
+// Address returns network bind address
+func (n *network) Address() string {
+	return n.Tunnel.Address()
+}
+
+// resolveNodes resolves network nodes to addresses
+func (n *network) resolveNodes() ([]string, error) {
+	// resolve the network address to network nodes
+	records, err := n.options.Resolver.Resolve(n.options.Name)
+	if err != nil {
+		return nil, err
+	}
+
+	nodeMap := make(map[string]bool)
+
+	// collect network node addresses
+	var nodes []string
+	for _, record := range records {
+		nodes = append(nodes, record.Address)
+		nodeMap[record.Address] = true
+	}
+
+	// append seed nodes if we have them
+	for _, node := range n.options.Nodes {
+		if _, ok := nodeMap[node]; !ok {
+			nodes = append(nodes, node)
+		}
+	}
+
+	return nodes, nil
+}
+
+// resolve continuously resolves network nodes and initializes network tunnel with resolved addresses
+func (n *network) resolve() {
+	resolve := time.NewTicker(ResolveTime)
+	defer resolve.Stop()
+
+	for {
+		select {
+		case <-n.closed:
+			return
+		case <-resolve.C:
+			nodes, err := n.resolveNodes()
+			if err != nil {
+				log.Debugf("Network failed to resolve nodes: %v", err)
+				continue
+			}
+			// initialize the tunnel
+			n.Tunnel.Init(
+				tunnel.Nodes(nodes...),
+			)
+		}
+	}
+}
+
+// handleNetConn handles network announcement messages
+func (n *network) handleNetConn(sess tunnel.Session, msg chan *transport.Message) {
+	for {
+		m := new(transport.Message)
+		if err := sess.Recv(m); err != nil {
+			// TODO: should we bail here?
+			log.Debugf("Network tunnel [%s] receive error: %v", NetworkChannel, err)
+			return
+		}
+
+		select {
+		case msg <- m:
+		case <-n.closed:
+			return
+		}
+	}
+}
+
+// acceptNetConn accepts connections from NetworkChannel
+func (n *network) acceptNetConn(l tunnel.Listener, recv chan *transport.Message) {
+	for {
+		// accept a connection
+		conn, err := l.Accept()
+		if err != nil {
+			// TODO: handle this
+			log.Debugf("Network tunnel [%s] accept error: %v", NetworkChannel, err)
+			return
+		}
+
+		select {
+		case <-n.closed:
+			return
+		default:
+			// go handle NetworkChannel connection
+			go n.handleNetConn(conn, recv)
+		}
+	}
+}
+
+// processNetChan processes messages received on NetworkChannel
+func (n *network) processNetChan(client transport.Client, listener tunnel.Listener) {
+	// receive network message queue
+	recv := make(chan *transport.Message, 128)
+
+	// accept NetworkChannel connections
+	go n.acceptNetConn(listener, recv)
+
+	for {
+		select {
+		case m := <-recv:
+			// switch on type of message and take action
+			switch m.Header["Micro-Method"] {
+			case "connect":
+				// mark the time the message has been received
+				now := time.Now()
+				pbNetConnect := &pbNet.Connect{}
+				if err := proto.Unmarshal(m.Body, pbNetConnect); err != nil {
+					log.Debugf("Network tunnel [%s] connect unmarshal error: %v", NetworkChannel, err)
+					continue
+				}
+				// don't process your own messages
+				if pbNetConnect.Node.Id == n.options.Id {
+					continue
+				}
+				n.Lock()
+				log.Debugf("Network received connect message from: %s", pbNetConnect.Node.Id)
+				// if the entry already exists skip adding it
+				if neighbour, ok := n.neighbours[pbNetConnect.Node.Id]; ok {
+					// update lastSeen timestamp
+					if n.neighbours[pbNetConnect.Node.Id].lastSeen.Before(now) {
+						neighbour.lastSeen = now
+					}
+					n.Unlock()
+					continue
+				}
+				// add a new neighbour
+				// NOTE: new node does not have any neighbours
+				n.neighbours[pbNetConnect.Node.Id] = &node{
+					id:         pbNetConnect.Node.Id,
+					address:    pbNetConnect.Node.Address,
+					neighbours: make(map[string]*node),
+					lastSeen:   now,
+				}
+				n.Unlock()
+				// advertise yourself to the network
+				if err := n.sendMsg("neighbour", NetworkChannel); err != nil {
+					log.Debugf("Network failed to advertise neighbours: %v", err)
+				}
+				// advertise all the routes when a new node has connected
+				if err := n.Router.Solicit(); err != nil {
+					log.Debugf("Network failed to solicit routes: %s", err)
+				}
+			case "neighbour":
+				// mark the time the message has been received
+				now := time.Now()
+				pbNetNeighbour := &pbNet.Neighbour{}
+				if err := proto.Unmarshal(m.Body, pbNetNeighbour); err != nil {
+					log.Debugf("Network tunnel [%s] neighbour unmarshal error: %v", NetworkChannel, err)
+					continue
+				}
+				// don't process your own messages
+				if pbNetNeighbour.Node.Id == n.options.Id {
+					continue
+				}
+				n.Lock()
+				log.Debugf("Network received neighbour message from: %s", pbNetNeighbour.Node.Id)
+				// only add the neighbour if it is NOT already in node's list of neighbours
+				_, exists := n.neighbours[pbNetNeighbour.Node.Id]
+				if !exists {
+					n.neighbours[pbNetNeighbour.Node.Id] = &node{
+						id:         pbNetNeighbour.Node.Id,
+						address:    pbNetNeighbour.Node.Address,
+						neighbours: make(map[string]*node),
+						lastSeen:   now,
+					}
+				}
+				// update lastSeen timestamp
+				if n.neighbours[pbNetNeighbour.Node.Id].lastSeen.Before(now) {
+					n.neighbours[pbNetNeighbour.Node.Id].lastSeen = now
+				}
+				// update/store the neighbour node neighbours
+				// NOTE: * we do NOT update lastSeen time for the neighbours of the neighbour
+				//	 * even though we are NOT interested in neighbours of neighbours here
+				// 	   we still allocate the map of neighbours for each of them
+				for _, pbNeighbour := range pbNetNeighbour.Neighbours {
+					neighbourNode := &node{
+						id:         pbNeighbour.Id,
+						address:    pbNeighbour.Address,
+						neighbours: make(map[string]*node),
+					}
+					n.neighbours[pbNetNeighbour.Node.Id].neighbours[pbNeighbour.Id] = neighbourNode
+				}
+				n.Unlock()
+				// send a solicit message when discovering a new node
+				// NOTE: we need to send the solicit message here after the Lock is released as sendMsg locks, too
+				if !exists {
+					if err := n.sendMsg("solicit", ControlChannel); err != nil {
+						log.Debugf("Network failed to send solicit message: %s", err)
+					}
+				}
+			case "close":
+				pbNetClose := &pbNet.Close{}
+				if err := proto.Unmarshal(m.Body, pbNetClose); err != nil {
+					log.Debugf("Network tunnel [%s] close unmarshal error: %v", NetworkChannel, err)
+					continue
+				}
+				// don't process your own messages
+				if pbNetClose.Node.Id == n.options.Id {
+					continue
+				}
+				n.Lock()
+				log.Debugf("Network received close message from: %s", pbNetClose.Node.Id)
+				if err := n.pruneNode(pbNetClose.Node.Id); err != nil {
+					log.Debugf("Network failed to prune the node %s: %v", pbNetClose.Node.Id, err)
+					continue
+				}
+				n.Unlock()
+			}
+		case <-n.closed:
+			return
+		}
+	}
+}
+
+// sendMsg sends a message to the tunnel channel
+func (n *network) sendMsg(msgType string, channel string) error {
+	node := &pbNet.Node{
+		Id:      n.options.Id,
+		Address: n.options.Address,
+	}
+
+	var protoMsg proto.Message
+
+	switch msgType {
+	case "connect":
+		protoMsg = &pbNet.Connect{
+			Node: node,
+		}
+	case "close":
+		protoMsg = &pbNet.Close{
+			Node: node,
+		}
+	case "solicit":
+		protoMsg = &pbNet.Solicit{
+			Node: node,
+		}
+	case "neighbour":
+		n.RLock()
+		nodes := make([]*pbNet.Node, len(n.neighbours))
+		i := 0
+		for id := range n.neighbours {
+			nodes[i] = &pbNet.Node{
+				Id:      id,
+				Address: n.neighbours[id].address,
+			}
+			i++
+		}
+		n.RUnlock()
+		protoMsg = &pbNet.Neighbour{
+			Node:       node,
+			Neighbours: nodes,
+		}
+	default:
+		return ErrMsgUnknown
+	}
+
+	body, err := proto.Marshal(protoMsg)
+	if err != nil {
+		return err
+	}
+	// create transport message and chuck it down the pipe
+	m := transport.Message{
+		Header: map[string]string{
+			"Micro-Method": msgType,
+		},
+		Body: body,
+	}
+
+	n.RLock()
+	client, ok := n.tunClient[channel]
+	if !ok {
+		n.RUnlock()
+		return ErrClientNotFound
+	}
+	n.RUnlock()
+
+	log.Debugf("Network sending %s message from: %s", msgType, node.Id)
+	if err := client.Send(&m); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// announce announces node neighbourhood to the network
+func (n *network) announce(client transport.Client) {
+	announce := time.NewTicker(AnnounceTime)
+	defer announce.Stop()
+
+	for {
+		select {
+		case <-n.closed:
+			return
+		case <-announce.C:
+			// advertise yourself to the network
+			if err := n.sendMsg("neighbour", NetworkChannel); err != nil {
+				log.Debugf("Network failed to advertise neighbours: %v", err)
+				continue
+			}
+		}
+	}
+}
+
+// pruneNode removes a node with given id from the list of neighbours. It also removes all routes originted by this node.
+// NOTE: this method is not thread-safe; when calling it make sure you lock the particular code segment
+func (n *network) pruneNode(id string) error {
+	delete(n.neighbours, id)
+	// lookup all the routes originated at this node
+	q := router.NewQuery(
+		router.QueryRouter(id),
+	)
+	routes, err := n.Router.Table().Query(q)
+	if err != nil && err != router.ErrRouteNotFound {
+		return err
+	}
+	// delete the found routes
+	log.Logf("Network deleting routes originated by router: %s", id)
+	for _, route := range routes {
+		if err := n.Router.Table().Delete(route); err != nil && err != router.ErrRouteNotFound {
+			return err
+		}
+	}
+
+	return nil
+}
+
+// prune the nodes that have not been seen for certain period of time defined by PruneTime
+// Additionally, prune also removes all the routes originated by these nodes
+func (n *network) prune() {
+	prune := time.NewTicker(PruneTime)
+	defer prune.Stop()
+
+	for {
+		select {
+		case <-n.closed:
+			return
+		case <-prune.C:
+			n.Lock()
+			for id, node := range n.neighbours {
+				if id == n.options.Id {
+					continue
+				}
+				if time.Since(node.lastSeen) > PruneTime {
+					log.Debugf("Network deleting node %s: reached prune time threshold", id)
+					if err := n.pruneNode(id); err != nil {
+						log.Debugf("Network failed to prune the node %s: %v", id, err)
+						continue
+					}
+				}
+			}
+			n.Unlock()
+		}
+	}
+}
+
+// handleCtrlConn handles ControlChannel connections
+func (n *network) handleCtrlConn(sess tunnel.Session, msg chan *transport.Message) {
+	for {
+		m := new(transport.Message)
+		if err := sess.Recv(m); err != nil {
+			// TODO: should we bail here?
+			log.Debugf("Network tunnel advert receive error: %v", err)
+			return
+		}
+
+		select {
+		case msg <- m:
+		case <-n.closed:
+			return
+		}
+	}
+}
+
+// acceptCtrlConn accepts connections from ControlChannel
+func (n *network) acceptCtrlConn(l tunnel.Listener, recv chan *transport.Message) {
+	for {
+		// accept a connection
+		conn, err := l.Accept()
+		if err != nil {
+			// TODO: handle this
+			log.Debugf("Network tunnel [%s] accept error: %v", ControlChannel, err)
+			return
+		}
+
+		select {
+		case <-n.closed:
+			return
+		default:
+			// go handle ControlChannel connection
+			go n.handleCtrlConn(conn, recv)
+		}
+	}
+}
+
+// setRouteMetric calculates metric of the route and updates it in place
+// - Local route metric is 1
+// - Routes with ID of adjacent neighbour are 10
+// - Routes of neighbours of the advertiser are 100
+// - Routes beyond your neighbourhood are 1000
+func (n *network) setRouteMetric(route *router.Route) {
+	// we are the origin of the route
+	if route.Router == n.options.Id {
+		route.Metric = 1
+		return
+	}
+
+	n.RLock()
+	// check if the route origin is our neighbour
+	if _, ok := n.neighbours[route.Router]; ok {
+		route.Metric = 10
+		n.RUnlock()
+		return
+	}
+
+	// check if the route origin is the neighbour of our neighbour
+	for _, node := range n.neighbours {
+		for id := range node.neighbours {
+			if route.Router == id {
+				route.Metric = 100
+				n.RUnlock()
+				return
+			}
+		}
+	}
+	n.RUnlock()
+
+	// the origin of the route is beyond our neighbourhood
+	route.Metric = 1000
+}
+
+// processCtrlChan processes messages received on ControlChannel
+func (n *network) processCtrlChan(client transport.Client, listener tunnel.Listener) {
+	// receive control message queue
+	recv := make(chan *transport.Message, 128)
+
+	// accept ControlChannel cconnections
+	go n.acceptCtrlConn(listener, recv)
+
+	for {
+		select {
+		case m := <-recv:
+			// switch on type of message and take action
+			switch m.Header["Micro-Method"] {
+			case "advert":
+				now := time.Now()
+				pbRtrAdvert := &pbRtr.Advert{}
+				if err := proto.Unmarshal(m.Body, pbRtrAdvert); err != nil {
+					log.Debugf("Network fail to unmarshal advert message: %v", err)
+					continue
+				}
+				// don't process your own messages
+				if pbRtrAdvert.Id == n.options.Id {
+					continue
+				}
+				// loookup advertising node in our neighbourhood
+				n.RLock()
+				log.Debugf("Network received advert message from: %s", pbRtrAdvert.Id)
+				advertNode, ok := n.neighbours[pbRtrAdvert.Id]
+				if !ok {
+					// advertising node has not been registered as our neighbour, yet
+					// let's add it to the map of our neighbours
+					advertNode = &node{
+						id:         pbRtrAdvert.Id,
+						neighbours: make(map[string]*node),
+						lastSeen:   now,
+					}
+					n.neighbours[pbRtrAdvert.Id] = advertNode
+					// send a solicit message when discovering a new node
+					if err := n.sendMsg("solicit", NetworkChannel); err != nil {
+						log.Debugf("Network failed to send solicit message: %s", err)
+					}
+				}
+				n.RUnlock()
+
+				var events []*router.Event
+				for _, event := range pbRtrAdvert.Events {
+					// set the address of the advertising node
+					// we know Route.Gateway is the address of advertNode
+					// NOTE: this is true only when advertNode had not been registered
+					// as our neighbour when we received the advert from it
+					if advertNode.address == "" {
+						advertNode.address = event.Route.Gateway
+					}
+					// if advertising node id is not the same as Route.Router
+					// we know the advertising node is not the origin of the route
+					if advertNode.id != event.Route.Router {
+						// if the origin router is not in the advertising node neighbourhood
+						// we can't rule out potential routing loops so we bail here
+						if _, ok := advertNode.neighbours[event.Route.Router]; !ok {
+							continue
+						}
+					}
+					route := router.Route{
+						Service: event.Route.Service,
+						Address: event.Route.Address,
+						Gateway: event.Route.Gateway,
+						Network: event.Route.Network,
+						Router:  event.Route.Router,
+						Link:    event.Route.Link,
+						Metric:  int(event.Route.Metric),
+					}
+					// set the route metric
+					n.setRouteMetric(&route)
+					// throw away metric bigger than 1000
+					if route.Metric > 1000 {
+						log.Debugf("Network route metric %d dropping node: %s", route.Metric, route.Router)
+						continue
+					}
+					// create router event
+					e := &router.Event{
+						Type:      router.EventType(event.Type),
+						Timestamp: time.Unix(0, pbRtrAdvert.Timestamp),
+						Route:     route,
+					}
+					events = append(events, e)
+				}
+				advert := &router.Advert{
+					Id:        pbRtrAdvert.Id,
+					Type:      router.AdvertType(pbRtrAdvert.Type),
+					Timestamp: time.Unix(0, pbRtrAdvert.Timestamp),
+					TTL:       time.Duration(pbRtrAdvert.Ttl),
+					Events:    events,
+				}
+
+				if err := n.Router.Process(advert); err != nil {
+					log.Debugf("Network failed to process advert %s: %v", advert.Id, err)
+					continue
+				}
+			case "solicit":
+				pbNetSolicit := &pbNet.Solicit{}
+				if err := proto.Unmarshal(m.Body, pbNetSolicit); err != nil {
+					log.Debugf("Network fail to unmarshal solicit message: %v", err)
+					continue
+				}
+				log.Debugf("Network received solicit message from: %s", pbNetSolicit.Node.Id)
+				// don't process your own messages
+				if pbNetSolicit.Node.Id == n.options.Id {
+					continue
+				}
+				// advertise all the routes when a new node has connected
+				if err := n.Router.Solicit(); err != nil {
+					log.Debugf("Network failed to solicit routes: %s", err)
+				}
+			}
+		case <-n.closed:
+			return
+		}
+	}
+}
+
+// advertise advertises routes to the network
+func (n *network) advertise(client transport.Client, advertChan <-chan *router.Advert) {
+	for {
+		select {
+		// process local adverts and randomly fire them at other nodes
+		case advert := <-advertChan:
+			// create a proto advert
+			var events []*pbRtr.Event
+			for _, event := range advert.Events {
+				// NOTE: we override the Gateway and Link fields here
+				route := &pbRtr.Route{
+					Service: event.Route.Service,
+					Address: event.Route.Address,
+					Gateway: n.options.Address,
+					Network: event.Route.Network,
+					Router:  event.Route.Router,
+					Link:    DefaultLink,
+					Metric:  int64(event.Route.Metric),
+				}
+				e := &pbRtr.Event{
+					Type:      pbRtr.EventType(event.Type),
+					Timestamp: event.Timestamp.UnixNano(),
+					Route:     route,
+				}
+				events = append(events, e)
+			}
+			pbRtrAdvert := &pbRtr.Advert{
+				Id:        advert.Id,
+				Type:      pbRtr.AdvertType(advert.Type),
+				Timestamp: advert.Timestamp.UnixNano(),
+				Events:    events,
+			}
+			body, err := proto.Marshal(pbRtrAdvert)
+			if err != nil {
+				// TODO: should we bail here?
+				log.Debugf("Network failed to marshal advert message: %v", err)
+				continue
+			}
+			// create transport message and chuck it down the pipe
+			m := transport.Message{
+				Header: map[string]string{
+					"Micro-Method": "advert",
+				},
+				Body: body,
+			}
+
+			log.Debugf("Network sending advert message from: %s", pbRtrAdvert.Id)
+			if err := client.Send(&m); err != nil {
+				log.Debugf("Network failed to send advert %s: %v", pbRtrAdvert.Id, err)
+				continue
+			}
+		case <-n.closed:
+			return
+		}
+	}
+}
+
+// Connect connects the network
+func (n *network) Connect() error {
+	n.Lock()
+	// return if already connected
+	if n.connected {
+		return nil
+	}
+
+	// try to resolve network nodes
+	nodes, err := n.resolveNodes()
+	if err != nil {
+		log.Debugf("Network failed to resolve nodes: %v", err)
+	}
+
+	// connect network tunnel
+	if err := n.Tunnel.Connect(); err != nil {
+		return err
+	}
+
+	// initialize the tunnel to resolved nodes
+	n.Tunnel.Init(
+		tunnel.Nodes(nodes...),
+	)
+
+	// dial into ControlChannel to send route adverts
+	ctrlClient, err := n.Tunnel.Dial(ControlChannel, tunnel.DialMulticast())
+	if err != nil {
+		return err
+	}
+
+	n.tunClient[ControlChannel] = ctrlClient
+
+	// listen on ControlChannel
+	ctrlListener, err := n.Tunnel.Listen(ControlChannel)
+	if err != nil {
+		return err
+	}
+
+	// dial into NetworkChannel to send network messages
+	netClient, err := n.Tunnel.Dial(NetworkChannel, tunnel.DialMulticast())
+	if err != nil {
+		return err
+	}
+
+	n.tunClient[NetworkChannel] = netClient
+
+	// listen on NetworkChannel
+	netListener, err := n.Tunnel.Listen(NetworkChannel)
+	if err != nil {
+		return err
+	}
+
+	// create closed channel
+	n.closed = make(chan bool)
+
+	// start the router
+	if err := n.options.Router.Start(); err != nil {
+		return err
+	}
+
+	// start advertising routes
+	advertChan, err := n.options.Router.Advertise()
+	if err != nil {
+		return err
+	}
+
+	// start the server
+	if err := n.server.Start(); err != nil {
+		return err
+	}
+	n.Unlock()
+
+	// send connect message to NetworkChannel
+	// NOTE: in theory we could do this as soon as
+	// Dial to NetworkChannel succeeds, but instead
+	// we initialize all other node resources first
+	if err := n.sendMsg("connect", NetworkChannel); err != nil {
+		log.Debugf("Network failed to send connect message: %s", err)
+	}
+
+	// go resolving network nodes
+	go n.resolve()
+	// broadcast neighbourhood
+	go n.announce(netClient)
+	// prune stale nodes
+	go n.prune()
+	// listen to network messages
+	go n.processNetChan(netClient, netListener)
+	// advertise service routes
+	go n.advertise(ctrlClient, advertChan)
+	// accept and process routes
+	go n.processCtrlChan(ctrlClient, ctrlListener)
+
+	n.Lock()
+	n.connected = true
+	n.Unlock()
+
+	return nil
+}
+
+// Nodes returns a list of all network nodes
+func (n *network) Nodes() []Node {
+	//track the visited nodes
+	visited := make(map[string]*node)
+	// queue of the nodes to visit
+	queue := list.New()
+
+	// we need to freeze the network graph here
+	// otherwise we might get invalid results
+	n.RLock()
+	defer n.RUnlock()
+
+	// push network node to the back of queue
+	queue.PushBack(n.node)
+	// mark the node as visited
+	visited[n.node.id] = n.node
+
+	// keep iterating over the queue until its empty
+	for queue.Len() > 0 {
+		// pop the node from the front of the queue
+		qnode := queue.Front()
+		// iterate through all of its neighbours
+		// mark the visited nodes; enqueue the non-visted
+		for id, node := range qnode.Value.(*node).neighbours {
+			if _, ok := visited[id]; !ok {
+				visited[id] = node
+				queue.PushBack(node)
+			}
+		}
+		// remove the node from the queue
+		queue.Remove(qnode)
+	}
+
+	var nodes []Node
+	// collect all the nodes and return them
+	for _, node := range visited {
+		nodes = append(nodes, node)
+	}
+
+	return nodes
+}
+
+func (n *network) close() error {
+	// stop the server
+	if err := n.server.Stop(); err != nil {
+		return err
+	}
+
+	// stop the router
+	if err := n.Router.Stop(); err != nil {
+		return err
+	}
+
+	// close the tunnel
+	if err := n.Tunnel.Close(); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// Close closes network connection
+func (n *network) Close() error {
+	// lock this operation
+	n.Lock()
+
+	if !n.connected {
+		n.Unlock()
+		return nil
+	}
+
+	select {
+	case <-n.closed:
+		n.Unlock()
+		return nil
+	default:
+		// TODO: send close message to the network channel
+		close(n.closed)
+		// set connected to false
+		n.connected = false
+
+		// unlock the lock otherwise we'll deadlock sending the close
+		n.Unlock()
+
+		// send close message only if we managed to connect to NetworkChannel
+		log.Debugf("Sending close message from: %s", n.options.Id)
+		if err := n.sendMsg("close", NetworkChannel); err != nil {
+			log.Debugf("Network failed to send close message: %s", err)
+		}
+	}
+
+	return n.close()
+}
+
+// Client returns network client
+func (n *network) Client() client.Client {
+	return n.client
+}
+
+// Server returns network server
+func (n *network) Server() server.Server {
+	return n.server
+}
diff --git a/network/handler/handler.go b/network/handler/handler.go
new file mode 100644
index 00000000..1d8d74a8
--- /dev/null
+++ b/network/handler/handler.go
@@ -0,0 +1,111 @@
+// Package handler implements network RPC handler
+package handler
+
+import (
+	"context"
+	"sort"
+
+	"github.com/micro/go-micro/errors"
+	"github.com/micro/go-micro/network"
+	pbNet "github.com/micro/go-micro/network/proto"
+	pbRtr "github.com/micro/go-micro/router/proto"
+)
+
+// Network implements network handler
+type Network struct {
+	Network network.Network
+}
+
+// ListRoutes returns a list of routing table routes
+func (n *Network) ListRoutes(ctx context.Context, req *pbRtr.Request, resp *pbRtr.ListResponse) error {
+	routes, err := n.Network.Options().Router.Table().List()
+	if err != nil {
+		return errors.InternalServerError("go.micro.network", "failed to list routes: %s", err)
+	}
+
+	var respRoutes []*pbRtr.Route
+	for _, route := range routes {
+		respRoute := &pbRtr.Route{
+			Service: route.Service,
+			Address: route.Address,
+			Gateway: route.Gateway,
+			Network: route.Network,
+			Router:  route.Router,
+			Link:    route.Link,
+			Metric:  int64(route.Metric),
+		}
+		respRoutes = append(respRoutes, respRoute)
+	}
+
+	resp.Routes = respRoutes
+
+	return nil
+}
+
+// ListNodes returns a list of all accessible nodes in the network
+func (n *Network) ListNodes(ctx context.Context, req *pbNet.ListRequest, resp *pbNet.ListResponse) error {
+	nodes := n.Network.Nodes()
+
+	var respNodes []*pbNet.Node
+	for _, node := range nodes {
+		respNode := &pbNet.Node{
+			Id:      node.Id(),
+			Address: node.Address(),
+		}
+		respNodes = append(respNodes, respNode)
+	}
+
+	resp.Nodes = respNodes
+
+	return nil
+}
+
+// Neighbourhood returns a list of immediate neighbours
+func (n *Network) Neighbourhood(ctx context.Context, req *pbNet.NeighbourhoodRequest, resp *pbNet.NeighbourhoodResponse) error {
+	// extract the id of the node to query
+	id := req.Id
+	// if no id is passed, we assume local node
+	if id == "" {
+		id = n.Network.Id()
+	}
+
+	// get all the nodes in the network
+	nodes := n.Network.Nodes()
+
+	// sort the slice of nodes
+	sort.Slice(nodes, func(i, j int) bool { return nodes[i].Id() <= nodes[j].Id() })
+	// find a node with a given id
+	i := sort.Search(len(nodes), func(j int) bool { return nodes[j].Id() >= id })
+
+	var neighbours []*pbNet.Node
+	// collect all the nodes in the neighbourhood of the found node
+	if i < len(nodes) && nodes[i].Id() == id {
+		for _, neighbour := range nodes[i].Neighbourhood() {
+			// don't return yourself in response
+			if neighbour.Id() == n.Network.Id() {
+				continue
+			}
+			pbNeighbour := &pbNet.Node{
+				Id:      neighbour.Id(),
+				Address: neighbour.Address(),
+			}
+			neighbours = append(neighbours, pbNeighbour)
+		}
+	}
+
+	// requested neighbourhood node
+	node := &pbNet.Node{
+		Id:      nodes[i].Id(),
+		Address: nodes[i].Address(),
+	}
+
+	// creaate neighbourhood answer
+	neighbourhood := &pbNet.Neighbour{
+		Node:       node,
+		Neighbours: neighbours,
+	}
+
+	resp.Neighbourhood = neighbourhood
+
+	return nil
+}
diff --git a/network/network.go b/network/network.go
index 15d8155f..5a9b7f6a 100644
--- a/network/network.go
+++ b/network/network.go
@@ -1,2 +1,60 @@
 // Package network is for creating internetworks
 package network
+
+import (
+	"time"
+
+	"github.com/micro/go-micro/client"
+	"github.com/micro/go-micro/server"
+)
+
+var (
+	// DefaultName is default network name
+	DefaultName = "go.micro"
+	// DefaultAddress is default network address
+	DefaultAddress = ":0"
+	// ResolveTime defines time interval to periodically resolve network nodes
+	ResolveTime = 1 * time.Minute
+	// AnnounceTime defines time interval to periodically announce node neighbours
+	AnnounceTime = 30 * time.Second
+	// PruneTime defines time interval to periodically check nodes that need to be pruned
+	// due to their not announcing their presence within this time interval
+	PruneTime = 90 * time.Second
+)
+
+// Node is network node
+type Node interface {
+	// Id is node id
+	Id() string
+	// Address is node bind address
+	Address() string
+	// Neighbourhood is node neighbourhood
+	Neighbourhood() []Node
+	// Network is the network node is in
+	Network() Network
+}
+
+// Network is micro network
+type Network interface {
+	// Node is network node
+	Node
+	// Options returns the network options
+	Options() Options
+	// Name of the network
+	Name() string
+	// Connect starts the resolver and tunnel server
+	Connect() error
+	// Nodes returns list of network nodes
+	Nodes() []Node
+	// Close stops the tunnel and resolving
+	Close() error
+	// Client is micro client
+	Client() client.Client
+	// Server is micro server
+	Server() server.Server
+}
+
+// NewNetwork returns a new network interface
+func NewNetwork(opts ...Option) Network {
+	return newNetwork(opts...)
+}
diff --git a/network/options.go b/network/options.go
new file mode 100644
index 00000000..e670cf4c
--- /dev/null
+++ b/network/options.go
@@ -0,0 +1,103 @@
+package network
+
+import (
+	"github.com/google/uuid"
+	"github.com/micro/go-micro/network/resolver"
+	"github.com/micro/go-micro/network/resolver/registry"
+	"github.com/micro/go-micro/proxy"
+	"github.com/micro/go-micro/proxy/mucp"
+	"github.com/micro/go-micro/router"
+	"github.com/micro/go-micro/tunnel"
+)
+
+type Option func(*Options)
+
+// Options configure network
+type Options struct {
+	// Id of the node
+	Id string
+	// Name of the network
+	Name string
+	// Address to bind to
+	Address string
+	// Nodes is a list of seed nodes
+	Nodes []string
+	// Tunnel is network tunnel
+	Tunnel tunnel.Tunnel
+	// Router is network router
+	Router router.Router
+	// Proxy is network proxy
+	Proxy proxy.Proxy
+	// Resolver is network resolver
+	Resolver resolver.Resolver
+}
+
+// Id sets the id of the network node
+func Id(id string) Option {
+	return func(o *Options) {
+		o.Id = id
+	}
+}
+
+// Name sets the network name
+func Name(n string) Option {
+	return func(o *Options) {
+		o.Name = n
+	}
+}
+
+// Address sets the network address
+func Address(a string) Option {
+	return func(o *Options) {
+		o.Address = a
+	}
+}
+
+// Nodes is a list of seed nodes used along
+// with resolved node
+func Nodes(n ...string) Option {
+	return func(o *Options) {
+		o.Nodes = n
+	}
+}
+
+// Tunnel sets the network tunnel
+func Tunnel(t tunnel.Tunnel) Option {
+	return func(o *Options) {
+		o.Tunnel = t
+	}
+}
+
+// Router sets the network router
+func Router(r router.Router) Option {
+	return func(o *Options) {
+		o.Router = r
+	}
+}
+
+// Proxy sets the network proxy
+func Proxy(p proxy.Proxy) Option {
+	return func(o *Options) {
+		o.Proxy = p
+	}
+}
+
+// Resolver is the network resolver
+func Resolver(r resolver.Resolver) Option {
+	return func(o *Options) {
+		o.Resolver = r
+	}
+}
+
+// DefaultOptions returns network default options
+func DefaultOptions() Options {
+	return Options{
+		Id:       uuid.New().String(),
+		Name:     DefaultName,
+		Address:  DefaultAddress,
+		Tunnel:   tunnel.NewTunnel(),
+		Router:   router.DefaultRouter,
+		Proxy:    mucp.NewProxy(),
+		Resolver: &registry.Resolver{},
+	}
+}
diff --git a/network/proto/network.micro.go b/network/proto/network.micro.go
new file mode 100644
index 00000000..c114ee96
--- /dev/null
+++ b/network/proto/network.micro.go
@@ -0,0 +1,126 @@
+// Code generated by protoc-gen-micro. DO NOT EDIT.
+// source: network.proto
+
+package go_micro_network
+
+import (
+	fmt "fmt"
+	proto "github.com/golang/protobuf/proto"
+	proto1 "github.com/micro/go-micro/router/proto"
+	math "math"
+)
+
+import (
+	context "context"
+	client "github.com/micro/go-micro/client"
+	server "github.com/micro/go-micro/server"
+)
+
+// 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.ProtoPackageIsVersion3 // please upgrade the proto package
+
+// Reference imports to suppress errors if they are not otherwise used.
+var _ context.Context
+var _ client.Option
+var _ server.Option
+
+// Client API for Network service
+
+type NetworkService interface {
+	ListRoutes(ctx context.Context, in *proto1.Request, opts ...client.CallOption) (*proto1.ListResponse, error)
+	ListNodes(ctx context.Context, in *ListRequest, opts ...client.CallOption) (*ListResponse, error)
+	Neighbourhood(ctx context.Context, in *NeighbourhoodRequest, opts ...client.CallOption) (*NeighbourhoodResponse, error)
+}
+
+type networkService struct {
+	c    client.Client
+	name string
+}
+
+func NewNetworkService(name string, c client.Client) NetworkService {
+	if c == nil {
+		c = client.NewClient()
+	}
+	if len(name) == 0 {
+		name = "go.micro.network"
+	}
+	return &networkService{
+		c:    c,
+		name: name,
+	}
+}
+
+func (c *networkService) ListRoutes(ctx context.Context, in *proto1.Request, opts ...client.CallOption) (*proto1.ListResponse, error) {
+	req := c.c.NewRequest(c.name, "Network.ListRoutes", in)
+	out := new(proto1.ListResponse)
+	err := c.c.Call(ctx, req, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *networkService) ListNodes(ctx context.Context, in *ListRequest, opts ...client.CallOption) (*ListResponse, error) {
+	req := c.c.NewRequest(c.name, "Network.ListNodes", in)
+	out := new(ListResponse)
+	err := c.c.Call(ctx, req, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *networkService) Neighbourhood(ctx context.Context, in *NeighbourhoodRequest, opts ...client.CallOption) (*NeighbourhoodResponse, error) {
+	req := c.c.NewRequest(c.name, "Network.Neighbourhood", in)
+	out := new(NeighbourhoodResponse)
+	err := c.c.Call(ctx, req, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+// Server API for Network service
+
+type NetworkHandler interface {
+	ListRoutes(context.Context, *proto1.Request, *proto1.ListResponse) error
+	ListNodes(context.Context, *ListRequest, *ListResponse) error
+	Neighbourhood(context.Context, *NeighbourhoodRequest, *NeighbourhoodResponse) error
+}
+
+func RegisterNetworkHandler(s server.Server, hdlr NetworkHandler, opts ...server.HandlerOption) error {
+	type network interface {
+		ListRoutes(ctx context.Context, in *proto1.Request, out *proto1.ListResponse) error
+		ListNodes(ctx context.Context, in *ListRequest, out *ListResponse) error
+		Neighbourhood(ctx context.Context, in *NeighbourhoodRequest, out *NeighbourhoodResponse) error
+	}
+	type Network struct {
+		network
+	}
+	h := &networkHandler{hdlr}
+	return s.Handle(s.NewHandler(&Network{h}, opts...))
+}
+
+type networkHandler struct {
+	NetworkHandler
+}
+
+func (h *networkHandler) ListRoutes(ctx context.Context, in *proto1.Request, out *proto1.ListResponse) error {
+	return h.NetworkHandler.ListRoutes(ctx, in, out)
+}
+
+func (h *networkHandler) ListNodes(ctx context.Context, in *ListRequest, out *ListResponse) error {
+	return h.NetworkHandler.ListNodes(ctx, in, out)
+}
+
+func (h *networkHandler) Neighbourhood(ctx context.Context, in *NeighbourhoodRequest, out *NeighbourhoodResponse) error {
+	return h.NetworkHandler.Neighbourhood(ctx, in, out)
+}
diff --git a/network/proto/network.pb.go b/network/proto/network.pb.go
new file mode 100644
index 00000000..dc6c0d48
--- /dev/null
+++ b/network/proto/network.pb.go
@@ -0,0 +1,438 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// source: network.proto
+
+package go_micro_network
+
+import (
+	fmt "fmt"
+	proto "github.com/golang/protobuf/proto"
+	_ "github.com/micro/go-micro/router/proto"
+	math "math"
+)
+
+// 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.ProtoPackageIsVersion3 // please upgrade the proto package
+
+// Empty request
+type ListRequest struct {
+	XXX_NoUnkeyedLiteral struct{} `json:"-"`
+	XXX_unrecognized     []byte   `json:"-"`
+	XXX_sizecache        int32    `json:"-"`
+}
+
+func (m *ListRequest) Reset()         { *m = ListRequest{} }
+func (m *ListRequest) String() string { return proto.CompactTextString(m) }
+func (*ListRequest) ProtoMessage()    {}
+func (*ListRequest) Descriptor() ([]byte, []int) {
+	return fileDescriptor_8571034d60397816, []int{0}
+}
+
+func (m *ListRequest) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_ListRequest.Unmarshal(m, b)
+}
+func (m *ListRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_ListRequest.Marshal(b, m, deterministic)
+}
+func (m *ListRequest) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_ListRequest.Merge(m, src)
+}
+func (m *ListRequest) XXX_Size() int {
+	return xxx_messageInfo_ListRequest.Size(m)
+}
+func (m *ListRequest) XXX_DiscardUnknown() {
+	xxx_messageInfo_ListRequest.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_ListRequest proto.InternalMessageInfo
+
+// ListResponse is returned by ListNodes and ListNeighbours
+type ListResponse struct {
+	Nodes                []*Node  `protobuf:"bytes,1,rep,name=nodes,proto3" json:"nodes,omitempty"`
+	XXX_NoUnkeyedLiteral struct{} `json:"-"`
+	XXX_unrecognized     []byte   `json:"-"`
+	XXX_sizecache        int32    `json:"-"`
+}
+
+func (m *ListResponse) Reset()         { *m = ListResponse{} }
+func (m *ListResponse) String() string { return proto.CompactTextString(m) }
+func (*ListResponse) ProtoMessage()    {}
+func (*ListResponse) Descriptor() ([]byte, []int) {
+	return fileDescriptor_8571034d60397816, []int{1}
+}
+
+func (m *ListResponse) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_ListResponse.Unmarshal(m, b)
+}
+func (m *ListResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_ListResponse.Marshal(b, m, deterministic)
+}
+func (m *ListResponse) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_ListResponse.Merge(m, src)
+}
+func (m *ListResponse) XXX_Size() int {
+	return xxx_messageInfo_ListResponse.Size(m)
+}
+func (m *ListResponse) XXX_DiscardUnknown() {
+	xxx_messageInfo_ListResponse.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_ListResponse proto.InternalMessageInfo
+
+func (m *ListResponse) GetNodes() []*Node {
+	if m != nil {
+		return m.Nodes
+	}
+	return nil
+}
+
+// NeighbourhoodRequest is sent to query node neighbourhood
+type NeighbourhoodRequest struct {
+	Id                   string   `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
+	XXX_NoUnkeyedLiteral struct{} `json:"-"`
+	XXX_unrecognized     []byte   `json:"-"`
+	XXX_sizecache        int32    `json:"-"`
+}
+
+func (m *NeighbourhoodRequest) Reset()         { *m = NeighbourhoodRequest{} }
+func (m *NeighbourhoodRequest) String() string { return proto.CompactTextString(m) }
+func (*NeighbourhoodRequest) ProtoMessage()    {}
+func (*NeighbourhoodRequest) Descriptor() ([]byte, []int) {
+	return fileDescriptor_8571034d60397816, []int{2}
+}
+
+func (m *NeighbourhoodRequest) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_NeighbourhoodRequest.Unmarshal(m, b)
+}
+func (m *NeighbourhoodRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_NeighbourhoodRequest.Marshal(b, m, deterministic)
+}
+func (m *NeighbourhoodRequest) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_NeighbourhoodRequest.Merge(m, src)
+}
+func (m *NeighbourhoodRequest) XXX_Size() int {
+	return xxx_messageInfo_NeighbourhoodRequest.Size(m)
+}
+func (m *NeighbourhoodRequest) XXX_DiscardUnknown() {
+	xxx_messageInfo_NeighbourhoodRequest.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_NeighbourhoodRequest proto.InternalMessageInfo
+
+func (m *NeighbourhoodRequest) GetId() string {
+	if m != nil {
+		return m.Id
+	}
+	return ""
+}
+
+// NeighbourhoodResponse contains node neighbourhood hierarchy
+type NeighbourhoodResponse struct {
+	Neighbourhood        *Neighbour `protobuf:"bytes,1,opt,name=neighbourhood,proto3" json:"neighbourhood,omitempty"`
+	XXX_NoUnkeyedLiteral struct{}   `json:"-"`
+	XXX_unrecognized     []byte     `json:"-"`
+	XXX_sizecache        int32      `json:"-"`
+}
+
+func (m *NeighbourhoodResponse) Reset()         { *m = NeighbourhoodResponse{} }
+func (m *NeighbourhoodResponse) String() string { return proto.CompactTextString(m) }
+func (*NeighbourhoodResponse) ProtoMessage()    {}
+func (*NeighbourhoodResponse) Descriptor() ([]byte, []int) {
+	return fileDescriptor_8571034d60397816, []int{3}
+}
+
+func (m *NeighbourhoodResponse) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_NeighbourhoodResponse.Unmarshal(m, b)
+}
+func (m *NeighbourhoodResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_NeighbourhoodResponse.Marshal(b, m, deterministic)
+}
+func (m *NeighbourhoodResponse) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_NeighbourhoodResponse.Merge(m, src)
+}
+func (m *NeighbourhoodResponse) XXX_Size() int {
+	return xxx_messageInfo_NeighbourhoodResponse.Size(m)
+}
+func (m *NeighbourhoodResponse) XXX_DiscardUnknown() {
+	xxx_messageInfo_NeighbourhoodResponse.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_NeighbourhoodResponse proto.InternalMessageInfo
+
+func (m *NeighbourhoodResponse) GetNeighbourhood() *Neighbour {
+	if m != nil {
+		return m.Neighbourhood
+	}
+	return nil
+}
+
+// Node is network node
+type Node struct {
+	// node ide
+	Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
+	// node address
+	Address              string   `protobuf:"bytes,2,opt,name=address,proto3" json:"address,omitempty"`
+	XXX_NoUnkeyedLiteral struct{} `json:"-"`
+	XXX_unrecognized     []byte   `json:"-"`
+	XXX_sizecache        int32    `json:"-"`
+}
+
+func (m *Node) Reset()         { *m = Node{} }
+func (m *Node) String() string { return proto.CompactTextString(m) }
+func (*Node) ProtoMessage()    {}
+func (*Node) Descriptor() ([]byte, []int) {
+	return fileDescriptor_8571034d60397816, []int{4}
+}
+
+func (m *Node) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_Node.Unmarshal(m, b)
+}
+func (m *Node) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_Node.Marshal(b, m, deterministic)
+}
+func (m *Node) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_Node.Merge(m, src)
+}
+func (m *Node) XXX_Size() int {
+	return xxx_messageInfo_Node.Size(m)
+}
+func (m *Node) XXX_DiscardUnknown() {
+	xxx_messageInfo_Node.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_Node proto.InternalMessageInfo
+
+func (m *Node) GetId() string {
+	if m != nil {
+		return m.Id
+	}
+	return ""
+}
+
+func (m *Node) GetAddress() string {
+	if m != nil {
+		return m.Address
+	}
+	return ""
+}
+
+// Connect is sent when the node connects to the network
+type Connect struct {
+	// network mode
+	Node                 *Node    `protobuf:"bytes,1,opt,name=node,proto3" json:"node,omitempty"`
+	XXX_NoUnkeyedLiteral struct{} `json:"-"`
+	XXX_unrecognized     []byte   `json:"-"`
+	XXX_sizecache        int32    `json:"-"`
+}
+
+func (m *Connect) Reset()         { *m = Connect{} }
+func (m *Connect) String() string { return proto.CompactTextString(m) }
+func (*Connect) ProtoMessage()    {}
+func (*Connect) Descriptor() ([]byte, []int) {
+	return fileDescriptor_8571034d60397816, []int{5}
+}
+
+func (m *Connect) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_Connect.Unmarshal(m, b)
+}
+func (m *Connect) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_Connect.Marshal(b, m, deterministic)
+}
+func (m *Connect) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_Connect.Merge(m, src)
+}
+func (m *Connect) XXX_Size() int {
+	return xxx_messageInfo_Connect.Size(m)
+}
+func (m *Connect) XXX_DiscardUnknown() {
+	xxx_messageInfo_Connect.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_Connect proto.InternalMessageInfo
+
+func (m *Connect) GetNode() *Node {
+	if m != nil {
+		return m.Node
+	}
+	return nil
+}
+
+// Close is sent when the node disconnects from the network
+type Close struct {
+	// network node
+	Node                 *Node    `protobuf:"bytes,1,opt,name=node,proto3" json:"node,omitempty"`
+	XXX_NoUnkeyedLiteral struct{} `json:"-"`
+	XXX_unrecognized     []byte   `json:"-"`
+	XXX_sizecache        int32    `json:"-"`
+}
+
+func (m *Close) Reset()         { *m = Close{} }
+func (m *Close) String() string { return proto.CompactTextString(m) }
+func (*Close) ProtoMessage()    {}
+func (*Close) Descriptor() ([]byte, []int) {
+	return fileDescriptor_8571034d60397816, []int{6}
+}
+
+func (m *Close) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_Close.Unmarshal(m, b)
+}
+func (m *Close) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_Close.Marshal(b, m, deterministic)
+}
+func (m *Close) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_Close.Merge(m, src)
+}
+func (m *Close) XXX_Size() int {
+	return xxx_messageInfo_Close.Size(m)
+}
+func (m *Close) XXX_DiscardUnknown() {
+	xxx_messageInfo_Close.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_Close proto.InternalMessageInfo
+
+func (m *Close) GetNode() *Node {
+	if m != nil {
+		return m.Node
+	}
+	return nil
+}
+
+// Solicit is sent when requesting route advertisement from the network nodes
+type Solicit struct {
+	// network node
+	Node                 *Node    `protobuf:"bytes,1,opt,name=node,proto3" json:"node,omitempty"`
+	XXX_NoUnkeyedLiteral struct{} `json:"-"`
+	XXX_unrecognized     []byte   `json:"-"`
+	XXX_sizecache        int32    `json:"-"`
+}
+
+func (m *Solicit) Reset()         { *m = Solicit{} }
+func (m *Solicit) String() string { return proto.CompactTextString(m) }
+func (*Solicit) ProtoMessage()    {}
+func (*Solicit) Descriptor() ([]byte, []int) {
+	return fileDescriptor_8571034d60397816, []int{7}
+}
+
+func (m *Solicit) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_Solicit.Unmarshal(m, b)
+}
+func (m *Solicit) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_Solicit.Marshal(b, m, deterministic)
+}
+func (m *Solicit) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_Solicit.Merge(m, src)
+}
+func (m *Solicit) XXX_Size() int {
+	return xxx_messageInfo_Solicit.Size(m)
+}
+func (m *Solicit) XXX_DiscardUnknown() {
+	xxx_messageInfo_Solicit.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_Solicit proto.InternalMessageInfo
+
+func (m *Solicit) GetNode() *Node {
+	if m != nil {
+		return m.Node
+	}
+	return nil
+}
+
+// Neighbour is used to nnounce node neighbourhood
+type Neighbour struct {
+	// network node
+	Node *Node `protobuf:"bytes,1,opt,name=node,proto3" json:"node,omitempty"`
+	// neighbours
+	Neighbours           []*Node  `protobuf:"bytes,3,rep,name=neighbours,proto3" json:"neighbours,omitempty"`
+	XXX_NoUnkeyedLiteral struct{} `json:"-"`
+	XXX_unrecognized     []byte   `json:"-"`
+	XXX_sizecache        int32    `json:"-"`
+}
+
+func (m *Neighbour) Reset()         { *m = Neighbour{} }
+func (m *Neighbour) String() string { return proto.CompactTextString(m) }
+func (*Neighbour) ProtoMessage()    {}
+func (*Neighbour) Descriptor() ([]byte, []int) {
+	return fileDescriptor_8571034d60397816, []int{8}
+}
+
+func (m *Neighbour) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_Neighbour.Unmarshal(m, b)
+}
+func (m *Neighbour) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_Neighbour.Marshal(b, m, deterministic)
+}
+func (m *Neighbour) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_Neighbour.Merge(m, src)
+}
+func (m *Neighbour) XXX_Size() int {
+	return xxx_messageInfo_Neighbour.Size(m)
+}
+func (m *Neighbour) XXX_DiscardUnknown() {
+	xxx_messageInfo_Neighbour.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_Neighbour proto.InternalMessageInfo
+
+func (m *Neighbour) GetNode() *Node {
+	if m != nil {
+		return m.Node
+	}
+	return nil
+}
+
+func (m *Neighbour) GetNeighbours() []*Node {
+	if m != nil {
+		return m.Neighbours
+	}
+	return nil
+}
+
+func init() {
+	proto.RegisterType((*ListRequest)(nil), "go.micro.network.ListRequest")
+	proto.RegisterType((*ListResponse)(nil), "go.micro.network.ListResponse")
+	proto.RegisterType((*NeighbourhoodRequest)(nil), "go.micro.network.NeighbourhoodRequest")
+	proto.RegisterType((*NeighbourhoodResponse)(nil), "go.micro.network.NeighbourhoodResponse")
+	proto.RegisterType((*Node)(nil), "go.micro.network.Node")
+	proto.RegisterType((*Connect)(nil), "go.micro.network.Connect")
+	proto.RegisterType((*Close)(nil), "go.micro.network.Close")
+	proto.RegisterType((*Solicit)(nil), "go.micro.network.Solicit")
+	proto.RegisterType((*Neighbour)(nil), "go.micro.network.Neighbour")
+}
+
+func init() { proto.RegisterFile("network.proto", fileDescriptor_8571034d60397816) }
+
+var fileDescriptor_8571034d60397816 = []byte{
+	// 360 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x53, 0x41, 0x4f, 0xf2, 0x40,
+	0x10, 0xfd, 0x28, 0xf0, 0x35, 0x0c, 0x1f, 0x5f, 0xcc, 0x46, 0x4d, 0x53, 0x83, 0x21, 0x7b, 0x40,
+	0x62, 0xb4, 0x18, 0x08, 0x9e, 0xbc, 0x18, 0x0e, 0x5e, 0x08, 0x87, 0x7a, 0xf3, 0x66, 0xbb, 0x9b,
+	0xb2, 0x11, 0x3a, 0xb8, 0xbb, 0x8d, 0x7f, 0xc0, 0x1f, 0x6e, 0xba, 0x5d, 0xb0, 0x80, 0x60, 0xb8,
+	0x75, 0xe6, 0xbd, 0x37, 0x6f, 0xa7, 0xfb, 0x16, 0x5a, 0x29, 0xd7, 0x1f, 0x28, 0xdf, 0x82, 0xa5,
+	0x44, 0x8d, 0xe4, 0x24, 0xc1, 0x60, 0x21, 0x62, 0x89, 0x81, 0xed, 0xfb, 0xc3, 0x44, 0xe8, 0x59,
+	0x16, 0x05, 0x31, 0x2e, 0xfa, 0x06, 0xe9, 0x27, 0x78, 0x5b, 0x7c, 0x48, 0xcc, 0x34, 0x97, 0x7d,
+	0xa3, 0xb4, 0x45, 0x31, 0x86, 0xb6, 0xa0, 0x39, 0x11, 0x4a, 0x87, 0xfc, 0x3d, 0xe3, 0x4a, 0xd3,
+	0x07, 0xf8, 0x57, 0x94, 0x6a, 0x89, 0xa9, 0xe2, 0xe4, 0x06, 0xea, 0x29, 0x32, 0xae, 0xbc, 0x4a,
+	0xa7, 0xda, 0x6b, 0x0e, 0xce, 0x83, 0x6d, 0xd7, 0x60, 0x8a, 0x8c, 0x87, 0x05, 0x89, 0x76, 0xe1,
+	0x74, 0xca, 0x45, 0x32, 0x8b, 0x30, 0x93, 0x33, 0x44, 0x66, 0xa7, 0x92, 0xff, 0xe0, 0x08, 0xe6,
+	0x55, 0x3a, 0x95, 0x5e, 0x23, 0x74, 0x04, 0xa3, 0x2f, 0x70, 0xb6, 0xc5, 0xb3, 0x76, 0x8f, 0xf9,
+	0x96, 0x25, 0xc0, 0x68, 0x9a, 0x83, 0x8b, 0x1f, 0x6c, 0x57, 0xb4, 0x70, 0x53, 0x41, 0xef, 0xa0,
+	0x96, 0x1f, 0x69, 0xdb, 0x93, 0x78, 0xe0, 0xbe, 0x32, 0x26, 0xb9, 0x52, 0x9e, 0x63, 0x9a, 0xab,
+	0x92, 0x8e, 0xc0, 0x1d, 0x63, 0x9a, 0xf2, 0x58, 0x93, 0x6b, 0xa8, 0xe5, 0x9b, 0x58, 0xdb, 0x7d,
+	0xdb, 0x1a, 0x0e, 0x1d, 0x42, 0x7d, 0x3c, 0x47, 0xc5, 0x8f, 0x12, 0x8d, 0xc0, 0x7d, 0xc6, 0xb9,
+	0x88, 0xc5, 0x71, 0x5e, 0x08, 0x8d, 0xf5, 0xc2, 0xc7, 0x08, 0xc9, 0x3d, 0xc0, 0xfa, 0xf7, 0x28,
+	0xaf, 0x7a, 0xf0, 0x12, 0x4b, 0xcc, 0xc1, 0xa7, 0x03, 0xee, 0xb4, 0x00, 0xc9, 0x13, 0x80, 0xc9,
+	0x44, 0x1e, 0x1b, 0x45, 0xbc, 0x6f, 0xb5, 0x0d, 0x92, 0xbd, 0x65, 0xbf, 0xbd, 0x83, 0x94, 0xa3,
+	0x44, 0xff, 0x90, 0x09, 0x34, 0xf2, 0x4e, 0x6e, 0xa6, 0x48, 0x7b, 0xf7, 0x14, 0xa5, 0x20, 0xfa,
+	0x97, 0xfb, 0xe0, 0xf5, 0xb4, 0x08, 0x5a, 0x1b, 0x21, 0x22, 0xdd, 0x03, 0x29, 0x29, 0xa5, 0xd1,
+	0xbf, 0xfa, 0x95, 0xb7, 0xf2, 0x88, 0xfe, 0x9a, 0x47, 0x32, 0xfc, 0x0a, 0x00, 0x00, 0xff, 0xff,
+	0x59, 0xcf, 0xab, 0xb5, 0x7c, 0x03, 0x00, 0x00,
+}
diff --git a/network/proto/network.proto b/network/proto/network.proto
new file mode 100644
index 00000000..6025d90b
--- /dev/null
+++ b/network/proto/network.proto
@@ -0,0 +1,64 @@
+syntax = "proto3";
+
+package go.micro.network;
+
+import "github.com/micro/go-micro/router/proto/router.proto";
+
+// Network service is usesd to gain visibility into networks
+service Network {
+        rpc ListRoutes(go.micro.router.Request) returns (go.micro.router.ListResponse) {};
+        rpc ListNodes(ListRequest) returns (ListResponse) {};
+        rpc Neighbourhood(NeighbourhoodRequest) returns (NeighbourhoodResponse) {};
+}
+
+// Empty request
+message ListRequest {}
+
+// ListResponse is returned by ListNodes and ListNeighbours
+message ListResponse {
+	repeated Node nodes = 1;
+}
+
+// NeighbourhoodRequest is sent to query node neighbourhood
+message NeighbourhoodRequest {
+        string id = 1;
+}
+
+// NeighbourhoodResponse contains node neighbourhood hierarchy
+message NeighbourhoodResponse {
+        Neighbour neighbourhood = 1;
+}
+
+// Node is network node
+message Node {
+        // node ide
+        string id = 1;
+        // node address
+        string address = 2;
+}
+
+// Connect is sent when the node connects to the network
+message Connect {
+        // network mode
+        Node node = 1;
+}
+
+// Close is sent when the node disconnects from the network
+message Close {
+        // network node
+        Node node = 1;
+}
+
+// Solicit is sent when requesting route advertisement from the network nodes
+message Solicit {
+        // network node
+        Node node = 1;
+}
+
+// Neighbour is used to nnounce node neighbourhood
+message Neighbour {
+        // network node
+        Node node = 1;
+        // neighbours
+        repeated Node neighbours = 3;
+}
diff --git a/network/resolver/dns/dns.go b/network/resolver/dns/dns.go
index ba109656..3cfa2746 100644
--- a/network/resolver/dns/dns.go
+++ b/network/resolver/dns/dns.go
@@ -8,6 +8,7 @@ import (
 	"github.com/micro/go-micro/network/resolver"
 )
 
+// Resolver is a DNS network resolve
 type Resolver struct{}
 
 // Resolve assumes ID is a domain name e.g micro.mu
diff --git a/network/resolver/http/http.go b/network/resolver/http/http.go
index b6025a59..055662a5 100644
--- a/network/resolver/http/http.go
+++ b/network/resolver/http/http.go
@@ -10,6 +10,7 @@ import (
 	"github.com/micro/go-micro/network/resolver"
 )
 
+// Resolver is a HTTP network resolver
 type Resolver struct {
 	// If not set, defaults to http
 	Proto string
diff --git a/network/resolver/registry/registry.go b/network/resolver/registry/registry.go
index 9fe80bc5..20fffc18 100644
--- a/network/resolver/registry/registry.go
+++ b/network/resolver/registry/registry.go
@@ -6,6 +6,7 @@ import (
 	"github.com/micro/go-micro/registry"
 )
 
+// Resolver is a registry network resolver
 type Resolver struct {
 	// Registry is the registry to use otherwise we use the defaul
 	Registry registry.Registry
diff --git a/network/resolver/resolver.go b/network/resolver/resolver.go
index 2eb0b2a2..369269df 100644
--- a/network/resolver/resolver.go
+++ b/network/resolver/resolver.go
@@ -5,7 +5,7 @@ package resolver
 // via the name to connect to. This is done based on Network.Name().
 // Before we can be part of any network, we have to connect to it.
 type Resolver interface {
-	// Resolve returns a list of addresses for an name
+	// Resolve returns a list of addresses for a name
 	Resolve(name string) ([]*Record, error)
 }
 
diff --git a/plugin/default.go b/plugin/default.go
new file mode 100644
index 00000000..bcafbc3b
--- /dev/null
+++ b/plugin/default.go
@@ -0,0 +1,125 @@
+// Package plugin provides the ability to load plugins
+package plugin
+
+import (
+	"errors"
+	"fmt"
+	"os"
+	"os/exec"
+	"path/filepath"
+	pg "plugin"
+	"strings"
+	"text/template"
+
+	"github.com/micro/go-micro/broker"
+	"github.com/micro/go-micro/client"
+	"github.com/micro/go-micro/client/selector"
+	"github.com/micro/go-micro/config/cmd"
+	"github.com/micro/go-micro/registry"
+	"github.com/micro/go-micro/server"
+	"github.com/micro/go-micro/transport"
+)
+
+type plugin struct{}
+
+// Init sets up the plugin
+func (p *plugin) Init(c *Config) error {
+	switch c.Type {
+	case "broker":
+		pg, ok := c.NewFunc.(func(...broker.Option) broker.Broker)
+		if !ok {
+			return fmt.Errorf("Invalid plugin %s", c.Name)
+		}
+		cmd.DefaultBrokers[c.Name] = pg
+	case "client":
+		pg, ok := c.NewFunc.(func(...client.Option) client.Client)
+		if !ok {
+			return fmt.Errorf("Invalid plugin %s", c.Name)
+		}
+		cmd.DefaultClients[c.Name] = pg
+	case "registry":
+		pg, ok := c.NewFunc.(func(...registry.Option) registry.Registry)
+		if !ok {
+			return fmt.Errorf("Invalid plugin %s", c.Name)
+		}
+		cmd.DefaultRegistries[c.Name] = pg
+
+	case "selector":
+		pg, ok := c.NewFunc.(func(...selector.Option) selector.Selector)
+		if !ok {
+			return fmt.Errorf("Invalid plugin %s", c.Name)
+		}
+		cmd.DefaultSelectors[c.Name] = pg
+	case "server":
+		pg, ok := c.NewFunc.(func(...server.Option) server.Server)
+		if !ok {
+			return fmt.Errorf("Invalid plugin %s", c.Name)
+		}
+		cmd.DefaultServers[c.Name] = pg
+	case "transport":
+		pg, ok := c.NewFunc.(func(...transport.Option) transport.Transport)
+		if !ok {
+			return fmt.Errorf("Invalid plugin %s", c.Name)
+		}
+		cmd.DefaultTransports[c.Name] = pg
+	default:
+		return fmt.Errorf("Unknown plugin type: %s for %s", c.Type, c.Name)
+	}
+
+	return nil
+}
+
+// Load loads a plugin created with `go build -buildmode=plugin`
+func (p *plugin) Load(path string) (*Config, error) {
+	plugin, err := pg.Open(path)
+	if err != nil {
+		return nil, err
+	}
+	s, err := plugin.Lookup("Plugin")
+	if err != nil {
+		return nil, err
+	}
+	pl, ok := s.(*Config)
+	if !ok {
+		return nil, errors.New("could not cast Plugin object")
+	}
+	return pl, nil
+}
+
+// Generate creates a go file at the specified path.
+// You must use `go build -buildmode=plugin`to build it.
+func (p *plugin) Generate(path string, c *Config) error {
+	f, err := os.Create(path)
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+	t, err := template.New(c.Name).Parse(tmpl)
+	if err != nil {
+		return err
+	}
+	return t.Execute(f, c)
+}
+
+// Build generates a dso plugin using the go command `go build -buildmode=plugin`
+func (p *plugin) Build(path string, c *Config) error {
+	path = strings.TrimSuffix(path, ".so")
+
+	// create go file in tmp path
+	temp := os.TempDir()
+	base := filepath.Base(path)
+	goFile := filepath.Join(temp, base+".go")
+
+	// generate .go file
+	if err := p.Generate(goFile, c); err != nil {
+		return err
+	}
+	// remove .go file
+	defer os.Remove(goFile)
+
+	if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil && !os.IsExist(err) {
+		return fmt.Errorf("Failed to create dir %s: %v", filepath.Dir(path), err)
+	}
+	cmd := exec.Command("go", "build", "-buildmode=plugin", "-o", path+".so", goFile)
+	return cmd.Run()
+}
diff --git a/plugin/plugin.go b/plugin/plugin.go
new file mode 100644
index 00000000..c1aecc6e
--- /dev/null
+++ b/plugin/plugin.go
@@ -0,0 +1,46 @@
+// Package plugin provides the ability to load plugins
+package plugin
+
+// Plugin is a plugin loaded from a file
+type Plugin interface {
+	// Initialise a plugin with the config
+	Init(c *Config) error
+	// Load loads a .so plugin at the given path
+	Load(path string) (*Config, error)
+	// Build a .so plugin with config at the path specified
+	Build(path string, c *Config) error
+}
+
+// Config is the plugin config
+type Config struct {
+	// Name of the plugin e.g rabbitmq
+	Name string
+	// Type of the plugin e.g broker
+	Type string
+	// Path specifies the import path
+	Path string
+	// NewFunc creates an instance of the plugin
+	NewFunc interface{}
+}
+
+var (
+	// Default plugin loader
+	DefaultPlugin = NewPlugin()
+)
+
+// NewPlugin creates a new plugin interface
+func NewPlugin() Plugin {
+	return &plugin{}
+}
+
+func Build(path string, c *Config) error {
+	return DefaultPlugin.Build(path, c)
+}
+
+func Load(path string) (*Config, error) {
+	return DefaultPlugin.Load(path)
+}
+
+func Init(c *Config) error {
+	return DefaultPlugin.Init(c)
+}
diff --git a/plugin/template.go b/plugin/template.go
new file mode 100644
index 00000000..dd559bfa
--- /dev/null
+++ b/plugin/template.go
@@ -0,0 +1,20 @@
+package plugin
+
+var (
+	tmpl = `
+package main
+
+import (
+	"github.com/micro/go-micro/plugin"
+
+	"{{.Path}}"
+)
+
+var Plugin = plugin.Config{
+	Name: "{{.Name}}",
+	Type: "{{.Type}}",
+	Path: "{{.Path}}",
+	NewFunc: {{.Name}}.{{.NewFunc}},
+}
+`
+)
diff --git a/proxy/mucp/mucp.go b/proxy/mucp/mucp.go
index ec95f407..9fccc6cc 100644
--- a/proxy/mucp/mucp.go
+++ b/proxy/mucp/mucp.go
@@ -161,9 +161,6 @@ func (p *Proxy) manageRouteCache(route router.Route, action string) error {
 		}
 		p.Routes[route.Service][route.Hash()] = route
 	case "delete":
-		if _, ok := p.Routes[route.Service]; !ok {
-			return fmt.Errorf("route not found")
-		}
 		delete(p.Routes[route.Service], route.Hash())
 	default:
 		return fmt.Errorf("unknown action: %s", action)
@@ -219,6 +216,10 @@ func (p *Proxy) ServeRequest(ctx context.Context, req server.Request, rsp server
 	// endpoint to call
 	endpoint := req.Endpoint()
 
+	if len(service) == 0 {
+		return errors.BadRequest("go.micro.proxy", "service name is blank")
+	}
+
 	// are we network routing or local routing
 	if len(p.Links) == 0 {
 		local = true
diff --git a/registry/handler/handler.go b/registry/handler/handler.go
new file mode 100644
index 00000000..f86148cd
--- /dev/null
+++ b/registry/handler/handler.go
@@ -0,0 +1,76 @@
+package handler
+
+import (
+	"context"
+
+	"github.com/micro/go-micro/errors"
+	"github.com/micro/go-micro/registry"
+	pb "github.com/micro/go-micro/registry/proto"
+	"github.com/micro/go-micro/registry/service"
+)
+
+type Registry struct {
+	// internal registry
+	Registry registry.Registry
+}
+
+func (r *Registry) GetService(ctx context.Context, req *pb.GetRequest, rsp *pb.GetResponse) error {
+	services, err := r.Registry.GetService(req.Service)
+	if err != nil {
+		return errors.InternalServerError("go.micro.registry", err.Error())
+	}
+	for _, srv := range services {
+		rsp.Services = append(rsp.Services, service.ToProto(srv))
+	}
+	return nil
+}
+
+func (r *Registry) Register(ctx context.Context, req *pb.Service, rsp *pb.EmptyResponse) error {
+	err := r.Registry.Register(service.ToService(req))
+	if err != nil {
+		return errors.InternalServerError("go.micro.registry", err.Error())
+	}
+	return nil
+}
+
+func (r *Registry) Deregister(ctx context.Context, req *pb.Service, rsp *pb.EmptyResponse) error {
+	err := r.Registry.Deregister(service.ToService(req))
+	if err != nil {
+		return errors.InternalServerError("go.micro.registry", err.Error())
+	}
+	return nil
+}
+
+func (r *Registry) ListServices(ctx context.Context, req *pb.ListRequest, rsp *pb.ListResponse) error {
+	services, err := r.Registry.ListServices()
+	if err != nil {
+		return errors.InternalServerError("go.micro.registry", err.Error())
+	}
+	for _, srv := range services {
+		rsp.Services = append(rsp.Services, service.ToProto(srv))
+	}
+	return nil
+}
+
+func (r *Registry) Watch(ctx context.Context, req *pb.WatchRequest, rsp pb.Registry_WatchStream) error {
+	watcher, err := r.Registry.Watch(registry.WatchService(req.Service))
+	if err != nil {
+		return errors.InternalServerError("go.micro.registry", err.Error())
+	}
+
+	for {
+		next, err := watcher.Next()
+		if err != nil {
+			return errors.InternalServerError("go.micro.registry", err.Error())
+		}
+		err = rsp.Send(&pb.Result{
+			Action:  next.Action,
+			Service: service.ToProto(next.Service),
+		})
+		if err != nil {
+			return errors.InternalServerError("go.micro.registry", err.Error())
+		}
+	}
+
+	return nil
+}
diff --git a/registry/mdns/mdns.go b/registry/mdns/mdns.go
index 6739c694..f84e8961 100644
--- a/registry/mdns/mdns.go
+++ b/registry/mdns/mdns.go
@@ -2,6 +2,8 @@
 package mdns
 
 import (
+	"context"
+
 	"github.com/micro/go-micro/registry"
 )
 
@@ -9,3 +11,13 @@ import (
 func NewRegistry(opts ...registry.Option) registry.Registry {
 	return registry.NewRegistry(opts...)
 }
+
+// Domain sets the mdnsDomain
+func Domain(d string) registry.Option {
+	return func(o *registry.Options) {
+		if o.Context == nil {
+			o.Context = context.Background()
+		}
+		o.Context = context.WithValue(o.Context, "mdns.domain", d)
+	}
+}
diff --git a/registry/mdns_registry.go b/registry/mdns_registry.go
index 8e68948a..039710a6 100644
--- a/registry/mdns_registry.go
+++ b/registry/mdns_registry.go
@@ -14,6 +14,11 @@ import (
 	hash "github.com/mitchellh/hashstructure"
 )
 
+var (
+	// use a .micro domain rather than .local
+	mdnsDomain = "micro"
+)
+
 type mdnsTxt struct {
 	Service   string
 	Version   string
@@ -29,6 +34,8 @@ type mdnsEntry struct {
 
 type mdnsRegistry struct {
 	opts Options
+	// the mdns domain
+	domain string
 
 	sync.Mutex
 	services map[string][]*mdnsEntry
@@ -36,11 +43,25 @@ type mdnsRegistry struct {
 
 func newRegistry(opts ...Option) Registry {
 	options := Options{
+		Context: context.Background(),
 		Timeout: time.Millisecond * 100,
 	}
 
+	for _, o := range opts {
+		o(&options)
+	}
+
+	// set the domain
+	domain := mdnsDomain
+
+	d, ok := options.Context.Value("mdns.domain").(string)
+	if ok {
+		domain = d
+	}
+
 	return &mdnsRegistry{
 		opts:     options,
+		domain:   domain,
 		services: make(map[string][]*mdnsEntry),
 	}
 }
@@ -66,7 +87,7 @@ func (m *mdnsRegistry) Register(service *Service, opts ...RegisterOption) error
 		s, err := mdns.NewMDNSService(
 			service.Name,
 			"_services",
-			"",
+			m.domain+".",
 			"",
 			9999,
 			[]net.IP{net.ParseIP("0.0.0.0")},
@@ -141,7 +162,7 @@ func (m *mdnsRegistry) Register(service *Service, opts ...RegisterOption) error
 		s, err := mdns.NewMDNSService(
 			node.Id,
 			service.Name,
-			"",
+			m.domain+".",
 			"",
 			port,
 			[]net.IP{net.ParseIP(host)},
@@ -214,6 +235,8 @@ func (m *mdnsRegistry) GetService(service string) ([]*Service, error) {
 	p.Context, _ = context.WithTimeout(context.Background(), m.opts.Timeout)
 	// set entries channel
 	p.Entries = entries
+	// set the domain
+	p.Domain = m.domain
 
 	go func() {
 		for {
@@ -223,7 +246,9 @@ func (m *mdnsRegistry) GetService(service string) ([]*Service, error) {
 				if p.Service == "_services" {
 					continue
 				}
-
+				if p.Domain != m.domain {
+					continue
+				}
 				if e.TTL == 0 {
 					continue
 				}
@@ -288,6 +313,8 @@ func (m *mdnsRegistry) ListServices() ([]*Service, error) {
 	p.Context, _ = context.WithTimeout(context.Background(), m.opts.Timeout)
 	// set entries channel
 	p.Entries = entries
+	// set domain
+	p.Domain = m.domain
 
 	var services []*Service
 
@@ -298,7 +325,9 @@ func (m *mdnsRegistry) ListServices() ([]*Service, error) {
 				if e.TTL == 0 {
 					continue
 				}
-
+				if !strings.HasSuffix(e.Name, p.Domain+".") {
+					continue
+				}
 				name := strings.TrimSuffix(e.Name, "."+p.Service+"."+p.Domain+".")
 				if !serviceMap[name] {
 					serviceMap[name] = true
@@ -329,9 +358,10 @@ func (m *mdnsRegistry) Watch(opts ...WatchOption) (Watcher, error) {
 	}
 
 	md := &mdnsWatcher{
-		wo:   wo,
-		ch:   make(chan *mdns.ServiceEntry, 32),
-		exit: make(chan struct{}),
+		wo:     wo,
+		ch:     make(chan *mdns.ServiceEntry, 32),
+		exit:   make(chan struct{}),
+		domain: m.domain,
 	}
 
 	go func() {
diff --git a/registry/mdns_watcher.go b/registry/mdns_watcher.go
index e1c326ff..ce13866f 100644
--- a/registry/mdns_watcher.go
+++ b/registry/mdns_watcher.go
@@ -11,6 +11,8 @@ type mdnsWatcher struct {
 	wo   WatchOptions
 	ch   chan *mdns.ServiceEntry
 	exit chan struct{}
+	// the mdns domain
+	domain string
 }
 
 func (m *mdnsWatcher) Next() (*Result, error) {
@@ -46,13 +48,14 @@ func (m *mdnsWatcher) Next() (*Result, error) {
 				Endpoints: txt.Endpoints,
 			}
 
-			// TODO: don't hardcode .local.
-			if !strings.HasSuffix(e.Name, "."+service.Name+".local.") {
+			// skip anything without the domain we care about
+			suffix := fmt.Sprintf(".%s.%s.", service.Name, m.domain)
+			if !strings.HasSuffix(e.Name, suffix) {
 				continue
 			}
 
 			service.Nodes = append(service.Nodes, &Node{
-				Id:       strings.TrimSuffix(e.Name, "."+service.Name+".local."),
+				Id:       strings.TrimSuffix(e.Name, suffix),
 				Address:  fmt.Sprintf("%s:%d", e.AddrV4.String(), e.Port),
 				Metadata: txt.Metadata,
 			})
diff --git a/registry/proto/registry.micro.go b/registry/proto/registry.micro.go
new file mode 100644
index 00000000..3b78fdbf
--- /dev/null
+++ b/registry/proto/registry.micro.go
@@ -0,0 +1,224 @@
+// Code generated by protoc-gen-micro. DO NOT EDIT.
+// source: micro/go-micro/registry/proto/registry.proto
+
+package go_micro_registry
+
+import (
+	fmt "fmt"
+	proto "github.com/golang/protobuf/proto"
+	math "math"
+)
+
+import (
+	context "context"
+	client "github.com/micro/go-micro/client"
+	server "github.com/micro/go-micro/server"
+)
+
+// 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.ProtoPackageIsVersion3 // please upgrade the proto package
+
+// Reference imports to suppress errors if they are not otherwise used.
+var _ context.Context
+var _ client.Option
+var _ server.Option
+
+// Client API for Registry service
+
+type RegistryService interface {
+	GetService(ctx context.Context, in *GetRequest, opts ...client.CallOption) (*GetResponse, error)
+	Register(ctx context.Context, in *Service, opts ...client.CallOption) (*EmptyResponse, error)
+	Deregister(ctx context.Context, in *Service, opts ...client.CallOption) (*EmptyResponse, error)
+	ListServices(ctx context.Context, in *ListRequest, opts ...client.CallOption) (*ListResponse, error)
+	Watch(ctx context.Context, in *WatchRequest, opts ...client.CallOption) (Registry_WatchService, error)
+}
+
+type registryService struct {
+	c    client.Client
+	name string
+}
+
+func NewRegistryService(name string, c client.Client) RegistryService {
+	if c == nil {
+		c = client.NewClient()
+	}
+	if len(name) == 0 {
+		name = "go.micro.registry"
+	}
+	return &registryService{
+		c:    c,
+		name: name,
+	}
+}
+
+func (c *registryService) GetService(ctx context.Context, in *GetRequest, opts ...client.CallOption) (*GetResponse, error) {
+	req := c.c.NewRequest(c.name, "Registry.GetService", in)
+	out := new(GetResponse)
+	err := c.c.Call(ctx, req, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *registryService) Register(ctx context.Context, in *Service, opts ...client.CallOption) (*EmptyResponse, error) {
+	req := c.c.NewRequest(c.name, "Registry.Register", in)
+	out := new(EmptyResponse)
+	err := c.c.Call(ctx, req, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *registryService) Deregister(ctx context.Context, in *Service, opts ...client.CallOption) (*EmptyResponse, error) {
+	req := c.c.NewRequest(c.name, "Registry.Deregister", in)
+	out := new(EmptyResponse)
+	err := c.c.Call(ctx, req, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *registryService) ListServices(ctx context.Context, in *ListRequest, opts ...client.CallOption) (*ListResponse, error) {
+	req := c.c.NewRequest(c.name, "Registry.ListServices", in)
+	out := new(ListResponse)
+	err := c.c.Call(ctx, req, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *registryService) Watch(ctx context.Context, in *WatchRequest, opts ...client.CallOption) (Registry_WatchService, error) {
+	req := c.c.NewRequest(c.name, "Registry.Watch", &WatchRequest{})
+	stream, err := c.c.Stream(ctx, req, opts...)
+	if err != nil {
+		return nil, err
+	}
+	if err := stream.Send(in); err != nil {
+		return nil, err
+	}
+	return &registryServiceWatch{stream}, nil
+}
+
+type Registry_WatchService interface {
+	SendMsg(interface{}) error
+	RecvMsg(interface{}) error
+	Close() error
+	Recv() (*Result, error)
+}
+
+type registryServiceWatch struct {
+	stream client.Stream
+}
+
+func (x *registryServiceWatch) Close() error {
+	return x.stream.Close()
+}
+
+func (x *registryServiceWatch) SendMsg(m interface{}) error {
+	return x.stream.Send(m)
+}
+
+func (x *registryServiceWatch) RecvMsg(m interface{}) error {
+	return x.stream.Recv(m)
+}
+
+func (x *registryServiceWatch) Recv() (*Result, error) {
+	m := new(Result)
+	err := x.stream.Recv(m)
+	if err != nil {
+		return nil, err
+	}
+	return m, nil
+}
+
+// Server API for Registry service
+
+type RegistryHandler interface {
+	GetService(context.Context, *GetRequest, *GetResponse) error
+	Register(context.Context, *Service, *EmptyResponse) error
+	Deregister(context.Context, *Service, *EmptyResponse) error
+	ListServices(context.Context, *ListRequest, *ListResponse) error
+	Watch(context.Context, *WatchRequest, Registry_WatchStream) error
+}
+
+func RegisterRegistryHandler(s server.Server, hdlr RegistryHandler, opts ...server.HandlerOption) error {
+	type registry interface {
+		GetService(ctx context.Context, in *GetRequest, out *GetResponse) error
+		Register(ctx context.Context, in *Service, out *EmptyResponse) error
+		Deregister(ctx context.Context, in *Service, out *EmptyResponse) error
+		ListServices(ctx context.Context, in *ListRequest, out *ListResponse) error
+		Watch(ctx context.Context, stream server.Stream) error
+	}
+	type Registry struct {
+		registry
+	}
+	h := &registryHandler{hdlr}
+	return s.Handle(s.NewHandler(&Registry{h}, opts...))
+}
+
+type registryHandler struct {
+	RegistryHandler
+}
+
+func (h *registryHandler) GetService(ctx context.Context, in *GetRequest, out *GetResponse) error {
+	return h.RegistryHandler.GetService(ctx, in, out)
+}
+
+func (h *registryHandler) Register(ctx context.Context, in *Service, out *EmptyResponse) error {
+	return h.RegistryHandler.Register(ctx, in, out)
+}
+
+func (h *registryHandler) Deregister(ctx context.Context, in *Service, out *EmptyResponse) error {
+	return h.RegistryHandler.Deregister(ctx, in, out)
+}
+
+func (h *registryHandler) ListServices(ctx context.Context, in *ListRequest, out *ListResponse) error {
+	return h.RegistryHandler.ListServices(ctx, in, out)
+}
+
+func (h *registryHandler) Watch(ctx context.Context, stream server.Stream) error {
+	m := new(WatchRequest)
+	if err := stream.Recv(m); err != nil {
+		return err
+	}
+	return h.RegistryHandler.Watch(ctx, m, &registryWatchStream{stream})
+}
+
+type Registry_WatchStream interface {
+	SendMsg(interface{}) error
+	RecvMsg(interface{}) error
+	Close() error
+	Send(*Result) error
+}
+
+type registryWatchStream struct {
+	stream server.Stream
+}
+
+func (x *registryWatchStream) Close() error {
+	return x.stream.Close()
+}
+
+func (x *registryWatchStream) SendMsg(m interface{}) error {
+	return x.stream.Send(m)
+}
+
+func (x *registryWatchStream) RecvMsg(m interface{}) error {
+	return x.stream.Recv(m)
+}
+
+func (x *registryWatchStream) Send(m *Result) error {
+	return x.stream.Send(m)
+}
diff --git a/registry/proto/registry.pb.go b/registry/proto/registry.pb.go
new file mode 100644
index 00000000..b05602ed
--- /dev/null
+++ b/registry/proto/registry.pb.go
@@ -0,0 +1,848 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// source: micro/go-micro/registry/proto/registry.proto
+
+package go_micro_registry
+
+import (
+	context "context"
+	fmt "fmt"
+	proto "github.com/golang/protobuf/proto"
+	grpc "google.golang.org/grpc"
+	math "math"
+)
+
+// 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.ProtoPackageIsVersion3 // please upgrade the proto package
+
+// Service represents a go-micro service
+type Service struct {
+	Name                 string            `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
+	Version              string            `protobuf:"bytes,2,opt,name=version,proto3" json:"version,omitempty"`
+	Metadata             map[string]string `protobuf:"bytes,3,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
+	Endpoints            []*Endpoint       `protobuf:"bytes,4,rep,name=endpoints,proto3" json:"endpoints,omitempty"`
+	Nodes                []*Node           `protobuf:"bytes,5,rep,name=nodes,proto3" json:"nodes,omitempty"`
+	XXX_NoUnkeyedLiteral struct{}          `json:"-"`
+	XXX_unrecognized     []byte            `json:"-"`
+	XXX_sizecache        int32             `json:"-"`
+}
+
+func (m *Service) Reset()         { *m = Service{} }
+func (m *Service) String() string { return proto.CompactTextString(m) }
+func (*Service) ProtoMessage()    {}
+func (*Service) Descriptor() ([]byte, []int) {
+	return fileDescriptor_f287a6b809166ad2, []int{0}
+}
+
+func (m *Service) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_Service.Unmarshal(m, b)
+}
+func (m *Service) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_Service.Marshal(b, m, deterministic)
+}
+func (m *Service) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_Service.Merge(m, src)
+}
+func (m *Service) XXX_Size() int {
+	return xxx_messageInfo_Service.Size(m)
+}
+func (m *Service) XXX_DiscardUnknown() {
+	xxx_messageInfo_Service.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_Service proto.InternalMessageInfo
+
+func (m *Service) GetName() string {
+	if m != nil {
+		return m.Name
+	}
+	return ""
+}
+
+func (m *Service) GetVersion() string {
+	if m != nil {
+		return m.Version
+	}
+	return ""
+}
+
+func (m *Service) GetMetadata() map[string]string {
+	if m != nil {
+		return m.Metadata
+	}
+	return nil
+}
+
+func (m *Service) GetEndpoints() []*Endpoint {
+	if m != nil {
+		return m.Endpoints
+	}
+	return nil
+}
+
+func (m *Service) GetNodes() []*Node {
+	if m != nil {
+		return m.Nodes
+	}
+	return nil
+}
+
+// Node represents the node the service is on
+type Node struct {
+	Id                   string            `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
+	Address              string            `protobuf:"bytes,2,opt,name=address,proto3" json:"address,omitempty"`
+	Port                 int64             `protobuf:"varint,3,opt,name=port,proto3" json:"port,omitempty"`
+	Metadata             map[string]string `protobuf:"bytes,4,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
+	XXX_NoUnkeyedLiteral struct{}          `json:"-"`
+	XXX_unrecognized     []byte            `json:"-"`
+	XXX_sizecache        int32             `json:"-"`
+}
+
+func (m *Node) Reset()         { *m = Node{} }
+func (m *Node) String() string { return proto.CompactTextString(m) }
+func (*Node) ProtoMessage()    {}
+func (*Node) Descriptor() ([]byte, []int) {
+	return fileDescriptor_f287a6b809166ad2, []int{1}
+}
+
+func (m *Node) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_Node.Unmarshal(m, b)
+}
+func (m *Node) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_Node.Marshal(b, m, deterministic)
+}
+func (m *Node) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_Node.Merge(m, src)
+}
+func (m *Node) XXX_Size() int {
+	return xxx_messageInfo_Node.Size(m)
+}
+func (m *Node) XXX_DiscardUnknown() {
+	xxx_messageInfo_Node.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_Node proto.InternalMessageInfo
+
+func (m *Node) GetId() string {
+	if m != nil {
+		return m.Id
+	}
+	return ""
+}
+
+func (m *Node) GetAddress() string {
+	if m != nil {
+		return m.Address
+	}
+	return ""
+}
+
+func (m *Node) GetPort() int64 {
+	if m != nil {
+		return m.Port
+	}
+	return 0
+}
+
+func (m *Node) GetMetadata() map[string]string {
+	if m != nil {
+		return m.Metadata
+	}
+	return nil
+}
+
+// Endpoint is a endpoint provided by a service
+type Endpoint struct {
+	Name                 string            `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
+	Request              *Value            `protobuf:"bytes,2,opt,name=request,proto3" json:"request,omitempty"`
+	Response             *Value            `protobuf:"bytes,3,opt,name=response,proto3" json:"response,omitempty"`
+	Metadata             map[string]string `protobuf:"bytes,4,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
+	XXX_NoUnkeyedLiteral struct{}          `json:"-"`
+	XXX_unrecognized     []byte            `json:"-"`
+	XXX_sizecache        int32             `json:"-"`
+}
+
+func (m *Endpoint) Reset()         { *m = Endpoint{} }
+func (m *Endpoint) String() string { return proto.CompactTextString(m) }
+func (*Endpoint) ProtoMessage()    {}
+func (*Endpoint) Descriptor() ([]byte, []int) {
+	return fileDescriptor_f287a6b809166ad2, []int{2}
+}
+
+func (m *Endpoint) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_Endpoint.Unmarshal(m, b)
+}
+func (m *Endpoint) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_Endpoint.Marshal(b, m, deterministic)
+}
+func (m *Endpoint) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_Endpoint.Merge(m, src)
+}
+func (m *Endpoint) XXX_Size() int {
+	return xxx_messageInfo_Endpoint.Size(m)
+}
+func (m *Endpoint) XXX_DiscardUnknown() {
+	xxx_messageInfo_Endpoint.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_Endpoint proto.InternalMessageInfo
+
+func (m *Endpoint) GetName() string {
+	if m != nil {
+		return m.Name
+	}
+	return ""
+}
+
+func (m *Endpoint) GetRequest() *Value {
+	if m != nil {
+		return m.Request
+	}
+	return nil
+}
+
+func (m *Endpoint) GetResponse() *Value {
+	if m != nil {
+		return m.Response
+	}
+	return nil
+}
+
+func (m *Endpoint) GetMetadata() map[string]string {
+	if m != nil {
+		return m.Metadata
+	}
+	return nil
+}
+
+// Value is an opaque value for a request or response
+type Value struct {
+	Name                 string   `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
+	Type                 string   `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"`
+	Values               []*Value `protobuf:"bytes,3,rep,name=values,proto3" json:"values,omitempty"`
+	XXX_NoUnkeyedLiteral struct{} `json:"-"`
+	XXX_unrecognized     []byte   `json:"-"`
+	XXX_sizecache        int32    `json:"-"`
+}
+
+func (m *Value) Reset()         { *m = Value{} }
+func (m *Value) String() string { return proto.CompactTextString(m) }
+func (*Value) ProtoMessage()    {}
+func (*Value) Descriptor() ([]byte, []int) {
+	return fileDescriptor_f287a6b809166ad2, []int{3}
+}
+
+func (m *Value) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_Value.Unmarshal(m, b)
+}
+func (m *Value) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_Value.Marshal(b, m, deterministic)
+}
+func (m *Value) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_Value.Merge(m, src)
+}
+func (m *Value) XXX_Size() int {
+	return xxx_messageInfo_Value.Size(m)
+}
+func (m *Value) XXX_DiscardUnknown() {
+	xxx_messageInfo_Value.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_Value proto.InternalMessageInfo
+
+func (m *Value) GetName() string {
+	if m != nil {
+		return m.Name
+	}
+	return ""
+}
+
+func (m *Value) GetType() string {
+	if m != nil {
+		return m.Type
+	}
+	return ""
+}
+
+func (m *Value) GetValues() []*Value {
+	if m != nil {
+		return m.Values
+	}
+	return nil
+}
+
+// Result is returns by the watcher
+type Result struct {
+	Action               string   `protobuf:"bytes,1,opt,name=action,proto3" json:"action,omitempty"`
+	Service              *Service `protobuf:"bytes,2,opt,name=service,proto3" json:"service,omitempty"`
+	Timestamp            int64    `protobuf:"varint,3,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
+	XXX_NoUnkeyedLiteral struct{} `json:"-"`
+	XXX_unrecognized     []byte   `json:"-"`
+	XXX_sizecache        int32    `json:"-"`
+}
+
+func (m *Result) Reset()         { *m = Result{} }
+func (m *Result) String() string { return proto.CompactTextString(m) }
+func (*Result) ProtoMessage()    {}
+func (*Result) Descriptor() ([]byte, []int) {
+	return fileDescriptor_f287a6b809166ad2, []int{4}
+}
+
+func (m *Result) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_Result.Unmarshal(m, b)
+}
+func (m *Result) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_Result.Marshal(b, m, deterministic)
+}
+func (m *Result) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_Result.Merge(m, src)
+}
+func (m *Result) XXX_Size() int {
+	return xxx_messageInfo_Result.Size(m)
+}
+func (m *Result) XXX_DiscardUnknown() {
+	xxx_messageInfo_Result.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_Result proto.InternalMessageInfo
+
+func (m *Result) GetAction() string {
+	if m != nil {
+		return m.Action
+	}
+	return ""
+}
+
+func (m *Result) GetService() *Service {
+	if m != nil {
+		return m.Service
+	}
+	return nil
+}
+
+func (m *Result) GetTimestamp() int64 {
+	if m != nil {
+		return m.Timestamp
+	}
+	return 0
+}
+
+type EmptyResponse struct {
+	XXX_NoUnkeyedLiteral struct{} `json:"-"`
+	XXX_unrecognized     []byte   `json:"-"`
+	XXX_sizecache        int32    `json:"-"`
+}
+
+func (m *EmptyResponse) Reset()         { *m = EmptyResponse{} }
+func (m *EmptyResponse) String() string { return proto.CompactTextString(m) }
+func (*EmptyResponse) ProtoMessage()    {}
+func (*EmptyResponse) Descriptor() ([]byte, []int) {
+	return fileDescriptor_f287a6b809166ad2, []int{5}
+}
+
+func (m *EmptyResponse) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_EmptyResponse.Unmarshal(m, b)
+}
+func (m *EmptyResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_EmptyResponse.Marshal(b, m, deterministic)
+}
+func (m *EmptyResponse) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_EmptyResponse.Merge(m, src)
+}
+func (m *EmptyResponse) XXX_Size() int {
+	return xxx_messageInfo_EmptyResponse.Size(m)
+}
+func (m *EmptyResponse) XXX_DiscardUnknown() {
+	xxx_messageInfo_EmptyResponse.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_EmptyResponse proto.InternalMessageInfo
+
+type GetRequest struct {
+	Service              string   `protobuf:"bytes,1,opt,name=service,proto3" json:"service,omitempty"`
+	XXX_NoUnkeyedLiteral struct{} `json:"-"`
+	XXX_unrecognized     []byte   `json:"-"`
+	XXX_sizecache        int32    `json:"-"`
+}
+
+func (m *GetRequest) Reset()         { *m = GetRequest{} }
+func (m *GetRequest) String() string { return proto.CompactTextString(m) }
+func (*GetRequest) ProtoMessage()    {}
+func (*GetRequest) Descriptor() ([]byte, []int) {
+	return fileDescriptor_f287a6b809166ad2, []int{6}
+}
+
+func (m *GetRequest) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_GetRequest.Unmarshal(m, b)
+}
+func (m *GetRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_GetRequest.Marshal(b, m, deterministic)
+}
+func (m *GetRequest) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_GetRequest.Merge(m, src)
+}
+func (m *GetRequest) XXX_Size() int {
+	return xxx_messageInfo_GetRequest.Size(m)
+}
+func (m *GetRequest) XXX_DiscardUnknown() {
+	xxx_messageInfo_GetRequest.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_GetRequest proto.InternalMessageInfo
+
+func (m *GetRequest) GetService() string {
+	if m != nil {
+		return m.Service
+	}
+	return ""
+}
+
+type GetResponse struct {
+	Services             []*Service `protobuf:"bytes,1,rep,name=services,proto3" json:"services,omitempty"`
+	XXX_NoUnkeyedLiteral struct{}   `json:"-"`
+	XXX_unrecognized     []byte     `json:"-"`
+	XXX_sizecache        int32      `json:"-"`
+}
+
+func (m *GetResponse) Reset()         { *m = GetResponse{} }
+func (m *GetResponse) String() string { return proto.CompactTextString(m) }
+func (*GetResponse) ProtoMessage()    {}
+func (*GetResponse) Descriptor() ([]byte, []int) {
+	return fileDescriptor_f287a6b809166ad2, []int{7}
+}
+
+func (m *GetResponse) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_GetResponse.Unmarshal(m, b)
+}
+func (m *GetResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_GetResponse.Marshal(b, m, deterministic)
+}
+func (m *GetResponse) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_GetResponse.Merge(m, src)
+}
+func (m *GetResponse) XXX_Size() int {
+	return xxx_messageInfo_GetResponse.Size(m)
+}
+func (m *GetResponse) XXX_DiscardUnknown() {
+	xxx_messageInfo_GetResponse.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_GetResponse proto.InternalMessageInfo
+
+func (m *GetResponse) GetServices() []*Service {
+	if m != nil {
+		return m.Services
+	}
+	return nil
+}
+
+type ListRequest struct {
+	XXX_NoUnkeyedLiteral struct{} `json:"-"`
+	XXX_unrecognized     []byte   `json:"-"`
+	XXX_sizecache        int32    `json:"-"`
+}
+
+func (m *ListRequest) Reset()         { *m = ListRequest{} }
+func (m *ListRequest) String() string { return proto.CompactTextString(m) }
+func (*ListRequest) ProtoMessage()    {}
+func (*ListRequest) Descriptor() ([]byte, []int) {
+	return fileDescriptor_f287a6b809166ad2, []int{8}
+}
+
+func (m *ListRequest) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_ListRequest.Unmarshal(m, b)
+}
+func (m *ListRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_ListRequest.Marshal(b, m, deterministic)
+}
+func (m *ListRequest) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_ListRequest.Merge(m, src)
+}
+func (m *ListRequest) XXX_Size() int {
+	return xxx_messageInfo_ListRequest.Size(m)
+}
+func (m *ListRequest) XXX_DiscardUnknown() {
+	xxx_messageInfo_ListRequest.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_ListRequest proto.InternalMessageInfo
+
+type ListResponse struct {
+	Services             []*Service `protobuf:"bytes,1,rep,name=services,proto3" json:"services,omitempty"`
+	XXX_NoUnkeyedLiteral struct{}   `json:"-"`
+	XXX_unrecognized     []byte     `json:"-"`
+	XXX_sizecache        int32      `json:"-"`
+}
+
+func (m *ListResponse) Reset()         { *m = ListResponse{} }
+func (m *ListResponse) String() string { return proto.CompactTextString(m) }
+func (*ListResponse) ProtoMessage()    {}
+func (*ListResponse) Descriptor() ([]byte, []int) {
+	return fileDescriptor_f287a6b809166ad2, []int{9}
+}
+
+func (m *ListResponse) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_ListResponse.Unmarshal(m, b)
+}
+func (m *ListResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_ListResponse.Marshal(b, m, deterministic)
+}
+func (m *ListResponse) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_ListResponse.Merge(m, src)
+}
+func (m *ListResponse) XXX_Size() int {
+	return xxx_messageInfo_ListResponse.Size(m)
+}
+func (m *ListResponse) XXX_DiscardUnknown() {
+	xxx_messageInfo_ListResponse.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_ListResponse proto.InternalMessageInfo
+
+func (m *ListResponse) GetServices() []*Service {
+	if m != nil {
+		return m.Services
+	}
+	return nil
+}
+
+type WatchRequest struct {
+	// service is optional
+	Service              string   `protobuf:"bytes,1,opt,name=service,proto3" json:"service,omitempty"`
+	XXX_NoUnkeyedLiteral struct{} `json:"-"`
+	XXX_unrecognized     []byte   `json:"-"`
+	XXX_sizecache        int32    `json:"-"`
+}
+
+func (m *WatchRequest) Reset()         { *m = WatchRequest{} }
+func (m *WatchRequest) String() string { return proto.CompactTextString(m) }
+func (*WatchRequest) ProtoMessage()    {}
+func (*WatchRequest) Descriptor() ([]byte, []int) {
+	return fileDescriptor_f287a6b809166ad2, []int{10}
+}
+
+func (m *WatchRequest) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_WatchRequest.Unmarshal(m, b)
+}
+func (m *WatchRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_WatchRequest.Marshal(b, m, deterministic)
+}
+func (m *WatchRequest) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_WatchRequest.Merge(m, src)
+}
+func (m *WatchRequest) XXX_Size() int {
+	return xxx_messageInfo_WatchRequest.Size(m)
+}
+func (m *WatchRequest) XXX_DiscardUnknown() {
+	xxx_messageInfo_WatchRequest.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_WatchRequest proto.InternalMessageInfo
+
+func (m *WatchRequest) GetService() string {
+	if m != nil {
+		return m.Service
+	}
+	return ""
+}
+
+func init() {
+	proto.RegisterType((*Service)(nil), "go.micro.registry.Service")
+	proto.RegisterMapType((map[string]string)(nil), "go.micro.registry.Service.MetadataEntry")
+	proto.RegisterType((*Node)(nil), "go.micro.registry.Node")
+	proto.RegisterMapType((map[string]string)(nil), "go.micro.registry.Node.MetadataEntry")
+	proto.RegisterType((*Endpoint)(nil), "go.micro.registry.Endpoint")
+	proto.RegisterMapType((map[string]string)(nil), "go.micro.registry.Endpoint.MetadataEntry")
+	proto.RegisterType((*Value)(nil), "go.micro.registry.Value")
+	proto.RegisterType((*Result)(nil), "go.micro.registry.Result")
+	proto.RegisterType((*EmptyResponse)(nil), "go.micro.registry.EmptyResponse")
+	proto.RegisterType((*GetRequest)(nil), "go.micro.registry.GetRequest")
+	proto.RegisterType((*GetResponse)(nil), "go.micro.registry.GetResponse")
+	proto.RegisterType((*ListRequest)(nil), "go.micro.registry.ListRequest")
+	proto.RegisterType((*ListResponse)(nil), "go.micro.registry.ListResponse")
+	proto.RegisterType((*WatchRequest)(nil), "go.micro.registry.WatchRequest")
+}
+
+func init() {
+	proto.RegisterFile("micro/go-micro/registry/proto/registry.proto", fileDescriptor_f287a6b809166ad2)
+}
+
+var fileDescriptor_f287a6b809166ad2 = []byte{
+	// 577 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x55, 0x6d, 0x8b, 0xd3, 0x4c,
+	0x14, 0x6d, 0x92, 0xbe, 0xde, 0x6e, 0x9f, 0x47, 0x2f, 0xa2, 0x31, 0xbe, 0x95, 0x80, 0x52, 0xc1,
+	0xcd, 0x2e, 0x75, 0x11, 0x5f, 0x3e, 0x09, 0x5b, 0x17, 0x64, 0x57, 0x70, 0x04, 0xfd, 0x1c, 0x9b,
+	0x4b, 0x0d, 0x6e, 0x5e, 0x9c, 0x99, 0x16, 0xfa, 0x1f, 0x04, 0xff, 0x84, 0x3f, 0xc5, 0x1f, 0x26,
+	0x99, 0xcc, 0x34, 0x5d, 0x36, 0xa9, 0x1f, 0x56, 0xbf, 0xcd, 0xcd, 0x9c, 0x73, 0xe6, 0x9e, 0x73,
+	0x67, 0x5a, 0x78, 0x92, 0xc4, 0x73, 0x9e, 0x1d, 0x2c, 0xb2, 0xfd, 0x72, 0xc1, 0x69, 0x11, 0x0b,
+	0xc9, 0xd7, 0x07, 0x39, 0xcf, 0x64, 0x55, 0x06, 0xaa, 0xc4, 0xeb, 0x8b, 0x2c, 0x50, 0xb8, 0xc0,
+	0x6c, 0xf8, 0x3f, 0x6d, 0xe8, 0x7d, 0x20, 0xbe, 0x8a, 0xe7, 0x84, 0x08, 0xed, 0x34, 0x4c, 0xc8,
+	0xb5, 0xc6, 0xd6, 0x64, 0xc0, 0xd4, 0x1a, 0x5d, 0xe8, 0xad, 0x88, 0x8b, 0x38, 0x4b, 0x5d, 0x5b,
+	0x7d, 0x36, 0x25, 0x1e, 0x43, 0x3f, 0x21, 0x19, 0x46, 0xa1, 0x0c, 0x5d, 0x67, 0xec, 0x4c, 0x86,
+	0xd3, 0x49, 0x70, 0x49, 0x3f, 0xd0, 0xda, 0xc1, 0x99, 0x86, 0xce, 0x52, 0xc9, 0xd7, 0x6c, 0xc3,
+	0xc4, 0x17, 0x30, 0xa0, 0x34, 0xca, 0xb3, 0x38, 0x95, 0xc2, 0x6d, 0x2b, 0x99, 0x3b, 0x35, 0x32,
+	0x33, 0x8d, 0x61, 0x15, 0x1a, 0xf7, 0xa1, 0x93, 0x66, 0x11, 0x09, 0xb7, 0xa3, 0x68, 0xb7, 0x6a,
+	0x68, 0xef, 0xb2, 0x88, 0x58, 0x89, 0xf2, 0x5e, 0xc1, 0xe8, 0x42, 0x13, 0x78, 0x0d, 0x9c, 0xaf,
+	0xb4, 0xd6, 0x6e, 0x8b, 0x25, 0xde, 0x80, 0xce, 0x2a, 0x3c, 0x5f, 0x92, 0xb6, 0x5a, 0x16, 0x2f,
+	0xed, 0xe7, 0x96, 0xff, 0xcb, 0x82, 0x76, 0x21, 0x86, 0xff, 0x81, 0x1d, 0x47, 0x9a, 0x63, 0xc7,
+	0x51, 0x91, 0x4f, 0x18, 0x45, 0x9c, 0x84, 0x30, 0xf9, 0xe8, 0xb2, 0x48, 0x33, 0xcf, 0xb8, 0x74,
+	0x9d, 0xb1, 0x35, 0x71, 0x98, 0x5a, 0xe3, 0xeb, 0xad, 0xcc, 0x4a, 0xb3, 0x0f, 0x1b, 0xba, 0x6e,
+	0x0a, 0xec, 0x6a, 0x36, 0xbe, 0xdb, 0xd0, 0x37, 0x51, 0xd6, 0x8e, 0x7b, 0x0a, 0x3d, 0x4e, 0xdf,
+	0x96, 0x24, 0xa4, 0x22, 0x0f, 0xa7, 0x6e, 0x4d, 0x7f, 0x1f, 0x0b, 0x3d, 0x66, 0x80, 0x78, 0x04,
+	0x7d, 0x4e, 0x22, 0xcf, 0x52, 0x41, 0xca, 0xec, 0x2e, 0xd2, 0x06, 0x89, 0xb3, 0x4b, 0x51, 0x3c,
+	0xde, 0x31, 0xf7, 0x7f, 0x13, 0x47, 0x08, 0x1d, 0xd5, 0x56, 0x6d, 0x14, 0x08, 0x6d, 0xb9, 0xce,
+	0x0d, 0x4b, 0xad, 0xf1, 0x10, 0xba, 0x8a, 0x2d, 0xf4, 0x8d, 0x6f, 0x36, 0xaa, 0x71, 0xbe, 0x84,
+	0x2e, 0x23, 0xb1, 0x3c, 0x97, 0x78, 0x13, 0xba, 0xe1, 0x5c, 0x16, 0x0f, 0xa9, 0x3c, 0x45, 0x57,
+	0x78, 0x04, 0x3d, 0x51, 0x3e, 0x12, 0x1d, 0xb9, 0xd7, 0xfc, 0x8c, 0x98, 0x81, 0xe2, 0x5d, 0x18,
+	0xc8, 0x38, 0x21, 0x21, 0xc3, 0x24, 0xd7, 0x57, 0xac, 0xfa, 0xe0, 0xff, 0x0f, 0xa3, 0x59, 0x92,
+	0xcb, 0x35, 0xd3, 0x69, 0xfb, 0x8f, 0x00, 0x4e, 0x48, 0x32, 0x3d, 0x31, 0xb7, 0x3a, 0xb2, 0xec,
+	0xc5, 0x94, 0xfe, 0x0c, 0x86, 0x0a, 0xa7, 0x87, 0xf4, 0x0c, 0xfa, 0x7a, 0x47, 0xb8, 0x96, 0x72,
+	0xbc, 0xab, 0xb9, 0x0d, 0xd6, 0x1f, 0xc1, 0xf0, 0x34, 0x16, 0xe6, 0x3c, 0xff, 0x0d, 0xec, 0x95,
+	0xe5, 0x15, 0x65, 0x27, 0xb0, 0xf7, 0x29, 0x94, 0xf3, 0x2f, 0x7f, 0xf4, 0x31, 0xfd, 0xe1, 0x40,
+	0x9f, 0x69, 0x21, 0x3c, 0x53, 0xe6, 0xcd, 0xaf, 0xdc, 0xbd, 0x9a, 0xa3, 0xaa, 0x6c, 0xbc, 0xfb,
+	0x4d, 0xdb, 0x3a, 0xc9, 0x16, 0xbe, 0x35, 0xd2, 0xc4, 0x71, 0x47, 0xdf, 0xde, 0xb8, 0xee, 0x3e,
+	0x5f, 0x98, 0x4a, 0x0b, 0x4f, 0x01, 0x8e, 0x89, 0xff, 0x2d, 0xb5, 0xf7, 0x65, 0xce, 0x9a, 0x22,
+	0xb0, 0xce, 0xcb, 0xd6, 0x5c, 0xbc, 0x07, 0x8d, 0xfb, 0x1b, 0xc9, 0x13, 0xe8, 0xa8, 0xc8, 0xb1,
+	0x0e, 0xbb, 0x3d, 0x0c, 0xef, 0x76, 0x0d, 0xa0, 0xbc, 0xfa, 0x7e, 0xeb, 0xd0, 0xfa, 0xdc, 0x55,
+	0x7f, 0x41, 0x4f, 0x7f, 0x07, 0x00, 0x00, 0xff, 0xff, 0x6b, 0x0b, 0x12, 0xd6, 0xb2, 0x06, 0x00,
+	0x00,
+}
+
+// Reference imports to suppress errors if they are not otherwise used.
+var _ context.Context
+var _ grpc.ClientConn
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+const _ = grpc.SupportPackageIsVersion4
+
+// RegistryClient is the client API for Registry service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
+type RegistryClient interface {
+	GetService(ctx context.Context, in *GetRequest, opts ...grpc.CallOption) (*GetResponse, error)
+	Register(ctx context.Context, in *Service, opts ...grpc.CallOption) (*EmptyResponse, error)
+	Deregister(ctx context.Context, in *Service, opts ...grpc.CallOption) (*EmptyResponse, error)
+	ListServices(ctx context.Context, in *ListRequest, opts ...grpc.CallOption) (*ListResponse, error)
+	Watch(ctx context.Context, in *WatchRequest, opts ...grpc.CallOption) (Registry_WatchClient, error)
+}
+
+type registryClient struct {
+	cc *grpc.ClientConn
+}
+
+func NewRegistryClient(cc *grpc.ClientConn) RegistryClient {
+	return &registryClient{cc}
+}
+
+func (c *registryClient) GetService(ctx context.Context, in *GetRequest, opts ...grpc.CallOption) (*GetResponse, error) {
+	out := new(GetResponse)
+	err := c.cc.Invoke(ctx, "/go.micro.registry.Registry/GetService", in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *registryClient) Register(ctx context.Context, in *Service, opts ...grpc.CallOption) (*EmptyResponse, error) {
+	out := new(EmptyResponse)
+	err := c.cc.Invoke(ctx, "/go.micro.registry.Registry/Register", in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *registryClient) Deregister(ctx context.Context, in *Service, opts ...grpc.CallOption) (*EmptyResponse, error) {
+	out := new(EmptyResponse)
+	err := c.cc.Invoke(ctx, "/go.micro.registry.Registry/Deregister", in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *registryClient) ListServices(ctx context.Context, in *ListRequest, opts ...grpc.CallOption) (*ListResponse, error) {
+	out := new(ListResponse)
+	err := c.cc.Invoke(ctx, "/go.micro.registry.Registry/ListServices", in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *registryClient) Watch(ctx context.Context, in *WatchRequest, opts ...grpc.CallOption) (Registry_WatchClient, error) {
+	stream, err := c.cc.NewStream(ctx, &_Registry_serviceDesc.Streams[0], "/go.micro.registry.Registry/Watch", opts...)
+	if err != nil {
+		return nil, err
+	}
+	x := &registryWatchClient{stream}
+	if err := x.ClientStream.SendMsg(in); err != nil {
+		return nil, err
+	}
+	if err := x.ClientStream.CloseSend(); err != nil {
+		return nil, err
+	}
+	return x, nil
+}
+
+type Registry_WatchClient interface {
+	Recv() (*Result, error)
+	grpc.ClientStream
+}
+
+type registryWatchClient struct {
+	grpc.ClientStream
+}
+
+func (x *registryWatchClient) Recv() (*Result, error) {
+	m := new(Result)
+	if err := x.ClientStream.RecvMsg(m); err != nil {
+		return nil, err
+	}
+	return m, nil
+}
+
+// RegistryServer is the server API for Registry service.
+type RegistryServer interface {
+	GetService(context.Context, *GetRequest) (*GetResponse, error)
+	Register(context.Context, *Service) (*EmptyResponse, error)
+	Deregister(context.Context, *Service) (*EmptyResponse, error)
+	ListServices(context.Context, *ListRequest) (*ListResponse, error)
+	Watch(*WatchRequest, Registry_WatchServer) error
+}
+
+func RegisterRegistryServer(s *grpc.Server, srv RegistryServer) {
+	s.RegisterService(&_Registry_serviceDesc, srv)
+}
+
+func _Registry_GetService_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(GetRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(RegistryServer).GetService(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/go.micro.registry.Registry/GetService",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(RegistryServer).GetService(ctx, req.(*GetRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _Registry_Register_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(Service)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(RegistryServer).Register(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/go.micro.registry.Registry/Register",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(RegistryServer).Register(ctx, req.(*Service))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _Registry_Deregister_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(Service)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(RegistryServer).Deregister(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/go.micro.registry.Registry/Deregister",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(RegistryServer).Deregister(ctx, req.(*Service))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _Registry_ListServices_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(ListRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(RegistryServer).ListServices(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/go.micro.registry.Registry/ListServices",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(RegistryServer).ListServices(ctx, req.(*ListRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _Registry_Watch_Handler(srv interface{}, stream grpc.ServerStream) error {
+	m := new(WatchRequest)
+	if err := stream.RecvMsg(m); err != nil {
+		return err
+	}
+	return srv.(RegistryServer).Watch(m, &registryWatchServer{stream})
+}
+
+type Registry_WatchServer interface {
+	Send(*Result) error
+	grpc.ServerStream
+}
+
+type registryWatchServer struct {
+	grpc.ServerStream
+}
+
+func (x *registryWatchServer) Send(m *Result) error {
+	return x.ServerStream.SendMsg(m)
+}
+
+var _Registry_serviceDesc = grpc.ServiceDesc{
+	ServiceName: "go.micro.registry.Registry",
+	HandlerType: (*RegistryServer)(nil),
+	Methods: []grpc.MethodDesc{
+		{
+			MethodName: "GetService",
+			Handler:    _Registry_GetService_Handler,
+		},
+		{
+			MethodName: "Register",
+			Handler:    _Registry_Register_Handler,
+		},
+		{
+			MethodName: "Deregister",
+			Handler:    _Registry_Deregister_Handler,
+		},
+		{
+			MethodName: "ListServices",
+			Handler:    _Registry_ListServices_Handler,
+		},
+	},
+	Streams: []grpc.StreamDesc{
+		{
+			StreamName:    "Watch",
+			Handler:       _Registry_Watch_Handler,
+			ServerStreams: true,
+		},
+	},
+	Metadata: "micro/go-micro/registry/proto/registry.proto",
+}
diff --git a/registry/proto/registry.proto b/registry/proto/registry.proto
new file mode 100644
index 00000000..8d7cc854
--- /dev/null
+++ b/registry/proto/registry.proto
@@ -0,0 +1,73 @@
+syntax = "proto3";
+
+package go.micro.registry;
+
+service Registry {
+	rpc GetService(GetRequest) returns (GetResponse) {};
+	rpc Register(Service) returns (EmptyResponse) {};
+	rpc Deregister(Service) returns (EmptyResponse) {};
+	rpc ListServices(ListRequest) returns (ListResponse) {};
+	rpc Watch(WatchRequest) returns (stream Result) {};
+}
+
+// Service represents a go-micro service
+message Service {
+	string name = 1;
+	string version = 2;
+	map<string,string> metadata = 3;
+	repeated Endpoint endpoints = 4;
+	repeated Node nodes = 5;
+}
+
+// Node represents the node the service is on
+message Node {
+	string id = 1;
+	string address = 2;
+	int64 port = 3;
+	map<string,string> metadata = 4;
+}
+
+// Endpoint is a endpoint provided by a service
+message Endpoint {
+	string name = 1;
+	Value request = 2;
+	Value response = 3;
+	map<string, string> metadata = 4;
+}
+
+// Value is an opaque value for a request or response
+message Value {
+	string name = 1;
+	string type = 2;
+	repeated Value values = 3;
+}
+
+// Result is returns by the watcher
+message Result {
+	string action = 1; // create, update, delete
+	Service service = 2;
+	int64 timestamp = 3; // unix timestamp
+}
+
+message EmptyResponse {}
+
+message GetRequest {
+	string service = 1;
+}
+
+message GetResponse {
+	repeated Service services = 1;
+}
+
+message ListRequest {
+	// TODO: filtering
+}
+
+message ListResponse {
+	repeated Service services = 1;
+}
+
+message WatchRequest {
+	// service is optional
+	string service = 1;
+}
diff --git a/registry/service/service.go b/registry/service/service.go
new file mode 100644
index 00000000..2473c32f
--- /dev/null
+++ b/registry/service/service.go
@@ -0,0 +1,155 @@
+// Package service uses the registry service
+package service
+
+import (
+	"context"
+	"time"
+
+	"github.com/micro/go-micro/client"
+	"github.com/micro/go-micro/registry"
+	pb "github.com/micro/go-micro/registry/proto"
+)
+
+var (
+	// The default service name
+	DefaultService = "go.micro.service"
+)
+
+type serviceRegistry struct {
+	opts registry.Options
+	// name of the registry
+	name string
+	// address
+	address []string
+	// client to call registry
+	client pb.RegistryService
+}
+
+func (s *serviceRegistry) callOpts() []client.CallOption {
+	var opts []client.CallOption
+
+	// set registry address
+	if len(s.address) > 0 {
+		opts = append(opts, client.WithAddress(s.address...))
+	}
+
+	// set timeout
+	if s.opts.Timeout > time.Duration(0) {
+		opts = append(opts, client.WithRequestTimeout(s.opts.Timeout))
+	}
+
+	return opts
+}
+
+func (s *serviceRegistry) Init(opts ...registry.Option) error {
+	for _, o := range opts {
+		o(&s.opts)
+	}
+	return nil
+}
+
+func (s *serviceRegistry) Options() registry.Options {
+	return s.opts
+}
+
+func (s *serviceRegistry) Register(srv *registry.Service, opts ...registry.RegisterOption) error {
+	var options registry.RegisterOptions
+	for _, o := range opts {
+		o(&options)
+	}
+
+	// register the service
+	_, err := s.client.Register(context.TODO(), ToProto(srv), s.callOpts()...)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (s *serviceRegistry) Deregister(srv *registry.Service) error {
+	// deregister the service
+	_, err := s.client.Deregister(context.TODO(), ToProto(srv), s.callOpts()...)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func (s *serviceRegistry) GetService(name string) ([]*registry.Service, error) {
+	rsp, err := s.client.GetService(context.TODO(), &pb.GetRequest{
+		Service: name,
+	}, s.callOpts()...)
+
+	if err != nil {
+		return nil, err
+	}
+
+	var services []*registry.Service
+	for _, service := range rsp.Services {
+		services = append(services, ToService(service))
+	}
+	return services, nil
+}
+
+func (s *serviceRegistry) ListServices() ([]*registry.Service, error) {
+	rsp, err := s.client.ListServices(context.TODO(), &pb.ListRequest{}, s.callOpts()...)
+	if err != nil {
+		return nil, err
+	}
+
+	var services []*registry.Service
+	for _, service := range rsp.Services {
+		services = append(services, ToService(service))
+	}
+
+	return services, nil
+}
+
+func (s *serviceRegistry) Watch(opts ...registry.WatchOption) (registry.Watcher, error) {
+	var options registry.WatchOptions
+	for _, o := range opts {
+		o(&options)
+	}
+
+	stream, err := s.client.Watch(context.TODO(), &pb.WatchRequest{
+		Service: options.Service,
+	}, s.callOpts()...)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return newWatcher(stream), nil
+}
+
+func (s *serviceRegistry) String() string {
+	return s.name
+}
+
+// NewRegistry returns a new registry service client
+func NewRegistry(opts ...registry.Option) registry.Registry {
+	var options registry.Options
+	for _, o := range opts {
+		o(&options)
+	}
+
+	// use mdns to find the service registry
+	mReg := registry.NewRegistry()
+
+	// create new client with mdns
+	cli := client.NewClient(
+		client.Registry(mReg),
+	)
+
+	// service name
+	// TODO: accept option
+	name := DefaultService
+
+	return &serviceRegistry{
+		opts:    options,
+		name:    name,
+		address: options.Addrs,
+		client:  pb.NewRegistryService(name, cli),
+	}
+}
diff --git a/registry/service/util.go b/registry/service/util.go
new file mode 100644
index 00000000..bbde4095
--- /dev/null
+++ b/registry/service/util.go
@@ -0,0 +1,133 @@
+package service
+
+import (
+	"github.com/micro/go-micro/registry"
+	pb "github.com/micro/go-micro/registry/proto"
+)
+
+func values(v []*registry.Value) []*pb.Value {
+	if len(v) == 0 {
+		return []*pb.Value{}
+	}
+
+	var vs []*pb.Value
+	for _, vi := range v {
+		vs = append(vs, &pb.Value{
+			Name:   vi.Name,
+			Type:   vi.Type,
+			Values: values(vi.Values),
+		})
+	}
+	return vs
+}
+
+func toValues(v []*pb.Value) []*registry.Value {
+	if len(v) == 0 {
+		return []*registry.Value{}
+	}
+
+	var vs []*registry.Value
+	for _, vi := range v {
+		vs = append(vs, &registry.Value{
+			Name:   vi.Name,
+			Type:   vi.Type,
+			Values: toValues(vi.Values),
+		})
+	}
+	return vs
+}
+
+func ToProto(s *registry.Service) *pb.Service {
+	var endpoints []*pb.Endpoint
+	for _, ep := range s.Endpoints {
+		var request, response *pb.Value
+
+		if ep.Request != nil {
+			request = &pb.Value{
+				Name:   ep.Request.Name,
+				Type:   ep.Request.Type,
+				Values: values(ep.Request.Values),
+			}
+		}
+
+		if ep.Response != nil {
+			response = &pb.Value{
+				Name:   ep.Response.Name,
+				Type:   ep.Response.Type,
+				Values: values(ep.Response.Values),
+			}
+		}
+
+		endpoints = append(endpoints, &pb.Endpoint{
+			Name:     ep.Name,
+			Request:  request,
+			Response: response,
+			Metadata: ep.Metadata,
+		})
+	}
+
+	var nodes []*pb.Node
+
+	for _, node := range s.Nodes {
+		nodes = append(nodes, &pb.Node{
+			Id:       node.Id,
+			Address:  node.Address,
+			Metadata: node.Metadata,
+		})
+	}
+
+	return &pb.Service{
+		Name:      s.Name,
+		Version:   s.Version,
+		Metadata:  s.Metadata,
+		Endpoints: endpoints,
+		Nodes:     nodes,
+	}
+}
+
+func ToService(s *pb.Service) *registry.Service {
+	var endpoints []*registry.Endpoint
+	for _, ep := range s.Endpoints {
+		var request, response *registry.Value
+
+		if ep.Request != nil {
+			request = &registry.Value{
+				Name:   ep.Request.Name,
+				Type:   ep.Request.Type,
+				Values: toValues(ep.Request.Values),
+			}
+		}
+
+		if ep.Response != nil {
+			response = &registry.Value{
+				Name:   ep.Response.Name,
+				Type:   ep.Response.Type,
+				Values: toValues(ep.Response.Values),
+			}
+		}
+
+		endpoints = append(endpoints, &registry.Endpoint{
+			Name:     ep.Name,
+			Request:  request,
+			Response: response,
+			Metadata: ep.Metadata,
+		})
+	}
+
+	var nodes []*registry.Node
+	for _, node := range s.Nodes {
+		nodes = append(nodes, &registry.Node{
+			Id:       node.Id,
+			Address:  node.Address,
+			Metadata: node.Metadata,
+		})
+	}
+
+	return &registry.Service{
+		Name:      s.Name,
+		Version:   s.Version,
+		Metadata:  s.Metadata,
+		Endpoints: endpoints,
+		Nodes:     nodes,
+	}
+}
diff --git a/registry/service/watcher.go b/registry/service/watcher.go
new file mode 100644
index 00000000..08f2144d
--- /dev/null
+++ b/registry/service/watcher.go
@@ -0,0 +1,49 @@
+package service
+
+import (
+	"github.com/micro/go-micro/registry"
+	pb "github.com/micro/go-micro/registry/proto"
+)
+
+type serviceWatcher struct {
+	stream pb.Registry_WatchService
+	closed chan bool
+}
+
+func (s *serviceWatcher) Next() (*registry.Result, error) {
+	for {
+		// check if closed
+		select {
+		case <-s.closed:
+			return nil, registry.ErrWatcherStopped
+		default:
+		}
+
+		r, err := s.stream.Recv()
+		if err != nil {
+			return nil, err
+		}
+
+		return &registry.Result{
+			Action:  r.Action,
+			Service: ToService(r.Service),
+		}, nil
+	}
+}
+
+func (s *serviceWatcher) Stop() {
+	select {
+	case <-s.closed:
+		return
+	default:
+		close(s.closed)
+		s.stream.Close()
+	}
+}
+
+func newWatcher(stream pb.Registry_WatchService) registry.Watcher {
+	return &serviceWatcher{
+		stream: stream,
+		closed: make(chan bool),
+	}
+}
diff --git a/router/default.go b/router/default.go
index bf3f0e60..cfc30548 100644
--- a/router/default.go
+++ b/router/default.go
@@ -10,6 +10,7 @@ import (
 
 	"github.com/google/uuid"
 	"github.com/micro/go-micro/registry"
+	"github.com/micro/go-micro/util/log"
 )
 
 const (
@@ -43,7 +44,7 @@ var (
 // router implements default router
 type router struct {
 	sync.RWMutex
-	opts      Options
+	options   Options
 	status    Status
 	table     *table
 	exit      chan struct{}
@@ -70,7 +71,7 @@ func newRouter(opts ...Option) Router {
 	status := Status{Code: Stopped, Error: nil}
 
 	return &router{
-		opts:        options,
+		options:     options,
 		status:      status,
 		table:       newTable(),
 		advertWg:    &sync.WaitGroup{},
@@ -85,7 +86,7 @@ func (r *router) Init(opts ...Option) error {
 	defer r.Unlock()
 
 	for _, o := range opts {
-		o(&r.opts)
+		o(&r.options)
 	}
 
 	return nil
@@ -94,10 +95,10 @@ func (r *router) Init(opts ...Option) error {
 // Options returns router options
 func (r *router) Options() Options {
 	r.Lock()
-	opts := r.opts
+	options := r.options
 	r.Unlock()
 
-	return opts
+	return options
 }
 
 // Table returns routing table
@@ -120,6 +121,9 @@ func (r *router) manageRoute(route Route, action string) error {
 		if err := r.table.Delete(route); err != nil && err != ErrRouteNotFound {
 			return fmt.Errorf("failed deleting route for service %s: %s", route.Service, err)
 		}
+	case "solicit":
+		// nothing to do here
+		return nil
 	default:
 		return fmt.Errorf("failed to manage route for service %s. Unknown action: %s", route.Service, action)
 	}
@@ -139,7 +143,8 @@ func (r *router) manageServiceRoutes(service *registry.Service, action string) e
 			Service: service.Name,
 			Address: node.Address,
 			Gateway: "",
-			Network: r.opts.Network,
+			Network: r.options.Network,
+			Router:  r.options.Id,
 			Link:    DefaultLink,
 			Metric:  DefaultLocalMetric,
 		}
@@ -278,13 +283,14 @@ func (r *router) publishAdvert(advType AdvertType, events []*Event) {
 	defer r.advertWg.Done()
 
 	a := &Advert{
-		Id:        r.opts.Id,
+		Id:        r.options.Id,
 		Type:      advType,
 		TTL:       DefaultAdvertTTL,
 		Timestamp: time.Now(),
 		Events:    events,
 	}
 
+	log.Debugf("Router publishing advert; %+v", a)
 	r.RLock()
 	for _, sub := range r.subscribers {
 		// check the exit chan first
@@ -529,20 +535,22 @@ func (r *router) Start() error {
 	}
 
 	// add all local service routes into the routing table
-	if err := r.manageRegistryRoutes(r.opts.Registry, "create"); err != nil {
+	if err := r.manageRegistryRoutes(r.options.Registry, "create"); err != nil {
 		e := fmt.Errorf("failed adding registry routes: %s", err)
 		r.status = Status{Code: Error, Error: e}
 		return e
 	}
 
 	// add default gateway into routing table
-	if r.opts.Gateway != "" {
+	if r.options.Gateway != "" {
 		// note, the only non-default value is the gateway
 		route := Route{
 			Service: "*",
 			Address: "*",
-			Gateway: r.opts.Gateway,
+			Gateway: r.options.Gateway,
 			Network: "*",
+			Router:  r.options.Id,
+			Link:    DefaultLink,
 			Metric:  DefaultLocalMetric,
 		}
 		if err := r.table.Create(route); err != nil {
@@ -557,7 +565,7 @@ func (r *router) Start() error {
 	r.exit = make(chan struct{})
 
 	// registry watcher
-	regWatcher, err := r.opts.Registry.Watch()
+	regWatcher, err := r.options.Registry.Watch()
 	if err != nil {
 		e := fmt.Errorf("failed creating registry watcher: %v", err)
 		r.status = Status{Code: Error, Error: e}
@@ -595,25 +603,14 @@ func (r *router) Advertise() (<-chan *Advert, error) {
 
 	switch r.status.Code {
 	case Advertising:
-		advertChan := make(chan *Advert)
+		advertChan := make(chan *Advert, 128)
 		r.subscribers[uuid.New().String()] = advertChan
 		return advertChan, nil
 	case Running:
-		// list routing table routes to announce
-		routes, err := r.table.List()
+		// list all the routes and pack them into even slice to advertise
+		events, err := r.flushRouteEvents(Create)
 		if err != nil {
-			return nil, fmt.Errorf("failed listing routes: %s", err)
-		}
-
-		// collect all the added routes before we attempt to add default gateway
-		events := make([]*Event, len(routes))
-		for i, route := range routes {
-			event := &Event{
-				Type:      Create,
-				Timestamp: time.Now(),
-				Route:     route,
-			}
-			events[i] = event
+			return nil, fmt.Errorf("failed to flush routes: %s", err)
 		}
 
 		// create event channels
@@ -646,7 +643,7 @@ func (r *router) Advertise() (<-chan *Advert, error) {
 		r.status = Status{Code: Advertising, Error: nil}
 
 		// create advert channel
-		advertChan := make(chan *Advert)
+		advertChan := make(chan *Advert, 128)
 		r.subscribers[uuid.New().String()] = advertChan
 
 		return advertChan, nil
@@ -669,6 +666,10 @@ func (r *router) Process(a *Advert) error {
 	})
 
 	for _, event := range events {
+		// skip if the router is the origin of this route
+		if event.Route.Router == r.options.Id {
+			continue
+		}
 		// create a copy of the route
 		route := event.Route
 		action := event.Type
@@ -680,6 +681,43 @@ func (r *router) Process(a *Advert) error {
 	return nil
 }
 
+// flushRouteEvents returns a slice of events, one per each route in the routing table
+func (r *router) flushRouteEvents(evType EventType) ([]*Event, error) {
+	// list all routes
+	routes, err := r.table.List()
+	if err != nil {
+		return nil, fmt.Errorf("failed listing routes: %s", err)
+	}
+
+	// build a list of events to advertise
+	events := make([]*Event, len(routes))
+	for i, route := range routes {
+		event := &Event{
+			Type:      evType,
+			Timestamp: time.Now(),
+			Route:     route,
+		}
+		events[i] = event
+	}
+
+	return events, nil
+}
+
+// Solicit advertises all of its routes to the network
+// It returns error if the router fails to list the routes
+func (r *router) Solicit() error {
+	events, err := r.flushRouteEvents(Update)
+	if err != nil {
+		return fmt.Errorf("failed solicit routes: %s", err)
+	}
+
+	// advertise the routes
+	r.advertWg.Add(1)
+	go r.publishAdvert(RouteUpdate, events)
+
+	return nil
+}
+
 // Lookup routes in the routing table
 func (r *router) Lookup(q Query) ([]Route, error) {
 	return r.table.Query(q)
diff --git a/router/handler/router.go b/router/handler/router.go
index a7571024..ac05f012 100644
--- a/router/handler/router.go
+++ b/router/handler/router.go
@@ -33,6 +33,7 @@ func (r *Router) Lookup(ctx context.Context, req *pb.LookupRequest, resp *pb.Loo
 			Address: route.Address,
 			Gateway: route.Gateway,
 			Network: route.Network,
+			Router:  route.Router,
 			Link:    route.Link,
 			Metric:  int64(route.Metric),
 		}
@@ -58,6 +59,7 @@ func (r *Router) Advertise(ctx context.Context, req *pb.Request, stream pb.Route
 				Address: event.Route.Address,
 				Gateway: event.Route.Gateway,
 				Network: event.Route.Network,
+				Router:  event.Route.Router,
 				Link:    event.Route.Link,
 				Metric:  int64(event.Route.Metric),
 			}
@@ -97,6 +99,7 @@ func (r *Router) Process(ctx context.Context, req *pb.Advert, rsp *pb.ProcessRes
 			Address: event.Route.Address,
 			Gateway: event.Route.Gateway,
 			Network: event.Route.Network,
+			Router:  event.Route.Router,
 			Link:    event.Route.Link,
 			Metric:  int(event.Route.Metric),
 		}
@@ -161,6 +164,7 @@ func (r *Router) Watch(ctx context.Context, req *pb.WatchRequest, stream pb.Rout
 			Address: event.Route.Address,
 			Gateway: event.Route.Gateway,
 			Network: event.Route.Network,
+			Router:  event.Route.Router,
 			Link:    event.Route.Link,
 			Metric:  int64(event.Route.Metric),
 		}
diff --git a/router/handler/table.go b/router/handler/table.go
index ad17fa51..63e3c96c 100644
--- a/router/handler/table.go
+++ b/router/handler/table.go
@@ -18,6 +18,7 @@ func (t *Table) Create(ctx context.Context, route *pb.Route, resp *pb.CreateResp
 		Address: route.Address,
 		Gateway: route.Gateway,
 		Network: route.Network,
+		Router:  route.Router,
 		Link:    route.Link,
 		Metric:  int(route.Metric),
 	})
@@ -34,6 +35,7 @@ func (t *Table) Update(ctx context.Context, route *pb.Route, resp *pb.UpdateResp
 		Address: route.Address,
 		Gateway: route.Gateway,
 		Network: route.Network,
+		Router:  route.Router,
 		Link:    route.Link,
 		Metric:  int(route.Metric),
 	})
@@ -50,6 +52,7 @@ func (t *Table) Delete(ctx context.Context, route *pb.Route, resp *pb.DeleteResp
 		Address: route.Address,
 		Gateway: route.Gateway,
 		Network: route.Network,
+		Router:  route.Router,
 		Link:    route.Link,
 		Metric:  int(route.Metric),
 	})
@@ -74,6 +77,7 @@ func (t *Table) List(ctx context.Context, req *pb.Request, resp *pb.ListResponse
 			Address: route.Address,
 			Gateway: route.Gateway,
 			Network: route.Network,
+			Router:  route.Router,
 			Link:    route.Link,
 			Metric:  int64(route.Metric),
 		}
@@ -102,6 +106,7 @@ func (t *Table) Query(ctx context.Context, req *pb.QueryRequest, resp *pb.QueryR
 			Address: route.Address,
 			Gateway: route.Gateway,
 			Network: route.Network,
+			Router:  route.Router,
 			Link:    route.Link,
 			Metric:  int64(route.Metric),
 		}
diff --git a/router/proto/router.pb.go b/router/proto/router.pb.go
index c448e484..6dedf13d 100644
--- a/router/proto/router.pb.go
+++ b/router/proto/router.pb.go
@@ -672,10 +672,12 @@ type Route struct {
 	Gateway string `protobuf:"bytes,3,opt,name=gateway,proto3" json:"gateway,omitempty"`
 	// the network for this destination
 	Network string `protobuf:"bytes,4,opt,name=network,proto3" json:"network,omitempty"`
+	// router if the router id
+	Router string `protobuf:"bytes,5,opt,name=router,proto3" json:"router,omitempty"`
 	// the network link
-	Link string `protobuf:"bytes,5,opt,name=link,proto3" json:"link,omitempty"`
+	Link string `protobuf:"bytes,6,opt,name=link,proto3" json:"link,omitempty"`
 	// the metric / score of this route
-	Metric               int64    `protobuf:"varint,6,opt,name=metric,proto3" json:"metric,omitempty"`
+	Metric               int64    `protobuf:"varint,7,opt,name=metric,proto3" json:"metric,omitempty"`
 	XXX_NoUnkeyedLiteral struct{} `json:"-"`
 	XXX_unrecognized     []byte   `json:"-"`
 	XXX_sizecache        int32    `json:"-"`
@@ -734,6 +736,13 @@ func (m *Route) GetNetwork() string {
 	return ""
 }
 
+func (m *Route) GetRouter() string {
+	if m != nil {
+		return m.Router
+	}
+	return ""
+}
+
 func (m *Route) GetLink() string {
 	if m != nil {
 		return m.Link
@@ -861,51 +870,51 @@ func init() {
 }
 
 var fileDescriptor_6a36eee0b1adf739 = []byte{
-	// 689 bytes of a gzipped FileDescriptorProto
+	// 699 bytes of a gzipped FileDescriptorProto
 	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x55, 0xcd, 0x4e, 0xdb, 0x40,
-	0x10, 0xb6, 0x93, 0xd8, 0x28, 0xd3, 0x10, 0xdc, 0x51, 0x05, 0x56, 0x5a, 0x20, 0xf2, 0x29, 0x42,
-	0xd4, 0xa9, 0xd2, 0x6b, 0xff, 0x02, 0xa5, 0xaa, 0x54, 0x0e, 0xad, 0x0b, 0xea, 0xd9, 0xd8, 0x23,
-	0x6a, 0x91, 0xd8, 0x66, 0x77, 0x03, 0xca, 0xb9, 0x8f, 0xd1, 0x27, 0xe8, 0x73, 0xf5, 0xda, 0x87,
-	0xa8, 0xbc, 0xbb, 0x0e, 0x21, 0xc6, 0x48, 0x70, 0xf2, 0xce, 0xdf, 0x37, 0xff, 0x63, 0x18, 0x4c,
-	0x93, 0x88, 0x65, 0xc3, 0xf3, 0xec, 0xa5, 0x7a, 0xb0, 0x6c, 0x26, 0x88, 0x0d, 0x73, 0x96, 0x89,
-	0x92, 0xf0, 0x25, 0x81, 0x1b, 0xe7, 0x99, 0x2f, 0x75, 0x7c, 0xc5, 0xf6, 0xda, 0xb0, 0x16, 0xd0,
-	0xe5, 0x8c, 0xb8, 0xf0, 0xde, 0x41, 0xe7, 0x38, 0xe1, 0x22, 0x20, 0x9e, 0x67, 0x29, 0x27, 0xf4,
-	0xc1, 0x96, 0x4a, 0xdc, 0x35, 0xfb, 0xcd, 0xc1, 0x93, 0xd1, 0xa6, 0xbf, 0x62, 0xec, 0x07, 0xc5,
-	0x27, 0xd0, 0x5a, 0xde, 0x5b, 0x58, 0x3f, 0xce, 0xb2, 0x8b, 0x59, 0xae, 0x01, 0x71, 0x1f, 0xac,
-	0xcb, 0x19, 0xb1, 0xb9, 0x6b, 0xf6, 0xcd, 0x3b, 0xed, 0xbf, 0x15, 0xd2, 0x40, 0x29, 0x79, 0x1f,
-	0xa0, 0x5b, 0x9a, 0x3f, 0x32, 0x80, 0x37, 0xd0, 0x51, 0x88, 0x8f, 0xf2, 0xff, 0x1e, 0xd6, 0xb5,
-	0xf5, 0x23, 0xdd, 0x77, 0xa1, 0xf3, 0x23, 0x14, 0xd1, 0xcf, 0xb2, 0x9e, 0x7f, 0x4c, 0xb0, 0xc7,
-	0xf1, 0x15, 0x31, 0x81, 0x5d, 0x68, 0x24, 0xb1, 0x0c, 0xa3, 0x1d, 0x34, 0x92, 0x18, 0x87, 0xd0,
-	0x12, 0xf3, 0x9c, 0xdc, 0x46, 0xdf, 0x1c, 0x74, 0x47, 0xcf, 0x2b, 0xc0, 0xca, 0xec, 0x64, 0x9e,
-	0x53, 0x20, 0x15, 0xf1, 0x05, 0xb4, 0x45, 0x32, 0x25, 0x2e, 0xc2, 0x69, 0xee, 0x36, 0xfb, 0xe6,
-	0xa0, 0x19, 0xdc, 0x30, 0xd0, 0x81, 0xa6, 0x10, 0x13, 0xb7, 0x25, 0xf9, 0xc5, 0xb3, 0x88, 0x9d,
-	0xae, 0x28, 0x15, 0xdc, 0xb5, 0x6a, 0x62, 0x3f, 0x2a, 0xc4, 0x81, 0xd6, 0xf2, 0x9e, 0xc2, 0xc6,
-	0x57, 0x96, 0x45, 0xc4, 0x79, 0x99, 0xbe, 0xe7, 0x40, 0xf7, 0x90, 0x51, 0x28, 0x68, 0x99, 0xf3,
-	0x91, 0x26, 0x74, 0x9b, 0x73, 0x9a, 0xc7, 0xcb, 0x3a, 0xbf, 0x4c, 0xb0, 0x24, 0x34, 0xfa, 0x3a,
-	0x47, 0x53, 0xe6, 0xd8, 0xbb, 0x3b, 0x80, 0xba, 0x14, 0x1b, 0xab, 0x29, 0xee, 0x83, 0x25, 0xed,
-	0x64, 0xf2, 0xf5, 0xbd, 0x50, 0x4a, 0xde, 0x29, 0x58, 0xb2, 0x97, 0xe8, 0xc2, 0x1a, 0x27, 0x76,
-	0x95, 0x44, 0xa4, 0xab, 0x5f, 0x92, 0x85, 0xe4, 0x3c, 0x14, 0x74, 0x1d, 0xce, 0xa5, 0xb3, 0x76,
-	0x50, 0x92, 0x85, 0x24, 0x25, 0x71, 0x9d, 0xb1, 0x0b, 0xe9, 0xac, 0x1d, 0x94, 0xa4, 0xf7, 0xdb,
-	0x04, 0x4b, 0xfa, 0xb9, 0x1f, 0x37, 0x8c, 0x63, 0x46, 0x9c, 0x97, 0xb8, 0x9a, 0x5c, 0xf6, 0xd8,
-	0xac, 0xf5, 0xd8, 0xba, 0xe5, 0x11, 0x11, 0x5a, 0x93, 0x24, 0xbd, 0x70, 0x2d, 0xc9, 0x96, 0x6f,
-	0xdc, 0x04, 0x7b, 0x4a, 0x82, 0x25, 0x91, 0x6b, 0xcb, 0x2a, 0x69, 0xca, 0x1b, 0x81, 0xfd, 0x5d,
-	0x84, 0x62, 0xc6, 0x0b, 0xab, 0x28, 0x8b, 0xcb, 0xd0, 0xe4, 0x1b, 0x9f, 0x81, 0x45, 0x8c, 0x65,
-	0x4c, 0x47, 0xa5, 0x08, 0x6f, 0x0c, 0x5d, 0x65, 0xb3, 0x98, 0xfa, 0x21, 0xd8, 0x5c, 0x72, 0xf4,
-	0xd6, 0x6c, 0x55, 0x2a, 0xad, 0x0d, 0xb4, 0xda, 0xde, 0x08, 0xe0, 0x66, 0x5c, 0x11, 0xa1, 0xab,
-	0xa8, 0x71, 0x9a, 0x66, 0xb3, 0x34, 0x22, 0xc7, 0x40, 0x07, 0x3a, 0x8a, 0xa7, 0x66, 0xc5, 0x31,
-	0xf7, 0x86, 0xd0, 0x5e, 0xb4, 0x1f, 0x01, 0x6c, 0x35, 0x68, 0x8e, 0x51, 0xbc, 0xd5, 0x88, 0x39,
-	0x66, 0xf1, 0xd6, 0x06, 0x8d, 0xd1, 0xbf, 0x06, 0xd8, 0xb2, 0xf2, 0x0c, 0xbf, 0x80, 0xad, 0xee,
-	0x04, 0xee, 0x54, 0x42, 0xbb, 0x75, 0x7f, 0x7a, 0xbb, 0xb5, 0x72, 0x3d, 0xac, 0x06, 0x1e, 0x80,
-	0x25, 0x77, 0x16, 0xb7, 0x2b, 0xba, 0xcb, 0xbb, 0xdc, 0xab, 0xd9, 0x1f, 0xcf, 0x78, 0x65, 0xe2,
-	0x01, 0xb4, 0x55, 0x7a, 0x09, 0x27, 0x74, 0xab, 0x83, 0xa9, 0x21, 0xb6, 0x6a, 0xb6, 0x5c, 0x62,
-	0x7c, 0x82, 0x35, 0xbd, 0x7f, 0x58, 0xa7, 0xd7, 0xeb, 0x57, 0x04, 0xab, 0x2b, 0x6b, 0xe0, 0xd1,
-	0x62, 0x06, 0xea, 0x03, 0xd9, 0xad, 0xeb, 0xe8, 0x02, 0x66, 0xf4, 0xb7, 0x01, 0xd6, 0x49, 0x78,
-	0x36, 0x21, 0x3c, 0x2c, 0x9b, 0x83, 0x35, 0x2b, 0x77, 0x07, 0xdc, 0xca, 0xd9, 0x30, 0x0a, 0x10,
-	0xd5, 0xd5, 0x07, 0x80, 0xac, 0x5c, 0x1a, 0x09, 0xa2, 0xc6, 0xe1, 0x01, 0x20, 0x2b, 0xc7, 0xc9,
-	0xc0, 0x31, 0xb4, 0x8a, 0x7f, 0xdc, 0x3d, 0xd5, 0xa9, 0x0e, 0xc2, 0xf2, 0x4f, 0xd1, 0x33, 0xf0,
-	0x73, 0x79, 0x5b, 0xb6, 0x6b, 0xfe, 0x27, 0x1a, 0x68, 0xa7, 0x4e, 0x5c, 0x22, 0x9d, 0xd9, 0xf2,
-	0x9f, 0xfc, 0xfa, 0x7f, 0x00, 0x00, 0x00, 0xff, 0xff, 0x8c, 0xd0, 0xc0, 0x27, 0xbf, 0x07, 0x00,
-	0x00,
+	0x10, 0xb6, 0x9d, 0xd8, 0x91, 0xa7, 0xc1, 0xb8, 0xa3, 0x0a, 0xac, 0xb4, 0x40, 0xe4, 0x53, 0x84,
+	0xa8, 0x53, 0xa5, 0xd7, 0xfe, 0x05, 0x4a, 0x55, 0xa9, 0x1c, 0x5a, 0x17, 0xd4, 0xb3, 0xb1, 0x57,
+	0xd4, 0x22, 0xb1, 0xcd, 0xee, 0x06, 0x94, 0x73, 0x9f, 0xa6, 0xe7, 0x3e, 0x52, 0xaf, 0x7d, 0x88,
+	0xca, 0xbb, 0xeb, 0x10, 0x62, 0x8c, 0x44, 0x4e, 0xde, 0x99, 0xf9, 0xe6, 0x9b, 0x99, 0xdd, 0x99,
+	0x31, 0x0c, 0xa6, 0x69, 0x4c, 0xf3, 0xe1, 0x45, 0xfe, 0x52, 0x1e, 0x68, 0x3e, 0xe3, 0x84, 0x0e,
+	0x0b, 0x9a, 0xf3, 0x4a, 0x08, 0x84, 0x80, 0x9b, 0x17, 0x79, 0x20, 0x30, 0x81, 0x54, 0xfb, 0x36,
+	0x74, 0x42, 0x72, 0x35, 0x23, 0x8c, 0xfb, 0xef, 0xa0, 0x7b, 0x92, 0x32, 0x1e, 0x12, 0x56, 0xe4,
+	0x19, 0x23, 0x18, 0x80, 0x25, 0x40, 0xcc, 0xd3, 0xfb, 0xad, 0xc1, 0x93, 0xd1, 0x56, 0xb0, 0xe2,
+	0x1c, 0x84, 0xe5, 0x27, 0x54, 0x28, 0xff, 0x2d, 0x6c, 0x9c, 0xe4, 0xf9, 0xe5, 0xac, 0x50, 0x84,
+	0x78, 0x00, 0xe6, 0xd5, 0x8c, 0xd0, 0xb9, 0xa7, 0xf7, 0xf5, 0x7b, 0xfd, 0xbf, 0x95, 0xd6, 0x50,
+	0x82, 0xfc, 0x0f, 0xe0, 0x54, 0xee, 0x6b, 0x26, 0xf0, 0x06, 0xba, 0x92, 0x71, 0xad, 0xf8, 0xef,
+	0x61, 0x43, 0x79, 0xaf, 0x19, 0xde, 0x81, 0xee, 0x8f, 0x88, 0xc7, 0x3f, 0xab, 0xfb, 0xfc, 0xad,
+	0x83, 0x35, 0x4e, 0xae, 0x09, 0xe5, 0xe8, 0x80, 0x91, 0x26, 0x22, 0x0d, 0x3b, 0x34, 0xd2, 0x04,
+	0x87, 0xd0, 0xe6, 0xf3, 0x82, 0x78, 0x46, 0x5f, 0x1f, 0x38, 0xa3, 0xe7, 0x35, 0x62, 0xe9, 0x76,
+	0x3a, 0x2f, 0x48, 0x28, 0x80, 0xf8, 0x02, 0x6c, 0x9e, 0x4e, 0x09, 0xe3, 0xd1, 0xb4, 0xf0, 0x5a,
+	0x7d, 0x7d, 0xd0, 0x0a, 0x6f, 0x15, 0xe8, 0x42, 0x8b, 0xf3, 0x89, 0xd7, 0x16, 0xfa, 0xf2, 0x58,
+	0xe6, 0x4e, 0xae, 0x49, 0xc6, 0x99, 0x67, 0x36, 0xe4, 0x7e, 0x5c, 0x9a, 0x43, 0x85, 0xf2, 0x9f,
+	0xc2, 0xe6, 0x57, 0x9a, 0xc7, 0x84, 0xb1, 0xaa, 0x7c, 0xdf, 0x05, 0xe7, 0x88, 0x92, 0x88, 0x93,
+	0x65, 0xcd, 0x47, 0x32, 0x21, 0x77, 0x35, 0x67, 0x45, 0xb2, 0x8c, 0xf9, 0xa5, 0x83, 0x29, 0xa8,
+	0x31, 0x50, 0x35, 0xea, 0xa2, 0xc6, 0xde, 0xfd, 0x09, 0x34, 0x95, 0x68, 0xac, 0x96, 0x78, 0x00,
+	0xa6, 0xf0, 0x13, 0xc5, 0x37, 0xbf, 0x85, 0x04, 0xf9, 0x67, 0x60, 0x8a, 0xb7, 0x44, 0x0f, 0x3a,
+	0x8c, 0xd0, 0xeb, 0x34, 0x26, 0xea, 0xf6, 0x2b, 0xb1, 0xb4, 0x5c, 0x44, 0x9c, 0xdc, 0x44, 0x73,
+	0x11, 0xcc, 0x0e, 0x2b, 0xb1, 0xb4, 0x64, 0x84, 0xdf, 0xe4, 0xf4, 0x52, 0x04, 0xb3, 0xc3, 0x4a,
+	0xf4, 0xff, 0xe8, 0x60, 0x8a, 0x38, 0x0f, 0xf3, 0x46, 0x49, 0x42, 0x09, 0x63, 0x15, 0xaf, 0x12,
+	0x97, 0x23, 0xb6, 0x1a, 0x23, 0xb6, 0xef, 0x44, 0xc4, 0x2d, 0xd5, 0x83, 0xd4, 0x33, 0x85, 0x41,
+	0x49, 0x88, 0xd0, 0x9e, 0xa4, 0xd9, 0xa5, 0x67, 0x09, 0xad, 0x38, 0x97, 0xd8, 0x29, 0xe1, 0x34,
+	0x8d, 0xbd, 0x8e, 0xb8, 0x3d, 0x25, 0xf9, 0x23, 0xb0, 0xbe, 0xf3, 0x88, 0xcf, 0x58, 0xe9, 0x15,
+	0xe7, 0x49, 0x95, 0xb2, 0x38, 0xe3, 0x33, 0x30, 0x09, 0xa5, 0x39, 0x55, 0xd9, 0x4a, 0xc1, 0x1f,
+	0x83, 0x23, 0x7d, 0x16, 0xd3, 0x30, 0x04, 0x8b, 0x09, 0x8d, 0x9a, 0xa6, 0xed, 0xda, 0x0b, 0x28,
+	0x07, 0x05, 0xdb, 0x1f, 0x01, 0xdc, 0xb6, 0x31, 0x22, 0x38, 0x52, 0x1a, 0x67, 0x59, 0x3e, 0xcb,
+	0x62, 0xe2, 0x6a, 0xe8, 0x42, 0x57, 0xea, 0x64, 0x0f, 0xb9, 0xfa, 0xfe, 0x10, 0xec, 0x45, 0x5b,
+	0x20, 0x80, 0x25, 0x1b, 0xd0, 0xd5, 0xca, 0xb3, 0x6c, 0x3d, 0x57, 0x2f, 0xcf, 0xca, 0xc1, 0x18,
+	0xfd, 0x33, 0xc0, 0x0a, 0xe5, 0x95, 0x7c, 0x01, 0x4b, 0xee, 0x0f, 0xdc, 0xad, 0xa5, 0x76, 0x67,
+	0x2f, 0xf5, 0xf6, 0x1a, 0xed, 0xaa, 0x89, 0x35, 0x3c, 0x04, 0x53, 0xcc, 0x32, 0xee, 0xd4, 0xb0,
+	0xcb, 0x33, 0xde, 0x6b, 0x98, 0x2b, 0x5f, 0x7b, 0xa5, 0xe3, 0x21, 0xd8, 0xb2, 0xbc, 0x94, 0x11,
+	0xf4, 0xea, 0x0d, 0xab, 0x28, 0xb6, 0x1b, 0xa6, 0x5f, 0x70, 0x7c, 0x82, 0x8e, 0x9a, 0x4b, 0x6c,
+	0xc2, 0xf5, 0xfa, 0x35, 0xc3, 0xea, 0x28, 0x6b, 0x78, 0xbc, 0xe8, 0x81, 0xe6, 0x44, 0xf6, 0x9a,
+	0x5e, 0x74, 0x41, 0x33, 0xfa, 0x6b, 0x80, 0x79, 0x1a, 0x9d, 0x4f, 0x08, 0x1e, 0x55, 0x8f, 0x83,
+	0x0d, 0xa3, 0x78, 0x0f, 0xdd, 0xca, 0x3a, 0xd1, 0x4a, 0x12, 0xf9, 0xaa, 0x8f, 0x20, 0x59, 0xd9,
+	0x40, 0x82, 0x44, 0xb6, 0xc3, 0x23, 0x48, 0x56, 0x96, 0x96, 0x86, 0x63, 0x68, 0x97, 0xff, 0xbe,
+	0x07, 0x6e, 0xa7, 0xde, 0x08, 0xcb, 0x3f, 0x4b, 0x5f, 0xc3, 0xcf, 0xd5, 0xce, 0xd9, 0x69, 0xf8,
+	0xcf, 0x28, 0xa2, 0xdd, 0x26, 0x73, 0xc5, 0x74, 0x6e, 0x89, 0x7f, 0xf5, 0xeb, 0xff, 0x01, 0x00,
+	0x00, 0xff, 0xff, 0xe2, 0xe9, 0xe2, 0x3b, 0xd7, 0x07, 0x00, 0x00,
 }
 
 // Reference imports to suppress errors if they are not otherwise used.
diff --git a/router/proto/router.proto b/router/proto/router.proto
index 3e18d456..9c2ebed4 100644
--- a/router/proto/router.proto
+++ b/router/proto/router.proto
@@ -7,6 +7,7 @@ service Router {
 	rpc Lookup(LookupRequest) returns (LookupResponse) {};
         rpc Watch(WatchRequest) returns (stream Event) {};
         rpc Advertise(Request) returns (stream Advert) {};
+        rpc Solicit(Request) returns (Response) {};
         rpc Process(Advert) returns (ProcessResponse) {};
 	rpc Status(Request) returns (StatusResponse) {};
 }
@@ -22,6 +23,9 @@ service Table {
 // Empty request
 message Request {}
 
+// Empty response
+message Response {}
+
 // ListResponse is returned by List
 message ListResponse {
 	repeated Route routes = 1;
@@ -37,10 +41,12 @@ message LookupResponse {
 	repeated Route routes = 1;
 }
 
+// QueryRequest queries Table for Routes
 message QueryRequest{
 	Query query = 1;
 }
 
+// QueryResponse is returned by Query
 message QueryResponse {
 	repeated Route routes = 1;
 }
@@ -117,10 +123,12 @@ message Route {
 	string gateway = 3;
 	// the network for this destination
 	string network = 4;
+	// router if the router id
+	string router = 5;
 	// the network link
-	string link = 5;
+	string link = 6;
 	// the metric / score of this route
-	int64 metric = 6;
+	int64 metric = 7;
 }
 
 message Status {
diff --git a/router/query.go b/router/query.go
index 68fb3588..3e2c3d38 100644
--- a/router/query.go
+++ b/router/query.go
@@ -11,29 +11,38 @@ type QueryOptions struct {
 	Gateway string
 	// Network is network address
 	Network string
+	// Router is router id
+	Router string
 }
 
-// QueryService sets destination address
+// QueryService sets service to query
 func QueryService(s string) QueryOption {
 	return func(o *QueryOptions) {
 		o.Service = s
 	}
 }
 
-// QueryGateway sets route gateway
+// QueryGateway sets gateway address to query
 func QueryGateway(g string) QueryOption {
 	return func(o *QueryOptions) {
 		o.Gateway = g
 	}
 }
 
-// QueryNetwork sets route network address
+// QueryNetwork sets network name to query
 func QueryNetwork(n string) QueryOption {
 	return func(o *QueryOptions) {
 		o.Network = n
 	}
 }
 
+// QueryRouter sets router id to query
+func QueryRouter(r string) QueryOption {
+	return func(o *QueryOptions) {
+		o.Router = r
+	}
+}
+
 // Query is routing table query
 type Query interface {
 	// Options returns query options
@@ -52,6 +61,7 @@ func NewQuery(opts ...QueryOption) Query {
 		Service: "*",
 		Gateway: "*",
 		Network: "*",
+		Router:  "*",
 	}
 
 	for _, o := range opts {
@@ -67,8 +77,3 @@ func NewQuery(opts ...QueryOption) Query {
 func (q *query) Options() QueryOptions {
 	return q.opts
 }
-
-// String prints routing table query in human readable form
-func (q query) String() string {
-	return "query"
-}
diff --git a/router/route.go b/router/route.go
index fa9dbc72..f2768403 100644
--- a/router/route.go
+++ b/router/route.go
@@ -7,9 +7,9 @@ import (
 var (
 	// DefaultLink is default network link
 	DefaultLink = "local"
-	// DefaultLocalMetric is default route cost metric for the local network
+	// DefaultLocalMetric is default route cost for a local route
 	DefaultLocalMetric = 1
-	// DefaultNetworkMetric is default route cost metric for the micro network
+	// DefaultNetworkMetric is default route cost for a network route
 	DefaultNetworkMetric = 10
 )
 
@@ -23,6 +23,8 @@ type Route struct {
 	Gateway string
 	// Network is network address
 	Network string
+	// Router is router id
+	Router string
 	// Link is network link
 	Link string
 	// Metric is the route cost metric
@@ -33,6 +35,6 @@ type Route struct {
 func (r *Route) Hash() uint64 {
 	h := fnv.New64()
 	h.Reset()
-	h.Write([]byte(r.Service + r.Address + r.Gateway + r.Network + r.Link))
+	h.Write([]byte(r.Service + r.Address + r.Gateway + r.Network + r.Router + r.Link))
 	return h.Sum64()
 }
diff --git a/router/router.go b/router/router.go
index 7d7ab0de..3e8f00cd 100644
--- a/router/router.go
+++ b/router/router.go
@@ -5,6 +5,17 @@ import (
 	"time"
 )
 
+var (
+	// DefaultAddress is default router address
+	DefaultAddress = ":9093"
+	// DefaultName is default router service name
+	DefaultName = "go.micro.router"
+	// DefaultNetwork is default micro network
+	DefaultNetwork = "go.micro"
+	// DefaultRouter is default network router
+	DefaultRouter = NewRouter()
+)
+
 // Router is an interface for a routing control plane
 type Router interface {
 	// Init initializes the router with options
@@ -17,6 +28,8 @@ type Router interface {
 	Advertise() (<-chan *Advert, error)
 	// Process processes incoming adverts
 	Process(*Advert) error
+	// Solicit advertises the whole routing table to the network
+	Solicit() error
 	// Lookup queries routes in the routing table
 	Lookup(Query) ([]Route, error)
 	// Watch returns a watcher which tracks updates to the routing table
@@ -31,16 +44,17 @@ type Router interface {
 	String() string
 }
 
+// Table is an interface for routing table
 type Table interface {
 	// Create new route in the routing table
 	Create(Route) error
-	// Delete deletes existing route from the routing table
+	// Delete existing route from the routing table
 	Delete(Route) error
-	// Update updates route in the routing table
+	// Update route in the routing table
 	Update(Route) error
-	// List returns the list of all routes in the table
+	// List all routes in the table
 	List() ([]Route, error)
-	// Query queries routes in the routing table
+	// Query routes in the routing table
 	Query(Query) ([]Route, error)
 }
 
@@ -125,17 +139,6 @@ type Advert struct {
 	Events []*Event
 }
 
-var (
-	// DefaultAddress is default router address
-	DefaultAddress = ":9093"
-	// DefaultName is default router service name
-	DefaultName = "go.micro.router"
-	// DefaultNetwork is default micro network
-	DefaultNetwork = "go.micro"
-	// DefaultRouter is default network router
-	DefaultRouter = NewRouter()
-)
-
 // NewRouter creates new Router and returns it
 func NewRouter(opts ...Option) Router {
 	return newRouter(opts...)
diff --git a/router/service/service.go b/router/service/service.go
index 77619762..f3063546 100644
--- a/router/service/service.go
+++ b/router/service/service.go
@@ -220,6 +220,42 @@ func (s *svc) Process(advert *router.Advert) error {
 	return nil
 }
 
+// Solicit advertise all routes
+func (s *svc) Solicit() error {
+	// list all the routes
+	routes, err := s.table.List()
+	if err != nil {
+		return err
+	}
+
+	// build events to advertise
+	events := make([]*router.Event, len(routes))
+	for i, _ := range events {
+		events[i] = &router.Event{
+			Type:      router.Update,
+			Timestamp: time.Now(),
+			Route:     routes[i],
+		}
+	}
+
+	advert := &router.Advert{
+		Id:        s.opts.Id,
+		Type:      router.RouteUpdate,
+		Timestamp: time.Now(),
+		TTL:       time.Duration(router.DefaultAdvertTTL),
+		Events:    events,
+	}
+
+	select {
+	case s.advertChan <- advert:
+	case <-s.exit:
+		close(s.advertChan)
+		return nil
+	}
+
+	return nil
+}
+
 // Status returns router status
 func (s *svc) Status() router.Status {
 	s.Lock()
diff --git a/router/table.go b/router/table.go
index 6b34bb00..c61b560c 100644
--- a/router/table.go
+++ b/router/table.go
@@ -8,7 +8,14 @@ import (
 	"github.com/google/uuid"
 )
 
-// table is an in memory routing table
+var (
+	// ErrRouteNotFound is returned when no route was found in the routing table
+	ErrRouteNotFound = errors.New("route not found")
+	// ErrDuplicateRoute is returned when the route already exists
+	ErrDuplicateRoute = errors.New("duplicate route")
+)
+
+// table is an in-memory routing table
 type table struct {
 	sync.RWMutex
 	// routes stores service routes
@@ -25,6 +32,19 @@ func newTable(opts ...Option) *table {
 	}
 }
 
+// sendEvent sends events to all subscribed watchers
+func (t *table) sendEvent(e *Event) {
+	t.RLock()
+	defer t.RUnlock()
+
+	for _, w := range t.watchers {
+		select {
+		case w.resChan <- e:
+		case <-w.done:
+		}
+	}
+}
+
 // Create creates new route in the routing table
 func (t *table) Create(r Route) error {
 	service := r.Service
@@ -63,8 +83,10 @@ func (t *table) Delete(r Route) error {
 		return ErrRouteNotFound
 	}
 
-	delete(t.routes[service], sum)
-	go t.sendEvent(&Event{Type: Delete, Timestamp: time.Now(), Route: r})
+	if _, ok := t.routes[service][sum]; ok {
+		delete(t.routes[service], sum)
+		go t.sendEvent(&Event{Type: Delete, Timestamp: time.Now(), Route: r})
+	}
 
 	return nil
 }
@@ -85,8 +107,10 @@ func (t *table) Update(r Route) error {
 		return nil
 	}
 
-	t.routes[service][sum] = r
-	go t.sendEvent(&Event{Type: Update, Timestamp: time.Now(), Route: r})
+	if _, ok := t.routes[service][sum]; !ok {
+		t.routes[service][sum] = r
+		go t.sendEvent(&Event{Type: Update, Timestamp: time.Now(), Route: r})
+	}
 
 	return nil
 }
@@ -106,21 +130,23 @@ func (t *table) List() ([]Route, error) {
 	return routes, nil
 }
 
-// isMatch checks if the route matches given network and router
-func isMatch(route Route, network, router string) bool {
-	if network == "*" || network == route.Network {
-		if router == "*" || router == route.Gateway {
-			return true
+// isMatch checks if the route matches given query options
+func isMatch(route Route, gateway, network, router string) bool {
+	if gateway == "*" || gateway == route.Gateway {
+		if network == "*" || network == route.Network {
+			if router == "*" || router == route.Router {
+				return true
+			}
 		}
 	}
 	return false
 }
 
 // findRoutes finds all the routes for given network and router and returns them
-func findRoutes(routes map[uint64]Route, network, router string) []Route {
+func findRoutes(routes map[uint64]Route, gateway, network, router string) []Route {
 	var results []Route
 	for _, route := range routes {
-		if isMatch(route, network, router) {
+		if isMatch(route, gateway, network, router) {
 			results = append(results, route)
 		}
 	}
@@ -136,13 +162,13 @@ func (t *table) Query(q Query) ([]Route, error) {
 		if _, ok := t.routes[q.Options().Service]; !ok {
 			return nil, ErrRouteNotFound
 		}
-		return findRoutes(t.routes[q.Options().Service], q.Options().Network, q.Options().Gateway), nil
+		return findRoutes(t.routes[q.Options().Service], q.Options().Gateway, q.Options().Network, q.Options().Router), nil
 	}
 
 	var results []Route
 	// search through all destinations
 	for _, routes := range t.routes {
-		results = append(results, findRoutes(routes, q.Options().Network, q.Options().Gateway)...)
+		results = append(results, findRoutes(routes, q.Options().Gateway, q.Options().Network, q.Options().Router)...)
 	}
 
 	return results, nil
@@ -181,23 +207,3 @@ func (t *table) Watch(opts ...WatchOption) (Watcher, error) {
 
 	return w, nil
 }
-
-// sendEvent sends events to all subscribed watchers
-func (t *table) sendEvent(e *Event) {
-	t.RLock()
-	defer t.RUnlock()
-
-	for _, w := range t.watchers {
-		select {
-		case w.resChan <- e:
-		case <-w.done:
-		}
-	}
-}
-
-var (
-	// ErrRouteNotFound is returned when no route was found in the routing table
-	ErrRouteNotFound = errors.New("route not found")
-	// ErrDuplicateRoute is returned when the route already exists
-	ErrDuplicateRoute = errors.New("duplicate route")
-)
diff --git a/router/table_test.go b/router/table_test.go
index 16c73f2f..d989ee3b 100644
--- a/router/table_test.go
+++ b/router/table_test.go
@@ -9,6 +9,7 @@ func testSetup() (*table, Route) {
 		Service: "dest.svc",
 		Gateway: "dest.gw",
 		Network: "dest.network",
+		Router:  "src.router",
 		Link:    "det.link",
 		Metric:  10,
 	}
@@ -109,11 +110,13 @@ func TestQuery(t *testing.T) {
 	svc := []string{"svc1", "svc2", "svc3"}
 	net := []string{"net1", "net2", "net1"}
 	gw := []string{"gw1", "gw2", "gw3"}
+	rtr := []string{"rtr1", "rt2", "rt3"}
 
 	for i := 0; i < len(svc); i++ {
 		route.Service = svc[i]
 		route.Network = net[i]
 		route.Gateway = gw[i]
+		route.Router = rtr[i]
 		if err := table.Create(route); err != nil {
 			t.Errorf("error adding route: %s", err)
 		}
@@ -127,8 +130,9 @@ func TestQuery(t *testing.T) {
 		t.Errorf("error looking up routes: %s", err)
 	}
 
-	// query particular net
-	query = NewQuery(QueryNetwork("net1"))
+	// query routes particular network
+	network := "net1"
+	query = NewQuery(QueryNetwork(network))
 
 	routes, err = table.Query(query)
 	if err != nil {
@@ -139,7 +143,13 @@ func TestQuery(t *testing.T) {
 		t.Errorf("incorrect number of routes returned. Expected: %d, found: %d", 2, len(routes))
 	}
 
-	// query particular gateway
+	for _, route := range routes {
+		if route.Network != network {
+			t.Errorf("incorrect route returned. Expected network: %s, found: %s", network, route.Network)
+		}
+	}
+
+	// query routes for particular gateway
 	gateway := "gw1"
 	query = NewQuery(QueryGateway(gateway))
 
@@ -156,11 +166,28 @@ func TestQuery(t *testing.T) {
 		t.Errorf("incorrect route returned. Expected gateway: %s, found: %s", gateway, routes[0].Gateway)
 	}
 
-	// query particular route
-	network := "net1"
+	// query routes for particular router
+	router := "rtr1"
+	query = NewQuery(QueryRouter(router))
+
+	routes, err = table.Query(query)
+	if err != nil {
+		t.Errorf("error looking up routes: %s", err)
+	}
+
+	if len(routes) != 1 {
+		t.Errorf("incorrect number of routes returned. Expected: %d, found: %d", 1, len(routes))
+	}
+
+	if routes[0].Router != router {
+		t.Errorf("incorrect route returned. Expected router: %s, found: %s", router, routes[0].Router)
+	}
+
+	// query particular gateway and network
 	query = NewQuery(
 		QueryGateway(gateway),
 		QueryNetwork(network),
+		QueryRouter(router),
 	)
 
 	routes, err = table.Query(query)
@@ -180,7 +207,11 @@ func TestQuery(t *testing.T) {
 		t.Errorf("incorrect network returned. Expected network: %s, found: %s", network, routes[0].Network)
 	}
 
-	// bullshit route query
+	if routes[0].Router != router {
+		t.Errorf("incorrect route returned. Expected router: %s, found: %s", router, routes[0].Router)
+	}
+
+	// non-existen route query
 	query = NewQuery(QueryService("foobar"))
 
 	routes, err = table.Query(query)
diff --git a/router/watcher.go b/router/watcher.go
index f2c8beac..148b88d4 100644
--- a/router/watcher.go
+++ b/router/watcher.go
@@ -6,6 +6,11 @@ import (
 	"time"
 )
 
+var (
+	// ErrWatcherStopped is returned when routing table watcher has been stopped
+	ErrWatcherStopped = errors.New("watcher stopped")
+)
+
 // EventType defines routing table event
 type EventType int
 
@@ -42,9 +47,6 @@ type Event struct {
 	Route Route
 }
 
-// WatchOption is used to define what routes to watch in the table
-type WatchOption func(*WatchOptions)
-
 // Watcher defines routing table watcher interface
 // Watcher returns updates to the routing table
 type Watcher interface {
@@ -56,7 +58,11 @@ type Watcher interface {
 	Stop()
 }
 
+// WatchOption is used to define what routes to watch in the table
+type WatchOption func(*WatchOptions)
+
 // WatchOptions are table watcher options
+// TODO: expand the options to watch based on other criteria
 type WatchOptions struct {
 	// Service allows to watch specific service routes
 	Service string
@@ -70,6 +76,7 @@ func WatchService(s string) WatchOption {
 	}
 }
 
+// tableWatcher implements routing table Watcher
 type tableWatcher struct {
 	sync.RWMutex
 	id      string
@@ -113,8 +120,3 @@ func (w *tableWatcher) Stop() {
 		close(w.done)
 	}
 }
-
-var (
-	// ErrWatcherStopped is returned when routing table watcher has been stopped
-	ErrWatcherStopped = errors.New("watcher stopped")
-)
diff --git a/service.go b/service.go
index e612a4b3..eed7f8ac 100644
--- a/service.go
+++ b/service.go
@@ -3,6 +3,7 @@ package micro
 import (
 	"os"
 	"os/signal"
+	"strings"
 	"sync"
 	"syscall"
 
@@ -10,7 +11,9 @@ import (
 	"github.com/micro/go-micro/config/cmd"
 	"github.com/micro/go-micro/debug/handler"
 	"github.com/micro/go-micro/metadata"
+	"github.com/micro/go-micro/plugin"
 	"github.com/micro/go-micro/server"
+	"github.com/micro/go-micro/util/log"
 )
 
 type service struct {
@@ -44,6 +47,24 @@ func (s *service) Init(opts ...Option) {
 	}
 
 	s.once.Do(func() {
+		// setup the plugins
+		for _, p := range strings.Split(os.Getenv("MICRO_PLUGIN"), ",") {
+			if len(p) == 0 {
+				continue
+			}
+
+			// load the plugin
+			c, err := plugin.Load(p)
+			if err != nil {
+				log.Fatal(err)
+			}
+
+			// initialise the plugin
+			if err := plugin.Init(c); err != nil {
+				log.Fatal(err)
+			}
+		}
+
 		// Initialise the command flags, overriding new service
 		_ = s.opts.Cmd.Init(
 			cmd.Broker(&s.opts.Broker),
diff --git a/tunnel/broker/broker.go b/tunnel/broker/broker.go
index 0a33c610..6778dfaa 100644
--- a/tunnel/broker/broker.go
+++ b/tunnel/broker/broker.go
@@ -58,7 +58,7 @@ func (t *tunBroker) Disconnect() error {
 func (t *tunBroker) Publish(topic string, m *broker.Message, opts ...broker.PublishOption) error {
 	// TODO: this is probably inefficient, we might want to just maintain an open connection
 	// it may be easier to add broadcast to the tunnel
-	c, err := t.tunnel.Dial(topic)
+	c, err := t.tunnel.Dial(topic, tunnel.DialMulticast())
 	if err != nil {
 		return err
 	}
diff --git a/tunnel/default.go b/tunnel/default.go
index 69a5d878..7d265544 100644
--- a/tunnel/default.go
+++ b/tunnel/default.go
@@ -1,9 +1,9 @@
 package tunnel
 
 import (
-	"crypto/sha256"
 	"errors"
-	"fmt"
+	"math/rand"
+	"strings"
 	"sync"
 	"time"
 
@@ -13,6 +13,8 @@ import (
 )
 
 var (
+	// DiscoverTime sets the time at which we fire discover messages
+	DiscoverTime = 60 * time.Second
 	// KeepAliveTime defines time interval we send keepalive messages to outbound links
 	KeepAliveTime = 30 * time.Second
 	// ReconnectTime defines time interval we periodically attempt to reconnect dead links
@@ -25,7 +27,10 @@ type tun struct {
 
 	sync.RWMutex
 
-	// tunnel token
+	// the unique id for this tunnel
+	id string
+
+	// tunnel token for authentication
 	token string
 
 	// to indicate if we're connected or not
@@ -37,8 +42,8 @@ type tun struct {
 	// close channel
 	closed chan bool
 
-	// a map of sockets based on Micro-Tunnel-Id
-	sockets map[string]*socket
+	// a map of sessions based on Micro-Tunnel-Channel
+	sessions map[string]*session
 
 	// outbound links
 	links map[string]*link
@@ -47,13 +52,6 @@ type tun struct {
 	listener transport.Listener
 }
 
-type link struct {
-	transport.Socket
-	id            string
-	loopback      bool
-	lastKeepAlive time.Time
-}
-
 // create new tunnel on top of a link
 func newTunnel(opts ...Option) *tun {
 	options := DefaultOptions()
@@ -62,12 +60,13 @@ func newTunnel(opts ...Option) *tun {
 	}
 
 	return &tun{
-		options: options,
-		token:   uuid.New().String(),
-		send:    make(chan *message, 128),
-		closed:  make(chan bool),
-		sockets: make(map[string]*socket),
-		links:   make(map[string]*link),
+		options:  options,
+		id:       options.Id,
+		token:    options.Token,
+		send:     make(chan *message, 128),
+		closed:   make(chan bool),
+		sessions: make(map[string]*session),
+		links:    make(map[string]*link),
 	}
 }
 
@@ -81,54 +80,118 @@ func (t *tun) Init(opts ...Option) error {
 	return nil
 }
 
-// getSocket returns a socket from the internal socket map.
-// It does this based on the Micro-Tunnel-Id and Micro-Tunnel-Session
-func (t *tun) getSocket(id, session string) (*socket, bool) {
-	// get the socket
+// getSession returns a session from the internal session map.
+// It does this based on the Micro-Tunnel-Channel and Micro-Tunnel-Session
+func (t *tun) getSession(channel, session string) (*session, bool) {
+	// get the session
 	t.RLock()
-	s, ok := t.sockets[id+session]
+	s, ok := t.sessions[channel+session]
 	t.RUnlock()
 	return s, ok
 }
 
-// newSocket creates a new socket and saves it
-func (t *tun) newSocket(id, session string) (*socket, bool) {
-	// hash the id
-	h := sha256.New()
-	h.Write([]byte(id))
-	id = fmt.Sprintf("%x", h.Sum(nil))
+func (t *tun) delSession(channel, session string) {
+	t.Lock()
+	delete(t.sessions, channel+session)
+	t.Unlock()
+}
 
-	// new socket
-	s := &socket{
-		id:      id,
-		session: session,
+// listChannels returns a list of listening channels
+func (t *tun) listChannels() []string {
+	t.RLock()
+	defer t.RUnlock()
+
+	var channels []string
+	for _, session := range t.sessions {
+		if session.session != "listener" {
+			continue
+		}
+		channels = append(channels, session.channel)
+	}
+	return channels
+}
+
+// newSession creates a new session and saves it
+func (t *tun) newSession(channel, sessionId string) (*session, bool) {
+	// new session
+	s := &session{
+		tunnel:  t.id,
+		channel: channel,
+		session: sessionId,
 		closed:  make(chan bool),
 		recv:    make(chan *message, 128),
 		send:    t.send,
 		wait:    make(chan bool),
+		errChan: make(chan error, 1),
 	}
 
-	// save socket
+	// save session
 	t.Lock()
-	_, ok := t.sockets[id+session]
+	_, ok := t.sessions[channel+sessionId]
 	if ok {
-		// socket already exists
+		// session already exists
 		t.Unlock()
 		return nil, false
 	}
 
-	t.sockets[id+session] = s
+	t.sessions[channel+sessionId] = s
 	t.Unlock()
 
-	// return socket
+	// return session
 	return s, true
 }
 
 // TODO: use tunnel id as part of the session
-func (t *tun) newSession() string {
+func (t *tun) newSessionId() string {
 	return uuid.New().String()
 }
 
+func (t *tun) announce(channel, session string, link *link) {
+	// create the "announce" response message for a discover request
+	msg := &transport.Message{
+		Header: map[string]string{
+			"Micro-Tunnel":         "announce",
+			"Micro-Tunnel-Id":      t.id,
+			"Micro-Tunnel-Channel": channel,
+			"Micro-Tunnel-Session": session,
+			"Micro-Tunnel-Link":    link.id,
+			"Micro-Tunnel-Token":   t.token,
+		},
+	}
+
+	// if no channel is present we've been asked to discover all channels
+	if len(channel) == 0 {
+		// get the list of channels
+		t.RLock()
+		channels := t.listChannels()
+		t.RUnlock()
+
+		// if there are no channels continue
+		if len(channels) == 0 {
+			return
+		}
+
+		// create a list of channels as comma separated list
+		channel = strings.Join(channels, ",")
+		// set channels as header
+		msg.Header["Micro-Tunnel-Channel"] = channel
+	} else {
+		// otherwise look for a single channel mapping
+		// looking for existing mapping as a listener
+		_, exists := t.getSession(channel, "listener")
+		if !exists {
+			return
+		}
+	}
+
+	log.Debugf("Tunnel sending announce for discovery of channel(s) %s", channel)
+
+	// send back the announcement
+	if err := link.Send(msg); err != nil {
+		log.Debugf("Tunnel failed to send announcement for channel(s) %s message: %v", channel, err)
+	}
+}
+
 // monitor monitors outbound links and attempts to reconnect to the failed ones
 func (t *tun) monitor() {
 	reconnect := time.NewTicker(ReconnectTime)
@@ -139,44 +202,63 @@ func (t *tun) monitor() {
 		case <-t.closed:
 			return
 		case <-reconnect.C:
+			var connect []string
+
+			// build list of unknown nodes to connect to
+			t.RLock()
 			for _, node := range t.options.Nodes {
-				t.Lock()
 				if _, ok := t.links[node]; !ok {
-					link, err := t.setupLink(node)
-					if err != nil {
-						log.Debugf("Tunnel failed to setup node link to %s: %v", node, err)
-						t.Unlock()
-						continue
-					}
-					t.links[node] = link
+					connect = append(connect, node)
 				}
+			}
+			t.RUnlock()
+
+			for _, node := range connect {
+				// create new link
+				link, err := t.setupLink(node)
+				if err != nil {
+					log.Debugf("Tunnel failed to setup node link to %s: %v", node, err)
+					continue
+				}
+				// set the link id to the node
+				// TODO: hash it
+				link.id = node
+				// save the link
+				t.Lock()
+				t.links[node] = link
 				t.Unlock()
 			}
 		}
 	}
 }
 
-// process outgoing messages sent by all local sockets
+// process outgoing messages sent by all local sessions
 func (t *tun) process() {
 	// manage the send buffer
-	// all pseudo sockets throw everything down this
+	// all pseudo sessions throw everything down this
 	for {
 		select {
 		case msg := <-t.send:
 			newMsg := &transport.Message{
 				Header: make(map[string]string),
-				Body:   msg.data.Body,
 			}
 
-			for k, v := range msg.data.Header {
-				newMsg.Header[k] = v
+			// set the data
+			if msg.data != nil {
+				for k, v := range msg.data.Header {
+					newMsg.Header[k] = v
+				}
+				newMsg.Body = msg.data.Body
 			}
 
 			// set message head
-			newMsg.Header["Micro-Tunnel"] = "message"
+			newMsg.Header["Micro-Tunnel"] = msg.typ
 
 			// set the tunnel id on the outgoing message
-			newMsg.Header["Micro-Tunnel-Id"] = msg.id
+			newMsg.Header["Micro-Tunnel-Id"] = msg.tunnel
+
+			// set the tunnel channel on the outgoing message
+			newMsg.Header["Micro-Tunnel-Channel"] = msg.channel
 
 			// set the session id
 			newMsg.Header["Micro-Tunnel-Session"] = msg.session
@@ -186,145 +268,355 @@ func (t *tun) process() {
 
 			// send the message via the interface
 			t.Lock()
+
 			if len(t.links) == 0 {
-				log.Debugf("No links to send to")
+				log.Debugf("No links to send message type: %s channel: %s", msg.typ, msg.channel)
 			}
+
+			var sent bool
+			var err error
+
 			for node, link := range t.links {
-				if link.loopback && msg.outbound {
+				// if the link is not connected skip it
+				if !link.connected {
+					log.Debugf("Link for node %s not connected", node)
+					err = errors.New("link not connected")
 					continue
 				}
+
+				// if the link was a loopback accepted connection
+				// and the message is being sent outbound via
+				// a dialled connection don't use this link
+				if link.loopback && msg.outbound {
+					err = errors.New("link is loopback")
+					continue
+				}
+
+				// if the message was being returned by the loopback listener
+				// send it back up the loopback link only
+				if msg.loopback && !link.loopback {
+					err = errors.New("link is not loopback")
+					continue
+				}
+
+				// check the multicast mappings
+				if msg.multicast {
+					link.RLock()
+					_, ok := link.channels[msg.channel]
+					link.RUnlock()
+					// channel mapping not found in link
+					if !ok {
+						continue
+					}
+				} else {
+					// if we're picking the link check the id
+					// this is where we explicitly set the link
+					// in a message received via the listen method
+					if len(msg.link) > 0 && link.id != msg.link {
+						err = errors.New("link not found")
+						continue
+					}
+				}
+
+				// send the message via the current link
 				log.Debugf("Sending %+v to %s", newMsg, node)
-				if err := link.Send(newMsg); err != nil {
-					log.Debugf("Tunnel error sending %+v to %s: %v", newMsg, node, err)
+
+				if errr := link.Send(newMsg); errr != nil {
+					log.Debugf("Tunnel error sending %+v to %s: %v", newMsg, node, errr)
+					err = errors.New(errr.Error())
+					// kill the link
+					link.Close()
+					// delete the link
 					delete(t.links, node)
 					continue
 				}
+
+				// is sent
+				sent = true
+
+				// keep sending broadcast messages
+				if msg.broadcast || msg.multicast {
+					continue
+				}
+
+				// break on unicast
+				break
 			}
+
 			t.Unlock()
+
+			// set the error if not sent
+			var gerr error
+			if !sent {
+				gerr = err
+			}
+
+			// skip if its not been set
+			if msg.errChan == nil {
+				continue
+			}
+
+			// return error non blocking
+			select {
+			case msg.errChan <- gerr:
+			default:
+			}
 		case <-t.closed:
 			return
 		}
 	}
 }
 
+func (t *tun) delLink(id string) {
+	t.Lock()
+	defer t.Unlock()
+	// get the link
+	link, ok := t.links[id]
+	if !ok {
+		return
+	}
+	// close and delete
+	link.Close()
+	delete(t.links, id)
+}
+
 // process incoming messages
 func (t *tun) listen(link *link) {
+	// remove the link on exit
+	defer func() {
+		log.Debugf("Tunnel deleting connection from %s", link.Remote())
+		t.delLink(link.Remote())
+	}()
+
+	// let us know if its a loopback
+	var loopback bool
+
 	for {
 		// process anything via the net interface
 		msg := new(transport.Message)
-		err := link.Recv(msg)
-		if err != nil {
+		if err := link.Recv(msg); err != nil {
 			log.Debugf("Tunnel link %s receive error: %#v", link.Remote(), err)
-			t.Lock()
-			delete(t.links, link.Remote())
-			t.Unlock()
 			return
 		}
 
-		switch msg.Header["Micro-Tunnel"] {
+		// always ensure we have the correct auth token
+		// TODO: segment the tunnel based on token
+		// e.g use it as the basis
+		token := msg.Header["Micro-Tunnel-Token"]
+		if token != t.token {
+			log.Debugf("Tunnel link %s received invalid token %s", token)
+			return
+		}
+
+		// message type
+		mtype := msg.Header["Micro-Tunnel"]
+		// the tunnel id
+		id := msg.Header["Micro-Tunnel-Id"]
+		// the tunnel channel
+		channel := msg.Header["Micro-Tunnel-Channel"]
+		// the session id
+		sessionId := msg.Header["Micro-Tunnel-Session"]
+
+		// if its not connected throw away the link
+		// the first message we process needs to be connect
+		if !link.connected && mtype != "connect" {
+			log.Debugf("Tunnel link %s not connected", link.id)
+			return
+		}
+
+		switch mtype {
 		case "connect":
 			log.Debugf("Tunnel link %s received connect message", link.Remote())
-			// check the Micro-Tunnel-Token
-			token, ok := msg.Header["Micro-Tunnel-Token"]
-			if !ok {
-				continue
-			}
 
+			link.Lock()
 			// are we connecting to ourselves?
-			if token == t.token {
-				t.Lock()
+			if id == t.id {
 				link.loopback = true
-				t.Unlock()
+				loopback = true
 			}
 
+			// set to remote node
+			link.id = id
+			// set as connected
+			link.connected = true
+			link.Unlock()
+
+			// save the link once connected
+			t.Lock()
+			t.links[link.Remote()] = link
+			t.Unlock()
+
+			// send back a discovery
+			go t.announce("", "", link)
 			// nothing more to do
 			continue
 		case "close":
-			log.Debugf("Tunnel link %s closing connection", link.Remote())
 			// TODO: handle the close message
 			// maybe report io.EOF or kill the link
-			continue
+
+			// close the link entirely
+			if len(channel) == 0 {
+				log.Debugf("Tunnel link %s received close message", link.Remote())
+				return
+			}
+
+			// the entire listener was closed so remove it from the mapping
+			if sessionId == "listener" {
+				link.Lock()
+				delete(link.channels, channel)
+				link.Unlock()
+				continue
+			}
+
+			// try get the dialing socket
+			s, exists := t.getSession(channel, sessionId)
+			if exists {
+				// close and continue
+				s.Close()
+				continue
+			}
+			// otherwise its a session mapping of sorts
 		case "keepalive":
 			log.Debugf("Tunnel link %s received keepalive", link.Remote())
 			t.Lock()
+			// save the keepalive
 			link.lastKeepAlive = time.Now()
 			t.Unlock()
 			continue
-		case "message":
+		// a new connection dialled outbound
+		case "open":
+			log.Debugf("Tunnel link %s received open %s %s", link.id, channel, sessionId)
+			// we just let it pass through to be processed
+		// an accept returned by the listener
+		case "accept":
+			s, exists := t.getSession(channel, sessionId)
+			// we don't need this
+			if exists && s.multicast {
+				s.accepted = true
+				continue
+			}
+			if exists && s.accepted {
+				continue
+			}
+		// a continued session
+		case "session":
 			// process message
 			log.Debugf("Received %+v from %s", msg, link.Remote())
+		// an announcement of a channel listener
+		case "announce":
+			// process the announcement
+			channels := strings.Split(channel, ",")
+
+			// update mapping in the link
+			link.Lock()
+			for _, channel := range channels {
+				link.channels[channel] = time.Now()
+			}
+			link.Unlock()
+
+			// this was an announcement not intended for anything
+			if sessionId == "listener" || sessionId == "" {
+				continue
+			}
+
+			// get the session that asked for the discovery
+			s, exists := t.getSession(channel, sessionId)
+			if exists {
+				// don't bother it's already discovered
+				if s.discovered {
+					continue
+				}
+
+				// send the announce back to the caller
+				s.recv <- &message{
+					typ:     "announce",
+					tunnel:  id,
+					channel: channel,
+					session: sessionId,
+					link:    link.id,
+				}
+			}
+			continue
+		case "discover":
+			// send back an announcement
+			go t.announce(channel, sessionId, link)
+			continue
 		default:
 			// blackhole it
 			continue
 		}
 
-		// strip message header
-		delete(msg.Header, "Micro-Tunnel")
-
-		// the tunnel id
-		id := msg.Header["Micro-Tunnel-Id"]
-		delete(msg.Header, "Micro-Tunnel-Id")
-
-		// the session id
-		session := msg.Header["Micro-Tunnel-Session"]
-		delete(msg.Header, "Micro-Tunnel-Session")
-
-		// strip token header
-		delete(msg.Header, "Micro-Tunnel-Token")
+		// strip tunnel message header
+		for k, _ := range msg.Header {
+			if strings.HasPrefix(k, "Micro-Tunnel") {
+				delete(msg.Header, k)
+			}
+		}
 
 		// if the session id is blank there's nothing we can do
 		// TODO: check this is the case, is there any reason
 		// why we'd have a blank session? Is the tunnel
 		// used for some other purpose?
-		if len(id) == 0 || len(session) == 0 {
+		if len(channel) == 0 || len(sessionId) == 0 {
 			continue
 		}
 
-		var s *socket
+		var s *session
 		var exists bool
 
+		// If its a loopback connection then we've enabled link direction
+		// listening side is used for listening, the dialling side for dialling
 		switch {
-		case link.loopback:
-			s, exists = t.getSocket(id, "listener")
+		case loopback, mtype == "open":
+			s, exists = t.getSession(channel, "listener")
+		// only return accept to the session
+		case mtype == "accept":
+			log.Debugf("Received accept message for %s %s", channel, sessionId)
+			s, exists = t.getSession(channel, sessionId)
+			if exists && s.accepted {
+				continue
+			}
 		default:
-			// get the socket based on the tunnel id and session
+			// get the session based on the tunnel id and session
 			// this could be something we dialed in which case
 			// we have a session for it otherwise its a listener
-			s, exists = t.getSocket(id, session)
+			s, exists = t.getSession(channel, sessionId)
 			if !exists {
 				// try get it based on just the tunnel id
 				// the assumption here is that a listener
 				// has no session but its set a listener session
-				s, exists = t.getSocket(id, "listener")
+				s, exists = t.getSession(channel, "listener")
 			}
 		}
-		// bail if no socket has been found
+
+		// bail if no session or listener has been found
 		if !exists {
-			log.Debugf("Tunnel skipping no socket exists")
+			log.Debugf("Tunnel skipping no session %s %s exists", channel, sessionId)
 			// drop it, we don't care about
 			// messages we don't know about
 			continue
 		}
-		log.Debugf("Tunnel using socket %s %s", s.id, s.session)
 
-		// is the socket closed?
+		// is the session closed?
 		select {
 		case <-s.closed:
 			// closed
-			delete(t.sockets, id)
+			delete(t.sessions, channel)
 			continue
 		default:
 			// process
 		}
 
-		// is the socket new?
+		log.Debugf("Tunnel using channel %s session %s", s.channel, s.session)
+
+		// is the session new?
 		select {
-		// if its new the socket is actually blocked waiting
+		// if its new the session is actually blocked waiting
 		// for a connection. so we check if its waiting.
 		case <-s.wait:
 		// if its waiting e.g its new then we close it
 		default:
-			// set remote address of the socket
+			// set remote address of the session
 			s.remote = msg.Header["Remote"]
 			close(s.wait)
 		}
@@ -337,9 +629,14 @@ func (t *tun) listen(link *link) {
 
 		// construct the internal message
 		imsg := &message{
-			id:      id,
-			session: session,
-			data:    tmsg,
+			tunnel:   id,
+			typ:      mtype,
+			channel:  channel,
+			session:  sessionId,
+			data:     tmsg,
+			link:     link.id,
+			loopback: loopback,
+			errChan:  make(chan error, 1),
 		}
 
 		// append to recv backlog
@@ -351,6 +648,30 @@ func (t *tun) listen(link *link) {
 	}
 }
 
+// discover sends channel discover requests periodically
+func (t *tun) discover(link *link) {
+	tick := time.NewTicker(DiscoverTime)
+	defer tick.Stop()
+
+	for {
+		select {
+		case <-tick.C:
+			// send a discovery message to all links
+			if err := link.Send(&transport.Message{
+				Header: map[string]string{
+					"Micro-Tunnel":       "discover",
+					"Micro-Tunnel-Id":    t.id,
+					"Micro-Tunnel-Token": t.token,
+				},
+			}); err != nil {
+				log.Debugf("Tunnel failed to send discover to link %s: %v", link.id, err)
+			}
+		case <-t.closed:
+			return
+		}
+	}
+}
+
 // keepalive periodically sends keepalive messages to link
 func (t *tun) keepalive(link *link) {
 	keepalive := time.NewTicker(KeepAliveTime)
@@ -366,13 +687,12 @@ func (t *tun) keepalive(link *link) {
 			if err := link.Send(&transport.Message{
 				Header: map[string]string{
 					"Micro-Tunnel":       "keepalive",
+					"Micro-Tunnel-Id":    t.id,
 					"Micro-Tunnel-Token": t.token,
 				},
 			}); err != nil {
 				log.Debugf("Error sending keepalive to link %v: %v", link.Remote(), err)
-				t.Lock()
-				delete(t.links, link.Remote())
-				t.Unlock()
+				t.delLink(link.Remote())
 				return
 			}
 		}
@@ -382,7 +702,7 @@ func (t *tun) keepalive(link *link) {
 // setupLink connects to node and returns link if successful
 // It returns error if the link failed to be established
 func (t *tun) setupLink(node string) (*link, error) {
-	log.Debugf("Tunnel dialing %s", node)
+	log.Debugf("Tunnel setting up link: %s", node)
 	c, err := t.options.Transport.Dial(node)
 	if err != nil {
 		log.Debugf("Tunnel failed to connect to %s: %v", node, err)
@@ -390,22 +710,24 @@ func (t *tun) setupLink(node string) (*link, error) {
 	}
 	log.Debugf("Tunnel connected to %s", node)
 
+	// send the first connect message
 	if err := c.Send(&transport.Message{
 		Header: map[string]string{
 			"Micro-Tunnel":       "connect",
+			"Micro-Tunnel-Id":    t.id,
 			"Micro-Tunnel-Token": t.token,
 		},
 	}); err != nil {
 		return nil, err
 	}
 
-	// save the link
-	id := uuid.New().String()
-	link := &link{
-		Socket: c,
-		id:     id,
-	}
-	t.links[node] = link
+	// create a new link
+	link := newLink(c)
+	// set link id to remote side
+	link.id = c.Remote()
+	// we made the outbound connection
+	// and sent the connect message
+	link.connected = true
 
 	// process incoming messages
 	go t.listen(link)
@@ -413,6 +735,9 @@ func (t *tun) setupLink(node string) (*link, error) {
 	// start keepalive monitor
 	go t.keepalive(link)
 
+	// discover things on the remote side
+	go t.discover(link)
+
 	return link, nil
 }
 
@@ -430,30 +755,18 @@ func (t *tun) connect() error {
 		// accept inbound connections
 		err := l.Accept(func(sock transport.Socket) {
 			log.Debugf("Tunnel accepted connection from %s", sock.Remote())
-			// save the link
-			id := uuid.New().String()
-			t.Lock()
-			link := &link{
-				Socket: sock,
-				id:     id,
-			}
-			t.links[sock.Remote()] = link
-			t.Unlock()
 
-			// delete the link
-			defer func() {
-				log.Debugf("Tunnel deleting connection from %s", sock.Remote())
-				t.Lock()
-				delete(t.links, sock.Remote())
-				t.Unlock()
-			}()
+			// create a new link
+			link := newLink(sock)
 
-			// listen for inbound messages
+			// listen for inbound messages.
+			// only save the link once connected.
+			// we do this inside liste
 			t.listen(link)
 		})
 
-		t.Lock()
-		defer t.Unlock()
+		t.RLock()
+		defer t.RUnlock()
 
 		// still connected but the tunnel died
 		if err != nil && t.connected {
@@ -473,8 +786,9 @@ func (t *tun) connect() error {
 			log.Debugf("Tunnel failed to establish node link to %s: %v", node, err)
 			continue
 		}
+
 		// save the link
-		t.links[node] = link
+		t.links[link.Remote()] = link
 	}
 
 	// process outbound messages to be sent
@@ -511,11 +825,18 @@ func (t *tun) Connect() error {
 }
 
 func (t *tun) close() error {
+	// close all the sessions
+	for id, s := range t.sessions {
+		s.Close()
+		delete(t.sessions, id)
+	}
+
 	// close all the links
 	for node, link := range t.links {
 		link.Send(&transport.Message{
 			Header: map[string]string{
 				"Micro-Tunnel":       "close",
+				"Micro-Tunnel-Id":    t.id,
 				"Micro-Tunnel-Token": t.token,
 			},
 		})
@@ -524,6 +845,7 @@ func (t *tun) close() error {
 	}
 
 	// close the listener
+	// this appears to be blocking
 	return t.listener.Close()
 }
 
@@ -547,16 +869,12 @@ func (t *tun) Close() error {
 		return nil
 	}
 
+	log.Debug("Tunnel closing")
+
 	select {
 	case <-t.closed:
 		return nil
 	default:
-		// close all the sockets
-		for id, s := range t.sockets {
-			s.Close()
-			delete(t.sockets, id)
-		}
-		// close the connection
 		close(t.closed)
 		t.connected = false
 
@@ -570,60 +888,200 @@ func (t *tun) Close() error {
 }
 
 // Dial an address
-func (t *tun) Dial(addr string) (Conn, error) {
-	log.Debugf("Tunnel dialing %s", addr)
-	c, ok := t.newSocket(addr, t.newSession())
+func (t *tun) Dial(channel string, opts ...DialOption) (Session, error) {
+	log.Debugf("Tunnel dialing %s", channel)
+	c, ok := t.newSession(channel, t.newSessionId())
 	if !ok {
-		return nil, errors.New("error dialing " + addr)
+		return nil, errors.New("error dialing " + channel)
 	}
 	// set remote
-	c.remote = addr
+	c.remote = channel
 	// set local
 	c.local = "local"
-	// outbound socket
+	// outbound session
 	c.outbound = true
 
+	// get opts
+	options := DialOptions{
+		Timeout: DefaultDialTimeout,
+	}
+
+	for _, o := range opts {
+		o(&options)
+	}
+
+	// set the multicast option
+	c.multicast = options.Multicast
+	// set the dial timeout
+	c.timeout = options.Timeout
+
+	now := time.Now()
+
+	after := func() time.Duration {
+		d := time.Since(now)
+		// dial timeout minus time since
+		wait := options.Timeout - d
+		if wait < time.Duration(0) {
+			return time.Duration(0)
+		}
+		return wait
+	}
+
+	var links []string
+
+	// non multicast so we need to find the link
+	t.RLock()
+	for _, link := range t.links {
+		link.RLock()
+		_, ok := link.channels[channel]
+		link.RUnlock()
+
+		// we have at least one channel mapping
+		if ok {
+			c.discovered = true
+			links = append(links, link.id)
+		}
+	}
+	t.RUnlock()
+
+	// discovered so set the link if not multicast
+	// TODO: pick the link efficiently based
+	// on link status and saturation.
+	if c.discovered && !c.multicast {
+		// set the link
+		i := rand.Intn(len(links))
+		c.link = links[i]
+	}
+
+	// shit fuck
+	if !c.discovered {
+		// create a new discovery message for this channel
+		msg := c.newMessage("discover")
+		msg.broadcast = true
+		msg.outbound = true
+		msg.link = ""
+
+		// send the discovery message
+		t.send <- msg
+
+		select {
+		case <-time.After(after()):
+			return nil, ErrDialTimeout
+		case err := <-c.errChan:
+			if err != nil {
+				return nil, err
+			}
+		}
+
+		var err error
+
+		// set a dialTimeout
+		dialTimeout := after()
+
+		// set a shorter delay for multicast
+		if c.multicast {
+			// shorten this
+			dialTimeout = time.Millisecond * 500
+		}
+
+		// wait for announce
+		select {
+		case msg := <-c.recv:
+			if msg.typ != "announce" {
+				err = ErrDiscoverChan
+			}
+		case <-time.After(dialTimeout):
+			err = ErrDialTimeout
+		}
+
+		// if its multicast just go ahead because this is best effort
+		if c.multicast {
+			c.discovered = true
+			c.accepted = true
+			return c, nil
+		}
+
+		// otherwise return an error
+		if err != nil {
+			return nil, err
+		}
+
+		// set discovered to true
+		c.discovered = true
+	}
+
+	// a unicast session so we call "open" and wait for an "accept"
+
+	// try to open the session
+	err := c.Open()
+	if err != nil {
+		// delete the session
+		t.delSession(c.channel, c.session)
+		return nil, err
+	}
+
 	return c, nil
 }
 
 // Accept a connection on the address
-func (t *tun) Listen(addr string) (Listener, error) {
-	log.Debugf("Tunnel listening on %s", addr)
-	// create a new socket by hashing the address
-	c, ok := t.newSocket(addr, "listener")
+func (t *tun) Listen(channel string) (Listener, error) {
+	log.Debugf("Tunnel listening on %s", channel)
+
+	// create a new session by hashing the address
+	c, ok := t.newSession(channel, "listener")
 	if !ok {
-		return nil, errors.New("already listening on " + addr)
+		return nil, errors.New("already listening on " + channel)
+	}
+
+	delFunc := func() {
+		t.delSession(channel, "listener")
 	}
 
 	// set remote. it will be replaced by the first message received
 	c.remote = "remote"
 	// set local
-	c.local = addr
+	c.local = channel
 
 	tl := &tunListener{
-		addr: addr,
+		channel: channel,
 		// the accept channel
-		accept: make(chan *socket, 128),
+		accept: make(chan *session, 128),
 		// the channel to close
 		closed: make(chan bool),
 		// tunnel closed channel
 		tunClosed: t.closed,
-		// the connection
-		conn: c,
-		// the listener socket
-		socket: c,
+		// the listener session
+		session: c,
+		// delete session
+		delFunc: delFunc,
 	}
 
 	// this kicks off the internal message processor
-	// for the listener so it can create pseudo sockets
+	// for the listener so it can create pseudo sessions
 	// per session if they do not exist or pass messages
 	// to the existign sessions
 	go tl.process()
 
+	// announces the listener channel to others
+	go tl.announce()
+
 	// return the listener
 	return tl, nil
 }
 
+func (t *tun) Links() []Link {
+	t.RLock()
+	defer t.RUnlock()
+
+	var links []Link
+
+	for _, link := range t.links {
+		links = append(links, link)
+	}
+
+	return links
+}
+
 func (t *tun) String() string {
 	return "mucp"
 }
diff --git a/tunnel/link.go b/tunnel/link.go
new file mode 100644
index 00000000..b360e826
--- /dev/null
+++ b/tunnel/link.go
@@ -0,0 +1,101 @@
+package tunnel
+
+import (
+	"sync"
+	"time"
+
+	"github.com/google/uuid"
+	"github.com/micro/go-micro/transport"
+)
+
+type link struct {
+	transport.Socket
+
+	sync.RWMutex
+
+	// unique id of this link e.g uuid
+	// which we define for ourselves
+	id string
+	// whether its a loopback connection
+	// this flag is used by the transport listener
+	// which accepts inbound quic connections
+	loopback bool
+	// whether its actually connected
+	// dialled side sets it to connected
+	// after sending the message. the
+	// listener waits for the connect
+	connected bool
+	// the last time we received a keepalive
+	// on this link from the remote side
+	lastKeepAlive time.Time
+	// channels keeps a mapping of channels and last seen
+	channels map[string]time.Time
+	// stop the link
+	closed chan bool
+}
+
+func newLink(s transport.Socket) *link {
+	l := &link{
+		Socket:   s,
+		id:       uuid.New().String(),
+		channels: make(map[string]time.Time),
+		closed:   make(chan bool),
+	}
+	go l.run()
+	return l
+}
+
+func (l *link) run() {
+	t := time.NewTicker(time.Minute)
+	defer t.Stop()
+
+	for {
+		select {
+		case <-l.closed:
+			return
+		case <-t.C:
+			// drop any channel mappings older than 2 minutes
+			var kill []string
+			killTime := time.Minute * 2
+
+			l.RLock()
+			for ch, t := range l.channels {
+				if d := time.Since(t); d > killTime {
+					kill = append(kill, ch)
+				}
+			}
+			l.RUnlock()
+
+			// if nothing to kill don't bother with a wasted lock
+			if len(kill) == 0 {
+				continue
+			}
+
+			// kill the channels!
+			l.Lock()
+			for _, ch := range kill {
+				delete(l.channels, ch)
+			}
+			l.Unlock()
+		}
+	}
+}
+
+func (l *link) Id() string {
+	l.RLock()
+	defer l.RUnlock()
+
+	return l.id
+}
+
+func (l *link) Close() error {
+	select {
+	case <-l.closed:
+		return nil
+	default:
+		close(l.closed)
+		return nil
+	}
+
+	return nil
+}
diff --git a/network/link/default.go b/tunnel/link/default.go
similarity index 100%
rename from network/link/default.go
rename to tunnel/link/default.go
diff --git a/network/link/link.go b/tunnel/link/link.go
similarity index 92%
rename from network/link/link.go
rename to tunnel/link/link.go
index 3acfd772..1c42831a 100644
--- a/network/link/link.go
+++ b/tunnel/link/link.go
@@ -8,8 +8,13 @@ import (
 	"github.com/micro/go-micro/transport"
 )
 
+var (
+	// ErrLinkClosed is returned when attempting i/o operation on the closed link
+	ErrLinkClosed = errors.New("link closed")
+)
+
 // Link is a layer on top of a transport socket with the
-// buffering send and recv queue's with the ability to
+// buffering send and recv queues with the ability to
 // measure the actual transport link and reconnect if
 // an address is specified.
 type Link interface {
@@ -28,10 +33,6 @@ type Link interface {
 	Length() int
 }
 
-var (
-	ErrLinkClosed = errors.New("link closed")
-)
-
 // NewLink creates a new link on top of a socket
 func NewLink(opts ...options.Option) Link {
 	return newLink(options.NewOptions(opts...))
diff --git a/tunnel/listener.go b/tunnel/listener.go
index 3002e7b6..ee394519 100644
--- a/tunnel/listener.go
+++ b/tunnel/listener.go
@@ -2,79 +2,148 @@ package tunnel
 
 import (
 	"io"
+	"time"
 
 	"github.com/micro/go-micro/util/log"
 )
 
 type tunListener struct {
 	// address of the listener
-	addr string
+	channel string
 	// the accept channel
-	accept chan *socket
+	accept chan *session
 	// the channel to close
 	closed chan bool
 	// the tunnel closed channel
 	tunClosed chan bool
-	// the connection
-	conn Conn
-	// the listener socket
-	socket *socket
+	// the listener session
+	session *session
+	// del func to kill listener
+	delFunc func()
+}
+
+// periodically announce self
+func (t *tunListener) announce() {
+	tick := time.NewTicker(time.Second * 30)
+	defer tick.Stop()
+
+	// first announcement
+	t.session.Announce()
+
+	for {
+		select {
+		case <-tick.C:
+			t.session.Announce()
+		case <-t.closed:
+			return
+		}
+	}
 }
 
 func (t *tunListener) process() {
 	// our connection map for session
-	conns := make(map[string]*socket)
+	conns := make(map[string]*session)
+
+	defer func() {
+		// close the sessions
+		for _, conn := range conns {
+			conn.Close()
+		}
+	}()
 
 	for {
 		select {
 		case <-t.closed:
 			return
+		case <-t.tunClosed:
+			t.Close()
+			return
 		// receive a new message
-		case m := <-t.socket.recv:
-			// get a socket
-			sock, ok := conns[m.session]
-			log.Debugf("Tunnel listener received id %s session %s exists: %t", m.id, m.session, ok)
+		case m := <-t.session.recv:
+			// get a session
+			sess, ok := conns[m.session]
+			log.Debugf("Tunnel listener received channel %s session %s exists: %t", m.channel, m.session, ok)
 			if !ok {
-				// create a new socket session
-				sock = &socket{
-					// our tunnel id
-					id: m.id,
+				switch m.typ {
+				case "open", "session":
+				default:
+					continue
+				}
+
+				// create a new session session
+				sess = &session{
+					// the id of the remote side
+					tunnel: m.tunnel,
+					// the channel
+					channel: m.channel,
 					// the session id
 					session: m.session,
+					// is loopback conn
+					loopback: m.loopback,
+					// the link the message was received on
+					link: m.link,
+					// set multicast
+					multicast: m.multicast,
 					// close chan
 					closed: make(chan bool),
 					// recv called by the acceptor
 					recv: make(chan *message, 128),
 					// use the internal send buffer
-					send: t.socket.send,
+					send: t.session.send,
 					// wait
 					wait: make(chan bool),
+					// error channel
+					errChan: make(chan error, 1),
 				}
 
-				// save the socket
-				conns[m.session] = sock
+				// save the session
+				conns[m.session] = sess
 
-				// send to accept chan
 				select {
 				case <-t.closed:
 					return
-				case t.accept <- sock:
+				// send to accept chan
+				case t.accept <- sess:
 				}
 			}
 
+			// an existing session was found
+
+			// received a close message
+			switch m.typ {
+			case "close":
+				select {
+				case <-sess.closed:
+					// no op
+					delete(conns, m.session)
+				default:
+					// close and delete session
+					close(sess.closed)
+					delete(conns, m.session)
+				}
+
+				// continue
+				continue
+			case "session":
+				// operate on this
+			default:
+				// non operational type
+				continue
+			}
+
 			// send this to the accept chan
 			select {
-			case <-sock.closed:
+			case <-sess.closed:
 				delete(conns, m.session)
-			case sock.recv <- m:
-				log.Debugf("Tunnel listener sent to recv chan id %s session %s", m.id, m.session)
+			case sess.recv <- m:
+				log.Debugf("Tunnel listener sent to recv chan channel %s session %s", m.channel, m.session)
 			}
 		}
 	}
 }
 
-func (t *tunListener) Addr() string {
-	return t.addr
+func (t *tunListener) Channel() string {
+	return t.channel
 }
 
 // Close closes tunnel listener
@@ -83,26 +152,33 @@ func (t *tunListener) Close() error {
 	case <-t.closed:
 		return nil
 	default:
+		// close and delete
+		t.delFunc()
+		t.session.Close()
 		close(t.closed)
 	}
 	return nil
 }
 
 // Everytime accept is called we essentially block till we get a new connection
-func (t *tunListener) Accept() (Conn, error) {
+func (t *tunListener) Accept() (Session, error) {
 	select {
-	// if the socket is closed return
+	// if the session is closed return
 	case <-t.closed:
 		return nil, io.EOF
 	case <-t.tunClosed:
 		// close the listener when the tunnel closes
-		t.Close()
 		return nil, io.EOF
 	// wait for a new connection
 	case c, ok := <-t.accept:
+		// check if the accept chan is closed
 		if !ok {
 			return nil, io.EOF
 		}
+		// send back the accept
+		if err := c.Accept(); err != nil {
+			return nil, err
+		}
 		return c, nil
 	}
 	return nil, nil
diff --git a/tunnel/options.go b/tunnel/options.go
index 99406b05..39795671 100644
--- a/tunnel/options.go
+++ b/tunnel/options.go
@@ -1,6 +1,8 @@
 package tunnel
 
 import (
+	"time"
+
 	"github.com/google/uuid"
 	"github.com/micro/go-micro/transport"
 	"github.com/micro/go-micro/transport/quic"
@@ -9,6 +11,8 @@ import (
 var (
 	// DefaultAddress is default tunnel bind address
 	DefaultAddress = ":0"
+	// The shared default token
+	DefaultToken = "micro"
 )
 
 type Option func(*Options)
@@ -21,10 +25,21 @@ type Options struct {
 	Address string
 	// Nodes are remote nodes
 	Nodes []string
+	// The shared auth token
+	Token string
 	// Transport listens to incoming connections
 	Transport transport.Transport
 }
 
+type DialOption func(*DialOptions)
+
+type DialOptions struct {
+	// specify a multicast connection
+	Multicast bool
+	// the dial timeout
+	Timeout time.Duration
+}
+
 // The tunnel id
 func Id(id string) Option {
 	return func(o *Options) {
@@ -46,6 +61,13 @@ func Nodes(n ...string) Option {
 	}
 }
 
+// Token sets the shared token for auth
+func Token(t string) Option {
+	return func(o *Options) {
+		o.Token = t
+	}
+}
+
 // Transport listens for incoming connections
 func Transport(t transport.Transport) Option {
 	return func(o *Options) {
@@ -58,6 +80,22 @@ func DefaultOptions() Options {
 	return Options{
 		Id:        uuid.New().String(),
 		Address:   DefaultAddress,
+		Token:     DefaultToken,
 		Transport: quic.NewTransport(),
 	}
 }
+
+// Dial options
+
+// Dial multicast sets the multicast option to send only to those mapped
+func DialMulticast() DialOption {
+	return func(o *DialOptions) {
+		o.Multicast = true
+	}
+}
+
+func DialTimeout(t time.Duration) DialOption {
+	return func(o *DialOptions) {
+		o.Timeout = t
+	}
+}
diff --git a/tunnel/session.go b/tunnel/session.go
new file mode 100644
index 00000000..a4c7a2fe
--- /dev/null
+++ b/tunnel/session.go
@@ -0,0 +1,286 @@
+package tunnel
+
+import (
+	"errors"
+	"io"
+	"time"
+
+	"github.com/micro/go-micro/transport"
+	"github.com/micro/go-micro/util/log"
+)
+
+// session is our pseudo session for transport.Socket
+type session struct {
+	// the tunnel id
+	tunnel string
+	// the channel name
+	channel string
+	// the session id based on Micro.Tunnel-Session
+	session string
+	// closed
+	closed chan bool
+	// remote addr
+	remote string
+	// local addr
+	local string
+	// send chan
+	send chan *message
+	// recv chan
+	recv chan *message
+	// wait until we have a connection
+	wait chan bool
+	// if the discovery worked
+	discovered bool
+	// if the session was accepted
+	accepted bool
+	// outbound marks the session as outbound dialled connection
+	outbound bool
+	// lookback marks the session as a loopback on the inbound
+	loopback bool
+	// if the session is multicast
+	multicast bool
+	// if the session is broadcast
+	broadcast bool
+	// the timeout
+	timeout time.Duration
+	// the link on which this message was received
+	link string
+	// the error response
+	errChan chan error
+}
+
+// message is sent over the send channel
+type message struct {
+	// type of message
+	typ string
+	// tunnel id
+	tunnel string
+	// channel name
+	channel string
+	// the session id
+	session string
+	// outbound marks the message as outbound
+	outbound bool
+	// loopback marks the message intended for loopback
+	loopback bool
+	// whether to send as multicast
+	multicast bool
+	// broadcast sets the broadcast type
+	broadcast bool
+	// the link to send the message on
+	link string
+	// transport data
+	data *transport.Message
+	// the error channel
+	errChan chan error
+}
+
+func (s *session) Remote() string {
+	return s.remote
+}
+
+func (s *session) Local() string {
+	return s.local
+}
+
+func (s *session) Id() string {
+	return s.session
+}
+
+func (s *session) Channel() string {
+	return s.channel
+}
+
+// newMessage creates a new message based on the session
+func (s *session) newMessage(typ string) *message {
+	return &message{
+		typ:       typ,
+		tunnel:    s.tunnel,
+		channel:   s.channel,
+		session:   s.session,
+		outbound:  s.outbound,
+		loopback:  s.loopback,
+		multicast: s.multicast,
+		link:      s.link,
+		errChan:   s.errChan,
+	}
+}
+
+// Open will fire the open message for the session. This is called by the dialler.
+func (s *session) Open() error {
+	// create a new message
+	msg := s.newMessage("open")
+
+	// send open message
+	s.send <- msg
+
+	// wait for an error response for send
+	select {
+	case err := <-msg.errChan:
+		if err != nil {
+			return err
+		}
+	case <-s.closed:
+		return io.EOF
+	}
+
+	// we don't wait on multicast
+	if s.multicast {
+		s.accepted = true
+		return nil
+	}
+
+	// now wait for the accept
+	select {
+	case msg = <-s.recv:
+		if msg.typ != "accept" {
+			log.Debugf("Received non accept message in Open %s", msg.typ)
+			return errors.New("failed to connect")
+		}
+		// set to accepted
+		s.accepted = true
+		// set link
+		s.link = msg.link
+	case <-time.After(s.timeout):
+		return ErrDialTimeout
+	case <-s.closed:
+		return io.EOF
+	}
+
+	return nil
+}
+
+// Accept sends the accept response to an open message from a dialled connection
+func (s *session) Accept() error {
+	msg := s.newMessage("accept")
+
+	// send the accept message
+	select {
+	case <-s.closed:
+		return io.EOF
+	case s.send <- msg:
+		return nil
+	}
+
+	// wait for send response
+	select {
+	case err := <-s.errChan:
+		if err != nil {
+			return err
+		}
+	case <-s.closed:
+		return io.EOF
+	}
+
+	return nil
+}
+
+// Announce sends an announcement to notify that this session exists. This is primarily used by the listener.
+func (s *session) Announce() error {
+	msg := s.newMessage("announce")
+	// we don't need an error back
+	msg.errChan = nil
+	// announce to all
+	msg.broadcast = true
+	// we don't need the link
+	msg.link = ""
+
+	select {
+	case s.send <- msg:
+		return nil
+	case <-s.closed:
+		return io.EOF
+	}
+}
+
+// Send is used to send a message
+func (s *session) Send(m *transport.Message) error {
+	select {
+	case <-s.closed:
+		return io.EOF
+	default:
+		// no op
+	}
+
+	// make copy
+	data := &transport.Message{
+		Header: make(map[string]string),
+		Body:   m.Body,
+	}
+
+	for k, v := range m.Header {
+		data.Header[k] = v
+	}
+
+	// create a new message
+	msg := s.newMessage("session")
+	// set the data
+	msg.data = data
+
+	// if multicast don't set the link
+	if s.multicast {
+		msg.link = ""
+	}
+
+	log.Debugf("Appending %+v to send backlog", msg)
+	// send the actual message
+	s.send <- msg
+
+	// wait for an error response
+	select {
+	case err := <-msg.errChan:
+		return err
+	case <-s.closed:
+		return io.EOF
+	}
+
+	return nil
+}
+
+// Recv is used to receive a message
+func (s *session) Recv(m *transport.Message) error {
+	select {
+	case <-s.closed:
+		return errors.New("session is closed")
+	default:
+		// no op
+	}
+	// recv from backlog
+	msg := <-s.recv
+
+	// check the error if one exists
+	select {
+	case err := <-msg.errChan:
+		return err
+	default:
+	}
+
+	log.Debugf("Received %+v from recv backlog", msg)
+	// set message
+	*m = *msg.data
+	// return nil
+	return nil
+}
+
+// Close closes the session by sending a close message
+func (s *session) Close() error {
+	select {
+	case <-s.closed:
+		// no op
+	default:
+		close(s.closed)
+
+		// append to backlog
+		msg := s.newMessage("close")
+		// no error response on close
+		msg.errChan = nil
+
+		// send the close message
+		select {
+		case s.send <- msg:
+		default:
+		}
+	}
+
+	return nil
+}
diff --git a/tunnel/socket.go b/tunnel/socket.go
deleted file mode 100644
index 2590a48e..00000000
--- a/tunnel/socket.go
+++ /dev/null
@@ -1,115 +0,0 @@
-package tunnel
-
-import (
-	"errors"
-
-	"github.com/micro/go-micro/transport"
-	"github.com/micro/go-micro/util/log"
-)
-
-// socket is our pseudo socket for transport.Socket
-type socket struct {
-	// socket id based on Micro-Tunnel
-	id string
-	// the session id based on Micro.Tunnel-Session
-	session string
-	// closed
-	closed chan bool
-	// remote addr
-	remote string
-	// local addr
-	local string
-	// send chan
-	send chan *message
-	// recv chan
-	recv chan *message
-	// wait until we have a connection
-	wait chan bool
-	// outbound marks the socket as outbound
-	outbound bool
-}
-
-// message is sent over the send channel
-type message struct {
-	// tunnel id
-	id string
-	// the session id
-	session string
-	// outbound marks the message as outbound
-	outbound bool
-	// transport data
-	data *transport.Message
-}
-
-func (s *socket) Remote() string {
-	return s.remote
-}
-
-func (s *socket) Local() string {
-	return s.local
-}
-
-func (s *socket) Id() string {
-	return s.id
-}
-
-func (s *socket) Session() string {
-	return s.session
-}
-
-func (s *socket) Send(m *transport.Message) error {
-	select {
-	case <-s.closed:
-		return errors.New("socket is closed")
-	default:
-		// no op
-	}
-
-	// make copy
-	data := &transport.Message{
-		Header: make(map[string]string),
-		Body:   m.Body,
-	}
-
-	for k, v := range m.Header {
-		data.Header[k] = v
-	}
-
-	// append to backlog
-	msg := &message{
-		id:       s.id,
-		session:  s.session,
-		outbound: s.outbound,
-		data:     data,
-	}
-	log.Debugf("Appending %+v to send backlog", msg)
-	s.send <- msg
-	return nil
-}
-
-func (s *socket) Recv(m *transport.Message) error {
-	select {
-	case <-s.closed:
-		return errors.New("socket is closed")
-	default:
-		// no op
-	}
-	// recv from backlog
-	msg := <-s.recv
-	log.Debugf("Received %+v from recv backlog", msg)
-	// set message
-	*m = *msg.data
-	// return nil
-	return nil
-}
-
-// Close closes the socket
-func (s *socket) Close() error {
-	select {
-	case <-s.closed:
-		// no op
-	default:
-		close(s.closed)
-	}
-	return nil
-}
diff --git a/tunnel/transport/listener.go b/tunnel/transport/listener.go
index b7a7280c..075f12cf 100644
--- a/tunnel/transport/listener.go
+++ b/tunnel/transport/listener.go
@@ -10,7 +10,7 @@ type tunListener struct {
 }
 
 func (t *tunListener) Addr() string {
-	return t.l.Addr()
+	return t.l.Channel()
 }
 
 func (t *tunListener) Close() error {
diff --git a/tunnel/tunnel.go b/tunnel/tunnel.go
index f7bc91cb..14604215 100644
--- a/tunnel/tunnel.go
+++ b/tunnel/tunnel.go
@@ -2,11 +2,23 @@
 package tunnel
 
 import (
+	"errors"
+	"time"
+
 	"github.com/micro/go-micro/transport"
 )
 
-// Tunnel creates a gre network tunnel on top of a link.
-// It establishes multiple streams using the Micro-Tunnel-Id header
+var (
+	// DefaultDialTimeout is the dial timeout if none is specified
+	DefaultDialTimeout = time.Second * 5
+	// ErrDialTimeout is returned by a call to Dial where the timeout occurs
+	ErrDialTimeout = errors.New("dial timeout")
+	// ErrDiscoverChan is returned when we failed to receive the "announce" back from a discovery
+	ErrDiscoverChan = errors.New("failed to discover channel")
+)
+
+// Tunnel creates a gre tunnel on top of the go-micro/transport.
+// It establishes multiple streams using the Micro-Tunnel-Channel header
 // and Micro-Tunnel-Session header. The tunnel id is a hash of
 // the address being requested.
 type Tunnel interface {
@@ -17,27 +29,37 @@ type Tunnel interface {
 	Connect() error
 	// Close closes the tunnel
 	Close() error
-	// Dial an endpoint
-	Dial(addr string) (Conn, error)
-	// Accept connections
-	Listen(addr string) (Listener, error)
+	// Connect to a channel
+	Dial(channel string, opts ...DialOption) (Session, error)
+	// Accept connections on a channel
+	Listen(channel string) (Listener, error)
+	// All the links the tunnel is connected to
+	Links() []Link
 	// Name of the tunnel implementation
 	String() string
 }
 
-// The listener provides similar constructs to the transport.Listener
-type Listener interface {
-	Addr() string
-	Close() error
-	Accept() (Conn, error)
+// Link represents internal links to the tunnel
+type Link interface {
+	// The id of the link
+	Id() string
+	// honours transport socket
+	transport.Socket
 }
 
-// Conn is a connection dialed or accepted which includes the tunnel id and session
-type Conn interface {
-	// Specifies the tunnel id
+// The listener provides similar constructs to the transport.Listener
+type Listener interface {
+	Accept() (Session, error)
+	Channel() string
+	Close() error
+}
+
+// Session is a unique session created when dialling or accepting connections on the tunnel
+type Session interface {
+	// The unique session id
 	Id() string
-	// The session
-	Session() string
+	// The channel name
+	Channel() string
 	// a transport socket
 	transport.Socket
 }
diff --git a/tunnel/tunnel_test.go b/tunnel/tunnel_test.go
index 3fc84b6d..fc76421b 100644
--- a/tunnel/tunnel_test.go
+++ b/tunnel/tunnel_test.go
@@ -187,30 +187,15 @@ func testBrokenTunAccept(t *testing.T, tun Tunnel, wait chan bool, wg *sync.Wait
 	if err := c.Recv(m); err != nil {
 		t.Fatal(err)
 	}
-	tun.Close()
 
-	// re-start tunnel
-	err = tun.Connect()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer tun.Close()
-
-	// listen on some virtual address
-	tl, err = tun.Listen("test-tunnel")
-	if err != nil {
-		t.Fatal(err)
+	// close all the links
+	for _, link := range tun.Links() {
+		link.Close()
 	}
 
 	// receiver ready; notify sender
 	wait <- true
 
-	// accept a connection
-	c, err = tl.Accept()
-	if err != nil {
-		t.Fatal(err)
-	}
-
 	// accept the message
 	m = new(transport.Message)
 	if err := c.Recv(m); err != nil {
@@ -279,6 +264,7 @@ func TestReconnectTunnel(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
+	defer tunB.Close()
 
 	// we manually override the tunnel.ReconnectTime value here
 	// this is so that we make the reconnects faster than the default 5s
@@ -289,6 +275,7 @@ func TestReconnectTunnel(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
+	defer tunA.Close()
 
 	wait := make(chan bool)
 
diff --git a/util/addr/addr.go b/util/addr/addr.go
index b2874533..911dacb0 100644
--- a/util/addr/addr.go
+++ b/util/addr/addr.go
@@ -40,14 +40,20 @@ func Extract(addr string) (string, error) {
 	}
 
 	var addrs []net.Addr
+	var loAddrs []net.Addr
 	for _, iface := range ifaces {
 		ifaceAddrs, err := iface.Addrs()
 		if err != nil {
 			// ignore error, interface can dissapear from system
 			continue
 		}
+		if iface.Flags&net.FlagLoopback != 0 {
+			loAddrs = append(loAddrs, ifaceAddrs...)
+			continue
+		}
 		addrs = append(addrs, ifaceAddrs...)
 	}
+	addrs = append(addrs, loAddrs...)
 
 	var ipAddr []byte
 	var publicIP []byte
diff --git a/util/log/log.go b/util/log/log.go
index 0b911aae..c8ae4acc 100644
--- a/util/log/log.go
+++ b/util/log/log.go
@@ -14,7 +14,7 @@ type Level int
 const (
 	LevelFatal Level = iota
 	LevelInfo
-	LevelWarn
+	LevelError
 	LevelDebug
 	LevelTrace
 )
@@ -29,16 +29,16 @@ var (
 
 func init() {
 	switch os.Getenv("MICRO_LOG_LEVEL") {
+	case "trace":
+		level = LevelTrace
 	case "debug":
 		level = LevelDebug
 	case "info":
 		level = LevelInfo
-	case "trace":
-		level = LevelTrace
+	case "error":
+		level = LevelError
 	case "fatal":
 		level = LevelFatal
-	case "warn":
-		level = LevelWarn
 	}
 }
 
@@ -98,14 +98,14 @@ func Infof(format string, v ...interface{}) {
 	WithLevelf(LevelInfo, format, v...)
 }
 
-// Warn provides warn level logging
-func Warn(v ...interface{}) {
-	WithLevel(LevelWarn, v...)
+// Error provides warn level logging
+func Error(v ...interface{}) {
+	WithLevel(LevelError, v...)
 }
 
-// Warnf provides warn level logging
-func Warnf(format string, v ...interface{}) {
-	WithLevelf(LevelWarn, format, v...)
+// Errorf provides warn level logging
+func Errorf(format string, v ...interface{}) {
+	WithLevelf(LevelError, format, v...)
 }
 
 // Fatal logs with Log and then exits with os.Exit(1)