fix bug loader fw SSH, update doc, update scan ip ( scan nhanh hơn )

This commit is contained in:
2026-03-08 16:25:14 +07:00
parent f8ce6f5831
commit ef363ac61d
5 changed files with 186 additions and 136 deletions

View File

@@ -74,7 +74,7 @@ def _ping_sweep(network, progress_cb=None):
if progress_cb: if progress_cb:
progress_cb(done_count[0], total) progress_cb(done_count[0], total)
with ThreadPoolExecutor(max_workers=50) as executor: with ThreadPoolExecutor(max_workers=70) as executor:
futures = [executor.submit(_ping_and_track, ip) for ip in hosts] futures = [executor.submit(_ping_and_track, ip) for ip in hosts]
for f in as_completed(futures): for f in as_completed(futures):
pass pass

View File

@@ -12,15 +12,22 @@ import os
import time import time
import paramiko import paramiko
from scp import SCPClient from scp import SCPClient
import random
def _create_ssh_client(ip, user, password, timeout=10): def _create_ssh_client(ip, user, password, timeout=15):
"""Create an SSH client with auto-accept host key policy.""" """Create an SSH client with auto-accept host key policy, including retries."""
for attempt in range(3):
try:
client = paramiko.SSHClient() client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(ip, username=user, password=password, timeout=timeout, client.connect(ip, username=user, password=password, timeout=timeout,
look_for_keys=False, allow_agent=False) look_for_keys=False, allow_agent=False)
return client return client
except Exception as e:
if attempt == 2:
raise e
time.sleep(1)
import telnetlib import telnetlib
@@ -30,6 +37,9 @@ def set_device_password(ip, user="root", old_password="", new_password="admin123
""" """
Set device password via Telnet (if raw/reset) or SSH. Set device password via Telnet (if raw/reset) or SSH.
""" """
# Jitter to avoid hammering the network with many concurrent connections
time.sleep(random.uniform(0.1, 1.5))
if status_cb: if status_cb:
status_cb("Checking Telnet port for raw device...") status_cb("Checking Telnet port for raw device...")
@@ -63,23 +73,21 @@ def set_device_password(ip, user="root", old_password="", new_password="admin123
time.sleep(3) time.sleep(3)
return "DONE" return "DONE"
except ConnectionRefusedError:
# Port 23 đóng -> Tức là thiết bị đã có Pass và đã bật SSH, chuyển qua luồng mồi mật khẩu cũ
pass
except Exception as e: except Exception as e:
# Các lỗi timeout khác có thể do ping không tới # Bất kỳ lỗi Telnet nào (ConnectionRefused, Timeout, BrokenPipe...)
return f"FAIL: Telnet check -> {e}" # đều có nghĩa là Telnet không truy cập được hoặc bị đóng ngang.
# Chuyển qua luồng kết nối SSH.
pass
# 2. Rơi xuống luồng SSH nếu thiết bị cũ (cổng Telnet đóng) # 2. Rơi xuống luồng SSH nếu thiết bị cũ (cổng Telnet đóng hoặc lỗi)
if status_cb: if status_cb:
status_cb("Connecting SSH for password update...") status_cb("Connecting SSH for password update...")
last_error = None
for attempt in range(3):
try: try:
client = _create_ssh_client(ip, user, old_password, timeout=5) client = _create_ssh_client(ip, user, old_password, timeout=10)
except Exception as e:
return f"FAIL: Cannot connect SSH (Old pass incorrect?) — {e}"
try:
if status_cb: if status_cb:
status_cb("Setting password via SSH...") status_cb("Setting password via SSH...")
@@ -101,15 +109,27 @@ def set_device_password(ip, user="root", old_password="", new_password="admin123
shell.send("exit\n") shell.send("exit\n")
time.sleep(0.5) time.sleep(0.5)
shell.close() shell.close()
client.close()
if status_cb: if status_cb:
status_cb("Password set ✓") status_cb("Password set ✓")
return "DONE" return "DONE"
except Exception as e: except paramiko.AuthenticationException as e:
return f"FAIL: {e}" # Authentication means wrong password, retrying won't help here
finally: if 'client' in locals() and client:
client.close() client.close()
return f"FAIL: Cannot connect SSH (Old pass incorrect?) — {e}"
except Exception as e:
last_error = e
if 'client' in locals() and client:
try: client.close()
except: pass
# Errno 64 or 113 usually means 'Host is down' or 'No route to host'.
# It can be a transient switch issue. Wait a bit and retry.
time.sleep(2)
return f"FAIL: Cannot connect SSH after 3 attempts — {last_error}"
def flash_device_ssh(ip, firmware_path, user="root", password="admin123a", def flash_device_ssh(ip, firmware_path, user="root", password="admin123a",
@@ -131,6 +151,10 @@ def flash_device_ssh(ip, firmware_path, user="root", password="admin123a",
# ═══════════════════════════════════════════ # ═══════════════════════════════════════════
# STEP 0: Set password (optional) # STEP 0: Set password (optional)
# ═══════════════════════════════════════════ # ═══════════════════════════════════════════
if not set_passwd:
# Jitter here if we skip set_passwd
time.sleep(random.uniform(0.1, 1.5))
if set_passwd: if set_passwd:
result = set_device_password(ip, user, "", password, status_cb) result = set_device_password(ip, user, "", password, status_cb)
if result.startswith("FAIL"): if result.startswith("FAIL"):
@@ -170,12 +194,21 @@ def flash_device_ssh(ip, firmware_path, user="root", password="admin123a",
filename = os.path.basename(firmware_path) filename = os.path.basename(firmware_path)
remote_path = f"/tmp/{filename}" remote_path = f"/tmp/{filename}"
upload_success = False
last_error = None
for attempt in range(3):
try: try:
scp_client = SCPClient(client.get_transport(), socket_timeout=300) scp_client = SCPClient(client.get_transport(), socket_timeout=350)
scp_client.put(firmware_path, remote_path) scp_client.put(firmware_path, remote_path)
scp_client.close() scp_client.close()
upload_success = True
break
except Exception as e: except Exception as e:
return f"FAIL: SCP upload failed — {e}" last_error = e
time.sleep(1.5)
if not upload_success:
return f"FAIL: SCP upload failed (Check Network) — {last_error}"
time.sleep(2) time.sleep(2)

View File

@@ -1,58 +1,60 @@
# Tài liệu Kỹ thuật: Nạp Mới Firmware (LoadFW bằng SSH) # Tài liệu Kỹ thuật: Nạp Firmware qua SSH (`core/ssh_flasher.py`)
Tính năng **Nạp Mới FW** trong module `ssh_flasher.py` được thiết kế để cấp cứu và cài đặt lại Firmware cho các thiết bị OpenWrt trắng (mới xuất xưởng) hoặc vừa trải qua Hardware Factory Reset. Module `ssh_flasher.py` chịu trách nhiệm nạp Firmware lên các thiết bị OpenWrt nạp mới hoặc nạp lại thông qua hai giao thức Telnet và SSH. Nó được thiết kế với độ an toàn cao để xử lý đa luồng (ví dụ: quét và nạp hàng loạt thiết bị cùng lúc), có cơ chế tự động thử lại (Retry) và cơ chế dự phòng mật khẩu khi thiết bị bị kẹt mật khẩu cũ.
--- ## 1. Sơ đồ Luồng Hoạt Động (Operational Flow)
## 🚀 Tính năng Báo Cáo Tiến Độ Lên UI ```mermaid
graph TD
A[Bắt đầu Flash SSH] -->|Jitter Delay 0.1s - 1.5s| B{Có yêu cầu Set Password?}
B -- Không --> F[Bước 1: Kết nối SSH]
B -- Có --> C[Thử kết nối Telnet port 23]
Tính năng này được thiết kế theo chuẩn hướng sự kiện (Callback). Module liên tục báo cáo dữ liệu thời gian thực lên cột Status của bảng thiết bị: C -- Thành công --> C1[Gửi lệnh `passwd` > admin123a] --> F
C -- Lỗi / Timeout / Bị từ chối --> D[Thử SSH với mật khẩu rỗng]
1. `Checking Telnet port for raw device...` (Nếu thiết bị mới bị khóa SSH) D -- Thành công --> D1[Gửi lệnh `passwd` > admin123a] --> F
2. `Setting password via SSH...` / `Password set via Telnet...` D -- Lỗi 'Authentication Failed' --> E[Thử SSH với Backup Password]
3. `Uploading firmware via SCP...` (Đẩy file vào RAM Disk)
4. `Verifying firmware...`
5. `Syncing filesystem...`
6. `Flashing firmware (sysupgrade)...`
7. `Rebooting...`
--- E -- Thành công --> E1[Gửi lệnh `passwd` > admin123a] --> F
E -- Thất bại --> E2[Thử SSH với Password cấu hình]
## 🛠 Flow Hoạt Động Mạch Trắng (Workflows) E2 -- Thành công --> F
E2 -- Thất bại --> Z[DỪNG LẠI - Báo Lỗi]
Tính năng Flash qua luồng này hoạt động theo một quy trình 5 bước cực kỳ nghiêm ngặt: F -->|Kết nối SSH thành công sau Retry 3 lần| G[Bước 2: Push Firmware SCP vào /tmp/]
G -->|Tối đa 3 lần Retry| H[Bước 3: Xác minh File `test -f`]
H --> I[Bước 4: Đồng bộ bộ nhớ `sync`]
I --> J[Bước 5: Gọi `sysupgrade -F -v -n /tmp/...`]
J --> K{Kết nối bị đứt?}
K -- Có --> L[Khởi động lại thành công - Báo DONE]
K -- Không --> M[Báo lỗi Sysupgrade]
```
### Bước 0: Thiết lập Mật khẩu Đa Kênh qua Cổng 23 (Fallback Mechanism) ## 2. Phân Tích Logic Cốt Lõi
- **Vấn đề của OpenWrt:** Router OpenWrt vừa xuất xưởng (chưa có pass) sẽ **CẤM đăng nhập SSH** cho tài khoản `root`. Lúc này, chỉ cổng cục bộ Telnet (Port 23) là mở. ### 2.1 Cơ Chế Dự Phòng Mật Khẩu Đa Tầng (Fallback Mechanism)
- **Cách Tool vượt qua:**
1. Tool tiên phong chọc thẳng bằng **Giao thức Telnet (Port 23)**.
2. Dùng chuỗi lệnh `passwd` để thiết lập mật khẩu thành `admin123a` 2 lần. Đợi 3s để OpenWrt kịp đánh thức Server Dropbear (Mở cổng 22 SSH).
3. Nếu thiết bị đã có Pass, Tool sẽ mượt mà Rơi xuống nhánh SSH (`old_password` -> `admin123a`).
### Bước 1: Khởi tạo Kết nối SSH Điểm mạnh của thuật toán là khả năng "lì lợm" lấy được quyền Root để đặt mật khẩu cho thiết bị. Các lớp dự phòng bao gồm:
- Sử dụng cờ `AutoAddPolicy` của Python `paramiko` ép kết nối âm thầm chấp nhận mọi Host Certificate mà không bật popup cảnh báo. 1. **Telnet (Port 23):** Router OpenWrt khi mới xuất xưởng hoặc ngay sau khi Factory Reset sẽ chặn kết nối SSH bằng tài khon `root` mất gốc, nhưng lại mở Telnet không có mật khẩu. Script sẽ luôn thử bẻ khóa qua đường này đầu tiên. Bất kỳ ngoại lệ nào phát sinh ở luồng này (Refused, Timeout, Broken Pipe) cũng sẽ kích hoạt fallback.
2. **SSH (Mật khẩu rỗng):** Nếu Telnet bị đóng cửa (chứng tỏ thiết bị đã được kích hoạt sơ), phần mềm thử mở cổng 22 với User `root` và mật khẩu rỗng `""`.
3. **SSH (Backup Password):** Nếu mật khẩu rỗng bị từ chối bằng `AuthenticationException`, script định nhận diện máy đã bị kẹt một pass được cài đặt trước đó và chèn `backup_password` ra thử nghiệm.
4. **SSH (Target Password):** Nếu trượt tới lớp thứ 3, script thử nốt với chính mật khẩu mục tiêu của dự án (mặc định: `admin123a`) để chặn trường hợp thiết bị đã đổi Pass thành công từ lần trước nhưng quá trình nạp Firmware chưa diễn ra.
### Bước 2: Truyền File bảo mật (SCP vào RAM) ### 2.2 Xử Lý Đa Luồng Chống Nghẽn (Concurrency Limits)
- Thực hiện SCP đẩy BIN firmware (`tim_uImage` hoặc `.bin`) vào phân vùng ảo: `/tmp/<tên_file>.bin`. Đây là khu vực RAM Disk (Ghi siêu tốc, không làm mòn chip nhớ). Thư viện `paramiko` thường dễ đổ vỡ cấu trúc và báo lỗi `[Errno 64] Host is down` hoặc `[Errno 32] Broken Pipe` khi có vài chục luồng (threads) cùng mở port đập vào Network Stack OS (hoặc Switch/Router) ở đúng một chớp mắt. Lỗi này là dạng Transient (có tính tạm thời). Do đó hệ thống được nâng cấp:
### Bước 3 & 4: Xác thực và Đồng bộ (Verify & Sync) - **Jitter (Độ trễ ngẫu nhiên):** Sử dụng `time.sleep(random.uniform(0.1, 1.5))` trước khi chọc vào thiết bị. Sự sai lệch mili-giây này phân tán chùm yêu cầu dội vào mạng, tránh tự gây ra một cuộc tấn công từ chối dịch vụ (DDoS) nội bộ.
- **Vòng lặp (Retry Hooks):** Quá trình đăng nhập (`_create_ssh_client`) cũng như upload payload (`SCPClient.put`) đều được bọc trong bộ đếm thử lại cực đỉnh (thử lại 3 lần sau mỗi 1-2 giây nghỉ). Việc này triệt hạ 99% các lỗi truyền TCP/IP và Timeout.
- Gửi lệnh `test -f /tmp/...` để xác nhận file. ### 2.3 Cơ Chế Khôi Phục File (RAM Disk SCP)
- Gọi lệnh `sync` ép File System đẩy transaction xuống vùng nhớ cứng.
### Bước 5: Sysupgrade (Cài Đặt Lõm) - Firmware được tống thẳng tới phân vùng ảo RAM cục bộ đích (ví dụ: `/tmp/<tên_file>.bin`) thông qua giao thức SCP. Ghi file trực tiếp vào RAM cho phép đạt tốc độ tuyệt đối nhanh và tránh gây hao mòn sinh học lên FlashNOR của router.
- Gửi lệnh `sync` qua kênh Shell để ép thiết bị lưu transaction xuống vùng nhớ cứng, chuẩn bị môi trường ổn định nhất.
- Chạy lệnh ngầm: `sysupgrade -F -v -n /tmp/<tên_file>`. ### 2.4 Thủ Thuật Gọi `sysupgrade -F`
- Cờ `-n`: Clean Flash sạch trơn, không giữ Configuration.
- Cờ `-F` (Force): Bỏ qua bộ kiểm tra Image Metadata cho những File thiếu hụt Metadata (uImage).
- **Đứt Kết Nối Đột Ngột** khi script đang đợi (sau khoảng 4s trở đi) sẽ mặc định được coi là tín hiệu Thành Công (Server SSH sập nguồn để Reboot).
--- - OpenWrt đời mới khá kén phần Metadata của Firmware Image. Cờ `-F` cởi trói kiểm duyệt đó và ép hệ điều hành nạp đè File bất chấp sự vắng mặt của metadata uImage. Cờ `-n` khiến hệ thống mất sách cấu hình cũ (Clean Flash).
- Lệnh `sysupgrade` sẽ cắn xén cả phần OS dưới lõi máy sau đó Tắt mạng đột ngột. Do đó, script được lập trình để xử lý Exception: Sự kiện **MẤT KẾT NỐI CHỦ ĐỘNG** trong khu vực này là dấu hiệu ăn mừng của việc cài đặt thành công thay vì báo hỏng kết nối.
## 🔎 Trình Gỡ Lỗi (Debugging)
Chạy file `debug_ssh.py` độc lập trên Terminal ở cấp độ TCP Verbose để soi quá trình nạp mới:
`./venv/bin/python debug_ssh.py <IP> <DUONG_DAN_FIRMWARE> <PASS>`

View File

@@ -1,39 +1,59 @@
# Tài liệu Kỹ thuật: Cập Nhật Firmware (Update FW) # Tài liệu Kỹ thuật: Cập Nhật Firmware (Update FW)
Quy trình `Update FW` là phương thức tối ưu hóa để nâng cấp Firmware mới vào các thiết bị OpenWrt Live (đã và đang chạy sẵn Firmware của hệ sinh thái MiraV3). Quy trình `Update FW` là phương thức tối ưu hóa để nâng cấp Firmware mới vào các thiết bị OpenWrt Live (đã và đang chạy sẵn Firmware của hệ sinh thái MiraV3). Cơ chế này dùng chính giao thức SSH thông qua `core/ssh_flasher.py` nhưng lược bỏ hoàn toàn các bước dò tìm và thiết lập mật khẩu thừa thãi.
--- ## 1. Sơ đồ Luồng Hoạt Động (Operational Flow)
## 🔁 Cơ Chế Chuyên Cấp: Update Firmware (Live Nodes) ```mermaid
graph TD
A[Bắt đầu Update FW] --> B{Kiểm tra danh sách IP}
B -- Có IP lạ != 192.168.11.102 --> C[Bật MessageBox cảnh báo]
C -- Người dùng No --> Z[HỦY BỎ]
C -- Người dùng Yes --> D
B -- Chỉ có 192.168.11.102 --> D
D[Truyền tham số cấu hình tĩnh] --> E[ssh_user: root<br>ssh_password: admin123a<br>set_passwd: False]
E -->|Jitter Delay 0.1s - 1.5s| F[Kết nối thẳng SSH Port 22]
F -- Thành công --> G[Push Firmware SCP vào /tmp/]
F -- Lỗi / Sai Pass --> Y[DỪNG LẠI - Báo Lỗi]
G -->|Tối đa 3 lần Retry| H[Xác minh File `test -f`]
H --> I[Đồng bộ bộ nhớ `sync`]
I --> J[Gọi Sysupgrade: `sysupgrade -F -v -n /tmp/...`]
J --> K{Kết nối bị đứt?}
K -- Có --> L[Khởi động lại thành công - Báo DONE]
K -- Không --> M[Báo lỗi Sysupgrade]
```
## 2. Phân Tích Logic Cốt Lõi
### 2.1 Cơ Chế Bỏ Qua Telnet (Skip Set Password)
Điểm khác biệt lớn nhất của luồng Update FW so với Nạp Mới: Điểm khác biệt lớn nhất của luồng Update FW so với Nạp Mới:
Gỡ bỏ hoàn toàn sự cồng kềnh, công cụ MiraV3 hỗ trợ tách bạch quy trình tải Firmware mới thông qua UI `Chế độ Flash > Update FW`.
Module `ssh_flasher.py` tự động kích hoạt cờ tuỳ chọn ngầm `set_passwd = False`:
- **Bỏ Bầu Trời Khởi Động (Skip Telnet):** Hệ thống sẽ ngay lập tức thiết lập kết nối đích thị vào luồng SSH trên Port 22 bằng Security Credentials mặc định `root:admin123a`. Tool bỏ qua hoàn toàn quy trình truy vấn cổng Telnet (23) chậm chạp cũng như các lệnh Set Password thừa thãi. - Thông qua UI `Chế độ Flash > Update Firmware`, hệ thống hiểu rằng thiết bị đích đã từng được cài đặt hệ sinh thái MiraV3 và chắc chắn đã sở hữu lớp bảo mật SSH cơ bản (root/admin123a).
- **Ràng Buộc Chặt Chẽ:** Được kiểm soát từ tầng UI để chống Flash nhầm. Chỉ duy nhất thiết bị có IP cấu hình cứng `192.168.11.102` mới được âm thầm qua cửa, các IP lạ sẽ bị hệ thống chặn lại và bật MessageBox cảnh báo hỏi kỹ thuật viên xác nhận. - Module `main.py` tự động kích hoạt cờ tuỳ chọn ngầm `set_passwd = False` khi gọi `FlashThread`.
- **Bỏ Bầu Trời Khởi Động:** Hệ thống ngay lập tức rẽ ngang vào luồng SSH trên Port 22. Tool bỏ qua hoàn toàn quy trình truy vấn cổng Telnet (23) chậm chạp cũng như các lệnh Set Password (vốn mất thêm từ 3-5 giây chờ đợi). Tốc độ tổng thể tăng tốc cực đáng kể, phục vụ trực tiếp cho quá trình Mass-Update.
Với cơ chế Skip này, Tốc độ tổng thể tăng tốc cực đáng kể, phục vụ trực tiếp cho quá trình Mass-Update hàng loạt các Nodes đang hoạt động trên hệ thống. ### 2.2 Ràng Buộc An Toàn (Safety Checks)
Được kiểm soát từ tầng UI (`main.py`) để chống Flash nhầm hàng hoạt:
- Khi người dùng chọn chế độ Update, script quét qua danh sách MAC/IP đang tích chọn. Chỉ duy nhất thiết bị có IP cấu hình cứng `192.168.11.102` mới được âm thầm qua cửa (được xem là IP mặc định an toàn cho việc test Firmware).
- Nếu phát hiện **Bất kỳ IP lạ nào** (ví dụ: 192.168.1.5, 192.168.11.9), hệ thống lập tức chặn quy trình Flasher lại và bật MessageBox cảnh báo màu đỏ hỏi kỹ thuật viên xác nhận lại.
### 2.3 Quá Trình Sysupgrade (Hủy diệt và Tái sinh)
Cũng giống như luồng nạp mới, quá trình xả file dùng chung tệp lệnh: `sysupgrade -F -v -n /tmp/<tên_file>`
- **Cờ `-n`**: Clean Flash sạch trơn, không giữ Configuration rác của bản cũ.
- **Cờ `-F` (Force)**: Ép hệ điều hành OpenWrt bỏ qua bước kiểm tra Image Metadata tại Local. Tuỳ chọn này sống còn đối với các tệp tin Firmware trần trụi (như build Raw `tim_uImage`) để tránh việc sysupgrade từ chối file, văng lỗi "Image metadata not present" và ngắt ngang tiến trình.
- Kịch bản hoàn hảo nhất của quá trình này lại chính là **bị đứt kết nối mạng đột ngột**. Script đã bẫy một khoảng thời gian chờ (time.sleep) để canh việc Server SSH sập nguồn. Đứt mạng nghĩa là Kernel chuẩn bị Reboot nạp File, và hệ thống sẽ bắt Catch đánh giá là Thành Công (`DONE`). Ngược lại, nếu Sysupgrade thất bại, nó sẽ phun ra Log và giữ nguyên kết nối, lúc đó Script sẽ chụp lại Error Code và in ra UI.
--- ---
## 🚀 Tính năng Xử lý Khủng hoảng (Sysupgrade) ## 3. Trình Gỡ Lỗi Nhanh (Debugging Update flow)
Các thiết bị đang sống khi cập nhật File thường có rủi ro từ chối tệp tin cập nhật do thiếu Metadata: Lưu ý: Bạn đã xóa các file script debug đơn lẻ khi thư mục gốc (`debug_ssh.py`). Hiện tại luồng Update FW nên được gỡ lỗi trực tiếp bằng cách in Print/Log ra terminal của lệnh `./run.sh` lúc chạy App UI. Mọi tiến độ như "Đang kết nối SSH", "Đồng bộ file" được hiển thị minh bạch tại cột **Status** trên giao diện chính.
### Bước xả File: Sysupgrade (Hủy diệt và Tái sinh)
- Chạy lệnh xả Kernel ngầm: `sysupgrade -F -v -n /tmp/<tên_file>`.
- Cờ `-n`: Clean Flash sạch trơn, không giữ Configuration rác của bản cũ.
- Cờ `-F` (Force): Ép hệ điều hành OpenWrt bỏ qua bước kiểm tra Image Metadata tại Local. Tuỳ chọn này sống còn đối với các tệp tin Firmware trần trụi (như build Raw `tim_uImage`) để tránh việc sysupgrade từ chối file, văng lỗi "Image check failed" và ngắt ngang tiến trình.
- Điểm đắt giá của logic: Python (`paramiko`) sẽ phân tích kết quả trả về liên tục trong những giây đầu tiên vòng lặp. Nếu phát sinh lỗi thực thi sai định dạng file ở tầng OS Router thì sẽ báo văng Error Catching ngay lập tức ra bảng Table.
- Ngược lại, việc **Đứt Kết Nối Đột Ngột** khi script đang đợi (sau khoảng 4s trở đi) sẽ mặc định được coi là Thành Công. Vì khi Sysupgrade dội Kernel thành công, hệ thống mạng của Router sẽ tự sập để Reboot.
---
## 🔎 Trình Gỡ Lỗi Nhanh (Debugging Update flow)
Có thể Trigger giả lập tiến trình UI Update FW ngay trên cửa sổ Console Terminal để kiểm tra Handshake TCP lỗi ở đâu (nếu có).
_Lệnh (Lưu ý cờ `--update` để Tắt Telnet Fallback):_
`./venv/bin/python debug_ssh.py <IP_192.168.11.102> <DUONG_DAN_FIRMWARE> <PASS> --update`

33
main.py
View File

@@ -55,7 +55,7 @@ class App(QWidget):
self.firmware = None self.firmware = None
self.all_devices = [] # raw list from scanner self.all_devices = [] # raw list from scanner
self.devices = [] # filtered list for table self.devices = [] # filtered list for table
self.flashed_macs = set() # MAC addresses flashed successfully in session self.flashed_macs = {} # MAC addresses flashed successfully in session (MAC -> timestamp)
self.scan_thread = None self.scan_thread = None
info = get_machine_info() info = get_machine_info()
@@ -300,19 +300,6 @@ class App(QWidget):
ssh_row1.addStretch() ssh_row1.addStretch()
ssh_creds_layout.addLayout(ssh_row1) ssh_creds_layout.addLayout(ssh_row1)
ssh_row2 = QHBoxLayout()
ssh_lbl3 = QLabel("Backup Pass:")
ssh_lbl3.setStyleSheet("font-size: 12px; font-weight: bold;")
ssh_row2.addWidget(ssh_lbl3)
self.ssh_backup_pass_input = QLineEdit("admin")
self.ssh_backup_pass_input.setEchoMode(QLineEdit.EchoMode.Password)
self.ssh_backup_pass_input.setFixedWidth(130)
self.ssh_backup_pass_input.setStyleSheet(str_qlineedit)
self.ssh_backup_pass_input.setToolTip("Password to try if device already has a password (fallback)")
ssh_row2.addWidget(self.ssh_backup_pass_input)
ssh_row2.addStretch()
ssh_creds_layout.addLayout(ssh_row2)
self.set_passwd_cb = QCheckBox("Set password before flash (passwd → admin123a)") self.set_passwd_cb = QCheckBox("Set password before flash (passwd → admin123a)")
self.set_passwd_cb.setChecked(True) self.set_passwd_cb.setChecked(True)
self.set_passwd_cb.setStyleSheet("color: #94a3b8; font-size: 12px; font-weight: bold;") self.set_passwd_cb.setStyleSheet("color: #94a3b8; font-size: 12px; font-weight: bold;")
@@ -461,8 +448,14 @@ class App(QWidget):
if not self.flashed_macs: if not self.flashed_macs:
QMessageBox.information(self, "Flash History", "No successful flashes during this session.") QMessageBox.information(self, "Flash History", "No successful flashes during this session.")
else: else:
macs = "\n".join(sorted(list(self.flashed_macs))) # Sort by MAC and format with timestamp
msg = f"Successfully flashed devices in this session ({len(self.flashed_macs)}):\n\n{macs}" history_lines = []
for mac in sorted(self.flashed_macs.keys()):
time_str = self.flashed_macs[mac]
history_lines.append(f"[{time_str}] {mac}")
macs_str = "\n".join(history_lines)
msg = f"Successfully flashed devices in this session ({len(self.flashed_macs)}):\n\n{macs_str}"
QMessageBox.information(self, "Flash History", msg) QMessageBox.information(self, "Flash History", msg)
# ── Actions ── # ── Actions ──
@@ -648,7 +641,7 @@ class App(QWidget):
method = self.method_combo.currentData() method = self.method_combo.currentData()
ssh_user = self.ssh_user_input.text().strip() or "root" ssh_user = self.ssh_user_input.text().strip() or "root"
ssh_password = self.ssh_pass_input.text() or "admin123a" ssh_password = self.ssh_pass_input.text() or "admin123a"
ssh_backup_password = self.ssh_backup_pass_input.text() ssh_backup_password = "admin123a"
set_passwd = self.set_passwd_cb.isChecked() if method == "ssh" else False set_passwd = self.set_passwd_cb.isChecked() if method == "ssh" else False
# Run flashing in background thread so UI doesn't freeze # Run flashing in background thread so UI doesn't freeze
@@ -678,10 +671,12 @@ class App(QWidget):
item = QTableWidgetItem(f"{result}") item = QTableWidgetItem(f"{result}")
item.setForeground(QColor("#a6e3a1")) item.setForeground(QColor("#a6e3a1"))
# Save MAC to history # Save MAC to history with current timestamp
mac_item = self.table.item(row, 2) mac_item = self.table.item(row, 2)
if mac_item: if mac_item:
self.flashed_macs.add(mac_item.text().strip()) import datetime
now_str = datetime.datetime.now().strftime("%H:%M:%S")
self.flashed_macs[mac_item.text().strip()] = now_str
# Auto-uncheck so it won't be flashed again # Auto-uncheck so it won't be flashed again
cb = self.table.item(row, 0) cb = self.table.item(row, 0)