1package brokerconfig
2
3import (
4 "errors"
5 "fmt"
6 "path/filepath"
7)
8
9// ResolveLayout validates one offline root and derives its sandbox paths.
10//
11// The root must be absolute and clean, must not be the filesystem root, must
12// not contain a symlinked component, and must not equal, contain, or sit inside
13// any reserved production directory. Reserved directories are compared cleaned
14// and absolute; empty entries are ignored. The result is purely textual: no
15// directory is created here, and the caller secures the paths with
16// pkg/safedir.EnsurePrivate.
17func ResolveLayout(rawRoot string, reserved []string) (Layout, error) {
18 if rawRoot == "" {
19 return Layout{}, errors.New("brokerconfig: offline root is empty")
20 }
21 if !filepath.IsAbs(rawRoot) {
22 return Layout{}, fmt.Errorf("brokerconfig: offline root %q is not absolute", rawRoot)
23 }
24 if cleaned := filepath.Clean(rawRoot); cleaned != rawRoot {
25 return Layout{}, fmt.Errorf("brokerconfig: offline root %q is not clean", rawRoot)
26 }
27 if rawRoot == string(filepath.Separator) {
28 return Layout{}, errors.New("brokerconfig: offline root must not be the filesystem root")
29 }
30 if err := rejectSymlinkedComponents(rawRoot); err != nil {
31 return Layout{}, err
32 }
33 for _, other := range reserved {
34 if other == "" {
35 continue
36 }
37 other = filepath.Clean(other)
38 if !filepath.IsAbs(other) {
39 return Layout{}, fmt.Errorf("brokerconfig: reserved directory %q is not absolute", other)
40 }
41 if overlaps(rawRoot, other) {
42 return Layout{}, fmt.Errorf("brokerconfig: offline root %q overlaps production path %q", rawRoot, other)
43 }
44 }
45 runtime := filepath.Join(rawRoot, RuntimeDirName)
46 return Layout{
47 Root: rawRoot,
48 Runtime: runtime,
49 State: filepath.Join(rawRoot, StateDirName),
50 Log: filepath.Join(rawRoot, LogDirName),
51 Spawn: filepath.Join(runtime, SpawnDirName),
52 reserved: append([]string(nil), reserved...),
53 }, nil
54}
55
56// Load reads and strictly validates the config marker inside layout.Root.
57//
58// The file must be a regular owner-only file that is not a symlink and is
59// bounded by MaxConfigBytes; the JSON must carry the exact marker, reject
60// unknown fields, duplicate keys, trailing data, invalid UTF-8, and excessive
61// nesting. Every registration is validated independently and endpoints must be
62// unique. Two registrations may alias one identical route (the same opaque
63// address) under different endpoints, but two that share the pool's exact
64// (authenticated identity, policy) pair must agree on that one route: the pool
65// keys a physical transport by the pair and ignores the address, so a second
66// route for the same pair is refused rather than silently dialed or ignored. A
67// missing or malformed config is an error, never an empty default.
68func Load(layout Layout) (*Config, error) {
69 return LoadPath(layout, filepath.Join(layout.Root, ConfigFileName))
70}
71
72// LocalMuxRoute returns the single provisioned Unix route a remote-side mux
73// helper bridges to. A configuration that provisions no Unix route, or more
74// than one, is refused: a helper must never guess which daemon-mux endpoint it
75// serves. The local binding's own carriage is deliberately excluded: the broker
76// dials it itself, and no remote-side helper ever bridges it.
77func (c *Config) LocalMuxRoute() (Route, error) {
78 if c == nil {
79 return Route{}, errors.New("brokerconfig: no configuration")
80 }
81 switch len(c.unixRoutes) {
82 case 0:
83 return Route{}, errors.New("brokerconfig: no Unix daemon-mux route is provisioned")
84 case 1:
85 return c.unixRoutes[0], nil
86 default:
87 return Route{}, errors.New("brokerconfig: more than one Unix daemon-mux route is provisioned")
88 }
89}