288 lines
16 KiB
Markdown
288 lines
16 KiB
Markdown
# ⚡ Mira Firmware Loader
|
||
|
||
Công cụ desktop dùng để **scan, phát hiện và flash firmware hàng loạt** cho các thiết bị OpenWrt trong mạng LAN.
|
||
Hỗ trợ nạp **thủ công** (chọn thiết bị → flash) và **tự động hóa** (scan → phát hiện → flash → retry).
|
||
|
||
> **Tech stack:** Python 3.9+ · PyQt6 · Paramiko/SCP · Scapy · Requests · PyInstaller
|
||
> **Phiên bản:** `1.2.0`
|
||
|
||
---
|
||
|
||
## 📁 Cấu trúc dự án
|
||
|
||
```text
|
||
Mira_Firmware_Loader/
|
||
├── main.py # UI chính (PyQt6) — App + AutoFlashWindow
|
||
├── version.txt # Số phiên bản ứng dụng
|
||
├── requirements.txt # Danh sách dependencies
|
||
├── core/
|
||
│ ├── scanner.py # Quét thiết bị mạng đa lớp (Ping sweep + ARP + Scapy)
|
||
│ ├── workers.py # ScanThread — chạy scanner trong background thread
|
||
│ ├── api_flash.py # Flash firmware qua LuCI HTTP API
|
||
│ ├── ssh_utils.py # SSH/SCP transport helpers dùng chung
|
||
│ ├── ssh_new_flash.py # Luồng SSH cho chế độ Nạp Mới (Telnet → set passwd → SSH)
|
||
│ ├── ssh_update_flash.py # Luồng SSH cho chế độ Update (SSH trực tiếp)
|
||
│ ├── flash_new_worker.py # NewFlashThread — QThread điều phối Nạp Mới FW
|
||
│ ├── flash_update_worker.py # UpdateFlashThread — QThread điều phối Update FW
|
||
│ └── auto_flash_worker.py # AutoFlashWorker — QThread tự động scan → flash → retry
|
||
├── ui/
|
||
│ ├── components.py # Custom Qt Widgets (CollapsibleGroupBox)
|
||
│ └── styles.py # Stylesheet toàn ứng dụng (STYLE + AUTO_STYLE)
|
||
├── utils/
|
||
│ ├── network.py # Helper IP / network (get_local_ip, get_default_network)
|
||
│ └── system.py # Lấy thông tin máy, resource path cho PyInstaller
|
||
├── docs/
|
||
│ ├── api_flash_docs.md # Tài liệu kỹ thuật LuCI API flash
|
||
│ ├── load_fw_ssh_docs.md # Tài liệu kỹ thuật SSH flash (cả 2 luồng)
|
||
│ ├── scanner_docs.md # Tài liệu kỹ thuật scanner
|
||
│ └── auto_flash_docs.md # Tài liệu kỹ thuật tự động hóa nạp FW
|
||
├── run.sh # Script khởi chạy (macOS/Linux)
|
||
├── run.bat # Script khởi chạy (Windows)
|
||
└── build_windows.bat # Script đóng gói thành .exe (Windows, PyInstaller)
|
||
```
|
||
|
||
---
|
||
|
||
## 🔧 Cài đặt & Chạy
|
||
|
||
### Yêu cầu
|
||
|
||
- Python **3.9+**
|
||
- Thư viện: `PyQt6`, `scapy`, `requests`, `paramiko`, `scp`, `pyinstaller` (để build)
|
||
- _Windows:_ Cài [Npcap](https://npcap.com/) để Scapy có thể gửi ARP broadcast (không bắt buộc — có fallback)
|
||
|
||
### Khởi chạy nhanh
|
||
|
||
**macOS / Linux:**
|
||
|
||
```bash
|
||
./run.sh
|
||
```
|
||
|
||
**Windows:**
|
||
|
||
```bat
|
||
run.bat
|
||
```
|
||
|
||
> Script tự tạo `venv` và cài dependencies nếu chưa có.
|
||
|
||
### 📦 Build file chạy độc lập (.exe) cho Windows
|
||
|
||
```bat
|
||
build_windows.bat
|
||
```
|
||
|
||
Output: `dist\Mira_Firmware_Loader.exe` — không cần cài Python trên máy đích.
|
||
|
||
---
|
||
|
||
## 🏗 Kiến trúc hệ thống
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────┐
|
||
│ main.py (UI) │
|
||
│ │
|
||
│ Machine Info │ Firmware Selector │ Network Scan │
|
||
│ ───────────────────────────────────────────────────── │
|
||
│ Device Table [ ] IP │ MAC │ Status │
|
||
│ ───────────────────────────────────────────────────── │
|
||
│ Flash Controls │
|
||
│ Flash Mode: [ New Flash | Update Firmware ] │
|
||
│ Method: [ API (LuCI) | SSH (paramiko) ] │
|
||
│ Concurrent devices: [SpinBox] │
|
||
│ [ ⚡ FLASH SELECTED DEVICES ] │
|
||
│ ───────────────────────────────────────────────────── │
|
||
│ [ 🤖 Tự động hóa nạp FW ] │
|
||
└────────────────────────┬─────────────────────────────────┘
|
||
│
|
||
┌─────────────────┼─────────────────┐
|
||
│ │ │
|
||
┌──────▼──────┐ ┌───────▼──────────┐ ┌───▼──────────────────┐
|
||
│ scanner.py │ │ Flash Workers │ │ AutoFlashWorker │
|
||
│ │ │ (thủ công) │ │ (tự động hóa) │
|
||
│ 1. Ping │ │ │ │ │
|
||
│ Sweep │ │ NewFlashThread │ │ Phase 1: Scan LAN │
|
||
│ 2. arp -a │ │ ├─ api_flash │ │ (tối đa 15 lần) │
|
||
│ 3. Scapy │ │ └─ ssh_new_flash│ │ Phase 2: Flash │
|
||
│ ARP │ │ │ │ (auto-retry x3) │
|
||
└─────────────┘ │ UpdateFlash │ │ ├─ api_flash │
|
||
│ Thread │ │ └─ ssh_new_flash │
|
||
│ └─ ssh_update │ └──────────────────────┘
|
||
└──────────────────┘
|
||
│
|
||
┌──────────▼──────────┐
|
||
│ ssh_utils.py │
|
||
│ (Transport Layer) │
|
||
│ _create_ssh_client │
|
||
│ _upload_firmware │
|
||
│ _verify_firmware │
|
||
│ _sync_and_sysupgr │
|
||
└─────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 🔄 Luồng hoạt động chi tiết
|
||
|
||
### 1. Quét mạng (Network Scan)
|
||
|
||
`scanner.py` dùng chiến lược 3 lớp, đảm bảo phát hiện đầy đủ mà không cần quyền Root:
|
||
|
||
| Giai đoạn | Mô tả |
|
||
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||
| **Ping Sweep** | Gửi ping đồng thời tới toàn bộ host trong dải `/24` (tất cả cùng lúc, không batching) để đánh thức thiết bị và điền ARP cache |
|
||
| **ARP Table** | Đọc `arp -a` bằng regex, hỗ trợ cả định dạng Windows (`cc-2d-...`) và macOS/Linux (`aa:bb:...`) |
|
||
| **Scapy ARP** | Chạy **song song** với ARP Table — gửi ARP broadcast để lấp khoảng trống. Yêu cầu Npcap (Windows) hoặc root (Linux); tự động bỏ qua nếu không khả dụng |
|
||
|
||
Kết quả được merge theo IP và sort tăng dần trước khi trả về UI.
|
||
|
||
### 2. Bảng thiết bị (Device Table)
|
||
|
||
- Hiển thị cột: checkbox, IP, MAC, Status
|
||
- Mặc định ẩn gateway và IP máy tính đang chạy (có thể bật "Show all")
|
||
- Thiết bị đã flash trong session được đánh dấu "Already Flashed" và tự bỏ tick
|
||
|
||
### 3. Flash Firmware (Thủ công)
|
||
|
||
Có 2 chế độ và 2 method, tổng cộng 3 luồng thực thi khác nhau:
|
||
|
||
#### Chế độ `New Flash` — dùng cho thiết bị vừa reset cứng
|
||
|
||
| Method | Luồng | Mô tả |
|
||
| -------------- | ------------------ | ------------------------------------------------------------------------------------------- |
|
||
| **API (LuCI)** | `api_flash.py` | Đăng nhập LuCI → upload firmware → Proceed. Hỗ trợ Barrier Breaker 14.07 và OpenWrt mới hơn |
|
||
| **SSH** | `ssh_new_flash.py` | Kết nối Telnet → đặt password mới → SSH → SCP upload → sysupgrade |
|
||
|
||
**Luồng SSH – New Flash chi tiết:**
|
||
|
||
1. Telnet port 23 (thiết bị vừa reset, chưa có pass)
|
||
2. Đặt password `admin123a` qua lệnh `passwd`
|
||
3. SSH vào với password vừa đặt
|
||
4. SCP upload firmware lên `/tmp/`
|
||
5. Verify file tồn tại
|
||
6. `sync && sysupgrade -F -v -n` → thiết bị reboot (connection drop = DONE)
|
||
|
||
#### Chế độ `Update Firmware` — dùng cho thiết bị đang chạy OpenWrt
|
||
|
||
Luồng `ssh_update_flash.py`, SSH trực tiếp (không qua Telnet):
|
||
|
||
1. SSH kết nối với `root` / `admin123a` (fallback: `admin`)
|
||
2. SCP upload firmware lên `/tmp/`
|
||
3. Verify file tồn tại
|
||
4. `sync && sysupgrade -F -v -n` → thiết bị reboot
|
||
|
||
> ⚠️ Update Mode hiển thị cảnh báo nếu IP thiết bị khác `192.168.11.102` và yêu cầu xác nhận.
|
||
|
||
### 4. 🤖 Tự động hóa nạp FW (MỚI)
|
||
|
||
Tính năng nạp FW hoàn toàn tự động — chỉ cần cấu hình và nhấn bắt đầu:
|
||
|
||
```
|
||
Cấu hình → Auto Scan LAN → Phát hiện đủ thiết bị → Auto Flash → Auto Retry → Thông báo
|
||
```
|
||
|
||
#### 4.1. Quy trình
|
||
|
||
| Phase | Mô tả |
|
||
| --------------------- | ------------------------------------------------------------------------- |
|
||
| **Phase 1: Scan LAN** | Scan mạng liên tục mỗi 5 giây, tối đa **15 lần**, cho đến khi đủ thiết bị |
|
||
| **Phase 2: Flash** | Nạp FW song song qua ThreadPoolExecutor, tự động **retry tối đa 3 lần** |
|
||
|
||
#### 4.2. Cấu hình
|
||
|
||
| Tham số | Mô tả | Mặc định |
|
||
| --------------- | --------------------------------------------- | ------------------- |
|
||
| **Firmware** | File firmware (.bin/.hex/.uf2) | Lấy từ cửa sổ chính |
|
||
| **Mạng** | Dải mạng LAN cần scan | Tự suy từ IP host |
|
||
| **Số lượng** | Số thiết bị cần nạp | 5 |
|
||
| **Phương thức** | API (LuCI) hoặc SSH | API (LuCI) |
|
||
| **Song song** | Số thiết bị nạp cùng lúc (0 = không giới hạn) | 10 |
|
||
|
||
#### 4.3. Auto-Retry nạp FW
|
||
|
||
Khi một thiết bị nạp thất bại, hệ thống tự động retry:
|
||
|
||
```
|
||
Lần 1 → FAIL: Connection timeout
|
||
⚠️ Log cảnh báo, chờ 2 giây...
|
||
Lần 2 → FAIL: Upload error
|
||
⚠️ Log cảnh báo, chờ 2 giây...
|
||
Lần 3 → DONE ✅ (hoặc ❌ báo lỗi sau 3 lần)
|
||
```
|
||
|
||
- Tối đa **3 lần retry** mỗi thiết bị (`MAX_FLASH_RETRIES`)
|
||
- Chờ **2 giây** giữa mỗi lần retry để thiết bị ổn định
|
||
- Nếu hết 3 lần vẫn fail → đánh dấu ❌, tiếp tục thiết bị tiếp theo
|
||
|
||
#### 4.4. Scan Timeout
|
||
|
||
- Nếu scan **15 lần** mà chưa đủ thiết bị → dừng và hiện cảnh báo
|
||
- Gợi ý kiểm tra: thiết bị đã bật chưa, dải mạng có đúng không
|
||
|
||
#### 4.5. Quy tắc bảo vệ IP `192.168.11.102`
|
||
|
||
| Chế độ | Được nạp 192.168.11.102? | Cơ chế |
|
||
| ------------------------ | :----------------------: | ----------------------------------- |
|
||
| **New Flash (thủ công)** | ❌ Không | Chặn trước khi flash, hiện cảnh báo |
|
||
| **Update FW (thủ công)** | ✅ Có | Cho phép bình thường |
|
||
| **Tự động hóa** | ❌ Không | Tự động lọc khỏi kết quả scan |
|
||
|
||
#### 4.6. Lịch sử nạp
|
||
|
||
Kết quả nạp được lưu ở 2 cấp:
|
||
|
||
| Nơi lưu | Phạm vi | Format |
|
||
| ------------------------------- | ---------------------- | ------------------------------------ |
|
||
| `AutoFlashWindow._auto_history` | Phiên tự động hiện tại | `list[(ip, mac, result, timestamp)]` |
|
||
| `App.flashed_macs` | Toàn bộ session app | `dict{MAC: (ip, mac, result, ts)}` |
|
||
|
||
- Cả thành công ✅ lẫn thất bại ❌ đều được ghi lại
|
||
- Nút "📋 Lịch sử nạp" hiển thị cùng format ở cả 2 cửa sổ: `[HH:MM:SS] ✅/❌ IP (MAC) — result`
|
||
|
||
### 5. Xử lý song song
|
||
|
||
| Tham số | Mô tả |
|
||
| ---------------------- | ------------------------------------------------------------------------ |
|
||
| **Concurrent devices** | Số thiết bị flash đồng thời (`ThreadPoolExecutor`). `0` = không giới hạn |
|
||
|
||
---
|
||
|
||
## ⚙️ Cấu hình mặc định
|
||
|
||
| Tham số | Giá trị mặc định | Mô tả |
|
||
| ---------------------- | ------------------------------------ | --------------------------------- |
|
||
| **Network** | Tự suy ra từ local IP (`x.x.x.0/24`) | Dải mạng để quét |
|
||
| **Flash Mode** | `New Flash` | Nạp mới hoặc Update |
|
||
| **Method** | `API (LuCI)` | Phương thức flash cho New Flash |
|
||
| **SSH User** | `root` | Hardcoded, không hiển thị trên UI |
|
||
| **SSH Password** | `admin123a` | Hardcoded, không hiển thị trên UI |
|
||
| **Concurrent devices** | `10` | Số luồng flash song song |
|
||
| **Show all** | Tắt | Ẩn gateway và IP máy host |
|
||
| **MAX_FLASH_RETRIES** | `3` | Số lần retry nạp FW (tự động) |
|
||
| **MAX_SCAN_ROUNDS** | `15` | Số lần scan tối đa (tự động) |
|
||
|
||
---
|
||
|
||
## 🛡️ Xử lý lỗi tổng hợp
|
||
|
||
| Tình huống | Hành vi |
|
||
| -------------------------------- | --------------------------------------------------- |
|
||
| Scan exception (network error) | Log lỗi, chờ 3s, scan lại (đếm vào MAX_SCAN_ROUNDS) |
|
||
| Scan 15 lần chưa đủ thiết bị | Hiện popup cảnh báo, dừng tự động |
|
||
| Flash thất bại lần 1–2 (tự động) | Log cảnh báo, chờ 2s, retry tự động |
|
||
| Flash thất bại sau 3 lần retry | Log lỗi, đánh dấu ❌, tiếp tục device tiếp theo |
|
||
| User nhấn DỪNG | Set stop flag, dừng scan/flash an toàn |
|
||
| IP 192.168.11.102 + New Flash | Chặn ngay, hiện cảnh báo |
|
||
| Chưa chọn firmware | Hiện popup cảnh báo, không cho bắt đầu |
|
||
| Mạng không hợp lệ | Hiện popup cảnh báo, không cho bắt đầu |
|
||
|
||
---
|
||
|
||
## 🔒 Lưu ý bảo mật & Quyền
|
||
|
||
- **Scapy (chế độ sâu):** Cần Npcap (Windows) hoặc `sudo` (macOS/Linux). App vẫn hoạt động mà không cần quyền Admin nhờ fallback Ping Sweep + `arp -a`.
|
||
- **CREATE_NO_WINDOW:** Khi gọi subprocess (`ping`, `arp`), ứng dụng dùng flag `CREATE_NO_WINDOW` trên Windows để ngăn cửa sổ console hiện ra.
|
||
- **HTTP thuần:** Firmware được upload qua HTTP (không HTTPS). Chỉ dùng trong mạng LAN nội bộ, không nên dùng trên mạng mở.
|
||
- **Credentials cố định:** SSH credentials (`root`/`admin123a`) được hardcode trong `flash_update_worker.py` và truyền từ `main.py`, không hiển thị trên UI.
|