z2k/lib/config_official.sh
Necronicle ca53593f68
Some checks are pending
CI / ShellCheck (push) Waiting to run
CI / Go Build & Test (push) Waiting to run
CI / Luacheck (push) Waiting to run
fix: config variables now expand properly (DROP_DPI_RST, SILENT_FALLBACK, ROBLOX)
These were inside a quoted heredoc (<<'CONFIG') which doesn't expand
variables, resulting in literal ${saved_...} in the config file.
Moved to unquoted heredoc (<<EOF) after the main block.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 19:24:59 +03:00

754 lines
36 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/sh
# lib/config_official.sh - Генерация официального config файла для zapret2
# Адаптировано для z2k с multi-profile стратегиями
# ==============================================================================
# ГЕНЕРАЦИЯ NFQWS2_OPT ИЗ СТРАТЕГИЙ Z2K
# ==============================================================================
generate_nfqws2_opt_from_strategies() {
# Генерирует NFQWS2_OPT для config файла на основе текущих стратегий
# Intentionally hardcoded: this function may be called before utils.sh sets
# the global CONFIG_DIR / ZAPRET2_DIR / LISTS_DIR variables, so we use
# local copies with known absolute paths.
local config_dir="/opt/etc/zapret2"
local extra_strats_dir="/opt/zapret2/extra_strats"
local lists_dir="/opt/zapret2/lists"
# Режим Austerusj: простые стратегии без хостлистов, из Zapret1.
# Если включен — генерируем минимальный конфиг и выходим.
local austerus_conf="${config_dir}/all_tcp443.conf"
if [ -f "$austerus_conf" ]; then
local ENABLED
ENABLED=$(safe_config_read "ENABLED" "$austerus_conf" "0")
if [ "$ENABLED" = "1" ]; then
cat <<'AUSTERUS_OPT'
NFQWS2_OPT="
--filter-tcp=80 --lua-desync=fake:payload=http_req:dir=out:blob=zero_256:badsum:badseq --lua-desync=multisplit:payload=http_req:dir=out --new
--filter-tcp=443 --out-range=-d4 --lua-desync=fake:payload=tls_client_hello:dir=out:blob=zero_256:badsum:badseq --lua-desync=fake:payload=tls_client_hello:dir=out:blob=tls_clienthello_www_google_com:badsum:badseq:repeats=1:tls_mod=sni=www.google.com,rnd,dupsid --lua-desync=multidisorder:payload=tls_client_hello:dir=out:pos=method+2,midsld,5 --new
--filter-udp=443 --out-range=-d4 --lua-desync=fake:payload=quic_initial:dir=out:blob=zero_256:badsum:repeats=1
"
AUSTERUS_OPT
return 0
fi
fi
# Загрузить текущие стратегии из категорий
local youtube_tcp=""
local youtube_gv_tcp=""
local rkn_tcp=""
local quic_udp=""
local discord_udp=""
# Прочитать стратегии из файлов категорий
if [ -f "${extra_strats_dir}/TCP/YT/Strategy.txt" ]; then
youtube_tcp=$(cat "${extra_strats_dir}/TCP/YT/Strategy.txt")
fi
if [ -f "${extra_strats_dir}/TCP/YT_GV/Strategy.txt" ]; then
youtube_gv_tcp=$(cat "${extra_strats_dir}/TCP/YT_GV/Strategy.txt")
fi
if [ -f "${extra_strats_dir}/TCP/RKN/Strategy.txt" ]; then
rkn_tcp=$(cat "${extra_strats_dir}/TCP/RKN/Strategy.txt")
fi
# YouTube QUIC autocircular modern (12 strategies, z2k morph prioritized).
# key=yt_quic ensures stable persistence key; nld=2 reduces churn on CDN subdomains.
quic_udp="--filter-udp=443 --filter-l7=quic --in-range=a --out-range=a --payload=all --lua-desync=circular:fails=3:time=60:udp_in=1:udp_out=4:key=yt_quic:nld=2 --lua-desync=z2k_quic_morph_v2:payload=quic_initial:dir=out:packets=2:noise=2:pad_min=12:pad_max=72:strategy=1 --lua-desync=z2k_quic_morph_v2:payload=quic_initial:dir=out:packets=2:profile=2:noise=2:pad_min=8:pad_max=64:ipfrag_pos_udp=16:ipfrag_pos2=56:ipfrag_overlap12=16:ipfrag_overlap23=8:strategy=2 --lua-desync=z2k_timing_morph:payload=quic_initial:dir=out:packets=2:chance=85:fakes=2:pad_min=12:pad_max=72:strategy=3 --lua-desync=fake:payload=quic_initial:dir=out:blob=quic5:repeats=3:ip_autottl=-2,3-20:strategy=3 --lua-desync=send:payload=quic_initial:dir=out:ipfrag=z2k_ipfrag3_tiny:ipfrag_pos_udp=8:ipfrag_pos2=32:ipfrag_overlap12=8:ipfrag_overlap23=8:ipfrag_disorder:ipfrag_next2=255:strategy=3 --lua-desync=drop:strategy=3 --lua-desync=fake:payload=quic_initial:dir=out:blob=quic5:repeats=4:ip_autottl=-2,3-20:strategy=4 --lua-desync=send:payload=quic_initial:dir=out:ipfrag=z2k_ipfrag3_tiny:ipfrag_pos_udp=8:ipfrag_pos2=32:ipfrag_overlap12=8:ipfrag_overlap23=8:ipfrag_disorder:ipfrag_next2=255:strategy=4 --lua-desync=drop:strategy=4 --lua-desync=fake:payload=quic_initial:dir=out:blob=quic_rutracker:repeats=6:strategy=5 --lua-desync=send:payload=quic_initial:dir=out:ipfrag=z2k_ipfrag3:ipfrag_pos_udp=16:ipfrag_pos2=48:ipfrag_overlap12=8:ipfrag_overlap23=8:ipfrag_disorder:ipfrag_next2=255:strategy=5 --lua-desync=drop:strategy=5 --lua-desync=fake:payload=quic_initial:dir=out:blob=fake_default_quic:repeats=6:ip_autottl=-2,3-20:strategy=6 --lua-desync=fake:payload=quic_initial:dir=out:blob=quic5:repeats=6:payload=all:ip_autottl=-2,3-20:strategy=7 --lua-desync=send:payload=quic_initial:dir=out:ipfrag:ipfrag_pos_udp=16:strategy=7 --lua-desync=drop:strategy=7 --lua-desync=udplen:payload=quic_initial:dir=out:increment=4:strategy=8 --lua-desync=fake:payload=quic_initial:dir=out:blob=quic5:repeats=2:strategy=8 --lua-desync=udplen:payload=quic_initial:dir=out:increment=8:pattern=0xFEA82025:strategy=9 --lua-desync=fake:payload=quic_initial:dir=out:blob=quic5:repeats=2:strategy=9 --lua-desync=fake:payload=quic_initial:dir=out:blob=0x00000000000000000000000000000000:repeats=2:payload=all:strategy=10 --lua-desync=send:payload=quic_initial:dir=out:ipfrag:ipfrag_pos_udp=8:strategy=10 --lua-desync=drop:strategy=10 --lua-desync=fake:payload=quic_initial:dir=out:blob=fake_default_quic:repeats=11:ip_autottl=-2,3-20:strategy=11 --lua-desync=send:payload=quic_initial:dir=out:ipfrag:ipfrag_pos_udp=24:strategy=11 --lua-desync=drop:strategy=11 --lua-desync=fake:payload=quic_initial:dir=out:blob=fake_default_quic:repeats=3:strategy=12"
# If category strategy files exist, prefer them over hardcoded QUIC defaults.
if [ -f "${extra_strats_dir}/UDP/YT/Strategy.txt" ]; then
quic_udp=$(cat "${extra_strats_dir}/UDP/YT/Strategy.txt")
fi
# Discord TCP profiles from zapret4rocket are absent; disable dedicated TCP Discord profile.
local discord_tcp_block=""
# Discord UDP (zapret4rocket-based + z2k autocircular on same primitive family).
discord_udp="--filter-udp=50000-50099,1400,3478-3481,5349,19294-19344 --filter-l7=discord,stun --in-range=-d100 --out-range=-d100 --payload=quic_initial,discord_ip_discovery --lua-desync=circular_locked:key=6:allow_nohost=1 --lua-desync=fake:payload=all:blob=quic_google:repeats=6:strategy=1 --lua-desync=fake:payload=all:blob=quic_google:repeats=4:strategy=2 --lua-desync=fake:payload=all:blob=quic_google:repeats=8:strategy=3 --lua-desync=fake:payload=all:blob=quic_google:repeats=6:ip_autottl=-2,3-20:strategy=4 --lua-desync=fake:payload=all:blob=fake_default_quic:repeats=6:strategy=5 --lua-desync=fake:payload=all:blob=quic5:repeats=6:strategy=6"
# Дефолтная стратегия если не загружена
local default_strategy="--filter-tcp=443,2053,2083,2087,2096,8443 --filter-l7=tls --payload=tls_client_hello,http_req,http_reply,unknown,tls_server_hello --out-range=-s34228 --lua-desync=fake:blob=fake_default_tls:repeats=6"
# Использовать дефолт если стратегия пустая
[ -z "$youtube_tcp" ] && youtube_tcp="$default_strategy"
[ -z "$youtube_gv_tcp" ] && youtube_gv_tcp="$default_strategy"
[ -z "$rkn_tcp" ] && rkn_tcp="$default_strategy"
# Force domain-level memory for all autocircular profiles.
# This prevents churn on frequently changing subdomains.
ensure_circular_nld2() {
local input="$1"
local out=""
local token=""
local opts=""
local part=""
local rest=""
local old_ifs="$IFS"
for token in $input; do
case "$token" in
--lua-desync=circular:*)
opts="${token#--lua-desync=circular:}"
rest=""
IFS=':'
for part in $opts; do
case "$part" in
nld=*) ;;
*) rest="${rest:+$rest:}$part" ;;
esac
done
IFS="$old_ifs"
if [ -n "$rest" ]; then
token="--lua-desync=circular:${rest}:nld=2"
else
token="--lua-desync=circular:nld=2"
fi
;;
esac
out="${out:+$out }$token"
done
IFS="$old_ifs"
printf '%s' "$out"
}
youtube_tcp=$(ensure_circular_nld2 "$youtube_tcp")
youtube_gv_tcp=$(ensure_circular_nld2 "$youtube_gv_tcp")
rkn_tcp=$(ensure_circular_nld2 "$rkn_tcp")
quic_udp=$(ensure_circular_nld2 "$quic_udp")
# Let YouTube TLS circular operate exactly as in the upstream manual.
# For LG webOS the orchestrator must see incoming packets on the circular
# stage itself (`--in-range=-s5556`), while actual desync instances must
# still stay limited by `--payload=tls_client_hello...`.
#
# This requires moving the top-level `--payload=` after circular and
# closing the incoming window right after circular with `--in-range=x`.
# Keeping payload before circular makes YouTube TCP/GV fail detection too
# blind and prevents real sequential rotation on TV clients.
ensure_youtube_tls_circular_manual_layout() {
local input="$1"
local token=""
local has_tls="0"
local has_circular="0"
local has_in_range=""
local before_circular=""
local circular_token=""
local after_circular=""
local saved_payload=""
local phase="before"
for token in $input; do
case "$token" in
--filter-l7=tls) has_tls="1" ;;
--lua-desync=circular:*) has_circular="1" ;;
--in-range=*) has_in_range="1" ;;
esac
done
if [ "$has_tls" != "1" ] || [ "$has_circular" != "1" ]; then
printf '%s' "$input"
return 0
fi
# If a profile already has an explicit in-range, leave it alone.
[ -n "$has_in_range" ] && {
printf '%s' "$input"
return 0
}
for token in $input; do
case "$phase" in
before)
case "$token" in
--payload=*)
saved_payload="$token"
;;
--lua-desync=circular:*)
circular_token="$token"
phase="after"
;;
*)
before_circular="${before_circular:+$before_circular }$token"
;;
esac
;;
after)
after_circular="${after_circular:+$after_circular }$token"
;;
esac
done
[ -z "$circular_token" ] && {
printf '%s' "$input"
return 0
}
# Fallback for malformed legacy inputs with no payload token.
[ -z "$saved_payload" ] && saved_payload="--payload=tls_client_hello"
printf '%s --in-range=-s5556 %s --in-range=x %s%s%s' \
"$before_circular" \
"$circular_token" \
"$saved_payload" \
"${after_circular:+ }" \
"$after_circular"
}
# YouTube TCP on LG webOS often fails as a silent TCP blackhole:
# repeated ClientHello attempts with no visible response_state/success_state.
# Manual payload reordering alone is not sufficient in that mode.
#
# Restore the older YouTube-only conservative TCP failure path:
# - expose incoming packets to circular via --in-range=-s5556
# - expose empty packets / retrans context via --payload=tls_client_hello,empty
# - keep desync strategy instances restricted to the original TLS payload
# - prevent successes from other devices on the same domain from resetting
# failure counters via success_detector=z2k_success_no_reset
#
# Scope is intentionally limited to youtube_tcp.
ensure_youtube_tls_failure_detection() {
local input="$1"
local token=""
local has_tls="0"
local has_circular="0"
local has_in_range=""
local saved_payload=""
local out=""
local circular_seen="0"
for token in $input; do
case "$token" in
--filter-l7=tls) has_tls="1" ;;
--lua-desync=circular:*) has_circular="1" ;;
--in-range=*) has_in_range="1" ;;
esac
done
if [ "$has_tls" != "1" ] || [ "$has_circular" != "1" ]; then
printf '%s' "$input"
return 0
fi
# If a profile already has an explicit in-range, leave it alone.
[ -n "$has_in_range" ] && {
printf '%s' "$input"
return 0
}
[ -z "$saved_payload" ] && saved_payload="--payload=tls_client_hello"
for token in $input; do
case "$token" in
--payload=*)
saved_payload="$token"
token=$(printf '%s' "$token" | sed 's/^--payload=tls_client_hello$/--payload=tls_client_hello,empty/')
;;
--lua-desync=circular:*)
out="${out:+$out }--in-range=-s5556"
case "$token" in
*:success_detector=*) ;;
*) token="${token}:success_detector=z2k_success_no_reset" ;;
esac
circular_seen="1"
;;
esac
if [ "$circular_seen" = "1" ]; then
case "$token" in
--lua-desync=circular:*) ;;
--lua-desync=*)
out="${out:+$out }--in-range=x $saved_payload"
circular_seen="2"
;;
esac
fi
out="${out:+$out }$token"
done
printf '%s' "$out"
}
youtube_tcp=$(ensure_youtube_tls_failure_detection "$youtube_tcp")
youtube_gv_tcp=$(ensure_youtube_tls_circular_manual_layout "$youtube_gv_tcp")
# RKN: всегда добавляем failure_detector=z2k_tls_alert_fatal.
# Без него circular слеп к ТСПУ-блокировкам (TLS alert от DPI не детектится,
# ротация не происходит). Убирали из-за false positives на YouTube,
# но для РКН это единственный надёжный детектор.
ensure_rkn_failure_detector() {
local input="$1"
local out=""
local token=""
for token in $input; do
case "$token" in
--lua-desync=circular:*)
case "$token" in
*failure_detector=*) ;;
*) token="${token}:failure_detector=z2k_tls_alert_fatal" ;;
esac
;;
esac
out="${out:+$out }$token"
done
printf '%s' "$out"
}
rkn_tcp=$(ensure_rkn_failure_detector "$rkn_tcp")
# Silent fallback для RKN — включается через меню (флаг-файл).
# failure_detection включает в себя manual_layout (--in-range + payload),
# поэтому они взаимоисключающие, не накладываются.
local rkn_silent_conf="${ZAPRET2_DIR:-/opt/zapret2}/config"
local RKN_SILENT_FALLBACK
RKN_SILENT_FALLBACK=$(safe_config_read "RKN_SILENT_FALLBACK" "$rkn_silent_conf" "0")
local rkn_silent_flag="${extra_strats_dir}/cache/autocircular/rkn_silent_fallback.flag"
if [ "$RKN_SILENT_FALLBACK" = "1" ]; then
rkn_tcp=$(ensure_youtube_tls_failure_detection "$rkn_tcp")
touch "$rkn_silent_flag" 2>/dev/null
else
rkn_tcp=$(ensure_youtube_tls_circular_manual_layout "$rkn_tcp")
rm -f "$rkn_silent_flag" 2>/dev/null
fi
# Генерировать NFQWS2_OPT в формате официального config
local nfqws2_opt_lines=""
# Helper: проверить наличие и непустоту hostlist-файлов
add_hostlist_line() {
local list_path="$1"
shift
if [ -s "$list_path" ]; then
nfqws2_opt_lines="$nfqws2_opt_lines$*\\n"
else
echo "WARN: hostlist file missing or empty: $list_path (skip profile)" 1>&2
print_warning "Hostlist missing or empty: $list_path — profile skipped"
fi
}
# RKN TCP (include Discord hostlist into RKN profile)
local rkn_hostlists="--hostlist=${extra_strats_dir}/TCP/RKN/List.txt"
[ -s "${extra_strats_dir}/TCP_Discord.txt" ] && rkn_hostlists="$rkn_hostlists --hostlist=${extra_strats_dir}/TCP_Discord.txt"
add_hostlist_line "${extra_strats_dir}/TCP/RKN/List.txt" "--hostlist-exclude=${lists_dir}/whitelist.txt $rkn_hostlists $rkn_tcp --new"
# YouTube TCP
add_hostlist_line "${extra_strats_dir}/TCP/YT/List.txt" "--hostlist-exclude=${lists_dir}/whitelist.txt --hostlist=${extra_strats_dir}/TCP/YT/List.txt $youtube_tcp --new"
# YouTube GV (список доменов статичен)
nfqws2_opt_lines="$nfqws2_opt_lines--hostlist-exclude=${lists_dir}/whitelist.txt --hostlist-domains=googlevideo.com $youtube_gv_tcp --new\\n"
# QUIC YT
add_hostlist_line "${extra_strats_dir}/UDP/YT/List.txt" "--hostlist-exclude=${lists_dir}/whitelist.txt --hostlist=${extra_strats_dir}/UDP/YT/List.txt $quic_udp --new"
# Discord TCP: currently disabled for autocircular profile set.
if [ -n "$discord_tcp_block" ]; then
add_hostlist_line "${extra_strats_dir}/TCP_Discord.txt" "$discord_tcp_block"
fi
# Discord UDP (no hostlist - STUN has no hostname, uses filter-l7=discord,stun + allow_nohost)
nfqws2_opt_lines="$nfqws2_opt_lines$discord_udp --new\\n"
# Game Filter UDP (custom protocols — needs payload=all to match unknown UDP)
# Autocircular rotates strategies with autottl, repeats, cutoff combinations
local game_conf="${ZAPRET2_DIR:-/opt/zapret2}/config"
local GAME_UDP_BYPASS
GAME_UDP_BYPASS=$(safe_config_read "ROBLOX_UDP_BYPASS" "$game_conf" "0")
if [ "$GAME_UDP_BYPASS" = "1" ]; then
local ipset_excl="${lists_dir}/ipset-exclude.txt"
local game_ipset_opt=""
[ -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"
# All 6 unique flowseal game filter combinations (all with autottl=2):
# Strategy 1: repeats=10, cutoff=n2 (FAKE TLS AUTO / ALT / ALT2 / ALT4 / SIMPLE FAKE ALT)
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"
# Strategy 2: repeats=10, cutoff=n3 (FAKE TLS AUTO ALT3)
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"
# Strategy 3: repeats=10, cutoff=n4 (ALT3 / ALT11)
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"
# Strategy 4: repeats=12, cutoff=n2 (general / ALT2 / ALT6-10)
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"
# Strategy 5: repeats=12, cutoff=n3 (ALT / SIMPLE FAKE / SIMPLE FAKE ALT2)
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"
# Strategy 6: repeats=14, cutoff=n3 (ALT5)
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
# HTTP RKN (port 80): autocircular bypass of ISP DPI redirect (302 → block page).
# 7 strategies from blockcheck2 results, ordered by simplicity.
# standard_failure_detector detects HTTP 302 redirects natively.
# --in-range=-s5556: let circular see HTTP response for failure detection.
# Strategy 1: http_methodeol (simplest HTTP manipulation)
# Strategy 2: syndata + multisplit
# Strategy 3: hostfakesplit with TTL=2
# Strategy 4: fake with badsum
# Strategy 5: fakedsplit at method+2 with badsum
# Strategy 6: z4r original (fake 0x0E + tcp_md5 + multisplit host+1)
# Strategy 7: fake badsum + multisplit method+2
add_hostlist_line "${extra_strats_dir}/TCP/RKN/List.txt" "--filter-tcp=80 --hostlist-exclude=${lists_dir}/whitelist.txt --hostlist=${extra_strats_dir}/TCP/RKN/List.txt --in-range=-s5556 --payload=http_req,empty --lua-desync=circular:fails=2:time=60:reset:key=http_rkn:nld=2:failure_detector=z2k_tls_alert_fatal --lua-desync=http_methodeol:payload=http_req:dir=out:strategy=1 --lua-desync=syndata:payload=http_req:dir=out:strategy=2 --lua-desync=multisplit:payload=http_req:dir=out:strategy=2 --lua-desync=hostfakesplit:payload=http_req:dir=out:ip_ttl=2:repeats=1:strategy=3 --lua-desync=fake:payload=http_req:dir=out:blob=fake_default_http:badsum:repeats=1:strategy=4 --lua-desync=fakedsplit:payload=http_req:dir=out:pos=method+2:badsum:strategy=5 --lua-desync=fake:payload=http_req:dir=out:blob=0x0E0E0F0E:tcp_md5:strategy=6 --lua-desync=multisplit:payload=http_req:dir=out:pos=host+1:seqovl=2:strategy=6 --lua-desync=fake:payload=http_req:dir=out:blob=fake_default_http:badsum:repeats=1:strategy=7 --lua-desync=multisplit:payload=http_req:dir=out:pos=method+2:strategy=7 --in-range=x --new"
local nfqws2_opt_value
nfqws2_opt_value=$(printf "%b" "$nfqws2_opt_lines" | sed '/^$/d')
cat <<NFQWS2_OPT
NFQWS2_OPT="
$nfqws2_opt_value
"
NFQWS2_OPT
}
# ==============================================================================
# СОЗДАНИЕ ОФИЦИАЛЬНОГО CONFIG ФАЙЛА
# ==============================================================================
create_official_config() {
# $1 - путь к config файлу (обычно /opt/zapret2/config)
local config_file="${1:-/opt/zapret2/config}"
print_info "Создание официального config файла: $config_file"
# Создать директорию если не существует
mkdir -p "$(dirname "$config_file")"
# Генерировать NFQWS2_OPT
local nfqws2_opt_section
nfqws2_opt_section=$(generate_nfqws2_opt_from_strategies)
# =========================================================================
# ВАЛИДАЦИЯ NFQWS2 ОПЦИЙ (ВАЖНО)
# =========================================================================
print_info "Валидация сгенерированных опций nfqws2..."
# Извлечь NFQWS2_OPT из сгенерированной секции (многострочный heredoc между кавычками)
local nfqws2_opt_value
nfqws2_opt_value=$(echo "$nfqws2_opt_section" | sed -n '/^NFQWS2_OPT="/,/^"$/{ /^NFQWS2_OPT="/d; /^"$/d; p; }')
# Загрузить модули для dry_run_nfqws()
if [ -f "/opt/zapret2/common/base.sh" ]; then
. "/opt/zapret2/common/base.sh"
fi
if [ -f "/opt/zapret2/common/linux_daemons.sh" ]; then
. "/opt/zapret2/common/linux_daemons.sh"
# Установить временно NFQWS2_OPT для проверки
export NFQWS2_OPT="$nfqws2_opt_value"
export NFQWS2="/opt/zapret2/nfq2/nfqws2"
# Проверить опции (dry_run может ложно падать если lua/blob файлы
# ещё не установлены — это нормально при первой установке)
if command -v dry_run_nfqws >/dev/null 2>&1; then
if dry_run_nfqws 2>/dev/null; then
print_success "Опции nfqws2 валидны (dry-run OK)"
else
print_info "dry-run nfqws2 не прошёл (нормально при установке — lua/blob файлы подключатся при запуске)"
fi
fi
else
print_info "Модули валидации не найдены, пропускаем проверку"
fi
z2k_have_cmd() { command -v "$1" >/dev/null 2>&1; }
# Получить FWTYPE и FLOWOFFLOAD из окружения (если установлены)
local fwtype_value="${FWTYPE:-iptables}"
local flowoffload_value="${FLOWOFFLOAD:-none}"
local tmpdir_value="${TMPDIR:-}"
# ==============================================================================
# IPv6 auto-detect (Keenetic)
# ==============================================================================
# Default behavior historically was DISABLE_IPV6=1 because many Keenetic builds
# don't ship ip6tables. Here we enable IPv6 only if:
# - IPv6 looks configured (default route or global address exists)
# - and the firewall backend can actually handle IPv6 rules:
# - iptables => ip6tables must exist
# - nftables => nft must exist
local disable_ipv6_value="${DISABLE_IPV6:-}"
if [ -z "$disable_ipv6_value" ]; then
disable_ipv6_value="1"
local v6_ok="0"
if z2k_have_cmd ip; then
ip -6 route show default 2>/dev/null | grep -q . && v6_ok="1"
if [ "$v6_ok" = "0" ]; then
ip -6 addr show scope global 2>/dev/null | grep -q "inet6" && v6_ok="1"
fi
fi
if [ "$v6_ok" = "1" ]; then
if [ "$fwtype_value" = "nftables" ]; then
if z2k_have_cmd nft; then
disable_ipv6_value="0"
print_info "IPv6 обнаружен, backend=nftables: включаем обработку IPv6 (DISABLE_IPV6=0)"
else
print_info "IPv6 обнаружен, но nft не найден: оставляем IPv6 отключенным (DISABLE_IPV6=1)"
fi
else
if z2k_have_cmd ip6tables; then
disable_ipv6_value="0"
print_info "IPv6 обнаружен, backend=iptables: включаем обработку IPv6 (DISABLE_IPV6=0)"
else
print_info "IPv6 обнаружен, но ip6tables не найден: оставляем IPv6 отключенным (DISABLE_IPV6=1)"
fi
fi
else
print_info "IPv6 не обнаружен (нет default route/global addr): оставляем IPv6 отключенным (DISABLE_IPV6=1)"
fi
else
print_info "DISABLE_IPV6 задан вручную: DISABLE_IPV6=$disable_ipv6_value"
fi
# Сохранить пользовательские настройки из существующего конфига
local saved_DROP_DPI_RST="0"
local saved_RKN_SILENT_FALLBACK="0"
local saved_ROBLOX_UDP_BYPASS="0"
if [ -f "$config_file" ]; then
saved_DROP_DPI_RST=$(safe_config_read "DROP_DPI_RST" "$config_file" "0")
saved_RKN_SILENT_FALLBACK=$(safe_config_read "RKN_SILENT_FALLBACK" "$config_file" "0")
saved_ROBLOX_UDP_BYPASS=$(safe_config_read "ROBLOX_UDP_BYPASS" "$config_file" "0")
fi
# Создать полный config файл
cat > "$config_file" <<CONFIG
# zapret2 configuration for Keenetic
# Generated by z2k installer
# Based on official zapret2 config structure
# ==============================================================================
# BASIC SETTINGS
# ==============================================================================
# Enable zapret2 service
ENABLED=1
# Mode filter: none, ipset, hostlist, autohostlist
# z2k uses hostlist mode — domains are controlled via explicit hostlist files
MODE_FILTER=hostlist
# Firewall type - AUTO-DETECTED by init script, DO NOT set manually
# Init script calls linux_fwtype() which detects iptables/nftables automatically
# If FWTYPE is set here, linux_fwtype() will skip detection!
#FWTYPE=iptables
# ==============================================================================
# NFQWS2 DAEMON SETTINGS
# ==============================================================================
# Enable nfqws2
NFQWS2_ENABLE=1
# TCP ports to process (will be filtered by --filter-tcp in NFQWS2_OPT)
NFQWS2_PORTS_TCP="80,443,2053,2083,2087,2096,8443"
# UDP ports to process (will be filtered by --filter-udp in NFQWS2_OPT)
NFQWS2_PORTS_UDP="443,50000-50099,1400,3478-3481,5349,19294-19344${saved_ROBLOX_UDP_BYPASS:+$([ "$saved_ROBLOX_UDP_BYPASS" = "1" ] && echo ',1024-65535')}"
# Packet direction filters (connbytes)
# NOTE: These are packet counts, NOT ranges
# PKT_OUT=20 means "first 20 packets" (connbytes 1:20)
# Official zapret2 defaults: TCP_PKT_OUT=20, UDP_PKT_OUT=5
NFQWS2_TCP_PKT_OUT="20"
NFQWS2_TCP_PKT_IN="10"
NFQWS2_UDP_PKT_OUT="5"
NFQWS2_UDP_PKT_IN="3"
# ==============================================================================
# NFQWS2 OPTIONS (MULTI-PROFILE MODE)
# ==============================================================================
# This section is auto-generated from z2k strategy database
# Each --new separator creates independent profile with own filters and strategy
# Order: RKN TCP → YouTube TCP → YouTube GV → QUIC YT → Discord UDP → HTTP RKN → Catch-all TCP
# Profiles use explicit hostlists from z2k list files without placeholder expansion.
# This avoids mixing with global hostlists from MODE_FILTER.
CONFIG
# Добавить сгенерированный NFQWS2_OPT
echo "$nfqws2_opt_section" >> "$config_file"
# Добавить остальные настройки
cat >> "$config_file" <<'CONFIG'
# ==============================================================================
# FIREWALL SETTINGS
# ==============================================================================
# Queue number for NFQUEUE
QNUM=200
# Firewall mark for desync prevention
DESYNC_MARK=0x40000000
DESYNC_MARK_POSTNAT=0x20000000
# Apply firewall rules in init script
INIT_APPLY_FW=1
# Flow offloading mode: none, software, hardware, donttouch
# Set during installation based on system detection
FLOWOFFLOAD=$flowoffload_value
# WAN interface override (space/comma separated). Empty = auto-detect
#WAN_IFACE=
# ==============================================================================
# SYSTEM SETTINGS
# ==============================================================================
# Temporary directory for downloads and processing
# Empty = use system default /tmp (tmpfs, in RAM)
# Set to disk path for low RAM systems (e.g., /opt/zapret2/tmp)
CONFIG
# Добавить TMPDIR только если установлен
if [ -n "$tmpdir_value" ]; then
echo "TMPDIR=$tmpdir_value" >> "$config_file"
else
echo "#TMPDIR=/opt/zapret2/tmp" >> "$config_file"
fi
# Disable IPv6 processing (0=enabled, 1=disabled)
# Auto-detected during install; can be overridden by setting DISABLE_IPV6 in environment/config.
echo "" >> "$config_file"
echo "# Disable IPv6 processing (0=enabled, 1=disabled)" >> "$config_file"
echo "DISABLE_IPV6=$disable_ipv6_value" >> "$config_file"
cat >> "$config_file" <<'CONFIG'
# ==============================================================================
# IPSET SETTINGS
# ==============================================================================
# Maximum elements in ipsets
SET_MAXELEM=522288
# ipset options
IPSET_OPT="hashsize 262144 maxelem $SET_MAXELEM"
# ip2net options
IP2NET_OPT4="--prefix-length=22-30 --v4-threshold=3/4"
IP2NET_OPT6="--prefix-length=56-64 --v6-threshold=5"
# AUTOHOSTLIST SETTINGS отключены — используется режим hostlist с явными списками доменов
# ==============================================================================
# CUSTOM SCRIPTS
# ==============================================================================
# Directory for custom scripts
CUSTOM_DIR="/opt/zapret2/init.d/keenetic"
# Disable custom.d scripts (50-stun4all, 50-discord-media).
# Discord voice/video is handled by nfqws2 strategies (profile 6), no extra daemons needed.
DISABLE_CUSTOM=1
# ==============================================================================
# MISCELLANEOUS
# ==============================================================================
# Temporary directory (if /tmp is too small)
#TMPDIR=/opt/zapret2/tmp
# User for zapret daemons (security hardening: drop privileges to nobody)
WS_USER=nobody
# Compress large lists
GZIP_LISTS=1
# Number of parallel threads for domain resolves
MDIG_THREADS=30
# EAI_AGAIN retries
MDIG_EAGAIN=10
MDIG_EAGAIN_DELAY=500
CONFIG
# Append settings that need variable expansion (heredoc with quotes doesn't expand)
cat >> "$config_file" <<EOF
# Passive DPI RST filter: drop injected TCP RST with IP ID 0x0-0xF
DROP_DPI_RST=${saved_DROP_DPI_RST}
# Silent fallback for RKN
RKN_SILENT_FALLBACK=${saved_RKN_SILENT_FALLBACK}
# Game filter
ROBLOX_UDP_BYPASS=${saved_ROBLOX_UDP_BYPASS}
EOF
print_success "Config файл создан: $config_file"
return 0
}
# ==============================================================================
# ОБНОВЛЕНИЕ NFQWS2_OPT В СУЩЕСТВУЮЩЕМ CONFIG
# ==============================================================================
update_nfqws2_opt_in_config() {
# Обновляет только секцию NFQWS2_OPT в существующем config файле
# $1 - путь к config файлу
local config_file="${1:-/opt/zapret2/config}"
if [ ! -f "$config_file" ]; then
print_error "Config файл не найден: $config_file"
return 1
fi
print_info "Обновление NFQWS2_OPT в: $config_file"
# Создать backup
cp "$config_file" "${config_file}.backup.$(date +%Y%m%d_%H%M%S)"
# Генерировать новый NFQWS2_OPT
local nfqws2_opt_section
nfqws2_opt_section=$(generate_nfqws2_opt_from_strategies)
# Создать временный файл
local temp_file="${config_file}.tmp"
# Удалить старый NFQWS2_OPT и добавить новый
awk '
/^NFQWS2_OPT=/ {
in_nfqws_opt=1
next
}
in_nfqws_opt && /^"$/ {
in_nfqws_opt=0
next
}
!in_nfqws_opt { print }
' "$config_file" > "$temp_file"
# Добавить новый NFQWS2_OPT в конец файла (перед последней секцией)
# Найти позицию для вставки (перед FIREWALL SETTINGS или в конец)
if grep -q "# FIREWALL SETTINGS" "$temp_file"; then
# Вставить перед FIREWALL SETTINGS
awk -v opt="$nfqws2_opt_section" '
/# FIREWALL SETTINGS/ {
print opt
print ""
}
{ print }
' "$temp_file" > "${temp_file}.2"
mv "${temp_file}.2" "$temp_file"
else
# Добавить в конец
echo "" >> "$temp_file"
echo "$nfqws2_opt_section" >> "$temp_file"
fi
# Заменить оригинальный файл
mv "$temp_file" "$config_file"
print_success "NFQWS2_OPT обновлён в config файле"
return 0
}
# ==============================================================================
# ЭКСПОРТ ФУНКЦИЙ
# ==============================================================================
# Функции доступны после source этого файла