carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package hostguard
2
3import (
4 "context"
5 "errors"
6 "time"
7
8 "github.com/carlosframework/platform/internal/hostclaim"
9 "github.com/carlosframework/platform/internal/release"
10)
11
12var (
13 ErrInvalidInput = errors.New("hostguard: invalid input")
14 ErrNoClaims = errors.New("hostguard: no claim reader")
15 ErrNoClaim = errors.New("hostguard: claim absent")
16 ErrMalformedClaim = errors.New("hostguard: malformed claim observation")
17 ErrNoHolder = errors.New("hostguard: claim has no holder")
18 ErrClaimIncarnation = errors.New("hostguard: captured incarnation differs from claim")
19 ErrRecordIncarnation = errors.New("hostguard: local incarnation differs")
20 ErrInactive = errors.New("hostguard: installation inactive")
21 ErrStampMismatch = errors.New("hostguard: captured stamp is not active")
22 ErrCheckRequired = errors.New("hostguard: identity check required")
23 ErrNotParticipant = errors.New("hostguard: box is not authorized to activate")
24 ErrRetiredStamp = errors.New("hostguard: installation stamp retired")
25 ErrOldStamp = errors.New("hostguard: installation stamp predates local history")
26 ErrFenced = errors.New("hostguard: execution denied by durable fence")
27)
28
29type ClaimReader interface {
30 Read(context.Context, string) (hostclaim.Observed, error)
31}
32
33type StoreReader struct{ Store release.Store }
34
35func (r StoreReader) Read(ctx context.Context, host string) (hostclaim.Observed, error) {
36 if r.Store == nil {
37 return hostclaim.Observed{}, ErrNoClaims
38 }
39 return hostclaim.Read(ctx, r.Store, host)
40}
41
42type Guard struct {
43 Root *RootAuthority
44 FenceView FenceViewProvider
45 DataDir string
46 LockDir string
47 Claims ClaimReader
48 openRecords func(string) (*recordDir, error)
49}
50
51func (g Guard) locked(ctx context.Context, host string, fn func(*recordDir) error) error {
52 if err := validateFileHost(host); err != nil {
53 return err
54 }
55 if g.DataDir == "" || ctx == nil {
56 return ErrInvalidInput
57 }
58 if g.Root != nil {
59 if g.Root.store == nil {
60 return ErrInvalidInput
61 }
62 unlock, err := g.Root.store.Lock(ctx, host+".lock")
63 if err != nil {
64 return err
65 }
66 defer unlock()
67 }
68 dir := g.LockDir
69 if dir == "" {
70 dir = "/run/carlos/hostguard"
71 }
72 lockCtx := ctx
73 cancel := func() {}
74 if g.Root != nil {
75 lockCtx, cancel = context.WithTimeout(ctx, 250*time.Millisecond)
76 }
77 defer cancel()
78 unlock, err := acquire(lockCtx, dir, host, g.Root != nil)
79 if err != nil {
80 // Contention and I/O errors are retryable, not hostile evidence.
81 // Only a positively unsafe lock can quarantine an installation.
82 if g.Root != nil && errors.Is(err, ErrUnsafeFile) {
83 r, readErr := g.Root.read(host)
84 if readErr == nil {
85 r.quarantined, r.settled = true, false
86 return errors.Join(err, g.Root.write(r))
87 }
88 if !errors.Is(readErr, ErrNoRecord) {
89 return errors.Join(err, readErr)
90 }
91 }
92 return err
93 }
94 defer unlock()
95 if g.Root != nil {
96 return fn(&recordDir{root: g.Root})
97 }
98 open := g.openRecords
99 if open == nil {
100 open = openRecordDir
101 }
102 d, err := open(g.DataDir)
103 if err != nil {
104 return err
105 }
106 defer d.close()
107 return fn(d)
108}
109
110func (g Guard) fresh(ctx context.Context, host string) (*hostclaim.Claim, error) {
111 obs, err := g.freshObserved(ctx, host)
112 if err != nil {
113 return nil, err
114 }
115 return obs.Claim, nil
116}
117
118func (g Guard) freshObserved(ctx context.Context, host string) (hostclaim.Observed, error) {
119 if err := ctx.Err(); err != nil {
120 return hostclaim.Observed{}, err
121 }
122 if g.Claims == nil {
123 return hostclaim.Observed{}, ErrNoClaims
124 }
125 obs, err := g.Claims.Read(ctx, host)
126 if err != nil {
127 return hostclaim.Observed{}, err
128 }
129 if err := ctx.Err(); err != nil {
130 return hostclaim.Observed{}, err
131 }
132 if obs.Host != host {
133 return hostclaim.Observed{}, ErrMalformedClaim
134 }
135 if obs.Absent {
136 return hostclaim.Observed{}, ErrNoClaim
137 }
138 if obs.Claim == nil || obs.Claim.Host != host {
139 return hostclaim.Observed{}, ErrMalformedClaim
140 }
141 if obs.Claim.Holder == nil {
142 return hostclaim.Observed{}, ErrNoHolder
143 }
144 if obs.Claim.Holder.Incarnation == "" {
145 return hostclaim.Observed{}, ErrMalformedClaim
146 }
147 return obs, nil
148}
149
150func (g Guard) With(ctx context.Context, host string, captured Captured, check func(*hostclaim.Claim) error, fn func() error) error {
151 return g.locked(ctx, host, func(d *recordDir) error {
152 c, err := g.fresh(ctx, host)
153 if err != nil {
154 return err
155 }
156 if (g.Root != nil || g.FenceView != nil) && c.Holder.Stamp != captured.Stamp {
157 return ErrStampMismatch
158 }
159 if g.FenceView != nil {
160 v, err := g.FenceView(ctx, host, captured)
161 if err != nil {
162 return err
163 }
164 if err := v.admit(host, captured, c.Holder.Session); err != nil {
165 return err
166 }
167 }
168 if c.Holder.Incarnation != captured.Incarnation {
169 return ErrClaimIncarnation
170 }
171 if check == nil {
172 return ErrCheckRequired
173 }
174 if err := check(c); err != nil {
175 return err
176 }
177 r, err := d.read(host)
178 if err != nil {
179 return err
180 }
181 if err := r.admits(c, captured); err != nil {
182 return err
183 }
184 if fn == nil {
185 return ErrInvalidInput
186 }
187 if err := ctx.Err(); err != nil {
188 return err
189 }
190 if g.FenceView != nil {
191 v, err := g.FenceView(ctx, host, captured)
192 if err != nil {
193 return err
194 }
195 if err := v.admit(host, captured, c.Holder.Session); err != nil {
196 return err
197 }
198 }
199 return fn()
200 })
201}
202
203// admits is With's reading of the local record: nil only when the record
204// lets the captured installation run claim c's session. AdmissionFenced
205// reads the same, so the two cannot disagree.
206func (r record) admits(c *hostclaim.Claim, captured Captured) error {
207 if r.quarantined || r.pendingInstallation != nil || r.barrier != nil {
208 return ErrFenced
209 }
210 if r.root && (r.installation.Account != c.Holder.Account || r.installation.App != c.Holder.App) {
211 return ErrRecordIncarnation
212 }
213 if r.Incarnation != captured.Incarnation {
214 return ErrRecordIncarnation
215 }
216 if r.ActiveStamp == 0 {
217 return ErrInactive
218 }
219 if captured.Stamp != r.ActiveStamp {
220 return ErrStampMismatch
221 }
222 if migrationAdmissionDenied(r.migrationDenies, r.FenceJournal, captured, c.Holder.Session) {
223 return ErrFenced
224 }
225 if r.ActiveFence != nil && r.ActiveFence.matchesSession(c, captured) {
226 return ErrFenced
227 }
228 for _, receipt := range r.FenceJournal {
229 if receipt.Target.matchesSession(c, captured) {
230 return ErrFenced
231 }
232 }
233 return nil
234}
235
236func (r *record) retire() {
237 if r.ActiveStamp == 0 {
238 return
239 }
240 if r.root {
241 r.rootRetired = append(r.rootRetired, Captured{Incarnation: r.Incarnation, Stamp: r.ActiveStamp})
242 }
243 r.RetiredStamps = append(r.RetiredStamps, r.ActiveStamp)
244 if !r.root && len(r.RetiredStamps) > 32 {
245 r.RetiredStamps = r.RetiredStamps[len(r.RetiredStamps)-32:]
246 }
247 r.ActiveStamp = 0
248}
249
250func (g Guard) Activate(ctx context.Context, host, box string, participant func(*hostclaim.Claim) bool) error {
251 if g.Root != nil {
252 return g.locked(ctx, host, func(d *recordDir) error {
253 c, err := g.fresh(ctx, host)
254 if err != nil {
255 return err
256 }
257 r, err := d.read(host)
258 if err != nil {
259 return err
260 }
261 if r.quarantined || r.pendingInstallation != nil || r.barrier != nil || r.Incarnation != c.Holder.Incarnation || r.ActiveStamp != c.Holder.Stamp || r.ActiveStamp == 0 || r.installation.Box != box {
262 return ErrFenced
263 }
264 return d.sync(host)
265 })
266 }
267 return g.locked(ctx, host, func(d *recordDir) error {
268 c, err := g.fresh(ctx, host)
269 if err != nil {
270 return err
271 }
272 if box == "" || (c.Op.Exec != box && (participant == nil || !participant(c))) {
273 return ErrNotParticipant
274 }
275 stamp := c.Holder.Stamp
276 if stamp == 0 {
277 return ErrMalformedClaim
278 }
279 r, err := d.read(host)
280 if err != nil && !errors.Is(err, ErrNoRecord) {
281 return err
282 }
283 if errors.Is(err, ErrNoRecord) {
284 r = record{V: 1, Host: host, Incarnation: c.Holder.Incarnation, RetiredStamps: []uint64{}}
285 } else if r.Incarnation != c.Holder.Incarnation {
286 // Fence evidence belongs to the machine, not the successor holder.
287 // Preserve it so delayed cleanup can prove and retire only its target.
288 r = record{V: 1, Host: host, Incarnation: c.Holder.Incarnation, RetiredStamps: []uint64{}, ActiveFence: r.ActiveFence, FenceJournal: r.FenceJournal}
289 } else {
290 for _, s := range r.RetiredStamps {
291 if stamp == s {
292 return ErrRetiredStamp
293 }
294 }
295 if stamp < r.ActiveStamp {
296 return ErrOldStamp
297 }
298 // Keep refusing trimmed stamps even after retirement sets active to zero.
299 if n := len(r.RetiredStamps); n != 0 && stamp < r.RetiredStamps[n-1] {
300 return ErrOldStamp
301 }
302 if stamp == r.ActiveStamp {
303 return d.sync(host)
304 }
305 r.retire()
306 }
307 r.ActiveStamp = stamp
308 if err := ctx.Err(); err != nil {
309 return err
310 }
311 return d.write(r)
312 })
313}
314
315func (g Guard) RetireInstallation(ctx context.Context, host string, check func(*hostclaim.Claim) error) error {
316 return g.locked(ctx, host, func(d *recordDir) error {
317 c, err := g.fresh(ctx, host)
318 if err != nil {
319 return err
320 }
321 if check == nil {
322 return ErrCheckRequired
323 }
324 if err := check(c); err != nil {
325 return err
326 }
327 r, err := d.read(host)
328 if err != nil {
329 return err
330 }
331 if r.Incarnation != c.Holder.Incarnation {
332 return ErrRecordIncarnation
333 }
334 if r.ActiveStamp == 0 {
335 return d.sync(host)
336 }
337 r.retire()
338 if err := ctx.Err(); err != nil {
339 return err
340 }
341 return d.write(r)
342 })
343}
344
345func (g Guard) readLocked(ctx context.Context, host string, fn func(*recordDir) error) error {
346 if g.Root == nil {
347 return g.locked(ctx, host, fn)
348 }
349 if validateFileHost(host) != nil || g.Root.store == nil {
350 return ErrInvalidInput
351 }
352 unlock, err := g.Root.store.Lock(ctx, host+".lock")
353 if err != nil {
354 return err
355 }
356 defer unlock()
357 return fn(&recordDir{root: g.Root})
358}
359