diff --git a/fe-app-tachyon/package.json b/fe-app-tachyon/package.json index c2d7c6c3..adc0e6ff 100644 --- a/fe-app-tachyon/package.json +++ b/fe-app-tachyon/package.json @@ -1,6 +1,6 @@ { "name": "fe-app-tachyon", - "version": "1.0.8", + "version": "1.0.9", "license": "MIT", "type": "module", "scripts": { diff --git a/tachyon/Makefile b/tachyon/Makefile index d4ac513a..3384b0a2 100644 --- a/tachyon/Makefile +++ b/tachyon/Makefile @@ -97,6 +97,9 @@ define Package/tachyon/install $(INSTALL_DIR) $(1)/usr/lib/tachyon $(CP) ./files/usr/lib/. $(1)/usr/lib/tachyon/ + $(INSTALL_DIR) $(1)/etc/hotplug.d/iface + $(INSTALL_BIN) ./files/etc/hotplug.d/iface/99-tachyon-wan-monitor $(1)/etc/hotplug.d/iface/99-tachyon-wan-monitor + sed -i -e 's/__COMPILED_VERSION_VARIABLE__/$(TACHYON_COMPILED_VERSION)/g' $(1)/usr/lib/tachyon/core/constants.uc endef diff --git a/tachyon/files/etc/hotplug.d/iface/99-tachyon-wan-monitor b/tachyon/files/etc/hotplug.d/iface/99-tachyon-wan-monitor new file mode 100644 index 00000000..9ed63439 --- /dev/null +++ b/tachyon/files/etc/hotplug.d/iface/99-tachyon-wan-monitor @@ -0,0 +1,14 @@ +#!/bin/sh +# Only process ifup and ifdown events for WAN interfaces +case "$INTERFACE" in + wan*|wwan*|lte*) + case "$ACTION" in + ifup) + /usr/bin/tachyon telegram send "🟢 Интерфейс *${INTERFACE}* поднялся!" + ;; + ifdown) + /usr/bin/tachyon telegram send "⚠️ Интерфейс *${INTERFACE}* упал!" + ;; + esac + ;; +esac diff --git a/tachyon/files/usr/lib/service/telegram.uc b/tachyon/files/usr/lib/service/telegram.uc index 6737f6d3..0ba87052 100644 --- a/tachyon/files/usr/lib/service/telegram.uc +++ b/tachyon/files/usr/lib/service/telegram.uc @@ -285,29 +285,180 @@ function run_speedtest(token, chat_id) { send_message_with_keyboard(token, chat_id, result_text, "Markdown"); } +function escape_html(text) { + text = replace(as_string(text), /&/g, "&"); + text = replace(text, //g, ">"); + return text; +} + +function manage_domain_list(action_type, domain, do_delete) { + let c = uci_core.cursor(); + if (!c) return { success: false, error: "Не удалось инициализировать UCI" }; + c.load(CONFIG_NAME); + + let target_section = null; + c.foreach(CONFIG_NAME, "section", function(s) { + if (s.enabled == "1" && s.action == action_type) { + target_section = s[".name"]; + return false; + } + }); + + if (!target_section) { + return { success: false, error: "Не найдено активное правило с действием '" + action_type + "'." }; + } + + let option_path = CONFIG_NAME + "." + target_section + ".domain"; + + if (do_delete) { + let deleted = uci_core.del_list(option_path, domain); + if (deleted) { + uci_core.commit(CONFIG_NAME); + command_status("/usr/bin/tachyon reload"); + return { success: true, message: "Домен `" + domain + "` успешно удален из правила." }; + } else { + return { success: false, error: "Домен `" + domain + "` не найден в правиле." }; + } + } else { + let current_val = uci_core.get(option_path); + let exists = false; + if (current_val != "") { + for (let item in split(current_val, " ")) { + if (item == domain) { + exists = true; + break; + } + } + } + if (exists) { + return { success: false, error: "Домен `" + domain + "` уже есть в правиле." }; + } + + let added = uci_core.add_list(option_path, domain); + if (added) { + uci_core.commit(CONFIG_NAME); + command_status("/usr/bin/tachyon reload"); + return { success: true, message: "Домен `" + domain + "` успешно добавлен в правило." }; + } + return { success: false, error: "Не удалось добавить домен в конфигурацию." }; + } +} + +function manage_mac_block(mac, do_unblock) { + let c = uci_core.cursor(); + if (!c) return { success: false, error: "Не удалось инициализировать UCI" }; + c.load("firewall"); + + mac = uc(as_string(mac)); + + if (do_unblock) { + let deleted = false; + c.foreach("firewall", "rule", function(r) { + if (uc(as_string(r.src_mac)) == mac && r.target == "REJECT") { + c.delete("firewall", r[".name"]); + deleted = true; + } + }); + + if (deleted) { + c.commit("firewall"); + command_status("/etc/init.d/firewall reload"); + return { success: true, message: "Блокировка с MAC-адреса `" + mac + "` успешно снята." }; + } + return { success: false, error: "Блокировка для MAC `" + mac + "` не найдена." }; + } else { + let exists = false; + c.foreach("firewall", "rule", function(r) { + if (uc(as_string(r.src_mac)) == mac && r.target == "REJECT") { + exists = true; + return false; + } + }); + + if (exists) { + return { success: false, error: "Устройство с MAC `" + mac + "` уже заблокировано." }; + } + + let section = c.add("firewall", "rule"); + c.set("firewall", section, "name", "Block_MAC_" + replace(mac, /:/g, "_")); + c.set("firewall", section, "src", "lan"); + c.set("firewall", section, "dest", "*"); + c.set("firewall", section, "src_mac", mac); + c.set("firewall", section, "target", "REJECT"); + c.commit("firewall"); + + command_status("/etc/init.d/firewall reload"); + return { success: true, message: "Устройство с MAC `" + mac + "` успешно заблокировано." }; + } +} + +function handle_backup(token, chat_id) { + send_message(token, chat_id, "⏳ Создание резервной копии настроек...", "HTML"); + + let backup_path = "/tmp/tachyon_backup.tar.gz"; + let tar_cmd = "tar -czf " + backup_path + " -C /etc config/ -C /usr/lib/tachyon/ providers/"; + let res = command_capture(tar_cmd); + + if (res.status != 0 || fs.stat(backup_path) == null) { + send_message(token, chat_id, "❌ Не удалось создать архив резервной копии.", "HTML"); + return; + } + + let url = "https://api.telegram.org/bot" + token + "/sendDocument"; + let args = [ + "curl", "-s", "-X", "POST", + "-F", "chat_id=" + as_string(chat_id), + "-F", "document=@" + backup_path + ]; + let proxy = get_proxy_args(); + for (let p in proxy) { + push(args, p); + } + push(args, url); + + let send_res = command_capture(command_from_args(args)); + fs.unlink(backup_path); + + if (send_res.status == 0) { + send_message(token, chat_id, "✅ Резервная копия успешно отправлена!", "HTML"); + } else { + send_message(token, chat_id, "❌ Ошибка при отправке файла резервной копии.", "HTML"); + } +} + function process_command(token, chat_id, text) { let cmd = trim(as_string(text)); if (cmd == "/start" || cmd == "/help") { - let help_text = "🤖 *Tachyon Bot* — Панель управления роутером\n\n" + - "Доступные команды:\n" + - "⚡ /status — Статус системы и RAM/CPU\n" + + let help_text = "🤖 Tachyon Bot — Панель управления роутером\n\n" + + "Доступные команды:\n" + + "📊 /status — Статус системы и RAM/CPU\n" + "📡 /servers — Список серверов и задержка\n" + "⚡ /test — Проверка интернет-соединения\n" + "🚀 /speed — Замер скорости интернета\n" + - "📊 /traffic — Статистика трафика sing-box\n" + + "📈 /traffic — Статистика трафика sing-box\n" + "🩺 /doctor — Диагностика и ремонт ошибок\n" + "🔄 /restart — Перезапустить службы Tachyon\n" + - "📋 /logs — Посмотреть последние логи\n"; - send_message_with_keyboard(token, chat_id, help_text, "Markdown"); + "📋 /logs — Посмотреть последние логи\n" + + "💾 /backup — Резервная копия настроек роутера\n" + + "🌐 /bypass_add <домен> — Добавить домен в обход\n" + + "🌐 /bypass_del <домен> — Удалить домен из обхода\n" + + "🌐 /direct_add <домен> — Добавить домен в прямой доступ\n" + + "🌐 /direct_del <домен> — Удалить домен из прямого доступа\n" + + "🚫 /block_mac <MAC> — Заблокировать устройство по MAC\n" + + "🔓 /unblock_mac <MAC> — Разблокировать устройство по MAC\n" + + "💻 /sh <команда> — Выполнить консольную команду\n" + + "❓ /help — Справка по командам\n"; + send_message_with_keyboard(token, chat_id, help_text, "HTML"); } else if (cmd == "/status") { let sys = get_system_status(); - let status_text = "📊 *Статус Tachyon роутера*\n\n" + - "Uptime: `" + sys.uptime + "`\n" + - "CPU Load: `" + sys.cpu + "`\n" + - "RAM: `" + sys.ram_avail + "MB / " + sys.ram_total + "MB` свободного\n" + - "sing-box: `" + sys.singbox + "`"; - send_message_with_keyboard(token, chat_id, status_text, "Markdown"); + let status_text = "📊 Статус Tachyon роутера\n\n" + + "Uptime: " + sys.uptime + "\n" + + "CPU Load: " + sys.cpu + "\n" + + "RAM: " + sys.ram_avail + "MB / " + sys.ram_total + "MB свободного\n" + + "sing-box: " + sys.singbox + ""; + send_message_with_keyboard(token, chat_id, status_text, "HTML"); } else if (cmd == "/servers") { handle_servers(token, chat_id); @@ -321,28 +472,119 @@ function process_command(token, chat_id, text) { else if (cmd == "/traffic") { handle_traffic(token, chat_id); } + else if (cmd == "/backup") { + handle_backup(token, chat_id); + } else if (cmd == "/doctor") { - send_message(token, chat_id, "⏳ *Запуск диагностики и авто-исправления...*"); + send_message(token, chat_id, "⏳ Запуск диагностики и авто-исправления...", "HTML"); let doctor_res = command_capture("/usr/bin/tachyon doctor"); let report = doctor_res.output || "Нет вывода диагностики."; - send_message_with_keyboard(token, chat_id, "🩺 *Результаты Tachyon Doctor:*\n\n```\n" + report + "\n```", "Markdown"); + send_message_with_keyboard(token, chat_id, "🩺 Результаты Tachyon Doctor:\n\n
" + escape_html(report) + "
", "HTML"); } else if (cmd == "/restart") { - send_message(token, chat_id, "🔄 *Перезапускаю службы Tachyon...*"); + send_message(token, chat_id, "🔄 Перезапускаю службы Tachyon...", "HTML"); let restart_status = command_status("/usr/bin/tachyon restart"); if (restart_status == 0) { - send_message_with_keyboard(token, chat_id, "✅ *Перезапуск выполнен успешно!*"); + send_message_with_keyboard(token, chat_id, "✅ Перезапуск выполнен успешно!", "HTML"); } else { - send_message_with_keyboard(token, chat_id, "❌ *Ошибка при перезапуске служб.*"); + send_message_with_keyboard(token, chat_id, "❌ Ошибка при перезапуске служб.", "HTML"); } } else if (cmd == "/logs") { let logs_res = command_capture("logread -e tachyon | tail -n 25"); let logs = logs_res.output || "Логи отсутствуют."; - send_message_with_keyboard(token, chat_id, "📋 *Последние логи Tachyon:*\n\n```\n" + logs + "\n```", "Markdown"); + send_message_with_keyboard(token, chat_id, "📋 Последние логи Tachyon:\n\n
" + escape_html(logs) + "
", "HTML"); + } + else if (match(cmd, /^\/bypass_add /) != null) { + let dom = trim(substr(cmd, 12)); + if (dom == "") { + send_message_with_keyboard(token, chat_id, "❌ Укажите домен. Пример: `/bypass_add site.com`", "HTML"); + return; + } + let res = manage_domain_list("bypass", dom, false); + if (res.success) { + send_message_with_keyboard(token, chat_id, "✅ " + res.message, "HTML"); + } else { + send_message_with_keyboard(token, chat_id, "❌ " + res.error, "HTML"); + } + } + else if (match(cmd, /^\/bypass_del /) != null) { + let dom = trim(substr(cmd, 12)); + if (dom == "") { + send_message_with_keyboard(token, chat_id, "❌ Укажите домен. Пример: `/bypass_del site.com`", "HTML"); + return; + } + let res = manage_domain_list("bypass", dom, true); + if (res.success) { + send_message_with_keyboard(token, chat_id, "✅ " + res.message, "HTML"); + } else { + send_message_with_keyboard(token, chat_id, "❌ " + res.error, "HTML"); + } + } + else if (match(cmd, /^\/direct_add /) != null) { + let dom = trim(substr(cmd, 12)); + if (dom == "") { + send_message_with_keyboard(token, chat_id, "❌ Укажите домен. Пример: `/direct_add site.com`", "HTML"); + return; + } + let res = manage_domain_list("direct", dom, false); + if (res.success) { + send_message_with_keyboard(token, chat_id, "✅ " + res.message, "HTML"); + } else { + send_message_with_keyboard(token, chat_id, "❌ " + res.error, "HTML"); + } + } + else if (match(cmd, /^\/direct_del /) != null) { + let dom = trim(substr(cmd, 12)); + if (dom == "") { + send_message_with_keyboard(token, chat_id, "❌ Укажите домен. Пример: `/direct_del site.com`", "HTML"); + return; + } + let res = manage_domain_list("direct", dom, true); + if (res.success) { + send_message_with_keyboard(token, chat_id, "✅ " + res.message, "HTML"); + } else { + send_message_with_keyboard(token, chat_id, "❌ " + res.error, "HTML"); + } + } + else if (match(cmd, /^\/block_mac /) != null) { + let mac = trim(substr(cmd, 11)); + if (match(mac, /^[0-9A-Fa-f]{2}(:[0-9A-Fa-f]{2}){5}$/) == null) { + send_message_with_keyboard(token, chat_id, "❌ Неверный формат MAC-адреса. Пример: `00:1A:2B:3C:4D:5E`", "HTML"); + return; + } + let res = manage_mac_block(mac, false); + if (res.success) { + send_message_with_keyboard(token, chat_id, "✅ " + res.message, "HTML"); + } else { + send_message_with_keyboard(token, chat_id, "❌ " + res.error, "HTML"); + } + } + else if (match(cmd, /^\/unblock_mac /) != null) { + let mac = trim(substr(cmd, 13)); + if (match(mac, /^[0-9A-Fa-f]{2}(:[0-9A-Fa-f]{2}){5}$/) == null) { + send_message_with_keyboard(token, chat_id, "❌ Неверный формат MAC-адреса. Пример: `00:1A:2B:3C:4D:5E`", "HTML"); + return; + } + let res = manage_mac_block(mac, true); + if (res.success) { + send_message_with_keyboard(token, chat_id, "✅ " + res.message, "HTML"); + } else { + send_message_with_keyboard(token, chat_id, "❌ " + res.error, "HTML"); + } + } + else if (match(cmd, /^\/sh /) != null) { + let shell_cmd = substr(cmd, 4); + send_message(token, chat_id, "⏳ Выполнение команды: " + escape_html(shell_cmd) + "", "HTML"); + let shell_res = command_capture(shell_cmd); + let output = shell_res.output || "Команда выполнена без вывода."; + if (length(output) > 3000) { + output = substr(output, 0, 3000) + "\n...[вывод обрезан]..."; + } + send_message_with_keyboard(token, chat_id, "💻 Результат (код " + shell_res.status + "):\n\n
" + escape_html(output) + "
", "HTML"); } else { - send_message_with_keyboard(token, chat_id, "⚠️ Неизвестная команда. Введите /help для списка команд."); + send_message_with_keyboard(token, chat_id, "⚠️ Неизвестная команда. Введите /help для списка команд.", "HTML"); } } @@ -384,10 +626,78 @@ function process_updates(token, admin_ids) { continue; } - process_command(token, chat_id, msg.text); } } - + +function check_new_dhcp_leases(token, admin_ids) { + let lease_file = "/tmp/dhcp.leases"; + let data = fs.readfile(lease_file); + if (!data) return; + + let known_macs_file = "/tmp/tachyon_known_macs.json"; + let known_macs = {}; + let known_data = fs.readfile(known_macs_file); + if (known_data) { + try { + known_macs = json(known_data); + } catch(e) {} + } + + let is_first_run = (length(keys(known_macs)) == 0); + let changed = false; + + for (let line in split(data, "\n")) { + line = trim(line); + if (line == "") continue; + let fields = split(line, / /); + if (length(fields) < 4) continue; + + let mac = uc(as_string(fields[1])); + let ip = fields[2]; + let hostname = fields[3] == "*" ? "Неизвестно" : fields[3]; + + if (!known_macs[mac]) { + known_macs[mac] = { ip: ip, hostname: hostname, first_seen: time() }; + changed = true; + + if (!is_first_run) { + let text = "📡 Подключено новое устройство!\n\n" + + "🖥️ Имя: " + escape_html(hostname) + "\n" + + "🌐 IP: " + ip + "\n" + + "🏷️ MAC: " + mac + ""; + + let payload = { + text: text, + parse_mode: "HTML", + reply_markup: { + inline_keyboard: [ + [ + { text: "❌ Заблокировать", callback_data: "/block_mac " + mac } + ] + ] + } + }; + + let admins = split(admin_ids, /,/); + for (let admin in admins) { + let chat_id = trim(admin); + if (chat_id != "") { + let p = { chat_id: int(chat_id) }; + for (let k in keys(payload)) { + p[k] = payload[k]; + } + tg_request(token, "sendMessage", p); + } + } + } + } + } + + if (changed) { + fs.writefile(known_macs_file, sprintf("%J", known_macs)); + } +} + function worker() { let cfg = settings(); if (cfg.enabled != "1" || !cfg.bot_token || !cfg.admin_ids) { @@ -411,6 +721,8 @@ function worker() { let poll_interval = int(cfg.poll_interval || "5"); if (poll_interval < 1) poll_interval = 1; + let last_dhcp_check = 0; + while (true) { cfg = settings(); if (cfg.enabled != "1") { @@ -418,6 +730,13 @@ function worker() { } process_updates(cfg.bot_token, cfg.admin_ids); + + let now = time(); + if (now - last_dhcp_check >= 30) { + check_new_dhcp_leases(cfg.bot_token, cfg.admin_ids); + last_dhcp_check = now; + } + sleep(poll_interval * 1000); } return 0; diff --git a/tests/shell_inventory.sh b/tests/shell_inventory.sh index adaf37e3..01e666ad 100644 --- a/tests/shell_inventory.sh +++ b/tests/shell_inventory.sh @@ -44,6 +44,7 @@ shell_scripts="$( expected_shell_scripts="$( printf '%s\n' \ 'luci-app-tachyon/root/etc/uci-defaults/50_luci-tachyon' \ + 'tachyon/files/etc/hotplug.d/iface/99-tachyon-wan-monitor' \ 'tachyon/files/etc/init.d/tachyon' | LC_ALL=C sort )"