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
2 changes: 1 addition & 1 deletion Tests/checks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ exit status is the number of checks that failed. Binaries, data and logs go to
| `policy` | `iOSClient/DiscoveryPolicy.swift`, with `main.swift`, `home.swift`, `sleep.swift` and `records.swift` | when the device looks nearby, its rows and their words, the route word, the wired dial, reconnects, the move off AWDL, a session following the best path (the cable, Wi-Fi, Direct), the remote rule and a remote session's move home (`moveHome`; `moveHomeTrust`: pinned TLS only; `moveHomeRow` and `HomeRows`: which row, by row, a refused row or a plain one never keeping the session from the Mac's own, with a model of the glue; `HomeMoveInfo`: the move's kind 18 held to the floor taken as it starts, the broadcast race among the cases); pairing at home's session rules (`home.swift`: the pin, the trust a dial starts, how a session at home ends, the ask's answer, the words; the reconnect of an unsaved Mac on an open door pinned to the key its lost session saw, with a model of the glue over every sequence of up to four rows under its name, A's or another key's on an open door, a door that asks devices to pair or a plain one, in Release and DEBUG: the security scan's finding 3, 2026-10-29; the plain one never dialed after that session, where a DEBUG build's reconnect had dialed it, and the person's tap whose dial saw no key between the rows, after which the reconnect pins the key the last session under the name saw, where it took any: the review of #255); issue #69's (`sleep.swift`: the browsers made again after the background and after a failure, what a browser made again shows while it settles or waits, a working cable session across one (its pong stamped at the return), the automatic reconnect's dials of a row that stays listed and their words, with models of a host stopped, a Mac a sleep proxy answers for, a new listing and the background; the Desktop asked for again after the Mac stopped it); a row's TXT record kept while its result stays listed (`records.swift`: the cable pulled, 2026-10-27, through the rows, their words, the sightings and the Remote rows; a browser made again, which starts afresh: a service that took the name while Sill was in the background reads by its name alone, the Remote row kept, no `newKey`; one that failed and waits; a DEBUG test row beside a result of its name, and two of one name; a name in two domains; 5,000 random sessions with rebuilds against a model) | 482 | 176 |
| `policy` | `iOSClient/DiscoveryPolicy.swift`, with `main.swift`, `home.swift`, `sleep.swift` and `records.swift` | when the device looks nearby, its rows and their words, the route word, the wired dial, reconnects, the move off AWDL, a session following the best path (the cable, Wi-Fi, Direct), the remote rule and a remote session's move home (`moveHome`; `moveHomeTrust`: pinned TLS only; `moveHomeRow` and `HomeRows`: which row, by row, a refused row or a plain one never keeping the session from the Mac's own, with a model of the glue; `HomeMoveInfo`: the move's kind 18 held to the floor taken as it starts, the broadcast race among the cases); pairing at home's session rules (`home.swift`: the pin, the trust a dial starts, how a session at home ends, the ask's answer, the words); issue #69's (`sleep.swift`: the browsers made again after the background and after a failure, what a browser made again shows while it settles or waits, a working cable session across one (its pong stamped at the return), the automatic reconnect's dials of a row that stays listed and their words, with models of a host stopped, a Mac a sleep proxy answers for, a new listing and the background; the Desktop asked for again after the Mac stopped it); a row's TXT record kept while its result stays listed (`records.swift`: the cable pulled, 2026-10-27, through the rows, their words, the sightings and the Remote rows; a browser made again, which starts afresh: a service that took the name while Sill was in the background reads by its name alone, the Remote row kept, no `newKey`; one that failed and waits; a DEBUG test row beside a result of its name, and two of one name; a name in two domains; 5,000 random sessions with rebuilds against a model); the Desktop that did not start (`sleep.swift`, 2026-10-28: after the device's own request a list within the 10 s spacing that still says nothing streams sets one re-check when the spacing ends, unless the person chose since; `autoDesktopRecheck` when it comes due; a model of the glue: the black screen's lists asked again at 10 s where the rule before stayed black, a Mac that keeps failing every 10 s and never faster, a person's pick, a late answer, a Desktop stopped within the spacing and after it, the session's end) | 486 | 176 |
| `protocol` | `Sources/StreamProtocol/*.swift`, then `crosscheck.py` | the address parser, SafeText, pairing codes and proofs, tags, the Mac ID, the certificate, kind 18's signature, framing (kind 32 the clipboard's, 29 to 31 and 33 unknown), kind 43's two types (every malformed payload refused, 2,000 random ones never trapping), and TLS 1.3 with pinned keys on loopback, the two ALPNs every host offers for good among them, and Reverse Scrolling's direction (`ScrollDirection`, issue #143: a finger's drag over the picture and two fingers on the on-screen trackpad turn around while the switch is on, their coast too; a connected trackpad's and a wheel's scroll never; nothing while it is off); the cross-check repeats the certificate and signature with Python and `/usr/bin/openssl` | 234 + 8 | 42 |
| `remote-rules` | `iOSClient/DiscoveryPolicy.swift`, `RemoteDialPolicy.swift`, `SavedMacs.swift` with `Sources/StreamProtocol` (`build.sh`), with `main.swift`, `home.swift` and `launch.swift` | the Remote rows and automatic remote dial, the order a saved Mac's addresses are tried in, what a failure means, saved Macs (DNS names distinguished from IP ranges, and typed destinations kept at their effective ports across a key change); pairing at home's model, a look-alike under a saved Mac's name among it, and the saved Macs' home fields (`home.swift`); the device key's rules (`DeviceKeyPolicy`) case by case, then a launch, a pairing, a SIGKILL and the next launch as StreamClient and DeviceIdentity chain them, through a stand-in Keychain and UserDefaults with each way either fails (a restore from another device's backup, a key never kept, kept and never found again, a failed read at launch or at a pairing, a lost write, unreadable defaults), and 4,000 random lives against the invariants (`launch.swift`, #140) | 172 | 88 |
| `remote-rules` | `iOSClient/DiscoveryPolicy.swift`, `RemoteDialPolicy.swift`, `SavedMacs.swift` with `Sources/StreamProtocol` (`build.sh`), with `main.swift`, `home.swift` and `launch.swift` | the Remote rows and automatic remote dial, the order a saved Mac's addresses are tried in, what a failure means, saved Macs (DNS names distinguished from IP ranges, and typed destinations kept at their effective ports across a key change); pairing at home's model, a look-alike under a saved Mac's name among it, and the saved Macs' home fields (`home.swift`); the device key's rules (`DeviceKeyPolicy`) case by case, then a launch, a pairing, a SIGKILL and the next launch as StreamClient and DeviceIdentity chain them, through a stand-in Keychain and UserDefaults with each way either fails (a restore from another device's backup, a key never kept, kept and never found again, a failed read at launch or at a pairing, a lost write, unreadable defaults), and 4,000 random lives against the invariants (`launch.swift`, #140); the dial failure's words where either end's VPN may be off (#309, `RemoteCopy`) | 179 | 90 |
| `policy` | `iOSClient/DiscoveryPolicy.swift`, with `main.swift`, `home.swift`, `sleep.swift` and `records.swift` | when the device looks nearby, its rows and their words, the route word, the wired dial, reconnects, the move off AWDL, a session following the best path (the cable, Wi-Fi, Direct), the remote rule and a remote session's move home (`moveHome`; `moveHomeTrust`: pinned TLS only; `moveHomeRow` and `HomeRows`: which row, by row, a refused row or a plain one never keeping the session from the Mac's own, with a model of the glue; `HomeMoveInfo`: the move's kind 18 held to the floor taken as it starts, the broadcast race among the cases); pairing at home's session rules (`home.swift`: the pin, the trust a dial starts, how a session at home ends, the ask's answer, the words); issue #69's (`sleep.swift`: the browsers made again after the background and after a failure, what a browser made again shows while it settles or waits, a working cable session across one (its pong stamped at the return), the automatic reconnect's dials of a row that stays listed and their words, with models of a host stopped, a Mac a sleep proxy answers for, a new listing and the background; the Desktop asked for again after the Mac stopped it); a row's TXT record kept while its result stays listed (`records.swift`: the cable pulled, 2026-10-27, through the rows, their words, the sightings and the Remote rows; a browser made again, which starts afresh: a service that took the name while Sill was in the background reads by its name alone, the Remote row kept, no `newKey`; one that failed and waits; a DEBUG test row beside a result of its name, and two of one name; a name in two domains; 5,000 random sessions with rebuilds against a model) | 464 | 165 |
| `protocol` | `Sources/StreamProtocol/*.swift`, then `crosscheck.py` | the address parser, SafeText, pairing codes and proofs, tags, the Mac ID, the certificate, kind 18's signature, framing (kind 32 the clipboard's, 29 to 31 and 33 unknown), kind 43's two types (every malformed payload refused, 2,000 random ones never trapping), and TLS 1.3 with pinned keys on loopback, the two ALPNs every host offers for good among them, and Reverse Scrolling's direction (`ScrollDirection`, issue #143: a finger's drag over the picture and two fingers on the on-screen trackpad turn around while the switch is on, their coast too; a connected trackpad's and a wheel's scroll never; nothing while it is off), and Aa's detents (`TextScale`, issue #179: Off a detent of its own, every detent back to its place and only Off no scale, what leaving a scale for Off means to the host, Off offered unless the window streams on the Mac's virtual display (while a window streams its running stream decides, the Virtual Display setting otherwise: over the Desktop or nothing, whatever their stream says), Aa usable only while a window streams); the cross-check repeats the certificate and signature with Python and `/usr/bin/openssl` | 272 + 8 | 61 |
| `remote-rules` | `iOSClient/DiscoveryPolicy.swift`, `RemoteDialPolicy.swift`, `SavedMacs.swift` with `Sources/StreamProtocol` (`build.sh`), with `main.swift` and `home.swift` | the Remote rows and automatic remote dial, the order a saved Mac's addresses are tried in, what a failure means, saved Macs (DNS names distinguished from IP ranges, and typed destinations kept at their effective ports across a key change); pairing at home's model, a look-alike under a saved Mac's name among it, and the saved Macs' home fields (`home.swift`) | 144 | 76 |
Expand Down
24 changes: 24 additions & 0 deletions Tests/checks/remote-rules/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,30 @@ check("remote order: distinct names keep the pairing date's order", SavedMacs.re
// Pairing at home (docs/home-pairing-plan.md): the home model and the saved Macs' home fields, in home.swift.
homeRemote()

// #309: where the device can't tell which end's VPN is off, the words name both, never only this device.
let wgC = R.Candidate(host: "10.8.0.2", port: 7455, kind: "vpn", via: "")
check("#309: Tailscale off, words for both ends",
RemoteCopy.dialFailure(.vpnOff, mac: "Mac mini", candidate: vpnC, vpnName: "Tailscale", device: "iPhone")
== "Tailscale looks off on this iPhone or on Mac mini. Turn it on on both, then tap Mac mini.")
check("#309: a Tailscale name: the same words",
RemoteCopy.dialFailure(.vpnOff, mac: "Mac mini", candidate: tsName, vpnName: "Tailscale", device: "iPad")
== "Tailscale looks off on this iPad or on Mac mini. Turn it on on both, then tap Mac mini.")
check("#309: a VPN with no service name",
RemoteCopy.dialFailure(.vpnOff, mac: "Studio", candidate: wgC, vpnName: nil, device: "iPhone")
== "Your VPN looks off on this iPhone or on Studio. Turn it on on both, then tap Studio.")
check("#309: no candidate known",
RemoteCopy.dialFailure(.vpnOff, mac: "Studio", candidate: nil, vpnName: nil, device: "iPhone")
== "Your VPN looks off on this iPhone or on Studio. Turn it on on both, then tap Studio.")
check("#309: a Tailscale name not found asks about both ends",
RemoteCopy.dialFailure(.nameNotFound, mac: "Mac mini", candidate: tsName, vpnName: "Tailscale", device: "iPhone")
== "This iPhone can’t look up mac.tail1.ts.net. Is Tailscale on, here and on Mac mini?")
check("#309: another name not found: as before",
RemoteCopy.dialFailure(.nameNotFound, mac: "Mac mini", candidate: nameC, vpnName: nil, device: "iPhone")
== "This iPhone can’t look up home.example.net.")
check("#309: no answer over Tailscale: the Mac's end, as before",
RemoteCopy.dialFailure(.noAnswer, mac: "Mac mini", candidate: vpnC, vpnName: "Tailscale", device: "iPhone")
== "Mac mini didn’t answer. It may be asleep, or Tailscale may be off on the Mac.")

// #140: the launch, a pairing, a SIGKILL and the next launch with each Keychain and UserDefaults fault, in launch.swift.
launchModel()

Expand Down
3 changes: 3 additions & 0 deletions Tests/checks/remote-rules/mutants.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
(D, "sightings: a leave dated at the first sighting", "for name in previous.since.keys where !listed.contains(name) { next.leftAt[name] = now }",
"for name in previous.since.keys where !listed.contains(name) { next.leftAt[name] = previous.since[name] }"),
(D, "flat retries", "static let remoteRetry: [Double] = [2, 4, 8, 10]", "static let remoteRetry: [Double] = [2, 2, 2, 2]"),
(R, "#309: VPN off blames this device alone", 'return "\\(name) looks off on this \\(device) or on \\(mac). Turn it on on both, then tap \\(mac)."',
'return "\\(name) looks off on this \\(device). Turn it on, then tap \\(mac)."'),
(R, "#309: a Tailscale name's lookup asks about this device alone", 'Is Tailscale on, here and on \\(mac)?"', 'Is Tailscale on?"'),
(R, "lastWorked ignored", " ordered.insert(ordered.remove(at: i), at: 0)\n", ""),
(R, "no dedupe", "return ordered.filter { seen.insert($0.key.lowercased()).inserted }", "return ordered"),
(R, "shared LAN not before internet", " ordered += lans.filter { shared($0) && !loopback($0) }\n ordered += internet.filter(\\.isName) + internet.filter { !$0.isName }\n",
Expand Down
3 changes: 2 additions & 1 deletion docs/DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -1515,7 +1515,8 @@ If it doesn't connect:
sleeping display, above), but it cannot wake a sleeping Mac:
for a Mac that should stay reachable, prevent automatic sleeping in System
Settings › Energy (or Battery).
- "Tailscale looks off on this iPad": turn it on on the device.
- "Tailscale looks off on this iPad or on ‹Mac›": turn it on on both; the device
can't tell which end is off (#309).
- Tailscale's Shields Up on the Mac blocks every incoming connection, and a
tailnet ACL must let the device reach the Mac on port 7455.
- Testing a port forward from inside your home network can fail on routers
Expand Down
6 changes: 3 additions & 3 deletions docs/remote-access-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -1581,8 +1581,8 @@ and types, `isExpensive`). Then, at every browser change and at every due time:
| wrong Mac | "This isn’t the Mac mini this ‹iPad› paired with. If Sill was set up again on it, forget it here and pair again." (no Connect Anyway) |
| revoked | "Mac mini no longer accepts this ‹iPad›. Pair it again from Sill’s Settings on the Mac." |
| refused | "Mac mini answered, but Sill isn’t accepting remote connections there. Check that Sill is open and Remote Access is on. If you changed its port, connect at home once." |
| VPN off here | "Tailscale looks off on this ‹iPad›. Turn it on, then tap Mac mini." ("Your VPN" when the address has no service name) |
| name not found | ".ts.net" names: "This ‹iPad› can’t look up mac-mini.tail1234.ts.net. Is Tailscale on?"; others: "This ‹iPad› can’t look up home.example.net." |
| VPN off here or on the Mac | "Tailscale looks off on this ‹iPad› or on Mac mini. Turn it on on both, then tap Mac mini." ("Your VPN" when the address has no service name; both ends, as the device can't tell which is off: #309) |
| name not found | ".ts.net" names: "This ‹iPad› can’t look up mac-mini.tail1234.ts.net. Is Tailscale on, here and on Mac mini?"; others: "This ‹iPad› can’t look up home.example.net." |
| not Sill | "Something answered at 203.0.113.9, but it isn’t Sill. Your home’s internet address may have changed." |
| didn't answer | "Mac mini didn’t answer. It may be asleep, or Tailscale may be off on the Mac." For a raw internet IP: "Mac mini didn’t answer at 203.0.113.9. If your home’s internet address changed, connect at home once to update it, or add an address name on the Mac." |
| Local Network | "To reach 192.168.1.20, allow Local Network for Sill in Settings." |
Expand Down Expand Up @@ -1756,7 +1756,7 @@ and types, `isExpensive`). Then, at every browser change and at every due time:
| Tailscale shields up, or ACLs that block 7455 | Dials time out with the "didn't answer" copy. The README's troubleshooting names both (an ACL must allow TCP 7455 to the Mac) |
| MagicDNS off on the device | The `.ts.net` name fails at once, and the IPs beside it are tried |
| A WireGuard VPN into the home router | The Mac lists "Wi‑Fi — 192.168.1.20" with its note. Away, the LAN address is dialled through the tunnel; the remote door classifies the routed private source as `lan` |
| The VPN off on the device | "Tailscale looks off on this ‹iPad›…"; the Remote row stays (it is a saved Mac) |
| The VPN off on the device or the Mac | "Tailscale looks off on this ‹iPad› or on ‹Mac›…"; the Remote row stays (it is a saved Mac) |
| The VPN off on the Mac | The pane shows "Tailscale — Not connected". The device: "didn't answer…" |
| The home's public IP changes | Names follow by themselves. A raw IP gets the "didn't answer at …" copy, and refreshes at the next home connection (verified kind 18) |
| Carrier-grade NAT or double NAT | The pane says so, in orange; a VPN is the way. Sill never opens router ports |
Expand Down
Loading