From f15ea07f44c53a58826ed2c42efd0d27d2099f14 Mon Sep 17 00:00:00 2001 From: Juergen Holly Date: Tue, 20 Oct 2026 10:05:52 +0200 Subject: [PATCH] implement warn on dnssd name collisions --- README.md | 72 ++++++++++++++-- entrypoint.sh | 85 ++++++++++++++++++- .../warn-on-dnssd-name-collisions/tasks.md | 18 ++-- 3 files changed, 159 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 0111f5a..c64027f 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ podman run -d --name printserver \ | `PRINTER_IP` | *(empty)* | IP address / host name of the network printer. A device URI is derived from it (see `PRINTER_PROTOCOL`). | | `PRINTER_URI` | *(empty)* | Full device URI. Overrides `PRINTER_IP` when set. | | `PRINTER_PROTOCOL` | `socket` | Protocol used with `PRINTER_IP`: `socket`, `lpd` or `ipp`. | -| `PRINTER_NAME` | `HL4150CDN` | CUPS queue name. Also used as the name advertised via AirPrint/DNS-SD. | +| `PRINTER_NAME` | `HL4150CDN` | CUPS queue name. Also the base of the advertised AirPrint/DNS-SD name (` @ `) — while no Description is set; `PRINTER_DESCRIPTION` overrides the advertised name. | | `PRINTER_DESCRIPTION` | *(empty)* | CUPS printer description (`lpadmin -D`), shown in the CUPS web UI. | | `PPD_FILE` | `/usr/share/cups/model/BR4150_2.PPD` | PPD used for the queue. | | `PRINTER_OPTIONS` | *(empty)* | Space-separated PPD option assignments (`key=value`) applied as queue defaults on every start, e.g. `BRColorMode=Vivid TonerSaveMode=False`. Discover the names with `lpoptions -p -l`. | @@ -130,14 +130,21 @@ Or specify the URI explicitly for full control: ### Advertised name -`PRINTER_NAME` is used as the CUPS queue name and as the name clients see in -AirPrint discovery. To show a descriptive label in the CUPS web interface, -set `PRINTER_DESCRIPTION` (this does not change the advertised name): +`PRINTER_NAME` is the CUPS queue name. It is also the base of the name clients +see in AirPrint discovery — CUPS advertises `" @ "` — but only as +long as no Description is set: `PRINTER_DESCRIPTION` (the `lpadmin -D` +Description) has precedence and becomes the advertised name instead: ```sh -e PRINTER_NAME=HL4150CDN -e PRINTER_DESCRIPTION="Brother HL-4150CDN (Office)" ``` +advertises the DNS-SD instance `Brother HL-4150CDN (Office) @ `, not +`HL4150CDN @ `. That instance name is what clients pin into their +`dnssd://` device URI, so every queue needs a **unique** Description: two +queues deriving the same name collide, and only one of them is ever advertised +(see *macOS shows the printer offline after some time* under Troubleshooting). + ### TLS certificate for `_ipps` CUPS advertises the queue twice over mDNS: `_ipp._tcp` (plain) and `_ipps._tcp` @@ -433,7 +440,7 @@ podman cp printserver:/etc/cups/ssl/server.crt . && ``` 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/` +over the same secure service (`ipptool -tv ipp://:631/printers/ ` prints the UUID; drop the `urn:uuid:` prefix): ```sh @@ -482,6 +489,61 @@ greyscale. Forcing Ghostscript with `-o pdftops-renderer=gs` would, but that path is not recommended for Brother printers (bugs in their PostScript interpreters). +### macOS shows the printer offline after some time + +Symptom: the printer was added and printed fine, then macOS reports it +*offline* ("printer is not responding") some hours later — typically after the +container was restarted or after queues were edited on the server. Restarting +the container does not help; removing the printer on the Mac and adding it +again fixes it — until it happens again. + +Cause: when macOS adds a Bonjour printer it pins the exact DNS-SD **instance +name** into the queue's device URI: + +```text +dnssd://Laserdrucker%20HL-4150CDN%20%40%20podp._ipps._tcp.local./?uuid=… + └── " @ " — must still resolve hours later +``` + +CUPS derives that name from the **Description**, not from the CUPS queue name +(the queue name is only the base while no Description is set). Two queues with +the same Description therefore derive the same instance name. Avahi can +register only one service per name, and CUPS 2.4 does not rename on collision +(upstream `scheduler/dnssd.c` still carries a `TODO` for it), so the losing +queue stays *Shared* but is never advertised. The Mac keeps resolving the name +that no longer exists and flips the queue to offline. The lost race is +deterministic — the same queue wins after every restart — which is why +restarting the container never helps, while remove/re-add re-pins the Mac to +the instance that is actually advertised. + +The entrypoint logs a warning at startup when it finds such a collision. To +check by hand: + +```sh +podman logs printserver 2>&1 | grep 'DNS-SD name' # entrypoint warning, if any +lpstat -v | grep dnssd # the instance name the Mac pinned +podman exec printserver lpstat -l -p # duplicate Descriptions? +dns-sd -B _ipp._tcp # one instance per name? +podman exec printserver avahi-browse -rt _ipps._tcp # what is really advertised +ipptool -tv ipp://:631/printers/ \ + /usr/share/cups/ipptool/get-printer-attributes.test # server side itself +``` + +Two queues showing the same `Description:` while `dns-sd -B` lists only one +instance is the collision. Fix it on the server: give each queue a unique +Description, or remove the redundant queue: + +```sh +lpadmin -h :631 -p laserdrucker-im-buero -D "Laserdrucker Buero" +# or drop the duplicate entirely: +lpadmin -h :631 -x laserdrucker-im-buero +``` + +Then remove the printer on the Mac and add it again — macOS re-pins the device +URI to the instance that is advertised now. The same remove/re-add is needed +after **every** Description change: the pinned name changes with it, and a +container restart alone cannot bring the old name back. + ## CUPS 3 and the future of PPDs Adding the queue with a PPD makes `lpadmin` print: diff --git a/entrypoint.sh b/entrypoint.sh index e47c1f6..aaec7b5 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -15,8 +15,11 @@ # PRINTER_IP IP address / host name of the network printer. # PRINTER_URI Full device URI; overrides PRINTER_IP when set. # PRINTER_PROTOCOL socket (default), lpd or ipp - used with PRINTER_IP. -# PRINTER_NAME CUPS queue name and advertised (DNS-SD) name. -# PRINTER_DESCRIPTION CUPS Description (lpadmin -D) shown in the web UI. +# PRINTER_NAME CUPS queue name; base of the advertised (DNS-SD) +# name " @ " while no Description is set. +# PRINTER_DESCRIPTION CUPS Description (lpadmin -D), shown in the web UI; +# takes precedence over PRINTER_NAME as the advertised +# (DNS-SD) name. # PPD_FILE PPD used for the queue. # PRINTER_OPTIONS Space-separated PPD option assignments (key=value) # applied as queue defaults on every start, e.g. @@ -400,6 +403,84 @@ if ! curl -fsS --noproxy '*' --max-time 2 -o /dev/null http://127.0.0.1:631/ 2>/ exit 1 fi +# --- DNS-SD name collision check -------------------------------------------- +# CUPS advertises every shared queue as the DNS-SD instance +# " @ " - the queue name when no Description is set. +# Avahi can register only one instance per name, and CUPS 2.4 does not rename +# on collision (scheduler/dnssd.c: "TODO: Handle collisions with +# avahi_alternate_service_name"), so with duplicate derived names one queue is +# silently never advertised. Clients that pinned the missing name - macOS +# stores the exact instance name in the queue's dnssd:// device URI - then +# report the printer offline. Detection only; a collision is never fatal +# (see README, "macOS shows the printer offline after some time"). +check_dnssd_name_collisions() { + # Queue names and Descriptions straight from cupsd, forced to the C locale + # so the "Description:" detail line parses regardless of the container + # locale. An empty or failed result skips the check silently. + lpstat_out="$(LC_ALL=C lpstat -l -p 2>/dev/null || true)" + [ -n "${lpstat_out}" ] || return 0 + + # One "basequeue1, queue2" line per colliding name. The Shared flags + # come from printers.conf (both and blocks, + # "Shared Yes/No"); a queue that cannot be read there (file not flushed + # after this run's lpadmin, no Shared line yet) counts as shared: a false + # warning is actionable, a missed one reproduces the incident. Every parse + # failure degrades to "no collision found" so that set -eu never trips + # inside this diagnostic. + collisions="$(printf '%s\n' "${lpstat_out}" | awk -v conf=/etc/cups/printers.conf ' + BEGIN { + while ((getline line < conf) > 0) { + if (line ~ /^<(Default)?Printer /) { + cur = line + sub(/^<(Default)?Printer /, "", cur) + sub(/>$/, "", cur) + } else if (line ~ /^<\/(Default)?Printer>$/) { + cur = "" + } else if (cur != "" && line ~ /^Shared /) { + flag[cur] = tolower(substr(line, 8)) + } + } + close(conf) + } + /^[ \t]*printer / { + curq = $2 + order[++n] = curq + next + } + /^[ \t]*Description: / { + sub(/^[ \t]*Description: /, "") + if (curq != "") desc[curq] = $0 + } + END { + for (i = 1; i <= n; i++) { + q = order[i] + if ((q in flag) && flag[q] == "no") continue + base = ((q in desc) && desc[q] != "") ? desc[q] : q + count[base]++ + list[base] = list[base] (list[base] == "" ? "" : ", ") q + } + for (b in count) + if (count[b] > 1) print b "\t" list[b] + }' || true)" + [ -n "${collisions}" ] || return 0 + + dnssd_host=" @ $(hostname -s 2>/dev/null || hostname 2>/dev/null || printf host)" + printf "%s\n" "${collisions}" \ + | while IFS="$(printf '\t')" read -r base queues; do + log "warning: queues ${queues} would all be advertised as the DNS-SD name" + log " '${base}${dnssd_host}' - Avahi keeps only one of them, so every" + log " other queue is silently NOT advertised, and clients pinned to the" + log " missing name (macOS dnssd:// device URI) show the printer offline." + log " fix: give each queue a unique Description (lpadmin -p -D \"...\")" + log " or remove the redundant queue (lpadmin -x ); then remove" + log " and re-add the printer on the client - see README, \"macOS shows" + log " the printer offline after some time\"." + done + return 0 +} + +check_dnssd_name_collisions + log "ready: CUPS on 631/tcp, mDNS on 5353/udp" # Keep the container running by waiting on cupsd. diff --git a/openspec/changes/warn-on-dnssd-name-collisions/tasks.md b/openspec/changes/warn-on-dnssd-name-collisions/tasks.md index 09ab0bf..8be9f02 100644 --- a/openspec/changes/warn-on-dnssd-name-collisions/tasks.md +++ b/openspec/changes/warn-on-dnssd-name-collisions/tasks.md @@ -2,7 +2,7 @@ ## 1. entrypoint: Erkennung von DNS-SD-Namenskollisionen -- [ ] 1.1 `entrypoint.sh`: Funktion `check_dnssd_name_collisions` ergänzen, +- [x] 1.1 `entrypoint.sh`: Funktion `check_dnssd_name_collisions` ergänzen, aufgerufen nach dem TCP-631-Check und vor `log "ready: CUPS on 631/tcp, mDNS on 5353/udp"`. Queue-Namen und Descriptions via `LC_ALL=C lpstat -l -p` lesen (Locale-fest, live aus @@ -13,7 +13,7 @@ Verifikation: `sh -n entrypoint.sh` läuft fehlerfrei, `grep -n 'check_dnssd_name_collisions' entrypoint.sh` zeigt Definition und Aufruf an der vorgesehenen Stelle. -- [ ] 1.2 Bei gleichen abgeleiteten Namen pro Kollisionsgruppe über den +- [x] 1.2 Bei gleichen abgeleiteten Namen pro Kollisionsgruppe über den bestehenden `log()`-Stil warnen (Mehrzeiler, analog zum TCP-631-Block): kollidierender Anzeigename ` @ `, betroffene Queue-Namen, Abhilfe (einzigartige Description je Queue per `lpadmin -D` oder @@ -23,7 +23,7 @@ Verifikation: `grep -n 'warning:' entrypoint.sh` findet die neuen Warnzeilen inkl. `lpadmin -D`-Remedies; das Verhalten selbst prüfen die Aufgaben 3.2 (Kollision) und 3.3 (keine Kollision). -- [ ] 1.3 Alle Parseschritte abfangen, ohne dass `set -eu` den Start abbricht +- [x] 1.3 Alle Parseschritte abfangen, ohne dass `set -eu` den Start abbricht (`|| true` / geblockte Substitutionen): fehlendes oder unlesbares `printers.conf`, fehlerhaftes `lpstat` → Prüfung still überspringen oder als geteilt @@ -33,7 +33,7 @@ ## 2. README: Troubleshooting und PRINTER_NAME-Korrektur -- [ ] 2.1 `README.md`: neuen Troubleshooting-Abschnitt +- [x] 2.1 `README.md`: neuen Troubleshooting-Abschnitt „macOS shows the printer offline after some time" (englisch, Stil der bestehenden `###`-Abschnitte) einfügen: Symptom (Queue existiert, Status offline, erst nach Stunden/Wechseln sichtbar), Ursache @@ -46,7 +46,7 @@ Hinweis, warum Restart allein nicht hilft: Kollision ist deterministisch). Verifikation: jeder im Abschnitt stehende Befehl läuft unverändert gegen den laufenden Container (lokal aus 3.1 bzw. gegen podp). -- [ ] 2.2 `PRINTER_NAME`-Wording korrigieren: README-Tabelle („Also used as +- [x] 2.2 `PRINTER_NAME`-Wording korrigieren: README-Tabelle („Also used as the name advertised via AirPrint/DNS-SD") und Fließtext unter *Environment variables* sowie die Kopfkommentarzeile in `entrypoint.sh` („advertised (DNS-SD) name") präzisieren: Der @@ -57,23 +57,23 @@ ## 3. Integrationstests (podman, lokal) -- [ ] 3.1 Image bauen und ohne Kollision starten. +- [x] 3.1 Image bauen und ohne Kollision starten. Verifikation: `podman build -t printserver:latest .` endet ohne Fehler; `podman run …` erreicht „ready: CUPS on 631/tcp, mDNS on 5353/udp"; `podman logs` enthält KEINE Kollisions-Warnung (falls 631 nicht bindet: `podman machine ssh -- sudo sysctl -w net.ipv4.ip_unprivileged_port_start=631` wie bei der Vorgängeränderung). -- [ ] 3.2 Kollision erzeugen und Warnung nachweisen. +- [x] 3.2 Kollision erzeugen und Warnung nachweisen. Verifikation: zweite Queue mit der Description des ersten Queues anlegen (`podman exec printserver lpadmin -p dup -E -v socket://192.0.2.1:9100 -D ""`), danach `podman restart printserver`: `podman logs` enthält die Warnung mit Anzeigename, beiden Queue-Namen und Remedie, und der Start endet trotzdem mit „ready" (Warnung nicht fatal). -- [ ] 3.3 Kollision beheben → Warnung verschwindet. +- [x] 3.3 Kollision beheben → Warnung verschwindet. Verifikation: `podman exec printserver lpadmin -p dup -D "Dup Queue"` (eindeutige Description), `podman restart printserver`: das Log enthält keine Kollisions-Warnung mehr und endet mit „ready". -- [ ] 3.4 Fehlendes `printers.conf` darf den Start nicht brechen. +- [x] 3.4 Fehlendes `printers.conf` darf den Start nicht brechen. Verifikation: `podman exec printserver sh -c 'cp /etc/cups/printers.conf /tmp/pc.bak && rm /etc/cups/printers.conf'` danach `podman restart printserver`: der Container erreicht „ready"; anschließend Status wiederherstellen