162 lines
4.9 KiB
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...)
|
|
}
|