Handling of the "cups" plug by snapd, especially auto-connection

How would a user normally make this change?

@ijohnson, to change the configuration of a classically installed cupsd one needs to change cupsd,conf. This can be done with either by a user with “lpadmin” group membership or by root. As it works as root, a program which installs a package can do that. A daemon in a Snap including its startup script also runs as root, but the confinements of the Snap usually block modifying external files or restarting an external daemon, but the CUPS Snap can access the global CUPS socket /run/cups/cups.sock and as its CUPS daemon’s startup script runs as root, it should be able to modify the classic CUPS’ configuration, with code like in cupsctl.

But I also came to another idea: As a locally running use application sees the locally available CUPS printers, lists them in its print dialog, and also prints on them, even without the local CUPS sharing its printers, I could add a mode to cups-browsed which discovers printers on a given CUPS socket (which is not the same as of the CUPS the cups-browsed is attached to) and creates queues on the Snap’s local CUPS appropriately, so that jobs to these queues get passed on to the classic CUPS. This would not need any configuration change on the classic CUPS.

Here I need to find out which is the better solution.

@ijohnson, good news, one does not need the classic CUPS to share printers. The shared bit is only about making the printers available to remote machines. On the local machine they are accessible through their usual URI ipp://localhost:631/printers/QUEUE. Without sharing they are only not DNS-SD-registered, so I need to add an option to cups-browsed to discover printers on a given socket via cupsGetDests() API.

@ijohnson, I am currently working on a cups-proxyd which mirrors the queues of the system’s cupsd to the Snap’s cupsd, so that the Snap’s cupsd has the same queues but passing the jobs unfiltered to the system’s cupsd (drivers are on the system’s cupsd).

I do not expand cups-browsed to use that, because operations here are well different, and complications as taking note of network interfaces or the implicitclass backend should be avoided. Also cups-browsed is supposed to get spun out of the CUPS Snap. cups-proxyd will then be run by the startup script for cupsd, no new “app” created in snapcraft.yaml.

There is only one problem which I discovered now with @alexmurray here, the “cups” plug for client Snaps only allows access to /run/cups/cups.sock not to the alternative socket in $SNAP_DATA, /var/snap/cups/common/run/cups.sock. This needs to be added, I am talking with @jamesh about this currently.

@ijohnson, @alexmurray, @jamesh, in case of the Snaps plugging “cups” auto-installing the CUPS Snap, we should even think about the “cups” plug only allowing access to /var/snap/cups/common/run/cups.sock and not to /run/cups/cups.sock.

Hi @till.kamppeter, from my proposal above, you don’t need to provide access to /var/snap/cups/common/run/cups.sock to client app snaps wishing to print via the cups interface. Instead in my proposal you would have definitions like this:

cups snap snapcraft.yaml (approximate)

slots:
  cups-socket:
    interface: content
    content: cups-socket
    source:
      write:
        - $SNAP_COMMON/cups.sock

apps:
  cupsd:
    slots: 
      - cups-control
  cups-print-only-test:
    environment:
      CUPS_SOCKET: /var/lib/cups/cups.sock
    plugs: {} # empty because it doesn't need any special permissions - 
              # it can access the socket hence it can print
  cups-admin-test:
    environment:
      CUPS_SOCKET: /var/lib/cups/cups.sock
    plugs:
      - cups-control # here we need to use cups-control since this test app
                     # needs to do more than just print, it needs admin things too

client snap wanting to print

plugs:
  cups-socket:
    interface: content
    content: cups-socket
    default-provider: cups # this line ensures that if the system installing this snap does not
                           # already have cups snap installed, it will get automatically installed
    target: ​/var/lib/cups/cups.sock # or wherever is convenient
environment:
  CUPS_SOCKET: /var/lib/cups/cups.sock

Under this situation as I described above, we don’t actually need to use the cups interface verbatim at all, we can resort to using only the cups-control interface and the cups-socket content interface.

Hopefully this helps.

@ijohnson, thank you very much.

You are using the CUPS socket /var/lib/cups/cups.sock for the client to access CUPS through. Is it intended that this is not the standard CUPS socket /run/cups/cups.sock? Or did you simply not remember correctly or using an old socket path?

@ijohnson, what will exactly happen if a Snap which wants to print is installed of a system without CUPS Snap and with classic CUPS installed? The classic CUPS is listening on /run/cups/cups.sock. The installation of the Snap triggers the installation of the CUPS Snap. Now the CUPS Snap sees the presence of the classic CUPS and therefore it goes into proxy/firewall mode (classic CUPS stays running without changes, Snap’s CUPS mirrror’s queues of classic CUPS and passes jobs on to there).

How does it now work that unsnapped apps printing to /run/cups/cups.sock print directly to the classic CUPS while the snapped app printing to /run/cups/cups.sock prints through the content interface connection to the snapped CUPS?

Or do you really mean /var/lib/cups/cups.sock as a different socket file compared to /run/cups/cups.sock? Why do you then use something in /var/lib/...? This would need a separate permission as it is outside the Snap. Would be $SNAP_COMMON/run/cups.sock not the better choice?

@jamesh, on Mattermost you told:

You probably can’t use the content interface directly, but having the cups and cups-control interfaces perform a bind mount is certainly possible.

How does this get implemented?

@jamesh on Mattermost:

you can’t use the content interface to bind mount to /run/cups. Also, if we put the logic into the cups interfaces, it hides the complexity from application snaps on the plug side, content the content interface can only mount to filesystem locations the snap controls like $SNAP, $SNAP_DATA, and $SNAP_COMMON.
While you can use layouts to mount from those locations to other parts of the file system, snapd doesn’t do well at always ordering the mounts (especially if it is updating an existing mount namespace)

@ijohnson, this means that you cannot use /var/lib/cups/cups.sock as target for the slot. It must be something like $SNAP_COMMON/run/cups.sock or so.

@ijohnson, @jamesh, I have now implemented the new proxy mode, as you have suggested:

It was a lot of work and took me a lot of time, as I had to create a new daemon (cups-proxyd) for auto-mirroring the queues of the system’s classic CUPS to the Snap’s CUPS, including the discoovered printers for which the classic CUPS auto-creates temporary queues.

I also had to create a new CUPS backend (proxy) to allow the mirrored queues of the snapped CUPS to pass the jobs on to their original queues on the classic CUPS without needing the classic CUPS to share the queues. So nothing on the configuration of the system’s CUPS has to be changed. The CUPS Snap in proxy mode does not do any administrative tasks on the system’s CUPS at all.

What is still missing and what I will do as the next step is to create the content interfaces.

Updated the README.md of the CUPS Snap:

This documents how the new proxy mode is working, how it is invoked, and also the stand-alone (CUPS Snap is system’s CUPS) and parallel (classic CUPS and snapped CUPS are two independent instances on one system) modes.

Some technical background info to the new proxy mode:

  • The proxy mode is invoked if there is an /etc/cups/cupsd.conf file present and readable (at least for root in the Snap), independent of whether there is actually a CUPS daemon running or not. This form of proxy mode invocation prevents race conditions on whether the CUPS Snap or the system’s CUPS starts first during boot. We cannot for example check for the presence of the cupsd executable file, as the Snap can only read /etc/cups in the system.
  • The mirroring of print queues from the system’s CUPS daemon to the Snap’s CUPS daemon is done by an auxiliary daemon named cups-proxyd. This daemon listens for appearing and disappearing of arbitrary IPP print services via DNS-SD (to get note of printers which could create temporary on-demand queues on the system’s CUPS) and for print queue addition/modification/removal and printer status change via system’s CUPS D-Bus notifications (to get note of queue modifications on the system’s CUPS even if the queues are not shared). On each event it freshly mirrors all queues from the system’s CUPS to the Snap’s CUPS and also removes disappeared queues from the Snap’s CUPS. It even mirrors temporary queues which CUPS creates on-demand for discovered IPP printers, by force-creating them on the system’s CUPS via a dummy access and then mirroring them.
  • There is no apps: entry in snapcraft.yaml for cups-proxyd. cups-proxyd is completely managed by the run-cupsd and stop-cupsd scripts. We do not run it as drop-in replacement for cups-browsed by the run-cups-browsed script as we want to spin out cups-browsed into its own Snap later.
  • cups-browsed is not run in proxy mode, the run-cups-browsed script is simply running alone, without cups-browsed, with a dummy daemon process which stop-cups-browsed can kill.
  • The configuration of the system’s CUPS does not need to be changed by the user for that, nor is it changed by the CUPS Snap (the CUPS Snap does no administrative action on the system’s CUPS at all). The printers of the system’s CUPS do not even need to get shared for the proxy to work. If unsnapped applications on the local machine can print, the proxy works.
  • To allow the queues of the Snap’s CUPS pass on the jobs to the original queues on the system’s CUPS without the system’s CUPS needing to share them, a special CUPS backend named proxy got created, which gets the system’s CUPS’ socket and the queue name via the device URI and prints the same way as lp would do, passing on the options of the original job.
  • The mirrored queues on the Snap’s CUPS do not filter the jobs any further than to PDF. The remaining filtering to the printer’s native language is done by the system’s CUPS. So the user’s drivers (especially proprietary ones) are continued to be used. The options are made available in the mirrored queus simply by copying the PPD files, but the filter rules in the PPD files are changed to stop the filtering at PDF.
  • cups-proxyd is simple, it has no configuration file, it is controlled only by its command line. It has a log file, in the CUPS Snap it is /var/snap/cups/current/var/log/cups-proxyd_log.
  • The source code of cups-proxyd and proxy is maintained in the CUPS Snap project, in the cups-proxyd/ subdirectory, as these only make sense in the Snap.

@ijohnson, CUPS listening on two domain sockets is no problem at all, it always was capable of this, simply two Listen ... lines in cupsd.conf pointing to the domain sockets do the trick. So (4) in your initial presentation of the proxy idea is solved by my following commit on the CUPS Snap (no change on CUPS itself needed):

It makes simply the run-cupsd script add two Listen ... lines to cupsd.conf when it finds out that we will run in stand-alone mode:

Listen /run/cups/cups.sock
Listen /var/snap/cups/common/run/cups.sock
Port 631
[...]

Now the snapped CUPS listens on /var/snap/cups/common/run/cups.sock in all three modes (stand-alone, proxy, parallel), so that snapped applications can always print to this domain socket. In stand-alone mode unsnapped applications print also to the snapped CUPS as it is also listening on /run/cups/cups.sock then.

1 Like

@ijohnson, @jamesh, now I only need to create the content interface for the snapped clients to force-install the CUPS Snap and having access to /var/snap/cups/common/run/cups.sock for printing, but at the same time the snapped clients need to also have access to D-Bus notifications of CUPS (push notifications from CUPS for printer status changes, job progress, …, no admin tasks possible via D-Bus) (as cups-control also provides).

What I like most here would be an interface named cups containing both the content interface and the D-Bus access. Or do we have to split into cups (or cups-dbus) for the D-Bus and cups-socket for the content interface (as in this example)?

@ijohnson, note that the name of the environment variable is CUPS_SERVER and not CUPS_SOCKET here.

Sorry for taking a while to respond on this. My thoughts are still that we should keep the things as simple as possible for applications, and preferably allow communication with the system CUPS instance.

Now for some specific feedback:

I think a bind-mount like the content interface provides here is going to be part of the solution, but I’m not sure we want to make use of the content interface directly. In particular:

  1. we can’t use this to talk to the host system CUPS.
  2. any application snap will need plug definition and environment variable boilerplate to make proper use of it.
  3. we can’t use it to make the CUPS socket appear at the default /run/cups/cups.sock location, since the content interface only allows bind mount targets under $SNAP/, $SNAP_DATA/, or $SNAP_COMMON/. While layouts allow creating bind mounts outside of those directories, trying to chain the content interface and layouts together is error prone.

One other option would be to modify the existing cups and cups-control interfaces to perform the mounts themselves. That would allow us to bind mount over /run/cups from some directory managed by the slot-side snap, and skip the bind mount when connecting to an implicit system slot.

As for what directory to use as the source for the bind mount, this could either be something fixed by the interface, or specified by an interface attribute on the slot. I don’t think we want to do anything fancy like perform a bind mount over /run/cups on the slot side. While that would allow an unmodified CUPS to create its socket in the normal location, it would also make it impossible to implement the “proxy cups” model, where a snapped CUPS talks to a system CUPS.

What is the advantage of having a snap being able to talk directly to the host system’s CUPS if the proxy mode of the cups snap can provide all the necessary information whilst being mediated?

This is inconvenient, but I don’t view it as a blocker or a large enough disadvantage to stop CUPS from operating in proxy mode, if it is more complicated than a few environment variables, then I think that something like a snapcraft extension and command-chain script could alleviate this situation.

I agree using the content interface with layouts has been error prone in the past, if you think this is a barrier to using it, then we should a) work on identifying those bugs as much as possible and b) if we are already having folks redirect where to find the cups socket via an environment variable, why not just make that environment variable point to somewhere we don’t have to use layouts with, i.e. $SNAP_COMMON, then this is not an issue.

I’m not opposed to this at all, it sounds like from @till.kamppeter’s comments that there are other things that are needed in addition to accessing the socket, mainly D-Bus access in order to print. Is that correct @till.kamppeter ? This does slightly change my proposal, but still I think can be done in a very clean and always mediating way.

I would really like to avoid as much as possible having an implicit slot here since AIUI the cups snap can always do mediation for any distro, then we should have every snap always use the cups snap to the point where implicit slots are not needed.

This isn’t a problem if we require that snaps always have to talk to CUPS through the cups snap’s shared socket, the work from @till.kamppeter ensures that this socket will always expose everything one can do natively outside of the snap sandbox, so from my understanding there is no advantage to a snap being able to talk directly to the host’s CUPS (which may not do any mediation remember).

Just to take a step back, I’m not trying to make this needlessly complicated. I just know that folks have wanted an easy way to print from snaps for a long time, specifically one which doesn’t require fiddling with permissions. In this proxy mode, with a few simple modifications (env vars and the like) to client applications enables those snaps to always be able to do the simple act of printing from inside a snap without needing to connect any interface on any distro that snaps support. Whereas if we just stick to the existing state of things with the cups slot being implicit on other distros, users still need to go and figure out how to connect interfaces and deal with permissions for “something as simple as printing”. I want to make the best experience available for all users of snaps, on any distro. If it’s a bit extra work to make this work, I think it’s well worth the delay to deliver the best experience for snap users wishing to print.

I have the following remarks:

  1. For sole printing and displaying print dialogs which list the available print queues and show the user-settable options for each print queue one only needs the domain socket of CUPS and if it is not at the standard location /run/cups/cups.sock we need to set the CUPS_SERVER environment variable to the actual location (the CUPS Snap is ALWAYS listening on /var/snap/cups/common/run/cups.sock and it is ALWAYS mediating).
  2. D-Bus notifications are an optional service of CUPS. If a client subscribes, CUPS gives push notifications via D-Bus about changes on printer status, available queues, configuration changes, job status, … They are not needed for printing and for displaying standard print dialogs, not used by most client applications which print. They improve possibilities though, as with them a print dialog could update on changes in real time and it could also give the user a way to track their jobs in real time.
  3. The cups-control interface is a complete interface for CUPS clients, it contains both access to the CUPS domain socket plus access to the D-Bus notification facility (and perhaps other features, so let us say cups-control allows access to the CUPS domain socket and the “CUPS extras”).
  4. We need to force snapped applications print through the snapped CUPS as the snapped CUPS always has Snap mediation, the system’s CUPS only in rare cases (and we cannot check this from the outside). To have assure that snapped applications and unsnapped applications see the same printers if we are on a system with unsnapped CUPS, we need to force-install the CUPS Snap in proxy mode.
  5. The interface of CUPS is rather complex, so writing a new “mediation shell daemon” which looks/behaves like CUPS for snapped applications and passes on non-admin requests to the system’s CUPS is way more complex and bug-prone than simply using the actual CUPS (the one in the CUPS Snap) as this “mediation shell daemon”.
  6. @jamesh came with an approach on Mattermost to use the xdg-desktop-portal which flatpak uses. This is more or less like the “Common Print Dialog” which I tried to establish back in 2006 together with some GUI experts but did not succeed due to lack of available coding workforce. The applications do not have their own print dialog but call functions via D-Bus to pop up a print dialog and to actually print. This would probably solve all the problems of the print dialogs I am complaining about, but this is not viable here as all applications would need highly invasive modifications for snapping them, an d snapping command line apps for headless servers will get impossible.
  7. So there seems to be no way to protect an existing CUPS on an arbitrary distribution against admin requests by simple AppArmor and other Snap-typical sandboxing techniques. The proxy CUPS seems to be the easiest-to-implement way.

So our print interface needs:

  1. Printing from Snaps forced through the CUPS of the CUPS Snap so that this CUPS mediates
  2. If the main system’s CUPS is some CUPS installed with the distribution (it is usually not mediating) the CUPS of the CUPS Snap works as proxy. This proxy mode is already implemented.
  3. If there is no CUPS from the distro, the CUPS of the CUPS Snap goes into stand-alone mode to work as the main CUPS. In this case the snapped CUPS also listens on /run/cups/cups.sock so that unsnapped apps can also print (this is also already implemented in the CUPS Snap).
  4. We can simply make the print interface let the CUPS_SERVER env variable be set to /var/snap/cups/common/run/cups.sockso that the snapped app prints to the snapped CUPS and in addition not allow the snapped app access /run/cups/cups.sock to protect against apps which override the env variable and switch (or hard-code) to /run/cups/cups.sock internally.
  5. Better than (4) is that if the print interface bind mounts /var/snap/cups/common/run/cups.sock to /run/cups/cups.sock in the application Snaps instead of setting the CUPS_SERVER env variable, as them badly (or maliciously) programmed apps hard-coding to /run/cups/cups.sock can also print, but only print, not administrate as the job goes through the CUPS of the CUPS Snap.
  6. Whatever the print interface does, it has also to support all the “CUPS extras” which I mentioned in (3) in the beginning of this posting.
  7. The cups-control interface should NEVER route requests through a proxy CUPS, as administrative requests through the proxy CUPS are technically not possible. It should always directly talk to the main CUPS which actually executes the jobs. So it should not do any bind mounting and ALWAYS talk to /run/cups/cups.sock. Then in distros with their own classic CUPS it talk’s to the distro’s CUPS and on systems with the CUPS Snap as main CUPS (stand-alone mode) it talks to the snapped CUPS.
  8. Both the print interface and the cups-control interface should support the very same “CUPS extras” in addition, the ones of the current cups-control interface.

I hope such a set of interfaces could be implemented, especially with (5) instead of (4). Please tell me what I should do from my side for that. Thanks.

@ijohnson, @jamesh, the answer got somewhat longer, but I hope this is all what we need to design the correct interface for printing.

This fact is a bit unfortunate and was not clear to me from your earlier comments. I will think on how best to handle this, but an important point to remember about cups-control is that existing snaps today may be using cups-control to print, and thus will not have the new mediating socket available in their snap namespace, and we should not break these existing snap use cases.