Skip to content

Commit

Permalink
all: support setgid, setuid on unix
Browse files Browse the repository at this point in the history
  • Loading branch information
ainar-g committed Jun 4, 2021
1 parent 3b87478 commit ba95d4a
Show file tree
Hide file tree
Showing 11 changed files with 192 additions and 30 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ and this project adheres to

### Added

- The ability to change group and user ID on startup on Unix ([#2763]).
- Experimental OpenBSD support for AMD64 and 64-bit ARM CPUs ([#2439]).
- Support for custom port in DNS-over-HTTPS profiles for Apple's devices
([#3172]).
Expand Down Expand Up @@ -56,6 +57,7 @@ released by then.
[#2439]: https://github.com/AdguardTeam/AdGuardHome/issues/2439
[#2441]: https://github.com/AdguardTeam/AdGuardHome/issues/2441
[#2443]: https://github.com/AdguardTeam/AdGuardHome/issues/2443
[#2763]: https://github.com/AdguardTeam/AdGuardHome/issues/2763
[#3136]: https://github.com/AdguardTeam/AdGuardHome/issues/3136
[#3172]: https://github.com/AdguardTeam/AdGuardHome/issues/3172
[#3184]: https://github.com/AdguardTeam/AdGuardHome/issues/3184
Expand Down
29 changes: 21 additions & 8 deletions internal/aghos/os.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,30 @@ package aghos
import (
"fmt"
"os/exec"
"runtime"
"syscall"

"github.com/AdguardTeam/golibs/errors"
)

// ErrUnsupported is returned when the functionality is unsupported on the
// current operating system.
//
// TODO(a.garipov): Make a structured error and use it everywhere instead of
// a bunch of fmt.Errorf and all that.
const ErrUnsupported errors.Error = "unsupported"
// UnsupportedError is returned by functions and methods when a particular
// operation Op cannot be performed on the current OS.
type UnsupportedError struct {
Op string
OS string
}

// Error implements the error interface for *UnsupportedError.
func (err *UnsupportedError) Error() (msg string) {
return fmt.Sprintf("%s is unsupported on %s", err.Op, err.OS)
}

// Unsupported is a helper that returns an *UnsupportedError with the Op field
// set to op and the OS field set to the current OS.
func Unsupported(op string) (err error) {
return &UnsupportedError{
Op: op,
OS: runtime.GOOS,
}
}

// CanBindPrivilegedPorts checks if current process can bind to privileged
// ports.
Expand Down
5 changes: 2 additions & 3 deletions internal/aghos/os_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
package aghos

import (
"fmt"
"syscall"

"golang.org/x/sys/windows"
Expand All @@ -16,7 +15,7 @@ func canBindPrivilegedPorts() (can bool, err error) {
}

func setRlimit(val uint64) (err error) {
return ErrUnsupported
return Unsupported("setrlimit")
}

func haveAdminRights() (bool, error) {
Expand All @@ -41,7 +40,7 @@ func haveAdminRights() (bool, error) {
}

func sendProcessSignal(pid int, sig syscall.Signal) error {
return fmt.Errorf("not supported on Windows")
return Unsupported("kill")
}

func isOpenWrt() (ok bool) {
Expand Down
11 changes: 11 additions & 0 deletions internal/aghos/user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package aghos

// SetGroup sets the effective group ID of the calling process.
func SetGroup(groupName string) (err error) {
return setGroup(groupName)
}

// SetUser sets the effective user ID of the calling process.
func SetUser(userName string) (err error) {
return setUser(userName)
}
50 changes: 50 additions & 0 deletions internal/aghos/user_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// +build darwin freebsd linux netbsd openbsd

//go:build darwin || freebsd || linux || netbsd || openbsd

package aghos

import (
"fmt"
"os/user"
"strconv"
"syscall"
)

func setGroup(groupName string) (err error) {
g, err := user.LookupGroup(groupName)
if err != nil {
return fmt.Errorf("looking up group: %w", err)
}

gid, err := strconv.Atoi(g.Gid)
if err != nil {
return fmt.Errorf("parsing gid: %w", err)
}

err = syscall.Setgid(gid)
if err != nil {
return fmt.Errorf("setting gid: %w", err)
}

return nil
}

func setUser(userName string) (err error) {
u, err := user.Lookup(userName)
if err != nil {
return fmt.Errorf("looking up user: %w", err)
}

uid, err := strconv.Atoi(u.Uid)
if err != nil {
return fmt.Errorf("parsing uid: %w", err)
}

err = syscall.Setuid(uid)
if err != nil {
return fmt.Errorf("setting uid: %w", err)
}

return nil
}
16 changes: 16 additions & 0 deletions internal/aghos/user_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// +build windows

//go:build windows

package aghos

// TODO(a.garipov): Think of a way to implement these. Perhaps by using
// syscall.CreateProcessAsUser or something from the golang.org/x/sys module.

func setGroup(_ string) (err error) {
return Unsupported("setgid")
}

func setUser(_ string) (err error) {
return Unsupported("setuid")
}
3 changes: 2 additions & 1 deletion internal/dhcpd/checkother.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"time"

"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/insomniacslk/dhcp/dhcpv4"
Expand Down Expand Up @@ -41,7 +42,7 @@ func CheckIfOtherDHCPServersPresentV4(ifaceName string) (ok bool, err error) {
// TODO(a.garipov): Find out what this is about. Perhaps this
// information is outdated or at least incomplete.
if runtime.GOOS == "darwin" {
return false, fmt.Errorf("can't find DHCP server: not supported on macOS")
return false, aghos.Unsupported("CheckIfOtherDHCPServersPresentV4")
}

srcIP := ifaceIPNet[0]
Expand Down
10 changes: 7 additions & 3 deletions internal/dhcpd/checkother_windows.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
// +build windows

//go:build windows

package dhcpd

import "fmt"
import "github.com/AdguardTeam/AdGuardHome/internal/aghos"

func CheckIfOtherDHCPServersPresentV4(ifaceName string) (bool, error) {
return false, fmt.Errorf("not supported")
return false, aghos.Unsupported("CheckIfOtherDHCPServersPresentV4")
}

func CheckIfOtherDHCPServersPresentV6(ifaceName string) (bool, error) {
return false, fmt.Errorf("not supported")
return false, aghos.Unsupported("CheckIfOtherDHCPServersPresentV6")
}
10 changes: 7 additions & 3 deletions internal/dhcpd/os_windows.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
// +build windows

//go:build windows

package dhcpd

import (
"net"

"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"golang.org/x/net/ipv4"
)

// Create a socket for receiving broadcast packets
func newBroadcastPacketConn(bindAddr net.IP, port int, ifname string) (*ipv4.PacketConn, error) {
return nil, errors.Error("newBroadcastPacketConn(): not supported on Windows")
func newBroadcastPacketConn(_ net.IP, _ int, _ string) (*ipv4.PacketConn, error) {
return nil, aghos.Unsupported("newBroadcastPacketConn")
}
23 changes: 19 additions & 4 deletions internal/home/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ type logSettings struct {
Verbose bool `yaml:"verbose"` // If true, verbose logging is enabled
}

// osConfig contains OS-related configuration.
type osConfig struct {
Group string `yaml:"group"`
User string `yaml:"user"`
}

// configuration is loaded from YAML
// field ordering is important -- yaml fields will mirror ordering from here
type configuration struct {
Expand All @@ -52,10 +58,16 @@ type configuration struct {
// AuthBlockMin is the duration, in minutes, of the block of new login
// attempts after AuthAttempts unsuccessful login attempts.
AuthBlockMin uint `yaml:"block_auth_min"`
ProxyURL string `yaml:"http_proxy"` // Proxy address for our HTTP client
Language string `yaml:"language"` // two-letter ISO 639-1 language code
RlimitNoFile uint64 `yaml:"rlimit_nofile"` // Maximum number of opened fd's per process (0: default)
DebugPProf bool `yaml:"debug_pprof"` // Enable pprof HTTP server on port 6060
ProxyURL string `yaml:"http_proxy"` // Proxy address for our HTTP client
Language string `yaml:"language"` // two-letter ISO 639-1 language code

// RlimitNoFile is the maximum number of opened fd's per process. Zero
// means use the default value.
//
// TODO(a.garipov): Make a migration and move this to osConfig.
RlimitNoFile uint64 `yaml:"rlimit_nofile"`

DebugPProf bool `yaml:"debug_pprof"` // Enable pprof HTTP server on port 6060

// TTL for a web session (in hours)
// An active session is automatically refreshed once a day.
Expand All @@ -75,6 +87,8 @@ type configuration struct {

logSettings `yaml:",inline"`

OSConfig *osConfig `yaml:"os"`

sync.RWMutex `yaml:"-"`

SchemaVersion int `yaml:"schema_version"` // keeping last so that users will be less tempted to change it -- used when upgrading between versions
Expand Down Expand Up @@ -184,6 +198,7 @@ var config = configuration{
LogMaxSize: 100,
LogMaxAge: 3,
},
OSConfig: &osConfig{},
SchemaVersion: currentSchemaVersion,
}

Expand Down
63 changes: 55 additions & 8 deletions internal/home/home.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ func Main(clientBuildFS fs.FS) {
// support OpenBSD currently. Either patch it to do so or make
// our own implementation of the service.System interface.
if runtime.GOOS == "openbsd" {
log.Fatal("service actions are not supported on openbsd")
log.Fatal("service actions are not supported on openbsd, see issue 3226")
}

handleServiceControlAction(args, clientBuildFS)
Expand Down Expand Up @@ -183,6 +183,57 @@ func setupContext(args options) {
Context.mux = http.NewServeMux()
}

// logIfUnsupported logs a formatted warning if the error is one of the
// unsupported errors and returns nil. If err is nil, logIfUnsupported returns
// nil. Otherise, it returns err.
func logIfUnsupported(msg string, err error) (outErr error) {
if unsupErr := (&aghos.UnsupportedError{}); errors.As(err, &unsupErr) {
log.Debug(msg, err)
} else if err != nil {
return err
}

return nil
}

// configureOS sets the OS-related configuration.
func configureOS(conf *configuration) (err error) {
if osConf := config.OSConfig; osConf != nil {
log.Info("%+v", osConf)
if osConf.Group != "" {
err = aghos.SetGroup(osConf.Group)
err = logIfUnsupported("warning: setting group", err)
if err != nil {
return fmt.Errorf("setting group: %w", err)
}

log.Info("group set to %s", osConf.Group)
}

if osConf.User != "" {
err = aghos.SetUser(osConf.User)
err = logIfUnsupported("warning: setting user", err)
if err != nil {
return fmt.Errorf("setting user: %w", err)
}

log.Info("user set to %s", osConf.User)
}
}

if config.RlimitNoFile != 0 {
err = aghos.SetRlimit(conf.RlimitNoFile)
err = logIfUnsupported("warning: setting rlimit", err)
if err != nil {
return fmt.Errorf("setting rlimit: %w", err)
}

log.Info("rlimit_nofile set to %d", config.RlimitNoFile)
}

return nil
}

func setupConfig(args options) (err error) {
config.DHCP.WorkDir = Context.workDir
config.DHCP.HTTPRegister = httpRegister
Expand Down Expand Up @@ -216,13 +267,6 @@ func setupConfig(args options) (err error) {
Context.clients.Init(config.Clients, Context.dhcpServer, Context.etcHosts)
config.Clients = nil

if config.RlimitNoFile != 0 {
err = aghos.SetRlimit(config.RlimitNoFile)
if err != nil && !errors.Is(err, aghos.ErrUnsupported) {
return fmt.Errorf("setting rlimit: %w", err)
}
}

// override bind host/port from the console
if args.bindHost != nil {
config.BindHost = args.bindHost
Expand Down Expand Up @@ -309,6 +353,9 @@ func run(args options, clientBuildFS fs.FS) {

setupContext(args)

err = configureOS(&config)
fatalOnError(err)

// clients package uses filtering package's static data (filtering.BlockedSvcKnown()),
// so we have to initialize filtering's static data first,
// but also avoid relying on automatic Go init() function
Expand Down

0 comments on commit ba95d4a

Please sign in to comment.