202 lines
11 KiB
Python
202 lines
11 KiB
Python
"""Exercise the production repeater setter and packet-rule evaluation together."""
|
|
from pathlib import Path
|
|
import os
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
|
|
from test_flood_rule_extensions import UTILS
|
|
from test_replay_reset_integration import extract_braced
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
PRELUDE = r'''
|
|
#include <algorithm>
|
|
#include <cassert>
|
|
#include <cctype>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <string>
|
|
#include <vector>
|
|
#include <Utils.h>
|
|
#include <helpers/FloodRuleCLI.h>
|
|
#include <helpers/RegionNameUtils.h>
|
|
#define MESH_ENABLE_FLOOD_RULE_ENGINE 1
|
|
#define FLOOD_PACKET_FILTER_SCOPE_NAME_LEN 32
|
|
#define FLOOD_GROUP_MODERATION_NAME_LEN 32
|
|
#define FLOOD_CHANNEL_KEY_PREFIX_LEN 8
|
|
#define FLOOD_PACKET_FILTER_PATH_PREFIX_BYTES_MAX 9
|
|
#define FLOOD_PACKET_FILTER_ANY_TYPE 255
|
|
#define FLOOD_PACKET_FILTER_MAX_HOPS 63
|
|
#define FLOOD_PACKET_FILTER_SLOTS 63
|
|
#define FLOOD_GROUP_MODERATION_RATE_UNLIMITED 65535
|
|
#define REGION_DENY_FLOOD 1
|
|
static const char FLOOD_PACKET_FILTER_USAGE[]="Err - usage";
|
|
static const char FLOOD_PACKET_FILTER_DUPLICATE[]="Err - duplicate";
|
|
mesh::Packet::Packet() : header(0), payload_len(0), path_len(0), transport_codes{}, path{}, payload{},
|
|
_rssi(0), _snr(0), tx_cr(0), flood_retry_policy(0) {}
|
|
static std::vector<uint8_t> decrypted;
|
|
static bool authentic = true;
|
|
int mesh::Utils::MACThenDecrypt(const uint8_t* secret, uint8_t* out, const uint8_t*, int) {
|
|
if (!authentic || secret[0]!=0x42)return 0;
|
|
assert(decrypted.size()<=MAX_PACKET_PAYLOAD);
|
|
memcpy(out,decrypted.data(),decrypted.size());return int(decrypted.size());
|
|
}
|
|
struct RegionEntry { const char* name="Town"; uint8_t flags=0; bool isWildcard()const{return false;} };
|
|
struct TransportKey { bool isNull()const{return true;} uint16_t calcTransportCode(const mesh::Packet*)const{return 0;} };
|
|
struct RegionMap {
|
|
static bool is_name_char(uint8_t c){return std::isalnum(c)||c=='_'||c=='-';}
|
|
RegionEntry* findByNamePrefix(const char*){return nullptr;}
|
|
int getTransportKeysFor(const RegionEntry&,TransportKey*,unsigned){return 0;}
|
|
};
|
|
void StrHelper::strzcpy(char* out,const char* in,size_t size){if(size)snprintf(out,size,"%s",in);}
|
|
void StrHelper::strncpy(char* out,const char* in,size_t size){if(size)snprintf(out,size,"%s",in);}
|
|
static void deriveFloodFilterScopeKey(const char*,TransportKey&){}
|
|
// Channel cryptography/resolution is separately exercised. The setter still
|
|
// performs every production key-vs-hash and content/routing compatibility gate.
|
|
static bool parseFloodRuleChannel(const char* text,uint8_t* secret,uint8_t& key_len,
|
|
uint8_t* prefix,char* name,size_t capacity) {
|
|
memset(secret,0,PUB_KEY_SIZE);memset(prefix,0,FLOOD_CHANNEL_KEY_PREFIX_LEN);
|
|
uint8_t hash;
|
|
if(FloodFilterPolicy::parseChannelHashMatcher(text,hash)){
|
|
key_len=1;secret[0]=hash;prefix[0]=hash;snprintf(name,capacity,"hash:%02X",hash);return true;
|
|
}
|
|
if(strcmp(text,"#weather")&&strcmp(text,"public"))return false;
|
|
key_len=16;secret[0]=0x42;snprintf(name,capacity,"%s",text);return true;
|
|
}
|
|
class MyMesh {
|
|
public:
|
|
@ENTRY@;
|
|
FloodPacketFilterEntry rows[63]{};
|
|
FloodPacketFilterEntry* flood_packet_filters=rows;
|
|
uint8_t flood_packet_filter_slots=63,flood_channel_data_rule_slot=255;
|
|
uint32_t flood_filter_probability_seed=0x12345678;
|
|
RegionMap region_map;
|
|
bool save_ok=true;
|
|
unsigned saves=0;
|
|
void setFloodPacketFilter(const char*,char*,bool require_explicit_action=false);
|
|
bool saveFloodPacketFilters(){++saves;return save_ok;}
|
|
void formatFloodPacketFilterDetail(int,char* out,size_t size,bool=false)const{snprintf(out,size,"> saved");}
|
|
bool isAnyTempRadioActive()const{return false;}
|
|
bool floodPacketFilterBlacklistMatches(const mesh::Packet*)const{return false;}
|
|
bool floodPacketFilterFieldsMatch(const FloodPacketFilterEntry&,const mesh::Packet*,bool,uint16_t,bool,const RegionEntry*,uint8_t)const;
|
|
bool authenticateFloodPacketFilterChannel(const FloodPacketFilterEntry&,const mesh::Packet*)const;
|
|
bool resolveFloodPacketFilterTargetRegion(const char*,TransportKey&,const char*&){return false;}
|
|
uint64_t applyFloodPacketFilterStop(uint64_t);
|
|
uint64_t evaluateFloodPacketFilterMatches(const mesh::Packet*,bool,const RegionEntry*,uint8_t);
|
|
};
|
|
'''
|
|
SCENARIOS = r'''
|
|
static void set(MyMesh& mesh,const char* args,bool expected=true,bool strict=true){
|
|
struct Reply {uint8_t before[8];char text[160];uint8_t after[8];} reply;
|
|
memset(&reply,0xa5,sizeof(reply));
|
|
mesh.setFloodPacketFilter(args,reply.text,strict);
|
|
assert(!strncmp(reply.text,expected?"OK":"Err",expected?2:3));
|
|
for(uint8_t byte:reply.before)assert(byte==0xa5);
|
|
for(uint8_t byte:reply.after)assert(byte==0xa5);
|
|
}
|
|
static void plain(const std::string& text){
|
|
decrypted.assign(5,0);decrypted.insert(decrypted.end(),text.begin(),text.end());decrypted.push_back(0);
|
|
}
|
|
static void parserAndRollback(){
|
|
MyMesh mesh;
|
|
set(mesh,R"(.1 grp_txt channel=#weather sender="^(Alice|Bob) [0-9]+$" text='weather (rain|sun)' len=20-80 route=flood prob=25 path=AA>BB$ drop)");
|
|
const auto original=mesh.rows[0];
|
|
assert(original.active&&original.drop_on_match&&original.path_hops==2&&original.extra.path_position==FloodRuleExtensions::Suffix);
|
|
assert(original.extra.probability==25&&original.extra.payload_set&&original.extra.min_payload==20&&original.extra.max_payload==80);
|
|
set(mesh,R"(.2 t=grp_txt c=#weather n="^(Alice|Bob) [0-9]+$" x='weather (rain|sun)' l=20-80 rt=f pr=25 pa=AA>BB$ d)");
|
|
assert(FloodFilterPolicy::sameRuleConfiguration(mesh.rows[0],mesh.rows[1]));
|
|
set(mesh,".1 enabled=off");assert(mesh.rows[0].active&&mesh.rows[0].extra.disabled);
|
|
auto disabled=original;disabled.extra.disabled=1;
|
|
assert(FloodFilterPolicy::sameRuleConfiguration(mesh.rows[0],disabled));
|
|
const auto saves=mesh.saves;
|
|
set(mesh,".1 off");assert(mesh.saves==saves);
|
|
mesh.flood_channel_data_rule_slot=0;
|
|
mesh.save_ok=false;
|
|
set(mesh,".1 e=on",false);assert(mesh.rows[0].extra.disabled);
|
|
assert(mesh.flood_channel_data_rule_slot==0);
|
|
mesh.save_ok=true;
|
|
set(mesh,".1 e=on");assert(mesh.flood_channel_data_rule_slot==255);
|
|
mesh.save_ok=false;
|
|
set(mesh,".1 any route=direct drop",false);
|
|
assert(FloodFilterPolicy::sameRuleConfiguration(mesh.rows[0],original));
|
|
mesh.save_ok=true;set(mesh,".1 on");assert(!mesh.rows[0].extra.disabled);
|
|
assert(FloodFilterPolicy::sameRuleConfiguration(mesh.rows[0],original));
|
|
for(const char* invalid:{".3 grp_txt c=hash:00 n=Alice drop",".3 grp_txt n=Alice drop",
|
|
".3 advert c=#weather n=Alice drop",".3 grp_txt c=#weather n=[ drop",
|
|
".3 any route=direct scope=Town", ".3 any route=any retry",
|
|
".3 any l=1-2 len=2-3 drop", ".3 any pa=AA>BB>CC>DD>EE drop",
|
|
".3 any", ".3 off", ".0 any drop", ".64 any drop"}) {
|
|
unsigned previous=mesh.saves;set(mesh,invalid,false);assert(mesh.saves==previous&&!mesh.rows[2].active);
|
|
}
|
|
set(mesh,".3 any route=direct drop");assert(mesh.rows[2].extra.route==FloodRuleExtensions::DirectRoute);
|
|
set(mesh,".4 any",true,false);assert(mesh.rows[3].drop_on_match);
|
|
}
|
|
static void allowAndDirect(){
|
|
MyMesh mesh;
|
|
set(mesh,".1 grp_txt c=#weather n=^Alice$ a pri=2 rt=a");
|
|
set(mesh,".2 grp_txt c=#weather d pri=1 rt=a");
|
|
mesh::Packet packet;packet.header=ROUTE_TYPE_FLOOD|(PAYLOAD_TYPE_GRP_TXT<<PH_TYPE_SHIFT);packet.payload_len=19;
|
|
assert(mesh.rows[0].stop_on_match&&!mesh.rows[0].drop_on_match&&mesh.rows[1].drop_on_match);
|
|
plain("Alice: hello");assert(mesh.evaluateFloodPacketFilterMatches(&packet,true,nullptr,0)==1);
|
|
plain("Bob: hello");assert(mesh.evaluateFloodPacketFilterMatches(&packet,true,nullptr,0)==2);
|
|
plain(std::string(140,'x')+"Alice: hello");assert(mesh.evaluateFloodPacketFilterMatches(&packet,true,nullptr,0)==2);
|
|
packet.header=ROUTE_TYPE_DIRECT|(PAYLOAD_TYPE_GRP_TXT<<PH_TYPE_SHIFT);
|
|
plain("Alice: hello");assert(mesh.evaluateFloodPacketFilterMatches(&packet,true,nullptr,0)==1);
|
|
plain("Bob: hello");assert(mesh.evaluateFloodPacketFilterMatches(&packet,true,nullptr,0)==2);
|
|
set(mesh,".2 off");assert(mesh.evaluateFloodPacketFilterMatches(&packet,true,nullptr,0)==0);
|
|
set(mesh,".2 on");authentic=false;
|
|
assert(mesh.evaluateFloodPacketFilterMatches(&packet,true,nullptr,0)==0);authentic=true;
|
|
// Existing radio rules keep their flood-only behavior until route is stated.
|
|
MyMesh legacy;set(legacy,".1 any drop");
|
|
assert(legacy.evaluateFloodPacketFilterMatches(&packet,true,nullptr,0)==0);
|
|
set(legacy,".1 any rt=d drop");assert(legacy.evaluateFloodPacketFilterMatches(&packet,true,nullptr,0)==1);
|
|
}
|
|
static void budgetExhaustion(){
|
|
MyMesh mesh;
|
|
for(unsigned slot=1;slot<=63;++slot){
|
|
char command[128];snprintf(command,sizeof(command),".%u grp_txt c=#weather x=^(a|aa)*b$ d",slot);set(mesh,command);
|
|
}
|
|
mesh::Packet packet;packet.header=ROUTE_TYPE_FLOOD|(PAYLOAD_TYPE_GRP_TXT<<PH_TYPE_SHIFT);packet.payload_len=19;
|
|
plain("Alice: "+std::string(160,'a'));
|
|
// A whole-packet work limit returns the reserved suppression bit rather
|
|
// than accidentally permitting a packet after a partial rule evaluation.
|
|
assert(mesh.evaluateFloodPacketFilterMatches(&packet,true,nullptr,0)==(uint64_t(1)<<63));
|
|
}
|
|
int main(){parserAndRollback();allowAndDirect();budgetExhaustion();}
|
|
'''
|
|
|
|
|
|
class FloodRuleCliTests(unittest.TestCase):
|
|
def test_actual_repeater_parser_and_evaluator(self):
|
|
source = (ROOT / "examples/simple_repeater/MyMesh.cpp").read_text()
|
|
header = (ROOT / "examples/simple_repeater/MyMesh.h").read_text()
|
|
parts = [PRELUDE.replace("@ENTRY@", extract_braced(header, "struct FloodPacketFilterEntry"))]
|
|
for marker in ("static const char* skipFloodFilterSpaces(", "static bool floodFilterAsciiEqual(",
|
|
"static bool floodFilterAsciiStartsWith(", "static bool parseFloodFilterUnsigned(",
|
|
"static bool parseFloodFilterPayloadType(", "static bool parseFloodFilterHopSpec(",
|
|
"static bool normalizeFloodFilterScopeName("):
|
|
parts.append(extract_braced(source, marker))
|
|
marker = "static bool parseFloodModerationUnsigned("
|
|
parts.append(extract_braced(source[source.rindex(marker):], marker))
|
|
for marker in ("void MyMesh::setFloodPacketFilter(", "bool MyMesh::floodPacketFilterFieldsMatch(",
|
|
"bool MyMesh::authenticateFloodPacketFilterChannel(", "uint64_t MyMesh::applyFloodPacketFilterStop(",
|
|
"uint64_t MyMesh::evaluateFloodPacketFilterMatches("):
|
|
parts.append(extract_braced(source, marker))
|
|
parts.append(SCENARIOS)
|
|
with tempfile.TemporaryDirectory(prefix="flood-rule-cli-") as folder:
|
|
folder = Path(folder)
|
|
(folder / "Utils.h").write_text(UTILS)
|
|
cpp = folder / "cli.cpp"
|
|
cpp.write_text("\n".join(parts))
|
|
binary = folder / "cli"
|
|
flags = []
|
|
if os.name != "nt":
|
|
flags = ["-fsanitize=address,undefined", "-fno-omit-frame-pointer", "-fno-pie", "-no-pie"]
|
|
subprocess.run(["g++", "-std=c++17", "-O1", "-Wall", "-Wextra", "-Werror", "-Wno-unused-parameter", *flags,
|
|
"-I" + str(folder), "-I" + str(ROOT / "src"), str(cpp), "-o", str(binary)], check=True)
|
|
subprocess.run([str(binary)], check=True)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|