@synopackageland/cli 0.13.1 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +44 -1
- package/dist/build-spk/generator.d.ts +3 -2
- package/dist/build-spk/generator.js +6 -850
- package/dist/build-spk/resource-worker.d.ts +2 -14
- package/dist/build-spk/resource-worker.js +3 -166
- package/dist/cli.js +208 -56
- package/dist/compose/analyze.d.ts +3 -61
- package/dist/compose/analyze.js +35 -776
- package/dist/compose/env.d.ts +5 -3
- package/dist/compose/env.js +17 -25
- package/dist/compose/runner.js +23 -6
- package/dist/contract/app-launch.d.ts +11 -0
- package/dist/contract/app-launch.js +18 -0
- package/dist/contract/web-launch.d.ts +3 -13
- package/dist/contract/web-launch.js +14 -81
- package/dist/ctl/cli.d.ts +36 -0
- package/dist/ctl/cli.js +218 -0
- package/dist/ctl/descriptor.d.ts +81 -0
- package/dist/ctl/descriptor.js +93 -0
- package/dist/ctl/dsm-auth.d.ts +10 -0
- package/dist/ctl/dsm-auth.js +30 -0
- package/dist/ctl/errors.d.ts +17 -0
- package/dist/ctl/errors.js +32 -0
- package/dist/ctl/flows.d.ts +11 -0
- package/dist/ctl/flows.js +52 -0
- package/dist/ctl/input.d.ts +20 -0
- package/dist/ctl/input.js +128 -0
- package/dist/ctl/json.d.ts +2 -0
- package/dist/ctl/json.js +38 -0
- package/dist/ctl/redaction.d.ts +15 -0
- package/dist/ctl/redaction.js +198 -0
- package/dist/ctl/service.d.ts +38 -0
- package/dist/ctl/service.js +156 -0
- package/dist/ctl/store.d.ts +51 -0
- package/dist/ctl/store.js +93 -0
- package/dist/ctl/transfers.d.ts +14 -0
- package/dist/ctl/transfers.js +74 -0
- package/dist/ctl/transport.d.ts +11 -0
- package/dist/ctl/transport.js +79 -0
- package/dist/deploy/deploy.d.ts +4 -3
- package/dist/deploy/deploy.js +5 -210
- package/dist/deploy/plan.js +26 -7
- package/dist/dev.js +5 -4
- package/dist/error-registry.js +1020 -831
- package/dist/info.js +120 -28
- package/dist/init.js +188 -64
- package/dist/inspect.js +21 -6
- package/dist/runtime/executor-digests.json +1 -12
- package/dist/skill.js +51 -9
- package/dist/standalone/api-description-inspect.d.ts +4 -0
- package/dist/standalone/api-description-inspect.js +12 -0
- package/dist/standalone/api-description.d.ts +7 -0
- package/dist/standalone/api-description.js +73 -0
- package/dist/standalone/build.d.ts +3 -0
- package/dist/standalone/build.js +140 -33
- package/dist/standalone/compose-errors.d.ts +3 -0
- package/dist/standalone/compose-errors.js +243 -0
- package/dist/standalone/compose.d.ts +97 -10
- package/dist/standalone/compose.js +306 -75
- package/dist/standalone/daemon.js +111 -29
- package/dist/standalone/dsm-window.js +55 -9
- package/dist/standalone/identity-gateway.d.ts +9 -2
- package/dist/standalone/identity-gateway.js +282 -66
- package/dist/standalone/inspect-spk.d.ts +15 -0
- package/dist/standalone/inspect-spk.js +714 -128
- package/dist/standalone/lifecycle.js +124 -42
- package/dist/standalone/manifest.d.ts +10 -1
- package/dist/standalone/manifest.js +308 -87
- package/dist/standalone/resource.js +6 -2
- package/dist/standalone/spk.d.ts +4 -1
- package/dist/standalone/spk.js +139 -39
- package/dist/template-paths.d.ts +0 -2
- package/dist/template-paths.js +0 -5
- package/dist/test-command.d.ts +1 -1
- package/dist/test-command.js +1 -1
- package/dist/validate.d.ts +3 -3
- package/dist/validate.js +32 -140
- package/package.json +5 -3
- package/schema/synology-app.schema.json +1 -1
- package/schema/synopkgland-api-v1.schema.json +423 -0
- package/skills/SKILL.md +34 -1
- package/skills/reference/compose.md +349 -0
- package/skills/reference/create.md +26 -4
- package/skills/reference/ctl.md +41 -0
- package/skills/reference/deploy.md +1 -1
- package/skills/reference/files.md +4 -0
- package/skills/reference/identity-gateway.md +8 -138
- package/skills/reference/portal.md +31 -0
- package/skills/reference/ui-vue.md +31 -0
- package/skills/reference/wrap.md +10 -30
- package/assets/go/daemon/fixture_linux_test.go +0 -15
- package/assets/go/daemon/fixture_other_test.go +0 -7
- package/assets/go/daemon/go.mod +0 -5
- package/assets/go/daemon/main.go +0 -636
- package/assets/go/daemon/main_test.go +0 -315
- package/assets/go/daemon/network.go +0 -107
- package/assets/go/daemon/network_test.go +0 -86
- package/assets/go/daemon/process_linux.go +0 -55
- package/assets/go/daemon/process_linux_test.go +0 -161
- package/assets/go/daemon/process_other.go +0 -14
- package/assets/go/daemon/testdata/child.go +0 -44
- package/assets/go/gateway/auth.go +0 -101
- package/assets/go/gateway/auth_test.go +0 -216
- package/assets/go/gateway/cgi.go +0 -133
- package/assets/go/gateway/cgi_test.go +0 -156
- package/assets/go/gateway/csrf.go +0 -73
- package/assets/go/gateway/csrf_test.go +0 -155
- package/assets/go/gateway/directory.go +0 -108
- package/assets/go/gateway/directory_test.go +0 -202
- package/assets/go/gateway/gateway.go +0 -412
- package/assets/go/gateway/gateway_test.go +0 -676
- package/assets/go/gateway/go.mod +0 -5
- package/assets/go/gateway/groups.go +0 -66
- package/assets/go/gateway/main.go +0 -45
- package/dist/build-spk/gateway.d.ts +0 -31
- package/dist/build-spk/gateway.js +0 -60
- package/dist/compose/privilege.d.ts +0 -14
- package/dist/compose/privilege.js +0 -102
|
@@ -1,315 +0,0 @@
|
|
|
1
|
-
package main
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"context"
|
|
5
|
-
"encoding/json"
|
|
6
|
-
"os"
|
|
7
|
-
"os/exec"
|
|
8
|
-
"os/user"
|
|
9
|
-
"path/filepath"
|
|
10
|
-
"runtime"
|
|
11
|
-
"strings"
|
|
12
|
-
"syscall"
|
|
13
|
-
"testing"
|
|
14
|
-
"time"
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
func fixture(t *testing.T, mode string) (string, string, string) {
|
|
18
|
-
t.Helper()
|
|
19
|
-
root, err := os.MkdirTemp("/tmp", "c1-runtime-")
|
|
20
|
-
if err != nil {
|
|
21
|
-
t.Fatal(err)
|
|
22
|
-
}
|
|
23
|
-
t.Cleanup(func() { os.RemoveAll(root) })
|
|
24
|
-
dest, data := filepath.Join(root, "target"), filepath.Join(root, "var")
|
|
25
|
-
if err := os.MkdirAll(filepath.Join(dest, "daemon/payload"), 0755); err != nil {
|
|
26
|
-
t.Fatal(err)
|
|
27
|
-
}
|
|
28
|
-
if err := os.Mkdir(data, 0700); err != nil {
|
|
29
|
-
t.Fatal(err)
|
|
30
|
-
}
|
|
31
|
-
control, child := os.Getenv("DAEMON_CONTROL_BIN"), os.Getenv("DAEMON_CHILD_BIN")
|
|
32
|
-
if control == "" {
|
|
33
|
-
control, child = filepath.Join(root, "control"), filepath.Join(root, "child")
|
|
34
|
-
for _, spec := range [][]string{{"build", "-o", control, "."}, {"build", "-o", child, "./testdata/child.go"}} {
|
|
35
|
-
if raw, err := exec.Command("go", spec...).CombinedOutput(); err != nil {
|
|
36
|
-
t.Fatalf("%v: %s", err, raw)
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
raw, err := os.ReadFile(child)
|
|
41
|
-
if err != nil {
|
|
42
|
-
t.Fatal(err)
|
|
43
|
-
}
|
|
44
|
-
app := filepath.Join(dest, "daemon/payload/app")
|
|
45
|
-
if err := os.WriteFile(app, raw, 0755); err != nil {
|
|
46
|
-
t.Fatal(err)
|
|
47
|
-
}
|
|
48
|
-
// macOS verifies a newly written executable on its first exec, which takes
|
|
49
|
-
// seconds on a loaded host; pay it here, not inside a one-second budget.
|
|
50
|
-
if raw, err := exec.Command(app, "cleanup").CombinedOutput(); err != nil {
|
|
51
|
-
t.Fatalf("payload warm-up: %v %s", err, raw)
|
|
52
|
-
}
|
|
53
|
-
cfg := config{Package: "DaemonFixture", Exec: "app", Args: []string{"${SYNOPKG_PKGVAR}/a space", "$(touch BAD)'", "${SYNOPKG_PKGDEST}"}, Env: map[string]string{"APP_MODE": mode}, Timeouts: deadlines{Start: 2, Stop: 2, Status: 1, Uninstall: 2}}
|
|
54
|
-
if os.Geteuid() == 0 {
|
|
55
|
-
cfg.Package = "nobody"
|
|
56
|
-
} else {
|
|
57
|
-
u, err := user.Current()
|
|
58
|
-
if err != nil {
|
|
59
|
-
t.Fatal(err)
|
|
60
|
-
}
|
|
61
|
-
cfg.Package = u.Username
|
|
62
|
-
}
|
|
63
|
-
raw, _ = json.Marshal(cfg)
|
|
64
|
-
if err := os.WriteFile(filepath.Join(dest, "daemon/config.json"), raw, 0644); err != nil {
|
|
65
|
-
t.Fatal(err)
|
|
66
|
-
}
|
|
67
|
-
if os.Geteuid() == 0 {
|
|
68
|
-
// Only inside the disposable root VM: run the shipped manager as nobody.
|
|
69
|
-
if runtime.GOOS != "linux" {
|
|
70
|
-
t.Fatal("root fixture requires Linux VM")
|
|
71
|
-
}
|
|
72
|
-
filepath.Walk(root, func(path string, _ os.FileInfo, err error) error {
|
|
73
|
-
if err == nil {
|
|
74
|
-
return os.Chown(path, 65534, 65534)
|
|
75
|
-
}
|
|
76
|
-
return err
|
|
77
|
-
})
|
|
78
|
-
os.Chmod(root, 0755)
|
|
79
|
-
}
|
|
80
|
-
t.Cleanup(func() { runControl(control, "stop", dest, data) })
|
|
81
|
-
return control, dest, data
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
func runControl(control, action, dest, data string) (int, string) {
|
|
85
|
-
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
|
|
86
|
-
defer cancel()
|
|
87
|
-
cmd := exec.CommandContext(ctx, control, action, dest, data)
|
|
88
|
-
cmd.Env = append(os.Environ(), "UNLISTED_SECRET=must-not-leak", "LD_PRELOAD=must-not-leak")
|
|
89
|
-
dropFixtureUser(cmd)
|
|
90
|
-
raw, err := cmd.CombinedOutput()
|
|
91
|
-
if err == nil {
|
|
92
|
-
return 0, string(raw)
|
|
93
|
-
}
|
|
94
|
-
if e, ok := err.(*exec.ExitError); ok {
|
|
95
|
-
return e.ExitCode(), string(raw)
|
|
96
|
-
}
|
|
97
|
-
return -1, err.Error()
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Readiness only proves the child stayed alive for a second, not that it reached
|
|
101
|
-
// main: on a loaded host a stop can kill it before it writes its line. Spawn
|
|
102
|
-
// identity therefore comes from the inventory, and log content is awaited.
|
|
103
|
-
func recordedChild(t *testing.T, data string) int {
|
|
104
|
-
t.Helper()
|
|
105
|
-
raw, err := os.ReadFile(filepath.Join(data, "daemon/daemon.pid"))
|
|
106
|
-
if err != nil {
|
|
107
|
-
t.Fatal(err)
|
|
108
|
-
}
|
|
109
|
-
var r record
|
|
110
|
-
if err := json.Unmarshal(raw, &r); err != nil || r.Child < 1 {
|
|
111
|
-
t.Fatalf("daemon inventory: %v %s", err, raw)
|
|
112
|
-
}
|
|
113
|
-
return r.Child
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
func awaitChildLines(t *testing.T, data string, want int) {
|
|
117
|
-
t.Helper()
|
|
118
|
-
path := filepath.Join(data, "daemon/daemon.log")
|
|
119
|
-
for deadline := time.Now().Add(10 * time.Second); ; time.Sleep(20 * time.Millisecond) {
|
|
120
|
-
raw, _ := os.ReadFile(path)
|
|
121
|
-
if got := strings.Count(string(raw), `"uid":`); got >= want || time.Now().After(deadline) {
|
|
122
|
-
if got != want {
|
|
123
|
-
t.Fatalf("child lines=%d want=%d: %s", got, want, raw)
|
|
124
|
-
}
|
|
125
|
-
return
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
func TestDaemonLifecycle(t *testing.T) {
|
|
131
|
-
control, dest, data := fixture(t, "normal")
|
|
132
|
-
step := func(action string, want int) {
|
|
133
|
-
t.Helper()
|
|
134
|
-
if rc, out := runControl(control, action, dest, data); rc != want {
|
|
135
|
-
t.Fatalf("%s rc=%d want=%d: %s", action, rc, want, out)
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
step("status", 3)
|
|
139
|
-
step("stop", 0)
|
|
140
|
-
step("start", 0)
|
|
141
|
-
awaitChildLines(t, data, 1)
|
|
142
|
-
first := recordedChild(t, data)
|
|
143
|
-
step("status", 0)
|
|
144
|
-
step("start", 0)
|
|
145
|
-
if child := recordedChild(t, data); child != first {
|
|
146
|
-
t.Fatalf("duplicate start spawned another child: %d replaced %d", child, first)
|
|
147
|
-
}
|
|
148
|
-
step("restart", 0)
|
|
149
|
-
if child := recordedChild(t, data); child == first {
|
|
150
|
-
t.Fatalf("restart kept child %d", child)
|
|
151
|
-
}
|
|
152
|
-
awaitChildLines(t, data, 2)
|
|
153
|
-
step("stop", 0)
|
|
154
|
-
step("stop", 0)
|
|
155
|
-
step("status", 3)
|
|
156
|
-
step("unknown", 1)
|
|
157
|
-
raw, err := os.ReadFile(filepath.Join(data, "daemon/daemon.log"))
|
|
158
|
-
if err != nil {
|
|
159
|
-
t.Fatal(err)
|
|
160
|
-
}
|
|
161
|
-
if strings.Count(string(raw), `"uid":`) != 2 {
|
|
162
|
-
t.Fatalf("unexpected child spawn: %s", raw)
|
|
163
|
-
}
|
|
164
|
-
if strings.Contains(string(raw), "must-not-leak") {
|
|
165
|
-
t.Fatalf("ambient environment leaked: %s", raw)
|
|
166
|
-
}
|
|
167
|
-
if !strings.Contains(string(raw), "$(touch BAD)'") || !strings.Contains(string(raw), "/a space") {
|
|
168
|
-
t.Fatalf("argv changed: %s", raw)
|
|
169
|
-
}
|
|
170
|
-
stat, _ := os.Stat(filepath.Join(data, "daemon/daemon.log"))
|
|
171
|
-
if stat.Mode().Perm() != 0600 {
|
|
172
|
-
t.Fatal("log permissions")
|
|
173
|
-
}
|
|
174
|
-
stat, err = os.Stat(filepath.Join(data, "daemon/tmp"))
|
|
175
|
-
if err != nil || stat.Mode().Perm() != 0700 {
|
|
176
|
-
t.Fatal("private TMPDIR missing", err)
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
func TestImmediateExitAndEscalation(t *testing.T) {
|
|
181
|
-
for _, mode := range []string{"exit", "ignore"} {
|
|
182
|
-
t.Run(mode, func(t *testing.T) {
|
|
183
|
-
control, dest, data := fixture(t, mode)
|
|
184
|
-
rc, out := runControl(control, "start", dest, data)
|
|
185
|
-
if mode == "exit" {
|
|
186
|
-
if rc != 1 {
|
|
187
|
-
t.Fatalf("false readiness: %d %s", rc, out)
|
|
188
|
-
}
|
|
189
|
-
} else {
|
|
190
|
-
if rc != 0 {
|
|
191
|
-
t.Fatalf("start: %d %s", rc, out)
|
|
192
|
-
}
|
|
193
|
-
if rc, out = runControl(control, "stop", dest, data); rc != 0 {
|
|
194
|
-
t.Fatalf("escalation: %d %s", rc, out)
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
if rc, out = runControl(control, "status", dest, data); rc != 3 {
|
|
198
|
-
t.Fatalf("residue: %d %s", rc, out)
|
|
199
|
-
}
|
|
200
|
-
})
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
func TestForeignStateAndRootRefusal(t *testing.T) {
|
|
205
|
-
control, dest, data := fixture(t, "normal")
|
|
206
|
-
if os.Geteuid() == 0 {
|
|
207
|
-
raw, err := exec.Command(control, "start", dest, data).CombinedOutput()
|
|
208
|
-
if err == nil || !strings.Contains(string(raw), "package account") {
|
|
209
|
-
t.Fatalf("root accepted: %s %v", raw, err)
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
os.Mkdir(filepath.Join(data, "daemon"), 0700)
|
|
213
|
-
if os.Geteuid() == 0 {
|
|
214
|
-
os.Chown(filepath.Join(data, "daemon"), 65534, 65534)
|
|
215
|
-
}
|
|
216
|
-
os.Symlink(filepath.Join(data, "victim"), filepath.Join(data, "daemon/daemon.log"))
|
|
217
|
-
if rc, out := runControl(control, "start", dest, data); rc != 1 {
|
|
218
|
-
t.Fatalf("symlink accepted: %d %s", rc, out)
|
|
219
|
-
}
|
|
220
|
-
if _, err := os.Stat(filepath.Join(data, "victim")); !os.IsNotExist(err) {
|
|
221
|
-
t.Fatal("followed log symlink")
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
func TestUninstallCleanup(t *testing.T) {
|
|
226
|
-
for _, mode := range []string{"cleanup", "cleanup-fail", "cleanup-hang"} {
|
|
227
|
-
t.Run(mode, func(t *testing.T) {
|
|
228
|
-
control, dest, data := fixture(t, "normal")
|
|
229
|
-
path := filepath.Join(dest, "daemon/config.json")
|
|
230
|
-
raw, _ := os.ReadFile(path)
|
|
231
|
-
var cfg config
|
|
232
|
-
json.Unmarshal(raw, &cfg)
|
|
233
|
-
cfg.UninstallArgs = []string{mode, "${SYNOPKG_PKGVAR}"}
|
|
234
|
-
raw, _ = json.Marshal(cfg)
|
|
235
|
-
os.WriteFile(path, raw, 0644)
|
|
236
|
-
if rc, out := runControl(control, "start", dest, data); rc != 0 {
|
|
237
|
-
t.Fatalf("start: %d %s", rc, out)
|
|
238
|
-
}
|
|
239
|
-
want := 1
|
|
240
|
-
if mode == "cleanup" {
|
|
241
|
-
want = 0
|
|
242
|
-
}
|
|
243
|
-
if rc, out := runControl(control, "uninstall", dest, data); rc != want {
|
|
244
|
-
t.Fatalf("cleanup rc=%d want=%d: %s", rc, want, out)
|
|
245
|
-
}
|
|
246
|
-
if rc, out := runControl(control, "status", dest, data); rc != 3 {
|
|
247
|
-
t.Fatalf("cleanup must not restart daemon: %d %s", rc, out)
|
|
248
|
-
}
|
|
249
|
-
})
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// A revoke on uninstall can outlast the stop deadline on a loaded NAS; it has
|
|
254
|
-
// its own budget instead of Stop-1.
|
|
255
|
-
func TestUninstallCommandHasItsOwnBudget(t *testing.T) {
|
|
256
|
-
control, dest, data := fixture(t, "normal")
|
|
257
|
-
path := filepath.Join(dest, "daemon/config.json")
|
|
258
|
-
raw, _ := os.ReadFile(path)
|
|
259
|
-
var cfg config
|
|
260
|
-
json.Unmarshal(raw, &cfg)
|
|
261
|
-
cfg.UninstallArgs = []string{"cleanup-slow"}
|
|
262
|
-
cfg.Timeouts.Uninstall = 5
|
|
263
|
-
raw, _ = json.Marshal(cfg)
|
|
264
|
-
os.WriteFile(path, raw, 0644)
|
|
265
|
-
if rc, out := runControl(control, "start", dest, data); rc != 0 {
|
|
266
|
-
t.Fatalf("start: %d %s", rc, out)
|
|
267
|
-
}
|
|
268
|
-
if rc, out := runControl(control, "uninstall", dest, data); rc != 0 {
|
|
269
|
-
t.Fatalf("slow uninstall within its budget failed: %d %s", rc, out)
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
func TestPackageAccountIdentity(t *testing.T) {
|
|
274
|
-
control, dest, data := fixture(t, "normal")
|
|
275
|
-
path := filepath.Join(dest, "daemon/config.json")
|
|
276
|
-
raw, _ := os.ReadFile(path)
|
|
277
|
-
var cfg config
|
|
278
|
-
json.Unmarshal(raw, &cfg)
|
|
279
|
-
cfg.Package = "root"
|
|
280
|
-
raw, _ = json.Marshal(cfg)
|
|
281
|
-
os.WriteFile(path, raw, 0644)
|
|
282
|
-
rc, out := runControl(control, "start", dest, data)
|
|
283
|
-
if rc == 0 {
|
|
284
|
-
runControl(control, "stop", dest, data)
|
|
285
|
-
}
|
|
286
|
-
if rc != 1 {
|
|
287
|
-
t.Fatalf("other account accepted: %d %s", rc, out)
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
func TestFIFOInventoryCannotBlockStatus(t *testing.T) {
|
|
292
|
-
control, dest, data := fixture(t, "normal")
|
|
293
|
-
state := filepath.Join(data, "daemon")
|
|
294
|
-
os.Mkdir(state, 0700)
|
|
295
|
-
path := filepath.Join(state, "daemon.pid")
|
|
296
|
-
t.Cleanup(func() { os.Remove(path) })
|
|
297
|
-
if err := syscall.Mkfifo(path, 0600); err != nil {
|
|
298
|
-
t.Fatal(err)
|
|
299
|
-
}
|
|
300
|
-
if os.Geteuid() == 0 {
|
|
301
|
-
os.Chown(state, 65534, 65534)
|
|
302
|
-
os.Chown(path, 65534, 65534)
|
|
303
|
-
}
|
|
304
|
-
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
305
|
-
defer cancel()
|
|
306
|
-
cmd := exec.CommandContext(ctx, control, "status", dest, data)
|
|
307
|
-
dropFixtureUser(cmd)
|
|
308
|
-
raw, err := cmd.CombinedOutput()
|
|
309
|
-
if ctx.Err() != nil {
|
|
310
|
-
t.Fatal("status blocked on FIFO")
|
|
311
|
-
}
|
|
312
|
-
if e, ok := err.(*exec.ExitError); !ok || e.ExitCode() != 1 {
|
|
313
|
-
t.Fatalf("FIFO status: %s %v", raw, err)
|
|
314
|
-
}
|
|
315
|
-
}
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
package main
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"bufio"
|
|
5
|
-
"errors"
|
|
6
|
-
"fmt"
|
|
7
|
-
"os"
|
|
8
|
-
"path/filepath"
|
|
9
|
-
"strconv"
|
|
10
|
-
"strings"
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
// Host-network containers use the package UID. The kernel tables, rather than
|
|
14
|
-
// an application's claimed environment setting, prove its listener bindings.
|
|
15
|
-
// This is a configuration guard, not a network namespace or a firewall.
|
|
16
|
-
type networkPolicy struct {
|
|
17
|
-
Ports []int `json:"ports"`
|
|
18
|
-
PublicPorts []int `json:"public_ports"`
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
func containsPort(ports []int, port int) bool {
|
|
22
|
-
for _, p := range ports {
|
|
23
|
-
if p == port {
|
|
24
|
-
return true
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
return false
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
func checkSocketTable(raw string, uid int, p *networkPolicy, udp bool) error {
|
|
31
|
-
_, err := socketPorts(raw, uid, p, udp)
|
|
32
|
-
return err
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
func socketPorts(raw string, uid int, p *networkPolicy, udp bool) (map[int]bool, error) {
|
|
36
|
-
found := map[int]bool{}
|
|
37
|
-
scan := bufio.NewScanner(strings.NewReader(raw))
|
|
38
|
-
if !scan.Scan() {
|
|
39
|
-
return nil, errors.New("missing socket table header")
|
|
40
|
-
}
|
|
41
|
-
for scan.Scan() {
|
|
42
|
-
f := strings.Fields(scan.Text())
|
|
43
|
-
if len(f) == 0 {
|
|
44
|
-
continue
|
|
45
|
-
}
|
|
46
|
-
if len(f) < 10 {
|
|
47
|
-
return nil, errors.New("invalid socket table")
|
|
48
|
-
}
|
|
49
|
-
owner, err := strconv.Atoi(f[7])
|
|
50
|
-
if err != nil {
|
|
51
|
-
return nil, err
|
|
52
|
-
}
|
|
53
|
-
if owner != uid {
|
|
54
|
-
continue
|
|
55
|
-
}
|
|
56
|
-
if (!udp && f[3] != "0A") || (udp && f[3] != "07") {
|
|
57
|
-
continue
|
|
58
|
-
}
|
|
59
|
-
endpoint := strings.Split(f[1], ":")
|
|
60
|
-
if len(endpoint) != 2 {
|
|
61
|
-
return nil, errors.New("invalid socket endpoint")
|
|
62
|
-
}
|
|
63
|
-
port64, err := strconv.ParseInt(endpoint[1], 16, 32)
|
|
64
|
-
if err != nil {
|
|
65
|
-
return nil, err
|
|
66
|
-
}
|
|
67
|
-
port := int(port64)
|
|
68
|
-
if !udp && containsPort(p.PublicPorts, port) {
|
|
69
|
-
continue
|
|
70
|
-
}
|
|
71
|
-
// /proc/net's IPv4 and IPv6 words are little-endian on supported DSM arches.
|
|
72
|
-
loopback := endpoint[0] == "0100007F" || endpoint[0] == "00000000000000000000000001000000"
|
|
73
|
-
if !loopback {
|
|
74
|
-
return nil, fmt.Errorf("compose package listener on port %d is not bound to loopback", port)
|
|
75
|
-
}
|
|
76
|
-
if !udp && containsPort(p.Ports, port) && endpoint[0] == "0100007F" {
|
|
77
|
-
found[port] = true
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
return found, scan.Err()
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
func checkNetwork(dir string, uid int, p *networkPolicy) error {
|
|
84
|
-
if p == nil {
|
|
85
|
-
return nil
|
|
86
|
-
}
|
|
87
|
-
found := map[int]bool{}
|
|
88
|
-
for _, name := range []string{"tcp", "tcp6", "udp", "udp6"} {
|
|
89
|
-
raw, err := os.ReadFile(filepath.Join(dir, name))
|
|
90
|
-
if err != nil {
|
|
91
|
-
return err
|
|
92
|
-
}
|
|
93
|
-
ports, err := socketPorts(string(raw), uid, p, strings.HasPrefix(name, "udp"))
|
|
94
|
-
if err != nil {
|
|
95
|
-
return err
|
|
96
|
-
}
|
|
97
|
-
for port := range ports {
|
|
98
|
-
found[port] = true
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
for _, port := range p.Ports {
|
|
102
|
-
if !found[port] {
|
|
103
|
-
return fmt.Errorf("compose upstream port %d is not ready as the package UID on 127.0.0.1", port)
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return nil
|
|
107
|
-
}
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
package main
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"fmt"
|
|
5
|
-
"os"
|
|
6
|
-
"path/filepath"
|
|
7
|
-
"testing"
|
|
8
|
-
)
|
|
9
|
-
|
|
10
|
-
func TestComposeNetworkPolicy(t *testing.T) {
|
|
11
|
-
uid := os.Geteuid()
|
|
12
|
-
for _, tc := range []struct {
|
|
13
|
-
name, addr, state, port string
|
|
14
|
-
ok bool
|
|
15
|
-
}{
|
|
16
|
-
{"loopback", "0100007F", "0A", "6D64", true},
|
|
17
|
-
{"wildcard", "00000000", "0A", "6D64", false},
|
|
18
|
-
{"other loopback", "0200007F", "0A", "6D64", false},
|
|
19
|
-
{"public gateway", "00000000", "0A", "6D60", true},
|
|
20
|
-
{"unknown port", "00000000", "0A", "6D66", false},
|
|
21
|
-
{"connected", "00000000", "01", "6D66", true},
|
|
22
|
-
} {
|
|
23
|
-
t.Run(tc.name, func(t *testing.T) {
|
|
24
|
-
line := socketFixture(tc.addr, tc.port, tc.state, uid)
|
|
25
|
-
err := checkSocketTable(line, uid, &networkPolicy{Ports: []int{28004}, PublicPorts: []int{28000}}, false)
|
|
26
|
-
if (err == nil) != tc.ok {
|
|
27
|
-
t.Fatalf("got %v, expected ok %v", err, tc.ok)
|
|
28
|
-
}
|
|
29
|
-
})
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
func TestComposeNetworkRequiresOwnedUpstream(t *testing.T) {
|
|
34
|
-
dir := t.TempDir()
|
|
35
|
-
for _, name := range []string{"tcp", "tcp6", "udp", "udp6"} {
|
|
36
|
-
os.WriteFile(filepath.Join(dir, name), []byte("header\n"), 0600)
|
|
37
|
-
}
|
|
38
|
-
p := &networkPolicy{Ports: []int{28004}, PublicPorts: []int{28000}}
|
|
39
|
-
if err := checkNetwork(dir, os.Geteuid(), p); err == nil {
|
|
40
|
-
t.Fatal("missing upstream accepted")
|
|
41
|
-
}
|
|
42
|
-
os.WriteFile(filepath.Join(dir, "tcp"), []byte(socketFixture("0100007F", "6D64", "0A", os.Geteuid()+1)), 0600)
|
|
43
|
-
if err := checkNetwork(dir, os.Geteuid(), p); err == nil {
|
|
44
|
-
t.Fatal("foreign upstream accepted")
|
|
45
|
-
}
|
|
46
|
-
os.WriteFile(filepath.Join(dir, "tcp"), []byte(socketFixture("0100007F", "6D64", "0A", os.Geteuid())), 0600)
|
|
47
|
-
if err := checkNetwork(dir, os.Geteuid(), p); err != nil {
|
|
48
|
-
t.Fatal(err)
|
|
49
|
-
}
|
|
50
|
-
os.WriteFile(filepath.Join(dir, "udp"), []byte(socketFixture("00000000", "6D66", "07", os.Geteuid())), 0600)
|
|
51
|
-
if err := checkNetwork(dir, os.Geteuid(), p); err == nil {
|
|
52
|
-
t.Fatal("UDP wildcard accepted")
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
func socketFixture(addr, port, state string, uid int) string {
|
|
57
|
-
return fmt.Sprintf("header\n 0: %s:%s 00000000:0000 %s 0:0 0:0 0 %d 0 1\n", addr, port, state, uid)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
func TestComposeUDPConnectedIsNotAListener(t *testing.T) {
|
|
61
|
-
raw := socketFixture("EA53110A", "BEEF", "01", os.Geteuid())
|
|
62
|
-
if err := checkSocketTable(raw, os.Geteuid(), &networkPolicy{Ports: []int{28004}}, true); err != nil {
|
|
63
|
-
t.Fatal(err)
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
func TestComposeIPv6AndUDPPublicPort(t *testing.T) {
|
|
68
|
-
p := &networkPolicy{Ports: []int{28004}, PublicPorts: []int{28000}}
|
|
69
|
-
for _, tc := range []struct {
|
|
70
|
-
addr, port string
|
|
71
|
-
udp, ok bool
|
|
72
|
-
}{
|
|
73
|
-
{"00000000000000000000000001000000", "6D64", false, true},
|
|
74
|
-
{"00000000000000000000000000000000", "6D64", false, false},
|
|
75
|
-
{"00000000", "6D60", true, false},
|
|
76
|
-
} {
|
|
77
|
-
state := "0A"
|
|
78
|
-
if tc.udp {
|
|
79
|
-
state = "07"
|
|
80
|
-
}
|
|
81
|
-
err := checkSocketTable(socketFixture(tc.addr, tc.port, state, os.Geteuid()), os.Geteuid(), p, tc.udp)
|
|
82
|
-
if (err == nil) != tc.ok {
|
|
83
|
-
t.Fatalf("%+v: %v", tc, err)
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
//go:build linux
|
|
2
|
-
|
|
3
|
-
package main
|
|
4
|
-
|
|
5
|
-
import (
|
|
6
|
-
"errors"
|
|
7
|
-
"net"
|
|
8
|
-
"os"
|
|
9
|
-
"strconv"
|
|
10
|
-
"syscall"
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
func childAttributes() *syscall.SysProcAttr { return &syscall.SysProcAttr{Pdeathsig: syscall.SIGKILL} }
|
|
14
|
-
func processBirth(pid int) string {
|
|
15
|
-
raw, err := os.ReadFile("/proc/" + strconv.Itoa(pid) + "/stat")
|
|
16
|
-
if err != nil {
|
|
17
|
-
return ""
|
|
18
|
-
}
|
|
19
|
-
birth, _ := parseBirth(string(raw))
|
|
20
|
-
return birth
|
|
21
|
-
}
|
|
22
|
-
func recordedChildAlive(pid int, birth string) bool {
|
|
23
|
-
raw, err := os.ReadFile("/proc/" + strconv.Itoa(pid) + "/stat")
|
|
24
|
-
if os.IsNotExist(err) {
|
|
25
|
-
return false
|
|
26
|
-
}
|
|
27
|
-
if err != nil {
|
|
28
|
-
return true
|
|
29
|
-
}
|
|
30
|
-
actual, alive := parseBirth(string(raw))
|
|
31
|
-
if actual == "" {
|
|
32
|
-
return true
|
|
33
|
-
}
|
|
34
|
-
return alive && (birth == "" || actual == birth)
|
|
35
|
-
}
|
|
36
|
-
func checkPeer(conn *net.UnixConn, pid int) error {
|
|
37
|
-
fd, err := conn.SyscallConn()
|
|
38
|
-
if err != nil {
|
|
39
|
-
return err
|
|
40
|
-
}
|
|
41
|
-
var peer *syscall.Ucred
|
|
42
|
-
var getErr error
|
|
43
|
-
if err := fd.Control(func(raw uintptr) {
|
|
44
|
-
peer, getErr = syscall.GetsockoptUcred(int(raw), syscall.SOL_SOCKET, syscall.SO_PEERCRED)
|
|
45
|
-
}); err != nil {
|
|
46
|
-
return err
|
|
47
|
-
}
|
|
48
|
-
if getErr != nil {
|
|
49
|
-
return getErr
|
|
50
|
-
}
|
|
51
|
-
if peer.Uid != uint32(os.Geteuid()) || peer.Pid != int32(pid) {
|
|
52
|
-
return errors.New("foreign daemon supervisor")
|
|
53
|
-
}
|
|
54
|
-
return nil
|
|
55
|
-
}
|