package main // What the module may not mount over (the review of 2026-10-08, findings 4a–4d): the mesh's own data on // this machine, a mount point that something else uses, a folder that holds files, and a share this // machine serves itself. And what the bundle leaves for the process so that it can tell: the paths the // mesh keeps data in on this machine, from the controller's `data` verb over the bus. import ( "encoding/json" "errors" "fmt" "io" "os" "path/filepath" "strings" ) // DataFile is what the bundle left from mesh-controller.data for this machine. const DataFile = BusDir + "/data.json" // DataPaths is where the mesh keeps data on this machine, as its controller says. type DataPaths struct { Machine string `json:"machine"` Asked string `json:"asked"` Paths []string `json:"paths"` // Error is why the last ask got no answer; the paths are then the last answer's, or none. Error string `json:"error,omitempty"` Answered string `json:"answered,omitempty"` } // Want is one share this machine asks of one server, put on the bus so that the server exports the share // to this machine and to nobody who does not ask (finding 5). The server decides the access it gives; this // is only what is asked. type Want struct { Server string `json:"server"` Share string `json:"share"` Access string `json:"access"` } // Wants are what a config asks of its server, by share name. func (c Config) Wants() []Want { out := []Want{} server := c.ServerNode() if server == "" { return out } for _, w := range c.Shares { access := "rw" if w.ReadOnly { access = "ro" } out = append(out, Want{Server: server, Share: w.Name, Access: access}) } sortWants(out) return out } func sortWants(w []Want) { for i := 1; i < len(w); i++ { for j := i; j > 0 && w[j].Share < w[j-1].Share; j-- { w[j], w[j-1] = w[j-1], w[j] } } } // DataFrom reads the paths out of mesh-controller.data's answer: `answer.data`, `data`, or the same as // text in `output`. An answer with no data list is an error, never an empty list: an empty list would // let every mount point through. func DataFrom(raw json.RawMessage) ([]string, error) { type item struct { Path string `json:"path"` } var whole struct { Answer *struct { Data *[]item `json:"data"` } `json:"answer"` Data *[]item `json:"data"` Output string `json:"output"` } if err := json.Unmarshal(raw, &whole); err != nil { return nil, fmt.Errorf("the controller's data answer is not JSON: %v", err) } var items *[]item switch { case whole.Answer != nil && whole.Answer.Data != nil: items = whole.Answer.Data case whole.Data != nil: items = whole.Data case whole.Output != "": var inner struct { Data *[]item `json:"data"` } if err := json.Unmarshal([]byte(whole.Output), &inner); err == nil && inner.Data != nil { items = inner.Data } } if items == nil { return nil, errors.New("the controller's data answer names no data list") } paths := []string{} for _, it := range *items { if strings.HasPrefix(it.Path, "/") { paths = append(paths, filepath.Clean(it.Path)) } } return paths, nil } // nearData refuses a mount point at, under or above a path the mesh keeps data in here: at or under, the // mount would hide the data; above, it would hide it too, and everything beside it. func nearData(point string, paths []string) error { for _, d := range paths { switch { case point == d: return fmt.Errorf("the mount point %s is at %s, where the mesh keeps data on this machine; it is never mounted over", point, d) case strings.HasPrefix(point, d+"/"): return fmt.Errorf("the mount point %s is under %s, where the mesh keeps data on this machine; it is never mounted over", point, d) case strings.HasPrefix(d, point+"/"): return fmt.Errorf("the mount point %s is above %s, where the mesh keeps data on this machine; mounting there would hide it", point, d) } } return nil } // sourceIs says whether what is mounted at a point is a pair's own source. For a network share the source // is compared as the kernel writes it; for a disk, the device its by-uuid link resolves to. func (m *Machine) sourceIs(kind, what string, mt Mounted) bool { switch kind { case "nfs": return strings.HasPrefix(mt.Type, "nfs") && strings.TrimSuffix(mt.Source, "/") == strings.TrimSuffix(what, "/") case "smb": return (mt.Type == "cifs" || mt.Type == "smb3") && strings.EqualFold(strings.TrimSuffix(mt.Source, "/"), strings.TrimSuffix(what, "/")) case "disk": dev, err := filepath.EvalSymlinks(filepath.Join(m.Root, what)) if err != nil { return false } src, err := filepath.EvalSymlinks(filepath.Join(m.Root, mt.Source)) return err == nil && src == dev } return false } // kindOf is a unit's kind from what it mounts. func kindOf(what, fstype string) string { switch { case strings.HasPrefix(fstype, "nfs"): return "nfs" case fstype == "cifs" || strings.HasPrefix(what, "//"): return "smb" case strings.HasPrefix(what, "/dev/"): return "disk" } return "" } // notOwnSource says why a mounted filesystem at a point is not the source a unit of the module mounts // there, or nothing when it is, or nothing is mounted there but an automount. func (m *Machine) notOwnSource(kind, what, point string, mounted map[string]Mounted) error { mt, there := mounted[point] if !there || mt.IsAutomount() { return nil } if kind != "" && m.sourceIs(kind, what, mt) { return nil } return fmt.Errorf("%s is mounted at %s (%s), which is not what the module mounts there (%s)", mt.Source, point, mt.Type, what) } // mountpointFree refuses a mount point another mount uses, or a folder that holds files. A point in // mountinfo is never opened, so a network server that is away cannot hold the look. func (m *Machine) mountpointFree(p Pair, own map[string]bool, mounted map[string]Mounted) error { ours := own[p.MountUnit] && own[p.AutoUnit] if mt, there := mounted[p.Mountpoint]; there { if mt.IsAutomount() { if ours { return nil } return fmt.Errorf("%s is already an automount that is not the module's (%s)", p.Mountpoint, mt.Source) } if ours && m.sourceIs(p.Kind, p.What, mt) { return nil } return fmt.Errorf("%s is already mounted: %s (%s) is there, so this share is not mounted over it", p.Mountpoint, mt.Source, mt.Type) } dir, err := os.Open(filepath.Join(m.Root, p.Mountpoint)) if err != nil { return nil // missing: it is made } defer dir.Close() info, err := dir.Stat() if err != nil { return fmt.Errorf("%s cannot be looked at: %v", p.Mountpoint, err) } if !info.IsDir() { return fmt.Errorf("%s is a file, not a folder", p.Mountpoint) } if names, err := dir.Readdirnames(1); err == nil && len(names) > 0 { return fmt.Errorf("%s is not empty (it holds %s); a mount would hide what is in it, so it is not mounted there", p.Mountpoint, names[0]) } else if err != nil && err != io.EOF { return fmt.Errorf("%s cannot be read: %v", p.Mountpoint, err) } return nil } // unitField reads one Key= of a unit file. func unitField(text, key string) string { for _, line := range strings.Split(text, "\n") { if v, ok := strings.CutPrefix(strings.TrimSpace(line), key+"="); ok { return strings.TrimSpace(v) } } return "" }