]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
varlinkctl: add pluggable protocol support to sd-varlink
authorMichael Vogt <michael@amutable.com>
Wed, 11 Feb 2026 15:01:18 +0000 (16:01 +0100)
committerMike Yuan <me@yhndnzj.com>
Fri, 13 Feb 2026 18:07:20 +0000 (19:07 +0100)
When sd_varlink_connect_url() gets an unknown URL we now
check if there is a `$LIBEXECDIR/varlink-bridges/$scheme`
binary and execute it (with the url as the first arguments).

This makes varlink more flexible as it provides a way to
dynamically add "bridges" in LIBEXECDIR/varlink-bridges/. This is
conceptually similar to the libvarlink `varlink --bridge` command
and allows to e.g. call varlink over http{,s} via e.g. the new
varlink-http-bridge.

With a running varlink-http-bridge [0] one can do:
```console
$ varlinkctl call http://localhost:8080/ws/sockets/io.systemd.Hostname \
    io.systemd.Hostname.Describe {}
{
        "Hostname" : "top",
...
```

Closes: https://github.com/systemd/systemd/issues/40640
[0] https://github.com/mvo5/varlink-http-bridge/pull/1

docs/ENVIRONMENT.md
man/sd-varlink.xml
meson.build
src/libsystemd/sd-varlink/sd-varlink.c
src/varlinkctl/meson.build

index fb2984b0ecb78d4d52b71b9faab5465c6fed60bb..2de688eb5df53777e2cbc9f52a33954d284cf41d 100644 (file)
@@ -760,6 +760,9 @@ Tools using the Varlink protocol (such as `varlinkctl`) or sd-bus (such as
   would listen on. If set to "-" the tool will turn stdin/stdout into a Varlink
   connection.
 
+* `$SYSTEMD_VARLINK_BRIDGES_DIR` – overrides the default `$LIBEXEC/varlink-bridges/`
+  path when looking up custom scheme bridge helper binaries.
+
 `systemd-mountfsd`:
 
 * `$SYSTEMD_MOUNTFSD_TRUSTED_DIRECTORIES` – takes a boolean argument. If true
index 282d6a330ef1f62ba5782a901bd7b51d73885ca0..576bdf1b7f8d1cea79deb0c6d487493e8c5af06d 100644 (file)
 
   <xi:include href="libsystemd-pkgconfig.xml" />
 
+  <refsect1>
+    <title>Directories</title>
+
+    <variablelist>
+      <varlistentry>
+        <term><filename>/usr/lib/systemd/varlink-bridges/</filename></term>
+        <listitem>
+          <para>When <function>sd_varlink_connect_url()</function> encounters a URL with a scheme that is not
+            natively supported, it looks for a bridge helper binary named after the URL scheme in this
+            directory. The binary is invoked the same way as <literal>exec:</literal> binaries but with the full
+            URL passed as the first command line argument.</para>
+
+          <para>For example, if
+            <command>varlinkctl introspect https://example.com/ws/sockets/io.systemd.Hostname</command>
+            is called, <command>varlinkctl</command> will look for an executable
+            <filename>/usr/lib/systemd/varlink-bridges/https</filename> and invoke it with
+            <literal>https://example.com/ws/sockets/io.systemd.Hostname</literal> as its only
+            argument.</para>
+        </listitem>
+      </varlistentry>
+    </variablelist>
+  </refsect1>
+
   <refsect1>
     <title>See Also</title>
     <para><simplelist type="inline">
index c0319122f4c5fd19c7eed556ebafb30892eace98..f562754c19bbd049f8026207ad6a441792ed0b64 100644 (file)
@@ -183,6 +183,7 @@ ntpservicelistdir = libexecdir / 'ntp-units.d'
 credstoredir = prefixdir / 'lib/credstore'
 pcrlockdir = prefixdir / 'lib/pcrlock.d'
 mimepackagesdir = prefixdir / 'share/mime/packages'
+varlinkbridgesdir = libexecdir / 'varlink-bridges'
 
 configfiledir = get_option('configfiledir')
 if configfiledir == ''
@@ -311,6 +312,7 @@ conf.set_quoted('USER_GENERATOR_DIR',                         usergeneratordir)
 conf.set_quoted('USER_KEYRING_PATH',                          pkgsysconfdir / 'import-pubring.pgp')
 conf.set_quoted('USER_KEYRING_PATH_LEGACY',                   pkgsysconfdir / 'import-pubring.gpg')
 conf.set_quoted('USER_PRESET_DIR',                            userpresetdir)
+conf.set_quoted('VARLINK_BRIDGES_DIR',                        varlinkbridgesdir)
 conf.set_quoted('VENDOR_KEYRING_PATH',                        libexecdir / 'import-pubring.pgp')
 
 conf.set('ANSI_OK_COLOR',                                     'ANSI_' + get_option('ok-color').underscorify().to_upper())
index de1d172762aad792cf372ba458b6b73613eb7337..32daf27d6a6e0c1d91ee8b0be576797e133b64d7 100644 (file)
@@ -485,6 +485,14 @@ static int varlink_connect_ssh_exec(sd_varlink **ret, const char *where) {
         return 0;
 }
 
+/* Do basic validation of the URL scheme (loosely following RFC 1738) */
+static bool is_valid_url_scheme(const char *s) {
+        return !isempty(s) &&
+                strchr(LOWERCASE_LETTERS, s[0]) &&
+                in_charset(s, LOWERCASE_LETTERS DIGITS "+.-") &&
+                filename_is_valid(s);
+}
+
 _public_ int sd_varlink_connect_url(sd_varlink **ret, const char *url) {
         _cleanup_free_ char *c = NULL;
         const char *p;
@@ -499,7 +507,7 @@ _public_ int sd_varlink_connect_url(sd_varlink **ret, const char *url) {
         assert_return(ret, -EINVAL);
         assert_return(url, -EINVAL);
 
-        // FIXME: Maybe add support for vsock: and ssh-exec: URL schemes here.
+        // FIXME: Maybe add support for vsock: URL schemes here.
 
         /* The Varlink URL scheme is a bit underdefined. We support only the spec-defined unix: transport for
          * now, plus exec:, ssh: transports we made up ourselves. Strictly speaking this shouldn't even be
@@ -514,11 +522,39 @@ _public_ int sd_varlink_connect_url(sd_varlink **ret, const char *url) {
                 scheme = SCHEME_SSH_UNIX;
         else if ((p = startswith(url, "ssh-exec:")))
                 scheme = SCHEME_SSH_EXEC;
-        else
-                return log_debug_errno(SYNTHETIC_ERRNO(EPROTONOSUPPORT), "URL scheme not supported.");
+        else {
+                /* scheme is not built-in: check if we have a bridge helper binary */
+                const char *colon = strchr(url, ':');
+                if (!colon)
+                        return log_debug_errno(SYNTHETIC_ERRNO(EPROTONOSUPPORT),
+                                               "Invalid URL '%s': does not contain a ':'", url);
+
+                _cleanup_free_ char *scheme_name = strndup(url, colon - url);
+                if (!scheme_name)
+                        return log_oom_debug();
+
+                if (!is_valid_url_scheme(scheme_name))
+                        return log_debug_errno(SYNTHETIC_ERRNO(EPROTONOSUPPORT),
+                                               "URL scheme not valid as bridge name: %s", scheme_name);
+
+                const char *bridges_dir = secure_getenv("SYSTEMD_VARLINK_BRIDGES_DIR") ?: VARLINK_BRIDGES_DIR;
+                _cleanup_free_ char *bridge = path_join(bridges_dir, scheme_name);
+                if (!bridge)
+                        return log_oom_debug();
+
+                if (access(bridge, X_OK) < 0) {
+                        if (errno == ENOENT)
+                                return log_debug_errno(SYNTHETIC_ERRNO(EPROTONOSUPPORT), "URL scheme '%s' not supported (and no auxiliary bridge binary is available).", scheme_name);
+
+                        return log_debug_errno(errno, "Failed to look up varlink bridge binary '%s': %m", bridge);
+                }
+
+                return sd_varlink_connect_exec(ret, bridge, STRV_MAKE(bridge, url));
+        }
 
         /* The varlink.org reference C library supports more than just file system paths. We might want to
-         * support that one day too. For now simply refuse that. */
+         * support that one day too. For now simply refuse that for our built-in schemes. It is fine for
+         * external scheme handled via plugins (see above). */
         if (p[strcspn(p, ";?#")] != '\0')
                 return log_debug_errno(SYNTHETIC_ERRNO(EPROTONOSUPPORT), "URL parameterization with ';', '?', '#' not supported.");
 
index c1074dc50a1a5b12ed47a97017450755a419fd2f..0ba12d46cdb23c76bb94e6e5a2c664edee4a143b 100644 (file)
@@ -11,3 +11,5 @@ executables += [
                 'sources' : varlinkctl_sources,
         },
 ]
+
+install_emptydir(varlinkbridgesdir)