Files
Zapret-Manager/ByeTube.sh
T
2026-09-20 20:11:12 +05:00

3063 lines
101 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
# ByeTube for LuCI installer
# Version: 1.0
BYEDPI_REPO="DPITrickster/ByeDPI-OpenWrt"
R="${BT_ROOT:-}"
BT_DIR="/opt/ByeTube"
BT_TMP_DIR="/tmp/ByeTube"
NEW_BYEDPI=0
NEW_HEV=0
LEGACY=0
MODE=install
NOSTART=0
say() { printf '\033[1;32m==>\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m!! \033[0m%s\n' "$*" >&2; }
die() { printf '\033[1;31mОшибка:\033[0m %s\n' "$*" >&2; exit 1; }
usage() {
cat <<'USAGE'
ByeTube — установщик для OpenWrt
sh install.sh установить и запустить
sh install.sh --no-start установить, но не запускать
sh install.sh --uninstall удалить (пакеты byedpi и hev остаются)
sh install.sh --purge удалить вместе с пакетами byedpi и hev-socks5-tunnel
BYEDPI_URL=<ссылка на .ipk/.apk> поставить byedpi с конкретной ссылки
FORCE_BYEDPI=1 переустановить byedpi
USAGE
}
parse_args() {
local a
for a in "$@"; do
case "$a" in
--no-start) NOSTART=1 ;;
--uninstall) MODE=uninstall ;;
--purge) MODE=purge ;;
-h|--help) usage; exit 0 ;;
*) die "неизвестный аргумент: $a" ;;
esac
done
}
pkg_has() {
if [ "$PM" = apk ]; then apk info -e "$1" >/dev/null 2>&1
else opkg list-installed 2>/dev/null | grep -q "^$1 - "; fi
}
pkg_add() { if [ "$PM" = apk ]; then apk add "$@"; else opkg install "$@"; fi; }
pkg_del() { if [ "$PM" = apk ]; then apk del "$@"; else opkg remove "$@"; fi; }
fetch() {
if command -v curl >/dev/null 2>&1; then
curl -fsSL --connect-timeout 15 -o "$2" "$1"
else
wget -q -T 20 -O "$2" "$1"
fi
}
run_uninstall() {
sh -s -- "$@" <<'BT_FILE_END_7f3a9c'
#!/bin/sh
R="${BT_ROOT:-}"
MODE="$1"
CURRENT_PATHS="/opt/ByeTube /tmp/ByeTube /etc/init.d/byetube /etc/config/byetube /etc/hotplug.d/firewall/90-byetube /usr/share/nftables.d/chain-pre/forward/50-byetube.nft /usr/share/luci/menu.d/luci-app-byetube.json /usr/share/rpcd/acl.d/luci-app-byetube.json /www/luci-static/resources/view/byetube /www/luci-static/resources/byetube /lib/upgrade/keep.d/byetube"
LEGACY_PATHS="/etc/init.d/ytbypass /etc/config/ytbypass /etc/hotplug.d/firewall/90-ytbypass /usr/bin/ytbypass /usr/libexec/ytbypass /usr/share/ytbypass /usr/share/nftables.d/chain-pre/forward/50-ytbypass.nft /usr/share/luci/menu.d/luci-app-ytbypass.json /usr/share/rpcd/acl.d/luci-app-ytbypass.json /www/luci-static/resources/view/ytbypass /www/luci-static/resources/ytbypass /lib/upgrade/keep.d/ytbypass /etc/ytbypass /var/etc/ytbypass /var/run/ytbypass.started /tmp/ytbypass-test"
live() { [ -z "$R" ]; }
confdir() {
local d
d=$(sed -n 's/^conf-dir=\([^,]*\).*/\1/p' "$R"/var/etc/dnsmasq.conf.* 2>/dev/null | head -n 1)
[ -n "$d" ] || d=/tmp/dnsmasq.d
echo "$d"
}
drop_ip_rules() {
live || return 0
while ip rule del pref 8900 2>/dev/null; do :; done
while ip -6 rule del pref 8900 2>/dev/null; do :; done
}
drop_dev() {
live || return 0
ip route del default dev "$1" table 89 2>/dev/null
ip -6 route del default dev "$1" table 89 2>/dev/null
nft delete table inet "$2" 2>/dev/null
}
stop_service() {
live || return 0
[ -x "/etc/init.d/$1" ] || return 0
"/etc/init.d/$1" stop >/dev/null 2>&1
"/etc/init.d/$1" disable >/dev/null 2>&1
}
remove_dnsmasq_conf() {
local d f changed=0
d="$R$(confdir)"
for f in "$@"; do
if [ -f "$d/$f" ]; then
rm -f "$d/$f"
changed=1
fi
done
[ "$changed" = "1" ]
}
remove_paths() {
local p
for p in "$@"; do
rm -rf "$R$p"
done
}
dns_changed=0
if [ "$MODE" != "--legacy" ]; then
stop_service byetube
drop_ip_rules
drop_dev byetube0 byetube
remove_dnsmasq_conf byetube.conf && dns_changed=1
fi
stop_service ytbypass
if [ "$MODE" != "--legacy" ] || [ ! -f "$R/tmp/ByeTube/started" ]; then
drop_ip_rules
fi
drop_dev ytb0 ytbypass
remove_dnsmasq_conf ytbypass.conf && dns_changed=1
if [ "$MODE" = "--legacy" ]; then
remove_paths $LEGACY_PATHS
else
remove_paths $CURRENT_PATHS $LEGACY_PATHS
fi
if live; then
[ "$dns_changed" = "1" ] && /etc/init.d/dnsmasq restart >/dev/null 2>&1
/etc/init.d/firewall reload >/dev/null 2>&1
rm -rf /tmp/luci-indexcache* /tmp/luci-modulecache
/etc/init.d/rpcd reload >/dev/null 2>&1
fi
exit 0
BT_FILE_END_7f3a9c
}
do_uninstall() {
say "Останавливаю и удаляю ByeTube"
[ -x "$R$BT_DIR/bin/byetube" ] && "$R$BT_DIR/bin/byetube" test stop >/dev/null 2>&1
run_uninstall
if [ "$MODE" = purge ]; then
say "Удаляю пакеты byedpi и hev-socks5-tunnel"
pkg_del byedpi hev-socks5-tunnel >/dev/null 2>&1
fi
say "Готово."
exit 0
}
fix_resolv() {
rm -f /tmp/resolv.conf
if grep -qs '^nameserver' /tmp/resolv.conf.d/resolv.conf.auto; then
cp /tmp/resolv.conf.d/resolv.conf.auto /tmp/resolv.conf
else
printf 'nameserver 1.1.1.1\nnameserver 8.8.8.8\n' > /tmp/resolv.conf
fi
}
ensure_dnsmasq_full() {
local p
if dnsmasq --version 2>/dev/null | grep -Eq '(^| )nftset( |$)'; then
say "dnsmasq с поддержкой nftset уже установлен"
return 0
fi
say "Заменяю dnsmasq на dnsmasq-full (нужен nftset)"
mkdir -p "$BT_TMP_DIR"
cp /etc/config/dhcp "$BT_TMP_DIR/dhcp.bak" 2>/dev/null
for p in dnsmasq dnsmasq-dhcpv6; do
pkg_has "$p" && pkg_del "$p" >/dev/null 2>&1
done
fix_resolv
if ! pkg_add dnsmasq-full; then
warn "не удалось поставить dnsmasq-full, возвращаю обычный dnsmasq"
fix_resolv
pkg_add dnsmasq
[ -f /etc/config/dhcp ] || cp "$BT_TMP_DIR/dhcp.bak" /etc/config/dhcp 2>/dev/null
/etc/init.d/dnsmasq restart >/dev/null 2>&1
die "dnsmasq-full не установлен"
fi
[ -f /etc/config/dhcp ] || cp "$BT_TMP_DIR/dhcp.bak" /etc/config/dhcp 2>/dev/null
/etc/init.d/dnsmasq enable >/dev/null 2>&1
/etc/init.d/dnsmasq restart >/dev/null 2>&1
}
install_byedpi() {
local url json cands f
if [ -x /usr/bin/ciadpi ] && [ "${FORCE_BYEDPI:-0}" != 1 ]; then
say "ByeDPI уже установлен"
return 0
fi
url="${BYEDPI_URL:-}"
if [ -z "$url" ]; then
say "Ищу пакет byedpi ($ARCH, .$EXT) в $BYEDPI_REPO"
json=$(fetch "https://api.github.com/repos/$BYEDPI_REPO/releases?per_page=40" - 2>/dev/null)
cands=$(printf '%s\n' "$json" \
| grep -o '"browser_download_url": *"[^"]*"' \
| sed 's/^[^:]*: *"//; s/"$//' \
| grep -E "/byedpi_[^/]*_${ARCH}\.${EXT}\$")
url=$(printf '%s\n' "$cands" | grep -F "$REL_MM" | head -n 1)
[ -n "$url" ] || url=$(printf '%s\n' "$cands" | head -n 1)
fi
[ -n "$url" ] || die "не нашёл пакет byedpi для $ARCH (.$EXT). Скачайте вручную с https://github.com/$BYEDPI_REPO/releases и запустите: BYEDPI_URL=<ссылка> sh install.sh"
say "Скачиваю $url"
mkdir -p "$BT_TMP_DIR/dl"
f="$BT_TMP_DIR/dl/$(basename "$url")"
fetch "$url" "$f" || die "не удалось скачать byedpi"
if [ "$PM" = apk ]; then apk add --allow-untrusted "$f"; else opkg install "$f"; fi \
|| die "не удалось установить byedpi"
rm -rf "$BT_TMP_DIR/dl"
NEW_BYEDPI=1
}
install_payload() {
mkdir -p "$R/etc/config"
[ -f "$R/etc/config/byetube" ] || cat > "$R/etc/config/byetube" <<'BT_FILE_END_7f3a9c'
config byetube 'main'
option enabled '1'
option byedpi_port '1088'
option byedpi_opts '-d1 -d3+s -s6+s -d9+s -s12+s -d15+s -s20+s -d25+s -s30+s -d35+s -r1+s -S -a1 -As -d1 -d3+s -s6+s -d9+s -s12+s -d15+s -s20+s -d25+s -s30+s -d35+s -S -a1'
option ipv6 '1'
option quic 'block'
option default_domains '1'
BT_FILE_END_7f3a9c
chmod 644 "$R/etc/config/byetube"
mkdir -p "$R/etc/hotplug.d/firewall"
cat > "$R/etc/hotplug.d/firewall/90-byetube" <<'BT_FILE_END_7f3a9c'
#!/bin/sh
[ -f /tmp/ByeTube/started ] || exit 0
nft list table inet byetube >/dev/null 2>&1 && exit 0
logger -t byetube "таблица nft пропала после reload firewall — восстанавливаю"
/etc/init.d/byetube restart >/dev/null 2>&1
exit 0
BT_FILE_END_7f3a9c
chmod 755 "$R/etc/hotplug.d/firewall/90-byetube"
mkdir -p "$R/etc/init.d"
cat > "$R/etc/init.d/byetube" <<'BT_FILE_END_7f3a9c'
#!/bin/sh /etc/rc.common
START=99
STOP=10
USE_PROCD=1
NAME=byetube
[ -r /opt/ByeTube/lib/common.sh ] || exit 0
. /opt/ByeTube/lib/common.sh
. /opt/ByeTube/lib/dns.sh
log() { logger -t "$NAME" "$*"; }
write_hev_conf() {
local port="$1" ipv6="$2" f="$BT_TMP/hev.yml"
{
echo "tunnel:"
echo " name: $TUN"
echo " mtu: 1500"
echo " ipv4: 198.18.0.1"
[ "$ipv6" = "1" ] && echo " ipv6: 'fc00::1'"
echo " post-up-script: $BT_HOME/lib/route-up.sh"
echo "socks5:"
echo " port: $port"
echo " address: 127.0.0.1"
echo " udp: 'udp'"
echo "misc:"
echo " log-level: warn"
} > "$f"
}
ensure_fw4_include() {
command -v fw4 >/dev/null 2>&1 || { log "fw4 не найден — нужен OpenWrt 22.03+"; return 0; }
nft list chain inet fw4 forward 2>/dev/null | grep -q "byetube" && return 0
[ -f /usr/share/nftables.d/chain-pre/forward/50-byetube.nft ] || return 0
log "перезагружаю firewall, чтобы подхватить правило forward"
/etc/init.d/firewall reload >/dev/null 2>&1
}
start_service() {
local enabled byedpi_port byedpi_opts ipv6 byedpi hev
config_load "$NAME"
config_get enabled main enabled 0
if [ "$enabled" != "1" ]; then
"$BT_HOME/lib/net.sh" purge
dns_remove
rm -f "$STARTED_FLAG"
return 0
fi
config_get byedpi_port main byedpi_port 1088
config_get byedpi_opts main byedpi_opts ""
byedpi_opts=$(byedpi_opts_clean "$byedpi_opts")
config_get ipv6 main ipv6 1
[ -f /proc/net/if_inet6 ] || ipv6=0
case "$byedpi_port" in
''|*[!0-9]*) log "некорректный порт: $byedpi_port"; return 1 ;;
esac
byedpi=$(find_byedpi)
hev=$(command -v hev-socks5-tunnel)
[ -n "$byedpi" ] || { log "ciadpi не найден: установите пакет byedpi"; return 1; }
[ -n "$hev" ] || { log "hev-socks5-tunnel не найден: установите пакет"; return 1; }
dnsmasq_has_nftset || { log "dnsmasq без поддержки nftset: установите dnsmasq-full"; return 1; }
[ -c /dev/net/tun ] || modprobe tun 2>/dev/null
bt_prepare
write_hev_conf "$byedpi_port" "$ipv6"
"$BT_HOME/lib/net.sh" up || { log "не удалось настроить nftables/маршрутизацию"; return 1; }
ensure_fw4_include
dns_apply "$ipv6"
procd_open_instance byedpi
procd_set_param command "$byedpi" -i 127.0.0.1 -p "$byedpi_port"
set -f
[ -n "$byedpi_opts" ] && procd_append_param command $byedpi_opts
set +f
procd_set_param respawn 3600 5 0
procd_set_param stderr 1
procd_close_instance
procd_open_instance hev
procd_set_param command "$hev" "$BT_TMP/hev.yml"
procd_set_param respawn 3600 5 0
procd_set_param stderr 1
procd_close_instance
touch "$STARTED_FLAG"
}
stop_service() {
rm -f "$STARTED_FLAG"
"$BT_HOME/lib/net.sh" down
}
service_triggers() {
procd_add_reload_trigger "$NAME"
}
BT_FILE_END_7f3a9c
chmod 755 "$R/etc/init.d/byetube"
mkdir -p "$R/lib/upgrade/keep.d"
cat > "$R/lib/upgrade/keep.d/byetube" <<'BT_FILE_END_7f3a9c'
/opt/ByeTube/custom/
BT_FILE_END_7f3a9c
chmod 644 "$R/lib/upgrade/keep.d/byetube"
mkdir -p "$R/opt/ByeTube/bin"
cat > "$R/opt/ByeTube/bin/byetube" <<'BT_FILE_END_7f3a9c'
#!/bin/sh
. "${BT_FUNCTIONS:-/lib/functions.sh}"
. "${BT_HOME:-/opt/ByeTube}/lib/common.sh"
. "$BT_HOME/lib/lists.sh"
svc_running() {
ubus call service list '{"name":"byetube"}' 2>/dev/null \
| jsonfilter -e "@.byetube.instances.$1.running" 2>/dev/null | grep -q true
}
count_set() {
nft list set inet "$NFT_TABLE" "$1" 2>/dev/null | grep -o 'expires' | wc -l
}
b() { if [ "$1" = "1" ]; then echo true; else echo false; fi; }
extra_count() {
config_list_foreach main domain _extra_inc
echo "$_extra_n"
}
_extra_n=0
_extra_inc() { _extra_n=$((_extra_n + 1)); }
cfg_valid() {
local v
case "$1" in
enabled|ipv6|default_domains)
case "$2" in 0|1) printf '%s' "$2" ;; *) return 1 ;; esac ;;
quic)
case "$2" in block|proxy) printf '%s' "$2" ;; *) return 1 ;; esac ;;
byedpi_port)
case "$2" in ''|*[!0-9]*) return 1 ;; esac
[ "$2" -ge 1 ] && [ "$2" -le 65535 ] || return 1
printf '%s' "$2" ;;
byedpi_opts)
v=$(printf '%s' "$2" | tr '\n\r\t' ' ' | sed 's/ */ /g; s/^ //; s/ $//')
[ -n "$v" ] || return 1
printf '%s' "$v" ;;
*) return 1 ;;
esac
}
cmd_config() {
local action="$1" k v nv args="" err="" ipv6s=false
shift
case "$action" in
get)
config_load byetube
config_get enabled main enabled 0
config_get byedpi_opts main byedpi_opts ""
config_get byedpi_port main byedpi_port 1088
config_get quic main quic block
config_get ipv6 main ipv6 1
config_get default_domains main default_domains 1
case "$byedpi_port" in ''|*[!0-9]*) byedpi_port=1088 ;; esac
[ -f /proc/net/if_inet6 ] && ipv6s=true
printf '{"enabled":%s,"byedpi_opts":"%s","byedpi_port":%s,"quic":"%s","ipv6":%s,"ipv6_supported":%s,"default_domains":%s}\n' \
"$(b "$enabled")" "$(json_esc "$byedpi_opts")" "$byedpi_port" "$quic" "$(b "$ipv6")" "$ipv6s" "$(b "$default_domains")"
;;
set)
while [ $# -ge 2 ]; do
k="$1"; v="$2"; shift 2
if ! nv=$(cfg_valid "$k" "$v"); then
err="$k"
break
fi
args="$args
$k=$nv"
done
if [ -n "$err" ]; then
printf '{"error":"Неверное значение параметра: %s"}\n' "$(json_esc "$err")"
return 0
fi
printf '%s\n' "$args" | while IFS='=' read -r k v; do
[ -n "$k" ] && uci set "byetube.main.$k=$v"
done
uci commit byetube
echo '{"ok":true}'
;;
*)
echo '{"error":"неизвестное действие"}'
;;
esac
}
case "$1" in
status)
config_load byetube
config_get enabled main enabled 0
config_get ipv6 main ipv6 1
config_get quic main quic block
config_get default_domains main default_domains 1
v_byedpi=0; svc_running byedpi && v_byedpi=1
v_hev=0; svc_running hev && v_hev=1
v_tun=0; ip link show "$TUN" >/dev/null 2>&1 && v_tun=1
v_nft=0; nft list table inet "$NFT_TABLE" >/dev/null 2>&1 && v_nft=1
v_rule=0; ip rule show 2>/dev/null | grep -q "lookup $TABLE" && v_rule=1
v_route=0; ip route show table "$TABLE" 2>/dev/null | grep -q "$TUN" && v_route=1
v_dns=0; [ -s "$(dnsmasq_confdir)/byetube.conf" ] && v_dns=1
v_nftset=0; dnsmasq_has_nftset && v_nftset=1
v_fw=0; nft list chain inet fw4 forward 2>/dev/null | grep -q byetube && v_fw=1
v_bin=0; { [ -x /usr/bin/ciadpi ] || [ -x /usr/bin/byedpi ]; } && [ -x /usr/bin/hev-socks5-tunnel ] && v_bin=1
v_ycust=0; list_is_custom youtube && v_ycust=1
printf '{"version":"%s","enabled":%s,"byedpi":%s,"hev":%s,"tun":%s,"nft":%s,"rule":%s,"route":%s,"dns":%s,"dnsmasq_nftset":%s,"fw":%s,"binaries":%s,"ipv6":%s,"quic":"%s","default_domains":%s,"youtube_count":%s,"youtube_custom":%s,"extra_count":%s,"ips4":%s,"ips6":%s}\n' \
"$BT_VERSION" "$(b "$enabled")" "$(b $v_byedpi)" "$(b $v_hev)" "$(b $v_tun)" "$(b $v_nft)" \
"$(b $v_rule)" "$(b $v_route)" "$(b $v_dns)" "$(b $v_nftset)" "$(b $v_fw)" "$(b $v_bin)" \
"$(b "$ipv6")" "$quic" "$(b "$default_domains")" "$(count_lines "$(list_file youtube)")" "$(b $v_ycust)" "$(extra_count)" \
"$(count_set yt4)" "$(count_set yt6)"
;;
config)
shift
cmd_config "$@"
;;
service)
case "$2" in
start|stop|restart) /etc/init.d/byetube "$2" >/dev/null 2>&1; echo '{"ok":true}' ;;
*) echo '{"error":"неизвестное действие"}' ;;
esac
;;
flush)
nft flush set inet "$NFT_TABLE" yt4 2>/dev/null
nft flush set inet "$NFT_TABLE" yt6 2>/dev/null
echo '{"ok":true}'
;;
ips)
nft list set inet "$NFT_TABLE" yt4 2>/dev/null
nft list set inet "$NFT_TABLE" yt6 2>/dev/null
;;
list)
shift
cmd_list "$@"
;;
diag)
arg="$2"; secs="${3:-20}"
LEASES="${BT_LEASES:-/tmp/dhcp.leases}"
CT="${BT_CT:-/proc/net/nf_conntrack}"
case "$secs" in ''|*[!0-9]*) secs=20 ;; esac
echo "=== YouTube Bypass: диагностика ==="
"$0" status
echo
echo "--- счётчики nft (растут, когда трафик попадает под правила) ---"
{ nft list chain inet "$NFT_TABLE" prerouting; nft list chain inet "$NFT_TABLE" forward; } 2>/dev/null \
| grep counter | sed 's/^[[:space:]]*//'
echo
echo "--- какие DNS раздаются клиентам по DHCP ---"
uci -q show dhcp | grep -E 'dhcp_option|\.dns=' || echo "своих dhcp_option/dns нет (клиенты получают DNS роутера)"
echo "--- IPv6: маршрут по умолчанию: $(ip -6 route show default 2>/dev/null | head -n 1)"
router_addrs=$(ip -o addr show 2>/dev/null | awk '{print $4}' | sed 's#/.*##' | tr '\n' ' ')
lan_ip=$(uci -q get network.lan.ipaddr | sed 's#/.*##')
[ -n "$lan_ip" ] && router_addrs="$router_addrs $lan_ip"
list_leases() {
echo "--- клиенты в DHCP ---"
if [ -s "$LEASES" ]; then
awk '{printf " %-16s %-18s %s\n", $3, $2, $4}' "$LEASES"
else
echo " (файл аренд пуст — смотрите IP в LuCI: Состояние → Обзор)"
fi
}
if [ -z "$arg" ]; then
echo
list_leases
echo
echo "Запустите: byetube diag <IP|MAC|имя телефона> [секунд, по умолчанию 20]"
exit 0
fi
client="$arg"
if ! printf '%s' "$arg" | grep -Eq '^[0-9]+(\.[0-9]+){3}$'; then
found=$(grep -i -- "$arg" "$LEASES" 2>/dev/null)
n=$(printf '%s\n' "$found" | grep -c .)
if [ "$n" -eq 0 ]; then
echo; echo "«$arg» не найден в DHCP-арендах."; list_leases; exit 0
elif [ "$n" -gt 1 ]; then
echo; echo "«$arg» подходит нескольким клиентам — уточните:"; list_leases; exit 0
fi
client=$(printf '%s\n' "$found" | awk '{print $3}')
fi
case " $router_addrs " in
*" $client "*)
echo
echo "$client — это адрес самого РОУТЕРА. Нужен IP телефона:"
list_leases
exit 0 ;;
esac
mac=$(ip neigh show 2>/dev/null | awk -v ip="$client" '$1==ip {for(i=1;i<=NF;i++) if($i=="lladdr") print $(i+1)}' | head -n 1)
ips="$client"
[ -n "$mac" ] && ips="$ips $(ip -6 neigh show 2>/dev/null | awk -v m="$mac" 'tolower($0) ~ tolower(m) {print $1}' | grep -v '^fe80' | tr '\n' ' ')"
echo
echo "=== клиент: $ips (MAC: ${mac:-не найден}) ==="
echo ">>> Собираю данные ${secs} с. ОТКРОЙТЕ YouTube НА ТЕЛЕФОНЕ (перезапустите приложение / обновите страницу) <<<"
raw="/tmp/ytb-ct.$$"; out="/tmp/ytb-diag.$$"; ins="/tmp/ytb-ins.$$"
: > "$raw"; : > "$ins"
i=0
while [ "$i" -lt "$secs" ]; do
cat "$CT" >> "$raw" 2>/dev/null
i=$((i + 1))
[ "$i" -lt "$secs" ] && sleep 1
done
awk -v ips=" $ips " '
{
proto=$3; src=""; dst=""; sport=""; dport=""; mark=0
for (i=1;i<=NF;i++) {
if (src=="" && $i ~ /^src=/) src=substr($i,5)
else if (dst=="" && $i ~ /^dst=/) dst=substr($i,5)
else if (sport=="" && $i ~ /^sport=/) sport=substr($i,7)
else if (dport=="" && $i ~ /^dport=/) dport=substr($i,7)
else if ($i ~ /^mark=/) mark=substr($i,6)+0
}
if (index(ips, " " src " ") == 0) next
key=proto " " src " " sport " " dst " " dport
if (!(key in P)) { P[key]=proto; D[key]=dst; Q[key]=dport; M[key]=0 }
if (int(mark/65536)%2==1) M[key]=1
}
END {
for (k in P) {
if (Q[k]=="53" || Q[k]=="853") c["DNS " P[k] " " D[k] ":" Q[k]]++
else if (Q[k]=="443") c["443 " P[k] " " (M[k] ? "tunnel" : "direct") " " D[k]]++
}
for (k in c) print k, c[k]
}' "$raw" 2>/dev/null | sort > "$out"
echo
echo "--- DNS-запросы клиента (куда он реально их шлёт) ---"
dns_ok=0; dns_bad=0
if grep -q '^DNS' "$out"; then
while read -r _ proto target n; do
host="${target%:*}"; port="${target##*:}"
if [ "$port" = "853" ]; then
who="DNS-over-TLS (Частный DNS) — МИМО роутера"; dns_bad=1
elif case " $router_addrs " in *" $host "*) true ;; *) false ;; esac; then
who="роутер (ок)"; dns_ok=1
else
who="НЕ роутер — набор для этого клиента не заполнится"; dns_bad=1
fi
echo " $proto $target x$n — $who"
done <<EOF_DNS
$(grep '^DNS' "$out")
EOF_DNS
else
echo " DNS-запросов не было (клиент мог отвечать из кэша или использует DoH по :443)"
fi
echo "--- соединения клиента на :443 ---"
tcp_t=$(awk '$1=="443" && $2=="tcp" && $3=="tunnel" {s+=$NF} END{print s+0}' "$out")
tcp_d=$(awk '$1=="443" && $2=="tcp" && $3=="direct" {s+=$NF} END{print s+0}' "$out")
udp_n=$(awk '$1=="443" && $2=="udp" {s+=$NF} END{print s+0}' "$out")
echo " tcp: через туннель — $tcp_t, напрямую — $tcp_d; udp/443 (QUIC): $udp_n"
echo "--- топ прямых TCP/443 (нет ли среди них YouTube/Google?) ---"
grep '^443 tcp direct' "$out" | awk '{printf "%09d %s\n", 999999999-$NF, $0}' | sort | head -n 8 | cut -d' ' -f2- | while read -r _ _ _ dst n; do
fam=yt4; case "$dst" in *:*) fam=yt6 ;; esac
if nft get element inet "$NFT_TABLE" "$fam" "{ $dst }" >/dev/null 2>&1; then
echo " $dst x$n — в наборе, но трафик не помечен" | tee -a "$ins"
else
echo " $dst x$n — не в наборе"
fi
done
in_set=$(grep -c . "$ins")
echo
found_any=0
if [ "$dns_bad" -eq 1 ]; then
found_any=1
if [ "$dns_ok" -eq 0 ]; then
echo "ВЫВОД: телефон шлёт DNS мимо роутера (Частный DNS / DoT / внешний DNS) — набор не заполняется, YouTube идёт напрямую."
else
echo "ВЫВОД: часть DNS-запросов телефона идёт мимо роутера (Частный DNS / DoT / внешний DNS) — для таких имён набор не заполнится."
fi
echo " Отключите «Частный DNS» и «Безопасный DNS» в Chrome, уберите статический DNS/VPN/AdGuard и переподключите Wi-Fi."
fi
if [ "$in_set" -gt 0 ]; then
found_any=1
echo "ВЫВОД: есть соединения на IP из набора, но без метки — пришлите этот вывод целиком."
fi
if [ "$tcp_t" -gt 0 ]; then
found_any=1
echo "ИНФО: часть трафика YouTube от телефона идёт через туннель — маршрутизация работает."
[ "$dns_bad" -eq 0 ] && [ "$in_set" -eq 0 ] && \
echo " Если видео всё равно не грузится — стратегия ByeDPI (вкладка «Тест стратегий») или домены, которых нет в списке."
fi
if [ "$tcp_t" -eq 0 ] && [ "$tcp_d" -eq 0 ] && [ "$dns_ok" -eq 0 ] && [ "$dns_bad" -eq 0 ]; then
found_any=1
echo "ВЫВОД: от этого клиента не было ни DNS, ни :443-трафика. Проверьте IP/MAC, что телефон в этой сети, и откройте YouTube во время сбора."
fi
[ "$found_any" -eq 1 ] || echo "ВЫВОД: однозначно определить не удалось — пришлите этот вывод целиком."
rm -f "$raw" "$out" "$ins"
;;
test)
shift
exec "$BT_HOME/lib/test.sh" "$@"
;;
set-strategy)
if nv=$(cfg_valid byedpi_opts "$2"); then
uci set "byetube.main.byedpi_opts=$nv" && uci commit byetube
/etc/init.d/byetube restart >/dev/null 2>&1
echo '{"ok":true}'
else
echo '{"error":"пустая стратегия"}'
fi
;;
version)
echo "$BT_VERSION"
;;
*)
echo "usage: byetube status|config|service|flush|ips|list|diag|test|set-strategy|version" >&2
exit 1
;;
esac
BT_FILE_END_7f3a9c
chmod 755 "$R/opt/ByeTube/bin/byetube"
mkdir -p "$R/opt/ByeTube/default"
cat > "$R/opt/ByeTube/default/domains.list" <<'BT_FILE_END_7f3a9c'
android.clients.google.com
beacons.gvt2.com
cdn.youtube.com
connectivitycheck.gstatic.com
fonts.googleapis.com
fonts.gstatic.com
ggpht.com
googleapis.com
googleplay.com
googleusercontent.com
googlevideo.com
gvt1.com
i.ytimg.com
i9.ytimg.com
jnn-pa.googleapis.com
kids.youtube.com
lh3.googleusercontent.com
m.youtube.com
manifest.googlevideo.com
music.youtube.com
nhacmp3youtube.com
play-fe.googleapis.com
play-games.googleusercontent.com
play-lh.googleusercontent.com
play.google.com
play.googleapis.com
prod-lt-playstoregatewayadapter-pa.googleapis.com
returnyoutubedislikeapi.com
s.ytimg.com
signaler-pa.youtube.com
studio.youtube.com
tv.youtube.com
wide-youtube.l.google.com
withyoutube.com
youtu.be
youtube-nocookie.com
youtube-ui.l.google.com
youtube.ae
youtube.al
youtube.am
youtube.at
youtube.az
youtube.ba
youtube.be
youtube.bg
youtube.bh
youtube.bo
youtube.by
youtube.ca
youtube.cat
youtube.ch
youtube.cl
youtube.co
youtube.co.ae
youtube.co.at
youtube.co.cr
youtube.co.hu
youtube.co.id
youtube.co.il
youtube.co.in
youtube.co.jp
youtube.co.ke
youtube.co.kr
youtube.co.ma
youtube.co.nz
youtube.co.th
youtube.co.tz
youtube.co.ug
youtube.co.uk
youtube.co.ve
youtube.co.za
youtube.co.zw
youtube.com
youtube.com.ar
youtube.com.au
youtube.com.az
youtube.com.bd
youtube.com.bh
youtube.com.bo
youtube.com.br
youtube.com.by
youtube.com.co
youtube.com.do
youtube.com.ec
youtube.com.ee
youtube.com.eg
youtube.com.es
youtube.com.gh
youtube.com.gr
youtube.com.gt
youtube.com.hk
youtube.com.hn
youtube.com.hr
youtube.com.jm
youtube.com.jo
youtube.com.kw
youtube.com.lb
youtube.com.lv
youtube.com.ly
youtube.com.mk
youtube.com.mt
youtube.com.mx
youtube.com.my
youtube.com.ng
youtube.com.ni
youtube.com.om
youtube.com.pa
youtube.com.pe
youtube.com.ph
youtube.com.pk
youtube.com.pt
youtube.com.py
youtube.com.qa
youtube.com.ro
youtube.com.sa
youtube.com.sg
youtube.com.sv
youtube.com.tn
youtube.com.tr
youtube.com.tw
youtube.com.ua
youtube.com.uy
youtube.com.ve
youtube.cr
youtube.cz
youtube.de
youtube.dk
youtube.ee
youtube.es
youtube.fi
youtube.fr
youtube.ge
youtube.googleapis.com
youtube.gr
youtube.gt
youtube.hk
youtube.hr
youtube.hu
youtube.ie
youtube.in
youtube.iq
youtube.is
youtube.it
youtube.jo
youtube.jp
youtube.kr
youtube.kz
youtube.la
youtube.lk
youtube.lt
youtube.lu
youtube.lv
youtube.ly
youtube.ma
youtube.md
youtube.me
youtube.mk
youtube.mn
youtube.mx
youtube.my
youtube.ng
youtube.ni
youtube.nl
youtube.no
youtube.pa
youtube.pe
youtube.ph
youtube.pk
youtube.pl
youtube.pr
youtube.pt
youtube.qa
youtube.ro
youtube.rs
youtube.ru
youtube.sa
youtube.se
youtube.sg
youtube.si
youtube.sk
youtube.sn
youtube.soy
youtube.sv
youtube.tn
youtube.tv
youtube.ua
youtube.ug
youtube.uy
youtube.vn
youtubeeducation.com
youtubeembeddedplayer.googleapis.com
youtubefanfest.com
youtubegaming.com
youtubego.co.id
youtubego.co.in
youtubego.com
youtubego.com.br
youtubego.id
youtubego.in
youtubei.googleapis.com
youtubei.youtube.com
youtubekids.com
youtubemobilesupport.com
yt-video-upload.l.google.com
yt.be
yt3.ggpht.com
yt3.googleusercontent.com
yt4.ggpht.com
ytimg.com
ytimg.l.google.com
yting.com
BT_FILE_END_7f3a9c
chmod 644 "$R/opt/ByeTube/default/domains.list"
mkdir -p "$R/opt/ByeTube/default"
cat > "$R/opt/ByeTube/default/strategies.txt" <<'BT_FILE_END_7f3a9c'
-f-200 -Qr -s3:5+sm -a1 -As -d1 -s4+sm -s8+sh -f-300 -d6+sh -a1 -At,r,s -o2 -f-30 -As -r5 -Mh -r6+sh -f-250 -s2:7+s -s3:6+sm -a1 -At,r,s -s3:5+sm -s6+s -s7:9+s -q30+sm -a1
-d1 -d3+s -s6+s -d9+s -s12+s -d15+s -s20+s -d25+s -s30+s -d35+s -r1+s -S -a1 -As -d1 -d3+s -s6+s -d9+s -s12+s -d15+s -s20+s -d25+s -s30+s -d35+s -S -a1
-q2 -s2 -s3+s -r3 -s4 -r4 -s5+s -r5+s -s6 -s7+s -r8 -s9+s -Qr -Mh,d,r -a1 -At,r -s2+s -r2 -d2 -s3 -r3 -r4 -s4 -d5+s -r5 -d6 -s7+s -d7 -a1
-o1 -d1 -a1 -At,r,s -s1 -d1 -s5+s -s10+s -s15+s -s20+s -r1+s -S -a1 -As -s1 -d1 -s5+s -s10+s -s15+s -s20+s -S -a1
-n "google.com" -Qr -f-204 -s1:5+sm -a1 -As -d1 -s3+s -s5+s -q7 -a1 -As -o2 -f-43 -a1 -As -r5 -Mh -s1:5+s -s3:7+sm -a1
-n "google.com" -Qr -f-205 -a1 -As -s1:3+sm -a1 -As -s5:8+sm -a1 -As -d3 -q7 -o2 -f-43 -f-85 -f-165 -r5 -Mh -a1
-d1+s -s50+s -a1 -As -f20 -r2+s -a1 -At -d2 -s1+s -s5+s -s10+s -s15+s -s25+s -s35+s -s50+s -s60+s -a1
-o1 -a1 -At,r,s -f-1 -a1 -At,r,s -d1:11+sm -S -a1 -At,r,s -n "google.com" -Qr -f1 -d1:11+sm -s1:11+sm -S -a1
-d1 -s1 -q1 -a1 -Ar -s5 -o1+s -d3+s -s6+s -d9+s -s12+s -d15+s -s20+s -d25+s -s30+s -d35+s -a1
-f1+nme -t6 -a1 -As -n "google.com" -Qr -s1:6+sm -a1 -As -s5:12+sm -a1 -As -d3 -q7 -r6 -Mh -a1
-d1 -s1+s -d3+s -s6+s -d9+s -s12+s -d15+s -s20+s -d25+s -s30+s -d35+s -a1
-d1 -s1+s -d1+s -s3+s -d6+s -s12+s -d14+s -s20+s -d24+s -s30+s -a1
-o1 -a1 -At,r,s -f-1 -a1 -Ar,s -o1 -a1 -At -r1+s -f-1 -t6 -a1
-d1 -s1+s -s3+s -s6+s -s9+s -s12+s -s15+s -s20+s -s30+s -a1
-d1 -d3+s -s6+s -d6+s -s7+s -d8+s -s10+s -a1 -t12 -At,s -r3
-f1 -t5 -n "google.com" -q3+h -Qr -f2 -q1 -r1+s -t15 -q1 -o2 -a1
-n "google.com" -d2:5:2+h -f-3 -r2+sm -o2 -o50+s -r2+s -f-4 -a1
-f-1 -Qr -s1+sm -d3+s -s5+sm -o2 -a1 -As -r1+s -d8+s -a1
-r-1+s -o20+sm -s3:7+sm -d5:3+sm -f300+s -Qr -f-1 -a1
-o2 -O4 -s1 -q1 -a1 -Ar -s5 -o1+s -f1+s -r20+s -a1
-o1 -r-5+se -a1 -At,r,s -d1 -n "google.com" -Qr -f-1 -a1
--fake -1 --ttl 8 --split 1+s --disorder 3+s -a1
-n "google.com" -Qr -f6+nr -d2 -d11 -f9+hm -o3 -t7 -a1
-r5+s -s25+s -a1 -At,r,s -s50 -r5+s -s50+s -a1
-d1 -d3+s -s6+s -d9+s -s20+s -d25+s -s30+s -a1
-d9+s -q20+s -s25+s -t5 -a1 -At,r,s -r1+h -a1
-q1+s -s29+s -s30+s -s14+s -o5+s -f-1 -S -a1
-d1 -s1+s -r1+s -e1 -m1 -o1+s -f-1 -t2 -a1
-d1 -o1 -a1 -Ar -o1 -a1 -At -f-1 -r1+s -a1
-d1 -s4 -d8 -s1+s -d5+s -s10+s -d20+s -a1
-f-1 -n "google.com" -Qr -s2+s -r3 -o20 -t4 -a1
-n "google.com" -Qr -d5+sm -f3+sm -o2 -t4 -a1
-o1 -a1 -Ar -q1 -a1 -At -f-1 -r1+s -a1
-q1 -a1 -Ar -o1 -a1 -At -f-1 -r1+s -a1
-s4+sn -r9+s -Qr -n "google.com" -S -a1
-o1 -d1 -r1+s -S -s1+s -d3+s -a1
-q1+s -s29+s -o5+s -f-1 -S -a1
-n "google.com" -Qr -m2 -f-1 -d7 -a1
-d1 -s1+s -r1+s -f-1 -t8 -a1
-o1 -a1 -An -f1+nme -t6 -a1
-n "google.com" -Qr -f-1 -r1+s -a1
-n "google.com" -Qr -d1:3 -f-1 -a1
-s1 -d3+s -a1 -At -r1+s -a1
-f-1 -t8 -n "google.com" -s1+s -a1
-n "google.com" -Qr -d1 -f-1 -a1
-f64+se -n "google.com" -t5 -a1
-o1 -a1 -At,r,s -d1 -a1
-d1+s -o2 -s5 -r5 -a1
-r8 -o2 -s7 -q4+s -a1
-o1 -f-1 -r-5+se -a1
-d6+s -q4+hm -o2 -a1
-s5+s -s35+s -m4 -a1
-f-1+sm -t7 -m2 -a1
-o1 -r-5+se -a1
-o1+s -d3+s -a1
-o1 -s4 -s6 -a1
-q1 -r25+s -a1
-d1 -s3+s -a1
-o3 -d7 -a1
-d7 -s2 -a1
-o1 -a1 -r-5+se
BT_FILE_END_7f3a9c
chmod 644 "$R/opt/ByeTube/default/strategies.txt"
mkdir -p "$R/opt/ByeTube/default"
cat > "$R/opt/ByeTube/default/test-domains.txt" <<'BT_FILE_END_7f3a9c'
# Google and Youtube
youtu.be
youtube.com
i.ytimg.com
i9.ytimg.com
yt3.ggpht.com
yt4.ggpht.com
googleapis.com
jnn-pa.googleapis.com
googleusercontent.com
signaler-pa.youtube.com
youtubei.googleapis.com
manifest.googlevideo.com
yt3.googleusercontent.com
# Googlevideo
rr1---sn-4axm-n8vs.googlevideo.com
rr1---sn-gvnuxaxjvh-o8ge.googlevideo.com
rr1---sn-ug5onuxaxjvh-p3ul.googlevideo.com
rr1---sn-ug5onuxaxjvh-n8v6.googlevideo.com
rr4---sn-q4flrnsl.googlevideo.com
rr10---sn-gvnuxaxjvh-304z.googlevideo.com
rr14---sn-n8v7kn7r.googlevideo.com
rr16---sn-axq7sn76.googlevideo.com
rr1---sn-8ph2xajvh-5xge.googlevideo.com
rr1---sn-gvnuxaxjvh-5gie.googlevideo.com
rr12---sn-gvnuxaxjvh-bvwz.googlevideo.com
rr5---sn-n8v7knez.googlevideo.com
rr1---sn-u5uuxaxjvhg0-ocje.googlevideo.com
rr2---sn-q4fl6ndl.googlevideo.com
rr5---sn-gvnuxaxjvh-n8vk.googlevideo.com
rr4---sn-jvhnu5g-c35d.googlevideo.com
rr1---sn-q4fl6n6y.googlevideo.com
rr2---sn-hgn7ynek.googlevideo.com
rr1---sn-xguxaxjvh-gufl.googlevideo.com
BT_FILE_END_7f3a9c
chmod 644 "$R/opt/ByeTube/default/test-domains.txt"
mkdir -p "$R/opt/ByeTube/lib"
cat > "$R/opt/ByeTube/lib/common.sh" <<'BT_FILE_END_7f3a9c'
BT_HOME="${BT_HOME:-/opt/ByeTube}"
BT_TMP="${BT_TMP:-/tmp/ByeTube}"
BT_FUNCTIONS="${BT_FUNCTIONS:-/lib/functions.sh}"
BT_DEFAULT="$BT_HOME/default"
BT_CUSTOM="$BT_HOME/custom"
BT_VERSION="1.0"
TUN=byetube0
MARK=0x10000
MASK=0x10000
TABLE=89
PREF=8900
NFT_TABLE=byetube
STARTED_FLAG="$BT_TMP/started"
TEST_DIR="$BT_TMP/test"
bt_prepare() {
mkdir -p "$BT_TMP"
}
dnsmasq_confdir() {
local d
d=$(sed -n 's/^conf-dir=\([^,]*\).*/\1/p' /var/etc/dnsmasq.conf.* 2>/dev/null | head -n 1)
[ -n "$d" ] || d=/tmp/dnsmasq.d
echo "$d"
}
dnsmasq_has_nftset() {
dnsmasq --version 2>/dev/null | grep -Eq '(^| )nftset( |$)'
}
find_byedpi() {
local b
for b in /usr/bin/ciadpi /usr/bin/byedpi; do
[ -x "$b" ] && { echo "$b"; return 0; }
done
command -v ciadpi
}
byedpi_opts_clean() {
printf '%s' "$1" | tr -d "\"'" | tr '\n\r\t' ' ' | sed 's/^ *//; s/ *$//; s/ */ /g'
}
json_esc() {
printf '%s' "$1" | tr -d '\000-\037' | sed 's/\\/\\\\/g; s/"/\\"/g'
}
count_lines() {
sed 's/#.*//' "$1" 2>/dev/null | tr -d '\r' | grep -c '[^[:space:]]'
}
list_name() {
case "$1" in
youtube) echo domains.list ;;
strategies) echo strategies.txt ;;
test-domains) echo test-domains.txt ;;
esac
}
list_file() {
local nm
nm=$(list_name "$1")
[ -n "$nm" ] || return 1
if [ -s "$BT_CUSTOM/$nm" ]; then
echo "$BT_CUSTOM/$nm"
else
echo "$BT_DEFAULT/$nm"
fi
}
list_is_custom() {
local nm
nm=$(list_name "$1")
[ -n "$nm" ] && [ -s "$BT_CUSTOM/$nm" ]
}
test_is_running() {
[ -f "$TEST_DIR/job.pid" ] && kill -0 "$(cat "$TEST_DIR/job.pid" 2>/dev/null)" 2>/dev/null
}
norm_domains() {
awk -v badf="$1" '
{
gsub(/\r/, "")
line = $0
sub(/^[ \t]+/, "", line); sub(/[ \t]+$/, "", line)
if (line == "") next
if (substr(line, 1, 1) == "#") { print line; next }
sub(/#.*/, "", line)
n = split(line, tok, /[ \t,;]+/)
for (i = 1; i <= n; i++) {
d = tolower(tok[i])
if (d == "") continue
sub(/^[a-z][a-z0-9+.-]*:\/\//, "", d)
sub(/[\/?#].*$/, "", d)
sub(/^\*?\./, "", d)
if (d ~ /^[a-z0-9]([a-z0-9._-]*[a-z0-9])?$/ && index(d, ".") > 0) {
if (!(d in seen)) { seen[d] = 1; print d }
} else if (!bad) { bad = 1; print tok[i] > badf }
}
}'
}
norm_strategies() {
awk '
{
gsub(/\r/, ""); gsub(/[ \t]+/, " ")
line = $0; sub(/^ /, "", line); sub(/ $/, "", line)
if (line == "") next
if (substr(line, 1, 1) == "#") { print line; next }
if (!(line in seen)) { seen[line] = 1; print line }
}'
}
BT_FILE_END_7f3a9c
chmod 755 "$R/opt/ByeTube/lib/common.sh"
mkdir -p "$R/opt/ByeTube/lib"
cat > "$R/opt/ByeTube/lib/dns-apply.sh" <<'BT_FILE_END_7f3a9c'
#!/bin/sh
. "${BT_FUNCTIONS:-/lib/functions.sh}"
. "${BT_HOME:-/opt/ByeTube}/lib/common.sh"
. "$BT_HOME/lib/dns.sh"
config_load byetube
config_get enabled main enabled 0
[ "$enabled" = "1" ] || exit 0
config_get ipv6 main ipv6 1
[ -f /proc/net/if_inet6 ] || ipv6=0
dns_apply "$ipv6"
exit 0
BT_FILE_END_7f3a9c
chmod 755 "$R/opt/ByeTube/lib/dns-apply.sh"
mkdir -p "$R/opt/ByeTube/lib"
cat > "$R/opt/ByeTube/lib/dns.sh" <<'BT_FILE_END_7f3a9c'
_echo() { echo "$1"; }
collect_domains() {
local use_default
config_get use_default main default_domains 1
{
[ "$use_default" = "1" ] && cat "$(list_file youtube)" 2>/dev/null
config_list_foreach main domain _echo
} | sed 's/#.*//; s/^[[:space:]]*//; s/[[:space:]]*$//' \
| tr '[:upper:]' '[:lower:]' \
| grep -E '^[a-z0-9]([a-z0-9._-]*[a-z0-9])?$' \
| sort -u \
| awk '
{ name[NR] = $0; have[$0] = 1 }
END {
for (i = 1; i <= NR; i++) {
n = split(name[i], p, ".")
suf = ""; redundant = 0
for (j = n; j >= 2; j--) {
suf = (suf == "") ? p[j] : p[j] "." suf
if (suf in have) { redundant = 1; break }
}
if (!redundant) print name[i]
}
}'
}
dns_remove() {
local conf
conf="$(dnsmasq_confdir)/byetube.conf"
[ -f "$conf" ] || return 0
rm -f "$conf"
/etc/init.d/dnsmasq restart >/dev/null 2>&1
}
dns_apply() {
local ipv6="$1" conf domains suffix new old
conf="$(dnsmasq_confdir)/byetube.conf"
domains=$(collect_domains)
if [ -z "$domains" ]; then
dns_remove
return 0
fi
suffix="4#inet#${NFT_TABLE}#yt4"
[ "$ipv6" = "1" ] && suffix="$suffix,6#inet#${NFT_TABLE}#yt6"
new=$(printf '%s\n' "$domains" | awk -v suffix="$suffix" -v max=800 '
{
if (cur != "" && length("nftset=/" cur "/" $0 "/" suffix) > max) {
print "nftset=/" cur "/" suffix
cur = ""
}
cur = (cur == "") ? $0 : cur "/" $0
}
END { if (cur != "") print "nftset=/" cur "/" suffix }')
old=$(cat "$conf" 2>/dev/null)
if [ "$old" != "$new" ]; then
mkdir -p "$(dirname "$conf")"
printf '%s\n' "$new" > "$conf"
/etc/init.d/dnsmasq restart >/dev/null 2>&1
fi
}
BT_FILE_END_7f3a9c
chmod 755 "$R/opt/ByeTube/lib/dns.sh"
mkdir -p "$R/opt/ByeTube/lib"
cat > "$R/opt/ByeTube/lib/lists.sh" <<'BT_FILE_END_7f3a9c'
list_apply_dns() {
local en
"$BT_HOME/lib/dns-apply.sh" >/dev/null 2>&1
en=$(uci -q get byetube.main.enabled)
[ "$en" = "1" ]
}
list_extra_get() {
uci -q get byetube.main.domain | tr ' ' '\n' | grep -v '^$'
}
list_extra_write() {
local file="$1" d
uci -q delete byetube.main.domain
while IFS= read -r d; do
[ -n "$d" ] && uci add_list "byetube.main.domain=$d"
done < "$file"
uci commit byetube
}
cmd_list() {
local action="$1" kind="$2" text="$3" nm tmp bad n f applied=false custom=false
case "$kind" in
youtube|strategies|test-domains|extra) ;;
*) echo '{"error":"неизвестный список"}'; return 0 ;;
esac
case "$action" in
get)
if [ "$kind" = "extra" ]; then
list_extra_get
else
f=$(list_file "$kind")
[ -f "$f" ] && cat "$f"
fi
return 0
;;
set|reset)
;;
*)
echo '{"error":"неизвестное действие"}'
return 0
;;
esac
case "$kind" in
strategies|test-domains)
if test_is_running; then
echo '{"error":"тест выполняется — остановите его перед изменением списка"}'
return 0
fi
;;
esac
nm=$(list_name "$kind")
mkdir -p "$BT_TMP"
tmp="$BT_TMP/list.$$"
bad="$BT_TMP/bad.$$"
if [ "$action" = "reset" ]; then
if [ "$kind" = "extra" ]; then
: > "$tmp"
list_extra_write "$tmp"
rm -f "$tmp"
n=0
else
rm -f "$BT_CUSTOM/$nm"
n=$(count_lines "$(list_file "$kind")")
fi
else
: > "$bad"
case "$kind" in
strategies) printf '%s\n' "$text" | norm_strategies > "$tmp" ;;
extra) printf '%s\n' "$text" | norm_domains "$bad" | grep -v '^#' > "$tmp" ;;
*) printf '%s\n' "$text" | norm_domains "$bad" > "$tmp" ;;
esac
if [ -s "$bad" ]; then
printf '{"error":"Некорректный домен: %s"}\n' "$(json_esc "$(head -n 1 "$bad")")"
rm -f "$tmp" "$bad"
return 0
fi
rm -f "$bad"
n=$(count_lines "$tmp")
if [ "$kind" = "extra" ]; then
list_extra_write "$tmp"
rm -f "$tmp"
else
if [ "${n:-0}" -le 0 ]; then
echo '{"error":"список пуст — чтобы вернуть встроенный, нажмите «Восстановить встроенный»"}'
rm -f "$tmp"
return 0
fi
mkdir -p "$BT_CUSTOM"
mv "$tmp" "$BT_CUSTOM/$nm"
fi
fi
case "$kind" in
youtube|extra) list_apply_dns && applied=true ;;
esac
if [ "$kind" != "extra" ] && list_is_custom "$kind"; then
custom=true
fi
[ "$kind" = "extra" ] && [ "${n:-0}" -gt 0 ] && custom=true
printf '{"ok":true,"count":%s,"custom":%s,"applied":%s}\n' "${n:-0}" "$custom" "$applied"
return 0
}
BT_FILE_END_7f3a9c
chmod 755 "$R/opt/ByeTube/lib/lists.sh"
mkdir -p "$R/opt/ByeTube/lib"
cat > "$R/opt/ByeTube/lib/net.sh" <<'BT_FILE_END_7f3a9c'
#!/bin/sh
. "${BT_FUNCTIONS:-/lib/functions.sh}"
. "${BT_HOME:-/opt/ByeTube}/lib/common.sh"
config_load byetube
config_get IPV6 main ipv6 1
config_get QUIC main quic block
[ -f /proc/net/if_inet6 ] || IPV6=0
rules_del() {
while ip rule del pref "$PREF" 2>/dev/null; do :; done
while ip -6 rule del pref "$PREF" 2>/dev/null; do :; done
ip route del default dev "$TUN" table "$TABLE" 2>/dev/null
ip -6 route del default dev "$TUN" table "$TABLE" 2>/dev/null
}
set_mark_stmt="counter ct mark set ct mark | $MARK meta mark set meta mark | $MARK"
gen_ruleset() {
cat <<NFT
table inet $NFT_TABLE {
set yt4 {
type ipv4_addr
flags timeout
timeout 12h
size 65535
}
set yt6 {
type ipv6_addr
flags timeout
timeout 12h
size 65535
}
chain prerouting {
type filter hook prerouting priority mangle; policy accept;
iifname "$TUN" return
ct mark & $MASK == $MARK meta mark set meta mark | $MARK return
meta l4proto tcp ip daddr @yt4 $set_mark_stmt
NFT
[ "$IPV6" = "1" ] && echo " meta l4proto tcp ip6 daddr @yt6 $set_mark_stmt"
if [ "$QUIC" = "proxy" ]; then
echo " udp dport 443 ip daddr @yt4 $set_mark_stmt"
[ "$IPV6" = "1" ] && echo " udp dport 443 ip6 daddr @yt6 $set_mark_stmt"
fi
echo " }"
if [ "$QUIC" != "proxy" ]; then
cat <<NFT
chain forward {
type filter hook forward priority filter - 10; policy accept;
udp dport 443 ip daddr @yt4 counter reject
udp dport 443 ip6 daddr @yt6 counter reject
}
NFT
fi
echo "}"
}
case "$1" in
up)
rules_del
nft delete table inet "$NFT_TABLE" 2>/dev/null
gen_ruleset | nft -f - || exit 1
ip rule add pref "$PREF" fwmark "$MARK/$MASK" lookup "$TABLE" || exit 1
[ "$IPV6" = "1" ] && ip -6 rule add pref "$PREF" fwmark "$MARK/$MASK" lookup "$TABLE"
ip link show "$TUN" >/dev/null 2>&1 && "$BT_HOME/lib/route-up.sh" "$TUN"
exit 0
;;
down)
rules_del
nft flush chain inet "$NFT_TABLE" prerouting 2>/dev/null
nft flush chain inet "$NFT_TABLE" forward 2>/dev/null
;;
purge)
rules_del
nft delete table inet "$NFT_TABLE" 2>/dev/null
;;
*)
echo "usage: $0 up|down|purge" >&2
exit 1
;;
esac
exit 0
BT_FILE_END_7f3a9c
chmod 755 "$R/opt/ByeTube/lib/net.sh"
mkdir -p "$R/opt/ByeTube/lib"
cat > "$R/opt/ByeTube/lib/route-up.sh" <<'BT_FILE_END_7f3a9c'
#!/bin/sh
. "${BT_FUNCTIONS:-/lib/functions.sh}"
. "${BT_HOME:-/opt/ByeTube}/lib/common.sh"
config_load byetube
config_get IPV6 main ipv6 1
[ -f /proc/net/if_inet6 ] || IPV6=0
ip link set "$TUN" up 2>/dev/null
ip route replace default dev "$TUN" table "$TABLE"
[ "$IPV6" = "1" ] && ip -6 route replace default dev "$TUN" table "$TABLE"
echo 2 > "/proc/sys/net/ipv4/conf/$TUN/rp_filter" 2>/dev/null
exit 0
BT_FILE_END_7f3a9c
chmod 755 "$R/opt/ByeTube/lib/route-up.sh"
mkdir -p "$R/opt/ByeTube/lib"
cat > "$R/opt/ByeTube/lib/test.sh" <<'BT_FILE_END_7f3a9c'
#!/bin/sh
. "${BT_FUNCTIONS:-/lib/functions.sh}"
. "${BT_HOME:-/opt/ByeTube}/lib/common.sh"
PIDF="$TEST_DIR/job.pid"
LOGF="$TEST_DIR/job.log"
RES="$TEST_DIR/results.txt"
RAW="$TEST_DIR/results.raw"
STOP="$TEST_DIR/stop"
CPID="$TEST_DIR/ciadpi.pid"
PORT_BASE=22000
UA='Mozilla/5.0 (Windows NT 10.0; Win64; x64) curl/8.0'
TAB=$(printf '\t')
cmd_start() {
if test_is_running; then
echo '{"started":true,"already_running":true}'
return 0
fi
command -v curl >/dev/null 2>&1 || { echo '{"error":"не установлен curl (apk add curl / opkg install curl)"}'; return 0; }
[ -n "$(find_byedpi)" ] || { echo '{"error":"не найден ciadpi (пакет byedpi)"}'; return 0; }
mkdir -p "$TEST_DIR"
rm -f "$STOP"
: > "$LOGF"
( "$0" run >>"$LOGF" 2>&1; echo "__DONE__ $?" >>"$LOGF" ) >/dev/null 2>&1 </dev/null &
echo $! > "$PIDF"
echo '{"started":true}'
}
cmd_stop() {
if ! test_is_running; then
echo '{"error":"тест не запущен"}'
return 0
fi
touch "$STOP"
[ -f "$CPID" ] && kill "$(cat "$CPID" 2>/dev/null)" 2>/dev/null
echo '{"ok":true}'
}
cmd_status() {
local running=false has=false cu=false rc="" sc=false dc=false
test_is_running && running=true
[ -s "$RES" ] && has=true
command -v curl >/dev/null 2>&1 && cu=true
[ -f "$LOGF" ] && rc=$(grep '^__DONE__' "$LOGF" | tail -n 1 | awk '{print $2}')
list_is_custom strategies && sc=true
list_is_custom test-domains && dc=true
printf '{"running":%s,"has_results":%s,"curl":%s,"strategies":%s,"test_domains":%s,"strategies_custom":%s,"test_domains_custom":%s,"rc":"%s"}\n' \
"$running" "$has" "$cu" "$(count_lines "$(list_file strategies)")" "$(count_lines "$(list_file test-domains)")" "$sc" "$dc" "$rc"
}
cmd_log() {
[ -f "$LOGF" ] && tail -n 400 "$LOGF" | grep -v '^__DONE__'
return 0
}
cmd_results() {
[ -s "$RES" ] && cat "$RES"
return 0
}
cmd_clear() {
if test_is_running; then
echo '{"error":"тест выполняется"}'
return 0
fi
rm -f "$RES" "$RAW"
echo '{"ok":true}'
}
check_url() {
local entry="$1" okfile="$2" logfile="$3" proxy="$4" host url rc
host="${entry%%|*}"
url="${entry#*|}"
if [ -n "$proxy" ]; then
curl -4 -sL --socks5 "$proxy" --connect-timeout 4 --max-time 6 --speed-time 3 --speed-limit 1 \
--range 0-65535 -A "$UA" -o /dev/null "$url" </dev/null >/dev/null 2>&1
else
curl -4 -sL --connect-timeout 4 --max-time 6 --speed-time 3 --speed-limit 1 \
--range 0-65535 -A "$UA" -o /dev/null "$url" </dev/null >/dev/null 2>&1
fi
rc=$?
if [ "$rc" -eq 0 ]; then
echo 1 >> "$okfile"
echo "[ OK ] $host" >> "$logfile"
else
echo "[FAIL] $host" >> "$logfile"
fi
}
check_all() {
local urls="$1" logfile="$2" proxy="$3" okf run=0 total=0 ok entry
okf="$TEST_DIR/ok.$$"
: > "$okf"
: > "$logfile"
while IFS= read -r entry; do
[ -n "$entry" ] || continue
[ -f "$STOP" ] && break
total=$((total + 1))
check_url "$entry" "$okf" "$logfile" "$proxy" &
run=$((run + 1))
if [ "$run" -ge "$PARALLEL" ]; then
wait
run=0
fi
done < "$urls"
wait
ok=$(wc -l < "$okf" | tr -d ' ')
rm -f "$okf"
echo "$ok $total"
}
cleanup() {
[ -f "$CPID" ] && kill "$(cat "$CPID" 2>/dev/null)" 2>/dev/null
rm -f "$CPID"
}
cmd_run() {
local BIN cand keys urls line k opts port idx total ntot res ok tot cok ctot cpid skipped=0 best
set -f
trap cleanup EXIT
trap 'exit 130' INT TERM
config_load byetube
config_get PARALLEL main test_parallel 8
config_get CUR main byedpi_opts ""
case "$PARALLEL" in ''|*[!0-9]*) PARALLEL=8 ;; esac
[ "$PARALLEL" -ge 1 ] || PARALLEL=8
BIN=$(find_byedpi)
[ -n "$BIN" ] || { echo "ОШИБКА: ciadpi не найден"; exit 1; }
command -v curl >/dev/null 2>&1 || { echo "ОШИБКА: не установлен curl"; exit 1; }
mkdir -p "$TEST_DIR"
rm -f "$STOP"
cand="$TEST_DIR/candidates.txt"
keys="$TEST_DIR/keys.txt"
urls="$TEST_DIR/urls.txt"
: > "$cand"; : > "$keys"; : > "$RAW"
echo "==> Собираем стратегии для теста"
{ printf '%s\n' "$CUR"; cat "$(list_file strategies)"; } | tr -d '\r' | while IFS= read -r line; do
line=$(printf '%s' "$line" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
case "$line" in ''|'#'*) continue ;; esac
k=$(byedpi_opts_clean "$line")
[ -n "$k" ] || continue
grep -qxF -- "$k" "$keys" && continue
echo "$k" >> "$keys"
echo "$line" >> "$cand"
done
total=$(wc -l < "$cand" | tr -d ' ')
[ "$total" -gt 0 ] || { echo "ОШИБКА: нет стратегий для теста"; exit 1; }
echo "==> Собираем список доменов"
tr -d '\r' < "$(list_file test-domains)" | sed 's/#.*//; s/^[[:space:]]*//; s/[[:space:]]*$//' \
| grep -E '^[A-Za-z0-9._-]+$' | awk '!s[$0]++' | sed 's#.*#&|https://&/#' > "$urls"
ntot=$(wc -l < "$urls" | tr -d ' ')
[ "$ntot" -gt 0 ] || { echo "ОШИБКА: нет доменов для теста"; exit 1; }
echo "==> Стратегий: $total, доменов: $ntot, параллельно: $PARALLEL"
echo "==> Контрольный тест: без обхода"
res=$(check_all "$urls" "$TEST_DIR/log_control.txt" "")
if [ -f "$STOP" ]; then
echo "==> Тест остановлен на контрольном замере, результатов нет"
rm -f "$STOP"
return 0
fi
cok=${res% *}; ctot=${res#* }
echo "==> Результат: $cok/$ctot"
idx=0
while IFS= read -r line; do
[ -f "$STOP" ] && break
idx=$((idx + 1))
opts=$(byedpi_opts_clean "$line")
port=$((PORT_BASE + idx))
echo "==> [$idx/$total] $line"
"$BIN" -i 127.0.0.1 -p "$port" $opts >/dev/null 2>&1 </dev/null &
cpid=$!
echo "$cpid" > "$CPID"
sleep 1
if ! kill -0 "$cpid" 2>/dev/null; then
echo " пропуск: ciadpi не запустился с этими параметрами"
skipped=$((skipped + 1))
continue
fi
res=$(check_all "$urls" "$TEST_DIR/log_$idx.txt" "127.0.0.1:$port")
kill "$cpid" 2>/dev/null
wait "$cpid" 2>/dev/null
rm -f "$CPID"
if [ -f "$STOP" ]; then
echo " прервано — эта стратегия в результаты не входит"
break
fi
ok=${res% *}; tot=${res#* }
ok=${ok:-0}
echo " результат: $ok/$tot"
printf '%06d\t%06d\t%s\t%s\t%s\n' "$((999999 - ok))" "$idx" "$ok" "$tot" "$line" >> "$RAW"
done < "$cand"
if [ -f "$STOP" ]; then
echo "==> Тест остановлен пользователем, показываю то, что успели проверить"
rm -f "$STOP"
else
echo "==> Тест завершён"
fi
[ "$skipped" -gt 0 ] && echo "==> Пропущено стратегий (не запустились): $skipped"
{
echo "Контрольный тест (без обхода) → $cok/$ctot"
sort "$RAW" | while IFS="$TAB" read -r _ _ ok tot line; do
echo "$line → $ok/$tot"
done
} > "$RES"
echo "==> Результаты"
cat "$RES"
best=$(sed -n '2p' "$RES")
[ -n "$best" ] && echo "==> Лучшая стратегия: $best"
echo "==> Основной сервис и его настройки не менялись. Применить стратегию можно кнопкой «Применить» в результатах."
return 0
}
case "$1" in
start) cmd_start ;;
stop) cmd_stop ;;
status) cmd_status ;;
log) cmd_log ;;
results) cmd_results ;;
clear) cmd_clear ;;
run) cmd_run ;;
*) echo "usage: byetube test start|stop|status|log|results|clear" >&2; exit 1 ;;
esac
exit 0
BT_FILE_END_7f3a9c
chmod 755 "$R/opt/ByeTube/lib/test.sh"
mkdir -p "$R/opt/ByeTube"
cat > "$R/opt/ByeTube/uninstall.sh" <<'BT_FILE_END_7f3a9c'
#!/bin/sh
R="${BT_ROOT:-}"
MODE="$1"
CURRENT_PATHS="/opt/ByeTube /tmp/ByeTube /etc/init.d/byetube /etc/config/byetube /etc/hotplug.d/firewall/90-byetube /usr/share/nftables.d/chain-pre/forward/50-byetube.nft /usr/share/luci/menu.d/luci-app-byetube.json /usr/share/rpcd/acl.d/luci-app-byetube.json /www/luci-static/resources/view/byetube /www/luci-static/resources/byetube /lib/upgrade/keep.d/byetube"
LEGACY_PATHS="/etc/init.d/ytbypass /etc/config/ytbypass /etc/hotplug.d/firewall/90-ytbypass /usr/bin/ytbypass /usr/libexec/ytbypass /usr/share/ytbypass /usr/share/nftables.d/chain-pre/forward/50-ytbypass.nft /usr/share/luci/menu.d/luci-app-ytbypass.json /usr/share/rpcd/acl.d/luci-app-ytbypass.json /www/luci-static/resources/view/ytbypass /www/luci-static/resources/ytbypass /lib/upgrade/keep.d/ytbypass /etc/ytbypass /var/etc/ytbypass /var/run/ytbypass.started /tmp/ytbypass-test"
live() { [ -z "$R" ]; }
confdir() {
local d
d=$(sed -n 's/^conf-dir=\([^,]*\).*/\1/p' "$R"/var/etc/dnsmasq.conf.* 2>/dev/null | head -n 1)
[ -n "$d" ] || d=/tmp/dnsmasq.d
echo "$d"
}
drop_ip_rules() {
live || return 0
while ip rule del pref 8900 2>/dev/null; do :; done
while ip -6 rule del pref 8900 2>/dev/null; do :; done
}
drop_dev() {
live || return 0
ip route del default dev "$1" table 89 2>/dev/null
ip -6 route del default dev "$1" table 89 2>/dev/null
nft delete table inet "$2" 2>/dev/null
}
stop_service() {
live || return 0
[ -x "/etc/init.d/$1" ] || return 0
"/etc/init.d/$1" stop >/dev/null 2>&1
"/etc/init.d/$1" disable >/dev/null 2>&1
}
remove_dnsmasq_conf() {
local d f changed=0
d="$R$(confdir)"
for f in "$@"; do
if [ -f "$d/$f" ]; then
rm -f "$d/$f"
changed=1
fi
done
[ "$changed" = "1" ]
}
remove_paths() {
local p
for p in "$@"; do
rm -rf "$R$p"
done
}
dns_changed=0
if [ "$MODE" != "--legacy" ]; then
stop_service byetube
drop_ip_rules
drop_dev byetube0 byetube
remove_dnsmasq_conf byetube.conf && dns_changed=1
fi
stop_service ytbypass
if [ "$MODE" != "--legacy" ] || [ ! -f "$R/tmp/ByeTube/started" ]; then
drop_ip_rules
fi
drop_dev ytb0 ytbypass
remove_dnsmasq_conf ytbypass.conf && dns_changed=1
if [ "$MODE" = "--legacy" ]; then
remove_paths $LEGACY_PATHS
else
remove_paths $CURRENT_PATHS $LEGACY_PATHS
fi
if live; then
[ "$dns_changed" = "1" ] && /etc/init.d/dnsmasq restart >/dev/null 2>&1
/etc/init.d/firewall reload >/dev/null 2>&1
rm -rf /tmp/luci-indexcache* /tmp/luci-modulecache
/etc/init.d/rpcd reload >/dev/null 2>&1
fi
exit 0
BT_FILE_END_7f3a9c
chmod 755 "$R/opt/ByeTube/uninstall.sh"
mkdir -p "$R/usr/share/luci/menu.d"
cat > "$R/usr/share/luci/menu.d/luci-app-byetube.json" <<'BT_FILE_END_7f3a9c'
{
"admin/services/byetube": {
"title": "ByeTube",
"order": 58,
"action": {
"type": "view",
"path": "byetube/main"
},
"depends": {
"acl": [ "luci-app-byetube" ],
"uci": { "byetube": true }
}
}
}
BT_FILE_END_7f3a9c
chmod 644 "$R/usr/share/luci/menu.d/luci-app-byetube.json"
mkdir -p "$R/usr/share/nftables.d/chain-pre/forward"
cat > "$R/usr/share/nftables.d/chain-pre/forward/50-byetube.nft" <<'BT_FILE_END_7f3a9c'
oifname "byetube0" accept comment "byetube"
BT_FILE_END_7f3a9c
chmod 644 "$R/usr/share/nftables.d/chain-pre/forward/50-byetube.nft"
mkdir -p "$R/usr/share/rpcd/acl.d"
cat > "$R/usr/share/rpcd/acl.d/luci-app-byetube.json" <<'BT_FILE_END_7f3a9c'
{
"luci-app-byetube": {
"description": "ByeTube",
"read": {
"file": {
"/opt/ByeTube/bin/byetube": [ "exec" ],
"/etc/init.d/byetube": [ "exec" ]
},
"uci": [ "byetube" ]
},
"write": {
"uci": [ "byetube" ]
}
}
}
BT_FILE_END_7f3a9c
chmod 644 "$R/usr/share/rpcd/acl.d/luci-app-byetube.json"
mkdir -p "$R/www/luci-static/resources/byetube"
cat > "$R/www/luci-static/resources/byetube/common.js" <<'BT_FILE_END_7f3a9c'
'use strict';
'require baseclass';
'require fs';
var CLI = '/opt/ByeTube/bin/byetube';
function callJson(args) {
return fs.exec(CLI, args).then(function(r) {
var out = (r.stdout || '').trim();
try { return JSON.parse(out); }
catch (e) { return { error: out || r.stderr || 'нет ответа' }; }
}).catch(function(e) { return { error: e.message }; });
}
function callText(args) {
return fs.exec(CLI, args).then(function(r) { return r.stdout || ''; })
.catch(function() { return ''; });
}
function detectMissingThemeVar() {
if (document.documentElement.hasAttribute('data-zm-theme-checked')) return;
document.documentElement.setAttribute('data-zm-theme-checked', '1');
try {
var declared = getComputedStyle(document.documentElement).getPropertyValue('--background-color-medium').trim();
if (declared) return;
var el = document.body, bg = '', hops = 0;
while (el && hops < 6) {
var c = getComputedStyle(el).backgroundColor;
if (c && c !== 'rgba(0, 0, 0, 0)' && c !== 'transparent') { bg = c; break; }
el = el.parentElement;
hops++;
}
var m = bg.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
if (!m) return;
var luminance = (0.299 * (+m[1]) + 0.587 * (+m[2]) + 0.114 * (+m[3])) / 255;
if (luminance < 0.5) document.documentElement.classList.add('zm-theme-dark');
} catch (e) {}
}
function injectCss() {
detectMissingThemeVar();
if (document.getElementById('bt-css')) return;
var l = document.createElement('link');
l.id = 'bt-css';
l.rel = 'stylesheet';
l.href = L.resource('view/byetube/style.css');
document.head.appendChild(l);
}
function badge(ok, textOk, textBad) {
var cls = ok ? 'zm-ok' : 'zm-bad';
return E('span', { 'class': 'zm-badge ' + cls }, [
E('span', { 'class': 'zm-dot' }),
ok ? textOk : textBad
]);
}
function toastContainer() {
var c = document.getElementById('zm-toast-container');
if (!c) {
c = document.createElement('div');
c.id = 'zm-toast-container';
document.body.appendChild(c);
}
return c;
}
function toast(message, kind, duration) {
var container = toastContainer();
var el = E('div', { 'class': 'zm-toast zm-toast-' + (kind || 'info') }, [
E('span', { 'class': 'zm-toast-icon' }, kind === 'error' ? '✕' : (kind === 'warning' ? '!' : '✓')),
E('span', { 'class': 'zm-toast-text' }, [ message ])
]);
container.appendChild(el);
requestAnimationFrame(function() { el.classList.add('zm-toast-show'); });
var hide = function() {
el.classList.remove('zm-toast-show');
setTimeout(function() { el.parentNode && el.parentNode.removeChild(el); }, 250);
};
el.addEventListener('click', hide);
setTimeout(hide, duration || (kind === 'error' ? 14000 : 9000));
}
function span(cls, text) {
var s = document.createElement('span');
s.className = cls;
s.textContent = text;
return s;
}
function scoreClass(ok, total, controlOk) {
if (ok === total) return 'bt-c-ok';
if (controlOk !== null && controlOk !== undefined) return ok > controlOk ? 'bt-c-warn' : 'bt-c-bad';
return ok > 0 ? 'bt-c-warn' : 'bt-c-bad';
}
function chipClass(ok, total, controlOk) {
return scoreClass(ok, total, controlOk).replace('bt-c-', 'bt-chip bt-chip-');
}
function logLine(line, ctx, nameOf) {
var div = document.createElement('div');
var m, p;
if ((m = line.match(/^==> \[(\d+)\/(\d+)\] (.*)$/))) {
div.appendChild(span('zm-log-arrow', '==> '));
div.appendChild(span('bt-c-key', '[' + m[1] + '/' + m[2] + '] '));
div.appendChild(span('bt-c-white', m[3]));
p = nameOf ? nameOf(m[3]) : null;
if (p) div.appendChild(span('bt-c-dim', ' ← ' + p));
}
else if ((m = line.match(/^==> Результат: (\d+)\/(\d+)$/))) {
ctx.ctrl = +m[1];
div.appendChild(span('zm-log-arrow', '==> '));
div.appendChild(span('bt-c-dim', 'Результат контрольного замера: '));
div.appendChild(span('bt-c-white', m[1] + '/' + m[2]));
}
else if ((m = line.match(/^(\s+)результат: (\d+)\/(\d+)$/))) {
div.appendChild(span('bt-c-dim', m[1] + 'результат: '));
div.appendChild(span(scoreClass(+m[2], +m[3], ctx.ctrl), m[2] + '/' + m[3]));
}
else if (/^\s+(пропуск|прервано)/.test(line)) {
div.className = 'zm-log-msg-warn';
div.textContent = line;
}
else if ((m = line.match(/^==> Лучшая стратегия: (.*?)\s*→\s*(\d+)\/(\d+)\s*$/))) {
div.appendChild(span('zm-log-arrow', '==> '));
div.appendChild(span('zm-log-msg-ok', 'Лучшая стратегия: '));
div.appendChild(span('bt-c-white', m[1] + ' '));
div.appendChild(span('bt-c-dim', '→ '));
div.appendChild(span(scoreClass(+m[2], +m[3], ctx.ctrl), m[2] + '/' + m[3]));
}
else if (/^==> (Тест завершён|Готово)/.test(line)) {
div.appendChild(span('zm-log-arrow', '==> '));
div.appendChild(span('zm-log-msg-ok', line.slice(4)));
}
else if (/^==> (Тест остановлен|Пропущено)/.test(line)) {
div.appendChild(span('zm-log-arrow', '==> '));
div.appendChild(span('zm-log-msg-warn', line.slice(4)));
}
else if (/^==> Результаты$/.test(line)) {
div.appendChild(span('zm-log-arrow', '==> '));
div.appendChild(span('zm-log-msg-info', 'Результаты'));
}
else if ((m = line.match(/^(.*?)\s*→\s*(\d+)\/(\d+)\s*$/))) {
if (/^Контрольный тест/.test(m[1])) {
ctx.ctrl = +m[2];
div.appendChild(span('bt-c-dim', m[1] + ' → '));
div.appendChild(span('bt-c-white', m[2] + '/' + m[3]));
} else {
div.appendChild(span('bt-c-white', m[1] + ' '));
div.appendChild(span('bt-c-dim', '→ '));
div.appendChild(span(scoreClass(+m[2], +m[3], ctx.ctrl), m[2] + '/' + m[3]));
}
}
else if (/^ОШИБКА/.test(line)) {
div.className = 'zm-log-msg-error';
div.textContent = line;
}
else if (/^==> /.test(line)) {
div.appendChild(span('zm-log-arrow', '==> '));
div.appendChild(span('zm-log-msg-info', line.slice(4)));
}
else {
div.className = 'zm-log-code';
div.textContent = line;
}
return div;
}
function renderTestLog(logEl, text, nameOf) {
var ctx = { ctrl: null };
while (logEl.firstChild) logEl.removeChild(logEl.firstChild);
(text || '').split('\n').forEach(function(l) {
if (l !== '') logEl.appendChild(logLine(l, ctx, nameOf));
});
logEl.scrollTop = logEl.scrollHeight;
}
function parseResults(text) {
var rows = [], control = null;
(text || '').split('\n').forEach(function(line) {
var m = line.match(/^(.*?)\s*→\s*(\d+)\/(\d+)\s*$/);
if (!m) return;
if (/^Контрольный тест/.test(m[1]))
control = { ok: +m[2], total: +m[3] };
else
rows.push({ opts: m[1], ok: +m[2], total: +m[3] });
});
rows.forEach(function(r, i) { r.i = i; });
rows.sort(function(a, b) { return (b.ok - a.ok) || (a.i - b.i); });
return { control: control, rows: rows };
}
return baseclass.extend({
injectCss: injectCss,
toast: toast,
badge: badge,
span: span,
chipClass: chipClass,
scoreClass: scoreClass,
renderTestLog: renderTestLog,
parseResults: parseResults,
callJson: callJson,
callText: callText,
status: function() { return callJson([ 'status' ]); },
config: function() { return callJson([ 'config', 'get' ]); },
configSet: function(pairs) {
var a = [ 'config', 'set' ];
Object.keys(pairs).forEach(function(k) { a.push(k, String(pairs[k])); });
return callJson(a);
},
service: function(action) { return callJson([ 'service', action ]); },
flush: function() { return callJson([ 'flush' ]); },
setStrategy: function(opts) { return callJson([ 'set-strategy', opts ]); },
listGet: function(kind) { return callText([ 'list', 'get', kind ]); },
listSet: function(kind, text) { return callJson([ 'list', 'set', kind, text ]); },
listReset: function(kind) { return callJson([ 'list', 'reset', kind ]); },
testStatus: function() { return callJson([ 'test', 'status' ]); },
testStart: function() { return callJson([ 'test', 'start' ]); },
testStop: function() { return callJson([ 'test', 'stop' ]); },
testClear: function() { return callJson([ 'test', 'clear' ]); },
testLog: function() { return callText([ 'test', 'log' ]); },
testResults: function() { return callText([ 'test', 'results' ]); }
});
BT_FILE_END_7f3a9c
chmod 644 "$R/www/luci-static/resources/byetube/common.js"
mkdir -p "$R/www/luci-static/resources/byetube"
cat > "$R/www/luci-static/resources/byetube/presets.js" <<'BT_FILE_END_7f3a9c'
'use strict';
'require baseclass';
var LIST = [
{
id: 'p1',
label: '1 · Каскад (по умолчанию)',
name: '1 · Каскад disorder/split + tlsrec + md5sig, авто-режим -As (по умолчанию)',
opts: '-d1 -d3+s -s6+s -d9+s -s12+s -d15+s -s20+s -d25+s -s30+s -d35+s -r1+s -S -a1 -As -d1 -d3+s -s6+s -d9+s -s12+s -d15+s -s20+s -d25+s -s30+s -d35+s -S -a1'
},
{
id: 'p2',
label: '2 · OOB + tlsrec',
name: '2 · Короткая: OOB + tlsrec у SNI',
opts: '-o1 -a1 -r-5+se'
},
{
id: 'p3',
label: '3 · Fake SNI + OOB',
name: '3 · Fake SNI google.com + disorder/OOB (TTL 4)',
opts: '-n "google.com" -Qr -d5+sm -f3+sm -o2 -t4 -a1'
},
{
id: 'p4',
label: '4 · Каскад без tlsrec',
name: '4 · Каскад disorder/split без tlsrec и авто-режима',
opts: '-d1 -s1+s -d3+s -s6+s -d9+s -s12+s -d15+s -s20+s -d25+s -s30+s -d35+s -a1'
},
{
id: 'p5',
label: '5 · Fake + disoob',
name: '5 · Fake + disoob + tlsrec (TTL 5 и 15)',
opts: '-f1 -t5 -n "google.com" -q3+h -Qr -f2 -q1 -r1+s -t15 -q1 -o2 -a1'
},
{
id: 'p6',
label: '6 · OOB + авто-режим',
name: '6 · OOB + tlsrec + авто-режим -At,r,s, fake google.com',
opts: '-o1 -r-5+se -a1 -At,r,s -d1 -n "google.com" -Qr -f-1 -a1'
}
];
function clean(s) {
return String(s == null ? '' : s).replace(/["']/g, '').replace(/\s+/g, ' ').trim();
}
function norm(s) {
return String(s == null ? '' : s).replace(/\s+/g, ' ').trim();
}
function find(opts) {
var c = clean(opts);
if (!c) return null;
for (var i = 0; i < LIST.length; i++)
if (clean(LIST[i].opts) === c) return LIST[i];
return null;
}
function byId(id) {
for (var i = 0; i < LIST.length; i++)
if (LIST[i].id === id) return LIST[i];
return null;
}
return baseclass.extend({
list: LIST,
DEFAULT: LIST[0].opts,
clean: clean,
norm: norm,
find: find,
byId: byId
});
BT_FILE_END_7f3a9c
chmod 644 "$R/www/luci-static/resources/byetube/presets.js"
mkdir -p "$R/www/luci-static/resources/view/byetube"
cat > "$R/www/luci-static/resources/view/byetube/main.js" <<'BT_FILE_END_7f3a9c'
'use strict';
'require view';
'require poll';
'require byetube.common as bt';
'require byetube.presets as presets';
var TABS = [
{ id: 'main', label: 'ByeTube' },
{ id: 'strategy', label: 'Стратегия' },
{ id: 'test', label: 'Тест стратегий' },
{ id: 'domains', label: 'Домены' }
];
function fill(node, kids) {
while (node.firstChild) node.removeChild(node.firstChild);
kids.forEach(function(k) {
if (k === null || k === undefined || k === '') return;
node.appendChild(typeof k === 'string' ? document.createTextNode(k) : k);
});
return node;
}
function row(label, node) {
return E('div', { 'class': 'zm-row' }, [ E('span', { 'class': 'zm-label' }, [ label ]), node ]);
}
function tile(label, cls, onclick, title) {
var attrs = { 'class': 'zm-tile' + (cls ? ' ' + cls : ''), 'click': onclick };
if (title) attrs.title = title;
return E('div', attrs, [ label ]);
}
function btn(label, cls, onclick) {
return E('button', { 'class': 'cbi-button' + (cls ? ' ' + cls : ''), 'click': onclick }, [ label ]);
}
function hint(text) {
return E('p', { 'class': 'zm-hint' }, [ text ]);
}
return view.extend({
load: function() {
bt.injectCss();
return Promise.all([
bt.status(),
bt.config(),
bt.testStatus(),
bt.listGet('youtube'),
bt.listGet('extra'),
bt.listGet('strategies'),
bt.listGet('test-domains')
]);
},
render: function(all) {
var st = all[0] || {};
var cfg = all[1] || {};
var tst = all[2] || {};
var texts = { 'youtube': all[3] || '', 'extra': all[4] || '', 'strategies': all[5] || '', 'test-domains': all[6] || '' };
var busy = false;
var activeTab = 'main';
var running = tst.running === true;
var wasRunning = running;
var editors = {};
var wrap = E('div', { 'class': 'zm-wrap' });
var tabBar = E('div', { 'class': 'zm-actions', 'style': 'margin-bottom:14px' });
var panels = {};
TABS.forEach(function(t) {
panels[t.id] = E('div', { 'style': t.id === activeTab ? '' : 'display:none' });
});
function renderTabBar() {
fill(tabBar, TABS.map(function(t) {
return E('button', {
'class': 'cbi-button' + (t.id === activeTab ? ' cbi-button-positive' : ''),
'click': function() {
activeTab = t.id;
TABS.forEach(function(t2) { panels[t2.id].style.display = t2.id === activeTab ? '' : 'none'; });
renderTabBar();
}
}, [ t.label ]);
}));
}
function guard() {
if (busy) {
bt.toast('Дождитесь завершения текущей операции', 'warning');
return true;
}
return false;
}
function isWorking() {
return !!(st.enabled && st.byedpi && st.hev && st.tun && st.nft && st.rule && st.route && st.dns && st.fw);
}
var portDirty = false;
var portInput = E('input', {
'class': 'cbi-input-text bt-input',
'type': 'text',
'value': String(cfg.byedpi_port || 1088),
'inputmode': 'numeric'
});
portInput.addEventListener('input', function() { portDirty = true; });
var statusCard = E('div', { 'class': 'zm-card' });
var optionsCard = E('div', { 'class': 'zm-card' });
var bannerEl = E('div', { 'class': 'zm-current-banner' });
var presetsCard = E('div', { 'class': 'zm-card' });
var domainsToggleCard = E('div', { 'class': 'zm-card' });
function refreshState() {
return Promise.all([ bt.status(), bt.config() ]).then(function(r) {
if (r[0] && !r[0].error) st = r[0];
if (r[1] && !r[1].error) cfg = r[1];
renderMain();
renderStrategy();
renderDomainsToggle();
renderResults(resultsText);
syncEditors();
});
}
function applyConfig(pairs, okMsg) {
if (guard()) return;
busy = true;
bt.toast('Применяем настройки', 'warning');
bt.configSet(pairs).then(function(res) {
if (res.error) {
busy = false;
bt.toast(res.error, 'error');
return null;
}
return bt.service('restart').then(function() {
busy = false;
bt.toast(okMsg || 'Настройки применены', 'info');
return refreshState();
});
});
}
function serviceAction(action, okMsg) {
if (guard()) return;
busy = true;
bt.toast('Выполняем', 'warning');
bt.service(action).then(function(res) {
busy = false;
if (res.error) { bt.toast(res.error, 'error'); return; }
bt.toast(okMsg, 'info');
refreshState();
});
}
function renderMain() {
var kids = [ E('h3', {}, [ 'Состояние' ]) ];
kids.push(row('Служба', isWorking()
? bt.badge(true, 'работает', '')
: bt.badge(false, '', st.enabled ? 'запущена не полностью' : 'выключена')));
if (st.binaries === false)
kids.push(row('Пакеты', bt.badge(false, '', 'не найден byedpi или hev-socks5-tunnel')));
kids.push(row('ByeDPI (ciadpi)', bt.badge(st.byedpi, 'запущен', 'не запущен')));
kids.push(row('hev-socks5-tunnel', bt.badge(st.hev, 'запущен', 'не запущен')));
kids.push(row('Интерфейс byetube0', bt.badge(st.tun, 'поднят', 'нет')));
kids.push(row('Маркировка nftables', bt.badge(st.nft, 'активна', 'нет')));
kids.push(row('Policy routing', bt.badge(st.rule && st.route, 'активен', 'нет')));
kids.push(row('dnsmasq → nftset', st.dnsmasq_nftset === false
? bt.badge(false, '', 'нужен dnsmasq-full')
: bt.badge(st.dns, 'домены загружены', 'нет')));
kids.push(row('Firewall forward', bt.badge(st.fw, 'разрешён', 'нет')));
kids.push(row('IP в наборах', E('span', {}, [ 'IPv4: ' + (st.ips4 || 0) + (st.ipv6 ? ', IPv6: ' + (st.ips6 || 0) : '') ])));
kids.push(row('Версия', E('span', {}, [ 'v' + (st.version || '?') ])));
kids.push(E('div', { 'class': 'zm-actions' }, [
st.enabled
? btn('Выключить', 'cbi-button-remove', function() {
applyConfig({ enabled: 0 }, 'ByeTube выключен');
})
: btn('Включить', 'cbi-button-positive', function() {
applyConfig({ enabled: 1 }, 'ByeTube включён');
}),
btn('Перезапустить', 'cbi-button-apply', function() { serviceAction('restart', 'Служба перезапущена'); }),
btn('Очистить наборы IP', '', function() {
if (guard()) return;
bt.flush().then(function() {
bt.toast('Наборы очищены. Перезапустите браузер или обновите DNS-кэш на клиентах.', 'info');
refreshState();
});
})
]));
fill(statusCard, kids);
var quic = cfg.quic || 'block';
if (!portDirty) portInput.value = String(cfg.byedpi_port || 1088);
var ipv6Row;
if (cfg.ipv6_supported === false)
ipv6Row = row('IPv6', bt.badge(false, '', 'нет IPv6 на роутере'));
else
ipv6Row = row('IPv6', E('div', { 'class': 'zm-grid' }, [
tile(cfg.ipv6 ? 'IPv6 включён' : 'IPv6 выключен', cfg.ipv6 ? 'zm-active' : 'zm-tile-off', function() {
applyConfig({ ipv6: cfg.ipv6 ? 0 : 1 }, cfg.ipv6 ? 'IPv6 выключен' : 'IPv6 включён');
})
]));
fill(optionsCard, [
E('h3', {}, [ 'Настройки' ]),
row('QUIC (UDP/443)', E('div', { 'class': 'zm-grid' }, [
tile('Блокировать', quic === 'block' ? 'zm-active' : '', function() {
if (quic !== 'block') applyConfig({ quic: 'block' }, 'QUIC блокируется');
}),
tile('Через прокси', quic === 'proxy' ? 'zm-active' : '', function() {
if (quic !== 'proxy') applyConfig({ quic: 'proxy' }, 'QUIC идёт через прокси');
})
])),
hint('Блокировка QUIC заставляет клиента быстро откатиться на TCP, который обрабатывает ByeDPI (рекомендуется).'),
ipv6Row,
row('Порт SOCKS5 ByeDPI', E('div', { 'class': 'zm-actions', 'style': 'margin:0' }, [
portInput,
btn('Сохранить', 'cbi-button-positive', function() {
var v = String(portInput.value).trim();
if (!/^\d+$/.test(v) || +v < 1 || +v > 65535) {
bt.toast('Порт должен быть числом от 1 до 65535', 'error');
return;
}
portDirty = false;
applyConfig({ byedpi_port: v }, 'Порт сохранён');
})
])),
hint('Локальный порт 127.0.0.1, отдельный экземпляр — штатная служба byedpi не затрагивается.')
]);
}
function currentPreset() {
return presets.find(cfg.byedpi_opts);
}
function setStrategy(opts, label) {
if (guard()) return;
busy = true;
bt.toast('Применяем стратегию', 'warning');
bt.setStrategy(opts).then(function(res) {
busy = false;
if (res.error) { bt.toast(res.error, 'error'); return; }
bt.toast((label || 'Стратегия') + ' применена', 'info');
refreshState();
});
}
var customTa = E('textarea', {
'class': 'zm-config-editor',
'spellcheck': 'false',
'wrap': 'soft',
'style': 'min-height:130px;white-space:pre-wrap'
}, [ cfg.byedpi_opts || '' ]);
function renderStrategy() {
var cur = currentPreset();
fill(bannerEl, cfg.byedpi_opts
? [ E('span', {}, [ 'Сейчас применено: ' ]), E('b', {}, [ cur ? cur.name : 'своя стратегия' ]) ]
: [ E('span', {}, [ 'Стратегия ещё не выбрана' ]) ]);
bannerEl.className = 'zm-current-banner' + (cfg.byedpi_opts ? '' : ' zm-current-empty');
fill(presetsCard, [
E('h3', {}, [ 'Готовые стратегии' ]),
E('div', { 'class': 'zm-grid' }, presets.list.map(function(p) {
return tile(p.label, cur && cur.id === p.id ? 'zm-active' : '', function() {
setStrategy(p.opts, p.label);
}, p.name + '\n' + p.opts);
})),
cfg.byedpi_opts ? E('div', { 'class': 'bt-cmd-line' }, [ cfg.byedpi_opts ]) : '',
hint('Стратегия зависит от провайдера — лучшую под вашего провайдера найдёт вкладка «Тест стратегий».')
]);
}
var customCard = E('div', { 'class': 'zm-card' }, [
E('h3', {}, [ 'Своя стратегия' ]),
hint('Параметры командной строки ciadpi одной строкой. Кавычки (-n "google.com") служба обрабатывает сама.'),
customTa,
E('div', { 'class': 'zm-actions' }, [
btn('Применить', 'cbi-button-positive', function() {
var v = presets.norm(customTa.value);
if (!v) { bt.toast('Стратегия не может быть пустой', 'error'); return; }
setStrategy(v, 'Своя стратегия');
}),
btn('Подставить текущую', '', function() { customTa.value = cfg.byedpi_opts || ''; })
])
]);
function makeEditor(o) {
var ta = E('textarea', {
'class': 'zm-config-editor',
'spellcheck': 'false',
'wrap': o.wrap ? 'soft' : 'off',
'style': 'min-height:' + o.height + 'px;' + (o.wrap ? 'white-space:pre-wrap' : '')
}, [ texts[o.kind] ]);
var meta = E('p', { 'class': 'zm-hint' });
var ed = { ta: ta, locked: false };
var saveBtn = btn('Сохранить', 'cbi-button-positive', onSave);
var resetBtn = o.resetLabel ? btn(o.resetLabel, '', onReset) : null;
ed.node = E('div', { 'class': 'zm-card' }, [
E('h3', {}, [ o.title ]),
hint(o.hint),
ta,
meta,
E('div', { 'class': 'zm-actions' }, resetBtn ? [ saveBtn, resetBtn ] : [ saveBtn ])
]);
ed.sync = function() {
var info = o.info();
fill(meta, [ o.meta(info) ]);
ta.disabled = ed.locked;
saveBtn.disabled = ed.locked;
if (resetBtn) resetBtn.disabled = ed.locked || !info.custom;
};
function reload() {
return bt.listGet(o.kind).then(function(t) {
texts[o.kind] = t;
ta.value = t;
});
}
function tail(res) {
if (!o.dns) return '';
return res.applied ? '. DNS обновлён' : '. Применится при включении службы';
}
function onSave() {
if (!ta.value.trim() && !o.allowEmpty) {
bt.toast('Список пуст. Чтобы вернуть встроенный, нажмите «' + (o.resetLabel || 'Восстановить') + '».', 'error');
return;
}
bt.listSet(o.kind, ta.value).then(function(res) {
if (res.error) { bt.toast(res.error, 'error'); return; }
o.apply(res);
reload().then(function() {
ed.sync();
bt.toast('Список сохранён' + tail(res), 'info');
});
});
}
function onReset() {
if (!confirm('Вернуть встроенный список? Ваши правки будут удалены.')) return;
bt.listReset(o.kind).then(function(res) {
if (res.error) { bt.toast(res.error, 'error'); return; }
o.apply(res);
reload().then(function() {
ed.sync();
bt.toast('Возвращён встроенный список' + tail(res), 'info');
});
});
}
return ed;
}
function syncEditors() {
Object.keys(editors).forEach(function(k) { editors[k].sync(); });
}
function listMeta(unit) {
return function(info) {
return (info.custom ? 'Свой список' : 'Встроенный список') + ' · ' + info.count + ' ' + unit;
};
}
editors['youtube'] = makeEditor({
kind: 'youtube', title: 'Список доменов YouTube', height: 300, wrap: false, dns: true,
hint: 'Для этих доменов трафик идёт через обход. Один домен в строке, поддомены подхватываются автоматически, можно вставлять ссылки. Вложенные записи (i.ytimg.com при наличии ytimg.com) отбрасываются сами.',
resetLabel: 'Восстановить встроенный',
info: function() { return { count: st.youtube_count || 0, custom: !!st.youtube_custom }; },
meta: listMeta('доменов'),
apply: function(res) { st.youtube_count = res.count; st.youtube_custom = res.custom; }
});
editors['extra'] = makeEditor({
kind: 'extra', title: 'Дополнительные домены', height: 130, wrap: false, dns: true, allowEmpty: true,
hint: 'Добавляются к списку выше. Один домен в строке; пустое поле — без дополнительных доменов.',
info: function() { return { count: st.extra_count || 0, custom: (st.extra_count || 0) > 0 }; },
meta: function(info) { return 'Дополнительных доменов: ' + info.count; },
apply: function(res) { st.extra_count = res.count; }
});
editors['strategies'] = makeEditor({
kind: 'strategies', title: 'Стратегии для теста', height: 260, wrap: true,
hint: 'Одна стратегия (параметры ciadpi) в строке. Строки, начинающиеся с #, — комментарии.',
resetLabel: 'Сбросить к встроенному',
info: function() { return { count: tst.strategies || 0, custom: !!tst.strategies_custom }; },
meta: listMeta('стратегий'),
apply: function(res) { tst.strategies = res.count; tst.strategies_custom = res.custom; }
});
editors['test-domains'] = makeEditor({
kind: 'test-domains', title: 'Домены для проверки', height: 220, wrap: false,
hint: 'Один домен в строке; можно вставлять ссылки. Нужны только для проверки доступности при тесте и на маршрутизацию не влияют.',
resetLabel: 'Сбросить к встроенному',
info: function() { return { count: tst.test_domains || 0, custom: !!tst.test_domains_custom }; },
meta: listMeta('доменов'),
apply: function(res) { tst.test_domains = res.count; tst.test_domains_custom = res.custom; }
});
function renderDomainsToggle() {
var on = cfg.default_domains !== false;
fill(domainsToggleCard, [
E('h3', {}, [ 'Домены YouTube' ]),
row('Использовать список', E('div', { 'class': 'zm-grid' }, [
tile(on ? 'Список используется' : 'Список выключен', on ? 'zm-active' : 'zm-tile-off', function() {
applyConfig({ default_domains: on ? 0 : 1 }, on ? 'Список выключен' : 'Список включён');
})
])),
hint('Сервисы Google часто делят IP-адреса, поэтому часть трафика Google (не только YouTube) тоже пойдёт через обход, а QUIC (UDP/443) к этим адресам будет блокироваться.')
]);
}
var testActions = E('div', { 'class': 'zm-actions' });
var testLog = E('pre', { 'class': 'zm-log' });
var testResults = E('div', { 'class': 'zm-card' });
function nameOf(opts) {
var p = presets.find(opts);
return p ? p.name : null;
}
function renderTestActions() {
var kids = [];
if (tst.curl === false) {
kids.push(bt.badge(false, '', 'не установлен curl'));
kids.push(hint('Нужен для теста: apk add curl (или opkg install curl).'));
} else if (running) {
kids.push(bt.badge(true, 'тест выполняется', ''));
kids.push(btn('Остановить тест', 'cbi-button-remove', doStop));
} else {
kids.push(btn('Запустить тест', 'cbi-button-positive', doStart));
if (tst.has_results) kids.push(btn('Очистить результаты', '', doClear));
}
fill(testActions, kids);
editors['strategies'].locked = running;
editors['test-domains'].locked = running;
syncEditors();
}
var resultsText = '';
function renderResults(text) {
resultsText = text || '';
var res = bt.parseResults(resultsText);
if (!res.rows.length && !res.control) {
fill(testResults, [ E('h3', {}, [ 'Результаты' ]), hint(running ? 'Тест выполняется…' : 'Пока нет результатов — запустите тест.') ]);
return;
}
var controlOk = res.control ? res.control.ok : null;
var curClean = presets.clean(cfg.byedpi_opts);
var legend = E('div', { 'class': 'bt-legend' }, []);
if (res.control) {
legend.appendChild(document.createTextNode('Контрольный замер (без обхода): '));
legend.appendChild(E('span', { 'class': 'bt-chip bt-chip-off' }, [ res.control.ok + '/' + res.control.total ]));
legend.appendChild(E('br'));
}
legend.appendChild(E('span', { 'class': 'bt-chip bt-chip-ok' }, [ 'все домены' ]));
legend.appendChild(E('span', { 'class': 'bt-chip bt-chip-warn' }, [ 'лучше контроля' ]));
legend.appendChild(E('span', { 'class': 'bt-chip bt-chip-bad' }, [ 'не лучше' ]));
legend.appendChild(document.createTextNode('При равенстве выше стоит стратегия, что раньше в списке.'));
var rows = res.rows.map(function(r, i) {
var p = presets.find(r.opts);
var isCur = presets.clean(r.opts) === curClean;
var body = [];
if (p || isCur) {
var nm = E('div', { 'class': 'bt-name' }, [ p ? p.name : '' ]);
if (isCur) nm.appendChild(bt.span('bt-c-cur', (p ? ' ' : '') + '(текущая)'));
body.push(nm);
}
body.push(E('div', { 'class': 'bt-cmd' }, [ r.opts ]));
return E('tr', {}, [
E('td', { 'class': 'bt-td-n' }, [ String(i + 1) ]),
E('td', { 'class': 'bt-td-s' }, [ E('span', { 'class': bt.chipClass(r.ok, r.total, controlOk) }, [ r.ok + '/' + r.total ]) ]),
E('td', { 'class': 'bt-td-c' }, body),
E('td', { 'class': 'bt-td-a' }, [
isCur ? '' : btn('Применить', 'cbi-button-apply', function() {
var q = 'Применить стратегию и перезапустить службу?\n\n' + (p ? p.name + '\n' : '') + r.opts;
if (confirm(q)) setStrategy(r.opts, p ? p.label : 'Стратегия');
})
])
]);
});
fill(testResults, [
E('h3', {}, [ 'Результаты' ]),
E('div', { 'class': 'zm-log zm-show bt-panel' }, [
legend,
E('table', { 'class': 'bt-table' }, [ E('tbody', {}, rows) ])
])
]);
}
function refreshResults() {
return bt.testResults().then(renderResults);
}
function refreshLog() {
return bt.testLog().then(function(t) {
if (t) testLog.classList.add('zm-show');
else testLog.classList.remove('zm-show');
bt.renderTestLog(testLog, t, nameOf);
});
}
function doStart() {
bt.testStart().then(function(res) {
if (res.error) { bt.toast(res.error, 'error'); return; }
running = true;
wasRunning = true;
tst.has_results = false;
renderResults('');
renderTestActions();
refreshLog();
bt.toast('Тест запущен', 'info');
});
}
function doStop() {
bt.testStop().then(function(res) {
if (res.error) { bt.toast(res.error, 'error'); return; }
bt.toast('Останавливаю тест…', 'warning');
});
}
function doClear() {
bt.testClear().then(function(res) {
if (res.error) { bt.toast(res.error, 'error'); return; }
tst.has_results = false;
renderTestActions();
renderResults('');
testLog.classList.remove('zm-show');
});
}
function tick() {
return bt.testStatus().then(function(s) {
if (!s || s.error) return;
tst = s;
running = s.running === true;
renderTestActions();
if (running || wasRunning) refreshLog();
if (wasRunning && !running) {
wasRunning = false;
refreshResults();
bt.toast('Тест завершён', 'info');
}
});
}
var testCard = E('div', { 'class': 'zm-card' }, [
E('h3', {}, [ 'Тест стратегий ByeDPI' ]),
hint('Каждая стратегия запускается во временном экземпляре ByeDPI на отдельном порту, а домены проверяются через него. Основной сервис и трафик клиентов не затрагиваются, настройки не меняются. Сначала делается контрольный замер без обхода. Тест идёт в фоне — вкладку можно закрыть.'),
testActions,
testLog
]);
panels['main'].appendChild(statusCard);
panels['main'].appendChild(optionsCard);
panels['strategy'].appendChild(bannerEl);
panels['strategy'].appendChild(presetsCard);
panels['strategy'].appendChild(customCard);
panels['test'].appendChild(testCard);
panels['test'].appendChild(testResults);
panels['test'].appendChild(editors['strategies'].node);
panels['test'].appendChild(editors['test-domains'].node);
panels['domains'].appendChild(domainsToggleCard);
panels['domains'].appendChild(editors['youtube'].node);
panels['domains'].appendChild(editors['extra'].node);
wrap.appendChild(E('div', { 'class': 'zm-header' }, [
E('h2', {}, [ 'ByeTube' ]),
E('span', { 'class': 'zm-header-by' }, [ 'by StressOzz' ])
]));
wrap.appendChild(tabBar);
TABS.forEach(function(t) { wrap.appendChild(panels[t.id]); });
renderTabBar();
renderMain();
renderStrategy();
renderDomainsToggle();
renderTestActions();
refreshLog();
if (running) renderResults('');
else refreshResults();
poll.add(function() {
return Promise.all([ refreshState(), tick() ]);
}, 5);
return wrap;
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});
BT_FILE_END_7f3a9c
chmod 644 "$R/www/luci-static/resources/view/byetube/main.js"
mkdir -p "$R/www/luci-static/resources/view/byetube"
cat > "$R/www/luci-static/resources/view/byetube/style.css" <<'BT_FILE_END_7f3a9c'
.zm-header { display: flex; align-items: baseline; gap: 10px; margin-bottom: -4px; flex-wrap: wrap; }
.zm-header h2 { margin: 0; font-size: 22px; font-weight: 700; }
.zm-header-by { font-size: 13px; opacity: .55; }
.zm-header-links { display: flex; gap: 8px; margin-left: auto; flex-wrap: wrap; }
.zm-header-links a {
font-size: 12.5px; font-weight: 600; text-decoration: none;
color: #229ed9; background: rgba(34,158,217,.1); border: 1px solid rgba(34,158,217,.25);
border-radius: 999px; padding: 5px 13px; transition: background .15s;
}
.zm-header-links a:hover { background: rgba(34,158,217,.18); }
.zm-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 14px; }
.zm-card {
background: var(--background-color-medium, #fff);
border: 1px solid rgba(0,0,0,.08);
border-radius: 12px;
padding: 18px 20px;
box-shadow: 0 1px 3px rgba(0,0,0,.05), 0 1px 2px rgba(0,0,0,.04);
overflow-wrap: break-word;
transition: box-shadow .15s;
}
html.zm-theme-dark .zm-card {
background: #1c2128;
border-color: rgba(255,255,255,.10);
box-shadow: 0 1px 3px rgba(0,0,0,.25), 0 1px 2px rgba(0,0,0,.2);
}
.zm-card:hover { box-shadow: 0 2px 8px rgba(0,0,0,.08); }
.zm-card h3 { margin: 0 0 12px 0; font-size: 15px; font-weight: 600; display: flex; align-items: center; gap: 8px; }
.zm-row { display: flex; align-items: center; gap: 12px; margin: 7px 0; font-size: 13px; flex-wrap: wrap; }
.zm-row .zm-label { opacity: .65; flex-shrink: 0; }
.zm-row > span:last-child { overflow-wrap: anywhere; }
.zm-badge { display: inline-flex; align-items: center; gap: 6px; padding: 3px 11px; border-radius: 999px; font-size: 12px; font-weight: 600; white-space: nowrap; }
.zm-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; flex-shrink: 0; }
.zm-ok { background: rgba(46,160,67,.12); color: #1a7f37; }
.zm-ok .zm-dot { background: #1a7f37; }
.zm-bad { background: rgba(207,34,46,.10); color: #cf222e; }
.zm-bad .zm-dot { background: #cf222e; }
.zm-warn { background: rgba(191,135,0,.12); color: #9a6700; }
.zm-warn .zm-dot { background: #9a6700; }
.zm-off { background: rgba(110,118,129,.12); color: #57606a; }
.zm-off .zm-dot { background: #57606a; }
.zm-actions { display: flex; gap: 12px; flex-wrap: wrap; align-items: center; margin: 14px 0; }
.zm-actions .cbi-button { margin: 0; }
.zm-grid { display: flex; flex-wrap: wrap; gap: 9px; }
.zm-grid-devices { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 9px; }
.zm-grid-devices .zm-tile { flex: none; min-width: 0; width: 100%; box-sizing: border-box; }
.zm-tile {
flex: 0 1 auto;
min-width: 90px;
max-width: 100%;
border: 1px solid rgba(0,0,0,.1);
border-radius: 9px;
padding: 10px 16px;
cursor: pointer;
text-align: center;
font-size: 13px;
line-height: 1.35;
overflow-wrap: break-word;
transition: border-color .15s, background .15s, transform .1s;
background: var(--background-color-low, #fafafa);
}
html.zm-theme-dark .zm-tile:not(.zm-active):not(.zm-tile-off) {
background: #22272e;
border-color: rgba(255,255,255,.12);
}
.zm-tile:hover { border-color: #1a7f37; transform: translateY(-1px); }
.zm-tile.zm-active {
border-color: #1a7f37; background: rgba(26,127,55,.16);
font-weight: 700; color: #15803d;
box-shadow: 0 0 0 2px rgba(26,127,55,.35);
}
.zm-tile.zm-active::before { content: "✓ "; }
.zm-tile.zm-tile-off {
border-color: rgba(207,34,46,.35); background: rgba(207,34,46,.08);
color: #cf222e; font-weight: 600;
}
.zm-tile.zm-tile-off::before { content: "✗ "; }
.zm-tile.zm-tile-off:hover { border-color: #cf222e; }
.zm-tile.zm-tile-pending { opacity: .55; border-style: dashed; cursor: not-allowed; }
.zm-tile.zm-tile-pending:hover { border-color: rgba(0,0,0,.1); transform: none; }
.zm-log {
background: #0d1117; color: #e6edf3;
font-family: ui-monospace, "SF Mono", "Cascadia Code", Consolas, "Liberation Mono", monospace;
font-size: 13.5px; line-height: 1.7;
border: 1px solid rgba(255,255,255,.10);
border-radius: 10px; padding: 16px 18px;
white-space: pre-wrap; word-break: break-word; overflow-wrap: anywhere;
max-height: 520px; min-height: 220px;
overflow-x: hidden; overflow-y: auto;
margin-top: 14px; display: none;
box-shadow: inset 0 0 0 1px rgba(0,0,0,.2);
}
.zm-log.zm-show { display: block; }
.zm-log:empty::before { content: "Ожидание вывода..."; opacity: .4; }
.zm-config-editor {
width: 100%; box-sizing: border-box; min-height: 420px;
background: #0d1117; color: #e6edf3;
font-family: ui-monospace, "SF Mono", "Cascadia Code", Consolas, "Liberation Mono", monospace;
font-size: 13px; line-height: 1.6;
border: 1px solid rgba(255,255,255,.1); border-radius: 10px;
padding: 14px 16px; margin: 10px 0;
white-space: pre; overflow: auto; resize: vertical;
}
html.zm-theme-dark .zm-config-editor { border-color: rgba(255,255,255,.14); }
.zm-log-arrow { color: #56d4dd; font-weight: 700; }
.zm-log-msg-info { color: #e3c04a; }
.zm-log-msg-ok { color: #3fb950; font-weight: 600; }
.zm-log-msg-error { color: #ff7b72; font-weight: 600; }
.zm-log-msg-warn { color: #ffa657; }
.zm-log-code { color: #8b949e; }
.zm-hint { font-size: 12px; opacity: .65; margin-top: 6px; line-height: 1.5; overflow-wrap: break-word; }
.zm-refresh-banner {
display: flex; align-items: center; justify-content: space-between; gap: 14px;
background: rgba(191,135,0,.12); border: 2px solid rgba(191,135,0,.35);
color: #9a6700; border-radius: 12px; padding: 16px 20px; font-size: 15px; font-weight: 500;
margin-top: 12px;
}
.zm-refresh-banner button { flex-shrink: 0; }
#zm-toast-container {
position: fixed; top: 20px; right: 20px; z-index: 10000;
display: flex; flex-direction: column; gap: 14px;
max-width: 520px;
}
.zm-toast {
display: flex; align-items: flex-start; gap: 14px;
background: #1c2128; color: #e6edf3;
padding: 22px 26px; border-radius: 14px;
box-shadow: 0 10px 40px rgba(0,0,0,.4);
font-size: 17px; line-height: 1.5; font-weight: 500; cursor: pointer;
opacity: 0; transform: translateX(24px);
transition: opacity .22s ease, transform .22s ease;
border-left: 6px solid #1a7f37;
}
.zm-toast-show { opacity: 1; transform: translateX(0); }
.zm-toast-error { border-left-color: #cf222e; }
.zm-toast-warning { border-left-color: #9a6700; }
.zm-toast-icon { flex-shrink: 0; font-weight: 700; font-size: 22px; line-height: 1.3; }
.zm-toast-info .zm-toast-icon { color: #3fb950; }
.zm-toast-error .zm-toast-icon { color: #ff7b72; }
.zm-toast-warning .zm-toast-icon { color: #e3b341; }
.zm-toast-text { overflow-wrap: anywhere; }
.zm-current-banner {
display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
background: rgba(26,127,55,.07); border: 1px solid rgba(26,127,55,.22);
border-radius: 10px; padding: 10px 16px; font-size: 13px; margin-bottom: 4px;
}
.zm-current-banner b { font-weight: 700; }
.zm-current-banner.zm-current-empty {
background: rgba(110,118,129,.08); border-color: rgba(110,118,129,.2);
}
.bt-panel { display: block; max-height: none; min-height: 0; margin-top: 10px; }
.bt-table { width: 100%; table-layout: fixed; border-collapse: collapse; border-spacing: 0; margin: 0; background: transparent; }
.bt-table td { padding: 8px 8px 8px 0; border: 0; border-top: 1px solid rgba(255,255,255,.10); vertical-align: top; background: transparent; color: inherit; white-space: normal; overflow-wrap: anywhere; }
.bt-table tr:first-child td { border-top: 0; }
.bt-td-n { width: 2.4em; color: #8b949e; }
.bt-td-s { width: 6.4em; }
.bt-td-a { width: 9em; text-align: right; padding-right: 0 !important; }
@media (max-width: 760px) {
.bt-table, .bt-table tbody { display: block; }
.bt-table tr { display: grid; grid-template-columns: auto 1fr auto; column-gap: 10px; align-items: center; padding: 9px 0; border-top: 1px solid rgba(255,255,255,.10); }
.bt-table tr:first-child { border-top: 0; }
.bt-table td { display: block; width: auto; border: 0 !important; padding: 0 !important; }
.bt-table td.bt-td-c { grid-column: 1 / -1; grid-row: 2; margin-top: 7px; }
}
.bt-cmd { color: #e6edf3; white-space: pre-wrap; word-break: break-word; overflow-wrap: anywhere; }
.bt-name { color: #8b949e; margin-bottom: 2px; }
.bt-chip { display: inline-block; min-width: 4.4em; text-align: center; padding: 1px 8px; border-radius: 5px; font-weight: 700; color: #0d1117; }
.bt-chip-ok { background: #3fb950; }
.bt-chip-warn { background: #ffa657; }
.bt-chip-bad { background: #ff7b72; }
.bt-chip-off { background: #8b949e; }
.bt-c-ok { color: #3fb950; font-weight: 700; }
.bt-c-warn { color: #ffa657; font-weight: 700; }
.bt-c-bad { color: #ff7b72; font-weight: 700; }
.bt-c-dim { color: #8b949e; }
.bt-c-white { color: #e6edf3; }
.bt-c-key { color: #e3c04a; font-weight: 700; }
.bt-c-cur { color: #56d4dd; font-weight: 700; }
.bt-legend { margin-bottom: 10px; color: #8b949e; }
.bt-legend .bt-chip { margin-right: 6px; }
.bt-input { width: 110px; box-sizing: border-box; }
.bt-cmd-line { font-family: ui-monospace, "SF Mono", "Cascadia Code", Consolas, "Liberation Mono", monospace; font-size: 12px; opacity: .75; overflow-wrap: anywhere; margin-top: 6px; }
.cbi-page-actions { display: none !important; }
BT_FILE_END_7f3a9c
chmod 644 "$R/www/luci-static/resources/view/byetube/style.css"
mkdir -p "$R$BT_DIR/custom"
}
migrate_legacy() {
local f moved=""
if [ ! -e "$R/etc/config/ytbypass" ] && [ ! -e "$R/etc/init.d/ytbypass" ] \
&& [ ! -d "$R/usr/libexec/ytbypass" ] && [ ! -e "$R/usr/bin/ytbypass" ]; then
return 0
fi
say "Найдена прежняя установка YouTube Bypass — переношу настройки в ByeTube"
LEGACY=1
if [ -f "$R/etc/config/ytbypass" ] && [ ! -f "$R/etc/config/byetube" ]; then
mkdir -p "$R/etc/config"
sed 's/^config ytbypass\([[:space:]]\)/config byetube\1/' "$R/etc/config/ytbypass" > "$R/etc/config/byetube"
moved="настройки"
fi
for f in strategies.txt test-domains.txt; do
if [ -s "$R/etc/ytbypass/$f" ] && [ ! -e "$R$BT_DIR/custom/$f" ]; then
mkdir -p "$R$BT_DIR/custom"
cp "$R/etc/ytbypass/$f" "$R$BT_DIR/custom/$f"
moved="$moved${moved:+, }$f"
fi
done
[ -n "$moved" ] && say "Перенесено: $moved"
return 0
}
finish_legacy() {
[ "$LEGACY" = 1 ] || return 0
say "Удаляю прежнюю установку ytbypass"
run_uninstall --legacy
}
update_default_strategy() {
local old1 old2 new cur
old1='--split 1 --disorder 3+s --mod-http=h,d --auto=torst --tlsrec 1+s'
old2='-o1 -r-5+se -a1 -At,r,s -d1 -n "google.com" -Qr -f-1 -a1'
new='-d1 -d3+s -s6+s -d9+s -s12+s -d15+s -s20+s -d25+s -s30+s -d35+s -r1+s -S -a1 -As -d1 -d3+s -s6+s -d9+s -s12+s -d15+s -s20+s -d25+s -s30+s -d35+s -S -a1'
cur=$(uci -q get byetube.main.byedpi_opts)
if [ "$cur" = "$old1" ] || [ "$cur" = "$old2" ]; then
uci set byetube.main.byedpi_opts="$new" && uci commit byetube
say "Стратегия по умолчанию обновлена"
fi
}
show_state() {
local ST IPS
ST=$("$BT_DIR/bin/byetube" status 2>/dev/null)
show() {
if [ "$(jsonfilter -s "$ST" -e "@.$1" 2>/dev/null)" = "true" ]; then
printf ' \033[32m[ok]\033[0m %s\n' "$2"
else
printf ' \033[31m[--]\033[0m %s\n' "$2"
fi
}
echo
say "Состояние:"
show byedpi "ByeDPI (ciadpi)"
show hev "hev-socks5-tunnel"
show tun "интерфейс byetube0"
show nft "правила nftables"
show route "policy routing"
show dns "dnsmasq -> nftset"
show fw "firewall forward"
IPS=$(jsonfilter -s "$ST" -e '@.ips4' 2>/dev/null)
echo " IP youtube.com в наборе после тестового резолва: ${IPS:-0}"
}
main() {
local p
parse_args "$@"
[ "$(id -u)" = "0" ] || die "нужны права root"
[ -f /etc/openwrt_release ] || die "это не OpenWrt"
. /etc/openwrt_release
ARCH="$DISTRIB_ARCH"
REL_MM=$(echo "$DISTRIB_RELEASE" | cut -d. -f1,2)
if command -v apk >/dev/null 2>&1; then
PM=apk; EXT=apk
elif command -v opkg >/dev/null 2>&1; then
PM=opkg; EXT=ipk
else
die "не найден ни apk, ни opkg"
fi
[ "$MODE" = install ] || do_uninstall
say "OpenWrt $DISTRIB_RELEASE, архитектура $ARCH, менеджер пакетов: $PM"
command -v fw4 >/dev/null 2>&1 || die "нужен firewall4 (OpenWrt 22.03+); hev-socks5-tunnel в пакетах — с 24.10"
command -v nft >/dev/null 2>&1 || die "не найден nft"
[ -d /www/luci-static/resources ] || warn "LuCI не найден — веб-интерфейс работать не будет (установите luci)"
if ip rule add pref 8999 fwmark 0x10000/0x10000 lookup 89 2>/dev/null; then
ip rule del pref 8999 2>/dev/null
else
die "ваш ip не поддерживает fwmark с маской. Установите ip-full: замените ip-tiny на ip-full и запустите установщик снова"
fi
say "Обновляю списки пакетов"
if [ "$PM" = apk ]; then apk update >/dev/null 2>&1 || warn "apk update не удался"
else opkg update >/dev/null 2>&1 || warn "opkg update не удался"; fi
pkg_has kmod-tun || { say "Ставлю kmod-tun"; pkg_add kmod-tun || die "kmod-tun не установлен"; }
ensure_dnsmasq_full
if ! pkg_has hev-socks5-tunnel; then
say "Ставлю hev-socks5-tunnel"
pkg_add hev-socks5-tunnel || die "hev-socks5-tunnel не найден в репозитории (есть в feeds OpenWrt 24.10+)"
NEW_HEV=1
fi
install_byedpi
for p in ca-bundle curl; do
pkg_has "$p" || pkg_add "$p" >/dev/null 2>&1 || warn "не удалось поставить $p — тест стратегий работать не будет (остальное — да)"
done
if [ "$NEW_BYEDPI" = 1 ] && [ -x /etc/init.d/byedpi ]; then
/etc/init.d/byedpi stop >/dev/null 2>&1; /etc/init.d/byedpi disable >/dev/null 2>&1
fi
if [ "$NEW_HEV" = 1 ] && [ -x /etc/init.d/hev-socks5-tunnel ]; then
/etc/init.d/hev-socks5-tunnel stop >/dev/null 2>&1; /etc/init.d/hev-socks5-tunnel disable >/dev/null 2>&1
fi
migrate_legacy
say "Устанавливаю ByeTube (сервис + LuCI) в $BT_DIR"
install_payload
finish_legacy
rm -rf /tmp/luci-indexcache* /tmp/luci-modulecache
/etc/init.d/rpcd reload >/dev/null 2>&1
update_default_strategy
/etc/init.d/byetube enable
if [ "$NOSTART" = 1 ]; then
say "Установлено. Запуск пропущен (--no-start): /etc/init.d/byetube start"
exit 0
fi
say "Запускаю"
/etc/init.d/byetube restart
sleep 4
nslookup youtube.com 127.0.0.1 >/dev/null 2>&1
sleep 1
show_state
cat <<MSG
Готово. Веб-интерфейс: LuCI -> Службы -> ByeTube.
Все файлы приложения — в $BT_DIR, временные — в $BT_TMP_DIR.
Проверка: откройте YouTube на устройстве в LAN (DNS — роутер), затем на роутере:
$BT_DIR/bin/byetube status состояние
$BT_DIR/bin/byetube ips IP, попавшие в наборы
$BT_DIR/bin/byetube diag диагностика клиента
logread -e byetube логи
Если клиент уже держал IP YouTube в DNS-кэше — перезапустите браузер / переподключите Wi-Fi.
Удаление: sh install.sh --uninstall
MSG
}
[ -n "${BT_LIB_ONLY:-}" ] || main "$@"