mirror of
https://github.com/tailscale/tailscale.git
synced 2025-04-23 09:21:41 +00:00

tsconsensus enables tsnet.Server instances to form a consensus. tsconsensus wraps hashicorp/raft with * the ability to do discovery via tailscale tags * inter node communication over tailscale * routing of commands to the leader Updates #14667 Signed-off-by: Fran Bull <fran@tailscale.com>
231 lines
4.9 KiB
Go
231 lines
4.9 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package tsconsensus
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/netip"
|
|
"testing"
|
|
|
|
"tailscale.com/ipn"
|
|
"tailscale.com/ipn/ipnstate"
|
|
"tailscale.com/tailcfg"
|
|
"tailscale.com/types/key"
|
|
"tailscale.com/types/views"
|
|
)
|
|
|
|
type testStatusGetter struct {
|
|
status *ipnstate.Status
|
|
}
|
|
|
|
func (sg testStatusGetter) getStatus(ctx context.Context) (*ipnstate.Status, error) {
|
|
return sg.status, nil
|
|
}
|
|
|
|
const testTag string = "tag:clusterTag"
|
|
|
|
func makeAuthTestPeer(i int, tags views.Slice[string]) *ipnstate.PeerStatus {
|
|
return &ipnstate.PeerStatus{
|
|
ID: tailcfg.StableNodeID(fmt.Sprintf("%d", i)),
|
|
Tags: &tags,
|
|
TailscaleIPs: []netip.Addr{
|
|
netip.AddrFrom4([4]byte{100, 0, 0, byte(i)}),
|
|
netip.MustParseAddr(fmt.Sprintf("fd7a:115c:a1e0:0::%d", i)),
|
|
},
|
|
}
|
|
}
|
|
|
|
func makeAuthTestPeers(tags [][]string) []*ipnstate.PeerStatus {
|
|
peers := make([]*ipnstate.PeerStatus, len(tags))
|
|
for i, ts := range tags {
|
|
peers[i] = makeAuthTestPeer(i, views.SliceOf(ts))
|
|
}
|
|
return peers
|
|
}
|
|
|
|
func authForStatus(s *ipnstate.Status) *authorization {
|
|
return &authorization{
|
|
sg: testStatusGetter{
|
|
status: s,
|
|
},
|
|
tag: testTag,
|
|
}
|
|
}
|
|
|
|
func authForPeers(self *ipnstate.PeerStatus, peers []*ipnstate.PeerStatus) *authorization {
|
|
s := &ipnstate.Status{
|
|
BackendState: ipn.Running.String(),
|
|
Self: self,
|
|
Peer: map[key.NodePublic]*ipnstate.PeerStatus{},
|
|
}
|
|
for _, p := range peers {
|
|
s.Peer[key.NewNode().Public()] = p
|
|
}
|
|
return authForStatus(s)
|
|
}
|
|
|
|
func TestAuthRefreshErrorsNotRunning(t *testing.T) {
|
|
tests := []struct {
|
|
in *ipnstate.Status
|
|
expected string
|
|
}{
|
|
{
|
|
in: nil,
|
|
expected: "no status",
|
|
},
|
|
{
|
|
in: &ipnstate.Status{
|
|
BackendState: "NeedsMachineAuth",
|
|
},
|
|
expected: "ts Server is not running",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.expected, func(t *testing.T) {
|
|
ctx := t.Context()
|
|
a := authForStatus(tt.in)
|
|
err := a.Refresh(ctx)
|
|
if err == nil {
|
|
t.Fatalf("expected err to be non-nil")
|
|
}
|
|
if err.Error() != tt.expected {
|
|
t.Fatalf("expected: %s, got: %s", tt.expected, err.Error())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAuthUnrefreshed(t *testing.T) {
|
|
a := authForStatus(nil)
|
|
if a.AllowsHost(netip.MustParseAddr("100.0.0.1")) {
|
|
t.Fatalf("never refreshed authorization, allowsHost: expected false, got true")
|
|
}
|
|
gotAllowedPeers := a.AllowedPeers()
|
|
if gotAllowedPeers.Len() != 0 {
|
|
t.Fatalf("never refreshed authorization, allowedPeers: expected [], got %v", gotAllowedPeers)
|
|
}
|
|
if a.SelfAllowed() != false {
|
|
t.Fatalf("never refreshed authorization, selfAllowed: expected false got true")
|
|
}
|
|
}
|
|
|
|
func TestAuthAllowsHost(t *testing.T) {
|
|
peerTags := [][]string{
|
|
{"woo"},
|
|
nil,
|
|
{"woo", testTag},
|
|
{testTag},
|
|
}
|
|
peers := makeAuthTestPeers(peerTags)
|
|
|
|
tests := []struct {
|
|
name string
|
|
peerStatus *ipnstate.PeerStatus
|
|
expected bool
|
|
}{
|
|
{
|
|
name: "tagged with different tag",
|
|
peerStatus: peers[0],
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "not tagged",
|
|
peerStatus: peers[1],
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "tags includes testTag",
|
|
peerStatus: peers[2],
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "only tag is testTag",
|
|
peerStatus: peers[3],
|
|
expected: true,
|
|
},
|
|
}
|
|
|
|
a := authForPeers(nil, peers)
|
|
err := a.Refresh(t.Context())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// test we get the expected result for any of the peers TailscaleIPs
|
|
for _, addr := range tt.peerStatus.TailscaleIPs {
|
|
got := a.AllowsHost(addr)
|
|
if got != tt.expected {
|
|
t.Fatalf("allowed for peer with tags: %v, expected: %t, got %t", tt.peerStatus.Tags, tt.expected, got)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAuthAllowedPeers(t *testing.T) {
|
|
ctx := t.Context()
|
|
peerTags := [][]string{
|
|
{"woo"},
|
|
nil,
|
|
{"woo", testTag},
|
|
{testTag},
|
|
}
|
|
peers := makeAuthTestPeers(peerTags)
|
|
a := authForPeers(nil, peers)
|
|
err := a.Refresh(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ps := a.AllowedPeers()
|
|
if ps.Len() != 2 {
|
|
t.Fatalf("expected: 2, got: %d", ps.Len())
|
|
}
|
|
for _, i := range []int{2, 3} {
|
|
if !ps.ContainsFunc(func(p *ipnstate.PeerStatus) bool {
|
|
return p.ID == peers[i].ID
|
|
}) {
|
|
t.Fatalf("expected peers[%d] to be in AllowedPeers because it is tagged with testTag", i)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAuthSelfAllowed(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
in []string
|
|
expected bool
|
|
}{
|
|
{
|
|
name: "self has different tag",
|
|
in: []string{"woo"},
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "selfs tags include testTag",
|
|
in: []string{"woo", testTag},
|
|
expected: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ctx := t.Context()
|
|
self := makeAuthTestPeer(0, views.SliceOf(tt.in))
|
|
a := authForPeers(self, nil)
|
|
err := a.Refresh(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := a.SelfAllowed()
|
|
if got != tt.expected {
|
|
t.Fatalf("expected: %t, got: %t", tt.expected, got)
|
|
}
|
|
})
|
|
}
|
|
}
|