podman
245 строк · 6.0 Кб
1//go:build darwin
2
3package main
4
5import (
6"bytes"
7"errors"
8"fmt"
9"io"
10"io/fs"
11"os"
12"path/filepath"
13"strings"
14"syscall"
15"text/template"
16
17"github.com/containers/storage/pkg/fileutils"
18"github.com/spf13/cobra"
19)
20
21const (
22mode755 = 0755
23mode644 = 0644
24)
25
26const launchConfig = `<?xml version="1.0" encoding="UTF-8"?>
27<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
28<plist version="1.0">
29<dict>
30<key>Label</key>
31<string>com.github.containers.podman.helper-{{.User}}</string>
32<key>ProgramArguments</key>
33<array>
34<string>{{.Program}}</string>
35<string>service</string>
36<string>{{.Target}}</string>
37</array>
38<key>inetdCompatibility</key>
39<dict>
40<key>Wait</key>
41<false/>
42</dict>
43<key>UserName</key>
44<string>root</string>
45<key>Sockets</key>
46<dict>
47<key>Listeners</key>
48<dict>
49<key>SockFamily</key>
50<string>Unix</string>
51<key>SockPathName</key>
52<string>/private/var/run/podman-helper-{{.User}}.socket</string>
53<key>SockPathOwner</key>
54<integer>{{.UID}}</integer>
55<key>SockPathMode</key>
56<!-- SockPathMode takes base 10 (384 = 0600) -->
57<integer>384</integer>
58<key>SockType</key>
59<string>stream</string>
60</dict>
61</dict>
62</dict>
63</plist>
64`
65
66type launchParams struct {
67Program string
68User string
69UID string
70Target string
71}
72
73var installCmd = &cobra.Command{
74Use: "install",
75Short: "Install the podman helper agent",
76Long: "Install the podman helper agent, which manages the /var/run/docker.sock link",
77PreRun: silentUsage,
78RunE: install,
79}
80
81func init() {
82addPrefixFlag(installCmd)
83rootCmd.AddCommand(installCmd)
84}
85
86func install(cmd *cobra.Command, args []string) error {
87userName, uid, homeDir, err := getUser()
88if err != nil {
89return err
90}
91
92labelName := fmt.Sprintf("com.github.containers.podman.helper-%s.plist", userName)
93fileName := filepath.Join("/Library", "LaunchDaemons", labelName)
94
95if err := fileutils.Exists(fileName); err == nil || !errors.Is(err, fs.ErrNotExist) {
96fmt.Fprintln(os.Stderr, "helper is already installed, skipping the install, uninstall first if you want to reinstall")
97return nil
98}
99
100prog, err := installExecutable(userName)
101if err != nil {
102return err
103}
104
105target := filepath.Join(homeDir, ".local", "share", "containers", "podman", "machine", "podman.sock")
106var buf bytes.Buffer
107t := template.Must(template.New("launchdConfig").Parse(launchConfig))
108err = t.Execute(&buf, launchParams{prog, userName, uid, target})
109if err != nil {
110return err
111}
112
113file, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE|os.O_EXCL, mode644)
114if err != nil {
115return fmt.Errorf("creating helper plist file: %w", err)
116}
117defer file.Close()
118_, err = buf.WriteTo(file)
119if err != nil {
120return err
121}
122
123if err = runDetectErr("launchctl", "load", fileName); err != nil {
124return fmt.Errorf("launchctl failed loading service: %w", err)
125}
126
127return nil
128}
129
130func restrictRecursive(targetDir string, until string) error {
131for targetDir != until && len(targetDir) > 1 {
132info, err := os.Lstat(targetDir)
133if err != nil {
134return err
135}
136if info.Mode()&fs.ModeSymlink != 0 {
137return fmt.Errorf("symlinks not allowed in helper paths (remove them and rerun): %s", targetDir)
138}
139if err = os.Chown(targetDir, 0, 0); err != nil {
140return fmt.Errorf("could not update ownership of helper path: %w", err)
141}
142if err = os.Chmod(targetDir, mode755|fs.ModeSticky); err != nil {
143return fmt.Errorf("could not update permissions of helper path: %w", err)
144}
145targetDir = filepath.Dir(targetDir)
146}
147
148return nil
149}
150
151func verifyRootDeep(path string) error {
152path = filepath.Clean(path)
153current := "/"
154segs := strings.Split(path, "/")
155depth := 0
156for i := 1; i < len(segs); i++ {
157seg := segs[i]
158current = filepath.Join(current, seg)
159info, err := os.Lstat(current)
160if err != nil {
161return err
162}
163
164stat := info.Sys().(*syscall.Stat_t)
165if stat.Uid != 0 {
166return fmt.Errorf("installation target path must be solely owned by root: %s is not", current)
167}
168
169if info.Mode()&fs.ModeSymlink != 0 {
170target, err := os.Readlink(current)
171if err != nil {
172return err
173}
174
175targetParts := strings.Split(target, "/")
176segs = append(targetParts, segs[i+1:]...)
177
178if depth++; depth > 1000 {
179return errors.New("reached max recursion depth, link structure is cyclical or too complex")
180}
181
182if !filepath.IsAbs(target) {
183current = filepath.Dir(current)
184i = -1 // Start at 0
185} else {
186current = "/"
187i = 0 // Skip empty first segment
188}
189}
190}
191
192return nil
193}
194
195func installExecutable(user string) (string, error) {
196// Since the installed executable runs as root, as a precaution verify root ownership of
197// the entire installation path, and utilize sticky + read-only perms for the helper path
198// suffix. The goal is to help users harden against privilege escalation from loose
199// filesystem permissions.
200//
201// Since userspace package management tools, such as brew, delegate management of system
202// paths to standard unix users, the daemon executable is copied into a separate more
203// restricted area of the filesystem.
204if err := verifyRootDeep(installPrefix); err != nil {
205return "", err
206}
207
208targetDir := filepath.Join(installPrefix, "podman", "helper", user)
209if err := os.MkdirAll(targetDir, mode755); err != nil {
210return "", fmt.Errorf("could not create helper directory structure: %w", err)
211}
212
213// Correct any incorrect perms on previously existing directories and verify no symlinks
214if err := restrictRecursive(targetDir, installPrefix); err != nil {
215return "", err
216}
217
218exec, err := os.Executable()
219if err != nil {
220return "", err
221}
222install := filepath.Join(targetDir, filepath.Base(exec))
223
224return install, copyFile(install, exec, mode755)
225}
226
227func copyFile(dest string, source string, perms fs.FileMode) error {
228in, err := os.Open(source)
229if err != nil {
230return err
231}
232
233defer in.Close()
234out, err := os.OpenFile(dest, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, perms)
235if err != nil {
236return err
237}
238
239defer out.Close()
240if _, err := io.Copy(out, in); err != nil {
241return err
242}
243
244return nil
245}
246