Skip to content

v1.18.0: contacts, socket activation, reconnect overhaul - #28

Merged
bethropolis merged 25 commits into
mainfrom
dev/next
Sep 15, 2026
Merged

bethropolis merged 25 commits into
mainfrom
dev/next

Conversation

@bethropolis

@bethropolis bethropolis commented Sep 15, 2026

Copy link
Copy Markdown
Owner

Highlights

  • Contacts plugin: sync and browse the phone address book (kcd contacts), counts-only watch events, per-device opt-in, wiped on unpair. (Phonebook/Contacts? #27 )
  • Socket activation: daemon starts on first client connection (kcd.socket), zero idle cost; old always-on service still supported.
  • Reconnect overhaul: persisted dial targets (LastIP/LastPort), dial sighted address for pairs, roam-aware backoff reset, mDNS advertised for daemon lifetime, UDP broadcast while a pair is offline.
  • Pairing hardening: side-channel peer verified against paired cert, constrained auto-accept (--expected-fingerprint, --known-only).
  • Battery: unknown state omitted until the first real packet (absent means unknown, not 0%).
  • Packaging: socket units shipped in tarballs, deb/rpm, and AUR; AUR post-install hook removed (documented instead).

Validation

  • CGO_ENABLED=0 go test -p 1 -count=1 ./... green, golangci-lint 0 issues, static binary verified.
  • goreleaser release --snapshot succeeds with kcd.socket confirmed in tarballs, deb, rpm, and rendered AUR PKGBUILD.

Only open outbound TCP on discovery for paired devices, active pairing
mode, or explicit 'kcd pair <id>' intent (one-shot dial + on-demand pair
request via last-seen discovery address). Discovered strangers are still
recorded as UNPAIRED (new-device default, with UNKNOWN migration) but
never dialled. '--connected' and clipboard/findmyphone auto-pick now mean
paired+connected; client docs require state==PAIRED for auto-select.
…ped names

pair_listen now only reports the candidate instead of auto-accepting, so
stale requests can no longer pair silently. 'kcd pair' asks Accept [y/N]
(default reject); -y/--yes keeps headless usage working.

Some senders transmit non-ASCII names as decimal byte escapes; decode them
to real UTF-8 on receipt, truncate runewise, and migrate stored names on
daemon load.
An unpaired stranger gets one ephemeral TCP dial per unpaired era so the
identity exchange makes both sides visible, closed again on the next
sighting while still unpaired. No timers or polling: a marker suppresses
re-dials, pairing mode and explicit pair intent bypass, unpair/reject
re-arms, and broadcast stop drops leftover unpaired connections.

kcd pair listen also retries past the daemon 60s timeout until Ctrl+C.
Liveness stays on the CONNECTED column; the flag now hides only unpaired
strangers instead of offline paired phones.
Add Report() accessor on the connectivity plugin, emit a cached
connectivity.update in the watch initial-state dump, and add a
kcd connectivity [--json] [device-id] query path (IPC command, daemon
route, client method) rendering per-SIM signal bars.
Add a state.snapshot event on watch connect covering all known devices
with cached battery/media/signal so clients boot from one connection.
Expose posAnchorMs on NowPlaying for drift-free position math and flag
in-flight album art with artPending plus an empty URL instead of an
unloadable kdeconnect:/ URI. Enrich devices --json via an IPC-only
DeviceSummary (disk shape unchanged) and add --json to battery, volume
list, sftp info/volumes with fish completions.

this is  basically to make it easier for external clients
last_seen is stamped at connect time and discovery sightings skip
connected devices, so the raw stamp goes stale for the whole session
and clients show "Xm ago" for a live phone. SummarizeDevice now
reports now for connected devices; stored stamp, prune, and
devices.json semantics untouched.
Paired sightings no longer trust unauthenticated discovery for dial
targets or names; redial LastIP:LastPort with per-device throttle.
Clamp discovery ports, throttle ephemeral dials, extend pair intent
past the one-shot trigger, and strip control/shell chars from names.
receiveAlbumArt could complete after a track change and publish the old
cover against the new track. Guard the attribution on matching art URL
and player; bytes stay cached for the new track's own request.
Bound MMS attachment streams with LimitReader and a 50MB cap.
Restrict notification IDs to filename-safe characters and confine
icon cache paths to the cache dir. Pass -- to notify-send and wtype
so remote text can't be misparsed as flags, and harden SMS
attachment filenames.
…ecutable auto-open

Strict user/host/port/path validation for the sshfs argv so remote
values can't be reinterpreted as ssh options. Only http/https URLs
reach xdg-open; anything else is logged and skipped. Never auto-open
executable file types (auto_open stays off by default).
Side-channel TLS used the same trust-any-cert profile as the main link
but never checked who presented it, so any holder of a cert could feed
bytes to a receiver or pull files from a sender. Pin each transfer to
the peer fingerprint snapshot at request time: receivers abort before
writing, senders abort before streaming. Unpinned (unpaired) devices
log a warning and proceed as before.
…-only flags

Bare pair --yes accepted the first device that asked, stranger or not.
Add --expected-fingerprint to pin the exact peer cert and --known-only
to restrict candidates to previously recorded devices; anything else is
rejected while listening continues. Print a warning when --yes runs
unconstrained. The listen result now carries the candidate fingerprint.
A fresh device published charge 0 / charging false, indistinguishable
from a genuine 0% reading — and at steady charge no later packet ever
corrected it. Track first-receipt per device and leave the battery
object out of summaries, the watch bootstrap dump, and the battery
query (which now fails closed) until then. Summaries carry batteryAgeMs
so clients can apply staleness rules.
… activation

The daemon adopts a systemd-passed IPC listener (LISTEN_FDS, stdlib
only) when configured with the default socket path, so any client
command — including watch — summons a stopped daemon instead of
retrying against an absent socket. Custom socket paths and
non-systemd systems keep the self-bind path. The socket unit owns the
runtime directory (declaring it in both units made service stops
delete the socket file) with mode 0600, and the installer enables the
socket instead of the always-on service.
LastIP/LastPort lived only in memory, so every daemon restart left
paired auto-dial with nothing to dial — reconnect waited entirely on
the phone connecting inbound while we advertise nothing. Store the
target in the device state file (validated on load; the file is
user-editable) and restore it at startup. Pure state, no new timers.
On hearing a paired phone, kcd redialed the remembered LastIP and
ignored the fresh source address in the sighting — so a phone that
roamed to a new DHCP address triggered dials to the dead old one
while the proof of its new address sat in the packet. Dial the
sighted ip:port (throttled as before); whoever answers must still
present the paired certificate or setup fails, so spoofed sightings
cost a dial, never a session. LastIP stays the fallback for silent
peers via the backoff loop.
A sighting from an address other than the failing target proves the
old backoff was aimed at a dead address, so later cycles restart at
the floor. Same-address sightings leave the counter alone, preserving
flap protection for a dying peer. No new timers — pure counter state.
…le broadcasting

mDNS registration lived inside the UDP broadcast loop, which is
stopped by default — so a quiet daemon was invisible to phones on
both discovery channels. Registration is responder-only (no idle
timers), so it now runs from daemon startup at negligible cost while
UDP broadcast keeps its on-demand lifecycle.
Run the UDP broadcaster while any paired device is disconnected so
roamed or restarted phones can find us back; fully stopped otherwise,
so connected steady state keeps zero timers. Starts are
reference-counted per owner, so reconnect needs and kcd pair neither
start nor stop each other's loop. Driven by connect/disconnect events
plus a startup sync — no polling.
Request UID/timestamp lists, fetch vCards for new or changed contacts,
and cache per-device .vcf files plus an index sidecar. Syncs trigger on
connect and via kcd contacts sync; kcd contacts list reads the cache
(empty when never synced — unknown, never zero). Unpairing wipes the
cached address book.

Defensive handling throughout: UIDs are filename-sanitized and
confined, empty UID lists never wipe the cache, response sizes are
capped, string and numeric timestamps both parse, display fields are
stripped of control characters, cache files are 0600, and the update
event carries counts only.
Add packaging/kcd-pkg.socket (distro variant without the
graphical-session tie-in) and include both socket units in the
tarballs, deb/rpm contents, and AUR package body. kcd-pkg.service
no longer declares RuntimeDirectory=kcd so stopping the service
can't tear the socket out from under the socket unit; the daemon
self-creates the dir in non-socket mode.

Drop the AUR post-install hook (packaging/kcd-bin.install);
post-install enablement is documented in the README and release
notes instead.

Point deb/rpm postinstall and the uninstall script at kcd.socket,
and document the one-command wake (paired phones reconnect on
their own afterwards). Also fix the Hyprland keybind snippet for
the Lua config format (hl.bind) and correct stale constructor
signatures in AGENTS.md.
Handle spawns sendDesktopNotification in a goroutine, so the fake's
calls slice was written off-test-goroutine while TestNotifySendEndsOptions
read it directly (and argFor read it unlocked). Guard argFor with the
existing mutex, add a locked lastCall snapshot, and poll it via waitFor
instead of a fixed sleep.
goreleaser check fails on the deprecated property and CI gates on it.
The verified hint is cosmetic; removing it changes nothing about the
published cask.
@bethropolis
bethropolis merged commit 8126626 into main Sep 15, 2026
9 checks passed
@bethropolis
bethropolis deleted the dev/next branch September 15, 2026 23:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant