Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • acides/hepto
  • reminec/hepto
  • lutangar/hepto
3 results
Show changes
Showing
with 1310 additions and 1160 deletions
package pki
import (
"crypto/x509"
"forge.tedomum.net/acides/hepto/hepto/pkg/pekahi"
"github.com/sirupsen/logrus"
)
// Merge a single node or master certificate
func mergeCert(local *pekahi.Certificate, remote *pekahi.Certificate) bool {
change := false
// Do not merge nothing
if remote == nil {
return change
}
// Import CSR to master for signing
if local.CSR == nil && remote.CSR != nil {
local.CSR = remote.CSR
change = true
}
// Import and save cert back to node
if local.Cert == nil && remote.Cert != nil {
local.Cert = remote.Cert
local.Save()
change = true
}
return change
}
func signCert(ca *pekahi.Certificate, c *pekahi.Certificate, template *x509.Certificate) {
if c.CSR != nil && c.Cert == nil {
logrus.Info("signing certificate ", c.CSR.Subject.String())
err := ca.Sign(c, template)
if err != nil {
logrus.Warnf("cannot sign certificate for %s: %s", c.CSR.Subject.String(), err)
}
}
}
package selfcontain
import (
"net"
"github.com/opencontainers/runc/libcontainer/configs"
"github.com/opencontainers/runc/libcontainer/devices"
"golang.org/x/sys/unix"
)
type Config struct {
// Container name
Name string
// Path to on-file container data
Data string
// Path to container root filesystem
Root string
// Name of the master interface for IPvlan
Master string
// Public IP of the container, can be nulled for autoconfiguration
IP net.IPNet
// Default gateway for the container, can be nulled for autoconfiguration
GW net.IP
// List of DNS servers for the container
DNS []net.IP
// List of non-standard capabilities (required capabilities are always enabled)
Capabilities []string
// List of non-standard devices
Devices []string
}
// Turns a selfcontain configuration into a runc/libcontainer one
func (c *Config) toLibcontainer() (*configs.Config, error) {
// Setup devices, first copy base devices, then discover and add
// configured devices
devicePaths := append(baseDevices, c.Devices...)
allowedDevices := make([]*devices.Device, len(devicePaths))
deviceRules := make([]*devices.Rule, len(devicePaths))
for n, path := range devicePaths {
device, err := devices.DeviceFromPath(path, "rw")
if err != nil {
return nil, err
}
device.Rule.Allow = true
allowedDevices[n] = device
deviceRules[n] = &device.Rule
}
// Concatenate base cpabilieis and additional ones from config
capabilities := append(baseCapabilities, c.Capabilities...)
// Build the configuration
return &configs.Config{
Rootfs: c.Root,
RootPropagation: unix.MS_SHARED | unix.MS_REC,
Hostname: c.Name,
// Make all capabilities inherited and ambiant
Capabilities: &configs.Capabilities{
Bounding: capabilities,
Effective: capabilities,
Inheritable: capabilities,
Permitted: capabilities,
Ambient: capabilities,
},
Namespaces: []configs.Namespace{
{Type: configs.NEWNS},
{Type: configs.NEWUTS},
{Type: configs.NEWIPC},
{Type: configs.NEWPID},
{Type: configs.NEWNET},
{Type: configs.NEWCGROUP},
},
Devices: allowedDevices,
Cgroups: &configs.Cgroup{
Name: c.Name,
Systemd: false,
Resources: &configs.Resources{
MemorySwappiness: nil,
Devices: deviceRules,
},
},
MaskPaths: maskedPath,
ReadonlyPaths: readOnlyPath,
Mounts: baseMounts,
Networks: baseNets,
ParentDeathSignal: 15,
}, nil
}
// The selfcontain package provides a containment structure to move the current
// process inside a restricted container.
//
// This is accomplished thanks to runc/libcontainer library which in turns uses
// C bindings to namespace primitives. Containment is not much configurable and
// is fine-tuned to hepto itself.
package selfcontain
import (
"os"
"path/filepath"
"github.com/opencontainers/runc/libcontainer"
"github.com/opencontainers/runc/libcontainer/configs"
_ "github.com/opencontainers/runc/libcontainer/nsenter"
"github.com/sirupsen/logrus"
)
type Container struct {
config *Config
container libcontainer.Container
process libcontainer.Process
}
// Containerize the current process by runnig the current binary inside a container
func New(config *Config) (*Container, error) {
// Prepare a libcontainer factory using the init path and args
factoryConfig := func(f *libcontainer.LinuxFactory) error {
f.InitPath = "/proc/self/exe"
f.InitArgs = []string{os.Args[0], argInit}
return nil
}
factory, err := libcontainer.New(config.Data, factoryConfig)
if err != nil {
return nil, err
}
// Create and wrap the libcontainer instance
containerConfig, err := config.toLibcontainer()
if err != nil {
return nil, err
}
container, err := factory.Create(config.Name, containerConfig)
if err != nil {
return nil, err
}
return &Container{
config: config,
container: container,
}, nil
}
func (c *Container) Start(args []string) error {
process := libcontainer.Process{
Args: append([]string{"/proc/self/exe"}, args...),
Stdin: os.Stdin,
Stdout: os.Stdout,
Stderr: os.Stderr,
Init: true,
}
// Simply start the process instead of running, at this point init will be waiting
// and listening on libcontainer control pipe
err := c.container.Start(&process)
if err != nil {
c.container.Destroy()
return err
}
pid, err := process.Pid()
if err != nil {
c.container.Destroy()
return err
}
logrus.Debug("running with pid ", os.Getpid())
logrus.Debug("namespaced with pid ", pid)
c.process = process
return nil
}
// Actually run the container and block until the process has returned
func (c *Container) Run() error {
defer c.Destroy()
err := c.container.Exec()
if err != nil {
return err
}
_, err = c.process.Wait()
return err
}
func (c *Container) Destroy() error {
logrus.Info("destroying container ", c.config.Name)
err := c.container.Destroy()
if err != nil {
logrus.Error(err)
return err
}
err = os.RemoveAll(filepath.Join(c.config.Data, c.config.Name))
if err != nil {
logrus.Error(err)
return err
}
return nil
}
func (c *Container) GetNS(nsType configs.NamespaceType) (string, error) {
state, err := c.container.State()
if err != nil {
return "", err
}
return state.NamespacePaths[nsType], nil
}
package selfcontain
import (
"github.com/opencontainers/runc/libcontainer/configs"
"golang.org/x/sys/unix"
)
// This argument is passed back to the forked process to notify it should behave
// as a libcontainer init, which in turn is handled by init()
const argInit = "selfcontain-arg-libcontainer"
// Restrict access to the bare minimum for container to run properly
// See https://pkg.go.dev/github.com/opencontainers/runc@v1.0.2/libcontainer/specconv
// for comments about issues with default restrictions
var baseDevices = []string{
"/dev/null",
"/dev/zero",
"/dev/urandom",
"/dev/random",
}
// These path will be mounted as a default base inside the container
var baseMounts = []*configs.Mount{
// Used by so many programs for reflection that they must be mounted
{
Source: "proc",
Destination: "/proc",
Device: "proc",
Flags: unix.MS_NOEXEC | unix.MS_NOSUID | unix.MS_NODEV,
},
{
Source: "sysfs",
Destination: "/sys",
Device: "sysfs",
Flags: unix.MS_NOEXEC | unix.MS_NOSUID | unix.MS_NODEV,
},
// Used for container management
{
Source: "cgroup",
Destination: "/sys/fs/cgroup",
Device: "cgroup",
Flags: unix.MS_NOEXEC | unix.MS_NOSUID | unix.MS_NODEV,
},
// Mounted as a tmpfs and separate device rules
{
Source: "tmpfs",
Destination: "/dev",
Device: "tmpfs",
Flags: unix.MS_NOSUID | unix.MS_STRICTATIME,
Data: "mode=755",
},
// Dedicated pts instead of device rules
{
Source: "devpts",
Destination: "/dev/pts",
Device: "devpts",
Flags: unix.MS_NOSUID | unix.MS_NOEXEC,
Data: "newinstance,ptmxmode=0666,mode=0620,gid=5",
},
// In-memory tmp
{
Source: "tmpfs",
Destination: "/tmp",
Device: "tmpfs",
Flags: unix.MS_NOSUID | unix.MS_STRICTATIME,
Data: "mode=755",
},
}
// Restrict capabilities to strictly required capabilities
// All capabilities are inheritable and ambient, so that init execve works properly
var baseCapabilities = []string{
// Required for later setting up networking
"CAP_NET_ADMIN",
}
// These networks will be setup as a default base inside the container
var baseNets = []*configs.Network{
{
Type: "loopback",
Address: "127.0.0.1/0",
Gateway: "localhost",
},
}
// These path should not be readable at all from the container, despite /proc being
// mounted there
var maskedPath = []string{
// This might leak system memory otherwise
"/proc/kcore",
"/sys/firmware",
}
// These path should never be written from inside the container
var readOnlyPath = []string{
// Kernel configuration shall not be modified
"/proc/sys",
// IRQ shall not be triggered or setup
"/proc/sysrq-trigger", "/proc/irq",
// System but shall not be written to
"/proc/bus",
}
package selfcontain
import (
"os"
"runtime"
"github.com/opencontainers/runc/libcontainer"
"github.com/sirupsen/logrus"
)
// libcontainer uses a three-step containerization technique:
// 1. spawn a fifo for later communication with containerized init
// 2. unshare the current process and fork/execve /proc/self/exe with a
// special argument to trigger later initialization
// 3. Use the fifo to communicatie with init and initialize mounts, etc.
//
// This init checks for said special argument and call into libcontainer
// initialization routines, which in turn will execve the Process provided
// command, which in our specific case is /proc/self/exe again.
//
// It seems we cannot avoid such complexity without dropping all of
// libcontainer, which would make containerization even more complex.
func init() {
if len(os.Args) > 1 && os.Args[1] == argInit {
// Do not start the full featured runtime
runtime.GOMAXPROCS(1)
runtime.LockOSThread()
// Run libcontainer initialization, which will fork/exec to the
// provided process executable, a.k.a ourselves
logrus.Debug("initializing self-contained app")
factory, _ := libcontainer.New("")
err := factory.StartInitialization()
if err != nil {
logrus.Fatal("could not run self-contained app: ", err)
}
}
}
package selfcontain
import (
"errors"
"fmt"
"io/ioutil"
"net"
"os"
"path"
"strings"
"github.com/containernetworking/plugins/pkg/ns"
"github.com/containernetworking/plugins/pkg/utils/sysctl"
"github.com/opencontainers/runc/libcontainer/configs"
"github.com/vishvananda/netlink"
)
const ACCEPT_PINFO = "net.ipv6.conf.eth0.accept_ra_pinfo"
const ACCEPT_DFTRTR = "net.ipv6.conf.eth0.accept_ra_defrtr"
// Setup networking inside the container
// This must be called from outside the container, since it requires both access to the
// host networking stack and the namespace networking stack
func (c *Container) SetupNetworking() error {
ifaceName, err := c.setupIPVlan(c.config.Master, 1500)
if err != nil {
return err
}
netns, err := c.findNetNS()
if err != nil {
return err
}
err = netns.Do(func(_ ns.NetNS) error {
// Rename the interface
iface, err := netlink.LinkByName(ifaceName)
if err != nil {
return err
}
err = netlink.LinkSetName(iface, "eth0")
if err != nil {
return err
}
err = netlink.LinkSetUp(iface)
if err != nil {
return err
}
// Setup addresses and routes
err = setupAddress(iface, c.config.IP)
if err != nil {
return err
}
err = setupGw(iface, c.config.GW)
if err != nil {
return err
}
// Setup DNS
err = setupDNS(c.config.DNS, c.config.Root)
if err != nil {
return err
}
err = setupHosts(c.config.Root)
if err != nil {
return err
}
err = setupCerts(c.config.Root)
if err != nil {
return err
}
return nil
})
return err
}
func (c *Container) findNetNS() (ns.NetNS, error) {
nsPath, err := c.GetNS(configs.NEWNET)
if err != nil {
return nil, err
}
netns, err := ns.GetNS(nsPath)
if err != nil {
return nil, err
}
return netns, nil
}
func (c *Container) setupIPVlan(master string, mtu int) (string, error) {
tmpName := "vethtmp"
netns, err := c.findNetNS()
if err != nil {
return "", err
}
masterIface, err := netlink.LinkByName(master)
if err != nil {
return "", err
}
ipvlan := &netlink.IPVlan{
LinkAttrs: netlink.LinkAttrs{
MTU: mtu,
Name: tmpName,
ParentIndex: masterIface.Attrs().Index,
Namespace: netlink.NsFd(int(netns.Fd())),
},
Mode: netlink.IPVLAN_MODE_L2,
}
err = netlink.LinkAdd(ipvlan)
if err != nil {
return "", err
}
return tmpName, nil
}
func setupAddress(iface netlink.Link, ip net.IPNet) error {
// Accept router advertisement for addresses if required,
// otherwise use provided IP
accept_ra := "1"
if len(ip.IP) > 0 {
accept_ra = "0"
addr := &netlink.Addr{
IPNet: &ip,
}
err := netlink.AddrAdd(iface, addr)
if err != nil {
return err
}
}
_, err := sysctl.Sysctl(ACCEPT_PINFO, accept_ra)
return err
}
func setupGw(iface netlink.Link, gw net.IP) error {
// Accept router advertisement for default routes if required,
// otherwise use provided gateway
accept_ra := "1"
if len(gw) > 0 {
// First add a link-local route to the gateway, so that
// out-of-lan default routes are handled properly
bits := 8 * len(gw)
err := netlink.RouteAdd(&netlink.Route{
LinkIndex: iface.Attrs().Index,
Scope: netlink.SCOPE_LINK,
Dst: &net.IPNet{
IP: gw,
Mask: net.CIDRMask(bits, bits),
},
})
if err != nil {
return err
}
err = netlink.RouteAdd(&netlink.Route{
LinkIndex: iface.Attrs().Index,
Scope: netlink.SCOPE_UNIVERSE,
Dst: &net.IPNet{},
Gw: gw,
})
if err != nil {
return err
}
}
_, err := sysctl.Sysctl(ACCEPT_DFTRTR, accept_ra)
return err
}
func setupDNS(servers []net.IP, root string) error {
lines := make([]string, len(servers))
for i, server := range servers {
lines[i] = fmt.Sprintf("nameserver %s", server.String())
}
resolv := []byte(strings.Join(lines, "\n"))
err := os.MkdirAll(path.Join(root, "etc"), 0755)
if err != nil {
return err
}
err = ioutil.WriteFile(path.Join(root, "etc/resolv.conf"), resolv, 0644)
return err
}
func setupHosts(root string) error {
hosts := []byte("::1 localhost")
err := ioutil.WriteFile(path.Join(root, "etc/hosts"), hosts, 0644)
return err
}
func setupCerts(root string) error {
// This is bluntly copied from go x509 package, because it is not
// exported unfortunately
var certFiles = []string{
"/etc/ssl/certs/ca-certificates.crt", // Debian/Ubuntu/Gentoo etc.
"/etc/pki/tls/certs/ca-bundle.crt", // Fedora/RHEL 6
"/etc/ssl/ca-bundle.pem", // OpenSUSE
"/etc/pki/tls/cacert.pem", // OpenELEC
"/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem", // CentOS/RHEL 7
"/etc/ssl/cert.pem", // Alpine Linux
}
for _, file := range certFiles {
data, err := os.ReadFile(file)
if err != nil {
continue
}
dest := path.Join(root, file)
err = os.MkdirAll(path.Dir(dest), 0o755)
if err != nil {
return err
}
err = os.WriteFile(dest, data, 0o644)
if err != nil {
return err
}
return nil
}
return errors.New("no certificate available")
}
package selfcontain
import (
"os"
"os/signal"
"github.com/sirupsen/logrus"
)
type runnable func()
const argRunFun = "selfcontain-run-fun"
func RunFun(config *Config, f runnable) error {
// Run the function if we are indeed inside the container
for _, arg := range os.Args {
if arg == argRunFun {
logrus.Debug("we are now running inside the container")
Evacuate()
logrus.Debug("starting the main routine")
f()
return nil
}
}
// Otherwise containerize ourselves
logrus.Debug("setting up a new container")
c, err := New(config)
if err != nil {
return err
}
defer c.Destroy()
logrus.Debug("starting the container init process")
err = c.Start(append(os.Args, argRunFun))
if err != nil {
return err
}
logrus.Debug("setting up container networking")
err = c.SetupNetworking()
if err != nil {
return err
}
// Make sure we are notified and that we destroy the
// container upon being interrupted
s := make(chan os.Signal, 1)
signal.Notify(s, os.Interrupt)
go func() {
<-s
logrus.Debug("interrupt signal caught, tearing down")
c.Destroy()
}()
logrus.Debug("container is ready, handing over")
return c.Run()
}
// Evacuate cgroups, which is required for many in-container use cases
// Remaining in the root cgroup would prevent creating any domain sub-cgroup
func Evacuate() {
// Libcontainer cgroup manager is not designed for evacuation and will
// fail in such a case, so we are using cgroupfs directly, which is
// explicitely available due to defaults, and simple since we are
// the only running process at the moment
logrus.Debug("evacuating self to /selfcontain")
err := os.Mkdir("/sys/fs/cgroup/selfcontain", 0o755)
if err != nil && !os.IsExist(err) {
logrus.Fatal("could not create evacuation cgroup: ", err)
}
err = os.WriteFile("/sys/fs/cgroup/selfcontain/cgroup.procs", []byte("0"), 0o755)
if err != nil {
logrus.Fatal("could not evacuate self: ", err)
}
}
package sml
import (
"github.com/hashicorp/memberlist"
"github.com/sirupsen/logrus"
)
// Cluster implements the memberlist.Delegate interface
func (m *Memberlist[M, S, MP, SP]) NotifyConflict(node, other *memberlist.Node) {
}
// Cluter implements the memberlist.Delegate interface
func (m *Memberlist[M, S, MP, SP]) NodeMeta(limit int) []byte {
n, err := m.Meta.Encode()
if err != nil {
logrus.Warn("could not encode node metadata")
return []byte{}
}
return n
}
// Clutser implements the memberlist.Delegate interface
func (m *Memberlist[M, S, MP, SP]) NotifyMsg([]byte) {
}
// Cluster implements the memberlist.Delegate interface
func (m *Memberlist[M, S, MP, SP]) GetBroadcasts(overhead, limit int) [][]byte {
return nil
}
// Cluster implements the memberlist.Delegate interface
func (m *Memberlist[M, S, MP, SP]) LocalState(join bool) []byte {
s, err := m.State.Encode()
if err != nil {
logrus.Warn("could not encode local state")
return []byte{}
}
return s
}
// Clutser implements the memberlist.Delegate interface
func (m *Memberlist[M, S, MP, SP]) MergeRemoteState(buf []byte, join bool) {
logrus.Debug("merging remote state")
change, err := m.State.Merge(buf)
if err != nil {
logrus.Warn("could not merge remote state:", err)
}
if change {
m.nodeChanges <- struct{}{}
}
logrus.Debug(m.State.String())
}
// Cluster implements the EventDelegate interface
func (m *Memberlist[M, S, MP, SP]) NotifyJoin(n *memberlist.Node) {
logrus.Debug("node joined: ", n.Name)
m.nodeChanges <- struct{}{}
}
// Node implements the EventDelegate interface
func (m *Memberlist[M, S, MP, SP]) NotifyLeave(n *memberlist.Node) {
m.nodeChanges <- struct{}{}
}
// Node implements the EventDelegate interface
func (m *Memberlist[M, S, MP, SP]) NotifyUpdate(n *memberlist.Node) {
m.nodeChanges <- struct{}{}
}
package sml
type Instrumentation interface {
// Instrumentation data was updated
Updates() <-chan struct{}
// Get the current minimum observed path MTU with any other node in the
// cluster, useful for setting cluster-wide MTU
MinMTU() int
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.