From 728438cc334cfe54fbec2d1a8430ac32f76479a5 Mon Sep 17 00:00:00 2001 From: pufferffish Date: Sat, 13 Apr 2024 02:25:52 +0100 Subject: [PATCH 1/5] Limit wireproxy's permissions with landlock --- cmd/wireproxy/main.go | 125 +++++++++++++++++++++++++++++++++++------- go.mod | 2 + go.sum | 4 ++ 3 files changed, 111 insertions(+), 20 deletions(-) diff --git a/cmd/wireproxy/main.go b/cmd/wireproxy/main.go index 9b10dfb..c5adc3e 100644 --- a/cmd/wireproxy/main.go +++ b/cmd/wireproxy/main.go @@ -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,94 @@ 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.V4.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.V4.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("/dev/stdin"), + landlock.RWFiles("/dev/stdout"), + landlock.RWFiles("/dev/stderr"), + landlock.RWFiles("/proc/self/fd"), + )) + default: + panic("invalid stage") + } +} + +func extractPort(addr string) uint16 { + _, portStr, err := net.SplitHostPort(addr) + if err != nil { + panic(err) + } + + port, err := strconv.Atoi(portStr) + if err != nil { + panic(err) + } + + return uint16(port) +} + +func lockNetwork(sections []wireproxy.RoutineSpawner, infoAddr *string) { + var rules []landlock.Rule + if infoAddr != nil { + 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.RestrictNet(rules...)) +} + func main() { s := make(chan os.Signal, 1) signal.Notify(s, syscall.SIGINT, syscall.SIGQUIT) @@ -59,18 +150,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 +185,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 +198,8 @@ func main() { return } + lockNetwork(conf.Routines, info) + if isDaemonProcess { os.Stdout, _ = os.Open(os.DevNull) os.Stderr, _ = os.Open(os.DevNull) @@ -139,8 +225,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 { diff --git a/go.mod b/go.mod index ab52783..73c8a6d 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( require ( github.com/google/btree v1.1.2 // indirect + github.com/landlock-lsm/go-landlock v0.0.0-20240216195629-efb66220540a // indirect github.com/sourcegraph/conc v0.3.0 // indirect golang.org/x/crypto v0.19.0 // indirect golang.org/x/net v0.21.0 // indirect @@ -22,4 +23,5 @@ require ( golang.org/x/time v0.5.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 // indirect + kernel.org/pub/linux/libs/security/libcap/psx v1.2.69 // indirect ) diff --git a/go.sum b/go.sum index 949c441..4799ba6 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/landlock-lsm/go-landlock v0.0.0-20240216195629-efb66220540a h1:dz+a1MiMQksVhejeZwqJuzPawYQBwug74J8PPtkLl9U= +github.com/landlock-lsm/go-landlock v0.0.0-20240216195629-efb66220540a/go.mod h1:1NY/VPO8xm3hXw3f+M65z+PJDLUaZA5cu7OfanxoUzY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= @@ -33,5 +35,7 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ= gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY= +kernel.org/pub/linux/libs/security/libcap/psx v1.2.69 h1:IdrOs1ZgwGw5CI+BH6GgVVlOt+LAXoPyh7enr8lfaXs= +kernel.org/pub/linux/libs/security/libcap/psx v1.2.69/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24= suah.dev/protect v1.2.3 h1:aHeoNwZ9YPp64hrYaN0g0djNE1eRujgH63CrfRrUKdc= suah.dev/protect v1.2.3/go.mod h1:n1R3XIbsnryKX7C1PO88i5Wgo0v8OTXm9K9FIKt4rfs= From 1e0391c281206c8d69a5cb011368cfafe2d8dd26 Mon Sep 17 00:00:00 2001 From: pufferffish Date: Sat, 13 Apr 2024 02:28:52 +0100 Subject: [PATCH 2/5] Show better debug message --- cmd/wireproxy/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/wireproxy/main.go b/cmd/wireproxy/main.go index c5adc3e..de2296f 100644 --- a/cmd/wireproxy/main.go +++ b/cmd/wireproxy/main.go @@ -106,12 +106,12 @@ func lock(stage string) { func extractPort(addr string) uint16 { _, portStr, err := net.SplitHostPort(addr) if err != nil { - panic(err) + panic(fmt.Errorf("failed to extract port from %s: %w", addr, err)) } port, err := strconv.Atoi(portStr) if err != nil { - panic(err) + panic(fmt.Errorf("failed to extract port from %s: %w", addr, err)) } return uint16(port) From f54319b736c74af2d90c773ace284775adf5d8d5 Mon Sep 17 00:00:00 2001 From: pufferffish Date: Sat, 13 Apr 2024 02:30:49 +0100 Subject: [PATCH 3/5] Fix crash when info is null --- cmd/wireproxy/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/wireproxy/main.go b/cmd/wireproxy/main.go index de2296f..8581b77 100644 --- a/cmd/wireproxy/main.go +++ b/cmd/wireproxy/main.go @@ -119,7 +119,7 @@ func extractPort(addr string) uint16 { func lockNetwork(sections []wireproxy.RoutineSpawner, infoAddr *string) { var rules []landlock.Rule - if infoAddr != nil { + if infoAddr != nil && *infoAddr != "" { rules = append(rules, landlock.BindTCP(extractPort(*infoAddr))) } From d40aa7f7255b3bef68cd248be03763735f392d66 Mon Sep 17 00:00:00 2001 From: pufferffish Date: Sat, 13 Apr 2024 02:33:43 +0100 Subject: [PATCH 4/5] Fix crash when landlock ABI is outdated --- cmd/wireproxy/main.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/wireproxy/main.go b/cmd/wireproxy/main.go index 8581b77..86f7a12 100644 --- a/cmd/wireproxy/main.go +++ b/cmd/wireproxy/main.go @@ -62,7 +62,7 @@ func lock(stage string) { // also remove unveil permission to lock unveil pledgeOrPanic("stdio rpath inet dns proc exec") // Linux - panicIfError(landlock.V4.BestEffort().RestrictPaths( + panicIfError(landlock.V1.BestEffort().RestrictPaths( landlock.RODirs("/"), )) case "boot-daemon": @@ -75,7 +75,7 @@ func lock(stage string) { pledgeOrPanic("stdio inet dns") // Linux net.DefaultResolver.PreferGo = true // needed to lock down dependencies - panicIfError(landlock.V4.BestEffort().RestrictPaths( + panicIfError(landlock.V1.BestEffort().RestrictPaths( landlock.ROFiles("/etc/resolv.conf"), landlock.ROFiles("/dev/fd"), landlock.ROFiles("/dev/zero"), @@ -136,7 +136,7 @@ func lockNetwork(sections []wireproxy.RoutineSpawner, infoAddr *string) { } } - panicIfError(landlock.V4.RestrictNet(rules...)) + panicIfError(landlock.V4.BestEffort().RestrictNet(rules...)) } func main() { From 8ad53be223dbea0cc1368f73f9ca6ea772e618d8 Mon Sep 17 00:00:00 2001 From: pufferffish Date: Sat, 13 Apr 2024 02:35:48 +0100 Subject: [PATCH 5/5] remove /dev/std{in,out,err} from landlock restriction --- cmd/wireproxy/main.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/cmd/wireproxy/main.go b/cmd/wireproxy/main.go index 86f7a12..a4e6a8d 100644 --- a/cmd/wireproxy/main.go +++ b/cmd/wireproxy/main.go @@ -93,9 +93,6 @@ func lock(stage string) { landlock.RWFiles("/dev/log"), landlock.RWFiles("/dev/null"), landlock.RWFiles("/dev/full"), - landlock.RWFiles("/dev/stdin"), - landlock.RWFiles("/dev/stdout"), - landlock.RWFiles("/dev/stderr"), landlock.RWFiles("/proc/self/fd"), )) default: