unmanbearpig/lsend

Scriptable LocalSend-compatible Rust CLI with explicit receive policies, persistent identity, JSON output, file-handling safeguards, and integration tests.

★ 0Forks 0RustGitHub ↗Compare

README

lsend

lsend is a scriptable LocalSend-compatible command-line peer. It discovers official LocalSend devices, sends files or text, and receives files in the foreground without requiring a TUI.

The protocol implementation is a pinned snapshot of the official LocalSend Rust core. lsend supplies the command contract, persistent identity, non-interactive output, receive policy, and safe file publication around it.

This is an independent project and is not affiliated with or endorsed by the LocalSend maintainers.

Status

The first interoperability milestone is implemented:

  • LocalSend Protocol v2.2 over HTTPS with mutual TLS.
  • Persistent certificate identity and certificate-fingerprint pinning.
  • IPv4 and IPv6 multicast discovery, including scoped IPv6 channels.
  • Direct HTTPS/HTTP targeting, aliases, and fingerprints.
  • File and text sending with PIN support and cancellation.
  • Foreground receiving with prompt or explicit auto-accept policy.
  • Streaming transfers, optional sender checksums, receiver size/hash checks, sanitized names, temporary files, and collision-safe publication.
  • Human output and JSON Lines events with stable error classes.

Pairing, browser share links, directory recursion, packaged releases, and a doctor command remain later milestones.

Documentation

The documentation index separates user and maintainer guides.

Build and install

Rust 1.93 or newer is required. The toolchain file selects the tested version.

cargo build --locked --release
cargo install --locked --path .

The official core is vendored, so a clean build does not need to clone the large upstream application repository or its Flutter submodule.

The first command generates a private identity, which may take several seconds. Later invocations reuse it from $XDG_CONFIG_HOME/lsend/identity.pem or ~/.config/lsend/identity.pem.

Quick start

Find devices:

lsend discover
lsend discover --json

Send files by alias or direct address:

lsend send --to "My Phone" report.pdf photo.jpg
lsend send --to alias:"My Phone" report.pdf
lsend send --to https://192.168.1.40:53317 report.pdf

Send text explicitly:

lsend text --to "My Phone" -- "hello from the terminal"

Receive with confirmation prompts:

lsend receive --output ~/Downloads

Receive one transfer non-interactively on a trusted network:

lsend --json receive \
  --accept all \
  --once \
  --output ./incoming

Protect the receiver with a PIN:

lsend receive --accept all --pin 123456
lsend send --to workstation --pin 123456 report.pdf

LSEND_PIN can provide the sender or receiver PIN without placing it directly in shell history. The value is never included in events or logs.

Target syntax

Input Meaning
My Phone Case-insensitive alias or exact fingerprint discovered on the LAN
alias:My Phone Explicit alias, including aliases containing :
fingerprint:ABCD... Explicit full certificate fingerprint
192.168.1.40 Direct HTTPS on port 53317
192.168.1.40:54444 Direct HTTPS on the given port
https://host.example:54444 Direct HTTPS hostname
http://192.168.1.40:53317 Explicit unencrypted HTTP

There is no automatic HTTPS-to-HTTP downgrade. HTTP must be requested explicitly and produces a warning.

Receive policy

--accept prompt is the default and requires a terminal. Machine-readable receive mode deliberately refuses that ambiguous combination:

--json receive requires --accept all

--accept all trusts every device that can authenticate to the listener and, when configured, provide the correct PIN. It should be used only on a trusted network.

Completed files are written under the selected output directory. Remote path components are discarded and illegal file-name characters are sanitized. Uploads first go to hidden temporary files and are only published after their declared size and optional SHA-256 checksum pass.

Collision policies:

  • rename (default): report.txt, report (1).txt, and so on.
  • reject: fail the individual upload without changing the existing file.
  • overwrite: atomically replace the existing destination where supported by the platform filesystem.

JSON Lines

--json writes one JSON object per event to stdout. Diagnostics and tracing remain on stderr. Typical send events are:

{"event":"resolving","target":"phone"}
{"event":"accepted","sessionId":"...","acceptedFiles":1,"acceptedBytes":42}
{"event":"fileComplete","fileName":"note.txt","size":42}
{"event":"complete","kind":"files","files":1,"bytes":42}

The receiver emits ready before accepting traffic. With --port 0, its port field contains the operating-system-selected port.

Exit codes

Code Meaning
0 Complete success
1 Internal failure
2 Invalid command input
3 Target not found or ambiguous
4 Declined, busy, or rate limited
5 PIN/authentication failure
6 Network or TLS failure
7 Size/hash/integrity failure
8 Cancelled or partial multi-file result

Development

cargo fmt -p lsend --check
cargo clippy -p lsend --all-targets --no-deps -- -D warnings
cargo test -p localsend --features full --locked
cargo test -p lsend --locked

See CONTRIBUTING.md for change discipline and the interoperability guide for the release matrix and official-core update procedure.

The test suite includes the official core's unit and integration tests, direct official-core oracle tests in both directions, and a real two-process multi-file lsend -> lsend transfer. These tests open HTTPS listeners, use independent persistent identities, complete mutual-TLS handshakes, pin receiver certificates, stream file bodies, and compare the received bytes.

Self-interoperability is not treated as sufficient for releases. The next release gate is a manual and then automated matrix against current official desktop and mobile builds in both directions.

The current package is intentionally publish = false: the vendored official core is not an independently published dependency. Install from a source checkout with cargo install --path .; release archives and package-manager formulas are a later milestone.

Upstream core

The exact source and update policy are recorded in vendor/localsend-core/UPSTREAM.md and NOTICE.md. Core updates must be imported as a reviewed snapshot and pass the full bidirectional compatibility suite before the recorded revision changes.

License

Apache-2.0. See LICENSE and NOTICE.md.

Contributors

unmanbearpig

Issues