package host import ( "bytes" "context" "encoding/json" "io" "net" "net/http" "path/filepath" "time" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/instancectl" ) type coldHandoverRequest struct { Prior hostguard.FenceTarget Next hostbox.ColdSuccessor } func (h ClaimRootHTTP) serveColdHandover(w http.ResponseWriter, r *http.Request, peer StopPeer) { var request coldHandoverRequest decoder := json.NewDecoder(io.LimitReader(r.Body, 32<<10)) decoder.DisallowUnknownFields() if decoder.Decode(&request) != nil || decoder.Decode(new(any)) != io.EOF || request.Prior.Box != h.Reconcile.Box || request.Prior.Enrollment != h.Reconcile.Enrollment { w.WriteHeader(http.StatusBadRequest) return } if request.Prior.Session == 0 && request.Prior.Op == "" && request.Prior.Phase == "" { prior, err := h.servingLegacyHandoverTarget(r.Context(), request) if err != nil { w.WriteHeader(http.StatusConflict) return } request.Prior = prior } if request.Prior.Session != 0 && (request.Prior.BootID != peer.BootID || request.Prior.ControllerID != peer.ControllerID) { // ServeHTTP authenticates the live requester independently. Recovery // preserves the predecessor's recorded tuple; only its exact completed // root receipt grants cleanup authority, never the requester's tuple. receipt, err := h.Stop.Coordinator.Guard.FenceReceiptFor(r.Context(), request.Prior) if err != nil || verifyRetirementReceipt(r.Context(), receipt) != nil { w.WriteHeader(http.StatusBadRequest) return } } if h.Launches == nil { w.WriteHeader(http.StatusServiceUnavailable) return } handover := h.coldHandover() var err error if request.Next.Op == "" && request.Next.Phase == "" { serving := h.servingHandover() serving.Validate = func(ctx context.Context, c *hostclaim.Claim, p hostguard.FenceTarget, n hostbox.ColdSuccessor, in hostbox.StopInventory, installation hostguard.RootInstallation) error { fresh, err := h.Stop.Authenticate(ctx, instancectl.RequestConn(r.Context())) if err != nil { return err } if fresh != peer { return ErrClaimStopIdentity } return h.validateServingHandover(ctx, c, p, n, in, installation) } err = serving.Run(r.Context(), request.Prior, request.Next) } else { err = handover.Run(r.Context(), request.Prior, request.Next) } if err != nil { w.WriteHeader(http.StatusConflict) return } w.WriteHeader(http.StatusNoContent) } // coldHandover is root's operation cold handover. Its successor admission // (roster binding N5, B1) runs before Run takes any step, for a new // handover and for a replay alike: the prior keeps its historical authority // (its exact receipt, record-to-record checks), the successor must be // stamped with the binding the roster gives this box now. func (h ClaimRootHTTP) coldHandover() hostbox.ColdHandover { return hostbox.ColdHandover{OperationsOnly: true, Guard: h.Stop.Coordinator.Guard, Claims: h.Stop.Coordinator.Evidence, Registry: h.Reconcile.Registry, Inventory: h.Reconcile.Inventory, Launches: h.Launches, Roots: h.Roots, JournalDir: filepath.Join(h.Reconcile.Retirement.JournalDir, "cold"), VerifyReceipt: verifyRetirementReceipt, Admit: h.admitColdSuccessor} } // admitColdSuccessor is the fresh successor check (roster binding N5): new // work must be allowed (a quarantined box is refused) and the successor must // name this box, its enrollment and its binding as the fresh identity reads // them. It is checked before any successor state exists. func (h ClaimRootHTTP) admitColdSuccessor(ctx context.Context, next hostbox.ColdSuccessor) error { if h.InventoryActor == nil { return hostbox.ErrUnsupported } if h.RequireNewWork != nil { if err := h.RequireNewWork(ctx); err != nil { return err } } actor, err := h.InventoryActor(ctx) if err != nil { return err } if actor.Binding == "" || next.Box != actor.Box || next.Enrollment != actor.Enrollment || next.DirectoryRevision != actor.Binding { return hostbox.ErrChanged } return nil } func coldAdmissionHook(guard hostguard.Guard, identity hostbox.IdentityProvider, socket string) func(context.Context, hostclaim.Observed, hostclaim.Observed) error { return coldHandoverWith(identity, socket, func(ctx context.Context, prior hostclaim.Observed) (hostguard.FenceReceipt, error) { old := prior.Claim.Holder if old.Session == 0 && prior.Claim.Op.Kind == hostclaim.KindServing && prior.Claim.Op.ID == "" && prior.Claim.Op.Phase == "" { actor, err := identity.Verify(ctx) if err != nil { return hostguard.FenceReceipt{}, err } return hostguard.FenceReceipt{Target: hostguard.FenceTarget{Host: prior.Host, Incarnation: old.Incarnation, Stamp: old.Stamp, Box: actor.Box, Enrollment: actor.Enrollment}}, nil } return guard.CompletedColdPredecessor(ctx, prior.Host, hostguard.Captured{Incarnation: old.Incarnation, Stamp: old.Stamp}, old.Session, prior.Claim.Op.ID, prior.Claim.Op.Phase) }) } // coldHandoverWith is coldAdmissionHook with the predecessor's completed // receipt found by find (extracted unchanged; region HA N6): root's // ColdHandover.Run validates the exact prior receipt and the successor. func coldHandoverWith(identity hostbox.IdentityProvider, socket string, find func(context.Context, hostclaim.Observed) (hostguard.FenceReceipt, error)) func(context.Context, hostclaim.Observed, hostclaim.Observed) error { return func(ctx context.Context, prior, admitted hostclaim.Observed) error { if prior.Claim == nil || prior.Claim.Holder == nil || admitted.Claim == nil || admitted.Claim.Holder == nil || prior.Host != admitted.Host { return hostbox.ErrChanged } next := admitted.Claim.Holder receipt, err := find(ctx, prior) if err != nil { return err } actor, err := identity.Verify(ctx) if err != nil { return err } request := coldHandoverRequest{Prior: receipt.Target, Next: hostbox.ColdSuccessor{Host: admitted.Host, Incarnation: next.Incarnation, Stamp: next.Stamp, Session: next.Session, Generation: next.Generation, Box: actor.Box, Enrollment: actor.Enrollment, DirectoryRevision: actor.Binding, ETag: admitted.ETag, Op: admitted.Claim.Op.ID, Phase: admitted.Claim.Op.Phase}} body, err := json.Marshal(request) if err != nil { return err } transport := &http.Transport{DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { return (&net.Dialer{}).DialContext(ctx, "unix", socket) }} defer transport.CloseIdleConnections() req, err := http.NewRequestWithContext(ctx, http.MethodPost, "http://instance-control/claim/handover", bytes.NewReader(body)) if err != nil { return err } response, err := (&http.Client{Transport: transport, Timeout: 2 * time.Minute}).Do(req) if err != nil { return err } defer response.Body.Close() if response.StatusCode != http.StatusNoContent { return hostbox.ErrChanged } return nil } }