Skip to content

Commit

Permalink
Merge pull request #9299 from hashicorp/b-detect-unloaded-kmod
Browse files Browse the repository at this point in the history
client/fingerprint: detect unloaded dynamic bridge kernel module
  • Loading branch information
shoenig authored Nov 10, 2020
2 parents 11b968c + 3ad75f0 commit 76328b9
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 35 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ BUG FIXES:
* core: Fixed a bug where a request to scale a job would fail if the job was not in the default namespace. [[GH-9296](https://github.com/hashicorp/nomad/pull/9296)]
* config (Enterprise): Fixed default enterprise config merging. [[GH-9083](https://github.com/hashicorp/nomad/pull/9083)]
* client: Fixed an issue with the Java fingerprinter on macOS causing pop-up notifications when no JVM installed. [[GH-9225](https://github.com/hashicorp/nomad/pull/9225)]
* client: Fixed an fingerprinter issue detecting bridge kernel module [[GH-9299](https://github.com/hashicorp/nomad/pull/9299)]
* consul: Fixed a bug to correctly validate task when using script-checks in group-level services [[GH-8952](https://github.com/hashicorp/nomad/issues/8952)]
* consul: Fixed a bug where canary_meta was not being interpolated with environment variables [[GH-9096](https://github.com/hashicorp/nomad/pull/9096)]
* consul/connect: Fixed a bug to correctly trigger updates on jobspec changes [[GH-9029](https://github.com/hashicorp/nomad/pull/9029)]
Expand Down
3 changes: 2 additions & 1 deletion client/fingerprint/bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ package fingerprint
import log "github.com/hashicorp/go-hclog"

type BridgeFingerprint struct {
logger log.Logger
StaticFingerprinter

logger log.Logger
}

func NewBridgeFingerprint(logger log.Logger) Fingerprint {
Expand Down
85 changes: 56 additions & 29 deletions client/fingerprint/bridge_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,71 +6,98 @@ import (
"os"
"regexp"

"github.com/hashicorp/go-multierror"
"github.com/hashicorp/nomad/nomad/structs"
"github.com/shirou/gopsutil/host"
)

const bridgeKernelModuleName = "bridge"

const (
dynamicModuleRe = `%s\s+.*$`
builtinModuleRe = `.+/%s.ko$`
dependsModuleRe = `.+/%s.ko:.*$`
)

func (f *BridgeFingerprint) Fingerprint(req *FingerprintRequest, resp *FingerprintResponse) error {
if err := f.checkKMod(bridgeKernelModuleName); err != nil {
if err := f.detect(bridgeKernelModuleName); err != nil {
f.logger.Warn("failed to detect bridge kernel module, bridge network mode disabled", "error", err)
return nil
}

resp.NodeResources = &structs.NodeResources{
Networks: []*structs.NetworkResource{
{
Mode: "bridge",
},
},
NodeNetworks: []*structs.NodeNetworkResource{
{
Mode: "bridge",
Device: req.Config.BridgeNetworkName,
},
},
Networks: []*structs.NetworkResource{{
Mode: "bridge",
}},
NodeNetworks: []*structs.NodeNetworkResource{{
Mode: "bridge",
Device: req.Config.BridgeNetworkName,
}},
}

resp.Detected = true
return nil
}

func (f *BridgeFingerprint) checkKMod(mod string) error {
func (f *BridgeFingerprint) regexp(pattern, module string) *regexp.Regexp {
return regexp.MustCompile(fmt.Sprintf(pattern, module))
}

func (f *BridgeFingerprint) detect(module string) error {
// accumulate errors from every place we might find the module
var errs error

// check if the module has been dynamically loaded
dynamicPath := "/proc/modules"
if err := f.searchFile(module, dynamicPath, f.regexp(dynamicModuleRe, module)); err != nil {
errs = multierror.Append(errs, err)
} else {
return nil
}

// will need kernel info to look for builtin and unloaded modules
hostInfo, err := host.Info()
if err != nil {
return err
}

dynErr := f.checkKModFile(mod, "/proc/modules", fmt.Sprintf("%s\\s+.*$", mod))
if dynErr == nil {
// check if the module is builtin to the kernel
builtinPath := fmt.Sprintf("/lib/modules/%s/modules.builtin", hostInfo.KernelVersion)
if err := f.searchFile(module, builtinPath, f.regexp(builtinModuleRe, module)); err != nil {
errs = multierror.Append(errs, err)
} else {
return nil
}

builtinErr := f.checkKModFile(mod,
fmt.Sprintf("/lib/modules/%s/modules.builtin", hostInfo.KernelVersion),
fmt.Sprintf(".+\\/%s.ko$", mod))
if builtinErr == nil {
// check if the module is dynamic but unloaded (will have a dep entry)
dependsPath := fmt.Sprintf("/lib/modules/%s/modules.dep", hostInfo.KernelVersion)
if err := f.searchFile(module, dependsPath, f.regexp(dependsModuleRe, module)); err != nil {
errs = multierror.Append(errs, err)
} else {
return nil
}

return fmt.Errorf("%v, %v", dynErr, builtinErr)
return errs
}

func (f *BridgeFingerprint) checkKModFile(mod, fileName, pattern string) error {
file, err := os.Open(fileName)
func (f *BridgeFingerprint) searchFile(module, filename string, re *regexp.Regexp) error {
file, err := os.Open(filename)
if err != nil {
return fmt.Errorf("could not read %s: %v", fileName, err)
return fmt.Errorf("failed to open %s: %v", filename, err)
}
defer file.Close()
defer func() {
_ = file.Close()
}()

scanner := bufio.NewScanner(file)
for scanner.Scan() {
if matched, err := regexp.MatchString(pattern, scanner.Text()); matched {
return nil
} else if err != nil {
return fmt.Errorf("could not parse %s: %v", fileName, err)
if re.MatchString(scanner.Text()) {
return nil // found the module!
}
}
if err := scanner.Err(); err != nil {
return fmt.Errorf("failed to scan %s: %v", filename, err)
}

return fmt.Errorf("could not detect kernel module %s", mod)
return fmt.Errorf("module %s not in %s", module, filename)
}
125 changes: 120 additions & 5 deletions client/fingerprint/bridge_linux_test.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,129 @@
package fingerprint

import (
"fmt"
"io"
"io/ioutil"
"os"
"strings"
"testing"

"github.com/hashicorp/nomad/helper/testlog"
"github.com/stretchr/testify/require"
)

func TestBridgeFingerprint_checkKMod(t *testing.T) {
require := require.New(t)
f := &BridgeFingerprint{}
require.NoError(f.checkKMod("ip_tables"))
require.Error(f.checkKMod("nonexistentmodule"))
func TestBridgeFingerprint_detect(t *testing.T) {
f := &BridgeFingerprint{logger: testlog.HCLogger(t)}
require.NoError(t, f.detect("ip_tables"))

err := f.detect("nonexistentmodule")
require.Error(t, err)
require.Contains(t, err.Error(), "3 errors occurred")
}

func writeFile(t *testing.T, prefix, content string) string {
f, err := ioutil.TempFile("", "bridge-fp-")
require.NoError(t, err)

_, err = io.Copy(f, strings.NewReader(content))
require.NoError(t, err)

err = f.Close()
require.NoError(t, err)

return f.Name()
}

func cleanupFile(t *testing.T, name string) {
err := os.Remove(name)
require.NoError(t, err)
}

const (
dynamicModuleContent = `
ip_tables 32768 0 - Live 0xffffffffc03ee000
x_tables 40960 1 ip_tables, Live 0xffffffffc03e3000
autofs4 45056 2 - Live 0xffffffffc03d7000
bpfilter 32768 0 - Live 0x0000000000000000
br_netfilter 28672 0 - Live 0x0000000000000000
bridge 176128 1 br_netfilter, Live 0x0000000000000000
btrfs 1253376 0 - Live 0xffffffffc02a4000
`

builtinModuleContent = `
kernel/drivers/mfd/max14577.ko
kernel/drivers/mfd/max77693.ko
kernel/drivers/mfd/sec-core.ko
kernel/drivers/mfd/sec-irq.ko
kernel/drivers/net/bridge.ko
kernel/drivers/net/tun.ko
kernel/drivers/net/xen-netfront.k
`

dependsModuleContent = `
kernel/net/bridge/netfilter/ebt_log.ko: kernel/net/netfilter/x_tables.ko
kernel/net/bridge/netfilter/ebt_nflog.ko: kernel/net/netfilter/x_tables.ko
kernel/net/bridge/bridge.ko: kernel/net/802/stp.ko kernel/net/llc/llc.ko
kernel/net/bridge/br_netfilter.ko: kernel/net/bridge/bridge.ko kernel/net/802/stp.ko kernel/net/llc/llc.ko
kernel/net/appletalk/appletalk.ko: kernel/net/802/psnap.ko kernel/net/llc/llc.ko
kernel/net/x25/x25.ko:
`
)

func TestBridgeFingerprint_search(t *testing.T) {
f := &BridgeFingerprint{logger: testlog.HCLogger(t)}

t.Run("dynamic loaded module", func(t *testing.T) {
t.Run("present", func(t *testing.T) {
file := writeFile(t, "bridge-fp-", dynamicModuleContent)
defer cleanupFile(t, file)

err := f.searchFile("bridge", file, f.regexp(dynamicModuleRe, "bridge"))
require.NoError(t, err)
})

t.Run("absent", func(t *testing.T) {
file := writeFile(t, "bridge-fp-", dynamicModuleContent)
defer cleanupFile(t, file)

err := f.searchFile("nonexistent", file, f.regexp(dynamicModuleRe, "nonexistent"))
require.EqualError(t, err, fmt.Sprintf("module nonexistent not in %s", file))
})
})

t.Run("builtin module", func(t *testing.T) {
t.Run("present", func(t *testing.T) {
file := writeFile(t, "bridge-fp-", builtinModuleContent)
defer cleanupFile(t, file)

err := f.searchFile("bridge", file, f.regexp(builtinModuleRe, "bridge"))
require.NoError(t, err)
})

t.Run("absent", func(t *testing.T) {
file := writeFile(t, "bridge-fp-", builtinModuleContent)
defer cleanupFile(t, file)

err := f.searchFile("nonexistent", file, f.regexp(builtinModuleRe, "nonexistent"))
require.EqualError(t, err, fmt.Sprintf("module nonexistent not in %s", file))
})
})

t.Run("dynamic unloaded module", func(t *testing.T) {
t.Run("present", func(t *testing.T) {
file := writeFile(t, "bridge-fp-", dependsModuleContent)
defer cleanupFile(t, file)

err := f.searchFile("bridge", file, f.regexp(dependsModuleRe, "bridge"))
require.NoError(t, err)
})

t.Run("absent", func(t *testing.T) {
file := writeFile(t, "bridge-fp-", dependsModuleContent)
defer cleanupFile(t, file)

err := f.searchFile("nonexistent", file, f.regexp(dependsModuleRe, "nonexistent"))
require.EqualError(t, err, fmt.Sprintf("module nonexistent not in %s", file))
})
})
}

0 comments on commit 76328b9

Please sign in to comment.