game-udp: z2k_game_udp Lua handler actually applies repeats + fix profile layout

The built-in fake() in upstream zapret-antidpi.lua calls
rawsend_payload_segmented without passing options, so `repeats=N` is silently
dropped for UDP payloads. This made every nfqws1-era Roblox recipe
(fake + blob=unknown_udp + repeats=10 + ttl=4) a no-op under nfqws2 — which
is why the existing 6 game strategies in config_official.sh "never worked"
for Evgeniy or anyone else. On top of that, each strategy referenced
blob=quic_initial_www_google_com, but the registered alias is quic_google
(the filename is just the on-disk backing), so even a working fake() would
have errored out with "blob unavailable".

z2k_game_udp mirrors zapret-antidpi.lua:rst() — deepcopy, apply_fooling,
apply_ip_id, rawsend_dissect_ipfrag(dis, desync_opts(desync)) — so repeats,
ip_ttl/ip_autottl, ipfrag, and ip_id all flow through the rawsend options
exactly like classic nfqws1. Confirmed end-to-end on a live Roblox session:
10-14 quic_google fakes per real packet, autocircular rotating across 6
strategy/cutoff combinations.

The six strategies also needed to share a single profile with the circular
action — prior revision put each --lua-desync under its own --new, so
profile matching picked the circular profile first and the strategies were
never reached.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Necronicle 2026-04-13 00:37:19 +03:00
parent e7295600ec
commit a6c607d75f
2 changed files with 125 additions and 15 deletions

View file

@ -1111,3 +1111,104 @@ function z2k_strategy_profile(ctx, desync)
end end
end end
end end
-- ---------------------------------------------------------------------------
-- z2k_game_udp: UDP fake-injection desync for game/unknown protocols.
-- ---------------------------------------------------------------------------
-- Problem this solves
-- nfqws2's built-in `fake` action (zapret-antidpi.lua:fake) calls
-- rawsend_payload_segmented(desync, fake_payload) WITHOUT options and without
-- apply_fooling(), so BOTH `ip_ttl` AND `repeats` are silently dropped for
-- UDP fakes. That made it impossible to replicate the classic nfqws1 recipe
--
-- --dpi-desync=fake
-- --dpi-desync-any-protocol=1
-- --dpi-desync-fake-unknown-udp=<blob>
-- --dpi-desync-ttl=4
-- --dpi-desync-repeats=10
--
-- which works reliably for Roblox and other low-latency UDP game protocols
-- behind Russian DPI on Keenetic.
--
-- What this handler does
-- For each outgoing UDP datagram that matches the payload filter:
-- 1. deepcopy the current dissect
-- 2. replace the L7 payload with the configured blob
-- 3. apply_fooling(..) — honours ip_ttl / ip6_ttl / ip_autottl / tcp_*
-- 4. apply_ip_id(..) — keeps ip_id sane
-- 5. rawsend_dissect_ipfrag with desync_opts(desync) so `repeats=N` from
-- the command line actually takes effect
-- The original packet is kept (no drop), matching nfqws1's fake behaviour
-- where the real packet is allowed through after the fakes.
--
-- Args (all optional unless noted)
-- blob=<name> REQUIRED. fake payload blob (e.g. quic_initial_www_google_com)
-- ip_ttl=<int> IPv4 TTL for the fake packet (e.g. 4)
-- ip6_ttl=<int> IPv6 hop limit for the fake packet
-- ip_autottl=<spec> auto-derived TTL (see zapret-lib parse_autottl)
-- repeats=<int> how many copies of the fake to emit per real packet
-- dir=out|in|any direction filter (default: out)
-- payload=<list> comma-separated l7 filter (default: all)
-- optional skip silently if blob is missing
-- badsum send with deliberately bad L4 checksum (DPI fooling)
--
-- Usage example (place in /opt/zapret2/init.d/<platform>/custom.d/)
-- NFQWS_OPT_DESYNC_GAME="--filter-udp=1024-65535 ${GAME_IPSET_OPT}\
-- --in-range=a --out-range=-n2 --payload=all \
-- --lua-desync=z2k_game_udp:dir=out:blob=quic_initial_www_google_com:ip_ttl=4:repeats=10"
--
-- Mirrors nfqws1 cutoff=n2 via --out-range=-n2 at the wrapper level, and
-- repeats=10 / ip_ttl=4 / blob=... via the handler args.
function z2k_game_udp(ctx, desync)
-- Always fire on outgoing side by default; cut off opposite direction so
-- the instance doesn't waste cycles on inbound replies.
direction_cutoff_opposite(ctx, desync)
-- Only UDP. For related icmp packets (e.g. ICMP unreachable) pass through
-- without cutting off the instance, mirroring fake()/rst() in zapret-antidpi.
if not desync.dis.udp then
if not desync.dis.icmp then instance_cutoff_shim(ctx, desync) end
return
end
if not (direction_check(desync) and payload_check(desync, "all")) then
return
end
-- Only emit fakes on the first replay pass (mirrors built-in fake).
if not replay_first(desync) then
DLOG("z2k_game_udp: not acting on further replay pieces")
return
end
if not desync.arg.blob then
error("z2k_game_udp: 'blob' arg required")
end
if desync.arg.optional and not blob_exist(desync, desync.arg.blob) then
DLOG("z2k_game_udp: blob '"..desync.arg.blob.."' not found. skipped")
return
end
local fake_payload = blob(desync, desync.arg.blob)
if b_debug then
DLOG("z2k_game_udp: blob="..desync.arg.blob.." ttl="..tostring(desync.arg.ip_ttl).." repeats="..tostring(desync.arg.repeats))
end
-- Build the fake packet from the current dissect. deepcopy so we don't
-- perturb the real packet the kernel will deliver afterwards.
local dis = deepcopy(desync.dis)
dis.payload = fake_payload
-- Apply ip_ttl / ip_autottl / ip6_ttl / badsum etc. Crucially, this is
-- where ip_ttl actually gets written to dis.ip.ip_ttl — the built-in
-- fake() skips this step, which is the whole reason we exist.
apply_fooling(desync, dis)
apply_ip_id(desync, dis, nil, "none")
-- rawsend_dissect_ipfrag honours options.rawsend.repeats, so supplying
-- the full desync_opts bundle makes `repeats=N` on the command line
-- emit N copies per real packet.
rawsend_dissect_ipfrag(dis, desync_opts(desync))
end

View file

@ -354,7 +354,17 @@ AUSTERUS_OPT
# Game Filter UDP (custom protocols — needs payload=all to match unknown UDP) # Game Filter UDP (custom protocols — needs payload=all to match unknown UDP)
# Autocircular rotates strategies with autottl, repeats, cutoff combinations # Autocircular rotates strategies with repeats / cutoff combinations.
#
# Uses z2k_game_udp (defined in files/lua/z2k-modern-core.lua) instead of
# the built-in `fake` primitive because upstream fake() in
# zapret-antidpi.lua does not pass options to rawsend, so `repeats=N` is
# silently dropped for UDP payloads — which broke every variant of the
# nfqws1 Roblox recipe (fake + repeats=10 + blob=unknown_udp). Our custom
# handler threads desync_opts through rawsend_dissect_ipfrag so repeats is
# actually applied, matching classic zapret1 behaviour. The blob alias is
# `quic_google` (registered in install.sh; the on-disk filename is
# quic_initial_www_google_com.bin).
local game_conf="${ZAPRET2_DIR:-/opt/zapret2}/config" local game_conf="${ZAPRET2_DIR:-/opt/zapret2}/config"
local GAME_UDP_BYPASS local GAME_UDP_BYPASS
GAME_UDP_BYPASS=$(safe_config_read "ROBLOX_UDP_BYPASS" "$game_conf" "0") GAME_UDP_BYPASS=$(safe_config_read "ROBLOX_UDP_BYPASS" "$game_conf" "0")
@ -362,20 +372,19 @@ AUSTERUS_OPT
local ipset_excl="${lists_dir}/ipset-exclude.txt" local ipset_excl="${lists_dir}/ipset-exclude.txt"
local game_ipset_opt="" local game_ipset_opt=""
[ -f "$ipset_excl" ] && game_ipset_opt="--ipset-exclude=${ipset_excl} " [ -f "$ipset_excl" ] && game_ipset_opt="--ipset-exclude=${ipset_excl} "
nfqws2_opt_lines="$nfqws2_opt_lines--filter-udp=1024-65535 ${game_ipset_opt}--in-range=a --out-range=a --lua-desync=circular:fails=2:time=30:udp_in=1:udp_out=4:key=game_udp --new\\n" # Single profile: circular selector + 6 z2k_game_udp strategies. nfqws2
# All 6 unique flowseal game filter combinations (all with autottl=2): # picks exactly one profile per flow, so the circular MUST live in the
# Strategy 1: repeats=10, cutoff=n2 (FAKE TLS AUTO / ALT / ALT2 / ALT4 / SIMPLE FAKE ALT) # same profile as the strategies it rotates. Keeping them in separate
nfqws2_opt_lines="$nfqws2_opt_lines--filter-udp=1024-65535 ${game_ipset_opt}--in-range=a --out-range=-n2 --lua-desync=fake:strategy=1:payload=all:dir=out:blob=quic_initial_www_google_com:repeats=10:ip_autottl=2,1-64 --new\\n" # --new profiles (as prior revisions did) meant the circular profile
# Strategy 2: repeats=10, cutoff=n3 (FAKE TLS AUTO ALT3) # matched first and the strategies were never reached.
nfqws2_opt_lines="$nfqws2_opt_lines--filter-udp=1024-65535 ${game_ipset_opt}--in-range=a --out-range=-n3 --lua-desync=fake:strategy=2:payload=all:dir=out:blob=quic_initial_www_google_com:repeats=10:ip_autottl=2,1-64 --new\\n" nfqws2_opt_lines="$nfqws2_opt_lines--filter-udp=1024-65535 ${game_ipset_opt}--in-range=a --out-range=a --payload=all \
# Strategy 3: repeats=10, cutoff=n4 (ALT3 / ALT11) --lua-desync=circular:fails=2:time=30:udp_in=1:udp_out=4:key=game_udp \
nfqws2_opt_lines="$nfqws2_opt_lines--filter-udp=1024-65535 ${game_ipset_opt}--in-range=a --out-range=-n4 --lua-desync=fake:strategy=3:payload=all:dir=out:blob=quic_initial_www_google_com:repeats=10:ip_autottl=2,1-64 --new\\n" --lua-desync=z2k_game_udp:strategy=1:payload=all:dir=out:blob=quic_google:repeats=10:ip_autottl=2,1-64:out_range=-n2 \
# Strategy 4: repeats=12, cutoff=n2 (general / ALT2 / ALT6-10) --lua-desync=z2k_game_udp:strategy=2:payload=all:dir=out:blob=quic_google:repeats=10:ip_autottl=2,1-64:out_range=-n3 \
nfqws2_opt_lines="$nfqws2_opt_lines--filter-udp=1024-65535 ${game_ipset_opt}--in-range=a --out-range=-n2 --lua-desync=fake:strategy=4:payload=all:dir=out:blob=quic_initial_www_google_com:repeats=12:ip_autottl=2,1-64 --new\\n" --lua-desync=z2k_game_udp:strategy=3:payload=all:dir=out:blob=quic_google:repeats=10:ip_autottl=2,1-64:out_range=-n4 \
# Strategy 5: repeats=12, cutoff=n3 (ALT / SIMPLE FAKE / SIMPLE FAKE ALT2) --lua-desync=z2k_game_udp:strategy=4:payload=all:dir=out:blob=quic_google:repeats=12:ip_autottl=2,1-64:out_range=-n2 \
nfqws2_opt_lines="$nfqws2_opt_lines--filter-udp=1024-65535 ${game_ipset_opt}--in-range=a --out-range=-n3 --lua-desync=fake:strategy=5:payload=all:dir=out:blob=quic_initial_www_google_com:repeats=12:ip_autottl=2,1-64 --new\\n" --lua-desync=z2k_game_udp:strategy=5:payload=all:dir=out:blob=quic_google:repeats=12:ip_autottl=2,1-64:out_range=-n3 \
# Strategy 6: repeats=14, cutoff=n3 (ALT5) --lua-desync=z2k_game_udp:strategy=6:payload=all:dir=out:blob=quic_google:repeats=14:ip_autottl=2,1-64:out_range=-n3 --new\\n"
nfqws2_opt_lines="$nfqws2_opt_lines--filter-udp=1024-65535 ${game_ipset_opt}--in-range=a --out-range=-n3 --lua-desync=fake:strategy=6:payload=all:dir=out:blob=quic_initial_www_google_com:repeats=14:ip_autottl=2,1-64 --new\\n"
fi fi
# HTTP RKN (port 80): autocircular bypass of ISP DPI redirect (302 → block page). # HTTP RKN (port 80): autocircular bypass of ISP DPI redirect (302 → block page).