tsconsensus: add a tsconsensus package
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>
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
// 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user