折腾了一下 Mac 和 Linux 的键鼠共享

本文最后更新于:2026年10月12日 晚上

桌上有一台 Linux 台式机,有显示器,也有自己的键盘鼠标。但平时大部分时间还是在用 Mac,每次想去台式机上点两下,就得换一套键鼠,多少有点麻烦。

于是想弄一个类似两台 Mac 之间通用控制的体验:鼠标往上一推,直接去操作上面的台式机;往下一拉,又回到 Mac。不想再买硬件,最好用现有的东西解决。

本来以为装个键鼠共享软件就结束了。然后想起来,校园网的 IP 会变;再然后又觉得,Mac 都被我拿走了,总不能还在控制桌上的电脑吧。

需求就这么一点点长出来了。这次拉上 Codex 一起折腾,最后是 Deskflow 加地址同步,再加一个用蓝牙判断远近的小程序。中间有几个坑还挺有意思,记录一下。

先让鼠标过去

这次的环境是 macOS 和 Arch Linux,台式机用 GNOME / Wayland,两端装的 Deskflow 都是 1.27.0。

Deskflow 做的事情很好理解:一台电脑提供键盘鼠标,另一台电脑接收输入。它有 macOS 和 Linux 版本,Wayland 支持会用到 libei 和 libportal,也支持剪贴板共享。

我的键盘鼠标在 Mac 这边,所以 Mac 是服务端,Linux 是客户端。这里的“服务端”容易让人下意识想到台式机,其实要看输入设备在哪边。

屏幕位置按真实摆放来配。下面是布局的最小示意,省略了其他选项;两端的屏幕名称需要和配置对应。

1
2
3
4
5
6
7
8
9
10
11
section: screens
mac:
desktop:
end

section: links
mac:
up = desktop
desktop:
down = mac
end

配好之后,鼠标从 Mac 屏幕上边缘出去,就应该进入台式机。两台机器还是各自运行自己的桌面,用的也是各自的显示器,只是共享了输入设备。

结果第一次试,日志显示已经连上,台式机却完全没反应。

连接成功,但没有权限控制

这个坑最容易让人往网络上想。端口通了,TLS 也握手成功了,怎么还是不能动?

最后发现 GNOME 的远程控制授权还没完成。台式机那边有一个“远程桌面 / 共享”的确认窗口,要允许键盘鼠标交互,再点共享。点完之后,才真的能控制。

在这套 Wayland 环境里,Deskflow 还要通过桌面提供的接口获得输入权限。Remote Desktop portal 的流程包括创建会话、选择输入设备、请求用户授权,授权之后才能接着建立 EIS 输入通道。官方的 Remote Desktop portal 文档 把这个流程写得很清楚。

所以这里至少有三件事要分别看:网络有没有通,对端身份有没有验证通过,桌面有没有允许它输入。前两件成功,不代表第三件也成功了。

后面保留了授权恢复状态,实际重启客户端时可以复用,没有每次都重新弹窗。不过最终还是得把鼠标推过去点一下,再敲几个字。盯着一行 connected,确实很容易提前宣布胜利。

校园网的 IP 不能写死

基础共享通了,下一件事是地址。

这两台机器的私网 IP 都可能变化。我本来就有一条通过内网穿透访问台式机的 SSH 通路,它有一个固定入口。正好可以用它告诉台式机:“Mac 现在换到这个地址了。”

最后把事情拆成了两条路:

内容 走哪里
鼠标、键盘和剪贴板数据 两台机器之间可直达的校园网地址,使用 Deskflow TLS
Mac 当前地址、是否在桌旁 原有 SSH 通道

台式机主动连接 Mac,所以客户端需要知道的是 Mac 的当前地址。台式机自己的私网地址变了,由原有的穿透通路承担管理入口的稳定性;前提是那条通路还能正常连上。

Mac 这边用一个 LaunchAgent,每 20 秒检查一次物理网卡地址。地址有变化就通过 SSH 同步;没变化且上次成功还不到 5 分钟,就跳过这次同步。这样不用每隔 20 秒都通过 SSH 再做一轮远端验证。即使执行同步,地址没变、客户端也正常时,就让它继续运行。

这里专门取物理网卡的地址,避免把 VPN、代理之类虚拟接口的地址一起塞过去。台式机收到的也只是候选列表,要自己验证能不能用。

IP 是位置,证书才是身份

如果只是探测一下 24800 端口能不能连接,再把这个 IP 写进配置,其实还差一步。

DHCP 可能把原来的地址分给别的设备。那个设备刚好也开了一个服务,并不能说明它就是我的 Mac。

所以接收端会先做 TLS 握手,取出服务端证书,计算 SHA-256,和已经信任的 Deskflow 证书指纹对照。通过了才更新 remoteHost,并原子替换配置。第一次建立信任时,两端指纹通过已有 SSH 通道核对;地址同步器本身没有“自动信任新证书”的权限。

大致就是:

1
2
3
4
5
6
7
Mac 当前地址
↓ 通过已有 SSH 通道发送候选列表
台式机逐个连接候选地址的 Deskflow 端口
↓ TLS 握手,核对已信任的证书指纹
更新客户端地址
↓ 确有需要,且距离许可有效时
重连 Deskflow

这次用的是 Deskflow 的自签名证书,探测时靠预先固定的指纹确认身份。跳过系统 CA 和主机名校验之后,这一步就更不能省了。

还有个不起眼的小坑:探测完最好正常结束 TLS 会话,发送 close_notify。粗暴关掉连接,可能把服务端日志弄得像一直有连接故障。健康检查本来是来帮忙的,没必要顺手制造一堆噪声。

当然,这套方法只解决“地址变化”。我这里实测两台机器之间的 TCP 可以直达;如果校园网启用了客户端隔离,换成正确 IP 也过不去,那是另一层网络问题。

既然都折腾了,再让它知道我在不在旁边

键鼠共享能用了之后,又冒出来一个需求:Mac 拿离桌面大概一两米时暂停,拿回来自动恢复。

拿 ping 来判断肯定不合适。人都走远了,机器仍然可能在线;连着同一个校园网,也不能说明就在同一张桌子旁边。

最后用了两台机器自带的蓝牙。台式机通过 BlueZ 广播一个专用 BLE Service UUID,Mac 上写一个 Swift 菜单栏程序,用 Core Bluetooth 扫描这个 UUID,记录信号强度 RSSI。

蓝牙在这里负责提供远近线索,键鼠数据仍然走网络。广播也没有带机器名,没有为了这件事去做一套蓝牙键鼠配对。BlueZ 的广播属性可以参考它的 LEAdvertisement 接口文档。

Mac 菜单栏会显示“⌨ 近”或者“⌨ 暂停”,方便一眼看到判断结果。只保留目标信号最近的一小段样本,用来校准和排查。

RSSI 就别硬换算成米了

我真正需要的是区分“在桌旁”和“已经拿走”,没有必要显示一个看起来很精确的 1.37 m。

实际校准时,先把 Mac 放在平时的位置采样,再拿到希望开始暂停的位置采样,最后放回来。这次平滑读数的中位数大致是:

摆放位置 RSSI
最开始放在桌旁 -46 dBm
拿到希望暂停的位置,约 1–2 米外 -60 dBm
放回桌旁 -45 dBm

这些是这次桌面摆放下的读数。换个朝向、身体挡一下、移动天线,结果都可能变,不能把某个 dBm 数字当成通用的“两米线”。

如果只设一个阈值,例如低于 -53 就断开、高于 -53 就连上,信号在边缘跳一跳,连接也跟着反复开关,用起来应该会很暴躁。

最后用了三个小处理:

  1. 最多保留最近 5 个样本,再排除超过 3 秒的样本,取中位数。
  2. 靠近和离开使用不同阈值:达到 -51 dBm 才考虑恢复,降到 -55 dBm 才考虑暂停。
  3. 新判断持续约 3 秒才切换;落在两个阈值之间,保持原来的状态。

这就是一个带迟滞和时间确认的状态机。下面是简化的伪代码,confirm 还要处理候选状态切换和持续时间,不能直接当完整程序运行:

1
2
3
4
5
6
7
8
9
10
11
12
recent = [s for s in last_five_samples if now - s.time <= 3]

if not recent:
reset_to_far() # 没有新证据,暂停
else:
rssi = median(s.rssi for s in recent)
if rssi >= -51:
confirm(near=True, seconds=3)
elif rssi <= -55:
confirm(near=False, seconds=3)
else:
cancel_candidate() # 取消切换计时,保持当前状态

完整实现里,恢复还要求确认期间确实收到过后续样本,不能靠一条强信号等上 3 秒就放行。3 秒窗口里已经没有样本时会先暂停;超过 5 秒没有新信号,会进一步清空历史状态,重新确认。蓝牙关闭、休眠或会话不再激活,也会撤销本机的靠近判断。

这两个时间不是“最后一次信号之后再多等 3+5 秒”,它们是不同的检查条件。

最折腾的居然是采样间隔

开始试的时候,Mac 明明一直在桌上,却会时不时变成暂停。

一看记录,有时十几秒都没有新的发现回调。原来的广播节奏比较慢,试着调到 100–150 毫秒之后,仍然出现过几秒的样本空档。对于一个要在几秒内判断离开的程序,这已经足够触发“信号丢了”。

最后把这台台式机的广播间隔配置调到 20 毫秒。随后约 45 秒的观察里,Mac 收到样本的中位间隔约 0.26 秒,最大间隔不足 1 秒,桌旁判断才稳定下来。

这里有个区别很容易漏掉:配置的广播间隔,不等于应用实际收到回调的间隔。 中间还隔着控制器、扫描调度和系统回调。调成 20 毫秒,也没有获得每秒 50 次的距离读数。

20 毫秒是这次环境里试出来的配置,功耗和无线占用还没有单独做对照测量。复用这个思路时,先记下实际样本时间,看有没有长空档,比直接把阈值改得越来越宽有用。

离开时发一句“断开”,还不够

接下来是我觉得这次最有意思的部分。

直觉上可以让 Mac 在判断离开时,通过 SSH 发一条命令停止 Deskflow。问题是,最需要它断开的时候,也最可能发不出这条命令:Mac 合盖了,网络断了,程序退出了,或者进程卡住了。

如果台式机只等那句“我走了”,就可能一直等不到。

所以改成短期租约:Mac 每 2 秒发送一次当前是否靠近,台式机收到有效的 near: true 才续约。租约有效期 8 秒,Linux 守卫每秒检查一次,没有有效租约就停止 Deskflow 客户端。

1
2
3
4
5
Mac 持续确认靠近 ── 每 2 秒续约 ──> Linux 允许客户端运行

Mac 判断离开 ── near: false ──> 撤销许可
SSH 输入流结束 ── EOF ──────────> 撤销本连接的许可
不再收到续约 ── 租约过期 ─────> 撤销许可

这样就不依赖最后一条断开通知一定送达。正常情况下可以主动撤销;通知没送到,旧许可也会自己过期。

租约的时间由 Linux 收到消息时自己记录,用包含休眠时间的单调时钟计时,并绑定当前启动的 boot_id。两台电脑不需要对表,也不用担心调整系统日期把有效期拉长。许可放在用户运行时目录,守卫启动前还会主动清掉旧租约。

SSH 重连时还有一个小细节:每条连接有自己的 owner。旧连接退出时,只能撤销自己那份许可,不能顺手把新连接刚写好的租约删掉。不然网络抖动一下,老连接收尾反而会把新连接踢下线。

Mac 的心跳线程也不能无条件重复发送缓存里的 near=true。如果负责蓝牙和界面的主循环卡住,心跳线程还活着,旧判断就可能被无限续期。因此发送前还会独立检查:本机状态是不是最近更新的,样本有没有过期。

写到这里,一个“拿走就断开”的小需求,已经开始长出分布式系统的形状了。

守卫自己卡住怎么办

Linux 守卫负责停客户端,那守卫自己出问题时,也得有人管。

这里交给 systemd。守卫用 Type=notify 配合 WatchdogSec=8,在正常工作时通知 systemd 自己还活着。客户端则通过 BindsTo 和 After 依赖守卫,启动前再用 ExecCondition 检查许可。相关语义可以看 systemd 的 service 配置说明 和 unit 依赖说明。

下面是客户端 drop-in 的关键部分;它依赖前面已经部署好的守卫脚本:

1
2
3
4
5
6
7
[Unit]
BindsTo=deskflow-proximity-guard.service
After=deskflow-proximity-guard.service

[Service]
ExecCondition=/usr/bin/python3 %h/.local/lib/deskflow-link/proximity_guard.py --check
TimeoutStopSec=3

只写 Restart=always 还不够。进程卡死但没有退出,重启策略不会凭空知道它已经不工作。watchdog 超时后,systemd 才能处理这个仍然占着 PID 的进程,再通过依赖关系停掉客户端。

守卫重新起来,也不会直接沿用先前那份“靠近”。它先清掉许可,等 Mac 提供新的有效状态。

别让另一个脚本偷偷开回去

加完距离守卫,还要回头看之前的地址同步脚本。

它原先有一个很合理的恢复逻辑:地址已经正确,但客户端没运行,就帮忙启动一下。

现在问题来了。Mac 已经拿走,守卫刚把客户端停掉,地址同步器又看见“服务不活跃”,热心地给它拉起来。两个脚本各自都觉得自己做得对。

所以地址同步器也要遵守同一份许可。它可以更新已经验证过的地址,但不能因为更新 IP 就创造一份靠近许可;需要启动或重启客户端时,必须通过距离检查。客户端自己的 ExecCondition 再检查一次,避免只靠调用方自觉。

这也是这次最值得记下来的一点:后来加了一个开关,要把原先所有“自动恢复”的入口一起梳理,光改眼前这一条路径不够。

这里约束的是这套服务的启动路径,同一用户当然仍可以手动运行另一个客户端。BLE UUID 也是公开广播,能够被仿冒或转发,因此距离判断只用来改善使用体验,不能当成身份认证。连接对端的校验仍然交给 SSH 和 Deskflow TLS。

最后跑到了什么程度

基础键鼠共享已经实际用起来了。距离校准时,Mac 放在希望暂停的位置,启用守卫后确认了台式机客户端停止;放回桌旁后,键鼠自动恢复,也实际推上去操作过。

除此之外,还专门测了几个“不好好工作”的情况。下面的耗时来自一次实机故障测试,观察的是客户端进程状态,不是端到端输入延迟:

测试 观察结果
提供有效的靠近心跳 客户端约 1.24 秒后运行
关闭心跳输入流,触发 EOF 客户端约 0.83 秒后停止
只续约一次,然后保持连接但不再发消息 最后一次心跳后约 9.10 秒停止
用 SIGSTOP 挂住 Linux 守卫,同时继续发送心跳 客户端约 8.07 秒后停止,守卫随后自动重启

这些时间还受检查周期和系统调度影响。RSSI 的 3 秒确认也只是其中一步,后面还有心跳和重连,实际多久能跨屏操作,要看整条流程。

两边的自启动也配好了:Mac 登录桌面后启动 Deskflow、距离守卫和地址同步;Linux 的客户端和守卫跟随 GNOME 图形会话。台式机的蓝牙信标配合用户 lingering,可以随开机后的用户服务管理器启动。这里没有实现登录界面上的键鼠接管,每次开机后,在进入图形桌面前,仍需使用各自的输入设备。

还有几件事要留着继续观察:额外一轮完整的“离开、自动暂停、回来、自动恢复”复测,整机重启和真实睡眠唤醒,都还没做完验证。动态地址恢复测过把客户端旧地址改错后自动修复,但没有为了验证去实际触发两台机器重新获取 DHCP 地址。现在能用和所有场景都测过,是两件事。

后话

以前折腾 GNOME,是想把桌面弄得顺眼一点。这次算是继续折腾桌面,只不过从改主题变成了改两台电脑之间的相处方式。

原本只是懒得换一次键鼠,最后顺便碰到了 Wayland 权限、证书固定、蓝牙采样、迟滞、租约和 watchdog。东西都不算大,放到真实环境里,边角问题倒是一个接一个。

鼠标往上一推就能过去,Mac 放回来也不用重新点连接,这两下用起来还是挺舒服的。

至于为了少伸一次手,最后写了几个后台服务这件事……先不算这笔账了。


折腾了一下 Mac 和 Linux 的键鼠共享
http://gensokyo.cn/2026/10/12/mac-linux-deskflow-proximity/
作者
s1ain
发布于
2026年10月12日
许可协议