├── .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 | landrun demo 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 | --------------------------------------------------------------------------------