diff --git a/cmd/wireproxy/main.go b/cmd/wireproxy/main.go index 9b10dfb..a4e6a8d 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,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 { 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=