Release v0.6.63: automatic LAN transport fallback and connection help

This commit is contained in:
agrynco committed 2026-11-10 19:30:50 +02:00
1 parent 3761b790ae
commit 5a2106899d
16 files changed
+376 -42

No files matched your search

+82 -1
View File
@@ -1,5 +1,6 @@
#include <winsock2.h>
#include "lan_input.h"
#include "lan_datagram.h"
#include "lan_mesh.h"
#include <iostream>
#include <deque>
@@ -344,6 +345,82 @@ void hubChecks()
endpoints[0].erase(endpoints[0].begin());hubs[0]->poll(endpoints[0],{},incoming[0],GetTickCount64()+1800);
check(!hubs[0]->ready().contains(ids[1])&&hubs[0]->ready().contains(ids[2]),"forgetting one pair revokes only its channel");
}
void fallbackChecks()
{
WSADATA data{};if(WSAStartup(MAKEWORD(2,2),&data))throw std::runtime_error("WSAStartup");
struct Close{~Close(){WSACleanup();}} close;
std::string a(32,'a'),b(32,'b'),key(64,'f');
for(bool udp:{false,true}){
auto pa=freePort(),pb=freePort(),blocked=freePort();InputHub left(a,pa),right(b,pb);
std::vector<InputEndpoint> ea{{b,key,htonl(INADDR_LOOPBACK),blocked}},eb{{a,key,htonl(INADDR_LOOPBACK),udp?blocked:pa}};
std::vector<RoutedInput> ia,ib;std::vector<Json> wire;
if(udp){auto send=[&](const auto &,const auto &j){wire.push_back(j);};left.datagrams(send);right.datagrams(send);}
auto now=GetTickCount64();
auto pump=[&]{
now+=10;left.poll(ea,{},ia,now);right.poll(eb,{},ib,now);
auto packets=std::move(wire);wire.clear();for(auto &j:packets){if(j.at("to")==a)left.receiveDatagram(j,now);else right.receiveDatagram(j,now);}Sleep(1);
};
for(int n=0;n<1000&&(left.ready().empty()||right.ready().empty());++n)pump();
check(left.ready().size()==1&&right.ready().size()==1,udp?"encrypted UDP connects when both TCP directions fail":"reverse TCP connects when direct TCP destination is unreachable");
check(left.status().find(udp?"encrypted UDP":"reverse TCP")!=std::string::npos,"status identifies the selected transport");
InputFrame frame{InputType::Key,11,'B',0,1};left.poll(ea,{{b,frame}},ia,now);
for(int n=0;n<100&&ib.empty();++n)pump();
check(ib.size()==1&&ib[0].frame==frame,"fallback carries input once to the destination");
auto old=left.ready().at(b);left.disconnect();ia.clear();ib.clear();
for(int n=0;n<1500&&(left.ready().empty()||right.ready().empty()||left.ready().at(b)==old);++n)pump();
check(left.ready().size()==1&&right.ready().size()==1&&left.ready().at(b)!=old,"fallback reconnect changes input epoch");
left.poll({}, {},ia,now);check(left.ready().empty(),"forget removes fallback channel");
}
{
InputHub hub(a,freePort());hub.datagrams([](const auto &,const auto &){});
std::vector<InputEndpoint> peers{{b,key,htonl(INADDR_LOOPBACK),freePort()}};std::vector<RoutedInput> in;
auto start=GetTickCount64();for(int i=0;i<=160;i++)hub.poll(peers,{},in,start+i*100);
check(hub.ready().empty()&&hub.needsHelp(),"all transports unavailable expose connection help after bounded attempts");
}
}
void datagramChecks()
{
std::string a(32,'a'),b(32,'b'),key(64,'f');InputDatagram left(a,b,key),right(b,a,key);
uint64_t now=1000;std::vector<Json> wire,history;std::vector<InputFrame> ia,ib;
auto send=[&](const auto &,const auto &j){wire.push_back(j);history.push_back(j);};
int packet=0;bool losses=false;
auto pump=[&]{
now+=10;left.poll({},ia,now,send);right.poll({},ib,now,send);
auto packets=std::move(wire);wire.clear();std::reverse(packets.begin(),packets.end());
for(auto &j:packets){if(losses&&++packet%4==0)continue;auto &target=j.at("to")==a?left:right;target.receive(j,now,send);target.receive(j,now,send);}
};
left.start(now);for(int n=0;n<100&&(!left.ready()||!right.ready());++n)pump();
check(left.ready()&&right.ready(),"UDP mutually authenticates saved pair keys");
wire.clear();InputFrame probe{InputType::Move,17,10,20,0};left.poll({probe},ia,now+60,send);now+=60;
auto valid=wire.back(),damaged=valid;auto ciphertext=damaged.at("data").get<std::string>();ciphertext[0]=ciphertext[0]=='0'?'1':'0';damaged["data"]=ciphertext;
rejects([&]{right.receive(damaged,now,send);},"modified UDP ciphertext is rejected");
auto publicWelcome=std::find_if(history.begin(),history.end(),[](const auto &j){return j.value("op","")=="welcome";});
auto publicProof=publicWelcome->at("proof").get<std::string>();
auto serverPacket=std::find_if(history.begin(),history.end(),[&](const auto &j){return j.value("op","")=="data"&&j.at("id")==b;});
auto encoded=serverPacket->at("data").get<std::string>();Bytes payload;
for(size_t i=0;i<encoded.size();i+=2)payload.push_back(static_cast<unsigned char>(std::stoul(encoded.substr(i,2),nullptr,16)));
InputCipher exposed(mac(publicProof,"packet|"+std::to_string(serverPacket->at("number").get<uint64_t>())));
rejects([&]{exposed.decryptPayload(payload);},"public handshake proof cannot decrypt UDP session data");
right.receive(valid,now,send);right.poll({},ib,now,send);
check(ib==std::vector<InputFrame>{probe},"rejected tampering does not advance the UDP sequence");
ib.clear();wire.clear();
std::vector<InputFrame> expected;for(int n=0;n<40;n++)expected.push_back({InputType::Key,17,n,0,n%2});
losses=true;left.poll(expected,ia,now,send);for(int n=0;n<180&&ib.size()<expected.size();++n)pump();
check(ib==expected,"lost reordered and duplicate UDP packets preserve exact event order without double key injection");
for(int n=0;n<50;n++)pump();
check(ib==expected,"replayed acknowledgements do not repeat delivered keys");
auto captured=history;left.reset();right.reset();ia.clear();ib.clear();
for(auto &j:captured)if(j.at("to")==a)left.receive(j,now,send);else right.receive(j,now,send);
check(!left.ready()&&!right.ready()&&ia.empty()&&ib.empty(),"old UDP session cannot authorize input after reset");
left.reset();right.reset();wire.clear();losses=false;left.start(now);
for(int n=0;n<100&&(!left.ready()||!right.ready());++n)pump();
check(left.ready()&&right.ready(),"fresh UDP session reconnects after reset");
left.poll({},ia,now+2100,send);right.poll({},ib,now+2100,send);
check(!left.ready()&&!right.ready(),"UDP heartbeat loss revokes input within two seconds");
InputDatagram wrong(b,a,std::string(64,'e'));left.reset();left.start(now);wire.clear();left.poll({},ia,now,send);
for(auto &j:wire)wrong.receive(j,now,[](const auto &,const auto &){});
check(!wrong.active()&&!wrong.ready(),"wrong pair key cannot start UDP handshake");
}
void channelChecks()
{
std::string a(32,'a'),b(32,'b'),secret(64,'f');auto na=randomHex(16),nb=randomHex(16);
@@ -383,6 +460,10 @@ void channelChecks()
client.poll(htonl(INADDR_LOOPBACK),{},received,start+11000);
check(!client.ready()&&client.status().find("authentication timed out")!=std::string::npos,
"initial authentication timeout distinguished from lost active connection");
client.poll(htonl(INADDR_LOOPBACK),{},received,start+13000);
check(client.status().find("authentication timed out")!=std::string::npos&&
client.status().find("Retrying automatically")!=std::string::npos,
"retry keeps the last connection failure visible");
}
{
auto port=freePort();InputChannel client(a,b,secret,1,port),server(b,a,secret,0,port);
@@ -426,6 +507,6 @@ void channelChecks()
}
int main()
{
try{channelChecks();hubChecks();routerChecks();meshChecks();std::cout<<"Passed "<<passed<<" LAN input checks\n";}
try{channelChecks();hubChecks();fallbackChecks();datagramChecks();routerChecks();meshChecks();std::cout<<"Passed "<<passed<<" LAN input checks\n";}
catch(const std::exception &e){std::cerr<<"FAIL: "<<e.what()<<'\n';return 1;}
}