landdown

Simple Sandboxing for shell scripts.

git clone git://mccd.space/landdown

main.go (3642B)

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