├── .github
└── workflows
│ ├── build.yml
│ └── go-compatibility.yml
├── .gitignore
├── LICENSE
├── README.md
├── cmd
└── landrun
│ └── main.go
├── demo.gif
├── go.mod
├── go.sum
├── internal
├── exec
│ └── runner.go
├── log
│ └── log.go
└── sandbox
│ └── sandbox.go
└── test.sh
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | pull_request:
7 | branches: [main]
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v4
14 |
15 | - name: Set up Go
16 | uses: actions/setup-go@v5
17 | with:
18 | go-version: "1.24.1"
19 | check-latest: true
20 |
21 | - name: Install dependencies
22 | run: go mod download
23 |
24 | - name: Build
25 | run: go build -v -o landrun ./cmd/landrun/main.go
26 |
27 | - name: Upload binary
28 | uses: actions/upload-artifact@v4
29 | with:
30 | name: landrun-linux-amd64
31 | path: ./landrun
32 |
--------------------------------------------------------------------------------
/.github/workflows/go-compatibility.yml:
--------------------------------------------------------------------------------
1 | name: Go version compatibility
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | strategy:
9 | matrix:
10 | go: ["1.18", "1.20", "1.22", "1.24"]
11 | name: Go ${{ matrix.go }} build
12 | steps:
13 | - uses: actions/checkout@v3
14 |
15 | - name: Set up Go ${{ matrix.go }}
16 | uses: actions/setup-go@v4
17 | with:
18 | go-version: ${{ matrix.go }}
19 |
20 | - name: Download dependencies
21 | run: go mod tidy
22 |
23 | - name: Build landrun
24 | run: go build ./cmd/landrun
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # If you prefer the allow list template instead of the deny list, see community template:
2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
3 | #
4 | # Binaries for programs and plugins
5 | *.exe
6 | *.exe~
7 | *.dll
8 | *.so
9 | *.dylib
10 |
11 | # Test binary, built with `go test -c`
12 | *.test
13 |
14 | # Output of the go coverage tool, specifically when used with LiteIDE
15 | *.out
16 |
17 | # Dependency directories (remove the comment below to include it)
18 | # vendor/
19 |
20 | # Go workspace file
21 | go.work
22 | go.work.sum
23 |
24 | # env file
25 | .env
26 | main
27 | tmp
28 | internal/sandbox/test_rw
29 | internal/sandbox/test_ro
30 | test_env
31 | landrun
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Armin ranjbar
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Landrun
2 |
3 | A lightweight, secure sandbox for running Linux processes using Landlock. Think firejail, but with kernel-level security and minimal overhead.
4 |
5 | Linux Landlock is a kernel-native security module that lets unprivileged processes sandbox themselves.
6 |
7 | Landrun is designed to make it practical to sandbox any command with fine-grained filesystem and network access controls. No root. No containers. No SELinux/AppArmor configs.
8 |
9 | It's lightweight, auditable, and wraps Landlock v5 features (file access + TCP restrictions).
10 |
11 | ## Features
12 |
13 | - 🔒 Kernel-level security using Landlock
14 | - 🚀 Lightweight and fast execution
15 | - 🛡️ Fine-grained access control for directories and files
16 | - 🔄 Support for read and write paths
17 | - ⚡ Path-specific execution permissions
18 | - 🌐 TCP network access control (binding and connecting)
19 |
20 | ## Demo
21 |
22 |
23 |
24 |
25 |
26 | ## Requirements
27 |
28 | - Linux kernel 5.13 or later with Landlock enabled
29 | - Linux kernel 6.7 or later for network restrictions (TCP bind/connect)
30 | - Go 1.18 or later (for building from source)
31 |
32 | ## Installation
33 |
34 | ### Quick Install
35 |
36 | ```bash
37 | go install github.com/zouuup/landrun/cmd/landrun@latest
38 | ```
39 |
40 | ### From Source
41 |
42 | ```bash
43 | git clone https://github.com/zouuup/landrun.git
44 | cd landrun
45 | go build -o landrun cmd/landrun/main.go
46 | sudo cp landrun /usr/local/bin/
47 | ```
48 |
49 | ### Distros
50 |
51 | #### Arch (AUR)
52 |
53 | - [stable](https://aur.archlinux.org/packages/landrun) maintained by [Vcalv](https://github.com/vcalv)
54 | - [latest commit](https://aur.archlinux.org/packages/landrun-git) maintained by [juxuanu](https://github.com/juxuanu/)
55 |
56 | #### Slackware
57 |
58 | maintained by [r1w1s1](https://github.com/r1w1s1)
59 |
60 | [Slackbuild](https://slackbuilds.org/repository/15.0/network/landrun/?search=landrun)
61 | ```bash
62 | sudo sbopkg -i packagename
63 | ```
64 |
65 | ## Usage
66 |
67 | Basic syntax:
68 |
69 | ```bash
70 | landrun [options] [args...]
71 | ```
72 |
73 | ### Options
74 |
75 | - `--ro `: Allow read-only access to specified path (can be specified multiple times or as comma-separated values)
76 | - `--rox `: Allow read-only access with execution to specified path (can be specified multiple times or as comma-separated values)
77 | - `--rw `: Allow read-write access to specified path (can be specified multiple times or as comma-separated values)
78 | - `--rwx `: Allow read-write access with execution to specified path (can be specified multiple times or as comma-separated values)
79 | - `--bind-tcp `: Allow binding to specified TCP port (can be specified multiple times or as comma-separated values)
80 | - `--connect-tcp `: Allow connecting to specified TCP port (can be specified multiple times or as comma-separated values)
81 | - `--env `: Environment variable to pass to the sandboxed command (format: KEY=VALUE or just KEY to pass current value)
82 | - `--best-effort`: Use best effort mode, falling back to less restrictive sandbox if necessary [default: disabled]
83 | - `--log-level `: Set logging level (error, info, debug) [default: "error"]
84 | - `--unrestricted-network`: Allows unrestricted network access (disables all network restrictions)
85 | - `--unrestricted-filesystem`: Allows unrestricted filesystem access (disables all filesystem restrictions)
86 | - `--add-exec`: Automatically adds the executing binary to --rox
87 | - `--ldd`: Automatically adds required libraries to --rox
88 |
89 | ### Important Notes
90 |
91 | - You must explicitly add the directory or files to the command you want to run with `--rox` flag
92 | - For system commands, you typically need to include `/usr/bin`, `/usr/lib`, and other system directories
93 | - Use `--rwx` for directories or files where you need both write access and the ability to execute files
94 | - Network restrictions require Linux kernel 6.7 or later with Landlock ABI v4
95 | - By default, no environment variables are passed to the sandboxed command. Use `--env` to explicitly pass environment variables
96 | - The `--best-effort` flag allows graceful degradation on older kernels that don't support all requested restrictions
97 | - Paths can be specified either using multiple flags or as comma-separated values (e.g., `--ro /usr,/lib,/home`)
98 | - If no paths or network rules are specified and neither unrestricted flag is set, landrun will apply maximum restrictions (denying all access)
99 |
100 | ### Environment Variables
101 |
102 | - `LANDRUN_LOG_LEVEL`: Set logging level (error, info, debug)
103 |
104 | ### Examples
105 |
106 | 1. Run a command that allows exec access to a specific file
107 |
108 | ```bash
109 | landrun --rox /usr/bin/ls --rox /usr/lib --ro /home ls /home
110 | ```
111 |
112 | 2. Run a command with read-only access to a directory:
113 |
114 | ```bash
115 | landrun --rox /usr/ --ro /path/to/dir ls /path/to/dir
116 | ```
117 |
118 | 3. Run a command with write access to a directory:
119 |
120 | ```bash
121 | landrun --rox /usr/bin --ro /lib --rw /path/to/dir touch /path/to/dir/newfile
122 | ```
123 |
124 | 4. Run a command with write access to a file:
125 |
126 | ```bash
127 | landrun --rox /usr/bin --ro /lib --rw /path/to/dir/newfile touch /path/to/dir/newfile
128 | ```
129 |
130 | 5. Run a command with execution permissions:
131 |
132 | ```bash
133 | landrun --rox /usr/ --ro /lib,/lib64 /usr/bin/bash
134 | ```
135 |
136 | 6. Run with debug logging:
137 |
138 | ```bash
139 | landrun --log-level debug --rox /usr/ --ro /lib,/lib64,/path/to/dir ls /path/to/dir
140 | ```
141 |
142 | 7. Run with network restrictions:
143 |
144 | ```bash
145 | landrun --rox /usr/ --ro /lib,/lib64 --bind-tcp 8080 --connect-tcp 80 /usr/bin/my-server
146 | ```
147 |
148 | This will allow the program to only bind to TCP port 8080 and connect to TCP port 80.
149 |
150 | 8. Run a DNS client with appropriate permissions:
151 |
152 | ```bash
153 | landrun --log-level debug --ro /etc,/usr --rox /usr/ --connect-tcp 443 nc kernel.org 443
154 | ```
155 |
156 | This allows connections to port 443, requires access to /etc/resolv.conf for resolving DNS.
157 |
158 | 9. Run a web server with selective network permissions:
159 |
160 | ```bash
161 | landrun --rox /usr/bin --ro /lib,/lib64,/var/www --rwx /var/log --bind-tcp 80,443 /usr/bin/nginx
162 | ```
163 |
164 | 10. Running anything without providing parameters is... maximum security jail!
165 |
166 | ```bash
167 | landrun ls
168 | ```
169 |
170 | 11. If you keep getting permission denied without knowing what exactly going on, best to use strace with it.
171 |
172 | ```bash
173 | landrun --rox /usr strace -f -e trace=all ls
174 | ```
175 |
176 | 12. Run with specific environment variables:
177 |
178 | ```bash
179 | landrun --rox /usr --ro /etc --env HOME --env PATH --env CUSTOM_VAR=my_value -- env
180 | ```
181 |
182 | This example passes the current HOME and PATH variables, plus a custom variable named CUSTOM_VAR.
183 |
184 | 13. Run command with explicity access to files instead of directories:
185 | ```bash
186 | landrun --rox /usr/lib/libc.so.6 --rox /usr/lib64/ld-linux-x86-64.so.2 --rox /usr/bin/true /usr/bin/true
187 | ```
188 |
189 | 14. Run a command with --add-exec which automatically adds target binary to --rox
190 |
191 | ```bash
192 | landrun --rox /usr/lib/ --add-exec /usr/bin/true
193 | ```
194 |
195 | 15. Run a command with --ldd and --add-exec which automatically adds required libraries and target binary to --rox
196 |
197 | ```bash
198 | landrun --ldd --add-exec /usr/bin/true
199 | ```
200 |
201 | Note that shared libs always need exec permission due to how they are loaded, PROT_EXEC on mmap() etc.
202 |
203 | ## Systemd Integration
204 |
205 | landrun can be integrated with systemd to run services with enhanced security. Here's an example of running nginx with landrun:
206 |
207 | 1. Create a systemd service file (e.g., `/etc/systemd/system/nginx-landrun.service`):
208 |
209 | ```ini
210 | [Unit]
211 | Description=nginx with landrun sandbox
212 | After=network.target
213 |
214 | [Service]
215 | Type=simple
216 | ExecStart=/usr/bin/landrun \
217 | --rox /usr/bin,/usr/lib \
218 | --ro /etc/nginx,/etc/ssl,/etc/passwd,/etc/group,/etc/nsswitch.conf \
219 | --rwx /var/log/nginx \
220 | --rwx /var/cache/nginx \
221 | --bind-tcp 80,443 \
222 | /usr/bin/nginx -g 'daemon off;'
223 | Restart=always
224 | User=nginx
225 | Group=nginx
226 |
227 | [Install]
228 | WantedBy=multi-user.target
229 | ```
230 |
231 | 2. Enable and start the service:
232 |
233 | ```bash
234 | sudo systemctl daemon-reload
235 | sudo systemctl enable nginx-landrun
236 | sudo systemctl start nginx-landrun
237 | ```
238 |
239 | 3. Check the service status:
240 |
241 | ```bash
242 | sudo systemctl status nginx-landrun
243 | ```
244 |
245 | This configuration:
246 | - Runs nginx with minimal required permissions
247 | - Allows binding to ports 80 and 443
248 | - Provides read-only access to configuration files
249 | - Allows write access only to log and cache directories
250 | - Runs as the nginx user and group
251 | - Automatically restarts on failure
252 |
253 | You can adjust the permissions based on your specific needs. For example, if you need to serve static files from `/var/www`, add `--ro /var/www` to the ExecStart line.
254 |
255 | ## Security
256 |
257 | landrun uses Linux's Landlock to create a secure sandbox environment. It provides:
258 |
259 | - File system access control
260 | - Directory access restrictions
261 | - Execution control
262 | - TCP network restrictions
263 | - Process isolation
264 | - Default restrictive mode when no rules are specified
265 |
266 | Landlock is an access-control system that enables processes to securely restrict themselves and their future children. As a stackable Linux Security Module (LSM), it creates additional security layers on top of existing system-wide access controls, helping to mitigate security impacts from bugs or malicious behavior in applications.
267 |
268 | ### Landlock Access Control Rights
269 |
270 | landrun leverages Landlock's fine-grained access control mechanisms, which include:
271 |
272 | **File-specific rights:**
273 |
274 | - Execute files (`LANDLOCK_ACCESS_FS_EXECUTE`)
275 | - Write to files (`LANDLOCK_ACCESS_FS_WRITE_FILE`)
276 | - Read files (`LANDLOCK_ACCESS_FS_READ_FILE`)
277 | - Truncate files (`LANDLOCK_ACCESS_FS_TRUNCATE`) - Available since Landlock ABI v3
278 | - IOCTL operations on devices (`LANDLOCK_ACCESS_FS_IOCTL_DEV`) - Available since Landlock ABI v5
279 |
280 | **Directory-specific rights:**
281 |
282 | - Read directory contents (`LANDLOCK_ACCESS_FS_READ_DIR`)
283 | - Remove directories (`LANDLOCK_ACCESS_FS_REMOVE_DIR`)
284 | - Remove files (`LANDLOCK_ACCESS_FS_REMOVE_FILE`)
285 | - Create various filesystem objects (char devices, directories, regular files, sockets, etc.)
286 | - Refer/reparent files across directories (`LANDLOCK_ACCESS_FS_REFER`) - Available since Landlock ABI v2
287 |
288 | **Network-specific rights** (requires Linux 6.7+ with Landlock ABI v4):
289 |
290 | - Bind to specific TCP ports (`LANDLOCK_ACCESS_NET_BIND_TCP`)
291 | - Connect to specific TCP ports (`LANDLOCK_ACCESS_NET_CONNECT_TCP`)
292 |
293 | ### Limitations
294 |
295 | - Landlock must be supported by your kernel
296 | - Network restrictions require Linux kernel 6.7 or later with Landlock ABI v4
297 | - Some operations may require additional permissions
298 | - Files or directories opened before sandboxing are not subject to Landlock restrictions
299 |
300 | ## Kernel Compatibility Table
301 |
302 | | Feature | Minimum Kernel Version | Landlock ABI Version |
303 | | ---------------------------------- | ---------------------- | -------------------- |
304 | | Basic filesystem sandboxing | 5.13 | 1 |
305 | | File referring/reparenting control | 5.19 | 2 |
306 | | File truncation control | 6.2 | 3 |
307 | | Network TCP restrictions | 6.7 | 4 |
308 | | IOCTL on special files | 6.10 | 5 |
309 |
310 | ## Troubleshooting
311 |
312 | If you receive "permission denied" or similar errors:
313 |
314 | 1. Ensure you've added all necessary paths with `--ro` or `--rw`
315 | 2. Try running with `--log-level debug` to see detailed permission information
316 | 3. Check that Landlock is supported and enabled on your system:
317 | ```bash
318 | grep -E 'landlock|lsm=' /boot/config-$(uname -r)
319 | # alternatively, if there are no /boot/config-* files
320 | zgrep -iE 'landlock|lsm=' /proc/config.gz
321 | # another alternate method
322 | grep -iE 'landlock|lsm=' /lib/modules/$(uname -r)/config
323 | ```
324 | You should see `CONFIG_SECURITY_LANDLOCK=y` and `lsm=landlock,...` in the output
325 | 4. For network restrictions, verify your kernel version is 6.7+ with Landlock ABI v4:
326 | ```bash
327 | uname -r
328 | ```
329 |
330 | ## Technical Details
331 |
332 | ### Implementation
333 |
334 | This project uses the [landlock-lsm/go-landlock](https://github.com/landlock-lsm/go-landlock) package for sandboxing, which provides both filesystem and network restrictions. The current implementation supports:
335 |
336 | - Read/write/execute restrictions for files and directories
337 | - TCP port binding restrictions
338 | - TCP port connection restrictions
339 | - Best-effort mode for graceful degradation on older kernels
340 |
341 | ### Best-Effort Mode
342 |
343 | When using `--best-effort` (disabled by default), landrun will gracefully degrade to using the best available Landlock version on the current kernel. This means:
344 |
345 | - On Linux 6.7+: Full filesystem and network restrictions
346 | - On Linux 6.2-6.6: Filesystem restrictions including truncation, but no network restrictions
347 | - On Linux 5.19-6.1: Basic filesystem restrictions including file reparenting, but no truncation control or network restrictions
348 | - On Linux 5.13-5.18: Basic filesystem restrictions without file reparenting, truncation control, or network restrictions
349 | - On older Linux: No restrictions (sandbox disabled)
350 |
351 | When no rules are specified and neither unrestricted flag is set, landrun will apply maximum restrictions available for the current kernel version.
352 |
353 | ### Tests
354 |
355 | The project includes a comprehensive test suite that verifies:
356 |
357 | - Basic filesystem access controls (read-only, read-write, execute)
358 | - Directory traversal and path handling
359 | - Network restrictions (TCP bind/connect)
360 | - Environment variable isolation
361 | - System command execution
362 | - Edge cases and regression tests
363 |
364 | Run the tests with:
365 |
366 | ```bash
367 | ./test.sh
368 | ```
369 |
370 | Use `--keep-binary` to preserve the test binary after completion:
371 |
372 | ```bash
373 | ./test.sh --keep-binary
374 | ```
375 |
376 | Use `--use-system` to test against the system-installed landrun binary:
377 |
378 | ```bash
379 | ./test.sh --use-system
380 | ```
381 |
382 | ## Future Features
383 |
384 | Based on the Linux Landlock API capabilities, we plan to add:
385 |
386 | - 🔒 Enhanced filesystem controls with more fine-grained permissions
387 | - 🌐 Support for UDP and other network protocol restrictions (when supported by Linux kernel)
388 | - 🔄 Process scoping and resource controls
389 | - 🛡️ Additional security features as they become available in the Landlock API
390 |
391 | ## Acknowledgements
392 |
393 | This project wouldn't exist without:
394 |
395 | - [Landlock](https://landlock.io), the kernel security module enabling unprivileged sandboxing - maintained by [@l0kod](https://github.com/l0kod)
396 | - [go-landlock](https://github.com/landlock-lsm/go-landlock), the Go bindings powering this tool - developed by [@gnoack](https://github.com/gnoack)
397 |
398 | ## Contributing
399 |
400 | Contributions are welcome! Please feel free to submit a Pull Request.
401 |
--------------------------------------------------------------------------------
/cmd/landrun/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "os"
5 | osexec "os/exec"
6 | "strings"
7 |
8 | "github.com/urfave/cli/v2"
9 | "github.com/zouuup/landrun/internal/exec"
10 | "github.com/zouuup/landrun/internal/log"
11 | "github.com/zouuup/landrun/internal/sandbox"
12 | )
13 |
14 | // Version is the current version of landrun
15 | const Version = "0.1.15"
16 |
17 | // getLibraryDependencies returns a list of library paths that the given binary depends on
18 | func getLibraryDependencies(binary string) ([]string, error) {
19 | cmd := osexec.Command("ldd", binary)
20 | output, err := cmd.Output()
21 | if err != nil {
22 | return nil, err
23 | }
24 |
25 | var libPaths []string
26 | lines := strings.Split(string(output), "\n")
27 | for _, line := range lines {
28 | // Skip empty lines and the first line (usually the binary name)
29 | if line == "" || !strings.Contains(line, "=>") {
30 | continue
31 | }
32 | // Extract the library path
33 | parts := strings.Fields(line)
34 | if len(parts) >= 3 {
35 | libPath := strings.Trim(parts[2], "()")
36 | if libPath != "" {
37 | libPaths = append(libPaths, libPath)
38 | }
39 | }
40 | }
41 | return libPaths, nil
42 | }
43 |
44 | func main() {
45 | app := &cli.App{
46 | Name: "landrun",
47 | Usage: "Run a command in a Landlock sandbox",
48 | Version: Version,
49 | Flags: []cli.Flag{
50 | &cli.StringFlag{
51 | Name: "log-level",
52 | Usage: "Set logging level (error, info, debug)",
53 | Value: "error",
54 | EnvVars: []string{"LANDRUN_LOG_LEVEL"},
55 | },
56 | &cli.StringSliceFlag{
57 | Name: "ro",
58 | Usage: "Allow read-only access to this path",
59 | },
60 | &cli.StringSliceFlag{
61 | Name: "rox",
62 | Usage: "Allow read-only access with execution to this path",
63 | },
64 | &cli.StringSliceFlag{
65 | Name: "rw",
66 | Usage: "Allow read-write access to this path",
67 | },
68 | &cli.StringSliceFlag{
69 | Name: "rwx",
70 | Usage: "Allow read-write access with execution to this path",
71 | },
72 | &cli.IntSliceFlag{
73 | Name: "bind-tcp",
74 | Usage: "Allow binding to these TCP ports",
75 | Hidden: false,
76 | },
77 | &cli.IntSliceFlag{
78 | Name: "connect-tcp",
79 | Usage: "Allow connecting to these TCP ports",
80 | Hidden: false,
81 | },
82 | &cli.BoolFlag{
83 | Name: "best-effort",
84 | Usage: "Use best effort mode (fall back to less restrictive sandbox if necessary)",
85 | Value: false,
86 | },
87 | &cli.StringSliceFlag{
88 | Name: "env",
89 | Usage: "Environment variables to pass to the sandboxed command (KEY=VALUE or just KEY to pass current value)",
90 | Value: cli.NewStringSlice(),
91 | },
92 | &cli.BoolFlag{
93 | Name: "unrestricted-filesystem",
94 | Usage: "Allow unrestricted filesystem access",
95 | Value: false,
96 | },
97 | &cli.BoolFlag{
98 | Name: "unrestricted-network",
99 | Usage: "Allow unrestricted network access",
100 | Value: false,
101 | },
102 | &cli.BoolFlag{
103 | Name: "ldd",
104 | Usage: "Automatically detect and add library dependencies to --rox",
105 | Value: false,
106 | },
107 | &cli.BoolFlag{
108 | Name: "add-exec",
109 | Usage: "Automatically add the executable path to --rox",
110 | Value: false,
111 | },
112 | },
113 | Before: func(c *cli.Context) error {
114 | log.SetLevel(c.String("log-level"))
115 | return nil
116 | },
117 | Action: func(c *cli.Context) error {
118 | args := c.Args().Slice()
119 | if len(args) == 0 {
120 | log.Fatal("Missing command to run")
121 | }
122 |
123 | // Combine --ro and --rox paths for read-only access
124 | readOnlyPaths := append([]string{}, c.StringSlice("ro")...)
125 | readOnlyPaths = append(readOnlyPaths, c.StringSlice("rox")...)
126 |
127 | // Combine --rw and --rwx paths for read-write access
128 | readWritePaths := append([]string{}, c.StringSlice("rw")...)
129 | readWritePaths = append(readWritePaths, c.StringSlice("rwx")...)
130 |
131 | // Combine --rox and --rwx paths for executable permissions
132 | readOnlyExecutablePaths := append([]string{}, c.StringSlice("rox")...)
133 | readWriteExecutablePaths := append([]string{}, c.StringSlice("rwx")...)
134 |
135 | binary, err := osexec.LookPath(args[0])
136 | if err != nil {
137 | log.Fatal("Failed to find binary: %v", err)
138 | }
139 |
140 | // Add command's directory to readOnlyExecutablePaths
141 | if c.Bool("add-exec") {
142 | readOnlyExecutablePaths = append(readOnlyExecutablePaths, binary)
143 | log.Debug("Added executable path: %v", binary)
144 | }
145 |
146 | // If --ldd flag is set, detect and add library dependencies
147 | if c.Bool("ldd") {
148 | libPaths, err := getLibraryDependencies(binary)
149 | if err != nil {
150 | log.Fatal("Failed to detect library dependencies: %v", err)
151 | }
152 | // Add library directories to readOnlyExecutablePaths
153 | readOnlyExecutablePaths = append(readOnlyExecutablePaths, libPaths...)
154 | log.Debug("Added library paths: %v", libPaths)
155 | }
156 |
157 | cfg := sandbox.Config{
158 | ReadOnlyPaths: readOnlyPaths,
159 | ReadWritePaths: readWritePaths,
160 | ReadOnlyExecutablePaths: readOnlyExecutablePaths,
161 | ReadWriteExecutablePaths: readWriteExecutablePaths,
162 | BindTCPPorts: c.IntSlice("bind-tcp"),
163 | ConnectTCPPorts: c.IntSlice("connect-tcp"),
164 | BestEffort: c.Bool("best-effort"),
165 | UnrestrictedFilesystem: c.Bool("unrestricted-filesystem"),
166 | UnrestrictedNetwork: c.Bool("unrestricted-network"),
167 | }
168 |
169 | // Process environment variables
170 | envVars := processEnvironmentVars(c.StringSlice("env"))
171 |
172 | if err := sandbox.Apply(cfg); err != nil {
173 | log.Fatal("Failed to apply sandbox: %v", err)
174 | }
175 |
176 | return exec.Run(args, envVars)
177 | },
178 | }
179 |
180 | if err := app.Run(os.Args); err != nil {
181 | log.Fatal("%v", err)
182 | }
183 | }
184 |
185 | // processEnvironmentVars processes the env flag values
186 | func processEnvironmentVars(envFlags []string) []string {
187 | result := []string{}
188 |
189 | for _, env := range envFlags {
190 | // If the flag is just a key (no = sign), get the value from the current environment
191 | if !strings.Contains(env, "=") {
192 | if val, exists := os.LookupEnv(env); exists {
193 | result = append(result, env+"="+val)
194 | }
195 | } else {
196 | // Flag already contains the value (KEY=VALUE format)
197 | result = append(result, env)
198 | }
199 | }
200 |
201 | return result
202 | }
203 |
--------------------------------------------------------------------------------
/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Zouuup/landrun/8fb7bf3bb5749abc3f2bd2adab9ea584af98a683/demo.gif
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/zouuup/landrun
2 |
3 | go 1.18
4 |
5 | require (
6 | github.com/landlock-lsm/go-landlock v0.0.0-20250303204525-1544bccde3a3
7 | github.com/urfave/cli/v2 v2.27.6
8 | )
9 |
10 | require (
11 | github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
12 | github.com/russross/blackfriday/v2 v2.1.0 // indirect
13 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
14 | golang.org/x/sys v0.26.0 // indirect
15 | kernel.org/pub/linux/libs/security/libcap/psx v1.2.70 // indirect
16 | )
17 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
2 | github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
3 | github.com/landlock-lsm/go-landlock v0.0.0-20250303204525-1544bccde3a3 h1:zcMi8R8vP0WrrXlFMNUBpDy/ydo3sTnCcUPowq1XmSc=
4 | github.com/landlock-lsm/go-landlock v0.0.0-20250303204525-1544bccde3a3/go.mod h1:RSub3ourNF8Hf+swvw49Catm3s7HVf4hzdFxDUnEzdA=
5 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
6 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
7 | github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g=
8 | github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
9 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
10 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
11 | golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
12 | golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
13 | kernel.org/pub/linux/libs/security/libcap/psx v1.2.70 h1:HsB2G/rEQiYyo1bGoQqHZ/Bvd6x1rERQTNdPr1FyWjI=
14 | kernel.org/pub/linux/libs/security/libcap/psx v1.2.70/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24=
15 |
--------------------------------------------------------------------------------
/internal/exec/runner.go:
--------------------------------------------------------------------------------
1 | package exec
2 |
3 | import (
4 | "os/exec"
5 | "syscall"
6 |
7 | "github.com/zouuup/landrun/internal/log"
8 | )
9 |
10 | func Run(args []string, env []string) error {
11 | binary, err := exec.LookPath(args[0])
12 | if err != nil {
13 | return err
14 | }
15 |
16 | log.Info("Executing: %v", args)
17 |
18 | // Only pass the explicitly specified environment variables
19 | // If env is empty, no environment variables will be passed
20 | return syscall.Exec(binary, args, env)
21 | }
22 |
--------------------------------------------------------------------------------
/internal/log/log.go:
--------------------------------------------------------------------------------
1 | package log
2 |
3 | import (
4 | "log"
5 | "os"
6 | "strings"
7 | )
8 |
9 | type Level int
10 |
11 | const (
12 | LevelError Level = iota
13 | LevelInfo
14 | LevelDebug
15 | )
16 |
17 | var (
18 | debug = log.New(os.Stderr, "[landrun:debug] ", log.LstdFlags)
19 | info = log.New(os.Stderr, "[landrun] ", log.LstdFlags)
20 | error = log.New(os.Stderr, "[landrun:error] ", log.LstdFlags)
21 |
22 | currentLevel = LevelInfo // default level
23 | )
24 |
25 | // SetLevel sets the logging level
26 | func SetLevel(level string) {
27 | switch strings.ToLower(level) {
28 | case "error":
29 | currentLevel = LevelError
30 | case "info":
31 | currentLevel = LevelInfo
32 | case "debug":
33 | currentLevel = LevelDebug
34 | default:
35 | currentLevel = LevelError
36 | }
37 | }
38 |
39 | // Debug logs a debug message
40 | func Debug(format string, v ...interface{}) {
41 | if currentLevel >= LevelDebug {
42 | debug.Printf(format, v...)
43 | }
44 | }
45 |
46 | // Info logs an info message
47 | func Info(format string, v ...interface{}) {
48 | if currentLevel >= LevelInfo {
49 | info.Printf(format, v...)
50 | }
51 | }
52 |
53 | // Error logs an error message
54 | func Error(format string, v ...interface{}) {
55 | if currentLevel >= LevelError {
56 | error.Printf(format, v...)
57 | }
58 | }
59 |
60 | // Fatal logs an error message and exits
61 | func Fatal(format string, v ...interface{}) {
62 | error.Printf(format, v...)
63 | os.Exit(1)
64 | }
65 |
--------------------------------------------------------------------------------
/internal/sandbox/sandbox.go:
--------------------------------------------------------------------------------
1 | package sandbox
2 |
3 | import (
4 | "fmt"
5 | "os"
6 |
7 | "github.com/landlock-lsm/go-landlock/landlock"
8 | "github.com/landlock-lsm/go-landlock/landlock/syscall"
9 | "github.com/zouuup/landrun/internal/log"
10 | )
11 |
12 | type Config struct {
13 | ReadOnlyPaths []string
14 | ReadWritePaths []string
15 | ReadOnlyExecutablePaths []string
16 | ReadWriteExecutablePaths []string
17 | BindTCPPorts []int
18 | ConnectTCPPorts []int
19 | BestEffort bool
20 | UnrestrictedFilesystem bool
21 | UnrestrictedNetwork bool
22 | }
23 |
24 | // getReadWriteExecutableRights returns a full set of permissions including execution
25 | func getReadWriteExecutableRights(dir bool) landlock.AccessFSSet {
26 | accessRights := landlock.AccessFSSet(0)
27 | accessRights |= landlock.AccessFSSet(syscall.AccessFSExecute)
28 | accessRights |= landlock.AccessFSSet(syscall.AccessFSReadFile)
29 | accessRights |= landlock.AccessFSSet(syscall.AccessFSWriteFile)
30 | accessRights |= landlock.AccessFSSet(syscall.AccessFSTruncate)
31 | accessRights |= landlock.AccessFSSet(syscall.AccessFSIoctlDev)
32 |
33 | if dir {
34 | accessRights |= landlock.AccessFSSet(syscall.AccessFSReadDir)
35 | accessRights |= landlock.AccessFSSet(syscall.AccessFSRemoveDir)
36 | accessRights |= landlock.AccessFSSet(syscall.AccessFSRemoveFile)
37 | accessRights |= landlock.AccessFSSet(syscall.AccessFSMakeChar)
38 | accessRights |= landlock.AccessFSSet(syscall.AccessFSMakeDir)
39 | accessRights |= landlock.AccessFSSet(syscall.AccessFSMakeReg)
40 | accessRights |= landlock.AccessFSSet(syscall.AccessFSMakeSock)
41 | accessRights |= landlock.AccessFSSet(syscall.AccessFSMakeFifo)
42 | accessRights |= landlock.AccessFSSet(syscall.AccessFSMakeBlock)
43 | accessRights |= landlock.AccessFSSet(syscall.AccessFSMakeSym)
44 | accessRights |= landlock.AccessFSSet(syscall.AccessFSRefer)
45 | }
46 |
47 | return accessRights
48 | }
49 |
50 | func getReadOnlyExecutableRights(dir bool) landlock.AccessFSSet {
51 | accessRights := landlock.AccessFSSet(0)
52 | accessRights |= landlock.AccessFSSet(syscall.AccessFSExecute)
53 | accessRights |= landlock.AccessFSSet(syscall.AccessFSReadFile)
54 | if dir {
55 | accessRights |= landlock.AccessFSSet(syscall.AccessFSReadDir)
56 | }
57 | return accessRights
58 | }
59 |
60 | // getReadOnlyRights returns permissions for read-only access
61 | func getReadOnlyRights(dir bool) landlock.AccessFSSet {
62 | accessRights := landlock.AccessFSSet(0)
63 | accessRights |= landlock.AccessFSSet(syscall.AccessFSReadFile)
64 | if dir {
65 | accessRights |= landlock.AccessFSSet(syscall.AccessFSReadDir)
66 | }
67 | return accessRights
68 | }
69 |
70 | // getReadWriteRights returns permissions for read-write access
71 | func getReadWriteRights(dir bool) landlock.AccessFSSet {
72 | accessRights := landlock.AccessFSSet(0)
73 | accessRights |= landlock.AccessFSSet(syscall.AccessFSReadFile)
74 | accessRights |= landlock.AccessFSSet(syscall.AccessFSWriteFile)
75 | accessRights |= landlock.AccessFSSet(syscall.AccessFSTruncate)
76 | accessRights |= landlock.AccessFSSet(syscall.AccessFSIoctlDev)
77 | if dir {
78 | accessRights |= landlock.AccessFSSet(syscall.AccessFSReadDir)
79 | accessRights |= landlock.AccessFSSet(syscall.AccessFSRemoveDir)
80 | accessRights |= landlock.AccessFSSet(syscall.AccessFSRemoveFile)
81 | accessRights |= landlock.AccessFSSet(syscall.AccessFSMakeChar)
82 | accessRights |= landlock.AccessFSSet(syscall.AccessFSMakeDir)
83 | accessRights |= landlock.AccessFSSet(syscall.AccessFSMakeReg)
84 | accessRights |= landlock.AccessFSSet(syscall.AccessFSMakeSock)
85 | accessRights |= landlock.AccessFSSet(syscall.AccessFSMakeFifo)
86 | accessRights |= landlock.AccessFSSet(syscall.AccessFSMakeBlock)
87 | accessRights |= landlock.AccessFSSet(syscall.AccessFSMakeSym)
88 | accessRights |= landlock.AccessFSSet(syscall.AccessFSRefer)
89 | }
90 |
91 | return accessRights
92 | }
93 |
94 | // isDirectory checks if the given path is a directory
95 | func isDirectory(path string) bool {
96 | fileInfo, err := os.Stat(path)
97 | if err != nil {
98 | return false
99 | }
100 | return fileInfo.IsDir()
101 | }
102 |
103 | func Apply(cfg Config) error {
104 | log.Info("Sandbox config: %+v", cfg)
105 |
106 | // Get the most advanced Landlock version available
107 | llCfg := landlock.V5
108 | if cfg.BestEffort {
109 | llCfg = llCfg.BestEffort()
110 | }
111 |
112 | // Collect our rules
113 | var file_rules []landlock.Rule
114 | var net_rules []landlock.Rule
115 |
116 | // Process executable paths
117 | for _, path := range cfg.ReadOnlyExecutablePaths {
118 | log.Debug("Adding read-only executable path: %s", path)
119 | file_rules = append(file_rules, landlock.PathAccess(getReadOnlyExecutableRights(isDirectory(path)), path))
120 | }
121 |
122 | for _, path := range cfg.ReadWriteExecutablePaths {
123 | log.Debug("Adding read-write executable path: %s", path)
124 | file_rules = append(file_rules, landlock.PathAccess(getReadWriteExecutableRights(isDirectory(path)), path))
125 | }
126 |
127 | // Process read-only paths
128 | for _, path := range cfg.ReadOnlyPaths {
129 | log.Debug("Adding read-only path: %s", path)
130 | file_rules = append(file_rules, landlock.PathAccess(getReadOnlyRights(isDirectory(path)), path))
131 | }
132 |
133 | // Process read-write paths
134 | for _, path := range cfg.ReadWritePaths {
135 | log.Debug("Adding read-write path: %s", path)
136 | file_rules = append(file_rules, landlock.PathAccess(getReadWriteRights(isDirectory(path)), path))
137 | }
138 |
139 | // Add rules for TCP port binding
140 | for _, port := range cfg.BindTCPPorts {
141 | log.Debug("Adding TCP bind port: %d", port)
142 | net_rules = append(net_rules, landlock.BindTCP(uint16(port)))
143 | }
144 |
145 | // Add rules for TCP connections
146 | for _, port := range cfg.ConnectTCPPorts {
147 | log.Debug("Adding TCP connect port: %d", port)
148 | net_rules = append(net_rules, landlock.ConnectTCP(uint16(port)))
149 | }
150 |
151 | if cfg.UnrestrictedFilesystem && cfg.UnrestrictedNetwork {
152 | log.Info("Unrestricted filesystem and network access enabled; no rules applied.")
153 | return nil
154 | }
155 |
156 | if cfg.UnrestrictedFilesystem {
157 | log.Info("Unrestricted filesystem access enabled.")
158 | }
159 |
160 | if cfg.UnrestrictedNetwork {
161 | log.Info("Unrestricted network access enabled")
162 | }
163 |
164 | // If we have no rules, just return
165 | if len(file_rules) == 0 && len(net_rules) == 0 && !cfg.UnrestrictedFilesystem && !cfg.UnrestrictedNetwork {
166 | log.Error("No rules provided, applying default restrictive rules, this will restrict anything landlock can do.")
167 | err := llCfg.Restrict()
168 | if err != nil {
169 | return fmt.Errorf("failed to apply default Landlock restrictions: %w", err)
170 | }
171 | log.Info("Default restrictive Landlock rules applied successfully")
172 | return nil
173 | }
174 |
175 | // Apply all rules at once
176 | log.Debug("Applying Landlock restrictions")
177 | if !cfg.UnrestrictedFilesystem {
178 | err := llCfg.RestrictPaths(file_rules...)
179 | if err != nil {
180 | return fmt.Errorf("failed to apply Landlock filesystem restrictions: %w", err)
181 | }
182 | }
183 | if !cfg.UnrestrictedNetwork {
184 | err := llCfg.RestrictNet(net_rules...)
185 | if err != nil {
186 | return fmt.Errorf("failed to apply Landlock network restrictions: %w", err)
187 | }
188 | }
189 |
190 | log.Info("Landlock restrictions applied successfully")
191 | return nil
192 | }
193 |
--------------------------------------------------------------------------------
/test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Check if we should keep the binary
4 | KEEP_BINARY=false
5 | USE_SYSTEM_BINARY=false
6 | NO_BUILD=false
7 |
8 | while [ "$#" -gt 0 ]; do
9 | case "$1" in
10 | "--keep-binary")
11 | KEEP_BINARY=true
12 | shift
13 | ;;
14 | "--use-system")
15 | USE_SYSTEM_BINARY=true
16 | shift
17 | ;;
18 | "--no-build")
19 | NO_BUILD=true
20 | shift
21 | ;;
22 | *)
23 | echo "Unknown parameter: $1"
24 | exit 1
25 | ;;
26 | esac
27 | done
28 |
29 | # Don't exit on error, we'll handle errors in the run_test function
30 | set +e
31 |
32 | # Colors for output
33 | RED='\033[0;31m'
34 | GREEN='\033[0;32m'
35 | YELLOW='\033[1;33m'
36 | NC='\033[0m' # No Color
37 |
38 | # Function to print colored output
39 | print_status() {
40 | echo -e "${YELLOW}[TEST]${NC} $1"
41 | }
42 |
43 | print_success() {
44 | echo -e "${GREEN}[SUCCESS]${NC} $1"
45 | }
46 |
47 | print_error() {
48 | echo -e "${RED}[ERROR]${NC} $1"
49 | }
50 |
51 | # Build the binary if not using system binary
52 | if [ "$USE_SYSTEM_BINARY" = false ]; then
53 | if [ "$NO_BUILD" = false ]; then
54 | print_status "Building landrun binary..."
55 | go build -o landrun cmd/landrun/main.go
56 | if [ $? -ne 0 ]; then
57 | print_error "Failed to build landrun binary"
58 | exit 1
59 | fi
60 | print_success "Binary built successfully"
61 | else
62 | print_success "Using already built landrun binary"
63 | fi
64 | fi
65 |
66 | # Create test directories
67 | TEST_DIR="test_env"
68 | RO_DIR="$TEST_DIR/ro"
69 | RO_DIR_NESTED_RO="$RO_DIR/ro_nested_ro_1"
70 | RO_DIR_NESTED_RW="$RO_DIR/ro_nested_rw_1"
71 | RO_DIR_NESTED_EXEC="$RO_DIR/ro_nested_exec"
72 |
73 | RW_DIR="$TEST_DIR/rw"
74 | RW_DIR_NESTED_RO="$RW_DIR/rw_nested_ro_1"
75 | RW_DIR_NESTED_RW="$RW_DIR/rw_nested_rw_1"
76 | RW_DIR_NESTED_EXEC="$RW_DIR/rw_nested_exec"
77 |
78 | EXEC_DIR="$TEST_DIR/exec"
79 | NESTED_DIR="$TEST_DIR/nested/path/deep"
80 |
81 | print_status "Setting up test environment..."
82 | rm -rf "$TEST_DIR"
83 | mkdir -p "$RO_DIR" "$RW_DIR" "$EXEC_DIR" "$NESTED_DIR" "$RO_DIR_NESTED_RO" "$RO_DIR_NESTED_RW" "$RO_DIR_NESTED_EXEC" "$RW_DIR_NESTED_RO" "$RW_DIR_NESTED_RW" "$RW_DIR_NESTED_EXEC"
84 |
85 | # Create test files
86 | echo "readonly content" > "$RO_DIR/test.txt"
87 | echo "readwrite content" > "$RW_DIR/test.txt"
88 | echo "nested content" > "$NESTED_DIR/test.txt"
89 | echo "#!/bin/bash" > "$EXEC_DIR/test.sh"
90 | echo "echo 'executable content'" >> "$EXEC_DIR/test.sh"
91 | chmod +x "$EXEC_DIR/test.sh"
92 | cp $EXEC_DIR/test.sh $EXEC_DIR/test2.sh
93 |
94 | cp "$RO_DIR/test.txt" "$RO_DIR_NESTED_RO/test.txt"
95 | cp "$RO_DIR/test.txt" "$RW_DIR_NESTED_RO/test.txt"
96 |
97 | cp "$RW_DIR/test.txt" "$RO_DIR_NESTED_RW/test.txt"
98 | cp "$RW_DIR/test.txt" "$RW_DIR_NESTED_RW/test.txt"
99 |
100 | cp "$EXEC_DIR/test.sh" "$RO_DIR_NESTED_EXEC/test.sh"
101 | cp "$EXEC_DIR/test.sh" "$RW_DIR_NESTED_EXEC/test.sh"
102 | cp "$EXEC_DIR/test.sh" "$RO_DIR_NESTED_RO/test.sh"
103 | cp "$EXEC_DIR/test.sh" "$RW_DIR_NESTED_RO/test.sh"
104 | cp "$EXEC_DIR/test.sh" "$RO_DIR_NESTED_RW/test.sh"
105 | cp "$EXEC_DIR/test.sh" "$RW_DIR_NESTED_RW/test.sh"
106 |
107 | # Create a script in RW dir to test execution in RW dirs
108 | echo "#!/bin/bash" > "$RW_DIR/rw_script.sh"
109 | echo "echo 'this script is in a read-write directory'" >> "$RW_DIR/rw_script.sh"
110 | chmod +x "$RW_DIR/rw_script.sh"
111 |
112 | # Function to run a test case
113 | run_test() {
114 | local name="$1"
115 | local cmd="$2"
116 | local expected_exit="$3"
117 |
118 | # Replace ./landrun with landrun if using system binary
119 | if [ "$USE_SYSTEM_BINARY" = true ]; then
120 | cmd="${cmd//.\/landrun/landrun}"
121 | fi
122 |
123 | print_status "Running test: $name"
124 | eval "$cmd"
125 | local exit_code=$?
126 |
127 | if [ $exit_code -eq $expected_exit ]; then
128 | print_success "Test passed: $name"
129 | return 0
130 | else
131 | print_error "Test failed: $name (expected exit $expected_exit, got $exit_code)"
132 | exit 1
133 | fi
134 | }
135 |
136 | # Test cases
137 | print_status "Starting test cases..."
138 |
139 | Basic access tests
140 | run_test "Read-only access to file" \
141 | "./landrun --log-level debug --rox /usr --ro /lib --ro /lib64 --ro $RO_DIR -- cat $RO_DIR/test.txt" \
142 | 0
143 |
144 | run_test "Read-only access to nested file" \
145 | "./landrun --log-level debug --rox /usr --ro /lib --ro /lib64 --ro $RO_DIR -- cat $RO_DIR_NESTED_RO/test.txt" \
146 | 0
147 |
148 | run_test "Write access to nested directory writable nested in read-only directory" \
149 | "./landrun --log-level debug --rox /usr --ro /lib --ro /lib64 --ro $RO_DIR --rw $RO_DIR_NESTED_RW -- touch $RO_DIR_NESTED_RW/created_file" \
150 | 0
151 |
152 | run_test "Write access to nested file writable nested in read-only directory" \
153 | "./landrun --log-level debug --rox /usr --ro /lib --ro /lib64 --ro $RO_DIR --rw $RO_DIR_NESTED_RW/created_file -- touch $RO_DIR_NESTED_RW/created_file" \
154 | 0
155 |
156 | run_test "Read-write access to file" \
157 | "./landrun --log-level debug --rox /usr --ro /lib --ro /lib64 --ro $RO_DIR --rw $RW_DIR touch $RW_DIR/new.txt" \
158 | 0
159 |
160 | run_test "No write access to read-only directory" \
161 | "./landrun --log-level debug --rox /usr --ro /lib --ro /lib64 --ro $RO_DIR --rw $RW_DIR touch $RO_DIR/new.txt" \
162 | 1
163 |
164 | # Executable permission tests
165 | run_test "Execute access with rox flag" \
166 | "./landrun --log-level debug --rox /usr --ro /lib --ro /lib64 --rox $EXEC_DIR -- $EXEC_DIR/test.sh" \
167 | 0
168 |
169 | run_test "Execute access with rox flag on file" \
170 | "./landrun --log-level debug --rox /usr --ro /lib --ro /lib64 --rox $EXEC_DIR/test.sh -- $EXEC_DIR/test.sh" \
171 | 0
172 |
173 | run_test "Execute access with rox flag on a file that is executable in same directory that one is allowed" \
174 | "./landrun --log-level debug --rox /usr --ro /lib --ro /lib64 --rox $EXEC_DIR/test.sh -- $EXEC_DIR/test2.sh" \
175 | 1
176 |
177 | run_test "Execute a file with --add-exec flag" \
178 | "./landrun --log-level debug --add-exec --rox /usr --ro /lib --ro /lib64 --rox $EXEC_DIR/test.sh -- $EXEC_DIR/test2.sh" \
179 | 0
180 |
181 | run_test "Execute a file with --add-exec and --ldd flag" \
182 | "./landrun --log-level debug --add-exec --ldd -- /usr/bin/true" \
183 | 0
184 |
185 |
186 | run_test "No execute access with just ro flag" \
187 | "./landrun --log-level debug --rox /usr --ro /lib --ro /lib64 --ro $EXEC_DIR -- $EXEC_DIR/test.sh" \
188 | 1
189 |
190 | run_test "Execute access in read-write directory" \
191 | "./landrun --log-level debug --rox /usr --ro /lib --ro /lib64 --rwx $RW_DIR -- $RW_DIR/rw_script.sh" \
192 | 0
193 |
194 | run_test "No execute access in read-write directory without rwx" \
195 | "./landrun --log-level debug --rox /usr --ro /lib --ro /lib64 --rw $RW_DIR -- $RW_DIR/rw_script.sh" \
196 | 1
197 |
198 | # Directory traversal tests
199 | run_test "Directory traversal with root access" \
200 | "./landrun --log-level debug --rox / -- ls /usr" \
201 | 0
202 |
203 | run_test "Deep directory traversal" \
204 | "./landrun --log-level debug --rox / -- ls $NESTED_DIR" \
205 | 0
206 |
207 | # Multiple paths and complex specifications
208 | run_test "Multiple read paths" \
209 | "./landrun --log-level debug --rox /usr --ro /lib --ro /lib64 --ro $RO_DIR --ro $NESTED_DIR -- cat $NESTED_DIR/test.txt" \
210 | 0
211 |
212 | run_test "Comma-separated paths" \
213 | "./landrun --log-level debug --rox /usr --ro /lib,/lib64,$RO_DIR -- cat $RO_DIR/test.txt" \
214 | 0
215 |
216 | # System command tests
217 | run_test "Simple system command" \
218 | "./landrun --log-level debug --rox /usr --ro /etc -- whoami" \
219 | 0
220 |
221 | run_test "System command with arguments" \
222 | "./landrun --log-level debug --rox / -- ls -la /usr/bin" \
223 | 0
224 |
225 | # Edge cases
226 | run_test "Non-existent read-only path" \
227 | "./landrun --log-level debug --ro /usr --ro /lib --ro /lib64 --ro /nonexistent/path -- ls" \
228 | 1
229 |
230 | run_test "No configuration" \
231 | "./landrun --log-level debug -- ls /" \
232 | 1
233 |
234 | # Process creation and redirection tests
235 | run_test "Process creation with pipe" \
236 | "./landrun --log-level debug --rox / -- bash -c 'ls /usr | grep bin'" \
237 | 0
238 |
239 | run_test "File redirection" \
240 | "./landrun --log-level debug --rox / --rw $RW_DIR -- bash -c 'ls /usr > $RW_DIR/output.txt && cat $RW_DIR/output.txt'" \
241 | 0
242 |
243 | # Network restrictions tests (if kernel supports it)
244 | run_test "TCP connection without permission" \
245 | "./landrun --log-level debug --rox /usr --ro / -- curl -s --connect-timeout 2 https://example.com" \
246 | 7
247 |
248 | run_test "TCP connection with permission" \
249 | "./landrun --log-level debug --rox /usr --ro / --connect-tcp 443 -- curl -s --connect-timeout 2 https://example.com" \
250 | 0
251 |
252 | # Environment isolation tests
253 | export TEST_ENV_VAR="test_value_123"
254 | run_test "Environment isolation" \
255 | "./landrun --log-level debug --rox /usr --ro / -- bash -c 'echo \$TEST_ENV_VAR'" \
256 | 0
257 |
258 | run_test "Environment isolation (no variables should be passed)" \
259 | "./landrun --log-level debug --rox /usr --ro / -- bash -c '[[ -z \$TEST_ENV_VAR ]] && echo \"No env var\" || echo \$TEST_ENV_VAR'" \
260 | 0
261 |
262 | run_test "Passing specific environment variable" \
263 | "./landrun --log-level debug --rox /usr --ro / --env TEST_ENV_VAR -- bash -c 'echo \$TEST_ENV_VAR | grep \"test_value_123\"'" \
264 | 0
265 |
266 | run_test "Passing custom environment variable" \
267 | "./landrun --log-level debug --rox /usr --ro / --env CUSTOM_VAR=custom_value -- bash -c 'echo \$CUSTOM_VAR | grep \"custom_value\"'" \
268 | 0
269 |
270 | # Combining different permission types
271 | run_test "Mixed permissions" \
272 | "./landrun --log-level debug --rox /usr --ro /lib --ro /lib64 --rox $EXEC_DIR --rwx $RW_DIR -- bash -c '$EXEC_DIR/test.sh > $RW_DIR/output.txt && cat $RW_DIR/output.txt'" \
273 | 0
274 |
275 | # Specific regression tests for bugs we fixed
276 | run_test "Root path traversal regression test" \
277 | "./landrun --log-level debug --rox /usr -- /usr/bin/ls /usr" \
278 | 0
279 |
280 | run_test "Execute from read-only paths regression test" \
281 | "./landrun --log-level debug --rox /usr --ro /usr/bin -- /usr/bin/id" \
282 | 0
283 |
284 | run_test "Unrestricted filesystem access" \
285 | "./landrun --log-level debug --unrestricted-filesystem ls /usr" \
286 | 0
287 |
288 | run_test "Unrestricted network access" \
289 | "./landrun --log-level debug --unrestricted-network --rox /usr --ro /etc -- curl -s --connect-timeout 2 https://example.com" \
290 | 0
291 |
292 | run_test "Restricted filesystem access" \
293 | "./landrun --log-level debug ls /usr" \
294 | 1
295 |
296 | run_test "Restricted network access" \
297 | "./landrun --log-level debug --rox /usr --ro /etc -- curl -s --connect-timeout 2 https://example.com" \
298 | 7
299 |
300 |
301 | # Cleanup
302 | print_status "Cleaning up..."
303 | rm -rf "$TEST_DIR"
304 | if [ "$KEEP_BINARY" = false ] && [ "$USE_SYSTEM_BINARY" = false ]; then
305 | rm -f landrun
306 | fi
307 |
308 | print_success "All tests completed!"
309 |
--------------------------------------------------------------------------------