Limit wireproxy's permissions with landlock (#108)

* Limit wireproxy's permissions with landlock

* Show better debug message

* Fix crash when info is null

* Fix crash when landlock ABI is outdated

* remove /dev/std{in,out,err} from landlock restriction
This commit is contained in:
pufferfish 2024-04-13 02:38:48 +01:00 committed by GitHub
parent eccf83a0cf
commit a6797166eb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 108 additions and 20 deletions

View file

@ -3,11 +3,14 @@ package main
import (
"context"
"fmt"
"github.com/landlock-lsm/go-landlock/landlock"
"log"
"net"
"net/http"
"os"
"os/exec"
"os/signal"
"strconv"
"syscall"
"github.com/akamensky/argparse"
@ -21,22 +24,22 @@ const daemonProcess = "daemon-process"
var version = "1.0.8-dev"
// attempts to pledge and panic if it fails
// this does nothing on non-OpenBSD systems
func pledgeOrPanic(promises string) {
err := protect.Pledge(promises)
func panicIfError(err error) {
if err != nil {
log.Fatal(err)
}
}
// attempts to pledge and panic if it fails
// this does nothing on non-OpenBSD systems
func pledgeOrPanic(promises string) {
panicIfError(protect.Pledge(promises))
}
// attempts to unveil and panic if it fails
// this does nothing on non-OpenBSD systems
func unveilOrPanic(path string, flags string) {
err := protect.Unveil(path, flags)
if err != nil {
log.Fatal(err)
}
panicIfError(protect.Unveil(path, flags))
}
// get the executable path via syscalls or infer it from argv
@ -48,6 +51,91 @@ func executablePath() string {
return programPath
}
func lock(stage string) {
switch stage {
case "boot":
exePath := executablePath()
// OpenBSD
unveilOrPanic("/", "r")
unveilOrPanic(exePath, "x")
// only allow standard stdio operation, file reading, networking, and exec
// also remove unveil permission to lock unveil
pledgeOrPanic("stdio rpath inet dns proc exec")
// Linux
panicIfError(landlock.V1.BestEffort().RestrictPaths(
landlock.RODirs("/"),
))
case "boot-daemon":
case "read-config":
// OpenBSD
pledgeOrPanic("stdio rpath inet dns")
case "ready":
// no file access is allowed from now on, only networking
// OpenBSD
pledgeOrPanic("stdio inet dns")
// Linux
net.DefaultResolver.PreferGo = true // needed to lock down dependencies
panicIfError(landlock.V1.BestEffort().RestrictPaths(
landlock.ROFiles("/etc/resolv.conf"),
landlock.ROFiles("/dev/fd"),
landlock.ROFiles("/dev/zero"),
landlock.ROFiles("/dev/urandom"),
landlock.ROFiles("/etc/localtime"),
landlock.ROFiles("/proc/self/stat"),
landlock.ROFiles("/proc/self/status"),
landlock.ROFiles("/usr/share/locale"),
landlock.ROFiles("/proc/self/cmdline"),
landlock.ROFiles("/usr/share/zoneinfo"),
landlock.ROFiles("/proc/sys/kernel/version"),
landlock.ROFiles("/proc/sys/kernel/ngroups_max"),
landlock.ROFiles("/proc/sys/kernel/cap_last_cap"),
landlock.ROFiles("/proc/sys/vm/overcommit_memory"),
landlock.RWFiles("/dev/log"),
landlock.RWFiles("/dev/null"),
landlock.RWFiles("/dev/full"),
landlock.RWFiles("/proc/self/fd"),
))
default:
panic("invalid stage")
}
}
func extractPort(addr string) uint16 {
_, portStr, err := net.SplitHostPort(addr)
if err != nil {
panic(fmt.Errorf("failed to extract port from %s: %w", addr, err))
}
port, err := strconv.Atoi(portStr)
if err != nil {
panic(fmt.Errorf("failed to extract port from %s: %w", addr, err))
}
return uint16(port)
}
func lockNetwork(sections []wireproxy.RoutineSpawner, infoAddr *string) {
var rules []landlock.Rule
if infoAddr != nil && *infoAddr != "" {
rules = append(rules, landlock.BindTCP(extractPort(*infoAddr)))
}
for _, section := range sections {
switch section := section.(type) {
case *wireproxy.TCPServerTunnelConfig:
rules = append(rules, landlock.ConnectTCP(extractPort(section.Target)))
case *wireproxy.HTTPConfig:
rules = append(rules, landlock.BindTCP(extractPort(section.BindAddress)))
case *wireproxy.TCPClientTunnelConfig:
rules = append(rules, landlock.ConnectTCP(uint16(section.BindAddress.Port)))
case *wireproxy.Socks5Config:
rules = append(rules, landlock.BindTCP(extractPort(section.BindAddress)))
}
}
panicIfError(landlock.V4.BestEffort().RestrictNet(rules...))
}
func main() {
s := make(chan os.Signal, 1)
signal.Notify(s, syscall.SIGINT, syscall.SIGQUIT)
@ -59,18 +147,12 @@ func main() {
}()
exePath := executablePath()
unveilOrPanic("/", "r")
unveilOrPanic(exePath, "x")
// only allow standard stdio operation, file reading, networking, and exec
// also remove unveil permission to lock unveil
pledgeOrPanic("stdio rpath inet dns proc exec")
lock("boot")
isDaemonProcess := len(os.Args) > 1 && os.Args[1] == daemonProcess
args := os.Args
if isDaemonProcess {
// remove proc and exec if they are not needed
pledgeOrPanic("stdio rpath inet dns")
lock("boot-daemon")
args = []string{args[0]}
args = append(args, os.Args[2:]...)
}
@ -100,8 +182,7 @@ func main() {
}
if !*daemon {
// remove proc and exec if they are not needed
pledgeOrPanic("stdio rpath inet dns")
lock("read-config")
}
conf, err := wireproxy.ParseConfig(*config)
@ -114,6 +195,8 @@ func main() {
return
}
lockNetwork(conf.Routines, info)
if isDaemonProcess {
os.Stdout, _ = os.Open(os.DevNull)
os.Stderr, _ = os.Open(os.DevNull)
@ -139,8 +222,7 @@ func main() {
logLevel = device.LogLevelSilent
}
// no file access is allowed from now on, only networking
pledgeOrPanic("stdio inet dns")
lock("ready")
tun, err := wireproxy.StartWireguard(conf.Device, logLevel)
if err != nil {