mirror of
https://github.com/juanfont/headscale.git
synced 2026-11-01 03:04:07 +09:00
IPAllocator.Next took the allocator mutex once per address family. When the IPv6 allocation failed, the IPv4 address had already been added to usedIPs and prev4 had moved past it, so the address stayed reserved until headscale restarted even though no node held it. Allocate both families under a single critical section, and on an IPv6 failure remove the IPv4 address from usedIPs and restore prev4.
575 lines
11 KiB
Go
575 lines
11 KiB
Go
package db
|
|
|
|
import (
|
|
"fmt"
|
|
"net/netip"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
"github.com/juanfont/headscale/hscontrol/types"
|
|
"github.com/juanfont/headscale/hscontrol/util"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"tailscale.com/net/tsaddr"
|
|
)
|
|
|
|
var mpp = func(pref string) *netip.Prefix {
|
|
p := netip.MustParsePrefix(pref)
|
|
return &p
|
|
}
|
|
|
|
var na = netip.MustParseAddr
|
|
|
|
var nap = func(pref string) *netip.Addr {
|
|
n := na(pref)
|
|
return &n
|
|
}
|
|
|
|
func TestIPAllocatorSequential(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
dbFunc func() *HSDatabase
|
|
|
|
prefix4 *netip.Prefix
|
|
prefix6 *netip.Prefix
|
|
getCount int
|
|
want4 []netip.Addr
|
|
want6 []netip.Addr
|
|
}{
|
|
{
|
|
name: "simple",
|
|
dbFunc: func() *HSDatabase {
|
|
return nil
|
|
},
|
|
|
|
prefix4: mpp("100.64.0.0/10"),
|
|
prefix6: mpp("fd7a:115c:a1e0::/48"),
|
|
|
|
getCount: 1,
|
|
|
|
want4: []netip.Addr{
|
|
na("100.64.0.1"),
|
|
},
|
|
want6: []netip.Addr{
|
|
na("fd7a:115c:a1e0::1"),
|
|
},
|
|
},
|
|
{
|
|
name: "simple-v4",
|
|
dbFunc: func() *HSDatabase {
|
|
return nil
|
|
},
|
|
|
|
prefix4: mpp("100.64.0.0/10"),
|
|
|
|
getCount: 1,
|
|
|
|
want4: []netip.Addr{
|
|
na("100.64.0.1"),
|
|
},
|
|
},
|
|
{
|
|
name: "simple-v6",
|
|
dbFunc: func() *HSDatabase {
|
|
return nil
|
|
},
|
|
|
|
prefix6: mpp("fd7a:115c:a1e0::/48"),
|
|
|
|
getCount: 1,
|
|
|
|
want6: []netip.Addr{
|
|
na("fd7a:115c:a1e0::1"),
|
|
},
|
|
},
|
|
{
|
|
name: "simple-with-db",
|
|
dbFunc: func() *HSDatabase {
|
|
db := dbForTest(t)
|
|
user := types.User{Name: ""}
|
|
db.DB.Save(&user)
|
|
|
|
db.DB.Save(&types.Node{
|
|
User: &user,
|
|
IPv4: nap("100.64.0.1"),
|
|
IPv6: nap("fd7a:115c:a1e0::1"),
|
|
})
|
|
|
|
return db
|
|
},
|
|
|
|
prefix4: mpp("100.64.0.0/10"),
|
|
prefix6: mpp("fd7a:115c:a1e0::/48"),
|
|
|
|
getCount: 1,
|
|
|
|
want4: []netip.Addr{
|
|
na("100.64.0.2"),
|
|
},
|
|
want6: []netip.Addr{
|
|
na("fd7a:115c:a1e0::2"),
|
|
},
|
|
},
|
|
{
|
|
name: "before-after-free-middle-in-db",
|
|
dbFunc: func() *HSDatabase {
|
|
db := dbForTest(t)
|
|
user := types.User{Name: ""}
|
|
db.DB.Save(&user)
|
|
|
|
db.DB.Save(&types.Node{
|
|
User: &user,
|
|
IPv4: nap("100.64.0.2"),
|
|
IPv6: nap("fd7a:115c:a1e0::2"),
|
|
})
|
|
|
|
return db
|
|
},
|
|
|
|
prefix4: mpp("100.64.0.0/10"),
|
|
prefix6: mpp("fd7a:115c:a1e0::/48"),
|
|
|
|
getCount: 2,
|
|
|
|
want4: []netip.Addr{
|
|
na("100.64.0.1"),
|
|
na("100.64.0.3"),
|
|
},
|
|
want6: []netip.Addr{
|
|
na("fd7a:115c:a1e0::1"),
|
|
na("fd7a:115c:a1e0::3"),
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
db := tt.dbFunc()
|
|
|
|
alloc, _ := NewIPAllocator(
|
|
db,
|
|
tt.prefix4,
|
|
tt.prefix6,
|
|
types.IPAllocationStrategySequential,
|
|
)
|
|
|
|
var (
|
|
got4s []netip.Addr
|
|
got6s []netip.Addr
|
|
)
|
|
|
|
for range tt.getCount {
|
|
got4, got6, err := alloc.Next()
|
|
if err != nil {
|
|
t.Fatalf("allocating next IP: %s", err)
|
|
}
|
|
|
|
if got4 != nil {
|
|
got4s = append(got4s, *got4)
|
|
}
|
|
|
|
if got6 != nil {
|
|
got6s = append(got6s, *got6)
|
|
}
|
|
}
|
|
|
|
if diff := cmp.Diff(tt.want4, got4s, util.Comparers...); diff != "" {
|
|
t.Errorf("IPAllocator 4s unexpected result (-want +got):\n%s", diff)
|
|
}
|
|
|
|
if diff := cmp.Diff(tt.want6, got6s, util.Comparers...); diff != "" {
|
|
t.Errorf("IPAllocator 6s unexpected result (-want +got):\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIPAllocatorRandom(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
dbFunc func() *HSDatabase
|
|
|
|
getCount int
|
|
|
|
prefix4 *netip.Prefix
|
|
prefix6 *netip.Prefix
|
|
want4 bool
|
|
want6 bool
|
|
}{
|
|
{
|
|
name: "simple",
|
|
dbFunc: func() *HSDatabase {
|
|
return nil
|
|
},
|
|
|
|
prefix4: mpp("100.64.0.0/10"),
|
|
prefix6: mpp("fd7a:115c:a1e0::/48"),
|
|
|
|
getCount: 1,
|
|
|
|
want4: true,
|
|
want6: true,
|
|
},
|
|
{
|
|
name: "simple-v4",
|
|
dbFunc: func() *HSDatabase {
|
|
return nil
|
|
},
|
|
|
|
prefix4: mpp("100.64.0.0/10"),
|
|
|
|
getCount: 1,
|
|
|
|
want4: true,
|
|
want6: false,
|
|
},
|
|
{
|
|
name: "simple-v6",
|
|
dbFunc: func() *HSDatabase {
|
|
return nil
|
|
},
|
|
|
|
prefix6: mpp("fd7a:115c:a1e0::/48"),
|
|
|
|
getCount: 1,
|
|
|
|
want4: false,
|
|
want6: true,
|
|
},
|
|
{
|
|
name: "generate-lots-of-random",
|
|
dbFunc: func() *HSDatabase {
|
|
return nil
|
|
},
|
|
|
|
prefix4: mpp("100.64.0.0/10"),
|
|
prefix6: mpp("fd7a:115c:a1e0::/48"),
|
|
|
|
getCount: 1000,
|
|
|
|
want4: true,
|
|
want6: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
db := tt.dbFunc()
|
|
|
|
alloc, _ := NewIPAllocator(db, tt.prefix4, tt.prefix6, types.IPAllocationStrategyRandom)
|
|
|
|
for range tt.getCount {
|
|
got4, got6, err := alloc.Next()
|
|
if err != nil {
|
|
t.Fatalf("allocating next IP: %s", err)
|
|
}
|
|
|
|
t.Logf("addrs ipv4: %v, ipv6: %v", got4, got6)
|
|
|
|
if tt.want4 {
|
|
if got4 == nil {
|
|
t.Fatalf("expected ipv4 addr, got nil")
|
|
}
|
|
}
|
|
|
|
if tt.want6 {
|
|
if got6 == nil {
|
|
t.Fatalf("expected ipv4 addr, got nil")
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBackfillIPAddresses(t *testing.T) {
|
|
fullNodeP := func(i int) *types.Node {
|
|
v4 := fmt.Sprintf("100.64.0.%d", i)
|
|
v6 := fmt.Sprintf("fd7a:115c:a1e0::%d", i)
|
|
|
|
return &types.Node{
|
|
IPv4: nap(v4),
|
|
IPv6: nap(v6),
|
|
}
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
dbFunc func() *HSDatabase
|
|
|
|
prefix4 *netip.Prefix
|
|
prefix6 *netip.Prefix
|
|
want types.Nodes
|
|
}{
|
|
{
|
|
name: "simple-backfill-ipv6",
|
|
dbFunc: func() *HSDatabase {
|
|
db := dbForTest(t)
|
|
user := types.User{Name: ""}
|
|
db.DB.Save(&user)
|
|
|
|
db.DB.Save(&types.Node{
|
|
User: &user,
|
|
IPv4: nap("100.64.0.1"),
|
|
})
|
|
|
|
return db
|
|
},
|
|
|
|
prefix4: mpp("100.64.0.0/10"),
|
|
prefix6: mpp("fd7a:115c:a1e0::/48"),
|
|
|
|
want: types.Nodes{
|
|
&types.Node{
|
|
IPv4: nap("100.64.0.1"),
|
|
IPv6: nap("fd7a:115c:a1e0::1"),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "simple-backfill-ipv4",
|
|
dbFunc: func() *HSDatabase {
|
|
db := dbForTest(t)
|
|
user := types.User{Name: ""}
|
|
db.DB.Save(&user)
|
|
|
|
db.DB.Save(&types.Node{
|
|
User: &user,
|
|
IPv6: nap("fd7a:115c:a1e0::1"),
|
|
})
|
|
|
|
return db
|
|
},
|
|
|
|
prefix4: mpp("100.64.0.0/10"),
|
|
prefix6: mpp("fd7a:115c:a1e0::/48"),
|
|
|
|
want: types.Nodes{
|
|
&types.Node{
|
|
IPv4: nap("100.64.0.1"),
|
|
IPv6: nap("fd7a:115c:a1e0::1"),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "simple-backfill-remove-ipv6",
|
|
dbFunc: func() *HSDatabase {
|
|
db := dbForTest(t)
|
|
user := types.User{Name: ""}
|
|
db.DB.Save(&user)
|
|
|
|
db.DB.Save(&types.Node{
|
|
User: &user,
|
|
IPv4: nap("100.64.0.1"),
|
|
IPv6: nap("fd7a:115c:a1e0::1"),
|
|
})
|
|
|
|
return db
|
|
},
|
|
|
|
prefix4: mpp("100.64.0.0/10"),
|
|
|
|
want: types.Nodes{
|
|
&types.Node{
|
|
IPv4: nap("100.64.0.1"),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "simple-backfill-remove-ipv4",
|
|
dbFunc: func() *HSDatabase {
|
|
db := dbForTest(t)
|
|
user := types.User{Name: ""}
|
|
db.DB.Save(&user)
|
|
|
|
db.DB.Save(&types.Node{
|
|
User: &user,
|
|
IPv4: nap("100.64.0.1"),
|
|
IPv6: nap("fd7a:115c:a1e0::1"),
|
|
})
|
|
|
|
return db
|
|
},
|
|
|
|
prefix6: mpp("fd7a:115c:a1e0::/48"),
|
|
|
|
want: types.Nodes{
|
|
&types.Node{
|
|
IPv6: nap("fd7a:115c:a1e0::1"),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "multi-backfill-ipv6",
|
|
dbFunc: func() *HSDatabase {
|
|
db := dbForTest(t)
|
|
user := types.User{Name: ""}
|
|
db.DB.Save(&user)
|
|
|
|
db.DB.Save(&types.Node{
|
|
User: &user,
|
|
IPv4: nap("100.64.0.1"),
|
|
})
|
|
db.DB.Save(&types.Node{
|
|
User: &user,
|
|
IPv4: nap("100.64.0.2"),
|
|
})
|
|
db.DB.Save(&types.Node{
|
|
User: &user,
|
|
IPv4: nap("100.64.0.3"),
|
|
})
|
|
db.DB.Save(&types.Node{
|
|
User: &user,
|
|
IPv4: nap("100.64.0.4"),
|
|
})
|
|
|
|
return db
|
|
},
|
|
|
|
prefix4: mpp("100.64.0.0/10"),
|
|
prefix6: mpp("fd7a:115c:a1e0::/48"),
|
|
|
|
want: types.Nodes{
|
|
fullNodeP(1),
|
|
fullNodeP(2),
|
|
fullNodeP(3),
|
|
fullNodeP(4),
|
|
},
|
|
},
|
|
}
|
|
|
|
comps := append(util.Comparers, cmpopts.IgnoreFields(types.Node{},
|
|
"ID",
|
|
"User",
|
|
"UserID",
|
|
"Endpoints",
|
|
"Hostinfo",
|
|
"CreatedAt",
|
|
"UpdatedAt",
|
|
))
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
db := tt.dbFunc()
|
|
|
|
alloc, err := NewIPAllocator(
|
|
db,
|
|
tt.prefix4,
|
|
tt.prefix6,
|
|
types.IPAllocationStrategySequential,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("failed to set up ip alloc: %s", err)
|
|
}
|
|
|
|
logs, err := db.BackfillNodeIPs(alloc)
|
|
if err != nil {
|
|
t.Fatalf("failed to backfill: %s", err)
|
|
}
|
|
|
|
t.Logf("backfill log: \n%s", strings.Join(logs, "\n"))
|
|
|
|
got, err := db.ListNodes()
|
|
if err != nil {
|
|
t.Fatalf("failed to get nodes: %s", err)
|
|
}
|
|
|
|
if diff := cmp.Diff(tt.want, got, comps...); diff != "" {
|
|
t.Errorf("Backfill unexpected result (-want +got):\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIPAllocatorNextNoReservedIPs(t *testing.T) {
|
|
db, err := newSQLiteTestDB()
|
|
require.NoError(t, err)
|
|
|
|
defer db.Close()
|
|
|
|
alloc, err := NewIPAllocator(
|
|
db,
|
|
new(tsaddr.CGNATRange()),
|
|
new(tsaddr.TailscaleULARange()),
|
|
types.IPAllocationStrategySequential,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("failed to set up ip alloc: %s", err)
|
|
}
|
|
|
|
// Validate that we do not give out 100.100.100.100
|
|
nextQuad100, err := alloc.next(na("100.100.100.99"), new(tsaddr.CGNATRange()))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, na("100.100.100.101"), *nextQuad100)
|
|
|
|
// Validate that we do not give out fd7a:115c:a1e0::53
|
|
nextQuad100v6, err := alloc.next(na("fd7a:115c:a1e0::52"), new(tsaddr.TailscaleULARange()))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, na("fd7a:115c:a1e0::54"), *nextQuad100v6)
|
|
|
|
// Validate that we do not give out fd7a:115c:a1e0::53
|
|
nextChrome, err := alloc.next(na("100.115.91.255"), new(tsaddr.CGNATRange()))
|
|
t.Logf("chrome: %s", nextChrome.String())
|
|
require.NoError(t, err)
|
|
assert.Equal(t, na("100.115.94.0"), *nextChrome)
|
|
}
|
|
|
|
// TestIPAllocatorNextIPv6OnlyExhausted pins that an IPv6 failure with no IPv4
|
|
// prefix has no IPv4 address to release.
|
|
func TestIPAllocatorNextIPv6OnlyExhausted(t *testing.T) {
|
|
alloc, err := NewIPAllocator(nil, nil, mpp("fd7a:115c:a1e0::/127"), types.IPAllocationStrategySequential)
|
|
require.NoError(t, err)
|
|
|
|
_, _, err = alloc.Next()
|
|
require.ErrorIs(t, err, ErrCouldNotAllocateIP)
|
|
}
|
|
|
|
// TestIPAllocatorNextReleasesIPv4WhenIPv6Fails uses a /127 because both of its
|
|
// addresses are reserved as network and broadcast, so IPv6 always fails. Both
|
|
// /30 addresses must stay free afterwards; the prev4 restore only shows under
|
|
// sequential, as random ignores prev4.
|
|
func TestIPAllocatorNextReleasesIPv4WhenIPv6Fails(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
strategy types.IPAllocationStrategy
|
|
}{
|
|
{
|
|
name: "sequential",
|
|
strategy: types.IPAllocationStrategySequential,
|
|
},
|
|
{
|
|
name: "random",
|
|
strategy: types.IPAllocationStrategyRandom,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
alloc, err := NewIPAllocator(
|
|
nil,
|
|
mpp("100.64.0.0/30"),
|
|
mpp("fd7a:115c:a1e0::/127"),
|
|
tt.strategy,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
got4, got6, err := alloc.Next()
|
|
require.ErrorIs(t, err, ErrCouldNotAllocateIP)
|
|
assert.Nil(t, got4)
|
|
assert.Nil(t, got6)
|
|
|
|
got := make([]netip.Addr, 0, 2)
|
|
|
|
for range 2 {
|
|
ip, err := alloc.allocateNext(&alloc.prev4, alloc.prefix4)
|
|
require.NoError(t, err)
|
|
|
|
got = append(got, *ip)
|
|
}
|
|
|
|
assert.ElementsMatch(t, []netip.Addr{na("100.64.0.1"), na("100.64.0.2")}, got)
|
|
})
|
|
}
|
|
}
|