From 843c6866a5ee680faa67fb03f3ccfd426c1de624 Mon Sep 17 00:00:00 2001 From: Juergen Holly Date: Mon, 19 Oct 2026 22:38:14 +0200 Subject: [PATCH] fix airprint discovery --- README.md | 35 +++++- cupsd.conf | 12 ++ .../fix-airprint-discovery/.openspec.yaml | 2 + .../changes/fix-airprint-discovery/design.md | 103 ++++++++++++++++++ .../fix-airprint-discovery/proposal.md | 68 ++++++++++++ .../specs/printserver/cups/spec.md | 19 ++++ .../changes/fix-airprint-discovery/tasks.md | 18 +++ 7 files changed, 252 insertions(+), 5 deletions(-) create mode 100644 openspec/changes/fix-airprint-discovery/.openspec.yaml create mode 100644 openspec/changes/fix-airprint-discovery/design.md create mode 100644 openspec/changes/fix-airprint-discovery/proposal.md create mode 100644 openspec/changes/fix-airprint-discovery/specs/printserver/cups/spec.md create mode 100644 openspec/changes/fix-airprint-discovery/tasks.md diff --git a/README.md b/README.md index 3e87bad..b5856c0 100644 --- a/README.md +++ b/README.md @@ -334,8 +334,31 @@ queries the printer live, offers all of them. On the Mac, macOS only takes its driverless AirPrint path when it accepts the printer's secure `_ipps` service; if that fails it silently falls back to its bundled generic PostScript driver. Duplex then stays hidden, because the generic PPD -gates it behind `APOptionalDuplexer False`. The usual cause is the certificate -(see *TLS certificate for `_ipps`*), so check that first: +gates it behind `APOptionalDuplexer False`. There are two causes; check the +Bonjour sub-types first. + +**1. The printer is advertised with the CUPS `_cups` sub-type.** macOS classifies +a Bonjour printer as *shared* when it can be browsed under `_ipp._tcp,_cups`, +and its AirPrint driver is only offered for a printer that carries `_universal` +and no `_cups` (`-[DNSSDPrinter isShared]` / `-[DNSSDPrinter airprintMatch]` in +`AddPrinter.app`'s `PMBrowser.framework`). CUPS registers +`_cups,_print,_universal` unless `BrowseDNSSDSubTypes` says otherwise, so the +queue is offered with the generic driver only. `cupsd.conf` therefore sets +`BrowseDNSSDSubTypes _print,_universal`. Confirm it is in effect: + +```sh +podman exec printserver avahi-browse -rt _ipp._tcp,_universal # lists the queue +podman exec printserver avahi-browse -rt _ipp._tcp,_cups # must not list it +``` + +The second command lists other CUPS servers on the network; only *this* queue +must be absent from it. If it does list the queue, the image's `cupsd.conf` did +not reach `/etc/cups` — a bind-mounted `/etc/cups` hides the image's copy; see +*Notes and limitations*. + +**2. The certificate is rejected.** Clients only accept it when the name they +connected to is in its SANs *and* they trust it (see *TLS certificate for +`_ipps`*). Check what is advertised and served: ```sh podman exec printserver avahi-browse -rt _ipps._tcp # what is advertised @@ -343,7 +366,7 @@ podman cp printserver:/etc/cups/ssl/server.crt . && openssl x509 -in server.crt -noout -subject -ext subjectAltName ``` -A verified way around it is to add the queue from the command line, driverless +A way around either cause is to add the queue from the command line, driverless over the same secure service (`ipptool -tv ipp://:631/printers/` prints the UUID; drop the `urn:uuid:` prefix): @@ -356,8 +379,10 @@ lpoptions -p -l # Duplex, ColorModel and tray 2 show up now In the GUI, remove the printer and add it again, then pick **AirPrint** (not *Generic PostScript Printer*) under *Use*. If only the generic entry is offered, -the secure connection is still being rejected — fix the certificate, or add the -queue from the command line as above. +either the `_cups` sub-type is still being advertised or the secure connection is +still being rejected — fix that, or add the queue from the command line as +above. macOS caches the failed attempt, so remove and re-add the printer after +changing anything on the server. ## CUPS 3 and the future of PPDs diff --git a/cupsd.conf b/cupsd.conf index e2e3f48..cc9b967 100644 --- a/cupsd.conf +++ b/cupsd.conf @@ -20,6 +20,18 @@ Browsing On BrowseLocalProtocols dnssd BrowseWebIF Yes +# Do not register the CUPS "_cups" Bonjour sub-type. +# +# macOS decides between the driverless AirPrint driver and its generic +# PostScript driver in its Add Printer browser (PMBrowser.framework): it offers +# AirPrint only for a service carrying the "_universal" sub-type, and it treats +# a service as *shared* as soon as it also carries "_cups" +# (-[DNSSDPrinter isShared]; -[DNSSDPrinter airprintMatch] then returns +# nothing). CUPS registers "_cups,_print,_universal" by default, which is why +# the queue is offered as "Generic PostScript Printer" only. Keep "_print" for +# clients that look for it. +BrowseDNSSDSubTypes _print,_universal + # Default authentication type, when authentication is required... DefaultAuthType Basic diff --git a/openspec/changes/fix-airprint-discovery/.openspec.yaml b/openspec/changes/fix-airprint-discovery/.openspec.yaml new file mode 100644 index 0000000..c1bc902 --- /dev/null +++ b/openspec/changes/fix-airprint-discovery/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-19 diff --git a/openspec/changes/fix-airprint-discovery/design.md b/openspec/changes/fix-airprint-discovery/design.md new file mode 100644 index 0000000..fb1cba3 --- /dev/null +++ b/openspec/changes/fix-airprint-discovery/design.md @@ -0,0 +1,103 @@ +# Design + +## Context + +See `proposal.md` — Why. + +The relevant facts, all from the running system (macOS 27.0.1 build 26A434, +CUPS 2.4.2 in the container) and the CUPS sources: + +- The container registers the queue with CUPS' own DNS-SD code + (`scheduler/dirsvc.c`): `_printer._tcp` unconditionally, and `_ipp._tcp` / + `_ipps._tcp` with the sub-type list from `DNSSDSubTypes`. The list is passed + to one `DNSServiceRegister` call as `,` (`snprintf(temp, "%s,%s", + type, subtypes)`), so `DNSSDSubTypes` decides the whole sub-type set. +- `DNSSDSubTypes` defaults to `_cups,_print,_universal` (`scheduler/conf.c`) and + is settable with the `BrowseDNSSDSubTypes` directive. +- The queue's TXT record already carries what a driverless client needs: `URF`, + `pdl=…,image/urf`, `TLS=1.2`, `Color=T`, `Duplex=T`, `mopria-certified=1.3`. +- macOS' Add Printer browser (`AddPrinter.app`'s `PMBrowser.framework`) browses + `_ipp._tcp,_cups` **and** `_ipp._tcp,_universal` (plus the `_ipps` forms) and + keys a printer's registration types by the `regType` the browse callback + reports. `-[DNSSDPrinter isShared]` is YES when any of them ends in `,_cups`, + and `-[DNSSDPrinter airprintMatch]` returns nothing when `isShared` is YES. + +## Goals / Non-Goals + +**Goals:** +- The queue is advertised so that macOS classifies it as an AirPrint printer and + offers the driverless driver, without changing anything else a client sees. + +**Non-Goals:** +- Replacing CUPS' DNS-SD advertising with hand-written Avahi service files, or + changing the TXT record CUPS generates. CUPS' advertisement works for every + other client; only the sub-type set is wrong. +- Changing the certificate, the queue's PPD or the device URI. + +## Decisions + +**Drop `_cups` from `DNSSDSubTypes` instead of suppressing CUPS' registration.** + +`BrowseDNSSDSubTypes _print,_universal` removes exactly the marker that makes +macOS treat the queue as shared, while keeping the `_universal` sub-type that +makes `isAirPrint` YES. `_print` is kept because CUPS ships it by default and +dropping it would remove information some clients use, with no benefit here. + +Alternatives considered: + +- *Advertise the queue with a hand-written Avahi service definition and turn + CUPS' own advertising off.* Gives full control over sub-types and TXT, but + duplicates what CUPS already generates (including the UUID and `printer-type` + TXT that CUPS clients resolve the queue with) and has to be kept in sync with + the queues. Only worth it if the sub-type turns out not to be sufficient. +- *Keep `_cups` and patch macOS' classification* — not possible; it is the + client's behaviour. +- *Stop sharing the printer (`printer-is-shared=false`)* — would remove the + queue from mDNS entirely. + +**Keep CUPS' TXT record and the `/cups` device URI.** + +The `/cups` path marker and the `printer-type` TXT key come from CUPS' own +advertisement and tell CUPS clients that this is a CUPS server; they are what +makes `dnssd://…/cups?uuid=…` resolve. macOS' sub-type-based `isShared` is a +separate mechanism from that marker, so removing the sub-type keeps CUPS +clients working while fixing macOS' driver choice. + +**Rely on `airprintMatch`'s remaining requirements being satisfied by the +existing advertisement.** + +After the sub-type fix, `airprintMatch` also needs a device-ID entry with the +TXT-derived `URF:` key and a `dnssd servicetype:` entry for `_ipp._tcp` / +`_ipps._tcp`, and a locally built generic URF PPD. Those come from the printer's +TXT record, which already carries `URF` and `pdl` (PMBrowser normalises TXT keys +to its `KEY:` form) and from the service type in the browse result — the same +inputs a real AirPrint printer provides. If the GUI still offers only the +generic driver, the fallback is the Avahi service file mentioned above. + +## Risks / Trade-offs + +- [The queue is still classified as shared because the `/cups` device URI or + `printer-type` TXT matter in a code path not covered by the disassembly] → + Small risk; the disassembly of `isShared`/`isAirPrint`/`airprintMatch` covers + the driver-choice gate. Verify on the client; fall back to an Avahi service + file if needed. +- [Some client relies on the `_cups` sub-type to identify the server] → CUPS' + `dnssd` backend uses the TXT `printer-type`/`cups_shared` marker for that, not + the sub-type, so the `/cups` URI is unchanged. Verify with `lpinfo -v` on a + CUPS client. +- [`cupsctl`'s rewrite of `/etc/cups/cupsd.conf` drops the directive] → + `cupsAdminSetServerSettings()` copies unrecognised directives through + verbatim (`cups/adminutil.c`), so `BrowseDNSSDSubTypes` survives the + `cupsctl --share-printers --remote-admin` the entrypoint runs after cupsd has + started. Confirmed by reading the write loop, not by a live run. +- [macOS caches the previous classification for an existing printer entry] → + Nothing the server can do; the printer has to be removed and added again. It + is documented in the README. + +## Migration Plan + +Rebuild and redeploy the image; no volume reset. `cupsd` re-registers the +queue's services on start and drops the `_cups` sub-type, so the change takes +effect immediately (mDNS caching on clients aside). Rollback is redeploying the +previous image; the extra sub-type reappears and macOS falls back to the generic +driver as before. diff --git a/openspec/changes/fix-airprint-discovery/proposal.md b/openspec/changes/fix-airprint-discovery/proposal.md new file mode 100644 index 0000000..2fa3baf --- /dev/null +++ b/openspec/changes/fix-airprint-discovery/proposal.md @@ -0,0 +1,68 @@ +# Proposal + +## Why + +macOS adds the queue with *Use:* = **Generic PostScript Printer** instead of the +driverless **AirPrint Printer**, so duplex, colour and tray 2 stay hidden — the +generic PPD gates duplex behind `APOptionalDuplexer False`. The certificate is +no longer the cause: `curl https://podp.local:631/` succeeds, CUPS serves the +expected certificate, and every IPP query the Mac makes against the queue +succeeds (7/7 `HTTP_STATUS_OK, ipp status 0`). iOS, which queries the printer +live, already offers every option. + +The remaining cause is a Bonjour sub-type. Disassembling +`AddPrinter.app/Contents/Frameworks/PMBrowser.framework` (macOS 27.0.1) shows the +gate for the AirPrint driver entry: + +- `-[DNSSDPrinter isShared]` returns YES as soon as any of the printer's + registration types ends in `,_cups`; the registration types are the `regType` + strings the Bonjour browse callback reports. +- `-[DNSSDPrinter airprintMatch]` returns nothing when `isShared` is YES, and + only builds the AirPrint driver when `isAirPrint` is YES — that is, when the + service was browsed under `_ipp._tcp,_universal` or `_ipps._tcp,_universal`. +- `-[DNSSDBrowser startBrowsing]` browses `_ipp._tcp,_cups` **and** + `_ipp._tcp,_universal` (and the `_ipps` equivalents), so a printer that + carries both sub-types is classified as *shared* and never gets the AirPrint + driver. + +CUPS registers `DNSSDSubTypes`, whose default is `_cups,_print,_universal` +(`scheduler/conf.c`), which is exactly that combination. The queue is therefore +advertised as a shared CUPS printer and macOS falls back to its generic driver. +The sub-types are configurable with the `BrowseDNSSDSubTypes` directive. + +## What Changes + +- Set `BrowseDNSSDSubTypes _print,_universal` in `cupsd.conf`, so the queue is + advertised with the AirPrint (`_universal`) sub-type and without the CUPS + (`_cups`) one. `_print` is kept for clients that look for it. +- Keep CUPS' own advertising otherwise (the unconditional `_printer._tcp` + registration, the `printer-type` TXT key and the resulting `/cups` device + URI), so CUPS clients keep resolving the queue exactly as before. +- Correct the README: the "macOS adds the printer as a Generic PostScript + printer" section blames the certificate alone and recommends adding the queue + from the command line; it now documents the sub-type cause first and how to + confirm what is advertised. + +Non-goals: hand-written Avahi service files, and patching CUPS' TXT record. +Neither is needed unless the sub-type alone turns out to be insufficient. + +## Capabilities + +### New Capabilities + + +### Modified Capabilities +- `printserver/cups`: the "Printer is shared and discoverable" requirement gains + a scenario that fixes which Bonjour sub-types are advertised, because the + advertised sub-types decide whether macOS offers the driverless AirPrint + driver. + +## Impact + +- `cupsd.conf`: one directive plus an explanatory comment. +- `README.md`: troubleshooting section corrected. +- `openspec/specs/printserver/cups/spec.md`: one scenario added. +- Clients: macOS should offer *AirPrint Printer* under *Use:*; the `_ipp`/`_ipps` + services, the `/cups` device URI and Linux/Windows clients are unaffected. + macOS caches the old classification per printer entry, so an existing entry + has to be removed and added again. diff --git a/openspec/changes/fix-airprint-discovery/specs/printserver/cups/spec.md b/openspec/changes/fix-airprint-discovery/specs/printserver/cups/spec.md new file mode 100644 index 0000000..9775f3a --- /dev/null +++ b/openspec/changes/fix-airprint-discovery/specs/printserver/cups/spec.md @@ -0,0 +1,19 @@ +# Spec Delta + +## MODIFIED Requirements + +### Requirement: Printer is shared and discoverable +The system SHALL share the queue over IPP and advertise it via AirPrint +(mDNS/DNS-SD). + +#### Scenario: IPP printer is shared +- **WHEN** CUPS runs in the container +- **THEN** the printer is shared via IPP for network clients + +#### Scenario: AirPrint discovery +- **WHEN** Avahi runs alongside CUPS +- **THEN** the printer is advertised via AirPrint with URF, sides and colour capabilities so macOS and iOS/iPadOS clients can discover it without a driver + +#### Scenario: Advertised DNS-SD sub-types +- **WHEN** the shared queue is advertised over mDNS +- **THEN** its `_ipp._tcp` and `_ipps._tcp` registrations carry the `_print` and `_universal` sub-types and carry no `_cups` sub-type, so a client that classifies a printer carrying `_cups` as a shared CUPS printer — macOS, in its Add Printer window — still offers its driverless AirPrint driver diff --git a/openspec/changes/fix-airprint-discovery/tasks.md b/openspec/changes/fix-airprint-discovery/tasks.md new file mode 100644 index 0000000..25c04c4 --- /dev/null +++ b/openspec/changes/fix-airprint-discovery/tasks.md @@ -0,0 +1,18 @@ +# Tasks + +## 1. Advertise the AirPrint sub-type without the CUPS sub-type + +- [x] 1.1 Add `BrowseDNSSDSubTypes _print,_universal` to `cupsd.conf` with the reason (macOS treats a printer carrying `_cups` as shared and then withholds its AirPrint driver), and verify the directive name, its default and the way it forms the registered sub-type list against the CUPS 2.4 sources (`scheduler/conf.c`, `scheduler/dirsvc.c`) +- [x] 1.2 Correct the README "macOS adds the printer as a Generic PostScript printer" section so it names the sub-type cause first, keeps the certificate as the second cause, and shows commands that actually report the advertisement; verify the documented `avahi-browse` commands are valid invocations of the installed Avahi client +- [ ] 1.3 Rebuild and redeploy the image on the podman host, then verify the advertisement: `podman exec systemd-print-server avahi-browse -rt _ipp._tcp,_universal` lists the queue and `avahi-browse -rt _ipp._tcp,_cups` does not list it (other CUPS servers on the network may appear there) +- [ ] 1.4 Verify nothing else regressed: `ipptool -tv ipp://podp.local:631/printers/` and the `ipps://` variant report ipp status 0, and `lpinfo -v` on a CUPS client still resolves the queue to a `dnssd://…/cups?uuid=…` URI + +## 2. Confirm macOS offers the driverless driver + +- [ ] 2.1 On the Mac, remove the queue and add it again; verify the *Use:* popup offers **AirPrint Printer**, `/etc/cups/ppd/.ppd` contains `*APAirPrint: True` and `lpoptions -p -l` lists the duplex, colour and tray-2 options that the generic PPD hid + +## Workflow follow-up + +- Archive this change (`/opsx-archive`) so the delta merges into `openspec/specs/printserver/cups/spec.md`. +- Archive the still-open `fix-ipps-certificate-lookup` change once its live verification task is done. +- Commit the implementation together with the archived changes using `jj`.