Skip to content

Commit

Permalink
Pull request: all: support setgid, setuid on unix
Browse files Browse the repository at this point in the history
Updates AdguardTeam#2763.

Squashed commit of the following:

commit bd2077c
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Fri Jun 4 16:25:17 2021 +0300

    all: move rlimit_nofile, imp docs

commit ba95d4a
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Fri Jun 4 15:12:23 2021 +0300

    all: support setgid, setuid on unix
  • Loading branch information
ainar-g authored and heyxkhoa committed Mar 17, 2023
1 parent 0c8b5d7 commit a66db4b
Show file tree
Hide file tree
Showing 14 changed files with 283 additions and 31 deletions.
4 changes: 4 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 All @@ -31,6 +32,8 @@ and this project adheres to

### Changed

- The setting `rlimit_nofile` is now in the `os` block of the configuration
file, together with the new `group` and `user` settings ([#2763]).
- Permissions on filter files are now `0o644` instead of `0o600` ([#3198]).

### Deprecated
Expand All @@ -56,6 +59,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
3 changes: 3 additions & 0 deletions HACKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@ on GitHub and most other Markdown renderers. -->
)
```

* Don't rely only on file names for build tags to work. Always add build tags
as well.

* Don't use `fmt.Sprintf` where a more structured approach to string
conversion could be used. For example, `net.JoinHostPort` or
`url.(*URL).String`.
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,19 @@ type logSettings struct {
Verbose bool `yaml:"verbose"` // If true, verbose logging is enabled
}

// osConfig contains OS-related configuration.
type osConfig struct {
// Group is the name of the group which AdGuard Home must switch to on
// startup. Empty string means no switching.
Group string `yaml:"group"`
// User is the name of the user which AdGuard Home must switch to on
// startup. Empty string means no switching.
User string `yaml:"user"`
// RlimitNoFile is the maximum number of opened fd's per process. Zero
// means use the default value.
RlimitNoFile uint64 `yaml:"rlimit_nofile"`
}

// configuration is loaded from YAML
// field ordering is important -- yaml fields will mirror ordering from here
type configuration struct {
Expand All @@ -52,10 +65,9 @@ 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
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
65 changes: 57 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,59 @@ 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) {
osConf := conf.OSConfig
if osConf == nil {
return nil
}

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 osConf.RlimitNoFile != 0 {
err = aghos.SetRlimit(osConf.RlimitNoFile)
err = logIfUnsupported("warning: setting rlimit", err)
if err != nil {
return fmt.Errorf("setting rlimit: %w", err)
}

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

return nil
}

func setupConfig(args options) (err error) {
config.DHCP.WorkDir = Context.workDir
config.DHCP.HTTPRegister = httpRegister
Expand Down Expand Up @@ -216,13 +269,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 +355,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
Loading

0 comments on commit a66db4b

Please sign in to comment.