Skip to content

Commit

Permalink
Replace IP allocation logic (netbirdio#342)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
braginini authored May 29, 2022
1 parent ec39202 commit 87631cb
Show file tree
Hide file tree
Showing 9 changed files with 121 additions and 66 deletions.
2 changes: 1 addition & 1 deletion client/testdata/store.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"Id": "af1c8024-ha40-4ce2-9418-34653101fc3c",
"Net": {
"IP": "100.64.0.0",
"Mask": "/8AAAA=="
"Mask": "//8AAA=="
},
"Dns": null
},
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ require (

require (
fyne.io/fyne/v2 v2.1.4
github.com/c-robinson/iplib v1.0.3
github.com/getlantern/systray v1.2.1
github.com/magiconair/properties v1.8.5
github.com/rs/xid v1.3.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
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/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
Expand Down
21 changes: 8 additions & 13 deletions management/server/account_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,10 +265,6 @@ func TestAccountManager_AddAccount(t *testing.T) {
userId := "account_creator"
expectedPeersSize := 0
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, "")
if err != nil {
Expand All @@ -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))
}

if account.Network.Net.String() != expectedNetwork.String() {
t.Errorf("expected account to have Network = %v, got %v", expectedNetwork.String(), account.Network.Net.String())
ipNet := net.IPNet{IP: net.ParseIP("100.64.0.0"), Mask: net.IPMask{255, 192, 0, 0}}
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())
}
}

Expand Down Expand Up @@ -419,7 +416,6 @@ func TestAccountManager_AddPeer(t *testing.T) {
return
}
expectedPeerKey := key.PublicKey().String()
expectedPeerIP := "100.64.0.1"
expectedSetupKey := setupKey.Key

peer, err := manager.AddPeer(setupKey.Key, "", &Peer{
Expand All @@ -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)
}

if peer.IP.String() != expectedPeerIP {
t.Errorf("expecting just added peer to have IP = %s, got %s", expectedPeerIP, peer.IP.String())
if !account.Network.Net.Contains(peer.IP) {
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 {
Expand Down Expand Up @@ -482,7 +478,6 @@ func TestAccountManager_AddPeerWithUserID(t *testing.T) {
return
}
expectedPeerKey := key.PublicKey().String()
expectedPeerIP := "100.64.0.1"
expectedUserId := userId

peer, err := manager.AddPeer("", userId, &Peer{
Expand All @@ -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)
}

if peer.IP.String() != expectedPeerIP {
t.Errorf("expecting just added peer to have IP = %s, got %s", expectedPeerIP, peer.IP.String())
if !account.Network.Net.Contains(peer.IP) {
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 {
Expand Down Expand Up @@ -596,7 +591,7 @@ func TestGetUsersFromAccount(t *testing.T) {
for _, userInfo := range userInfos {
id := userInfo.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.Email, "")
}
Expand Down
6 changes: 4 additions & 2 deletions management/server/management_proto_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,8 +251,10 @@ func Test_SyncProtocol(t *testing.T) {
t.Fatal("expecting SyncResponse to have NetworkMap with a non-nil PeerConfig")
}

if networkMap.GetPeerConfig().GetAddress() != "100.64.0.1/16" {
t.Fatal("expecting SyncResponse to have NetworkMap with a PeerConfig having valid Address")
expectedIPNet := net.IPNet{IP: net.ParseIP("100.64.0.0"), Mask: net.IPMask{255, 192, 0, 0}}
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 {
Expand Down
10 changes: 4 additions & 6 deletions management/server/management_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,6 @@ var _ = Describe("Management service", func() {
err = encryption.DecryptMessage(serverPubKey, key, encryptedResponse.Body, resp)
Expect(err).NotTo(HaveOccurred())

Expect(resp.PeerConfig.Address).To(BeEquivalentTo("100.64.0.1/16"))

expectedSignalConfig := &mgmtProto.HostConfig{
Uri: "signal.wiretrustee.com:10000",
Protocol: mgmtProto.HostConfig_HTTP,
Expand Down Expand Up @@ -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() {
Specify("all of the 50 peers will get updates of 10 newly registered peers", func() {
initialPeers := 20
Specify("all of the 10 peers will get updates of 10 newly registered peers", func() {
initialPeers := 10
additionalPeers := 10

var peers []wgtypes.Key
Expand Down Expand Up @@ -367,7 +365,7 @@ var _ = Describe("Management service", func() {
key, _ := wgtypes.GenerateKey()
loginPeerWithValidSetupKey(serverPubKey, key, client)
rand.Seed(time.Now().UnixNano())
n := rand.Intn(500)
n := rand.Intn(200)
time.Sleep(time.Duration(n) * time.Millisecond)
}

Expand Down
104 changes: 61 additions & 43 deletions management/server/network.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
package server

import (
"encoding/binary"
"fmt"
"github.com/c-robinson/iplib"
"github.com/rs/xid"
"math/rand"
"net"
"sync"
)

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}
"time"
)

type NetworkMap struct {
Expand All @@ -30,10 +27,19 @@ type Network struct {
}

// 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 {

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{
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: "",
Serial: 0}
}
Expand Down Expand Up @@ -63,51 +69,63 @@ func (n *Network) Copy() *Network {

// AllocatePeerIP pics an available IP from an net.IPNet.
// 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) {
takenIpMap := make(map[string]net.IP)
takenIpMap[ipNet.IP.String()] = ipNet.IP
takenIPMap := make(map[string]struct{})
takenIPMap[ipNet.IP.String()] = struct{}{}
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)
if len(ips) == 0 {
return nil, fmt.Errorf("failed allocating new IP for the ipNet %s - network is out of IPs", ipNet.String())
}

// pick a random IP
s := rand.NewSource(time.Now().Unix())
r := rand.New(s)
intn := r.Intn(len(ips))

return ips[intn], nil
}

// GetNextIP returns the next IP from the given IP address. If the given IP is
// the last IP of a v4 or v6 range, the same IP is returned.
// 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
}
// 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) {

nextIP := make(net.IP, len(ip))
switch len(ip) {
case net.IPv4len:
ipU32 := binary.BigEndian.Uint32(ip)
ipU32++
binary.BigEndian.PutUint32(nextIP, ipU32)
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])
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))
}
return nextIP
}

// remove network address and broadcast address
lenIPs := len(ips)
switch {
case lenIPs < 2:
return ips, lenIPs, nil

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
}
}
}
39 changes: 39 additions & 0 deletions management/server/network_test.go
Original file line number Diff line number Diff line change
@@ -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())
}
}
}
2 changes: 1 addition & 1 deletion management/server/testdata/store.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"Id": "af1c8024-ha40-4ce2-9418-34653101fc3c",
"Net": {
"IP": "100.64.0.0",
"Mask": "/8AAAA=="
"Mask": "//8AAA=="
},
"Dns": null
},
Expand Down

0 comments on commit 87631cb

Please sign in to comment.