Compare commits

..

No commits in common. "master" and "v1.0" have entirely different histories.

15 changed files with 112 additions and 331 deletions

4
.gitignore vendored
View file

@ -1,3 +1 @@
port80 example/example
port80fg
port80and443

View file

@ -1,4 +1,4 @@
Copyright (C) 2022 Marius Schellenberger Copyright (C) 2016 Marius Schellenberger
All rights reserved. All rights reserved.
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without

View file

@ -1,5 +1,3 @@
# godrop - drop privileges # godrop - drop privileges
Godrop is a simple library to drop privileges on Linux and OpenBSD. Godrop is a simple library to drop privileges on linux maschines.
See the examples directory on how to use the `Drop` and `MultiDrop` functions.

View file

@ -2,12 +2,11 @@ package main
import ( import (
"fmt" "fmt"
"log"
"net" "net"
"net/http" "net/http"
"os" "os"
"git.giftfish.de/ston1th/godrop/v2" "git.giftfish.de/ston1th/godrop"
) )
func main() { func main() {
@ -17,16 +16,16 @@ func main() {
} }
err := godrop.Drop(cfg, func() (net.Listener, error) { return net.Listen("tcp", ":80") }) err := godrop.Drop(cfg, func() (net.Listener, error) { return net.Listen("tcp", ":80") })
if err != nil { if err != nil {
log.Fatal(err) fmt.Println(err)
} }
l, err := godrop.GetListener() l, err := godrop.GetListener()
if err != nil { if err != nil {
log.Fatal("Failed to listen on FD 3:", err) fmt.Println("Failed to listen on FD 3:", err)
os.Exit(1)
} }
_, port, _ := net.SplitHostPort(l.Addr().String())
http.Serve(l, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 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()) fmt.Fprintf(w, "pid %d\nuid %d\ngid %d", os.Getpid(), os.Getuid(), os.Getgid())
})) }))
} }

View file

@ -1,54 +0,0 @@
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())
}))
}

View file

@ -1,35 +0,0 @@
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())
}))
}

5
go.mod
View file

@ -1,5 +0,0 @@
module git.giftfish.de/ston1th/godrop/v2
go 1.19
require golang.org/x/sys v0.24.0

2
go.sum
View file

@ -1,2 +0,0 @@
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=

173
godrop.go
View file

@ -1,76 +1,129 @@
// 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 package godrop
import ( import (
"errors" "errors"
"fmt" "io/ioutil"
"net" "net"
"os" "os"
"os/exec" "os/exec"
"os/signal" "regexp"
"strconv"
"syscall" "syscall"
) )
// Config represents the drop config 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")
}
type Config struct { type Config struct {
// User is the user to drop privileges to.
User string User string
// Group is the group to drop privileges to.
Group string 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 Chroot string
// Set to true, to run the process in the foreground.
Foreground bool
} }
// Drop will spawn a new process and hand over the listening socket file descriptor // Drop will spawn a new process, hand over the listening socket file descriptor and terminate itself after
func Drop(c Config, f func() (net.Listener, error)) error { func Drop(c Config, f func() (net.Listener, error)) error {
return MultiDrop(c, func() ([]net.Listener, error) { uid, err := UID(c.User)
l, err := f() if err != nil {
return []net.Listener{l}, err 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
} }
// MultiDrop will spawn a new process and hand over the all listening sockets 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")
}
// MultiDrop will spawn a new process, hand over the all listening sockets and terminate itself after
func MultiDrop(c Config, f func() ([]net.Listener, error)) error { func MultiDrop(c Config, f func() ([]net.Listener, error)) error {
uid, err := userID(c.User) uid, err := UID(c.User)
if err != nil { if err != nil {
return errors.New("godrop: " + err.Error()) return err
} }
if uid == 0 { gid, err := GID(c.Group)
return fmt.Errorf("godrop: you can't drop privileges to uid 0 (%s)", c.User)
}
gid, err := groupID(c.Group)
if err != nil { if err != nil {
return errors.New("godrop: " + err.Error()) return err
} }
switch os.Getuid() { switch os.Getuid() {
case 0: case 0:
cmd := exec.Command(os.Args[0], os.Args[1:]...) cmd := exec.Command(os.Args[0], os.Args[1:]...)
ln, err := f() ln, err := f()
if err != nil { if err != nil {
return errors.New("godrop: " + err.Error()) return err
} }
for i, v := range ln { for _, v := range ln {
var f *os.File l, ok := v.(*net.TCPListener)
switch l := v.(type) { if !ok {
case *net.TCPListener: return errors.New("godrop: interface conversion failed")
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 { if err != nil {
return fmt.Errorf("godrop: index %d %s", i, err) return err
} }
cmd.ExtraFiles = append(cmd.ExtraFiles, f) cmd.ExtraFiles = append(cmd.ExtraFiles, f)
} }
@ -84,54 +137,24 @@ func MultiDrop(c Config, f func() ([]net.Listener, error)) error {
Setsid: true, Setsid: true,
} }
if c.Foreground {
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
}
if err := cmd.Start(); err != nil { if err := cmd.Start(); err != nil {
return errors.New("godrop: " + err.Error()) return err
} }
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() cmd.Process.Release()
os.Exit(0) os.Exit(0)
case uid: case uid:
return nil return nil
} }
return errors.New("godrop: dropping priviledges failed") return errors.New("godrop: droping priviledges failed")
} }
// GetListener returns the listener socket of file descriptor 3 // GetListener returns the listener socket of file descriptor 3
func GetListener() (net.Listener, error) { func GetListener() (net.Listener, error) {
return GetListenerFd(3) return net.FileListener(os.NewFile(3, "[socket]"))
} }
// GetListenerFd returns the listener socket of the given file descriptor // GetListenerFd returns the listener socket of the given file descriptor
func GetListenerFd(fd int) (net.Listener, error) { func GetListenerFd(fd int) (net.Listener, error) {
if fd < 3 { return net.FileListener(os.NewFile(uintptr(fd), "[socket]"))
return nil, errors.New("godrop: fd is less than 3")
}
f := os.NewFile(uintptr(fd), "")
defer f.Close()
return net.FileListener(f)
} }

View file

@ -1,11 +1,9 @@
// Copyright (C) 2022 Marius Schellenberger
package godrop package godrop
import "testing" import "testing"
func TestUserID(t *testing.T) { func TestUID(t *testing.T) {
uid, err := userID("root") uid, err := UID("root")
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
@ -14,8 +12,8 @@ func TestUserID(t *testing.T) {
} }
} }
func TestGroupID(t *testing.T) { func TestGID(t *testing.T) {
gid, err := groupID("root") gid, err := GID("root")
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }

View file

@ -1,35 +0,0 @@
// 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)
}

View file

@ -1,15 +0,0 @@
// 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 }

View file

@ -1,44 +0,0 @@
// 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
}

View file

@ -1,12 +0,0 @@
// 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 }

View file

@ -1,33 +0,0 @@
// 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
}