Files
mesh-catalog/modules/forticlient/README.md
T
jochen 51460de958
mesh/merge-gate pass: builds forticlient, systemd-resolved → g14, shanks; no bus step; 2 wait(s) for a person; every machine composes with the change as it…
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
forticlient: hand the client's resolver file to the machine's own resolver (hq ADR 0247)
FortiClient writes /etc/resolv.conf itself on connect and never tells
resolved a link's DNS. The module now requires split-dns and runs an
adapter as root that reads the client's servers and domains from its write,
routes them over the client's tunnel through the resolver's socket on the
machine, takes the write so the resolver's file is back at once, and takes
the route away when the tunnel goes. Nothing of it crosses the bus.
2026-10-07 21:35:50 +02:00

134 lines
9.0 KiB
Markdown

# forticlient
The FortiClient VPN client on the workstations, as a module (novox/hq ADR 0208): its tray in the
operator's session and the vendor's service behind it, and an adapter that hands the client's resolver
file to the machine's own resolver (novox/hq ADR 0247). It requires `x11-display` and `split-dns`, so it
is assigned only where a display server and the machine's own resolver (`systemd-resolved`) are held on
the same machine.
**This is the operator's work VPN.** Nothing of its configuration is the mesh's: no profile, no
credential, no gateway, no certificate is declared, read, printed or stored by the module or its
tools. The tools report running and connected state only.
## Owns
| what | where |
|---|---|
| the vendor's scheduler service, which holds the tunnel | `forticlient.service`, running and enabled |
| the adapter to the machine's own resolver | `forticlient-tools split-dns`, a process as root |
Nothing else. It holds no seat, makes no contribution and writes no file.
## The client's names: the adapter (ADR 0247)
FortiClient's Linux client, connecting, moves `/etc/resolv.conf` aside and writes its own: its servers,
reached through its tunnel, and the company's search domains. It never tells systemd-resolved or
NetworkManager a link's DNS, and never writes the file again during a session. On its own that file
either cuts the machine off from the mesh's names (while it stands) or is overwritten by the mesh and
cuts the person off from the company's names (for the rest of the session). Research 033 measured both.
**The quirk is the client's, so the adapter is this module's.** The machine's resolver keeps the client's
write and holds it for whoever handles it. The adapter, once a second:
1. asks the resolver, over its socket on the machine, for the write standing now;
2. if the file's header says FortiClient wrote it, reads its servers and its `search` and `domain` lines,
and waits up to 15 s for the client's tunnel interface (`fctvpn…`) to be up;
3. calls the resolver's `route` with the tunnel, those domains and those servers, taking the write in the
same call. The resolver puts its own file back at once, and from then on the company's domains go to
the company's servers over the tunnel, and every other name to the mesh's resolvers;
4. when the tunnel goes, calls `unroute`;
5. every 10 s, checks the route is still in place (a resolver restarted forgets it) and gives it again.
A write that is not the client's, one with no tunnel within 15 s, one with nothing to route and one the
resolver refuses are left to the resolver, which puts its own file back after 90 s. The node-engine then
says it (ADR 0241's `rewritten`, naming the writer). So a failed handover is loud.
**Search domains route, they do not expand.** The client's domains become routing domains: a full name
under one goes to the company's servers. A short name is not completed with them, because the machine's
resolver file is the mesh's and lists no search domains.
**All of it stays on the machine.** The adapter runs as root and talks to the resolver over its root-only
socket, never over the bus. Its journal names counts, never a server, a domain or the tunnel. The client's
configuration is still never opened: what is read is the file the client wrote where every program reads
it. A VPN whose client tells systemd-resolved its link's DNS itself needs no adapter.
- **The client is kept as found.** `forticlient-vpn` (7.4.3) is not in the official repositories: on
both workstations it is a foreign (AUR) package that repackages the vendor's build, installed
explicitly. The host installs from the official repositories only, so the module cannot declare it.
ADR 0205's pinned archive does not fit: it is a vendor binary set with a root service, a firewall
helper and an install script. It waits for the mesh's package repository (research 027 question 1,
option P2). Until then a fresh workstation installs it by hand.
- **The service is declared, and so depended on.** `forticlient.service` is the package's unit, running
and enabled on both workstations. Declared running and enabled, it is held in that state, and on a
machine without the package the host refuses it by name (*does not exist on this machine*): loud,
never a silent pass. The `asus-zephyrus-g14` module does the same with its foreign daemons. The
module never restarts it: a change to nothing of the module's would, and nothing of the module's
changes.
- **The configuration stays the operator's, and unread.** `/etc/forticlient/`, the client's database
under `/opt/forticlient/`, the account's FortiClient settings, the VPN profiles, saved credentials
and certificates are set in the client's own window. They are found (ADR 0182), and unlike any other
found file, the tools do not even read them.
## How it starts: the vendor's autostart entry, and nothing else
The tray has two processes: `fortitraylauncher`, which starts and watches `fortitray`. The package's
install script links `/etc/xdg/autostart/Fortitray.desktop` to the package's
`/opt/forticlient/Fortitray.desktop` (`Exec=/opt/forticlient/fortitraylauncher`). The session runs it
once at login through the `i3` module's `dex --autostart --environment i3`. **That entry is the tray's
one start.** The module adds no `xinitrc` slot and no `node-display-session` exec, because either would
start it a second time. The link is the vendor's, made by its install script; the mesh does not make
or remove it.
The tunnel is not the tray's: the service's processes hold it, as root. Ending the tray leaves a
connected tunnel connected.
## Tools
They are served by the node's runtime as the operator account (ADR 0175).
| tool | does |
|---|---|
| `forticlient_status` (r) | <ul><li>the installed version, and that it is from outside the official repositories</li><li>the service: active, enabled</li><li>whether the launcher and the tray run: pid, since, and the scope or unit they run in</li><li>what starts the tray at login</li><li>connected or not, as the number of the client's tunnel interfaces that are up</li></ul> |
| `forticlient_restart` (a) | asks the tray and its launcher to end (SIGTERM), forces them after 5 s, and starts the launcher in the operator's session as a transient user unit `mesh-forticlient-tray`, so it outlives the tools runtime. The launcher starts the tray. The service and the tunnel are not touched. Refused plainly when nobody is logged in to the desktop |
| `forticlient_check` (r) | <ul><li>the package is installed</li><li>the service is running and enabled</li><li>exactly one start: the vendor's entry is present and not hidden by an entry of the account, and `dex` is installed</li><li>no window-manager exec</li><li>one launcher and one tray run in a desktop session</li></ul>Being connected is never a finding: that is the operator's to decide. Each finding says what to do |
**What the tools never touch**, held by the tests (a fake machine carries a profile, a gateway, an
address, a secret and a certificate where the client keeps them, and no answer may hold any of them):
- no file under `/etc/forticlient` or the account's FortiClient settings is opened, and under
`/opt/forticlient` only the tray's autostart entry, through its link (it names the launcher and
nothing else);
- the vendor's command-line client and `fortivpn` are never run, and its logs are never read;
- a process is named by its command name only, never by its arguments;
- *connected* is whether an interface named `fctvpn…` is up, from its flags. The interface's name
(it carries an identifier) and its addresses are never answered. A tunnel of a kind that brings up
no such interface (IPsec) is not seen, and the answer says *not connected*.
## What changes when it is assigned
| | laptop | desktop |
|---|---|---|
| package | none: `forticlient-vpn` 7.4.3.5411, explicit, foreign | the same |
| service | none: running and enabled | the same |
| tray | none: dex starts it from the vendor's entry, in the login session's scope | none on disk. **No tray runs now:** that session began before `dex` was installed, and the predecessor's window manager never started it. The next login is the first that starts it |
## Migration (ADR 0182)
Nothing is required on either machine. On the desktop, log out and in once, or run
`forticlient_restart`, and the tray runs from its one start. `forticlient_check` then answers `ok`.
## Leaves as found
Everything of the client's: its configuration and database, the VPN profiles and credentials, its
logs, `/etc/xdg/autostart/Fortitray.desktop` (the vendor's link), the package itself.
## Relies on
- **The package, installed by hand.** Without it the host refuses the service by name.
- **`i3`'s `dex` line for the start**: XDG autostart has no seat. Assigned without `i3`, the tray does
not start. `forticlient_check` says so.
- A display server on the same machine (`x11-display`, ADR 0208 §3).
- The machine's own resolver on the same machine (`split-dns`, ADR 0247): `systemd-resolved`, assigned
before this module requires it, on every machine this module runs on.