LAN peer printing
Printing on the shared printers of other devices on the same network, without a server.
Two devices on the same local network find each other and print on each other's shared printers, without a server in between. A device that shares its printers is a host. A device that prints on them connects to the host and opens a session.
LAN peer printing is available from sPrint 0.1.2.
When to use it
Use it when a device prints on a printer that is connected to another device, for example a tablet that prints on the label printer of the computer at the packing desk, or when the devices must work without a connection to a server. To receive jobs from Superroute, use the print node. A print node can also send its jobs to the printers of a connected device.
Sharing is off until the app starts a host. No printer is shared until the app names it, and every new device needs the owner's approval unless another access mode is set.
Turning sharing on
The device that owns the printers creates a host over its printer registry and starts it. The host accepts connections on TCP port 17778. Its settings:
- Identity: a key that identifies the device. Other devices pin its fingerprint. Without a saved state the host creates a new identity.
- Name: the device name shown to other devices.
- Access mode: open, password or approval. Approval is the default.
- Password: used in password mode. The host keeps only a hash of it.
- Shared printers: the printers of the registry that other devices may use, named one by one.
- Discoverable: whether the device announces itself on the network. Off by default. Other devices can still connect by address.
// C / Go (cgo)
#include "sprint.h"
/* The printer owner. state_json is the PeerHostState stored last time ("{}" the first time). */
char *err = NULL;
long long registry = sprint_registry_open("printers.json", &err);
char config[4096];
snprintf(config, sizeof config, "{\"name\":\"Front desk\",\"state\":%s}", state_json);
long long host = sprint_peer_host_new(config, registry, 0, on_peer_event, NULL, &err);
/* on_peer_event: store the state of every "peer_state" event, answer "peer_pending" with sprint_peer_host_decide() */
sprint_free(sprint_peer_host_set_shared(host, "[\"Labels\"]")); /* nothing is shared until named */
sprint_free(sprint_peer_host_set_mode(host, "approval")); /* open | password | approval */
sprint_free(sprint_peer_host_set_discoverable(host, 1)); /* announced on the network */
sprint_free(sprint_peer_host_start(host)); /* TCP port 17778 */
// C# / .NET
using Superroute.SPrint;
// The printer owner
using var host = new PeerHost(printers, state: savedPeerState, name: "Front desk");
host.StateChanged += (_, s) => Save(s); // identity, mode, shared printers, trust list
host.ApprovalRequested += (_, p) => host.Decide(p.Id, AskOwner(p.Name, p.Fingerprint, p.Note));
host.SetShared(new[] { "Labels" }); // nothing is shared until named
host.SetMode(PeerModes.Approval); // or Open, or Password with SetPassword(…)
host.SetDiscoverable(true); // announced on the network
host.Start(); // TCP port 17778
# Python
from superroute_sprint import PeerHost, PeerHostState, PeerModes
# The printer owner
def on_peer(e):
if e.kind == "peer_state":
save(e.state.json) # identity, mode, shared printers, trust list
elif e.kind == "peer_pending":
host.decide(e.pending.id, ask_owner(e.pending.name, e.pending.fingerprint, e.pending.note))
host = PeerHost(printers, state=PeerHostState(saved_json), name="Front desk", on_event=on_peer)
host.set_shared(["Labels"]) # nothing is shared until named
host.set_mode(PeerModes.APPROVAL) # or OPEN, or PASSWORD with set_password(…)
host.set_discoverable(True) # announced on the network
host.start() # TCP port 17778
// Swift (iOS, macOS)
import SPrint
// The printer owner
let host = try PeerHost(registry: printers, name: "Front desk", state: savedState) // nil the first time
Task {
for await event in host.events {
switch event {
case .state(let s): save(s) // identity, mode, shared printers, trust list
case .pending(let p): ask(p) // then host.decide(p.id, approve: true / false)
default: break
}
}
}
try host.setShared(["Labels"]) // nothing is shared until named
try host.setMode(PeerMode.approval) // or .open, or .password with host.setPassword(_:)
try host.setDiscoverable(true) // announced on the network
try host.start() // TCP port 17778
// Kotlin / Android
// The printer owner (off the main thread; keep the process running while sharing)
val host = PeerHost(registry, name = "Front desk", state = savedJson?.let(PeerHostState::parse)) { ev ->
when (ev) {
is PeerEvent.State -> save(ev.state.toJson().toString()) // identity, mode, shared printers, trust list
is PeerEvent.Pending -> askOwner(ev.pending) // then host.decide(id, approve = true)
else -> {}
}
}
host.setShared(listOf("Labels")) // nothing is shared until named
host.setMode(PeerModes.APPROVAL) // or OPEN, or PASSWORD with setPassword(…)
host.setDiscoverable(true) // announced on the network
host.start() // TCP port 17778
// Dart / Flutter
// The printer owner
final host = await PeerHost.create(printers, name: 'Front desk', state: saved); // saved: null the first time
host.events.listen((e) {
switch (e) {
case PeerStateEvent(:final state): save(state.toJson()); // identity, mode, shared printers, trust list
case PeerPendingEvent(:final pending): ask(pending); // then host.decide(pending.id, approve: true)
default:
}
});
await host.setShared(['Labels']); // nothing is shared until named
await host.setMode(PeerModes.approval); // or open, or password with setPassword(…)
await host.setDiscoverable(true); // announced on the network
await host.start(); // TCP port 17778
Saving the state
sPrint keeps no files for LAN peer printing. The identity, the access mode, the password hash, the shared printers, the trust list and the discoverable setting form one state. The host sends it in a state event after every change. Store it privately and pass it back when the host is created again: with the same identity, devices that trust this device keep trusting it.
Access modes
| Mode | Meaning |
|---|---|
open |
Every device on the network may connect. |
password |
A device connects with the password. After six wrong passwords the device is refused for a while. |
approval |
The owner approves each new device. An approved device joins the trust list and reconnects without a new approval. |
Approving, the trust list and revoking
In approval mode each request arrives as a pending event with the device's fingerprint, its name and the note it sent. The app shows the request to the owner and passes the decision to the host. The requesting device waits at most two minutes for the decision.
The trust list is checked on every call. Revoking a device removes it from the trust list and ends its sessions at once. The host lists the pending requests and the connected devices at any time.
Finding and connecting to a device
A browse lists the devices that announce themselves on the network, with their fingerprint, name, access mode, address and version. A device that does not announce itself is reached by its address and port.
Pass the fingerprint from the browse, or one entered by the user: the connection is refused when the device presents another key. Without a fingerprint the first key seen is trusted. Show the full fingerprint so that the user can check it.
In password mode pass the password. In approval mode pass a note for the owner: the connection is pending until the owner decides. Wait for the approval; the wait ends with a refusal or after two minutes at most. The connecting device has its own identity too. Store its seed so that the owner's approval stays valid.
// C / Go (cgo)
/* The printer user. seed_b64 is the stored identity seed ("" makes a new one). */
long long me = sprint_peer_identity_new(seed_b64, &err);
char *found = sprint_peers_browse(3000); /* {"peers": [{fingerprint, name, mode, addr, version}]} */
long long session = sprint_peer_connect(
"{\"addr\":\"192.168.1.20:17778\",\"fingerprint\":\"a1b2:c3d4:…\",\"note\":\"Packing desk\"}",
me, on_session_event, NULL, &err); /* err: "(BAD_PASSWORD)", "(THROTTLED)", … */
char *status = sprint_peer_status(session); /* "pending" in approval mode */
char *decision = sprint_peer_wait_approval(session, 0); /* code "denied" or "timeout" */
char *result = sprint_peer_print(session, "Labels", label_job_json); /* PrintResult */
sprint_free(sprint_registry_attach_peer(registry, session)); /* targets "peer:<fingerprint>/<printer>" */
// C# / .NET
// The printer user
using var me = PeerIdentity.FromSeed(savedSeed); // or new PeerIdentity(), then store me.Seed
var peer = Discovery.BrowsePeers().First();
using var session = PeerSession.Connect(me, new PeerConnectOptions { Addr = peer.Addr, Fingerprint = peer.Fingerprint, Note = "Packing desk" });
if (session.Status.Status == "pending") session.WaitApproval(); // SPrintException Code "denied" / "timeout"
session.Ended += (_, e) => Console.WriteLine($"ended: {e.Reason}");
var result = session.Print("Labels", new PrintJob { Type = JobTypes.Label, Label = label });
printers.AttachPeer(session); // targets "peer:<fingerprint>/<printer>"
# Python
from superroute_sprint import PeerIdentity, PeerSession, Discovery, PrintJob, JobTypes
# The printer user
me = PeerIdentity.from_seed(saved_seed)
peer = Discovery.browse_peers()[0]
session = PeerSession.connect(me, peer.addr, fingerprint=peer.fingerprint, note="Packing desk",
on_event=lambda e: print("ended:", e.reason))
if session.status.status == "pending":
session.wait_approval() # SPrintError code "denied" / "timeout"
result = session.print("Labels", PrintJob(type=JobTypes.LABEL, label=label))
printers.attach_peer(session) # targets "peer:<fingerprint>/<printer>"
// Swift (iOS, macOS)
// The printer user
let me = try PeerIdentity(seed: storedSeed) // or PeerIdentity() and store me.seed
let peer = try Peers.browse(timeout: 3)[0]
let session = try Peers.connect(to: peer.addr, identity: me, fingerprint: peer.fingerprint,
note: "Warehouse iPad") // password: for password mode
if try session.status().isPending { try session.waitApproval() } // SPrintError "denied" / "timeout"
let result = try session.print(PrintJob(type: JobType.label, label: label), printer: "Labels")
try printers.attachPeer(session) // targets "peer:<fingerprint>/<printer>"
// Kotlin / Android
// The printer user
val me = PeerIdentity.fromSeed(storedSeed) // or PeerIdentity.create(), then store me.seed
val peer = SPrint.browsePeers(3000).first()
val session = SPrint.connectPeer(peer.addr, me, fingerprint = peer.fingerprint, note = "Warehouse tablet") { ev ->
if (ev is PeerEvent.Ended) { /* revoked, expired or unreachable */ }
}
if (session.status.pending) session.waitApproval() // "denied" / "timeout"
val result = session.print("Labels", PrintJob(JobTypes.LABEL, label = label))
registry.attachPeer(session) // targets "peer:<fingerprint>/<printer>"
// Dart / Flutter
// The printer user
final me = await PeerIdentity.fromSeed(storedSeed); // or PeerIdentity.create(), then store me.seed
final peer = (await Peers.browse(timeout: const Duration(seconds: 3))).first;
final session = await Peers.connect(peer.addr, me, fingerprint: peer.fingerprint, note: 'Warehouse tablet');
session.events.listen((e) { if (e is PeerEndedEvent) { /* revoked, expired or unreachable */ } });
if ((await session.status()).isPending) await session.waitApproval(); // "denied" / "timeout"
final result = await session.print(PrintJob(type: JobTypes.label, label: label), printer: 'Labels');
await printers.attachPeer(session); // targets "peer:<fingerprint>/<printer>"
Printing on a device's printer
A session lists the shared printers of the device and prints label, PDF, HTML, image and raw jobs on them, with the same result as a local print. The session keeps itself alive and reports its end with a reason: revoked, expired or unreachable.
Adding a device's printers to the registry
When a session is attached to a printer registry, the shared printers of the device become printers of the registry, named as below. Jobs for them go through the session, so a print node can send the jobs it receives from Superroute to the printers of a connected device. Detaching the device removes them.
peer:<fingerprint>/<printer>
Platform requirements
| Platform | What the app declares or allows | Note |
|---|---|---|
| Android | INTERNET, ACCESS_NETWORK_STATE, CHANGE_WIFI_MULTICAST_STATE |
Declared by the library. No runtime permission is asked. Keep the process running while sharing, for example in a foreground service. |
| iOS | NSLocalNetworkUsageDescription, NSBonjourServices: _superprinter._tcp (Info.plist) |
iOS asks the user for local network access the first time. Until access is granted, a browse finds no device. |
| macOS | com.apple.security.network.client, com.apple.security.network.server; NSLocalNetworkUsageDescription (macOS 15+) |
For sandboxed apps only. |
| Windows | TCP 17778, UDP 5353 |
Allow incoming TCP on port 17778 and multicast DNS on UDP port 5353 in the firewall. |
| Linux, Raspberry Pi | TCP 17778, UDP 5353 |
Allow incoming TCP on port 17778 and multicast DNS on UDP port 5353 in the firewall. |
| Browser | — |
Not available: a browser cannot announce itself or accept connections. |
On iOS add the local network usage description and the service type to Info.plist:
<key>NSLocalNetworkUsageDescription</key>
<string>Finds and shares printers with other devices on this network.</string>
<key>NSBonjourServices</key>
<array>
<string>_superprinter._tcp</string>
</array>
All devices must be on the same network. Guest networks that isolate their devices block LAN peer printing.
Security
- Each device proves its identity by signing a one-time challenge with its key.
- Connections are encrypted with a certificate tied to the device identity and pinned by its fingerprint.
- A password is kept as a salted hash. Repeated wrong passwords from one device are refused for a while.
- The trust list is checked on every call, so a revoked device is refused at once.
- A device sees and uses only the printers that are shared.
- A job that names a printer address of its own is refused.
SuperPrinter devices
sPrint uses the peer protocol of the SuperPrinter apps, version 1.1.0, without changes: discovery as _superprinter._tcp on the local network and HTTPS on port 17778. A device with sPrint and a device with SuperPrinter 1.1 print on each other's printers.