landdown
Simple Sandboxing for shell scripts.
git clone git://mccd.space/landdown
| Log | Files | Refs | README | LICENSE |
main.go (3642B)
1 package main
2
3 import (
4 "bufio"
5 "bytes"
6 "fmt"
7 "os"
8 "os/exec"
9 "strconv"
10 "strings"
11 "syscall"
12
13 "golang.org/x/sys/unix"
14
15 "github.com/landlock-lsm/go-landlock/landlock"
16 )
17
18 func main() {
19 if len(os.Args) < 2 {
20 fmt.Fprintf(os.Stderr, "landdown - simple sandboxing utility\n")
21 fmt.Println("You probably don't want to call this utility directly. Instead, you should use it in a shell script.")
22 fmt.Println("See readme for more information and usage examples.")
23 os.Exit(1)
24 }
25
26 scriptPath := os.Args[1]
27 extraArgs := os.Args[2:]
28
29 data, err := os.ReadFile(scriptPath)
30 if err != nil {
31 panic(err)
32 }
33
34 var roPaths, roFilePaths, rwFilePaths, rwPaths []string
35 var netRules []landlock.Rule = []landlock.Rule{}
36 var execCmd []string
37 var stdinData []byte
38
39 scanner := bufio.NewScanner(bytes.NewReader(data))
40 lineNum := 0
41 for scanner.Scan() {
42 lineNum++
43 line := strings.TrimSpace(scanner.Text())
44
45 // Skip the first shebang
46 if lineNum == 1 && strings.HasPrefix(line, "#!") {
47 continue
48 }
49
50 if strings.HasPrefix(line, "#!") {
51 execCmd = strings.Fields(line[2:])
52 // Everything remaining is stdin
53 var rest bytes.Buffer
54 for scanner.Scan() {
55 rest.Write(scanner.Bytes())
56 rest.WriteByte('\n')
57 }
58 stdinData = rest.Bytes()
59 break
60 }
61
62 // Ignore white space and comments
63 if line == "" || strings.HasPrefix(line, "#") {
64 continue
65 }
66
67 switch {
68 case strings.HasPrefix(line, "ro "):
69 roPaths = append(roPaths, strings.TrimSpace(line[3:]))
70 case strings.HasPrefix(line, "rof "):
71 roFilePaths = append(roFilePaths, strings.TrimSpace(line[4:]))
72 case strings.HasPrefix(line, "rw "):
73 rwPaths = append(rwPaths, strings.TrimSpace(line[3:]))
74 case strings.HasPrefix(line, "rwf "):
75 rwFilePaths = append(rwFilePaths, strings.TrimSpace(line[4:]))
76 case strings.HasPrefix(line, "bind "):
77 s := strings.TrimSpace(line[5:])
78 port, err := strconv.ParseUint(s, 10, 16)
79 if err != nil {
80 panic("Invalid bind rule. Must be an integer. Ex: bind 8080")
81 }
82 netRules = append(netRules, landlock.BindTCP(uint16(port)))
83 case strings.HasPrefix(line, "connect "):
84 s := strings.TrimSpace(line[8:])
85 port, err := strconv.ParseUint(s, 10, 16)
86 if err != nil {
87 panic("Invalid connect rule. Must be an integer. Ex: connect 443")
88 }
89 netRules = append(netRules, landlock.ConnectTCP(uint16(port)))
90 default:
91 panic(fmt.Sprintf("line %d: unknown directive: %s", lineNum, line))
92 }
93 }
94
95 if len(execCmd) == 0 {
96 panic("no exec target found (second #! line)")
97 }
98
99 // Apply landlock
100 var rules []landlock.Rule
101 if len(roPaths) > 0 {
102 rules = append(rules, landlock.RODirs(roPaths...))
103 }
104 if len(roFilePaths) > 0 {
105 rules = append(rules, landlock.ROFiles(roFilePaths...))
106 }
107 if len(rwPaths) > 0 {
108 rules = append(rules, landlock.RWDirs(rwPaths...))
109 }
110 if len(rwFilePaths) > 0 {
111 rules = append(rules, landlock.RWFiles(rwFilePaths...))
112 }
113 rules = append(rules, netRules...)
114
115 if len(execCmd) == 0 {
116 panic("no exec target found (second #! line)")
117 }
118
119 if err := landlock.V7.BestEffort().Restrict(rules...); err != nil {
120 panic(err)
121 }
122
123 fullPath, err := exec.LookPath(execCmd[0])
124 if err != nil {
125 panic(err)
126 }
127
128 argv := append(execCmd, extraArgs...)
129
130 // Create a memfile that is the content of the script
131 // we want to run, execute the script with that.
132 if len(stdinData) > 0 {
133 fd, err := unix.MemfdCreate("landdown", 0)
134 if err != nil {
135 panic(err)
136 }
137 if _,err := unix.Write(fd,stdinData); err != nil {
138 panic(err)
139 }
140 argv = append(argv, fmt.Sprintf("/dev/fd/%d", fd))
141 }
142
143 if err := syscall.Exec(fullPath, argv, os.Environ()); err != nil {
144 panic(err)
145 }
146 }