Replace IP allocation logic (#342)

The peer IP allocation logic was allocating sequential peer IP from the 100.64.0.0/10 
address block.
Each account is created with a random subnet from 100.64.0.0/10.
The total amount of potential subnets is 64.
The new logic allocates random peer IP
from the account subnet.
This gives us flexibility to add support for
multi subnet accounts without overlapping IPs.
This commit is contained in:
Misha Bragin 2022-05-29 22:43:39 +02:00 committed by GitHub
parent ec39202590
commit 87631cbc8b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 122 additions and 67 deletions

View File

@ -18,7 +18,7 @@
"Id": "af1c8024-ha40-4ce2-9418-34653101fc3c", "Id": "af1c8024-ha40-4ce2-9418-34653101fc3c",
"Net": { "Net": {
"IP": "100.64.0.0", "IP": "100.64.0.0",
"Mask": "/8AAAA==" "Mask": "//8AAA=="
}, },
"Dns": null "Dns": null
}, },

1
go.mod
View File

@ -29,6 +29,7 @@ require (
require ( require (
fyne.io/fyne/v2 v2.1.4 fyne.io/fyne/v2 v2.1.4
github.com/c-robinson/iplib v1.0.3
github.com/getlantern/systray v1.2.1 github.com/getlantern/systray v1.2.1
github.com/magiconair/properties v1.8.5 github.com/magiconair/properties v1.8.5
github.com/rs/xid v1.3.0 github.com/rs/xid v1.3.0

2
go.sum
View File

@ -70,6 +70,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/c-robinson/iplib v1.0.3 h1:NG0UF0GoEsrC1/vyfX1Lx2Ss7CySWl3KqqXh3q4DdPU=
github.com/c-robinson/iplib v1.0.3/go.mod h1:i3LuuFL1hRT5gFpBRnEydzw8R6yhGkF4szNDIbF8pgo=
github.com/cenkalti/backoff/v4 v4.1.2 h1:6Yo7N8UP2K6LWZnW94DLVSSrbobcWdVzAYOisuDPIFo= github.com/cenkalti/backoff/v4 v4.1.2 h1:6Yo7N8UP2K6LWZnW94DLVSSrbobcWdVzAYOisuDPIFo=
github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=

View File

@ -265,10 +265,6 @@ func TestAccountManager_AddAccount(t *testing.T) {
userId := "account_creator" userId := "account_creator"
expectedPeersSize := 0 expectedPeersSize := 0
expectedSetupKeysSize := 2 expectedSetupKeysSize := 2
expectedNetwork := net.IPNet{
IP: net.IP{100, 64, 0, 0},
Mask: net.IPMask{255, 255, 0, 0},
}
account, err := manager.AddAccount(expectedId, userId, "") account, err := manager.AddAccount(expectedId, userId, "")
if err != nil { if err != nil {
@ -287,8 +283,9 @@ func TestAccountManager_AddAccount(t *testing.T) {
t.Errorf("expected account to have len(SetupKeys) = %v, got %v", expectedSetupKeysSize, len(account.SetupKeys)) t.Errorf("expected account to have len(SetupKeys) = %v, got %v", expectedSetupKeysSize, len(account.SetupKeys))
} }
if account.Network.Net.String() != expectedNetwork.String() { ipNet := net.IPNet{IP: net.ParseIP("100.64.0.0"), Mask: net.IPMask{255, 192, 0, 0}}
t.Errorf("expected account to have Network = %v, got %v", expectedNetwork.String(), account.Network.Net.String()) if !ipNet.Contains(account.Network.Net.IP) {
t.Errorf("expected account's Network to be a subnet of %v, got %v", ipNet.String(), account.Network.Net.String())
} }
} }
@ -419,7 +416,6 @@ func TestAccountManager_AddPeer(t *testing.T) {
return return
} }
expectedPeerKey := key.PublicKey().String() expectedPeerKey := key.PublicKey().String()
expectedPeerIP := "100.64.0.1"
expectedSetupKey := setupKey.Key expectedSetupKey := setupKey.Key
peer, err := manager.AddPeer(setupKey.Key, "", &Peer{ peer, err := manager.AddPeer(setupKey.Key, "", &Peer{
@ -442,8 +438,8 @@ func TestAccountManager_AddPeer(t *testing.T) {
t.Errorf("expecting just added peer to have key = %s, got %s", expectedPeerKey, peer.Key) t.Errorf("expecting just added peer to have key = %s, got %s", expectedPeerKey, peer.Key)
} }
if peer.IP.String() != expectedPeerIP { if !account.Network.Net.Contains(peer.IP) {
t.Errorf("expecting just added peer to have IP = %s, got %s", expectedPeerIP, peer.IP.String()) t.Errorf("expecting just added peer's IP %s to be in a network range %s", peer.IP.String(), account.Network.Net.String())
} }
if peer.SetupKey != expectedSetupKey { if peer.SetupKey != expectedSetupKey {
@ -482,7 +478,6 @@ func TestAccountManager_AddPeerWithUserID(t *testing.T) {
return return
} }
expectedPeerKey := key.PublicKey().String() expectedPeerKey := key.PublicKey().String()
expectedPeerIP := "100.64.0.1"
expectedUserId := userId expectedUserId := userId
peer, err := manager.AddPeer("", userId, &Peer{ peer, err := manager.AddPeer("", userId, &Peer{
@ -505,8 +500,8 @@ func TestAccountManager_AddPeerWithUserID(t *testing.T) {
t.Errorf("expecting just added peer to have key = %s, got %s", expectedPeerKey, peer.Key) t.Errorf("expecting just added peer to have key = %s, got %s", expectedPeerKey, peer.Key)
} }
if peer.IP.String() != expectedPeerIP { if !account.Network.Net.Contains(peer.IP) {
t.Errorf("expecting just added peer to have IP = %s, got %s", expectedPeerIP, peer.IP.String()) t.Errorf("expecting just added peer's IP %s to be in a network range %s", peer.IP.String(), account.Network.Net.String())
} }
if peer.UserID != expectedUserId { if peer.UserID != expectedUserId {
@ -596,7 +591,7 @@ func TestGetUsersFromAccount(t *testing.T) {
for _, userInfo := range userInfos { for _, userInfo := range userInfos {
id := userInfo.ID id := userInfo.ID
assert.Equal(t, userInfo.ID, users[id].Id) assert.Equal(t, userInfo.ID, users[id].Id)
assert.Equal(t, string(userInfo.Role), string(users[id].Role)) assert.Equal(t, userInfo.Role, string(users[id].Role))
assert.Equal(t, userInfo.Name, "") assert.Equal(t, userInfo.Name, "")
assert.Equal(t, userInfo.Email, "") assert.Equal(t, userInfo.Email, "")
} }

View File

@ -251,8 +251,10 @@ func Test_SyncProtocol(t *testing.T) {
t.Fatal("expecting SyncResponse to have NetworkMap with a non-nil PeerConfig") t.Fatal("expecting SyncResponse to have NetworkMap with a non-nil PeerConfig")
} }
if networkMap.GetPeerConfig().GetAddress() != "100.64.0.1/16" { expectedIPNet := net.IPNet{IP: net.ParseIP("100.64.0.0"), Mask: net.IPMask{255, 192, 0, 0}}
t.Fatal("expecting SyncResponse to have NetworkMap with a PeerConfig having valid Address") ip, _, _ := net.ParseCIDR(networkMap.GetPeerConfig().GetAddress())
if !expectedIPNet.Contains(ip) {
t.Fatalf("expecting SyncResponse to have NetworkMap with a PeerConfig having valid IP address %s", networkMap.GetPeerConfig().GetAddress())
} }
if networkMap.GetSerial() <= 0 { if networkMap.GetSerial() <= 0 {

View File

@ -107,8 +107,6 @@ var _ = Describe("Management service", func() {
err = encryption.DecryptMessage(serverPubKey, key, encryptedResponse.Body, resp) err = encryption.DecryptMessage(serverPubKey, key, encryptedResponse.Body, resp)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(resp.PeerConfig.Address).To(BeEquivalentTo("100.64.0.1/16"))
expectedSignalConfig := &mgmtProto.HostConfig{ expectedSignalConfig := &mgmtProto.HostConfig{
Uri: "signal.wiretrustee.com:10000", Uri: "signal.wiretrustee.com:10000",
Protocol: mgmtProto.HostConfig_HTTP, Protocol: mgmtProto.HostConfig_HTTP,
@ -308,10 +306,10 @@ var _ = Describe("Management service", func() {
}) })
}) })
Context("when there are 50 peers registered under one account", func() { Context("when there are 10 peers registered under one account", func() {
Context("when there are 10 more peers registered under the same account", func() { Context("when there are 10 more peers registered under the same account", func() {
Specify("all of the 50 peers will get updates of 10 newly registered peers", func() { Specify("all of the 10 peers will get updates of 10 newly registered peers", func() {
initialPeers := 20 initialPeers := 10
additionalPeers := 10 additionalPeers := 10
var peers []wgtypes.Key var peers []wgtypes.Key
@ -367,7 +365,7 @@ var _ = Describe("Management service", func() {
key, _ := wgtypes.GenerateKey() key, _ := wgtypes.GenerateKey()
loginPeerWithValidSetupKey(serverPubKey, key, client) loginPeerWithValidSetupKey(serverPubKey, key, client)
rand.Seed(time.Now().UnixNano()) rand.Seed(time.Now().UnixNano())
n := rand.Intn(500) n := rand.Intn(200)
time.Sleep(time.Duration(n) * time.Millisecond) time.Sleep(time.Duration(n) * time.Millisecond)
} }

View File

@ -1,16 +1,13 @@
package server package server
import ( import (
"encoding/binary"
"fmt" "fmt"
"github.com/c-robinson/iplib"
"github.com/rs/xid" "github.com/rs/xid"
"math/rand"
"net" "net"
"sync" "sync"
) "time"
var (
upperIPv4 = []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff, 0xff, 255, 255, 255, 255}
upperIPv6 = []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}
) )
type NetworkMap struct { type NetworkMap struct {
@ -30,10 +27,19 @@ type Network struct {
} }
// NewNetwork creates a new Network initializing it with a Serial=0 // NewNetwork creates a new Network initializing it with a Serial=0
// It takes a random /16 subnet from 100.64.0.0/10 (64 different subnets)
func NewNetwork() *Network { func NewNetwork() *Network {
n := iplib.NewNet4(net.ParseIP("100.64.0.0"), 10)
sub, _ := n.Subnet(16)
s := rand.NewSource(time.Now().Unix())
r := rand.New(s)
intn := r.Intn(len(sub))
return &Network{ return &Network{
Id: xid.New().String(), Id: xid.New().String(),
Net: net.IPNet{IP: net.ParseIP("100.64.0.0"), Mask: net.IPMask{255, 255, 0, 0}}, Net: sub[intn].IPNet,
Dns: "", Dns: "",
Serial: 0} Serial: 0}
} }
@ -63,51 +69,63 @@ func (n *Network) Copy() *Network {
// AllocatePeerIP pics an available IP from an net.IPNet. // AllocatePeerIP pics an available IP from an net.IPNet.
// This method considers already taken IPs and reuses IPs if there are gaps in takenIps // This method considers already taken IPs and reuses IPs if there are gaps in takenIps
// E.g. if ipNet=100.30.0.0/16 and takenIps=[100.30.0.1, 100.30.0.5] then the result would be 100.30.0.2 // E.g. if ipNet=100.30.0.0/16 and takenIps=[100.30.0.1, 100.30.0.4] then the result would be 100.30.0.2 or 100.30.0.3
func AllocatePeerIP(ipNet net.IPNet, takenIps []net.IP) (net.IP, error) { func AllocatePeerIP(ipNet net.IPNet, takenIps []net.IP) (net.IP, error) {
takenIpMap := make(map[string]net.IP) takenIPMap := make(map[string]struct{})
takenIpMap[ipNet.IP.String()] = ipNet.IP takenIPMap[ipNet.IP.String()] = struct{}{}
for _, ip := range takenIps { for _, ip := range takenIps {
takenIpMap[ip.String()] = ip takenIPMap[ip.String()] = struct{}{}
}
for ip := ipNet.IP.Mask(ipNet.Mask); ipNet.Contains(ip); ip = GetNextIP(ip) {
if _, ok := takenIpMap[ip.String()]; !ok {
return ip, nil
}
} }
ips, _, err := generateIPs(&ipNet, takenIPMap)
if err != nil {
return nil, fmt.Errorf("failed allocating new IP for the ipNet %s and takenIps %s", ipNet.String(), takenIps) return nil, fmt.Errorf("failed allocating new IP for the ipNet %s and takenIps %s", ipNet.String(), takenIps)
} }
// GetNextIP returns the next IP from the given IP address. If the given IP is if len(ips) == 0 {
// the last IP of a v4 or v6 range, the same IP is returned. return nil, fmt.Errorf("failed allocating new IP for the ipNet %s - network is out of IPs", ipNet.String())
// Credits to Cilium team.
// Copyright 2017-2020 Authors of Cilium
func GetNextIP(ip net.IP) net.IP {
if ip.Equal(upperIPv4) || ip.Equal(upperIPv6) {
return ip
} }
nextIP := make(net.IP, len(ip)) // pick a random IP
switch len(ip) { s := rand.NewSource(time.Now().Unix())
case net.IPv4len: r := rand.New(s)
ipU32 := binary.BigEndian.Uint32(ip) intn := r.Intn(len(ips))
ipU32++
binary.BigEndian.PutUint32(nextIP, ipU32) return ips[intn], nil
return nextIP
case net.IPv6len:
ipU64 := binary.BigEndian.Uint64(ip[net.IPv6len/2:])
ipU64++
binary.BigEndian.PutUint64(nextIP[net.IPv6len/2:], ipU64)
if ipU64 == 0 {
ipU64 = binary.BigEndian.Uint64(ip[:net.IPv6len/2])
ipU64++
binary.BigEndian.PutUint64(nextIP[:net.IPv6len/2], ipU64)
} else {
copy(nextIP[:net.IPv6len/2], ip[:net.IPv6len/2])
} }
return nextIP
// generateIPs generates a list of all possible IPs of the given network excluding IPs specified in the exclusion list
func generateIPs(ipNet *net.IPNet, exclusions map[string]struct{}) ([]net.IP, int, error) {
var ips []net.IP
for ip := ipNet.IP.Mask(ipNet.Mask); ipNet.Contains(ip); incIP(ip) {
if _, ok := exclusions[ip.String()]; !ok && ip[3] != 0 {
ips = append(ips, copyIP(ip))
}
}
// remove network address and broadcast address
lenIPs := len(ips)
switch {
case lenIPs < 2:
return ips, lenIPs, nil
default: default:
return ip return ips[1 : len(ips)-1], lenIPs - 2, nil
}
}
func copyIP(ip net.IP) net.IP {
dup := make(net.IP, len(ip))
copy(dup, ip)
return dup
}
func incIP(ip net.IP) {
for j := len(ip) - 1; j >= 0; j-- {
ip[j]++
if ip[j] > 0 {
break
}
} }
} }

View File

@ -0,0 +1,39 @@
package server
import (
"github.com/stretchr/testify/assert"
"net"
"testing"
)
func TestNewNetwork(t *testing.T) {
network := NewNetwork()
// generated net should be a subnet of a larger 100.64.0.0/10 net
ipNet := net.IPNet{IP: net.ParseIP("100.64.0.0"), Mask: net.IPMask{255, 192, 0, 0}}
assert.Equal(t, ipNet.Contains(network.Net.IP), true)
}
func TestAllocatePeerIP(t *testing.T) {
ipNet := net.IPNet{IP: net.ParseIP("100.64.0.0"), Mask: net.IPMask{255, 255, 255, 0}}
var ips []net.IP
for i := 0; i < 253; i++ {
ip, err := AllocatePeerIP(ipNet, ips)
if err != nil {
t.Fatal(err)
}
ips = append(ips, ip)
}
assert.Len(t, ips, 253)
uniq := make(map[string]struct{})
for _, ip := range ips {
if _, ok := uniq[ip.String()]; !ok {
uniq[ip.String()] = struct{}{}
} else {
t.Errorf("found duplicate IP %s", ip.String())
}
}
}

View File

@ -21,7 +21,7 @@
"Id": "af1c8024-ha40-4ce2-9418-34653101fc3c", "Id": "af1c8024-ha40-4ce2-9418-34653101fc3c",
"Net": { "Net": {
"IP": "100.64.0.0", "IP": "100.64.0.0",
"Mask": "/8AAAA==" "Mask": "//8AAA=="
}, },
"Dns": null "Dns": null
}, },