Ai đang ở trong mạng nhà bạn? Kiểm kê thiết bị bằng Python
Câu hỏi xuyên suốt: mạng Wi-Fi của câu lạc bộ có bao nhiêu thiết bị, mỗi thiết bị là gì, và làm sao biết có thiết bị lạ vừa vào?
Thời lượng: khoảng 2 giờ, chia hai buổi: bước 1–5 (hiểu và quan sát), bước 6–8 (làm công cụ và phòng thủ). Có 5 bài tập, bài cuối làm ở nhà
Bài này làm gì?
5 phútViệc này gọi là kiểm kê: đếm xem nhà mình có những gì. Không biết mình có gì thì không bảo vệ được (bài 1).
Bốn việc, làm theo thứ tự
| Việc | Câu hỏi | Làm ở |
|---|---|---|
| 1. Tìm | Ai đang ở trong mạng? | Bước 2 và 3 |
| 2. Nhận diện | Thiết bị đó là gì? | Bước 4 |
| 3. Xem cổng | Nó đang mở cửa nào? | Bước 4 và 7 |
| 4. So sánh | Có ai mới vào không? | Bước 7 và 8 |
Cần chuẩn bị
- Máy tính đang nối vào đúng mạng Wi-Fi cần kiểm kê.
- Python 3.8 trở lên và terminal (đã cài ở bài 6). Không cài thêm gì.
- Mật khẩu router: không bắt buộc. Bước 7 có cách học mà không cần mạng nào.
Bước 1 · Ba thứ cần biết: IP, MAC, port
10 phútMạng Wi-Fi giống một tòa chung cư. Mỗi thiết bị là một căn phòng. Muốn tìm đúng phòng, bạn cần phân biệt ba thứ:
| Thứ | Ví dụ | Giống như | Có đổi không? |
|---|---|---|---|
192.168.1.40 | Số phòng | Có. Router cho mượn, mai có thể đổi | |
3c:22:fb:aa:bb:cc | Số in trên card mạng | Gần như không (trừ MAC ngẫu nhiên, ở bước 4) | |
631 | Ô cửa của một dịch vụ (in, web…) | Cố định theo dịch vụ |
.40, mai có thể là .52. Muốn nhận ra đúng thiết bị, hãy nhìn MAC.Ai phát số phòng?
giữ phòng số .1 (cửa ra Internet). Nó cũng làm việc : thiết bị nào vào mạng cũng được phát một IP tự động. Một mạng nhà thường có khoảng 254 phòng, từ 192.168.1.1 đến 192.168.1.254.
Đọc thêm: mạng /24 và /22 là gì? (không bắt buộc)
Số sau dấu / cho biết mạng to hay nhỏ. Cách viết này gọi là . /24 có 256 địa chỉ. Nhà bạn có thể lớn hơn: /22 có 1024. Công cụ Python ở bước 6 tự đọc kích thước mạng, bạn không cần đoán.
arp -a của bạn có dòng 192.168.71.255. Địa chỉ quảng bá là địa chỉ cuối của dải, nên dải này kết thúc ở .71.255: bốn khối 256 địa chỉ, tức /22. Đoán nhầm là /24 chỉ nhìn thấy khối đầu tiên.Bước 2 · Tìm thiết bị bằng ARP
20 phútLaptop biết IP của máy in, nhưng chưa biết MAC. Nó cần MAC để giao dữ liệu đúng chỗ. Cách tìm MAC gọi là .
Máy bạn tự làm việc này. Bạn không phải làm gì, nhưng xem được kết quả.
BƯỚC 1Xem bảng ARP trên máy bạn
arp -aarp -aip neigh
# hoặc: arp -aLệnh này chỉ đọc sổ ARP có sẵn trong máy bạn. Nó không gửi gì ra mạng, không quét Wi-Fi và không cần mật khẩu router.
? (192.168.1.1) at f4:ec:38:11:22:33 on en0 ifscope [ethernet]
? (192.168.1.40) at 3c:22:fb:aa:bb:cc on en0 ifscope [ethernet]
? (192.168.1.77) at (incomplete) on en0 ifscope [ethernet]
? (192.168.1.255) at ff:ff:ff:ff:ff:ff on en0 ifscope [ethernet]
mdns.mcast.net (224.0.0.251) at 1:0:5e:0:0:fb on en0 ifscope permanent [ethernet]BƯỚC 2Đọc một dòng
Lấy dòng ? (192.168.1.40) at 3c:22:fb:aa:bb:cc on en0 …. Chỉ cần nhìn bốn mảnh:
| Phần | Ý nghĩa |
|---|---|
(192.168.1.40) | IP của thiết bị. |
at 3c:22:fb:aa:bb:cc | MAC của thiết bị đó. |
on en0 | Card mạng của máy bạn đang dùng. Không phải của thiết bị kia. |
at (incomplete) | Không ai trả lời. Chưa phải thiết bị. |
Dấu ? ở đầu dòng chỉ có nghĩa là chưa có tên máy. Không phải cảnh báo.
Đọc thêm: đọc đầy đủ một dòng, cách đọc trên Windows và Linux (không bắt buộc)
xx.)| Phần | Ý nghĩa |
|---|---|
ifscope | Mục gắn với card mạng đó. Không phải tên thiết bị, cũng không phải lỗi. |
[ethernet] | Nhãn kiểu liên kết. Wi-Fi trên Mac cũng có thể hiện nhãn này. |
permanent | Mục do chính máy tạo, không hết hạn. Không có nghĩa thiết bị luôn bật. |
ff:ff:ff:ff:ff:ff | Địa chỉ "gọi cả mạng" (broadcast), không phải một máy. |
224.0.0.251 / 01:00:5e:… | Địa chỉ nhóm (multicast) cho mDNS, không phải một máy. |
Windows: mỗi hàng có Internet Address (IP), Physical Address (MAC), Type (dynamic là học tự động). Linux: ip neigh cho dòng kiểu 192.168.1.40 dev wlan0 lladdr 3c:22:fb:aa:bb:cc REACHABLE. lladdr là MAC. REACHABLE là mới xác nhận gần đây, STALE là cần xác nhận lại, FAILED là không ai trả lời.
Lệnh nào xem máy bạn, lệnh nào quét cả Wi-Fi?
Đây là chỗ dễ nhầm nhất. Cùng là "xem thiết bị", nhưng mỗi lệnh nhìn một phạm vi khác nhau:
| Lệnh | Làm gì | Nhìn thấy |
|---|---|---|
arp -a | Đọc lại sổ ARP đã có sẵn trong máy. Không gửi gì ra mạng. | Máy bạn. Chỉ thấy thiết bị máy bạn từng nói chuyện |
ping IP | Hỏi đúng một thiết bị. Máy bạn hỏi ARP để tìm nó, nên sổ có thêm một dòng | Một thiết bị |
| Vòng ping cả dải (bước 4 bên dưới) | Ping từng IP từ .1 đến .254, rồi đọc sổ ARP | Cả Wi-Fi, không cần cài gì |
nmap -sn (bước 5) | Gõ cửa cả dải IP, rồi in ai trả lời | Cả Wi-Fi (cùng dải mạng) |
lan_inventory.py (bước 6–7) | Gõ cửa cả dải, đọc lại sổ ARP, nhận diện từng thiết bị | Cả Wi-Fi |
--demo, --only 127.0.0.1 (bước 7) | Dữ liệu mẫu, hoặc chỉ quét cổng của chính máy bạn | Không quét Wi-Fi |
arp -a giống mở danh bạ điện thoại của bạn: chỉ có những số bạn từng gọi. Quét cả dải giống gọi thử từng số trong cả khu rồi xem ai bắt máy. Danh bạ ít số không có nghĩa là khu ít người.BƯỚC 3Thử trên máy của bạn
- Chạy lệnh ở trên. Chọn một dòng, chỉ ra đâu là IP, đâu là MAC.
- Nếu biết IP một thiết bị đang bật trong nhà, chạy
ping IP_THIET_BIvài giây rồi nhấn Ctrl+C. ThayIP_THIET_BIbằng địa chỉ thật. - Chạy
arp -alại. Dòng của thiết bị đó thường xuất hiện. Sổ của máy bạn vừa "lớn lên" vì bạn đã hỏi thêm một thiết bị. Quét cả dải (bước 6–7) chính là làm việc này cho mọi IP cùng lúc.
BƯỚC 4Quét cả Wi-Fi bằng vài dòng lệnh, chưa cần Python
Giờ làm điều ở bước 3: ping thử mọi IP trong dải, để máy bạn hỏi ARP tới từng cái. Rồi đọc lại sổ. Không cần cài gì thêm. Chỉ làm trên mạng của bạn.
Việc 1. Biết dải mạng của mình. Lấy IP của máy bạn:
ipconfig getifaddr en0ipconfighostname -IVí dụ ra 192.168.68.57, thì dải của bạn bắt đầu bằng 192.168.68. (Windows: tìm dòng "IPv4 Address"). Trong lệnh dưới, thay 192.168.68 bằng ba số đầu của máy bạn.
Việc 2. Ping thử cả dải (từ .1 đến .254), rồi xem sổ ARP:
( for i in $(seq 1 254); do ping -c 1 -t 1 192.168.68.$i >/dev/null 2>&1 & done; wait )
arp -a1..254 | ForEach-Object { ping.exe -n 1 -w 200 192.168.68.$_ | Out-Null }
arp -a( for i in $(seq 1 254); do ping -c 1 -W 1 192.168.68.$i >/dev/null 2>&1 & done; wait )
ip neigh| Phần | Ý nghĩa |
|---|---|
for i in … seq 1 254 | Lặp 254 lần, mỗi lần là một IP từ .1 đến .254. |
ping -c 1 … | Ping mỗi IP đúng một lần. Việc ping khiến máy bạn hỏi ARP tới IP đó. |
>/dev/null 2>&1 | Giấu kết quả ping cho màn hình khỏi bị tràn. Ta chỉ cần sổ ARP. |
& … wait | Ping song song cho nhanh, rồi chờ tất cả xong. |
arp -a | Đọc lại sổ. Lúc này sổ đã đầy hơn nhiều. |
Việc 3. So sánh. Đếm số thiết bị thật (theo bước 3) trước và sau khi ping cả dải. Số sau thường lớn hơn, vì giờ bạn đã hỏi cả khu chứ không chỉ đọc danh bạ cũ.
192.168.68.0/22 gồm .68, .69, .70, .71), hãy lặp lại với 192.168.69, .70, .71. Công cụ Python ở bước 6 tự đọc kích thước mạng và nhận diện luôn từng thiết bị, nên đỡ phải làm tay.Đọc thêm: vì sao ARP thấy cả thiết bị chặn ping? (không bắt buộc)
Ping dùng một cơ chế khác (ICMP), ARP thì hỏi địa chỉ MAC. Thiết bị có thể chặn ping nhưng vẫn trả lời ARP để nhận dữ liệu trong mạng. Dù vậy, máy đang ngủ hoặc ở mạng bị cách ly vẫn có thể bị sót. Kết quả luôn là "những gì quan sát được", không phải danh sách đầy đủ đã xác minh.
Bước 3 · Đếm cho đúng
15 phútKhông phải dòng nào trong bảng ARP cũng là một thiết bị. Dùng bảng này để lọc:
| Dòng trông như | Là gì | Tính là thiết bị? |
|---|---|---|
(incomplete) | Không ai trả lời | Không |
ff:ff:ff:ff:ff:ff, IP đuôi .255 | "Gọi cả mạng" (broadcast) | Không |
IP 224.…, MAC 01:00:5e… | Địa chỉ nhóm (multicast) | Không |
IP 169.254.… | IP tự đặt khi không có router cấp | Không |
| IP và MAC bình thường | Một thiết bị thật | Có |
BƯỚC 1Thử đọc một bảng ARP thật
Bảng này lấy từ máy một học viên (ba byte cuối của MAC được che bằng xx). Đếm xem có mấy thiết bị thật, rồi mở đáp án.
? (169.254.169.254) at (incomplete) on en0 [ethernet]
? (192.168.68.1) at bc:07:1d:xx:xx:xx on en0 ifscope [ethernet]
? (192.168.68.50) at e4:fe:43:xx:xx:xx on en0 ifscope [ethernet]
? (192.168.68.51) at e4:fe:43:xx:xx:xx on en0 ifscope [ethernet]
? (192.168.68.54) at 24:9e:7d:xx:xx:xx on en0 ifscope [ethernet]
? (192.168.68.58) at 6a:6c:4a:xx:xx:xx on en0 ifscope permanent [ethernet]
? (192.168.71.255) at ff:ff:ff:ff:ff:ff on en0 ifscope [ethernet]
mdns.mcast.net (224.0.0.251) at 1:0:5e:0:0:fb on en0 ifscope permanent [ethernet]Xem đáp án
| Dòng | Kết luận |
|---|---|
169.254.169.254 | Loại. incomplete và IP tự đặt |
.68.1 · bc:07:1d | Router. IP đuôi .1, hãng TP-Link |
.68.50 · e4:fe:43 | Thiết bị 1. Hãng Xiaomi |
.68.51 · e4:fe:43 | Thiết bị 2. Cùng hãng nhưng ba byte cuối khác, nên là máy khác |
.68.54 · 24:9e:7d | Thiết bị 3. Hãng Roborock, nhiều khả năng robot hút bụi |
.68.58 · permanent | Chính máy bạn (chữ permanent) |
192.168.71.255 | Loại. Broadcast |
224.0.0.251 | Loại. Multicast |
Đáp án: 5 thiết bị (router, hai thiết bị Xiaomi, một robot Roborock, và máy đang chạy lệnh). Còn một mẹo: dòng broadcast kết thúc ở .71.255 cho thấy mạng này to hơn /24 (xem Hình 2). Khi quét cả dải ở bước 7, chính mạng này ra tới 11 thiết bị: bảng ARP lúc đầu mới chỉ là một phần.
Vì sao đếm hoài vẫn lệch?
- Thiết bị đang ngủ. Điện thoại tắt màn hình hay bỏ qua gói tin. Quét 2–3 lần, cách nhau vài phút.
- MAC ngẫu nhiên. Một điện thoại có thể hiện như thiết bị mới sau khi đổi cài đặt (bước 4).
- Mạng khách. Máy ở mạng khách không thấy máy ở mạng chính, và ngược lại.
- Thêm router phụ hoặc Wi-Fi mesh. Một số thiết bị nằm ở dải IP khác.
BÀI TẬP 1 · 10 PHÚT
Đọc bảng ARP: bao nhiêu thiết bị thật?
Bảng ARP của một mạng nhà (địa chỉ minh họa):
? (192.168.1.1) at f4:ec:38:11:22:33
? (192.168.1.23) at 6a:6c:4a:21:dc:21 (máy đang chạy lệnh)
? (192.168.1.40) at 3c:22:fb:aa:bb:cc
? (192.168.1.41) at 3a:5b:7c:12:34:56
? (192.168.1.60) at 3a:5b:7c:12:34:56
? (192.168.1.77) at (incomplete)
? (192.168.1.255) at ff:ff:ff:ff:ff:ff
mdns.mcast.net (224.0.0.251) at 1:0:5e:0:0:fb- Mạng có bao nhiêu thiết bị thật?
- Hai dòng
.41và.60cùng một MAC cho biết điều gì? - Dòng
.77có nghĩa gì?
Xem đáp án
- Loại
.255,224.0.0.251và.77. Còn 5 IP, nhưng.41và.60cùng MAC, nên là 4 thiết bị: router, máy đang chạy lệnh, thiết bị.40, thiết bị.41/.60. - Một thiết bị giữ hai IP (ví dụ IP cũ chưa hết hạn). Vì vậy đếm theo MAC chính xác hơn đếm theo IP.
- Máy bạn đã hỏi ARP cho
.77nhưng không ai trả lời: IP trống, hoặc thiết bị đang ngủ. Không tính.
Bước 4 · Nhận diện: thiết bị này là gì?
30 phútCó IP và MAC rồi, giờ đoán: thiết bị gì, hãng nào, hệ điều hành nào. Không manh mối nào đủ chắc một mình, nên ta làm như thám tử: ghép bốn manh mối.
| Manh mối | Hỏi gì | Lấy ở đâu |
|---|---|---|
| 1. MAC | Hãng nào làm card mạng? | Ba byte đầu của MAC |
| 2. Tên máy | Thiết bị tự đặt tên gì? | Router, DNS, mDNS |
| 3. Cổng mở | Nó đang chạy dịch vụ gì? | Quét port |
| 4. TTL | Hệ điều hành nào? | Lệnh ping |
BƯỚC 1Manh mối 1 · MAC cho biết hãng
Ba byte đầu của MAC là mã hãng (), giống đầu số điện thoại cho biết nhà mạng. Ba byte sau là số riêng của từng card.
| Ba byte đầu | Tra ra hãng | Đoán loại thiết bị |
|---|---|---|
bc:07:1d | TP-Link | Router, bộ phát Wi-Fi |
e4:fe:43 | Xiaomi | Điện thoại, TV box, camera… cần thêm manh mối |
24:9e:7d | Roborock | Robot hút bụi |
2, 6, A hoặc E (như 6a:…, da:…) thì đó là MAC ngẫu nhiên. Bỏ qua manh mối 1, dùng ba manh mối còn lại. Đây là tính năng bảo vệ riêng tư, không phải thiết bị đáng ngờ.Đọc thêm: vì sao chữ thứ hai nói lên điều đó, và cách tự tra hãng (không bắt buộc)
Hai bit cuối của byte đầu là hai "công tắc":
| Chữ thứ hai | Ý nghĩa | Ví dụ |
|---|---|---|
0, 4, 8, C | MAC do hãng cấp: tra được hãng | bc, e4, 24 |
2, 6, A, E | MAC tự đặt: không tra được hãng | 6a, da, 3a |
1, 3, 5, 7, 9, B, D, F | Địa chỉ nhóm, không phải một thiết bị | 01, ff |
Tự tra hãng: tải danh sách công khai của IEEE (một lần, vài MB), rồi tìm ba byte đầu. File dùng dấu gạch nối và chữ hoa.
curl -o oui.txt https://standards-oui.ieee.org/oui/oui.txt
grep -i "^BC-07-1D" oui.txtcurl.exe -o oui.txt https://standards-oui.ieee.org/oui/oui.txt
Select-String -Path oui.txt -Pattern "^BC-07-1D"curl -o oui.txt https://standards-oui.ieee.org/oui/oui.txt
grep -i "^BC-07-1D" oui.txtBC-07-1D (hex) TP-Link Systems Inc.Hãng nhỏ đôi khi hiện tên "IEEE Registration Authority" vì dùng chung một mã hãng. Hãng chip Wi-Fi (Espressif, Realtek…) cũng có thể hiện thay cho hãng bán sản phẩm. Gặp tên lạ thì tra xem hãng đó làm gì. Người có chủ ý còn đổi được MAC, nên manh mối này là gợi ý, không phải chứng cứ.
BƯỚC 2Manh mối 2 · Tên máy
Nhiều thiết bị tự khai tên. Tên do người đặt, nên có thể sai, nhưng rất hữu ích:
| Mẫu tên | Gợi ý |
|---|---|
ten-macbook.local, MacBook-Air | Máy Mac |
iPhone-cua-Lan, iPad | Thiết bị Apple |
galaxy-s24, android-xxxx | Điện thoại Android |
npi-3f2a, HP…, EPSON… | Máy in |
DESKTOP-4F7A, PC-THUQUY | Máy tính Windows |
cam-phong-1, ipcam | Camera |
BƯỚC 3Manh mối 3 · Cổng mở
Mỗi dịch vụ mở một cửa (port) quen thuộc (bài 5). Thấy cửa nào mở thì đoán được máy làm gì.
| Port | Dịch vụ | Thường ở | Cần chú ý |
|---|---|---|---|
| 22 | SSH (quản trị từ xa, có mã hóa) | Máy Linux, NAS, router | Trung bình |
| 23 | Telnet (quản trị từ xa, không mã hóa) | Camera, router đời cũ | Cao: nên tắt |
| 80, 8080 | Web (không mã hóa) | Router, camera, máy in | Trung bình |
| 443 | Web (có mã hóa) | Như trên | Thấp |
| 445 | Chia sẻ file Windows | Máy Windows, NAS | Cao nếu mật khẩu yếu |
| 554 | Truyền video | Camera IP | Trung bình |
| 631, 9100 | In ấn | Máy in mạng | Thấp |
| 3389 | Remote Desktop (điều khiển từ xa Windows) | Máy Windows | Cao: hay bị dò mật khẩu |
| 62078 | Đồng bộ iPhone | iPhone, iPad | Thấp (chỉ là manh mối) |
BƯỚC 4Manh mối 4 · TTL cho biết họ hệ điều hành
Khi bạn ping, thiết bị trả lời kèm một con số gọi là . Mỗi hệ điều hành bắt đầu từ một số riêng, nên con số này gợi ý hệ điều hành:
| TTL thấy được | Nhóm hệ điều hành | Ví dụ |
|---|---|---|
| 64 | Linux, macOS, iOS, Android, và phần lớn router, camera | MacBook, iPhone, Android, router TP-Link |
| 128 | Windows | Máy tính Windows |
| 255 | Thiết bị mạng, nhiều máy in, và cả thiết bị nhúng nhỏ (robot hút bụi, ổ cắm thông minh) | Switch, máy in, robot Roborock, thiết bị Xiaomi |
ping -c 1 192.168.68.1ping -n 1 192.168.68.1ping -c 1 192.168.68.164 bytes from 192.168.68.1: icmp_seq=0 ttl=64 time=3.1 msĐọc: ttl=64, nên là nhóm Linux, macOS, iOS, Android. Trên Windows dòng này có dạng TTL=128. Số 64 chung cho nhiều hệ điều hành, và thiết bị chặn ping thì không có kết quả. Công cụ ở bước 6 tự ping và in nhóm này. Riêng số 255 dễ đoán nhầm: khi chạy thật ở nhà một học viên, robot hút bụi và thiết bị Xiaomi đều trả TTL 255 dù không phải máy in. Vì vậy hãy luôn ghép TTL với hãng và cổng.
Ghép các manh mối lại
| Thiết bị | Xem MAC Wi-Fi ở đâu |
|---|---|
| iPhone, iPad | Cài đặt → Wi-Fi → bấm (i) cạnh tên mạng → "Địa chỉ Wi-Fi" |
| Android | Cài đặt → Giới thiệu điện thoại → Trạng thái → Địa chỉ MAC Wi-Fi |
| Windows | ipconfig /all, dòng "Physical Address" của card Wi-Fi |
| macOS | ifconfig en0 | grep ether |
| Máy in, camera, TV | Menu thông tin, tem dưới thiết bị, hoặc trang web quản lý |
Đọc thêm: thêm manh mối: dòng chào dịch vụ, mDNS, nmap (không bắt buộc)
Dòng chào (). Nhiều dịch vụ tự giới thiệu khi bạn nối vào, ví dụ máy chủ web trả Server: lighttpd, SSH trả SSH-2.0-OpenSSH_9.6 Ubuntu. Python ở bước 6 đọc giúp bạn.
Thiết bị tự công bố (). Máy Apple, máy in, Chromecast… công bố loại dịch vụ của mình:
| Tên dịch vụ | Thiết bị thường gặp |
|---|---|
_airplay._tcp | Apple TV, loa, Mac |
_ipp._tcp, _printer._tcp | Máy in |
_googlecast._tcp | Chromecast, TV Google |
_smb._tcp, _ssh._tcp | Máy tính, NAS |
nmap đoán hệ điều hành kỹ hơn TTL. Chỉ dùng với thiết bị của bạn:
sudo nmap -O 192.168.1.40Kết quả có dạng Linux 4.15 - 5.19 kèm mức tin cậy. Thiết bị quá đặc biệt thì nmap báo "No exact OS matches".
Dấu hiệu gián tiếp: cổng 62078 mở → iPhone/iPad. Cổng 5000 và 7000 → Mac hoặc Apple TV. Banner có Microsoft-IIS → Windows. Banner có Ubuntu hoặc OpenSSH → Linux. Tất cả đều có thể bị sửa.
Dựa vào đâu để phân biệt? Sáu câu hỏi, hỏi theo thứ tự
Với mỗi thiết bị, tự hỏi lần lượt. Dừng lại khi đã đủ chắc:
| Câu hỏi | Nếu có thì nghiêng về | Chắc đến đâu |
|---|---|---|
1. IP có đuôi .1 (hoặc là IP gateway của máy bạn)? | Router | Rất chắc |
| 2. Hãng chỉ làm một loại thiết bị? (Roborock, Epson, Hikvision…) | Đúng loại đó: robot hút bụi, máy in, camera | Khá chắc |
| 3. Hãng làm nhiều loại? (Xiaomi, Samsung, Apple…) | Chưa kết luận được. Cần cổng, tên hoặc đối chiếu | Thấp |
| 4. Hãng mạng (TP-Link…) và mở cổng 80, 443? | Thiết bị mạng có trang quản lý: router, bộ phát Wi-Fi, mesh | Khá chắc |
5. MAC ngẫu nhiên (chữ thứ hai là 2, 6, A, E) và không mở cổng nào? | Điện thoại, máy tính bảng, laptop hiện đại | Trung bình |
| 6. Còn chưa rõ? | Đối chiếu với chính thiết bị hoặc app của router, ghi "chưa xác minh" | Chốt cuối |
Ví dụ trọn vẹn: mạng 192.168.68.0/22
Đây là kết quả quét thật một mạng nhà (11 thiết bị, ba byte cuối của MAC được che). Áp dụng sáu câu hỏi cho từng dòng:
| IP | MAC · hãng | Cổng mở | TTL | Kết luận và lý do | Cách xác minh |
|---|---|---|---|---|---|
.68.1 | bc:07:1d · TP-Link | 80, 443 | 64 | Router. IP đuôi .1 (câu 1), hãng mạng và có trang quản lý (câu 4) | Mở http://192.168.68.1 trên trình duyệt |
.71.248, .71.249, .71.250 | 60:a4:b7, bc:07:1d, bc:07:1d · TP-Link | 80, 443 | 64 | Ba bộ phát Wi-Fi mesh. Cùng hãng mạng, cùng mở 80 và 443 (câu 4). Ba MAC khác nhau nên là ba máy, không phải một máy có ba IP | App hoặc trang quản lý của TP-Link, mục danh sách thiết bị mesh |
.68.50 | 24:9e:7d · Roborock | không | 255 | Robot hút bụi. Hãng chỉ làm một loại (câu 2) | Mở app Roborock, so MAC |
.68.54, .68.55 | e4:fe:43 · Xiaomi | không | 255 | Hai thiết bị Xiaomi, chưa biết loại. Xiaomi làm nhiều loại (câu 3). Hai MAC khác nhau nên là hai máy | App Mi Home xem danh sách thiết bị |
.68.52, .68.56, .68.59 | 76:…, 62:…, 42:… · MAC ngẫu nhiên | không | 64 | Điện thoại hoặc máy tính bảng. Chữ thứ hai là 6, 2, 2 (câu 5) | Hỏi người trong nhà. Xem MAC Wi-Fi trong cài đặt máy |
.68.57 | 6a:… · MAC ngẫu nhiên | 5000, 7000 | 64 | Chính máy đang chạy lệnh (một Mac). Cổng AirPlay 5000 và 7000, chữ permanent trong arp -a | Đối chiếu IP của máy bằng ipconfig getifaddr en0 |
- Chỉ có router và robot là gần như chắc chắn. Còn lại là "nhiều khả năng", muốn chắc phải xác minh.
- Cùng hãng, khác MAC nghĩa là nhiều máy, như ba bộ mesh TP-Link và hai thiết bị Xiaomi.
- TTL 255 của Roborock và Xiaomi không phải máy in. Thiết bị nhúng nhỏ cũng dùng số này, nên TTL chỉ là manh mối phụ.
- Mỗi lần quét có thể ra số khác nhau: mạng này lần đầu thấy 5, lần sau 8, rồi 11, vì điện thoại ngủ và bộ mesh trả lời chậm. Hãy quét vài lần rồi ghi "ít nhất N".
BÀI TẬP 2 · 10 PHÚT
Nhận diện năm thiết bị: loại và hệ điều hành
Kết quả quét (hãng và tên là minh họa). TTL là TTL của gói trả lời ping:
| # | MAC / hãng | Tên | Port mở | TTL |
|---|---|---|---|---|
| A | Hãng máy in X | npi-3f2a | 80, 631, 9100 | 255 |
| B | MAC ngẫu nhiên | (không tên) | 62078 | 64 |
| C | Hãng camera Y | cam-phong-1 | 23, 80, 554 | 64 |
| D | MAC ngẫu nhiên | galaxy-s24 | không có | 64 |
| E | Intel | PC-THUQUY | 445, 3389 | 128 |
Với mỗi thiết bị: đoán loại, đoán nhóm hệ điều hành, nêu bằng chứng, và nói thiết bị nào cần xem xét về bảo mật.
Xem đáp án
- A: máy in mạng. Hãng máy in, tên kiểu máy in, port 631 và 9100, TTL 255. Rất chắc. Nên đặt mật khẩu cho trang quản lý ở port 80.
- B: nhiều khả năng iPhone hoặc iPad. Port 62078 là của Apple, MAC ngẫu nhiên khớp "Địa chỉ Wi-Fi riêng", TTL 64. Khá chắc, nên đối chiếu trong cài đặt máy.
- C: camera IP. Hãng camera, tên có chữ cam, port 554. Port 23 (Telnet) mở là rủi ro lớn: không mã hóa, hay dùng mật khẩu mặc định. Cần tắt Telnet, đổi mật khẩu, cập nhật firmware.
- D: điện thoại Android. Tên gợi ý, MAC ngẫu nhiên, không mở cổng nào (bình thường), TTL 64. Chắc ở mức vừa vì tên do người đặt.
- E: máy Windows của thủ quỹ. Cần kiểm tra: có thật sự cần Remote Desktop không, mật khẩu có mạnh không. Máy giữ dữ liệu lệ phí nên ưu tiên cao.
Bước 5 · Công cụ có sẵn: nhìn nhanh, chưa cần code
8 phútTrước khi tự viết code, biết vài công cụ có sẵn để đối chiếu kết quả sau này. Phần này chỉ xem, không thay đổi gì trên thiết bị nào.
| Công cụ | Làm gì | Nhìn thấy | Cần gì |
|---|---|---|---|
arp -a (bước 2) | Đọc sổ ARP có sẵn, không gửi gì ra mạng | Máy bạn: chỉ thiết bị đã nói chuyện | Không cần gì thêm |
nmap -sn | Gõ cửa cả dải, in MAC và hãng | Cả Wi-Fi | Cài nmap (bài 6) |
| Fing, Advanced IP Scanner, LanScan | Ứng dụng có giao diện, tra hãng sẵn | Cả Wi-Fi | Chỉ cài từ trang chính thức |
| Trang quản trị router | Danh sách thiết bị router đã cấp IP | Cả Wi-Fi, theo góc nhìn của router | Mật khẩu router (không bắt buộc) |
Quét ping bằng nmap
sudo nmap -sn 192.168.1.0/24nmap -sn 192.168.1.0/24sudo nmap -sn 192.168.1.0/24Thay 192.168.1.0/24 bằng dải mạng của bạn (xem IP máy ở bài 6). sudo hỏi mật khẩu đăng nhập máy tính của bạn, không phải mật khẩu router.
Nmap scan report for 192.168.1.1
Host is up (0.0021s latency).
MAC Address: F4:EC:38:11:22:33 (Hang router A)
Nmap scan report for 192.168.1.40
Host is up (0.11s latency).
MAC Address: 3C:22:FB:AA:BB:CC (Hang dien thoai B)
Nmap done: 256 IP addresses (2 hosts up) scanned in 2.4 secondsĐọc thêm: xem thiết bị tự công bố bằng mDNS (không bắt buộc)
Liệt kê loại dịch vụ đang có trong mạng:
dns-sd -B _services._dns-sd._udp local# Windows không có sẵn công cụ tương đương.
# Dùng ứng dụng như Advanced IP Scanner.avahi-browse -a -tĐọc: _airplay là thiết bị nhận AirPlay, _ipp là máy in, _googlecast là Chromecast hoặc TV Google.
Bước 6 · Lab: tạo công cụ kiểm kê bằng Python
12 phútGiờ ghép mọi thứ thành một chương trình lan_inventory.py. Một lệnh ra bảng thiết bị (IP, MAC, hãng, tên, cổng mở), rồi lưu lại để so sánh lần sau. Chỉ dùng thư viện có sẵn của Python. Không cần mật khẩu router.
- Gõ cửa cả dải IP bằng một gói UDP nhỏ, để máy bạn tự hỏi ARP.
- Đọc bảng ARP và loại các dòng không phải thiết bị.
- Nhận diện từng thiết bị: hãng, tên, cổng mở, TTL.
- Lưu ra file JSON và so với lần trước.
Xem trước: một lần kiểm kê diễn ra thế nào?
Theo chân chiếc laptop: từ hỏi địa chỉ đến ghi sổ. Mỗi cảnh tương ứng một việc trong code.
local_ip() → detect_network(ip)Chương trình đọc IP và mặt nạ mạng của máy bạn. /24 trong ví dụ có địa chỉ từ .0 đến .255; không lấy dải ví dụ để áp vào mọi nhà.
Điều cần hiểu: Biết phạm vi cần hỏi trước khi tìm thiết bị.
poke(ip)Gửi UDP khiến hệ điều hành tìm MAC bằng ARP nếu chưa có trong bộ nhớ. Việc này được lặp cho các IP trong dải; thiết bị chặn ping vẫn có thể trả lời ARP.
Điều cần hiểu: Học được cặp IP ↔ MAC của thiết bị trả lời.
read_neighbors() → is_unicast()Một dòng ARP chưa chắc là một thiết bị. Chương trình bỏ dòng chưa có MAC và địa chỉ quảng bá/nhóm; nhận diện theo MAC đầy đủ.
Điều cần hiểu: Có danh sách thiết bị quan sát được, có thể còn thiếu.
scan_ports(ip) → guess_device(device)Cổng mở nghĩa là một dịch vụ chấp nhận kết nối. Hãng, tên, cổng và TTL cùng gợi ý loại thiết bị; chúng chưa chứng minh được danh tính.
Điều cần hiểu: Kết luận: nhiều khả năng là máy in; cần đối chiếu.
--save baseline.jsonFile JSON là cuốn sổ lưu kết quả, không phải một chương trình khác. Lần chạy sau dùng --baseline baseline.json để đọc sổ cũ và so với quan sát mới.
Điều cần hiểu: Chu trình: hỏi → đọc → nhận diện → lưu → so sánh.
Hình minh họa không chạy Python và không gửi gói tin. Bấm Tiếp để học theo nhịp của bạn; Phát tự chuyển cảnh mỗi 9 giây.
BƯỚC 1Tạo thư mục làm việc
mkdir ~/lan-lab
cd ~/lan-lab
pwdmkdir $HOME\lan-lab
cd $HOME\lan-lab
pwdmkdir ~/lan-lab
cd ~/lan-lab
pwdmkdir tạo thư mục, cd đi vào, pwd in ra bạn đang đứng ở đâu (bài 6). Mọi file của lab nằm trong thư mục này.
BƯỚC 2Tạo file lan_inventory.py
Cách dễ nhất: bấm tải file dưới đây, chuyển vào thư mục lan-lab. Muốn xem chương trình, mở “Xem mã Python”. Nếu tự dán mã vào trình soạn thảo, lưu đuôi .py, không phải .txt. Nếu chọn “Tạo file bằng terminal”, sao chép trọn khối lệnh của hệ điều hành bạn đang dùng.
lan_inventory.py · Tải file vào thư mục lan-lab, hoặc mở mã để sao chép vào trình soạn thảo.
Xem mã Python · có tô màu và số dòng
# lan_inventory.py - liệt kê thiết bị trong mạng nhà của BẠN:
# IP, MAC, hãng sản xuất, tên, port mở.
# Chỉ chạy trên mạng của chính bạn hoặc nơi được cho phép.
# Chỉ dùng thư viện có sẵn của Python 3.
import argparse
import ipaddress
import json
import platform
import re
import socket
import subprocess
import time
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
# Bảng port quen thuộc: số port -> tên dịch vụ thường chạy ở đó.
# Công cụ chỉ thử các port có trong bảng này.
PORTS = {21: "ftp", 22: "ssh", 23: "telnet", 53: "dns", 80: "http", 139: "netbios", 443: "https",
445: "smb", 554: "rtsp (camera)", 631: "ipp (may in)", 3389: "rdp", 5000: "http-alt/airplay",
7000: "airplay", 8000: "http-alt", 8008: "chromecast", 8080: "http-proxy", 8443: "https-alt",
9100: "may in", 62078: "iphone-sync"}
MAX_HOSTS = 1024 # giới hạn số địa chỉ mỗi lần quét, để gõ nhầm dải quá lớn không làm nghẽn mạng
def local_ip():
# Mục đích: biết IP của máy mình trong mạng LAN (một máy có thể có nhiều card mạng).
# connect() với UDP KHÔNG gửi gói nào ra mạng; nó chỉ bắt hệ điều hành chọn
# card mạng sẽ dùng để tới địa chỉ 192.0.2.1 (địa chỉ dành riêng cho tài liệu,
# không ai dùng). Sau đó ta đọc IP của card mạng đó.
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.connect(("192.0.2.1", 9))
return s.getsockname()[0]
def detect_network(ip):
# Đọc mặt nạ mạng thật của card mạng (cho biết dải IP gồm bao nhiêu địa chỉ).
# Không đọc được thì coi là /24 (256 địa chỉ, mạng nhà phổ biến nhất).
system = platform.system()
try:
if system == "Linux":
out = subprocess.run(["ip", "-o", "-4", "addr"], capture_output=True, text=True).stdout
m = re.search(re.escape(ip) + r"/(\d+)", out)
if m:
return ipaddress.ip_network(f"{ip}/{m.group(1)}", strict=False)
elif system == "Windows":
lines = subprocess.run(["ipconfig"], capture_output=True, text=True).stdout.splitlines()
for i, line in enumerate(lines):
if ip in line:
for nxt in lines[i + 1:i + 3]:
m = re.search(r"(255\.\d+\.\d+\.\d+)", nxt)
if m:
return ipaddress.ip_network(f"{ip}/{m.group(1)}", strict=False)
else:
out = subprocess.run(["ifconfig"], capture_output=True, text=True).stdout
m = re.search(r"inet " + re.escape(ip) + r" netmask (0x[0-9a-f]+)", out)
if m:
prefix = bin(int(m.group(1), 16)).count("1")
return ipaddress.ip_network(f"{ip}/{prefix}", strict=False)
except (OSError, ValueError):
pass
return ipaddress.ip_network(f"{ip}/24", strict=False)
def normalize_mac(raw):
# macOS in MAC không đệm số 0 ("a:b:c:1:2:3"), Windows dùng dấu gạch nối: đưa về
# dạng chuẩn aa:bb:cc:dd:ee:ff
parts = re.split(r"[:-]", raw.lower())
return ":".join(p.zfill(2) for p in parts) if len(parts) == 6 else None
def read_neighbors():
system = platform.system()
if system == "Linux":
out = subprocess.run(["ip", "neigh"], capture_output=True, text=True).stdout
pattern = r"^(\d+\.\d+\.\d+\.\d+) .*lladdr ([0-9a-f:]{11,17})"
else:
out = subprocess.run(["arp", "-a"], capture_output=True, text=True).stdout
pattern = r"(\d+\.\d+\.\d+\.\d+)\)? (?:at )?\s*([0-9a-fA-F]{1,2}(?:[:-][0-9a-fA-F]{1,2}){5})"
found = {}
for line in out.splitlines():
m = re.search(pattern, line)
if m and normalize_mac(m.group(2)):
found[m.group(1)] = normalize_mac(m.group(2))
return found
def is_unicast(ip, mac):
a = ipaddress.ip_address(ip)
return not (a.is_multicast or ip.endswith(".255") or mac == "ff:ff:ff:ff:ff:ff" or mac.startswith("01:00:5e"))
def poke(ip):
# MỤC ĐÍCH: "gõ cửa" một IP để máy phải hỏi ARP "ai giữ IP này?".
# Thiết bị nào có thật cũng trả lời ARP (kể cả khi nó chặn ping), nên MAC của
# nó sẽ nằm trong bảng ARP và ta đọc được.
try:
# [1] Tạo socket UDP (SOCK_DGRAM): chỉ "ném" gói đi, không bắt tay, không
# chờ trả lời
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
# [2] Gửi 1 byte rỗng tới port 9. Nội dung không quan trọng; điều quan
# trọng là hệ điều hành phải hỏi ARP để biết MAC của IP này TRƯỚC
# khi đưa gói ra card mạng
s.sendto(b"\0", (ip, 9))
except OSError:
# [3] Thiết bị trả lời "port này không mở" cũng có thể gây lỗi; ta không cần
# kết quả đó -> bỏ qua
pass
def load_oui(path):
table = {}
if path and Path(path).exists():
for line in Path(path).read_text(encoding="utf-8", errors="ignore").splitlines():
if "(hex)" in line:
prefix, _, name = line.partition("(hex)")
table[prefix.strip().replace("-", ":").lower()] = name.strip()
return table
def describe_mac(mac, oui):
# [1] Đọc byte đầu của MAC. Bit 0x02 bật = MAC do thiết bị tự đặt/ngẫu nhiên
# (điện thoại hay làm vậy để khó bị theo dõi) -> không có hãng thật nào để
# tra, trả lời luôn
if int(mac[:2], 16) & 0x02:
return "MAC rieng/ngau nhien (dien thoai thuong bat tinh nang nay)"
# [2] MAC thật: 8 ký tự đầu ("aa:bb:cc" = 3 byte OUI) là mã hãng; tra trong bảng
# oui.txt (không có thì nói rõ lý do)
return oui.get(mac[:8], "khong co trong oui.txt" if oui else "chua co oui.txt")
def ttl_of(ip):
# Gửi đúng 1 gói ping tới thiết bị ĐÃ TÌM THẤY để đọc TTL.
# (Việc tìm thiết bị không dựa vào ping.)
# [1] Mỗi hệ điều hành viết lệnh ping khác nhau: -n (Windows) / -c (macOS,
# Linux) là số gói; -w, -t, -W là thời gian chờ
system = platform.system()
if system == "Windows":
cmd = ["ping", "-n", "1", "-w", "1000", ip]
elif system == "Darwin":
cmd = ["ping", "-c", "1", "-t", "2", ip]
else:
cmd = ["ping", "-c", "1", "-W", "1", ip]
# [2] Chạy lệnh ping và đọc chữ nó in ra; chờ tối đa 4 giây. Lỗi hoặc quá hạn
# thì trả None (không rõ)
try:
out = subprocess.run(cmd, capture_output=True, text=True, timeout=4).stdout
except (OSError, subprocess.SubprocessError):
return None
# [3] Tìm trong chữ đó đoạn "ttl=64" (hoặc "TTL=128"...) bằng regex; re.I =
# không phân biệt hoa thường
m = re.search(r"ttl[=:]\s*(\d+)", out, re.I)
# [4] Có thì trả về con số TTL, không thì trả None
return int(m.group(1)) if m else None
def guess_os(ttl):
# TTL khởi đầu của gói trả lời: ~64 Linux/macOS/iOS/Android, ~128 Windows,
# ~255 thiết bị mạng. Chỉ là gợi ý. (Gói đi qua mỗi router bị trừ 1 nên số
# nhận được có thể nhỏ hơn số khởi đầu một chút, vì vậy ta chia theo KHOẢNG.)
# [1] Không đọc được TTL (thiết bị chặn ping): không đoán bừa
if ttl is None:
return "khong ro (thiet bi chan ping)"
# [2] TTL từ 64 trở xuống: nhóm Linux/macOS/iOS/Android
if ttl <= 64:
return f"nhom Linux/macOS/iOS/Android (TTL {ttl})"
# [3] TTL trên 64 đến 128: nhóm Windows
if ttl <= 128:
return f"nhom Windows (TTL {ttl})"
# [4] Còn lại (tới 255): thiết bị mạng hoặc máy in
return f"thiet bi mang/may in (TTL {ttl})"
# Bảng 1 - đoán theo CỔNG: thiết bị mở MỘT TRONG các cổng bên trái
# thì gợi ý nhãn bên phải.
HINTS = [({631, 9100}, "may in"), ({554}, "camera"), ({62078}, "iPhone/iPad"), ({8008, 8009}, "Chromecast/TV Google"),
({3389}, "may Windows (Remote Desktop)"), ({445, 139}, "chia se file Windows/NAS"), ({22}, "may Linux/NAS/router (SSH)"),
({5000, 7000}, "Mac/Apple TV (AirPlay)"), ({23}, "thiet bi cu (Telnet): coi chung")]
# Bảng 2 - đoán theo TÊN HOST: tên (viết thường) có chứa từ khóa bên trái
# thì gợi ý nhãn bên phải.
NAME_HINTS = [("iphone", "iPhone"), ("ipad", "iPad"), ("galaxy", "Samsung Galaxy"), ("android", "Android"),
("macbook", "Mac"), ("printer", "may in"), ("tv", "TV"), ("cam", "camera"), ("router", "router")]
def guess_device(device):
# Ghép các manh mối thành nhãn gợi ý: cổng mở (HINTS) + tên host (NAME_HINTS).
# [1] Lấy tập các số port đang mở của thiết bị
open_ports = {p["port"] for p in device["ports"]}
# [2] Với mỗi dòng của HINTS: nếu tập cổng của dòng đó GIAO với cổng đang mở
# (phép &) thì lấy nhãn của dòng đó
clues = [label for ports, label in HINTS if open_ports & ports]
# [3] Đổi tên host sang chữ thường để so khớp không phân biệt hoa/thường
name = device["hostname"].lower()
# [4] Với mỗi từ khóa của NAME_HINTS có trong tên host: thêm nhãn tương ứng
clues += [label for key, label in NAME_HINTS if key in name]
# [5] set() bỏ nhãn trùng, sorted() sắp xếp để kết quả ổn định giữa các lần chạy
return sorted(set(clues))
def hostname(ip):
try:
return socket.gethostbyaddr(ip)[0]
except OSError:
return ""
def grab_banner(ip, port):
try:
with socket.create_connection((ip, port), timeout=0.6) as s:
s.settimeout(0.6)
if port in (80, 8000, 8008, 8080):
s.sendall(b"HEAD / HTTP/1.0\r\n\r\n")
data = s.recv(200).decode("utf-8", errors="replace")
except OSError:
return ""
for line in data.splitlines():
if line.lower().startswith("server:"):
return line[7:].strip()
return data.splitlines()[0][:60] if data else ""
def scan_ports(ip):
# Tìm các port đang mở của MỘT thiết bị: thử kết nối TCP tới từng port trong PORTS.
def probe(port):
# [1] Thử kết nối TCP tới 1 port, chờ tối đa 0,5 giây. connect_ex() trả 0
# nếu kết nối thành công (= port mở), trả mã lỗi khác 0 nếu không (không
# ném exception như connect()). Mở -> trả số port, không mở -> None
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(0.5)
return port if s.connect_ex((ip, port)) == 0 else None
# [2] Thử mọi port CÙNG LÚC bằng nhiều luồng (thread), không thử lần lượt: nhanh
# hơn rất nhiều vì phần lớn thời gian là ĐỢI
with ThreadPoolExecutor(max_workers=len(PORTS)) as pool:
# [3] Giữ lại port mở (bỏ None), sắp xếp từ nhỏ đến lớn
open_ports = sorted(p for p in pool.map(probe, PORTS) if p)
# [4] Với mỗi port mở: ghi tên dịch vụ quen thuộc và đọc "banner" (câu tự giới
# thiệu của dịch vụ, nếu có)
return [{"port": p, "service": PORTS[p], "banner": grab_banner(ip, p)} for p in open_ports]
def build_inventory(network, oui, with_ports):
hosts = [str(h) for h in network.hosts()] or [str(network.network_address)]
# [1] "Gõ cửa" mọi địa chỉ trong dải cùng lúc bằng 64 luồng (poke() ở trên)
with ThreadPoolExecutor(max_workers=64) as pool:
list(pool.map(poke, hosts))
# [2] Đợi 2 giây để các thiết bị kịp trả lời ARP và hệ điều hành kịp ghi vào
# bảng ARP
time.sleep(2)
# [3] Đọc bảng ARP (IP -> MAC), chỉ giữ IP thuộc dải đang quét và bỏ địa chỉ
# broadcast/multicast (không phải thiết bị thật)
neighbors = {ip: mac for ip, mac in read_neighbors().items() if ip in hosts and is_unicast(ip, mac)}
me = local_ip()
if me in hosts:
neighbors.setdefault(me, "(may nay)")
devices = []
for ip in sorted(neighbors, key=lambda x: ipaddress.ip_address(x)):
mac = neighbors[ip]
is_self = mac == "(may nay)"
devices.append({"ip": ip, "mac": mac,
"vendor": "may cua ban" if is_self else describe_mac(mac, oui),
"hostname": hostname(ip),
"ports": scan_ports(ip) if with_ports else []})
devices[-1]["os_guess"] = guess_os(ttl_of(ip)) if with_ports else ""
devices[-1]["guess"] = guess_device(devices[-1])
return devices
def _dev(ip, mac, vendor, host, ports=(), ttl=None):
d = {"ip": ip, "mac": mac, "vendor": vendor, "hostname": host,
"ports": [{"port": p, "service": PORTS.get(p, "?"), "banner": b} for p, b in ports]}
d["os_guess"] = guess_os(ttl)
d["guess"] = guess_device(d)
return d
def demo_inventory(day):
# Dữ liệu MẪU để học đọc kết quả, không quét mạng thật. Hãng và tên là giả định.
private = "MAC rieng/ngau nhien (dien thoai thuong bat tinh nang nay)"
devices = [
_dev("192.168.1.1", "f4:ec:38:11:22:33", "Hang router A", "router.lan", [(80, "Server: lighttpd")], 64),
_dev("192.168.1.23", "6a:6c:4a:21:dc:21", private, "macbook-cua-ban.lan", ttl=64),
_dev("192.168.1.40", "3c:22:fb:aa:bb:cc", "Hang may in X", "npi-3f2a",
[(80, "Server: Embedded HTTP Server"), (631, ""), (9100, "")], 255),
_dev("192.168.1.45", "9c:b2:00:44:55:66", "Hang camera Y", "cam-phong-1",
[(23, ""), (80, "Server: CamHTTPD/1.0"), (554, "")], 64),
_dev("192.168.1.60", "3a:5b:7c:12:34:56", private, "galaxy-s24.lan", ttl=64),
_dev("192.168.1.70", "00:1b:21:aa:bb:cc", "Hang may tinh Z", "PC-THUQUY", [(445, ""), (3389, "")], 128),
]
if day == 1:
devices.append(_dev("192.168.1.52", "da:a1:19:00:00:01", private, "", [(62078, "")], 64))
else:
devices[-1] = _dev("192.168.1.70", "00:1b:21:aa:bb:cc", "Hang may tinh Z", "PC-THUQUY",
[(445, ""), (8000, "Server: SimpleHTTP/0.6 Python/3.9.6")], 128)
devices.append(_dev("192.168.1.77", "c2:11:aa:bb:cc:dd", private, "", ttl=64))
return sorted(devices, key=lambda d: ipaddress.ip_address(d["ip"]))
def show(devices):
print(f"{'IP':<16}{'MAC':<19}{'Hang / ghi chu':<34}Ten")
for d in devices:
print(f"{d['ip']:<16}{d['mac']:<19}{d['vendor'][:32]:<34}{d['hostname'][:30]}")
for p in d["ports"]:
print(f" mo port {p['port']:<6}{p['service']:<20}{p['banner']}")
if d.get("guess") or d.get("os_guess"):
print(f" DOAN: {', '.join(d.get('guess', [])) or 'chua du dau hieu'} | HDH: {d.get('os_guess') or '-'}")
print(f"\nTong cong: {len(devices)} thiet bi tra loi.")
def compare(devices, baseline_path):
old = {d["mac"]: d for d in json.loads(Path(baseline_path).read_text(encoding="utf-8"))}
now = {d["mac"]: d for d in devices}
new = [d for mac, d in now.items() if mac not in old]
gone = [d for mac, d in old.items() if mac not in now]
print("\nThiet bi MOI so voi lan truoc:", [f"{d['ip']} ({d['mac']})" for d in new] or "khong co")
print("Thiet bi KHONG con tra loi:", [f"{d['ip']} ({d['mac']})" for d in gone] or "khong co")
for mac in sorted(now.keys() & old.keys()):
before = {p["port"] for p in old[mac]["ports"]}
after = {p["port"] for p in now[mac]["ports"]}
if before != after:
print(f"Port THAY DOI o {now[mac]['ip']}: moi mo {sorted(after - before)}, da dong {sorted(before - after)}")
def main():
ap = argparse.ArgumentParser(description="Liet ke thiet bi trong mang nha cua ban")
ap.add_argument("--net", help="dai mang, vi du 192.168.1.0/24 (mac dinh: tu doc mat na mang cua may ban)")
ap.add_argument("--oui", default="oui.txt", help="file oui.txt cua IEEE de tra hang san xuat")
ap.add_argument("--no-ports", action="store_true", help="khong quet port")
ap.add_argument("--save", default="inventory.json", help="file luu ket qua")
ap.add_argument("--baseline", help="file ket qua lan truoc de so sanh thiet bi moi")
ap.add_argument("--demo", type=int, choices=[1, 2], help="dung du lieu MAU (ngay 1 hoac ngay 2), khong quet mang that")
ap.add_argument("--only", help="chi quet port cua mot IP rieng (vi du 127.0.0.1), bo qua buoc tim thiet bi")
args = ap.parse_args()
if args.only:
ip = ipaddress.ip_address(args.only)
if not ip.is_private:
raise SystemExit("Chi quet IP rieng (192.168.x.x, 10.x.x.x, 127.0.0.1...).")
found = scan_ports(str(ip))
print(f"Port mo tren {ip}:", ", ".join(f"{p['port']}/{p['service']}" for p in found) or "khong co")
for p in found:
print(f" {p['port']:<6}{p['service']:<20}{p['banner']}")
guess = guess_device({"ports": found, "hostname": hostname(str(ip))})
print("DOAN:", ", ".join(guess) or "chua du dau hieu")
return
if args.demo:
print(f"[DU LIEU MAU ngay {args.demo}: khong quet mang that]")
devices = demo_inventory(args.demo)
show(devices)
Path(args.save).write_text(json.dumps(devices, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"Da luu {args.save}")
if args.baseline:
compare(devices, args.baseline)
return
# [1] Dải mạng cần quét: người dùng gõ --net thì dùng, không thì tự đọc từ card
# mạng của máy
network = ipaddress.ip_network(args.net, strict=False) if args.net else detect_network(local_ip())
# [2] Chốt chặn an toàn: chỉ nhận dải IP RIÊNG và tối đa 1024 địa chỉ; ngoài ra
# thoát ngay, chưa gửi gói nào
if not network.is_private or network.num_addresses > MAX_HOSTS:
raise SystemExit("Chi quet dai IP rieng (192.168.x.x, 10.x.x.x...) va khong qua 1024 dia chi.")
print(f"Dang kiem tra {network} ({network.num_addresses} dia chi), cho khoang 5-30 giay...")
devices = build_inventory(network, load_oui(args.oui), not args.no_ports)
show(devices)
Path(args.save).write_text(json.dumps(devices, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"Da luu {args.save}")
if args.baseline:
compare(devices, args.baseline)
if __name__ == "__main__":
main()
Tạo file bằng terminal · chọn hệ điều hành của bạn
cat > lan_inventory.py <<'EOF'
# lan_inventory.py - liệt kê thiết bị trong mạng nhà của BẠN:
# IP, MAC, hãng sản xuất, tên, port mở.
# Chỉ chạy trên mạng của chính bạn hoặc nơi được cho phép.
# Chỉ dùng thư viện có sẵn của Python 3.
import argparse
import ipaddress
import json
import platform
import re
import socket
import subprocess
import time
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
# Bảng port quen thuộc: số port -> tên dịch vụ thường chạy ở đó.
# Công cụ chỉ thử các port có trong bảng này.
PORTS = {21: "ftp", 22: "ssh", 23: "telnet", 53: "dns", 80: "http", 139: "netbios", 443: "https",
445: "smb", 554: "rtsp (camera)", 631: "ipp (may in)", 3389: "rdp", 5000: "http-alt/airplay",
7000: "airplay", 8000: "http-alt", 8008: "chromecast", 8080: "http-proxy", 8443: "https-alt",
9100: "may in", 62078: "iphone-sync"}
MAX_HOSTS = 1024 # giới hạn số địa chỉ mỗi lần quét, để gõ nhầm dải quá lớn không làm nghẽn mạng
def local_ip():
# Mục đích: biết IP của máy mình trong mạng LAN (một máy có thể có nhiều card mạng).
# connect() với UDP KHÔNG gửi gói nào ra mạng; nó chỉ bắt hệ điều hành chọn
# card mạng sẽ dùng để tới địa chỉ 192.0.2.1 (địa chỉ dành riêng cho tài liệu,
# không ai dùng). Sau đó ta đọc IP của card mạng đó.
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.connect(("192.0.2.1", 9))
return s.getsockname()[0]
def detect_network(ip):
# Đọc mặt nạ mạng thật của card mạng (cho biết dải IP gồm bao nhiêu địa chỉ).
# Không đọc được thì coi là /24 (256 địa chỉ, mạng nhà phổ biến nhất).
system = platform.system()
try:
if system == "Linux":
out = subprocess.run(["ip", "-o", "-4", "addr"], capture_output=True, text=True).stdout
m = re.search(re.escape(ip) + r"/(\d+)", out)
if m:
return ipaddress.ip_network(f"{ip}/{m.group(1)}", strict=False)
elif system == "Windows":
lines = subprocess.run(["ipconfig"], capture_output=True, text=True).stdout.splitlines()
for i, line in enumerate(lines):
if ip in line:
for nxt in lines[i + 1:i + 3]:
m = re.search(r"(255\.\d+\.\d+\.\d+)", nxt)
if m:
return ipaddress.ip_network(f"{ip}/{m.group(1)}", strict=False)
else:
out = subprocess.run(["ifconfig"], capture_output=True, text=True).stdout
m = re.search(r"inet " + re.escape(ip) + r" netmask (0x[0-9a-f]+)", out)
if m:
prefix = bin(int(m.group(1), 16)).count("1")
return ipaddress.ip_network(f"{ip}/{prefix}", strict=False)
except (OSError, ValueError):
pass
return ipaddress.ip_network(f"{ip}/24", strict=False)
def normalize_mac(raw):
# macOS in MAC không đệm số 0 ("a:b:c:1:2:3"), Windows dùng dấu gạch nối: đưa về
# dạng chuẩn aa:bb:cc:dd:ee:ff
parts = re.split(r"[:-]", raw.lower())
return ":".join(p.zfill(2) for p in parts) if len(parts) == 6 else None
def read_neighbors():
system = platform.system()
if system == "Linux":
out = subprocess.run(["ip", "neigh"], capture_output=True, text=True).stdout
pattern = r"^(\d+\.\d+\.\d+\.\d+) .*lladdr ([0-9a-f:]{11,17})"
else:
out = subprocess.run(["arp", "-a"], capture_output=True, text=True).stdout
pattern = r"(\d+\.\d+\.\d+\.\d+)\)? (?:at )?\s*([0-9a-fA-F]{1,2}(?:[:-][0-9a-fA-F]{1,2}){5})"
found = {}
for line in out.splitlines():
m = re.search(pattern, line)
if m and normalize_mac(m.group(2)):
found[m.group(1)] = normalize_mac(m.group(2))
return found
def is_unicast(ip, mac):
a = ipaddress.ip_address(ip)
return not (a.is_multicast or ip.endswith(".255") or mac == "ff:ff:ff:ff:ff:ff" or mac.startswith("01:00:5e"))
def poke(ip):
# MỤC ĐÍCH: "gõ cửa" một IP để máy phải hỏi ARP "ai giữ IP này?".
# Thiết bị nào có thật cũng trả lời ARP (kể cả khi nó chặn ping), nên MAC của
# nó sẽ nằm trong bảng ARP và ta đọc được.
try:
# [1] Tạo socket UDP (SOCK_DGRAM): chỉ "ném" gói đi, không bắt tay, không
# chờ trả lời
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
# [2] Gửi 1 byte rỗng tới port 9. Nội dung không quan trọng; điều quan
# trọng là hệ điều hành phải hỏi ARP để biết MAC của IP này TRƯỚC
# khi đưa gói ra card mạng
s.sendto(b"\0", (ip, 9))
except OSError:
# [3] Thiết bị trả lời "port này không mở" cũng có thể gây lỗi; ta không cần
# kết quả đó -> bỏ qua
pass
def load_oui(path):
table = {}
if path and Path(path).exists():
for line in Path(path).read_text(encoding="utf-8", errors="ignore").splitlines():
if "(hex)" in line:
prefix, _, name = line.partition("(hex)")
table[prefix.strip().replace("-", ":").lower()] = name.strip()
return table
def describe_mac(mac, oui):
# [1] Đọc byte đầu của MAC. Bit 0x02 bật = MAC do thiết bị tự đặt/ngẫu nhiên
# (điện thoại hay làm vậy để khó bị theo dõi) -> không có hãng thật nào để
# tra, trả lời luôn
if int(mac[:2], 16) & 0x02:
return "MAC rieng/ngau nhien (dien thoai thuong bat tinh nang nay)"
# [2] MAC thật: 8 ký tự đầu ("aa:bb:cc" = 3 byte OUI) là mã hãng; tra trong bảng
# oui.txt (không có thì nói rõ lý do)
return oui.get(mac[:8], "khong co trong oui.txt" if oui else "chua co oui.txt")
def ttl_of(ip):
# Gửi đúng 1 gói ping tới thiết bị ĐÃ TÌM THẤY để đọc TTL.
# (Việc tìm thiết bị không dựa vào ping.)
# [1] Mỗi hệ điều hành viết lệnh ping khác nhau: -n (Windows) / -c (macOS,
# Linux) là số gói; -w, -t, -W là thời gian chờ
system = platform.system()
if system == "Windows":
cmd = ["ping", "-n", "1", "-w", "1000", ip]
elif system == "Darwin":
cmd = ["ping", "-c", "1", "-t", "2", ip]
else:
cmd = ["ping", "-c", "1", "-W", "1", ip]
# [2] Chạy lệnh ping và đọc chữ nó in ra; chờ tối đa 4 giây. Lỗi hoặc quá hạn
# thì trả None (không rõ)
try:
out = subprocess.run(cmd, capture_output=True, text=True, timeout=4).stdout
except (OSError, subprocess.SubprocessError):
return None
# [3] Tìm trong chữ đó đoạn "ttl=64" (hoặc "TTL=128"...) bằng regex; re.I =
# không phân biệt hoa thường
m = re.search(r"ttl[=:]\s*(\d+)", out, re.I)
# [4] Có thì trả về con số TTL, không thì trả None
return int(m.group(1)) if m else None
def guess_os(ttl):
# TTL khởi đầu của gói trả lời: ~64 Linux/macOS/iOS/Android, ~128 Windows,
# ~255 thiết bị mạng. Chỉ là gợi ý. (Gói đi qua mỗi router bị trừ 1 nên số
# nhận được có thể nhỏ hơn số khởi đầu một chút, vì vậy ta chia theo KHOẢNG.)
# [1] Không đọc được TTL (thiết bị chặn ping): không đoán bừa
if ttl is None:
return "khong ro (thiet bi chan ping)"
# [2] TTL từ 64 trở xuống: nhóm Linux/macOS/iOS/Android
if ttl <= 64:
return f"nhom Linux/macOS/iOS/Android (TTL {ttl})"
# [3] TTL trên 64 đến 128: nhóm Windows
if ttl <= 128:
return f"nhom Windows (TTL {ttl})"
# [4] Còn lại (tới 255): thiết bị mạng hoặc máy in
return f"thiet bi mang/may in (TTL {ttl})"
# Bảng 1 - đoán theo CỔNG: thiết bị mở MỘT TRONG các cổng bên trái
# thì gợi ý nhãn bên phải.
HINTS = [({631, 9100}, "may in"), ({554}, "camera"), ({62078}, "iPhone/iPad"), ({8008, 8009}, "Chromecast/TV Google"),
({3389}, "may Windows (Remote Desktop)"), ({445, 139}, "chia se file Windows/NAS"), ({22}, "may Linux/NAS/router (SSH)"),
({5000, 7000}, "Mac/Apple TV (AirPlay)"), ({23}, "thiet bi cu (Telnet): coi chung")]
# Bảng 2 - đoán theo TÊN HOST: tên (viết thường) có chứa từ khóa bên trái
# thì gợi ý nhãn bên phải.
NAME_HINTS = [("iphone", "iPhone"), ("ipad", "iPad"), ("galaxy", "Samsung Galaxy"), ("android", "Android"),
("macbook", "Mac"), ("printer", "may in"), ("tv", "TV"), ("cam", "camera"), ("router", "router")]
def guess_device(device):
# Ghép các manh mối thành nhãn gợi ý: cổng mở (HINTS) + tên host (NAME_HINTS).
# [1] Lấy tập các số port đang mở của thiết bị
open_ports = {p["port"] for p in device["ports"]}
# [2] Với mỗi dòng của HINTS: nếu tập cổng của dòng đó GIAO với cổng đang mở
# (phép &) thì lấy nhãn của dòng đó
clues = [label for ports, label in HINTS if open_ports & ports]
# [3] Đổi tên host sang chữ thường để so khớp không phân biệt hoa/thường
name = device["hostname"].lower()
# [4] Với mỗi từ khóa của NAME_HINTS có trong tên host: thêm nhãn tương ứng
clues += [label for key, label in NAME_HINTS if key in name]
# [5] set() bỏ nhãn trùng, sorted() sắp xếp để kết quả ổn định giữa các lần chạy
return sorted(set(clues))
def hostname(ip):
try:
return socket.gethostbyaddr(ip)[0]
except OSError:
return ""
def grab_banner(ip, port):
try:
with socket.create_connection((ip, port), timeout=0.6) as s:
s.settimeout(0.6)
if port in (80, 8000, 8008, 8080):
s.sendall(b"HEAD / HTTP/1.0\r\n\r\n")
data = s.recv(200).decode("utf-8", errors="replace")
except OSError:
return ""
for line in data.splitlines():
if line.lower().startswith("server:"):
return line[7:].strip()
return data.splitlines()[0][:60] if data else ""
def scan_ports(ip):
# Tìm các port đang mở của MỘT thiết bị: thử kết nối TCP tới từng port trong PORTS.
def probe(port):
# [1] Thử kết nối TCP tới 1 port, chờ tối đa 0,5 giây. connect_ex() trả 0
# nếu kết nối thành công (= port mở), trả mã lỗi khác 0 nếu không (không
# ném exception như connect()). Mở -> trả số port, không mở -> None
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(0.5)
return port if s.connect_ex((ip, port)) == 0 else None
# [2] Thử mọi port CÙNG LÚC bằng nhiều luồng (thread), không thử lần lượt: nhanh
# hơn rất nhiều vì phần lớn thời gian là ĐỢI
with ThreadPoolExecutor(max_workers=len(PORTS)) as pool:
# [3] Giữ lại port mở (bỏ None), sắp xếp từ nhỏ đến lớn
open_ports = sorted(p for p in pool.map(probe, PORTS) if p)
# [4] Với mỗi port mở: ghi tên dịch vụ quen thuộc và đọc "banner" (câu tự giới
# thiệu của dịch vụ, nếu có)
return [{"port": p, "service": PORTS[p], "banner": grab_banner(ip, p)} for p in open_ports]
def build_inventory(network, oui, with_ports):
hosts = [str(h) for h in network.hosts()] or [str(network.network_address)]
# [1] "Gõ cửa" mọi địa chỉ trong dải cùng lúc bằng 64 luồng (poke() ở trên)
with ThreadPoolExecutor(max_workers=64) as pool:
list(pool.map(poke, hosts))
# [2] Đợi 2 giây để các thiết bị kịp trả lời ARP và hệ điều hành kịp ghi vào
# bảng ARP
time.sleep(2)
# [3] Đọc bảng ARP (IP -> MAC), chỉ giữ IP thuộc dải đang quét và bỏ địa chỉ
# broadcast/multicast (không phải thiết bị thật)
neighbors = {ip: mac for ip, mac in read_neighbors().items() if ip in hosts and is_unicast(ip, mac)}
me = local_ip()
if me in hosts:
neighbors.setdefault(me, "(may nay)")
devices = []
for ip in sorted(neighbors, key=lambda x: ipaddress.ip_address(x)):
mac = neighbors[ip]
is_self = mac == "(may nay)"
devices.append({"ip": ip, "mac": mac,
"vendor": "may cua ban" if is_self else describe_mac(mac, oui),
"hostname": hostname(ip),
"ports": scan_ports(ip) if with_ports else []})
devices[-1]["os_guess"] = guess_os(ttl_of(ip)) if with_ports else ""
devices[-1]["guess"] = guess_device(devices[-1])
return devices
def _dev(ip, mac, vendor, host, ports=(), ttl=None):
d = {"ip": ip, "mac": mac, "vendor": vendor, "hostname": host,
"ports": [{"port": p, "service": PORTS.get(p, "?"), "banner": b} for p, b in ports]}
d["os_guess"] = guess_os(ttl)
d["guess"] = guess_device(d)
return d
def demo_inventory(day):
# Dữ liệu MẪU để học đọc kết quả, không quét mạng thật. Hãng và tên là giả định.
private = "MAC rieng/ngau nhien (dien thoai thuong bat tinh nang nay)"
devices = [
_dev("192.168.1.1", "f4:ec:38:11:22:33", "Hang router A", "router.lan", [(80, "Server: lighttpd")], 64),
_dev("192.168.1.23", "6a:6c:4a:21:dc:21", private, "macbook-cua-ban.lan", ttl=64),
_dev("192.168.1.40", "3c:22:fb:aa:bb:cc", "Hang may in X", "npi-3f2a",
[(80, "Server: Embedded HTTP Server"), (631, ""), (9100, "")], 255),
_dev("192.168.1.45", "9c:b2:00:44:55:66", "Hang camera Y", "cam-phong-1",
[(23, ""), (80, "Server: CamHTTPD/1.0"), (554, "")], 64),
_dev("192.168.1.60", "3a:5b:7c:12:34:56", private, "galaxy-s24.lan", ttl=64),
_dev("192.168.1.70", "00:1b:21:aa:bb:cc", "Hang may tinh Z", "PC-THUQUY", [(445, ""), (3389, "")], 128),
]
if day == 1:
devices.append(_dev("192.168.1.52", "da:a1:19:00:00:01", private, "", [(62078, "")], 64))
else:
devices[-1] = _dev("192.168.1.70", "00:1b:21:aa:bb:cc", "Hang may tinh Z", "PC-THUQUY",
[(445, ""), (8000, "Server: SimpleHTTP/0.6 Python/3.9.6")], 128)
devices.append(_dev("192.168.1.77", "c2:11:aa:bb:cc:dd", private, "", ttl=64))
return sorted(devices, key=lambda d: ipaddress.ip_address(d["ip"]))
def show(devices):
print(f"{'IP':<16}{'MAC':<19}{'Hang / ghi chu':<34}Ten")
for d in devices:
print(f"{d['ip']:<16}{d['mac']:<19}{d['vendor'][:32]:<34}{d['hostname'][:30]}")
for p in d["ports"]:
print(f" mo port {p['port']:<6}{p['service']:<20}{p['banner']}")
if d.get("guess") or d.get("os_guess"):
print(f" DOAN: {', '.join(d.get('guess', [])) or 'chua du dau hieu'} | HDH: {d.get('os_guess') or '-'}")
print(f"\nTong cong: {len(devices)} thiet bi tra loi.")
def compare(devices, baseline_path):
old = {d["mac"]: d for d in json.loads(Path(baseline_path).read_text(encoding="utf-8"))}
now = {d["mac"]: d for d in devices}
new = [d for mac, d in now.items() if mac not in old]
gone = [d for mac, d in old.items() if mac not in now]
print("\nThiet bi MOI so voi lan truoc:", [f"{d['ip']} ({d['mac']})" for d in new] or "khong co")
print("Thiet bi KHONG con tra loi:", [f"{d['ip']} ({d['mac']})" for d in gone] or "khong co")
for mac in sorted(now.keys() & old.keys()):
before = {p["port"] for p in old[mac]["ports"]}
after = {p["port"] for p in now[mac]["ports"]}
if before != after:
print(f"Port THAY DOI o {now[mac]['ip']}: moi mo {sorted(after - before)}, da dong {sorted(before - after)}")
def main():
ap = argparse.ArgumentParser(description="Liet ke thiet bi trong mang nha cua ban")
ap.add_argument("--net", help="dai mang, vi du 192.168.1.0/24 (mac dinh: tu doc mat na mang cua may ban)")
ap.add_argument("--oui", default="oui.txt", help="file oui.txt cua IEEE de tra hang san xuat")
ap.add_argument("--no-ports", action="store_true", help="khong quet port")
ap.add_argument("--save", default="inventory.json", help="file luu ket qua")
ap.add_argument("--baseline", help="file ket qua lan truoc de so sanh thiet bi moi")
ap.add_argument("--demo", type=int, choices=[1, 2], help="dung du lieu MAU (ngay 1 hoac ngay 2), khong quet mang that")
ap.add_argument("--only", help="chi quet port cua mot IP rieng (vi du 127.0.0.1), bo qua buoc tim thiet bi")
args = ap.parse_args()
if args.only:
ip = ipaddress.ip_address(args.only)
if not ip.is_private:
raise SystemExit("Chi quet IP rieng (192.168.x.x, 10.x.x.x, 127.0.0.1...).")
found = scan_ports(str(ip))
print(f"Port mo tren {ip}:", ", ".join(f"{p['port']}/{p['service']}" for p in found) or "khong co")
for p in found:
print(f" {p['port']:<6}{p['service']:<20}{p['banner']}")
guess = guess_device({"ports": found, "hostname": hostname(str(ip))})
print("DOAN:", ", ".join(guess) or "chua du dau hieu")
return
if args.demo:
print(f"[DU LIEU MAU ngay {args.demo}: khong quet mang that]")
devices = demo_inventory(args.demo)
show(devices)
Path(args.save).write_text(json.dumps(devices, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"Da luu {args.save}")
if args.baseline:
compare(devices, args.baseline)
return
# [1] Dải mạng cần quét: người dùng gõ --net thì dùng, không thì tự đọc từ card
# mạng của máy
network = ipaddress.ip_network(args.net, strict=False) if args.net else detect_network(local_ip())
# [2] Chốt chặn an toàn: chỉ nhận dải IP RIÊNG và tối đa 1024 địa chỉ; ngoài ra
# thoát ngay, chưa gửi gói nào
if not network.is_private or network.num_addresses > MAX_HOSTS:
raise SystemExit("Chi quet dai IP rieng (192.168.x.x, 10.x.x.x...) va khong qua 1024 dia chi.")
print(f"Dang kiem tra {network} ({network.num_addresses} dia chi), cho khoang 5-30 giay...")
devices = build_inventory(network, load_oui(args.oui), not args.no_ports)
show(devices)
Path(args.save).write_text(json.dumps(devices, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"Da luu {args.save}")
if args.baseline:
compare(devices, args.baseline)
if __name__ == "__main__":
main()
EOF@'
# lan_inventory.py - liệt kê thiết bị trong mạng nhà của BẠN:
# IP, MAC, hãng sản xuất, tên, port mở.
# Chỉ chạy trên mạng của chính bạn hoặc nơi được cho phép.
# Chỉ dùng thư viện có sẵn của Python 3.
import argparse
import ipaddress
import json
import platform
import re
import socket
import subprocess
import time
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
# Bảng port quen thuộc: số port -> tên dịch vụ thường chạy ở đó.
# Công cụ chỉ thử các port có trong bảng này.
PORTS = {21: "ftp", 22: "ssh", 23: "telnet", 53: "dns", 80: "http", 139: "netbios", 443: "https",
445: "smb", 554: "rtsp (camera)", 631: "ipp (may in)", 3389: "rdp", 5000: "http-alt/airplay",
7000: "airplay", 8000: "http-alt", 8008: "chromecast", 8080: "http-proxy", 8443: "https-alt",
9100: "may in", 62078: "iphone-sync"}
MAX_HOSTS = 1024 # giới hạn số địa chỉ mỗi lần quét, để gõ nhầm dải quá lớn không làm nghẽn mạng
def local_ip():
# Mục đích: biết IP của máy mình trong mạng LAN (một máy có thể có nhiều card mạng).
# connect() với UDP KHÔNG gửi gói nào ra mạng; nó chỉ bắt hệ điều hành chọn
# card mạng sẽ dùng để tới địa chỉ 192.0.2.1 (địa chỉ dành riêng cho tài liệu,
# không ai dùng). Sau đó ta đọc IP của card mạng đó.
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.connect(("192.0.2.1", 9))
return s.getsockname()[0]
def detect_network(ip):
# Đọc mặt nạ mạng thật của card mạng (cho biết dải IP gồm bao nhiêu địa chỉ).
# Không đọc được thì coi là /24 (256 địa chỉ, mạng nhà phổ biến nhất).
system = platform.system()
try:
if system == "Linux":
out = subprocess.run(["ip", "-o", "-4", "addr"], capture_output=True, text=True).stdout
m = re.search(re.escape(ip) + r"/(\d+)", out)
if m:
return ipaddress.ip_network(f"{ip}/{m.group(1)}", strict=False)
elif system == "Windows":
lines = subprocess.run(["ipconfig"], capture_output=True, text=True).stdout.splitlines()
for i, line in enumerate(lines):
if ip in line:
for nxt in lines[i + 1:i + 3]:
m = re.search(r"(255\.\d+\.\d+\.\d+)", nxt)
if m:
return ipaddress.ip_network(f"{ip}/{m.group(1)}", strict=False)
else:
out = subprocess.run(["ifconfig"], capture_output=True, text=True).stdout
m = re.search(r"inet " + re.escape(ip) + r" netmask (0x[0-9a-f]+)", out)
if m:
prefix = bin(int(m.group(1), 16)).count("1")
return ipaddress.ip_network(f"{ip}/{prefix}", strict=False)
except (OSError, ValueError):
pass
return ipaddress.ip_network(f"{ip}/24", strict=False)
def normalize_mac(raw):
# macOS in MAC không đệm số 0 ("a:b:c:1:2:3"), Windows dùng dấu gạch nối: đưa về
# dạng chuẩn aa:bb:cc:dd:ee:ff
parts = re.split(r"[:-]", raw.lower())
return ":".join(p.zfill(2) for p in parts) if len(parts) == 6 else None
def read_neighbors():
system = platform.system()
if system == "Linux":
out = subprocess.run(["ip", "neigh"], capture_output=True, text=True).stdout
pattern = r"^(\d+\.\d+\.\d+\.\d+) .*lladdr ([0-9a-f:]{11,17})"
else:
out = subprocess.run(["arp", "-a"], capture_output=True, text=True).stdout
pattern = r"(\d+\.\d+\.\d+\.\d+)\)? (?:at )?\s*([0-9a-fA-F]{1,2}(?:[:-][0-9a-fA-F]{1,2}){5})"
found = {}
for line in out.splitlines():
m = re.search(pattern, line)
if m and normalize_mac(m.group(2)):
found[m.group(1)] = normalize_mac(m.group(2))
return found
def is_unicast(ip, mac):
a = ipaddress.ip_address(ip)
return not (a.is_multicast or ip.endswith(".255") or mac == "ff:ff:ff:ff:ff:ff" or mac.startswith("01:00:5e"))
def poke(ip):
# MỤC ĐÍCH: "gõ cửa" một IP để máy phải hỏi ARP "ai giữ IP này?".
# Thiết bị nào có thật cũng trả lời ARP (kể cả khi nó chặn ping), nên MAC của
# nó sẽ nằm trong bảng ARP và ta đọc được.
try:
# [1] Tạo socket UDP (SOCK_DGRAM): chỉ "ném" gói đi, không bắt tay, không
# chờ trả lời
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
# [2] Gửi 1 byte rỗng tới port 9. Nội dung không quan trọng; điều quan
# trọng là hệ điều hành phải hỏi ARP để biết MAC của IP này TRƯỚC
# khi đưa gói ra card mạng
s.sendto(b"\0", (ip, 9))
except OSError:
# [3] Thiết bị trả lời "port này không mở" cũng có thể gây lỗi; ta không cần
# kết quả đó -> bỏ qua
pass
def load_oui(path):
table = {}
if path and Path(path).exists():
for line in Path(path).read_text(encoding="utf-8", errors="ignore").splitlines():
if "(hex)" in line:
prefix, _, name = line.partition("(hex)")
table[prefix.strip().replace("-", ":").lower()] = name.strip()
return table
def describe_mac(mac, oui):
# [1] Đọc byte đầu của MAC. Bit 0x02 bật = MAC do thiết bị tự đặt/ngẫu nhiên
# (điện thoại hay làm vậy để khó bị theo dõi) -> không có hãng thật nào để
# tra, trả lời luôn
if int(mac[:2], 16) & 0x02:
return "MAC rieng/ngau nhien (dien thoai thuong bat tinh nang nay)"
# [2] MAC thật: 8 ký tự đầu ("aa:bb:cc" = 3 byte OUI) là mã hãng; tra trong bảng
# oui.txt (không có thì nói rõ lý do)
return oui.get(mac[:8], "khong co trong oui.txt" if oui else "chua co oui.txt")
def ttl_of(ip):
# Gửi đúng 1 gói ping tới thiết bị ĐÃ TÌM THẤY để đọc TTL.
# (Việc tìm thiết bị không dựa vào ping.)
# [1] Mỗi hệ điều hành viết lệnh ping khác nhau: -n (Windows) / -c (macOS,
# Linux) là số gói; -w, -t, -W là thời gian chờ
system = platform.system()
if system == "Windows":
cmd = ["ping", "-n", "1", "-w", "1000", ip]
elif system == "Darwin":
cmd = ["ping", "-c", "1", "-t", "2", ip]
else:
cmd = ["ping", "-c", "1", "-W", "1", ip]
# [2] Chạy lệnh ping và đọc chữ nó in ra; chờ tối đa 4 giây. Lỗi hoặc quá hạn
# thì trả None (không rõ)
try:
out = subprocess.run(cmd, capture_output=True, text=True, timeout=4).stdout
except (OSError, subprocess.SubprocessError):
return None
# [3] Tìm trong chữ đó đoạn "ttl=64" (hoặc "TTL=128"...) bằng regex; re.I =
# không phân biệt hoa thường
m = re.search(r"ttl[=:]\s*(\d+)", out, re.I)
# [4] Có thì trả về con số TTL, không thì trả None
return int(m.group(1)) if m else None
def guess_os(ttl):
# TTL khởi đầu của gói trả lời: ~64 Linux/macOS/iOS/Android, ~128 Windows,
# ~255 thiết bị mạng. Chỉ là gợi ý. (Gói đi qua mỗi router bị trừ 1 nên số
# nhận được có thể nhỏ hơn số khởi đầu một chút, vì vậy ta chia theo KHOẢNG.)
# [1] Không đọc được TTL (thiết bị chặn ping): không đoán bừa
if ttl is None:
return "khong ro (thiet bi chan ping)"
# [2] TTL từ 64 trở xuống: nhóm Linux/macOS/iOS/Android
if ttl <= 64:
return f"nhom Linux/macOS/iOS/Android (TTL {ttl})"
# [3] TTL trên 64 đến 128: nhóm Windows
if ttl <= 128:
return f"nhom Windows (TTL {ttl})"
# [4] Còn lại (tới 255): thiết bị mạng hoặc máy in
return f"thiet bi mang/may in (TTL {ttl})"
# Bảng 1 - đoán theo CỔNG: thiết bị mở MỘT TRONG các cổng bên trái
# thì gợi ý nhãn bên phải.
HINTS = [({631, 9100}, "may in"), ({554}, "camera"), ({62078}, "iPhone/iPad"), ({8008, 8009}, "Chromecast/TV Google"),
({3389}, "may Windows (Remote Desktop)"), ({445, 139}, "chia se file Windows/NAS"), ({22}, "may Linux/NAS/router (SSH)"),
({5000, 7000}, "Mac/Apple TV (AirPlay)"), ({23}, "thiet bi cu (Telnet): coi chung")]
# Bảng 2 - đoán theo TÊN HOST: tên (viết thường) có chứa từ khóa bên trái
# thì gợi ý nhãn bên phải.
NAME_HINTS = [("iphone", "iPhone"), ("ipad", "iPad"), ("galaxy", "Samsung Galaxy"), ("android", "Android"),
("macbook", "Mac"), ("printer", "may in"), ("tv", "TV"), ("cam", "camera"), ("router", "router")]
def guess_device(device):
# Ghép các manh mối thành nhãn gợi ý: cổng mở (HINTS) + tên host (NAME_HINTS).
# [1] Lấy tập các số port đang mở của thiết bị
open_ports = {p["port"] for p in device["ports"]}
# [2] Với mỗi dòng của HINTS: nếu tập cổng của dòng đó GIAO với cổng đang mở
# (phép &) thì lấy nhãn của dòng đó
clues = [label for ports, label in HINTS if open_ports & ports]
# [3] Đổi tên host sang chữ thường để so khớp không phân biệt hoa/thường
name = device["hostname"].lower()
# [4] Với mỗi từ khóa của NAME_HINTS có trong tên host: thêm nhãn tương ứng
clues += [label for key, label in NAME_HINTS if key in name]
# [5] set() bỏ nhãn trùng, sorted() sắp xếp để kết quả ổn định giữa các lần chạy
return sorted(set(clues))
def hostname(ip):
try:
return socket.gethostbyaddr(ip)[0]
except OSError:
return ""
def grab_banner(ip, port):
try:
with socket.create_connection((ip, port), timeout=0.6) as s:
s.settimeout(0.6)
if port in (80, 8000, 8008, 8080):
s.sendall(b"HEAD / HTTP/1.0\r\n\r\n")
data = s.recv(200).decode("utf-8", errors="replace")
except OSError:
return ""
for line in data.splitlines():
if line.lower().startswith("server:"):
return line[7:].strip()
return data.splitlines()[0][:60] if data else ""
def scan_ports(ip):
# Tìm các port đang mở của MỘT thiết bị: thử kết nối TCP tới từng port trong PORTS.
def probe(port):
# [1] Thử kết nối TCP tới 1 port, chờ tối đa 0,5 giây. connect_ex() trả 0
# nếu kết nối thành công (= port mở), trả mã lỗi khác 0 nếu không (không
# ném exception như connect()). Mở -> trả số port, không mở -> None
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(0.5)
return port if s.connect_ex((ip, port)) == 0 else None
# [2] Thử mọi port CÙNG LÚC bằng nhiều luồng (thread), không thử lần lượt: nhanh
# hơn rất nhiều vì phần lớn thời gian là ĐỢI
with ThreadPoolExecutor(max_workers=len(PORTS)) as pool:
# [3] Giữ lại port mở (bỏ None), sắp xếp từ nhỏ đến lớn
open_ports = sorted(p for p in pool.map(probe, PORTS) if p)
# [4] Với mỗi port mở: ghi tên dịch vụ quen thuộc và đọc "banner" (câu tự giới
# thiệu của dịch vụ, nếu có)
return [{"port": p, "service": PORTS[p], "banner": grab_banner(ip, p)} for p in open_ports]
def build_inventory(network, oui, with_ports):
hosts = [str(h) for h in network.hosts()] or [str(network.network_address)]
# [1] "Gõ cửa" mọi địa chỉ trong dải cùng lúc bằng 64 luồng (poke() ở trên)
with ThreadPoolExecutor(max_workers=64) as pool:
list(pool.map(poke, hosts))
# [2] Đợi 2 giây để các thiết bị kịp trả lời ARP và hệ điều hành kịp ghi vào
# bảng ARP
time.sleep(2)
# [3] Đọc bảng ARP (IP -> MAC), chỉ giữ IP thuộc dải đang quét và bỏ địa chỉ
# broadcast/multicast (không phải thiết bị thật)
neighbors = {ip: mac for ip, mac in read_neighbors().items() if ip in hosts and is_unicast(ip, mac)}
me = local_ip()
if me in hosts:
neighbors.setdefault(me, "(may nay)")
devices = []
for ip in sorted(neighbors, key=lambda x: ipaddress.ip_address(x)):
mac = neighbors[ip]
is_self = mac == "(may nay)"
devices.append({"ip": ip, "mac": mac,
"vendor": "may cua ban" if is_self else describe_mac(mac, oui),
"hostname": hostname(ip),
"ports": scan_ports(ip) if with_ports else []})
devices[-1]["os_guess"] = guess_os(ttl_of(ip)) if with_ports else ""
devices[-1]["guess"] = guess_device(devices[-1])
return devices
def _dev(ip, mac, vendor, host, ports=(), ttl=None):
d = {"ip": ip, "mac": mac, "vendor": vendor, "hostname": host,
"ports": [{"port": p, "service": PORTS.get(p, "?"), "banner": b} for p, b in ports]}
d["os_guess"] = guess_os(ttl)
d["guess"] = guess_device(d)
return d
def demo_inventory(day):
# Dữ liệu MẪU để học đọc kết quả, không quét mạng thật. Hãng và tên là giả định.
private = "MAC rieng/ngau nhien (dien thoai thuong bat tinh nang nay)"
devices = [
_dev("192.168.1.1", "f4:ec:38:11:22:33", "Hang router A", "router.lan", [(80, "Server: lighttpd")], 64),
_dev("192.168.1.23", "6a:6c:4a:21:dc:21", private, "macbook-cua-ban.lan", ttl=64),
_dev("192.168.1.40", "3c:22:fb:aa:bb:cc", "Hang may in X", "npi-3f2a",
[(80, "Server: Embedded HTTP Server"), (631, ""), (9100, "")], 255),
_dev("192.168.1.45", "9c:b2:00:44:55:66", "Hang camera Y", "cam-phong-1",
[(23, ""), (80, "Server: CamHTTPD/1.0"), (554, "")], 64),
_dev("192.168.1.60", "3a:5b:7c:12:34:56", private, "galaxy-s24.lan", ttl=64),
_dev("192.168.1.70", "00:1b:21:aa:bb:cc", "Hang may tinh Z", "PC-THUQUY", [(445, ""), (3389, "")], 128),
]
if day == 1:
devices.append(_dev("192.168.1.52", "da:a1:19:00:00:01", private, "", [(62078, "")], 64))
else:
devices[-1] = _dev("192.168.1.70", "00:1b:21:aa:bb:cc", "Hang may tinh Z", "PC-THUQUY",
[(445, ""), (8000, "Server: SimpleHTTP/0.6 Python/3.9.6")], 128)
devices.append(_dev("192.168.1.77", "c2:11:aa:bb:cc:dd", private, "", ttl=64))
return sorted(devices, key=lambda d: ipaddress.ip_address(d["ip"]))
def show(devices):
print(f"{'IP':<16}{'MAC':<19}{'Hang / ghi chu':<34}Ten")
for d in devices:
print(f"{d['ip']:<16}{d['mac']:<19}{d['vendor'][:32]:<34}{d['hostname'][:30]}")
for p in d["ports"]:
print(f" mo port {p['port']:<6}{p['service']:<20}{p['banner']}")
if d.get("guess") or d.get("os_guess"):
print(f" DOAN: {', '.join(d.get('guess', [])) or 'chua du dau hieu'} | HDH: {d.get('os_guess') or '-'}")
print(f"\nTong cong: {len(devices)} thiet bi tra loi.")
def compare(devices, baseline_path):
old = {d["mac"]: d for d in json.loads(Path(baseline_path).read_text(encoding="utf-8"))}
now = {d["mac"]: d for d in devices}
new = [d for mac, d in now.items() if mac not in old]
gone = [d for mac, d in old.items() if mac not in now]
print("\nThiet bi MOI so voi lan truoc:", [f"{d['ip']} ({d['mac']})" for d in new] or "khong co")
print("Thiet bi KHONG con tra loi:", [f"{d['ip']} ({d['mac']})" for d in gone] or "khong co")
for mac in sorted(now.keys() & old.keys()):
before = {p["port"] for p in old[mac]["ports"]}
after = {p["port"] for p in now[mac]["ports"]}
if before != after:
print(f"Port THAY DOI o {now[mac]['ip']}: moi mo {sorted(after - before)}, da dong {sorted(before - after)}")
def main():
ap = argparse.ArgumentParser(description="Liet ke thiet bi trong mang nha cua ban")
ap.add_argument("--net", help="dai mang, vi du 192.168.1.0/24 (mac dinh: tu doc mat na mang cua may ban)")
ap.add_argument("--oui", default="oui.txt", help="file oui.txt cua IEEE de tra hang san xuat")
ap.add_argument("--no-ports", action="store_true", help="khong quet port")
ap.add_argument("--save", default="inventory.json", help="file luu ket qua")
ap.add_argument("--baseline", help="file ket qua lan truoc de so sanh thiet bi moi")
ap.add_argument("--demo", type=int, choices=[1, 2], help="dung du lieu MAU (ngay 1 hoac ngay 2), khong quet mang that")
ap.add_argument("--only", help="chi quet port cua mot IP rieng (vi du 127.0.0.1), bo qua buoc tim thiet bi")
args = ap.parse_args()
if args.only:
ip = ipaddress.ip_address(args.only)
if not ip.is_private:
raise SystemExit("Chi quet IP rieng (192.168.x.x, 10.x.x.x, 127.0.0.1...).")
found = scan_ports(str(ip))
print(f"Port mo tren {ip}:", ", ".join(f"{p['port']}/{p['service']}" for p in found) or "khong co")
for p in found:
print(f" {p['port']:<6}{p['service']:<20}{p['banner']}")
guess = guess_device({"ports": found, "hostname": hostname(str(ip))})
print("DOAN:", ", ".join(guess) or "chua du dau hieu")
return
if args.demo:
print(f"[DU LIEU MAU ngay {args.demo}: khong quet mang that]")
devices = demo_inventory(args.demo)
show(devices)
Path(args.save).write_text(json.dumps(devices, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"Da luu {args.save}")
if args.baseline:
compare(devices, args.baseline)
return
# [1] Dải mạng cần quét: người dùng gõ --net thì dùng, không thì tự đọc từ card
# mạng của máy
network = ipaddress.ip_network(args.net, strict=False) if args.net else detect_network(local_ip())
# [2] Chốt chặn an toàn: chỉ nhận dải IP RIÊNG và tối đa 1024 địa chỉ; ngoài ra
# thoát ngay, chưa gửi gói nào
if not network.is_private or network.num_addresses > MAX_HOSTS:
raise SystemExit("Chi quet dai IP rieng (192.168.x.x, 10.x.x.x...) va khong qua 1024 dia chi.")
print(f"Dang kiem tra {network} ({network.num_addresses} dia chi), cho khoang 5-30 giay...")
devices = build_inventory(network, load_oui(args.oui), not args.no_ports)
show(devices)
Path(args.save).write_text(json.dumps(devices, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"Da luu {args.save}")
if args.baseline:
compare(devices, args.baseline)
if __name__ == "__main__":
main()
'@ | Set-Content -Path lan_inventory.py -Encoding utf8cat > lan_inventory.py <<'EOF'
# lan_inventory.py - liệt kê thiết bị trong mạng nhà của BẠN:
# IP, MAC, hãng sản xuất, tên, port mở.
# Chỉ chạy trên mạng của chính bạn hoặc nơi được cho phép.
# Chỉ dùng thư viện có sẵn của Python 3.
import argparse
import ipaddress
import json
import platform
import re
import socket
import subprocess
import time
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
# Bảng port quen thuộc: số port -> tên dịch vụ thường chạy ở đó.
# Công cụ chỉ thử các port có trong bảng này.
PORTS = {21: "ftp", 22: "ssh", 23: "telnet", 53: "dns", 80: "http", 139: "netbios", 443: "https",
445: "smb", 554: "rtsp (camera)", 631: "ipp (may in)", 3389: "rdp", 5000: "http-alt/airplay",
7000: "airplay", 8000: "http-alt", 8008: "chromecast", 8080: "http-proxy", 8443: "https-alt",
9100: "may in", 62078: "iphone-sync"}
MAX_HOSTS = 1024 # giới hạn số địa chỉ mỗi lần quét, để gõ nhầm dải quá lớn không làm nghẽn mạng
def local_ip():
# Mục đích: biết IP của máy mình trong mạng LAN (một máy có thể có nhiều card mạng).
# connect() với UDP KHÔNG gửi gói nào ra mạng; nó chỉ bắt hệ điều hành chọn
# card mạng sẽ dùng để tới địa chỉ 192.0.2.1 (địa chỉ dành riêng cho tài liệu,
# không ai dùng). Sau đó ta đọc IP của card mạng đó.
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.connect(("192.0.2.1", 9))
return s.getsockname()[0]
def detect_network(ip):
# Đọc mặt nạ mạng thật của card mạng (cho biết dải IP gồm bao nhiêu địa chỉ).
# Không đọc được thì coi là /24 (256 địa chỉ, mạng nhà phổ biến nhất).
system = platform.system()
try:
if system == "Linux":
out = subprocess.run(["ip", "-o", "-4", "addr"], capture_output=True, text=True).stdout
m = re.search(re.escape(ip) + r"/(\d+)", out)
if m:
return ipaddress.ip_network(f"{ip}/{m.group(1)}", strict=False)
elif system == "Windows":
lines = subprocess.run(["ipconfig"], capture_output=True, text=True).stdout.splitlines()
for i, line in enumerate(lines):
if ip in line:
for nxt in lines[i + 1:i + 3]:
m = re.search(r"(255\.\d+\.\d+\.\d+)", nxt)
if m:
return ipaddress.ip_network(f"{ip}/{m.group(1)}", strict=False)
else:
out = subprocess.run(["ifconfig"], capture_output=True, text=True).stdout
m = re.search(r"inet " + re.escape(ip) + r" netmask (0x[0-9a-f]+)", out)
if m:
prefix = bin(int(m.group(1), 16)).count("1")
return ipaddress.ip_network(f"{ip}/{prefix}", strict=False)
except (OSError, ValueError):
pass
return ipaddress.ip_network(f"{ip}/24", strict=False)
def normalize_mac(raw):
# macOS in MAC không đệm số 0 ("a:b:c:1:2:3"), Windows dùng dấu gạch nối: đưa về
# dạng chuẩn aa:bb:cc:dd:ee:ff
parts = re.split(r"[:-]", raw.lower())
return ":".join(p.zfill(2) for p in parts) if len(parts) == 6 else None
def read_neighbors():
system = platform.system()
if system == "Linux":
out = subprocess.run(["ip", "neigh"], capture_output=True, text=True).stdout
pattern = r"^(\d+\.\d+\.\d+\.\d+) .*lladdr ([0-9a-f:]{11,17})"
else:
out = subprocess.run(["arp", "-a"], capture_output=True, text=True).stdout
pattern = r"(\d+\.\d+\.\d+\.\d+)\)? (?:at )?\s*([0-9a-fA-F]{1,2}(?:[:-][0-9a-fA-F]{1,2}){5})"
found = {}
for line in out.splitlines():
m = re.search(pattern, line)
if m and normalize_mac(m.group(2)):
found[m.group(1)] = normalize_mac(m.group(2))
return found
def is_unicast(ip, mac):
a = ipaddress.ip_address(ip)
return not (a.is_multicast or ip.endswith(".255") or mac == "ff:ff:ff:ff:ff:ff" or mac.startswith("01:00:5e"))
def poke(ip):
# MỤC ĐÍCH: "gõ cửa" một IP để máy phải hỏi ARP "ai giữ IP này?".
# Thiết bị nào có thật cũng trả lời ARP (kể cả khi nó chặn ping), nên MAC của
# nó sẽ nằm trong bảng ARP và ta đọc được.
try:
# [1] Tạo socket UDP (SOCK_DGRAM): chỉ "ném" gói đi, không bắt tay, không
# chờ trả lời
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
# [2] Gửi 1 byte rỗng tới port 9. Nội dung không quan trọng; điều quan
# trọng là hệ điều hành phải hỏi ARP để biết MAC của IP này TRƯỚC
# khi đưa gói ra card mạng
s.sendto(b"\0", (ip, 9))
except OSError:
# [3] Thiết bị trả lời "port này không mở" cũng có thể gây lỗi; ta không cần
# kết quả đó -> bỏ qua
pass
def load_oui(path):
table = {}
if path and Path(path).exists():
for line in Path(path).read_text(encoding="utf-8", errors="ignore").splitlines():
if "(hex)" in line:
prefix, _, name = line.partition("(hex)")
table[prefix.strip().replace("-", ":").lower()] = name.strip()
return table
def describe_mac(mac, oui):
# [1] Đọc byte đầu của MAC. Bit 0x02 bật = MAC do thiết bị tự đặt/ngẫu nhiên
# (điện thoại hay làm vậy để khó bị theo dõi) -> không có hãng thật nào để
# tra, trả lời luôn
if int(mac[:2], 16) & 0x02:
return "MAC rieng/ngau nhien (dien thoai thuong bat tinh nang nay)"
# [2] MAC thật: 8 ký tự đầu ("aa:bb:cc" = 3 byte OUI) là mã hãng; tra trong bảng
# oui.txt (không có thì nói rõ lý do)
return oui.get(mac[:8], "khong co trong oui.txt" if oui else "chua co oui.txt")
def ttl_of(ip):
# Gửi đúng 1 gói ping tới thiết bị ĐÃ TÌM THẤY để đọc TTL.
# (Việc tìm thiết bị không dựa vào ping.)
# [1] Mỗi hệ điều hành viết lệnh ping khác nhau: -n (Windows) / -c (macOS,
# Linux) là số gói; -w, -t, -W là thời gian chờ
system = platform.system()
if system == "Windows":
cmd = ["ping", "-n", "1", "-w", "1000", ip]
elif system == "Darwin":
cmd = ["ping", "-c", "1", "-t", "2", ip]
else:
cmd = ["ping", "-c", "1", "-W", "1", ip]
# [2] Chạy lệnh ping và đọc chữ nó in ra; chờ tối đa 4 giây. Lỗi hoặc quá hạn
# thì trả None (không rõ)
try:
out = subprocess.run(cmd, capture_output=True, text=True, timeout=4).stdout
except (OSError, subprocess.SubprocessError):
return None
# [3] Tìm trong chữ đó đoạn "ttl=64" (hoặc "TTL=128"...) bằng regex; re.I =
# không phân biệt hoa thường
m = re.search(r"ttl[=:]\s*(\d+)", out, re.I)
# [4] Có thì trả về con số TTL, không thì trả None
return int(m.group(1)) if m else None
def guess_os(ttl):
# TTL khởi đầu của gói trả lời: ~64 Linux/macOS/iOS/Android, ~128 Windows,
# ~255 thiết bị mạng. Chỉ là gợi ý. (Gói đi qua mỗi router bị trừ 1 nên số
# nhận được có thể nhỏ hơn số khởi đầu một chút, vì vậy ta chia theo KHOẢNG.)
# [1] Không đọc được TTL (thiết bị chặn ping): không đoán bừa
if ttl is None:
return "khong ro (thiet bi chan ping)"
# [2] TTL từ 64 trở xuống: nhóm Linux/macOS/iOS/Android
if ttl <= 64:
return f"nhom Linux/macOS/iOS/Android (TTL {ttl})"
# [3] TTL trên 64 đến 128: nhóm Windows
if ttl <= 128:
return f"nhom Windows (TTL {ttl})"
# [4] Còn lại (tới 255): thiết bị mạng hoặc máy in
return f"thiet bi mang/may in (TTL {ttl})"
# Bảng 1 - đoán theo CỔNG: thiết bị mở MỘT TRONG các cổng bên trái
# thì gợi ý nhãn bên phải.
HINTS = [({631, 9100}, "may in"), ({554}, "camera"), ({62078}, "iPhone/iPad"), ({8008, 8009}, "Chromecast/TV Google"),
({3389}, "may Windows (Remote Desktop)"), ({445, 139}, "chia se file Windows/NAS"), ({22}, "may Linux/NAS/router (SSH)"),
({5000, 7000}, "Mac/Apple TV (AirPlay)"), ({23}, "thiet bi cu (Telnet): coi chung")]
# Bảng 2 - đoán theo TÊN HOST: tên (viết thường) có chứa từ khóa bên trái
# thì gợi ý nhãn bên phải.
NAME_HINTS = [("iphone", "iPhone"), ("ipad", "iPad"), ("galaxy", "Samsung Galaxy"), ("android", "Android"),
("macbook", "Mac"), ("printer", "may in"), ("tv", "TV"), ("cam", "camera"), ("router", "router")]
def guess_device(device):
# Ghép các manh mối thành nhãn gợi ý: cổng mở (HINTS) + tên host (NAME_HINTS).
# [1] Lấy tập các số port đang mở của thiết bị
open_ports = {p["port"] for p in device["ports"]}
# [2] Với mỗi dòng của HINTS: nếu tập cổng của dòng đó GIAO với cổng đang mở
# (phép &) thì lấy nhãn của dòng đó
clues = [label for ports, label in HINTS if open_ports & ports]
# [3] Đổi tên host sang chữ thường để so khớp không phân biệt hoa/thường
name = device["hostname"].lower()
# [4] Với mỗi từ khóa của NAME_HINTS có trong tên host: thêm nhãn tương ứng
clues += [label for key, label in NAME_HINTS if key in name]
# [5] set() bỏ nhãn trùng, sorted() sắp xếp để kết quả ổn định giữa các lần chạy
return sorted(set(clues))
def hostname(ip):
try:
return socket.gethostbyaddr(ip)[0]
except OSError:
return ""
def grab_banner(ip, port):
try:
with socket.create_connection((ip, port), timeout=0.6) as s:
s.settimeout(0.6)
if port in (80, 8000, 8008, 8080):
s.sendall(b"HEAD / HTTP/1.0\r\n\r\n")
data = s.recv(200).decode("utf-8", errors="replace")
except OSError:
return ""
for line in data.splitlines():
if line.lower().startswith("server:"):
return line[7:].strip()
return data.splitlines()[0][:60] if data else ""
def scan_ports(ip):
# Tìm các port đang mở của MỘT thiết bị: thử kết nối TCP tới từng port trong PORTS.
def probe(port):
# [1] Thử kết nối TCP tới 1 port, chờ tối đa 0,5 giây. connect_ex() trả 0
# nếu kết nối thành công (= port mở), trả mã lỗi khác 0 nếu không (không
# ném exception như connect()). Mở -> trả số port, không mở -> None
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(0.5)
return port if s.connect_ex((ip, port)) == 0 else None
# [2] Thử mọi port CÙNG LÚC bằng nhiều luồng (thread), không thử lần lượt: nhanh
# hơn rất nhiều vì phần lớn thời gian là ĐỢI
with ThreadPoolExecutor(max_workers=len(PORTS)) as pool:
# [3] Giữ lại port mở (bỏ None), sắp xếp từ nhỏ đến lớn
open_ports = sorted(p for p in pool.map(probe, PORTS) if p)
# [4] Với mỗi port mở: ghi tên dịch vụ quen thuộc và đọc "banner" (câu tự giới
# thiệu của dịch vụ, nếu có)
return [{"port": p, "service": PORTS[p], "banner": grab_banner(ip, p)} for p in open_ports]
def build_inventory(network, oui, with_ports):
hosts = [str(h) for h in network.hosts()] or [str(network.network_address)]
# [1] "Gõ cửa" mọi địa chỉ trong dải cùng lúc bằng 64 luồng (poke() ở trên)
with ThreadPoolExecutor(max_workers=64) as pool:
list(pool.map(poke, hosts))
# [2] Đợi 2 giây để các thiết bị kịp trả lời ARP và hệ điều hành kịp ghi vào
# bảng ARP
time.sleep(2)
# [3] Đọc bảng ARP (IP -> MAC), chỉ giữ IP thuộc dải đang quét và bỏ địa chỉ
# broadcast/multicast (không phải thiết bị thật)
neighbors = {ip: mac for ip, mac in read_neighbors().items() if ip in hosts and is_unicast(ip, mac)}
me = local_ip()
if me in hosts:
neighbors.setdefault(me, "(may nay)")
devices = []
for ip in sorted(neighbors, key=lambda x: ipaddress.ip_address(x)):
mac = neighbors[ip]
is_self = mac == "(may nay)"
devices.append({"ip": ip, "mac": mac,
"vendor": "may cua ban" if is_self else describe_mac(mac, oui),
"hostname": hostname(ip),
"ports": scan_ports(ip) if with_ports else []})
devices[-1]["os_guess"] = guess_os(ttl_of(ip)) if with_ports else ""
devices[-1]["guess"] = guess_device(devices[-1])
return devices
def _dev(ip, mac, vendor, host, ports=(), ttl=None):
d = {"ip": ip, "mac": mac, "vendor": vendor, "hostname": host,
"ports": [{"port": p, "service": PORTS.get(p, "?"), "banner": b} for p, b in ports]}
d["os_guess"] = guess_os(ttl)
d["guess"] = guess_device(d)
return d
def demo_inventory(day):
# Dữ liệu MẪU để học đọc kết quả, không quét mạng thật. Hãng và tên là giả định.
private = "MAC rieng/ngau nhien (dien thoai thuong bat tinh nang nay)"
devices = [
_dev("192.168.1.1", "f4:ec:38:11:22:33", "Hang router A", "router.lan", [(80, "Server: lighttpd")], 64),
_dev("192.168.1.23", "6a:6c:4a:21:dc:21", private, "macbook-cua-ban.lan", ttl=64),
_dev("192.168.1.40", "3c:22:fb:aa:bb:cc", "Hang may in X", "npi-3f2a",
[(80, "Server: Embedded HTTP Server"), (631, ""), (9100, "")], 255),
_dev("192.168.1.45", "9c:b2:00:44:55:66", "Hang camera Y", "cam-phong-1",
[(23, ""), (80, "Server: CamHTTPD/1.0"), (554, "")], 64),
_dev("192.168.1.60", "3a:5b:7c:12:34:56", private, "galaxy-s24.lan", ttl=64),
_dev("192.168.1.70", "00:1b:21:aa:bb:cc", "Hang may tinh Z", "PC-THUQUY", [(445, ""), (3389, "")], 128),
]
if day == 1:
devices.append(_dev("192.168.1.52", "da:a1:19:00:00:01", private, "", [(62078, "")], 64))
else:
devices[-1] = _dev("192.168.1.70", "00:1b:21:aa:bb:cc", "Hang may tinh Z", "PC-THUQUY",
[(445, ""), (8000, "Server: SimpleHTTP/0.6 Python/3.9.6")], 128)
devices.append(_dev("192.168.1.77", "c2:11:aa:bb:cc:dd", private, "", ttl=64))
return sorted(devices, key=lambda d: ipaddress.ip_address(d["ip"]))
def show(devices):
print(f"{'IP':<16}{'MAC':<19}{'Hang / ghi chu':<34}Ten")
for d in devices:
print(f"{d['ip']:<16}{d['mac']:<19}{d['vendor'][:32]:<34}{d['hostname'][:30]}")
for p in d["ports"]:
print(f" mo port {p['port']:<6}{p['service']:<20}{p['banner']}")
if d.get("guess") or d.get("os_guess"):
print(f" DOAN: {', '.join(d.get('guess', [])) or 'chua du dau hieu'} | HDH: {d.get('os_guess') or '-'}")
print(f"\nTong cong: {len(devices)} thiet bi tra loi.")
def compare(devices, baseline_path):
old = {d["mac"]: d for d in json.loads(Path(baseline_path).read_text(encoding="utf-8"))}
now = {d["mac"]: d for d in devices}
new = [d for mac, d in now.items() if mac not in old]
gone = [d for mac, d in old.items() if mac not in now]
print("\nThiet bi MOI so voi lan truoc:", [f"{d['ip']} ({d['mac']})" for d in new] or "khong co")
print("Thiet bi KHONG con tra loi:", [f"{d['ip']} ({d['mac']})" for d in gone] or "khong co")
for mac in sorted(now.keys() & old.keys()):
before = {p["port"] for p in old[mac]["ports"]}
after = {p["port"] for p in now[mac]["ports"]}
if before != after:
print(f"Port THAY DOI o {now[mac]['ip']}: moi mo {sorted(after - before)}, da dong {sorted(before - after)}")
def main():
ap = argparse.ArgumentParser(description="Liet ke thiet bi trong mang nha cua ban")
ap.add_argument("--net", help="dai mang, vi du 192.168.1.0/24 (mac dinh: tu doc mat na mang cua may ban)")
ap.add_argument("--oui", default="oui.txt", help="file oui.txt cua IEEE de tra hang san xuat")
ap.add_argument("--no-ports", action="store_true", help="khong quet port")
ap.add_argument("--save", default="inventory.json", help="file luu ket qua")
ap.add_argument("--baseline", help="file ket qua lan truoc de so sanh thiet bi moi")
ap.add_argument("--demo", type=int, choices=[1, 2], help="dung du lieu MAU (ngay 1 hoac ngay 2), khong quet mang that")
ap.add_argument("--only", help="chi quet port cua mot IP rieng (vi du 127.0.0.1), bo qua buoc tim thiet bi")
args = ap.parse_args()
if args.only:
ip = ipaddress.ip_address(args.only)
if not ip.is_private:
raise SystemExit("Chi quet IP rieng (192.168.x.x, 10.x.x.x, 127.0.0.1...).")
found = scan_ports(str(ip))
print(f"Port mo tren {ip}:", ", ".join(f"{p['port']}/{p['service']}" for p in found) or "khong co")
for p in found:
print(f" {p['port']:<6}{p['service']:<20}{p['banner']}")
guess = guess_device({"ports": found, "hostname": hostname(str(ip))})
print("DOAN:", ", ".join(guess) or "chua du dau hieu")
return
if args.demo:
print(f"[DU LIEU MAU ngay {args.demo}: khong quet mang that]")
devices = demo_inventory(args.demo)
show(devices)
Path(args.save).write_text(json.dumps(devices, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"Da luu {args.save}")
if args.baseline:
compare(devices, args.baseline)
return
# [1] Dải mạng cần quét: người dùng gõ --net thì dùng, không thì tự đọc từ card
# mạng của máy
network = ipaddress.ip_network(args.net, strict=False) if args.net else detect_network(local_ip())
# [2] Chốt chặn an toàn: chỉ nhận dải IP RIÊNG và tối đa 1024 địa chỉ; ngoài ra
# thoát ngay, chưa gửi gói nào
if not network.is_private or network.num_addresses > MAX_HOSTS:
raise SystemExit("Chi quet dai IP rieng (192.168.x.x, 10.x.x.x...) va khong qua 1024 dia chi.")
print(f"Dang kiem tra {network} ({network.num_addresses} dia chi), cho khoang 5-30 giay...")
devices = build_inventory(network, load_oui(args.oui), not args.no_ports)
show(devices)
Path(args.save).write_text(json.dumps(devices, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"Da luu {args.save}")
if args.baseline:
compare(devices, args.baseline)
if __name__ == "__main__":
main()
EOFBƯỚC 3Kiểm tra file đã tạo
Chạy ls (macOS/Linux) hoặc dir (Windows), phải thấy lan_inventory.py. Rồi xem trợ giúp. Lệnh này chưa quét gì:
python3 lan_inventory.py --helppy lan_inventory.py --helppython3 lan_inventory.py --helpBƯỚC 4Tải danh sách hãng sản xuất (nên làm)
File oui.txt của IEEE liệt kê mọi mã hãng. Tải về, đặt cạnh lan_inventory.py:
curl -o oui.txt https://standards-oui.ieee.org/oui/oui.txtcurl.exe -o oui.txt https://standards-oui.ieee.org/oui/oui.txtcurl -o oui.txt https://standards-oui.ieee.org/oui/oui.txtlan_inventory.py: chương trình sẽ chạy.oui.txt: danh sách để tra hãng, không phải chương trình.baseline.json: chưa có lúc này; được tạo khi chạy với--saveở bước 7.
Không có file này chương trình vẫn chạy, chỉ hiện "chua co oui.txt" ở cột hãng. Đến đây bạn đã có công cụ. Bước 7 chạy nó.
Đọc thêm: từng hàm trong code làm gì (không bắt buộc)
| Phần | Ý nghĩa |
|---|---|
local_ip() | Hỏi hệ điều hành "bạn dùng IP nào để ra mạng?" bằng một socket UDP. Lệnh connect của UDP không gửi gói nào, chỉ chọn card mạng. |
detect_network(ip) | Đọc mặt nạ mạng thật của card để biết mạng là /24, /22… Không đọc được thì dùng /24. |
read_neighbors() | Chạy arp -a (hoặc ip neigh) và tách cặp IP–MAC. Đưa mọi dạng viết MAC về aa:bb:cc:dd:ee:ff. |
is_unicast() | Loại địa chỉ quảng bá và địa chỉ nhóm (bước 3). |
poke(ip) | Gửi một byte UDP tới port 9 của IP đó. Mục đích duy nhất: buộc máy bạn hỏi ARP. Chạy song song 64 luồng nên cả dải /24 xong trong vài giây. |
load_oui(), describe_mac() | Đọc oui.txt và tra ba byte đầu. MAC ngẫu nhiên thì in "MAC riêng" thay vì tra sai. |
hostname(ip) | Tra ngược DNS để lấy tên máy nếu thiết bị có khai. |
ttl_of(ip), guess_os(ttl) | Ping một lần tới thiết bị đã tìm thấy, đọc TTL, quy ra nhóm hệ điều hành. |
guess_device(device) | Đoán loại thiết bị từ cổng mở và tên máy, in ở dòng DOAN:. Chỉ là gợi ý. |
scan_ports(ip) | Thử kết nối TCP tới khoảng 20 port phổ biến, timeout 0,5 giây. Kết nối được là port đang mở. Đây là dạng quét nhẹ nhất. |
grab_banner(ip, port) | Với mỗi port mở, đọc dòng chào của dịch vụ. |
build_inventory() | Ghép các bước: gõ cửa cả dải, chờ 2 giây, đọc ARP, nhận diện từng thiết bị. |
compare() | So với file lần trước: thiết bị mới, thiết bị không còn trả lời, và port thay đổi. |
demo_inventory(day) | Trả về danh sách thiết bị mẫu để học đọc kết quả mà không quét mạng. |
main() | Đọc tham số dòng lệnh. Từ chối dải không phải IP riêng hoặc lớn hơn 1024 địa chỉ, để không quét nhầm Internet hay mạng người khác. |
Bước 7 · Chạy công cụ, kể cả khi không có mật khẩu router
20 phútCông cụ chỉ cần máy bạn đang nối vào mạng cần xem. Không cần mật khẩu router. Chọn một trong bốn cách, đi từ dễ đến thật:
| Cách | Quét ở đâu | Cần gì | Hợp khi |
|---|---|---|---|
1. Dữ liệu mẫu (--demo) | Không quét gì, chỉ in dữ liệu có sẵn | Chỉ Python | Chưa có mạng nào, hoặc trình diễn trên lớp |
2. Thiết bị giả (--only 127.0.0.1) | Chỉ máy bạn (quét cổng của chính nó) | Chỉ Python | Muốn xem quét port chạy thật, không chạm mạng nào |
| 3. Hotspot điện thoại | Cả mạng hotspot (vài thiết bị) | Điện thoại, laptop | Không có mật khẩu router, muốn tập trên mạng thật an toàn |
| 4. Mạng thật | Cả Wi-Fi của bạn | Được phép của chủ mạng | Mạng nhà bạn, hoặc câu lạc bộ đã đồng ý |
Nếu giảng cho cả lớp: cả lớp cùng làm cách 1, rồi cách 2, rồi mỗi người làm cách 3 hoặc 4.
Cách 1 · Dữ liệu mẫu: học đọc kết quả, không cần mạng
Hoạt họa: hôm nay khác hôm qua ở đâu?
Dùng đúng ba thay đổi của dữ liệu mẫu ở bên dưới. Đây là mô phỏng; không quét mạng của bạn.
--demo 1 --save ngay1.jsonChương trình lấy dữ liệu mẫu ngày 1 và ghi vào ngay1.json. Hãy giữ file này trước khi chạy lệnh thứ hai.
Điều cần hiểu: Có mốc để so sánh.
Thiet bi MOI: 192.168.1.77Chương trình so MAC đầy đủ giữa hai lần. MAC mới có thể là thiết bị mới, hoặc điện thoại quen vừa đổi MAC riêng.
Điều cần hiểu: Mới trong quan sát ≠ kẻ xâm nhập.
Thiet bi KHONG con tra loi: 192.168.1.52Không trả lời chỉ mô tả lần quan sát này. Chưa thể kết luận điện thoại đã rời mạng hoặc bị tấn công.
Điều cần hiểu: Thử lại và đối chiếu với người dùng.
Port THAY DOI: moi mo [8000], da dong [3389]Chương trình theo cùng MAC rồi so danh sách cổng. Hỏi người dùng máy thủ quỹ có vừa chạy máy chủ thử không; tắt dịch vụ nếu không cần.
Điều cần hiểu: Kiểm kê phải theo dõi cả thiết bị lẫn dịch vụ.
Hình minh họa không chạy Python và không gửi gói tin. Bấm Tiếp để học theo nhịp của bạn; Phát tự chuyển cảnh mỗi 9 giây.
--demo tạo một mạng giả định gồm bảy thiết bị, không gửi gói nào ra mạng. Chạy "ngày 1", rồi "ngày 2" và so với ngày 1:
python3 lan_inventory.py --demo 1 --save ngay1.json
python3 lan_inventory.py --demo 2 --baseline ngay1.json --save ngay2.jsonpy lan_inventory.py --demo 1 --save ngay1.json
py lan_inventory.py --demo 2 --baseline ngay1.json --save ngay2.jsonpython3 lan_inventory.py --demo 1 --save ngay1.json
python3 lan_inventory.py --demo 2 --baseline ngay1.json --save ngay2.json[DU LIEU MAU ngay 1: khong quet mang that]
IP MAC Hang / ghi chu Ten
192.168.1.1 f4:ec:38:11:22:33 Hang router A router.lan
mo port 80 http Server: lighttpd
DOAN: router | HDH: nhom Linux/macOS/iOS/Android (TTL 64)
192.168.1.40 3c:22:fb:aa:bb:cc Hang may in X npi-3f2a
mo port 80 http Server: Embedded HTTP Server
mo port 631 ipp (may in)
mo port 9100 may in
DOAN: may in | HDH: thiet bi mang/may in (TTL 255)
192.168.1.45 9c:b2:00:44:55:66 Hang camera Y cam-phong-1
mo port 23 telnet
mo port 80 http Server: CamHTTPD/1.0
mo port 554 rtsp (camera)
DOAN: camera, thiet bi cu (Telnet): coi chung | HDH: nhom Linux/macOS/iOS/Android (TTL 64)
192.168.1.52 da:a1:19:00:00:01 MAC rieng/ngau nhien (dien thoai
mo port 62078 iphone-sync
DOAN: iPhone/iPad | HDH: nhom Linux/macOS/iOS/Android (TTL 64)
192.168.1.70 00:1b:21:aa:bb:cc Hang may tinh Z PC-THUQUY
mo port 445 smb
mo port 3389 rdp
DOAN: chia se file Windows/NAS, may Windows (Remote Desktop) | HDH: nhom Windows (TTL 128)
...
Tong cong: 7 thiet bi tra loi.Đọc: dòng mo port là cổng mở. Dòng DOAN là bước ghép manh mối của bước 4 làm tự động. Dòng HDH là họ hệ điều hành đoán từ TTL. Cả hai chỉ là gợi ý.
Thiet bi MOI so voi lan truoc: ['192.168.1.77 (c2:11:aa:bb:cc:dd)']
Thiet bi KHONG con tra loi: ['192.168.1.52 (da:a1:19:00:00:01)']
Port THAY DOI o 192.168.1.70: moi mo [8000], da dong [3389]Đọc như một nhà kiểm kê:
.77mới, MAC ngẫu nhiên, không tên: cần xác minh (bước 8)..52không còn trả lời: có thể điện thoại đã về..70(máy thủ quỹ) đã đóng Remote Desktop (tốt), nhưng mới mở port 8000: ai đó vừa chạy máy chủ thử trên máy giữ dữ liệu lệ phí. Cần hỏi và tắt nếu không cần.- Camera
.45vẫn mở Telnet cả hai ngày: rủi ro chưa được xử lý.
Đó là toàn bộ ý nghĩa của việc so sánh hai lần quét.
Cách 2 · Thiết bị giả trên chính máy bạn
Hai terminal, cùng một chiếc laptop
Terminal A mở dịch vụ giả lập. Giữ A chạy và dùng B để tìm các cổng của chính máy đó.
python3 fake_devices.py127.0.0.1 là địa chỉ quay về chính máy bạn. Chương trình giả lập chỉ lắng nghe ở địa chỉ này. Windows dùng py thay python3.
Điều cần hiểu: Giữ Terminal A mở; đừng chạy lệnh B trong A.
python3 lan_inventory.py --only 127.0.0.1--only bỏ qua bước tìm thiết bị trong Wi-Fi. B kết nối tới các cổng của máy bạn; A nhận kết nối ở các cổng giả lập.
Điều cần hiểu: Thấy 4 cổng giả lập và có thể thêm cổng thật của máy.
DOAN: iPhone/iPad, may Windows, may inCông cụ đoán theo cổng nên một máy có thể nhận nhiều nhãn. Dòng chào cũng có thể được lập trình giả. Đây là lý do phải ghép bằng chứng.
Điều cần hiểu: Cổng và banner mô tả dịch vụ; chưa xác minh loại máy.
Ctrl+C ở A → chạy lại lệnh ở BCác cổng do fake_devices.py mở sẽ biến mất khi dừng chương trình. Các dịch vụ khác có sẵn trên máy vẫn có thể được tìm thấy.
Điều cần hiểu: Bạn vừa chứng kiến: dừng dịch vụ → cổng đóng.
Hình minh họa không chạy Python và không gửi gói tin. Bấm Tiếp để học theo nhịp của bạn; Phát tự chuyển cảnh mỗi 9 giây.
Cách này tạo dịch vụ thật chạy ở 127.0.0.1 (chính máy bạn), giả làm một thiết bị đáng ngờ, rồi dùng công cụ quét port để tìm ra nó. Tạo thêm file fake_devices.py:
fake_devices.py · Tải file vào thư mục lan-lab, hoặc mở mã để sao chép vào trình soạn thảo.
Xem mã Python · có tô màu và số dòng
# fake_devices.py - gia lap mot "thiet bi dang ngo" ngay tren may ban de thu quet port. Chi nghe 127.0.0.1.
# Chay: python3 fake_devices.py roi o terminal khac: python3 lan_inventory.py --only 127.0.0.1
import socket
import threading
# port -> noi dung tra loi (None = nhan ket noi roi dong im lang)
FAKE = {
8080: b"HTTP/1.0 200 OK\r\nServer: CamHTTPD/1.0 (camera gia lap)\r\n\r\n",
9100: None,
62078: None,
3389: None,
}
def serve(port, reply):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("127.0.0.1", port))
s.listen(5)
while True:
conn, _ = s.accept()
with conn:
if reply:
conn.settimeout(1)
try:
conn.recv(200)
except OSError:
pass
conn.sendall(reply)
for port, reply in FAKE.items():
threading.Thread(target=serve, args=(port, reply), daemon=True).start()
print("Dang gia lap thiet bi o 127.0.0.1, cac port:", ", ".join(str(p) for p in FAKE))
print("Nhan Ctrl+C de dung.")
threading.Event().wait()
Tạo file bằng terminal · chọn hệ điều hành của bạn
cat > fake_devices.py <<'EOF'
# fake_devices.py - gia lap mot "thiet bi dang ngo" ngay tren may ban de thu quet port. Chi nghe 127.0.0.1.
# Chay: python3 fake_devices.py roi o terminal khac: python3 lan_inventory.py --only 127.0.0.1
import socket
import threading
# port -> noi dung tra loi (None = nhan ket noi roi dong im lang)
FAKE = {
8080: b"HTTP/1.0 200 OK\r\nServer: CamHTTPD/1.0 (camera gia lap)\r\n\r\n",
9100: None,
62078: None,
3389: None,
}
def serve(port, reply):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("127.0.0.1", port))
s.listen(5)
while True:
conn, _ = s.accept()
with conn:
if reply:
conn.settimeout(1)
try:
conn.recv(200)
except OSError:
pass
conn.sendall(reply)
for port, reply in FAKE.items():
threading.Thread(target=serve, args=(port, reply), daemon=True).start()
print("Dang gia lap thiet bi o 127.0.0.1, cac port:", ", ".join(str(p) for p in FAKE))
print("Nhan Ctrl+C de dung.")
threading.Event().wait()
EOF@'
# fake_devices.py - gia lap mot "thiet bi dang ngo" ngay tren may ban de thu quet port. Chi nghe 127.0.0.1.
# Chay: python3 fake_devices.py roi o terminal khac: python3 lan_inventory.py --only 127.0.0.1
import socket
import threading
# port -> noi dung tra loi (None = nhan ket noi roi dong im lang)
FAKE = {
8080: b"HTTP/1.0 200 OK\r\nServer: CamHTTPD/1.0 (camera gia lap)\r\n\r\n",
9100: None,
62078: None,
3389: None,
}
def serve(port, reply):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("127.0.0.1", port))
s.listen(5)
while True:
conn, _ = s.accept()
with conn:
if reply:
conn.settimeout(1)
try:
conn.recv(200)
except OSError:
pass
conn.sendall(reply)
for port, reply in FAKE.items():
threading.Thread(target=serve, args=(port, reply), daemon=True).start()
print("Dang gia lap thiet bi o 127.0.0.1, cac port:", ", ".join(str(p) for p in FAKE))
print("Nhan Ctrl+C de dung.")
threading.Event().wait()
'@ | Set-Content -Path fake_devices.py -Encoding utf8cat > fake_devices.py <<'EOF'
# fake_devices.py - gia lap mot "thiet bi dang ngo" ngay tren may ban de thu quet port. Chi nghe 127.0.0.1.
# Chay: python3 fake_devices.py roi o terminal khac: python3 lan_inventory.py --only 127.0.0.1
import socket
import threading
# port -> noi dung tra loi (None = nhan ket noi roi dong im lang)
FAKE = {
8080: b"HTTP/1.0 200 OK\r\nServer: CamHTTPD/1.0 (camera gia lap)\r\n\r\n",
9100: None,
62078: None,
3389: None,
}
def serve(port, reply):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("127.0.0.1", port))
s.listen(5)
while True:
conn, _ = s.accept()
with conn:
if reply:
conn.settimeout(1)
try:
conn.recv(200)
except OSError:
pass
conn.sendall(reply)
for port, reply in FAKE.items():
threading.Thread(target=serve, args=(port, reply), daemon=True).start()
print("Dang gia lap thiet bi o 127.0.0.1, cac port:", ", ".join(str(p) for p in FAKE))
print("Nhan Ctrl+C de dung.")
threading.Event().wait()
EOFMở hai terminal. Terminal A giữ chương trình giả lập chạy. Terminal B quét 127.0.0.1 (--only bỏ qua bước tìm thiết bị, chỉ quét port của đúng một IP):
# Terminal A
python3 fake_devices.py
# Terminal B
python3 lan_inventory.py --only 127.0.0.1# Terminal A
py fake_devices.py
# Terminal B
py lan_inventory.py --only 127.0.0.1# Terminal A
python3 fake_devices.py
# Terminal B
python3 lan_inventory.py --only 127.0.0.1Port mo tren 127.0.0.1: 3389/rdp, 8080/http-proxy, 9100/may in, 62078/iphone-sync
3389 rdp
8080 http-proxy CamHTTPD/1.0 (camera gia lap)
9100 may in
62078 iphone-sync
DOAN: iPhone/iPad, may Windows (Remote Desktop), may inCamHTTPD/1.0 (camera gia lap). Nhưng một thiết bị lại giống cả iPhone, máy Windows và máy in cùng lúc. Đó là dấu hiệu của thiết bị "lạ" hoặc giả mạo. Nếu máy bạn hiện thêm port 5000, 7000 (AirPlay của macOS) hay 8000 (máy chủ thử ở bài 6), đó là dịch vụ có thật trên máy bạn: đúng là thứ kiểm kê để bạn nhìn thấy. Dừng bằng Ctrl+C ở Terminal A khi xong.Cách 3 · Hotspot điện thoại: mạng của riêng bạn
Hotspot: điện thoại đóng vai router
Hình minh họa một mạng của riêng bạn. Địa chỉ thực tế có thể khác hình.
Nối laptop vào hotspot trướcĐiện thoại phát Wi-Fi trở thành điểm kết nối của mạng thử. Kiểm tra tên Wi-Fi trên laptop để tránh quét nhầm mạng đang nối.
Điều cần hiểu: Chỉ kiểm kê mạng thử mà bạn quản lý.
--net 172.20.10.0/28 --save hotspot.jsonChỉ dùng dải ví dụ khi nó khớp IP/mặt nạ trên laptop. Có thể bỏ --net để công cụ tự đọc cấu hình mạng.
Điều cần hiểu: Kết quả phụ thuộc dải thật và khả năng trả lời của các máy.
So danh sách với màn hình hotspotMột số hotspot cách ly các máy khách, nên laptop không nhìn thấy mọi máy đang kết nối. Kết quả ít hơn không chứng minh thiết bị không tồn tại.
Điều cần hiểu: Ghi rõ giới hạn của lần quan sát.
Hình minh họa không chạy Python và không gửi gói tin. Bấm Tiếp để học theo nhịp của bạn; Phát tự chuyển cảnh mỗi 9 giây.
Biến điện thoại thành "router" của riêng bạn. Bạn là người quản trị mạng này nên không phải xin mật khẩu của ai.
- Bật điểm phát Wi-Fi trên điện thoại (iPhone: Điểm truy cập cá nhân, Android: Điểm phát sóng). Đặt mật khẩu dài.
- Nối laptop vào Wi-Fi đó. Có thể nối thêm một thiết bị của bạn bè đồng ý tham gia.
- Chạy công cụ. iPhone cấp dải nhỏ
172.20.10.0/28:
python3 lan_inventory.py --net 172.20.10.0/28 --save hotspot.jsonpy lan_inventory.py --net 172.20.10.0/28 --save hotspot.jsonpython3 lan_inventory.py --net 172.20.10.0/28 --save hotspot.jsonHotspot Android thường dùng 192.168.x.0/24: bỏ --net để chương trình tự chọn. Bạn nên thấy điện thoại (thường .1), laptop và thiết bị bạn nối thêm. Đối chiếu: số thiết bị đếm được phải khớp với màn hình hotspot (Android thường liệt kê thiết bị). Thiếu thì chạy lại vài lần.
Cách 4 · Mạng thật (cần được phép)
Mạng thật: từ bản ghi đầu tới lần kiểm tra sau
Đi từng cảnh để hiểu hai tham số --save và --baseline trước khi chạy lệnh.
lan_inventory.py --save baseline.json--save đặt tên file kết quả. Lần đầu chưa có bản cũ nên chưa biết thiết bị nào mới so với hôm qua.
Điều cần hiểu: Có bản ghi đầu để đối chiếu.
lan_inventory.py --baseline baseline.json --save today.json--baseline là file đem ra so, --save là nơi lưu kết quả mới. Dùng hai tên khác nhau để giữ bản gốc cho việc đối chiếu.
Điều cần hiểu: Nhận danh sách mới, không trả lời và thay đổi cổng.
Quan sát → đối chiếu → ghi việc cần làmKết quả công cụ là đầu vào cho bảng kiểm kê. Ghi loại thiết bị, bằng chứng và mức chắc chắn; không gán nhãn nguy hiểm chỉ vì một dòng mới xuất hiện.
Điều cần hiểu: Bước 8 hướng dẫn xác minh và xử lý.
Hình minh họa không chạy Python và không gửi gói tin. Bấm Tiếp để học theo nhịp của bạn; Phát tự chuyển cảnh mỗi 9 giây.
Dùng khi bạn đang ở mạng nhà hoặc mạng câu lạc bộ mà bạn được phép kiểm kê. Chạy lần đầu để lấy "ảnh chụp" ban đầu:
Vẫn một dòng? Xem dòng đầu terminal: nếu có
(.venv) hoặc bạn cài Python bằng uv, pyenv, conda, thì python3 đang là bản Python riêng và macOS có thể chặn nó. Chạy bằng Python có sẵn của macOS: /usr/bin/python3 lan_inventory.py --save baseline.json.python3 lan_inventory.py --save baseline.jsonpy lan_inventory.py --save baseline.jsonpython3 lan_inventory.py --save baseline.json| Phần | Ý nghĩa |
|---|---|
--save baseline.json | Lưu kết quả vào file JSON: "ảnh chụp" mạng hôm nay để so sánh lần sau. |
--net 10.0.0.0/22 | Chỉ cần khi mạng không phải /24 và công cụ đoán sai. Bình thường cứ bỏ qua. |
--no-ports | Bỏ bước quét port: nhanh hơn, chỉ ra IP, MAC, hãng, tên. |
arp -a ở bước 2?Lệnh này gõ cửa mọi IP trong dải (nên chờ vài giây), rồi mới đọc sổ ARP. Vì vậy danh sách thường dài hơn kết quả arp -a bạn chạy lúc đầu. Hãy so số dòng hai lần.Chờ khoảng 5 đến 30 giây. Chương trình in không dấu để không lỗi font trong terminal. Kết quả mẫu (hãng, tên, địa chỉ là minh họa):
Dang kiem tra 192.168.1.0/24 (256 dia chi), cho khoang 5-30 giay...
IP MAC Hang / ghi chu Ten
192.168.1.1 f4:ec:38:11:22:33 Hang router A router.lan
mo port 80 http Server: lighttpd
DOAN: router | HDH: nhom Linux/macOS/iOS/Android (TTL 64)
192.168.1.23 6a:6c:4a:21:dc:21 MAC rieng/ngau nhien (dien thoai macbook-cua-ban.lan
DOAN: Mac | HDH: nhom Linux/macOS/iOS/Android (TTL 64)
192.168.1.40 3c:22:fb:aa:bb:cc Hang may in X npi-3f2a
mo port 80 http Server: Embedded HTTP Server
mo port 631 ipp (may in)
mo port 9100 may in
DOAN: may in | HDH: thiet bi mang/may in (TTL 255)
192.168.1.52 da:a1:19:00:00:01 MAC rieng/ngau nhien (dien thoai
DOAN: chua du dau hieu | HDH: nhom Linux/macOS/iOS/Android (TTL 64)
Tong cong: 4 thiet bi tra loi.
Da luu baseline.json192.168.68.0/22 (1024 địa chỉ): mỗi lần quét xong trong khoảng 25 giây, tra đúng hãng TP-Link, Roborock, Xiaomi. Số thiết bị ra 5, rồi 8, rồi 11 qua các lần quét (thiết bị ngủ thức dậy, bộ mesh trả lời chậm). Bảng nhận diện đầy đủ của mạng này nằm ở Bước 4. Mạng của bạn ra nhiều hơn hoặc ít hơn đều bình thường..1: router. Cổng 80 là trang quản lý..40: hãng máy in, hai cổng in ấn, TTL 255. Rất chắc là máy in..52: MAC riêng, không mở cổng, TTL 64. Nhiều khả năng là điện thoại.
Vài giờ hoặc vài ngày sau, chạy lại và chỉ cho chương trình file cũ để so sánh:
python3 lan_inventory.py --baseline baseline.json --save today.jsonpy lan_inventory.py --baseline baseline.json --save today.jsonpython3 lan_inventory.py --baseline baseline.json --save today.jsonThiết bị mới chưa chắc là kẻ lạ: điện thoại đổi MAC ngẫu nhiên, hoặc khách vừa đến. Cách xử lý ở bước 8.
Đọc thêm: lỗi thường gặp khi chạy công cụ (không bắt buộc)
| Dấu hiệu | Nguyên nhân thường gặp | Cách xử lý |
|---|---|---|
| Chỉ thấy vài thiết bị | Điện thoại đang ngủ, hoặc máy bạn ở mạng khách | Chạy lại 2–3 lần cách vài phút. Nối đúng mạng chính |
| Chỉ thấy đúng máy mình, hoặc báo "No route to host" (macOS mới) | macOS chưa cho Terminal hoặc VS Code truy cập mạng cục bộ | Khi macOS hỏi, chọn Cho phép. Đã từ chối: Cài đặt hệ thống → Quyền riêng tư và bảo mật → Mạng cục bộ, bật cho Terminal hoặc Visual Studio Code, rồi mở lại ứng dụng. Vẫn không được và terminal có (.venv): dùng /usr/bin/python3 thay cho python3 |
| Quét hotspot iPhone chậm | Dải mặc định rộng hơn dải /28 của hotspot | Thêm --net 172.20.10.0/28 |
| Báo "Chi quet dai IP rieng …" | Dải IP không phải dải riêng, hoặc quá lớn | Kiểm tra --net. Đây là bảo vệ có chủ ý |
| Quét ra dải lạ như 10.8.0.0/24 | Máy đang bật VPN | Tắt VPN rồi chạy lại |
| Cột hãng ghi "chua co oui.txt" | Chưa tải bước 4 của lab, hoặc sai thư mục | Tải oui.txt vào cùng thư mục với lan_inventory.py |
| Rất nhiều "MAC rieng" | Thiết bị bật MAC ngẫu nhiên | Bình thường. Dựa vào tên và port |
python3: command not found | Chưa cài Python hoặc chưa mở lại terminal | Xem bước 0 của bài 6 |
Đọc thêm: hỏi thiết bị tự giới thiệu bằng SSDP (không bắt buộc)
Một đoạn Python ngắn gửi một gói UDP hỏi "ai là thiết bị UPnP?" và in tên thiết bị trả lời. Hữu ích để nhận diện TV, router, máy in. Chỉ chạy trên mạng của bạn:
ssdp_discover.py · Tải file vào thư mục lan-lab, hoặc mở mã để sao chép vào trình soạn thảo.
Xem mã Python · có tô màu và số dòng
# ssdp_discover.py - hoi "ai la thiet bi UPnP o day?" bang goi UDP multicast. TV, router, may in thuong tra loi.
# Chi chay tren mang cua ban. Mat khoang 4 giay.
import socket
import time
MESSAGE = ("M-SEARCH * HTTP/1.1\r\nHOST: 239.255.255.250:1900\r\n"
'MAN: "ssdp:discover"\r\nMX: 2\r\nST: ssdp:all\r\n\r\n').encode()
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.settimeout(1)
s.sendto(MESSAGE, ("239.255.255.250", 1900))
seen = {}
deadline = time.time() + 4
while time.time() < deadline:
try:
data, addr = s.recvfrom(4096)
except socket.timeout:
continue
headers = {}
for line in data.decode("utf-8", errors="replace").splitlines()[1:]:
key, _, value = line.partition(":")
headers[key.strip().upper()] = value.strip()
seen.setdefault(addr[0], headers)
for ip, h in sorted(seen.items()):
print(f"{ip:<16} SERVER={h.get('SERVER', '?')}")
print(f"{'':<16} LOCATION={h.get('LOCATION', '?')}")
print(f"\nCo {len(seen)} thiet bi tra loi SSDP (thiet bi khong ho tro UPnP se im lang).")
Tạo file bằng terminal · chọn hệ điều hành của bạn
cat > ssdp_discover.py <<'EOF'
# ssdp_discover.py - hoi "ai la thiet bi UPnP o day?" bang goi UDP multicast. TV, router, may in thuong tra loi.
# Chi chay tren mang cua ban. Mat khoang 4 giay.
import socket
import time
MESSAGE = ("M-SEARCH * HTTP/1.1\r\nHOST: 239.255.255.250:1900\r\n"
'MAN: "ssdp:discover"\r\nMX: 2\r\nST: ssdp:all\r\n\r\n').encode()
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.settimeout(1)
s.sendto(MESSAGE, ("239.255.255.250", 1900))
seen = {}
deadline = time.time() + 4
while time.time() < deadline:
try:
data, addr = s.recvfrom(4096)
except socket.timeout:
continue
headers = {}
for line in data.decode("utf-8", errors="replace").splitlines()[1:]:
key, _, value = line.partition(":")
headers[key.strip().upper()] = value.strip()
seen.setdefault(addr[0], headers)
for ip, h in sorted(seen.items()):
print(f"{ip:<16} SERVER={h.get('SERVER', '?')}")
print(f"{'':<16} LOCATION={h.get('LOCATION', '?')}")
print(f"\nCo {len(seen)} thiet bi tra loi SSDP (thiet bi khong ho tro UPnP se im lang).")
EOF@'
# ssdp_discover.py - hoi "ai la thiet bi UPnP o day?" bang goi UDP multicast. TV, router, may in thuong tra loi.
# Chi chay tren mang cua ban. Mat khoang 4 giay.
import socket
import time
MESSAGE = ("M-SEARCH * HTTP/1.1\r\nHOST: 239.255.255.250:1900\r\n"
'MAN: "ssdp:discover"\r\nMX: 2\r\nST: ssdp:all\r\n\r\n').encode()
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.settimeout(1)
s.sendto(MESSAGE, ("239.255.255.250", 1900))
seen = {}
deadline = time.time() + 4
while time.time() < deadline:
try:
data, addr = s.recvfrom(4096)
except socket.timeout:
continue
headers = {}
for line in data.decode("utf-8", errors="replace").splitlines()[1:]:
key, _, value = line.partition(":")
headers[key.strip().upper()] = value.strip()
seen.setdefault(addr[0], headers)
for ip, h in sorted(seen.items()):
print(f"{ip:<16} SERVER={h.get('SERVER', '?')}")
print(f"{'':<16} LOCATION={h.get('LOCATION', '?')}")
print(f"\nCo {len(seen)} thiet bi tra loi SSDP (thiet bi khong ho tro UPnP se im lang).")
'@ | Set-Content -Path ssdp_discover.py -Encoding utf8cat > ssdp_discover.py <<'EOF'
# ssdp_discover.py - hoi "ai la thiet bi UPnP o day?" bang goi UDP multicast. TV, router, may in thuong tra loi.
# Chi chay tren mang cua ban. Mat khoang 4 giay.
import socket
import time
MESSAGE = ("M-SEARCH * HTTP/1.1\r\nHOST: 239.255.255.250:1900\r\n"
'MAN: "ssdp:discover"\r\nMX: 2\r\nST: ssdp:all\r\n\r\n').encode()
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.settimeout(1)
s.sendto(MESSAGE, ("239.255.255.250", 1900))
seen = {}
deadline = time.time() + 4
while time.time() < deadline:
try:
data, addr = s.recvfrom(4096)
except socket.timeout:
continue
headers = {}
for line in data.decode("utf-8", errors="replace").splitlines()[1:]:
key, _, value = line.partition(":")
headers[key.strip().upper()] = value.strip()
seen.setdefault(addr[0], headers)
for ip, h in sorted(seen.items()):
print(f"{ip:<16} SERVER={h.get('SERVER', '?')}")
print(f"{'':<16} LOCATION={h.get('LOCATION', '?')}")
print(f"\nCo {len(seen)} thiet bi tra loi SSDP (thiet bi khong ho tro UPnP se im lang).")
EOFpython3 ssdp_discover.pypy ssdp_discover.pypython3 ssdp_discover.py192.168.1.1 SERVER=Linux/4.4 UPnP/1.0 router-hang-A/1.0
LOCATION=http://192.168.1.1:5000/rootDesc.xml
192.168.1.30 SERVER=Linux/3.10 UPnP/1.0 Samsung-TV/1.0
LOCATION=http://192.168.1.30:9197/dmr
Co 2 thiet bi tra loi SSDP (thiet bi khong ho tro UPnP se im lang).Thiết bị không hỗ trợ UPnP sẽ không trả lời, nên đoạn này chỉ bổ sung, không thay thế các bước trên. Giao thức này gọi là .
BÀI TẬP 3 · 10 PHÚT
Chạy công cụ và lập bảng kiểm kê
Chạy lan_inventory.py theo một trong bốn cách. Lập bảng có các cột: IP, MAC, hãng, tên, port mở, loại thiết bị (đoán), bằng chứng. Ghi "chưa rõ" cho thiết bị chưa nhận ra. Đếm: bao nhiêu thiết bị bạn đoán được chắc, bao nhiêu còn chưa rõ?
Xem đáp án
Không có đáp án duy nhất. Một bảng tốt: mỗi dòng có loại thiết bị kèm bằng chứng cụ thể (ví dụ "máy in vì hãng X và port 631/9100"), thiết bị nào không chắc thì ghi mức chắc chắn. Với dòng "chưa rõ": nhờ người trong nhà nhận, ngắt Wi-Fi từng thiết bị xem dòng nào biến mất, hoặc xem MAC trong cài đặt thiết bị. Thường sẽ thấy: router, máy tính, điện thoại, TV, máy in, vài thiết bị thông minh. Ít nhất một số dùng MAC riêng nên không tra được hãng.
Bước 8 · Thấy thiết bị lạ thì làm gì?
15 phútThiết bị mới: xác minh trước khi xử lý
Một tình huống minh họa có hai kết quả có thể xảy ra. Hãy đi hết các cảnh để phân biệt chúng.
Ghi IP, MAC đầy đủ và thời điểmMAC riêng/ngẫu nhiên là tính năng thường gặp trên điện thoại. Chỉ nhìn IP, hãng hoặc việc không mở cổng thì chưa biết chủ thiết bị.
Điều cần hiểu: Việc đầu tiên: hỏi những người đang dùng mạng.
Hỏi chủ máy → đối chiếu MAC trong cài đặt Wi-FiCó thể ngắt Wi-Fi thiết bị nghi là tương ứng rồi quét lại để bổ sung bằng chứng. Bảng ARP cũ có thể còn giữ dòng, nên không dùng một lần biến mất/xuất hiện để kết luận.
Điều cần hiểu: Đối chiếu trực tiếp giúp xác minh danh tính.
Khớp bằng chứng → cập nhật ghi chúGhi tên thiết bị và chủ sử dụng. Nếu có mạng khách được cách ly đúng, cho khách dùng mạng đó để giảm truy cập tới máy giữ dữ liệu.
Điều cần hiểu: Hoàn thành nhánh đã nhận ra chủ thiết bị.
Không xác minh được → quản trị viên xử lý → quét lạiTheo quyền quản trị của bạn: đổi mật khẩu Wi-Fi, kiểm tra WPA2/WPA3, tắt WPS, ngắt các phiên kết nối chưa rõ và nối lại thiết bị đã biết. Ghi việc đã làm và kiểm kê lại để xác nhận.
Điều cần hiểu: Đây là nhánh thay thế khi chưa nhận ra thiết bị.
Hình minh họa không chạy Python và không gửi gói tin. Bấm Tiếp để học theo nhịp của bạn; Phát tự chuyển cảnh mỗi 9 giây.
Đừng vội kết luận có người xâm nhập. Làm theo thứ tự:
- Hỏi trước. Ai vừa mang thiết bị mới đến? Khách có xin mật khẩu Wi-Fi hôm nay không?
- Đối chiếu. Tên, hãng, port có khớp thiết bị nào bạn biết không? Tắt Wi-Fi từng thiết bị, xem dòng nào biến mất.
- Vẫn không nhận ra thì coi như không đáng tin. Đổi mật khẩu Wi-Fi sang mật khẩu dài mới (bài 3), rồi nối lại các thiết bị đã biết. Thiết bị lạ sẽ bị loại.
- Ghi lại thời điểm, MAC, IP và việc bạn đã làm (bài 4).
Giảm rủi ro cho mạng Wi-Fi
| Việc cần làm | Vì sao |
|---|---|
| Dùng WPA3 (hoặc ít nhất WPA2) và mật khẩu Wi-Fi dài | Mật khẩu yếu cho kẻ ở gần đoán ra và vào hẳn mạng |
| Tắt WPS (nút kết nối nhanh bằng mã PIN) | WPS từng có lỗi cho phép đoán PIN |
| Tạo mạng khách cho khách | Khách không chạm được vào máy in, máy thủ quỹ |
| Đặt camera, TV, thiết bị thông minh vào mạng riêng hoặc mạng khách | Chúng hay ít được cập nhật. Nếu một cái bị chiếm, nó không nên thấy máy giữ dữ liệu |
| Cập nhật firmware, đổi mật khẩu quản trị mặc định | Phần lớn vụ xâm nhập dùng mật khẩu mặc định hoặc lỗi đã có bản vá |
| Tắt dịch vụ không dùng (Remote Desktop, Telnet, chia sẻ file) | Mỗi cổng mở là một điểm có thể bị tấn công |
| Quét kiểm kê định kỳ và so với lần trước | Thiết bị mới hoặc port mới được phát hiện sớm |
Nối với bài 2: kiểm kê là bước nhận biết tài sản. Máy thủ quỹ giữ dữ liệu lệ phí, thiệt hại cao hơn TV phòng sinh hoạt, nên ưu tiên hơn.
BÀI TẬP 4 · 8 PHÚT
Chẩn đoán bốn tình huống
Với mỗi tình huống, nêu giải thích có thể nhất và việc làm đầu tiên:
- Trang router báo 12 thiết bị, nhưng câu lạc bộ chỉ có khoảng 7.
- Công cụ thấy một thiết bị mới có MAC ngẫu nhiên, tên rỗng, không mở cổng nào, xuất hiện lúc 22:00 rồi biến mất sáng hôm sau.
- Camera có port 23 (Telnet) mở.
- Bạn chạy công cụ ở nhà và chỉ thấy máy mình và router, dù nhà có nhiều máy khác đang bật.
Xem đáp án
- Router thường liệt kê cả thiết bị đã rời đi, hoặc thiết bị phụ (loa, bóng đèn). Đối chiếu từng MAC với bản kiểm kê. Việc đầu tiên: ghi tên cho từng thiết bị trong trang router.
- Nhiều khả năng là điện thoại của người đến buổi tối, dùng MAC ngẫu nhiên. Hỏi người trong nhà, đối chiếu MAC trong cài đặt máy. Không ai nhận thì đổi mật khẩu Wi-Fi.
- Rủi ro cao: Telnet không mã hóa, hay dùng mật khẩu mặc định. Tắt Telnet, đổi mật khẩu, cập nhật firmware, đưa camera sang mạng riêng.
- Có thể máy bạn ở mạng khách, hoặc bật cách ly Wi-Fi, hoặc bật VPN. Kiểm tra mạng đang nối, tắt VPN, xem trang router.
BÀI TẬP 5 · Về nhà, 25 PHÚT
Viết báo cáo kiểm kê mạng và kế hoạch xử lý
Với mạng của bạn (hoặc mạng thử được phép), làm ba việc:
- Chạy
lan_inventory.py --save baseline.json, rồi chạy lại sau ít nhất một ngày với--baseline baseline.json. - Lập bảng kiểm kê (như bài tập 3). Đánh dấu thiết bị cần xem xét: mở Telnet, mở Remote Desktop, MAC ngẫu nhiên chưa nhận ra, thiết bị không ai nhận.
- Viết ngắn: ba rủi ro lớn nhất theo mức độ, mỗi rủi ro kèm một việc cụ thể và người chịu trách nhiệm.
Không đưa file JSON hay bảng chứa MAC và IP thật của người khác lên nơi công khai.
Xem đáp án
Không có đáp án duy nhất. Một báo cáo tốt: mỗi thiết bị có bằng chứng nhận diện; có phân biệt "đã xác minh" với "đoán"; rủi ro xếp theo thiệt hại (máy giữ dữ liệu quan trọng, dịch vụ không mã hóa, thiết bị không ai nhận) chứ không theo số lượng; biện pháp cụ thể, ví dụ "tắt Telnet trên camera phòng 1, người phụ trách: Minh, trước thứ Sáu".
Tóm tắt
- Số thiết bị phụ thuộc nguồn bạn hỏi: router, bảng , hay quét chủ động. Đếm theo MAC, không đếm theo IP, và ghi "ít nhất N".
- giúp tìm MAC từ IP trong cùng mạng. Bảng ARP có thể cũ hoặc thiếu, nên chưa phải danh sách đầy đủ.
- cho biết hãng qua , nhưng không cho biết thiết bị là gì. MAC ngẫu nhiên thì không tra được hãng.
- Nhận diện là ghép manh mối: hãng, tên, cổng mở, TTL. Kết luận là "nhiều khả năng". Muốn chắc, đối chiếu với chính thiết bị.
- Kiểm kê chỉ có giá trị khi lặp lại và so sánh. Thấy thiết bị lạ thì xác minh trước, rồi đổi mật khẩu Wi-Fi, tách mạng khách và IoT, tắt dịch vụ không dùng.
- Chỉ quét mạng của chính mình hoặc nơi được cho phép.
Tự kiểm tra · 5 câu có giải thích +
Ghi chú cá nhân +
Tự lưu trên trình duyệt. Không nhập thông tin bí mật.