mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-28 19:41:17 +00:00
456 lines
13 KiB
Go
456 lines
13 KiB
Go
package envdetect
|
|
|
|
import (
|
|
"errors"
|
|
"net"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestDetectContainer_SystemdDetectVirtDocker(t *testing.T) {
|
|
probe := fakeEnvironmentProbe{
|
|
lookPathPresent: map[string]bool{"systemd-detect-virt": true},
|
|
commandOutput: map[string][]byte{
|
|
"systemd-detect-virt\x00--container": []byte("docker\n"),
|
|
},
|
|
}
|
|
|
|
isContainer, containerType := detectContainer(probe)
|
|
if !isContainer {
|
|
t.Fatalf("expected container, got non-container")
|
|
}
|
|
if containerType != "docker" {
|
|
t.Fatalf("containerType = %q, want docker", containerType)
|
|
}
|
|
}
|
|
|
|
func TestDetectContainer_SystemdDetectVirtUnknown(t *testing.T) {
|
|
probe := fakeEnvironmentProbe{
|
|
lookPathPresent: map[string]bool{"systemd-detect-virt": true},
|
|
commandOutput: map[string][]byte{
|
|
"systemd-detect-virt\x00--container": []byte("rkt\n"),
|
|
},
|
|
}
|
|
|
|
isContainer, containerType := detectContainer(probe)
|
|
if !isContainer {
|
|
t.Fatalf("expected container, got non-container")
|
|
}
|
|
if containerType != "rkt" {
|
|
t.Fatalf("containerType = %q, want rkt", containerType)
|
|
}
|
|
}
|
|
|
|
func TestDetectContainer_MarkersAndCgroup(t *testing.T) {
|
|
t.Run("marker-containerenv", func(t *testing.T) {
|
|
probe := fakeEnvironmentProbe{
|
|
statPresent: map[string]bool{"/run/.containerenv": true},
|
|
}
|
|
|
|
isContainer, containerType := detectContainer(probe)
|
|
if !isContainer || containerType != "docker" {
|
|
t.Fatalf("marker detection = (%v, %q), want (true, docker)", isContainer, containerType)
|
|
}
|
|
})
|
|
|
|
t.Run("cgroup-docker", func(t *testing.T) {
|
|
probe := fakeEnvironmentProbe{
|
|
fileData: map[string][]byte{
|
|
"/proc/1/cgroup": []byte("12:cpu:/docker/abc\n"),
|
|
},
|
|
}
|
|
|
|
isContainer, containerType := detectContainer(probe)
|
|
if !isContainer || containerType != "docker" {
|
|
t.Fatalf("cgroup detection = (%v, %q), want (true, docker)", isContainer, containerType)
|
|
}
|
|
})
|
|
|
|
t.Run("environ-lxc", func(t *testing.T) {
|
|
probe := fakeEnvironmentProbe{
|
|
fileErr: map[string]error{
|
|
"/proc/1/cgroup": errors.New("nope"),
|
|
},
|
|
fileData: map[string][]byte{
|
|
"/proc/1/environ": []byte("container=lxc\x00"),
|
|
},
|
|
}
|
|
|
|
isContainer, containerType := detectContainer(probe)
|
|
if !isContainer || containerType != "lxc" {
|
|
t.Fatalf("environ detection = (%v, %q), want (true, lxc)", isContainer, containerType)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestDetectContainer_NoMarkers(t *testing.T) {
|
|
probe := fakeEnvironmentProbe{
|
|
fileErr: map[string]error{
|
|
"/proc/1/cgroup": os.ErrNotExist,
|
|
"/proc/1/environ": os.ErrNotExist,
|
|
},
|
|
}
|
|
|
|
isContainer, containerType := detectContainer(probe)
|
|
if isContainer {
|
|
t.Fatalf("expected non-container, got container type %q", containerType)
|
|
}
|
|
if containerType != "" {
|
|
t.Fatalf("containerType = %q, want empty", containerType)
|
|
}
|
|
}
|
|
|
|
func TestDetectNativeEnvironment_FallbackOnError(t *testing.T) {
|
|
profile := &EnvironmentProfile{
|
|
Policy: DefaultScanPolicy(),
|
|
Metadata: map[string]string{},
|
|
}
|
|
probe := fakeEnvironmentProbe{interfacesErr: errors.New("boom")}
|
|
|
|
result, err := detectNativeEnvironment(profile, probe)
|
|
if err != nil {
|
|
t.Fatalf("detectNativeEnvironment returned error: %v", err)
|
|
}
|
|
if len(result.Phases) == 0 || result.Phases[0].Name != "fallback_common_subnets" {
|
|
t.Fatalf("expected fallback subnets, got %#v", result.Phases)
|
|
}
|
|
|
|
found := false
|
|
for _, warn := range result.Warnings {
|
|
if strings.Contains(warn, "Failed to enumerate interfaces") {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("expected warning about interface enumeration, got %#v", result.Warnings)
|
|
}
|
|
}
|
|
|
|
func TestDetectNativeEnvironment_NoSubnetsFallback(t *testing.T) {
|
|
profile := &EnvironmentProfile{
|
|
Policy: DefaultScanPolicy(),
|
|
Metadata: map[string]string{},
|
|
}
|
|
probe := fakeEnvironmentProbe{
|
|
interfaces: []ifaceInfo{
|
|
{
|
|
Name: "lo",
|
|
Flags: net.FlagUp | net.FlagLoopback,
|
|
Addrs: []net.Addr{
|
|
&net.IPNet{IP: net.IPv4(127, 0, 0, 1), Mask: net.CIDRMask(8, 32)},
|
|
},
|
|
},
|
|
{
|
|
Name: "eth0",
|
|
Flags: net.FlagUp,
|
|
Addrs: []net.Addr{
|
|
&net.IPNet{IP: net.IPv4(169, 254, 1, 2), Mask: net.CIDRMask(16, 32)},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
result, err := detectNativeEnvironment(profile, probe)
|
|
if err != nil {
|
|
t.Fatalf("detectNativeEnvironment returned error: %v", err)
|
|
}
|
|
if len(result.Phases) == 0 || result.Phases[0].Name != "fallback_common_subnets" {
|
|
t.Fatalf("expected fallback subnets, got %#v", result.Phases)
|
|
}
|
|
|
|
found := false
|
|
for _, warn := range result.Warnings {
|
|
if strings.Contains(warn, "No active IPv4 interfaces found") {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("expected warning about missing interfaces, got %#v", result.Warnings)
|
|
}
|
|
}
|
|
|
|
func TestGetAllLocalSubnets_DedupAndSkip(t *testing.T) {
|
|
probe := fakeEnvironmentProbe{
|
|
interfaces: []ifaceInfo{
|
|
{
|
|
Name: "lo",
|
|
Flags: net.FlagUp | net.FlagLoopback,
|
|
Addrs: []net.Addr{&net.IPNet{IP: net.IPv4(127, 0, 0, 1), Mask: net.CIDRMask(8, 32)}},
|
|
},
|
|
{
|
|
Name: "down0",
|
|
Flags: 0,
|
|
Addrs: []net.Addr{&net.IPNet{IP: net.IPv4(10, 0, 0, 2), Mask: net.CIDRMask(24, 32)}},
|
|
},
|
|
{
|
|
Name: "err0",
|
|
Flags: net.FlagUp,
|
|
AddrsErr: errors.New("boom"),
|
|
},
|
|
{
|
|
Name: "v6",
|
|
Flags: net.FlagUp,
|
|
Addrs: []net.Addr{&net.IPNet{IP: net.ParseIP("fe80::1"), Mask: net.CIDRMask(64, 128)}},
|
|
},
|
|
{
|
|
Name: "eth0",
|
|
Flags: net.FlagUp,
|
|
Addrs: []net.Addr{&net.IPNet{IP: net.IPv4(192, 168, 50, 10), Mask: net.CIDRMask(24, 32)}},
|
|
},
|
|
{
|
|
Name: "eth1",
|
|
Flags: net.FlagUp,
|
|
Addrs: []net.Addr{&net.IPNet{IP: net.IPv4(192, 168, 50, 20), Mask: net.CIDRMask(24, 32)}},
|
|
},
|
|
{
|
|
Name: "linklocal",
|
|
Flags: net.FlagUp,
|
|
Addrs: []net.Addr{&net.IPNet{IP: net.IPv4(169, 254, 10, 2), Mask: net.CIDRMask(16, 32)}},
|
|
},
|
|
},
|
|
}
|
|
|
|
subnets, err := getAllLocalSubnets(probe)
|
|
if err != nil {
|
|
t.Fatalf("getAllLocalSubnets returned error: %v", err)
|
|
}
|
|
if len(subnets) != 1 {
|
|
t.Fatalf("subnets = %#v, want 1 unique subnet", subnets)
|
|
}
|
|
if got := subnets[0].String(); got != mustIPNet(t, "192.168.50.0/24").String() {
|
|
t.Fatalf("subnet = %q, want 192.168.50.0/24", got)
|
|
}
|
|
}
|
|
|
|
func TestIsDockerHostMode_InterfaceError(t *testing.T) {
|
|
probe := fakeEnvironmentProbe{interfacesErr: errors.New("boom")}
|
|
|
|
hostMode, warnings := isDockerHostMode(probe)
|
|
if hostMode {
|
|
t.Fatalf("expected hostMode=false on error")
|
|
}
|
|
if len(warnings) == 0 || !strings.Contains(warnings[0], "Unable to enumerate interfaces") {
|
|
t.Fatalf("warnings = %#v, want interface enumeration warning", warnings)
|
|
}
|
|
}
|
|
|
|
func TestDetectDockerEnvironment_BridgeFallback(t *testing.T) {
|
|
profile := &EnvironmentProfile{
|
|
Policy: DefaultScanPolicy(),
|
|
Metadata: map[string]string{},
|
|
}
|
|
profile.Policy.ScanGateways = false
|
|
|
|
probe := fakeEnvironmentProbe{
|
|
fileErr: map[string]error{
|
|
"/proc/net/route": errors.New("nope"),
|
|
},
|
|
interfaces: []ifaceInfo{
|
|
{
|
|
Name: "lo",
|
|
Flags: net.FlagUp | net.FlagLoopback,
|
|
Addrs: []net.Addr{&net.IPNet{IP: net.IPv4(127, 0, 0, 1), Mask: net.CIDRMask(8, 32)}},
|
|
},
|
|
},
|
|
}
|
|
|
|
result, err := detectDockerEnvironment(profile, probe)
|
|
if err != nil {
|
|
t.Fatalf("detectDockerEnvironment returned error: %v", err)
|
|
}
|
|
if result.Type != DockerBridge {
|
|
t.Fatalf("Type = %v, want %v", result.Type, DockerBridge)
|
|
}
|
|
if len(result.Phases) == 0 || result.Phases[0].Name != "fallback_common_subnets" {
|
|
t.Fatalf("expected fallback subnets, got %#v", result.Phases)
|
|
}
|
|
}
|
|
|
|
func TestDetectHostNetworkFromContainer_NonStandardGateway(t *testing.T) {
|
|
route := strings.Join([]string{
|
|
"Iface\tDestination\tGateway",
|
|
"eth0\t00000000\t2A00000A",
|
|
}, "\n")
|
|
|
|
probe := fakeEnvironmentProbe{
|
|
fileData: map[string][]byte{"/proc/net/route": []byte(route)},
|
|
}
|
|
|
|
subnets, confidence, warnings := detectHostNetworkFromContainer(probe)
|
|
if confidence != 0.4 {
|
|
t.Fatalf("confidence = %v, want 0.4", confidence)
|
|
}
|
|
if len(subnets) != 1 || subnets[0].String() != "10.0.0.0/24" {
|
|
t.Fatalf("subnets = %#v, want 10.0.0.0/24", subnets)
|
|
}
|
|
found := false
|
|
for _, warn := range warnings {
|
|
if strings.Contains(warn, "does not end with .1 or .254") {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("warnings = %#v, want non-standard gateway warning", warnings)
|
|
}
|
|
}
|
|
|
|
func TestDetectHostNetworkFromContainer_GatewayErrorFallback(t *testing.T) {
|
|
probe := fakeEnvironmentProbe{
|
|
fileErr: map[string]error{"/proc/net/route": errors.New("boom")},
|
|
}
|
|
|
|
subnets, confidence, warnings := detectHostNetworkFromContainer(probe)
|
|
if confidence != 0.3 {
|
|
t.Fatalf("confidence = %v, want 0.3", confidence)
|
|
}
|
|
if len(subnets) == 0 {
|
|
t.Fatalf("expected fallback subnets, got none")
|
|
}
|
|
if len(warnings) == 0 || !strings.Contains(warnings[0], "Could not determine default gateway") {
|
|
t.Fatalf("warnings = %#v, want gateway warning", warnings)
|
|
}
|
|
}
|
|
|
|
func TestGetDefaultGateway_InvalidHex(t *testing.T) {
|
|
route := strings.Join([]string{
|
|
"Iface\tDestination\tGateway",
|
|
"eth0\t00000000\tZZZZZZZZ",
|
|
}, "\n")
|
|
|
|
probe := fakeEnvironmentProbe{
|
|
fileData: map[string][]byte{"/proc/net/route": []byte(route)},
|
|
}
|
|
|
|
_, err := getDefaultGateway(probe)
|
|
if err == nil {
|
|
t.Fatalf("expected error, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), "failed to parse default gateway") {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCountKernelRoutes_ReadFileError(t *testing.T) {
|
|
probe := fakeEnvironmentProbe{
|
|
fileErr: map[string]error{"/proc/net/route": errors.New("boom")},
|
|
}
|
|
|
|
count, warn := countKernelRoutes(probe)
|
|
if count != 0 {
|
|
t.Fatalf("count = %d, want 0", count)
|
|
}
|
|
if warn == "" || !strings.Contains(warn, "Unable to read /proc/net/route") {
|
|
t.Fatalf("warn = %q, want read error warning", warn)
|
|
}
|
|
}
|
|
|
|
func TestIsLXCPrivileged_ErrorPaths(t *testing.T) {
|
|
t.Run("permission", func(t *testing.T) {
|
|
probe := fakeEnvironmentProbe{
|
|
fileErr: map[string]error{"/proc/self/uid_map": os.ErrPermission},
|
|
}
|
|
privileged, warn := isLXCPrivileged(probe)
|
|
if privileged {
|
|
t.Fatalf("expected unprivileged on permission error")
|
|
}
|
|
if !strings.Contains(warn, "permission denied") {
|
|
t.Fatalf("warn = %q, want permission warning", warn)
|
|
}
|
|
})
|
|
|
|
t.Run("bad-format", func(t *testing.T) {
|
|
probe := fakeEnvironmentProbe{
|
|
fileData: map[string][]byte{"/proc/self/uid_map": []byte("0 0\n")},
|
|
}
|
|
privileged, warn := isLXCPrivileged(probe)
|
|
if privileged {
|
|
t.Fatalf("expected unprivileged on format error")
|
|
}
|
|
if !strings.Contains(warn, "Unexpected format") {
|
|
t.Fatalf("warn = %q, want format warning", warn)
|
|
}
|
|
})
|
|
|
|
t.Run("bad-hostuid", func(t *testing.T) {
|
|
probe := fakeEnvironmentProbe{
|
|
fileData: map[string][]byte{"/proc/self/uid_map": []byte("0 x 1\n")},
|
|
}
|
|
privileged, warn := isLXCPrivileged(probe)
|
|
if privileged {
|
|
t.Fatalf("expected unprivileged on parse error")
|
|
}
|
|
if !strings.Contains(warn, "Failed to parse uid_map") {
|
|
t.Fatalf("warn = %q, want parse warning", warn)
|
|
}
|
|
})
|
|
|
|
t.Run("bad-length", func(t *testing.T) {
|
|
probe := fakeEnvironmentProbe{
|
|
fileData: map[string][]byte{"/proc/self/uid_map": []byte("0 0 nope\n")},
|
|
}
|
|
privileged, warn := isLXCPrivileged(probe)
|
|
if privileged {
|
|
t.Fatalf("expected unprivileged on length parse error")
|
|
}
|
|
if !strings.Contains(warn, "Failed to parse uid_map length") {
|
|
t.Fatalf("warn = %q, want length warning", warn)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestDetectEnvironment_UnsupportedContainerType(t *testing.T) {
|
|
probe := fakeEnvironmentProbe{
|
|
lookPathPresent: map[string]bool{"systemd-detect-virt": true},
|
|
commandOutput: map[string][]byte{
|
|
"systemd-detect-virt\x00--container": []byte("rkt\n"),
|
|
},
|
|
}
|
|
|
|
profile, err := detectEnvironment(probe)
|
|
if err != nil {
|
|
t.Fatalf("detectEnvironment returned error: %v", err)
|
|
}
|
|
if profile.Type != Unknown {
|
|
t.Fatalf("Type = %v, want %v", profile.Type, Unknown)
|
|
}
|
|
if profile.Metadata["container_type"] != "rkt" {
|
|
t.Fatalf("container_type = %q, want rkt", profile.Metadata["container_type"])
|
|
}
|
|
if len(profile.Phases) == 0 || profile.Phases[0].Name != "fallback_common_subnets" {
|
|
t.Fatalf("expected fallback subnets, got %#v", profile.Phases)
|
|
}
|
|
}
|
|
|
|
func TestDetectLXCEnvironment_UnprivilegedFallback(t *testing.T) {
|
|
profile := &EnvironmentProfile{
|
|
Policy: DefaultScanPolicy(),
|
|
Metadata: map[string]string{},
|
|
}
|
|
profile.Policy.ScanGateways = false
|
|
|
|
probe := fakeEnvironmentProbe{
|
|
fileData: map[string][]byte{"/proc/self/uid_map": []byte("0 100000 65536\n")},
|
|
interfaces: []ifaceInfo{
|
|
{
|
|
Name: "lo",
|
|
Flags: net.FlagUp | net.FlagLoopback,
|
|
Addrs: []net.Addr{&net.IPNet{IP: net.IPv4(127, 0, 0, 1), Mask: net.CIDRMask(8, 32)}},
|
|
},
|
|
},
|
|
}
|
|
|
|
result, err := detectLXCEnvironment(profile, probe)
|
|
if err != nil {
|
|
t.Fatalf("detectLXCEnvironment returned error: %v", err)
|
|
}
|
|
if result.Type != LXCUnprivileged {
|
|
t.Fatalf("Type = %v, want %v", result.Type, LXCUnprivileged)
|
|
}
|
|
if len(result.Phases) == 0 || result.Phases[0].Name != "fallback_common_subnets" {
|
|
t.Fatalf("expected fallback subnets, got %#v", result.Phases)
|
|
}
|
|
}
|