commit
2771375d6e
@ -0,0 +1,199 @@ |
||||
{ config, pkgs, lib, ... }: |
||||
|
||||
let |
||||
toplevelConfig = config; |
||||
inherit (lib) types; |
||||
inherit (import ../system/boot/systemd-lib.nix { |
||||
inherit config pkgs lib; |
||||
}) mkPathSafeName; |
||||
in { |
||||
options.systemd.services = lib.mkOption { |
||||
type = types.attrsOf (types.submodule ({ name, config, ... }: { |
||||
options.confinement.enable = lib.mkOption { |
||||
type = types.bool; |
||||
default = false; |
||||
description = '' |
||||
If set, all the required runtime store paths for this service are |
||||
bind-mounted into a <literal>tmpfs</literal>-based <citerefentry> |
||||
<refentrytitle>chroot</refentrytitle> |
||||
<manvolnum>2</manvolnum> |
||||
</citerefentry>. |
||||
''; |
||||
}; |
||||
|
||||
options.confinement.fullUnit = lib.mkOption { |
||||
type = types.bool; |
||||
default = false; |
||||
description = '' |
||||
Whether to include the full closure of the systemd unit file into the |
||||
chroot, instead of just the dependencies for the executables. |
||||
|
||||
<warning><para>While it may be tempting to just enable this option to |
||||
make things work quickly, please be aware that this might add paths |
||||
to the closure of the chroot that you didn't anticipate. It's better |
||||
to use <option>confinement.packages</option> to <emphasis |
||||
role="strong">explicitly</emphasis> add additional store paths to the |
||||
chroot.</para></warning> |
||||
''; |
||||
}; |
||||
|
||||
options.confinement.packages = lib.mkOption { |
||||
type = types.listOf (types.either types.str types.package); |
||||
default = []; |
||||
description = let |
||||
mkScOption = optName: "<option>serviceConfig.${optName}</option>"; |
||||
in '' |
||||
Additional packages or strings with context to add to the closure of |
||||
the chroot. By default, this includes all the packages from the |
||||
${lib.concatMapStringsSep ", " mkScOption [ |
||||
"ExecReload" "ExecStartPost" "ExecStartPre" "ExecStop" |
||||
"ExecStopPost" |
||||
]} and ${mkScOption "ExecStart"} options. If you want to have all the |
||||
dependencies of this systemd unit, you can use |
||||
<option>confinement.fullUnit</option>. |
||||
|
||||
<note><para>The store paths listed in <option>path</option> are |
||||
<emphasis role="strong">not</emphasis> included in the closure as |
||||
well as paths from other options except those listed |
||||
above.</para></note> |
||||
''; |
||||
}; |
||||
|
||||
options.confinement.binSh = lib.mkOption { |
||||
type = types.nullOr types.path; |
||||
default = toplevelConfig.environment.binsh; |
||||
defaultText = "config.environment.binsh"; |
||||
example = lib.literalExample "\${pkgs.dash}/bin/dash"; |
||||
description = '' |
||||
The program to make available as <filename>/bin/sh</filename> inside |
||||
the chroot. If this is set to <literal>null</literal>, no |
||||
<filename>/bin/sh</filename> is provided at all. |
||||
|
||||
This is useful for some applications, which for example use the |
||||
<citerefentry> |
||||
<refentrytitle>system</refentrytitle> |
||||
<manvolnum>3</manvolnum> |
||||
</citerefentry> library function to execute commands. |
||||
''; |
||||
}; |
||||
|
||||
options.confinement.mode = lib.mkOption { |
||||
type = types.enum [ "full-apivfs" "chroot-only" ]; |
||||
default = "full-apivfs"; |
||||
description = '' |
||||
The value <literal>full-apivfs</literal> (the default) sets up |
||||
private <filename class="directory">/dev</filename>, <filename |
||||
class="directory">/proc</filename>, <filename |
||||
class="directory">/sys</filename> and <filename |
||||
class="directory">/tmp</filename> file systems in a separate user |
||||
name space. |
||||
|
||||
If this is set to <literal>chroot-only</literal>, only the file |
||||
system name space is set up along with the call to <citerefentry> |
||||
<refentrytitle>chroot</refentrytitle> |
||||
<manvolnum>2</manvolnum> |
||||
</citerefentry>. |
||||
|
||||
<note><para>This doesn't cover network namespaces and is solely for |
||||
file system level isolation.</para></note> |
||||
''; |
||||
}; |
||||
|
||||
config = let |
||||
rootName = "${mkPathSafeName name}-chroot"; |
||||
inherit (config.confinement) binSh fullUnit; |
||||
wantsAPIVFS = lib.mkDefault (config.confinement.mode == "full-apivfs"); |
||||
in lib.mkIf config.confinement.enable { |
||||
serviceConfig = { |
||||
RootDirectory = pkgs.runCommand rootName {} "mkdir \"$out\""; |
||||
TemporaryFileSystem = "/"; |
||||
PrivateMounts = lib.mkDefault true; |
||||
|
||||
# https://github.com/NixOS/nixpkgs/issues/14645 is a future attempt |
||||
# to change some of these to default to true. |
||||
# |
||||
# If we run in chroot-only mode, having something like PrivateDevices |
||||
# set to true by default will mount /dev within the chroot, whereas |
||||
# with "chroot-only" it's expected that there are no /dev, /proc and |
||||
# /sys file systems available. |
||||
# |
||||
# However, if this suddenly becomes true, the attack surface will |
||||
# increase, so let's explicitly set these options to true/false |
||||
# depending on the mode. |
||||
MountAPIVFS = wantsAPIVFS; |
||||
PrivateDevices = wantsAPIVFS; |
||||
PrivateTmp = wantsAPIVFS; |
||||
PrivateUsers = wantsAPIVFS; |
||||
ProtectControlGroups = wantsAPIVFS; |
||||
ProtectKernelModules = wantsAPIVFS; |
||||
ProtectKernelTunables = wantsAPIVFS; |
||||
}; |
||||
confinement.packages = let |
||||
execOpts = [ |
||||
"ExecReload" "ExecStart" "ExecStartPost" "ExecStartPre" "ExecStop" |
||||
"ExecStopPost" |
||||
]; |
||||
execPkgs = lib.concatMap (opt: let |
||||
isSet = config.serviceConfig ? ${opt}; |
||||
in lib.optional isSet config.serviceConfig.${opt}) execOpts; |
||||
unitAttrs = toplevelConfig.systemd.units."${name}.service"; |
||||
allPkgs = lib.singleton (builtins.toJSON unitAttrs); |
||||
unitPkgs = if fullUnit then allPkgs else execPkgs; |
||||
in unitPkgs ++ lib.optional (binSh != null) binSh; |
||||
}; |
||||
})); |
||||
}; |
||||
|
||||
config.assertions = lib.concatLists (lib.mapAttrsToList (name: cfg: let |
||||
whatOpt = optName: "The 'serviceConfig' option '${optName}' for" |
||||
+ " service '${name}' is enabled in conjunction with" |
||||
+ " 'confinement.enable'"; |
||||
in lib.optionals cfg.confinement.enable [ |
||||
{ assertion = !cfg.serviceConfig.RootDirectoryStartOnly or false; |
||||
message = "${whatOpt "RootDirectoryStartOnly"}, but right now systemd" |
||||
+ " doesn't support restricting bind-mounts to 'ExecStart'." |
||||
+ " Please either define a separate service or find a way to run" |
||||
+ " commands other than ExecStart within the chroot."; |
||||
} |
||||
{ assertion = !cfg.serviceConfig.DynamicUser or false; |
||||
message = "${whatOpt "DynamicUser"}. Please create a dedicated user via" |
||||
+ " the 'users.users' option instead as this combination is" |
||||
+ " currently not supported."; |
||||
} |
||||
]) config.systemd.services); |
||||
|
||||
config.systemd.packages = lib.concatLists (lib.mapAttrsToList (name: cfg: let |
||||
rootPaths = let |
||||
contents = lib.concatStringsSep "\n" cfg.confinement.packages; |
||||
in pkgs.writeText "${mkPathSafeName name}-string-contexts.txt" contents; |
||||
|
||||
chrootPaths = pkgs.runCommand "${mkPathSafeName name}-chroot-paths" { |
||||
closureInfo = pkgs.closureInfo { inherit rootPaths; }; |
||||
serviceName = "${name}.service"; |
||||
excludedPath = rootPaths; |
||||
} '' |
||||
mkdir -p "$out/lib/systemd/system" |
||||
serviceFile="$out/lib/systemd/system/$serviceName" |
||||
|
||||
echo '[Service]' > "$serviceFile" |
||||
|
||||
# /bin/sh is special here, because the option value could contain a |
||||
# symlink and we need to properly resolve it. |
||||
${lib.optionalString (cfg.confinement.binSh != null) '' |
||||
binsh=${lib.escapeShellArg cfg.confinement.binSh} |
||||
realprog="$(readlink -e "$binsh")" |
||||
echo "BindReadOnlyPaths=$realprog:/bin/sh" >> "$serviceFile" |
||||
''} |
||||
|
||||
while read storePath; do |
||||
if [ -L "$storePath" ]; then |
||||
# Currently, systemd can't cope with symlinks in Bind(ReadOnly)Paths, |
||||
# so let's just bind-mount the target to that location. |
||||
echo "BindReadOnlyPaths=$(readlink -e "$storePath"):$storePath" |
||||
elif [ "$storePath" != "$excludedPath" ]; then |
||||
echo "BindReadOnlyPaths=$storePath" |
||||
fi |
||||
done < "$closureInfo/store-paths" >> "$serviceFile" |
||||
''; |
||||
in lib.optional cfg.confinement.enable chrootPaths) config.systemd.services); |
||||
} |
@ -0,0 +1,60 @@ |
||||
{ config, pkgs, lib, ... }: |
||||
|
||||
let |
||||
cfg = config.services.mailcatcher; |
||||
|
||||
inherit (lib) mkEnableOption mkIf mkOption types; |
||||
in |
||||
{ |
||||
# interface |
||||
|
||||
options = { |
||||
|
||||
services.mailcatcher = { |
||||
enable = mkEnableOption "Enable MailCatcher."; |
||||
|
||||
http.ip = mkOption { |
||||
type = types.str; |
||||
default = "127.0.0.1"; |
||||
description = "The ip address of the http server."; |
||||
}; |
||||
|
||||
http.port = mkOption { |
||||
type = types.port; |
||||
default = 1080; |
||||
description = "The port address of the http server."; |
||||
}; |
||||
|
||||
smtp.ip = mkOption { |
||||
type = types.str; |
||||
default = "127.0.0.1"; |
||||
description = "The ip address of the smtp server."; |
||||
}; |
||||
|
||||
smtp.port = mkOption { |
||||
type = types.port; |
||||
default = 1025; |
||||
description = "The port address of the smtp server."; |
||||
}; |
||||
}; |
||||
|
||||
}; |
||||
|
||||
# implementation |
||||
|
||||
config = mkIf cfg.enable { |
||||
environment.systemPackages = [ pkgs.mailcatcher ]; |
||||
|
||||
systemd.services.mailcatcher = { |
||||
description = "MailCatcher Service"; |
||||
after = [ "network.target" ]; |
||||
wantedBy = [ "multi-user.target" ]; |
||||
|
||||
serviceConfig = { |
||||
DynamicUser = true; |
||||
Restart = "always"; |
||||
ExecStart = "${pkgs.mailcatcher}/bin/mailcatcher --foreground --no-quit --http-ip ${cfg.http.ip} --http-port ${toString cfg.http.port} --smtp-ip ${cfg.smtp.ip} --smtp-port ${toString cfg.smtp.port}"; |
||||
}; |
||||
}; |
||||
}; |
||||
} |
@ -0,0 +1,118 @@ |
||||
{ config, pkgs, lib, ... }: |
||||
|
||||
let |
||||
|
||||
cfg = config.services.quicktun; |
||||
|
||||
in |
||||
|
||||
with lib; |
||||
|
||||
{ |
||||
options = { |
||||
|
||||
services.quicktun = mkOption { |
||||
default = { }; |
||||
description = "QuickTun tunnels"; |
||||
type = types.attrsOf (types.submodule { |
||||
options = { |
||||
tunMode = mkOption { |
||||
type = types.int; |
||||
default = 0; |
||||
example = 1; |
||||
description = ""; |
||||
}; |
||||
|
||||
remoteAddress = mkOption { |
||||
type = types.str; |
||||
example = "tunnel.example.com"; |
||||
description = ""; |
||||
}; |
||||
|
||||
localAddress = mkOption { |
||||
type = types.str; |
||||
example = "0.0.0.0"; |
||||
description = ""; |
||||
}; |
||||
|
||||
localPort = mkOption { |
||||
type = types.int; |
||||
default = 2998; |
||||
description = ""; |
||||
}; |
||||
|
||||
remotePort = mkOption { |
||||
type = types.int; |
||||
default = 2998; |
||||
description = ""; |
||||
}; |
||||
|
||||
remoteFloat = mkOption { |
||||
type = types.int; |
||||
default = 0; |
||||
description = ""; |
||||
}; |
||||
|
||||
protocol = mkOption { |
||||
type = types.str; |
||||
default = "nacltai"; |
||||
description = ""; |
||||
}; |
||||
|
||||
privateKey = mkOption { |
||||
type = types.str; |
||||
description = ""; |
||||
}; |
||||
|
||||
publicKey = mkOption { |
||||
type = types.str; |
||||
description = ""; |
||||
}; |
||||
|
||||
timeWindow = mkOption { |
||||
type = types.int; |
||||
default = 5; |
||||
description = ""; |
||||
}; |
||||
|
||||
upScript = mkOption { |
||||
type = types.lines; |
||||
default = ""; |
||||
description = ""; |
||||
}; |
||||
}; |
||||
}); |
||||
}; |
||||
|
||||
}; |
||||
|
||||
config = mkIf (cfg != []) { |
||||
systemd.services = fold (a: b: a // b) {} ( |
||||
mapAttrsToList (name: qtcfg: { |
||||
"quicktun-${name}" = { |
||||
wantedBy = [ "multi-user.target" ]; |
||||
after = [ "network.target" ]; |
||||
environment = { |
||||
"INTERFACE" = name; |
||||
"TUN_MODE" = toString qtcfg.tunMode; |
||||
"REMOTE_ADDRESS" = qtcfg.remoteAddress; |
||||
"LOCAL_ADDRESS" = qtcfg.localAddress; |
||||
"LOCAL_PORT" = toString qtcfg.localPort; |
||||
"REMOTE_PORT" = toString qtcfg.remotePort; |
||||
"REMOTE_FLOAT" = toString qtcfg.remoteFloat; |
||||
"PRIVATE_KEY" = qtcfg.privateKey; |
||||
"PUBLIC_KEY" = qtcfg.publicKey; |
||||
"TIME_WINDOW" = toString qtcfg.timeWindow; |
||||
"TUN_UP_SCRIPT" = pkgs.writeScript "quicktun-${name}-up.sh" qtcfg.upScript; |
||||
"SUID" = "nobody"; |
||||
}; |
||||
serviceConfig = { |
||||
Type = "simple"; |
||||
ExecStart = "${pkgs.quicktun}/bin/quicktun.${qtcfg.protocol}"; |
||||
}; |
||||
}; |
||||
}) cfg |
||||
); |
||||
}; |
||||
|
||||
} |
@ -0,0 +1,233 @@ |
||||
{ config, lib, pkgs, ... }: |
||||
|
||||
with lib; |
||||
let |
||||
cfg = config.docker-containers; |
||||
|
||||
dockerContainer = |
||||
{ name, config, ... }: { |
||||
|
||||
options = { |
||||
|
||||
image = mkOption { |
||||
type = types.str; |
||||
description = "Docker image to run."; |
||||
example = "library/hello-world"; |
||||
}; |
||||
|
||||
cmd = mkOption { |
||||
type = with types; listOf str; |
||||
default = []; |
||||
description = "Commandline arguments to pass to the image's entrypoint."; |
||||
example = literalExample '' |
||||
["--port=9000"] |
||||
''; |
||||
}; |
||||
|
||||
entrypoint = mkOption { |
||||
type = with types; nullOr str; |
||||
description = "Overwrite the default entrypoint of the image."; |
||||
default = null; |
||||
example = "/bin/my-app"; |
||||
}; |
||||
|
||||
environment = mkOption { |
||||
type = with types; attrsOf str; |
||||
default = {}; |
||||
description = "Environment variables to set for this container."; |
||||
example = literalExample '' |
||||
{ |
||||
DATABASE_HOST = "db.example.com"; |
||||
DATABASE_PORT = "3306"; |
||||
} |
||||
''; |
||||
}; |
||||
|
||||
log-driver = mkOption { |
||||
type = types.str; |
||||
default = "none"; |
||||
description = '' |
||||
Logging driver for the container. The default of |
||||
<literal>"none"</literal> means that the container's logs will be |
||||
handled as part of the systemd unit. Setting this to |
||||
<literal>"journald"</literal> will result in duplicate logging, but |
||||
the container's logs will be visible to the <command>docker |
||||
logs</command> command. |
||||
|
||||
For more details and a full list of logging drivers, refer to the |
||||
<link xlink:href="https://docs.docker.com/engine/reference/run/#logging-drivers---log-driver"> |
||||
Docker engine documentation</link> |
||||
''; |
||||
}; |
||||
|
||||
ports = mkOption { |
||||
type = with types; listOf str; |
||||
default = []; |
||||
description = '' |
||||
Network ports to publish from the container to the outer host. |
||||
</para> |
||||
<para> |
||||
Valid formats: |
||||
</para> |
||||
<itemizedlist> |
||||
<listitem> |
||||
<para> |
||||
<literal><ip>:<hostPort>:<containerPort></literal> |
||||
</para> |
||||
</listitem> |
||||
<listitem> |
||||
<para> |
||||
<literal><ip>::<containerPort></literal> |
||||
</para> |
||||
</listitem> |
||||
<listitem> |
||||
<para> |
||||
<literal><hostPort>:<containerPort></literal> |
||||
</para> |
||||
</listitem> |
||||
<listitem> |
||||
<para> |
||||
<literal><containerPort></literal> |
||||
</para> |
||||
</listitem> |
||||
</itemizedlist> |
||||
<para> |
||||
Both <literal>hostPort</literal> and |
||||
<literal>containerPort</literal> can be specified as a range of |
||||
ports. When specifying ranges for both, the number of container |
||||
ports in the range must match the number of host ports in the |
||||
range. Example: <literal>1234-1236:1234-1236/tcp</literal> |
||||
</para> |
||||
<para> |
||||
When specifying a range for <literal>hostPort</literal> only, the |
||||
<literal>containerPort</literal> must <emphasis>not</emphasis> be a |
||||
range. In this case, the container port is published somewhere |
||||
within the specified <literal>hostPort</literal> range. Example: |
||||
<literal>1234-1236:1234/tcp</literal> |
||||
</para> |
||||
<para> |
||||
Refer to the |
||||
<link xlink:href="https://docs.docker.com/engine/reference/run/#expose-incoming-ports"> |
||||
Docker engine documentation</link> for full details. |
||||
''; |
||||
example = literalExample '' |
||||
[ |
||||
"8080:9000" |
||||
] |
||||
''; |
||||
}; |
||||
|
||||
user = mkOption { |
||||
type = with types; nullOr str; |
||||
default = null; |
||||
description = '' |
||||
Override the username or UID (and optionally groupname or GID) used |
||||
in the container. |
||||
''; |
||||
example = "nobody:nogroup"; |
||||
}; |
||||
|
||||
volumes = mkOption { |
||||
type = with types; listOf str; |
||||
default = []; |
||||
description = '' |
||||
List of volumes to attach to this container. |
||||
|
||||
Note that this is a list of <literal>"src:dst"</literal> strings to |
||||
allow for <literal>src</literal> to refer to |
||||
<literal>/nix/store</literal> paths, which would difficult with an |
||||
attribute set. There are also a variety of mount options available |
||||
as a third field; please refer to the |
||||
<link xlink:href="https://docs.docker.com/engine/reference/run/#volume-shared-filesystems"> |
||||
docker engine documentation</link> for details. |
||||
''; |
||||
example = literalExample '' |
||||
[ |
||||
"volume_name:/path/inside/container" |
||||
"/path/on/host:/path/inside/container" |
||||
] |
||||
''; |
||||
}; |
||||
|
||||
workdir = mkOption { |
||||
type = with types; nullOr str; |
||||
default = null; |
||||
description = "Override the default working directory for the container."; |
||||
example = "/var/lib/hello_world"; |
||||
}; |
||||
|
||||
extraDockerOptions = mkOption { |
||||
type = with types; listOf str; |
||||
default = []; |
||||
description = "Extra options for <command>docker run</command>."; |
||||
example = literalExample '' |
||||
["--network=host"] |
||||
''; |
||||
}; |
||||
}; |
||||
}; |
||||
|
||||
mkService = name: container: { |
||||
wantedBy = [ "multi-user.target" ]; |
||||
after = [ "docker.service" "docker.socket" ]; |
||||
requires = [ "docker.service" "docker.socket" ]; |
||||
serviceConfig = { |
||||
ExecStart = concatStringsSep " \\\n " ([ |
||||
"${pkgs.docker}/bin/docker run" |
||||
"--rm" |
||||
"--name=%n" |
||||
"--log-driver=${container.log-driver}" |
||||
] ++ optional (! isNull container.entrypoint) |
||||
"--entrypoint=${escapeShellArg container.entrypoint}" |
||||
++ (mapAttrsToList (k: v: "-e ${escapeShellArg k}=${escapeShellArg v}") container.environment) |
||||
++ map (p: "-p ${escapeShellArg p}") container.ports |
||||
++ optional (! isNull container.user) "-u ${escapeShellArg container.user}" |
||||
++ map (v: "-v ${escapeShellArg v}") container.volumes |
||||
++ optional (! isNull container.workdir) "-w ${escapeShellArg container.workdir}" |
||||
++ map escapeShellArg container.extraDockerOptions |
||||
++ [container.image] |
||||
++ map escapeShellArg container.cmd |
||||
); |
||||
ExecStartPre = "-${pkgs.docker}/bin/docker rm -f %n"; |
||||
ExecStop = "${pkgs.docker}/bin/docker stop %n"; |
||||
ExecStopPost = "-${pkgs.docker}/bin/docker rm -f %n"; |
||||
|
||||
### There is no generalized way of supporting `reload` for docker |
||||
### containers. Some containers may respond well to SIGHUP sent to their |
||||
### init process, but it is not guaranteed; some apps have other reload |
||||
### mechanisms, some don't have a reload signal at all, and some docker |
||||
### images just have broken signal handling. The best compromise in this |
||||
### case is probably to leave ExecReload undefined, so `systemctl reload` |
||||
### will at least result in an error instead of potentially undefined |
||||
### behaviour. |
||||
### |
||||
### Advanced users can still override this part of the unit to implement |
||||
### a custom reload handler, since the result of all this is a normal |
||||
### systemd service from the perspective of the NixOS module system. |
||||
### |
||||
# ExecReload = ...; |
||||
### |
||||
|
||||
TimeoutStartSec = 0; |
||||
TimeoutStopSec = 120; |
||||
Restart = "always"; |
||||
}; |
||||
}; |
||||
|
||||
in { |
||||
|
||||
options.docker-containers = mkOption { |
||||
default = {}; |
||||
type = types.attrsOf (types.submodule dockerContainer); |
||||
description = "Docker containers to run as systemd services."; |
||||
}; |
||||
|
||||
config = mkIf (cfg != {}) { |
||||
|
||||
systemd.services = mapAttrs' (n: v: nameValuePair "docker-${n}" (mkService n v)) cfg; |
||||
|
||||
virtualisation.docker.enable = true; |
||||
|
||||
}; |
||||
|
||||
} |
@ -0,0 +1,29 @@ |
||||
# Test Docker containers as systemd units |
||||
|
||||
import ./make-test.nix ({ pkgs, lib, ... }: { |
||||
name = "docker-containers"; |
||||
meta = { |
||||
maintainers = with lib.maintainers; [ benley ]; |
||||
}; |
||||
|
||||
nodes = { |
||||
docker = { pkgs, ... }: |
||||
{ |
||||
virtualisation.docker.enable = true; |
||||
|
||||
virtualisation.dockerPreloader.images = [ pkgs.dockerTools.examples.nginx ]; |
||||
|
||||
docker-containers.nginx = { |
||||
image = "nginx-container"; |
||||
ports = ["8181:80"]; |
||||
}; |
||||
}; |
||||
}; |
||||
|
||||
testScript = '' |
||||
startAll; |
||||
$docker->waitForUnit("docker-nginx.service"); |
||||
$docker->waitForOpenPort(8181); |
||||
$docker->waitUntilSucceeds("curl http://localhost:8181|grep Hello"); |
||||
''; |
||||
}) |
@ -0,0 +1,17 @@ |
||||
import ./make-test.nix ({ pkgs, ...} : { |
||||
name = "kernel-testing"; |
||||
meta = with pkgs.stdenv.lib.maintainers; { |
||||
maintainers = [ nequissimus ]; |
||||
}; |
||||
|
||||
machine = { pkgs, ... }: |
||||
{ |
||||
boot.kernelPackages = pkgs.linuxPackages_testing; |
||||
}; |
||||
|
||||
testScript = |
||||
'' |
||||
$machine->succeed("uname -s | grep 'Linux'"); |
||||
$machine->succeed("uname -a | grep '${pkgs.linuxPackages_testing.kernel.modDirVersion}'"); |
||||
''; |
||||
}) |
@ -0,0 +1,26 @@ |
||||
import ./make-test.nix ({ lib, ... }: |
||||
|
||||
{ |
||||
name = "mailcatcher"; |
||||
meta.maintainers = [ lib.maintainers.aanderse ]; |
||||
|
||||
machine = |
||||
{ pkgs, ... }: |
||||
{ |
||||
services.mailcatcher.enable = true; |
||||
|
||||
networking.defaultMailServer.directDelivery = true; |
||||
networking.defaultMailServer.hostName = "localhost:1025"; |
||||
|
||||
environment.systemPackages = [ pkgs.mailutils ]; |
||||
}; |
||||
|
||||
testScript = '' |
||||
startAll; |
||||
|
||||
$machine->waitForUnit('mailcatcher.service'); |
||||
$machine->waitForOpenPort('1025'); |
||||
$machine->succeed('echo "this is the body of the email" | mail -s "subject" root@example.org'); |
||||
$machine->succeed('curl http://localhost:1080/messages/1.source') =~ /this is the body of the email/ or die; |
||||
''; |
||||
}) |
@ -0,0 +1,168 @@ |
||||
import ./make-test.nix { |
||||
name = "systemd-confinement"; |
||||
|
||||
machine = { pkgs, lib, ... }: let |
||||
testServer = pkgs.writeScript "testserver.sh" '' |
||||
#!${pkgs.stdenv.shell} |
||||
export PATH=${lib.escapeShellArg "${pkgs.coreutils}/bin"} |
||||
${lib.escapeShellArg pkgs.stdenv.shell} 2>&1 |
||||
echo "exit-status:$?" |
||||
''; |
||||
|
||||
testClient = pkgs.writeScriptBin "chroot-exec" '' |
||||
#!${pkgs.stdenv.shell} -e |
||||
output="$(echo "$@" | nc -NU "/run/test$(< /teststep).sock")" |
||||
ret="$(echo "$output" | sed -nre '$s/^exit-status:([0-9]+)$/\1/p')" |
||||
echo "$output" | head -n -1 |
||||
exit "''${ret:-1}" |
||||
''; |
||||
|
||||
mkTestStep = num: { description, config ? {}, testScript }: { |
||||
systemd.sockets."test${toString num}" = { |
||||
description = "Socket for Test Service ${toString num}"; |
||||
wantedBy = [ "sockets.target" ]; |
||||
socketConfig.ListenStream = "/run/test${toString num}.sock"; |
||||
socketConfig.Accept = true; |
||||
}; |
||||
|
||||
systemd.services."test${toString num}@" = { |
||||
description = "Confined Test Service ${toString num}"; |
||||
confinement = (config.confinement or {}) // { enable = true; }; |
||||
serviceConfig = (config.serviceConfig or {}) // { |
||||
ExecStart = testServer; |
||||
StandardInput = "socket"; |
||||
}; |
||||
} // removeAttrs config [ "confinement" "serviceConfig" ]; |
||||
|
||||
__testSteps = lib.mkOrder num '' |
||||
subtest '${lib.escape ["\\" "'"] description}', sub { |
||||
$machine->succeed('echo ${toString num} > /teststep'); |
||||
${testScript} |
||||
}; |
||||
''; |
||||
}; |
||||
|
||||
in { |
||||
imports = lib.imap1 mkTestStep [ |
||||
{ description = "chroot-only confinement"; |
||||
config.confinement.mode = "chroot-only"; |
||||
testScript = '' |
||||
$machine->succeed( |
||||
'test "$(chroot-exec ls -1 / | paste -sd,)" = bin,nix', |
||||
'test "$(chroot-exec id -u)" = 0', |
||||
'chroot-exec chown 65534 /bin', |
||||
); |
||||
''; |
||||
} |
||||
{ description = "full confinement with APIVFS"; |
||||
testScript = '' |
||||
$machine->fail( |
||||
'chroot-exec ls -l /etc', |
||||
'chroot-exec ls -l /run', |
||||
'chroot-exec chown 65534 /bin', |
||||
); |
||||
$machine->succeed( |
||||
'test "$(chroot-exec id -u)" = 0', |
||||
'chroot-exec chown 0 /bin', |
||||
); |
||||
''; |
||||
} |
||||
{ description = "check existence of bind-mounted /etc"; |
||||
config.serviceConfig.BindReadOnlyPaths = [ "/etc" ]; |
||||
testScript = '' |
||||
$machine->succeed('test -n "$(chroot-exec cat /etc/passwd)"'); |
||||
''; |
||||
} |
||||
{ description = "check if User/Group really runs as non-root"; |
||||
config.serviceConfig.User = "chroot-testuser"; |
||||
config.serviceConfig.Group = "chroot-testgroup"; |
||||
testScript = '' |
||||
$machine->succeed('chroot-exec ls -l /dev'); |
||||
$machine->succeed('test "$(chroot-exec id -u)" != 0'); |
||||
$machine->fail('chroot-exec touch /bin/test'); |
||||
''; |
||||
} |
||||
(let |
||||
symlink = pkgs.runCommand "symlink" { |
||||
target = pkgs.writeText "symlink-target" "got me\n"; |
||||
} "ln -s \"$target\" \"$out\""; |
||||
in { |
||||
description = "check if symlinks are properly bind-mounted"; |
||||
config.confinement.packages = lib.singleton symlink; |
||||
testScript = '' |
||||
$machine->fail('chroot-exec test -e /etc'); |
||||
$machine->succeed('chroot-exec cat ${symlink} >&2'); |
||||
$machine->succeed('test "$(chroot-exec cat ${symlink})" = "got me"'); |
||||
''; |
||||
}) |
||||
{ description = "check if StateDirectory works"; |
||||
config.serviceConfig.User = "chroot-testuser"; |
||||
config.serviceConfig.Group = "chroot-testgroup"; |
||||
config.serviceConfig.StateDirectory = "testme"; |
||||
testScript = '' |
||||
$machine->succeed('chroot-exec touch /tmp/canary'); |
||||
$machine->succeed('chroot-exec "echo works > /var/lib/testme/foo"'); |
||||
$machine->succeed('test "$(< /var/lib/testme/foo)" = works'); |
||||
$machine->succeed('test ! -e /tmp/canary'); |
||||
''; |
||||
} |
||||
{ description = "check if /bin/sh works"; |
||||
testScript = '' |
||||
$machine->succeed( |
||||
'chroot-exec test -e /bin/sh', |
||||
'test "$(chroot-exec \'/bin/sh -c "echo bar"\')" = bar', |
||||
); |
||||
''; |
||||
} |
||||
{ description = "check if suppressing /bin/sh works"; |
||||
config.confinement.binSh = null; |
||||
testScript = '' |
||||
$machine->succeed( |
||||
'chroot-exec test ! -e /bin/sh', |
||||
'test "$(chroot-exec \'/bin/sh -c "echo foo"\')" != foo', |
||||
); |
||||
''; |
||||
} |
||||
{ description = "check if we can set /bin/sh to something different"; |
||||
config.confinement.binSh = "${pkgs.hello}/bin/hello"; |
||||
testScript = '' |
||||
$machine->succeed( |
||||
'chroot-exec test -e /bin/sh', |
||||
'test "$(chroot-exec /bin/sh -g foo)" = foo', |
||||
); |
||||
''; |
||||
} |
||||
{ description = "check if only Exec* dependencies are included"; |
||||
config.environment.FOOBAR = pkgs.writeText "foobar" "eek\n"; |
||||
testScript = '' |
||||
$machine->succeed('test "$(chroot-exec \'cat "$FOOBAR"\')" != eek'); |
||||
''; |
||||
} |
||||
{ description = "check if all unit dependencies are included"; |
||||
config.environment.FOOBAR = pkgs.writeText "foobar" "eek\n"; |
||||
config.confinement.fullUnit = true; |
||||
testScript = '' |
||||
$machine->succeed('test "$(chroot-exec \'cat "$FOOBAR"\')" = eek'); |
||||
''; |
||||
} |
||||
]; |
||||
|
||||
options.__testSteps = lib.mkOption { |
||||
type = lib.types.lines; |
||||
description = "All of the test steps combined as a single script."; |
||||
}; |
||||
|
||||
config.environment.systemPackages = lib.singleton testClient; |
||||
|
||||
config.users.groups.chroot-testgroup = {}; |
||||
config.users.users.chroot-testuser = { |
||||
description = "Chroot Test User"; |
||||
group = "chroot-testgroup"; |
||||
}; |
||||
}; |
||||
|
||||
testScript = { nodes, ... }: '' |
||||
$machine->waitForUnit('multi-user.target'); |
||||
${nodes.machine.config.__testSteps} |
||||
''; |
||||
} |
@ -0,0 +1,97 @@ |
||||
let |
||||
wg-snakeoil-keys = import ./snakeoil-keys.nix; |
||||
in |
||||
|
||||
import ../make-test.nix ({ pkgs, ...} : { |
||||
name = "wireguard"; |
||||
meta = with pkgs.stdenv.lib.maintainers; { |
||||
maintainers = [ ma27 ]; |
||||
}; |
||||
|
||||
nodes = { |
||||
peer0 = { lib, ... }: { |
||||
boot.kernel.sysctl = { |
||||
"net.ipv6.conf.all.forwarding" = "1"; |
||||
"net.ipv6.conf.default.forwarding" = "1"; |
||||
"net.ipv4.ip_forward" = "1"; |
||||
}; |
||||
|
||||
networking.useDHCP = false; |
||||
networking.interfaces.eth1 = { |
||||
ipv4.addresses = lib.singleton { |
||||
address = "192.168.0.1"; |
||||
prefixLength = 24; |
||||
}; |
||||
ipv6.addresses = lib.singleton { |
||||
address = "fd00::1"; |
||||
prefixLength = 64; |
||||
}; |
||||
}; |
||||
|
||||
networking.firewall.allowedUDPPorts = [ 23542 ]; |
||||
networking.wireguard.interfaces.wg0 = { |
||||
ips = [ "10.23.42.1/32" "fc00::1/128" ]; |
||||
listenPort = 23542; |
||||
|
||||
inherit (wg-snakeoil-keys.peer0) privateKey; |
||||
|
||||
peers = lib.singleton { |
||||
allowedIPs = [ "10.23.42.2/32" "fc00::2/128" ]; |
||||
|
||||
inherit (wg-snakeoil-keys.peer1) publicKey; |
||||
}; |
||||
}; |
||||
}; |
||||
|
||||
peer1 = { pkgs, lib, ... }: { |
||||
boot.kernel.sysctl = { |
||||
"net.ipv6.conf.all.forwarding" = "1"; |
||||
"net.ipv6.conf.default.forwarding" = "1"; |
||||
"net.ipv4.ip_forward" = "1"; |
||||
}; |
||||
|
||||
networking.useDHCP = false; |
||||
networking.interfaces.eth1 = { |
||||
ipv4.addresses = lib.singleton { |
||||
address = "192.168.0.2"; |
||||
prefixLength = 24; |
||||
}; |
||||
ipv6.addresses = lib.singleton { |
||||
address = "fd00::2"; |
||||
prefixLength = 64; |
||||
}; |
||||
}; |
||||
|
||||
networking.wireguard.interfaces.wg0 = { |
||||
ips = [ "10.23.42.2/32" "fc00::2/128" ]; |
||||
listenPort = 23542; |
||||
allowedIPsAsRoutes = false; |
||||
|
||||
inherit (wg-snakeoil-keys.peer1) privateKey; |
||||
|
||||
peers = lib.singleton { |
||||
allowedIPs = [ "0.0.0.0/0" "::/0" ]; |
||||
endpoint = "192.168.0.1:23542"; |
||||
persistentKeepalive = 25; |
||||
|
||||
inherit (wg-snakeoil-keys.peer0) publicKey; |
||||
}; |
||||
|
||||
postSetup = let inherit (pkgs) iproute; in '' |
||||
${iproute}/bin/ip route replace 10.23.42.1/32 dev wg0 |
||||
${iproute}/bin/ip route replace fc00::1/128 dev wg0 |
||||
''; |
||||
}; |
||||
}; |
||||
}; |
||||
|
||||
testScript = '' |
||||
startAll; |
||||
|
||||
$peer0->waitForUnit("wireguard-wg0.service"); |
||||
$peer1->waitForUnit("wireguard-wg0.service"); |
||||
|
||||
$peer1->succeed("ping -c5 fc00::1"); |
||||
$peer1->succeed("ping -c5 10.23.42.1") |
||||
''; |
||||
}) |
@ -0,0 +1,11 @@ |
||||
{ |
||||
peer0 = { |
||||
privateKey = "OPuVRS2T0/AtHDp3PXkNuLQYDiqJaBEEnYe42BSnJnQ="; |
||||
publicKey = "IujkG119YPr2cVQzJkSLYCdjpHIDjvr/qH1w1tdKswY="; |
||||
}; |
||||
|
||||
peer1 = { |
||||
privateKey = "uO8JVo/sanx2DOM0L9GUEtzKZ82RGkRnYgpaYc7iXmg="; |
||||
publicKey = "Ks9yRJIi/0vYgRmn14mIOQRwkcUGBujYINbMpik2SBI="; |
||||
}; |
||||
} |
@ -0,0 +1,50 @@ |
||||
{ stdenv, fetchFromGitHub, audiofile, libvorbis, fltk, fftw, fftwFloat, |
||||
minixml, pkgconfig, libmad, libjack2, portaudio, libsamplerate }: |
||||
|
||||
stdenv.mkDerivation { |
||||
pname = "paulstretch"; |
||||
version = "2.2-2"; |
||||
|
||||
src = fetchFromGitHub { |
||||
owner = "paulnasca"; |
||||
repo = "paulstretch_cpp"; |
||||
rev = "7f5c3993abe420661ea0b808304b0e2b4b0048c5"; |
||||
sha256 = "06dy03dbz1yznhsn0xvsnkpc5drzwrgxbxdx0hfpsjn2xcg0jrnc"; |
||||
}; |
||||
|
||||
nativeBuildInputs = [ pkgconfig ]; |
||||
|
||||
buildInputs = [ |
||||
audiofile |
||||
libvorbis |
||||
fltk |
||||
fftw |
||||
fftwFloat |
||||
minixml |
||||
libmad |
||||
libjack2 |
||||
portaudio |
||||
libsamplerate |
||||
]; |
||||
|
||||
buildPhase = '' |
||||
bash compile_linux_fftw_jack.sh |
||||
''; |
||||
|
||||
installPhase = '' |
||||
install -Dm555 ./paulstretch $out/bin/paulstretch |
||||
''; |
||||
|
||||
meta = with stdenv.lib; { |
||||
description = "Produces high quality extreme sound stretching"; |
||||
longDescription = '' |
||||
This is a program for stretching the audio. It is suitable only for |
||||
extreme sound stretching of the audio (like 50x) and for applying |
||||
special effects by "spectral smoothing" the sounds. |
||||
It can transform any sound/music to a texture. |
||||
''; |
||||
homepage = http://hypermammut.sourceforge.net/paulstretch/; |
||||
platforms = platforms.linux; |
||||
license = licenses.gpl2; |
||||
}; |
||||
} |
@ -0,0 +1,63 @@ |
||||
{ lib, stdenv, python3, fetchFromGitHub, makeWrapper, buildEnv, aspellDicts |
||||
# Use `lib.collect lib.isDerivation aspellDicts;` to make all dictionaries |
||||
# available. |
||||
, enchantAspellDicts ? with aspellDicts; [ en en-computers en-science ] |
||||
}: |
||||
|
||||
let |
||||
version = "7.0.4"; |
||||
python = let |
||||
packageOverrides = self: super: { |
||||
markdown = super.markdown.overridePythonAttrs(old: rec { |
||||
src = super.fetchPypi { |
||||
version = "3.0.1"; |
||||
pname = "Markdown"; |
||||
sha256 = "d02e0f9b04c500cde6637c11ad7c72671f359b87b9fe924b2383649d8841db7c"; |
||||
}; |
||||
}); |
||||
|
||||
chardet = super.chardet.overridePythonAttrs(old: rec { |
||||
src = super.fetchPypi { |
||||
version = "2.3.0"; |
||||
pname = "chardet"; |
||||
sha256 = "e53e38b3a4afe6d1132de62b7400a4ac363452dc5dfcf8d88e8e0cce663c68aa"; |
||||
}; |
||||
}); |
||||
}; |
||||
in python3.override { inherit packageOverrides; }; |
||||
pythonEnv = python.withPackages (ps: with ps; [ |
||||
pyqt5 docutils pyenchant Markups markdown pygments chardet |
||||
]); |
||||
in python.pkgs.buildPythonApplication { |
||||
inherit version; |
||||
pname = "retext"; |
||||
|
||||
src = fetchFromGitHub { |
||||
owner = "retext-project"; |
||||
repo = "retext"; |
||||
rev = "${version}"; |
||||
sha256 = "1zcapywspc9v5zf5cxqkcy019np9n41gmryqixj66zsvd544c6si"; |
||||
}; |
||||
|
||||
doCheck = false; |
||||
|
||||
nativeBuildInputs = [ makeWrapper ]; |
||||
propagatedBuildInputs = [ pythonEnv ]; |
||||
|
||||
postInstall = '' |
||||
mv $out/bin/retext $out/bin/.retext |
||||
makeWrapper "$out/bin/.retext" "$out/bin/retext" \ |
||||
--set ASPELL_CONF "dict-dir ${buildEnv { |
||||
name = "aspell-all-dicts"; |
||||
paths = map (path: "${path}/lib/aspell") enchantAspellDicts; |
||||
}}" |
||||
''; |
||||
|
||||
meta = with stdenv.lib; { |
||||
homepage = https://github.com/retext-project/retext/; |
||||
description = "Simple but powerful editor for Markdown and reStructuredText"; |
||||
license = licenses.gpl3; |
||||
maintainers = with maintainers; [ klntsky ]; |
||||
platforms = platforms.unix; |
||||
}; |
||||
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue