]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
core: make ProtectHostname= optionally take a hostname 35626/head
authorYu Watanabe <watanabe.yu+github@gmail.com>
Sun, 15 Dec 2024 01:36:42 +0000 (10:36 +0900)
committerYu Watanabe <watanabe.yu+github@gmail.com>
Mon, 16 Dec 2024 14:55:44 +0000 (23:55 +0900)
Closes #35623.

man/org.freedesktop.systemd1.xml
man/systemd.exec.xml
src/core/dbus-execute.c
src/core/exec-invoke.c
src/core/execute-serialize.c
src/core/execute.c
src/core/execute.h
src/core/load-fragment-gperf.gperf.in
src/core/load-fragment.c
src/shared/bus-unit-util.c
test/units/TEST-07-PID1.protect-hostname.sh

index d196f4767cea270d6db5b0645f3df4f9bcb18bb6..2ab21f060796f655d33075f780d112cc9e27e90b 100644 (file)
@@ -3359,7 +3359,7 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice {
       @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
       readonly b ProtectHostname = ...;
       @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
-      readonly s ProtectHostnameEx = '...';
+      readonly (ss) ProtectHostnameEx = ...;
       @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
       readonly b MemoryKSM = ...;
       @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
@@ -4885,8 +4885,9 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice {
       <para><varname>ProtectHostnameEx</varname> implement the destination parameter of the
       unit file setting <varname>ProtectHostname=</varname> listed in
       <citerefentry><refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum></citerefentry>.
-      Unlike boolean <varname>ProtectHostname</varname>, <varname>ProtectHostnameEx</varname>
-      is a string type.</para>
+      Unlike boolean <varname>ProtectHostname</varname>, <varname>ProtectHostnameEx</varname> is a pair of
+      strings, the first one is a boolean string or special value <literal>private</literal>, and the second
+      one is an optional private hostname that will be set in a new UTS namespace for the unit.</para>
     </refsect2>
   </refsect1>
 
@@ -5552,7 +5553,7 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket {
       @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
       readonly b ProtectHostname = ...;
       @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
-      readonly s ProtectHostnameEx = '...';
+      readonly (ss) ProtectHostnameEx = ...;
       @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
       readonly b MemoryKSM = ...;
       @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
@@ -7561,7 +7562,7 @@ node /org/freedesktop/systemd1/unit/home_2emount {
       @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
       readonly b ProtectHostname = ...;
       @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
-      readonly s ProtectHostnameEx = '...';
+      readonly (ss) ProtectHostnameEx = ...;
       @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
       readonly b MemoryKSM = ...;
       @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
@@ -9537,7 +9538,7 @@ node /org/freedesktop/systemd1/unit/dev_2dsda3_2eswap {
       @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
       readonly b ProtectHostname = ...;
       @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
-      readonly s ProtectHostnameEx = '...';
+      readonly (ss) ProtectHostnameEx = ...;
       @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
       readonly b MemoryKSM = ...;
       @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
index 0413ac9025b7c7dd33045da9c2966993023b4074..fe84be0b851c2695d0aa5ef1a2c3de06c1ffaf1b 100644 (file)
@@ -2062,11 +2062,13 @@ BindReadOnlyPaths=/var/lib/systemd</programlisting>
       <varlistentry>
         <term><varname>ProtectHostname=</varname></term>
 
-        <listitem><para>Takes a boolean argument or <literal>private</literal>. If enabled, sets up a new UTS namespace
-        for the executed processes. If set to a true value, changing hostname or domainname via
-        <function>sethostname()</function> and <function>setdomainname()</function> system calls is prevented. If set to
-        <literal>private</literal>, changing hostname or domainname is allowed but only affects the unit's UTS namespace.
-        Defaults to off.</para>
+        <listitem><para>Takes a boolean argument or <literal>private</literal>. If enabled, sets up a new UTS
+        namespace for the executed processes. If enabled, a hostname can be optionally specified following a
+        colon (e.g. <literal>yes:foo</literal> or <literal>private:host.example.com</literal>), and the
+        hostname is set in the new UTS namespace for the unit. If set to a true value, changing hostname or
+        domainname via <function>sethostname()</function> and <function>setdomainname()</function> system
+        calls is prevented. If set to <literal>private</literal>, changing hostname or domainname is allowed
+        but only affects the unit's UTS namespace. Defaults to off.</para>
 
         <para>Note that the implementation of this setting might be impossible (for example if UTS namespaces
         are not available), and the unit should be written in a way that does not solely rely on this setting
index bfd6694683cf1585c141907a1cdbdee0c216cf0a..5ac08c996f1b7a4f528cb1f2cf265ea451cfa4e3 100644 (file)
@@ -13,6 +13,7 @@
 #include "creds-util.h"
 #include "dbus-execute.h"
 #include "dbus-util.h"
+#include "dns-domain.h"
 #include "env-util.h"
 #include "errno-list.h"
 #include "escape.h"
@@ -21,6 +22,7 @@
 #include "fd-util.h"
 #include "fileio.h"
 #include "hexdecoct.h"
+#include "hostname-util.h"
 #include "iovec-util.h"
 #include "ioprio-util.h"
 #include "journal-file.h"
@@ -64,7 +66,6 @@ static BUS_DEFINE_PROPERTY_GET_REF(property_get_private_tmp_ex, "s", PrivateTmp,
 static BUS_DEFINE_PROPERTY_GET_REF(property_get_private_users_ex, "s", PrivateUsers, private_users_to_string);
 static BUS_DEFINE_PROPERTY_GET_REF(property_get_protect_control_groups_ex, "s", ProtectControlGroups, protect_control_groups_to_string);
 static BUS_DEFINE_PROPERTY_GET_REF(property_get_private_pids, "s", PrivatePIDs, private_pids_to_string);
-static BUS_DEFINE_PROPERTY_GET_REF(property_get_protect_hostname_ex, "s", ProtectHostname, protect_hostname_to_string);
 static BUS_DEFINE_PROPERTY_GET_REF(property_get_syslog_level, "i", int, LOG_PRI);
 static BUS_DEFINE_PROPERTY_GET_REF(property_get_syslog_facility, "i", int, LOG_FAC);
 static BUS_DEFINE_PROPERTY_GET(property_get_cpu_affinity_from_numa, "b", ExecContext, exec_context_get_cpu_affinity_from_numa);
@@ -1084,6 +1085,20 @@ static int property_get_protect_hostname(
         return sd_bus_message_append_basic(reply, 'b', &b);
 }
 
+static int property_get_protect_hostname_ex(
+                sd_bus *bus,
+                const char *path,
+                const char *interface,
+                const char *property,
+                sd_bus_message *reply,
+                void *userdata,
+                sd_bus_error *error) {
+
+        ExecContext *c = ASSERT_PTR(userdata);
+
+        return sd_bus_message_append(reply, "(ss)", protect_hostname_to_string(c->protect_hostname), c->private_hostname);
+}
+
 const sd_bus_vtable bus_exec_vtable[] = {
         SD_BUS_VTABLE_START(0),
         SD_BUS_PROPERTY("Environment", "as", NULL, offsetof(ExecContext, environment), SD_BUS_VTABLE_PROPERTY_CONST),
@@ -1259,7 +1274,7 @@ const sd_bus_vtable bus_exec_vtable[] = {
         SD_BUS_PROPERTY("ProtectProc", "s", property_get_protect_proc, offsetof(ExecContext, protect_proc), SD_BUS_VTABLE_PROPERTY_CONST),
         SD_BUS_PROPERTY("ProcSubset", "s", property_get_proc_subset, offsetof(ExecContext, proc_subset), SD_BUS_VTABLE_PROPERTY_CONST),
         SD_BUS_PROPERTY("ProtectHostname", "b", property_get_protect_hostname, offsetof(ExecContext, protect_hostname), SD_BUS_VTABLE_PROPERTY_CONST),
-        SD_BUS_PROPERTY("ProtectHostnameEx", "s", property_get_protect_hostname_ex, offsetof(ExecContext, protect_hostname), SD_BUS_VTABLE_PROPERTY_CONST),
+        SD_BUS_PROPERTY("ProtectHostnameEx", "(ss)", property_get_protect_hostname_ex, 0, SD_BUS_VTABLE_PROPERTY_CONST),
         SD_BUS_PROPERTY("MemoryKSM", "b", bus_property_get_tristate, offsetof(ExecContext, memory_ksm), SD_BUS_VTABLE_PROPERTY_CONST),
         SD_BUS_PROPERTY("NetworkNamespacePath", "s", NULL, offsetof(ExecContext, network_namespace_path), SD_BUS_VTABLE_PROPERTY_CONST),
         SD_BUS_PROPERTY("IPCNamespacePath", "s", NULL, offsetof(ExecContext, ipc_namespace_path), SD_BUS_VTABLE_PROPERTY_CONST),
@@ -2027,21 +2042,29 @@ int bus_exec_context_set_transient_property(
         }
 
         if (streq(name, "ProtectHostnameEx")) {
-                const char *s;
-                ProtectHostname t;
+                const char *s, *h = NULL;
 
-                r = sd_bus_message_read(message, "s", &s);
+                r = sd_bus_message_read(message, "(ss)", &s, &h);
                 if (r < 0)
                         return r;
 
-                t = protect_hostname_from_string(s);
-                if (t < 0)
+                if (!isempty(h) && !hostname_is_valid(h, /* flags = */ 0))
+                        return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid hostname in %s setting: %s", name, h);
+
+                ProtectHostname t = protect_hostname_from_string(s);
+                if (t < 0 || (t == PROTECT_HOSTNAME_NO && !isempty(h)))
                         return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid %s setting: %s", name, s);
 
                 if (!UNIT_WRITE_FLAGS_NOOP(flags)) {
                         c->protect_hostname = t;
-                        (void) unit_write_settingf(u, flags, name, "ProtectHostname=%s",
-                                                   protect_hostname_to_string(c->protect_hostname));
+                        r = free_and_strdup(&c->private_hostname, empty_to_null(h));
+                        if (r < 0)
+                                return r;
+
+                        (void) unit_write_settingf(u, flags, name, "ProtectHostname=%s%s%s",
+                                                   protect_hostname_to_string(c->protect_hostname),
+                                                   c->private_hostname ? ":" : "",
+                                                   strempty(c->private_hostname));
                 }
 
                 return 1;
index fe1ee884dae8d1349dda17ad2e500c72ebebe7cd..ea21d6d42ba2d15bbd6fcc52d090a3c78d2cc559 100644 (file)
@@ -40,6 +40,7 @@
 #include "exit-status.h"
 #include "fd-util.h"
 #include "hexdecoct.h"
+#include "hostname-setup.h"
 #include "io-util.h"
 #include "iovec-util.h"
 #include "journal-send.h"
@@ -1698,6 +1699,8 @@ static int apply_restrict_filesystems(const ExecContext *c, const ExecParameters
 #endif
 
 static int apply_protect_hostname(const ExecContext *c, const ExecParameters *p, int *ret_exit_status) {
+        int r;
+
         assert(c);
         assert(p);
 
@@ -1714,6 +1717,13 @@ static int apply_protect_hostname(const ExecContext *c, const ExecParameters *p,
                         log_exec_warning(c, p,
                                          "ProtectHostname=%s is configured, but UTS namespace setup is prohibited (container manager?), ignoring namespace setup.",
                                          protect_hostname_to_string(c->protect_hostname));
+
+                } else if (c->private_hostname) {
+                        r = sethostname_idempotent(c->private_hostname);
+                        if (r < 0) {
+                                *ret_exit_status = EXIT_NAMESPACE;
+                                return log_exec_error_errno(c, p, r, "Failed to set private hostname '%s': %m", c->private_hostname);
+                        }
                 }
         } else
                 log_exec_warning(c, p,
@@ -1722,8 +1732,6 @@ static int apply_protect_hostname(const ExecContext *c, const ExecParameters *p,
 
 #if HAVE_SECCOMP
         if (c->protect_hostname == PROTECT_HOSTNAME_YES) {
-                int r;
-
                 if (skip_seccomp_unavailable(c, p, "ProtectHostname="))
                         return 0;
 
index 9dce5a9c2587ea79deb792dba1fb185f838c8a26..f05c69bf2c5aca5720a6fb8fb3d2394045b6aa48 100644 (file)
@@ -1982,6 +1982,10 @@ static int exec_context_serialize(const ExecContext *c, FILE *f) {
         if (r < 0)
                 return r;
 
+        r = serialize_item(f, "exec-context-private-hostname", c->private_hostname);
+        if (r < 0)
+                return r;
+
         r = serialize_item(f, "exec-context-protect-proc", protect_proc_to_string(c->protect_proc));
         if (r < 0)
                 return r;
@@ -2884,6 +2888,10 @@ static int exec_context_deserialize(ExecContext *c, FILE *f) {
                         c->protect_hostname = protect_hostname_from_string(val);
                         if (c->protect_hostname < 0)
                                 return -EINVAL;
+                } else if ((val = startswith(l, "exec-context-private-hostname="))) {
+                        r = free_and_strdup(&c->private_hostname, val);
+                        if (r < 0)
+                                return r;
                 } else if ((val = startswith(l, "exec-context-protect-proc="))) {
                         c->protect_proc = protect_proc_from_string(val);
                         if (c->protect_proc < 0)
index a01096a70005216adaea059b37f7275558117c0d..3c8d4c2be1211363e939d829d797c81972269f50 100644 (file)
@@ -723,6 +723,8 @@ void exec_context_done(ExecContext *c) {
         c->root_image_policy = image_policy_free(c->root_image_policy);
         c->mount_image_policy = image_policy_free(c->mount_image_policy);
         c->extension_image_policy = image_policy_free(c->extension_image_policy);
+
+        c->private_hostname = mfree(c->private_hostname);
 }
 
 int exec_context_destroy_runtime_directory(const ExecContext *c, const char *runtime_prefix) {
@@ -1066,7 +1068,7 @@ void exec_context_dump(const ExecContext *c, FILE* f, const char *prefix) {
                 "%sRestrictRealtime: %s\n"
                 "%sRestrictSUIDSGID: %s\n"
                 "%sKeyringMode: %s\n"
-                "%sProtectHostname: %s\n"
+                "%sProtectHostname: %s%s%s\n"
                 "%sProtectProc: %s\n"
                 "%sProcSubset: %s\n",
                 prefix, c->umask,
@@ -1093,7 +1095,7 @@ void exec_context_dump(const ExecContext *c, FILE* f, const char *prefix) {
                 prefix, yes_no(c->restrict_realtime),
                 prefix, yes_no(c->restrict_suid_sgid),
                 prefix, exec_keyring_mode_to_string(c->keyring_mode),
-                prefix, protect_hostname_to_string(c->protect_hostname),
+                prefix, protect_hostname_to_string(c->protect_hostname), c->private_hostname ? ":" : "", strempty(c->private_hostname),
                 prefix, protect_proc_to_string(c->protect_proc),
                 prefix, proc_subset_to_string(c->proc_subset));
 
index f1b94e7f4c00311196a508bafbe0e8591e8a628f..6421f19cc447aace46cccbcefb149e5e2b6e6876 100644 (file)
@@ -337,6 +337,7 @@ struct ExecContext {
         ProtectHome protect_home;
         PrivatePIDs private_pids;
         ProtectHostname protect_hostname;
+        char *private_hostname;
 
         bool dynamic_user;
         bool remove_ipc;
index 90290a8b0eed918f87443df169ed13503d2060aa..04a560110e60105b83d3ed2cc45fe75baa4cb7f6 100644 (file)
 {% else %}
 {{type}}.SmackProcessLabel,                   config_parse_warn_compat,                           DISABLED_CONFIGURATION,             0
 {% endif %}
-{{type}}.ProtectHostname,                     config_parse_protect_hostname,                      0,                                  offsetof({{type}}, exec_context.protect_hostname)
+{{type}}.ProtectHostname,                     config_parse_protect_hostname,                      0,                                  offsetof({{type}}, exec_context)
 {{type}}.MemoryKSM,                           config_parse_tristate,                              0,                                  offsetof({{type}}, exec_context.memory_ksm)
 {%- endmacro -%}
 
index a108216a960452c365a063c30955b716cdc87b4e..eca3ee6872b0013ddfdfb4b00a2dc1bf3c501a05 100644 (file)
@@ -40,6 +40,7 @@
 #include "fs-util.h"
 #include "fstab-util.h"
 #include "hexdecoct.h"
+#include "hostname-util.h"
 #include "iovec-util.h"
 #include "ioprio-util.h"
 #include "ip-protocol-list.h"
@@ -141,7 +142,6 @@ DEFINE_CONFIG_PARSE_ENUM(config_parse_exec_utmp_mode, exec_utmp_mode, ExecUtmpMo
 DEFINE_CONFIG_PARSE_ENUM(config_parse_job_mode, job_mode, JobMode);
 DEFINE_CONFIG_PARSE_ENUM(config_parse_notify_access, notify_access, NotifyAccess);
 DEFINE_CONFIG_PARSE_ENUM(config_parse_protect_home, protect_home, ProtectHome);
-DEFINE_CONFIG_PARSE_ENUM(config_parse_protect_hostname, protect_hostname, ProtectHostname);
 DEFINE_CONFIG_PARSE_ENUM(config_parse_protect_system, protect_system, ProtectSystem);
 DEFINE_CONFIG_PARSE_ENUM(config_parse_exec_preserve_mode, exec_preserve_mode, ExecPreserveMode);
 DEFINE_CONFIG_PARSE_ENUM(config_parse_service_type, service_type, ServiceType);
@@ -6743,3 +6743,53 @@ int config_parse_cgroup_nft_set(
 
         return config_parse_nft_set(unit, filename, line, section, section_line, lvalue, ltype, rvalue, &c->nft_set_context, u);
 }
+
+int config_parse_protect_hostname(
+                const char *unit,
+                const char *filename,
+                unsigned line,
+                const char *section,
+                unsigned section_line,
+                const char *lvalue,
+                int ltype,
+                const char *rvalue,
+                void *data,
+                void *userdata) {
+
+        ExecContext *c = ASSERT_PTR(data);
+        Unit *u = ASSERT_PTR(userdata);
+        _cleanup_free_ char *h = NULL, *p = NULL;
+        int r;
+
+        if (isempty(rvalue)) {
+                c->protect_hostname = PROTECT_HOSTNAME_NO;
+                c->private_hostname = mfree(c->private_hostname);
+                return 1;
+        }
+
+        const char *colon = strchr(rvalue, ':');
+        if (colon) {
+                r = unit_full_printf_full(u, colon + 1, HOST_NAME_MAX, &h);
+                if (r < 0) {
+                        log_syntax(unit, LOG_WARNING, filename, line, r,
+                                   "Failed to resolve unit specifiers in '%s', ignoring: %m", colon + 1);
+                        return 0;
+                }
+
+                if (!hostname_is_valid(h, /* flags = */ 0))
+                        return log_syntax(unit, LOG_WARNING, filename, line, 0,
+                                          "Invalid hostname is specified to %s=, ignoring: %s", lvalue, h);
+
+                p = strndup(rvalue, colon - rvalue);
+                if (!p)
+                        return log_oom();
+        }
+
+        ProtectHostname t = protect_hostname_from_string(p ?: rvalue);
+        if (t < 0 || (t == PROTECT_HOSTNAME_NO && h))
+                return log_syntax_parse_error(unit, filename, line, 0, lvalue, rvalue);
+
+        c->protect_hostname = t;
+        free_and_replace(c->private_hostname, h);
+        return 1;
+}
index 4e623036d0353d0fee2082541323f51134eb06fe..a499c0e9845973137e3bc3f9d1fa1ec1306dba93 100644 (file)
@@ -1045,7 +1045,6 @@ static int bus_append_execute_property(sd_bus_message *m, const char *field, con
                               "SyslogIdentifier",
                               "ProtectSystem",
                               "ProtectHome",
-                              "ProtectHostnameEx",
                               "PrivateTmpEx",
                               "PrivateUsersEx",
                               "ProtectControlGroupsEx",
@@ -2269,6 +2268,24 @@ static int bus_append_execute_property(sd_bus_message *m, const char *field, con
                 return 1;
         }
 
+        if (streq(field, "ProtectHostnameEx")) {
+                const char *colon = strchr(eq, ':');
+                if (colon) {
+                        if (isempty(colon + 1))
+                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to parse argument: %s=%s", field, eq);
+
+                        _cleanup_free_ char *p = strndup(eq, colon - eq);
+                        if (!p)
+                                return -ENOMEM;
+
+                        r = sd_bus_message_append(m, "(sv)", field, "(ss)", p, colon + 1);
+                } else
+                        r = sd_bus_message_append(m, "(sv)", field, "(ss)", eq, NULL);
+                if (r < 0)
+                        return bus_log_create_error(r);
+
+                return 1;
+        }
         return 0;
 }
 
index c2ede395535f5c32fef237a6d7e0b507adc9c6fe..10d448b80debf95e6446251df8bba26925981a6e 100755 (executable)
@@ -21,6 +21,33 @@ testcase_yes() {
     # can only set hostname.
     (! systemd-run --wait -p ProtectHostname=yes hostname foo)
 
+    # ProtectHostname=yes can optionally take a hostname.
+    systemd-run --wait -p ProtectHostnameEx=yes:hoge \
+        -P bash -xec '
+            test "$(hostname)" = "hoge"
+            (! hostname foo)
+            test "$(hostname)" = "hoge"
+        '
+
+    # Verify host hostname is unchanged.
+    test "$(hostname)" = "$LEGACY_HOSTNAME"
+    test "$(hostnamectl hostname)" = "$HOSTNAME_FROM_SYSTEMD"
+
+    # ProtectHostname= supportes specifiers.
+    mkdir -p /run/systemd/system/
+    cat >/run/systemd/system/test-protect-hostname-yes@.service <<EOF
+[Service]
+Type=oneshot
+ExecStart=bash -xec 'test "\$\$(hostname)" = "%i"; (! hostname foo); test "\$\$(hostname)" = "%i"'
+ProtectHostname=yes:%i
+EOF
+    systemctl daemon-reload
+    systemctl start --wait test-protect-hostname-yes@hoge.example.com.service
+
+    # Verify host hostname is unchanged.
+    test "$(hostname)" = "$LEGACY_HOSTNAME"
+    test "$(hostnamectl hostname)" = "$HOSTNAME_FROM_SYSTEMD"
+
     systemd-run --wait -p ProtectHostname=yes -p PrivateMounts=yes \
         findmnt --mountpoint /proc/sys/kernel/hostname
 }
@@ -36,9 +63,51 @@ testcase_private() {
     test "$(hostname)" = "$LEGACY_HOSTNAME"
     test "$(hostnamectl hostname)" = "$HOSTNAME_FROM_SYSTEMD"
 
+    # ProtectHostname=private can optionally take a hostname.
+    systemd-run --wait -p ProtectHostnameEx=private:hoge \
+        -P bash -xec '
+            test "$(hostname)" = "hoge"
+            hostname foo
+            test "$(hostname)" = "foo"
+        '
+
+    # Verify host hostname is unchanged.
+    test "$(hostname)" = "$LEGACY_HOSTNAME"
+    test "$(hostnamectl hostname)" = "$HOSTNAME_FROM_SYSTEMD"
+
+    # ProtectHostname= supportes specifiers.
+    mkdir -p /run/systemd/system/
+    cat >/run/systemd/system/test-protect-hostname-private@.service <<EOF
+[Service]
+Type=oneshot
+ExecStart=bash -xec 'test "\$\$(hostname)" = "%i"; hostname foo; test "\$\$(hostname)" = "foo"'
+ProtectHostname=private:%i
+EOF
+    systemctl daemon-reload
+    systemctl start --wait test-protect-hostname-private@hoge.example.com.service
+
+    # Verify host hostname is unchanged.
+    test "$(hostname)" = "$LEGACY_HOSTNAME"
+    test "$(hostnamectl hostname)" = "$HOSTNAME_FROM_SYSTEMD"
+
     # Verify /proc/sys/kernel/hostname is not bind mounted from host read-only.
     (! systemd-run --wait -p ProtectHostnameEx=private -p PrivateMounts=yes \
         findmnt --mountpoint /proc/sys/kernel/hostname)
 }
 
+testcase_invalid() {
+    # ProtectHostname=no cannot take hostname.
+    (! systemd-run --wait -p ProtectHostnameEx=no:hoge true)
+
+    # Invalid hostname.
+    (! systemd-run --wait -p ProtectHostnameEx=yes: true)
+    (! systemd-run --wait -p ProtectHostnameEx=yes:.foo true)
+    (! systemd-run --wait -p ProtectHostnameEx=yes:foo.-example.com true)
+    (! systemd-run --wait -p ProtectHostnameEx=yes:foo..example.com true)
+    (! systemd-run --wait -p ProtectHostnameEx=private: true)
+    (! systemd-run --wait -p ProtectHostnameEx=private:.foo true)
+    (! systemd-run --wait -p ProtectHostnameEx=private:foo.-example.com true)
+    (! systemd-run --wait -p ProtectHostnameEx=private:foo..example.com true)
+}
+
 run_testcases