carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "bytes"
5 "context"
6 "io"
7 "net"
8 "net/netip"
9 "os"
10 "path/filepath"
11 "slices"
12 "strings"
13
14 "github.com/carlosframework/platform/internal/instancectl"
15 "github.com/carlosframework/platform/internal/provision"
16 "github.com/carlosframework/platform/internal/rootstate"
17)
18
19func claimReservedDropin(s claimUnitShape) []byte {
20 source := filepath.Join("/run/carlos-control/exe/.claim", provision.InstanceID(s.Host))
21 instance := provision.InstanceID(s.Host)
22 if s.Unit == instancectl.UnitName(instancectl.RoleRun, s.Host) {
23 instance = "%I"
24 }
25 return []byte("[Service]\nBindReadOnlyPaths=" + source + "/launch.json:" + filepath.Join(provision.InstanceDir("/data", s.Host), instancectl.LaunchFileName(instancectl.RoleRun)) + "\nBindReadOnlyPaths=" + source + "/exec.sock:" + instancectl.ClaimExecSocket + "\nBindReadOnlyPaths=" + source + "/permit.json:" + instancectl.ClaimExecSnapshot + "\nBindReadOnlyPaths=/proc/sys/kernel/random/boot_id:" + instancectl.ClaimExecBootID + "\nExecStart=\nExecStart=" + instancectl.SystemBinary + " instance-exec -claim-reservation=" + instancectl.ClaimExecSnapshot + " run " + instance + "\n")
26}
27
28type claimEffectiveRefusal struct {
29 field string
30 cause error
31}
32
33func (e claimEffectiveRefusal) Error() string { return e.cause.Error() }
34func (e claimEffectiveRefusal) Unwrap() error { return e.cause }
35
36func claimEffectiveUnit(ctx context.Context, s claimUnitShape, ctl func(context.Context, ...string) (string, error)) error {
37 if ctl == nil {
38 return claimEffectiveRefusal{"unknown", os.ErrPermission}
39 }
40 ordinary := s.Unit == instancectl.UnitName(instancectl.RoleRun, s.Host)
41 fragment := "/etc/systemd/system/" + s.Unit
42 var definition []byte
43 if ordinary {
44 fragment = "/etc/systemd/system/carlos-instance@.service"
45 var err error
46 definition, err = unitsFS.ReadFile("units/carlos-instance@.service")
47 if err != nil {
48 return err
49 }
50 f, err := rootstate.OpenTrustedFile(fragment)
51 if err != nil {
52 return err
53 }
54 body, err := io.ReadAll(io.LimitReader(f, rootstate.MaxRecord+1))
55 f.Close()
56 if err != nil {
57 return err
58 }
59 if !bytes.Equal(body, definition) {
60 return os.ErrPermission
61 }
62 } else {
63 var err error
64 definition, err = claimBoundUnitDefinition(s)
65 if err != nil {
66 return err
67 }
68 }
69 scalar := map[string]string{"User": "carlos", "Group": "carlos", "NoNewPrivileges": "yes", "ProtectSystem": "strict", "ProtectHome": "yes", "PrivateTmp": "yes", "PrivateDevices": "yes", "PrivateIPC": "yes", "ProtectControlGroups": "yes", "ProtectProc": "invisible", "ProcSubset": "pid", "PrivatePIDs": "yes", "RestrictNamespaces": "yes", "CapabilityBoundingSet": "", "MemoryDenyWriteExecute": "yes", "ProtectKernelTunables": "yes", "ProtectKernelModules": "yes", "ProtectKernelLogs": "yes", "ProtectClock": "yes", "ProtectHostname": "yes", "RemoveIPC": "yes", "LockPersonality": "yes", "RestrictRealtime": "yes", "Restart": "no", "Slice": instancectl.SliceName(s.Host)}
70 if s.Addr != "" {
71 host, port, e := net.SplitHostPort(s.Addr)
72 if e != nil {
73 return e
74 }
75 family := "ipv4"
76 if net.ParseIP(host).To4() == nil {
77 family = "ipv6"
78 }
79 scalar["SocketBindAllow"] = family + ":tcp:" + port
80 scalar["SocketBindDeny"] = "any"
81 }
82 keys := []string{"FragmentPath", "DropInPaths", "NeedDaemonReload", "ExecStart", "IPAddressAllow", "IPAddressDeny", "BindsTo", "PartOf"}
83 for key := range scalar {
84 keys = append(keys, key)
85 }
86 slices.Sort(keys)
87 raw, err := ctl(ctx, "show", "-p", strings.Join(keys, ","), s.Unit, "--all")
88 if err != nil {
89 return err
90 }
91 props := map[string]string{}
92 for _, line := range strings.Split(raw, "\n") {
93 if key, value, ok := strings.Cut(line, "="); ok {
94 if _, seen := props[key]; seen {
95 return claimEffectiveRefusal{"unknown", os.ErrPermission}
96 }
97 props[key] = value
98 }
99 }
100 for _, key := range keys {
101 if _, ok := props[key]; !ok {
102 return claimEffectiveRefusal{"missing:" + key, os.ErrPermission}
103 }
104 }
105 if props["FragmentPath"] != fragment || props["NeedDaemonReload"] != "no" {
106 return claimEffectiveRefusal{"unknown", os.ErrPermission}
107 }
108 for key, want := range scalar {
109 if props[key] != want && !(key == "SocketBindAllow" && props[key] == strings.Replace(want, ":tcp:", ":tcp", 1)) {
110 return claimEffectiveRefusal{"value:" + key + "=" + props[key], os.ErrPermission}
111 }
112 }
113 if !ordinary && (strings.Contains(props["BindsTo"], "carlos-edge.service") || strings.Contains(props["PartOf"], "carlos-edge.service")) {
114 return claimEffectiveRefusal{"edge-coupling", os.ErrPermission}
115 }
116 executable := "/usr/bin/false"
117 if ordinary {
118 executable = instancectl.SystemBinary
119 }
120 if props["DropInPaths"] != "" {
121 path := filepath.Join("/run/systemd/system", s.Unit+".d", "60-claim-launch.conf")
122 if props["DropInPaths"] != path {
123 return claimEffectiveRefusal{"dropins", os.ErrPermission}
124 }
125 f, err := openClaimUnitDropin(s.Unit)
126 if err != nil {
127 return claimEffectiveRefusal{"dropin-file", err}
128 }
129 body, err := io.ReadAll(io.LimitReader(f, rootstate.MaxRecord+1))
130 f.Close()
131 if err != nil {
132 return err
133 }
134 if !bytes.Equal(body, claimReservedDropin(s)) {
135 return claimEffectiveRefusal{"dropin-bytes", os.ErrPermission}
136 }
137 executable = instancectl.SystemBinary
138 }
139 if strings.Count(props["ExecStart"], "path=") != 1 || !strings.Contains(props["ExecStart"], "path="+executable+" ;") {
140 return claimEffectiveRefusal{"unknown", os.ErrPermission}
141 }
142 for _, key := range []string{"IPAddressAllow", "IPAddressDeny"} {
143 want := ""
144 for _, line := range strings.Split(string(definition), "\n") {
145 if strings.HasPrefix(line, key+"=") {
146 want += " " + strings.TrimPrefix(line, key+"=")
147 }
148 }
149 a, e := claimIPSet(want)
150 if e != nil {
151 return e
152 }
153 b, e := claimIPSet(props[key])
154 if e != nil {
155 return e
156 }
157 if !slices.Equal(a, b) {
158 return claimEffectiveRefusal{"network:" + key, os.ErrPermission}
159 }
160 }
161 return nil
162}
163
164func claimIPSet(raw string) ([]string, error) {
165 var out []string
166 raw = strings.ReplaceAll(raw, "localhost", "127.0.0.0/8 ::1/128")
167 raw = strings.ReplaceAll(raw, "multicast", "224.0.0.0/4 ff00::/8")
168 for _, value := range strings.Fields(raw) {
169 p, e := netip.ParsePrefix(value)
170 if e != nil {
171 return nil, os.ErrPermission
172 }
173 out = append(out, p.Masked().String())
174 }
175 slices.Sort(out)
176 out = slices.Compact(out)
177 // systemd removes redundant subnets when attaching the running unit's
178 // filter. Compare address sets, including before and after that reduction.
179 kept := make([]string, 0, len(out))
180 for _, value := range out {
181 p := netip.MustParsePrefix(value)
182 covered := false
183 for _, other := range out {
184 q := netip.MustParsePrefix(other)
185 if q.Bits() < p.Bits() && q.Contains(p.Addr()) {
186 covered = true
187 break
188 }
189 }
190 if !covered {
191 kept = append(kept, value)
192 }
193 }
194 return kept, nil
195}
196