Repository: scionproto/scion
Pull request #4448 (2023-11-24 11:15:31)

dist: build debian packages for multiple platforms
Build debian packages for amd64, arm64, i386 and armel.
- There are separate packages for router, control, daemon, dispatcher, gateway and tools (scion and scion-pki).
- The packages include systemd unit files to run the services. For the daemon, dispatcher and gateway one instance per host is supported by the systemd service, and a default configuration file is included. For router and control, multiple instances per host are supported, and as a consequence of this, no default configuration file is provided.
- Currently, there is no man page contained in the packages. We should be able to build these from our existing manuals, but it seems to require a significant amount of fiddling to get something useful.

Building the .deb packages uses bazel with `rules_pkg`.
The target `//dist:deb_all` cross-builds packages for the default set of target platforms. Alternatively, the target `//dist:deb` allows to build (all) packages for the current target platform. This current platform can be set with the `--platforms`  bazel option (see https://github.com/bazelbuild/rules_go#how-do-i-cross-compile for more details).
- To increase reuse of build results while cross-building, some internal targets related to openapi forcibly ignore the target platform
- The package version is based on the current git tag. `rules_pkg` can include this version _in_ the package metadata, but bazel _cannot_ spit out appropriately named package files. This is addressed by copying and renaming the package files after build in a make target `make dist-deb`.

Contributes to #4425.

<!-- Reviewable:start -->
- - -
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/scionproto/scion/4448)
<!-- Reviewable:end -->


jiceatscion: > > _[Reviewable](https://reviewable.io/reviews/scionproto/scion/4448)_ status: :shipit: complete! all files reviewed, all discussions resolved (waiting on @oncilla)

Never mind. I found it.


Patch:
diff --git a/.buildkite/hooks/pre-command b/.buildkite/hooks/pre-command
index 38f630752..13ccb7e58 100755
--- a/.buildkite/hooks/pre-command
+++ b/.buildkite/hooks/pre-command
@@ -30,9 +30,10 @@ rm -f $HOME/.bazelrc
 # --nostamp is required for better caching (only on non-release jobs).
 if [ "$BUILDKITE_PIPELINE_SLUG" == "scion" ]; then
     echo "build --nostamp" > $HOME/.bazelrc
-    # Also set a fixed GIT_VERSION so that the workspace_status_command always
-    # returns the same value on CI to improve cache reuse.
-    export GIT_VERSION="ci-fixed"
+    # Shorten the git version to omit commit information, improving cache reuse.
+    # The format of git-version is "<tag>-<number-of-commits-since-the-tag>-<commit-short-hash>"
+    # This will be shortened to "<tag>-modified-ci"
+    export GIT_VERSION=$(tools/git-version | sed 's/-.*/-modified-ci/')
 else
     echo "build --stamp" > $HOME/.bazelrc
 fi
diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml
index 96fd38b64..233efe828 100644
--- a/.buildkite/pipeline.yml
+++ b/.buildkite/pipeline.yml
@@ -12,7 +12,7 @@ steps:
     plugins:
       - scionproto/metahook#v0.3.0:
           post-artifact: |
-            cat << EOF | buildkite-agent annotate --style "info"
+            cat << EOF | buildkite-agent annotate --style "info" --context "binaries"
             #### Build outputs
             - <a href="artifact://bazel-bin/scion.tar">SCION binaries</a>
             - <a href="artifact://bazel-bin/scion-ci.tar">SCION test tools and utilities</a>
@@ -23,6 +23,28 @@ steps:
         - exit_status: 255 # Forced agent shutdown
     timeout_in_minutes: 10
   - wait
+  - label: "Package :debian:"
+    command:
+      - make dist-deb
+      - cd deb;
+      - tar -chaf scion-deb-amd64.tar.gz *_amd64.deb
+      - tar -chaf scion-deb-arm64.tar.gz *_arm64.deb
+      - tar -chaf scion-deb-i386.tar.gz *_i386.deb
+      - tar -chaf scion-deb-armel.tar.gz *_armel.deb
+    artifact_paths:
+      - "deb/*.tar.gz"
+    plugins:
+      - scionproto/metahook#v0.3.0:
+          post-artifact: |
+            cat << EOF | buildkite-agent annotate --style "info" --context "packages"
+            #### Packages :debian:
+            - <a href="artifact://deb/scion-deb-amd64.tar.gz">amd64</a>
+            - <a href="artifact://deb/scion-deb-arm64.tar.gz">arm64</a>
+            - <a href="artifact://deb/scion-deb-i386.tar.gz">i386</a>
+            - <a href="artifact://deb/scion-deb-armel.tar.gz">armel</a>
+            EOF
+    key: dist-deb
+    retry: *automatic-retry
   - label: "Unit Tests :bazel:"
     command:
       - bazel test --config=race --config=unit_all
@@ -70,6 +92,7 @@ steps:
     timeout_in_minutes: 20
     key: check_generated
     retry: *automatic-retry
+  - wait
   - group: "End to End"
     key: e2e
     steps:
diff --git a/.gitignore b/.gitignore
index e994a71ed..d3df276f7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -66,6 +66,10 @@ doc/venv/
 /bin/*
 !/bin/.keepme
 
+# Generated package files
+##########################
+/deb/
+
 # CTags
 ##########################
 tags
diff --git a/Makefile b/Makefile
index ad506aded..48e9aa6e0 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-.PHONY: all build build-dev antlr clean docker-images gazelle go.mod licenses mocks protobuf scion-topo test test-integration write_all_source_files
+.PHONY: all build build-dev dist-deb antlr clean docker-images gazelle go.mod licenses mocks protobuf scion-topo test test-integration write_all_source_files
 
 build-dev:
 	rm -f bin/*
@@ -11,6 +11,21 @@ build:
 	bazel build //:scion
 	tar -kxf bazel-bin/scion.tar -C bin
 
+dist-deb:
+	bazel build //dist:deb_all
+	mkdir -p deb; rm -f deb/*;
+	@ # Bazel cannot include the version in the filename, if we want to set it automatically from the git tag.
+	@ # Extract the version from the .deb "control" manifest and expand the "__" in the filename to "_<version>_".
+	@ #   See e.g. https://en.wikipedia.org/wiki/Deb_(file_format)#Control_archive
+	@for f in `bazel cquery //dist:deb_all --output=files 2>/dev/null`; do \
+		if [ -f "$$f" ]; then \
+			bf=`basename $$f`; \
+			v="$$(ar p $$f control.tar.gz | tar -xz --to-stdout ./control | sed -n 's/Version: //p')"; \
+			bfv=$${bf%%__*}_$${v}_$${bf#*__}; \
+			cp -v "$$f" deb/$$bfv; \
+		fi \
+	done
+
 # all: performs the code-generation steps and then builds; the generated code
 # is git controlled, and therefore this is only necessary when changing the
 # sources for the code generation.
diff --git a/README.md b/README.md
index 0c3016ad6..7842cf3a2 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,10 @@
 # SCION
 
 [![Slack chat](https://img.shields.io/badge/chat%20on-slack-blue?logo=slack)](https://scionproto.slack.com)
+[![Matrix chat](https://img.shields.io/badge/chat%20on-matrix-blue?logo=matrix)](https://matrix.to/#/#dev:matrix.scion.org)
+[![Awesome](https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg)](https://github.com/scionproto/awesome-scion)
 [![ReadTheDocs](https://img.shields.io/badge/doc-reference-blue?version=latest&style=flat&label=docs&logo=read-the-docs&logoColor=white)](https://docs.scion.org/en/latest)
-[![Documentation](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white)](https://pkg.go.dev/github.com/scionproto/scion)
+[![Go Docs](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white)](https://pkg.go.dev/github.com/scionproto/scion)
 [![Nightly Build](https://badge.buildkite.com/b70b65b38a75eb8724f41a6f1203c9327cfb767f07a0c1934e.svg)](https://buildkite.com/scionproto/scion-nightly/builds/latest)
 [![Go Report Card](https://goreportcard.com/badge/github.com/scionproto/scion)](https://goreportcard.com/report/github.com/scionproto/scion)
 [![GitHub issues](https://img.shields.io/github/issues/scionproto/scion/help%20wanted.svg?label=help%20wanted&color=purple)](https://github.com/scionproto/scion/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22)
@@ -10,28 +12,42 @@
 [![Release](https://img.shields.io/github/release-pre/scionproto/scion.svg)](https://github.com/scionproto/scion/releases)
 [![License](https://img.shields.io/github/license/scionproto/scion.svg?maxAge=2592000)](https://github.com/scionproto/scion/blob/master/LICENSE)
 
-Welcome to the open-source implementation of
-[SCION](http://www.scion-architecture.net) (Scalability, Control and Isolation
-On next-generation Networks), a future Internet architecture. SCION is the first
-clean-slate Internet architecture designed to provide route control, failure
-isolation, and explicit trust information for end-to-end communication. To find
-out more about the project, please visit our [documentation
-site](https://docs.scion.org/en/latest/).
+Welcome to the open-source implementation of [SCION](http://www.scion-architecture.net)
+(Scalability, Control and Isolation On next-generation Networks), a future Internet architecture.
+SCION provides route control, failure isolation, and explicit trust information for end-to-end communication.
+To find out more about the project, please visit our [documentation site](https://docs.scion.org/en/latest/).
 
-## Connecting to the SCION Test Network
+## Installation
 
-Join [SCIONLab](https://www.scionlab.org) if you're interested in playing with
-SCION in an operational global test deployment of SCION. As part of the SCIONLab
-project, we support [pre-built binaries as Debian
-packages](https://docs.scionlab.org/content/install/).
+Installation packages for Debian and derivatives are available for x86-64, arm64, x86-32 and arm.
+These packages can be found in the [latest release](https://github.com/scionproto/scion/releases/latest).
+Packages for in-development versions can be found from the [latest nightly build](https://buildkite.com/scionproto/scion-nightly/builds/latest).
 
-## Building
+Alternatively, "naked" pre-built binaries are available for Linux x86-64 and
+can be downloaded from the [latest release](https://github.com/scionproto/scion/releases/latest) or the
+[latest nightly build](https://buildkite.com/scionproto/scion-nightly/builds/latest).
 
-To find out how to work with SCION, please visit our [documentation
-site](https://docs.scion.org/en/latest/dev/setup.html)
-for instructions on how to install build dependencies, build and run SCION.
+### Build from sources
 
-Pre-built binaries for x86-64 Linux are available from the [latest nightly build](https://buildkite.com/scionproto/scion-nightly/builds/latest).
+SCION can be built with `go build`. To build all binaries used in a SCION deployment (i.e.
+excluding the testing and development tools), run
+
+```sh
+CGO_ENABLED=0 go build -o bin ./router/... ./control/... ./dispatcher/... ./daemon/... ./scion/... ./scion-pki/... ./gateway/...
+```
+
+The default way to build SCION, however, uses Bazel.
+In particular, this allows to run all the tests, linters etc.
+Please visit our [documentation site](https://docs.scion.org/en/latest/dev/setup.html) for
+instructions on how to set up Bazel and the full development environment.
+
+### Connecting to the SCION Network
+
+Join [SCIONLab](https://www.scionlab.org) if you're interested in playing with SCION in an
+operational global test deployment of SCION.
+
+The [awesome-scion](https://github.com/scionproto/awesome-scion#deployments) list contains
+pointers to production deployments of SCION.
 
 ## Contributing
 
diff --git a/dist/BUILD.bazel b/dist/BUILD.bazel
new file mode 100644
index 000000000..ca60b5f43
--- /dev/null
+++ b/dist/BUILD.bazel
@@ -0,0 +1,121 @@
+load(":package.bzl", "scion_pkg_deb")
+load(":platform.bzl", "multiplatform_filegroup")
+load(":git_version.bzl", "git_version")
+
+git_version(
+    name = "git_version",
+    tags = ["manual"],
+    visibility = ["//visibility:private"],
+)
+
+scion_pkg_deb(
+    name = "router_deb",
+    depends = [
+        "adduser",
+    ],
+    description = "SCION inter-domain network architecture border router",
+    executables = {
+        "//router/cmd/router:router": "scion-router",
+    },
+    package = "scion-router",
+    postinst = "debian/scion.postinst",
+    systemds = ["systemd/scion-router@.service"],
+    version_file = ":git_version",
+)
+
+scion_pkg_deb(
+    name = "control_deb",
+    configs = [],
+    depends = [
+        "adduser",
+        "scion-dispatcher",
+    ],
+    description = "SCION inter-domain network architecture control service",
+    executables = {
+        "//control/cmd/control:control": "scion-control",
+    },
+    package = "scion-control",
+    systemds = ["systemd/scion-control@.service"],
+    version_file = ":git_version",
+)
+
+scion_pkg_deb(
+    name = "dispatcher_deb",
+    configs = ["conffiles/dispatcher.toml"],
+    depends = [
+        "adduser",
+    ],
+    description = "SCION dispatcher",
+    executables = {
+        "//dispatcher/cmd/dispatcher:dispatcher": "scion-dispatcher",
+    },
+    package = "scion-dispatcher",
+    postinst = "debian/scion.postinst",
+    systemds = ["systemd/scion-dispatcher.service"],
+    version_file = ":git_version",
+)
+
+scion_pkg_deb(
+    name = "daemon_deb",
+    configs = ["conffiles/sciond.toml"],
+    depends = [
+        "adduser",
+    ],
+    description = "SCION daemon",
+    executables = {
+        "//daemon/cmd/daemon:daemon": "scion-daemon",
+    },
+    package = "scion-daemon",
+    postinst = "debian/scion.postinst",
+    systemds = ["systemd/scion-daemon.service"],
+    version_file = ":git_version",
+)
+
+scion_pkg_deb(
+    name = "gateway_deb",
+    configs = [
+        "conffiles/sig.json",
+        "conffiles/sig.toml",
+    ],
+    depends = [
+        "adduser",
+        "scion-dispatcher",
+        "scion-daemon",
+    ],
+    description = "SCION-IP Gateway",
+    executables = {
+        "//gateway/cmd/gateway:gateway": "scion-ip-gateway",
+    },
+    package = "scion-ip-gateway",
+    systemds = ["systemd/scion-ip-gateway.service"],
+    version_file = ":git_version",
+)
+
+scion_pkg_deb(
+    name = "tools_deb",
+    depends = [
+        "adduser",
+        "scion-dispatcher",
+        "scion-daemon",
+    ],
+    description = "SCION tools",
+    executables = {
+        "//scion/cmd/scion:scion": "scion",
+        "//scion-pki/cmd/scion-pki:scion-pki": "scion-pki",
+    },
+    package = "scion-tools",
+    version_file = ":git_version",
+)
+
+multiplatform_filegroup(
+    name = "deb",
+    srcs = [
+        "control_deb",
+        "daemon_deb",
+        "dispatcher_deb",
+        "gateway_deb",
+        "router_deb",
+        "tools_deb",
+    ],
+    visibility = ["//dist:__subpackages__"],
+)
diff --git a/dist/conffiles/dispatcher.toml b/dist/conffiles/dispatcher.toml
new file mode 100644
index 000000000..d6f83a1dd
--- /dev/null
+++ b/dist/conffiles/dispatcher.toml
@@ -0,0 +1,10 @@
+[dispatcher]
+id = "dispatcher"
+socket_file_mode = "0777"
+
+[log.console]
+level = "info"
+
+# Optionally expose metrics and other local inspection endpoints.
+# [metrics]
+# prometheus = "[127.0.0.1]:30441"
diff --git a/dist/conffiles/sciond.toml b/dist/conffiles/sciond.toml
new file mode 100644
index 000000000..3abf018ab
--- /dev/null
+++ b/dist/conffiles/sciond.toml
@@ -0,0 +1,21 @@
+[general]
+id = "sd"
+config_dir = "/etc/scion"
+reconnect_to_dispatcher = true
+
+[path_db]
+connection = "/var/lib/scion/sd.path.db"
+
+[trust_db]
+connection = "/var/lib/scion/sd.trust.db"
+
+# Optionally enable DRKey
+# [drkey_level2_db]
+# connection = "/var/lib/scion/sd.drkey_level2.db"
+
+[log.console]
+level = "info"
+
+# Optionally expose metrics and other local inspection endpoints.
+# [metrics]
+# prometheus = "127.0.0.1:30455"
diff --git a/dist/conffiles/sig.json b/dist/conffiles/sig.json
new file mode 100644
index 000000000..346a9f6e2
--- /dev/null
+++ b/dist/conffiles/sig.json
@@ -0,0 +1,11 @@
+{
+    "ASes": {
+        "<remote_sig_AS>": {
+            "Nets": [
+                "<remote_sig_IPnet>"
+            ]
+        }
+    },
+    "ConfigVersion": 9001
+}
+
diff --git a/dist/conffiles/sig.toml b/dist/conffiles/sig.toml
new file mode 100644
index 000000000..5fb8995ae
--- /dev/null
+++ b/dist/conffiles/sig.toml
@@ -0,0 +1,12 @@
+[gateway]
+traffic_policy_file = "/etc/scion/sig.json"
+
+[tunnel]
+name = "sig"
+
+[log.console]
+level = "info"
+
+# Optionally expose metrics and other local inspection endpoints.
+# [metrics]
+# prometheus = "127.0.0.1:30456"
diff --git a/dist/debian/scion.postinst b/dist/debian/scion.postinst
new file mode 100644
index 000000000..4e67e3857
--- /dev/null
+++ b/dist/debian/scion.postinst
@@ -0,0 +1,26 @@
+#!/bin/bash
+set -e
+
+# summary of how this script can be called:
+#        * <postinst> `configure' <most-recently-configured-version>
+#        * <old-postinst> `abort-upgrade' <new version>
+#        * <conflictor's-postinst> `abort-remove' `in-favour' <package>
+#          <new-version>
+#        * <postinst> `abort-remove'
+#        * <deconfigured's-postinst> `abort-deconfigure' `in-favour'
+#          <failed-install-package> <version> `removing'
+#          <conflicting-package> <version>
+# for details, see http://www.debian.org/doc/debian-policy/ or
+# the debian-policy package
+
+case "$1" in
+    configure)
+        # Create system user
+        adduser --system --home /var/lib/scion --group scion
+        # Create configuration directory
+        mkdir /etc/scion/ >& /dev/null || true
+        chown scion:scion /etc/scion/
+        ;;
+    *)
+        ;;
+esac
diff --git a/dist/git_version.bzl b/dist/git_version.bzl
new file mode 100644
index 000000000..3c0164db2
--- /dev/null
+++ b/dist/git_version.bzl
@@ -0,0 +1,20 @@
+def _git_version_impl(ctx):
+    ctx.actions.run_shell(
+        outputs = [ctx.outputs.outfile],
+        inputs = [ctx.info_file],
+        command = r"sed -n 's/STABLE_GIT_VERSION\s*v\?//p' " + ctx.info_file.path + " > " + ctx.outputs.outfile.path,
+    )
+
+git_version = rule(
+    doc = """
+    Extracts the STABLE_GIT_VERSION from the workspace_status_command output.
+    See also .bazelrc and tools/bazel-build-env.
+
+    The output of this rule is a file containing the version only.
+    The leading "v" from the git tag is removed.
+    """,
+    implementation = _git_version_impl,
+    outputs = {
+        "outfile": "git-version",
+    },
+)
diff --git a/dist/package.bzl b/dist/package.bzl
new file mode 100644
index 000000000..6efa2b88d
--- /dev/null
+++ b/dist/package.bzl
@@ -0,0 +1,92 @@
+load("@rules_pkg//pkg:pkg.bzl", "pkg_deb", "pkg_tar")
+
+SCION_PKG_HOMEPAGE = "https://github.com/scionproto/scion"
+SCION_PKG_MAINTAINER = "SCION Contributors"
+SCION_PKG_LICENSE = "Apache 2.0"
+SCION_PKG_PRIORITY = "optional"
+SCION_PKG_SECTION = "net"
+
+def scion_pkg_deb(name, executables = {}, systemds = [], configs = [], **kwargs):
+    """
+    The package content, the _data_ arg for the pkg_deb rule, is assembled from:
+
+    - executables: Map Label (the executable) -> string, the basename of the executable in the package
+      Executables are installed to /usr/bin/
+    - systemds: List[string], the systemd unit files to be installed in /lib/systemd/system/
+    - configs:  List[string], the configuration files to be installed in /etc/scion/
+
+    The values for the following pkg_deb args are set to a default value:
+    - homepage
+    - maintainer
+    - priority
+    - section
+    - license
+    - conffiles, set based on data.configs
+    - architecture, set based on the platform.
+
+    The caller needs to set:
+    - package
+    - description
+    - version/version_file
+    and any of the optional control directives.
+    """
+
+    data = "%s_data" % name
+    _scion_pkg_deb_data(
+        name = data,
+        executables = executables,
+        systemds = systemds,
+        configs = configs,
+        visibility = ["//visibility:private"],
+        tags = ["manual"],
+    )
+    conffiles = ["/etc/scion/" + _basename(file) for file in configs]
+
+    kwargs.setdefault("homepage", SCION_PKG_HOMEPAGE)
+    kwargs.setdefault("maintainer", SCION_PKG_MAINTAINER)
+    kwargs.setdefault("priority", SCION_PKG_PRIORITY)
+    kwargs.setdefault("section", SCION_PKG_SECTION)
+    kwargs.setdefault("license", SCION_PKG_LICENSE)
+    kwargs.setdefault("conffiles", conffiles)
+    if "architecture" not in kwargs:
+        kwargs["architecture"] = select({
+            "@platforms//cpu:x86_64": "amd64",
+            "@platforms//cpu:x86_32": "i386",
+            "@platforms//cpu:aarch64": "arm64",
+            "@platforms//cpu:arm": "armel",
+            "@platforms//cpu:s390x": "s390x",
+            # Note: some rules_go toolchains don't (currently) seem to map (cleanly) to @platforms//cpu.
+            # "@platforms//cpu:ppc": "ppc64",
+            # "@platforms//cpu:ppc64le": "ppc64le",
+        })
+    pkg_deb(
+        name = name,
+        data = data,
+        target_compatible_with = ["@platforms//os:linux"],
+        **kwargs
+    )
+
+def _scion_pkg_deb_data(name, executables, systemds, configs, **kwargs):
+    executable_files = {label: "/usr/bin/" + basename for label, basename in executables.items()}
+    systemd_files = {file: "/lib/systemd/system/" + _basename(file) for file in systemds}
+    config_files = {file: "/etc/scion/" + _basename(file) for file in configs}
+
+    files = {}
+    files.update(executable_files)
+    files.update(systemd_files)
+    files.update(config_files)
+
+    pkg_tar(
+        name = name,
+        extension = "tar.gz",
+        files = files,
+        modes = {
+            exec_filepath: "755"
+            for exec_filepath in executable_files.values()
+        },
+        mode = "644",  # for everything else
+        **kwargs
+    )
+
+def _basename(s):
+    return s.split("/")[-1]
diff --git a/dist/platform.bzl b/dist/platform.bzl
new file mode 100644
index 000000000..b107b5746
--- /dev/null
+++ b/dist/platform.bzl
@@ -0,0 +1,34 @@
+load("@aspect_bazel_lib//lib:transitions.bzl", "platform_transition_filegroup")
+
+DEFAULT_PLATFORMS = [
+    "@io_bazel_rules_go//go/toolchain:linux_amd64",
+    "@io_bazel_rules_go//go/toolchain:linux_arm64",
+    "@io_bazel_rules_go//go/toolchain:linux_386",
+    "@io_bazel_rules_go//go/toolchain:linux_arm",
+]
+
+def multiplatform_filegroup(name, srcs, target_platforms = DEFAULT_PLATFORMS, **kwargs):
+    all_platforms = []
+    for target_platform in target_platforms:
+        platform_name = target_platform.split(":")[-1]
+        platform_transition_filegroup(
+            name = name + "_" + platform_name,
+            srcs = srcs,
+            target_platform = target_platform,
+        )
+        all_platforms.append(name + "_" + platform_name)
+
+    native.filegroup(
+        name = name + "_all",
+        srcs = all_platforms,
+        **kwargs
+    )
+
+    # also add the default filegroup, without platform transition, but
+    # only build it when explicitly requested
+    native.filegroup(
+        name = name,
+        srcs = srcs,
+        tags = ["manual"],
+        **kwargs
+    )
diff --git a/dist/systemd/scion-control@.service b/dist/systemd/scion-control@.service
new file mode 100644
index 000000000..ba832a40a
--- /dev/null
+++ b/dist/systemd/scion-control@.service
@@ -0,0 +1,17 @@
+[Unit]
+Description=SCION Control Service
+Documentation=https://docs.scion.org
+After=network-online.target scion-dispatcher.service
+Wants=scion-dispatcher.service
+StartLimitBurst=1
+StartLimitInterval=1s
+
+[Service]
+Type=simple
+User=scion
+Group=scion
+ExecStart=/usr/bin/scion-control --config /etc/scion/%i.toml
+Restart=on-failure
+
+[Install]
+WantedBy=multi-user.target
diff --git a/dist/systemd/scion-daemon.service b/dist/systemd/scion-daemon.service
new file mode 100644
index 000000000..1580cdc11
--- /dev/null
+++ b/dist/systemd/scion-daemon.service
@@ -0,0 +1,17 @@
+[Unit]
+Description=SCION Daemon
+Documentation=https://docs.scion.org
+After=network-online.target scion-dispatcher.service
+Wants=scion-dispatcher.service
+StartLimitBurst=1
+StartLimitInterval=1s
+
+[Service]
+Type=simple
+User=scion
+Group=scion
+ExecStart=/usr/bin/scion-daemon --config /etc/scion/sciond.toml
+Restart=on-failure
+
+[Install]
+WantedBy=multi-user.target
diff --git a/dist/systemd/scion-dispatcher.service b/dist/systemd/scion-dispatcher.service
new file mode 100644
index 000000000..8621330f7
--- /dev/null
+++ b/dist/systemd/scion-dispatcher.service
@@ -0,0 +1,19 @@
+[Unit]
+Description=SCION Dispatcher
+Documentation=https://docs.scion.org
+After=network-online.target
+StartLimitBurst=1
+StartLimitInterval=1s
+
+[Service]
+Type=simple
+User=scion
+Group=scion
+ExecStartPre=/bin/rm -rf /run/shm/dispatcher/
+ExecStart=/usr/bin/scion-dispatcher --config /etc/scion/dispatcher.toml
+LimitNOFILE=4096
+Restart=on-failure
+RestartSec=10
+
+[Install]
+WantedBy=multi-user.target
diff --git a/dist/systemd/scion-ip-gateway.service b/dist/systemd/scion-ip-gateway.service
new file mode 100644
index 000000000..f1f9fbbf7
--- /dev/null
+++ b/dist/systemd/scion-ip-gateway.service
@@ -0,0 +1,18 @@
+[Unit]
+Description=SCION IP Gateway
+Documentation=https://docs.scion.org
+After=network-online.target scion-daemon.service
+Wants=scion-daemon.service
+StartLimitBurst=1
+StartLimitInterval=1s
+
+[Service]
+Type=simple
+User=scion
+Group=scion
+AmbientCapabilities=cap_net_admin
+ExecStart=/usr/bin/scion-ip-gateway --config /etc/scion/sig.toml
+Restart=on-failure
+
+[Install]
+WantedBy=multi-user.target
diff --git a/dist/systemd/scion-router@.service b/dist/systemd/scion-router@.service
new file mode 100644
index 000000000..f4677d7a1
--- /dev/null
+++ b/dist/systemd/scion-router@.service
@@ -0,0 +1,16 @@
+[Unit]
+Description=SCION Router
+Documentation=https://docs.scion.org
+After=network-online.target
+StartLimitBurst=1
+StartLimitInterval=1s
+
+[Service]
+Type=simple
+User=scion
+Group=scion
+ExecStart=/usr/bin/scion-router --config /etc/scion/%i.toml
+Restart=on-failure
+
+[Install]
+WantedBy=multi-user.target
diff --git a/doc/dev/build.rst b/doc/dev/build.rst
new file mode 100644
index 000000000..379499e3c
--- /dev/null
+++ b/doc/dev/build.rst
@@ -0,0 +1,142 @@
+********
+Building
+********
+
+Building with go build
+======================
+
+SCION can be built with ``go build`` without any other system prerequisites.
+
+Please be aware that go build **is not the recommended setup for development** on SCION.
+Not all tests and checks can be run in this setup. We use Bazel to orchestrate all of this.
+Without running all checks locally, it is likely that there will be frustrating cycles with the CI
+system rejecting your changes.
+See :doc:`setup` for instructions on how to set up Bazel and the full development environment.
+
+Prerequisites
+-------------
+
+#. Clone the SCION repository into your workspace.
+
+   .. code-block:: bash
+
+      git clone https://github.com/scionproto/scion
+      cd scion
+
+#. Determine the go version used in the Bazel setup; the ``WORKSPACE`` file
+   specifies this version in the ``go_register_toolchains`` clause.
+
+   .. literalinclude:: /../WORKSPACE
+      :start-at: go_register_toolchains(
+      :end-at: )
+      :emphasize-lines: 3
+
+   Building with newer go versions *usually* works.
+
+#. Install go. Either follow `the official instructions <https://go.dev/doc/install>`_
+   or check the `Ubuntu specific installation options on the golang wiki <https://github.com/golang/go/wiki/Ubuntu>`_.
+
+Build
+-----
+
+* **Build only "distributables"**, without development and testing tools
+
+   .. code-block:: bash
+
+      CGO_ENABLED=0 go build -o bin/ ./{router,control,dispatcher,daemon,scion,scion-pki,gateway}/cmd/...
+
+* **Build all**
+
+   .. code-block:: bash
+
+      go build -o bin/ ./...
+
+Options
+-------
+
+* sqlite implementations: two different sqlite implementations can be chosen at build time:
+
+  - `modernc/sqlite <https://pkg.go.dev/modernc.org/sqlite>`_: **default**. A pure go implementation of sqlite (transpiled from C).
+  - `mattn/go-sqlite3 <https://github.com/mattn/go-sqlite3>`_: A CGO wrapper for the official sqlite implementation.
+    It is well established but requires CGO; this makes it impossible to build static binaries and
+    executables are dependent on a minimum glibc version.
+
+  Specify build tag (``go build -tags=<...>``) either ``sqlite_modernc`` or ``sqlite_mattn``.
+
+Building with Bazel
+===================
+
+Please be aware that the following instructions only result in a minimal build
+environment. Not all tests and checks can be run in this setup.
+See :doc:`setup` for instructions on how to set up Bazel and the full development environment.
+
+Prerequisites
+-------------
+
+#. Clone the SCION repository into your workspace.
+
+   .. code-block:: bash
+
+      git clone https://github.com/scionproto/scion
+      cd scion
+
+#. Install Bazel: either follow the official instructions at `<https://bazel.build/install>`_, or
+   run our helper script:
+
+   .. code-block::
+
+      tools/install_bazel
+
+#. Remove remote cache options from ``.bazelrc``; the default setup is useful to limit Bazel's
+   cache size when contributing to SCION, but requires a running docker container acting as the
+   "remote" cache service
+
+   .. code-block::
+
+      sed -e '/--remote_cache=/d' -i .bazelrc
+
+   Alternatively, if you have docker installed, you can run ``./scion.sh bazel-remote`` to start
+   the cache service.
+
+Build
+-----
+
+* **Build only "distributables"**, without development and testing tools
+
+   .. code-block:: sh
+
+      make build                          # or, ...
+      bazel build //:scion                # or, ...
+      bazel build //control/cmd/control //router/cmd/router <...>
+
+* **Build all**
+
+   .. code-block:: sh
+
+      make build-dev                      # or, ...
+      make                                # or, ...
+      bazel build //:scion //:scion-ci
+
+Options
+-------
+
+* Bundling the management API documentation with the binaries.
+
+   .. code-block:: sh
+
+      bazel build --//:mgmtapi_bundle_doc=true //:scion
+
+* sqlite implementations: specify a build tag, ``sqlite_modernc`` or ``sqlite_mattn``.
+
+   .. code-block:: sh
+
+      bazel build --define gotags=sqlite_mattn <...>
+
+
+.. seealso::
+
+   :doc:`setup`
+      Instructions for :doc:`installing the full development environment <setup>`.
+
+   :doc:`/manuals/install`
+      Information for :doc:`installing SCION from per-built binaries or packages </manuals/install>`.
diff --git a/doc/dev/setup.rst b/doc/dev/setup.rst
index 4b6e5e7aa..6d477963e 100644
--- a/doc/dev/setup.rst
+++ b/doc/dev/setup.rst
@@ -3,6 +3,12 @@
 Setting up the Development Environment
 ======================================
 
+.. hint::
+
+   These instructions describe the setup for building and running all integration tests with bazel,
+   docker and various other tools and scripts.
+   See :doc:`build` for instructions focussing only on how to build the SCION executables.
+
 Prerequisites
 -------------
 
@@ -28,15 +34,13 @@ Prerequisites
    Please follow the instructions for
    `Install Compose Plugin <https://docs.docker.com/compose/install/linux/#install-using-the-repository>`_.
 
-Bazel
+Setup
 -----
 
-#. Clone the SCION repository into the appropriate directory inside your workspace. In the commands below,
-   replace ``${WORKSPACE}`` with the directory in which you want to set up the project:
+#. Clone the SCION repository into your workspace.
 
    .. code-block:: bash
 
-      cd ${WORKSPACE}
       git clone https://github.com/scionproto/scion
       cd scion
 
@@ -72,8 +76,9 @@ Bazel
 
       make
 
-  .. hint:: This builds tools for tests in addition to the main SCION services (e.g., `end2end`);
-     if you don't require those, you can only build the SCION services by running ``make build``.
+   .. hint:: This builds tools for tests in addition to the main SCION services (e.g., `end2end`);
+      if you don't require those, you can only build the SCION services by running ``make build``.
+      See :doc:`build` for more details.
 
 #. Finally, check that tests run correctly:
 
@@ -90,46 +95,6 @@ Bazel
       make lint
 
 
-Alternative: go build
----------------------
-
-Alternatively to building with bazel, the SCION services and tools can be built
-with ``go build``.
-Please be aware that **this is not the recommended setup for development**.
-Not all checks and linters can be run in this setup. Without running all checks
-locally, it is likely that there will be frustrating cycles with the CI system
-rejecting your changes.
-
-#. Determine the go version used in the bazel setup; the ``WORKSPACE`` file
-   specifies this version in the ``go_register_toolchains`` clause.
-
-   .. literalinclude:: /../WORKSPACE
-      :start-at: go_register_toolchains(
-      :end-at: )
-      :emphasize-lines: 3
-
-   Building with newer go versions *usually* works.
-
-#. Install go. Either follow `the official instructions <https://go.dev/doc/install>`_
-   or check the `Ubuntu specific installation options on the golang wiki <https://github.com/golang/go/wiki/Ubuntu>`_.
-
-#. Decide which implementation of sqlite you want to use:
-
-   - `mattn`: A cgo implementation. It is well established but makes go
-     executables dependent on a minimum glibc version.
-   - `modernc`: A pure go implementation. It does not cause glibc version
-     issues but is less common. modernc is currently recommended due to
-     the glibc issue.
-
-#. Build SCION services and tools.
-
-   .. code-block:: bash
-
-      go build -o -tags sqlite_<impl> bin ./<service>/cmd/<service>...
-
-   where <impl> is one of `modernc` or `mattn`.
-
-
 Tips and Tricks
 ---------------
 .. toctree::
diff --git a/doc/index.rst b/doc/index.rst
index ce12baffa..6c1a83b5a 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -57,6 +57,7 @@ implementation <https://github.com/scionproto/scion>`_.
    :caption: Reference Manuals
    :hidden:
 
+   manuals/install
    manuals/control
    manuals/router
    manuals/gateway
@@ -70,11 +71,13 @@ implementation <https://github.com/scionproto/scion>`_.
    snet API <https://pkg.go.dev/github.com/scionproto/scion/pkg/snet>
 
 * **For operators of SCION end hosts**:
+  :doc:`manuals/install` |
   :doc:`command/scion/scion` |
   :doc:`manuals/daemon` |
   :doc:`manuals/dispatcher`
 
 * **For operators of** :term:`SCION ASes <AS>`:
+  :doc:`manuals/install` |
   :doc:`manuals/control` |
   :doc:`manuals/router` |
   :doc:`manuals/gateway` |
@@ -98,6 +101,7 @@ Developer Documentation
 
    dev/contribute
    dev/setup
+   dev/build
    dev/run
    dev/style/index
    dev/testing/index
@@ -113,6 +117,7 @@ Start with the :doc:`dev/contribute` to contribute to the open-source SCION impl
 
 * **Building and Running**:
   :doc:`dev/setup` |
+  :doc:`dev/build` |
   :doc:`dev/run` |
   :doc:`dev/dependencies` |
   :doc:`dev/testing/index`
diff --git a/doc/manuals/install.rst b/doc/manuals/install.rst
new file mode 100644
index 000000000..83ca84aa4
--- /dev/null
+++ b/doc/manuals/install.rst
@@ -0,0 +1,114 @@
+************
+Installation
+************
+
+Debian packages
+===============
+
+Installation packages for Debian and derivatives are available for x86-64, arm64, x86-32 and arm.
+
+These packages can be found in the `latest release <https://github.com/scionproto/scion/releases/latest>`_.
+Packages for in-development versions can be found from the `latest nightly build <https://buildkite.com/scionproto/scion-nightly/builds/latest>`_.
+
+.. warning::
+
+   Tests are run only for x86-64. For the other platforms, we cross-compile and don't operate a
+   corresponding test infrastructure. We plan to add test infrastructure also for arm64, but not for
+   the 32 bit platforms.
+
+.. note::
+
+   There is currently no apt repository from which the packages can be installed directly.
+
+.. hint::
+
+   **Systemd**
+
+   The packages include systemd units which can be used to run the SCION components.
+   There are various introduction documents on how to interact with systemd, for example
+   https://wiki.archlinux.org/title/Systemd#Using_units, or https://linuxhandbook.com/systemctl-commands/.
+
+   Very briefly:
+
+   * ``systemctl start <unit>`` / ``systemctl stop <unit>``: start/stop a unit immediately
+   * ``systemctl enable <unit>`` / ``systemctl disable <unit>``: enable/disable a unit to start automatically at boot
+   * ``systemctl status <unit>``: display the status of a unit
+   * ``journalct -u <unit>``: show log of unit
+
+
+Packages
+--------
+
+:doc:`scion-control <control>`
+   :Executable: ``/usr/bin/scion-control``
+   :Systemd Unit:
+      The ``scion-control@.service`` systemd unit template file allows running multiple program
+      instances per host.
+      Create one :ref:`control-conf-toml` file per program instance in ``/etc/scion``.
+      The basename of the configuration file is the instance parameter (the part after the ``@``) for
+      the corresponding systemd template unit instance.
+
+      Example: create configuration ``/etc/scion/cs-1.toml`` and start
+      ``systemctl start scion-control@cs-1.service``.
+
+:doc:`scion-router <router>`
+   :Executable: ``/usr/bin/scion-router``
+   :Systemd Unit:
+      The ``scion-router@.service`` systemd unit template file allows running multiple program
+      instances per host.
+      Create one :ref:`router-conf-toml` file per router instance in ``/etc/scion``.
+      The basename of the configuration file is the instance parameter (the part after the ``@``) for
+      the corresponding systemd template unit instance.
+
+      Example: create configuration ``/etc/scion/br-1.toml`` and start
+      ``systemctl start scion-router@br-1.service``.
+
+:doc:`scion-ip-gateway <gateway>`
+   :Executable: ``/usr/bin/scion-ip-gateway``
+   :Systemd Unit:
+      The ``scion-ip-gateway.service`` systemd unit refers to the default ``/etc/scion/sig.toml``
+      configuration and the traffic policy file ``/etc/scion/sig.json``.
+      The default traffic policy file is incomplete and must be edited before starting the service.
+
+:doc:`scion-daemon <daemon>`
+   The scion-daemon and the scion-dispatcher together form the end host SCION stack.
+
+   :Executable: ``/usr/bin/scion-daemon``
+   :Systemd Unit:
+      The ``scion-daemon.service`` systemd unit refers to the default
+      ``/etc/scion/sciond.toml`` configuration file.
+
+:doc:`scion-dispatcher <dispatcher>`
+   :Executable: ``/usr/bin/scion-dispatcher``
+   :Systemd Unit:
+      The ``scion-dispatcher.service`` systemd unit refers to the default
+      ``/etc/scion/dispatcher.toml`` configuration file.
+
+scion-tools
+   The :doc:`scion </command/scion/scion>` and :doc:`scion-pki</command/scion-pki/scion-pki>`
+   command line tools.
+
+   :Executables: ``/usr/bin/scion``, ``/usr/bin/scion-pki``
+
+.. admonition:: Note
+
+   The configuration manuals for gateway, daemon and dispatcher are currently incomplete.
+
+   In the meantime, the ``sample config`` subcommand (e.g. ``scion-daemon sample config``)
+   describes the available configuration options.
+
+
+Prebuilt Binaries
+=================
+
+"Naked" pre-built binaries are available for Linux x86-64 and
+can be downloaded from the `latest release <https://github.com/scionproto/scion/releases/latest>`_,
+or from the `latest nightly build <https://buildkite.com/scionproto/scion-nightly/builds/latest>`_.
+
+These binaries are statically linked and can run with little requirements on the operating system.
+
+
+.. seealso::
+
+   :doc:`/dev/build`
+      Instructions for :doc:`building from source </dev/build>`.
diff --git a/nogo.json b/nogo.json
index d3ada1568..d9ee896b1 100644
--- a/nogo.json
+++ b/nogo.json
@@ -102,7 +102,8 @@
     },
     "shift": {
         "exclude_files": {
-            "/com_github_marten_seemann_qtls/": ""
+            "/com_github_marten_seemann_qtls/": "",
+            "/org_modernc_mathutil/": ""
         }
     },
     "stdmethods": {
diff --git a/private/mgmtapi/api.bzl b/private/mgmtapi/api.bzl
index 51872d095..e9fc05144 100644
--- a/private/mgmtapi/api.bzl
+++ b/private/mgmtapi/api.bzl
@@ -5,6 +5,7 @@ Macros for generating Go code from OpenAPI specs.
 load("//tools/lint:write_source_files.bzl", "write_source_files")
 load("//rules_openapi:defs.bzl", _openapi_generate_go = "openapi_generate_go")
 load("@npm//private/mgmtapi/tools:@redocly/cli/package_json.bzl", redocly_bin = "bin")
+load("@aspect_bazel_lib//lib:transitions.bzl", "platform_transition_filegroup")
 
 def openapi_docs(
         name,
@@ -20,12 +21,14 @@ def openapi_docs(
         out: The output HTML file.
         **kwargs: Additional arguments to pass to openapi binary.
     """
-
-    redocly_bin.openapi(
+    _target_platform_independent(
+        redocly_bin.openapi,
         name = name,
         srcs = [src],
         outs = [out],
         args = ["build-docs", "--output", "../../../$@", "../../../$(location {})".format(src)],
+        visibility = ["//visibility:private"],
+        tags = ["manual"],
         **kwargs
     )
 
@@ -59,7 +62,8 @@ def openapi_bundle(
         ],
         **kwargs
     )
-    native.genrule(
+    _target_platform_independent(
+        native.genrule,
         name = name,
         srcs = [name + "-no-header"],
         outs = [name + ".bzl.gen.yml"],
@@ -99,7 +103,8 @@ def openapi_generate_go(
         kwargs["out_" + typ] = typ + ".bzl.gen.go"
         write_files[typ + ".gen.go"] = src
 
-    _openapi_generate_go(
+    _target_platform_independent(
+        _openapi_generate_go,
         name = name,
         **kwargs
     )
@@ -108,3 +113,24 @@ def openapi_generate_go(
         name = "write_files",
         files = write_files,
     )
+
+def _target_platform_independent(func, name, **kwargs):
+    kwargs_vt = {}
+    if "visibility" in kwargs:
+        kwargs_vt["visibility"] = kwargs.pop("visibility")
+    if "tags" in kwargs:
+        kwargs_vt["tags"] = kwargs.pop("tags")
+
+    func(
+        name = name + "-platform-independent",
+        visibility = ["//visibility:private"],
+        tags = ["manual"],
+        **kwargs
+    )
+
+    platform_transition_filegroup(
+        name = name,
+        srcs = [name + "-platform-independent"],
+        target_platform = "@local_config_platform//:host",  # reset to default value, to allow reusing this for different target platforms
+        **kwargs_vt
+    )


Test patch:
diff --git a/dist/test/BUILD.bazel b/dist/test/BUILD.bazel
new file mode 100644
index 000000000..ac3f31aef
--- /dev/null
+++ b/dist/test/BUILD.bazel
@@ -0,0 +1,15 @@
+sh_test(
+    name = "deb_test",
+    srcs = ["deb_test.sh"],
+    data = [
+        "Dockerfile",
+        "//dist:deb",
+    ],
+    env = {
+        "SCION_DEB_PACKAGES": "$(locations //dist:deb)",
+    },
+    tags = [
+        "exclusive",
+        "integration",
+    ],
+)
diff --git a/dist/test/Dockerfile b/dist/test/Dockerfile
new file mode 100644
index 000000000..439bb256c
--- /dev/null
+++ b/dist/test/Dockerfile
@@ -0,0 +1,12 @@
+FROM debian:12-slim
+
+# Force debconf (called by apt-get) to be noninteractive
+ENV DEBIAN_FRONTEND=noninteractive
+RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections
+
+RUN apt-get update && apt-get install --assume-yes systemd libcap2-bin
+
+ENV container docker
+
+# Only "boot" a minimal system with journald and nothing else
+CMD ["/bin/systemd", "--unit", "systemd-journald.socket"]
diff --git a/dist/test/README.md b/dist/test/README.md
new file mode 100644
index 000000000..0432944eb
--- /dev/null
+++ b/dist/test/README.md
@@ -0,0 +1,34 @@
+# Test for Debian packages
+
+This is a minimal test for the debian packages built in dist/BUILD.bazel.
+
+## Run
+
+There are two ways to run this test:
+
+```sh
+# Build packages to bazel internal directory and run test
+bazel test --test_output=streamed //dist/test:deb_test
+```
+
+OR
+
+```sh
+# Build packages  .. or any other way to get the packages into deb/
+make dist-deb
+# Run the test script
+dist/test/deb_test.sh
+```
+
+## Scope
+
+The test should determine whether
+
+- the packages can be installed
+- the binaries in the packages are runnable
+- the systemd units in the packages can be used to interact with the SCION services
+
+The test does **not** attempt to simulate a working SCION network.
+The assumption is that if the services installed from the packages
+can be started (meaning they don't crash immediately after startup), the
+findings of the various acceptence and end-to-end integration tests apply.
diff --git a/dist/test/deb_test.sh b/dist/test/deb_test.sh
new file mode 100755
index 000000000..2ae617977
--- /dev/null
+++ b/dist/test/deb_test.sh
@@ -0,0 +1,143 @@
+#!/bin/bash
+
+set -euo pipefail
+
+set -x
+if [ -n ${SCION_DEB_PACKAGES+x} ]; then
+    # Invocation from bazel:
+    # SCION_DEB_PACKAGES is a space-separated list of filenames of (symlinks to) .deb packages.
+    # Below we mount this stuff into a docker container, which won't work with symlinks.
+    # Copy everything into a tmp directory.
+    tmpdir="${TEST_TMPDIR?}"
+    cp ${SCION_DEB_PACKAGES} "${tmpdir}"
+    SCION_DEB_PACKAGES_DIR=$(realpath ${tmpdir})
+else
+    SCION_ROOT=$(realpath $(dirname $0)/../../)
+    SCION_DEB_PACKAGES_DIR=${SCION_DEB_PACKAGES_DIR:-${SCION_ROOT}/deb}
+fi
+DEBUG=${DEBUG:-0}
+set +x
+
+function cleanup {
+    docker container rm -f debian-systemd || true
+    docker image rm --no-prune debian-systemd || true
+}
+cleanup
+
+if [ "$DEBUG" == 0 ]; then  # if DEBUG: keep container debian-systemd running after test
+    trap cleanup EXIT
+fi
+
+# Note: specify absolute path to Dockerfile because docker will not follow bazel's symlinks.
+# Luckily we don't need anything else in this directory.
+docker build -t debian-systemd -f $(realpath dist/test/Dockerfile) dist/test
+
+# Start container with systemd in PID 1.
+# Note: there are ways to avoid --privileged, but its unreliable and appears to depend on the host system
+docker run -d --rm --name debian-systemd -t \
+    --tmpfs /tmp \
+    --tmpfs /run \
+    --tmpfs /run/lock \
+    --tmpfs /run/shm \
+    -v $SCION_DEB_PACKAGES_DIR:/deb \
+    --privileged \
+    debian-systemd:latest
+
+docker exec -i debian-systemd /bin/bash <<'EOF'
+    set -xeuo pipefail
+    arch=$(dpkg --print-architecture)
+
+    # check that the deb files are all here (avoid cryptic error from apt-get)
+    stat /deb/scion-{router,control,dispatcher,daemon,ip-gateway,tools}_*_${arch}.deb > /dev/null
+
+    # router
+    apt-get install /deb/scion-router_*_${arch}.deb
+    cat > /etc/scion/br-1.toml <<INNER_EOF
+        [general]
+        id = "br-1"
+        config_dir = "/etc/scion"
+INNER_EOF
+    cat > /etc/scion/topology.json <<INNER_EOF
+        {
+            "isd_as": "1-ff00:0:a",
+            "mtu": 1472,
+            "border_routers": {
+                "br-1": {
+                    "internal_addr": "127.0.0.1:30001"
+                }
+            },
+            "control_service": {
+                "cs-1": {
+                    "addr": "127.0.0.1:31002"
+                }
+            }
+        }
+INNER_EOF
+    mkdir /etc/scion/keys
+    echo -n 0123456789abcdef | base64 | tee /etc/scion/keys/master{0,1}.key
+    systemctl enable --now scion-router@br-1.service
+    sleep 1
+    systemctl status scion-router@br-1.service
+
+    # dispatcher
+    apt-get install /deb/scion-dispatcher_*_${arch}.deb
+    systemctl enable --now scion-dispatcher.service
+    sleep 1
+    systemctl status scion-dispatcher.service
+    systemctl stop scion-dispatcher.service
+
+    # tools
+    # Install first so we can directly use them to generate some testcrypto
+    # This has a depency on the daemon package
+    apt-get install /deb/scion-tools_*_${arch}.deb /deb/scion-daemon_*_${arch}.deb
+    pushd /tmp/
+    scion-pki testcrypto --topo <(cat << INNER_EOF
+ASes:
+    "1-ff00:0:1": {core: true, voting: true, issuing: true, authoritative: true}
+    "1-ff00:0:a": {cert_issuer: "1-ff00:0:1"}
+INNER_EOF
+    )
+    cp -r gen/ASff00_0_a/* /etc/scion/
+    cp gen/ISD1/trcs/* /etc/scion/certs/
+    popd
+    # ... (to be continued)
+
+    # control
+    apt-get install /deb/scion-control*_${arch}.deb
+    cat > /etc/scion/cs-1.toml << INNER_EOF
+        general.id = "cs-1"
+        general.config_dir = "/etc/scion"
+        trust_db.connection = "/var/lib/scion/cs-1.trust.db"
+        beacon_db.connection = "/var/lib/scion/cs-1.beacon.db"
+        path_db.connection = "/var/lib/scion/cs-1.path.db"
+INNER_EOF
+    systemctl enable --now scion-control@cs-1.service
+    sleep 1
+    systemctl status scion-control@cs-1.service
+    systemctl is-active scion-dispatcher.service  # should be re-started as dependency
+    systemctl stop scion-control@cs-1.service scion-dispatcher.service
+
+    # daemon
+    systemctl enable --now scion-daemon.service
+    systemctl status scion-daemon.service
+    sleep 1
+    systemctl is-active scion-dispatcher.service  # should be re-started as dependency
+    # ... tools (continued)
+    #     now with the daemon running, we can test `scion` e.g. to inspect our local SCION address
+    scion address
+    systemctl stop scion-daemon.service scion-dispatcher.service
+
+    # scion-ip-gateway
+    apt-get install /deb/scion-ip-gateway_*_${arch}.deb
+    systemctl start scion-ip-gateway.service
+    sleep 1
+    # Note: this starts even if the default sig.json is not a valid configuration
+    systemctl status scion-ip-gateway.service
+    systemctl is-active scion-dispatcher.service scion-daemon.service # should be re-started as dependency
+    # Note: the gateway will only create a tunnel device once a session with a
+    # neighbor is up. This is too complicated to arrange in this test. Instead,
+    # we just ensure that the process has the required capabilities to do so.
+    getpcaps $(pidof scion-ip-gateway) | tee /dev/stderr | grep -q "cap_net_admin" || echo "missing capability 'cap_net_admin'"
+
+    echo "Success!"
+EOF