carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package instancectl
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "io"
8 "net"
9 "os"
10 "path/filepath"
11 "strconv"
12 "time"
13
14 "github.com/carlosframework/platform/internal/provision"
15 "github.com/carlosframework/platform/internal/safefile"
16)
17
18// ClaimExecPermit is root's one-launch reservation. SafeUntil is CLOCK_BOOTTIME
19// nanoseconds on BootID; the trampoline compares it immediately before ForkExec.
20type ClaimExecPermit struct {
21 Reservation, Inputs, BootID string
22 SafeUntil int64
23 Containment uint8 `json:",omitempty"`
24}
25
26func (p ClaimExecPermit) Valid() bool {
27 return p.Reservation != "" && p.Inputs != "" && p.BootID != "" && p.SafeUntil > 0
28}
29
30const ClaimExecSocket = "/run/carlos/claim-exec.sock"
31const ClaimExecSnapshot = "/run/carlos/claim-exec.json"
32const ClaimExecBootID = "/run/carlos/claim-boot-id"
33
34func (u ClaimUnits) StartReserved(ctx context.Context, in ClaimUnit, l Launch, p ClaimExecPermit, check func() error) (ClaimUnitStarted, error) {
35 if !p.Valid() || p.Containment != 1 || check == nil {
36 return ClaimUnitStarted{}, ErrClaimUnit
37 }
38 return u.start(ctx, in, &l, check, &p)
39}
40
41type claimExecGate struct {
42 listener *net.UnixListener
43 path string
44 permit ClaimExecPermit
45}
46
47// Keep the closed socket inode for ExecStopPost namespace setup. clearLaunch
48// removes it only after the unit has stopped and its cgroup is empty.
49func (g *claimExecGate) close() { g.listener.Close() }
50func (u ClaimUnits) prepareExecGate(ctx context.Context, host string, p ClaimExecPermit) (*claimExecGate, error) {
51 source, dropin, err := u.launchPaths(host)
52 if err != nil {
53 return nil, err
54 }
55 raw, err := json.Marshal(p)
56 if err != nil {
57 return nil, err
58 }
59 if err = safefile.WriteFile(filepath.Join(source, "permit.json"), raw, 0444); err != nil {
60 return nil, err
61 }
62 socket := filepath.Join(source, "exec.sock")
63 // An old socket is removed only after start proved the old unit stopped.
64 if err = safefile.Remove(socket); err != nil && !errors.Is(err, os.ErrNotExist) {
65 return nil, err
66 }
67 ln, err := claimListenExecSocket(source)
68 if err != nil {
69 return nil, err
70 }
71 ln.SetUnlinkOnClose(false)
72 gate := &claimExecGate{ln, socket, p}
73 fail := func(e error) (*claimExecGate, error) { gate.close(); return nil, e }
74 // Host ancestry remains root-private; inside the namespace peer identity is
75 // checked against the pinned unit MainPID, never against a supplied PID.
76 body, err := safefile.ReadFile(filepath.Join(dropin, "60-claim-launch.conf"))
77 if err != nil {
78 return fail(err)
79 }
80 instance := "%I"
81 if u.BoundUnit != "" {
82 instance = provision.InstanceID(host)
83 }
84 body = append(body, []byte("BindReadOnlyPaths="+socket+":"+ClaimExecSocket+"\nBindReadOnlyPaths="+filepath.Join(source, "permit.json")+":"+ClaimExecSnapshot+"\nBindReadOnlyPaths=/proc/sys/kernel/random/boot_id:"+ClaimExecBootID+"\nExecStart=\nExecStart="+SystemBinary+" instance-exec -claim-reservation="+ClaimExecSnapshot+" run "+instance+"\n")...)
85 if err = safefile.WriteFile(filepath.Join(dropin, "60-claim-launch.conf"), body, 0644); err != nil {
86 return fail(err)
87 }
88 if _, err = u.Systemctl(ctx, "daemon-reload"); err != nil {
89 return fail(err)
90 }
91 return gate, nil
92}
93
94func (g *claimExecGate) authorize(ctx context.Context, u ClaimUnits, s *ClaimUnitStarted, check func() error) error {
95 deadline := time.Now().Add(u.budget())
96 if d, ok := ctx.Deadline(); ok && d.Before(deadline) {
97 deadline = d
98 }
99 if err := g.listener.SetDeadline(deadline); err != nil {
100 return err
101 }
102 stop := context.AfterFunc(ctx, func() { g.listener.Close() })
103 defer stop()
104 conn, err := g.listener.AcceptUnix()
105 if err != nil {
106 return err
107 }
108 defer conn.Close()
109 closeOnCancel := context.AfterFunc(ctx, func() { conn.Close() })
110 defer closeOnCancel()
111 if err = conn.SetDeadline(deadline); err != nil {
112 return err
113 }
114 if err = u.pinExecPeer(ctx, conn, s); err != nil {
115 return err
116 }
117 if err = u.Same(ctx, *s); err != nil {
118 return err
119 }
120 var asked ClaimExecPermit
121 dec := json.NewDecoder(io.LimitReader(conn, 4096))
122 if err = dec.Decode(&asked); err != nil {
123 return err
124 }
125 if asked != g.permit {
126 return ErrClaimUnit
127 }
128 if err = check(); err != nil {
129 return err
130 }
131 if err = u.Same(ctx, *s); err != nil {
132 return err
133 }
134 if u.BoundAddr != "" {
135 if err = u.restrictBoundNetwork(*s); err != nil {
136 return err
137 }
138 if err = u.Same(ctx, *s); err != nil {
139 return err
140 }
141 }
142 // One connection, one permit. Closing/restarting root cannot replay it.
143 return json.NewEncoder(conn).Encode(g.permit)
144}
145
146// PrivatePIDs may replace systemd's setup PID with the namespace init PID.
147// Only this pre-exec handshake may pin that transition. The invocation and
148// cgroup must stay fixed, and systemd must identify the socket peer as MainPID.
149func (u ClaimUnits) pinExecPeer(ctx context.Context, conn net.Conn, s *ClaimUnitStarted) error {
150 for {
151 if err := ctx.Err(); err != nil {
152 return err
153 }
154 if err := claimExecPeer(conn, s.MainPID); err == nil {
155 return nil
156 }
157 state, err := u.show(ctx, s.Unit, "InvocationID", "MainPID", "ControlGroup", "ActiveState")
158 if err != nil {
159 return err
160 }
161 if state["InvocationID"] != s.InvocationID || state["ControlGroup"] != "/"+s.ControlGroup || state["ActiveState"] != "active" {
162 return ErrClaimUnit
163 }
164 pid, err := strconv.Atoi(state["MainPID"])
165 if err != nil || pid <= 0 {
166 return ErrClaimUnit
167 }
168 if err := claimExecPeer(conn, pid); err == nil {
169 s.MainPID = pid
170 return nil
171 }
172 select {
173 case <-ctx.Done():
174 return ctx.Err()
175 case <-time.After(u.poll()):
176 }
177 }
178}
179