Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 59 additions & 14 deletions docs/sync.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,22 @@ can't read something, it leaves it alone on both devices.

## Modules

| Module | Job |
| ------------- | --------------------------------------------------------------------- |
| Module | Job |
| ------------- | ---------------------------------------------------------------------- |
| `manifest.rs` | what a folder holds, what couldn't be read, and what counts as content |
| `plan.rs` | what a round does, for both devices at once (pure) |
| `merge.rs` | three-way merge of a note |
| `preview.rs` | a plan as someone can read it before it runs, and a note's line diff |
| `apply.rs` | carrying out one device's half of a plan, safely |
| `round.rs` | one round, from both ends: the coordinator and the device answering |
| `ids.rs` | repairing two notes that carry one page id, the way the app does |
| `session.rs` | the transport: streams, message limits, silence |
| `protocol.rs` | wire messages |
| `pairing.rs` | pairing codes and claims |
| `state.rs` | identity, paired devices, baselines, lineage, ancestors, spool |
| `mod.rs` | endpoint, peer tasks, Tauri commands |
| `e2e/` | real devices over loopback: scenarios, and seeded random work (fuzz) |
| `plan.rs` | what a round does, for both devices at once (pure) |
| `merge.rs` | three-way merge of a note |
| `preview.rs` | a plan as someone can read it before it runs, and a note's line diff |
| `apply.rs` | carrying out one device's half of a plan, safely |
| `round.rs` | one round, from both ends: the coordinator and the device answering |
| `ids.rs` | repairing two notes that carry one page id, the way the app does |
| `session.rs` | the transport: streams, message limits, silence |
| `protocol.rs` | wire messages |
| `pairing.rs` | pairing codes and claims |
| `nearby.rs` | finding devices on the same network over mDNS |
| `state.rs` | identity, paired devices, baselines, lineage, ancestors, spool |
| `mod.rs` | endpoint, peer tasks, Tauri commands |
| `e2e/` | real devices over loopback: scenarios, and seeded random work (fuzz) |

`plan.rs` has no disk or network access (three manifests and two lineages in, operations out), so
its rules are tested as cases, and `e2e/` tests them again as whole rounds between real devices.
Expand Down Expand Up @@ -316,6 +317,27 @@ path.
doesn't. Prompts are handled one at a time and recheck the secret, so a second device with the
same code is refused as "used or replaced". If a freshly pasted code is refused, the claiming
device removes the pairing.
- **Nearby, without a code:** works like Bluetooth, with two switches under Settings → Sync →
"Pair with a nearby device":
- **Discoverable** (`sync_set_discoverable`) lets other devices on the network learn this one's
name (`Request::Introduce`) and ask to pair (`Knock`, a `Claim` with no secret). Off, both are
refused (`not_discoverable`). It is never saved: leaving the Sync page turns it off, and so do
a reload and a restart.
- **Find nearby devices** (`sync_search_nearby`) asks every device heard over mDNS who it is,
every `SEARCH_EVERY` for `SEARCH_WINDOW` (or until Stop or leaving the page), and lists the
discoverable ones. Searching doesn't make a device discoverable, nor the other way round.

Picking a device on the list sends the `Knock`, and the device picked asks **"Pair with
<name>?"** exactly as for a code. That click is the whole gate, so:
- Only a discoverable device takes a knock, and only from a device it hears over mDNS, so nobody
can ask from across the internet, or from the network while nobody is pairing.
- Both devices show the same six **check digits** (`pairing::check_digits`, from both endpoint
ids) while the prompt is up. Names are whatever a device calls itself, so a stranger can take
yours; matching digits mean each device is talking to the other. Allow only if they match.
- One turned down can't knock again until `COOLDOWN` is over (`Throttle::block`), and unanswered
knocks count toward `MAX_ATTEMPTS`, so a stranger can't keep a prompt on screen.
- The pairing code isn't involved and isn't rotated.

- **Allowlist:** iroh authenticates the peer's key before any data is read, so a paired id is
allowed and anything else isn't. An unpaired peer can only send a `Claim` (capped at
`MAX_CLAIM`).
Expand All @@ -327,6 +349,29 @@ path.
synced folder. A malformed `sync.json` is reported, not regenerated, because regenerating would
change the device's identity.

## Devices on the same network

A device is dialed by its endpoint id. The addresses saved at pairing go stale as soon as either
device changes network, and iroh's own lookup (n0's DNS and relay) needs the internet. So with sync on,
each device also announces itself over mDNS (`nearby.rs`) as `_set-sync._udp.local`, under its own
service name rather than iroh's shared one so Set only hears from Set, and iroh resolves a paired
device's id from those announcements alongside DNS. Two devices on one Wi-Fi then sync directly, with
no internet, wherever DHCP has moved them.

- **Nothing more is trusted.** An announcement is an endpoint id and its addresses: a pairing code
without the secret. Being nearby grants nothing; the allowlist and someone clicking Allow are
still the only ways in (see Pairing and trust).
- **What it reveals.** Anyone on the network can see that a Set device is there and its endpoint id,
which is stable. The device name isn't in the announcement; another Set device on the network can
learn it only while this one is discoverable.
- **Optional.** Where the network allows no multicast, or macOS hasn't been allowed Local Network
access (`NSLocalNetworkUsageDescription` in `Info.plist`; refused sends fail with "No route to
host"), the lookup is logged as `sync.mdns_unavailable` or simply hears nothing, and sync goes
through the relay as before.
- **Shown.** A paired device announcing itself is marked `nearby` in its status, and Settings → Sync
says "on this network" beside it. Pairing with a device found this way is under Pairing and
trust.

## Transport

- Incoming streams are each served on their own task (`session::serve_streams`). At most `IN_FLIGHT`
Expand Down
61 changes: 59 additions & 2 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ iroh = "1"
# pair mismatched lines. Fixed in Cargo.lock; if `cargo update` brings it back, `cargo update -p
# wmi`. scripts/check-lockfile.mjs guards it.

# Paired devices on the same network, found without the relay (src/sync/nearby.rs).
iroh-mdns-address-lookup = "0.6"
tokio-stream = "0.1"
# `EndpointTicket`: endpoint id plus how to reach it, as one pasteable string.
iroh-tickets = "1"
postcard = { version = "1", features = ["alloc"] }
Expand Down
9 changes: 8 additions & 1 deletion src-tauri/Info.plist
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- NSMicrophoneUsageDescription is required: macOS kills a process that opens an input device without it. See src/dictation/audio.rs. -->
<!-- NSMicrophoneUsageDescription is required: macOS kills a process that opens an input device without it. See src/dictation/audio.rs.
NSLocalNetworkUsageDescription is the Local Network prompt's text. Until it is allowed, sync's mDNS sends fail with "No route to host" and paired devices are only reached through the relay. See src/sync/nearby.rs. -->
<plist version="1.0">
<dict>
<key>NSMicrophoneUsageDescription</key>
<string>Set uses the microphone for dictation. Your voice is transcribed on this Mac and never leaves it.</string>
<key>NSLocalNetworkUsageDescription</key>
<string>Set finds your paired devices on this network so it can sync with them directly.</string>
<key>NSBonjourServices</key>
<array>
<string>_set-sync._udp</string>
</array>
</dict>
</plist>
3 changes: 3 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,9 @@ pub fn run() {
sync::sync_regenerate_pairing_code,
sync::sync_answer_pair,
sync::sync_pair,
sync::sync_search_nearby,
sync::sync_set_discoverable,
sync::sync_pair_nearby,
sync::sync_unpair,
sync::sync_now,
sync::sync_answer_preview,
Expand Down
Loading
Loading