diff --git a/internal/mod/builtin_linux.go b/internal/mod/builtin_linux.go new file mode 100644 index 00000000..35991204 --- /dev/null +++ b/internal/mod/builtin_linux.go @@ -0,0 +1,33 @@ +package mod + +import ( + "bufio" + "errors" + "fmt" + "os" + "path/filepath" + "strings" +) + +var errBuiltinModuleNotFound = errors.New("builtin module not found") + +func checkModulesBuiltin(modulesPath, moduleName string) error { + f, err := os.Open(filepath.Join(modulesPath, "modules.builtin")) + if err != nil { + return err + } + defer f.Close() + + moduleName = strings.TrimSuffix(moduleName, ".ko") + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := scanner.Text() + line = strings.TrimSuffix(line, ".ko") + if strings.HasSuffix(line, "/"+moduleName) { + return nil + } + } + + return fmt.Errorf("%w: %s", errBuiltinModuleNotFound, moduleName) +} diff --git a/internal/mod/configgz_linux.go b/internal/mod/configgz_linux.go new file mode 100644 index 00000000..a8a932e6 --- /dev/null +++ b/internal/mod/configgz_linux.go @@ -0,0 +1,132 @@ +package mod + +import ( + "bufio" + "compress/gzip" + "errors" + "fmt" + "os" + "strings" +) + +var ( + errModuleNameUnknown = errors.New("unknown module name") + errKernelFeatureIsModule = errors.New("kernel feature is a module, not built-in") + errKernelFeatureNotSet = errors.New("kernel feature not set") + errKernelFeatureNotFound = errors.New("kernel feature not found") +) + +// checkProcConfig checks /proc/config.gz for a the kernel feature corresponding +// to the given module name. If the kernel feature is found and set to "y", it returns nil. +// If the kernel feature is found and set to "m", it returns an error indicating that the kernel +// feature is a module, not built-in. +// If the kernel feature is found and not set, it returns an error indicating that the kernel +// feature is not set. If the kernel feature is not found, it returns an error indicating that the kernel +// feature is not found. +func checkProcConfig(moduleName string) error { + f, err := os.Open("/proc/config.gz") + if err != nil { + return err + } + defer f.Close() + + gz, err := gzip.NewReader(f) + if err != nil { + return fmt.Errorf("creating gzip reader: %w", err) + } + defer gz.Close() + + // If any group of kernel features is satisfied, then the module is considered supported. + kernelFeatureGroups, ok := moduleNameToKernelFeatureGroups(moduleName) + if !ok { + return fmt.Errorf("%w: %s", errModuleNameUnknown, moduleName) + } + groups := make([]map[string]bool, len(kernelFeatureGroups)) + for i, group := range kernelFeatureGroups { + featureToOK := make(map[string]bool) + for _, feature := range group { + featureToOK[feature] = false + } + groups[i] = featureToOK + } + + scanner := bufio.NewScanner(gz) + for scanner.Scan() { + line := scanner.Text() + for _, featureToOK := range groups { + for name, ok := range featureToOK { + switch { + case ok: + case strings.HasPrefix(line, name+"=m"): + return fmt.Errorf("%w: %s", errKernelFeatureIsModule, name) + case strings.HasPrefix(line, name+"=y"): + featureToOK[name] = true + if allFeaturesOK(featureToOK) { + return nil + } + case strings.HasPrefix(line, "# "+name+" is not set"): + return fmt.Errorf("%w: %s", errKernelFeatureNotSet, name) + } + } + } + } + + return fmt.Errorf("%w: for module name %s", errKernelFeatureNotFound, moduleName) +} + +func moduleNameToKernelFeatureGroups(moduleName string) (featureGroups [][]string, ok bool) { + moduleMap := map[string][][]string{ + "nf_tables": {{"CONFIG_NF_TABLES"}}, + + // Netfilter Matches + "xt_conntrack": {{"CONFIG_NETFILTER_XT_MATCH_CONNTRACK"}}, + "xt_connmark": { + {"CONFIG_NETFILTER_XT_CONNMARK"}, + {"CONFIG_NETFILTER_XT_MATCH_CONNMARK", "CONFIG_NETFILTER_XT_TARGET_CONNMARK"}, + }, + "xt_mark": { + {"CONFIG_NETFILTER_XT_MARK"}, + {"CONFIG_NETFILTER_XT_MATCH_MARK", "CONFIG_NETFILTER_XT_TARGET_MARK"}, + }, + "nf_conntrack_netlink": {{"CONFIG_NF_CT_NETLINK"}}, + "nf_reject_ipv4": {{"CONFIG_NF_REJECT_IPV4"}}, + + // Common Netfilter Targets + "xt_log": {{"CONFIG_NETFILTER_XT_TARGET_LOG"}}, + "xt_reject": { + {"CONFIG_IP_NF_TARGET_REJECT", "CONFIG_NF_REJECT_IPV4"}, + {"CONFIG_NETFILTER_XT_TARGET_REJECT", "CONFIG_NF_REJECT_IPV4"}, + }, + "xt_masquerade": {{"CONFIG_NETFILTER_XT_TARGET_MASQUERADE"}}, + + // Additional Netfilter Matches + "xt_addrtype": {{"CONFIG_NETFILTER_XT_MATCH_ADDRTYPE"}}, + "xt_comment": {{"CONFIG_NETFILTER_XT_MATCH_COMMENT"}}, + "xt_multiport": {{"CONFIG_NETFILTER_XT_MATCH_MULTIPORT"}}, + "xt_state": {{"CONFIG_NETFILTER_XT_MATCH_STATE"}}, + "xt_tcpudp": {{"CONFIG_NETFILTER_XT_MATCH_TCPUDP"}}, + + // Tunneling and Virtualization + "tun": {{"CONFIG_TUN"}}, + "bridge": {{"CONFIG_BRIDGE"}}, + "veth": {{"CONFIG_VETH"}}, + "vxlan": {{"CONFIG_VXLAN"}}, + "wireguard": {{"CONFIG_WIREGUARD"}}, + + // Filesystems + "overlay": {{"CONFIG_OVERLAY_FS"}}, + "fuse": {{"CONFIG_FUSE_FS"}}, + } + + featureGroups, ok = moduleMap[strings.ToLower(moduleName)] + return featureGroups, ok +} + +func allFeaturesOK(featureToOK map[string]bool) bool { + for _, ok := range featureToOK { + if !ok { + return false + } + } + return true +} diff --git a/internal/mod/info_linux.go b/internal/mod/info_linux.go index d169e379..25fea26e 100644 --- a/internal/mod/info_linux.go +++ b/internal/mod/info_linux.go @@ -30,36 +30,7 @@ type moduleInfo struct { var ErrModulesDirectoryNotFound = errors.New("modules directory not found") -func getModulesInfo() (modulesInfo map[string]moduleInfo, err error) { - var utsName unix.Utsname - err = unix.Uname(&utsName) - if err != nil { - return nil, fmt.Errorf("getting unix uname release: %w", err) - } - release := unix.ByteSliceToString(utsName.Release[:]) - release = strings.TrimSpace(release) - - modulePaths := []string{ - filepath.Join("/lib/modules", release), - filepath.Join("/usr/lib/modules", release), - } - - var modulesPath string - var found bool - for _, modulesPath = range modulePaths { - info, err := os.Stat(modulesPath) - if err == nil && info.IsDir() { - found = true - break - } - } - - if !found { - return nil, fmt.Errorf("%w: %s are not valid existing directories"+ - "; have you bind mounted the /lib/modules directory?", - ErrModulesDirectoryNotFound, strings.Join(modulePaths, ", ")) - } - +func getModulesInfo(modulesPath string) (modulesInfo map[string]moduleInfo, err error) { dependencyFilepath := filepath.Join(modulesPath, "modules.dep") dependencyFile, err := os.Open(dependencyFilepath) if err != nil { @@ -111,6 +82,39 @@ func getModulesInfo() (modulesInfo map[string]moduleInfo, err error) { return modulesInfo, nil } +func getModulesPath() (string, error) { + release, err := getReleaseName() + if err != nil { + return "", fmt.Errorf("getting release name: %w", err) + } + + modulePaths := []string{ + filepath.Join("/lib/modules", release), + filepath.Join("/usr/lib/modules", release), + } + + for _, modulesPath := range modulePaths { + info, err := os.Stat(modulesPath) + if err == nil && info.IsDir() { + return modulesPath, nil + } + } + return "", fmt.Errorf("%w: %s are not valid existing directories"+ + "; have you bind mounted the /lib/modules directory?", + ErrModulesDirectoryNotFound, strings.Join(modulePaths, ", ")) +} + +func getReleaseName() (release string, err error) { + var utsName unix.Utsname + err = unix.Uname(&utsName) + if err != nil { + return "", fmt.Errorf("getting unix uname release: %w", err) + } + release = unix.ByteSliceToString(utsName.Release[:]) + release = strings.TrimSpace(release) + return release, nil +} + func getBuiltinModules(modulesDirPath string, modulesInfo map[string]moduleInfo) error { file, err := os.Open(filepath.Join(modulesDirPath, "modules.builtin")) if err != nil { diff --git a/internal/mod/probe_linux.go b/internal/mod/probe_linux.go index 80269328..2bda6cf8 100644 --- a/internal/mod/probe_linux.go +++ b/internal/mod/probe_linux.go @@ -1,12 +1,49 @@ package mod import ( + "errors" "fmt" ) -// Probe loads the given kernel module and its dependencies. +// Probe is a expanded version of modprobe, in which it checks if the Kernel +// built-in features contain the given module name. +// It first tries to locate the modules directory in [getModulesPath]. +// If it fails (like on WSL), it then only checks for the kernel feature +// in /proc/config.gz with [checkProcConfig]. +// Otherwise, it first checks if the modules directory modules.builtin +// file contains the given module name in [checkModulesBuiltin]. +// If the module is not found, it then runs the classic [modProbe] behavior, +// trying to load the module in the kernel. +// If this fails, it does one final try running [checkProcConfig]. func Probe(moduleName string) error { - modulesInfo, err := getModulesInfo() + modulesPath, err := getModulesPath() + if err != nil { + if errors.Is(err, ErrModulesDirectoryNotFound) { + err = checkProcConfig(moduleName) + if err != nil { + return fmt.Errorf("checking /proc/config.gz: %w", err) + } + return nil + } + return fmt.Errorf("getting modules path: %w", err) + } + + err = checkModulesBuiltin(modulesPath, moduleName) + if err != nil { + err = modProbe(modulesPath, moduleName) + if err != nil { + err = checkProcConfig(moduleName) + if err != nil { + return fmt.Errorf("checking /proc/config.gz: %w", err) + } + } + } + return nil +} + +// modProbe is the classic modprobe behavior. +func modProbe(modulesPath, moduleName string) error { + modulesInfo, err := getModulesInfo(modulesPath) if err != nil { return fmt.Errorf("getting modules information: %w", err) }