feat: establish NekoNest Cloud control and relay
This commit is contained in:
@@ -0,0 +1,316 @@
|
||||
package registry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/klarkxy/nekonest-cloud/relay/internal/authsnapshot"
|
||||
"github.com/klarkxy/nekonest-cloud/relay/internal/controlplane"
|
||||
)
|
||||
|
||||
type snapshotSource struct{}
|
||||
|
||||
func (snapshotSource) AuthorizationSnapshot(context.Context, string, int64) (controlplane.SnapshotResponse, error) {
|
||||
return controlplane.SnapshotResponse{}, context.Canceled
|
||||
}
|
||||
func (snapshotSource) AuthorizationDelta(context.Context, string, int64) (controlplane.DeltaResponse, error) {
|
||||
return controlplane.DeltaResponse{}, context.Canceled
|
||||
}
|
||||
|
||||
type signer struct {
|
||||
public ed25519.PublicKey
|
||||
private ed25519.PrivateKey
|
||||
keyring authsnapshot.Keyring
|
||||
}
|
||||
|
||||
func newSigner(t *testing.T, now time.Time) signer {
|
||||
t.Helper()
|
||||
public, private, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return signer{
|
||||
public: public,
|
||||
private: private,
|
||||
keyring: authsnapshot.Keyring{"key": {
|
||||
PublicKey: public, NotBefore: now.Add(-time.Hour), RetainUntil: now.Add(time.Hour),
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
func (signer signer) response(t *testing.T, now time.Time, tenantID, deviceID string) controlplane.SnapshotResponse {
|
||||
t.Helper()
|
||||
key := base64.RawURLEncoding.EncodeToString(make([]byte, 32))
|
||||
payload := authsnapshot.Payload{
|
||||
SnapshotVersion: 1,
|
||||
TenantID: tenantID,
|
||||
TenantStatus: "active",
|
||||
HomeRegion: "test",
|
||||
RelayNodeID: "node_test",
|
||||
PlacementGeneration: 1,
|
||||
AuthorizationRevision: 1,
|
||||
Devices: []authsnapshot.Device{{
|
||||
DeviceID: deviceID,
|
||||
CredentialHash: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
IdentityFingerprint: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
Ed25519Public: key,
|
||||
X25519Public: key,
|
||||
Name: "Host",
|
||||
OS: "linux",
|
||||
}},
|
||||
IssuedAt: now.Add(-time.Second).Format(time.RFC3339Nano),
|
||||
ExpiresAt: now.Add(4 * time.Minute).Format(time.RFC3339Nano),
|
||||
}
|
||||
bytes, err := authsnapshot.SigningBytes(payload)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
jwk, _ := json.Marshal(map[string]string{
|
||||
"kty": "OKP", "crv": "Ed25519", "kid": "key",
|
||||
"x": base64.RawURLEncoding.EncodeToString(signer.public),
|
||||
})
|
||||
return controlplane.SnapshotResponse{
|
||||
Snapshot: authsnapshot.Signed{
|
||||
Algorithm: "Ed25519", KID: "key", Payload: payload,
|
||||
Signature: base64.RawURLEncoding.EncodeToString(ed25519.Sign(signer.private, bytes)),
|
||||
},
|
||||
PublicKeyJWK: jwk,
|
||||
}
|
||||
}
|
||||
|
||||
func (signer signer) resign(t *testing.T, response controlplane.SnapshotResponse) controlplane.SnapshotResponse {
|
||||
t.Helper()
|
||||
bytes, err := authsnapshot.SigningBytes(response.Snapshot.Payload)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
response.Snapshot.Signature = base64.RawURLEncoding.EncodeToString(ed25519.Sign(signer.private, bytes))
|
||||
return response
|
||||
}
|
||||
|
||||
func TestDefaultAuthorizationPollingFitsRevocationObjective(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
signer := newSigner(t, now)
|
||||
registryValue, err := New(Config{
|
||||
DataRoot: t.TempDir(), NodeID: "node_test",
|
||||
AllowedOrigins: []string{"https://pwa.example"},
|
||||
RouteSecret: []byte("0123456789abcdef0123456789abcdef"),
|
||||
Keyring: signer.keyring, ControlPlane: snapshotSource{},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer registryValue.Close()
|
||||
if registryValue.config.DeltaInterval != 5*time.Second || registryValue.config.RequestTimeout != 3*time.Second {
|
||||
t.Fatalf("delta=%s timeout=%s", registryValue.config.DeltaInterval, registryValue.config.RequestTimeout)
|
||||
}
|
||||
if worstHealthy := registryValue.config.DeltaInterval + 2*registryValue.config.RequestTimeout; worstHealthy >= 15*time.Second {
|
||||
t.Fatalf("healthy revocation path exceeds objective: %s", worstHealthy)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConcurrentAcceptReturnsSingleTenantEngine(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
signer := newSigner(t, now)
|
||||
registryValue, err := New(Config{
|
||||
DataRoot: t.TempDir(), NodeID: "node_test",
|
||||
AllowedOrigins: []string{"https://pwa.example"},
|
||||
RouteSecret: []byte("0123456789abcdef0123456789abcdef"),
|
||||
Keyring: signer.keyring, ControlPlane: snapshotSource{},
|
||||
RefreshInterval: time.Hour, DeltaInterval: time.Hour,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer registryValue.Close()
|
||||
response := signer.response(t, now,
|
||||
"tenant_0123456789abcdef0123456789abcdef",
|
||||
"host_0123456789abcdef0123456789abcdef",
|
||||
)
|
||||
results := make([]*Tenant, 8)
|
||||
errorsFound := make([]error, 8)
|
||||
var wait sync.WaitGroup
|
||||
for index := range results {
|
||||
wait.Add(1)
|
||||
go func(index int) {
|
||||
defer wait.Done()
|
||||
results[index], errorsFound[index] = registryValue.Accept(context.Background(), response)
|
||||
}(index)
|
||||
}
|
||||
wait.Wait()
|
||||
for index := range results {
|
||||
if errorsFound[index] != nil || results[index] != results[0] {
|
||||
t.Fatalf("result[%d]=%p err=%v; first=%p", index, results[index], errorsFound[index], results[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeviceIdentityCannotBeOwnedByTwoTenants(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
signer := newSigner(t, now)
|
||||
registryValue, err := New(Config{
|
||||
DataRoot: t.TempDir(), NodeID: "node_test",
|
||||
AllowedOrigins: []string{"https://pwa.example"},
|
||||
RouteSecret: []byte("0123456789abcdef0123456789abcdef"),
|
||||
Keyring: signer.keyring, ControlPlane: snapshotSource{},
|
||||
RefreshInterval: time.Hour, DeltaInterval: time.Hour,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer registryValue.Close()
|
||||
deviceID := "host_0123456789abcdef0123456789abcdef"
|
||||
first := signer.response(t, now, "tenant_0123456789abcdef0123456789abcdef", deviceID)
|
||||
second := signer.response(t, now, "tenant_fedcba9876543210fedcba9876543210", deviceID)
|
||||
if _, err := registryValue.Accept(context.Background(), first); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := registryValue.Accept(context.Background(), second); err == nil {
|
||||
t.Fatal("cross-tenant device identity was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOlderAuthorizationRevisionCannotRestoreRevokedDevice(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
signer := newSigner(t, now)
|
||||
registryValue, err := New(Config{
|
||||
DataRoot: t.TempDir(), NodeID: "node_test",
|
||||
AllowedOrigins: []string{"https://pwa.example"},
|
||||
RouteSecret: []byte("0123456789abcdef0123456789abcdef"),
|
||||
Keyring: signer.keyring, ControlPlane: snapshotSource{},
|
||||
RefreshInterval: time.Hour, DeltaInterval: time.Hour,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer registryValue.Close()
|
||||
tenantID := "tenant_0123456789abcdef0123456789abcdef"
|
||||
deviceID := "host_0123456789abcdef0123456789abcdef"
|
||||
revisionOne := signer.response(t, now, tenantID, deviceID)
|
||||
if _, err := registryValue.Accept(context.Background(), revisionOne); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
revisionTwo := revisionOne
|
||||
revisionTwo.Snapshot.Payload.AuthorizationRevision = 2
|
||||
revisionTwo.Snapshot.Payload.Devices = nil
|
||||
revisionTwo = signer.resign(t, revisionTwo)
|
||||
if _, err := registryValue.Accept(context.Background(), revisionTwo); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := registryValue.ByDeviceID(deviceID); ok {
|
||||
t.Fatal("revoked device remained routable after newer revision")
|
||||
}
|
||||
if _, err := registryValue.Accept(context.Background(), revisionOne); err == nil {
|
||||
t.Fatal("older signed authorization revision restored a revoked device")
|
||||
}
|
||||
if _, ok := registryValue.ByDeviceID(deviceID); ok {
|
||||
t.Fatal("stale revision restored revoked device routing")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpiredTenantReleasesActiveCapacity(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
signer := newSigner(t, now)
|
||||
registryValue, err := New(Config{
|
||||
DataRoot: t.TempDir(), NodeID: "node_test",
|
||||
AllowedOrigins: []string{"https://pwa.example"},
|
||||
RouteSecret: []byte("0123456789abcdef0123456789abcdef"),
|
||||
Keyring: signer.keyring, ControlPlane: snapshotSource{},
|
||||
RefreshInterval: time.Hour, DeltaInterval: time.Hour, MaxTenants: 1,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer registryValue.Close()
|
||||
firstTenantID := "tenant_0123456789abcdef0123456789abcdef"
|
||||
first, err := registryValue.Accept(context.Background(), signer.response(
|
||||
t, now, firstTenantID, "host_0123456789abcdef0123456789abcdef",
|
||||
))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
registryValue.mu.RLock()
|
||||
firstSlot := registryValue.slots[firstTenantID]
|
||||
registryValue.mu.RUnlock()
|
||||
registryValue.expire(firstSlot, first)
|
||||
if _, err := registryValue.Accept(context.Background(), signer.response(
|
||||
t, now, "tenant_fedcba9876543210fedcba9876543210", "host_fedcba9876543210fedcba9876543210",
|
||||
)); err != nil {
|
||||
t.Fatalf("capacity was not released after expiry: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuiesceFencesExactGeneration(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
signer := newSigner(t, now)
|
||||
registryValue, err := New(Config{
|
||||
DataRoot: t.TempDir(), NodeID: "node_test",
|
||||
AllowedOrigins: []string{"https://pwa.example"},
|
||||
RouteSecret: []byte("0123456789abcdef0123456789abcdef"),
|
||||
Keyring: signer.keyring, ControlPlane: snapshotSource{},
|
||||
RefreshInterval: time.Hour, DeltaInterval: time.Hour,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer registryValue.Close()
|
||||
tenantID := "tenant_0123456789abcdef0123456789abcdef"
|
||||
if _, err := registryValue.Accept(context.Background(), signer.response(
|
||||
t, now, tenantID, "host_0123456789abcdef0123456789abcdef",
|
||||
)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := registryValue.Quiesce(tenantID, 2); err == nil {
|
||||
t.Fatal("wrong generation was quiesced")
|
||||
}
|
||||
paths, err := registryValue.Quiesce(tenantID, 1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if paths.Database == "" {
|
||||
t.Fatal("quiesce did not return the fenced tenant paths")
|
||||
}
|
||||
if _, ok := registryValue.ByDeviceID("host_0123456789abcdef0123456789abcdef"); ok {
|
||||
t.Fatal("quiesced tenant remained routable")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouteHintIsOpaqueAuthenticatedAndCrossNodeDecodable(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
signer := newSigner(t, now)
|
||||
secret := []byte("0123456789abcdef0123456789abcdef")
|
||||
registryValue, err := New(Config{
|
||||
DataRoot: t.TempDir(), NodeID: "node_test",
|
||||
AllowedOrigins: []string{"https://pwa.example"}, RouteSecret: secret,
|
||||
Keyring: signer.keyring, ControlPlane: snapshotSource{},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer registryValue.Close()
|
||||
tenantID := "tenant_0123456789abcdef0123456789abcdef"
|
||||
hint := registryValue.routeHint(tenantID, 19)
|
||||
if hint == "" || strings.Contains(hint, tenantID) {
|
||||
t.Fatalf("route hint leaked tenant identity: %q", hint)
|
||||
}
|
||||
decodedTenant, generation, err := registryValue.DecodeRouteHint(hint)
|
||||
if err != nil || decodedTenant != tenantID || generation != 19 {
|
||||
t.Fatalf("decoded tenant=%q generation=%d err=%v", decodedTenant, generation, err)
|
||||
}
|
||||
tamperedBytes, err := base64.RawURLEncoding.DecodeString(hint)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tamperedBytes[len(tamperedBytes)/2] ^= 0x01
|
||||
tampered := base64.RawURLEncoding.EncodeToString(tamperedBytes)
|
||||
if _, _, err := registryValue.DecodeRouteHint(tampered); err == nil {
|
||||
t.Fatal("tampered route hint was accepted")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user