Introduce notion of device groups
diff --git a/core/vpn/in_memory_manager.go b/core/vpn/in_memory_manager.go
index ea9e04b..f23ff43 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,46 +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) *InMemoryManager {
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 := m.genNetworkMap(&d)
- // TODO(giolekva): run this in a goroutine
- for _, peer := range m.devices {
- if peer.PublicKey != d.PublicKey {
- netMap := m.genNetworkMap(peer)
- 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 {
@@ -60,22 +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 := m.genNetworkMap(peer)
- for _, cb := range m.callbacks[peer.PublicKey] {
- cb(netMap)
+ 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
}
}
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()
@@ -94,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 {
vpnIP, err := m.ipm.Get(d.PublicKey)
// NOTE(giolekva): Should not happen as devices must have been already registered and assigned IP address.
@@ -110,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 {
- panic(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
}
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 7919393..fdb29cb 100644
--- a/core/vpn/manager.go
+++ b/core/vpn/manager.go
@@ -10,12 +10,20 @@
// It does enforce device to device ACLs but delegates user authorization to the client.
type Manager interface {
// Registers new device with given public key and name.
- // 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(name string, pubKey types.PublicKey) (*types.NetworkMap, error)
+ // New device is isolated from the rest of the network until it is explicitely added to
+ // an existing group.
+ RegisterDevice(name string, pubKey types.PublicKey) 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 give 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