-- standard automation/orchestration code -- this is related to making dynamic strategy decisions without rewriting or altering strategy function code -- orchestrators can decide which instances to call or not to call or pass them dynamic arguments -- failure and success detectors test potential block conditions for orchestrators -- standard host key generator for per-host storage -- arg: reqhost - require hostname, do not work with ip -- arg: nld=N - cut hostname to N level domain. NLD=2 static.intranet.microsoft.com => microsoft.com function standard_hostkey(desync) local hostkey = desync.track and desync.track.hostname if hostkey then if desync.arg.nld and tonumber(desync.arg.nld)>0 then -- dissect_nld returns nil if domain is invalid or does not have this NLD -- fall back to original hostkey if it fails local hktemp = dissect_nld(hostkey, tonumber(desync.arg.nld)) if hktemp then hostkey = hktemp end end elseif not desync.arg.reqhost then hostkey = host_ip(desync) end return hostkey end -- per-host storage -- arg: key - a string - table name inside autostate table. to allow multiple orchestrator instances to use single host storage -- arg: hostkey - hostkey generator function name function automate_host_record(desync) local hostkey, hkf, askey if desync.arg.hostkey then if type(_G[desync.arg.hostkey])~="function" then error("automate: invalid hostkey function '"..desync.arg.hostkey.."'") end hkf = _G[desync.arg.hostkey] else hkf = standard_hostkey end hostkey = hkf(desync) if not hostkey then DLOG("automate: host record key unavailable") return nil end askey = (desync.arg.key and #desync.arg.key>0) and desync.arg.key or desync.func_instance DLOG("automate: host record key 'autostate."..askey.."."..hostkey.."'") if not autostate then autostate = {} end if not autostate[askey] then autostate[askey] = {} end if not autostate[askey][hostkey] then autostate[askey][hostkey] = {} end return autostate[askey][hostkey] end -- per-connection storage function automate_conn_record(desync) if not desync.track.lua_state.automate then desync.track.lua_state.automate = {} end return desync.track.lua_state.automate end -- counts failure, optionally (if crec is given) prevents dup failure counts in a single connection -- if 'maxtime' between failures is exceeded then failure count is reset -- return true if threshold ('fails') is reached -- hres is host record. host or ip bound table -- cres is connection record. connection bound table function automate_failure_counter(hrec, crec, fails, maxtime) if crec and crec.failure then DLOG("automate: duplicate failure in the same connection. not counted") else if crec then crec.failure = true end local tnow=os.time() if not hrec.failure_time_last then hrec.failure_time_last = tnow end if not hrec.failure_counter then hrec.failure_counter = 0 elseif tnow>(hrec.failure_time_last + maxtime) then DLOG("automate: failure counter reset because last failure was "..(tnow - hrec.failure_time_last).." seconds ago") hrec.failure_counter = 0 end hrec.failure_counter = hrec.failure_counter + 1 hrec.failure_time_last = tnow if b_debug then DLOG("automate: failure counter "..hrec.failure_counter..(fails and ('/'..fails) or '')) end if fails and hrec.failure_counter>=fails then hrec.failure_counter = nil -- reset counter return true end end return false end -- resets failure counter if it has started counting function automate_failure_counter_reset(hrec) if hrec.failure_counter then DLOG("automate: failure counter reset") hrec.failure_counter = nil end end -- location is url compatible with Location: header -- hostname is original hostname function is_dpi_redirect(hostname, location) local ds = dissect_url(location) if ds.domain then local sld1 = dissect_nld(hostname,2) local sld2 = dissect_nld(ds.domain,2) return sld2 and sld1~=sld2 end return false end function standard_detector_defaults(arg) return { inseq = tonumber(arg.inseq) or 4096, retrans = tonumber(arg.retrans) or 3, maxseq = tonumber(arg.maxseq) or 32768, udp_in = tonumber(arg.udp_in) or 1, udp_out = tonumber(arg.udp_out) or 4, no_http_redirect = arg.no_http_redirect, no_rst = arg.no_rst } end -- standard failure detector -- works with tcp and udp -- detected failures: -- incoming RST -- incoming http redirection -- outgoing retransmissions -- udp too much out with too few in -- arg: maxseq= - tcp: test retransmissions only within this relative sequence. default is 32K -- arg: retrans=N - tcp: retrans count threshold. default is 3 -- arg: inseq= - tcp: maximum relative sequence number to treat incoming RST as DPI reset. default is 4K -- arg: no_http_redirect - tcp: disable http_reply dpi redirect trigger -- arg: no_rst - tcp: disable incoming RST trigger -- arg: udp_out - udp: >= outgoing udp packets. default is 4 -- arg: udp_in - udp: with <= incoming udp packets. default is 1 function standard_failure_detector(desync, crec) local arg = standard_detector_defaults(desync.arg) local trigger = false if desync.dis.tcp then local seq = pos_get(desync,'s') if desync.outgoing then if #desync.dis.payload>0 and arg.retrans and arg.maxseq>0 and seq<=arg.maxseq and (crec.retrans or 0)=arg.retrans end end else if not arg.no_rst and arg.inseq>0 and bitand(desync.dis.tcp.th_flags, TH_RST)~=0 and seq>=1 then trigger = seq<=arg.inseq if b_debug then if trigger then DLOG("standard_failure_detector: incoming RST s"..seq.." in range s"..arg.inseq) else DLOG("standard_failure_detector: not counting incoming RST s"..seq.." beyond s"..arg.inseq) end end elseif not arg.no_http_redirect and desync.l7payload=="http_reply" and desync.track.hostname then local hdis = http_dissect_reply(desync.dis.payload) if hdis and (hdis.code==302 or hdis.code==307) and hdis.headers.location and hdis.headers.location then trigger = is_dpi_redirect(desync.track.hostname, hdis.headers.location.value) if b_debug then if trigger then DLOG("standard_failure_detector: http redirect "..hdis.code.." to '"..hdis.headers.location.value.."'. looks like DPI redirect.") else DLOG("standard_failure_detector: http redirect "..hdis.code.." to '"..hdis.headers.location.value.."'. NOT a DPI redirect.") end end end end end elseif desync.dis.udp then if desync.outgoing then if arg.udp_out>0 then local pos_out = pos_get(desync,'n',false) local pos_in = pos_get(desync,'n',true) trigger = pos_out>=arg.udp_out and pos_in<=arg.udp_in if trigger then if b_debug then DLOG("standard_failure_detector: arg.udp_out "..pos_out..">="..arg.udp_out.." arg.udp_in "..pos_in.."<="..arg.udp_in) end end end end end return trigger end -- standard success detector -- success means previous failures were temporary and counter should be reset -- detected successes: -- tcp: outgoing seq is beyond 'maxseq' and maxseq>0 -- tcp: incoming seq is beyond 'inseq' and inseq>0 -- udp: incoming packets count > `udp_in` and `udp_out`>0 -- arg: maxseq= - tcp: success if outgoing relative sequence is beyond this value. default is 32K -- arg: inseq= - tcp: success if incoming relative sequence is beyond this value. default is 4K -- arg: udp_out - udp : must be nil or >0 to test udp_in -- arg: udp_in - udp: if number if incoming packets > udp_in it means success function standard_success_detector(desync, crec) local arg = standard_detector_defaults(desync.arg) if desync.dis.tcp then local seq = pos_get(desync,'s') if desync.outgoing then if arg.maxseq>0 and seq>arg.maxseq then DLOG("standard_success_detector: outgoing s"..seq.." is beyond s"..arg.maxseq..". treating connection as successful") return true end else if arg.inseq>0 and seq>arg.inseq then DLOG("standard_success_detector: incoming s"..seq.." is beyond s"..arg.inseq..". treating connection as successful") return true end end elseif desync.dis.udp then if not desync.outgoing then local pos = pos_get(desync,'n') if arg.udp_out>0 and pos>arg.udp_in then if b_debug then DLOG("standard_success_detector: arg.udp_in "..pos..">"..arg.udp_in) end return true end end end return false end -- calls success and failure detectors -- resets counter if success is detected -- increases counter if failure is detected -- returns true if failure counter exceeds threshold function automate_failure_check(desync, hrec, crec) if crec.nocheck then return false end local failure_detector, success_detector if desync.arg.failure_detector then if type(_G[desync.arg.failure_detector])~="function" then error("automate: invalid failure detector function '"..desync.arg.failure_detector.."'") end failure_detector = _G[desync.arg.failure_detector] else failure_detector = standard_failure_detector end if desync.arg.success_detector then if type(_G[desync.arg.success_detector])~="function" then error("automate: invalid success detector function '"..desync.arg.success_detector.."'") end success_detector = _G[desync.arg.success_detector] else success_detector = standard_success_detector end if success_detector(desync, crec) then crec.nocheck = true DLOG("automate: success detected") automate_failure_counter_reset(hrec) return false end if failure_detector(desync, crec) then crec.nocheck = true DLOG("automate: failure detected") local fails = tonumber(desync.arg.fails) or 3 local maxtime = tonumber(desync.arg.time) or 60 return automate_failure_counter(hrec, crec, fails, maxtime) end return false end -- circularily change strategy numbers when failure count reaches threshold ('fails') -- this orchestrator requires redirection of incoming traffic to cache RST and http replies ! -- each orchestrated instance must have strategy=N arg, where N starts from 1 and increment without gaps -- if 'final' arg is present in an orchestrated instance it stops rotation -- arg: fails=N - failture count threshold. default is 3 -- arg: time= - if last failure happened earlier than `maxtime` seconds ago - reset failure counter. default is 60. -- arg: success_detector - success detector function name -- arg: failure_detector - failure detector function name -- arg: hostkey - hostkey generator function name -- args for failure detector - see standard_failure_detector or your own detector -- args for success detector - see standard_success_detector or your own detector -- args for hostkey generator - see standard_hostkey or your own generator -- test case: nfqws2 --qnum 200 --debug --lua-init=@zapret-lib.lua --lua-init=@zapret-auto.lua --in-range=-s34228 --lua-desync=circular --lua-desync=argdebug:strategy=1 --lua-desync=argdebug:strategy=2 function circular(ctx, desync) local function count_strategies(hrec) if not hrec.ctstrategy then local uniq={} local n=0 for i,instance in pairs(desync.plan) do if instance.arg.strategy then n = tonumber(instance.arg.strategy) if not n or n<1 then error("circular: strategy number '"..tostring(instance.arg.strategy).."' is invalid") end uniq[tonumber(instance.arg.strategy)] = true if instance.arg.final then hrec.final = n end end end n=0 for i,v in pairs(uniq) do n=n+1 end if n~=#uniq then error("circular: strategies numbers must start from 1 and increment. gaps are not allowed.") end hrec.ctstrategy = n end end -- take over execution. prevent further instance execution in case of error orchestrate(ctx, desync) if not desync.track then DLOG_ERR("circular: conntrack is missing but required") return end local hrec = automate_host_record(desync) if not hrec then DLOG("circular: passing with no tampering") return end count_strategies(hrec) if hrec.ctstrategy==0 then error("circular: add strategy=N tag argument to each following instance ! N must start from 1 and increment") end if not hrec.nstrategy then DLOG("circular: start from strategy 1") hrec.nstrategy = 1 end local verdict = VERDICT_PASS if hrec.final~=hrec.nstrategy then local crec = automate_conn_record(desync) if automate_failure_check(desync, hrec, crec) then hrec.nstrategy = (hrec.nstrategy % hrec.ctstrategy) + 1 DLOG("circular: rotate strategy to "..hrec.nstrategy) if hrec.nstrategy == hrec.final then DLOG("circular: final strategy "..hrec.final.." reached. will rotate no more.") end end end DLOG("circular: current strategy "..hrec.nstrategy) while true do local instance = plan_instance_pop(desync) if not instance then break end if instance.arg.strategy and tonumber(instance.arg.strategy)==hrec.nstrategy then verdict = plan_instance_execute(desync, verdict, instance) end end return verdict end -- test iff functions function cond_true(desync) return true end function cond_false(desync) return false end -- arg: percent - of true . 50 by default function cond_random(desync) return math.random(0,99)<(tonumber(desync.arg.percent) or 50) end -- this iif function detects packets having 'arg.pattern' string in their payload -- test case : nfqws2 --qnum 200 --debug --lua-init=@zapret-lib.lua --lua-init=@zapret-auto.lua --lua-desync=condition:iff=cond_payload_str:pattern=1234 --lua-desync=argdebug:testarg=1 --lua-desync=argdebug:testarg=2:morearg=xyz -- test case (true) : echo aaz1234zzz | ncat -4u 1.1.1.1 443 -- test case (false) : echo aaze124zzz | ncat -4u 1.1.1.1 443 function cond_payload_str(desync) if not desync.arg.pattern then error("cond_payload_str: missing 'pattern'") end return string.find(desync.dis.payload,desync.arg.pattern,1,true) end -- check iff function available. error if not function require_iff(desync, name) if not desync.arg.iff then error(name..": missing 'iff' function") end if type(_G[desync.arg.iff])~="function" then error(name..": invalid 'iff' function '"..desync.arg.iff.."'") end end -- execute further desync instances only if user-provided 'iff' function returns true -- for example, this can be used by custom protocol detectors -- arg: iff - condition function. takes desync as arg and returns bool. (cant use 'if' because of reserved word) -- arg: neg - invert condition function result -- test case : nfqws2 --qnum 200 --debug --lua-init=@zapret-lib.lua --lua-init=@zapret-auto.lua --lua-desync=condition:iff=cond_random --lua-desync=argdebug:testarg=1 --lua-desync=argdebug:testarg=2:morearg=xyz function condition(ctx, desync) require_iff(desync, "condition") orchestrate(ctx, desync) if logical_xor(_G[desync.arg.iff](desync), desync.arg.neg) then DLOG("condition: true") return replay_execution_plan(desync) else DLOG("condition: false") plan_clear(desync) end end -- clear execution plan if user provided 'iff' functions returns true -- can be used with other orchestrators to stop execution conditionally -- arg: iff - condition function. takes desync as arg and returns bool. (cant use 'if' because of reserved word) -- arg: neg - invert condition function result -- test case : nfqws2 --qnum 200 --debug --lua-init=@zapret-lib.lua --lua-init=@zapret-auto.lua --in-range=-s1 --lua-desync=circular --lua-desync=stopif:iff=cond_random:strategy=1 --lua-desync=argdebug:strategy=1 --lua-desync=argdebug:strategy=2 function stopif(ctx, desync) require_iff(desync, "stopif") orchestrate(ctx, desync) if logical_xor(_G[desync.arg.iff](desync), desync.arg.neg) then DLOG("stopif: true") plan_clear(desync) else -- do not do anything. allow other orchestrator to finish the plan DLOG("stopif: false") end end