nepenthes/components/stutter.lua
2025-09-09 18:41:42 +00:00

225 lines
4.3 KiB
Lua
Executable file

#!/usr/bin/env lua5.4
local cqueues = require 'cqueues'
local auxlib = require 'cqueues.auxlib'
local corewait = require 'daemonparts.corewait'
local rand = require 'openssl.rand'
for i = 1, 5 do
if not rand.ready() then
-- luacov: disable
if i == 5 then
error("Unable to seed")
end
-- luacov: enable
end
end
local _M = {}
local function merge( a, b )
for i in ipairs(b) do
a[ #a + 1 ] = b[i]
end
return a
end
--
-- Recursive algorithm: Given a number of bytes per second,
-- create two sets of bytes/sec that eat half the time and
-- half the bytes, randomly distributed; split both of those
-- until the delay is under a suitable threshold and/or there's
-- not enough bytes to reasonably split the packet.
--
local function split( delay, bytes )
if bytes < 2 then
return { {
bytes = bytes,
delay = delay
} }
end
if delay < 500 then
return { {
bytes = bytes,
delay = delay
} }
end
local left_bytes
if bytes == 2 then
left_bytes = 1
else
left_bytes = rand.uniform( bytes - 1 ) + 1
end
local right_bytes = bytes - left_bytes
local left_delay = rand.uniform( delay - 1 ) + 1
local right_delay = delay - left_delay
local left = split( left_delay, left_bytes )
local right = split( right_delay, right_bytes )
return merge(left, right)
end
---
-- Generate a unique pattern of when and how much data is sent.
--
function _M.generate_pattern( delay, bytes )
assert(delay > 0)
assert(bytes > 0)
--
-- The original Nepenthes 1.x algorithm. Easily identified by
-- it's constant rate of output.
--
--local chunk_size = #s
--local delay = rate
--repeat
--chunk_size = chunk_size // 2
--delay = delay / 2
--until delay < 1
--
-- Fire the recursively splitting packet randomizer. Math is entirely
-- integer, so convert delay into milliseconds.
--
local ret = split( delay * 1000, bytes )
--
-- Occasionally, part of the tree the above creates doesn't have
-- enough bytes for the amount of delay it needs to split up - let's
-- just reroll those. Add the packet with the most bytes and retry.
--
local largest = 1
local to_fix = {}
for i, v in ipairs(ret) do
if v.bytes > ret[largest].bytes then
largest = i
end
if v.delay > 500 then
to_fix[ #to_fix + 1 ] = i
end
end
if #to_fix > 0 then
local new_delay = ret[largest].delay
local new_bytes = ret[largest].bytes
for i, fix in ipairs( to_fix ) do -- luacheck: ignore 213
new_delay = new_delay + ret[fix].delay
new_bytes = new_bytes + ret[fix].bytes
end
to_fix[ #to_fix + 1 ] = largest
table.sort(to_fix, function( a, b ) return a > b end )
for i, fix in ipairs(to_fix) do -- luacheck: ignore 213
table.remove(ret, fix)
end
ret = merge( ret, split( new_delay, new_bytes ) )
end
--
-- Convert delay back into seconds.
--
for i, v in ipairs( ret ) do -- luacheck: ignore 213
v.delay = v.delay / 1000
end
return ret
end
function _M.delay_iterator( s, log_entry, pattern )
--
-- Use a coroutine as the actual iterator. This way <close>
-- works as expected, and the log entry gets marked 'complete'
-- no matter what happens in the end.
--
local start_time = cqueues.monotime()
local iter = coroutine.create(function()
local sl <close> = log_entry
repeat
local block = table.remove(pattern, 1)
if not block then
sl.bytes_sent = sl.bytes_sent + #s
return s
end
local ret = s:sub(1, block.bytes)
s = s:sub(block.bytes + 1, #s)
sl.bytes_sent = sl.bytes_sent + #ret
sl.delay = cqueues.monotime() - start_time
corewait.poll(block.delay)
if #s > 0 then
coroutine.yield( ret )
else
return ret
end
until #s <= 0
end)
--
-- Iterator wrapper so Microdyne knows what to do with it.
-- Also use cqueues.auxlib.resume to avoid the 'nested coroutine'
-- problem - otherwise it would be simpler to just call
-- coroutine.wrap().
--
-- Because to-be-closed values in a coroutine aren't closed on
-- error, we also need to leverage the garbage collector to
-- clean up. :(
--
return setmetatable({},
{
__call = function()
if coroutine.status( iter ) == 'dead' then
coroutine.close( iter )
return nil
end
local ret, out = auxlib.resume( iter )
if not ret then
coroutine.close( iter )
error(out)
end
return out
end,
__gc = function()
coroutine.close( iter )
end
})
end
return _M