carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1//go:build linux
2
3package host
4
5import (
6 "bytes"
7 "context"
8 "encoding/json"
9 "errors"
10 "net"
11 "net/http"
12 "testing"
13
14 "github.com/carlosframework/platform/internal/hostclaim"
15 execlease "github.com/carlosframework/platform/internal/hostexec"
16 "github.com/carlosframework/platform/internal/provision"
17 "github.com/carlosframework/platform/internal/rootparticipation"
18)
19
20func TestPlan14ExecutionBrokerTransport(t *testing.T) {
21 t.Parallel()
22 f, root, iq := plan14InstallRegistration(t, hostclaim.KindLanding, provision.LandChecking)
23 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}
24 peer := StopPeer{UID: 1001, PID: 42, ProcessStart: "start", BootID: "boot", ControllerID: "controller"}
25 root.Stop.Authenticate = func(context.Context, net.Conn) (StopPeer, error) { return peer, nil }
26 calls := 0
27 retired := false
28 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) {
29 calls++
30 if retired {
31 return execlease.ExecutionAuthority{}, errReinstallRetired
32 }
33 a, e := root.executionAuthority(ctx, q, p)
34 a.Migration = 1
35 return a, e
36 }, 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() }})
37 if err != nil {
38 t.Fatal(err)
39 }
40 root.Execution = b
41 t.Cleanup(func() { _ = b.Close(context.Background()) })
42 socket := serveClaimRoot(t, root)
43 tr := &http.Transport{DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
44 return (&net.Dialer{}).DialContext(ctx, "unix", socket)
45 }}
46 defer tr.CloseIdleConnections()
47 raw, _ := json.Marshal(q)
48 for _, field := range []string{`"expiry":"2099-01-01"`, `"pid":42`, `"path":"/data"`, `"duration":3600`, `"safe_until":999999999999`} {
49 forged := append(append([]byte{}, raw[:len(raw)-1]...), []byte(","+field+"}")...)
50 req, err := http.NewRequestWithContext(t.Context(), http.MethodPost, "http://control/claim/execution", bytes.NewReader(forged))
51 if err != nil {
52 t.Fatal(err)
53 }
54 resp, err := (&http.Client{Transport: tr}).Do(req)
55 if err != nil {
56 t.Fatal(err)
57 }
58 resp.Body.Close()
59 if resp.StatusCode != http.StatusBadRequest || calls != 0 {
60 t.Fatalf("untrusted field %s reached authority: status=%d calls=%d", field, resp.StatusCode, calls)
61 }
62 }
63 oversized := append(append([]byte(nil), raw...), bytes.Repeat([]byte(" "), 16<<10)...)
64 req, err := http.NewRequestWithContext(t.Context(), http.MethodPost, "http://control/claim/execution", bytes.NewReader(oversized))
65 if err != nil {
66 t.Fatal(err)
67 }
68 resp, err := (&http.Client{Transport: tr}).Do(req)
69 if err != nil {
70 t.Fatal(err)
71 }
72 resp.Body.Close()
73 if resp.StatusCode != http.StatusBadRequest || calls != 0 {
74 t.Fatalf("oversized request reached authority: status=%d calls=%d", resp.StatusCode, calls)
75 }
76 retired = true
77 if _, err := rootExecutionClient(socket)(t.Context(), q); !errors.Is(err, errReinstallRetired) || errors.Is(err, execlease.ErrLost) || err.Error() != q.Host+": "+errReinstallRetired.Error() {
78 t.Fatalf("retired refusal lost: %v", err)
79 }
80 retired = false
81 out, err := rootExecutionClient(socket)(t.Context(), q)
82 if err != nil {
83 t.Fatal(err)
84 }
85 if out.Status != "active" || out.Token == "" {
86 t.Fatal(out)
87 }
88 again, err := rootExecutionClient(socket)(t.Context(), q)
89 if err != nil || again != out {
90 t.Fatalf("replay: %+v %v", again, err)
91 }
92 peer.ProcessStart = "reused"
93 q.Operation = "executionView"
94 q.ID = "view"
95 q.Token = out.Token
96 if _, err := rootExecutionClient(socket)(t.Context(), q); err == nil {
97 t.Fatal("PID reuse borrowed execution")
98 }
99}
100
101func TestPlan14ExecutionServingTransport(t *testing.T) {
102 t.Parallel()
103 f, root, iq := plan14InstallRegistration(t, hostclaim.KindServing, "")
104 if _, err := root.commitRootInventory(t.Context(), iq); err != nil {
105 t.Fatal(err)
106 }
107 peer := StopPeer{UID: 1001, PID: 42, ProcessStart: "start", BootID: "boot", ControllerID: "controller"}
108 root.Stop.Authenticate = func(context.Context, net.Conn) (StopPeer, error) { return peer, nil }
109 disk := &rollbackDisk{dir: t.TempDir()}
110 raw, _ := json.Marshal(rootparticipation.Census{Schema: 1, Box: "box-1", Enrollment: "en", Migration: 1, Revision: 1, LaunchRevision: 1, Complete: true})
111 if err := disk.Write("census.json", raw); err != nil {
112 t.Fatal(err)
113 }
114 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() }})
115 if err != nil {
116 t.Fatal(err)
117 }
118 root.Execution = b
119 t.Cleanup(func() { _ = b.Close(context.Background()) })
120 call := rootExecutionClient(serveClaimRoot(t, root))
121 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}
122 for _, mismatch := range []string{"none", "stamp", "session", "incarnation", "op", "token"} {
123 t.Run(mismatch, func(t *testing.T) {
124 bad := q
125 switch mismatch {
126 case "stamp":
127 bad.Stamp++
128 case "session":
129 bad.Session++
130 case "incarnation":
131 bad.Incarnation = "other"
132 case "op":
133 bad.Op = "other"
134 case "token":
135 bad.Token = "other"
136 }
137 out, err := call(t.Context(), bad)
138 if mismatch == "none" {
139 if err != nil || out.Status != "serving" || out.Token != "" || out.SafeUntil != 0 {
140 t.Fatalf("serving response: %+v %v", out, err)
141 }
142 } else if err == nil {
143 t.Fatalf("mismatched %s admitted: %+v", mismatch, out)
144 }
145 })
146 }
147}
148
149func TestPlan14ExecutionServingResponseShape(t *testing.T) {
150 q := execlease.ExecutionRequest{Schema: 1, ID: "view", Operation: "executionView", Host: "shop.example.com", Incarnation: "inc", Stamp: 1, Session: 1}
151 for _, attack := range []string{"none", "active token", "deadline", "lease", "wrong session"} {
152 t.Run(attack, func(t *testing.T) {
153 out := execlease.ExecutionResponse{Schema: 1, Host: q.Host, Incarnation: q.Incarnation, Stamp: q.Stamp, Session: q.Session, BootID: "boot", Status: "serving"}
154 switch attack {
155 case "active token":
156 out.Status, out.Token = "active", "unexpected-token"
157 case "deadline":
158 out.SafeUntil = 1234
159 case "lease":
160 out.LeaseETag = "lease"
161 case "wrong session":
162 out.Session++
163 }
164 socket := serveClaimRoot(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _ = json.NewEncoder(w).Encode(out) }))
165 _, err := rootExecutionClient(socket)(t.Context(), q)
166 if attack == "none" && err != nil {
167 t.Fatal(err)
168 }
169 if attack != "none" && err == nil {
170 t.Fatalf("accepted %s response", attack)
171 }
172 })
173 }
174}
175