ref/discovery: Implement a ble plugin for discovery.

Change-Id: I45f7f0fda6e399e10f533d6ac72488365e7828f3
diff --git a/lib/discovery/plugins/ble/advertisement.go b/lib/discovery/plugins/ble/advertisement.go
new file mode 100644
index 0000000..04c87b9
--- /dev/null
+++ b/lib/discovery/plugins/ble/advertisement.go
@@ -0,0 +1,91 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package ble
+
+import (
+	"strings"
+
+	"fmt"
+	"net/url"
+
+	vdiscovery "v.io/v23/discovery"
+	"v.io/x/ref/lib/discovery"
+
+	"github.com/pborman/uuid"
+)
+
+type bleAdv struct {
+	serviceUUID uuid.UUID
+	instanceID  []byte
+	attrs       map[string][]byte
+}
+
+var (
+	// This uuids are v4 uuid generated out of band.  These constants need
+	// to be accessible in all the languages that have a ble implementation
+
+	// The attribute uuid for the unique service id
+	instanceUUID = "f6445c7f-73fd-4b8d-98d0-c4e02b087844"
+
+	// The attribute uuid for the interface name
+	interfaceNameUUID = "d4789810-4db0-40d8-9658-92f8e304d578"
+
+	addrUUID = "f123fb0e-770f-4e46-b8ad-aee4185ab5a1"
+)
+
+func newAdvertisment(adv discovery.Advertisement) bleAdv {
+	cleanAddrs := make([]string, len(adv.Addrs))
+	for i, v := range adv.Addrs {
+		cleanAddrs[i] = url.QueryEscape(v)
+	}
+	attrs := map[string][]byte{
+		instanceUUID:      adv.InstanceUuid,
+		interfaceNameUUID: []byte(adv.InterfaceName),
+		addrUUID:          []byte(strings.Join(cleanAddrs, "&")),
+	}
+
+	for k, v := range adv.Attrs {
+		hexUUID := discovery.NewAttributeUUID(k).String()
+		attrs[hexUUID] = []byte(k + "=" + v)
+	}
+	return bleAdv{
+		instanceID:  adv.InstanceUuid,
+		serviceUUID: adv.ServiceUuid,
+		attrs:       attrs,
+	}
+}
+
+func (a *bleAdv) toDiscoveryAdvertisement() (*discovery.Advertisement, error) {
+	out := &discovery.Advertisement{
+		Service: vdiscovery.Service{
+			Attrs:         vdiscovery.Attributes{},
+			InterfaceName: string(a.attrs[interfaceNameUUID]),
+			InstanceUuid:  a.instanceID,
+		},
+		ServiceUuid: a.serviceUUID,
+	}
+	out.Addrs = strings.Split(string(a.attrs[addrUUID]), "&")
+	var err error
+	for i, v := range out.Addrs {
+		out.Addrs[i], err = url.QueryUnescape(v)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	for k, v := range a.attrs {
+		if k == instanceUUID || k == interfaceNameUUID || k == addrUUID {
+			continue
+		}
+		parts := strings.SplitN(string(v), "=", 2)
+		if len(parts) != 2 {
+			return nil, fmt.Errorf("incorrectly formatted value, %s", v)
+		}
+		out.Attrs[parts[0]] = parts[1]
+
+	}
+
+	return out, nil
+}
diff --git a/lib/discovery/plugins/ble/advertisement_test.go b/lib/discovery/plugins/ble/advertisement_test.go
new file mode 100644
index 0000000..5b4025c
--- /dev/null
+++ b/lib/discovery/plugins/ble/advertisement_test.go
@@ -0,0 +1,37 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package ble
+
+import (
+	"github.com/pborman/uuid"
+	"reflect"
+	"testing"
+	vdiscovery "v.io/v23/discovery"
+	"v.io/x/ref/lib/discovery"
+)
+
+func TestConvertingBackAndForth(t *testing.T) {
+	v23Adv := discovery.Advertisement{
+		Service: vdiscovery.Service{
+			Addrs:        []string{"localhost:1000", "example.com:540"},
+			InstanceUuid: []byte(uuid.NewUUID()),
+			Attrs: map[string]string{
+				"key1": "value1",
+				"key2": "value2",
+			},
+		},
+		ServiceUuid: uuid.NewUUID(),
+	}
+
+	adv := newAdvertisment(v23Adv)
+	out, err := adv.toDiscoveryAdvertisement()
+	if err != nil {
+		t.Errorf("unexpected error: %v", err)
+	}
+
+	if !reflect.DeepEqual(&v23Adv, out) {
+		t.Errorf("input does not equal output: %v, %v", v23Adv, out)
+	}
+}
diff --git a/lib/discovery/plugins/ble/neighborhood.go b/lib/discovery/plugins/ble/neighborhood.go
new file mode 100644
index 0000000..c909855
--- /dev/null
+++ b/lib/discovery/plugins/ble/neighborhood.go
@@ -0,0 +1,472 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package ble implements a ble protocol to support the Vanadium discovery api.
+// The package exports a discovery.Plugin that can be used to use ble for discovery.
+// The advertising packet of Vanadium device should contain a manufacturer data field
+// with the manufacturer id of 1001.  The first 8 bytes of the data is the hash of
+// the services exported.  If the hash has not changed, then it is expected that the
+// services and the properties they contain has not changed either.
+package ble
+
+import (
+	"encoding/hex"
+	"hash/fnv"
+	"log"
+	"reflect"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/paypal/gatt"
+	"github.com/pborman/uuid"
+	"v.io/x/ref/lib/discovery"
+)
+
+const (
+	// TODO(bjornick): Make sure this is actually unique.
+	manufacturerId = uint16(1001)
+	ttl            = time.Minute * 5
+)
+
+var (
+	// These constants are taken from:
+	// https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicsHome.aspx
+	attrGAPUUID  = gatt.UUID16(0x1800)
+	attrGATTUUID = gatt.UUID16(0x1801)
+)
+
+func newV23Service(uuid uuid.UUID, attrs map[string][]byte) *gatt.Service {
+	s := gatt.NewService(gatt.MustParseUUID(uuid.String()))
+	for k, v := range attrs {
+		s.AddCharacteristic(gatt.MustParseUUID(k)).SetValue(v)
+	}
+	return s
+}
+
+type bleCacheEntry struct {
+	id             string
+	advertisements map[string]*bleAdv
+	hash           string
+	lastSeen       time.Time
+}
+
+type bleNeighborhood struct {
+	mu sync.Mutex
+
+	// key is the hex encoded hash of the bleCacheEntry
+	neighborsHashCache map[string]*bleCacheEntry
+	// key is the hex encoded ID of the device.
+	knownNeighbors map[string]*bleCacheEntry
+	// key is the hex-encoded instance id
+	services map[string]*gatt.Service
+	// Scanners out standing calls to Scan that need be serviced.  Each time a
+	// new device appears or disappears, the scanner is notified of the event.
+	// The key is the unique scanner id for the scanner.
+	scannersById map[int64]*scanner
+
+	// scannersByService maps from human readable service uuid to a map of
+	// scanner id to scanner.
+	scannersByService map[string]map[int64]*scanner
+
+	// If both sides try to connect to each other at the same time, then only
+	// one will succeed and the other hangs forever.  This means that the side
+	// that hangs won't ever start scanning or advertising again.  To avoid this
+	// we timeout any connections that don't finish in under 4 seconds.  This
+	// channel is closed when a connection has been made successfully, to notify
+	// the cancel goroutine that it doesn't need to do anything.  This is a map
+	// from the hex encoded device id to the channel to close.
+	timeoutMap map[string]chan struct{}
+
+	// The hash that we use to avoid multiple connections are stored in the
+	// advertising data, so we need to store somewhere in the bleNeighorhood
+	// until we are ready to save the new device data.  This map is
+	// the keeper of the data.  This is a map from hex encoded device id to
+	// hex encoded hash.
+	pendingHashMap map[string]string
+	name           string
+	device         gatt.Device
+	stopped        chan struct{}
+	nextScanId     int64
+}
+
+func newBleNeighborhood(name string) (*bleNeighborhood, error) {
+	b := &bleNeighborhood{
+		neighborsHashCache: make(map[string]*bleCacheEntry),
+		knownNeighbors:     make(map[string]*bleCacheEntry),
+		name:               name,
+		services:           make(map[string]*gatt.Service),
+		scannersById:       make(map[int64]*scanner),
+		scannersByService:  make(map[string]map[int64]*scanner),
+		timeoutMap:         make(map[string]chan struct{}),
+		pendingHashMap:     make(map[string]string),
+		stopped:            make(chan struct{}),
+	}
+	if err := b.startBLEService(); err != nil {
+		return nil, err
+	}
+
+	go b.checkTTL()
+	return b, nil
+}
+
+func (b *bleNeighborhood) checkTTL() {
+	for {
+		select {
+		case <-b.stopped:
+			return
+		case <-time.After(time.Minute):
+			b.mu.Lock()
+			now := time.Now()
+
+			for k, entry := range b.neighborsHashCache {
+				if entry.lastSeen.Add(ttl).Before(now) {
+					delete(b.neighborsHashCache, k)
+					delete(b.knownNeighbors, entry.id)
+					for id, adv := range entry.advertisements {
+						for _, scanner := range b.scannersById {
+							scanner.handleChange(uuid.Parse(id), adv, nil)
+						}
+					}
+				}
+			}
+			b.mu.Unlock()
+		}
+	}
+}
+
+func (b *bleNeighborhood) addAdvertisement(adv bleAdv) {
+	b.mu.Lock()
+	b.services[hex.EncodeToString(adv.instanceID)] = newV23Service(adv.serviceUUID, adv.attrs)
+	v := make([]*gatt.Service, len(b.services))
+	i := 0
+	for _, s := range b.services {
+		v[i] = s
+		i++
+	}
+	b.mu.Unlock()
+	b.device.SetServices(v)
+}
+
+func (b *bleNeighborhood) removeService(id []byte) {
+	b.mu.Lock()
+	delete(b.services, hex.EncodeToString(id))
+	v := make([]*gatt.Service, 0, len(b.services))
+	for _, s := range b.services {
+		v = append(v, s)
+	}
+	b.mu.Unlock()
+	b.device.SetServices(v)
+}
+
+func (b *bleNeighborhood) addScanner(uuid uuid.UUID) (chan *discovery.Advertisement, int64) {
+	ch := make(chan *discovery.Advertisement)
+	s := &scanner{
+		uuid: uuid,
+		ch:   ch,
+	}
+	b.mu.Lock()
+	id := b.nextScanId
+	b.nextScanId++
+	b.scannersById[id] = s
+	key := uuid.String()
+	m, found := b.scannersByService[key]
+	if !found {
+		m = map[int64]*scanner{}
+		b.scannersByService[key] = m
+	}
+	m[id] = s
+	b.mu.Unlock()
+	return ch, id
+}
+
+func (b *bleNeighborhood) removeScanner(id int64) {
+	b.mu.Lock()
+	scanner, found := b.scannersById[id]
+	if found {
+		scanner.stop()
+	}
+	delete(b.scannersById, id)
+	key := scanner.uuid.String()
+	delete(b.scannersByService[key], id)
+	b.mu.Unlock()
+}
+
+func (b *bleNeighborhood) Stop() error {
+	close(b.stopped)
+	b.device.StopAdvertising()
+	b.device.StopScanning()
+	return nil
+}
+
+func (b *bleNeighborhood) advertiseAndScan() {
+	select {
+	case <-b.stopped:
+		return
+	default:
+	}
+	b.device.Advertise(b.computeAdvertisement())
+	// TODO(bjornick): Don't scan unless there is a scanner running.
+	b.device.Scan([]gatt.UUID{}, true)
+}
+
+// seenHash returns whether or not we have seen the hash <h> before.
+func (b *bleNeighborhood) seenHash(id string, h []byte) bool {
+	b.mu.Lock()
+	defer b.mu.Unlock()
+	key := hex.EncodeToString(h)
+	entry, ok := b.neighborsHashCache[key]
+	if !ok {
+		b.pendingHashMap[id] = key
+		return false
+	}
+
+	if entry.id != id {
+		// This can happen either because two different devices chose the same
+		// endpoint and name, or that one device changed its mac address.  It
+		// seems more likely that the latter happened
+		// TODO(bjornick): Deal with the first case.
+		entry.id = id
+	}
+	entry.lastSeen = time.Now()
+	return true
+}
+
+// getVanadiumHash returns the hash of the vanadium device, if this advertisement
+// is from a vanadium device.
+func getVanadiumHash(a *gatt.Advertisement) ([]byte, bool) {
+	md := a.ManufacturerData
+	// The manufacturer data for other vanadium devices contains the hash in
+	// the first 8 bytes of the data portion of the packet.  Since we can't tell
+	// gatt to only call us for advertisements from a particular manufacturer, we
+	// have to decode the manufacturer field to:
+	//   1) figure out if this is a vanadium device
+	//   2) find the hash of the data.
+	// The formnat of the manufacturer data is:
+	//    2-bytes for the manufacturer id (in little endian)
+	//    1-byte for the length of the data segment
+	//    <the actual data>
+	if len(md) < 2 {
+		return nil, false
+	}
+	if md[0] != uint8(0xe9) || md[1] != uint8(0x03) {
+		return nil, false
+	}
+	return md[3:], true
+}
+
+// gattUUIDtoUUID converts a gatt.UUID to uuid.UUID.
+func gattUUIDtoUUID(u gatt.UUID) (uuid.UUID, error) {
+	// We can't just do uuid.Parse(u.String()), because the uuid code expects
+	// the '-' to be in the string, but gatt.UUID.String() basically does
+	// hex.EncodeToString.  Instead we have decode the bytes with the hex
+	// decoder and just cast it to a uuid.UUID.
+	bytes, err := hex.DecodeString(u.String())
+	return uuid.UUID(bytes), err
+}
+
+func (b *bleNeighborhood) getAllServices(p gatt.Peripheral) {
+	b.mu.Lock()
+	h := b.pendingHashMap[p.ID()]
+	delete(b.pendingHashMap, p.ID())
+	b.mu.Unlock()
+	defer func() {
+		b.mu.Lock()
+		ch := b.timeoutMap[p.ID()]
+		delete(b.timeoutMap, p.ID())
+		b.mu.Unlock()
+		if ch != nil {
+			close(ch)
+		}
+	}()
+	/*
+		if err := p.SetMTU(500); err != nil {
+			log.Errorf("Failed to set MTU, err: %s", err)
+			return
+		}
+	*/
+
+	ss, err := p.DiscoverServices(nil)
+
+	if err != nil {
+		log.Printf("Failed to discover services, err: %s\n", err)
+		return
+	}
+
+	services := map[string]*bleAdv{}
+	for _, s := range ss {
+		if s.UUID().Equal(attrGAPUUID) {
+			continue
+		}
+
+		cs, err := p.DiscoverCharacteristics(nil, s)
+		if err != nil {
+			log.Printf("Failed to discover characteristics: %s\n", err)
+			continue
+		}
+
+		charMap := map[string][]byte{}
+		for _, c := range cs {
+			if s.UUID().Equal(attrGATTUUID) {
+				continue
+			}
+			u, err := gattUUIDtoUUID(c.UUID())
+			if err != nil {
+				log.Printf("malformed uuid:%v\n", c.UUID().String())
+				continue
+			}
+			key := u.String()
+			v, err := p.ReadLongCharacteristic(c)
+			if err != nil {
+				log.Printf("Failed to read the characteristc (%s): %v\n", key, err)
+				continue
+
+			}
+
+			charMap[key] = v
+		}
+		uid, err := gattUUIDtoUUID(s.UUID())
+		if err != nil {
+			log.Printf("Failed to decode string: %v\n", err)
+		}
+		services[uid.String()] = &bleAdv{
+			serviceUUID: uid,
+			attrs:       charMap,
+			instanceID:  charMap[strings.Replace(instanceUUID, "-", "", -1)],
+		}
+	}
+	b.saveDevice(h, p.ID(), services)
+}
+
+func (b *bleNeighborhood) startBLEService() error {
+	d, err := gatt.NewDevice(gattOptions...)
+	if err != nil {
+		return err
+	}
+	onPeriphDiscovered := func(p gatt.Peripheral, a *gatt.Advertisement, rssi int) {
+		h, v := getVanadiumHash(a)
+		if v && !b.seenHash(p.ID(), h) {
+			log.Println("trying to connect to ", p.Name())
+			// We stop the scanning and advertising so we can connect to the new device.
+			// If one device is changing too frequently we might never find all the devices,
+			// since we restart the scan every time we finish connecting, but hopefully
+			// that is rare.
+			p.Device().StopScanning()
+			p.Device().StopAdvertising()
+			p.Device().Connect(p)
+			b.mu.Lock()
+			cancel := make(chan struct{}, 1)
+			b.timeoutMap[p.ID()] = cancel
+			b.mu.Unlock()
+			go func() {
+				select {
+				case <-time.After(4 * time.Second):
+				case <-cancel:
+				}
+				b.mu.Lock()
+				if b.timeoutMap[p.ID()] == cancel {
+					delete(b.timeoutMap, p.ID())
+				}
+				b.mu.Unlock()
+				p.Device().CancelConnection(p)
+				b.advertiseAndScan()
+			}()
+		}
+	}
+
+	onPeriphConnected := func(p gatt.Peripheral, err error) {
+		if err != nil {
+			log.Println("Failed to connect:", err)
+			return
+		}
+		b.getAllServices(p)
+	}
+
+	onStateChanged := func(d gatt.Device, s gatt.State) {
+		log.Printf("State: %s\n", s)
+		switch s {
+		case gatt.StatePoweredOn:
+			defaultServices := addDefaultServices(b.name)
+			for k, v := range defaultServices {
+				b.services[k] = v
+				d.AddService(v)
+			}
+			b.advertiseAndScan()
+		default:
+			d.StopScanning()
+			d.StopAdvertising()
+		}
+	}
+
+	d.Handle(
+		gatt.CentralConnected(func(c gatt.Central) { log.Printf("Connect: %v\n", c.ID()) }),
+		gatt.CentralDisconnected(func(c gatt.Central) { log.Printf("Disconnected: %v\n", c.ID()) }),
+		gatt.PeripheralDiscovered(onPeriphDiscovered),
+		gatt.PeripheralConnected(onPeriphConnected),
+	)
+
+	d.Init(onStateChanged)
+	b.device = d
+	return nil
+}
+
+func (b *bleNeighborhood) saveDevice(hash string, id string, services map[string]*bleAdv) {
+	b.mu.Lock()
+	defer b.mu.Unlock()
+	if _, found := b.neighborsHashCache[hash]; found {
+		log.Printf("Skipping a new save for the same hash (%s)\n",
+			hash)
+		return
+	}
+	oldAdvs := map[string]*bleAdv{}
+	if oldEntry, ok := b.knownNeighbors[id]; ok {
+		oldAdvs = oldEntry.advertisements
+	}
+
+	newEntry := &bleCacheEntry{
+		id:             id,
+		hash:           hash,
+		advertisements: services,
+		lastSeen:       time.Now(),
+	}
+	b.neighborsHashCache[hash] = newEntry
+	b.knownNeighbors[id] = newEntry
+	for id, oldAdv := range oldAdvs {
+		newValue := services[id]
+		if !reflect.DeepEqual(oldAdv, newValue) {
+			uid := uuid.Parse(id)
+			for _, s := range b.scannersByService[id] {
+				s.handleChange(uid, oldAdv, newValue)
+			}
+		}
+	}
+
+	for id, newAdv := range newEntry.advertisements {
+		if _, ok := oldAdvs[id]; !ok {
+			uid := uuid.Parse(id)
+			for _, s := range b.scannersByService[id] {
+				s.handleChange(uid, nil, newAdv)
+			}
+		}
+	}
+}
+
+func (b *bleNeighborhood) computeAdvertisement() *gatt.AdvPacket {
+	// The hash is:
+	// Hash(Hash(name),Hash(b.endpoints))
+	hasher := fnv.New64()
+	w := func(field string) {
+		tmp := fnv.New64()
+		tmp.Write([]byte(field))
+		hasher.Write(tmp.Sum(nil))
+	}
+	w(b.name)
+	for k, _ := range b.services {
+		w(k)
+	}
+	adv := &gatt.AdvPacket{}
+	adv.AppendManufacturerData(manufacturerId, hasher.Sum(nil))
+	adv.AppendName(b.name)
+	return adv
+}
diff --git a/lib/discovery/plugins/ble/plugin.go b/lib/discovery/plugins/ble/plugin.go
new file mode 100644
index 0000000..6c7d36b
--- /dev/null
+++ b/lib/discovery/plugins/ble/plugin.go
@@ -0,0 +1,60 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// For now, this plugin only works on Linux machines.
+// TODO(bjornick): Make this work on Mac and Android.
+
+package ble
+
+import (
+	"v.io/v23/context"
+	"v.io/x/ref/lib/discovery"
+
+	"github.com/pborman/uuid"
+)
+
+type blePlugin struct {
+	b       *bleNeighborhood
+	trigger *discovery.Trigger
+}
+
+func (b *blePlugin) Advertise(ctx *context.T, ad *discovery.Advertisement) error {
+	b.b.addAdvertisement(newAdvertisment(*ad))
+	b.trigger.Add(func() {
+		b.b.removeService(ad.InstanceUuid)
+	}, ctx.Done())
+	return nil
+}
+
+func (b *blePlugin) Scan(ctx *context.T, serviceUuid uuid.UUID, scan chan<- *discovery.Advertisement) error {
+	ch, id := b.b.addScanner(serviceUuid)
+	drain := func() {
+		for range ch {
+		}
+	}
+	go func() {
+		defer func() {
+			b.b.removeScanner(id)
+			go drain()
+		}()
+	L:
+		for {
+			select {
+			case <-ctx.Done():
+				break L
+			case a := <-ch:
+				scan <- a
+			}
+		}
+	}()
+	return nil
+}
+
+func NewPlugin(name string) (discovery.Plugin, error) {
+	b, err := newBleNeighborhood(name)
+	if err != nil {
+		return nil, err
+	}
+	return &blePlugin{b: b, trigger: discovery.NewTrigger()}, nil
+}
diff --git a/lib/discovery/plugins/ble/scanner.go b/lib/discovery/plugins/ble/scanner.go
new file mode 100644
index 0000000..9b379a1
--- /dev/null
+++ b/lib/discovery/plugins/ble/scanner.go
@@ -0,0 +1,53 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package ble
+
+import (
+	"log"
+	"sync"
+
+	"v.io/x/ref/lib/discovery"
+
+	"github.com/pborman/uuid"
+)
+
+type scanner struct {
+	mu   sync.Mutex
+	uuid uuid.UUID
+	ch   chan *discovery.Advertisement
+	done bool
+}
+
+func (s *scanner) handleChange(id uuid.UUID, oldAdv *bleAdv, newAdv *bleAdv) error {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	if s.done {
+		return nil
+	}
+	if oldAdv != nil {
+		a, err := oldAdv.toDiscoveryAdvertisement()
+		if err != nil {
+			log.Println("failed to convert advertisement:", err)
+		}
+		a.Lost = true
+		s.ch <- a
+	}
+
+	if newAdv != nil {
+		a, err := newAdv.toDiscoveryAdvertisement()
+		if err != nil {
+			return err
+		}
+		s.ch <- a
+	}
+	return nil
+}
+
+func (s *scanner) stop() {
+	s.mu.Lock()
+	s.done = true
+	close(s.ch)
+	s.mu.Unlock()
+}
diff --git a/lib/discovery/plugins/ble/services_darwin.go b/lib/discovery/plugins/ble/services_darwin.go
new file mode 100644
index 0000000..98a2031
--- /dev/null
+++ b/lib/discovery/plugins/ble/services_darwin.go
@@ -0,0 +1,15 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package ble
+
+import "github.com/paypal/gatt"
+
+var gattOptions = []gatt.Option{
+	gatt.MacDeviceRole(gatt.CentralManager),
+}
+
+func addDefaultServices(string) map[string]*gatt.Service {
+	return map[string]*gatt.Service{}
+}
diff --git a/lib/discovery/plugins/ble/services_linux.go b/lib/discovery/plugins/ble/services_linux.go
new file mode 100644
index 0000000..f6987bd
--- /dev/null
+++ b/lib/discovery/plugins/ble/services_linux.go
@@ -0,0 +1,72 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package ble
+
+import (
+	"github.com/paypal/gatt"
+	"github.com/paypal/gatt/linux/cmd"
+)
+
+var (
+	// These constants are taken from:
+	// https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicsHome.aspx
+	attrDeviceNameUUID        = gatt.UUID16(0x2A00)
+	attrAppearanceUUID        = gatt.UUID16(0x2A01)
+	attrPeripheralPrivacyUUID = gatt.UUID16(0x2A02)
+	attrReconnectionAddrUUID  = gatt.UUID16(0x2A03)
+	attrPreferredParamsUUID   = gatt.UUID16(0x2A04)
+
+	attrServiceChangedUUID = gatt.UUID16(0x2A05)
+
+	// https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.gap.appearance.xml
+	gapCharAppearanceGenericComputer = []byte{0x00, 0x80}
+)
+
+func newGapService(name string) *gatt.Service {
+	s := gatt.NewService(attrGAPUUID)
+	s.AddCharacteristic(attrDeviceNameUUID).SetValue([]byte(name))
+	s.AddCharacteristic(attrAppearanceUUID).SetValue(gapCharAppearanceGenericComputer)
+	// Disable peripheral privacy
+	s.AddCharacteristic(attrPeripheralPrivacyUUID).SetValue([]byte{0x00})
+
+	// Make up some value for a required field:
+	// https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicsHome.aspx
+	s.AddCharacteristic(attrReconnectionAddrUUID).SetValue([]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00})
+	// Preferred params are:
+	//   min connection interval: 0x0006 * 1.25ms (7.5 ms)
+	//   max connection interval: 0x0006 * 1.25ms (7.5 ms)
+	//   slave latency: 0
+	//   connection supervision timeout multiplier: 0x07d0 (2000)
+	s.AddCharacteristic(attrPreferredParamsUUID).SetValue([]byte{0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0xd0, 0x07})
+	return s
+}
+
+func newGattService() *gatt.Service {
+	s := gatt.NewService(attrGATTUUID)
+	s.AddCharacteristic(attrServiceChangedUUID).HandleNotifyFunc(
+		func(r gatt.Request, n gatt.Notifier) {})
+	return s
+}
+
+var gattOptions = []gatt.Option{
+	gatt.LnxMaxConnections(1),
+	gatt.LnxDeviceID(-1, true),
+	gatt.LnxSetAdvertisingParameters(&cmd.LESetAdvertisingParameters{
+		// Set an advertising rate of 150ms.  This value is multipled by
+		// 0.625ms to get the actual rate.
+		AdvertisingIntervalMin: 0x00f4,
+		AdvertisingIntervalMax: 0x00f4,
+		AdvertisingChannelMap:  0x7,
+	}),
+}
+
+func addDefaultServices(name string) map[string]*gatt.Service {
+	s1 := newGapService(name)
+	s2 := newGattService()
+	return map[string]*gatt.Service{
+		s1.UUID().String(): s1,
+		s2.UUID().String(): s2,
+	}
+}
diff --git a/lib/discovery/uuid.go b/lib/discovery/uuid.go
index 221ef5a..e18457b 100644
--- a/lib/discovery/uuid.go
+++ b/lib/discovery/uuid.go
@@ -12,6 +12,9 @@
 	// UUID of Vanadium namespace.
 	// Generated from UUID5("00000000-0000-0000-0000-000000000000", "v.io").
 	v23UUID uuid.UUID = uuid.UUID{0x3d, 0xd1, 0xd5, 0xa8, 0x2e, 0xef, 0x58, 0x16, 0xa7, 0x20, 0xf8, 0x8b, 0x9b, 0xcf, 0x6e, 0xe4}
+
+	// Generated from UUID5("00000000-0000-0000-0000-000000000000", "v.io/attrs").
+	v23AttrUUID uuid.UUID = uuid.UUID{0x94, 0x2b, 0x61, 0x64, 0x12, 0x79, 0x5e, 0xb6, 0xb6, 0x43, 0xc9, 0x0c, 0x4c, 0xcc, 0x8a, 0x72}
 )
 
 // NewServiceUUID returns a version 5 UUID for the given interface name.
@@ -24,3 +27,8 @@
 func NewInstanceUUID() uuid.UUID {
 	return uuid.NewRandom()
 }
+
+// NewAttributeUUID returns a version 5 UUID for the given key.
+func NewAttributeUUID(key string) uuid.UUID {
+	return uuid.NewSHA1(v23AttrUUID, []byte(key))
+}
diff --git a/runtime/internal/naming/namespace/mount.go b/runtime/internal/naming/namespace/mount.go
index c431560..f181b08 100644
--- a/runtime/internal/naming/namespace/mount.go
+++ b/runtime/internal/naming/namespace/mount.go
@@ -44,7 +44,7 @@
 			}
 		}
 	}
-	
+
 	me, err := ns.ResolveToMountTable(ctx, name, opts...)
 	if err == nil {
 		copts := append(getCallOpts(opts), options.Preresolved{me})