package host import ( "bytes" "context" "io" "net" "net/netip" "os" "path/filepath" "slices" "strings" "github.com/carlosframework/platform/internal/instancectl" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/rootstate" ) func claimReservedDropin(s claimUnitShape) []byte { source := filepath.Join("/run/carlos-control/exe/.claim", provision.InstanceID(s.Host)) instance := provision.InstanceID(s.Host) if s.Unit == instancectl.UnitName(instancectl.RoleRun, s.Host) { instance = "%I" } 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") } type claimEffectiveRefusal struct { field string cause error } func (e claimEffectiveRefusal) Error() string { return e.cause.Error() } func (e claimEffectiveRefusal) Unwrap() error { return e.cause } func claimEffectiveUnit(ctx context.Context, s claimUnitShape, ctl func(context.Context, ...string) (string, error)) error { if ctl == nil { return claimEffectiveRefusal{"unknown", os.ErrPermission} } ordinary := s.Unit == instancectl.UnitName(instancectl.RoleRun, s.Host) fragment := "/etc/systemd/system/" + s.Unit var definition []byte if ordinary { fragment = "/etc/systemd/system/carlos-instance@.service" var err error definition, err = unitsFS.ReadFile("units/carlos-instance@.service") if err != nil { return err } f, err := rootstate.OpenTrustedFile(fragment) if err != nil { return err } body, err := io.ReadAll(io.LimitReader(f, rootstate.MaxRecord+1)) f.Close() if err != nil { return err } if !bytes.Equal(body, definition) { return os.ErrPermission } } else { var err error definition, err = claimBoundUnitDefinition(s) if err != nil { return err } } 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)} if s.Addr != "" { host, port, e := net.SplitHostPort(s.Addr) if e != nil { return e } family := "ipv4" if net.ParseIP(host).To4() == nil { family = "ipv6" } scalar["SocketBindAllow"] = family + ":tcp:" + port scalar["SocketBindDeny"] = "any" } keys := []string{"FragmentPath", "DropInPaths", "NeedDaemonReload", "ExecStart", "IPAddressAllow", "IPAddressDeny", "BindsTo", "PartOf"} for key := range scalar { keys = append(keys, key) } slices.Sort(keys) raw, err := ctl(ctx, "show", "-p", strings.Join(keys, ","), s.Unit, "--all") if err != nil { return err } props := map[string]string{} for _, line := range strings.Split(raw, "\n") { if key, value, ok := strings.Cut(line, "="); ok { if _, seen := props[key]; seen { return claimEffectiveRefusal{"unknown", os.ErrPermission} } props[key] = value } } for _, key := range keys { if _, ok := props[key]; !ok { return claimEffectiveRefusal{"missing:" + key, os.ErrPermission} } } if props["FragmentPath"] != fragment || props["NeedDaemonReload"] != "no" { return claimEffectiveRefusal{"unknown", os.ErrPermission} } for key, want := range scalar { if props[key] != want && !(key == "SocketBindAllow" && props[key] == strings.Replace(want, ":tcp:", ":tcp", 1)) { return claimEffectiveRefusal{"value:" + key + "=" + props[key], os.ErrPermission} } } if !ordinary && (strings.Contains(props["BindsTo"], "carlos-edge.service") || strings.Contains(props["PartOf"], "carlos-edge.service")) { return claimEffectiveRefusal{"edge-coupling", os.ErrPermission} } executable := "/usr/bin/false" if ordinary { executable = instancectl.SystemBinary } if props["DropInPaths"] != "" { path := filepath.Join("/run/systemd/system", s.Unit+".d", "60-claim-launch.conf") if props["DropInPaths"] != path { return claimEffectiveRefusal{"dropins", os.ErrPermission} } f, err := openClaimUnitDropin(s.Unit) if err != nil { return claimEffectiveRefusal{"dropin-file", err} } body, err := io.ReadAll(io.LimitReader(f, rootstate.MaxRecord+1)) f.Close() if err != nil { return err } if !bytes.Equal(body, claimReservedDropin(s)) { return claimEffectiveRefusal{"dropin-bytes", os.ErrPermission} } executable = instancectl.SystemBinary } if strings.Count(props["ExecStart"], "path=") != 1 || !strings.Contains(props["ExecStart"], "path="+executable+" ;") { return claimEffectiveRefusal{"unknown", os.ErrPermission} } for _, key := range []string{"IPAddressAllow", "IPAddressDeny"} { want := "" for _, line := range strings.Split(string(definition), "\n") { if strings.HasPrefix(line, key+"=") { want += " " + strings.TrimPrefix(line, key+"=") } } a, e := claimIPSet(want) if e != nil { return e } b, e := claimIPSet(props[key]) if e != nil { return e } if !slices.Equal(a, b) { return claimEffectiveRefusal{"network:" + key, os.ErrPermission} } } return nil } func claimIPSet(raw string) ([]string, error) { var out []string raw = strings.ReplaceAll(raw, "localhost", "127.0.0.0/8 ::1/128") raw = strings.ReplaceAll(raw, "multicast", "224.0.0.0/4 ff00::/8") for _, value := range strings.Fields(raw) { p, e := netip.ParsePrefix(value) if e != nil { return nil, os.ErrPermission } out = append(out, p.Masked().String()) } slices.Sort(out) out = slices.Compact(out) // systemd removes redundant subnets when attaching the running unit's // filter. Compare address sets, including before and after that reduction. kept := make([]string, 0, len(out)) for _, value := range out { p := netip.MustParsePrefix(value) covered := false for _, other := range out { q := netip.MustParsePrefix(other) if q.Bits() < p.Bits() && q.Contains(p.Addr()) { covered = true break } } if !covered { kept = append(kept, value) } } return kept, nil }