Merge pull request #57 from LukeSamkharadze/patch-1
docs(readme): fix grammatical & spelling issues 🎂 🎂
diff --git a/core/vpn/engine/wireguard.go b/core/vpn/engine/wireguard.go
index 6059634..35e4747 100644
--- a/core/vpn/engine/wireguard.go
+++ b/core/vpn/engine/wireguard.go
@@ -1,7 +1,6 @@
package engine
import (
- "encoding/hex"
"fmt"
"log"
@@ -160,9 +159,7 @@
}
func (e *WireguardEngine) DiscoEndpoint() string {
- k := e.DiscoKey()
- discoHex := hex.EncodeToString(k[:])
- return fmt.Sprintf("%s%s", discoHex, controlclient.EndpointDiscoSuffix)
+ return e.DiscoKey().Endpoint()
}
func (e *WireguardEngine) Ping(ip netaddr.IP, cb func(*ipnstate.PingResult)) {
diff --git a/core/vpn/engine/wireguard_test.go b/core/vpn/engine/wireguard_test.go
index 39488e3..04927af 100644
--- a/core/vpn/engine/wireguard_test.go
+++ b/core/vpn/engine/wireguard_test.go
@@ -113,7 +113,8 @@
p := <-ping
if p.Err != "" {
t.Error(p.Err)
+ } else {
+ log.Printf("Ping received: %+v\n", p)
}
- log.Printf("Ping received: %+v\n", p)
}
}
diff --git a/core/vpn/in_memory_manager.go b/core/vpn/in_memory_manager.go
new file mode 100644
index 0000000..a95bfd4
--- /dev/null
+++ b/core/vpn/in_memory_manager.go
@@ -0,0 +1,250 @@
+package vpn
+
+import (
+ "fmt"
+ "strconv"
+ "sync"
+
+ "github.com/giolekva/pcloud/core/vpn/types"
+)
+
+func errorDeviceNotFound(pubKey types.PublicKey) error {
+ 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
+ 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),
+ 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) error {
+ m.lock.Lock()
+ defer m.lock.Unlock()
+ if _, ok := m.keyToDevices[d.PublicKey]; ok {
+ return fmt.Errorf("Device with given public key is already registered: %s", d.PublicKey)
+ }
+ if _, err := m.ipm.New(d.PublicKey); err != nil {
+ return err
+ }
+ m.keyToDevices[d.PublicKey] = &d
+ m.devices = append(m.devices, &d)
+ m.callbacks[d.PublicKey] = make([]NetworkMapChangeCallback, 0)
+ m.deviceToGroups[d.PublicKey] = make([]*types.Group, 0)
+ return nil
+}
+
+func (m *InMemoryManager) RemoveDevice(pubKey types.PublicKey) error {
+ m.lock.Lock()
+ defer m.lock.Unlock()
+ if _, ok := m.keyToDevices[pubKey]; !ok {
+ return errorDeviceNotFound(pubKey)
+ }
+ 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
+ }
+ }
+ 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, err := m.genNetworkMap(d)
+ m.notifyPeers(d, g)
+ return ret, err
+}
+
+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)
+}
+
+func (m *InMemoryManager) GetNetworkMap(pubKey types.PublicKey) (*types.NetworkMap, error) {
+ m.lock.Lock()
+ defer m.lock.Unlock()
+ if d, ok := m.keyToDevices[pubKey]; ok {
+ return m.genNetworkMap(d)
+ }
+ return nil, errorDeviceNotFound(pubKey)
+}
+
+func (m *InMemoryManager) AddNetworkMapChangeCallback(pubKey types.PublicKey, cb NetworkMapChangeCallback) error {
+ m.lock.Lock()
+ defer m.lock.Unlock()
+ if _, ok := m.keyToDevices[pubKey]; ok {
+ m.callbacks[pubKey] = append(m.callbacks[pubKey], cb)
+ }
+ 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, err := m.genNetworkMap(peer)
+ if err != nil {
+ panic(err) // TODO(giolekva): handle properly
+ }
+ 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.
+ // Maybe should return error anyways instead of panic?
+ if err != nil {
+ return nil, err
+ }
+ ret := types.NetworkMap{
+ Self: types.Node{
+ PublicKey: d.PublicKey,
+ DiscoKey: d.DiscoKey,
+ DiscoEndpoint: d.DiscoKey.Endpoint(),
+ IPPort: d.IPPort,
+ VPNIP: vpnIP,
+ },
+ }
+ 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,
+ })
+ }
+ }
+ return &ret, nil
+}
diff --git a/core/vpn/in_memory_manager_test.go b/core/vpn/in_memory_manager_test.go
new file mode 100644
index 0000000..3e3f258
--- /dev/null
+++ b/core/vpn/in_memory_manager_test.go
@@ -0,0 +1,99 @@
+package vpn
+
+import (
+ "log"
+ "testing"
+
+ "inet.af/netaddr"
+ "tailscale.com/ipn/ipnstate"
+
+ "github.com/giolekva/pcloud/core/vpn/engine"
+ "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 {
+ t.Fatal(err)
+ }
+ privKeyB := types.NewPrivateKey()
+ b, err := engine.NewFakeWireguardEngine(12346, privKeyB)
+ if err != nil {
+ t.Fatal(err)
+ }
+ err = m.RegisterDevice(types.DeviceInfo{
+ privKeyA.Public(),
+ a.DiscoKey(),
+ netaddr.MustParseIPPort("127.0.0.1:12345"),
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ m.AddNetworkMapChangeCallback(privKeyA.Public(), func(nm *types.NetworkMap) {
+ log.Printf("a: Received new NetworkMap: %+v\n", nm)
+ if err := a.Configure(nm); err != nil {
+ t.Fatal(err)
+ }
+ })
+ err = m.RegisterDevice(types.DeviceInfo{
+ privKeyB.Public(),
+ b.DiscoKey(),
+ netaddr.MustParseIPPort("127.0.0.1:12346"),
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ m.AddNetworkMapChangeCallback(privKeyB.Public(), func(nm *types.NetworkMap) {
+ log.Printf("b: Received new NetworkMap: %+v\n", nm)
+ if err := b.Configure(nm); err != nil {
+ 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)
+ }
+ ping := make(chan *ipnstate.PingResult, 2)
+ pingCb := func(p *ipnstate.PingResult) {
+ ping <- p
+ }
+ a.Ping(nmb.Self.VPNIP, pingCb)
+ b.Ping(nma.Self.VPNIP, pingCb)
+ for i := 0; i < 2; i++ {
+ p := <-ping
+ if p.Err != "" {
+ t.Error(p.Err)
+ } else {
+ log.Printf("Ping received: %+v\n", p)
+ }
+ }
+ if err := m.RemoveDevice(privKeyA.Public()); err != nil {
+ t.Fatal(err)
+ }
+ b.Ping(nma.Self.VPNIP, pingCb)
+ p := <-ping
+ 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/ip_manager.go b/core/vpn/ip_manager.go
new file mode 100644
index 0000000..6758f3e
--- /dev/null
+++ b/core/vpn/ip_manager.go
@@ -0,0 +1,45 @@
+package vpn
+
+import (
+ "fmt"
+
+ "github.com/giolekva/pcloud/core/vpn/types"
+
+ "inet.af/netaddr"
+)
+
+// TODO(giolekva): Add Disable method which marks given IP as non-usable for future.
+// It will be used when devices get removed from the network, in which case IP should not be reused for safety reasons.
+type IPManager interface {
+ New(pubKey types.PublicKey) (netaddr.IP, error)
+ Get(pubKey types.PublicKey) (netaddr.IP, error)
+}
+
+type SequentialIPManager struct {
+ cur netaddr.IP
+ keyToIP map[types.PublicKey]netaddr.IP
+}
+
+func NewSequentialIPManager(start netaddr.IP) IPManager {
+ return &SequentialIPManager{
+ cur: start,
+ keyToIP: make(map[types.PublicKey]netaddr.IP),
+ }
+}
+
+func (m *SequentialIPManager) New(pubKey types.PublicKey) (netaddr.IP, error) {
+ ip := m.cur
+ if _, ok := m.keyToIP[pubKey]; ok {
+ return netaddr.IP{}, fmt.Errorf("Device with public key %s has already been assigned IP", pubKey)
+ }
+ m.keyToIP[pubKey] = ip
+ m.cur = m.cur.Next()
+ return ip, nil
+}
+
+func (m *SequentialIPManager) Get(pubKey types.PublicKey) (netaddr.IP, error) {
+ if ip, ok := m.keyToIP[pubKey]; ok {
+ return ip, nil
+ }
+ return netaddr.IP{}, fmt.Errorf("Device with public key %s pubKey does not have VPN IP assigned.", pubKey)
+}
diff --git a/core/vpn/ip_manager_test.go b/core/vpn/ip_manager_test.go
new file mode 100644
index 0000000..3a09569
--- /dev/null
+++ b/core/vpn/ip_manager_test.go
@@ -0,0 +1,52 @@
+package vpn
+
+import (
+ "log"
+ "testing"
+
+ "github.com/giolekva/pcloud/core/vpn/types"
+ "inet.af/netaddr"
+)
+
+func TestNewGet(t *testing.T) {
+ m := NewSequentialIPManager(netaddr.MustParseIP("10.0.0.1"))
+ a := types.NewPrivateKey()
+ b := types.NewPrivateKey()
+ ipA, err := m.New(a.Public())
+ if err != nil {
+ log.Fatal(err)
+ }
+ if ipA.String() != "10.0.0.1" {
+ t.Fatalf("Expected 10.0.0.1 Got: %s", ipA.String())
+ }
+ ipA, err = m.Get(a.Public())
+ if err != nil {
+ log.Fatal(err)
+ }
+ if ipA.String() != "10.0.0.1" {
+ t.Fatalf("Expected 10.0.0.1 Got: %s", ipA.String())
+ }
+ ipB, err := m.New(b.Public())
+ if err != nil {
+ log.Fatal(err)
+ }
+ if ipB.String() != "10.0.0.2" {
+ t.Fatalf("Expected 10.0.0.2 Got: %s", ipB.String())
+ }
+ ipB, err = m.Get(b.Public())
+ if err != nil {
+ log.Fatal(err)
+ }
+ if ipB.String() != "10.0.0.2" {
+ t.Fatalf("Expected 10.0.0.2 Got: %s", ipB.String())
+ }
+
+}
+
+func TestGetNonExistentPublicKey(t *testing.T) {
+ m := NewSequentialIPManager(netaddr.MustParseIP("10.0.0.1"))
+ a := types.NewPrivateKey()
+ if _, err := m.Get(a.Public()); err == nil {
+ t.Fatal("Returned IP for non existent public key")
+ }
+}
diff --git a/core/vpn/manager.go b/core/vpn/manager.go
new file mode 100644
index 0000000..d0d3ef0
--- /dev/null
+++ b/core/vpn/manager.go
@@ -0,0 +1,36 @@
+package vpn
+
+import (
+ "github.com/giolekva/pcloud/core/vpn/types"
+)
+
+type NetworkMapChangeCallback func(*types.NetworkMap)
+
+// 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.
+ // 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) 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
+ // private key is able to decrypt and use it.
+ GetNetworkMap(pubKey types.PublicKey) (*types.NetworkMap, error)
+ // AddNetworkMapChangeCallback can be used to receive new network configurations
+ // for a device with given public key.
+ AddNetworkMapChangeCallback(pubKey types.PublicKey, cb NetworkMapChangeCallback) error
+}
diff --git a/core/vpn/types/key.go b/core/vpn/types/key.go
index 1008634..5d02146 100644
--- a/core/vpn/types/key.go
+++ b/core/vpn/types/key.go
@@ -1,6 +1,12 @@
package types
-import "tailscale.com/types/key"
+import (
+ "encoding/hex"
+ "fmt"
+
+ "tailscale.com/control/controlclient"
+ "tailscale.com/types/key"
+)
// Generates new private key.
func NewPrivateKey() PrivateKey {
@@ -11,3 +17,8 @@
func (k PrivateKey) Public() PublicKey {
return PublicKey(key.Private(k).Public())
}
+
+func (k DiscoKey) Endpoint() string {
+ discoHex := hex.EncodeToString(k[:])
+ return fmt.Sprintf("%s%s", discoHex, controlclient.EndpointDiscoSuffix)
+}
diff --git a/core/vpn/types/types.go b/core/vpn/types/types.go
index a8a1ed7..459240b 100644
--- a/core/vpn/types/types.go
+++ b/core/vpn/types/types.go
@@ -15,6 +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