288 lines
7.1 KiB
Go
288 lines
7.1 KiB
Go
// Package container provides tools for introspecting containers.
|
|
package container
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
|
|
"github.com/syndtr/gocapability/capability"
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
const (
|
|
// RuntimeDocker is the string for the docker runtime.
|
|
RuntimeDocker = "docker"
|
|
// RuntimeRkt is the string for the rkt runtime.
|
|
RuntimeRkt = "rkt"
|
|
// RuntimeNspawn is the string for the systemd-nspawn runtime.
|
|
RuntimeNspawn = "systemd-nspawn"
|
|
// RuntimeLXC is the string for the lxc runtime.
|
|
RuntimeLXC = "lxc"
|
|
// RuntimeLXCLibvirt is the string for the lxc-libvirt runtime.
|
|
RuntimeLXCLibvirt = "lxc-libvirt"
|
|
// RuntimeOpenVZ is the string for the openvz runtime.
|
|
RuntimeOpenVZ = "openvz"
|
|
// RuntimeKubernetes is the string for the kubernetes runtime.
|
|
RuntimeKubernetes = "kube"
|
|
// RuntimeGarden is the string for the garden runtime.
|
|
RuntimeGarden = "garden"
|
|
|
|
uint32Max = 4294967295
|
|
)
|
|
|
|
var (
|
|
// ErrContainerRuntimeNotFound describes when a container runtime could not be found.
|
|
ErrContainerRuntimeNotFound = errors.New("container runtime could not be found")
|
|
|
|
runtimes = []string{RuntimeDocker, RuntimeRkt, RuntimeNspawn, RuntimeLXC, RuntimeLXCLibvirt, RuntimeOpenVZ, RuntimeKubernetes, RuntimeGarden}
|
|
)
|
|
|
|
// DetectRuntime returns the container runtime the process is running in.
|
|
func DetectRuntime() (string, error) {
|
|
// read the cgroups file
|
|
cgroups := readFile("/proc/self/cgroup")
|
|
if len(cgroups) > 0 {
|
|
for _, runtime := range runtimes {
|
|
if strings.Contains(cgroups, runtime) {
|
|
return runtime, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// /proc/vz exists in container and outside of the container, /proc/bc only outside of the container.
|
|
if fileExists("/proc/vz") && !fileExists("/proc/bc") {
|
|
return RuntimeOpenVZ, nil
|
|
}
|
|
|
|
ctrenv := os.Getenv("container")
|
|
if ctrenv != "" {
|
|
for _, runtime := range runtimes {
|
|
if ctrenv == runtime {
|
|
return runtime, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// PID 1 might have dropped this information into a file in /run.
|
|
// Read from /run/systemd/container since it is better than accessing /proc/1/environ,
|
|
// which needs CAP_SYS_PTRACE
|
|
f := readFile("/run/systemd/container")
|
|
if len(f) > 0 {
|
|
for _, runtime := range runtimes {
|
|
if f == runtime {
|
|
return runtime, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return "not-found", ErrContainerRuntimeNotFound
|
|
}
|
|
|
|
// HasNamespace determines if the container is using a particular namespace or the
|
|
// host namespace.
|
|
// The device number of an unnamespaced /proc/1/ns/{ns} is 4 and anything else is
|
|
// higher.
|
|
func HasNamespace(ns string) (bool, error) {
|
|
file := fmt.Sprintf("/proc/1/ns/%s", ns)
|
|
|
|
// Use Lstat to not follow the symlink.
|
|
var info syscall.Stat_t
|
|
if err := syscall.Lstat(file, &info); err != nil {
|
|
return false, &os.PathError{Op: "lstat", Path: file, Err: err}
|
|
}
|
|
|
|
// Get the device number. If it is higher than 4 it is in a namespace.
|
|
if info.Dev > 4 {
|
|
return true, nil
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
// AppArmorProfile determines the apparmor profile for a container.
|
|
func AppArmorProfile() string {
|
|
f := readFile("/proc/self/attr/current")
|
|
if f == "" {
|
|
return "none"
|
|
}
|
|
return f
|
|
}
|
|
|
|
// UserMapping holds the values for a {uid,gid}_map.
|
|
type UserMapping struct {
|
|
ContainerID int64
|
|
HostID int64
|
|
Range int64
|
|
}
|
|
|
|
// UserNamespace determines if the container is running in a UserNamespace and returns the mappings if so.
|
|
func UserNamespace() (bool, []UserMapping) {
|
|
f := readFile("/proc/self/uid_map")
|
|
if len(f) < 0 {
|
|
// user namespace is uninitialized
|
|
return true, nil
|
|
}
|
|
|
|
userNs, mappings, err := readUserMappings(f)
|
|
if err != nil {
|
|
return false, nil
|
|
}
|
|
|
|
return userNs, mappings
|
|
}
|
|
|
|
func readUserMappings(f string) (iuserNS bool, mappings []UserMapping, err error) {
|
|
parts := strings.Split(f, " ")
|
|
parts = deleteEmpty(parts)
|
|
if len(parts) < 3 {
|
|
return false, nil, nil
|
|
}
|
|
|
|
for i := 0; i < len(parts); i += 3 {
|
|
nsu, hu, r := parts[i], parts[i+1], parts[i+2]
|
|
mapping := UserMapping{}
|
|
|
|
mapping.ContainerID, err = strconv.ParseInt(nsu, 10, 0)
|
|
if err != nil {
|
|
return false, nil, nil
|
|
}
|
|
mapping.HostID, err = strconv.ParseInt(hu, 10, 0)
|
|
if err != nil {
|
|
return false, nil, nil
|
|
}
|
|
mapping.Range, err = strconv.ParseInt(r, 10, 0)
|
|
if err != nil {
|
|
return false, nil, nil
|
|
}
|
|
|
|
if mapping.ContainerID == 0 && mapping.HostID == 0 && mapping.Range == uint32Max {
|
|
return false, nil, nil
|
|
}
|
|
|
|
mappings = append(mappings, mapping)
|
|
}
|
|
|
|
return true, mappings, nil
|
|
}
|
|
|
|
// Capabilities returns the allowed capabilities in the container.
|
|
func Capabilities() (map[string][]string, error) {
|
|
allCaps := capability.List()
|
|
|
|
caps, err := capability.NewPid(0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
allowedCaps := map[string][]string{}
|
|
allowedCaps["EFFECTIVE | PERMITTED | INHERITABLE"] = []string{}
|
|
allowedCaps["BOUNDING"] = []string{}
|
|
allowedCaps["AMBIENT"] = []string{}
|
|
|
|
for _, cap := range allCaps {
|
|
if caps.Get(capability.CAPS, cap) {
|
|
allowedCaps["EFFECTIVE | PERMITTED | INHERITABLE"] = append(allowedCaps["EFFECTIVE | PERMITTED | INHERITABLE"], cap.String())
|
|
}
|
|
if caps.Get(capability.BOUNDING, cap) {
|
|
allowedCaps["BOUNDING"] = append(allowedCaps["BOUNDING"], cap.String())
|
|
}
|
|
if caps.Get(capability.AMBIENT, cap) {
|
|
allowedCaps["AMBIENT"] = append(allowedCaps["AMBIENT"], cap.String())
|
|
}
|
|
}
|
|
|
|
return allowedCaps, nil
|
|
}
|
|
|
|
// Chroot detects if we are running in a chroot or a pivot_root.
|
|
// Currently, we can not distinguish between the two.
|
|
func Chroot() (bool, error) {
|
|
var a, b syscall.Stat_t
|
|
|
|
if err := syscall.Lstat("/proc/1/root", &a); err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if err := syscall.Lstat("/", &b); err != nil {
|
|
return false, err
|
|
}
|
|
|
|
return a.Ino == b.Ino && a.Dev == b.Dev, nil
|
|
}
|
|
|
|
// SeccompEnforcingMode returns the seccomp enforcing level (disabled, filtering, strict)
|
|
func SeccompEnforcingMode() (string, error) {
|
|
// Read from /proc/self/status Linux 3.8+
|
|
s := readFile("/proc/self/status")
|
|
|
|
// Pre linux 3.8
|
|
if !strings.Contains(s, "Seccomp") {
|
|
// Check if Seccomp is supported, via CONFIG_SECCOMP.
|
|
if err := unix.Prctl(unix.PR_GET_SECCOMP, 0, 0, 0, 0); err != unix.EINVAL {
|
|
// Make sure the kernel has CONFIG_SECCOMP_FILTER.
|
|
if err := unix.Prctl(unix.PR_SET_SECCOMP, unix.SECCOMP_MODE_FILTER, 0, 0, 0); err != unix.EINVAL {
|
|
return "strict", nil
|
|
}
|
|
}
|
|
return "disabled", nil
|
|
}
|
|
|
|
// Split status file string by line
|
|
statusMappings := strings.Split(s, "\n")
|
|
statusMappings = deleteEmpty(statusMappings)
|
|
|
|
mode := "-1"
|
|
for _, line := range statusMappings {
|
|
if strings.Contains(line, "Seccomp:") {
|
|
mode = string(line[len(line)-1])
|
|
}
|
|
}
|
|
|
|
seccompModes := map[string]string{
|
|
"0": "disabled",
|
|
"1": "strict",
|
|
"2": "filtering",
|
|
}
|
|
|
|
seccompMode, ok := seccompModes[mode]
|
|
if !ok {
|
|
return "", errors.New("could not retrieve seccomp filtering status")
|
|
}
|
|
|
|
return seccompMode, nil
|
|
}
|
|
|
|
func fileExists(file string) bool {
|
|
if _, err := os.Stat(file); !os.IsNotExist(err) {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func readFile(file string) string {
|
|
if !fileExists(file) {
|
|
return ""
|
|
}
|
|
|
|
b, err := ioutil.ReadFile(file)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(string(b))
|
|
}
|
|
|
|
func deleteEmpty(s []string) []string {
|
|
var r []string
|
|
for _, str := range s {
|
|
if strings.TrimSpace(str) != "" {
|
|
r = append(r, strings.TrimSpace(str))
|
|
}
|
|
}
|
|
return r
|
|
}
|