Files
DenozordecandCursor f8e0520817 feat(httpapi): harden control plane and speed up prefix refresh
Очередь job получила lease и fencing, телеметрия и прогресс deploy сохраняются, ревизия пишет manifest входов. Обновление префиксов не переписывает неизменённый снимок, большие наборы идут через COPY, а устаревшая ревизия не попадает в deploy. Бандл ноды кэшируется, prune и постановка job больше не держат горячий путь.

Co-authored-by: Cursor <[email protected]>
2026-11-02 17:23:59 +07:00

287 lines
9.4 KiB
Go

package bundle
import (
"archive/tar"
"bytes"
"compress/gzip"
"crypto/ed25519"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
)
type testEntry struct {
header tar.Header
body []byte
}
func archiveEntries(t *testing.T, entries []testEntry) []byte {
t.Helper()
var b bytes.Buffer
gz := gzip.NewWriter(&b)
tw := tar.NewWriter(gz)
for _, entry := range entries {
h := entry.header
if h.Typeflag == 0 {
h.Typeflag = tar.TypeReg
}
h.Size = int64(len(entry.body))
if err := tw.WriteHeader(&h); err != nil {
t.Fatal(err)
}
if _, err := tw.Write(entry.body); err != nil {
t.Fatal(err)
}
}
if err := tw.Close(); err != nil {
t.Fatal(err)
}
if err := gz.Close(); err != nil {
t.Fatal(err)
}
return b.Bytes()
}
func testKey() ed25519.PrivateKey {
return ed25519.NewKeyFromSeed(bytes.Repeat([]byte{7}, ed25519.SeedSize))
}
func TestBundleResourceLimits(t *testing.T) {
pub := testKey().Public().(ed25519.PublicKey)
cases := []struct {
name string
entries []testEntry
limits Limits
}{
{"file", []testEntry{{header: tar.Header{Name: "bird.conf"}, body: bytes.Repeat([]byte{'a'}, 1025)}}, Limits{MaxFileBytes: 1024}},
{"expanded", []testEntry{{header: tar.Header{Name: "bird.conf"}, body: bytes.Repeat([]byte{'a'}, 8192)}}, Limits{MaxExpandedBytes: 4096}},
{"manifest", []testEntry{{header: tar.Header{Name: manifestName}, body: bytes.Repeat([]byte{'a'}, 1025)}}, Limits{MaxManifestBytes: 1024}},
{"entries", []testEntry{{header: tar.Header{Name: "a"}}, {header: tar.Header{Name: "b"}}, {header: tar.Header{Name: "c"}}}, Limits{MaxEntries: 2}},
{"pax-metadata", []testEntry{{header: tar.Header{Name: "bird.conf", Format: tar.FormatPAX, PAXRecords: map[string]string{"comment": strings.Repeat("a", 8192)}}}}, Limits{MaxExpandedBytes: 4096}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
raw := archiveEntries(t, tc.entries)
if _, err := VerifyGzippedTarWithLimits(raw, pub, tc.limits); !errors.Is(err, ErrLimit) {
t.Fatalf("want limit, got %v", err)
}
})
}
if _, err := VerifyGzippedTarWithLimits(make([]byte, 65), pub, Limits{MaxCompressedBytes: 64}); !errors.Is(err, ErrLimit) {
t.Fatalf("compressed cap: %v", err)
}
}
func TestBundleRejectsUnsafeEntries(t *testing.T) {
pub := testKey().Public().(ed25519.PublicKey)
for _, name := range []string{"../escape", "/etc/bird.conf", `bird.d\escape`, "C:/escape", "bird.d/../escape", "a//b", "a/./b", "NUL", "bird.conf:stream", "a. /b"} {
t.Run(name, func(t *testing.T) {
raw := archiveEntries(t, []testEntry{{header: tar.Header{Name: name}, body: []byte("x")}})
if _, err := VerifyGzippedTar(raw, pub); err == nil {
t.Fatal("unsafe path accepted")
}
})
}
for _, kind := range []byte{tar.TypeSymlink, tar.TypeLink, tar.TypeDir, tar.TypeFifo} {
raw := archiveEntries(t, []testEntry{{header: tar.Header{Name: "bird.conf", Typeflag: kind, Linkname: "outside"}}})
if _, err := VerifyGzippedTar(raw, pub); err == nil {
t.Fatalf("type %d accepted", kind)
}
}
raw := archiveEntries(t, []testEntry{{header: tar.Header{Name: "bird.conf"}}, {header: tar.Header{Name: "bird.conf"}}})
if _, err := VerifyGzippedTar(raw, pub); err == nil || !strings.Contains(err.Error(), "duplicate") {
t.Fatalf("duplicate accepted: %v", err)
}
}
func TestBundleChecksTrailerAndTrailingExpansion(t *testing.T) {
priv := testKey()
pub := priv.Public().(ed25519.PublicKey)
raw, err := BuildGzippedTar("r", "s", map[string]string{"bird.conf": "router id 192.0.2.1;"}, priv)
if err != nil {
t.Fatal(err)
}
corrupt := append([]byte{}, raw...)
corrupt[len(corrupt)-8] ^= 0xff
if _, err = VerifyGzippedTar(corrupt, pub); err == nil {
t.Fatal("gzip checksum ignored after tar EOF")
}
var extra bytes.Buffer
gz := gzip.NewWriter(&extra)
if _, err = gz.Write(bytes.Repeat([]byte{'x'}, 32<<10)); err != nil {
t.Fatal(err)
}
if err = gz.Close(); err != nil {
t.Fatal(err)
}
joined := append(append([]byte{}, raw...), extra.Bytes()...)
if _, err = VerifyGzippedTarWithLimits(joined, pub, Limits{MaxExpandedBytes: 8 << 10}); !errors.Is(err, ErrLimit) {
t.Fatalf("trailing gzip member bypassed budget: %v", err)
}
}
func TestBundleRejectsPAXSparseRegularFile(t *testing.T) {
// archive/tar expands PAX sparse holes even though the returned type is a
// regular file. Build a physical one-byte entry with a 4096-byte logical size.
record := func(key, value string) string {
body := key + "=" + value + "\n"
n := len(body) + 2
for {
next := len(fmt.Sprint(n)) + 1 + len(body)
if next == n {
return fmt.Sprintf("%d %s", n, body)
}
n = next
}
}
pax := record("GNU.sparse.size", "4096") + record("GNU.sparse.numblocks", "1") + record("GNU.sparse.map", "0,1")
var physical bytes.Buffer
tw := tar.NewWriter(&physical)
for _, entry := range []testEntry{{header: tar.Header{Name: "PaxHeader", Size: int64(len(pax))}, body: []byte(pax)}, {header: tar.Header{Name: "bird.conf", Size: 1}, body: []byte("x")}} {
if err := tw.WriteHeader(&entry.header); err != nil {
t.Fatal(err)
}
if _, err := tw.Write(entry.body); err != nil {
t.Fatal(err)
}
}
if err := tw.Close(); err != nil {
t.Fatal(err)
}
data := physical.Bytes()
data[156] = tar.TypeXHeader
for i := 148; i < 156; i++ {
data[i] = ' '
}
sum := 0
for _, b := range data[:512] {
sum += int(b)
}
copy(data[148:156], fmt.Sprintf("%06o\x00 ", sum))
h, err := tar.NewReader(bytes.NewReader(data)).Next()
if err != nil || h.Typeflag != tar.TypeReg || h.Size != 4096 {
t.Fatalf("invalid sparse regression fixture %+v %v", h, err)
}
var compressed bytes.Buffer
gz := gzip.NewWriter(&compressed)
if _, err = gz.Write(data); err != nil {
t.Fatal(err)
}
if err = gz.Close(); err != nil {
t.Fatal(err)
}
if _, err = VerifyGzippedTar(compressed.Bytes(), testKey().Public().(ed25519.PublicKey)); err == nil || !strings.Contains(err.Error(), "sparse") {
t.Fatalf("PAX sparse accepted: %v", err)
}
}
func TestBundleRejectsUnmanifestedPayload(t *testing.T) {
priv := testKey()
raw, err := BuildGzippedTar("r", "s", map[string]string{"bird.conf": "x"}, priv)
if err != nil {
t.Fatal(err)
}
gr, err := gzip.NewReader(bytes.NewReader(raw))
if err != nil {
t.Fatal(err)
}
defer func() { _ = gr.Close() }()
tr := tar.NewReader(gr)
var entries []testEntry
for {
h, e := tr.Next()
if e == io.EOF {
break
}
if e != nil {
t.Fatal(e)
}
b, e := io.ReadAll(tr)
if e != nil {
t.Fatal(e)
}
entries = append(entries, testEntry{header: *h, body: b})
}
entries = append(entries, testEntry{header: tar.Header{Name: "unsigned.conf"}, body: []byte("unsigned")})
if _, err = VerifyGzippedTar(archiveEntries(t, entries), priv.Public().(ed25519.PublicKey)); err == nil {
t.Fatal("unmanifested payload accepted")
}
}
func TestExtractionDoesNotFollowLinks(t *testing.T) {
root := t.TempDir()
outside := filepath.Join(t.TempDir(), "outside")
if err := os.WriteFile(outside, []byte("preserve"), 0o600); err != nil {
t.Fatal(err)
}
dest := filepath.Join(root, "bird.conf")
if err := os.Link(outside, dest); err != nil {
if runtime.GOOS == "linux" {
t.Fatalf("Linux link regression requires hard links: %v", err)
}
t.Skipf("hard links unavailable: %v", err)
}
v := &VerifiedContents{Manifest: Manifest{Files: []FileEntry{{Path: "bird.conf"}}}, Files: map[string][]byte{"bird.conf": []byte("replacement")}}
if err := WriteExtractedFiles(root, v); err != nil {
t.Fatal(err)
}
got, err := os.ReadFile(outside)
if err != nil || string(got) != "preserve" {
t.Fatalf("hard link modified outside: %s %v", got, err)
}
if err = os.Remove(dest); err != nil {
t.Fatal(err)
}
if err = os.Symlink(outside, dest); err != nil {
if runtime.GOOS == "linux" {
t.Fatalf("Linux link regression requires symlinks: %v", err)
}
t.Skipf("symlinks unavailable: %v", err)
}
if err = WriteExtractedFiles(root, v); err == nil {
t.Fatal("symlink destination accepted")
}
got, err = os.ReadFile(outside)
if err != nil || string(got) != "preserve" {
t.Fatal("symlink modified outside")
}
}
func TestBundleBuilderBoundsAndPortablePaths(t *testing.T) {
for _, name := range []string{"/bird.conf", "a/../bird.conf", manifestName, sigFileName, `bird.d\bad`} {
if _, err := BuildGzippedTar("r", "s", map[string]string{name: "x"}, testKey()); err == nil {
t.Fatalf("invalid builder path accepted: %q", name)
}
}
if _, err := BuildGzippedTarWithLimits("r", "s", map[string]string{"bird.conf": strings.Repeat("x", 1025)}, testKey(), Limits{MaxFileBytes: 1024}); !errors.Is(err, ErrLimit) {
t.Fatalf("builder file cap %v", err)
}
if _, err := BuildGzippedTarWithLimits("r", "s", map[string]string{"bird.conf": "x"}, testKey(), Limits{MaxExpandedBytes: 1024}); !errors.Is(err, ErrLimit) {
t.Fatalf("builder tar overhead cap %v", err)
}
}
// This is a decoded-size regression using repeated representative route lines,
// not a unique-prefix cardinality test or a BIRD validity/performance benchmark.
func TestDefaultLimitsAllowMillionRepresentativeIPv6RouteLines(t *testing.T) {
if testing.Short() {
t.Skip("large representative config")
}
config := strings.Repeat("route 2001:db8:1234:5678::/64 blackhole;\n", 1_000_000)
raw, err := BuildGzippedTar("large", "speaker", map[string]string{"bird.conf": config}, testKey())
if err != nil {
t.Fatal(err)
}
v, err := VerifyGzippedTar(raw, testKey().Public().(ed25519.PublicKey))
if err != nil {
t.Fatal(err)
}
if len(v.Files["bird.conf"]) != len(config) {
t.Fatal("large config truncated")
}
}