You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tailscale/util/kmod/kmod.go

162 lines
4.9 KiB
Go

// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build linux
// +build linux
// Package kmod provides a simple API to attempt to ensure that a kernel
// module is loaded in a wide variety of environments, and otherwise
// report descriptive loggable error strings.
// This package does not have extensive unit testing, as the broader set
// of challenges associated with the package come from a wide variety of
// distribution and linux version differences that are problematic to
// mock/stub/emulate, including syscall boundary behaviors. The program
// `ensuremod` is kept nearby the source that provides a method for
// integration testing.
package kmod
import (
"bytes"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"go4.org/mem"
"golang.org/x/sys/unix"
"kernel.org/pub/linux/libs/security/libcap/cap"
"pault.ag/go/modprobe"
"tailscale.com/util/lineread"
"tailscale.com/util/multierr"
)
// hasKernelModule attempts to find a kernel module by name using procfs and
// sysfs. If the module is found to be loaded, true is returned, in all other
// cases false is returned, regardless of errors or a missing module.
func hasKernelModule(name string) (bool, error) {
if _, err := os.Stat(filepath.Join("/sys/module", name)); err == nil {
return true, nil
}
prefix := mem.S(name + " ")
stopFound := errors.New("")
err := lineread.File("/proc/modules", func(line []byte) error {
if mem.HasPrefix(mem.B(line), prefix) {
return stopFound
}
return nil
})
if err == stopFound {
return true, nil
}
if err != nil {
err = fmt.Errorf("module %s not found in /sys/module or /proc/modules: %w", name, err)
}
return false, err
}
// canInstallModule attempts to determine if the current process has sufficient
// privilege to install modules. If the capabilities API can be queried without
// error, then the result depends on the SYS_MODULE effective capability,
// otherwise returns true only if the current process is running as root. A
// result of true implies that it may be worth trying to install a module, not
// that doing so will work.
func canInstallModule() (bool, error) {
caps, err := cap.GetPID(0) // 0 = current process
if err == nil {
// errors from GetFlag are either due to the receiver being
// uninitialized, or the kernel gave junk results, both of which aren't
// very meaningful out of context to a user, so this error is mostly
// ignored.
b, err := caps.GetFlag(cap.Effective, cap.SYS_MODULE)
if err == nil {
return b, nil
}
}
// could not determine a well known result from capabilities, make an
// assumption based on uid.
if os.Getuid() == 0 {
return true, nil
}
return false, fmt.Errorf("not running as root, and unable to check kernel module capabilities")
}
// firstExecutable checks paths for a path that exists and is executable by the current user.
func firstExecutable(paths ...string) string {
for _, path := range paths {
if unix.Access(path, unix.X_OK) == nil {
return path
}
}
return ""
}
// runModprobe runs `modprobePath name` and reports summary error output on error.
func runModprobe(name, modprobePath string) error {
cmd := exec.Command(modprobePath, name)
out, err := cmd.CombinedOutput()
if err != nil {
err = fmt.Errorf("%q failed: %w; %s", fmt.Sprintf("%s %s", modprobePath, name), err, bytes.TrimSpace(out))
}
return err
}
// tryInstallModule attempts to find a modprobe binary to run either in
// well-known paths, or in $PATH, and runs it. If it can not find a modprobe to
// run, it instead falls back to a syscall interface to attempt to install a
// module.
func tryInstallModule(name string) error {
path := firstExecutable("/usr/sbin/modprobe", "/sbin/modprobe")
if path != "" {
return runModprobe(name, path)
}
path, err := exec.LookPath("modprobe")
if err == nil {
return runModprobe(name, path)
}
err = modprobe.Load(name, "")
if err != nil {
err = fmt.Errorf("unable to find modprobe(1), and load of module %s failed with: %w", name, err)
}
return err
}
// EnsureModule attempts to ensure that the given module is installed, returning
// true only if it has been found or successfully installed, otherwise false is
// returned along with a list of informational errors about probe attempts.
func EnsureModule(name string) (bool, error) {
has, hasErr := hasKernelModule(name)
if has {
return has, nil
}
var errors []error
if hasErr != nil {
errors = append(errors, hasErr)
}
can, canErr := canInstallModule()
if can && canErr != nil {
errors = append(errors, canErr)
}
if !can {
if canErr == nil {
errors = append(errors, fmt.Errorf("module %q not found, and current user can not install modules", name))
}
}
if can {
if err := tryInstallModule(name); err == nil {
return true, nil
} else {
errors = append(errors, err)
}
}
return false, multierr.New(errors...)
}