一、创作灵感
日常家庭、小型局域网环境中,我们经常面临内网设备管理混乱的问题:不清楚网段内有哪些存活设备、设备对应IP/MAC地址、开放了哪些服务端口,同时无法快速区分空闲IP与占用IP,排查网络故障、接入新设备时十分繁琐。
闲置的华为EC6108v9C机顶盒硬件性能足够、功耗极低,非常适合作为7×24小时在线的小型内网服务器。因此我将该机顶盒刷入Ubuntu 20.04.6 LTS系统,依托其常驻运行的特性,开发了这款开机自启动局域网自动扫描程序。
本程序核心解决手动扫描内网低效、数据零散、无法持续监控的痛点,实现机顶盒开机自动执行全量局域网探测,自动汇总本机网络信息、内网存活设备、端口开放情况、IP占用状态、路由链路等核心数据,生成标准化日志文件,让小型局域网管理、故障排查、设备盘点实现自动化、轻量化,无需人工干预,低成本实现内网运维监控。
二、程序整体介绍
本程序基于Python3开发,适配Ubuntu 20.04系统,专门适配华为EC6108v9C机顶盒运行环境。程序部署后设置为开机自启,设备每次上电开机后,会自动完成局域网全方位扫描、网络信息采集、数据分析,并将所有结果规整写入本地日志文件,全程无人工操作,实现轻量化、无人值守的内网运维监控。
三、程序核心详细功能
1. 自动创建日志目录与文件
程序启动后优先检测日志存储路径 /home/ubuntu/downloads/systeminformation.txt,若上级目录不存在则自动创建,避免路径报错、日志写入失败,保证程序稳定运行,所有扫描数据统一归档,方便后续查看、复盘与故障溯源。
2. 本机基础网络信息采集
自动读取并汇总本机全部核心网络参数,替代手动输入查询命令,精准获取:设备主机名、本机内网IP地址、子网掩码、默认网关、DNS服务器地址,完整记录机顶盒本机网络配置,为内网环境基线记录提供依据。
3. 全网TCP端口智能扫描识别
内置百余种常用端口与服务映射库,覆盖运维、监控、物联网、数据库、视频监控、虚拟化等主流端口(HTTP/HTTPS、SSH、RDP、MySQL、Redis、ONVIF、Docker、SIP等)。程序对内网所有存活设备进行定向TCP端口扫描,自动识别开放端口对应的服务名称,区分已知服务与未知端口,精准检测设备运行服务、潜在暴露端口与安全风险。同时设置合理超时时间,兼顾扫描速度与准确率,适配机顶盒低功耗硬件性能。
4. ARP全网存活设备扫描
调用arp-scan工具遍历整个局域网网段,自动探测所有在线设备,精准采集每台设备的IP地址、MAC地址、设备厂商信息。同时对扫描结果按IP段位自动排序,输出规整清晰的设备列表,完整盘点内网所有在线终端、摄像头、路由器、工控设备、智能家居等设备,解决内网设备台账混乱问题。
5. 单设备端口绑定检测
针对每一台扫描到的内网存活设备,单独执行端口扫描,将设备IP、MAC、厂商、开放端口/服务一一对应展示,直观查看每台设备的运行服务状态,快速发现异常设备、陌生接入终端、违规开放端口,提升内网安全性。
6. 路由链路追踪检测
自动执行路由追踪任务,以公共DNS 114.114.114.114为目标,最大探测10跳路由节点,记录局域网出口路由层级、各跳网关IP地址。可用于排查网络卡顿、断连、路由跳转异常、外网访问故障等问题,快速定位网络链路故障点。
7. 网段IP占用与空闲分析
基于本机CIDR网段信息,自动遍历整个局域网可用IP段,智能区分已占用IP后缀、空闲IP后缀,并分别规整输出。解决内网IP冲突、设备IP分配混乱问题,后续新增内网设备可直接选用空闲IP,无需手动探测,极大提升组网效率。
8. 开机自启+无人值守运行
程序部署完成后配置为Ubuntu开机自启动,机顶盒每次上电开机后自动后台执行全套扫描任务,无需人工登录、手动运行,全程自动化采集数据,开机即更新最新内网全网状态,实现7×24小时轻量化内网监控。
四、运行环境说明
-
硬件设备:华为EC6108v9C机顶盒
-
操作系统:Ubuntu 20.04.6 LTS
-
运行环境:Python3 原生环境,无需额外复杂依赖
-
依赖工具:arp-scan、traceroute(系统基础工具,安装即可使用)
-
运行模式:开机自启、无人值守、自动写入日志归档
五、项目优势
-
低成本低功耗:利用闲置机顶盒改造,替代昂贵的专业运维主机、软路由,7×24小时运行功耗极低;
-
全自动化运维:开机自动扫描、自动归档、无需人工干预,适合新手与家庭、小型工作室内网使用;
-
数据维度全面:涵盖设备盘点、端口检测、路由排查、IP资源分析、本机网络信息五大核心运维维度;
-
适配性极强:专门适配机顶盒Ubuntu轻量化系统,运行流畅不卡顿,硬件资源占用极低;
-
实用性拉满:可用于内网设备盘点、网络故障排查、安全风险检测、IP资源管理、陌生设备接入监控。
总结常用端口号:
PORT_SERVICE_MAP = {
20: "FTP‑DATA",
21: "FTP",
22: "SSH",
23: "Telnet",
25: "SMTP",
42: "WINS",
49: "TACACS+",
53: "DNS",
67: "DHCP‑Server",
68: "DHCP‑Client",
69: "TFTP",
80: "HTTP",
88: "Kerberos",
110: "POP3",
123: "NTP",
135: "MS‑RPC",
137: "NetBIOS‑NS",
138: "NetBIOS‑DGM",
139: "NetBIOS‑SSN",
143: "IMAP",
161: "SNMP",
162: "SNMP‑Trap",
179: "BGP",
194: "IRC",
322: "ONVIF‑Playback",
389: "LDAP",
443: "HTTPS",
445: "SMB",
465: "SMTPS",
514: "Syslog",
554: "RTSP",
587: "SMTP‑SUB",
636: "LDAPS",
873: "rsync",
990: "FTPS",
992: "TelnetS",
993: "IMAPS",
995: "POP3S",
1080: "SOCKS",
1194: "OpenVPN",
1433: "MSSQL",
1521: "Oracle",
1723: "PPTP",
1812: "RADIUS",
1813: "RADIUS‑Accounting",
2049: "NFS",
3000: "ONVIF1/Grafana",
3128: "Squid",
3268: "LDAP‑GC",
3306: "MySQL",
3322: "DVR",
3389: "RDP",
34567: "HIK‑ISAPI",
34599: "HIK‑Manage",
5000: "UPnP‑RTSP",
5001: "HTTPS‑Stream",
5050: "SIP",
5060: "SIP",
5061: "SIP‑TLS",
5432: "PostgreSQL",
5900: "VNC",
6036: "UNIVIEW‑Private",
6060: "UNIVIEW‑Discover",
6379: "Redis",
6443: "Kubernetes",
6667: "IPC‑Private",
6789: "DAHUA‑Alarm",
8000: "HIK‑iVMS",
8008: "HTTP2",
8080: "HTTP‑Proxy/K8s‑Web",
8200: "HIK‑SADP",
8443: "HTTPS‑Alt/K8s‑HTTPS",
8450: "HIK‑Event",
8554: "RTSP‑ALT",
8899: "IPC‑Config",
9000: "DVR/NVR",
9090: "Web/Prometheus",
9200: "Elasticsearch",
10000: "Webmin",
10050: "Zabbix‑Agent",
10051: "Zabbix‑Server",
10554: "RTSP1",
19444: "RTSP2",
19999: "Netdata",
2375: "Docker",
2376: "Docker2",
27017: "MongoDB",
37777: "DAHUA‑Media",
37778: "DAHUA‑Stream",
}
程序源文
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import ipaddress
import socket
import subprocess
file_path = "/home/ubuntu/downloads/systeminformation.txt"
parent_dir = os.path.dirname(file_path)
if not os.path.exists(parent_dir):
os.makedirs(parent_dir)
print(f"目录不存在,已创建目录:{parent_dir}")
PORT_SERVICE_MAP = {
20: "FTP‑DATA",
21: "FTP",
22: "SSH",
23: "Telnet",
25: "SMTP",
42: "WINS",
49: "TACACS+",
53: "DNS",
67: "DHCP‑Server",
68: "DHCP‑Client",
69: "TFTP",
80: "HTTP",
88: "Kerberos",
110: "POP3",
123: "NTP",
135: "MS‑RPC",
137: "NetBIOS‑NS",
138: "NetBIOS‑DGM",
139: "NetBIOS‑SSN",
143: "IMAP",
161: "SNMP",
162: "SNMP‑Trap",
179: "BGP",
194: "IRC",
322: "ONVIF‑Playback",
389: "LDAP",
443: "HTTPS",
445: "SMB",
465: "SMTPS",
514: "Syslog",
554: "RTSP",
587: "SMTP‑SUB",
636: "LDAPS",
873: "rsync",
990: "FTPS",
992: "TelnetS",
993: "IMAPS",
995: "POP3S",
1080: "SOCKS",
1194: "OpenVPN",
1433: "MSSQL",
1521: "Oracle",
1723: "PPTP",
1812: "RADIUS",
1813: "RADIUS‑Accounting",
2049: "NFS",
3000: "ONVIF1/Grafana",
3128: "Squid",
3268: "LDAP‑GC",
3306: "MySQL",
3322: "DVR",
3389: "RDP",
34567: "HIK‑ISAPI",
34599: "HIK‑Manage",
5000: "UPnP‑RTSP",
5001: "HTTPS‑Stream",
5050: "SIP",
5060: "SIP",
5061: "SIP‑TLS",
5432: "PostgreSQL",
5900: "VNC",
6036: "UNIVIEW‑Private",
6060: "UNIVIEW‑Discover",
6379: "Redis",
6443: "Kubernetes",
6667: "IPC‑Private",
6789: "DAHUA‑Alarm",
8000: "HIK‑iVMS",
8008: "HTTP2",
8080: "HTTP‑Proxy/K8s‑Web",
8200: "HIK‑SADP",
8443: "HTTPS‑Alt/K8s‑HTTPS",
8450: "HIK‑Event",
8554: "RTSP‑ALT",
8899: "IPC‑Config",
9000: "DVR/NVR",
9090: "Web/Prometheus",
9200: "Elasticsearch",
10000: "Webmin",
10050: "Zabbix‑Agent",
10051: "Zabbix‑Server",
10554: "RTSP1",
19444: "RTSP2",
19999: "Netdata",
2375: "Docker",
2376: "Docker2",
27017: "MongoDB",
37777: "DAHUA‑Media",
37778: "DAHUA‑Stream",
}
SCAN_TCP = sorted(PORT_SERVICE_MAP.keys())
SOCKET_TIMEOUT = 1.2
def scan_open_tcp_ports(ip_str):
open_list = []
for p in SCAN_TCP:
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(SOCKET_TIMEOUT)
code = s.connect_ex((ip_str, p))
s.close()
if code == 0:
service = PORT_SERVICE_MAP.get(p, "Unknown")
open_list.append(f"{p}‑{service}")
except Exception:
continue
return open_list
def get_hostname():
return subprocess.check_output("hostname", shell=True, encoding="utf-8").strip()
def get_lan_info():
ip_output = subprocess.check_output("ip -4 route show default", shell=True, encoding="utf-8")
gateway = ""
for line in ip_output.splitlines():
if line.startswith("default"):
gateway = line.split("via")[1].split()[0]
break
addr_out = subprocess.check_output("ip -4 addr", shell=True, encoding="utf-8")
ip_addr = ""
netmask = ""
cidr_val = None
for line in addr_out.splitlines():
if "inet " in line and not "127.0.0.1" in line:
parts = line.strip().split()
ip_cidr = parts[1]
ip_addr, cidr_str = ip_cidr.split("/")
cidr_val = int(cidr_str)
mask = (0xFFFFFFFF << (32 - cidr_val)) & 0xFFFFFFFF
netmask = f"{(mask>>24)&0xff}.{(mask>>16)&0xff}.{(mask>>8)&0xff}.{mask&0xff}"
break
return ip_addr, netmask, gateway, cidr_val
def get_dns():
dns_list = []
with open("/etc/resolv.conf", "r", encoding="utf-8") as f:
for line in f:
if line.strip().startswith("nameserver"):
dns = line.strip().split()[1]
dns_list.append(dns)
return ",".join(dns_list)
def get_traceroute_hops():
cmd = ["traceroute", "-n", "-m", "10", "-w", "1", "114.114.114.114"]
try:
result = subprocess.check_output(cmd, shell=False, encoding="utf-8", stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as e:
result = e.output
hop_list = []
for line in result.splitlines():
if line.startswith("traceroute to"):
continue
parts = line.strip().split()
if len(parts) < 2:
continue
try:
int(parts[0])
hop_num = int(parts[0])
hop_ip = parts[1]
if hop_ip != "*":
hop_list.append((hop_num, hop_ip))
except ValueError:
continue
return hop_list
def get_arp_scan_sorted():
cmd = ["arp-scan", "--localnet"]
try:
out = subprocess.check_output(cmd, shell=False, encoding="utf-8", stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as e:
out = e.output
device_list = []
used_ip_set = set()
for line in out.splitlines():
line = line.strip()
if not line:
continue
if line.startswith("Interface:") or line.startswith("Starting") or line.startswith("Ending") or line.startswith("#"):
continue
parts = line.split("\t")
if len(parts) < 2:
continue
ip_str = parts[0].strip()
mac_str = parts[1].strip()
vendor_str = parts[2].strip() if len(parts)>=3 else "Unknown"
if vendor_str == "(Unknown)":
vendor_str = "Unknown"
try:
ip_obj = ipaddress.IPv4Address(ip_str)
device_list.append((ip_obj, ip_str, mac_str, vendor_str))
used_ip_set.add(ip_str)
except ipaddress.AddressValueError:
continue
device_list.sort(key=lambda x:x[0])
out_lines = []
sep = "----------------------------------------"
for _, ip, mac, vendor in device_list:
port_items = scan_open_tcp_ports(ip)
port_text = " ".join(port_items) if port_items else "无开放端口"
out_lines.append(f"{ip} {mac}")
out_lines.append(f"{vendor}")
out_lines.append(f"开放TCP端口:{port_text}")
out_lines.append(sep)
return out_lines, used_ip_set
def calc_free_used_ip_suffix(local_ip_str, cidr, occupied_ip_set):
local_ip = ipaddress.IPv4Address(local_ip_str)
net = ipaddress.IPv4Network(f"{local_ip}/{cidr}", strict=False)
free_suffix = []
used_suffix = []
for host_ip in net.hosts():
ip_str = str(host_ip)
suffix = int(ip_str.split(".")[-1])
if ip_str in occupied_ip_set:
used_suffix.append(suffix)
else:
free_suffix.append(suffix)
free_suffix.sort()
used_suffix.sort()
return net, free_suffix, used_suffix
if __name__ == "__main__":
print(f"目标文件:{file_path}")
host_name = get_hostname()
ip_local, netmask, gateway, cidr = get_lan_info()
dns = get_dns()
hops = get_traceroute_hops()
arp_devices, occupied_ips = get_arp_scan_sorted()
net_obj, free_suffix_list, used_suffix_list = calc_free_used_ip_suffix(ip_local, cidr, occupied_ips)
content_lines = [
f"本机的名称是:{host_name}",
f"本机的IP地址是:{ip_local}",
f"网关:{gateway}",
f"子网掩码:{netmask}",
f"DNS:{dns}",
"路由追踪(最大10跳,目标114.114.114.114):"
]
for num, ip_hop in hops:
content_lines.append(f"第{num}层路由器IP地址:{ip_hop}")
content_lines.append("")
content_lines.append("ARP扫描内网存活设备:")
if len(arp_devices) > 0:
content_lines.extend(arp_devices)
else:
content_lines.append("未扫描到内网设备或需要sudo权限运行")
content_lines.append("")
content_lines.append("网段IP地址分析:")
content_lines.append(f"本机IP:{ip_local}")
content_lines.append("空闲IP后缀:" + " ".join(str(x) for x in free_suffix_list))
content_lines.append("不可用IP后缀:" + " ".join(str(x) for x in used_suffix_list))
with open(file_path, "w", encoding="utf-8") as f:
for line in content_lines:
f.write(line + "\n")
print("写入完成!")