Files
zapret-win-bundle/blockcheck/zapret2/lua/zapret-lib.lua
bol-van d1f72b1edf bundle
2025-12-14 18:52:10 +03:00

1399 lines
45 KiB
Lua
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

HEXDUMP_DLOG_MAX = HEXDUMP_DLOG_MAX or 32
NOT3=bitnot(3)
NOT7=bitnot(7)
-- xor pid,tid,sec,nsec
math.randomseed(bitxor(getpid(),gettid(),clock_gettime()))
-- basic desync function
-- execute given lua code. "desync" is temporary set as global var to be accessible to the code
-- useful for simple fast actions without writing a func
-- arg: code=<lua_code>
function luaexec(ctx, desync)
if not desync.arg.code then
error("luaexec: no 'code' parameter")
end
local fname = desync.func_instance.."_luaexec_code"
if not _G[fname] then
_G[fname] = load(desync.arg.code, fname)
end
-- allow dynamic code to access desync
_G.desync = desync
_G[fname]()
_G.desync = nil
end
-- basic desync function
-- does nothing just acknowledges when it's called
-- no args
function pass(ctx, desync)
DLOG("pass")
end
-- basic desync function
-- prints desync to DLOG
function pktdebug(ctx, desync)
DLOG("desync:")
var_debug(desync)
end
-- basic desync function
-- prints function args
function argdebug(ctx, desync)
var_debug(desync.arg)
end
-- basic desync function
-- prints conntrack positions to DLOG
function posdebug(ctx, desync)
if not desync.track then
DLOG("posdebug: no track")
return
end
local s="posdebug: "..(desync.outgoing and "out" or "in").." time +"..desync.track.pos.dt.."s direct"
for i,pos in pairs({'n','d','b','s','p'}) do
s=s.." "..pos..pos_get(desync, pos, false)
end
s=s.." reverse"
for i,pos in pairs({'n','d','b','s','p'}) do
s=s.." "..pos..pos_get(desync, pos, true)
end
s=s.." payload "..#desync.dis.payload
if desync.reasm_data then
s=s.." reasm "..#desync.reasm_data
end
if desync.decrypt_data then
s=s.." decrypt "..#desync.decrypt_data
end
if desync.replay_piece_count then
s=s.." replay "..desync.replay_piece.."/"..desync.replay_piece_count
end
DLOG(s)
end
-- basic desync function
-- set l7payload to 'arg.payload' if reasm.data or desync.dis.payload contains 'arg.pattern' substring
-- NOTE : this does not set payload on C code side !
-- NOTE : C code will not see payload change. --payload args take only payloads known to C code and cause error if unknown.
-- arg: pattern - substring for search inside reasm_data or desync.dis.payload
-- arg: payload - set desync.l7payload to this if detected
-- arg: undetected - set desync.l7payload to this if not detected
-- test case : nfqws2 --qnum 200 --debug --lua-init=@zapret-lib.lua --lua-init=@zapret-antidpi.lua --lua-init=@zapret-auto.lua --lua-desync=detect_payload_str:pattern=1234:payload=my --lua-desync=fake:blob=0x1234:payload=my
function detect_payload_str(ctx, desync)
if not desync.arg.pattern then
error("detect_payload_str: missing 'pattern'")
end
local data = desync.reasm_data or desync.dis.payload
local b = string.find(data,desync.arg.pattern,1,true)
if b then
DLOG("detect_payload_str: detected '"..desync.arg.payload.."'")
if desync.arg.payload then desync.l7payload = desync.arg.payload end
else
DLOG("detect_payload_str: not detected '"..desync.arg.payload.."'")
if desync.arg.undetected then desync.l7payload = desync.arg.undetected end
end
end
-- this shim is needed then function is orchestrated. ctx services not available
-- have to emulate cutoff in LUA using connection persistent table track.lua_state
function instance_cutoff_shim(ctx, desync, dir)
if ctx then
instance_cutoff(ctx, dir)
elseif not desync.track then
DLOG("instance_cutoff_shim: cannot cutoff '"..desync.func_instance.."' because conntrack is absent")
else
if not desync.track.lua_state.cutoff_shim then
desync.track.lua_state.cutoff_shim = {}
end
if not desync.track.lua_state.cutoff_shim[desync.func_instance] then
desync.track.lua_state.cutoff_shim[desync.func_instance] = {}
end
if type(dir)=="nil" then
-- cutoff both directions by default
desync.track.lua_state.cutoff_shim[desync.func_instance][true] = true
desync.track.lua_state.cutoff_shim[desync.func_instance][false] = true
else
desync.track.lua_state.cutoff_shim[desync.func_instance][dir] = true
end
if b_debug then DLOG("instance_cutoff_shim: cutoff '"..desync.func_instance.."' in="..tostring(type(dir)=="nil" and true or not dir).." out="..tostring(type(dir)=="nil" or dir)) end
end
end
function cutoff_shim_check(desync)
if not desync.track then
DLOG("cutoff_shim_check: cannot check '"..desync.func_instance.."' cutoff because conntrack is absent")
return false
else
local b=desync.track.lua_state.cutoff_shim and
desync.track.lua_state.cutoff_shim[desync.func_instance] and
desync.track.lua_state.cutoff_shim[desync.func_instance][desync.outgoing]
if b and b_debug then
DLOG("cutoff_shim_check: '"..desync.func_instance.."' "..(desync.outgoing and "out" or "in").." cutoff")
end
return b
end
end
-- applies # and $ prefixes. #var means var length, %var means var value
function apply_arg_prefix(desync)
for a,v in pairs(desync.arg) do
local c = string.sub(v,1,1)
if c=='#' then
local blb = blob(desync,string.sub(v,2))
desync.arg[a] = (type(blb)=='string' or type(blb)=='table') and #blb or 0
elseif c=='%' then
desync.arg[a] = blob(desync,string.sub(v,2))
elseif c=='\\' then
c = string.sub(v,2,2);
if c=='#' or c=='%' then
desync.arg[a] = string.sub(v,2)
end
end
end
end
-- copy instance identification and args from execution plan to desync table
-- NOTE : to not lose VERDICT_MODIFY dissect changes pass original desync table
-- NOTE : if a copy was passed and VERDICT_MODIFY returned you must copy modified dissect back to desync table or resend it and return VERDICT_DROP
-- NOTE : args and some fields are substituted. if you need them - make a copy before calling this.
function apply_execution_plan(desync, instance)
desync.func = instance.func
desync.func_n = instance.func_n
desync.func_instance = instance.func_instance
desync.arg = deepcopy(instance.arg)
apply_arg_prefix(desync)
end
-- produce resulting verdict from 2 verdicts
function verdict_aggregate(v1, v2)
local v
v1 = v1 or VERDICT_PASS
v2 = v2 or VERDICT_PASS
if v1==VERDICT_DROP or v2==VERDICT_DROP then
v=VERDICT_DROP
elseif v1==VERDICT_MODIFY or v2==VERDICT_MODIFY then
v=VERDICT_MODIFY
else
v=VERDICT_PASS
end
return v
end
function plan_instance_execute(desync, verdict, instance)
apply_execution_plan(desync, instance)
if cutoff_shim_check(desync) then
DLOG("plan_instance_execute: not calling '"..desync.func_instance.."' because of voluntary cutoff")
elseif not payload_match_filter(desync.l7payload, instance.payload_filter) then
DLOG("plan_instance_execute: not calling '"..desync.func_instance.."' because payload '"..desync.l7payload.."' does not match filter '"..instance.payload_filter.."'")
elseif not pos_check_range(desync, instance.range) then
DLOG("plan_instance_execute: not calling '"..desync.func_instance.."' because pos "..pos_str(desync,instance.range.from).." "..pos_str(desync,instance.range.to).." is out of range '"..pos_range_str(instance.range).."'")
else
DLOG("plan_instance_execute: calling '"..desync.func_instance.."'")
verdict = verdict_aggregate(verdict,_G[instance.func](nil, desync))
end
return verdict
end
function plan_instance_pop(desync)
return (desync.plan and #desync.plan>0) and table.remove(desync.plan, 1)
end
function plan_clear(desync)
while table.remove(desync.plan) do end
end
-- this approach allows nested orchestrators
function orchestrate(ctx, desync)
if not desync.plan then
execution_plan_cancel(ctx)
desync.plan = execution_plan(ctx)
end
end
-- copy desync preserving lua_state
function desync_copy(desync)
local dcopy = deepcopy(desync)
if desync.track then
-- preserve lua state
dcopy.track.lua_state = desync.track.lua_state
end
if desync.plan then
-- preserve execution plan
dcopy.plan = desync.plan
end
return dcopy
end
-- redo what whould be done without orchestration
function replay_execution_plan(desync)
local verdict = VERDICT_PASS
while true do
local instance = plan_instance_pop(desync)
if not instance then break end
verdict = plan_instance_execute(desync, verdict, instance)
end
return verdict
end
-- this function demonstrates how to stop execution of upcoming desync instances and take over their job
-- this can be used, for example, for orchestrating conditional processing without modifying of desync functions code
-- test case : nfqws2 --qnum 200 --debug --lua-init=@zapret-lib.lua --lua-desync=desync_orchestrator_example --lua-desync=pass --lua-desync=pass
function desync_orchestrator_example(ctx, desync)
DLOG("orchestrator: taking over upcoming desync instances")
orchestrate(ctx, desync)
return replay_execution_plan(desync)
end
-- these functions duplicate range check logic from C code
-- mode must be n,d,b,s,x,a
-- pos is {mode,pos}
-- range is {from={mode,pos}, to={mode,pos}, upper_cutoff}
-- upper_cutoff = true means non-inclusive upper boundary
function pos_get_pos(track_pos, mode)
if track_pos then
if mode=='n' then
return track_pos.pcounter
elseif mode=='d' then
return track_pos.pdcounter
elseif mode=='b' then
return track_pos.pbcounter
elseif track_pos.tcp then
if mode=='s' then
return track_pos.tcp.rseq
elseif mode=='p' then
return track_pos.tcp.pos
end
end
end
return 0
end
function pos_get(desync, mode, reverse)
if desync.track then
local track_pos = reverse and desync.track.pos.reverse or desync.track.pos.direct
return pos_get_pos(track_pos,mode)
end
return 0
end
function pos_check_from(desync, range)
if range.from.mode == 'x' then return false end
if range.from.mode ~= 'a' then
if desync.track then
return pos_get(desync, range.from.mode) >= range.from.pos
else
return false
end
end
return true;
end
function pos_check_to(desync, range)
local ps
if range.to.mode == 'x' then return false end
if range.to.mode ~= 'a' then
if desync.track then
ps = pos_get(desync, range.to.mode)
return (ps < range.to.pos) or not range.upper_cutoff and (ps == range.to.pos)
else
return false
end
end
return true;
end
function pos_check_range(desync, range)
return pos_check_from(desync,range) and pos_check_to(desync,range)
end
function pos_range_str(range)
return range.from.mode..range.from.pos..(range.upper_cutoff and '<' or '-')..range.to.mode..range.to.pos
end
function pos_str(desync, pos)
return pos.mode..pos_get(desync, pos.mode)
end
function is_retransmission(desync)
return desync.track and desync.track.pos.direct.tcp and 0==bitand(u32add(desync.track.pos.direct.tcp.uppos_prev, -desync.track.pos.direct.tcp.pos), 0x80000000)
end
-- prepare standard rawsend options from desync
-- repeats - how many time send the packet
-- ifout - override outbound interface (if --bind_fix4, --bind-fix6 enabled)
-- fwmark - override fwmark. desync mark bit(s) will be set unconditionally
function rawsend_opts(desync)
return {
repeats = desync.arg.repeats,
ifout = desync.arg.ifout or desync.ifout,
fwmark = desync.arg.fwmark or desync.fwmark
}
end
-- only basic options. no repeats
function rawsend_opts_base(desync)
return {
ifout = desync.arg.ifout or desync.ifout,
fwmark = desync.arg.fwmark or desync.fwmark
}
end
-- prepare standard reconstruct options from desync
-- badsum - make L4 checksum invalid
-- ip6_preserve_next - use next protocol fields from dissect, do not auto fill values. can be set from code only, not from args
-- ip6_last_proto - last ipv6 "next" protocol. used only by "reconstruct_ip6hdr". can be set from code only, not from args
function reconstruct_opts(desync)
return {
badsum = desync.arg.badsum
}
end
-- combined desync opts
function desync_opts(desync)
return {
rawsend = rawsend_opts(desync),
reconstruct = reconstruct_opts(desync),
ipfrag = desync.arg,
ipid = desync.arg,
fooling = desync.arg
}
end
-- convert binary string to hex data
function string2hex(s)
local ss = ""
for i = 1, #s do
if i>1 then
ss = ss .. " "
end
ss = ss .. string.format("%02X", string.byte(s, i))
end
return ss
end
function has_nonprintable(s)
return s:match("[^ -\\r\\n\\t]")
end
function make_readable(v)
if type(v)=="string" then
return string.gsub(v,"[^ -]",".");
else
return tostring(v)
end
end
-- return hex dump of a binary string if it has nonprintable characters or string itself otherwise
function str_or_hex(s)
if has_nonprintable(s) then
return string2hex(s)
else
return s
end
end
function logical_xor(a,b)
return a and not b or not a and b
end
-- print to DLOG any variable. tables are expanded in the tree form, unprintables strings are hex dumped
function var_debug(v)
local function dbg(v,level)
if type(v)=="table" then
for key, value in pairs(v) do
DLOG(string.rep(" ",2*level).."."..tostring(key))
dbg(v[key],level+1)
end
elseif type(v)=="string" then
DLOG(string.rep(" ",2*level)..type(v).." "..str_or_hex(v))
else
DLOG(string.rep(" ",2*level)..type(v).." "..make_readable(v))
end
end
dbg(v,0)
end
-- make hex dump
function hexdump(s,max)
local l = max<#s and max or #s
local ss = string.sub(s,1,l)
return string2hex(ss)..(#s>max and " ... " or " " )..make_readable(ss)..(#s>max and " ... " or "" )
end
-- make hex dump limited by HEXDUMP_DLOG_MAX chars
function hexdump_dlog(s)
return hexdump(s,HEXDUMP_DLOG_MAX)
end
-- make copy of an array recursively
function deepcopy(orig, copies)
copies = copies or {}
local orig_type = type(orig)
local copy
if orig_type == 'table' then
if copies[orig] then
copy = copies[orig]
else
copy = {}
copies[orig] = copy
for orig_key, orig_value in next, orig, nil do
copy[deepcopy(orig_key, copies)] = deepcopy(orig_value, copies)
end
setmetatable(copy, deepcopy(getmetatable(orig), copies))
end
else -- number, string, boolean, etc
copy = orig
end
return copy
end
-- check if string 'v' in comma separated list 's'
function in_list(s, v)
if s then
for elem in string.gmatch(s, "[^,]+") do
if elem==v then
return true
end
end
end
return false
end
-- blobs can be 0xHEX, field name in desync or global var
-- if name is nil - return def
function blob(desync, name, def)
if not name or #name==0 then
if def then
return def
else
error("empty blob name")
end
end
local blob
if string.sub(name,1,2)=="0x" then
blob = parse_hex(string.sub(name,3))
if not blob then
error("invalid hex string : "..name)
end
else
blob = desync[name]
if not blob then
-- use global var if no field in dissect table
blob = _G[name]
if not blob then
error("blob '"..name.."' unavailable")
end
end
end
return blob
end
function blob_or_def(desync, name, def)
return name and blob(desync,name,def) or def
end
-- repeat pattern as needed to extract part of it with any length
-- pat="12345" len=10 offset=4 => "4512345123"
function pattern(pat, offset, len)
if not pat or #pat==0 then
error("pattern: bad or empty pattern")
end
local off = (offset-1) % #pat
local pats = divint((len + #pat - 1), #pat) + (off==0 and 0 or 1)
return string.sub(string.rep(pat,pats),off+1,off+len)
end
-- decrease by 1 all number values in the array
function zero_based_pos(a)
if not a then return nil end
local b={}
for i,v in ipairs(a) do
b[i] = type(a[i])=="number" and a[i] - 1 or a[i]
end
return b
end
-- delete elements with number value 1
function delete_pos_1(a)
local i=1
while i<=#a do
if type(a[i])=="number" and a[i] == 1 then
table.remove(a,i)
else
i = i+1
end
end
return a
end
-- find pos of the next eol and pos of the next non-eol character after eol
function find_next_line(s, pos)
local p1, p2
p1 = string.find(s,"[\r\n]",pos)
if p1 then
p2 = p1
p1 = p1-1
if string.sub(s,p2,p2)=='\r' then p2=p2+1 end
if string.sub(s,p2,p2)=='\n' then p2=p2+1 end
if p2>#s then p2=nil end
else
p1 = #s
end
return p1,p2
end
function http_dissect_header(header)
local p1,p2
p1,p2 = string.find(header,":")
if p1 then
p2=string.find(header,"[^ \t]",p2+1)
return string.sub(header,1,p1-1), p2 and string.sub(header,p2) or "", p1-1, p2 or #header
end
return nil
end
-- make table with structured http header representation
function http_dissect_headers(http, pos)
local eol,pnext,header,value,idx,headers,pos_endheader,pos_startvalue
headers={}
while pos do
eol,pnext = find_next_line(http,pos)
header = string.sub(http,pos,eol)
if #header == 0 then break end
header,value,pos_endheader,pos_startvalue = http_dissect_header(header)
if header then
headers[string.lower(header)] = { header = header, value = value, pos_start = pos, pos_end = eol, pos_header_end = pos+pos_endheader-1, pos_value_start = pos+pos_startvalue-1 }
end
pos=pnext
end
return headers
end
-- make table with structured http request representation
function http_dissect_req(http)
if not http then return nil; end
local eol,pnext,req,hdrpos
local pos=1
-- skip methodeol empty line(s)
while pos do
eol,pnext = find_next_line(http,pos)
req = string.sub(http,pos,eol)
pos=pnext
if #req>0 then break end
end
hdrpos = pos
if not req or #req==0 then return nil end
pos = string.find(req,"[ \t]")
if not pos then return nil end
local method = string.sub(req,1,pos-1);
pos = string.find(req,"[^ \t]",pos+1)
if not pos then return nil end
pnext = string.find(req,"[ \t]",pos+1)
if not pnext then pnext = #http + 1 end
local uri = string.sub(req,pos,pnext-1)
return { method = method, uri = uri, headers = http_dissect_headers(http,hdrpos) }
end
function http_dissect_reply(http)
if not http then return nil; end
local s, pos, code
s = string.sub(http,1,8)
if s~="HTTP/1.1" and s~="HTTP/1.0" then return nil end
pos = string.find(http,"[ \t\r\n]",10)
code = tonumber(string.sub(http,10,pos-1))
if not code then return nil end
pos = find_next_line(http,pos)
return { code = code, headers = http_dissect_headers(http,pos) }
end
function dissect_url(url)
local p1,pb,pstart,pend
local proto, creds, domain, port, uri
p1 = string.find(url,"[^ \t]")
if not p1 then return nil end
pb = p1
pstart,pend = string.find(url,"[a-z]+://",p1)
if pend then
proto = string.sub(url,pstart,pend-3)
p1 = pend+1
end
pstart,pend = string.find(url,"[@/]",p1)
if pend and string.sub(url,pstart,pend)=='@' then
creds = string.sub(url,p1,pend-1)
p1 = pend+1
end
pstart,pend = string.find(url,"/",p1,true)
if pend then
if pend==pb then
uri = string.sub(url,pb)
else
uri = string.sub(url,pend)
domain = string.sub(url,p1,pend-1)
end
else
if proto then
domain = string.sub(url,p1)
else
uri = string.sub(url,p1)
end
end
if domain then
pstart,pend = string.find(domain,':',1,true)
if pend then
port = string.sub(domain, pend+1)
domain = string.sub(domain, 1, pstart-1)
end
end
return { proto = proto, creds = creds, domain = domain, port = port, uri=uri }
end
function dissect_nld(domain, level)
if domain then
local n=1
for pos=#domain,1,-1 do
if string.sub(domain,pos,pos)=='.' then
if n==level then
return string.sub(domain, pos+1)
end
n=n+1
end
end
if n==level then
return domain
end
end
return nil
end
-- support sni=%var
function tls_mod_shim(desync, blob, modlist, payload)
local p1,p2 = string.find(modlist,"sni=%%[^,]+")
if p1 then
local var = string.sub(modlist,p1+5,p2)
local val = desync[var] or _G[var]
if not val then
error("tls_mod_shim: non-existent var '"..var.."'")
end
modlist = string.sub(modlist,1,p1+3)..val..string.sub(modlist,p2+1)
end
return tls_mod(blob,modlist,payload)
end
-- convert comma separated list of tcp flags to tcp.th_flags bit field
function parse_tcp_flags(s)
local flags={FIN=TH_FIN, SYN=TH_SYN, RST=TH_RST, PSH=TH_PUSH, PUSH=TH_PUSH, ACK=TH_ACK, URG=TH_URG, ECE=TH_ECE, CWR=TH_CWR}
local f=0
local s_upper = string.upper(s)
for flag in string.gmatch(s_upper, "[^,]+") do
if flags[flag] then
f = bitor(f,flags[flag])
else
error("tcp flag '"..flag.."' is invalid")
end
end
return f
end
-- find first tcp options of specified kind in dissect.tcp.options
function find_tcp_option(options, kind)
if options then
for i, opt in pairs(options) do
if opt.kind==kind then return i end
end
end
return nil
end
-- find first ipv6 extension header of specified protocol in dissect.ip6.exthdr
function find_ip6_exthdr(exthdr, proto)
if exthdr then
for i, hdr in pairs(exthdr) do
if hdr.type==proto then return i end
end
end
return nil
end
-- insert ipv6 extension header at specified index. fix next proto chain
function insert_ip6_exthdr(ip6, idx, header_type, data)
local prev
if not ip6.exthdr then ip6.exthdr={} end
if not idx then
-- insert to the end
idx = #ip6.exthdr+1
elseif idx<0 or idx>(#ip6.exthdr+1) then
error("insert_ip6_exthdr: invalid index "..idx)
end
if idx==1 then
prev = ip6.ip6_nxt
ip6.ip6_nxt = header_type
else
prev = ip6.exthdr[idx-1].next
ip6.exthdr[idx-1].next = header_type
end
table.insert(ip6.exthdr, idx, {type = header_type, data = data, next = prev})
end
-- delete ipv6 extension header at specified index. fix next proto chain
function del_ip6_exthdr(ip6, idx)
if idx<=0 or idx>#ip6.exthdr then
error("delete_ip6_exthdr: nonexistent index "..idx)
end
local nxt = ip6.exthdr[idx].next
if idx==1 then
ip6.ip6_nxt = nxt
else
ip6.exthdr[idx-1].next = nxt
end
table.remove(ip6.exthdr, idx)
end
-- fills next proto fields in ipv6 header and extension headers
function fix_ip6_next(ip6, last_proto)
if ip6.exthdr and #ip6.exthdr>0 then
for i=1,#ip6.exthdr do
if i==1 then
-- first header
ip6.ip6_nxt = ip6.exthdr[i].type
end
ip6.exthdr[i].next = i==#ip6.exthdr and (last_proto or IPPROTO_NONE) or ip6.exthdr[i+1].type
end
else
-- no headers
ip6.ip6_nxt = last_proto or IPPROTO_NONE
end
end
-- parse autottl : delta,min-max
function parse_autottl(s)
if s then
local delta,min,max = string.match(s,"([-+]?%d+),(%d+)-(%d+)")
min = tonumber(min)
max = tonumber(max)
delta = tonumber(delta)
if not delta or min>max then
error("parse_autottl: invalid value '"..s.."'")
end
return {delta=delta,min=min,max=max}
else
return nil
end
end
-- calculate ttl value based on incoming_ttl and parsed attl definition (delta,min-max)
function autottl(incoming_ttl, attl)
local function hop_count_guess(incoming_ttl)
-- 18.65.168.125 ( cloudfront ) 255
-- 157.254.246.178 128
-- 1.1.1.1 64
-- guess original ttl. consider path lengths less than 32 hops
local orig
if incoming_ttl>223 then
orig=255
elseif incoming_ttl<128 and incoming_ttl>96 then
orig=128
elseif incoming_ttl<64 and incoming_ttl>32 then
orig=64
else
return nil
end
return orig-incoming_ttl
end
-- return guessed fake ttl value. 0 means unsuccessfull, should not perform autottl fooling
local function autottl_eval(hop_count, attl)
local d,fake
d = hop_count + attl.delta
if d<attl.min then fake=attl.min
elseif d>attl.max then fake=attl.max
else fake=d
end
if attl.delta<0 and fake>=hop_count or attl.delta>=0 and fake<hop_count then return nil end
return fake
end
local hops = hop_count_guess(incoming_ttl)
if not hops then return nil end
return autottl_eval(hops,attl)
end
-- apply standard header mods :
-- ip_ttl=N - set ipv.ip_ttl to N
-- ip6_ttl=N - set ip6.ip6_hlim to N
-- ip_autottl=delta,min-max - set ip.ip_ttl to auto discovered ttl
-- ip6_autottl=delta,min-max - set ip.ip_ttl to auto discovered ttl
-- ip6_hopbyhop[=hex] - add hopbyhop ipv6 header with optional data. data size must be 6+N*8. all zero by default.
-- ip6_hopbyhop2[=hex] - add second hopbyhop ipv6 header with optional data. data size must be 6+N*8. all zero by default.
-- ip6_destopt[=hex] - add destopt ipv6 header with optional data. data size must be 6+N*8. all zero by default.
-- ip6_routing[=hex] - add routing ipv6 header with optional data. data size must be 6+N*8. all zero by default.
-- ip6_ah[=hex] - add authentication ipv6 header with optional data. data size must be 6+N*4. 0000 + 4 random bytes by default.
-- tcp_seq=N - add N to tcp.th_seq
-- tcp_ack=N - add N to tcp.th_ack
-- tcp_ts=N - add N to timestamp value
-- tcp_md5[=hex] - add MD5 header with optional 16-byte data. all zero by default.
-- tcp_flags_set=<list> - set tcp flags in comma separated list
-- tcp_flags_unset=<list> - unset tcp flags in comma separated list
-- tcp_ts_up - move timestamp tcp option to the top if it's present. this allows linux not to accept badack segments without badseq. this is very strange discovery but it works.
-- fool - custom fooling function : fool_func(dis, fooling_options)
function apply_fooling(desync, dis, fooling_options)
local function prepare_bin(hex,def)
local bin = parse_hex(hex)
if not bin then error("apply_fooling: invalid hex string '"..hex.."'") end
return #bin>0 and bin or def
end
local function ttl_discover(arg_ttl,arg_autottl)
local ttl
if arg_autottl and desync.track then
if desync.track.incoming_ttl then
-- use lua_cache to store discovered autottl
if type(desync.track.lua_state.autottl_cache)~="table" then desync.track.lua_state.autottl_cache={} end
if type(desync.track.lua_state.autottl_cache[desync.func_instance])~="table" then desync.track.lua_state.autottl_cache[desync.func_instance]={} end
if not desync.track.lua_state.autottl_cache[desync.func_instance].autottl_found then
desync.track.lua_state.autottl_cache[desync.func_instance].autottl = autottl(desync.track.incoming_ttl,parse_autottl(arg_autottl))
if desync.track.lua_state.autottl_cache[desync.func_instance].autottl then
desync.track.lua_state.autottl_cache[desync.func_instance].autottl_found = true
DLOG("apply_fooling: discovered autottl "..desync.track.lua_state.autottl_cache[desync.func_instance].autottl)
else
DLOG("apply_fooling: could not discover autottl")
end
elseif desync.track.lua_state.autottl_cache[desync.func_instance].autottl then
DLOG("apply_fooling: using cached autottl "..desync.track.lua_state.autottl_cache[desync.func_instance].autottl)
end
ttl=desync.track.lua_state.autottl_cache[desync.func_instance].autottl
else
DLOG("apply_fooling: cannot apply autottl because incoming ttl unknown")
end
end
if not ttl and tonumber(arg_ttl) then
ttl = tonumber(arg_ttl)
end
--io.stderr:write("TTL "..tostring(ttl).."\n")
return ttl
end
local function move_ts_top()
local tsidx = find_tcp_option(dis.tcp.options, TCP_KIND_TS)
if tsidx and tsidx>1 then
table.insert(dis.tcp.options, 1, dis.tcp.options[tsidx])
table.remove(dis.tcp.options, tsidx + 1)
end
end
-- take default fooling from desync.arg
if not fooling_options then fooling_options = desync.arg end
-- use current packet if dissect not given
if not dis then dis = desync.dis end
if dis.tcp then
if tonumber(fooling_options.tcp_seq) then
dis.tcp.th_seq = u32add(dis.tcp.th_seq, fooling_options.tcp_seq)
end
if tonumber(fooling_options.tcp_ack) then
dis.tcp.th_ack = u32add(dis.tcp.th_ack, fooling_options.tcp_ack)
end
if fooling_options.tcp_flags_unset then
dis.tcp.th_flags = bitand(dis.tcp.th_flags, bitnot(parse_tcp_flags(fooling_options.tcp_flags_unset)))
end
if fooling_options.tcp_flags_set then
dis.tcp.th_flags = bitor(dis.tcp.th_flags, parse_tcp_flags(fooling_options.tcp_flags_set))
end
if tonumber(fooling_options.tcp_ts) then
local idx = find_tcp_option(dis.tcp.options,TCP_KIND_TS)
if idx and (dis.tcp.options[idx].data and #dis.tcp.options[idx].data or 0)==8 then
dis.tcp.options[idx].data = bu32(u32add(u32(dis.tcp.options[idx].data),fooling_options.tcp_ts))..string.sub(dis.tcp.options[idx].data,5)
else
DLOG("apply_fooling: timestamp tcp option not present or invalid")
end
end
if fooling_options.tcp_md5 then
if find_tcp_option(dis.tcp.options,TCP_KIND_MD5) then
DLOG("apply_fooling: md5 option already present")
else
table.insert(dis.tcp.options,{kind=TCP_KIND_MD5, data=prepare_bin(fooling_options.tcp_md5,brandom(16))})
end
end
if fooling_options.tcp_ts_up then
move_ts_top(dis.tcp.options)
end
end
if dis.ip6 then
local bin
if fooling_options.ip6_hopbyhop then
bin = prepare_bin(fooling_options.ip6_hopbyhop,"\x00\x00\x00\x00\x00\x00")
insert_ip6_exthdr(dis.ip6,nil,IPPROTO_HOPOPTS,bin)
end
if fooling_options.ip6_hopbyhop2 then
bin = prepare_bin(fooling_options.ip6_hopbyhop2,"\x00\x00\x00\x00\x00\x00")
insert_ip6_exthdr(dis.ip6,nil,IPPROTO_HOPOPTS,bin)
end
-- for possible unfragmentable part
if fooling_options.ip6_destopt then
bin = prepare_bin(fooling_options.ip6_destopt,"\x00\x00\x00\x00\x00\x00")
insert_ip6_exthdr(dis.ip6,nil,IPPROTO_DSTOPTS,bin)
end
if fooling_options.ip6_routing then
bin = prepare_bin(fooling_options.ip6_routing,"\x00\x00\x00\x00\x00\x00")
insert_ip6_exthdr(dis.ip6,nil,IPPROTO_ROUTING,bin)
end
-- for possible fragmentable part
if fooling_options.ip6_destopt2 then
bin = prepare_bin(fooling_options.ip6_destopt2,"\x00\x00\x00\x00\x00\x00")
insert_ip6_exthdr(dis.ip6,nil,IPPROTO_DSTOPTS,bin)
end
if fooling_options.ip6_ah then
-- by default truncated authentication header - only 6 bytes
bin = prepare_bin(fooling_options.ip6_ah,"\x00\x00"..brandom(4))
insert_ip6_exthdr(dis.ip6,nil,IPPROTO_AH,bin)
end
end
if dis.ip then
local ttl = ttl_discover(fooling_options.ip_ttl,fooling_options.ip_autottl)
if ttl then dis.ip.ip_ttl = ttl end
end
if dis.ip6 then
local ttl = ttl_discover(fooling_options.ip6_ttl,fooling_options.ip6_autottl)
if ttl then dis.ip6.ip6_hlim = ttl end
end
if fooling_options.fool and #fooling_options.fool>0 then
if type(_G[fooling_options.fool])=="function" then
DLOG("apply_fooling: calling '"..fooling_options.fool.."'")
_G[fooling_options.fool](dis, fooling_options)
else
error("apply_fooling: fool function '"..tostring(fooling_options.fool).."' does not exist")
end
end
end
-- assign dis.ip.ip_id value according to policy in ipid_options or desync.arg. apply def or "seq" policy if no ip_id options
-- ip_id=seq|rnd|zero|none
-- ip_id_conn - in 'seq' mode save current ip_id in track.lua_state to use it between packets
-- remember ip_id in desync
function apply_ip_id(desync, dis, ipid_options, def)
-- use current packet if dissect not given
if not dis then dis = desync.dis end
if dis.ip then -- ip_id is ipv4 only, ipv6 doesn't have it
-- take default ipid options from desync.arg
if not ipid_options then ipid_options = desync.arg end
local mode = ipid_options.ip_id or def or "seq"
if mode == "seq" then
if desync.track and ipid_options.ip_id_conn then
dis.ip.ip_id = desync.track.lua_state.ip_id or dis.ip.ip_id
desync.track.lua_state.ip_id = dis.ip.ip_id + 1
else
dis.ip.ip_id = desync.ip_id or dis.ip.ip_id
desync.ip_id = dis.ip.ip_id + 1
end
elseif mode == "zero" then
dis.ip.ip_id = 0
elseif mode == "rnd" then
dis.ip.ip_id = math.random(1,0xFFFF)
end
end
end
-- return length of ipv4 or ipv6 header without options and extension headers. should be 20 for ipv4 and 40 for ipv6.
function l3_base_len(dis)
if dis.ip then
return IP_BASE_LEN
elseif dis.ip6 then
return IP6_BASE_LEN
else
return 0
end
end
-- return length of ipv4 options or summary length of all ipv6 extension headers
-- ip6_exthdr_last_idx - count lengths for headers up to this index
function l3_extra_len(dis, ip6_exthdr_last_idx)
local l=0
if dis.ip then
if dis.ip.options then
l = bitand(#dis.ip.options+3,NOT3)
end
elseif dis.ip6 and dis.ip6.exthdr then
local ct
if ip6_exthdr_last_idx and ip6_exthdr_last_idx<=#dis.ip6.exthdr then
ct = ip6_exthdr_last_idx
else
ct = #dis.ip6.exthdr
end
for i=1, ct do
if dis.ip6.exthdr[i].type == IPPROTO_AH then
-- length in 32-bit words
l = l + bitand(3+2+#dis.ip6.exthdr[i].data,NOT3)
else
-- length in 64-bit words
l = l + bitand(7+2+#dis.ip6.exthdr[i].data,NOT7)
end
end
end
return l
end
-- return length of ipv4/ipv6 header with options/extension headers
function l3_len(dis)
return l3_base_len(dis)+l3_extra_len(dis)
end
-- return length of tcp/udp headers without options. should be 20 for tcp and 8 for udp.
function l4_base_len(dis)
if dis.tcp then
return TCP_BASE_LEN
elseif dis.udp then
return UDP_BASE_LEN
else
return 0
end
end
-- return length of tcp options or 0 if not tcp
function l4_extra_len(dis)
local l=0
if dis.tcp and dis.tcp.options then
for i=1, #dis.tcp.options do
l = l + 1
if dis.tcp.options[i].kind~=TCP_KIND_NOOP and dis.tcp.options[i].kind~=TCP_KIND_END then
l = l + 1
if dis.tcp.options[i].data then l = l + #dis.tcp.options[i].data end
end
end
-- 4 byte aligned
l = bitand(3+l,NOT3)
end
return l
end
-- return length of tcp header with options or base length of udp header - 8 bytes
function l4_len(dis)
return l4_base_len(dis)+l4_extra_len(dis)
end
-- return summary extra length of ipv4/ipv6 and tcp headers. 0 if no options, no ext headers
function l3l4_extra_len(dis)
return l3_extra_len(dis)+l4_extra_len(dis)
end
-- return summary length of ipv4/ipv6 and tcp/udp headers
function l3l4_len(dis)
return l3_len(dis)+l4_len(dis)
end
-- return summary length of ipv4/ipv6 , tcp/udp headers and payload
function packet_len(dis)
return l3l4_len(dis) + #dis.payload
end
-- option : ipfrag.ipfrag_disorder - send fragments from last to first
function rawsend_dissect_ipfrag(dis, options)
if options and options.ipfrag and options.ipfrag.ipfrag then
local frag_func = options.ipfrag.ipfrag=="" and "ipfrag2" or options.ipfrag.ipfrag
if type(_G[frag_func]) ~= "function" then
error("rawsend_dissect_ipfrag: ipfrag function '"..tostring(frag_func).."' does not exist")
end
local fragments = _G[frag_func](dis, options.ipfrag)
-- allow ipfrag function to do extheader magic with non-standard "next protocol"
-- NOTE : dis.ip6 must have valid next protocol fields !!!!!
local reconstruct_frag = options.reconstruct and deepcopy(options.reconstruct) or {}
reconstruct_frag.ip6_preserve_next = true
if fragments then
if options.ipfrag.ipfrag_disorder then
for i=#fragments,1,-1 do
DLOG("sending ip fragment "..i)
-- C function
if not rawsend_dissect(fragments[i], options.rawsend, reconstruct_frag) then return false end
end
else
for i, d in pairs(fragments) do
DLOG("sending ip fragment "..i)
-- C function
if not rawsend_dissect(d, options.rawsend, reconstruct_frag) then return false end
end
end
return true
end
-- ipfrag failed. send unfragmented
end
-- C function
return rawsend_dissect(dis, options and options.rawsend, options and options.reconstruct)
end
-- send dissect with tcp segmentation based on mss value. appply specified rawsend options.
function rawsend_dissect_segmented(desync, dis, mss, options)
local discopy = deepcopy(dis)
apply_fooling(desync, discopy, options and options.fooling)
if dis.tcp then
local extra_len = l3l4_extra_len(discopy)
if extra_len >= mss then return false end
local max_data = mss - extra_len
if #discopy.payload > max_data then
local pos=1
local len
local payload=discopy.payload
while pos <= #payload do
len = #payload - pos + 1
if len > max_data then len = max_data end
discopy.payload = string.sub(payload,pos,pos+len-1)
apply_ip_id(desync, discopy, options and options.ipid)
if not rawsend_dissect_ipfrag(discopy, options) then
-- stop if failed
return false
end
discopy.tcp.th_seq = discopy.tcp.th_seq + len
pos = pos + len
end
return true
end
end
apply_ip_id(desync, discopy, options and options.ipid)
-- no reason to segment
return rawsend_dissect_ipfrag(discopy, options)
end
-- send specified payload based on existing L3/L4 headers in the dissect. add seq to tcp.th_seq.
function rawsend_payload_segmented(desync, payload, seq, options)
options = options or desync_opts(desync)
local dis = deepcopy(desync.dis)
if payload then dis.payload = payload end
if dis.tcp and seq then
dis.tcp.th_seq = dis.tcp.th_seq + seq
end
return rawsend_dissect_segmented(desync, dis, desync.tcp_mss, options)
end
-- check if desync.outgoing comply with arg.dir or def if it's not present or "out" of they are not present both. dir can be "in","out","any"
function direction_check(desync, def)
local dir = desync.arg.dir or def or "out"
return desync.outgoing and desync.arg.dir~="in" or not desync.outgoing and dir~="out"
end
-- if dir "in" or "out" cutoff current desync function from opposite direction
function direction_cutoff_opposite(ctx, desync, def)
local dir = desync.arg.dir or def or "out"
if dir=="out" then
-- cutoff in
instance_cutoff_shim(ctx, desync, false)
elseif dir=="in" then
-- cutoff out
instance_cutoff_shim(ctx, desync, true)
end
end
-- return true if l7payload matches filter l7payload_filter - comma separated list of payload types
function payload_match_filter(l7payload, l7payload_filter, def)
local argpl = l7payload_filter or def or "known"
local neg = string.sub(argpl,1,1)=="~"
local pl = neg and string.sub(argpl,2) or argpl
return neg ~= (in_list(pl, "all") or in_list(pl, l7payload) or in_list(pl, "known") and l7payload~="unknown" and l7payload~="empty")
end
-- check if desync payload type comply with payload type list in arg.payload
-- if arg.payload is not present - check for known payload - not empty and not unknown (nfqws1 behavior without "--desync-any-protocol" option)
-- if arg.payload is prefixed with '~' - it means negation
function payload_check(desync, def)
local b = payload_match_filter(desync.l7payload, desync.arg.payload, def)
if not b and b_debug then
local argpl = desync.arg.payload or def or "known"
DLOG("payload_check: payload '"..desync.l7payload.."' does not pass '"..argpl.."' filter")
end
return b
end
-- return name of replay drop field in track.lua_state for the current desync function instance
function replay_drop_key(desync)
return desync.func_instance .. "_replay_drop"
end
-- set/unset replay drop flag in track.lua_state for the current desync function instance
function replay_drop_set(desync, v)
if desync.track then
if v == nil then v=true end
local rdk = replay_drop_key(desync)
if v then
if desync.replay then desync.track.lua_state[replay_drop_key] = true end
else
desync.track.lua_state[replay_drop_key] = nil
end
end
end
-- auto unset replay drop flag if desync is not replay or it's the last replay piece
-- return true if the caller should return VERDICT_DROP
function replay_drop(desync)
if desync.track then
local drop = desync.replay and desync.track.lua_state[replay_drop_key]
if not desync.replay or desync.replay_piece_last then
-- replay stopped or last piece of reasm
replay_drop_set(desync, false)
end
if drop then
DLOG("dropping replay packet because reasm was already sent")
return true
end
end
return false
end
-- true if desync is not replay or it's the first replay piece
function replay_first(desync)
return not desync.replay or desync.replay_piece==1
end
-- generate random host
-- template "google.com", len=16 : h82aj.google.com
-- template "google.com", len=11 : .google.com
-- template "google.com", len=10 : google.com
-- template "google.com", len=7 : gle.com
-- no template, len=6 : b8c54a
-- no template, len=7 : u9a.edu
-- no template, len=10 : jgha7c.com
function genhost(len, template)
if template and #template>0 then
if len <= #template then
return string.sub(template,#template-len+1)
elseif len==(#template+1) then
return "."..template
else
return brandom_az(1)..brandom_az09(len-#template-2).."."..template
end
else
if len>=7 then
local tlds = {"com","org","net","edu","gov","biz"}
local tld = tlds[math.random(#tlds)]
return brandom_az(1)..brandom_az09(len-#tld-1-1).."."..tld
else
return brandom_az(1)..brandom_az09(len-1)
end
end
end
-- return ip addr of target host in text form
function host_ip(desync)
return desync.target.ip and ntop(desync.target.ip) or desync.target.ip6 and ntop(desync.target.ip6)
end
-- return hostname of target host if present or ip address in text form otherwise
function host_or_ip(desync)
if desync.track and desync.track.hostname then
return desync.track.hostname
end
return host_ip(desync)
end
function is_absolute_path(path)
if string.sub(path,1,1)=='/' then return true end
local un = uname()
return string.sub(un.sysname,1,6)=="CYGWIN" and string.sub(path,2,2)==':'
end
function append_path(path,file)
return string.sub(path,#path,#path)=='/' and path..file or path.."/"..file
end
function writeable_file_name(filename)
if is_absolute_path(filename) then return filename end
local writedir = os.getenv("WRITEABLE")
if not writedir then return filename end
return append_path(writedir, filename)
end
-- arg : wsize=N . tcp window size
-- arg : scale=N . tcp option scale factor
-- return : true of changed anything
function wsize_rewrite(dis, arg)
local b = false
if arg.wsize then
local wsize = tonumber(arg.wsize)
DLOG("window size "..dis.tcp.th_win.." => "..wsize)
dis.tcp.th_win = tonumber(arg.wsize)
b = true
end
if arg.scale then
local scale = tonumber(arg.scale)
local i = find_tcp_option(dis.tcp.options, TCP_KIND_SCALE)
if i then
local oldscale = u8(dis.tcp.options[i].data)
if scale>oldscale then
DLOG("not increasing scale factor")
elseif scale<oldscale then
DLOG("scale factor "..oldscale.." => "..scale)
dis.tcp.options[i].data = bu8(scale)
b = true
end
end
end
return b
end
-- standard fragmentation to 2 ip fragments
-- function returns 2 dissects with fragments
-- option : ipfrag_pos_udp - udp frag position. ipv4 : starting from L4 header. ipb6: starting from fragmentable part. must be multiple of 8. default 8
-- option : ipfrag_pos_tcp - tcp frag position. ipv4 : starting from L4 header. ipb6: starting from fragmentable part. must be multiple of 8. default 32
-- option : ipfrag_next - next protocol field in ipv6 fragment extenstion header of the second fragment. same as first by default.
function ipfrag2(dis, ipfrag_options)
local function frag_idx(exthdr)
-- fragment header after hopbyhop, destopt, routing
-- allow second destopt header to be in fragmentable part
-- test case : --lua-desync=send:ipfrag:ipfrag_pos_tcp=40:ip6_hopbyhop:ip6_destopt:ip6_destopt2
-- WINDOWS may not send second ipv6 fragment with next protocol 60 (destopt)
-- test case windows : --lua-desync=send:ipfrag:ipfrag_pos_tcp=40:ip6_hopbyhop:ip6_destopt:ip6_destopt2:ipfrag_next=255
if exthdr then
local first_destopts
for i=1,#exthdr do
if exthdr[i].type==IPPROTO_DSTOPTS then
first_destopts = i
break
end
end
for i=#exthdr,1,-1 do
if exthdr[i].type==IPPROTO_HOPOPTS or exthdr[i].type==IPPROTO_ROUTING or (exthdr[i].type==IPPROTO_DSTOPTS and i==first_destopts) then
return i+1
end
end
end
return 1
end
local pos
local dis1, dis2
local l3
if dis.tcp then
pos = ipfrag_options.ipfrag_pos_tcp or 32
elseif dis.udp then
pos = ipfrag_options.ipfrag_pos_udp or 8
else
pos = ipfrag_options.ipfrag_pos or 32
end
DLOG("ipfrag2")
if not pos then
error("ipfrag2: no frag position")
end
l3 = l3_len(dis)
if bitand(pos,7)~=0 then
error("ipfrag2: frag position must be multiple of 8")
end
if (pos+l3)>0xFFFF then
error("ipfrag2: too high frag offset")
end
local plen = l3 + l4_len(dis) + #dis.payload
if (pos+l3)>=plen then
DLOG("ipfrag2: ip frag pos exceeds packet length. ipfrag cancelled.")
return nil
end
if dis.ip then
-- ipv4 frag is done by both lua and C part
-- lua code must correctly set ip_len, IP_MF and ip_off and provide full unfragmented payload
-- ip_len must be set to valid value as it would appear in the fragmented packet
-- ip_off must be set to fragment offset and IP_MF bit must be set if it's not the last fragment
-- C code constructs unfragmented packet then moves everything after ip header according to ip_off and ip_len
-- ip_id must not be zero or fragment will be dropped
local ip_id = dis.ip.ip_id==0 and math.random(1,0xFFFF) or dis.ip.ip_id
dis1 = deepcopy(dis)
-- ip_len holds the whole packet length starting from the ip header. it includes ip, transport headers and payload
dis1.ip.ip_len = l3 + pos -- ip header + first part up to frag pos
dis1.ip.ip_off = IP_MF -- offset 0, IP_MF - more fragments
dis1.ip.ip_id = ip_id
dis2 = deepcopy(dis)
dis2.ip.ip_off = bitrshift(pos,3) -- offset = frag pos, IP_MF - not set
dis2.ip.ip_len = plen - pos -- unfragmented packet length - frag pos
dis2.ip.ip_id = ip_id
end
if dis.ip6 then
-- ipv6 frag is done by both lua and C part
-- lua code must insert fragmentation extension header at any desirable position, fill fragment offset, more fragments flag and ident
-- lua must set up ip6_plen as it would appear in the fragmented packet
-- C code constructs unfragmented packet then moves fragmentable part as needed
local idxfrag = frag_idx(dis.ip6.exthdr)
local l3extra = l3_extra_len(dis, idxfrag-1) + 8 -- all ext headers before frag + 8 bytes for frag header
local ident = math.random(1,0xFFFFFFFF)
dis1 = deepcopy(dis)
insert_ip6_exthdr(dis1.ip6, idxfrag, IPPROTO_FRAGMENT, bu16(IP6F_MORE_FRAG)..bu32(ident))
dis1.ip6.ip6_plen = l3extra + pos
dis2 = deepcopy(dis)
insert_ip6_exthdr(dis2.ip6, idxfrag, IPPROTO_FRAGMENT, bu16(pos)..bu32(ident))
-- only next proto of the first fragment is considered by standard
-- fragments with non-zero offset can have different "next protocol" field
-- this can be used to evade protection systems
if ipfrag_options.ipfrag_next then
dis2.ip6.exthdr[idxfrag].next = tonumber(ipfrag_options.ipfrag_next)
end
dis2.ip6.ip6_plen = plen - IP6_BASE_LEN + 8 - pos -- packet len without frag + 8 byte frag header - ipv6 base header
end
return {dis1,dis2}
end