Files
printserver/README.md
T
juergen 50497780c7
ci/woodpecker/push/build Pipeline was successful
fix ipps certificate lookup
2026-10-19 20:39:02 +02:00

396 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Printserver
A containerized CUPS print server that shares a **Brother HL-4150CDN** network
printer over IPP and advertises it via AirPrint (mDNS/DNS-SD). It prints by
sending **PostScript (BR-Script3)** straight to the printer, so no vendor driver
and no 32-bit libraries run in the container.
The container bundles:
- CUPS (IPP + web admin on port 631) with cups-filters for format conversion
- Avahi / DNS-SD for AirPrint discovery (UDP 5353)
- Brother's official HL-4150CDN **BR-Script3 PPD** for device options
## How it works
The HL-4150CDN emulates PCL6 and BR-Script3 (PostScript 3), and accepts
PostScript over raw AppSocket (port 9100). CUPS converts whatever a client
sends into PostScript and the BR-Script3 PPD turns the selected queue options
into PostScript `setpagedevice` instructions:
```
macOS / iOS / Windows / Linux container printer
┌─────────────────┐ IPP/AirPrint ┌──────────────────────────┐ raw ┌────────────┐
│ AirPrint (URF) │ ───────────────▶ │ CUPS │ ──────▶ │ HL-4150CDN │
│ PDF / PostScript│ │ URF/PWG/PDF → PostScript│ :9100 │ BR-Script3 │
└─────────────────┘ │ pstops → setpagedevice │ └────────────┘
└──────────────────────────┘
```
For example, `Duplex=DuplexNoTumble BRPrintQuality=Color InputSlot=Tray2`
becomes:
```postscript
<</Duplex true /Tumble false>>setpagedevice
<</BRProcessColor 4 /BRHrc 0>>setpagedevice
<</ManualFeed false /BRTraysw false /BRFeeder 1>> setpagedevice
```
## Layout
| Path | Purpose |
| --- | --- |
| `Containerfile` | Image definition (Debian base, CUPS + cups-filters + Avahi, BR-Script3 PPD) |
| `cupsd.conf` | Container CUPS configuration (listen on all interfaces, share printers) |
| `entrypoint.sh` | Starts D-Bus, Avahi and CUPS, generates the `_ipps` certificate, then registers the printer queue |
| `assets/Brother HL-4150CDN/ppd/` | Official BR-Script3 PPD used for the queue |
| `assets/Brother HL-4150CDN/Windows/` | Vendor Windows driver the PPD was extracted from |
## Prerequisites
- Podman (or a compatible builder such as Docker/Buildah)
- The printer reachable on the network, with a known IP address or hostname
## Build
Run from the repository root:
```sh
podman build -t printserver:latest .
```
The image is architecture-independent (no i386 or other 32-bit binaries), so it
builds natively on x86_64 and arm64 hosts alike.
## Run
mDNS does not work through a normal container network, so use **host
networking**:
```sh
podman run -d --name printserver \
--network host \
-e PRINTER_IP=192.168.100.7 \
-e PRINTER_NAME=HL-4150CDN \
printserver:latest
```
### Environment variables
| Variable | Default | Description |
| --- | --- | --- |
| `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_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. |
| `CERT_EXTRA_NAMES` | *(empty)* | Comma-separated extra DNS names and/or IP addresses to put into the generated `_ipps` certificate. |
| `CERT_DAYS` | `3650` | Validity of the generated certificate, in days. |
No queue is created when neither `PRINTER_IP` nor `PRINTER_URI` is set.
### Choosing the connection
The HL-4150CDN accepts PostScript directly, so the default is raw AppSocket.
Set `PRINTER_IP` and let the entrypoint build the URI:
| `PRINTER_PROTOCOL` | Resulting URI |
| --- | --- |
| `socket` (default) | `socket://<ip>:9100` — raw AppSocket/JetDirect |
| `lpd` | `lpd://<ip>/binary_p1` — LPR/LPD |
| `ipp` | `ipp://<ip>/ipp/print` |
For example, a printer at `192.168.100.7`:
```sh
-e PRINTER_IP=192.168.100.7
```
Or specify the URI explicitly for full control:
```sh
-e PRINTER_URI=socket://192.168.100.7:9100
```
### 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):
```sh
-e PRINTER_NAME=HL4150CDN -e PRINTER_DESCRIPTION="Brother HL-4150CDN (Office)"
```
### TLS certificate for `_ipps`
CUPS advertises the queue twice over mDNS: `_ipp._tcp` (plain) and `_ipps._tcp`
(TLS). macOS and iOS prefer the secure one, and *validate the certificate
against the name they connected to*. The name clients see is Avahi's
`<host>.local`, which is not necessarily the container's host name — so the
entrypoint installs its own self-signed certificate instead of leaving it to
CUPS. Its SANs cover the container host name, `<host name>.local`, Avahi's
FQDN, `localhost`, `127.0.0.1` and everything in `CERT_EXTRA_NAMES`:
```sh
-e CERT_EXTRA_NAMES=192.168.100.20,print.example.org
```
The certificate lives in `/etc/cups/ssl` (so that directory must be writable:
use a named volume, see the bind-mount caveat under *Notes and limitations*)
and is only regenerated when one of the names it must cover is missing, so a
restart normally logs `TLS certificate is up to date`.
It is also copied — as `<name>.crt`/`<name>.key` — for every name CUPS may look
it up by, because CUPS 2.4 does not choose the certificate by the name the client
requested: it reverse-resolves the *local* address the client connected to and
loads `<name>.crt` from `/etc/cups/ssl`. A host-networked container resolves its
own LAN address to the name Podman writes into `/etc/hosts`
(`host.containers.internal`), and IPv6 literals are used as-is, so those names
need a copy too. Otherwise CUPS mints a certificate of its own for the address,
whose SANs do not cover the advertised `.local` name.
Inspect what the server actually presents:
```sh
podman cp printserver:/etc/cups/ssl/server.crt .
openssl x509 -in server.crt -noout -subject -ext subjectAltName
echo | openssl s_client -connect localhost:631 -servername "$(hostname).local" \
| openssl x509 -noout -subject -ext subjectAltName
```
Because the certificate is self-signed, a fully validating client (macOS/iOS
AirPrint, `curl` without `-k`) rejects it until you trust it. CUPS' own tools
(`lpadmin`, `ipptool`) are lenient, so plain printing works without this step:
- **macOS** (needs your password):
```sh
sudo security add-trusted-cert -d -r trustRoot \
-k /Library/Keychains/System.keychain ./server.crt
```
- **iOS/iPadOS**: AirDrop or mail `server.crt` to the device, install the
profile, then enable full trust under *Settings → General → About →
Certificate Trust Settings*.
Alternatively, keep using plain IPP (port 631 without TLS) — printing and
duplex/colour options work there too.
## Printing options
The BR-Script3 PPD exposes the HL-4150CDN's hardware options. Common ones:
| Option | PPD keyword | Values |
| --- | --- | --- |
| Paper source | `InputSlot` | `AutoSelect`, `Tray1`, `Tray2`, `MPTray` |
| Two-sided printing | `Duplex` | `None`, `DuplexNoTumble` (long edge), `DuplexTumble` (short edge) |
| Colour / mono | `BRPrintQuality` | `Auto`, `Color`, `Black` |
| Colour mode | `BRColorMode` | `Normal`, `Vivid`, `None` |
| Media type | `BRMediaType` | `Plain`, `Thin`, `Thick`, `Recycled`, `Glossy`, `Label`, envelopes, … |
| Print quality | `CAPT` | `Fine` (600×600), `SuperFine` (2400 dpi class) |
| Toner save | `TonerSaveMode` | `True`, `False` |
| Secure print | `BRJobHold` | `None`, `Private` (+ `BRJobPIN`) |
They can be selected per job, e.g.:
```sh
podman exec printserver lp -d HL4150CDN \
-o Duplex=DuplexNoTumble -o BRPrintQuality=Color -o InputSlot=Tray1 report.pdf
```
## Client support
| Platform | Path | Notes |
| --- | --- | --- |
| macOS | AirPrint / IPP Everywhere | Discovered via mDNS; no driver needed. If *Use:* shows *Generic PostScript Printer*, see [Troubleshooting](#macos-adds-the-printer-as-a-generic-postscript-printer) |
| iOS / iPadOS | AirPrint | Discovered via mDNS; no driver needed |
| Linux | IPP / IPP Everywhere | Native or with the shared PPD |
| Windows | IPP or raw 9100 | Install Brother's **BR-Script3** Windows driver, pointed at the printer or the shared queue |
Windows uses a client-side Brother driver; the server does not share via SMB.
## Verify
Browse the CUPS web interface:
```sh
open http://localhost:631/ # printers
open http://localhost:631/admin # administration
```
From the host, or inside the container:
```sh
podman exec printserver lpstat -t # queues and status
podman exec printserver lpstat -v HL4150CDN # device URI
podman exec printserver lpoptions -p HL4150CDN -l # available options
podman exec printserver avahi-browse -rt _ipp._tcp # mDNS advertisement
```
A print job can be submitted with:
```sh
podman exec printserver lp -d HL4150CDN /etc/hostname
```
On macOS/iOS the shared queue should appear in the printer list via AirPrint.
## Notes and limitations
- **Host networking is required** for Avahi to send and receive mDNS traffic.
`-p 631:631` alone is enough for direct IPP access but not for AirPrint
discovery.
- **Printer state is not persisted by default.** The queue is recreated on every
start from `PRINTER_IP`/`PRINTER_URI`, so nothing has to survive a restart. To
keep CUPS state (extra queues, subscriptions) across container replacement,
mount a volume over `/etc/cups` — **use a named volume**:
```sh
-v printserver-cups:/etc/cups
```
Podman seeds a *new named volume* with the image's `/etc/cups`, so `cupsd.conf`
is present. A **bind mount to an empty host directory does not**: it hides the
image's configuration, `cupsd` then exits immediately (status 1, silently —
its default `ErrorLog` is syslog), the container stops, and the entrypoint's
`lpadmin` fails with a misleading
`lpadmin: Unable to connect to server: Bad file descriptor`. This is easiest
to hit from a quadlet that bind-mounts a share, e.g.
`Volume=/nfs/share/print-server/:/etc/cups/:Z`. If you must bind-mount, make
sure the directory already contains a `cupsd.conf`; also note that a read-only
share, NFS `root_squash` or SELinux relabeling can still prevent `cupsd` from
writing its state there. For a named volume in a quadlet, use
`Volume=printserver-cups:/etc/cups` (a bare name, no leading `/`).
- Colour handling is done by the printer's own PostScript RIP. Output may differ
slightly from Brother's host-side (rasterising) driver.
- The queue advertises AirPrint capabilities (`image/urf`, `image/pwg-raster`,
`sides-supported`, `color-supported`). macOS and iOS printing is converted by
cups-filters; verify end to end on your network.
## Troubleshooting
### The container runs but nothing answers on 631
The queue can be advertised over mDNS while nothing is actually listening, so
clients discover a printer they cannot reach. Check inside the container:
```sh
podman exec printserver curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:631/
podman exec printserver tail -20 /var/log/cups/error_log
```
**`Unable to open listen socket for address 0.0.0.0:631 - Permission denied`** —
`cupsd` never bound port 631. This is the normal outcome of **rootless Podman
with `Network=host`**: port 631 is privileged, and a rootless container does not
hold `CAP_NET_BIND_SERVICE` in the host network namespace (adding the capability
does not help, because the capability must belong to the *host* user namespace).
`cupsd` keeps running on its domain socket, so `lpadmin`/`lpstat` inside the
container still work — which makes the failure easy to miss. Fix one of:
- allow unprivileged binding of port 631 on the host, persistently (needs root):
```sh
echo 'net.ipv4.ip_unprivileged_port_start=631' | sudo tee /etc/sysctl.d/90-unprivileged-ports.conf
sudo sysctl --system
```
- run the container as a rootful (system) service rather than a user quadlet;
- or serve on an unprivileged port (CUPS advertises the actual port in its SRV
record, but be aware some AirPrint clients assume 631).
On SELinux systems, verify there is no port-labelling denial as well
(`sudo ausearch -m avc -ts recent`). Also ensure 631/tcp and 5353/udp are open in
the firewall.
### macOS/iOS sees the printer but reports it cannot connect
If plain IPP works but the *secure* connection fails, this is the `_ipps`
service: macOS prefers `_ipps._tcp`, which CUPS advertises unconditionally
(also when no certificate exists). The certificate lives in `/etc/cups/ssl`, so
that directory must be writable — use a **writable (named) volume**; see the
bind-mount caveat under *Notes and limitations*. If it is not writable, CUPS
logs `Unable to encrypt connection: Unable to create server credentials` and the
secure connection fails while plain IPP keeps working.
The other cause is the certificate itself: clients only accept it when the name
they connected to is in its SANs *and* they trust it (see *TLS certificate for
`_ipps`*). Typical symptoms: `curl https://<host>.local:631/` ends with
`(60) SSL certificate problem`, or `ipptool -tv ipps://…` succeeds while
macOS/iOS refuse. Add the missing name via `CERT_EXTRA_NAMES` or trust the
certificate. After changing anything, remove the printer in macOS and add it
again — macOS caches the failed attempt.
### macOS adds the printer as a Generic PostScript printer
Symptom: the queue prints, but *Use:* shows **Generic PostScript Printer** and
the print dialog offers no duplex, no colour and no tray 2 — while iOS, which
queries the printer live, offers all of them. On the Mac,
`lpoptions -p <queue> -l` lists only `APOptionalDuplexer` plus
`Duplex`/`PageSize`/`ManualFeed`/`InputSlot`, and
`/etc/cups/ppd/<queue>.ppd` contains `*PCFileName: "Generic.ppd"`.
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:
```sh
podman exec printserver avahi-browse -rt _ipps._tcp # what is advertised
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
over the same secure service (`ipptool -tv ipp://<host>:631/printers/<queue>`
prints the UUID; drop the `urn:uuid:` prefix):
```sh
sudo lpadmin -x <queue>
sudo lpadmin -p <queue> -E -m everywhere \
-v "dnssd://<queue>%20%40%20<host>._ipps._tcp.local./?uuid=<printer-uuid>"
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
*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.
## CUPS 3 and the future of PPDs
Adding the queue with a PPD makes `lpadmin` print:
```
lpadmin: Printer drivers are deprecated and will stop working in a future version of CUPS.
```
This is an advance notice, not an error. OpenPrinting deprecated PPDs, printer
drivers and filter executables during the CUPS 2.x series, and **CUPS 3.x drops
them entirely** in favour of an all-IPP architecture (a *local* server for
temporary driverless queues plus a *sharing* server for persistent ones, with
filtering limited to PDF, PWG Raster, Apple Raster and PCLm). Any queue created
with a PPD (`-P`) or a driver model (`-m <driver>`) triggers the message — it is
not specific to Brother, and there is no way to keep the PPD and silence it. A
raw queue and `-m everywhere` (driverless) do *not* print it.
The driverless replacement is a **Printer Application**: a PAPPL-based service
that emulates a driverless IPP printer and does the format/option conversion on
the other side. For a PostScript device like the HL-4150CDN that role is filled
by the **PostScript Printer Application** (`ps-printer-app`), which takes over
exactly what our BR-Script3 PPD does today — the PPD does not disappear, it just
moves out of CUPS and into the printer application.
Note the HL-4150CDN is itself *not* IPP Everywhere / AirPrint capable (Brother
lists it as such), so the plain `-m everywhere` shortcut is not available here.
This container instead advertises AirPrint to its *clients* and performs the
PostScript conversion itself.
Nothing needs to change right now: the distro images used here ship CUPS 2.4.x
and PPD-based drivers keep working for years. When you eventually move to
CUPS 3.x, run `ps-printer-app` (or `legacy-printer-app`) alongside CUPS and
create the queue driverlessly against it (`ipp://…`), leaving the PPD inside the
printer application.