Snapping CPDB CUPS backend, a user daemon using D-Bus

We want to provide the complete printing stack in Snaps, especially needed for full printing functionality in all-Snap Linux distributions like Ubuntu Core or Ubuntu Core Desktop, but also to have a future release of the “classic” Ubuntu using these Snaps to provide its printing functionality.

There are already Snaps of CUPS, ipp-usb, and of nearly all free software printer drivers in 6 Printer Application Snaps.

What is missing are the Common Print Dialog Backends (CPDB). With these backends print dialogs do not interact directly with the print technology in use (usually CUPS but also print-to-file, cloud printing services, …) and so changes in these technologies (like the transition from CUPS 2.x with PPDs and driver filters to all-IPP CUPS 3.x) or introduction of new cloud print services does not require changes in all print dialogs but only in the backend which is maintained by the maintainer of the corresponding print technology.

Each CPDB backend is a user daemon, providing a D-Bus service (on the session bus) for all print system interactions as listing all available printers, receiving continuous updates on this list, showing all capabilities and user-settable options of a selected printer, print on a selected printer sending print data and option settings. The daemon is designed to not stay permanently running but being D-Bus activated and closed when closing the print dialog (but adding an option to alternatively running it permanently would not be complicated).

A print dialog with CPDB support (CPDB frontend) lists all available D-Bus services and finds the CPDB backends under them (org.openprinting.Backend.XXX service names). It then polls the printer lists and subscribes to updates on them for each backend to build the complete list. If a user chooses an entry, the dialog communicates with the backend which has provided that entry for knowing the options and/or printing.

Now we want to create a Snap of the most important backend, the one for CUPS. We take the GIT repository of cpdb-backend-cups (“snap” branch) where the following files are added:

  • snap/snapcraft.yaml
  • snap/local/run-cpdb-backend-cups

I want to say thank you to Biswadeep Purkayastha for starting the work on snapping this. He stepped up when he was attending my talk and my Snap workshop on the Opportunity Open Source which I organized in the IIT Mandi in India. He also went through the slides and exercises of my Daemon Snapper’s Workshop from the Ubuntu Summit 2022 and read @kenvandine’s 2nd blog about Ubuntu Core Desktop with a first example of a user daemon and a D-Bus-triggered daemon launch.

Below is what I have tested. Everything where I have doubts or questions, where I found a possible bug, or where snapd perhaps needs another feature, I have written in bold. Here I want to ask the Snap experts (@jamesh, @kenvandine, @sergiusens, …) for help and discussion. Thanks in advcance.

For everyone who wants to try it out, note that user daemons and D-Bus activation are only experimental features and need to get enabled first:

sudo snap set system experimental.user-daemons=true
sudo snap set system experimental.dbus-activation=true

The features exist already for longer time but they are not yet activated because they are not completely implemented yet, especially the service control commands (start, stop, reload, … the daemon) only support system daemons and no user daemons yet.

To get some deeper information about user daemons and D-Bus activation than only the short mention by @kenvandine in his blog, see the following two discussion threads where @jamesh (thanks a lot) explains everything:

The snapcraft.yaml is a little long, as it has parts for Ghostscript and QPDF, for building libcupsfilters. The backend does not actually need the filter functions for PDF and PostScript processing, so I am looking into ./configure options to optionally omit these dependency-causing filter functions in libcupsfilters 2.1.0.

But the important part is to make the backend considered a user daemon in the Snap and clients (CPDB frontends aka print dialogs) can access it. So to follow best its original design we put the following into snapcraft.yaml (you find it near the file’s beginning):

slots:
  dbus-openprinting-backend-cups:
    interface: dbus
    bus: session
    name: org.openprinting.Backend.CUPS

apps:
  cpdb-backend-cups:
    command: scripts/run-cpdb-backend-cups
    daemon: simple
    daemon-scope: user
    activates-on: 
    - dbus-openprinting-backend-cups
    plugs: [cups]

We define the a slot for the D-Bus service which our backend provides. The service is provided on the session bus as the daemon is running as part of the user session, with the user’s rights, not as root, as a snapped system daemon would do. The name is the D-Bus service name as used by the backend.

The app definition for the backend contains as command a call of the wrapper daemon. As in other Snaps, like the CUPS Snap the script sets environment variables and creates directories so that the actual daemon can run inside the Snap’s sandbox. In the end the script starts the actual backend.

The daemon-scope: user makes the daemon be a user daemon. It runs as normal user and not as root and by default it is started when the user logs in and stopped when they log out (or when the daemon terminates by itself). We do not want to have it running all the time and therefore we use D-Bus activation, via activates-on: with the name of the slot which needs to get accessed so that the launch of the daemon gets triggered. Here we use the name of the D-Bus slot we just defined.

The plugs: [cups] allows the backend to communicate with CUPS, to get the list of printers, the options, and to send off the jobs. These are exactly the tasks of printing via a print dialog, so cups is good enough here and cups-control is not needed.

Let us build the Snap via

snapcraft -v --debug

The -v gives us verbose logging, showing especially the build processes, and --debug leaves us in the shell of the build process when an error occurs, so that we can investigate whether everything is correct inside the build container (leave the shell with exit when done). All logging is also thrown into a file, see the end of the build process’ output (after exiting the shell in case of an error).

Do not install your Snap yet.

Now prepare the test. Run

cpdb-text-frontend

The command is supposed to list an entry for each of your CUPS queues plus one entry to print into a PDF file, if appropriate CPDB backends are installed, the CUPS backend for the former and the print-to-file backend for the latter. It offers commands with which you could also show option settings and actually print, all operations which a print dialog is supposed to do. Enter help to list the commands and stop to quit. Do not worry that you often do not see a prompt. The commands also work without the prompt being visible.

Run

sudo apt install cpdb-libs-tools

if the command is not available.

If you do not get your CUPS queues listed even if you have some (lpstat -v, create a CUPS queue for the further tests if you do not have one), you are fine for the test, if you get them listed, you need to remove a classically installed version of the backend:

sudo apt remove cpdb-backend-cups

or, if you installed right from source, do make uninstall or rmove the following files:

/usr/share/dbus-1/services/org.openprinting.Backend.CUPS.service
/usr/lib/print-backends/cups
/usr/lib/*/print-backends/cups

cpdb-text-frontend should not list your CUPS print queues any more.

Then install the Snap

sudo snap install --dangerous cpdb-backend-cups_1.0_amd64.snap

and connect the cups interface (Note: CUPS Snap must be installed):

sudo snap connect cpdb-backend-cups:cups cups:cups

To test it, run

cpdb-text-frontend

again.

You will get something like (you may only get the errors and no Save_as_PDF entry):

$ cpdb-text-frontend 
[Error] [Frontend] Error getting CUPS printer list : GDBus.Error:org.freedesktop.DBus.Error.AccessDenied: An AppArmor policy prevents this sender from sending this message to this recipient; type="method_call", sender=":1.1546" (uid=1000 pid=369218 comm="cpdb-text-frontend" label="unconfined") interface="org.openprinting.PrintBackend" member="GetPrinterList" error name="(unset)" requested_reply="0" destination=":1.1547" (uid=1000 pid=369223 comm="/snap/cpdb-backend-cups/x9/usr/lib/print-backends/" label="snap.cpdb-backend-cu-------------------------
Printer Save_As_PDF
name: Save_As_PDF
location: localhost
info: Printing to a PDF File
make and model: Save_As_PDF
accepting jobs? yes
state: idle
backend: FILE
-------------------------

> 

Stop it

> stop

** Message: 23:00:08.536: Stopping front end..

$

and run

$ ps aux | grep cpdb
till      369223  0.2  0.1 332092 16616 ?        Ssl  22:58   0:00 /snap/cpdb-backend-cups/x9/usr/lib/print-backends/cups
$

You see now that the D-Bus activation worked. The ps command shows that the daemon is started, and as user (you) and not as root. But instead of a list of the CUPS queues cpdb-text-frontend gives you error messages, AppArmor blocking communication between the unconfined (classically installed) frontend and the snapped backend.

2023-10-20T23:14:28.376581+02:00 till-x1nano dbus-daemon[6549]: [session uid=1000 pid=6549] Activating via systemd: service name='org.openprinting.Backend.CUPS' unit='snap.cpdb-backend-cups.cpdb-backend-cups.service' requested by ':1.1551' (uid=1000 pid=370011 comm="cpdb-text-frontend" label="unconfined")
2023-10-20T23:14:28.402829+02:00 till-x1nano systemd[6505]: Started snap.cpdb-backend-cups.cpdb-backend-cups.service - Service for snap application cpdb-backend-cups.cpdb-backend-cups.
2023-10-20T23:14:28.441131+02:00 till-x1nano dbus-daemon[6549]: [session uid=1000 pid=6549] Successfully activated service 'org.openprinting.Backend.CUPS'
2023-10-20T23:14:28.441403+02:00 till-x1nano kernel: [127710.102634] audit: type=1326 audit(1697836468.435:4559): auid=1000 uid=1000 gid=1000 ses=3 subj=snap.cpdb-backend-cups.cpdb-backend-cups pid=370016 comm="cups" exe="/snap/cpdb-backend-cups/x9/usr/lib/print-backends/cups" sig=0 arch=c000003e syscall=314 compat=0 ip=0x7f893e4baa3d code=0x50000
2023-10-20T23:14:28.441404+02:00 till-x1nano kernel: [127710.103460] audit: type=1400 audit(1697836468.435:4560): apparmor="DENIED" operation="connect" class="file" profile="snap.cpdb-backend-cups.cpdb-backend-cups" name="/run/dbus/system_bus_socket" pid=370016 comm="cups" requested_mask="wr" denied_mask="wr" fsuid=1000 ouid=0
2023-10-20T23:14:28.441558+02:00 till-x1nano dbus-daemon[6549]: apparmor="DENIED" operation="dbus_method_call"  bus="session" path="/" interface="org.freedesktop.DBus.Properties" member="GetAll" name=":1.1551" mask="receive" pid=370016 label="snap.cpdb-backend-cups.cpdb-backend-cups" peer_pid=370011 peer_label="unconfined"
2023-10-20T23:14:28.441733+02:00 till-x1nano dbus-daemon[6549]: apparmor="DENIED" operation="dbus_method_call"  bus="session" path="/" interface="org.openprinting.PrintBackend" member="GetPrinterList" name=":1.1551" mask="receive" pid=370016 label="snap.cpdb-backend-cups.cpdb-backend-cups" peer_pid=370011 peer_label="unconfined"
[...]
2023-10-20T23:14:34.058663+02:00 till-x1nano dbus-daemon[6549]: apparmor="DENIED" operation="dbus_signal"  bus="session" path="/" interface="org.openprinting.PrintFrontend" member="StopListing" name=":1.1551" mask="receive" pid=370016 label="snap.cpdb-backend-cups.cpdb-backend-cups" peer_pid=370011 peer_label="unconfined"

These lines show that the user daemon got successfully started via D-Bus activation (first 3 lines). The problem is that the actual D-Bus communication of the backend does not work, both with CUPS despite we have connected the cups interface (so the cups interface needs to get fixed, it does not allow the access to CUPS’ D-Bus services, as cups-control allows, and these services are not administrative, they are for print dialogs, so Snaps plugging cups should have access to them.) and with the CPDB frontend (cpdb-text-frontend, lines 6-8).

If we look at the documentation for a slot definition for an interface: dbus slot, the client would need to have a plug for this slot to get access. First, this requires the client to be a Snap and we are testing with a classically installed client, and second, if we have a snapped client, it would need to have a plug for each CPDB backend, which defeats the principle of CPDB. A CPDB frontend should be independent of the backends. It should use all backends which are present, also those which are created after the frontend got released. So we cannot require that each frontend Snap has an explicit plug for each backend Snap. See also above the actual method used. We need a general (session) D-Bus access interface for the frontends. We need the slots for the D-Bus activation but we need a general D-Bus access for being able to access arbitrary backends.

What we need is a D-Bus interface for Snaps providing a D-Bus service so that both classically installed clients and snapped clients can access the Snap’s D-Bus service. Also clients (both snapped or classically installed) must be able to browse D-Bus services (list all available ones) and pick the desired ones (for example all org.openprinting.Backend.*). So a Snap containing a service must be able to make the service available (specifying itsomehow for best security) and an unconfined client must be able to access by this already. A snapped client should plug some kind of D-Bus interface, with options for general access to available (both snapped and unconfined) D-Bus services or to specify a group of services, also with wildcards or regexps.

Now let us install with --devmode to drop all confinements:

sudo snap install --dangerous --devmode cpdb-backend-cups_1.0_amd64.snap

Kill any old instance of the backend:

killall cups

and test again:

$ cpdb-text-frontend 
-------------------------
Printer Save_As_PDF
name: Save_As_PDF
location: localhost
info: Printing to a PDF File
make and model: Save_As_PDF
accepting jobs? yes
state: idle
backend: FILE
-------------------------

-------------------------
Printer HP_OfficeJet_Pro_8730_5B78A3_USB
name: HP_OfficeJet_Pro_8730_5B78A3_USB
location: 
info: HP OfficeJet Pro 8730 [5B78A3] (USB)
make and model: HP HP OfficeJet Pro 8730
accepting jobs? no
state: NA
backend: CUPS
-------------------------

[...]

>

Now all the CUPS queues get listed.

I did also another test:

The CUPS Snap contains D-Bus services to notify about changes on print queues or on queued jobs. These services are available to unconfined clients (I can stop the cups-browsed contained in the CUPS Snap and attach an unconfined cups-browsed to the CUPS Snap’s cupsd and that cups-browsed works as well with the Snap’s cupsd as the Snap’s cups-browsed does).

This motivated me to modify the snapcraft.yaml of cpdb-backend-cups as follows:

# Remove "slots: dbus-openprinting-backend-cups ..."

apps:
  cpdb-backend-cups:
    command: scripts/run-cpdb-backend-cups
    daemon: simple
    daemon-scope: user
    plugs: [cups]

This makes the daemon just running right away, all the time while the user is logged in. And there is nothing specific to D-Bus.

We build the Snap again and install it (with confinement):

killall cups
snapcraft -v --debug
sudo snap install --dangerous cpdb-backend-cups_1.0_amd64.snap

Test again (damon already running):

$ ps auxwww | grep cpdb
till      376195 15.6  0.1 258372 16128 ?        Ssl  00:09   0:00 /snap/cpdb-backend-cups/x12/usr/lib/print-backends/cups
$ cpdb-text-frontend 
-------------------------
Printer Save_As_PDF
name: Save_As_PDF
location: localhost
info: Printing to a PDF File
make and model: Save_As_PDF
accepting jobs? yes
state: idle
backend: FILE
-------------------------

> 

And with running daemon we do not see any CUPS queue but also no errors.

So let us see what we get in /var/log/syslog. For the startup of the damon (when installing the Snap) we get:

023-10-21T00:12:05.370445+02:00 till-x1nano systemd[6505]: Started snap.cpdb-backend-cups.cpdb-backend-cups.service - Service for snap application cpdb-backend-cups.cpdb-backend-cups.

Only a report of successful start, no DENIED messages.

And when running cpdb-text-frontend no further lines concerning cpdb-backend-cups appear.

What is strange here is that I can use the D-Bus services of the snapped CUPS with unsnapped clients, but I cannot do it with services of the snapped cpdb-backend-cups. Is this due to the fact that CUPS is a system daemon and the CPDB backend a user daemon? Or why this difference?

I am grateful if we could find a solution for this, as it is needed ofr a smooth printing experience in both Ubuntu Core Desktop and in any future classic Ubuntu with the printing stack provided by Snaps.

1 Like

Hi Till,

To help get the print backends working, it would be helpful if you could describe the details of the communication you expect to be allowed. It’s not particularly useful to just say things don’t work, since you’re the most knowledgeable about this software.

As some starting points, it’d be useful to be able to answer the following:

  1. How does an app that wants to use the dialogs discover which ones are installed?
  2. How does the app communicate with the backends? (e.g. if it is via D-Bus, what bus names, object paths, interfaces, and methods/signals are used?)
  3. How many backends can be installed on a machine?
  4. Do you expect snapped apps to be able to talk to non-snapped backends?
  5. Do you expect non-snapped apps to be able to talk to snapped backends?

Sorry, I was perhaps a bit busy when I did the initial posting, here I will give some additional info.

The available CPDB backends (D-Bus services) are found by the function cpdbActivateBackends(), using the D-Bus calls “ListNames” for all already running D-Bus services and “ListActivatableNames” for all D-Bus services which get auto-activated when one tries to access them, both done on the D-Bus proxy.

After that the resulting list is searched for services with names starting with “org.openprinting.Backend.” as these are the backends. Once done so we have a list of available backends and can send requests like listing all available printers to each of them.

The communication of the print dialog (app) with the backend is done by D-Bus, on the session bus, service names are “org.openprinting.Backend.XXX”, with “XXX” being the name of the backend.

Any amount of backends can be installed, and especially the application does not know the names beforehand, it must be possible to add backends with any name to the system, especially if for example a new cloud printing service comes up, the backend needs to be found without the application knowing its name.

Snapped apps must be able to use unsnapped backends to allow installing the snapped app also on a classically installed system.

Also non-snapped apps should be able to use snapped backends, once for a classically installed Ubuntu after having the printing stack switched over to Snap or if a cloud printing service provides its backend as Snap and the user installs it on a classically installed system.

If you want this framework to work well in a sandboxed world, I would strongly suggest writing a specification separate from the code describing how the various components communicate with each other. This would be very useful in writing the snapd interfaces, and help in determining if the code is behaving as expected.

If backends or frontends are sandboxed, they won’t necessarily be using the same versions of cpdb-libs, so it’s not useful to treat the communication as an implementation detail.

From a quick look at the Backend and Frontend interfaces:

  1. The D-Bus methods are named inconsistently: some beginning with lower case and others with upper case. The usual convention is to use “CamelCase” style names.

  2. The printFile method takes a file_path_name argument, which I take to be the data the application wants to print. This seems fragile when sandboxing is involved: If the app and backend are running in different mount namespaces, then there’s no guarantee that the file path will be available in both contexts or refer to the same file. It would also be possible for the app to print a file it couldn’t otherwise read. As an alternative, xdg-desktop-portal’s Print portal has the application pass a file descriptor from which the job can be read:

https://github.com/flatpak/xdg-desktop-portal/blob/36dc6e947c15b81f15d92133bd35631ee084db66/data/org.freedesktop.portal.Print.xml#L39-L42

  1. What security/privacy protections are backends expected to implement. In particular:

    • If a confined application calls getAllJobs, which jobs will it be able to see?
    • What happens if a confined application calls cancelJob on a print job it didn’t create?
  2. Is there any way for an app to know if its print job has completed without repeatedly calling getAllJobs? That seems like a fairly common thing to want to do.

  3. The translations methods look like they’d require a lot of method calls to get localised data back. While there is a getAllTranslations method, it’s not clear what the keys are in the dictionary it returns. Is there any reason why you wouldn’t want to return localised strings from GetAllOptions?

  4. Rather than passing a string printer_id argument to most methods, have you considered exposing each under its own object path? You could then expose a bunch of information about the printer as simple D-Bus properties. You could also use the PropertiesChanged signal to indicate when they change. PrinterAdded, PrinterRemoved, GetPrinterList, and getDefaultPrinter could then use object paths instead of string IDs.

  5. The Frontend interface only seems to consist of signals. Who would be listening to those signals? If the backends are only activated on use, is it a problem that they might not see the signals? Why would an app send a HideRemotePrinters signal rather than just filtering out the remote printers provided by the backends? If two apps are displaying print dialogs simultaneously, what happens if one wants to show remote printers and the other hide them?

When you’ve got a document describing all the ways apps and backends communicate, I’d also recommend asking someone from the security team to give it a review.

What are the Common Print Dialog Backends (CPDB) good for?

See also OpenPrinting web site

The CPDB are for making a connection between the print dialogs of GUI applications and the print technologies/print systems like CUPS, cloud printing services, print to file,…

Up to now, each developer of a print dialog is taking care of this connection by themselves, but these developers are usually larger free software projects like GNOME/GTK, KDE/Qt, Mozilla (Firefox, Thunderbird), Chromium, LibreOffice, … The actual development happened often years ago and once done, nobody was taking care of any changes which happened on CUPS for example, also in the times where we had Google Cloud Print it got never integrated and so never gained relevance for Linux users.

Several years ago CUPS introduced its first step into the New Architecture, its way into the world of driverless IPP printers. CUPS got the ability to auto-create temporary print queues when jobs are sent to IPP print destinations (network printers, shared remote CUPS queues, IPP-over-USB) available to the local machine and so the setting up a printer by creating a CUPS queue for it is not required any more, one can just print on driverless IPP printers (and all modern printers currently on the market are such driverless IPP printer).

Problem is that such print destinations were not visible in the print dialogs due to the fact that they used the wrong API functions of libcups. The dialogs listed only permanent, manually created print queues. A newer set of API functions in libcups got created and dialogs have to use this one to also include IPP print destinations where CUPS would automatically create a temporary queue for when trying to print on them.

Here the GUI developers were not keeping pace with the cUPS development and fixed/updated their dialogs to cover this. As stop-gap I made cups-browsed automatically creating permanent CUPS queues for these destinations so that the dialogs find them. I want to be able to get rid of this workaround.

When one contacts the projects it is difficult to impossible to find people who feel responsible for the print dialogs and fix them.

Such changes in CUPS could happen again and again, the next one is already in front of us: CUPS 3.x doing completely away with PPD files and classic driver filters. So the GUI developers are required to change the print dialogs again.

Also there can show up internet/cloud print services which should be accessible like printers in print dialogs.

So my idea was to take the responsibility of keeping the print dialogs up-to-date from the GUI developers and let the print-technology specific parts of the dialogs being centrally maintained by the developers of the print technologies themselves, OpenPrinting for CUPS, the provider of a cloud printing service for their cloud printing service.

So I am introducing a D-Bus protocol to decouple the print dialog GUI from the part which communicates with the print technology. The former are the frontends and the latter are the backends, one backend for each print technology.

The backends, which I call the Common Print Dialog Backends, are completely independent of concrete GUIs, so all GUIs (GNOME, KDE, …) are supposed to use the very same CUPS backend and when something changes in CUPS we at OpenPrinting adapt the backend appropriately, so that all print dialogs immediately will work with the new CUPS.

And if somebody would offer a new cloud printing service, they only would need to provide a CPDB backend for it so that users install the backend and their cloud print queues are immediately available in all their print dialogs.

How did we implement it?

To have the dialogs communicating with the backends, we have created a D-Bus communication protocol. The print dialog (fronted) must be able to

  • List all available print destinations
  • For a selected print destination get all the basic capabilities of the printer and all the user-settable options with all the possible choices.
  • Print a job with job data and user-selected option settings to a selected printer

A special challenge is that there can be more than one CPDB backend installed (CUPS + print to file + cloud printing + …) and in this case the frontend has to find all these backends and to list all the destinations of all the backends. Each entry in the list of available destinations contains not only the queue name but also the name of the backend which provides the queue, so that for getting the option list and for printing we can directly talk to the appropriate backend.

The backends are user (session) daemons which are started triggered by them getting accessed via D-Bus. They are running as the same user as which the application is running, so that the user does not gain additional rights by using the backends and security and privacy are assured.

Running the CPDB backend daemon(s) on-demand, triggered via D-Bus, saves resources.

We use the session D-Bus.

To find all available CPDB backends without needing to find their executables or any of their auxiliary files in the file system (which is not accessible for a snapped frontend) the D-Bus proxy is queried (see above). “ListNames” for all already running D-Bus services and “ListActivatableNames” for all D-Bus services which get auto-activated when one tries to access them. Each service in the resulting list whose name starts with “org.openprinting.Backend.” is assumed to be a CPDB backend.

Possible problems/further challenges

I am not an expert in D-Bus, nor any contributors on CPDB necessarily are, so above-mentioned approach for our CPDB backends is not necessarily the best approach our the way D-Bus should be used in our case. Any help is welcome here.

The concept got originally designed in 2017 where we did not yet think about sandboxed packaging or use in immutable operating system distributions. Adaptations for sandboxing, especially the method for finding all available CPDB backends where added recently.

Especially on this method for finding all available backends I am somewhat in doubt, as it looks like that it seems that this is not the way how one should actually do it in D-Bus and also one could intercept it by creating a service with a name starting with “org.openprinting.Backend.”.

What need our Snaps be able to do?

In our situation we have an application which accesses an arbitrary number of user daemons/D-Bus services via session D-Bus and the application does not contain any hardcoded list of which these services are. Each service has the same D-Bus API. The current slots/plugs interfaces for D-Bus allows only having one application connecting with one D-Bus service. Appropriate interfaces are needed here, currently, the Snap of the CUPS backend for CPDB works only in --devmode (no confinements).

What we can change in the CPDB upstream code

  • Keep frontend API: There are already applications (GNOME print dialog, KDE print dialog, Chromium print dialog) which us CPDB via its frontend API, so we should avoid to change the API as far as possible. As we are still in beta small change/fixes in the API are still possible.
  • The D-Bus communication can be vastly changed though, especially better fitted to getting snapped and for security. Even a replacement by something else than D-Bus would be possible if required or more suitable.

Probably this problem can be solved on different ways, either modifying the CPDB code so that CPDB works with current snapd or modifying snapd so that it fits with the current CPDB code.

Some remarks to the questions:

  1. Naming of D-Bus methods can still be changed without problems.
  2. This we should cange for snappability. I will here extend the API to allow supplying a file descriptor for accessing the print data.
  3. As the CPDB backends are user daemons triggered via the session bus I assume that they are running as the same user as the client application and so have the same rights with respect to CUPS as the application has.
  4. I would have to look into this. 5.I will contact the contributor who has implement it or better involve him in this discussion.

Also important is that in the end we are not only able to use CPDB with both application and backend being snapped, but also with the application snapped and the backends unsnapped or vice versa, to support both Ubuntu Core Desktop but also classic distros where client applications and/or CPDB backends can be snapped but may not be snapped.

So @jamesh @kenvandine @robert.ancell @pedronis any ideas how one could get this working?

1 Like

By the way, Michael Sweet has added a D-Bus interface to CUPS 3.x, exactly the local server of CUPS 3.x, a CUPS daemon which runs as a user daemon and will replace the system daemon cupsd of CUPS 2.x for systems which do not share any printer.

This D-Bus interface does more or less exactly the same as our D-Bus interface for the CPDB, but probably in a more efficient and secure way. It is exactly IPP over D-Bus and so probably most of its code is actually existing in the libcups and has stabilized over many years. The code of it is referenced in this issue report:

https://github.com/OpenPrinting/cups-local/issues/1

If we replace or D-Bus interface by this one, we could drop the CUPS backend starting from CUPS 3.x (and so avoid a converter from one D-Bus protocol to another) and other backends, like for cloud services would use the same interface to receive jobs as the local CUPS server does.

Important to note is that the local server of CUPS 3.x is scheduled for release only in ~Q3 2024 and so to make use of it in CPDB for Ubuntu Core Desktop in Q2 2024 we would probably need to temporarily copy CUPS code into CPDB.

When adopting this D-Bus interface it is important to only replace the D-Bus interface in CPDB and conserve the APIs for frontends and backends to not lose the work already done on the print dialogs.

Advantage is that IPP is a protocol which is established and optimized during two decades and therefore much more secure and stable than the young original D-Bus interface of CPDB, which is mostly created by inexperienced contributors. It also will be much easier to add the OAuth2 support which is already under development in CUPS 2.5.x, CUPS 3.x, and libcups3. Also the current CUPS backend will get vastly simplified and from CUPS 3.x on CUPS is its own backend. And our snapping work on the CUPS backend can be transferred to the local CUPS server of CUPS 3.x.

Disadvantage is that we have more coding work on CPDB by that.

WDYT?

One thing still to be checked here is whether we can handle more than one backend at a time with this D-Bus interface.

I don’t need you to tell me why CPDB is useful: I’m taking that as a given. My point was that the current design seems to be built on the assumption that everything is running with the same privileges.

If you want to extend this to work with sandboxing, then it is worth thinking about what client applications should be allowed to do. I suggested creating a protocol design document because my experience is that treating it as an implementation detail of a library causes problems when different components end up using different versions of the library (e.g. an app based on core26 talking to a backend based on core24). It also makes it easier to reason about what any AppArmor security policies we build should look like.

From a quick look, we’re still in pretty early days for CPDB’s adoption. There is a GTK 4 backend that’s not enabled by default upstream, and that’s about it. If anything needs to change, this seems like the time to do it.

@jamesh, I have studied the case more, especially how it exactly works on the D-Bus and described design of the D-Bus communication. I also put up what are the requirements for the Snaps and what changes could be made on the upstream code to improve its snappability.

Security: User daemon on session D-Bus

First off. as the CPDB backends are user daemons, activated and communicated with via the session D-Bus (not the system one), they should always run as the same user and therefore with the same privileges as the user running the frontend. If different users open the print dilaog to the same time, for each one separate instanves of the CPDB backends, under their privileges will get started. This also makes sure that the CPDB backend for CUPS sends the jobs with the same privileges to CUPS as the frontend does, so all jobs printed before the introduction of CPDB should also print on CUPS via CPDB. In addition, if a user creates their own backend and simply fires it up on the command line, they do not gain additional privileges by that.

Design description of the D-Bus use

We have 2 interfaces, defined in the CPDB library package cpdb-libs, therefore the are absolutely identical for each frontend (print dialog) and for each backend. The main interface, org.openprinting.PrintBackend, defined in the file cpdb/interface/org.openprinting.Backend.xml of cpdb-libs (turned into C code by gdbus-codegen), is provided by the backends and contains methods for all tasks a print dialog needed: List all available printers, get the capabilities and user-settable options for a given printer, print job on a given printer … It also contains signals to notify the dialog of new printers which appear, and of printers which disappear or change while the dialog is open. This is the interface of the D-Bus service which the backends provide.

The other interface is org.openprinting.PrintFrontend defined in the file cpdb/interface/org.openprinting.Frontend.xml of cpdb-libs and implemented in the frontends. It contains only signals, no methods (note that the frontends are the client and the backends are the server). The signals are to tell all available backends to switch the mode of listing available printers. We use signals here and not backend-(server)-side methods so that with a single call we can switch listing modes of all backends at once.

The interfaces, their methods, signals, and parameters defined in the XML files should be straightforward, so I do not explain them here individually.

Each backend by itself provides everything so that a print dialog get what it needs from the printing system. It is a straightforward D-Bus interface for a 1:1 communication between the print dialog and the backend. As a user daemon on the session bus the backend has the same privileges as the frontend (print dialog) user.

It gets more complex with the fact that when we open a print dialog, we want to find all available backends and list all printers of them together in the print dialog. This works as follows:

When a backend gets (classically) installed, we do not only install the backend executable itself (/usr/lib/x86_64-linux-gnu/print-backends/cups in case of Mantic’s cpdb-backend-cups Debian package) but also /usr/share/dbus-1/services/org.openprinting.Backend.CUPS.service which registers the backend as a D-Bus-activated service (“Activatable”).

The frontend calls both the ListNames and ListActivatableNames methods of the D-Bus daemon’s org.freedesktop.DBus interface to generate a list of the bus names of all active (providing executable is running) and all activatable (providing executable is not running but service is registered) D-Bus services on our session bus. Each backend creates a well-known bus name starting with org.openprinting.Backend. and so for finding all available backends, independent whether they are already running or not, we find all list entries starting with org.openprinting.Backend. and on all these D-Bus services we assume that they are CPDB backends and we call their method to list all available printers. As we also subscribe to the backend’s signals, we get the list of available printers permanently updated while the frontend is running.

Details on finding the backends and listing all printers

This all is done in the source file cpdb/cpdb-frontend.c of cpdb-libs. The frontend (for example tools/cpdb-text-frontend.c) calls at first cpdbGetNewFrontendObj() to create the data structure (in main() of tools/cpdb-text-frontend.c) and then cpdbConnectToDBus() to connect to the D-Bus, find the backends and create the initial list of available printers (in control_thread() of tools/cpdb-text-frontend.c).

cpdbConnectToDBus() (in cpdb/cpdb-frontend.c) starts with creating a D-Bus connection (get_dbus_connection()) and after that acquires a well-known bus name, via g_bus_own_name_on_connection(). In case of successfully acquiring the name the callback function on_name_acquired() does the remaining tasks while get_dbus_connection() is waiting for this to finish in a loop.

on_name_acquired() subscribes to the backend’s signals assigning callback functions which update the list of available printers (g_dbus_connection_signal_subscribe()), after that exports the frontend interface (g_dbus_interface_skeleton_export()), and in the end it calls cpdbActivateBackends().

cpdbActivateBackends() then finds the available backends as described above. It creates a proxy (g_dbus_proxy_new_sync()) for org.freedesktop.DBus, the D-Bus daemon’s own D-Bus service, and using this one (via g_dbus_proxy_call_sync()) to call the methods ListNames and ListActivatableNames of org.freedesktop.DBus. I joins the resulting lists of bus names of active and acrivatable D-Bus services on this (session) bus and then it loops through the list and if a name starts with org.openprinting.Backend.it assumes that this is a backend, adds it to the list of available backends and polls its current list of available printers calling fetchPrinterListFromBackend().

Which actions our Snaps need to do

First, as we do not only snap for Snap-only systems like Ubuntu Core or Ubuntu Core Desktop but also want to use the CUPS Snap and other printing-related Snaps in the classically installed Ubuntu, CPDB must work with any combination of snapped or unsnapped frontends and backends. We can have a classic distro in which we install a Snap of an app which has a CPDB-enabled print dialog, but the CUPS backend is classically installed, and we add a backend for a cloud printing service which comes as Snap … And other, unsnapped applications should also see the printers of both backends in their CPDB-enabled print dialogs …

Important is that frontends and backends are usually not in the same Snap, but in different, separate Snaps, or one in a Snap and the other classically installed.

A snapped frontend needs D-Bus access, at least to the session D-Bus. It must be able to send D-Bus messages to any service on the session bus and also trigger the activation of not yet activated services, especially also on services which were not yet known at build time of the frontend/application Snap.

The frontend needs especially to do all the operations mentioned above, especially connecting to D-Bus services which are discovered at run-time and also to use the methods of org.freedesktop.DBus to discover appropriate services.

A snapped backend must be snapped as a user daemon to be activated via D-Bus, under the name org.openprinting.Backend.XXX (XXX is name of the particular backend), by both snapped or unsnapped frontends.

If the D-Bus activation under these conditions is not possible, one could easily add support for permanently running the backends to backend packages.

What we can change and what we need to change for better snappability

1. REQUIRED - Print job via file descriptor

In the file cpdb/interface/org.openprinting.Backend.xml the method for actually printing a job is defined as follows:

<node>
    <interface name="org.openprinting.PrintBackend">
        [...]
        <method name="printFile">
            <arg name="printer_id" direction="in" type="s" />
            <arg name="file_path_name" direction="in" type="s"/>
            <arg name="num_settings" direction="in" type="i"/>
            <arg name="settings" direction="in" type="a(ss)"/>
            <arg name="final_file_path" direction="in" type="s" />
            <arg name="jobid" direction="out" type="s" />
        </method>
        [...]
    </interface>
</node>

According to this the job content has to be put into a physical file and the path name has to be supplied to the backend. This does not work in Snap. We (upstream) need to change this to use a file descriptor (does this work with Snap?) or something else (Unix socket?).

2. OPTIONAL - Run CPDB backends permanently during the whole session instead of triggering them by D-Bus

In the case that we are not able to snap the backend as a D-Bus-activated user daemon in the way that it can also be triggered by unsnapped frontends or Snaps who did not get paired with this backend at build time, we need to let the backend run as permanent user daemon, starting when the user logs in and stopping when they log out. This requires slight changes upstrem, for example not stopping the backend when there is no open frontend dialog any more.

To make the permanent running of the backends more resource-saving, we could run them in an idle mode, not communicating with the actual print technology, like CUPS, simply only listening for signals from the D-Bus and when there is a signal (or method call), then we power up to normal operation mode and go back to idle mode if there is no open frontend dialog any more.

3. OPTIONAL - Find backends by sending a signal

If we are running the backends permanently, we can, alternatively to call the D-Bus daemon’s ListNames and ListActivatableNames methods add 2 new signals to the frontend’s org.openprinting.PrintFrontend interface, one to tell that our frontend is starting, to make backends switching from idle into normal mode, taking note that there is a frontend connected, and sending a complete list of its currently available printers to the frontend. The other signal would tell the backend s that we are closing the frontend and so the backend can see when there are no open frontends any more and turn back into idle mode.

The whole point of application sandboxing is that a user can run applications that do not have all the privileges the user has. When we let a sandboxed application talk to a daemon, we expect the daemon to only let the app access resources belonging to the app. In this particular case, the main constraints we’d want are:

  1. the app should only be able to create print jobs from data it can access.
  2. the app should only be able to see the status of the print jobs it created.

If your model involves applications talking to each backend directly, then that means that each backend would need to enforce these restrictions itself.

As I said in my previous post, this seems like a weird choice. The backends are potentially in use by a number of applications running simultaneously. So having those apps try to manipulate the global state of the backends is a weird choice.

As an example, let’s say I’m running two applications A and B simultaneously:

  • A only wants to use local printers, so sends out the HideRemotePrinters signal.
  • B wants to print to remote printers, so sends out the UnhideRemotePrinters signal.

What happens now? Does the behaviour change depending on which order I start the two apps?

And with the way backends are D-Bus activatable, what happens if a backend isn’t running when an app sends one of these signals?

I’ll also restate my previous suggestion:

I’m less interested in the internal details of how each component is implemented internally: it’s how the components communicate that would influence how we structure any snapd interfaces, and decide whether any changes are warranted to better fit in a sandboxed environment.

As I mentioned, for specifying the job data to be printed we will change the CPDB implementation from specifying the file path, but instead, give a file descriptor or a domain socket through which we will stream the print job data (whatever is easier to handle when passing data from one Snap to another, here I am asking people who are more knowledgeable about Snap than me).

As already mentioned, a file the app can access, another Snap, here the backend Snap is not necessarily able to access. Also the app could supply the path of a file which is not able to access but the backend would be able to access (especially if the backend is not snapped). Changing to send print job data as a stream we require the app to have access to the actual print data to be able to send it.

The CPDB backends do not save or read any user configuration, like for example default printers, so they do not need any access to the user’s home directory when we let them receive the print job data as a stream, via file descriptor or domain socket. So when snapping them we do not let them plug the home interface.

I have now checked all 5 print dialogs which are commonly used in desktop applications: GTK (GNOME), Qt (KDE), Mozilla (Firefox, Thunderbird), Chromium Browser, LibreOffice. None of them has facilities to display the job queue of the destination printer nor keep track of a job once sent off to the printer. Also none of them allows canceling print jobs. They even do not keep track of the app instance’s own print jobs.

Currently CPDB with its D-Bus protocol supports listing of the queued jobs, telling the number of queued jobs, and canceling a job. This is functionality the user might be allowed to use (as far as the print system permits) but not necessarily an arbitrary application installed from the Snap Store.

The best bet is probably to remove the job-handling-related functionality from CPDB so that the D-Bus protocol simply does not allow a frontend app to see queued jobs or to cancel a job. As the currently available print dialogs do not use this functionality we would not break anything by removing it.

In addition, the Snap of the CPDB backend for CUPS will only plug the cups interface and not cups-control. Alternatively, the CPDB backend for CUPS could be included as an additional app in the CUPS Snap, this way it is assured that the backend is always matching the CUPS version in use.

By doing the changes on CPDB mentioned above, passing job data as a stream and removing job handling functionality the frontend/backend communication protocol of CPDB should already enforce the restrictions.

Here I will also change the design of CPDB. Said signals are meant to filter the list of available printers in the print dialogs, to more easily find the desired printer, having less clutter. Problem is, as you mention, if the user has the print dialogs of more than one app open and selects different filtering options.

So we should not implement this filtering on the backend side but on the dialog/frontend side. To do so, I will do away with these signals and let the backend always list all available printers, but if a function of the frontend library to set a filtering option is called, the frontend library will do the filtering, simply skipping the printers the user wants to have filtering away when passing the printers reported by the backend to the print dialog.

Me detailing about the internal implementation of CPDB was meant to help someone more expert in Snap than me to tell which interfaces I have actually to use or how to create a custom interface which accomodates CPDB. Or to give me suggestions how to modify the D-Bus protocol that it works with existing interfaces.

The way how the D-Bus protocol principally works I have described in the section “Design description of the D-Bus use” in my previous post.

I’m asking for details of how the components communicate because that is directly relevant to writing a snapd interface to support this functionality, since it will be mediating that communication. It will also useful when doing a security review of the new interface.

The kind of detail I am after is:

  1. What specific D-Bus method calls does a client use to discover available backends?
  2. What specific D-Bus signals does a client need to learn about:
    • New backends?
    • New printers provided by a backend?
    • State changes for existing printers?
  3. What specific D-Bus method calls does a client make to create a print job?
  4. The API seems to be trying to also cover the use case of a printer settings app. Do we want to gate those methods/signals off from basic clients that only want to print stuff?

On the security side, I was having a look through the cpdb-backend-file source, and it appears to write client provided data to an arbitrary client provided path. That seems like the kind of thing that could enable a privilege escalation. In the case where cpdb-backend-file is running unconfined, some examples:

  1. write a desktop file to ~/.config/autostart to run arbitrary code on the next login.
  2. write data to ~/.ssh/authorized_keys to get an unconfined shell session if there’s an ssh daemon running on the local system.

These would be mitigated with a snap confined cpdb-backend-file, but it’s not clear what value this provides over the client implementing “save to file” itself (which seems to be what the GTK print dialog does).

The frontend calls both the ListNames and ListActivatableNames methods of the D-Bus daemon’s org.freedesktop.DBus interface (through a proxy) to generate a list of the bus names of all active (providing executable is running) and all activatable (providing executable is not running but service is registered) D-Bus services on our session bus. Each CPDB backend creates a well-known bus name starting with org.openprinting.Backend. and so for finding all available backends, independent whether they are already running or not, we find all list entries starting with org.openprinting.Backend. and on all these D-Bus services we assume that they are CPDB backends and we call their method GetPrinterList to list all available printers. As we also subscribe to the backend’s signals, we get the list of available printers permanently updated while the frontend is running (see below).

There is no facility to discover new backends appearing while the print dialog is open. If there actually appears a new backend (user installs new backend package while print dialog of an app is open, a very, very rarely happening case), it only gets visible when closing the dialog and opening it again, as then the procedure of (1) (see above) is executed again.

We could easily fix this. To do so, if we get a signal from a backend and this backend is not under those which we had found initially, we could simply add it on the spot, call its GetPrinterList method to get up-to-date with it and we are done.

As printers in the network can quickly appear and disappear and when you print you easily open the print dialog and forget to turn on your printer, we are updating the list of available printers provided by the already installed backends continuously. For this, we subscribe to the signal PrinterAdded of the interface org.openprinting.PrintBackend which all backends provide. The callback function we assign makes a printer entry from the signal’s arguments. The signal is emitted by the backend once for each new printer appearing. In case of CUPS it is when a new CUPS queue is created and also when a new IPP print destination (like a network printer) appears.

Here the interface org.openprinting.PrintBackend of all backends provides the signal PrinterStateChanged with all needed info in its arguments.

In addition we have also the PrinterRemoved for disappearing printers.

When opening the dialog, already before we find the backends, we subscribe to these three signals withour specifing a concrete backend, so that signals from any backend match. This way we assure that we do not miss any signal from the backends.

The D-Bus communication for a full print dialog session is as follows and happens every time when a print dialog is opened (all methods mentioned here are of the interface org.openprinting.PrintBackend of all backends):

  • List all printers as described above, finding the backends via the ListNames and ListActivatableNames methods of the D-Bus daemon’s org.freedesktop.DBus interface and then calling the method GetPrinterList on each backend and receiving the 3 signals to keep the list up to date while the dialog is open. We also call the method getDefaultPrinter to pre-select the default printer in the dialog.
  • When the user selects (clicks) a printer, call the method GetAllOptions (on the backend providing the selected printer) to get a list of all user-settable options with their available choices, to allow the dialog to build appropriate widgets so that the user can set the desired options.
  • getPrinterState and isAcceptingJobs provide additional displayable info for the printer entries in the dialog and determine whether the “Print” button should be active or not when the printer is selected.
  • The methods get*Translation* provide human-readable strings and translations for everything which can be displayed in the dialog and the frontend can call them whenever needed.
  • If the user clicks finally on “Print”, the method printFile is called to send off the job to the backend, along with the option settings the user has selected. Note that here we need to fix CPDB, as we need to pass out the job data to the backend as a stream, supplying a file descriptor or domain socket (Snap experts: What is more suitable for Snap-to-Snap transfer?) instead of a file path.

That is all what the CPDB API does and what it is capable of, after removing the methods getActiveJobsCount, getAllJobs, and cancelJob from CPDB.

No, the API does not support administrative tasks like creating or modifying print queues. Support for listing and canceling jobs I will remove as none of the current print dialogs has such functionality anyway and this would actually allow applications to do more than just printing.

The Snap of the backend for CUPS I will also let plug only the cups interface and not cups-control.

The file backend I will discontinue and declare it as for development or workshop example only. So we will not try to snap it. All the 5 dialogs we deal with have their own, built-in print-to-file functionality running in the application Snaps and so we do not need to provide a backend for this use case any more. When suggesting its implementation 5 years ago I was not aware that the dialogs already come with this functionality.

What we need to change on CPDB up to now

  1. printFile method of backends needs to pass the job data as stream (file descriptor or domain socket), not as file specified by a path.
  2. The methods getActiveJobsCount, getAllJobs, and cancelJob need to get removed from CPDB
  3. The file backend of CPDB needs to get discontinued.
  4. Filtering of the printer list in the dialog should be completely managed by the frontends, and not by sending signals to the backends, to allow different filtering on print dialogs which are open to the same time.
  5. Add new backends while the dialog is open. See above.