No description
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
HWE70 7e0075418a Ignore browser temporaries in the watcher, and fail loudly on a missing file
The watcher launched a session twice per download, and the first one was
always a dead end. Chromium does not only use "<name>.crdownload": for some
downloads it writes a hidden ".org.chromium.Chromium.XXXXXX" first and
renames it to the final name a moment later. close_write fires on the
temporary name, which is gone by the time wfica gets there.

What made this expensive to diagnose is how Citrix reports a missing file:
it opens its own file chooser, pointing at a directory that looks empty. No
path, no filename, no hint at who asked -- from the outside it looks as if
the browser had started the session itself.

Observed in the watcher log:

  09:10:12 Detected ICA file: /home/marc/Downloads/.org.chromium.Chromium.A9Y57Z
  09:10:13 Detected ICA file: /home/marc/Downloads/3bdc81b1-18ea-49a6-b161-616e98ac95bc

Filtering on the leading dot rather than that one literal prefix also keeps
Chrome's ".com.google.Chrome.XXXXXX" and Firefox's temporaries out, and
costs nothing -- a real download never arrives as a hidden file.

ica-launch.sh now checks the file exists before handing it to Citrix. The
watcher no longer produces such a path, but any caller can, and a one-line
error beats an empty file chooser.

Verified against a simulated download sequence: the temp-then-rename pattern
now yields exactly one launch, on the final name, with the file present; a
plain "<name>.ica" download still yields one; a dead path exits 1 with a
message naming it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 09:32:38 +02:00
.gitignore Add container setup for Citrix ICAClient on Fedora (webkit2gtk3 gap workaround) 2026-08-17 18:20:36 +02:00
ica-download-watcher.sh Ignore browser temporaries in the watcher, and fail loudly on a missing file 2026-09-09 09:32:38 +02:00
ica-launch.sh Ignore browser temporaries in the watcher, and fail loudly on a missing file 2026-09-09 09:32:38 +02:00
install-browser-launcher.sh Correct the PSS explanation: middleware gap, not a card limitation 2026-08-19 23:01:40 +02:00
install-download-watcher.sh Add a content-based watcher for .ica downloads without a filename extension 2026-09-03 16:21:28 +02:00
install-mime-handler.sh Translate repository documentation to English 2026-08-19 22:46:11 +02:00
LICENSE Add container setup for Citrix ICAClient on Fedora (webkit2gtk3 gap workaround) 2026-08-17 18:20:36 +02:00
README.md Add a content-based watcher for .ica downloads without a filename extension 2026-09-03 16:21:28 +02:00
setup-container.sh Bind-mount /dev instead of listing camera devices one by one 2026-09-09 09:25:12 +02:00

🖥️ fedora-ica-client

License: MIT AI-assisted

Citrix Workspace App (ICAClient) is built and tested against a handful of enterprise distributions — Fedora is not among them, and it moves its underpinnings along quickly. This project therefore wraps the client in a Podman container based on Ubuntu 24.04 LTS (officially supported by Citrix) and passes graphics, smart card reader, webcam and audio through cleanly. A Fedora version jump no longer drags the Citrix installation down with it.

Also mirrored on OpenCommit (git.highwire.de is LAN-only).

This project was developed with the assistance of Claude (Anthropic).

🧩 Why a container?

Citrix supports only a few Linux distributions (RHEL, Ubuntu LTS, Debian, openSUSE). Fedora is not one of them and moves considerably faster: libraries disappear, ABIs change. That is exactly what broke the client here once before, when Fedora dropped webkit2gtk3 outright. The container decouples the client's runtime environment from the host — moving to the next Fedora release leaves the installation untouched.

Why Ubuntu 24.04 and not CentOS? Earlier versions of this project used CentOS Stream 9, because webkit2gtk3 was still natively available there. That is obsolete: the client now ships webkit2gtk-4.0 itself and unpacks it during installation. Compared to CentOS, Ubuntu saves two workarounds:

  • The library paths in scardConfig.json are natively correct — the package is built for Debian/Ubuntu. On CentOS they had to be rewritten.
  • pcsc-lite is recent enough to talk to the host's pcscd without a custom build. On CentOS (1.9.4) that build was required.

🚀 Setup

Requirements: podman, a downloaded icaclient-*_amd64.deb from the Citrix download page (the Debian/Ubuntu variant, not the RHEL RPM), and a running graphical session (X11 or Wayland/XWayland).

git clone https://git.highwire.de/HWE70/fedora-ica-client.git
cd fedora-ica-client
./setup-container.sh
./install-mime-handler.sh   # make .ica files openable straight from file manager/browser
./install-browser-launcher.sh https://gateway.example.com   # set up the login browser

The .deb is looked up in ~/Downloads and one level below; a different location can be set via ICACLIENT_DEB_GLOB.

On the first run, setup-container.sh performs these steps in order:

  1. Create the container — Ubuntu 24.04, --network host (the ICA client is a pure network client, not a server, so host networking is exactly right here), --pid host (see below), and targeted bind mounts for the home directory, the X11 socket, XDG_RUNTIME_DIR and the PC/SC daemon socket.
  2. Install the Citrix .debapt resolves the dependencies; the bundled webkit2gtk-4.0 is unpacked by the package itself.
  3. Install OpenSC — not a dependency of the Citrix package, but required for the smart card.
  4. AuthManConfig.xml: register the PKCS#11 module for certificate/smart card logon.
  5. Import system CAs — copy the host's CA trust into the Citrix-specific keystore, see Certificates.
  6. Smoke tests + snapshot — check the card reader through the host's pcscd, check HdxRtcEngine for missing libraries, then save the finished state as an image.

The deviceTRUST channel is not part of this — it is off by default and has to be requested explicitly.

💾 Persistence: build once, rebuild only for a new client

Installation takes several minutes. The finished state is therefore saved into a local image via podman commit (localhost/fedora-ica-client:latest, plus a tag carrying the client version).

That makes the container disposable — the working state lives in the image:

./setup-container.sh                 # image present? → create the container in seconds
ICA_RECREATE=1 ./setup-container.sh  # recreate the container from the image (e.g. different video devices)
ICA_REBUILD=1  ./setup-container.sh  # discard the image, install from scratch

The image carries the installed client version as a label. If a newer icaclient-*.deb sits in ~/Downloads, the script points that out on the next run all by itself:

Note: newer Citrix client found (26.04.10.1 -> 26.07.0.1).
      Adopt it with: ICA_REBUILD=1 ./setup-container.sh

So a genuine rebuild is only ever needed for a client update.

ica-launch.sh follows suit automatically: if the container is merely stopped after a reboot, it gets started; if it is gone entirely but the image is still there, it is recreated from it. A stopped container otherwise looks a lot like "broken, rebuild it" — podman exec merely reports is not running.

Careful with older revisions: setup-container.sh now only deletes an existing container when ICA_RECREATE/ICA_REBUILD is set. Earlier versions unconditionally started with podman rm -f — if installation then aborted, the working container was gone and the new one incomplete.

🪪 Smart card access

The container gets no raw USB passthrough for the card reader. Instead it connects through the shared pcscd socket (/run/pcscd) to the pcscd daemon already running on the host — exactly like any other local application. This only works if:

  • --pid host is set. On connect, pcscd presumably checks the process credentials (SO_PEERCRED) of the client process. Without a shared PID namespace, pcscd sees a PID that does not exist outside the container and then simply stops answering requests (the client hangs in poll() indefinitely, with no error message).
  • the pcsc-lite client version matches the host. The IPC handshake between client and daemon is not compatible across arbitrary version boundaries. When it does not match, the connection is established but the client then hangs exactly as with the PID namespace problem above — for an entirely different reason. Ubuntu 24.04 ships pcsc-lite 2.0.3, which works with current host daemons (2.x), so a custom build is no longer needed. (On the earlier CentOS base with 1.9.4 it was.)

The smoke test at the end of the script tells you whether it worked, or check directly:

podman exec fedora-ica-client opensc-tool --list-readers

If the reader list appears immediately, the protocol matches. If the call hangs until the timeout, the client version does not fit.

📷 Webcam

Existing /dev/video* devices are detected and passed through automatically when the container is created. This can be controlled via ICA_VIDEO_DEVICES:

ICA_VIDEO_DEVICES="/dev/video0 /dev/video2" ./setup-container.sh  # only these
ICA_VIDEO_DEVICES="" ./setup-container.sh                          # none at all

Individual devices are passed through deliberately, never the entire /dev tree — if the host runs VirtualBox with its own /dev/vboxusb device nodes, for example, rootless Podman otherwise fails outright with "Permission denied" while recreating those special devices in the user namespace, before the container even starts.

Because the devices are a runtime property of the container and are not part of the image, they have to be set again on every recreate — which is precisely what the automatic detection is for.

Audio works, camera does not? Then the device is usually already missing on the host: audio goes through the mounted PipeWire/PulseAudio socket, while the camera needs a real device node. Check on the host first:

ls /dev/video*

If there is nothing, another application has claimed the camera via usbfs and thereby displaced uvcvideo — typical for VMs or containers with USB passthrough (a /dev/bus/usb bind into a Windows VM, say). Find out who holds it and which driver is bound:

sudo fuser -v /dev/bus/usb/<bus>/<dev>
ls -l /sys/bus/usb/devices/<n-m>:1.0/driver   # should point to uvcvideo, not usbfs

Once that access ends, uvcvideo binds again by itself, the /dev/video* nodes reappear, and the container needs to be recreated once (ICA_RECREATE=1 ./setup-container.sh). Test inside the container:

podman exec fedora-ica-client gst-launch-1.0 -q v4l2src device=/dev/video0 num-buffers=5 ! fakesink

🎭 Teams: "Background could not be applied"

If optimized Microsoft Teams only reports that a background cannot be applied, wget is usually simply missing inside the container. Citrix lists it as a prerequisite for the background effects, and the client invokes it as an external program to fetch the images — the corresponding error strings live in lib/HdxRtcEngine_ext/libwebrpc.so:

wget --compression=auto --version
wget failed with exit code:
wget terminated by signal:

A minimal container does not ship wget, and Teams does not name the cause. The setup therefore installs it and verifies it as a smoke test. Cross-check:

podman exec fedora-ica-client wget --compression=auto --version

The container also needs internet access for this — which --network host provides.

Two limits of the feature remain regardless: background effects are not supported on Arm64 at all, and the choice is restricted to blurring plus the bundled Teams backgrounds.

🔐 Certificates: "Cannot connect to ..."

If the client reports something like this when opening an .ica file

Cannot connect to 0.0.0.2 - <resource name>

then it is not a network or routing problem — that IP does not exist at all. For connections through a Citrix Gateway, the Address field of the .ica file holds no hostname but an STA ticket:

Address      = ;40;<STA-ID>;<ticket>
SSLProxyHost = gateway.example.com:443

The client is supposed to connect to SSLProxyHost and redeem the ticket there. If the TLS handshake to the gateway already fails, it falls back to reading the Address field as an address — and ;40;… turns into a nonsense IP.

The cause is almost always the certificate store: the Linux ICAClient does not use the system trust store, only /opt/Citrix/ICAClient/keystore/cacerts, and that ships with about a dozen CAs. If the root of the gateway chain is missing there, the connection aborts. The fact that the same page loads fine in a browser proves nothing — the browser uses the system trust store.

Cross-check (must return Verify return code: 0 (ok)):

podman exec fedora-ica-client openssl s_client -connect gateway.example.com:443 \
  -servername gateway.example.com \
  -CApath /opt/Citrix/ICAClient/keystore/cacerts </dev/null 2>&1 | grep "Verify return code"

If you get num=19 (self-signed certificate in certificate chain) instead, the root is missing from the Citrix keystore. Step 5 of the setup therefore copies the host's system trust there and runs ctx_rehash, so the CAs do not have to be maintained by hand after every container rebuild.

.ica files are short-lived. The STA ticket is valid for only a few minutes. A file from two hours ago fails even with a perfect configuration — always download a fresh one from StoreFront for testing.

🛡️ deviceTRUST client extension

The Workspace app ships the deviceTRUST extension, but does not activate it: it sits under /opt/Citrix/ICAClient/DeviceTRUST/ as an unpacked dtclient-linux-x86_64-release.zip and waits for an administrator.

Off by default, and that default was paid for. Switching the channel on cost the webcam inside the session. Everything on this side stayed healthy — capture from the container worked, uvcvideo was bound to all interfaces, no process held /dev/video*, HdxRtcEngine had no missing libraries and its logs were identical before and after. So the loss happened at the policy level on the VDA. Removing the channel brought the camera straight back.

The likely mechanism: until then the VDA saw no deviceTRUST channel at all, afterwards it saw one reporting an incomplete device context, because provider_os cannot work in a container — Failed to open system bus { busResult:-2 }. There is no D-Bus system bus in here, only the session bus via XDG_RUNTIME_DIR. A device that reports and then fails the compliance check is treated more harshly than one that never spoke up.

Do not enable this without knowing which deviceTRUST policies the gateway operator actually has in place. The server-side policy is not visible from the client, so switching it on blind can silently cost devices in the session.

Enable it explicitly with:

ICA_DEVICETRUST=1 ./setup-container.sh

The setup then installs the virtual channel only, not the daemon that Citrix' own deploy.sh additionally sets up when running as root. That daemon is a systemd socket unit, and systemd is not PID 1 in a container, so systemctl enable fails there — and since deploy.sh runs under set -e, it would exit non-zero after the channel is already in place. The setup therefore reproduces its installIca() step and leaves only the module.ini rewrite to Citrix' deploy.awk.

The channel does not need that daemon to report: it runs inside wfica itself and polls the devicetrust, name, os and whois providers about once a minute — visible in /tmp/ctxdt.log. The library carries 23 providers in total (hardware, network, smartcardreader, user, display, certificate, location, power and more), so four is a thin context even before provider_os fails on top.

🚫 provider_os cannot work in this container

provider_os asks org.freedesktop.hostname1 and org.freedesktop.systemd1 for the OS identity, both on the D-Bus system bus. A container has none of its own, so the setup mounts the host's socket whenever deviceTRUST is in play.

That mount is necessary but not sufficient. Rootless Podman maps container root onto the host UID; the bus checks SO_PEERCRED and sees that host UID, while libdbus authenticates with its own getuid()0 inside the container. The two disagree:

claimed UID answer from the host bus
0 (what the container believes it is) REJECTED EXTERNAL
host UID (what SO_PEERCRED reports) OK

Only a matching user namespace (--userns=keep-id) makes the two agree. This setup deliberately does not use it: the container installs its packages as root, and running as a non-root user would leave the image's root-owned files read-only at runtime. Changing that is a redesign, not a flag — and worth doing only once it is clear that Linux clients are in scope for the operator's deviceTRUST rollout at all.

The smoke test therefore performs a real D-Bus handshake rather than checking that the socket exists. A presence check would report success for a channel that goes on to report an OS-less device context.

Removing it again (module.ini comes back byte-identical):

podman exec fedora-ica-client bash -c 'tmp=$(mktemp -d); unzip -q -o /opt/Citrix/ICAClient/DeviceTRUST/dtclient-linux-x86_64-release.zip -d "$tmp"; rm -f /opt/Citrix/ICAClient/CTXDT.DLL; m=$(readlink -f /opt/Citrix/ICAClient/config/module.ini); awk -v register=0 -f "$tmp/deploy.awk" "$m" > "$m.new"; mv -f "$m.new" "$m"; rm -rf /usr/bin/local/deviceTRUST "$tmp"'

Check the result:

podman exec fedora-ica-client ldd /opt/Citrix/ICAClient/CTXDT.DLL | grep "not found"
podman exec fedora-ica-client grep -A1 '^\[ctxdt\]' /opt/Citrix/ICAClient/nls/en/module.ini

Registered is not the same as loadable. The channel needs libupower-glib3 and libnm0 — deviceTRUST reports battery and network state, and neither library is a dependency of the icaclient package, because Citrix does not install the extension itself. If they are missing, the ICA client silently skips the channel at logon and connects anyway. The setup installs both and checks with ldd afterwards; the check for the module.ini entry alone would not catch this.

deploy.awk cannot report failure. Its END block reads if (foundICA = 0) — an assignment, not a comparison, so the branch never fires and awk exits 0 even when the [ICA 3.0] section was never found and nothing was registered. Hence the separate smoke test.

📎 Opening .ica files

./ica-launch.sh ~/Downloads/some.ica

install-mime-handler.sh registers this as the system default for application/x-ica, so .ica files downloaded in a browser end up in the container automatically.

🩹 Stopgap: gateway serving downloads without a .ica extension

install-mime-handler.sh and the auto-open preference in install-browser-launcher.sh both key off the file extension. If the gateway serves the download without a filename extension at all (a bare download ID, Content-Type still correctly application/x-ica), neither mechanism can trigger — Chromium never even offers to open it, and double-clicking it in the file manager finds no MIME association because there is nothing to look up. That is a gateway-side change; it showed up here on 2026-09-03 (first extension-less download in the browser's own history at 09:35, hours before this client was touched that day), coincident with a StoreFront-side "please update your client" nudge that was likewise unrelated to the client actually being outdated.

ica-download-watcher.sh works around it by recognising the file by content instead of by name — every file Citrix generates opens with a [WFClient] section — and calling ica-launch.sh on it directly:

./install-download-watcher.sh

This installs and starts a systemd --user service that watches ~/Downloads via inotifywait and reacts to newly completed downloads (CLOSE_WRITE for files written directly under their final name, MOVED_TO for Chromium's usual <name>.crdownload → final-name rename). Log at ~/.local/share/ica-download-watcher.log.

systemctl --user status ica-download-watcher.service
systemctl --user disable --now ica-download-watcher.service   # once the gateway is fixed

Because it matches on content, it will open any new file under ~/Downloads that happens to start with [WFClient], regardless of where it came from — acceptable for a personal stopgap, worth knowing if that folder is shared with anything else.

🕒 Timezone: the session runs on UTC

A container image carries no timezone of its own — Ubuntu starts out on Etc/UTC. That is easy to miss, because it does not show up in the container but in the session: wfica reads the timezone through glibc (the binary references /etc/localtime and tzset) and hands it to the server via HDX time zone redirection. With the redirection active on the server side, the whole Citrix desktop then runs on UTC — clock, Outlook, meeting invitations.

Two places take care of this, because a container is created far less often than it is launched:

  • setup-container.sh creates the container with --tz local plus a TZ matching the host, and afterwards compares the UTC offsets of host and container. That verification matters: for an unknown zone name glibc falls back to UTC without an error message.
  • ica-launch.sh additionally passes TZ in on every launch. That way an existing container is right without being recreated, and the timezone follows the host after a change.

Deliberately a zone name (Europe/Berlin), not a fixed offset — otherwise daylight saving breaks twice a year.

Cross-check (both must print the same offset):

date +%z; podman exec fedora-ica-client date +%z

If they differ despite the setup, tzdata is missing from the image; the setup installs it explicitly for that reason. A running session keeps the old timezone: it is negotiated when the connection is established, so it takes a fresh .ica logon.

🪪 Certificate logon at the gateway: forcing TLS 1.2

The typical picture: you pick the certificate, enter the card's PIN — and the browser aborts with

ERR_SSL_CLIENT_AUTH_NO_COMMON_ALGORITHMS

Or the logon page offers no certificate logon at all despite the card being inserted, and goes straight to the full form with password and second factor.

Both share the same cause, and it is not the gateway but the combination of TLS 1.3 and the PKCS#11 module driving the card:

TLS 1.3 removed the old PKCS#1 v1.5 signatures for CertificateVerify (RFC 8446). For RSA keys, only RSA-PSS (rsa_pss_rsae_*, rsa_pss_pss_*) or ECDSA are permitted there. If the PKCS#11 module offers no PSS mechanism, no signature algorithm is left that both the card stack and the TLS 1.3 handshake support: exactly the message above.

Check your own card's mechanism list — if neither of these comes back, the PSS path is unavailable:

pkcs11-tool --module /usr/lib64/pkcs11/opensc-pkcs11.so -M | grep -iE "pss|x-509"

x-509 matters because OpenSC synthesizes PSS on top of a raw RSA operation (CKM_RSA_X_509). Without raw RSA it cannot offer PSS even though OpenSC itself has supported PSS since 0.20.

This is usually a middleware limitation, not a hardware one. Do not conclude the card is incapable: several card families support PSS in hardware while the corresponding OpenSC driver exposes neither PSS nor raw RSA. A telling sign is the same card working under Windows without any TLS tweak — there a vendor minidriver drives it through CNG, which can produce PSS. So the fix below works around the Linux card stack, not the card.

Under TLS 1.2, rsa_pkcs1_sha256 is perfectly legal, the card can sign, and certificate logon works.

A Chromium-based browser can be pinned to TLS 1.2:

brave-browser --ssl-version-max=tls1.2
# likewise: chromium --ssl-version-max=tls1.2, google-chrome --ssl-version-max=tls1.2

So this is not needed manually on every logon, install-browser-launcher.sh sets it up permanently:

./install-browser-launcher.sh https://gateway.example.com

That creates three things:

What Where
Wrapper script ~/.local/bin/citrix-login-browser
Desktop entry "Citrix Login (TLS 1.2)" ~/.local/share/applications/citrix-login-browser.desktop
Dedicated browser profile ~/.config/citrix-login-browser

The dedicated profile is deliberate: it keeps the lowered TLS maximum scoped to the logon session instead of normal browsing. Client certificates come from ~/.pki/nssdb independently of that — the profile directory does not affect them.

The gateway URL is written into the wrapper at install time and therefore does not live in this repository. It can be overridden at runtime via CITRIX_GATEWAY_URL, and a different URL also works as an argument.

Opening .ica downloads directly

Chromium-based browsers only open downloaded files automatically when the extension is listed in the profile under download.extensions_to_open — that is the setting behind "Always open files of this type". A freshly created profile does not know it, which is why the .ica file otherwise just lands in the downloads folder.

The wrapper adds ica itself on every start if it is missing. To open the file, the browser then calls xdg-open and ends up at the system handler for application/x-ica, which install-mime-handler.sh points at ica-launch.sh.

The write only happens while the browser is not running: Chromium keeps the preferences in memory and overwrites the file on exit — a write alongside it would be lost.

If the browser classifies .ica as a dangerous file type and still only saves it, a managed policy helps:

sudo mkdir -p /etc/brave/policies/managed
sudo tee /etc/brave/policies/managed/ica-autoopen.json >/dev/null <<'EOF'
{
  "AutoOpenFileTypes": ["ica"],
  "AutoOpenAllowedForURLs": ["https://gateway.example.com"]
}
EOF

Firefox has no comparable command-line switch; there, only security.tls.version.max=3 in about:config would do it, which applies globally and is therefore less suitable than a separately launched browser.

You can measure whether the server requests a client certificate under TLS 1.2 at all:

openssl s_client -connect gateway.example.com:443 -servername gateway.example.com \
  -tls1_2 </dev/null 2>&1 | grep -i "Acceptable client certificate"

For the browser to offer the card in the first place, the PKCS#11 module must be present in the NSS database that Chrome and Brave use for client certificates:

modutil -dbdir sql:$HOME/.pki/nssdb -list

If p11-kit-proxy shows up there, OpenSC is usually already wired in transparently and nothing else needs doing.

⚠️ A limitation of the ICAClient itself: no certificate logon in the client

With some Citrix Gateway configurations that use a customized logon page (NetScaler Gateway "nFactor" multi-factor flow), the Linux client does not detect an available client certificate/smart card automatically, even though AuthManagerDaemon internally finds readers and certificates just fine — Citrix' own documentation confirms: "Multifactor (nFactor) authentication supports only form-based authentication." That is documented client behaviour, not a container misconfiguration.

Logon therefore happens in the browser: perform the certificate logon there (see Certificate logon at the gateway — without forcing TLS 1.2 you end up in the full form flow in the browser as well) and then open the resulting .ica file in the container as described above.

🔧 Troubleshooting

  • ERR_SSL_CLIENT_AUTH_NO_COMMON_ALGORITHMS after entering the PIN: the PKCS#11 module offers no RSA-PSS, but TLS 1.3 requires it. Usually a middleware gap rather than a card one. Pin the browser to TLS 1.2 — see Certificate logon at the gateway.

  • Session shows the wrong time (usually UTC): the container has no timezone of its own and passes that on to the session. Cross-check with date +%z; podman exec fedora-ica-client date +%z — see Timezone.

  • Smart card hangs, no error message: almost always the pcsc-lite version or a missing --pid host. For the cross-check see Smart card accessopensc-tool --list-readers must return the reader list immediately rather than running into a timeout.

  • Teams shows "not optimized", camera greyed out, audio works: then the optimization engine is not starting. Check:

    podman exec fedora-ica-client ldd /opt/Citrix/ICAClient/util/HdxRtcEngine | grep "not found"
    

    If libc++.so.1, libc++abi.so.1 or libunwind.so.1 are missing there, the client is too old: only newer versions ship those libraries under lib/HdxRtcEngine_ext/ themselves and find them through an RPATH. Audio still works in that case, because the generic audio redirection does not need the engine — hence the misleading symptom. The setup now covers this as a smoke test.