Add encrypted LAN mouse and keyboard sharing with four-edge handoff

This commit is contained in:
agrynco committed 2026-11-06 16:28:43 +02:00
1 parent 9b565dd55e
commit e0bfdd9a90
15 files changed
+1093 -104

No files matched your search

+116 -6
View File
@@ -3,6 +3,7 @@
#include <iphlpapi.h>
#include "lan.h"
#include "lan_pairing.h"
#include "lan_input.h"
#include <atomic>
#include <mutex>
#include <thread>
@@ -55,6 +56,17 @@ struct LanService::Impl
std::atomic<bool> stopping{false};
std::mutex mutex;
std::thread worker;
bool shareInput{};
std::string inputPeer;
int inputEdge=1;
std::atomic<bool> inputConnected{false},abortInput{false};
std::atomic<uint64_t> inputEpoch{0};
std::atomic<uint64_t> inputConfiguration{0};
uint64_t observedEpoch{},observedRevision{};
std::mutex inputMutex;
std::vector<lan::InputFrame> inputOut,inputIn;
std::unique_ptr<lan::InputRouter> router;
std::string inputStatus="Input sharing disabled.";
explicit Impl(const std::filesystem::path &root) : path(root/L"lan-pairing.bin")
{
if(std::filesystem::exists(path))
@@ -62,6 +74,8 @@ struct LanService::Impl
auto j=Json::parse(unprotect(readFile(path,256*1024)));
if(j.value("version",0)!=1 || !lan::identifier(j.value("id",""))) throw std::runtime_error("Invalid local LAN configuration.");
id=j.at("id"); enabled=j.value("enabled",false); trusted=j.at("trusted");
shareInput=j.value("shareInput",false);inputPeer=j.value("inputPeer","");inputEdge=j.value("inputEdge",1);
if(inputEdge<0||inputEdge>3||(shareInput&&!trusted.contains(inputPeer)))throw std::runtime_error("Invalid input sharing configuration.");
if(!trusted.is_object()||trusted.size()>64) throw std::runtime_error("Invalid paired computer list.");
for(auto &[key,entry]:trusted.items())
if(!lan::identifier(key)||key==id||entry.at("key").get<std::string>().size()!=64||
@@ -69,10 +83,43 @@ struct LanService::Impl
throw std::runtime_error("Invalid paired computer key.");
}
else id=lan::randomHex(16);
configureRouter();
if(enabled) start();
}
~Impl() { stop(); }
void save() { atomicWrite(path,protect(encoded({{"version",1},{"id",id},{"enabled",enabled},{"trusted",trusted}}))); }
void save() { atomicWrite(path,protect(encoded({{"version",1},{"id",id},{"enabled",enabled},{"trusted",trusted},
{"shareInput",shareInput},{"inputPeer",inputPeer},{"inputEdge",inputEdge}}))); }
bool queue(lan::InputFrame frame)
{
if(!inputConnected)return false;
std::lock_guard lock(inputMutex);
if(inputOut.size()>=512){abortInput=true;return false;}
inputOut.push_back(frame);return true;
}
void configureRouter()
{
router.reset();inputConnected=false;abortInput=true;++inputEpoch;++inputConfiguration;
{std::lock_guard lock(inputMutex);inputOut.clear();inputIn.clear();}
if(enabled&&shareInput)router=std::make_unique<lan::InputRouter>(lan::windowsInputEnvironment(),[this](auto f){return queue(f);},inputEdge,id<inputPeer);
inputStatus=enabled&&shareInput?"Connecting input channel...":"Input sharing disabled.";
observedRevision=0;
}
bool takeChange()
{
auto revision=router?router->revision():0;bool changed=revision!=observedRevision;observedRevision=revision;return changed;
}
bool tickInput(bool allowEnter)
{
if(!router)return false;
auto epoch=inputEpoch.load();if(epoch!=observedEpoch){router->cancel(GetTickCount64());observedEpoch=epoch;}
router->tick(inputConnected,GetTickCount64());
std::vector<lan::InputFrame> incoming;
{std::lock_guard lock(inputMutex);incoming.swap(inputIn);}
for(auto &frame:incoming)
if(!allowEnter&&frame.type==lan::InputType::Enter)queue({lan::InputType::Return,frame.token,32768});
else router->receive(frame,GetTickCount64());
return takeChange();
}
void start()
{
stopping=false; epoch=lan::randomHex(16); status="Searching for KSwitcher computers...";
@@ -80,8 +127,11 @@ struct LanService::Impl
}
void stop()
{
if(router)router->cancel(GetTickCount64());
stopping=true;
if(worker.joinable()) worker.join();
inputConnected=false;++inputEpoch;
{std::lock_guard lock(inputMutex);inputOut.clear();inputIn.clear();}
std::lock_guard lock(mutex); pending.reset(); peers.clear();
}
void send(SOCKET socket,const sockaddr_in &address,const Json &j)
@@ -128,7 +178,7 @@ struct LanService::Impl
auto previous=trusted;
trusted[other]={{"name",peer.name},{"key",pending->derived}};
try {save();} catch(...) {trusted=std::move(previous);throw;}
peer.verified=now; status="Paired. Input and clipboard sharing are not enabled in this preview.";
peer.verified=now; status="Paired. Select a screen position and enable mouse and keyboard on both PCs.";
}
}
}
@@ -145,12 +195,21 @@ struct LanService::Impl
throw std::runtime_error("Cannot open LAN discovery socket.");
sockaddr_in bound{}; bound.sin_family=AF_INET; bound.sin_port=htons(lan::Port);
if(bind(socket,(sockaddr*)&bound,sizeof(bound))) throw std::runtime_error("UDP port 45883 is in use. Only one LAN-enabled KSwitcher may run per computer.");
uint64_t nextHello=0,nextOffer=0;
uint64_t nextHello=0,nextOffer=0,lastChannelGeneration=0;
std::unique_ptr<lan::InputChannel> channel;
std::string channelPeer,channelSecret;int channelEdge=-1;
while(!stopping)
{
auto now=GetTickCount64();
std::string selectedPeer,selectedSecret;int selectedEdge=-1;uint32_t selectedAddress=0;uint64_t configuration=0;
{
std::lock_guard lock(mutex);
configuration=inputConfiguration.load();
if(shareInput&&trusted.contains(inputPeer))
{
selectedPeer=inputPeer;selectedSecret=trusted.at(inputPeer).at("key");selectedEdge=inputEdge;
if(peers.contains(inputPeer))selectedAddress=peers.at(inputPeer).address.sin_addr.s_addr;
}
if(now>=nextHello)
{
epoch=lan::randomHex(16); // Fresh challenge prevents replayed presence from staying online.
@@ -169,7 +228,30 @@ struct LanService::Impl
send(socket,peers.at(pending->peer).address,pending->offer()); nextOffer=now+1000;
}
}
fd_set read; FD_ZERO(&read); FD_SET(socket,&read); timeval delay{0,100000};
if(channelPeer!=selectedPeer||channelEdge!=selectedEdge||channelSecret!=selectedSecret)
{
inputConnected=false;++inputEpoch;channel.reset();
{std::lock_guard lock(inputMutex);inputOut.clear();inputIn.clear();}
channelPeer=selectedPeer;channelEdge=selectedEdge;channelSecret=selectedSecret;lastChannelGeneration=0;
if(!selectedPeer.empty())channel=std::make_unique<lan::InputChannel>(id,selectedPeer,selectedSecret,selectedEdge);
}
if(channel)
{
std::vector<lan::InputFrame> out,in;
{std::lock_guard lock(inputMutex);out.swap(inputOut);}
if(abortInput.exchange(false)){channel->disconnect();out.clear();}
channel->poll(selectedAddress,out,in,now);
if(configuration!=inputConfiguration.load()){in.clear();channel->disconnect();}
if(lastChannelGeneration!=channel->generation())
{
lastChannelGeneration=channel->generation();inputConnected=false;++inputEpoch;
std::lock_guard lock(inputMutex);inputOut.clear();inputIn.clear();
}
inputConnected=channel->ready()&&!abortInput.load()&&configuration==inputConfiguration.load();
{std::lock_guard lock(inputMutex);if(inputIn.size()+in.size()>512)abortInput=true;else inputIn.insert(inputIn.end(),in.begin(),in.end());}
{std::lock_guard lock(mutex);inputStatus=channel->status();}
}
fd_set read; FD_ZERO(&read); FD_SET(socket,&read); timeval delay{0,channel?5000:100000};
int ready=select(0,&read,nullptr,nullptr,&delay);
if(ready==SOCKET_ERROR) throw std::runtime_error("LAN discovery socket failed.");
if(ready>0) for(int count=0;count<64;++count)
@@ -184,6 +266,8 @@ struct LanService::Impl
}
}
catch(const std::exception &e) { std::lock_guard lock(mutex); status=std::string("LAN stopped: ")+e.what(); }
inputConnected=false;++inputEpoch;
{std::lock_guard lock(mutex);if(shareInput)inputStatus="Input connection closed.";}
if(socket!=INVALID_SOCKET) closesocket(socket);
if(winsock) WSACleanup();
}
@@ -195,9 +279,19 @@ struct LanService::Impl
stop();
bool previous=enabled; enabled=j.at("enabled").get<bool>();
try {save();} catch(...) {enabled=previous;if(enabled) start();throw;}
configureRouter();
if(enabled) start(); else status="Disabled. No discovery sockets or worker thread.";
}
std::lock_guard lock(mutex);
if(action=="input")
{
bool active=j.at("enabled");auto peer=j.value("peer",inputPeer);int edge=j.value("edge",inputEdge);
if(edge<0||edge>3||(active&&(!enabled||!trusted.contains(peer))))throw std::runtime_error("Enable discovery and select a paired KSwitcher first.");
auto beforePeer=inputPeer;int beforeEdge=inputEdge;bool beforeActive=shareInput;
shareInput=active;inputPeer=peer;inputEdge=edge;
try{save();}catch(...){shareInput=beforeActive;inputPeer=beforePeer;inputEdge=beforeEdge;throw;}
configureRouter();inputStatus=active?"Connecting input channel...":"Input sharing disabled.";
}
if(action=="code") return encoded({{"code",lan::randomHex(16)}});
if(action=="cancel") { pending.reset(); status="Pairing cancelled."; }
if(action=="pair")
@@ -208,8 +302,18 @@ struct LanService::Impl
pending=std::make_unique<lan::Pairing>(id,peer,j.at("code").get<std::string>(),GetTickCount64());
status="Waiting for the same code and Pair on the other KSwitcher (2 minutes)...";
}
if(action=="forget") { auto peer=j.at("peer").get<std::string>(); trusted.erase(peer); if(pending&&pending->peer==peer) pending.reset(); save(); status="Pair removed on this computer."; }
Json result={{"enabled",enabled},{"id",id},{"name",name},{"status",status},{"pending",pending&&!pending->confirmed},{"peers",Json::array()}};
if(action=="forget")
{
auto peer=j.at("peer").get<std::string>();auto oldTrusted=trusted;bool oldSharing=shareInput;
trusted.erase(peer);if(peer==inputPeer)shareInput=false;
try{save();}catch(...){trusted=std::move(oldTrusted);shareInput=oldSharing;throw;}
if(pending&&pending->peer==peer)pending.reset();
if(peer==inputPeer)configureRouter();
status="Pair removed on this computer.";
}
Json result={{"enabled",enabled},{"id",id},{"name",name},{"status",status},{"pending",pending&&!pending->confirmed},{"peers",Json::array()},
{"shareInput",shareInput},{"inputPeer",inputPeer},{"inputEdge",inputEdge},{"inputConnected",inputConnected.load()},
{"inputStatus",enabled&&shareInput?inputStatus:"Input sharing disabled."},{"controlMode",router?int(router->mode()):0}};
auto now=GetTickCount64();
for(const auto &[peer,p]:peers)
{
@@ -225,4 +329,10 @@ struct LanService::Impl
LanService::LanService(const std::filesystem::path &path) : impl(std::make_unique<Impl>(path)) {}
LanService::~LanService()=default;
Bytes LanService::command(const Bytes &command) { return impl->command(command); }
bool LanService::inputEnabled()const{return impl->enabled&&impl->shareInput;}
bool LanService::tickInput(bool allowEnter){return impl->tickInput(allowEnter);}
bool LanService::keyboard(const KBDLLHOOKSTRUCT &k,bool down){return impl->router&&impl->router->keyboard(k,down,GetTickCount64());}
bool LanService::mouse(UINT message,const MSLLHOOKSTRUCT &m,bool allowEnter){return impl->router&&impl->router->mouse(message,m,GetTickCount64(),allowEnter);}
bool LanService::takeInputChange(){return impl->takeChange();}
void LanService::cancelInput(){if(impl->router)impl->router->cancel(GetTickCount64());impl->abortInput=true;}
}