From 3910ab974c30f24dac534baa201e0083033641ca Mon Sep 17 00:00:00 2001 From: Juergen Holly Date: Tue, 20 Oct 2026 10:05:00 +0200 Subject: [PATCH] add warn on dnssd name collision change spec --- .../.openspec.yaml | 2 + .../warn-on-dnssd-name-collisions/design.md | 98 +++++++++++++++++++ .../warn-on-dnssd-name-collisions/proposal.md | 59 +++++++++++ .../specs/printserver/cups/spec.md | 47 +++++++++ .../warn-on-dnssd-name-collisions/tasks.md | 90 +++++++++++++++++ 5 files changed, 296 insertions(+) create mode 100644 openspec/changes/warn-on-dnssd-name-collisions/.openspec.yaml create mode 100644 openspec/changes/warn-on-dnssd-name-collisions/design.md create mode 100644 openspec/changes/warn-on-dnssd-name-collisions/proposal.md create mode 100644 openspec/changes/warn-on-dnssd-name-collisions/specs/printserver/cups/spec.md create mode 100644 openspec/changes/warn-on-dnssd-name-collisions/tasks.md diff --git a/openspec/changes/warn-on-dnssd-name-collisions/.openspec.yaml b/openspec/changes/warn-on-dnssd-name-collisions/.openspec.yaml new file mode 100644 index 0000000..9076a66 --- /dev/null +++ b/openspec/changes/warn-on-dnssd-name-collisions/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-20 diff --git a/openspec/changes/warn-on-dnssd-name-collisions/design.md b/openspec/changes/warn-on-dnssd-name-collisions/design.md new file mode 100644 index 0000000..90178d1 --- /dev/null +++ b/openspec/changes/warn-on-dnssd-name-collisions/design.md @@ -0,0 +1,98 @@ +# Design + +## Context + +`entrypoint.sh` starts D-Bus, Avahi and cupsd, configures the managed queue +(`lpadmin -D/-o/-E` on every start) and then verifies cupsd answers on TCP 631. +Any queue present in cupsd — including ones added manually via the web UI — is +advertised as the DNS-SD instance `" @ "` (queue name when no +Description is set). Avahi registers only one instance per name: with two +queues deriving the same name, one is silently never advertised, and CUPS 2.4 +does not rename on collision (upstream `scheduler/dnssd.c`, `TODO: +Handle collisions`). Clients that pinned the losing name (macOS stores it in +the `dnssd://` device URI) then report the printer offline. See proposal.md for +the incident. The container ships `cups-client` (guaranteed `lpstat`), +`avahi-utils` and `curl` (Containerfile). + +## Goals / Non-Goals + +**Goals:** +- At startup, detect when two or more queues would derive the same DNS-SD + instance name and log an actionable warning naming the queues. +- Keep the check strictly best-effort: it must never affect startup success or + queue configuration (consistent with the existing warning style under + `set -eu`). +- Give operators a documented way to confirm and fix the condition by hand + (README troubleshooting entry). + +**Non-Goals:** +- No automatic remediation (no renaming, no un-sharing, no deleting queues). +- No change to how/when `lpadmin` runs — the restart re-registration churn is + out of scope for this change. +- No client-side tooling; the check runs only inside the container. + +## Decisions + +**D1 — Derive names from live cupsd data, not from mDNS.** +The check compares *derived* instance names (`Description` or queue name per +queue) instead of listening to actual Avahi registrations. Derivation is +deterministic and race-free at startup; observing mDNS (`avahi-browse`) would +depend on registration timing and only detect collisions after the fact. +- *Alternative considered:* post-startup mDNS audit ("shared queue missing from + advertisements") — catches more failure modes but is timing-sensitive and + needs instance→queue mapping via TXT `rp`; rejected as the primary mechanism, + still documented as a manual verification step in the README. + +**D2 — Inputs: `LC_ALL=C lpstat -l -p` for names/Descriptions, `printers.conf` +for the shared flag.** +- Queue names + Descriptions come from `lpstat -l -p` run with `LC_ALL=C` so the + `Description:` line parses regardless of container locale (the live value is + authoritative — it is exactly what cupsd will register). +- Only shared queues are advertised, so the shared flag comes from + `/etc/cups/printers.conf` (`` block: `Shared yes`). When the flag + cannot be determined (file not yet flushed after this run's `lpadmin`, queue + missing from it), the queue is **treated as shared** — a collision warning + for a not-yet-shared queue is still actionable, whereas staying silent on a + shared one reproduces the original incident. +- *Alternative considered:* `ipptool` Get-Printer-Attributes — richer data, but + availability of `ipptool` in the image is not guaranteed by the Containerfile + package list; rejected to keep the check dependency-free beyond `cups-client`. + +**D3 — Placement and failure mode.** +A `check_dnssd_name_collisions` function runs after the queue configuration +blocks and the TCP reachability check, immediately before the final `ready` +log line, so the warning is the last thing an operator sees in `podman logs`. +It logs through the existing `log()` helper (`[entrypoint] warning: …` +followed by indented detail lines, mirroring the TCP-631 error block) and never +exits non-zero; every parse failure degrades to "check skipped" (the script +runs under `set -eu`, so all command substitutions are guarded with `|| true`). + +**D4 — Collision = exact match on the name base.** +The `@ ` suffix is identical for every queue on the server, so equality +is decided on the base alone; the full derived name (base + `@ `) +is still printed in the warning so it matches what `dns-sd -B` shows. +The remedy text offers the two fixes that worked on the live system: give each +queue a unique Description (`lpadmin -D`), or remove the redundant queue +(`lpadmin -x`), followed by remove/re-add of the client queue (README details +why clients must be re-added). + +## Risks / Trade-offs + +- [Queues created *after* startup are not re-checked] → acceptable: the check + runs on every container start, and the README documents a manual + `dns-sd -B _ipp._tcp` verification for live changes. +- [Stale shared flag if `printers.conf` is not flushed in time] → mitigated by + assuming shared when unknown (errs toward warning), see D2. +- [False warning when a shared flag parse misreads a queue block] → parsing is + scoped to `` … `` blocks with exact keyword matches; + a misread can at worst add a queue to the comparison, and the warning is + advisory only. +- [Warning becomes noise on intentionally duplicated setups] → duplication is + never functional in CUPS/Avahi (one registrant always loses), so there is no + legitimate configuration the warning would nag about. + +## Migration Plan + +Log-output-only change: deploy by rebuilding the image and restarting the +container (normal quadlet cycle). No state, config or client impact; rollback +is reverting `entrypoint.sh`/README. diff --git a/openspec/changes/warn-on-dnssd-name-collisions/proposal.md b/openspec/changes/warn-on-dnssd-name-collisions/proposal.md new file mode 100644 index 0000000..d97a2cd --- /dev/null +++ b/openspec/changes/warn-on-dnssd-name-collisions/proposal.md @@ -0,0 +1,59 @@ +# Proposal + +## Why + +On the live deployment (`podp`), two queues ended up with the identical CUPS +Description, so both derived the same DNS-SD instance name +(`" @ "`). Avahi can register only one of them and CUPS 2.4 +never resolves the collision (upstream `scheduler/dnssd.c` still has +`/* TODO: Handle collisions with avahi_alternate_service_name */`), so one queue +showed `printer-is-shared=true` but was never advertised. macOS pins the exact +instance name into the queue's `dnssd://` device URI; when the pinned name stops +being announced, the Mac reports the printer **offline**. Because the collision +is deterministic, container restarts never helped and only remove/re-add on the +client fixed it — hours of debugging with no server-side signal pointing at the +cause. + +## What Changes + +- **`entrypoint.sh`**: after cupsd is up, derive the DNS-SD instance name of + every shared queue (`Description` if set, otherwise the queue name, plus + `@ `) and log a prominent, non-fatal warning when two or more queues + derive the same instance name — naming the affected queues and the fix + (give each queue a unique Description, or delete the redundant queue). +- **`README.md`**: new Troubleshooting entry "macOS shows the printer offline + after some time" documenting the symptom, root cause (client-side instance + name pinning + duplicate-Description collision), verification commands + (`dns-sd -B`, `lpstat -l -p`, `ipptool`), and the fix including the + remove/re-add step on macOS. +- **Docs correction**: the `PRINTER_NAME` description in the `entrypoint.sh` + header and in the README currently claims it is the "advertised (DNS-SD) + name" — that is only true while no Description is set; the Description + overrides it. Correct both places, and clarify the matching requirement in + the `printserver/cups` spec (same claim). + +Out of scope: changing how queues are configured (no automatic renaming or +de-duplication), and avoiding the `lpadmin` re-registration churn on restart. + +## Capabilities + +### New Capabilities + +(none) + +### Modified Capabilities + +- `printserver/cups`: adds a requirement that the container detects DNS-SD + instance-name collisions between shared queues at startup and reports them in + the container log, so a queue that silently loses its advertisement is visible + to the operator instead of surfacing as an "offline" client hours later. + +## Impact + +- `entrypoint.sh` — one additional startup diagnostic block (log output only; + no change to queue creation, advertisement, or exit behaviour). +- `README.md` — one new Troubleshooting section; correction of the + `PRINTER_NAME` wording. +- No changes to `cupsd.conf`, the `Containerfile`, or runtime behaviour seen by + clients; the fix for an existing collision is still a manual operator action + (documented in the README). diff --git a/openspec/changes/warn-on-dnssd-name-collisions/specs/printserver/cups/spec.md b/openspec/changes/warn-on-dnssd-name-collisions/specs/printserver/cups/spec.md new file mode 100644 index 0000000..48787e6 --- /dev/null +++ b/openspec/changes/warn-on-dnssd-name-collisions/specs/printserver/cups/spec.md @@ -0,0 +1,47 @@ +# Spec Delta + +## ADDED Requirements + +### Requirement: DNS-SD name collisions are reported at startup +The container SHALL, once CUPS is running, derive the DNS-SD instance name of +every shared queue — its Description if set, otherwise its queue name, suffixed +with `@ ` — and SHALL log a warning when two or more queues derive the +same instance name. The warning SHALL name the affected queues and the remedy, +and SHALL NOT prevent the container from starting. + +#### Scenario: Two queues derive the same instance name +- **WHEN** two shared queues have Descriptions (or queue names) that derive the same DNS-SD instance name +- **THEN** startup logs a warning naming the colliding instance name, both affected queues and the remedy (a unique Description per queue, or removing the redundant queue), and the container continues to start normally + +#### Scenario: A queue without a Description collides with another queue's Description +- **WHEN** one shared queue has no Description and would advertise its queue name while another shared queue's Description yields the same instance name +- **THEN** the collision is reported the same way, because both derive the same advertised name + +#### Scenario: All derived instance names are unique +- **WHEN** every shared queue derives a distinct DNS-SD instance name +- **THEN** no collision warning is logged and startup proceeds as before + +## MODIFIED Requirements + +### Requirement: Printer endpoint and advertised name are configurable at runtime +The container SHALL accept the network printer address and the advertised printer name through environment variables so the same image can target a different printer or name without rebuilding. + +#### Scenario: Printer IP is provided +- **WHEN** the container is started with `PRINTER_IP` set +- **THEN** a device URI is derived from that address and the Brother HL-4150CDN queue is created pointing at it + +#### Scenario: Printer protocol is selected +- **WHEN** the container is started with `PRINTER_IP` and `PRINTER_PROTOCOL` +- **THEN** the device URI uses that protocol (socket, lpd or ipp), defaulting to socket + +#### Scenario: Full URI override +- **WHEN** `PRINTER_URI` is set +- **THEN** it is used as the device URI and `PRINTER_IP` is ignored + +#### Scenario: Custom advertised name +- **WHEN** the container is started with `PRINTER_NAME` +- **THEN** the CUPS queue uses that name, and the advertised DNS-SD instance name (` @ `) is derived from it unless a Description is also set + +#### Scenario: Custom CUPS description +- **WHEN** the container is started with `PRINTER_DESCRIPTION` +- **THEN** the queue's CUPS Description (Info) is set to that value, and the advertised DNS-SD instance name is derived from the Description instead of the queue name diff --git a/openspec/changes/warn-on-dnssd-name-collisions/tasks.md b/openspec/changes/warn-on-dnssd-name-collisions/tasks.md new file mode 100644 index 0000000..09ab0bf --- /dev/null +++ b/openspec/changes/warn-on-dnssd-name-collisions/tasks.md @@ -0,0 +1,90 @@ +# Tasks + +## 1. entrypoint: Erkennung von DNS-SD-Namenskollisionen + +- [ ] 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 + cupsd), Shared-Flag je Queue aus `/etc/cups/printers.conf` + (``-Block, `Shared yes`); unbekannter Shared-Status zählt als + geteilt. Pro Queue den Anzeigenamen ableiten: Description, sonst + Queue-Name. + 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 + bestehenden `log()`-Stil warnen (Mehrzeiler, analog zum TCP-631-Block): + kollidierender Anzeigename ` @ `, betroffene Queue-Namen, + Abhilfe (einzigartige Description je Queue per `lpadmin -D` oder + redundante Queue entfernen per `lpadmin -x`, danach Client-Queue auf dem + Mac entfernen und neu hinzufügen — Details im README). Bei eindeutigen + Namen keine Warnung ausgeben. + 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 + (`|| true` / geblockte Substitutionen): fehlendes oder unlesbares + `printers.conf`, + fehlerhaftes `lpstat` → Prüfung still überspringen oder als geteilt + annehmen, niemals `exit` und niemals Abbruch der Queue-Einrichtung. + Verifikation: `sh -n entrypoint.sh` fehlerfrei; Startverhalten mit + fehlendem `printers.conf` ist in 3.4 abgedeckt. + +## 2. README: Troubleshooting und PRINTER_NAME-Korrektur + +- [ ] 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 + (macOS pinnt den exakten DNS-SD-Instanznamen `" @ "` + in die `dnssd://`-Device-URI; zwei Queues mit gleicher Description + kollidieren, Avahi registriert nur eine, CUPS 2.4 benennt nicht um), + Diagnose-Befehle (`podman logs` Warnung, `dns-sd -B _ipp._tcp`, + `avahi-browse -rt _ipp._tcp`, `lpstat -l -p`, `ipptool -tv`), Heilung + (eindeutige Description bzw. `-x`, dann Remove/Re-add auf macOS — mit dem + 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 + 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 + Queue-Name ist nur die Basis, solange keine Description gesetzt ist — + `PRINTER_DESCRIPTION` (bzw. `-D`) hat Vorrang für den Anzeigenamen. + Verifikation: `grep -n 'PRINTER_NAME' README.md entrypoint.sh` zeigt an + allen drei Stellen die korrigierte Formulierung mit Description-Vorrang. + +## 3. Integrationstests (podman, lokal) + +- [ ] 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. + 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. + 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. + 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 + (`podman exec … cp /tmp/pc.bak /etc/cups/printers.conf`) bzw. den + Testcontainer verwerfen. + +## Workflow follow-up + +- README-Abschnitt und Warnung gegen die echte Konstellation auf `podp` + prüfen: nach Deploy (Image neu bauen, Quadlet-Restart) muss die Warnung für + `laser`/`laserdrucker-im-buero` erscheinen, bis die Descriptions eindeutig + gemacht oder die redundante Queue entfernt ist — danach muss sie weg sein. +- Nach Review und grünem Durchlauf die Änderung mit `/opsx-archive` + archivieren und das Ergebnis prüfen.