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 "bytes"
5 "context"
6 "errors"
7 "time"
8
9 "github.com/carlosframework/platform/internal/hostclaim"
10)
11
12var (
13 ErrFenceActive = errors.New("hostguard: another fence is active")
14 ErrFenceTarget = errors.New("hostguard: fence target does not match authority")
15 ErrNoFenceReceipt = errors.New("hostguard: no fence receipt for target")
16 ErrReceiptChanged = errors.New("hostguard: fence receipt bytes changed")
17)
18
19// FenceTarget contains authority identities only. Filesystem paths, PIDs and
20// unit names are discovered and authenticated by the trusted root verifier.
21type FenceTarget struct {
22 Region string `json:"region,omitempty"`
23 ClaimGen uint64 `json:"claim_gen,omitempty"`
24 ObligationID string `json:"obligation_id,omitempty"`
25 FenceID string `json:"fence_id"`
26 Host string `json:"host"`
27 Incarnation string `json:"incarnation"`
28 Stamp uint64 `json:"stamp"`
29 Session uint64 `json:"session"`
30 Op string `json:"op,omitempty"`
31 Phase string `json:"phase,omitempty"`
32 Box string `json:"box"`
33 Enrollment string `json:"enrollment"`
34 BootID string `json:"boot_id"`
35 ControllerID string `json:"controller_id"`
36 ClaimETag string `json:"claim_etag"`
37 DirectoryRevision string `json:"directory_revision"`
38 // RosterRevision is the roster write counter the stop's requester read,
39 // persisted with the fence before any stop effect. It is the one source of
40 // a stop receipt's directory_revision numeral (roster binding N1); the
41 // installation's binding is DirectoryRevision. It is receipt data only:
42 // retry authority ignores it (sameAuthority) and the journal v1 tuple
43 // never hashes it.
44 RosterRevision uint64 `json:"roster_revision,omitempty"`
45}
46
47func (t FenceTarget) validate(host string) error {
48 if validateFileHost(host) != nil || t.FenceID == "" || t.Host != host || t.Incarnation == "" || t.Stamp == 0 || t.Session == 0 ||
49 t.Box == "" || t.Enrollment == "" || t.BootID == "" || t.ControllerID == "" || t.ClaimETag == "" || t.DirectoryRevision == "" {
50 return ErrMalformedRecord
51 }
52 return nil
53}
54
55func (t FenceTarget) sameAuthority(other FenceTarget) bool {
56 t.FenceID, other.FenceID = "", ""
57 // The fresh request has already revalidated this session and operation.
58 // A bookkeeping CAS must not replace the persisted fence or its evidence.
59 t.ClaimETag, other.ClaimETag = "", ""
60 t.ClaimGen, other.ClaimGen = 0, 0
61 // The receipt numeral is the roster write counter at the request, which
62 // unrelated roster writes move between retries of one stop. The persisted
63 // target keeps the original numeral; a retry never replaces it (B2).
64 t.RosterRevision, other.RosterRevision = 0, 0
65 return t == other
66}
67
68func (t FenceTarget) matchesSession(c *hostclaim.Claim, captured Captured) bool {
69 return c != nil && c.Holder != nil && t.Host == c.Host && t.Incarnation == captured.Incarnation &&
70 t.Incarnation == c.Holder.Incarnation && t.Stamp == captured.Stamp && t.Stamp == c.Holder.Stamp &&
71 t.Session == c.Holder.Session
72}
73
74func (t FenceTarget) matchesAuthority(c *hostclaim.Claim, captured Captured) bool {
75 return t.matchesSession(c, captured) && t.Op == c.Op.ID && t.Phase == c.Op.Phase
76}
77
78type FenceReceipt struct {
79 Target FenceTarget `json:"target"`
80 Bytes []byte `json:"bytes"`
81 EvidenceAt time.Time `json:"evidence_at"`
82}
83
84func (r FenceReceipt) validate(host string) error {
85 if err := r.Target.validate(host); err != nil || len(r.Bytes) == 0 || r.EvidenceAt.IsZero() {
86 return ErrMalformedRecord
87 }
88 return nil
89}
90
91// BeginFence checks fresh claim authority while holding the host lock, then
92// durably installs the deny before any privileged stop work begins.
93func (g Guard) BeginFence(ctx context.Context, target FenceTarget) (FenceTarget, error) {
94 var authoritative FenceTarget
95 err := g.locked(ctx, target.Host, func(d *recordDir) error {
96 if err := target.validate(target.Host); err != nil {
97 return ErrInvalidInput
98 }
99 obs, err := g.freshObserved(ctx, target.Host)
100 if err != nil {
101 return err
102 }
103 c := obs.Claim
104 if obs.ETag != target.ClaimETag || !StopAuthorityMatches(c, target) {
105 return ErrFenceTarget
106 }
107 r, err := d.read(target.Host)
108 if err != nil {
109 return err
110 }
111 if r.Incarnation != target.Incarnation || r.ActiveStamp != target.Stamp {
112 return ErrFenceTarget
113 }
114 if r.ActiveFence != nil {
115 if *r.ActiveFence == target {
116 authoritative = *r.ActiveFence
117 return d.sync(target.Host)
118 }
119 if r.ActiveFence.sameAuthority(target) {
120 authoritative = *r.ActiveFence
121 return d.sync(target.Host)
122 }
123 // A completed predecessor remains independently cleanable from its
124 // immutable journal entry. An unsettled predecessor keeps the deny
125 // slot and cannot be overwritten by a successor request.
126 completed := false
127 for _, receipt := range r.FenceJournal {
128 if receipt.Target == *r.ActiveFence {
129 completed = true
130 break
131 }
132 }
133 if !completed {
134 return ErrFenceActive
135 }
136 }
137 r.ActiveFence = &target
138 authoritative = target
139 return d.write(r)
140 })
141 return authoritative, err
142}
143
144// FenceReceiptFor returns the immutable completed evidence for exactly target.
145// It does not consult the claim and is safe for transport recovery.
146func (g Guard) FenceReceiptFor(ctx context.Context, target FenceTarget) (FenceReceipt, error) {
147 var found FenceReceipt
148 err := g.fenceLocked(ctx, target.Host, Captured{Incarnation: target.Incarnation, Stamp: target.Stamp}, func(d *recordDir) error {
149 r, err := d.read(target.Host)
150 if err != nil {
151 return err
152 }
153 for _, receipt := range r.FenceJournal {
154 if receipt.Target == target {
155 found = receipt
156 return nil
157 }
158 }
159 return ErrNoFenceReceipt
160 })
161 return found, err
162}
163
164type FenceVerify func(context.Context, FenceTarget, []byte, time.Time) error
165
166// CompleteFence verifies root evidence without holding the host lock, then
167// journals its exact bytes. Replaying the same receipt is idempotent.
168func (g Guard) CompleteFence(ctx context.Context, target FenceTarget, receipt []byte, evidenceAt time.Time, verify FenceVerify) error {
169 if ctx == nil || verify == nil || len(receipt) == 0 || evidenceAt.IsZero() {
170 return ErrInvalidInput
171 }
172 if err := verify(ctx, target, receipt, evidenceAt); err != nil {
173 return err
174 }
175 return g.locked(ctx, target.Host, func(d *recordDir) error {
176 r, err := d.read(target.Host)
177 if err != nil {
178 return err
179 }
180 for _, old := range r.FenceJournal {
181 if old.Target == target {
182 if old.EvidenceAt.Equal(evidenceAt) && bytes.Equal(old.Bytes, receipt) {
183 return d.sync(target.Host)
184 }
185 return ErrReceiptChanged
186 }
187 }
188 if r.ActiveFence == nil || *r.ActiveFence != target {
189 return ErrFenceTarget
190 }
191 r.FenceJournal = append(r.FenceJournal, FenceReceipt{Target: target, Bytes: append([]byte(nil), receipt...), EvidenceAt: evidenceAt})
192 return d.write(r)
193 })
194}
195
196type CleanupVerify func(context.Context, FenceReceipt) error
197
198// CleanupWith authorizes from the persisted fence journal, so claim loss,
199// control-store outage, a free claim, or a successor cannot widen its target.
200func (g Guard) CleanupWith(ctx context.Context, target FenceTarget, verify CleanupVerify, fn func() error) error {
201 return g.cleanupWith(ctx, target, verify, fn, false)
202}
203
204// CleanupRetireInstallationWith retires only an exact still-current target
205// before cleanup effects. A successor record is preserved byte-for-byte.
206func (g Guard) CleanupRetireInstallationWith(ctx context.Context, target FenceTarget, verify CleanupVerify, fn func() error) error {
207 return g.cleanupWith(ctx, target, verify, fn, true)
208}
209
210func (g Guard) cleanupWith(ctx context.Context, target FenceTarget, verify CleanupVerify, fn func() error, retire bool) error {
211 if ctx == nil || verify == nil || fn == nil {
212 return ErrInvalidInput
213 }
214 var receipt FenceReceipt
215 err := g.locked(ctx, target.Host, func(d *recordDir) error {
216 r, err := d.read(target.Host)
217 if err != nil {
218 return err
219 }
220 for _, candidate := range r.FenceJournal {
221 if candidate.Target == target {
222 receipt = candidate
223 return nil
224 }
225 }
226 return ErrNoFenceReceipt
227 })
228 if err != nil {
229 return err
230 }
231 if err := verify(ctx, receipt); err != nil {
232 return err
233 }
234 return g.locked(ctx, target.Host, func(d *recordDir) error {
235 r, err := d.read(target.Host)
236 if err != nil {
237 return err
238 }
239 found := false
240 for _, candidate := range r.FenceJournal {
241 if candidate.Target == target && candidate.EvidenceAt.Equal(receipt.EvidenceAt) && bytes.Equal(candidate.Bytes, receipt.Bytes) {
242 found = true
243 break
244 }
245 }
246 if !found {
247 return ErrNoFenceReceipt
248 }
249 if retire && r.Incarnation == target.Incarnation && r.ActiveStamp == target.Stamp {
250 r.retire()
251 if err := d.write(r); err != nil {
252 return err
253 }
254 }
255 if err := fn(); err != nil {
256 return err
257 }
258 return d.sync(target.Host)
259 })
260}
261
262func StopAuthorityMatches(c *hostclaim.Claim, t FenceTarget) bool {
263 if !t.matchesAuthority(c, Captured{Incarnation: t.Incarnation, Stamp: t.Stamp}) {
264 return false
265 }
266 if t.ObligationID == "relocation-release" {
267 if c.Op.Kind != hostclaim.KindRelocating || c.Op.Phase != "releasing" || c.Op.Exec != t.Box || c.Op.Intent["source_box"] != t.Box || c.Op.Intent["region"] != t.Region {
268 return false
269 }
270 for _, ob := range c.Obligations {
271 if ob.ID == t.ObligationID && ob.Op == t.Op && ob.Kind == "relocation-release" && ob.Park == nil && ob.Box == t.Box && ob.Region == t.Region && ob.Owner == "region:"+t.Region {
272 return true
273 }
274 }
275 return false
276 }
277 if c.Op.Kind != hostclaim.KindServing {
278 // A writer-session stop names no chore, so it is only ever the
279 // landing's own current writer on this box (claim Plan 9 D4).
280 if writerStop(t.ObligationID) {
281 id, ok := LandingWriterStop(c, t.Box, t.Region)
282 if c.Op.Kind == hostclaim.KindRestoring || c.Op.Kind == hostclaim.KindReverting {
283 id, ok = RecoveryWriterStop(c, t.Box, t.Region)
284 }
285 if c.Op.Kind == hostclaim.KindRelocating {
286 id, ok = RelocationWriterStop(c, t.Box, t.Region)
287 }
288 if !ok || id != t.ObligationID {
289 return false
290 }
291 }
292 return c.Op.Exec == t.Box && t.Op != "" && t.Phase != ""
293 }
294 if t.Op != "" || t.Phase != "" || t.ObligationID != "" || t.Region == "" || c.Holder.WriterBox != t.Box || (c.Op.Exec != "" && c.Op.Exec != t.Box) {
295 return false
296 }
297 matched := false
298 for _, a := range c.Holder.Admissions {
299 if a.Session == t.Session {
300 if matched || a.Box != t.Box || a.Region != t.Region || a.Generation != c.Holder.Generation {
301 return false
302 }
303 matched = true
304 }
305 }
306 return matched
307}
308