Files
privatae-bastion/client-go/endpoint_test.go
T
Privatae 8822d5df85
ci / personal-data (push) Successful in 5s
ci / test (push) Successful in 3m51s
ci / fuzz (push) Successful in 2m55s
bastion 0.55.3
2026-10-25 23:16:45 +00:00

208 lines
7.0 KiB
Go

//go:build !windows
// Uses fakeToken from pairing_redeem_test.go, unix-only.
package main
// A tunnel endpoint arrives from the host three ways: in the pairing
// response, in the rotate directive on a heartbeat answer, and from the
// pairing file those were saved to. Each one refuses an endpoint that is
// not one host:port, the file by never serving that pairing (the
// device-level checks are in claims_tunnel_test.go).
import (
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
)
// A pairing response whose endpoint, or any entry of its endpoints list, is
// not one host:port is refused and nothing is saved. An IPv4, an IPv6 and a
// host name endpoint pair.
func TestAPairingResponseWithAnUnsafeEndpointSavesNothing(t *testing.T) {
cfgHome := uiTestConfigDir(t)
withMarkerAt(t, filepath.Join(t.TempDir(), "machine.json"))
var endpoint, hostName string
var endpoints []string
redeem := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_ = json.NewEncoder(w).Encode(map[string]any{
"operator_crt": "CERT", "ca_crt": "CA", "bearer": "b",
"endpoint": endpoint, "endpoints": endpoints,
"endpoint_pubkey": testWGKey(t), "bastion_tunnel_ip": "2001:db8::a:1",
"host_tunnel_ip": "2001:db8::e:3", "host_name": hostName,
})
}))
defer redeem.Close()
old := redeemURLBase
redeemURLBase = redeem.URL
t.Cleanup(func() { redeemURLBase = old })
hostName = "acme.host.privatae.ai"
for _, bad := range badTunnelEndpoints {
if bad == "" {
continue // a missing endpoint has its own refusal
}
for _, c := range []struct {
endpoint string
list []string
}{{bad, nil}, {"203.0.113.7:51820", []string{"[2001:db8::7]:51820", bad}}} {
endpoint, endpoints = c.endpoint, c.list
if _, _, err := pairWithToken(fakeToken("acme"), false, false); err == nil || !strings.Contains(err.Error(), "not a host:port") {
t.Errorf("%q in %+v: the pairing must be refused for its endpoint, got %v", bad, c, err)
}
}
}
_ = filepath.Walk(cfgHome, func(path string, fi os.FileInfo, err error) error {
if err == nil && !fi.IsDir() {
t.Errorf("a refused pairing left a file behind: %s", path)
}
return nil
})
for _, good := range goodTunnelEndpoints {
uiTestConfigDir(t) // a local bastion serves one host: a fresh box each
withMarkerAt(t, filepath.Join(t.TempDir(), "machine.json"))
endpoint, endpoints = good, []string{good}
cfg, _, err := pairWithToken(fakeToken("acme"), false, false)
if err != nil {
t.Fatalf("%q must pair: %v", good, err)
}
if cfg.Endpoint != good || len(cfg.Endpoints) != 1 || cfg.Endpoints[0] != good {
t.Fatalf("%q: saved %q %q", good, cfg.Endpoint, cfg.Endpoints)
}
}
}
// A pairing file that already holds an unsafe endpoint (written by an older
// build, or by hand) loads as invalid, naming the file and why: it is never
// served, while every other pairing still loads, status shows it, and unpair
// removes it. A machine that upgrades is never stopped by one such file.
func TestAPairingFileWithAnUnsafeEndpointLoadsButIsNeverServed(t *testing.T) {
uiTestConfigDir(t)
withMarkerAt(t, filepath.Join(t.TempDir(), "machine.json"))
supervised(t)
prev := unpairNeedsRoot
unpairNeedsRoot = func() bool { return false }
t.Cleanup(func() { unpairNeedsRoot = prev })
pd, err := pairingsDir()
if err != nil {
t.Fatal(err)
}
write := func(c *Config) string {
t.Helper()
b, _ := json.Marshal(c)
p := filepath.Join(pd, c.HostName+".json")
if err := os.WriteFile(p, b, 0o600); err != nil {
t.Fatal(err)
}
return p
}
const host, other = "acme.host.privatae.ai", "other.host.privatae.ai"
write(&Config{HostName: other, Endpoint: goodTunnelEndpoints[0]})
for _, bad := range badTunnelEndpoints {
if bad == "" {
continue // a pairing over a socket has no endpoint
}
for _, c := range []*Config{
{HostName: host, Endpoint: bad},
{HostName: host, Endpoint: "203.0.113.7:51820", Endpoints: []string{bad}},
} {
p := write(c)
cfgs, err := loadPairings()
if err != nil || len(cfgs) != 2 {
t.Fatalf("%q: one bad file must not fail the load: %v %v", bad, cfgs, err)
}
got := pairingFor(cfgs, host)
if got.active() || got.stateName() != "invalid" {
t.Errorf("%q: the pairing must load invalid: active=%v %q", bad, got.active(), got.stateName())
}
if out := statusOutput(t); !strings.Contains(out, host+" [invalid]") ||
!strings.Contains(out, "not a host:port") || !strings.Contains(out, p) {
t.Errorf("%q: status must show the pairing invalid, naming its file and why:\n%s", bad, out)
}
if !pairingFor(cfgs, other).active() {
t.Errorf("%q: the other pairing must still be served", bad)
}
if startPairing(got) {
t.Errorf("%q: an invalid pairing was started", bad)
}
}
}
// The supervisor starts nothing for it either (the other pairing is
// removed first: starting it would raise a real tunnel).
if err := os.Remove(filepath.Join(pd, other+".json")); err != nil {
t.Fatal(err)
}
superviseOnce()
supervisor.Lock()
running := len(supervisor.runs)
supervisor.Unlock()
if running != 0 {
t.Fatalf("the supervisor started %d run(s) for an invalid pairing", running)
}
if h, err := unpairHost(host); err != nil || h != host {
t.Fatalf("unpair must remove an invalid pairing: %q %v", h, err)
}
if cfgs, err := loadPairings(); err != nil || len(cfgs) != 0 {
t.Fatalf("after unpair nothing is left: %v %v", cfgs, err)
}
for _, good := range goodTunnelEndpoints {
write(&Config{HostName: host, Endpoint: good, Endpoints: goodTunnelEndpoints})
if cfgs, err := loadPairings(); err != nil || len(cfgs) != 1 || cfgs[0].Endpoint != good || !cfgs[0].active() {
t.Fatalf("%q must load and be served: %v %v", good, cfgs, err)
}
}
write(&Config{HostName: host, Transport: transportUnix})
if cfgs, err := loadPairings(); err != nil || len(cfgs) != 1 || !cfgs[0].active() {
t.Fatalf("a pairing with no endpoint must load and be served: %v %v", cfgs, err)
}
}
// statusOutput is what `status` prints.
func statusOutput(t *testing.T) string {
t.Helper()
r, w, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
stdout := os.Stdout
os.Stdout = w
serr := cmdStatus()
os.Stdout = stdout
_ = w.Close()
out, _ := io.ReadAll(r)
if serr != nil {
t.Fatalf("status: %v", serr)
}
return string(out)
}
// A rotate directive that names any unsafe endpoint is not acted on; one
// that names only host:port entries is.
func TestARotateDirectiveWithAnUnsafeEndpointIsIgnored(t *testing.T) {
for _, bad := range badTunnelEndpoints {
if bad == "" {
continue // an empty entry is no entry
}
for _, d := range [][]byte{
directive(bad, nil, "PK="),
directive("203.0.113.7:51820", []string{bad, "[2001:db8::7]:51820"}, "PK="),
} {
if c, pk := parseRotateTarget(d); len(c) != 0 || pk != "" {
t.Errorf("%q: the directive must be ignored, got %q %q", bad, c, pk)
}
}
}
c, pk := parseRotateTarget(directive("203.0.113.7:51820", goodTunnelEndpoints, "PK="))
if pk != "PK=" || !sameList(c, goodTunnelEndpoints) {
t.Fatalf("a directive of host:port entries must be taken: %q %q", c, pk)
}
}