diff --git a/plt-build/src/main/kotlin/moe/rosa/planterette/Main.kt b/plt-build/src/main/kotlin/moe/rosa/planterette/Main.kt deleted file mode 100644 index aba9c1a..0000000 --- a/plt-build/src/main/kotlin/moe/rosa/planterette/Main.kt +++ /dev/null @@ -1,2 +0,0 @@ -package moe.rosa.planterette - diff --git a/plt-build/src/main/kotlin/moe/rosa/planterette/PlanteretteConfig.kt b/plt-build/src/main/kotlin/moe/rosa/planterette/PlanteretteConfig.kt index 709c92b..7f8ebb6 100644 --- a/plt-build/src/main/kotlin/moe/rosa/planterette/PlanteretteConfig.kt +++ b/plt-build/src/main/kotlin/moe/rosa/planterette/PlanteretteConfig.kt @@ -2,4 +2,8 @@ package moe.rosa.planterette import moe.rosa.planterette.hakurei.HakureiConfig +/** + * Represents a Planterette build configuration. + * @param hakurei Hakurei container configuration for the application. + */ data class PlanteretteConfig(var hakurei: HakureiConfig?) \ No newline at end of file diff --git a/plt-build/src/main/kotlin/moe/rosa/planterette/dsl/HakureiDSL.kt b/plt-build/src/main/kotlin/moe/rosa/planterette/dsl/HakureiDSL.kt index 01f50ff..af7d2cc 100644 --- a/plt-build/src/main/kotlin/moe/rosa/planterette/dsl/HakureiDSL.kt +++ b/plt-build/src/main/kotlin/moe/rosa/planterette/dsl/HakureiDSL.kt @@ -44,11 +44,7 @@ annotation class FSOverlayDSL fun PlanteretteConfig.hakurei(id: String, init: @HakureiDSL HakureiConfig.() -> Unit) { this.hakurei = HakureiConfig(id).apply(init) } -@HakureiDSL -fun HakureiConfig.executable(path: String, vararg args: String) { - this.path = AbsolutePath(path) - this.args = args.toList() -} + @HakureiDSL enum class DSLEnablements { Wayland, @@ -73,18 +69,7 @@ fun HakureiConfig.enable(vararg enablements: DSLEnablements) { fun HakureiConfig.directWayland(directWayland: Boolean = true) { this.directWayland = directWayland } -@HakureiDSL -fun HakureiConfig.username(username: String) { - this.username = username -} -@HakureiDSL -fun HakureiConfig.shell(shell: String) { - this.shell = AbsolutePath(shell) -} -@HakureiDSL -fun HakureiConfig.home(home: String) { - this.home = AbsolutePath(home) -} + //TODO(mae) automatic identity? @HakureiDSL fun HakureiConfig.identity(identity: Int? = null) { @@ -237,7 +222,23 @@ fun ContainerConfig.mapRealUid(mapRealUid: Boolean = true) { fun ContainerConfig.device(device: Boolean = true) { this.device = device } - +@ContainerDSL +fun ContainerConfig.username(username: String) { + this.username = username +} +@ContainerDSL +fun ContainerConfig.shell(shell: String) { + this.shell = AbsolutePath(shell) +} +@ContainerDSL +fun ContainerConfig.home(home: String) { + this.home = AbsolutePath(home) +} +@ContainerDSL +fun ContainerConfig.executable(path: String, vararg args: String) { + this.path = AbsolutePath(path) + this.args = args.toList() +} @FilesystemDSL data class FilesystemConfigs(val configs: MutableList = mutableListOf()) diff --git a/plt-build/src/main/kotlin/moe/rosa/planterette/hakurei/Filesystem.kt b/plt-build/src/main/kotlin/moe/rosa/planterette/hakurei/Filesystem.kt index 1aaa5c3..5a486e8 100644 --- a/plt-build/src/main/kotlin/moe/rosa/planterette/hakurei/Filesystem.kt +++ b/plt-build/src/main/kotlin/moe/rosa/planterette/hakurei/Filesystem.kt @@ -6,9 +6,70 @@ import kotlinx.serialization.encoding.* import java.nio.file.Path + /** - * AbsolutePath holds a pathname checked to be absolute. - * @constructor checks pathname and returns a new AbsolutePath if pathname is absolute. + * Points to the file system root. + */ +val ROOT = AbsolutePath("/") + +/** + * Points to the directory for system-specific configuration. + */ +val ETC = AbsolutePath("/etc") + +/** + * Points to the place for small temporary files. + */ +val TMP = AbsolutePath("/tmp") + +/** + * Points to a "tmpfs" file system for system packages to place runtime data, socket files, and similar. + */ +val RUN = AbsolutePath("/run") + +/** + * Points to a directory containing per-user runtime directories, + * each usually individually mounted "tmpfs" instances. + */ +val RUN_USER: AbsolutePath = RUN + "user/" + +/** + * Points to persistent, variable system data. Writable during normal system operation. + */ +val VAR = AbsolutePath("/var/") + +/** + * Points to persistent system data. + */ +val VAR_LIB: AbsolutePath = VAR + "lib/" + +/** + * Points to a nonstandard directory that is usually empty. + */ +val VAR_EMPTY: AbsolutePath = VAR + "empty/" + +/** + * Points to the root directory for device nodes. + */ +val DEV = AbsolutePath("/dev/") + +/** + * Points to a virtual kernel file system exposing the process list and other functionality. + */ +val PROC = AbsolutePath("/proc/") + +/** + * Points to a hierarchy below `/proc/` that exposes a number of kernel tunables. + */ +val PROC_SYS: AbsolutePath = PROC + "sys/" + +/** + * Points to a virtual kernel file system exposing discovered devices and other functionality. + */ +val SYS = AbsolutePath("/sys") +/** + * Holds a pathname checked to be absolute. + * @constructor checks pathname and returns a new [AbsolutePath] if pathname is absolute. */ @Serializable(with = AbsolutePathSerializer::class) data class AbsolutePath(val pathname: String, @Transient val path: Path = Path.of(pathname)) { @@ -17,16 +78,19 @@ data class AbsolutePath(val pathname: String, @Transient val path: Path = Path.o throw AbsolutePathException(pathname) } } + //TODO discuss if we should keep this operator overloading around, i think it makes things cleaner but ik ozy doesn't like operator overloading operator fun plus(other: String): AbsolutePath { return AbsolutePath(pathname + other) } + operator fun plus(other: AbsolutePath): AbsolutePath { + return AbsolutePath(pathname + other.pathname) + } companion object { fun isAbsolute(pathname: String): Boolean { return Path.of(pathname).isAbsolute } } } - object AbsolutePathSerializer : KSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(this::class.qualifiedName!!, PrimitiveKind.STRING) override fun serialize(encoder: Encoder, value: AbsolutePath) { @@ -40,12 +104,24 @@ object AbsolutePathSerializer : KSerializer { } /** - * AbsolutePathException is returned by @see AbsolutePath() and holds the invalid pathname. + * Returned by [AbsolutePath()] and holds the invalid pathname. */ data class AbsolutePathException(val pathname: String) : IllegalArgumentException("Path $pathname is not absolute") @Serializable sealed interface FilesystemConfig +/** + * Represents a host to container bind mount. + * @param target mount point in container, same as source if empty + * @param source host filesystem path to make available to the container + * @param write do not mount target read only + * @param device do not disable device files on target, implies write + * @param ensure create source as a directory if it does not exist + * @param optional skip this mount point if source does not exist + * @param special enable special behavior: + * for autoroot, target must be set to [Filesystem.ROOT]; + * for autoetc, target must be set to [Filesystem.ETC] + */ @Serializable @SerialName("bind") data class FSBind( @@ -58,6 +134,13 @@ data class FSBind( val special: Boolean? = null, ) : FilesystemConfig +/** + * Represents an ephemeral (temporary) container mount point. + * @param target mount point in container + * @param write do not mount filesystem read-only + * @param size upper limit on the size of the filesystem + * @param perm initial permission bits of the new filesystem + */ @Serializable @SerialName("ephemeral") data class FSEphemeral( @@ -67,6 +150,12 @@ data class FSEphemeral( val perm: Int, ) : FilesystemConfig +/** + * Represents a symlink in the container filesystem. + * @param target link path in container + * @param linkname linkname the symlink points to + * @param dereference whether to dereference linkname before creating the link + */ @Serializable @SerialName("link") data class FSLink( @@ -75,11 +164,18 @@ data class FSLink( val dereference: Boolean, ) : FilesystemConfig +/** + * Represents an overlay mount point. + * @param target mount point in container + * @param lower any filesystem, does not need to be on a writable filesystem + * @param upper the upperdir is normally on a writable filesystem, leave as null to mount Lower readonly + * @param work the workdir needs to be an empty directory on the same filesystem as `upper`, must not be null if `upper` is populated + */ @Serializable @SerialName("overlay") data class FSOverlay( @SerialName("dst") val target: AbsolutePath, val lower: List, - val upper: AbsolutePath, - val work: AbsolutePath, + val upper: AbsolutePath? = null, + val work: AbsolutePath? = null, ) : FilesystemConfig \ No newline at end of file diff --git a/plt-build/src/main/kotlin/moe/rosa/planterette/hakurei/Hakurei.kt b/plt-build/src/main/kotlin/moe/rosa/planterette/hakurei/Hakurei.kt index b3528d1..f46a574 100644 --- a/plt-build/src/main/kotlin/moe/rosa/planterette/hakurei/Hakurei.kt +++ b/plt-build/src/main/kotlin/moe/rosa/planterette/hakurei/Hakurei.kt @@ -1,19 +1,38 @@ package moe.rosa.planterette.hakurei import kotlinx.serialization.* +import java.time.Duration +val WAIT_DELAY_DEFAULT = Duration.ofSeconds(1)!! +val WAIT_DELAY_MAX = Duration.ofSeconds(30)!! + +const val IDENTITY_MIN = 0 +const val IDENTITY_MAX = 9999 + +/** + * [HakureiConfig] configures an application container. + * @param id Reverse-DNS style configured arbitrary identifier string. + * Passed to wayland security-context-v1 and used as part of defaults in dbus session proxy. + * @param enablements System services to make available in the container. + * @param sessionBus Session D-Bus proxy configuration. + * If set to null, session bus proxy assume built-in defaults. + * @param systemBus System D-Bus proxy configuration. + * If set to nil, system bus proxy is disabled. + * @param directWayland Direct access to wayland socket, no attempt is made to attach security-context-v1 + * and the bare socket is made available to the container. + * @param extraPerms Extra acl update ops to perform before setuid. + * @param identity Numerical application id, passed to hsu, used to derive init user namespace credentials. + * @param groups Init user namespace supplementary groups inherited by all container processes. + * @param container High level container configuration. + */ @Serializable data class HakureiConfig( var id: String? = null, - var path: AbsolutePath? = null, - var args: List? = null, + var enablements: Enablements? = null, @SerialName("session_bus") var sessionBus: DBusConfig? = null, @SerialName("system_bus") var systemBus: DBusConfig? = null, @SerialName("direct_wayland") var directWayland: Boolean? = null, - var username: String? = null, - var shell: AbsolutePath? = null, - var home: AbsolutePath? = null, @SerialName("extra_perms") var extraPerms: List? = null, var identity: Int? = null, @@ -22,6 +41,36 @@ data class HakureiConfig( var container: ContainerConfig? = null, ) +/** + * Describes the container configuration to be applied to the container. + * @param hostname Container UTS namespace hostname. + * @param waitDelay Duration in nanoseconds to wait for after interrupting the initial process. + * Defaults to [WAIT_DELAY_DEFAULT] if less than or equals to zero, + * or [WAIT_DELAY_MAX] if greater than [WAIT_DELAY_MAX]. + * + * @param seccompCompat Emit Flatpak-compatible seccomp filter programs. + * @param devel Allow ptrace and friends. + * @param userns Allow userns creation and container setup syscalls. + * @param hostNet Share host net namespace. + * @param hostAbstract Share abstract unix socket scope. + * @param tty Allow dangerous terminal I/O (faking input). + * @param multiarch Allow multiarch. + * + * @param env Initial process environment variables. + * + * @param mapRealUid Map target user uid to privileged user uid in the container user namespace. + * Some programs fail to connect to dbus session running as a different uid, + * this option works around it by mapping priv-side caller uid in container. + * + * @param device Mount `/dev/` from the init mount namespace as-is in the container mount namespace. + * @param filesystem Container mount points. + * If the first element targets /, it is inserted early and excluded from path hiding. + * @param username String used as the username of the emulated user, validated against the default `NAME_REGEX` from adduser. + * @param shell Pathname of shell in the container filesystem to use for the emulated user. + * @param home Directory in the container filesystem to enter and use as the home directory of the emulated user. + * @param path Pathname to executable file in the container filesystem. + * @param args Final args passed to the initial program. + */ @Serializable data class ContainerConfig( var hostname: String? = null, @@ -40,8 +89,17 @@ data class ContainerConfig( var device: Boolean? = null, var filesystem: List? = null, + + var username: String? = "chronos", + var shell: AbsolutePath? = null, + var home: AbsolutePath? = null, + var path: AbsolutePath? = null, + var args: List? = null, ) +/** + * Describes an acl update op. + */ @Serializable data class ExtraPermsConfig( var ensure: Boolean? = null, @@ -71,6 +129,16 @@ data class ExtraPermsConfig( } } +/** + * Configures the `xdg-dbus-proxy` process. + * @param see Set `see` policy for `NAME` (`--see=NAME`) + * @param talk Set `talk` policy for `NAME` (`--talk=NAME`) + * @param own Set `own` policy for `NAME` (`--own=NAME) + * @param call Set `RULE` for calls on `NAME` (`--call=NAME=RULE`) + * @param broadcast Set `RULE` for broadcasts from `NAME` (`--broadcast=NAME=RULE`) + * @param log Turn on logging (`--log`) + * @param filter Enable filtering (`--filter`) + */ @Serializable data class DBusConfig( var see: List? = null, @@ -82,6 +150,9 @@ data class DBusConfig( var filter: Boolean? = null, ) +/** + * Represents an optional host service to export to the target user. + */ @Serializable data class Enablements( var wayland: Boolean? = null, diff --git a/plt-build/src/test/kotlin/DSLTest.kt b/plt-build/src/test/kotlin/DSLTest.kt index 0044dab..c870af1 100644 --- a/plt-build/src/test/kotlin/DSLTest.kt +++ b/plt-build/src/test/kotlin/DSLTest.kt @@ -7,13 +7,7 @@ class DSLTest { companion object { val HAKUREI_DSL_TEST = planterette { hakurei("org.chromium.Chromium") { - executable("/run/current-system/sw/bin/chromium", - "chromium", - "--ignore-gpu-blocklist", - "--disable-smooth-scrolling", - "--enable-features=UseOzonePlatform", - "--ozone-platform=wayland" - ) + enable(Wayland, DBus, Pulse) dbus { session { @@ -38,9 +32,7 @@ class DSLTest { filter() } } - username("chronos") - shell("/run/current-system/sw/bin/zsh") - home("/data/data/org.chromium.Chromium") + extraPerms( perm("/var/lib/hakurei/u0") { ensure() @@ -67,6 +59,16 @@ class DSLTest { "GOOGLE_DEFAULT_CLIENT_SECRET" to "OTJgUOQcT7lO7GsGZq2G4IlT") mapRealUid() device() + executable("/run/current-system/sw/bin/chromium", + "chromium", + "--ignore-gpu-blocklist", + "--disable-smooth-scrolling", + "--enable-features=UseOzonePlatform", + "--ozone-platform=wayland" + ) + username("chronos") + shell("/run/current-system/sw/bin/zsh") + home("/data/data/org.chromium.Chromium") filesystem { bind("/var/lib/hakurei/base/org.debian" to "/") { write() @@ -80,11 +82,10 @@ class DSLTest { perm(493) } overlay("/nix/store") { - lower("/mnt-root/nix/.ro-store") - upper("/mnt-root/nix/.rw-store/upper") - work("/mnt-root/nix/.rw-store/work") + lower("/var/lib/hakurei/base/org.nixos/ro-store") + upper("/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/upper") + work("/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/work") } - bind("/nix/store") link("/run/current-system") { dereference() } diff --git a/plt-build/src/test/kotlin/HakureiTest.kt b/plt-build/src/test/kotlin/HakureiTest.kt index ad5537f..e3347d0 100644 --- a/plt-build/src/test/kotlin/HakureiTest.kt +++ b/plt-build/src/test/kotlin/HakureiTest.kt @@ -8,14 +8,7 @@ class HakureiTest { companion object { val TEMPLATE_DATA = HakureiConfig( id = "org.chromium.Chromium", - path = AbsolutePath("/run/current-system/sw/bin/chromium"), - args = listOf( - "chromium", - "--ignore-gpu-blocklist", - "--disable-smooth-scrolling", - "--enable-features=UseOzonePlatform", - "--ozone-platform=wayland" - ), + enablements = Enablements( wayland = true, dbus = true, @@ -57,9 +50,7 @@ class HakureiTest { broadcast = null, filter = true ), - username = "chronos", - shell = AbsolutePath("/run/current-system/sw/bin/zsh"), - home = AbsolutePath("/data/data/org.chromium.Chromium"), + extraPerms = listOf( ExtraPermsConfig( ensure = true, @@ -119,13 +110,10 @@ class HakureiTest { FSOverlay( target = AbsolutePath("/nix/store"), lower = listOf( - AbsolutePath("/mnt-root/nix/.ro-store") + AbsolutePath("/var/lib/hakurei/base/org.nixos/ro-store") ), - upper = AbsolutePath("/mnt-root/nix/.rw-store/upper"), - work = AbsolutePath("/mnt-root/nix/.rw-store/work") - ), - FSBind( - source = AbsolutePath("/nix/store") + upper = AbsolutePath("/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/upper"), + work = AbsolutePath("/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/work") ), FSLink( target = AbsolutePath("/run/current-system"), @@ -148,7 +136,19 @@ class HakureiTest { device = true, optional = true ) - ) + ), + + username = "chronos", + shell = AbsolutePath("/run/current-system/sw/bin/zsh"), + home = AbsolutePath("/data/data/org.chromium.Chromium"), + path = AbsolutePath("/run/current-system/sw/bin/chromium"), + args = listOf( + "chromium", + "--ignore-gpu-blocklist", + "--disable-smooth-scrolling", + "--enable-features=UseOzonePlatform", + "--ozone-platform=wayland" + ), ) ) val TEMPLATE_JSON = ProcessBuilder("hakurei", "template")