//go:build linux package host import ( "bytes" "context" "encoding/json" "errors" "net" "net/http" "testing" "github.com/carlosframework/platform/internal/hostclaim" execlease "github.com/carlosframework/platform/internal/hostexec" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/rootparticipation" ) func TestPlan14ExecutionBrokerTransport(t *testing.T) { t.Parallel() f, root, iq := plan14InstallRegistration(t, hostclaim.KindLanding, provision.LandChecking) q := execlease.ExecutionRequest{Schema: 1, ID: "request-1", Operation: "executionBegin", Host: iq.Target.Host, Incarnation: iq.Target.Incarnation, Stamp: iq.Target.Stamp, Session: iq.Target.Session, Op: iq.Target.Op} peer := StopPeer{UID: 1001, PID: 42, ProcessStart: "start", BootID: "boot", ControllerID: "controller"} root.Stop.Authenticate = func(context.Context, net.Conn) (StopPeer, error) { return peer, nil } calls := 0 retired := false b, err := execlease.NewRootBroker(execlease.RootBrokerConfig{State: &rollbackDisk{dir: t.TempDir()}, Remote: f.claims, Clock: execlease.SystemClock{}, BootID: peer.BootID, Identity: "broker", Authority: func(ctx context.Context, q execlease.ExecutionRequest, p execlease.ControllerProcess) (execlease.ExecutionAuthority, error) { calls++ if retired { return execlease.ExecutionAuthority{}, errReinstallRetired } a, e := root.executionAuthority(ctx, q, p) a.Migration = 1 return a, e }, Alive: func(context.Context, execlease.ControllerProcess) error { return nil }, Cleanup: func(context.Context, execlease.ExecutionRecord) error { return nil }, Settle: func(context.Context, execlease.ExecutionRecord) error { return nil }, Exclude: func(_ context.Context, fn func() error) error { return fn() }}) if err != nil { t.Fatal(err) } root.Execution = b t.Cleanup(func() { _ = b.Close(context.Background()) }) socket := serveClaimRoot(t, root) tr := &http.Transport{DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { return (&net.Dialer{}).DialContext(ctx, "unix", socket) }} defer tr.CloseIdleConnections() raw, _ := json.Marshal(q) for _, field := range []string{`"expiry":"2099-01-01"`, `"pid":42`, `"path":"/data"`, `"duration":3600`, `"safe_until":999999999999`} { forged := append(append([]byte{}, raw[:len(raw)-1]...), []byte(","+field+"}")...) req, err := http.NewRequestWithContext(t.Context(), http.MethodPost, "http://control/claim/execution", bytes.NewReader(forged)) if err != nil { t.Fatal(err) } resp, err := (&http.Client{Transport: tr}).Do(req) if err != nil { t.Fatal(err) } resp.Body.Close() if resp.StatusCode != http.StatusBadRequest || calls != 0 { t.Fatalf("untrusted field %s reached authority: status=%d calls=%d", field, resp.StatusCode, calls) } } oversized := append(append([]byte(nil), raw...), bytes.Repeat([]byte(" "), 16<<10)...) req, err := http.NewRequestWithContext(t.Context(), http.MethodPost, "http://control/claim/execution", bytes.NewReader(oversized)) if err != nil { t.Fatal(err) } resp, err := (&http.Client{Transport: tr}).Do(req) if err != nil { t.Fatal(err) } resp.Body.Close() if resp.StatusCode != http.StatusBadRequest || calls != 0 { t.Fatalf("oversized request reached authority: status=%d calls=%d", resp.StatusCode, calls) } retired = true if _, err := rootExecutionClient(socket)(t.Context(), q); !errors.Is(err, errReinstallRetired) || errors.Is(err, execlease.ErrLost) || err.Error() != q.Host+": "+errReinstallRetired.Error() { t.Fatalf("retired refusal lost: %v", err) } retired = false out, err := rootExecutionClient(socket)(t.Context(), q) if err != nil { t.Fatal(err) } if out.Status != "active" || out.Token == "" { t.Fatal(out) } again, err := rootExecutionClient(socket)(t.Context(), q) if err != nil || again != out { t.Fatalf("replay: %+v %v", again, err) } peer.ProcessStart = "reused" q.Operation = "executionView" q.ID = "view" q.Token = out.Token if _, err := rootExecutionClient(socket)(t.Context(), q); err == nil { t.Fatal("PID reuse borrowed execution") } } func TestPlan14ExecutionServingTransport(t *testing.T) { t.Parallel() f, root, iq := plan14InstallRegistration(t, hostclaim.KindServing, "") if _, err := root.commitRootInventory(t.Context(), iq); err != nil { t.Fatal(err) } peer := StopPeer{UID: 1001, PID: 42, ProcessStart: "start", BootID: "boot", ControllerID: "controller"} root.Stop.Authenticate = func(context.Context, net.Conn) (StopPeer, error) { return peer, nil } disk := &rollbackDisk{dir: t.TempDir()} raw, _ := json.Marshal(rootparticipation.Census{Schema: 1, Box: "box-1", Enrollment: "en", Migration: 1, Revision: 1, LaunchRevision: 1, Complete: true}) if err := disk.Write("census.json", raw); err != nil { t.Fatal(err) } b, err := execlease.NewRootBroker(execlease.RootBrokerConfig{State: disk, Remote: f.claims, Clock: execlease.SystemClock{}, BootID: peer.BootID, Identity: "broker", Authority: root.executionAuthority, Alive: func(context.Context, execlease.ControllerProcess) error { return nil }, Cleanup: func(context.Context, execlease.ExecutionRecord) error { return nil }, Settle: func(context.Context, execlease.ExecutionRecord) error { return nil }, Exclude: func(_ context.Context, fn func() error) error { return fn() }}) if err != nil { t.Fatal(err) } root.Execution = b t.Cleanup(func() { _ = b.Close(context.Background()) }) call := rootExecutionClient(serveClaimRoot(t, root)) q := execlease.ExecutionRequest{Schema: 1, ID: "serving-view", Operation: "executionView", Host: iq.Target.Host, Incarnation: iq.Target.Incarnation, Stamp: iq.Target.Stamp, Session: iq.Target.Session, Op: iq.Target.Op} for _, mismatch := range []string{"none", "stamp", "session", "incarnation", "op", "token"} { t.Run(mismatch, func(t *testing.T) { bad := q switch mismatch { case "stamp": bad.Stamp++ case "session": bad.Session++ case "incarnation": bad.Incarnation = "other" case "op": bad.Op = "other" case "token": bad.Token = "other" } out, err := call(t.Context(), bad) if mismatch == "none" { if err != nil || out.Status != "serving" || out.Token != "" || out.SafeUntil != 0 { t.Fatalf("serving response: %+v %v", out, err) } } else if err == nil { t.Fatalf("mismatched %s admitted: %+v", mismatch, out) } }) } } func TestPlan14ExecutionServingResponseShape(t *testing.T) { q := execlease.ExecutionRequest{Schema: 1, ID: "view", Operation: "executionView", Host: "shop.example.com", Incarnation: "inc", Stamp: 1, Session: 1} for _, attack := range []string{"none", "active token", "deadline", "lease", "wrong session"} { t.Run(attack, func(t *testing.T) { out := execlease.ExecutionResponse{Schema: 1, Host: q.Host, Incarnation: q.Incarnation, Stamp: q.Stamp, Session: q.Session, BootID: "boot", Status: "serving"} switch attack { case "active token": out.Status, out.Token = "active", "unexpected-token" case "deadline": out.SafeUntil = 1234 case "lease": out.LeaseETag = "lease" case "wrong session": out.Session++ } socket := serveClaimRoot(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _ = json.NewEncoder(w).Encode(out) })) _, err := rootExecutionClient(socket)(t.Context(), q) if attack == "none" && err != nil { t.Fatal(err) } if attack != "none" && err == nil { t.Fatalf("accepted %s response", attack) } }) } }