From 4ed6940c884db2e34b375c75ebbe6c114e416538 Mon Sep 17 00:00:00 2001 From: StressOzz <131419324+StressOzz@users.noreply.github.com> Date: Sun, 20 Sep 2026 20:11:12 +0500 Subject: [PATCH] Create ByeTube.sh --- ByeTube.sh | 3062 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 3062 insertions(+) create mode 100644 ByeTube.sh diff --git a/ByeTube.sh b/ByeTube.sh new file mode 100644 index 0000000..a7a952f --- /dev/null +++ b/ByeTube.sh @@ -0,0 +1,3062 @@ +#!/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 [секунд, по умолчанию 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 </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 </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 "$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 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 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 "$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 < Службы -> 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 "$@"