Files
vnidrop/crates/vnidrop/CORE_FLOW.md
Hammed Abass d0c8d8774a fix(security): address medium findings for ACL, limits, and UX
Tighten approve-endpoint to active shares with TTL sessions, reject non-file
FDs, lower default ticket/approval/size caps, show endpoint IDs and Public-mode
warnings, harden Android receive path checks, and run cargo-audit in CI.
2026-07-13 18:54:28 +02:00

5.5 KiB

VniDrop Core Send/Receive Flow

This crate owns the transfer backend. Platform/UI code should pass file handles or paths into Rust and react to CoreEvent updates; it should not move file bytes through Kotlin memory.

Send

  1. initialize(app_data_dir, event_sink) starts the Iroh endpoint, blob provider, handshake protocol, SQLite repository, and event hub.
  2. share_files(sources, metadata) validates platform sources, streams each file into iroh-blobs, stores a collection, and returns a VniDrop ticket.
  3. New VniDrop shares are ApprovalRequired by default. A copied ticket is not enough to read bytes until the sender approves the receiver endpoint.
  4. The blob provider is default-deny: only hashes registered for an active share (collection root and each member blob) may be served, and only when access policy allows that remote endpoint. Unknown hashes are refused.
  5. The sender observes receiver requests through CoreEvent entries with phase="approval" and can query them with list_receiver_requests(transfer_id).
  6. respond_receiver_request(request_id, accepted, reason) accepts or refuses a pending request. Accepted requests create a time-limited access session for the receiver endpoint.
  7. Ticket strings are capabilities. Share events emit hash/size metadata only — never the full ticket payload.

Receive

  1. receive(ticket, output_dir, receiver_name) parses and validates the ticket.
  2. VniDrop tickets first connect to the handshake ALPN /vnidrop/handshake/2 and send RequestTransfer metadata to the sender.
  3. If approved, the receiver connects to the blobs ALPN, downloads the collection, and streams files to output_dir.
  4. If refused, expired, unknown, or cancelled, the receive transfer is marked failed or cancelled and emits an error/lifecycle event.
  5. Only vnd1: VniDrop tickets are accepted. Raw iroh BlobTicket strings are rejected at parse time so receive always runs the approval handshake.

Core States And Events

  • Transfer statuses: sharing, receiving, done, failed, cancelled, stopped.
  • Main event phases: endpoint, import, ticket, handshake, approval, access, transfer, download, export, delivery, lifecycle, error.
  • Events are sent to CoreEventSink immediately and persisted through the event hub. list_events flushes queued persistence before reading SQLite.
  • shutdown() is idempotent and flushes events before stopping the router.

Platform File Rules

  • Desktop uses normal filesystem paths.
  • Android opens SAF/content URIs in Kotlin and passes a borrowed file descriptor; Rust duplicates the descriptor before streaming.
  • iOS starts the security-scoped URL lease in Kotlin and keeps it alive while Rust streams from the accessible file URL/path.

Durability And Filesystem Policy

  • SQLite records have a local UUID in addition to the protocol transfer ID. Schema-v2 records are migrated in place and keep their tickets and history.
  • Imports and receives are recorded before work begins. A process restart marks interrupted work failed and expires approval requests that no longer have an in-memory responder.
  • Persisted shares are restored only when their root collection is complete and readable. Missing or corrupt roots fail closed and emit a recovery event.
  • Receive destinations use a no-overwrite policy. Rust writes a uniquely named temporary file in the destination directory, syncs it, and publishes it with a no-clobber hard link when the filesystem supports it. On platforms that reject hard links (notably Android emulated external storage), publication falls back to an exclusive rename (renameat2(RENAME_NOREPLACE) / renamex_np(RENAME_EXCL)). Failure or cancellation removes the temporary file. Stale VniDrop temporary files are cleaned on later writes.
  • Android defaults to the shared system Downloads collection via MediaStore (ReceiveFolderKind.AndroidPublicDownloads on API 29+). Files show up in the user's Downloads UI like a browser download. Custom folders still use a SAF tree URI from the folder picker. Both Android sinks stream through ReceiveOutputSink instead of raw filesystem paths. Pre-Android 10 falls back to the public Downloads path with legacy storage permission.
  • Foreign output sinks receive exactly one terminal callback after a successful start_file: finish_file or abort_file.

Blob Retention Policy

Stopping a share immediately removes its provider mapping and approval state, so outstanding VniDrop tickets can no longer download content. Physical blob chunks are not force-deleted at stop time because content-addressed chunks may be shared by another active collection. They remain eligible for the blob store's garbage collection. Restart reconciliation never restores a stopped share.

Resource Limits

CoreLimits controls source count, collection files and bytes, path and ticket sizes, metadata, retained events, pending approvals, concurrent transfers, and the event persistence queue. initialize uses conservative defaults (including bounded ticket size, pending approvals, and total collection bytes); initialize_with_limits supports stricter deployments and tests. Cheap limits are checked before durable or network work, while remote collection limits are checked before downloading file content.

Manual approve_endpoint_for_transfer only applies to active shares, requires a non-empty endpoint id, and creates a time-limited session (same TTL as handshake approval), never a permanent grant.