fix airprint discovery
ci/woodpecker/push/build Pipeline was successful

This commit is contained in:
juergen committed 2026-10-19 22:44:08 +02:00
1 parent 50497780c7
commit 843c6866a5
7 files changed
+252 -5

No files matched your search

+30 -5
View File
@@ -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 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 secure `_ipps` service; if that fails it silently falls back to its bundled
generic PostScript driver. Duplex then stays hidden, because the generic PPD generic PostScript driver. Duplex then stays hidden, because the generic PPD
gates it behind `APOptionalDuplexer False`. The usual cause is the certificate gates it behind `APOptionalDuplexer False`. There are two causes; check the
(see *TLS certificate for `_ipps`*), so check that first: 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 ```sh
podman exec printserver avahi-browse -rt _ipps._tcp # what is advertised 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 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://<host>:631/printers/<queue>` over the same secure service (`ipptool -tv ipp://<host>:631/printers/<queue>`
prints the UUID; drop the `urn:uuid:` prefix): prints the UUID; drop the `urn:uuid:` prefix):
@@ -356,8 +379,10 @@ lpoptions -p <queue> -l # Duplex, ColorModel and tray 2 show up now
In the GUI, remove the printer and add it again, then pick **AirPrint** (not 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, *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 either the `_cups` sub-type is still being advertised or the secure connection is
queue from the command line as above. 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 ## CUPS 3 and the future of PPDs
+12
View File
@@ -20,6 +20,18 @@ Browsing On
BrowseLocalProtocols dnssd BrowseLocalProtocols dnssd
BrowseWebIF Yes 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... # Default authentication type, when authentication is required...
DefaultAuthType Basic DefaultAuthType Basic
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-19
@@ -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 `<type>,<subtypes>` (`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.
@@ -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
<!-- None: this fixes the advertisement required by an existing capability. -->
### 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.
@@ -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
@@ -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/<queue>` 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/<queue>.ppd` contains `*APAirPrint: True` and `lpoptions -p <queue> -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`.