#include #include #include "lan_channel.h" #include "lan_datagram.h" #include #include namespace ks::lan { namespace { void checked(NTSTATUS s){if(s<0) throw std::runtime_error("Input channel authentication failed.");} void put32(Bytes &b,uint32_t v){for(int n=24;n>=0;n-=8)b.push_back(static_cast(v>>n));} uint32_t get32(const unsigned char *p){return (uint32_t(p[0])<<24)|(uint32_t(p[1])<<16)|(uint32_t(p[2])<<8)|p[3];} Bytes keyBytes(const std::string &key) { if(key.size()!=64||key.find_first_not_of("0123456789abcdef")!=key.npos) throw std::runtime_error("Invalid input key."); Bytes bytes;for(size_t i=0;i(std::stoul(key.substr(i,2),nullptr,16)));return bytes; } bool nonblocking(SOCKET s){u_long value=1;BOOL yes=TRUE;return !ioctlsocket(s,FIONBIO,&value)&&!setsockopt(s,IPPROTO_TCP,TCP_NODELAY,(char*)&yes,sizeof(yes));} void closeSocket(SOCKET &s){if(s!=INVALID_SOCKET){closesocket(s);s=INVALID_SOCKET;}} } struct InputCipher::Impl { BCRYPT_ALG_HANDLE alg{};BCRYPT_KEY_HANDLE key{};uint64_t sequence{}; explicit Impl(const std::string &secret) { auto bytes=keyBytes(secret); try { checked(BCryptOpenAlgorithmProvider(&alg,BCRYPT_AES_ALGORITHM,nullptr,0)); checked(BCryptSetProperty(alg,BCRYPT_CHAINING_MODE,(PUCHAR)BCRYPT_CHAIN_MODE_GCM,sizeof(BCRYPT_CHAIN_MODE_GCM),0)); checked(BCryptGenerateSymmetricKey(alg,&key,nullptr,0,bytes.data(),ULONG(bytes.size()),0)); } catch(...) {if(key)BCryptDestroyKey(key);if(alg)BCryptCloseAlgorithmProvider(alg,0);throw;} SecureZeroMemory(bytes.data(),bytes.size()); } ~Impl(){if(key)BCryptDestroyKey(key);if(alg)BCryptCloseAlgorithmProvider(alg,0);} Bytes crypt(Bytes payload,Bytes &tag,bool encrypt) { if(sequence==std::numeric_limits::max())throw std::runtime_error("Input session exhausted."); unsigned char nonce[12]{};for(int i=0;i<8;++i)nonce[11-i]=static_cast(sequence>>(i*8)); BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO info;BCRYPT_INIT_AUTH_MODE_INFO(info); info.pbNonce=nonce;info.cbNonce=sizeof(nonce);info.pbTag=tag.data();info.cbTag=ULONG(tag.size()); // Protocol/direction are bound by key derivation; sequence and payload length by GCM. Bytes result(payload.size());ULONG written{}; auto fn=encrypt?BCryptEncrypt:BCryptDecrypt; checked(fn(key,payload.data(),ULONG(payload.size()),&info,nullptr,0,result.data(),ULONG(result.size()),&written,0)); if(written!=payload.size())throw std::runtime_error("Invalid input frame size."); ++sequence;return result; } }; InputCipher::InputCipher(const std::string &key):impl(std::make_unique(key)){} InputCipher::~InputCipher()=default; Bytes InputCipher::encryptPayload(const Bytes &plain) { if(plain.empty()||plain.size()>1024)throw std::runtime_error("Invalid file packet size."); Bytes tag(16);auto result=impl->crypt(plain,tag,true);result.insert(result.end(),tag.begin(),tag.end());return result; } Bytes InputCipher::decryptPayload(const Bytes &packet) { if(packet.size()<17||packet.size()>1040)throw std::runtime_error("Invalid file packet size."); Bytes tag(packet.end()-16,packet.end());return impl->crypt(Bytes(packet.begin(),packet.end()-16),tag,false); } Bytes InputCipher::encrypt(const InputFrame &f) { Bytes plain;put32(plain,uint32_t(f.type));put32(plain,f.token);put32(plain,uint32_t(f.a));put32(plain,uint32_t(f.b));put32(plain,uint32_t(f.c)); Bytes tag(16);auto result=impl->crypt(std::move(plain),tag,true);result.insert(result.end(),tag.begin(),tag.end());return result; } InputFrame InputCipher::decrypt(const Bytes &packet) { if(packet.size()!=36)throw std::runtime_error("Invalid encrypted input frame."); Bytes tag(packet.begin()+20,packet.end());auto plain=impl->crypt(Bytes(packet.begin(),packet.begin()+20),tag,false); auto type=get32(plain.data());if(type>uint32_t(InputType::Cross))throw std::runtime_error("Unsupported input frame."); return {InputType(type),get32(plain.data()+4),int32_t(get32(plain.data()+8)),int32_t(get32(plain.data()+12)),int32_t(get32(plain.data()+16))}; } std::string inputSessionKey(const std::string &secret,const std::string &from,const std::string &to,const std::string &a,const std::string &b,int edge) { if(!identifier(from)||!identifier(to)||from==to||!identifier(a)||!identifier(b))throw std::runtime_error("Invalid session identity."); return mac(secret,"KSwitcher input v1|"+from+"|"+to+"|"+a+"|"+b+"|"+std::to_string(edge)); } struct InputChannel::Impl { std::string local,peer,secret,nonce,state="Waiting for the paired computer...",lastError; int edge;uint16_t port;SOCKET listener=INVALID_SOCKET,socket=INVALID_SOCKET; bool connecting{},authenticated{},helloRead{},managed{}; uint64_t changed{},lastRead{},lastPing{},retry{},waitingSince{}; bool reverse{}; Bytes incoming,outgoing; std::unique_ptr tx,rx; Impl(std::string a,std::string b,std::string key,int side,uint16_t p,bool multiple):local(std::move(a)),peer(std::move(b)),secret(std::move(key)),edge(side),port(p),managed(multiple){} ~Impl(){closeSocket(socket);closeSocket(listener);} void reset(uint64_t now,const std::string &reason) { closeSocket(socket);connecting=authenticated=helloRead=false;incoming.clear();outgoing.clear();tx.reset();rx.reset();++changed;retry=now+1500;waitingSince=0;state=reason;lastError=reason; } void begin(uint64_t now) { nonce=randomHex(16);lastRead=lastPing=now; auto hello=Json{{"app","KSwitcher-input"},{"version",managed?2:1},{"id",local},{"to",peer},{"nonce",nonce},{"edge",edge}}.dump(); put32(outgoing,uint32_t(hello.size()));outgoing.insert(outgoing.end(),hello.begin(),hello.end()); state="Authenticating paired KSwitcher..."; } void poll(uint32_t address,const std::vector &frames,std::vector &received,uint64_t now) { try { if(!waitingSince)waitingSince=now; if(socket==INVALID_SOCKET&&nowpeer) { listener=::socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);BOOL yes=TRUE; if(listener==INVALID_SOCKET||setsockopt(listener,SOL_SOCKET,SO_EXCLUSIVEADDRUSE,(char*)&yes,sizeof(yes))||!nonblocking(listener)){closeSocket(listener);throw std::runtime_error("Cannot open input listener.");} sockaddr_in a{};a.sin_family=AF_INET;a.sin_port=htons(port); if(bind(listener,(sockaddr*)&a,sizeof(a))||listen(listener,1)) {closeSocket(listener);throw std::runtime_error("TCP input port is unavailable.");} } if(socket==INVALID_SOCKET && address && now>=retry) { if(local=1500)) { reverse=local>peer; socket=::socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);if(socket==INVALID_SOCKET||!nonblocking(socket))throw std::runtime_error("Cannot open input connection."); sockaddr_in a{};a.sin_family=AF_INET;a.sin_port=htons(port);a.sin_addr.s_addr=address; int result=connect(socket,(sockaddr*)&a,sizeof(a)); if(result && WSAGetLastError()!=WSAEWOULDBLOCK)throw std::runtime_error("Paired computer is not accepting input connections."); connecting=result!=0;begin(now); if(connecting)state=reverse?"Trying reverse TCP connection...":"Trying direct TCP connection..."; } else if(!managed) { sockaddr_in a{};int length=sizeof(a);socket=accept(listener,(sockaddr*)&a,&length); if(socket!=INVALID_SOCKET) { if(a.sin_addr.s_addr!=address){closeSocket(socket);return;} if(!nonblocking(socket))throw std::runtime_error("Cannot configure input connection."); begin(now); } } } if(socket==INVALID_SOCKET)return; // Give TCP establishment time for SYN retransmission / firewall approval. // Keep the short fail-safe only for an already authenticated input session. if(now-lastRead>(authenticated?2000:10000)) throw std::runtime_error(authenticated ? "Input connection lost (no heartbeat for 2 seconds); control returned locally." : connecting ? "Cannot reach TCP 45884. Enable input on both PCs and check the receiving PC firewall." : "TCP connected, but input authentication timed out. Check pairing and input setup on both PCs."); if(connecting) { fd_set writes,errors;FD_ZERO(&writes);FD_ZERO(&errors);FD_SET(socket,&writes);FD_SET(socket,&errors);timeval wait{}; if(select(0,nullptr,&writes,&errors,&wait)<=0)return; int error=0;int size=sizeof(error); if(getsockopt(socket,SOL_SOCKET,SO_ERROR,(char*)&error,&size))error=WSAGetLastError(); if(error)throw std::runtime_error("Cannot connect to TCP 45884 (Windows error "+std::to_string(error)+"). Enable input on both PCs and check the receiving PC firewall."); connecting=false;state="TCP connected; authenticating paired KSwitcher..."; } if(authenticated) { for(auto &f:frames){auto bytes=tx->encrypt(f);outgoing.insert(outgoing.end(),bytes.begin(),bytes.end());} if(now-lastPing>=400){auto bytes=tx->encrypt({InputType::Ping});outgoing.insert(outgoing.end(),bytes.begin(),bytes.end());lastPing=now;} } if(outgoing.size()>32768)throw std::runtime_error("Input queue overflow; control returned locally."); if(!outgoing.empty()) { int count=::send(socket,(char*)outgoing.data(),int(outgoing.size()),0); if(count>0)outgoing.erase(outgoing.begin(),outgoing.begin()+count); else if(count==0||WSAGetLastError()!=WSAEWOULDBLOCK)throw std::runtime_error("Input send failed."); } unsigned char data[4096]; for(int batch=0;batch<8;++batch) { int count=recv(socket,(char*)data,sizeof(data),0); if(count==0)throw std::runtime_error("Other KSwitcher disconnected."); if(count<0){if(WSAGetLastError()!=WSAEWOULDBLOCK)throw std::runtime_error("Input receive failed.");break;} incoming.insert(incoming.end(),data,data+count); } if(incoming.size()>32768)throw std::runtime_error("Input receive queue overflow."); if(!helloRead && incoming.size()>=4) { size_t length=get32(incoming.data());if(length>512||length<32)throw std::runtime_error("Invalid input handshake."); if(incoming.size()(inputSessionKey(secret,local,peer,nonce,remoteNonce,managed?-1:edge)); rx=std::make_unique(inputSessionKey(secret,peer,local,remoteNonce,nonce,managed?-1:(edge^1))); incoming.erase(incoming.begin(),incoming.begin()+4+length);helloRead=true; auto bytes=tx->encrypt({InputType::Ready});outgoing.insert(outgoing.end(),bytes.begin(),bytes.end()); } while(helloRead && incoming.size()>=36) { auto f=rx->decrypt(Bytes(incoming.begin(),incoming.begin()+36));incoming.erase(incoming.begin(),incoming.begin()+36);lastRead=now; if(!authenticated){if(f.type!=InputType::Ready)throw std::runtime_error("Input received before authentication.");authenticated=true;lastError.clear();state="Input connected. Move through the configured screen edge.";} else if(f.type==InputType::Ready)throw std::runtime_error("Repeated input handshake."); else if(f.type!=InputType::Ping)received.push_back(f); } } catch(const std::exception &e){received.clear();reset(now,e.what());} } }; InputChannel::InputChannel(std::string a,std::string b,std::string key,int edge,uint16_t port,bool managed):impl(std::make_unique(std::move(a),std::move(b),std::move(key),edge,port,managed)){} InputChannel::~InputChannel()=default; void InputChannel::poll(uint32_t ip,const std::vector&out,std::vector&in,uint64_t now){impl->poll(ip,out,in,now);} bool InputChannel::adopt(uintptr_t accepted,uint64_t now) { if(!impl->managed||impl->authenticated||!nonblocking(SOCKET(accepted)))return false; // Reverse connection wins a simultaneous connect race. Never replace an // authenticated channel or accept two sockets for the same input stream. if(impl->socket!=INVALID_SOCKET){ if(impl->local>impl->peer||!impl->connecting)return false; impl->reset(now,"Accepting reverse TCP connection."); } impl->reverse=impl->localpeer; impl->connecting=false;impl->begin(now);impl->socket=SOCKET(accepted);return true; } bool InputChannel::ready()const{return impl->authenticated;} uint64_t InputChannel::generation()const{return impl->changed;} std::string InputChannel::status()const{ return impl->lastError.empty() ? impl->state : impl->lastError+" Retrying automatically."; } void InputChannel::disconnect(){impl->reset(GetTickCount64(),"Input disconnected; local control restored.");} std::string InputChannel::transport()const{return impl->reverse?"reverse TCP":"direct TCP";} struct InputHub::Impl { struct Link { InputEndpoint endpoint;std::unique_ptr channel;std::unique_ptr udp;uint64_t generation{},epoch{},since{},failedSince{};bool udpReady{};std::string lastState; }; struct Pending {SOCKET socket;uint32_t address;uint64_t since;}; std::string local;uint16_t port;SOCKET listener=INVALID_SOCKET; std::map links;std::vector pending;uint64_t epoch{},retry{}; std::string error; InputHub::SendDatagram sendDatagram; uint64_t currentTime{}; Impl(std::string id,uint16_t p):local(std::move(id)),port(p){} ~Impl(){closeSocket(listener);for(auto &p:pending)closeSocket(p.socket);} void poll(const std::vector &endpoints,const std::vector &out,std::vector &in,uint64_t now) { currentTime=now; for(auto it=links.begin();it!=links.end();){ auto match=std::find_if(endpoints.begin(),endpoints.end(),[&](auto &e){return e.id==it->first&&e.key==it->second.endpoint.key&&e.port==it->second.endpoint.port;}); if(match==endpoints.end())it=links.erase(it);else ++it; } for(auto &e:endpoints)if(e.id!=local&&!links.contains(e.id)){ Link link;link.endpoint=e;link.channel=std::make_unique(local,e.id,e.key,0,e.port,true); link.udp=std::make_unique(local,e.id,e.key);link.since=link.failedSince=now;link.epoch=++epoch;links.emplace(e.id,std::move(link)); } // Discovery is a hint, not the lifetime of authenticated input sessions. // Keep the last usable address across missed announcements. A new address // is used only once a channel is down, never to tear down working input. for(auto &e:endpoints){if(e.id==local)continue;auto &link=links.at(e.id); if(e.address&&!link.channel->ready()&&e.address!=link.endpoint.address){ link.channel->disconnect();link.endpoint.address=e.address; } } bool listenNeeded=!links.empty(); if(!listenNeeded){closeSocket(listener);for(auto &p:pending)closeSocket(p.socket);pending.clear();} if(listenNeeded&&listener==INVALID_SOCKET&&now>=retry){ listener=::socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);BOOL yes=TRUE;sockaddr_in a{};a.sin_family=AF_INET;a.sin_port=htons(port); if(listener==INVALID_SOCKET||setsockopt(listener,SOL_SOCKET,SO_EXCLUSIVEADDRUSE,(char*)&yes,sizeof(yes))||!nonblocking(listener)||bind(listener,(sockaddr*)&a,sizeof(a))||listen(listener,SOMAXCONN)){ auto detail="TCP input port "+std::to_string(port)+" is unavailable (Windows error "+std::to_string(WSAGetLastError())+")."; if(error!=detail)log(L"LAN input: "+wide(detail)); closeSocket(listener);retry=now+1500;error=std::move(detail); }else {error.clear();log(L"LAN input listener ready on TCP "+std::to_wstring(port));} } if(listener!=INVALID_SOCKET)for(int n=0;n<16&&pending.size()<64;++n){ sockaddr_in a{};int size=sizeof(a);SOCKET sock=accept(listener,(sockaddr*)&a,&size);if(sock==INVALID_SOCKET)break; if(!nonblocking(sock)){closeSocket(sock);continue;}pending.push_back({sock,a.sin_addr.s_addr,now}); } for(auto it=pending.begin();it!=pending.end();){ bool done=now-it->since>2000;unsigned char bytes[516];int count=recv(it->socket,(char*)bytes,sizeof(bytes),MSG_PEEK); if(count==0||(count<0&&WSAGetLastError()!=WSAEWOULDBLOCK))done=true; if(!done&&count>=4){ auto length=get32(bytes);if(length<32||length>512)done=true; else if(count>=int(length+4)){ done=true; try{auto hello=Json::parse(bytes+4,bytes+4+length);auto id=hello.value("id","");auto link=links.find(id); if(hello.value("app","")=="KSwitcher-input"&&hello.value("to","")==local&&hello.value("version",0)==2&&link!=links.end()&&!link->second.udp->active()&&link->second.channel->adopt(uintptr_t(it->socket),now))it->socket=INVALID_SOCKET; }catch(const std::exception &){} } } if(done){closeSocket(it->socket);it=pending.erase(it);}else ++it; } for(auto &[id,link]:links){ std::vector frames,received;for(auto &f:out)if(f.peer==id&&(!f.epoch||f.epoch==link.epoch))enqueueInput(frames,f.frame); if(sendDatagram&&!link.channel->ready()&&!link.udp->active()&&local=5000){link.udp->start(now);link.channel->disconnect();frames.clear();} if(link.udp->active()){ link.udp->poll(frames,received,now,sendDatagram); if(!link.udp->active()){link.since=now;link.channel->disconnect();received.clear();} }else link.channel->poll(link.endpoint.address,frames,received,now); if(link.udpReady!=link.udp->ready()){link.udpReady=link.udp->ready();link.epoch=++epoch;} bool ready=link.channel->ready()||link.udp->ready(); if(ready)link.failedSince=link.since=now; auto state=link.udp->ready()?"Input connected via encrypted UDP.":link.udp->active()?"Trying encrypted UDP over discovery...":link.channel->ready()?"Input connected via "+link.channel->transport()+".":link.channel->status(); if(state!=link.lastState){link.lastState=state;log(L"LAN input peer "+wide(id.substr(0,8))+L": "+wide(state));} if(link.generation!=link.channel->generation()){link.generation=link.channel->generation();link.epoch=++epoch;} for(auto &f:received)in.push_back({id,f,link.epoch}); } } }; InputHub::InputHub(std::string id,uint16_t port):impl(std::make_unique(std::move(id),port)){} InputHub::~InputHub()=default; void InputHub::poll(const std::vector &peers,const std::vector &out,std::vector &in,uint64_t now){impl->poll(peers,out,in,now);} std::map InputHub::ready()const{std::map result;for(auto &[id,l]:impl->links)if(l.channel->ready()||l.udp->ready())result[id]=l.epoch;return result;} std::string InputHub::status()const{ auto count=ready().size();if(count==impl->links.size()&&count){ std::string routes;for(auto &[id,l]:impl->links){if(!routes.empty())routes+=", ";routes+=id.substr(0,8)+": "+(l.udp->ready()?"encrypted UDP":l.channel->transport());} return "Input connected ("+routes+"). Move through a neighboring screen edge."; } if(needsHelp())return "Automatic connection attempts have not succeeded for every PC. Enable input on both PCs. Open Connection help for firewall commands."; if(!impl->error.empty())return impl->error; for(auto &[id,l]:impl->links)if(!l.channel->ready()&&!l.udp->ready())return l.lastState; return "Waiting for paired computers..."; } void InputHub::disconnect(){for(auto &[id,l]:impl->links){l.channel->disconnect();l.udp->reset();l.since=impl->currentTime;}} void InputHub::datagrams(SendDatagram send){impl->sendDatagram=std::move(send);} void InputHub::receiveDatagram(const Json &j,uint64_t now){ if(!impl->sendDatagram)return; auto it=impl->links.find(j.value("id",""));if(it==impl->links.end()||it->second.channel->ready())return; auto &link=it->second;bool active=link.udp->active(); try{link.udp->receive(j,now,impl->sendDatagram);}catch(const std::exception &){return;} if(!active&&link.udp->active())link.channel->disconnect(); } bool InputHub::needsHelp()const{ for(auto &[id,l]:impl->links)if(!l.channel->ready()&&!l.udp->ready()&&impl->currentTime-l.failedSince>=15000)return true; return false; } }