package main // DCN machine-store handlers — the box-side wall for the Distributed Context // Network. Dedicated Go handlers, NO exec, NO shell — like the context-store // handlers, these never touch p8AllowedOps. Wire framing reuses sendMgmt / // sendMgmtExit: O = stdout (a JSON result object), R = stderr, X = exit code. // // THE TRUST MODEL, honestly: the pod gate already default-denies un-granted DCN ops, // but for DCN the box must be its OWN wall — a Go handler on hardware you own // refuses — so the signed grant is synced HERE (dcn_grant_sync), the // entity's verify key is PINNED on first sync (TOFU over the mTLS+bearer lane the // pairing already trusts; a later key change is refused), and every op re-verifies // the stored grant's signature + expiry before touching a byte. A compromised pod // can ask; an unsigned/expired/out-of-mask/out-of-root ask is refused on the box. // // The op vocabulary is read-only (list / stat / read_range). There is NO delete // handler — the operation does not exist on this surface, by design. // // Exit convention (the mapping the host applies to a machine op's result): // 0 — handled; O carries a JSON object whose "state" is ok|gone // 126 — refused (no grant / bad signature / mask / excludes / path escape) // 1 — handler error (bad input, IO failure) import ( "crypto/ed25519" "encoding/base64" "encoding/json" "fmt" "io/fs" "os" "path/filepath" "runtime" "sort" "strconv" "strings" "time" "github.com/gorilla/websocket" ) const dcnMaxRangeBytes = 512 * 1024 const dcnMaxListEntries = 1000 // dcnGrantFile is /grants.json — the box's copy of the user's signed // store grants, plus the pinned entity verify key. 0600; the box's private state. // The dir is PER PAIRING (Config.dcnDir): the pin is per-entity, and multi-homing // (0.10.0) means several entities share this daemon — a shared pin would let one // entity's key verify another's grants. type dcnGrantFile struct { // b64 raw ed25519. PINNED by whichever writer reaches it first — a DCN store // sync was the only one until 0.24.3, when session.go added the session // frame's own key so a Terminal needs no folder grant. Both write this one // field, and a key that disagrees with the pin is REFUSED, never overwritten. VerifyPubkey string `json:"verify_pubkey"` Stores map[string]dcnGrantRec `json:"stores"` // Reach grants (service_reach.go): which host may reach which server in // this machine's service slot. Same signed-record shape and the same pinned // key as a store grant; verified by reachVerifyRec, which checks its kind. Reach map[string]dcnGrantRec `json:"reach,omitempty"` // ReachDropped: reach grants this box dropped because their SERVER was // stopped (reachDropForSlot), reported in the enforcing report so the host // revokes its row instead of re-pushing a grant for a server that is gone. // Newest last, capped at reachDroppedMax. ReachDropped []int64 `json:"reach_dropped,omitempty"` // Coder grants (coder_grant.go): the standing grant that lets this host // start coder runs here without a per-run approval. Same signed-record // shape and pinned key; verified by coderVerifyRec, which checks its kind. Coder map[string]dcnGrantRec `json:"coder,omitempty"` } type dcnGrantRec struct { TermsCanonical string `json:"terms_canonical"` // b64 of the EXACT signed bytes Signature string `json:"signature"` // b64 ed25519 over those bytes // The user's registered backup key as an age recipient — carried OUTSIDE // the signed bytes (a public key; the terms stay exactly what was // signed). The managed local agent seals the store's catalog to it. SealRecipient string `json:"seal_recipient,omitempty"` // RootReal is where the granted folder led when this box installed the // grant: its real path, pinned here (dcn_store.go), outside the signed // bytes. An op whose root leads anywhere else is refused. RootReal string `json:"root_real,omitempty"` } // dcnTerms is parsed FROM the verified canonical bytes — never from anything else. type dcnTerms struct { Kind string `json:"kind"` Class string `json:"class"` Server string `json:"server"` Root string `json:"root"` Mask []string `json:"mask"` Excludes []string `json:"excludes"` Expires string `json:"expires"` // The local agent's cadence + the sensitive flag — part of the approval purpose, // so a pod cannot change them under an approval the user gave. Sweep string `json:"sweep,omitempty"` Sensitive bool `json:"sensitive,omitempty"` TTLDays int `json:"ttl_days,omitempty"` // shown to the user; the signed expiry may not exceed it // Approval this box issued after the user confirmed the request HERE // (stepup.go). Required at sync when a second factor is enrolled on this // machine; ignored otherwise. Approval string `json:"approval,omitempty"` } func dcnLoadFile(dir string) (*dcnGrantFile, error) { raw, err := os.ReadFile(filepath.Join(dir, "grants.json")) if os.IsNotExist(err) { return &dcnGrantFile{Stores: map[string]dcnGrantRec{}}, nil } if err != nil { return nil, err } var f dcnGrantFile if err := json.Unmarshal(raw, &f); err != nil { return nil, err } if f.Stores == nil { f.Stores = map[string]dcnGrantRec{} } return &f, nil } // pinVerifyKey decides which entity_sign key this pairing's grants are // verified under, and pins it into f when it was not yet. Returns (pinned, // refusal): pinned means f changed and the caller saves it. // // LOCAL box: TOFU from the first offer over the mTLS+bearer lane the pairing // already trusts; an offer that disagrees with the pin is refused (the rule // since the DCN lane existed). // // ANCHORED box: the key pinned AT ANCHORING (anchor.go pinnedKeyFor) — the // anchor host's from claim-anchor, a member's from the anchor-signed pairing // grant. A key the ANCHOR host offers on this lane is checked against its pin // and never pinned from: a host that could pin its own key on first use could // then sign its own grants, and the anchor host is the one that writes here. // // A MEMBER whose pairing grant carries no verify_key keeps first-use pinning // for now: the anchor host does not hold another host's signing key today // (a host signs only its own pairing grants), so refusing // would strand every member pairing on the fleet. The grant field is read as // soon as a host fills it, and the member's offer is then checked against it. func pinVerifyKey(f *dcnGrantFile, pairing, offered string) (bool, string) { offered = strings.TrimSpace(offered) if offered != "" { if raw, err := base64.StdEncoding.DecodeString(offered); err != nil || len(raw) != ed25519.PublicKeySize { return false, "host offered a malformed verify key" } } if currentAnchor() != nil { want := pinnedKeyFor(pairing) switch { case want == "" && f.VerifyPubkey == "": if offered == "" { return false, "no entity key pinned on this box yet (default-deny)" } f.VerifyPubkey = offered // a member's first use, until its grant carries verify_key return true, "" case want != "" && f.VerifyPubkey == "": f.VerifyPubkey = want if offered != "" && offered != want { return false, "verify key mismatch — the host offered a key that is not the one pinned at anchoring" } return true, "" case want != "" && f.VerifyPubkey != want: return false, "verify key mismatch — the key pinned here is not the one pinned at anchoring (re-enrol this box to rotate keys)" } if offered != "" && offered != f.VerifyPubkey { return false, "verify key mismatch — refusing re-pin (re-enrol this box to rotate keys)" } return false, "" } if f.VerifyPubkey == "" && offered != "" { f.VerifyPubkey = offered // TOFU pin, first use only return true, "" } if offered != "" && f.VerifyPubkey != offered { return false, "verify key mismatch — refusing re-pin (re-enrol this box to rotate keys)" } return false, "" } func dcnSaveFile(dir string, f *dcnGrantFile) error { raw, err := json.MarshalIndent(f, "", " ") if err != nil { return err } return atomicWrite(filepath.Join(dir, "grants.json"), raw) } // dcnAudit appends one JSONL record to /audit.jsonl — the box-side // accounting ("every read is logged for you"). Best-effort: auditing never blocks // a result, but a refusal is still a refusal whether or not the line lands. func dcnAudit(dir, op string, storeID int64, path, decision string) { rec, _ := json.Marshal(map[string]any{ "ts": time.Now().UTC().Format(time.RFC3339), "op": op, "store_id": storeID, "path": path, "decision": decision, }) auditToSystemLog(auditFilePairingD, auditPairingOfDir(dir), rec) fh, err := os.OpenFile(filepath.Join(dir, "audit.jsonl"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600) if err != nil { return } defer fh.Close() _, _ = fh.Write(append(rec, '\n')) } // dcnVerifyRec checks one stored grant record against the pinned key and returns its // parsed terms. Re-verified on EVERY op — a tampered grants.json authorizes nothing. func dcnVerifyRec(pinnedKeyB64 string, rec dcnGrantRec) (*dcnTerms, error) { canonical, err := verifySignedRecord(pinnedKeyB64, rec) if err != nil { return nil, err } var terms dcnTerms if err := json.Unmarshal(canonical, &terms); err != nil { return nil, fmt.Errorf("signed terms unparsable") } // ITS KIND, CHECKED. The entity key signs several kinds of record (store // grants, reach grants, …) and this verifier used to parse `kind` without // comparing it, so any record the key had signed could be offered as a // store grant. The host has signed "dcn_store_grant" on every store grant // since store grants existed, so none is refused by this. if terms.Kind != "dcn_store_grant" { return nil, fmt.Errorf("signed record is a %q, not a dcn_store_grant", terms.Kind) } // NO OPEN-ENDED TERMS. A grant with no expiry used to verify forever; the // host has always signed one (365 days by default), so refusing its absence // costs nothing real and closes a grant that could never lapse. exp, err := time.Parse(time.RFC3339, terms.Expires) if err != nil { return nil, fmt.Errorf("grant states no expiry (open-ended terms are refused)") } if !exp.After(time.Now()) { return nil, fmt.Errorf("grant expired") } return &terms, nil } // dcnResolvePath sandbox-checks rpath against the granted root: cleanRel (traversal // wall) THEN symlink resolution — the final resolved target must stay under the // resolved root (a symlink inside the store pointing outside it is an escape). func dcnResolvePath(root, rpath string) (string, bool) { joined, ok := cleanRel(root, rpath) if !ok { return "", false } resolvedRoot, err := filepath.EvalSymlinks(filepath.Clean(root)) if err != nil { return "", false } // Resolve the deepest existing ancestor of joined (the file itself may not exist // yet for stat-of-missing; a missing path can't be a symlink escape). probe := joined for { r, err := filepath.EvalSymlinks(probe) if err == nil { if r != resolvedRoot && !strings.HasPrefix(r, resolvedRoot+string(os.PathSeparator)) { return "", false } break } // Missing is fine, and so is a broken link as the last part of the path: // a stat of it reads as gone and a move renames the link itself. A path // that goes THROUGH a link that leads nowhere (or loops) is refused: // nothing can be reached through it, and where it would lead cannot be // shown to be inside the root. if _, lerr := os.Lstat(probe); lerr == nil && probe != joined { return "", false } parent := filepath.Dir(probe) if parent == probe { return "", false } probe = parent } return joined, true } // dcnExcluded reports whether rpath trips any exclude pattern, or is inside a // local search agent's folder (dcnInAgentDirOn), which no grant reaches. A // pattern of one folder name matches any segment of the path (".ssh", "*.key", // ".env*"); a pattern with a "/" (".config/gcloud") matches that run of // consecutive segments wherever it sits in the path, each one a glob. Names // are compared the way this platform's file system compares them (dcnNameKey). // rpath is a real relative path; a path a host sent goes through dcnExcludedAsSent. func dcnExcluded(excludes []string, rpath string) bool { return dcnExcludedOn(runtime.GOOS, excludes, rpath) } func dcnExcludedOn(goos string, excludes []string, rpath string) bool { if dcnInAgentDirOn(goos, rpath) { return true } segs := strings.Split(filepath.ToSlash(rpath), "/") for i, seg := range segs { segs[i] = dcnNameKey(goos, seg) } for _, pat := range excludes { pat = strings.TrimSpace(pat) if pat == "" { continue } var run []string for _, p := range strings.Split(pat, "/") { if p != "" { run = append(run, dcnNameKey(goos, p)) } } if dcnRunMatches(run, segs) { return true } } return false } // dcnRunMatches says whether the globs in run match len(run) consecutive // segments of segs, starting anywhere. An empty run matches nothing. func dcnRunMatches(run, segs []string) bool { if len(run) == 0 { return false } for i := 0; i+len(run) <= len(segs); i++ { all := true for j, p := range run { if ok, _ := filepath.Match(p, segs[i+j]); !ok { all = false break } } if all { return true } } return false } // dcnAgentDir is the folder the local search agent keeps everything it makes // in, beside the files it reads: the plaintext of every file in scope, the // sanitized text, vectors, the catalog and sealed bundles. The bastion puts // it at /.agent wherever it may write into the folder (freeagent_managed.go; // the agent's own default too), and a folder inside a store can hold another // store's. The agent's walker never reads one, at any depth and in any case; // the file ops never serve one either. const dcnAgentDir = ".agent" // dcnInAgentDirOn reports whether any segment of rpath names an agent's // folder, as the file system on goos names it, and in any case on every OS // (as the agent's own walker judges it: a ".Agent" on a volume that ignores // case is the agent's folder). func dcnInAgentDirOn(goos, rpath string) bool { for _, seg := range strings.Split(filepath.ToSlash(rpath), "/") { if strings.EqualFold(dcnNameKey(goos, seg), dcnAgentDir) { return true } } return false } // dcnNameKey is a name as the file system on goos tells names apart. Windows // and macOS ignore case by default, so ".SSH" opens ".ssh" there. Windows also // drops trailing dots and spaces from a name (".ssh." opens ".ssh") and reads // "name:stream" as a stream of the file "name" (server.key::$DATA is server.key). func dcnNameKey(goos, name string) string { switch goos { case "windows": if i := strings.IndexByte(name, ':'); i >= 0 { name = name[:i] } return strings.ToLower(strings.TrimRight(name, " .")) case "darwin": return strings.ToLower(name) } return name } // dcnExcludedAsSent is dcnExcluded for a path a host sent, and it judges what // the path OPENS rather than its text, both ways it can differ: // - the spelling: cleanRel trims and cleans before it opens, so " .ssh/id_rsa" // or "a.key " would get past a check of the text as sent; // - the links: a link inside the store to an excluded folder ("keys" -> ".ssh") // makes "keys/id_rsa" open .ssh/id_rsa. So the path is also judged where it // leads, relative to the resolved root. // // A path cleanRel refuses is judged as sent (the sandbox refuses it anyway). func dcnExcludedAsSent(excludes []string, root, rpath string) bool { return dcnJudgedAsSent(root, rpath, func(rel string) bool { return dcnExcluded(excludes, rel) }) } // dcnAgentAsSent is whether a path a host sent opens an agent's folder (by its // spelling or through a link), judged as dcnExcludedAsSent judges excludes. // dcnExcluded refuses the same paths; this one exists so the refusal can say // why. func dcnAgentAsSent(root, rpath string) bool { return dcnJudgedAsSent(root, rpath, func(rel string) bool { return dcnInAgentDirOn(runtime.GOOS, rel) }) } // dcnJudgedAsSent applies judge to what a host's path opens: the cleaned path // relative to the root, then where it leads relative to the resolved root. func dcnJudgedAsSent(root, rpath string, judge func(rel string) bool) bool { joined, ok := cleanRel(root, rpath) if !ok { return judge(rpath) } if rel, err := filepath.Rel(filepath.Clean(root), joined); err != nil || judge(rel) { return true } resolvedRoot, err := filepath.EvalSymlinks(filepath.Clean(root)) if err != nil { return true } rel, err := filepath.Rel(resolvedRoot, dcnWhereItLeads(joined)) if err != nil { return true } return judge(rel) } // dcnWhereItLeads is p with its deepest existing part's links resolved and the // rest (not there yet) appended: where a read of p opens, or a move to p lands. func dcnWhereItLeads(p string) string { rest := "" for probe := p; ; { if r, err := filepath.EvalSymlinks(probe); err == nil { return filepath.Join(r, rest) } parent := filepath.Dir(probe) if parent == probe { return p } rest = filepath.Join(filepath.Base(probe), rest) probe = parent } } func dcnHasMask(terms *dcnTerms, op string) bool { for _, m := range terms.Mask { if m == op { return true } } return false } func dcnRefuse(ws *websocket.Conn, dir, op string, storeID int64, path, why string) { dcnAudit(dir, op, storeID, path, "refused: "+why) sendMgmt(ws, 'R', []byte(why+"\n")) sendMgmtExit(ws, 126) } func dcnResult(ws *websocket.Conn, dir, op string, storeID int64, path string, obj map[string]any) { state, _ := obj["state"].(string) dcnAudit(dir, op, storeID, path, state) raw, err := json.Marshal(obj) if err != nil { sendMgmt(ws, 'R', []byte("result marshal failed\n")) sendMgmtExit(ws, 1) return } sendMgmt(ws, 'O', raw) sendMgmtExit(ws, 0) } // dcnLoadGrant fetches + re-verifies the grant for storeID and gates (op, rpath) // against the 'read' mask — list/stat/read_range. dcnLoadGrantMask is the same // wall with the mask named by the caller (search needs 'search'). func dcnLoadGrant(ws *websocket.Conn, dir, op string, storeID int64, rpath string) *dcnTerms { return dcnLoadGrantMask(ws, dir, op, storeID, rpath, "read") } func dcnLoadGrantMask(ws *websocket.Conn, dir, op string, storeID int64, rpath, mask string) *dcnTerms { f, err := dcnLoadFile(dir) if err != nil { dcnRefuse(ws, dir, op, storeID, rpath, "grant store unreadable") return nil } if f.VerifyPubkey == "" { dcnRefuse(ws, dir, op, storeID, rpath, "no grant synced to this box (default-deny)") return nil } rec, ok := f.Stores[fmt.Sprint(storeID)] if !ok { dcnRefuse(ws, dir, op, storeID, rpath, "no grant for this store on this box (default-deny)") return nil } terms, err := dcnVerifyRec(f.VerifyPubkey, rec) if err != nil { dcnRefuse(ws, dir, op, storeID, rpath, "grant refused: "+err.Error()) return nil } if terms.Class != "machine" || terms.Root == "" { dcnRefuse(ws, dir, op, storeID, rpath, "grant is not a machine store") return nil } // list/stat/read_range are read-class ops; search (freeagent.go) needs the // 'search' mask the user granted separately. if !dcnHasMask(terms, mask) { dcnRefuse(ws, dir, op, storeID, rpath, "op '"+mask+"' not in the store's granted mask") return nil } if rpath != "" && dcnAgentAsSent(terms.Root, rpath) { dcnRefuse(ws, dir, op, storeID, rpath, "path is in the local search agent's own folder (.agent), which no grant reaches") return nil } if rpath != "" && dcnExcludedAsSent(terms.Excludes, terms.Root, rpath) { dcnRefuse(ws, dir, op, storeID, rpath, "path is excluded by the grant") return nil } return terms } type dcnOpArgs struct { Path string `json:"path"` Prefix string `json:"prefix"` StartByte int64 `json:"start_byte"` EndByte int64 `json:"end_byte"` Limit int `json:"limit"` // local agent form (freeagent.go): a search query, and a document id for // read_range in place of a path. Absent on the classic file ops. Query string `json:"query"` K int `json:"k"` Doc string `json:"doc"` Kind string `json:"kind"` // catalog: "dirs" (default) | "files" Offset int `json:"offset"` // catalog: page start } func dcnParseArgs(ws *websocket.Conn, args []string) (int64, *dcnOpArgs, bool) { if len(args) < 2 { sendMgmt(ws, 'R', []byte("need \n")) sendMgmtExit(ws, 1) return 0, nil, false } var storeID int64 if _, err := fmt.Sscan(strings.TrimSpace(args[0]), &storeID); err != nil { sendMgmt(ws, 'R', []byte("bad store_id\n")) sendMgmtExit(ws, 1) return 0, nil, false } var a dcnOpArgs if err := json.Unmarshal([]byte(args[1]), &a); err != nil { sendMgmt(ws, 'R', []byte("bad json args\n")) sendMgmtExit(ws, 1) return 0, nil, false } return storeID, &a, true } // handleDcnGrantSync installs/updates ONE signed grant: args[0] = b64 JSON // {store_id, terms_canonical, signature, verify_pubkey}. First sync pins the verify // key; a different key later is REFUSED (re-pinning is a re-enrol level event, not // an op). The record is verified before it is stored — the box never persists a // grant it couldn't enforce. func handleDcnGrantSync(ws *websocket.Conn, dir string, args []string) { if len(args) < 1 { sendMgmt(ws, 'R', []byte("dcn_grant_sync: need \n")) sendMgmtExit(ws, 1) return } raw, err := base64.StdEncoding.DecodeString(strings.TrimSpace(args[0])) if err != nil { sendMgmt(ws, 'R', []byte("bad base64\n")) sendMgmtExit(ws, 1) return } var payload struct { StoreID int64 `json:"store_id"` TermsCanonical string `json:"terms_canonical"` Signature string `json:"signature"` VerifyPubkey string `json:"verify_pubkey"` SealRecipient string `json:"seal_recipient"` } if err := json.Unmarshal(raw, &payload); err != nil { sendMgmt(ws, 'R', []byte("bad payload json\n")) sendMgmtExit(ws, 1) return } grantsMu.Lock() // one load→modify→save at a time (reconcile.go) defer grantsMu.Unlock() f, err := dcnLoadFile(dir) if err != nil { sendMgmt(ws, 'R', []byte("grant store unreadable\n")) sendMgmtExit(ws, 1) return } // The key this grant is verified under. On a LOCAL box: TOFU from this // first sync, over the mTLS+bearer lane the pairing already trusts. On an // ANCHORED box: the key pinned AT ANCHORING (anchor.go pinnedKeyFor) — // the anchor's from claim-anchor, a member's from the anchor-signed // pairing grant — and never one this sync offers. A host that could pin // its own key on its first sync could sign its own grants. self := "" if a := mgmtAccountOf(ws); a != nil { self = a.self } if _, why := pinVerifyKey(f, self, payload.VerifyPubkey); why != "" { dcnRefuse(ws, dir, "grant_sync", payload.StoreID, "", why) return } rec := dcnGrantRec{TermsCanonical: payload.TermsCanonical, Signature: payload.Signature, SealRecipient: strings.TrimSpace(payload.SealRecipient)} terms, err := dcnVerifyRec(f.VerifyPubkey, rec) if err != nil { dcnRefuse(ws, dir, "grant_sync", payload.StoreID, "", "grant does not verify: "+err.Error()) return } // The folder this grant names is pinned as it resolves now; a re-sync or a // renewal of the same root keeps the pin it already has (dcn_store.go). if prev, held := f.Stores[fmt.Sprint(payload.StoreID)]; held { prevRoot := "" if pt, perr := dcnVerifyRec(f.VerifyPubkey, prev); perr == nil { prevRoot = pt.Root } rec.RootReal = dcnPinFor(&prev, prevRoot, terms.Root) } else { rec.RootReal = dcnPinFor(nil, "", terms.Root) } if hn, _ := os.Hostname(); hn != "" && terms.Server != "" && terms.Server != hn { dcnRefuse(ws, dir, "grant_sync", payload.StoreID, "", fmt.Sprintf("grant is for server '%s', this box is '%s'", terms.Server, hn)) return } // Second factor on THIS box (stepup.go): when enrolled here, the grant must // carry an approval this machine issued for exactly these terms, after the // user confirmed them HERE. A pod can sign anything; this box enforces only // what the user approved on it. A re-sync of the SAME signed record while it // is still held (drift repair) needs no gate: identical bytes, already // approved. The approval is BURNED once the grant is installed, so a revoke // is final: the pod cannot re-install a dropped grant under the old // approval. Consequence: a box that loses grants.json cannot be repaired by // the heartbeat resync — the user re-grants and approves on the machine. gated := false if prev, held := f.Stores[fmt.Sprint(payload.StoreID)]; !held || prev.Signature != rec.Signature { if approvalRequired(dir) { purpose, perr := purposeForGrant(terms) if perr != nil { dcnRefuse(ws, dir, "grant_sync", payload.StoreID, "", "grant refused: "+perr.Error()) return } if err := boxApproval(dir, terms.Approval, purpose); err != nil { fmt.Printf("grant_sync store %d refused: %s (a re-grant approved on this machine is the repair)\n", payload.StoreID, err) dcnRefuse(ws, dir, "grant_sync", payload.StoreID, "", err.Error()) return } // The user approved "for N days"; the signed expiry may not outrun it, // and a grant with no stated duration is not approvable here at all. if terms.TTLDays <= 0 { dcnRefuse(ws, dir, "grant_sync", payload.StoreID, "", "grant states no duration (ttl_days)") return } { if exp, err := time.Parse(time.RFC3339, terms.Expires); err != nil || exp.After(time.Now().Add(time.Duration(terms.TTLDays+1)*24*time.Hour)) { dcnRefuse(ws, dir, "grant_sync", payload.StoreID, "", "grant expiry exceeds the approved duration") return } } gated = true } } f.Stores[fmt.Sprint(payload.StoreID)] = rec if err := dcnSaveFile(dir, f); err != nil { sendMgmt(ws, 'R', []byte("grant persist failed: "+err.Error()+"\n")) sendMgmtExit(ws, 1) return } if gated { markApprovalUsed(dir, terms.Approval) // one approval installs the grant once; a revoke is final } // local agent, managed (freeagent_managed.go): a grant with 'search' means // this box installs and runs the store's local agent itself; one without it // stops any instance a previous grant started. if dcnHasMask(terms, "search") { faEnsureStore(dir, payload.StoreID, terms, sweepFromCanonical(rec.TermsCanonical), rec.SealRecipient) } else { faStop(payload.StoreID) } dcnResult(ws, dir, "grant_sync", payload.StoreID, "", map[string]any{ "state": "ok", "root": terms.Root, "mask": terms.Mask, "freeagent": dcnHasMask(terms, "search")}) } // dcnDropStore removes one store's grant from this box and stops its managed // local agent. Two callers: the dcn_grant_drop op (the host pushes a revoke the // moment it happens) and the heartbeat reconcile (a revoke the box missed while // it was offline — reconcile.go). Before this existed a revoked grant stayed // valid on the box until its signed expiry (default 365 days) and the local agent // kept indexing. Returns whether the store was present. func dcnDropStore(dir string, storeID int64, why string) (bool, error) { grantsMu.Lock() defer grantsMu.Unlock() return dcnDropStoreLocked(dir, storeID, why) } // dcnDropStoreLocked is dcnDropStore for a caller that already holds grantsMu // (the reconcile loop, which walks the file it is modifying). func dcnDropStoreLocked(dir string, storeID int64, why string) (bool, error) { f, err := dcnLoadFile(dir) if err != nil { return false, err } key := strconv.FormatInt(storeID, 10) if _, ok := f.Stores[key]; !ok { faStop(storeID) // nothing on disk, but never leave an orphan instance running return false, nil } delete(f.Stores, key) if err := dcnSaveFile(dir, f); err != nil { return true, err } faStop(storeID) dcnAudit(dir, "grant_drop", storeID, "", why) return true, nil } // handleDcnGrantDrop — the host revoked a store grant: forget it here, now. // A tighten needs no verification (an attacker who can speak this lane can at // most remove access, never widen it), so the op carries just the store id. func handleDcnGrantDrop(ws *websocket.Conn, dir string, args []string) { if len(args) < 1 { sendMgmt(ws, 'R', []byte("dcn_grant_drop: need \n")) sendMgmtExit(ws, 1) return } storeID, err := strconv.ParseInt(strings.TrimSpace(args[0]), 10, 64) if err != nil { sendMgmt(ws, 'R', []byte("dcn_grant_drop: bad store_id\n")) sendMgmtExit(ws, 1) return } present, err := dcnDropStore(dir, storeID, "host-revoked") if err != nil { sendMgmt(ws, 'R', []byte("grant drop failed: "+err.Error()+"\n")) sendMgmtExit(ws, 1) return } dcnResult(ws, dir, "grant_drop", storeID, "", map[string]any{ "state": "ok", "dropped": present}) } // handleDcnList walks the granted root (optionally under a prefix) and returns up to // `limit` entries, excludes skipped. Read-only; never follows a path outside the root. func handleDcnList(ws *websocket.Conn, dir string, args []string) { storeID, a, ok := dcnParseArgs(ws, args) if !ok { return } terms := dcnLoadGrant(ws, dir, "list", storeID, a.Prefix) if terms == nil { return } base := "." if a.Prefix != "" { p, ok := dcnResolvePath(terms.Root, a.Prefix) if !ok { dcnRefuse(ws, dir, "list", storeID, a.Prefix, "prefix escapes the store root") return } rel, err := dcnRel(terms.Root, p) if err != nil { dcnRefuse(ws, dir, "list", storeID, a.Prefix, "prefix escapes the store root") return } base = filepath.ToSlash(rel) } r, err := dcnOpenStore(dir, storeID, terms) if err != nil { dcnRefuse(ws, dir, "list", storeID, a.Prefix, err.Error()) return } defer r.Close() limit := a.Limit if limit <= 0 || limit > dcnMaxListEntries { limit = dcnMaxListEntries } type entry struct { Path string `json:"path"` Size int64 `json:"size_bytes"` Mtime string `json:"mtime"` } var files []entry truncated := false // Walked through the store's handle: a link that leads out of it is not // followed, whatever the names do during the walk. _ = fs.WalkDir(r.FS(), base, func(p string, d fs.DirEntry, err error) error { if err != nil { return nil // unreadable subtree: skip, never abort the listing } rel := filepath.FromSlash(p) if rel == "." { return nil } if dcnExcluded(terms.Excludes, rel) { if d.IsDir() { return filepath.SkipDir } return nil } if d.IsDir() { return nil } if len(files) >= limit { truncated = true return filepath.SkipAll } info, ierr := d.Info() if ierr != nil { return nil } files = append(files, entry{Path: filepath.ToSlash(rel), Size: info.Size(), Mtime: info.ModTime().UTC().Format(time.RFC3339)}) return nil }) sort.Slice(files, func(i, j int) bool { return files[i].Path < files[j].Path }) dcnResult(ws, dir, "list", storeID, a.Prefix, map[string]any{ "state": "ok", "files": files, "count": len(files), "truncated": truncated}) } // handleDcnStat stats one path. Missing → state=gone (a typed fact, not an error). func handleDcnStat(ws *websocket.Conn, dir string, args []string) { storeID, a, ok := dcnParseArgs(ws, args) if !ok { return } terms := dcnLoadGrant(ws, dir, "stat", storeID, a.Path) if terms == nil { return } abs, okp := dcnResolvePath(terms.Root, a.Path) if !okp { dcnRefuse(ws, dir, "stat", storeID, a.Path, "path escapes the store root") return } rel, err := dcnRel(terms.Root, abs) if err != nil { dcnRefuse(ws, dir, "stat", storeID, a.Path, err.Error()) return } r, err := dcnOpenStore(dir, storeID, terms) if err != nil { dcnRefuse(ws, dir, "stat", storeID, a.Path, err.Error()) return } defer r.Close() info, err := r.Stat(rel) if err != nil { if dcnEscaped(err) { dcnRefuse(ws, dir, "stat", storeID, a.Path, "path escapes the store root") return } if os.IsNotExist(err) { dcnResult(ws, dir, "stat", storeID, a.Path, map[string]any{ "state": "gone", "path": a.Path}) return } sendMgmt(ws, 'R', []byte(err.Error()+"\n")) sendMgmtExit(ws, 1) return } dcnResult(ws, dir, "stat", storeID, a.Path, map[string]any{ "state": "ok", "path": a.Path, "size_bytes": info.Size(), "mtime": info.ModTime().UTC().Format(time.RFC3339), "is_dir": info.IsDir()}) } // handleDcnReadRange returns ONE scoped byte range of one file (b64). The range is // capped box-side regardless of what was asked — the cap protects the lane. func handleDcnReadRange(ws *websocket.Conn, dir string, args []string) { storeID, a, ok := dcnParseArgs(ws, args) if !ok { return } // A document id (from a local agent search hit) reads the SANITIZED text // through the local agent instead of file bytes from disk. Same op name, // same 'read' mask, a different — narrower — source. if strings.TrimSpace(a.Doc) != "" { handleDcnReadRangeDoc(ws, dir, storeID, a) return } terms := dcnLoadGrant(ws, dir, "read_range", storeID, a.Path) if terms == nil { return } if a.Path == "" { sendMgmt(ws, 'R', []byte("read_range: path required\n")) sendMgmtExit(ws, 1) return } abs, okp := dcnResolvePath(terms.Root, a.Path) if !okp { dcnRefuse(ws, dir, "read_range", storeID, a.Path, "path escapes the store root") return } rel, err := dcnRel(terms.Root, abs) if err != nil { dcnRefuse(ws, dir, "read_range", storeID, a.Path, err.Error()) return } r, err := dcnOpenStore(dir, storeID, terms) if err != nil { dcnRefuse(ws, dir, "read_range", storeID, a.Path, err.Error()) return } defer r.Close() // Opened through the store's handle: a name swapped for a link to a file // outside the store after the check above is refused, never followed. fh, err := r.Open(rel) if err != nil { if dcnEscaped(err) { dcnRefuse(ws, dir, "read_range", storeID, a.Path, "path escapes the store root") return } if os.IsNotExist(err) { dcnResult(ws, dir, "read_range", storeID, a.Path, map[string]any{ "state": "gone", "path": a.Path}) return } sendMgmt(ws, 'R', []byte(err.Error()+"\n")) sendMgmtExit(ws, 1) return } defer fh.Close() info, err := fh.Stat() if err != nil || info.IsDir() { dcnRefuse(ws, dir, "read_range", storeID, a.Path, "not a readable file") return } start := a.StartByte if start < 0 { start = 0 } end := a.EndByte if end <= start || end > start+dcnMaxRangeBytes { end = start + dcnMaxRangeBytes } if end > info.Size() { end = info.Size() } var buf []byte if end > start { buf = make([]byte, end-start) if _, err := fh.ReadAt(buf, start); err != nil && end < info.Size() { sendMgmt(ws, 'R', []byte(err.Error()+"\n")) sendMgmtExit(ws, 1) return } } dcnResult(ws, dir, "read_range", storeID, a.Path, map[string]any{ "state": "ok", "path": a.Path, "start_byte": start, "end_byte": end, "size_bytes": info.Size(), "content_b64": base64.StdEncoding.EncodeToString(buf)}) }