#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 局域网设备完整扫描脚本(零依赖,Python 3 标准库) 在你要扫描的局域网内任意一台机器上运行,扫描结果自动上报到服务器。 用法: python lan_scan.py 自动探测网段并扫描,上报默认服务器 python lan_scan.py 192.168.1.0/24 指定网段 python lan_scan.py --server http://121.43.27.162:8081 指定上报地址 会抓取:IP、MAC、制造商(OUI)、主机名、设备类型、开放端口。 """ import socket, subprocess, threading, json, http.client, platform, re, sys, time from concurrent.futures import ThreadPoolExecutor from urllib.parse import urlparse SERVER = "http://121.43.27.162:8081" PORTS = [80, 443, 22, 23, 445, 3389, 8080, 8000, 5000, 554, 9100, 631, 3000, 8888, 9090, 139] # 常见厂商 OUI 前缀(前 3 字节) OUI = { "F4B52F": "TP-Link", "C06C0D": "TP-Link", "000E8F": "TP-Link", "D461DA": "TP-Link", "286C07": "Xiaomi", "640980": "Xiaomi", "7811DC": "Xiaomi", "F0B429": "Xiaomi", "A483E7": "Apple", "9801A7": "Apple", "F01898": "Apple", "ACBC32": "Apple", "001B21": "Intel", "3CA067": "Intel", "A0369F": "Intel", "001599": "Samsung", "E458B8": "Samsung", "B827EB": "Raspberry Pi", "DCA632": "Raspberry Pi", "001132": "Synology", "0011BA": "Synology", "0019B9": "Dell", "001AA0": "Dell", "14DDA9": "ASUS", "2CF05D": "ASUS", "00E04C": "Realtek", "0014BF": "Cisco", "5CCF7F": "Espressif", "84F3EB": "Espressif", "005056": "VMware", "020000": "VMware", "EC2280": "Huawei", "24E43F": "Huawei", "C82A14": "Apple", "40B034": "Hikvision", "8CE748": "Hikvision", "4409B8": "Hikvision", "3C1E04": "D-Link", "C8D3A3": "D-Link", "30CDA7": "Samsung", "D0176A": "Samsung", "080026": "Google", "94B10A": "Google", } def detect_net(): """探测本机所在网段""" ip = None try: s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.connect(("8.8.8.8", 80)) ip = s.getsockname()[0] s.close() except Exception: pass if not ip: try: ip = socket.gethostbyname(socket.gethostname()) except Exception: ip = "192.168.1.100" p = ip.split(".") return ip, "%s.%s.%s.0/24" % (p[0], p[1], p[2]) def expand_net(net): """把 x.x.x.0/24 展开成 IP 列表""" m = re.match(r"(\d+\.\d+\.\d+)\.(\d+)/(\d+)", net) if m: base, _, bits = m.group(1), int(m.group(2)), int(m.group(3)) host_bits = 32 - bits if host_bits <= 8: return [base + "." + str(i) for i in range(1, 2 ** host_bits - 1)] # 默认按 /24 m = re.match(r"(\d+\.\d+\.\d+)\.\d+", net) if m: return [m.group(1) + "." + str(i) for i in range(1, 255)] return [] def ping(ip): try: if platform.system() == "Windows": r = subprocess.run(["ping", "-n", "1", "-w", "300", ip], capture_output=True, timeout=3) else: r = subprocess.run(["ping", "-c", "1", "-W", "1", ip], capture_output=True, timeout=3) return r.returncode == 0 except Exception: return False def get_arp_table(): """读取 ARP 表 -> {ip: mac}""" table = {} try: if platform.system() == "Windows": out = subprocess.run(["arp", "-a"], capture_output=True, timeout=5).stdout for line in out.decode(errors="replace").splitlines(): m = re.search(r"(\d+\.\d+\.\d+\.\d+)\s+([0-9a-fA-F]{2}-[0-9a-fA-F]{2}-[0-9a-fA-F]{2}-[0-9a-fA-F]{2}-[0-9a-fA-F]{2}-[0-9a-fA-F]{2})", line) if m: table[m.group(1)] = m.group(2).replace("-", ":").upper() else: for line in open("/proc/net/arp").read().splitlines()[1:]: parts = line.split() if len(parts) >= 4 and parts[3] != "00:00:00:00:00:00": table[parts[0]] = parts[3].upper() except Exception: pass return table def scan_ports(ip): """TCP connect 扫描常见端口""" open_ports = [] def probe(port): try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(0.4) if s.connect_ex((ip, port)) == 0: open_ports.append(port) s.close() except Exception: pass with ThreadPoolExecutor(max_workers=32) as ex: list(ex.map(probe, PORTS)) return sorted(open_ports) def vendor(mac): if not mac: return "" oui = mac.replace(":", "").replace("-", "")[:6].upper() return OUI.get(oui, "") def get_hostname(ip): try: return socket.gethostbyaddr(ip)[0] except Exception: return "" def device_type(ip, host, mac, ports, vendor_name): h = host.lower() v = (vendor_name or "").lower() if "router" in h or "gateway" in h or "openwrt" in h: return "路由器" if "nas" in h or "synology" in h or "qnap" in h: return "NAS" if "cam" in h or "ipc" in h or v in ("hikvision",): return "摄像头" if "printer" in h or 9100 in ports or 631 in ports: return "打印机" if "desktop" in h or "pc" in h or "laptop" in h or 445 in ports or 3389 in ports: return "电脑" if v in ("apple", "xiaomi", "samsung", "google") or "phone" in h: return "手机 / 智能设备" if 554 in ports: return "摄像头" if 23 in ports and 22 in ports: return "网络设备" if 22 in ports: return "Linux / 网络设备" if 5000 in ports: return "NAS / 路由器" if "esp" in v or "espressif" in v: return "IoT 设备" return "网络设备" def report(server, net, devices): url = urlparse(server) body = json.dumps({"net": net, "devices": devices}, ensure_ascii=False) try: conn = http.client.HTTPConnection(url.hostname, url.port or 80, timeout=15) conn.request("POST", "/api/report", body, {"Content-Type": "application/json"}) resp = conn.getresponse() print("[上报] HTTP %s: %s" % (resp.status, resp.read().decode()[:100])) conn.close() except Exception as e: print("[上报失败] %s" % e) def main(): args = sys.argv[1:] server = SERVER net = None i = 0 while i < len(args): if args[i] == "--server" and i + 1 < len(args): server = args[i + 1]; i += 2; continue net = args[i]; i += 1 local_ip, detected = detect_net() net = net or detected print("本机 IP: %s" % local_ip) print("扫描网段: %s" % net) print("上报服务器: %s" % server) ips = expand_net(net) print("目标地址数: %d,开始探测存活设备…" % len(ips)) # 并发 ping 扫描 alive = [] with ThreadPoolExecutor(max_workers=64) as ex: for ip, ok in zip(ips, ex.map(ping, ips)): if ok: alive.append(ip) print("存活设备: %d 台" % len(alive)) arp = get_arp_table() devices = [] for ip in alive: ports = scan_ports(ip) mac = arp.get(ip, "") v = vendor(mac) host = get_hostname(ip) t = device_type(ip, host, mac, ports, v) d = { "ip": ip, "mac": mac, "hostname": host, "vendor": v or "未知", "type": t, "ports": ports, } # 管理入口(优先 HTTP 端口) for p in (80, 8080, 8000, 5000, 3000, 443): if p in ports: d["manage_url"] = "http://%s:%s" % (ip, p) if p != 80 else "http://%s" % ip break devices.append(d) line = " %-16s %-18s %-14s %-8s %s" % (ip, mac or "-", v or "-", t, ",".join(map(str, ports)) or "-") print(line) print("共 %d 台设备,上报中…" % len(devices)) report(server, net, devices) print("完成。刷新 http://%s 即可查看。" % server) if __name__ == "__main__": main()