# ipp-usb

![GitHub](https://img.shields.io/github/license/OpenPrinting/ipp-usb)
[![Go Report Card](https://goreportcard.com/badge/github.com/OpenPrinting/ipp-usb)](https://goreportcard.com/badge/github.com/OpenPrinting/ipp-usb)

## Introduction

[IPP-over-USB](https://www.usb.org/document-library/ipp-protocol-10)
allows using the IPP protocol, normally designed for network printers,
to be used with USB printers as well.

The idea behind this standard is simple: It allows to send HTTP
requests to the device via a USB connection, so enabling IPP, eSCL
(AirScan) and web console on devices without Ethernet or WiFi
connections.

Unfortunately, the naive implementation, which simply relays a TCP
connection to USB, does not work. It happens because closing the TCP
connection on the client side has a useful side effect of discarding
all data sent to this connection from the server side, but it does not
happen with USB connections. In the case of USB, all data not received
by the client will remain in the USB buffers, and the next time the
client connects to the device, it will receive unexpected data, left
from the previous abnormally completed request.

Actually, it is an obvious flaw in the IPP-over-USB standard, but we
have to live with it.

So the implementation, once the HTTP request is sent, must read the
entire HTTP response, which means that the implementation must
understand the HTTP protocol, and effectively implement a HTTP reverse
proxy, backed by the IPP-over-USB connection to the device.

And this is what the **ipp-usb** program actually does.

## Features in detail

* Implements HTTP proxy, backed by USB connection to IPP-over-USB device
* Full support of IPP printing, eSCL scanning, and web admin interface
* DNS-SD advertising for all supported services
* DNS-SD parameters for IPP based on IPP get-printer-attributes query
* DNS-SD parameters for eSCL based on parsing GET /eSCL/ScannerCapabilities response
* TCP port allocation for device is bound to particular device (combination of
VendorID, ProductID and device serial number), so if the user has multiple
devices, they will receive the same TCP port when connected. This allocation
is persisted on a disk
* Automatic DNS-SD name conflict resolution. The finally chosen device's
network name is persisted on a disk
* Can be started by **UDEV** or run in standalone mode
* Can share printer to other computers on a network, or use the loopback interface only
* Can generate very detailed logs for possible troubleshooting

## Under the hood

Though looks simple, ipp-usb does many non obvious things under the hood

* Client-side HTTP connections are completely decoupled from printer-side HTTP-over-USB connections
* HTTP requests are sanitized, missed headers are added
* HTTP protocol upgraded from 1.0 to 1.1, if needed
* Attempts to upgrade HTTP connection to winsock, if unwisely made by web console, are
prohibited, because it can steal USB connection for a long time
* Client HTTP requests are fairly balanced between all available 2-3 USB connections,
regardless of number and persistence of client connections
* Dropping connection by client properly handled in all cases, even in a middle of sending.
In a worst case, printer may receive truncated document, but HTTP transaction will always be
performed correctly

## Memory footprint

Being written on Go, ipp-usb has a large executable size. However, its
memory consumption is not very high. When single device is connected,
ipp-usb RSS is similar or even slightly less in comparison to ippusbxd.
And because ipp-usb handles all devices in a single process, it uses noticeably
less memory that ippusbxd, when serving 2 or more devices.

## External dependencies

This program has very few external dependencies, namely:
* `libusb` for USB access
* `libavahi-common` and `libavahi-client` for DNS-SD
* Running Avahi daemon

## Binary packages

Binary packages available for the following Linux distros:
* **Debian** (10)
* **Fedora** (29, 30, 31 and 32)
* **openSUSE** (Tumbleweed)
* **Ubuntu** (18.04, 19.04, 19.10 and 20.04)

**Linux Mint** users may use Ubuntu packages:
* Linux Mint 18.x - use packages for Ubuntu 16.04
* Linux Mint 19.x - use packages for Ubuntu 18.04

Follow this link for downloads: https://download.opensuse.org/repositories/home:/pzz/

## Not only Linux

We are glad to announce that `ipp-usb` was recently included into the
FreeBSD ports: https://www.freshports.org/print/ipp-usb/

Hope, NetBSD/OpenBSD support will be added as well, so technology
becomes not Linux-only, but UNIX-wide.

## The ipp-usb Snap

ipp-usb is also available as a Snap in the Snap Store: https://snapcraft.io/ipp-usb

Before you install the Snap, uninstall any already existing
installation of ipp-usb.

Simply install it via any GUI client for the Snap Store (Like "Ubuntu
Software") or via command line:

    sudo snap install --edge ipp-usb

Now you can connect and disconnect IPP-over-USB devices and ipp-usb
gets started by the Snap whenever needed. Also devices which are
already connected during boot, start, or update of the Snap are
considered.

You can also use

    ipp-usb status

to check the status of the running ipp-usb daemon (supported device
must be connected for the ipp-usb daemon to be running, accesses only
the ipp-usb daemon of the Snap) and

    ipp-usb check

to scan the USB for the presence of potentially supported USB devices
(7/1/4 interface protocol). This command requires access to the raw
USB and therefore on many systems root privileges are required.

The Snap is automatically updated when further development on ipp-usb
happens.

The configuration file is here:

    /var/snap/ipp-usb/common/etc/ipp-usb.conf

You can edit it and afterwards restart the Snap to use the changed
configuration.

Incompatibilities of particular devices are handled by workarounds
defined in the quirk files. You find them here:

    /var/snap/ipp-usb/common/quirks

You can add your own quirk files (but if they solve your problem,
please report an issue here, with your quirk file attached, so that
others with the same problem will get helped, too).

For quick tests you can also edit the existing files, but they will
get replaced (and so your changes lost) on the next update of the
Snap, as we are changing them on any report of further device
incompatibilities.

The log file is here

    /var/snap/ipp-usb/common/var/log

and device state files (to assure that each device appears on the same
port and with the same DNS-SD service name) are here:

    /var/snap/ipp-usb/common/var/dev

You can also build the Snap locally. This is useful when

* You want to modify ipp-usb
* You want to learn about snapping Go projects
* You want to learn about how to use UDEV from within a Snap (note that a Snap cannot install UDEV rules into the system)

To do so, run from the main directory of this source repository

    snapcraft snap

and then install the resulting Snap with

    sudo snap install --dangerous ipp-usb*.snap

An installed Snap from the Snap Store will get overwritten/replaced by your Snap.

Some technical notes about this Snap:

Snapping the Go project with one Go library taken from upstream (and
not from Ubuntu Core) was rather straight-forward. Only observation
was that the Go plugin seems not to do "make install". So I had to use
an "override-build" to manually install the auxiliary files
(ipp-usb.conf, quirk files). I also have adapted the auxiliary file
and state directories in paths.go in the "override-build" scriptlet.

The real challenge of this Snap was to trigger ipp-usb on the
appearing (and also the presence) of IPP-over-USB devices.

In the classic installation of ipp-usb (via "make install" or RPM/DEB
package installation) a UDEV rules file and a systemd service file (in
systemd-udev/) are installed, so that the system automatically triggers
the launch of ipp-usb when an appropriate device is connected or
already present. A Snap is not able to do so. It cannot install any
files into the system. It can only bring its own, static file system
and create files only in its own state directory. These locations are
not scanned for UDEV rules.

So the Snap must discover the devices without its own UDEV rules, but
it still can use UDEV. The trick is to do a generic monitoring of UDEV
events and filtering out the USB devices with IPP-over-USB interface
(7/1/4). If such a device appears, we trigger and ipp-usb launch. We
also check on startup of the Snap whether there is such a device
already and if so, we also trigger an ipp-usb launch.

ipp-usb is run, as in the classic installation, with "udev"
argument. This way it stops by itself when there is no device any more
(and we do not need to observe the disappearal events of the devices)
and it is assured that only one single instance of ipp-usb is running.

To do this with low coding effort I use the UDEV command line tool
udevadm in a shell script (snap/local/run-ipp-usb). Once it runs in
"monitor" mode to observe the UDEV events. Then we parse the output
lines to only consider the ones for a device appearing and run
"udevadm info -q property" on each device path, to get the properties
and filter the 7/1/4 interface. In the beginning we use "udevadm
trigger" to find the already passed appearal event of a device which
is already present. So the shell script is an auxiliary daemon to
start ipp-usb when needed.

## The ipp-usb Rock

### Install from GitHub Container Registry
#### Prerequisites

1. **Docker Installed**: Ensure Docker is installed on your system. You can download it from the [official Docker website](https://www.docker.com/get-started).
```sh
  sudo snap install docker
```

#### **Running the `ipp-usb` Container with Persistent Storage**  

To run the `ipp-usb` container while ensuring that its state persists across restarts, follow these steps.  

#### **1. Pull the `ipp-usb` Docker Image**  
The latest image is available on the GitHub Container Registry. Pull it using:  
```sh
sudo docker pull ghcr.io/openprinting/ipp-usb:latest
```

#### **2. Create Persistent Storage Volumes**  
To ensure that configuration settings and printer state persist across container restarts, create two Docker volumes:  

```sh
sudo docker volume create ipp-usb-state
sudo docker volume create ipp-usb-conf
```  

These volumes store:  

- **`ipp-usb-state` (`/var/ipp-usb/`)**  
  - Device state files (`/var/ipp-usb/dev/`) – Ensures stable TCP port allocation and DNS-SD name resolution.  
  - Lock files (`/var/ipp-usb/lock/`) – Prevents multiple instances from running simultaneously.  
  - Log files (`/var/log/ipp-usb/`) – Can be optionally mounted if needed for debugging.  

- **`ipp-usb-conf` (`/etc/ipp-usb/`)**  
  - Configuration file (`/etc/ipp-usb/ipp-usb.conf`) – Stores user-defined settings.  
  - Quirks files (`/etc/ipp-usb/quirks/`) – Contains printer-specific workarounds.  

#### **3. Run the Container with Required Mounts**  
Start the container with appropriate options:  

```sh
sudo docker run -d --network host \
    -v /dev/bus/usb:/dev/bus/usb:ro \
    -v ipp-usb-state:/var/ipp-usb \
    -v ipp-usb-conf:/etc/ipp-usb \
    --device-cgroup-rule='c 189:* rmw' \
    --name ipp-usb \
    ghcr.io/openprinting/ipp-usb:latest
```

- **`--network host`** → Uses the host’s network to enable proper IPP-over-USB and Avahi service discovery.  
- **`-v /dev/bus/usb:/dev/bus/usb:ro`** → Grants read-only access to USB devices for printer detection.  
- **`-v ipp-usb-state:/var/ipp-usb`** → Mounts the persistent storage volume for printer state, lock files, and logs.  
- **`-v ipp-usb-conf:/etc/ipp-usb`** → Ensures configuration and quirks files persist across reboots.  
- **`--device-cgroup-rule='c 189:* rmw'`** → Grants read, write, and management permissions for USB printers inside the container.  


### Building and Running `ipp-usb` Locally

#### Prerequisites

1. **Docker Installed**: Ensure Docker is installed on your system. You can download it from the [official Docker website](https://www.docker.com/get-started) or from the Snap Store:
```sh
  sudo snap install docker
```

2. **Rockcraft**: Rockcraft should be installed. You can install Rockcraft using the following command:
```sh
  sudo snap install rockcraft --classic
```

**To Build and Run the `ipp-usb` Rock Image Locally, follow these steps** 

#### **1. Build the `ipp-usb` Rock Image**  
The first step is to build the Rock image from the `rockcraft.yaml` configuration file. This image will include all required dependencies and configurations for `ipp-usb`.  

Navigate to the directory containing `rockcraft.yaml` and run:  
```sh
  rockcraft pack -v
```  

#### **2. Convert the Rock Image to a Docker Image**  
Once the `.rock` file is built, convert it into a Docker image using:  
```sh
sudo rockcraft.skopeo --insecure-policy copy oci-archive:<rock_image> docker-daemon:ipp-usb:latest
```  

#### **3. Create Persistent Storage Volumes**  
To ensure that configuration settings and printer state persist across container restarts, create two Docker volumes:  

```sh
sudo docker volume create ipp-usb-state
sudo docker volume create ipp-usb-conf
```  

These volumes store:  

- **`ipp-usb-state` (`/var/ipp-usb/`)**  
  - Device state files (`/var/ipp-usb/dev/`) – Ensures stable TCP port allocation and DNS-SD name resolution.  
  - Lock files (`/var/ipp-usb/lock/`) – Prevents multiple instances from running simultaneously.  
  - Log files (`/var/log/ipp-usb/`) – Can be optionally mounted if needed for debugging.  

- **`ipp-usb-conf` (`/etc/ipp-usb/`)**  
  - Configuration file (`/etc/ipp-usb/ipp-usb.conf`) – Stores user-defined settings.  
  - Quirks files (`/etc/ipp-usb/quirks/`) – Contains printer-specific workarounds.  

#### **4. Run the Container with Required Mounts**  
Start the container with appropriate options:  

```sh
sudo docker run -d --network host \
    -v /dev/bus/usb:/dev/bus/usb:ro \
    -v ipp-usb-state:/var/ipp-usb \
    -v ipp-usb-conf:/etc/ipp-usb \
    --device-cgroup-rule='c 189:* rmw' \
    --name ipp-usb \
    ipp-usb:latest
```

- **`--network host`** → Uses the host’s network to enable proper IPP-over-USB and Avahi service discovery.  
- **`-v /dev/bus/usb:/dev/bus/usb:ro`** → Grants read-only access to USB devices for printer detection.  
- **`-v ipp-usb-state:/var/ipp-usb`** → Mounts the persistent storage volume for printer state, lock files, and logs.  
- **`-v ipp-usb-conf:/etc/ipp-usb`** → Ensures configuration and quirks files persist across reboots.  
- **`--device-cgroup-rule='c 189:* rmw'`** → Grants read, write, and management permissions for USB printers inside the container.  

### Accessing the Container Shell

To enter the running `ipp-usb` container and access a shell inside it, use:
```sh
  sudo docker exec -it ipp-usb bash
```
This allows you to inspect logs, debug issues, or manually run commands inside the container.

### Configuration  

The `ipp-usb` container uses a configuration file located at:  
```sh
/etc/ipp-usb/ipp-usb.conf
```
By default, the container uses the built-in configuration, which can be modified from inside the container. 

**Modifying the Configuration File Inside the Container**  

#### **1. Enter the Running Container**  
Use the following command to access the container’s shell:  
```sh
sudo docker exec -it ipp-usb bash
```

#### **2. Open the Configuration File in Nano**  
Once inside the container, open the configuration file using `nano`:  
```sh
nano /etc/ipp-usb/ipp-usb.conf
```

#### **3. Edit and Save the File**  
- Make the necessary changes to the file.  
- Press `CTRL + X`, then `Y`, and hit `Enter` to save the changes.  

#### **4. Restart the Container to Apply Changes**  
Exit the container and restart it to apply the updated configuration:  
```sh
sudo docker restart ipp-usb
```

### **Viewing Logs in the `ipp-usb` Container**  

The `ipp-usb` container logs important events and errors to `/var/log/ipp-usb/`. Logs are categorized into:  

- **Main log file:** `/var/log/ipp-usb/main.log` → Captures overall service activity, including errors and general events.  
- **Per-device logs:** `/var/log/ipp-usb/<DEVICE>.log` → Individual log files for each detected printer, helpful for troubleshooting device-specific issues.  

#### **1. Using Docker Logs**  

To view real-time logs from the running container, use:  
```sh
sudo docker logs -f ipp-usb
```  
- The `-f` flag follows the logs in real-time.  
- Replace `ipp-usb` with your actual container name if different.  

#### **2. Viewing Logs Inside the Container**  

If you need to inspect logs manually, first enter the container shell:  
```sh
sudo docker exec -it ipp-usb bash
```  

Once inside the container, you can use the following commands:  

#### **2.1 Monitor Logs in Real-Time**  

- **Main log file:**  
  ```sh
  tail -f /var/log/ipp-usb/main.log
  ```  
  This continuously displays new log entries for the entire service.  

- **Per-device log file (Replace `<DEVICE>` with your printer's identifier):**  
  ```sh
  tail -f /var/log/ipp-usb/<DEVICE>.log
  ```  

#### **2.2 Display Full Log Files**  

- **Show full contents of the main log file:**  
  ```sh
  cat /var/log/ipp-usb/main.log
  ```  

- **Show logs for a specific device:**  
  ```sh
  cat /var/log/ipp-usb/<DEVICE>.log
  ```  

#### **2.3 List Available Device Logs**  

To see all available per-device log files, run:  
```sh
ls /var/log/ipp-usb/
```  

#### **3. Persisting Logs Across Container Restarts**  

If you want logs to persist across container restarts, you should mount the log directory using a Docker volume or a host directory.  

**Required Docker Flags for Log Persistence:**  
- `-v <volume-name>:/var/log/ipp-usb` → Mounts a Docker volume for log persistence.  
- **or**  
- `-v <host-directory>:/var/log/ipp-usb` → Mounts a host directory to retain logs outside the container.  

These options ensure that logs remain available even after the container stops or is recreated, allowing for easier troubleshooting and auditing.

## Installation from source

You will need to install the following packages (exact name depends
of your Linux distro):
* libusb development files
* libavahi-client and libavahi-common development files
* gcc
* Go compiler
* pkg-config
* git, make and so on

Building is really simple:

    git clone https://github.com/OpenPrinting/ipp-usb.git
    cd ipp-usb
    make

Then you may `make install` or just try to run `./ipp-usb` directly from
the build directory

## Avahi Notes (exposing printer to localhost)

IPP-over-USB normally exposes printer to localhost only, hence it
requires DNS-SD announces to work for localhost.

This requires Avahi 0.8.0 or newer. Older Avahi versions do not
support announcing to localhost.

Some Linux distros (for example recent Ubuntu and Fedora versions)
have their Avahi patched to support localhost, others (for example
Debian) not.

To determine if your Avahi supports localhost, run the following
command in one terminal session:
```
    avahi-publish -s test _test._tcp 1234
```
And simultaneously the following command in another terminal session
on the same machine:
```
    avahi-browse _test._tcp -r
```
If you see localhost in the avahi-browse output, like this:
```
    =     lo IPv4 test                                          _test._tcp           local
       hostname = [localhost]
       address = [127.0.0.1]
       port = [1234]
       txt = []
```
your Avahi is OK. Otherwise, update or patching is required.

So users of distros that ship a too old Avahi and without the patch
have three possibilities:
1. Update Avahi to 0.8.0 or newer
2. Apply the patch by themself, rebuild and reinstall avahi-daemon
3. Configure `ipp-usb` to run on all network interfaces, not only on loopback

If you decide to apply the patch, get it as `avahi/avahi-localhost.patch`
in this package or [download it here](https://raw.githubusercontent.com/OpenPrinting/ipp-usb/master/avahi/avahi-localhost.patch).

The third method is simple to do, just replace `interface = loopback`
with `interface = all` in the `ipp-usb.conf` file, but this has the
disadvantage of exposing your local USB-connected printer to the
entire local network, which can be an unwanted side effect, especially
in a big corporative network.
