merge
diff --git a/core/vpn/in_memory_manager.go b/core/vpn/in_memory_manager.go
index 5efbfda..3f12635 100644
--- a/core/vpn/in_memory_manager.go
+++ b/core/vpn/in_memory_manager.go
@@ -1,8 +1,8 @@
 package vpn
 
 import (
-	"errors"
 	"fmt"
+	"strconv"
 	"sync"
 
 	"github.com/giolekva/pcloud/core/vpn/types"
@@ -12,53 +12,47 @@
 	return fmt.Errorf("Device not found: %s", pubKey)
 }
 
+func errorGroupNotFound(id types.GroupID) error {
+	return fmt.Errorf("Group not found: %s", id)
+}
+
 type InMemoryManager struct {
-	lock         sync.Mutex
-	devices      []*types.DeviceInfo
-	keyToDevices map[types.PublicKey]*types.DeviceInfo
-	callbacks    map[types.PublicKey][]NetworkMapChangeCallback
-	ipm          IPManager
+	lock           sync.Mutex
+	devices        []*types.DeviceInfo
+	keyToDevices   map[types.PublicKey]*types.DeviceInfo
+	currGroupID    int64
+	groups         map[types.GroupID]*types.Group
+	deviceToGroups map[types.PublicKey][]*types.Group
+	callbacks      map[types.PublicKey][]NetworkMapChangeCallback
+	ipm            IPManager
 }
 
 func NewInMemoryManager(ipm IPManager) Manager {
 	return &InMemoryManager{
-		devices:      make([]*types.DeviceInfo, 0),
-		keyToDevices: make(map[types.PublicKey]*types.DeviceInfo),
-		callbacks:    make(map[types.PublicKey][]NetworkMapChangeCallback),
-		ipm:          ipm,
+		devices:        make([]*types.DeviceInfo, 0),
+		keyToDevices:   make(map[types.PublicKey]*types.DeviceInfo),
+		callbacks:      make(map[types.PublicKey][]NetworkMapChangeCallback),
+		currGroupID:    0,
+		groups:         make(map[types.GroupID]*types.Group),
+		deviceToGroups: make(map[types.PublicKey][]*types.Group),
+		ipm:            ipm,
 	}
 }
 
-func (m *InMemoryManager) RegisterDevice(d types.DeviceInfo) (*types.NetworkMap, error) {
+func (m *InMemoryManager) RegisterDevice(d types.DeviceInfo) error {
 	m.lock.Lock()
 	defer m.lock.Unlock()
 	if _, ok := m.keyToDevices[d.PublicKey]; ok {
-		return nil, errors.New(fmt.Sprintf("Device with given public key is already registered: %s", d.PublicKey))
+		return fmt.Errorf("Device with given public key is already registered: %s", d.PublicKey)
 	}
 	if _, err := m.ipm.New(d.PublicKey); err != nil {
-		return nil, err
+		return err
 	}
 	m.keyToDevices[d.PublicKey] = &d
 	m.devices = append(m.devices, &d)
 	m.callbacks[d.PublicKey] = make([]NetworkMapChangeCallback, 0)
-	ret, err := m.genNetworkMap(&d)
-	if err != nil {
-		return nil, err
-	}
-	// TODO(giolekva): run this in a goroutine
-	for _, peer := range m.devices {
-		if peer.PublicKey != d.PublicKey {
-			netMap, err := m.genNetworkMap(peer)
-			if err != nil {
-				// TODO(giolekva): maybe return netmap of requested device anyways?
-				return nil, err
-			}
-			for _, cb := range m.callbacks[peer.PublicKey] {
-				cb(netMap)
-			}
-		}
-	}
-	return ret, nil
+	m.deviceToGroups[d.PublicKey] = make([]*types.Group, 0)
+	return nil
 }
 
 func (m *InMemoryManager) RemoveDevice(pubKey types.PublicKey) error {
@@ -67,25 +61,124 @@
 	if _, ok := m.keyToDevices[pubKey]; !ok {
 		return errorDeviceNotFound(pubKey)
 	}
-	delete(m.keyToDevices, pubKey) // TODO(giolekva): maybe mark as deleted?
+	for _, g := range m.deviceToGroups[pubKey] {
+		m.removeDeviceFromGroupNoLock(pubKey, g.ID)
+	}
+	delete(m.deviceToGroups, pubKey)
+	delete(m.callbacks, pubKey)
+	found := false
 	for i, peer := range m.devices {
 		if peer.PublicKey == pubKey {
 			m.devices[i] = m.devices[len(m.devices)-1]
 			m.devices = m.devices[:len(m.devices)-1]
+			found = true
+			break
 		}
 	}
-	for _, peer := range m.devices {
-		netMap, err := m.genNetworkMap(peer)
-		if err != nil {
+	if !found {
+		panic("MUST not happen, device not found")
+	}
+	delete(m.keyToDevices, pubKey) // TODO(giolekva): maybe mark as deleted?
+	return nil
+}
+
+func (m *InMemoryManager) CreateGroup(name string) (types.GroupID, error) {
+	m.lock.Lock()
+	defer m.lock.Unlock()
+	id := types.GroupID(strconv.FormatInt(m.currGroupID, 10))
+	m.groups[id] = &types.Group{
+		ID:    id,
+		Name:  name,
+		Peers: make([]*types.DeviceInfo, 0),
+	}
+	m.currGroupID++
+	return id, nil
+}
+
+func (m *InMemoryManager) DeleteGroup(id types.GroupID) error {
+	m.lock.Lock()
+	defer m.lock.Unlock()
+	g, ok := m.groups[id]
+	if !ok {
+		return errorGroupNotFound(id)
+	}
+	// TODO(giolekva): optimize, current implementation calls callbacks group size squared times.
+	for _, peer := range g.Peers {
+		if _, err := m.removeDeviceFromGroupNoLock(peer.PublicKey, id); err != nil {
 			return err
 		}
-		for _, cb := range m.callbacks[peer.PublicKey] {
-			cb(netMap)
-		}
 	}
 	return nil
 }
 
+func (m *InMemoryManager) AddDeviceToGroup(pubKey types.PublicKey, id types.GroupID) (*types.NetworkMap, error) {
+	m.lock.Lock()
+	defer m.lock.Unlock()
+	d, ok := m.keyToDevices[pubKey]
+	if !ok {
+		return nil, errorDeviceNotFound(pubKey)
+	}
+	g, ok := m.groups[id]
+	if !ok {
+		return nil, errorGroupNotFound(id)
+	}
+	groups, ok := m.deviceToGroups[pubKey]
+	if !ok {
+		groups = make([]*types.Group, 1)
+	}
+	// TODO(giolekva): Check if device is already in the group and return error if so.
+	g.Peers = append(g.Peers, d)
+	groups = append(groups, g)
+	m.deviceToGroups[pubKey] = groups
+	ret := m.genNetworkMap(d)
+	m.notifyPeers(d, g)
+	return ret, nil
+}
+
+func (m *InMemoryManager) RemoveDeviceFromGroup(pubKey types.PublicKey, id types.GroupID) (*types.NetworkMap, error) {
+	m.lock.Lock()
+	defer m.lock.Unlock()
+	return m.removeDeviceFromGroupNoLock(pubKey, id)
+}
+
+func (m *InMemoryManager) removeDeviceFromGroupNoLock(pubKey types.PublicKey, id types.GroupID) (*types.NetworkMap, error) {
+	d, ok := m.keyToDevices[pubKey]
+	if !ok {
+		return nil, errorDeviceNotFound(pubKey)
+	}
+	g, ok := m.groups[id]
+	if !ok {
+		return nil, errorGroupNotFound(id)
+	}
+	groups := m.deviceToGroups[pubKey]
+	found := false
+	for i, group := range groups {
+		if id == group.ID {
+			groups[i] = groups[len(groups)-1]
+			groups = groups[:len(groups)-1]
+			m.deviceToGroups[pubKey] = groups
+			found = true
+			break
+		}
+	}
+	if !found {
+		return nil, fmt.Errorf("Device %s is not part of the group %s", pubKey, id)
+	}
+	found = false
+	for i, peer := range g.Peers {
+		if pubKey == peer.PublicKey {
+			g.Peers[i] = g.Peers[len(g.Peers)-1]
+			g.Peers = g.Peers[:len(g.Peers)-1]
+			found = true
+		}
+	}
+	if !found {
+		panic("Should not reach")
+	}
+	m.notifyPeers(d, g)
+	return m.genNetworkMap(d), nil
+}
+
 func (m *InMemoryManager) GetNetworkMap(pubKey types.PublicKey) (*types.NetworkMap, error) {
 	m.lock.Lock()
 	defer m.lock.Unlock()
@@ -104,6 +197,18 @@
 	return errorDeviceNotFound(pubKey)
 }
 
+func (m *InMemoryManager) notifyPeers(d *types.DeviceInfo, g *types.Group) {
+	// TODO(giolekva): maybe run this in a goroutine?
+	for _, peer := range g.Peers {
+		if peer.PublicKey != d.PublicKey {
+			netMap := m.genNetworkMap(peer)
+			for _, cb := range m.callbacks[peer.PublicKey] {
+				cb(netMap)
+			}
+		}
+	}
+}
+
 func (m *InMemoryManager) genNetworkMap(d *types.DeviceInfo) (*types.NetworkMap, error) {
 	vpnIP, err := m.ipm.Get(d.PublicKey)
 	// NOTE(giolekva): Should not happen as devices must have been already registered and assigned IP address.
@@ -120,21 +225,23 @@
 			VPNIP:         vpnIP,
 		},
 	}
-	for _, peer := range m.devices {
-		if d.PublicKey == peer.PublicKey {
-			continue
+	for _, group := range m.deviceToGroups[d.PublicKey] {
+		for _, peer := range group.Peers {
+			if d.PublicKey == peer.PublicKey {
+				continue
+			}
+			vpnIP, err := m.ipm.Get(peer.PublicKey)
+			if err != nil {
+				panic(err)
+			}
+			ret.Peers = append(ret.Peers, types.Node{
+				PublicKey:     peer.PublicKey,
+				DiscoKey:      peer.DiscoKey,
+				DiscoEndpoint: peer.DiscoKey.Endpoint(),
+				IPPort:        peer.IPPort,
+				VPNIP:         vpnIP,
+			})
 		}
-		vpnIP, err := m.ipm.Get(peer.PublicKey)
-		if err != nil {
-			return nil, err
-		}
-		ret.Peers = append(ret.Peers, types.Node{
-			PublicKey:     peer.PublicKey,
-			DiscoKey:      peer.DiscoKey,
-			DiscoEndpoint: peer.DiscoKey.Endpoint(),
-			IPPort:        peer.IPPort,
-			VPNIP:         vpnIP,
-		})
 	}
 	return &ret, nil
 }
diff --git a/core/vpn/in_memory_manager_test.go b/core/vpn/in_memory_manager_test.go
index 880aacc..3e3f258 100644
--- a/core/vpn/in_memory_manager_test.go
+++ b/core/vpn/in_memory_manager_test.go
@@ -11,9 +11,14 @@
 	"github.com/giolekva/pcloud/core/vpn/types"
 )
 
+// TODO(giolekva): split into multiple smaller tests
 func TestTwoPeers(t *testing.T) {
 	ipm := NewSequentialIPManager(netaddr.MustParseIP("10.0.0.1"))
 	m := NewInMemoryManager(ipm)
+	groupId, err := m.CreateGroup("test")
+	if err != nil {
+		t.Fatal(err)
+	}
 	privKeyA := types.NewPrivateKey()
 	a, err := engine.NewFakeWireguardEngine(12345, privKeyA)
 	if err != nil {
@@ -24,7 +29,7 @@
 	if err != nil {
 		t.Fatal(err)
 	}
-	nma, err := m.RegisterDevice(types.DeviceInfo{
+	err = m.RegisterDevice(types.DeviceInfo{
 		privKeyA.Public(),
 		a.DiscoKey(),
 		netaddr.MustParseIPPort("127.0.0.1:12345"),
@@ -38,10 +43,7 @@
 			t.Fatal(err)
 		}
 	})
-	if err := a.Configure(nma); err != nil {
-		t.Fatal(err)
-	}
-	nmb, err := m.RegisterDevice(types.DeviceInfo{
+	err = m.RegisterDevice(types.DeviceInfo{
 		privKeyB.Public(),
 		b.DiscoKey(),
 		netaddr.MustParseIPPort("127.0.0.1:12346"),
@@ -55,6 +57,17 @@
 			t.Fatal(err)
 		}
 	})
+	nma, err := m.AddDeviceToGroup(privKeyA.Public(), groupId)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if err := a.Configure(nma); err != nil {
+		t.Fatal(err)
+	}
+	nmb, err := m.AddDeviceToGroup(privKeyB.Public(), groupId)
+	if err != nil {
+		t.Fatal(err)
+	}
 	if err := b.Configure(nmb); err != nil {
 		t.Fatal(err)
 	}
@@ -80,4 +93,7 @@
 	if p.Err == "" {
 		t.Fatalf("Ping received even after removing device: %+v", p)
 	}
+	if err := m.DeleteGroup(groupId); err != nil {
+		t.Fatal(err)
+	}
 }
diff --git a/core/vpn/manager.go b/core/vpn/manager.go
index 78e8449..d0d3ef0 100644
--- a/core/vpn/manager.go
+++ b/core/vpn/manager.go
@@ -9,13 +9,22 @@
 // Manager interface manages mesh VPN configuration for all the devices registed by all users.
 // It does enforce device to device ACLs but delegates user authorization to the client.
 type Manager interface {
-	// Registers new device..
+	// Registers new device.
 	// Returns VPN network configuration on success and error otherwise.
 	// By default new devices have access to other machines owned by the same user
 	// and a PCloud entrypoint.
-	RegisterDevice(d types.DeviceInfo) (*types.NetworkMap, error)
+	RegisterDevice(d types.DeviceInfo) error
 	// Completely removes device with given public key from the network.
 	RemoveDevice(pubKey types.PublicKey) error
+	// Creates new group with given name and returns it's id.
+	// Name does not have to be unique.
+	CreateGroup(name string) (types.GroupID, error)
+	// Deletes group with given id.
+	DeleteGroup(id types.GroupID) error
+	// Adds device with given public key to the group and returns updated network configuration.
+	AddDeviceToGroup(pubKey types.PublicKey, id types.GroupID) (*types.NetworkMap, error)
+	// Removes device from the group and returns updated network configuration.
+	RemoveDeviceFromGroup(pubKey types.PublicKey, id types.GroupID) (*types.NetworkMap, error)
 	// Returns network configuration for a device with a given public key.
 	// Result of this call must be encrypted with the same public key before
 	// sending it back to the client, so only the owner of it's corresponding
diff --git a/core/vpn/types/types.go b/core/vpn/types/types.go
index c421821..459240b 100644
--- a/core/vpn/types/types.go
+++ b/core/vpn/types/types.go
@@ -15,12 +15,21 @@
 //Public discovery key of the device.
 type DiscoKey wgcfg.Key
 
+// Unique group identifier.
+type GroupID string
+
 type DeviceInfo struct {
 	PublicKey PublicKey
 	DiscoKey  DiscoKey
 	IPPort    netaddr.IPPort
 }
 
+type Group struct {
+	ID    GroupID
+	Name  string
+	Peers []*DeviceInfo
+}
+
 // Represents single node in the network.
 type Node struct {
 	PublicKey     PublicKey