diff --git a/.gitignore b/.gitignore index 528cd5b..af033d5 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -example/example +port80 +port80fg +port80and443 diff --git a/LICENSE b/LICENSE index 9e52d9b..aa39852 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (C) 2016 Marius Schellenberger +Copyright (C) 2022 Marius Schellenberger All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/README.md b/README.md index 6410577..bcd5479 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ # godrop - drop privileges -Godrop is a simple library to drop privileges on linux maschines. +Godrop is a simple library to drop privileges on Linux and OpenBSD. + +See the examples directory on how to use the `Drop` and `MultiDrop` functions. diff --git a/example/main.go b/example/port80.go similarity index 60% rename from example/main.go rename to example/port80.go index 2e4f848..9a67d27 100644 --- a/example/main.go +++ b/example/port80.go @@ -2,11 +2,12 @@ package main import ( "fmt" + "log" "net" "net/http" "os" - "git.giftfish.de/ston1th/godrop" + "git.giftfish.de/ston1th/godrop/v2" ) func main() { @@ -16,16 +17,16 @@ func main() { } err := godrop.Drop(cfg, func() (net.Listener, error) { return net.Listen("tcp", ":80") }) if err != nil { - fmt.Println(err) + log.Fatal(err) } l, err := godrop.GetListener() if err != nil { - fmt.Println("Failed to listen on FD 3:", err) - os.Exit(1) + log.Fatal("Failed to listen on FD 3:", err) } + _, port, _ := net.SplitHostPort(l.Addr().String()) http.Serve(l, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintf(w, "pid %d\nuid %d\ngid %d", os.Getpid(), os.Getuid(), os.Getgid()) + fmt.Fprintf(w, "port %s\npid %d\nuid %d\ngid %d", port, os.Getpid(), os.Getuid(), os.Getgid()) })) } diff --git a/example/port80and443.go b/example/port80and443.go new file mode 100644 index 0000000..f1ba7ce --- /dev/null +++ b/example/port80and443.go @@ -0,0 +1,54 @@ +package main + +import ( + "fmt" + "log" + "net" + "net/http" + "os" + + "git.giftfish.de/ston1th/godrop/v2" +) + +func main() { + cfg := godrop.Config{ + User: "nobody", + Group: "nobody", + Foreground: true, + } + err := godrop.MultiDrop(cfg, func() ([]net.Listener, error) { + l1, err := net.Listen("tcp", ":80") + if err != nil { + return nil, err + } + l2, err := net.Listen("tcp", ":443") + return []net.Listener{l1, l2}, err + }) + if err != nil { + log.Fatal(err) + } + + l1, err := godrop.GetListener() + if err != nil { + log.Fatal("Failed to listen on FD 3:", err) + } + _, port1, _ := net.SplitHostPort(l1.Addr().String()) + + l2, err := godrop.GetListenerFd(4) + if err != nil { + log.Fatal("Failed to listen on FD 4:", err) + } + _, port2, _ := net.SplitHostPort(l2.Addr().String()) + + fmt.Printf("port1 %s\nport2 %s\npid %d\nuid %d\ngid %d\n", port1, port2, os.Getpid(), os.Getuid(), os.Getgid()) + + go func() { + http.Serve(l1, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "port %s\npid %d\nuid %d\ngid %d", port1, os.Getpid(), os.Getuid(), os.Getgid()) + })) + }() + + http.Serve(l2, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "port %s\npid %d\nuid %d\ngid %d", port2, os.Getpid(), os.Getuid(), os.Getgid()) + })) +} diff --git a/example/port80fg.go b/example/port80fg.go new file mode 100644 index 0000000..c924719 --- /dev/null +++ b/example/port80fg.go @@ -0,0 +1,35 @@ +package main + +import ( + "fmt" + "log" + "net" + "net/http" + "os" + + "git.giftfish.de/ston1th/godrop/v2" +) + +func main() { + cfg := godrop.Config{ + User: "nobody", + Group: "nobody", + Foreground: true, + } + err := godrop.Drop(cfg, func() (net.Listener, error) { return net.Listen("tcp", ":80") }) + if err != nil { + log.Fatal(err) + } + + l, err := godrop.GetListener() + if err != nil { + log.Fatal("Failed to listen on FD 3:", err) + } + _, port, _ := net.SplitHostPort(l.Addr().String()) + + fmt.Printf("port %s\npid %d\nuid %d\ngid %d\n", port, os.Getpid(), os.Getuid(), os.Getgid()) + + http.Serve(l, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "port %s\npid %d\nuid %d\ngid %d", port, os.Getpid(), os.Getuid(), os.Getgid()) + })) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c939651 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module git.giftfish.de/ston1th/godrop/v2 + +go 1.19 + +require golang.org/x/sys v0.24.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d88e7bd --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/godrop.go b/godrop.go index d9a4531..37c7e75 100644 --- a/godrop.go +++ b/godrop.go @@ -1,129 +1,76 @@ +// Copyright (C) 2022 Marius Schellenberger + +//go:build go1.11 +// +build go1.11 + +// Package godrop provides a simple library to drop privileges on Linux and OpenBSD. package godrop import ( "errors" - "io/ioutil" + "fmt" "net" "os" "os/exec" - "regexp" - "strconv" + "os/signal" "syscall" ) -func readFile(name, file string) (id int, err error) { - id = -1 - if name == "" { - return - } - f, err := os.Open(file) - if err != nil { - return - } - b, err := ioutil.ReadAll(f) - if err != nil { - return - } - r := regexp.MustCompile(name + ":.*:(\\d+)") - m := r.FindAllStringSubmatch(string(b), 1) - if len(m) == 1 { - if len(m[0]) == 2 { - return strconv.Atoi(m[0][1]) - } - } - return -} - -// UID of user -func UID(user string) (int, error) { - return readFile(user, "/etc/passwd") -} - -// GID of group -func GID(group string) (int, error) { - return readFile(group, "/etc/group") -} - +// Config represents the drop config type Config struct { - User string - Group string + // User is the user to drop privileges to. + User string + // Group is the group to drop privileges to. + Group string + // Chroot is the directory to chroot into. Leave this emptry for no chroot. + // When compiling without cgo, make sure the chroot directory contains the /etc/passwd and /etc/group files. Chroot string + // Set to true, to run the process in the foreground. + Foreground bool } -// Drop will spawn a new process, hand over the listening socket file descriptor and terminate itself after +// Drop will spawn a new process and hand over the listening socket file descriptor func Drop(c Config, f func() (net.Listener, error)) error { - uid, err := UID(c.User) - if err != nil { - return err - } - gid, err := GID(c.Group) - if err != nil { - return err - } - switch os.Getuid() { - case 0: - ln, err := f() - if err != nil { - return err - } - - l, ok := ln.(*net.TCPListener) - if !ok { - return errors.New("godrop: interface conversion failed") - } - f, err := l.File() - if err != nil { - return err - } - - cmd := exec.Command(os.Args[0], os.Args[1:]...) - cmd.ExtraFiles = []*os.File{f} - cmd.SysProcAttr = &syscall.SysProcAttr{ - Chroot: c.Chroot, - Credential: &syscall.Credential{ - Uid: uint32(uid), - Gid: uint32(gid), - }, - Setsid: true, - } - - if err := cmd.Start(); err != nil { - return err - } - - cmd.Process.Release() - os.Exit(0) - case uid: - return nil - } - return errors.New("godrop: droping priviledges failed") + return MultiDrop(c, func() ([]net.Listener, error) { + l, err := f() + return []net.Listener{l}, err + }) } -// MultiDrop will spawn a new process, hand over the all listening sockets and terminate itself after +// MultiDrop will spawn a new process and hand over the all listening sockets func MultiDrop(c Config, f func() ([]net.Listener, error)) error { - uid, err := UID(c.User) + uid, err := userID(c.User) if err != nil { - return err + return errors.New("godrop: " + err.Error()) } - gid, err := GID(c.Group) + if uid == 0 { + return fmt.Errorf("godrop: you can't drop privileges to uid 0 (%s)", c.User) + } + gid, err := groupID(c.Group) if err != nil { - return err + return errors.New("godrop: " + err.Error()) } switch os.Getuid() { case 0: cmd := exec.Command(os.Args[0], os.Args[1:]...) ln, err := f() if err != nil { - return err + return errors.New("godrop: " + err.Error()) } - for _, v := range ln { - l, ok := v.(*net.TCPListener) - if !ok { - return errors.New("godrop: interface conversion failed") + for i, v := range ln { + var f *os.File + switch l := v.(type) { + case *net.TCPListener: + f, err = l.File() + l.Close() + case *net.UnixListener: + f, err = l.File() + l.Close() + default: + return fmt.Errorf("godrop: index %d listener is not type of either *net.TCPListener or *net.UnixListener", i) } - f, err := l.File() if err != nil { - return err + return fmt.Errorf("godrop: index %d %s", i, err) } cmd.ExtraFiles = append(cmd.ExtraFiles, f) } @@ -137,24 +84,54 @@ func MultiDrop(c Config, f func() ([]net.Listener, error)) error { Setsid: true, } - if err := cmd.Start(); err != nil { - return err + if c.Foreground { + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr } + if err := cmd.Start(); err != nil { + return errors.New("godrop: " + err.Error()) + } + + if c.Foreground { + go func() { + term := make(chan os.Signal) + signal.Notify(term, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) + sigs := make(chan os.Signal) + signal.Notify(sigs, syscall.SIGSTOP, syscall.SIGCONT, syscall.SIGUSR1, syscall.SIGUSR2) + for { + select { + case sig := <-term: + cmd.Process.Signal(sig) + return + case sig := <-sigs: + cmd.Process.Signal(sig) + } + } + }() + _ = cmd.Wait() + os.Exit(int(cmd.ProcessState.Sys().(syscall.WaitStatus))) + } cmd.Process.Release() os.Exit(0) case uid: return nil } - return errors.New("godrop: droping priviledges failed") + return errors.New("godrop: dropping priviledges failed") } // GetListener returns the listener socket of file descriptor 3 func GetListener() (net.Listener, error) { - return net.FileListener(os.NewFile(3, "[socket]")) + return GetListenerFd(3) } // GetListenerFd returns the listener socket of the given file descriptor func GetListenerFd(fd int) (net.Listener, error) { - return net.FileListener(os.NewFile(uintptr(fd), "[socket]")) + if fd < 3 { + return nil, errors.New("godrop: fd is less than 3") + } + f := os.NewFile(uintptr(fd), "") + defer f.Close() + return net.FileListener(f) } diff --git a/lookup.go b/lookup.go new file mode 100644 index 0000000..725dc82 --- /dev/null +++ b/lookup.go @@ -0,0 +1,35 @@ +// Copyright (C) 2022 Marius Schellenberger + +//go:build go1.11 +// +build go1.11 + +package godrop + +import ( + "os/user" + "strconv" +) + +func atoi(a string) (int, error) { + i, err := strconv.Atoi(a) + if err != nil { + return -1, err + } + return i, nil +} + +func userID(username string) (int, error) { + u, err := user.Lookup(username) + if err != nil { + return -1, err + } + return atoi(u.Uid) +} + +func groupID(name string) (int, error) { + g, err := user.LookupGroup(name) + if err != nil { + return -1, err + } + return atoi(g.Gid) +} diff --git a/godrop_test.go b/lookup_test.go similarity index 57% rename from godrop_test.go rename to lookup_test.go index 38b629d..49d2a78 100644 --- a/godrop_test.go +++ b/lookup_test.go @@ -1,9 +1,11 @@ +// Copyright (C) 2022 Marius Schellenberger + package godrop import "testing" -func TestUID(t *testing.T) { - uid, err := UID("root") +func TestUserID(t *testing.T) { + uid, err := userID("root") if err != nil { t.Error(err) } @@ -12,8 +14,8 @@ func TestUID(t *testing.T) { } } -func TestGID(t *testing.T) { - gid, err := GID("root") +func TestGroupID(t *testing.T) { + gid, err := groupID("root") if err != nil { t.Error(err) } diff --git a/pledge.go b/pledge.go new file mode 100644 index 0000000..8c97aba --- /dev/null +++ b/pledge.go @@ -0,0 +1,15 @@ +// Copyright (C) 2022 Marius Schellenberger + +//go:build !openbsd +// +build !openbsd + +package godrop + +// Pledge is currently only supported on OpenBSD. +func Pledge(promises, execpromises string) error { return nil } + +// PledgePromises is currently only supported on OpenBSD. +func PledgePromises(promises string) error { return nil } + +// PledgeExecPromises is currently only supported on OpenBSD. +func PledgeExecpromises(execpromises string) error { return nil } diff --git a/pledge_openbsd.go b/pledge_openbsd.go new file mode 100644 index 0000000..0af656a --- /dev/null +++ b/pledge_openbsd.go @@ -0,0 +1,44 @@ +// Copyright (C) 2022 Marius Schellenberger + +//go:build openbsd +// +build openbsd + +package godrop + +import ( + "errors" + "golang.org/x/sys/unix" +) + +// Pledge is a wrapper for x/sys/unix Pledge. +// +// See https://go.googlesource.com/sys/+/master/unix/openbsd_pledge.go for usage. +func Pledge(promises, execpromises string) (err error) { + err = unix.Pledge(promises, execpromises) + if err != nil { + err = errors.New("pledge: " + err.Error()) + } + return +} + +// PledgePromises is a wrapper for x/sys/unix PledgePromises. +// +// See https://go.googlesource.com/sys/+/master/unix/openbsd_pledge.go for usage. +func PledgePromises(promises string) (err error) { + err = unix.PledgePromises(promises) + if err != nil { + err = errors.New("pledge: " + err.Error()) + } + return +} + +// PledgeExecpromises is a wrapper for x/sys/unix PledgeExecpromises. +// +// See https://go.googlesource.com/sys/+/master/unix/openbsd_pledge.go for usage. +func PledgeExecpromises(execpromises string) (err error) { + err = unix.PledgeExecpromises(execpromises) + if err != nil { + err = errors.New("pledge: " + err.Error()) + } + return +} diff --git a/unveil.go b/unveil.go new file mode 100644 index 0000000..412851b --- /dev/null +++ b/unveil.go @@ -0,0 +1,12 @@ +// Copyright (C) 2022 Marius Schellenberger + +//go:build !openbsd +// +build !openbsd + +package godrop + +// Unveil is currently only supported on OpenBSD. +func Unveil(path, flags string) error { return nil } + +// UnveilBlock is currently only supported on OpenBSD. +func UnveilBlock() error { return nil } diff --git a/unveil_openbsd.go b/unveil_openbsd.go new file mode 100644 index 0000000..f1e4bbc --- /dev/null +++ b/unveil_openbsd.go @@ -0,0 +1,33 @@ +// Copyright (C) 2022 Marius Schellenberger + +//go:build openbsd +// +build openbsd + +package godrop + +import ( + "errors" + "golang.org/x/sys/unix" +) + +// Unveil is a wrapper for x/sys/unix Unveil. +// +// See https://go.googlesource.com/sys/+/master/unix/openbsd_unveil.go for usage. +func Unveil(path, flags string) (err error) { + err = unix.Unveil(path, flags) + if err != nil { + err = errors.New("unveil: " + err.Error()) + } + return +} + +// UnveilBlock is a wrapper for x/sys/unix UnveilBlock. +// +// See https://go.googlesource.com/sys/+/master/unix/openbsd_unveil.go for usage. +func UnveilBlock() (err error) { + err = unix.UnveilBlock() + if err != nil { + err = errors.New("unveil: " + err.Error()) + } + return +}