node/core: add sysctls

Change-Id: I47b0d639a62f73f134430c5164a35eef2b5622d7
Reviewed-on: https://review.monogon.dev/c/monogon/+/2273
Tested-by: Jenkins CI
Reviewed-by: Lorenz Brun <lorenz@monogon.tech>
diff --git a/metropolis/pkg/sysctl/BUILD.bazel b/metropolis/pkg/sysctl/BUILD.bazel
new file mode 100644
index 0000000..a945a03
--- /dev/null
+++ b/metropolis/pkg/sysctl/BUILD.bazel
@@ -0,0 +1,8 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "sysctl",
+    srcs = ["options.go"],
+    importpath = "source.monogon.dev/metropolis/pkg/sysctl",
+    visibility = ["//visibility:public"],
+)
diff --git a/metropolis/pkg/sysctl/options.go b/metropolis/pkg/sysctl/options.go
new file mode 100644
index 0000000..b5e1e36
--- /dev/null
+++ b/metropolis/pkg/sysctl/options.go
@@ -0,0 +1,29 @@
+package sysctl
+
+import (
+	"fmt"
+	"os"
+	"path"
+	"strings"
+)
+
+// Options contains sysctl options to apply
+type Options map[string]string
+
+// Apply attempts to apply all options in Options. It aborts on the first
+// one which returns an error when applying.
+func (o Options) Apply() error {
+	for name, value := range o {
+		filePath := path.Join("/proc/sys/", strings.ReplaceAll(name, ".", "/"))
+		optionFile, err := os.OpenFile(filePath, os.O_WRONLY, 0)
+		if err != nil {
+			return fmt.Errorf("failed to set option %v: %w", name, err)
+		}
+		if _, err := optionFile.WriteString(value + "\n"); err != nil {
+			optionFile.Close()
+			return fmt.Errorf("failed to set option %v: %w", name, err)
+		}
+		optionFile.Close() // In a loop, defer'ing could open a lot of FDs
+	}
+	return nil
+}