386 lines
17 KiB
Markdown
386 lines
17 KiB
Markdown
# 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`. 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.
|