简介:这是一份面向家长与家庭教育场景的上网控制软件源码资源,用于帮助父母管理孩子的网络访问行为,解决网址过滤、上网时长限制与设备管控等实际问题,适合具备一定C#基础的开发者学习或二次开发。压缩包共40个文件,约4.11MB,以cs源码文件为主,配合resx资源、ico图标、exe可执行程序、dll与pdb调试文件,以及csproj、sln工程配置和settings、manifest等辅助文件,构成一套可编译运行的完整工程。内容围绕网址检测、锁屏与系统操作等模块展开,包含窗体设计、资源定义与程序入口等结构,便于读者理解家长控制类工具的功能划分与实现思路。目前已有454人学习下载,可作为网络行为管理、桌面端监控类项目的参考范例,帮助开发者快速掌握过滤规则、时段控制与界面交互的落地方式。
1. 上网家长控制软件:从“能拦”到“拦得准”的工程拆解
很多家长第一次装上网家长控制软件,诉求都很朴素:别让孩子刷到不该看的东西,别在写作业时偷偷打游戏。但真到自己动手做一套,或者给家里那台旧路由器刷个固件时,才发现事情没那么简单。拦不住、误拦、被绕过,是三个最常翻车的点。这套软件本质上是一套跑在网关或终端上的流量识别与策略执行系统,核心能力就三块:识别谁在上网、识别在访问什么、决定放行还是阻断。它适合两类人:一类是想给自己家搭一套可控环境的开发者,另一类是需要在产品里集成家长控制模块的工程师。下面按“先跑通最小闭环,再抠识别精度,最后处理绕过与性能”的顺序讲。
2. 最小可用闭环:DNS 过滤加时间窗策略怎么落地
2.1 为什么第一版不建议直接上深度包检测
新手最容易犯的错,是一上来就想做全流量深度包检测(DPI),觉得只有看清每一个包才叫控制。实际落地时,DPI 对硬件要求高,家用网关那点 CPU 跑不动,而且加密流量占比越来越高,硬解只会让设备发热、延迟飙升。更务实的路径是分层:第一层用 DNS 过滤挡住绝大多数域名级访问,第二层用时间窗策略控制“能不能上网”,第三层才在必要时对特定应用做特征识别。DNS 过滤的好处是成本极低,一个树莓派或者旧笔记本就能跑,而且对 HTTPS 站点同样有效,因为域名解析这一步是绕不开的。
我一般会先搭一个 DNS 转发加过滤的服务,把家里所有设备的 DNS 都指到它上面。这样不需要在每台设备装客户端,也不用改路由器固件,改一下 DHCP 下发的 DNS 地址就行。下面是一个基于 Python 的最小 DNS 过滤示例,用 dnslib 库实现,逻辑是查白名单和黑名单,命中黑名单就返回一个空地址。
# dns_filter.py # 依赖: pip install dnslib from dnslib import DNSRecord, RR, QTYPE, A from dnslib.server import DNSServer, BaseResolver # 黑名单:命中则返回 0.0.0.0,相当于阻断 BLOCKLIST = { "example-game.com", "example-video.net", } # 白名单优先级高于黑名单,用于放行必要服务 ALLOWLIST = { "school-example.edu", } class ParentalResolver(BaseResolver): def resolve(self, request, handler): reply = request.reply() qname = str(request.q.qname).rstrip(".").lower() qtype = QTYPE[request.q.qtype] # 只处理 A 记录,其他类型直接转发或忽略 if qtype != "A": return reply if qname in ALLOWLIST: # 白名单直接返回一个占位地址,实际应转发到上游 reply.add_answer(RR(qname, QTYPE.A, rdata=A("192.168.1.1"), ttl=60)) return reply if qname in BLOCKLIST: # 返回 0.0.0.0 表示阻断,客户端会连接失败 reply.add_answer(RR(qname, QTYPE.A, rdata=A("0.0.0.0"), ttl=60)) return reply # 未命中任何名单,返回一个默认地址,实际应转发 reply.add_answer(RR(qname, QTYPE.A, rdata=A("192.168.1.1"), ttl=60)) return reply if __name__ == "__main__": resolver = ParentalResolver() server = DNSServer(resolver, port=53, address="0.0.0.0") server.start()这段代码的关键参数有三个:BLOCKLIST和ALLOWLIST是策略集合,实际生产中应该从数据库或配置文件加载,而不是硬编码;ttl=60控制缓存时间,设短一点能让策略变更更快生效,但会增加查询量;返回0.0.0.0是一种阻断方式,优点是客户端立刻失败,缺点是有些应用会重试导致日志刷屏。更优雅的做法是返回一个内网 Web 服务器的地址,弹一个提示页,但那就需要额外跑一个 HTTP 服务。
2.2 时间窗策略:用 cron 还是自己写调度
时间窗是家长控制里最实用的功能,比如“周一到周五 19:00 到 21:00 允许上网,其余时间阻断”。实现方式有两种:一种是用系统 cron 在特定时间点切换防火墙规则或 DNS 过滤开关;另一种是在过滤服务里内置一个时间判断,每次查询时检查当前是否在允许时段。cron 的优点是简单,缺点是粒度粗,切换瞬间可能有连接残留;内置判断更灵活,但每次查询都要算一次时间,高频查询下要注意性能。
我一般会选内置判断,因为可以做到秒级精度,而且方便做“临时加时”这种交互。下面是一个时间窗判断的片段,配合上面的 DNS 过滤器使用。
# time_window.py from datetime import datetime, time # 定义允许上网的时间段:周一到周五 19:00-21:00,周末 9:00-21:00 WEEKDAY_WINDOW = (time(19, 0), time(21, 0)) WEEKEND_WINDOW = (time(9, 0), time(21, 0)) def is_allowed_now(now=None): now = now or datetime.now() weekday = now.weekday() # 0 是周一,6 是周日 current = now.time() if weekday < 5: start, end = WEEKDAY_WINDOW else: start, end = WEEKEND_WINDOW return start <= current <= end # 在 resolver 里调用 # if not is_allowed_now(): # return block_response()这里要注意时区问题,datetime.now()取的是系统本地时间,如果设备时区设错,策略就会错乱。另外,跨天的时间段(比如 22:00 到次日 6:00)需要额外处理,不能简单用start <= current <= end,得判断current >= start or current <= end。这个坑我在第一次做的时候踩过,孩子半夜还能上网,查了半天才发现是时间窗写反了。
3. 识别精度:域名匹配、SNI 嗅探与误拦处理
3.1 域名匹配不是字符串相等那么简单
DNS 过滤的核心是域名匹配,但实际域名千奇百怪。比如www.example.com和example.com是不是同一个策略?a.b.example.com要不要继承example.com的规则?还有 CDN 域名,同一个 IP 对应几百个域名,按 IP 封会误伤一大片。我一般会实现三级匹配:精确匹配、后缀匹配、关键词匹配。精确匹配用于白名单,后缀匹配用于黑名单(比如封example.com连带封所有子域),关键词匹配作为兜底但容易误伤,要谨慎用。
下面是一个域名匹配的示例,用后缀树的方式做高效匹配。
# domain_matcher.py class DomainMatcher: def __init__(self): self.exact = set() self.suffix = set() self.keywords = [] def add_exact(self, domain): self.exact.add(domain.lower()) def add_suffix(self, domain): # 去掉开头的点,统一格式 self.suffix.add(domain.lower().lstrip(".")) def add_keyword(self, kw): self.keywords.append(kw.lower()) def match(self, domain): domain = domain.lower().rstrip(".") if domain in self.exact: return "exact" # 后缀匹配:example.com 能匹配 www.example.com parts = domain.split(".") for i in range(len(parts)): candidate = ".".join(parts[i:]) if candidate in self.suffix: return "suffix" for kw in self.keywords: if kw in domain: return "keyword" return None参数说明:exact集合用于精确放行,比如学校域名;suffix集合用于封禁整个域,比如游戏平台;keywords是最后手段,比如封含“game”的域名,但会误伤example.com/game这种正常路径,所以只建议在域名层面用,不要对 URL 路径用。匹配顺序必须是精确优先、后缀其次、关键词最后,否则白名单会被黑名单覆盖。
3.2 SNI 嗅探:加密流量下还能看到什么
DNS 过滤只能挡域名解析,如果设备直接用 IP 访问,或者用了 DoH(DNS over HTTPS),DNS 层就失效了。这时候需要看 TLS 握手时的 SNI 字段。SNI 是明文传输的,里面包含目标域名,可以在网关抓包解析。但要注意,SNI 嗅探需要处理性能,千兆网线下每秒几万个包,用 Python 抓包会丢包,得用 C 或者 eBPF。我一般会在 Linux 网关上用tcpdump配合tshark做离线分析,线上则用nDPI或Suricata这类成熟库。
下面是一个用scapy解析 SNI 的示例,只适合低流量场景做验证。
# sni_sniffer.py # 依赖: pip install scapy from scapy.all import sniff, TCP, IP, Raw def extract_sni(payload): # TLS ClientHello 的 handshake type 是 0x01 if len(payload) < 6: return None if payload[0] != 0x16: # handshake return None # 跳过 record 头,找 handshake 类型 if payload[5] != 0x01: return None # 简化解析:实际要按 TLS 结构逐层偏移 # 这里只做演示,生产环境用 nDPI try: # 找到 server_name 扩展的偏移,不同实现有差异 idx = payload.find(b"\x00\x00") # 仅示意 if idx > 0: return payload[idx+4:idx+4+20].decode("utf-8", errors="ignore") except Exception: return None return None def handle_packet(pkt): if pkt.haslayer(TCP) and pkt.haslayer(Raw): sni = extract_sni(bytes(pkt[Raw])) if sni: print("SNI:", sni) # 只抓 443 端口,避免全量抓包 sniff(filter="tcp port 443", prn=handle_packet, store=0)这段代码的extract_sni是简化版,真实 TLS 解析要按record -> handshake -> client hello -> extensions逐层读长度字段,偏移算错就会读出乱码。参数上,filter="tcp port 443"能减少干扰,但 DoH 走 443 也会被看到,只是 SNI 可能是 CDN 域名,需要结合 IP 反查。性能上,sniff默认用 libpcap,高流量下会丢包,建议用count限制包数或改用nDPI的 C 接口。
3.3 误拦处理:白名单怎么设计才不背锅
误拦是家长控制软件最容易被投诉的点。孩子说“老师发的链接打不开”,一查是关键词匹配把example-school.com封了。避免误拦的核心是白名单要覆盖三类:教育类域名、常用工具类域名、支付类域名。教育类包括学校官网、在线作业平台;工具类包括搜索引擎、地图、文档协作;支付类包括银行、支付平台,这些被封会导致家长自己也没法用。白名单的优先级必须高于黑名单,而且白名单要支持通配符,比如*.edu.cn。
我一般会维护一个allowlist.txt,每行一个域名,支持*.前缀表示子域通配。加载时解析成后缀集合,匹配时先查白名单再查黑名单。另外,白名单要允许家长手动添加,并且添加后立即生效,不能等重启。实现上可以用文件监听(inotify)或者定时重载,我倾向用inotify,因为延迟低。
4. 绕过与对抗:DoH、代理和 MAC 随机化怎么破
4.1 DoH 让 DNS 过滤失效,怎么补
DoH(DNS over HTTPS)把 DNS 查询加密后走 443 端口,传统的 DNS 过滤完全看不到。孩子只要在浏览器里开启“安全 DNS”,或者装个支持 DoH 的浏览器,DNS 过滤就废了。应对方式有三种:一是封禁已知的 DoH 服务器 IP 和域名,比如cloudflare-dns.com、dns.google,但这是猫鼠游戏;二是在网关上做 SNI 嗅探,发现 DoH 流量就阻断;三是强制所有设备用指定的 DNS,通过防火墙规则把 53 端口和 853 端口的流量重定向到自己的 DNS 服务。
我一般会组合用:先封常见 DoH 域名,再用 iptables 把 53 和 853 重定向。下面是一个 iptables 示例。
# 把局域网内所有设备的 DNS 查询重定向到本地 DNS 服务 # 假设本地 DNS 服务监听 192.168.1.2:53 iptables -t nat -A PREROUTING -i br0 -p udp --dport 53 -j DNAT --to-destination 192.168.1.2:53 iptables -t nat -A PREROUTING -i br0 -p tcp --dport 53 -j DNAT --to-destination 192.168.1.2:53 # 封禁 DoT 的 853 端口 iptables -A FORWARD -p tcp --dport 853 -j DROP iptables -A FORWARD -p udp --dport 853 -j DROP参数说明:-i br0是局域网网桥接口,不同系统可能是eth0或br-lan;DNAT把目标地址改成自己的 DNS,这样即使设备手动设了 8.8.8.8 也会被劫持;DROP853 端口会阻断 DoT,但有些应用会回退到普通 DNS,所以重定向 53 是关键。注意,如果家长自己需要用 DoH,得把家长的设备 MAC 加入白名单,跳过重定向。
4.2 MAC 随机化与设备识别
现代手机默认开启 MAC 随机化,每次连 Wi-Fi 都换一个 MAC,导致基于 MAC 的策略全部失效。识别设备不能只看 MAC,要结合 DHCP 指纹、主机名、User-Agent 等多维度。我一般会在 DHCP 服务里记录hostname和client-id,然后和 MAC 做关联。即使 MAC 变了,hostname通常不变,可以据此识别。如果hostname也被改,那就只能靠行为特征,比如访问的域名集合,但这复杂度就高了。
下面是一个 DHCP 指纹记录的示例,用dnsmasq的日志解析。
# dnsmasq 日志格式示例 # dnsmasq-dhcp: DHCPACK(br0) 192.168.1.100 aa:bb:cc:dd:ee:ff hostname # 用 awk 提取 MAC 和 hostname 的对应关系 awk '/DHCPACK/ {print $4, $5, $6}' /var/log/dnsmasq.log | sort -u这个命令输出IP MAC hostname三列,可以导入数据库做设备档案。参数上,$4是 IP,$5是 MAC,$6是主机名,但不同 dnsmasq 版本字段位置可能不同,要先head看几行确认。如果主机名是android-xxxx这种随机串,那就只能靠 IP 段或者时间段来区分了。
5. 避坑与排查:那些让我熬夜的翻车现场
5.1 现象:策略改了但不生效,重启才好
原因:DNS 缓存没刷新,客户端和本地 DNS 都缓存了旧记录。解决:把 TTL 设短,同时在策略变更后主动清空本地 DNS 缓存,或者给响应加no-cache头。更彻底的做法是策略变更时发一个SIGHUP给 DNS 服务,让它重载配置。
5.2 现象:孩子说“网速变慢”,测速只有原来一半
原因:DNS 过滤服务性能不足,或者 iptables 规则太多导致包处理变慢。解决:用top看 DNS 进程 CPU,如果跑满就换 C 实现或者加缓存;iptables 规则用iptables -L -n -v看命中计数,把高频规则放前面,低频的用ipset合并。
5.3 现象:某些 App 完全用不了,但网页正常
原因:App 不走系统 DNS,而是自己内置 DoH 或者直接连 IP。解决:抓包看 App 的流量特征,如果是固定 IP,就封 IP;如果是 DoH,就按 4.1 的方法重定向。注意,封 IP 可能误伤同 IP 的其他服务,要谨慎。
5.4 现象:白名单加了学校域名,但还是打不开
原因:学校网站引用了第三方 CDN 的 JS 和 CSS,这些域名不在白名单里。解决:用浏览器开发者工具看 Network 面板,把关键第三方域名也加白。或者更省事,对教育类流量做“宽松模式”,只封黑名单,不限制其他。
5.5 现象:日志里大量0.0.0.0查询,设备疯狂重试
原因:返回0.0.0.0后,有些应用会不断重试,导致日志爆炸和电量消耗。解决:改成返回一个内网提示页的 IP,或者返回NXDOMAIN,让客户端认为域名不存在,减少重试。NXDOMAIN在 dnslib 里可以用reply.header.rcode = RCODE.NXDOMAIN设置。
6. 进阶技巧:用 eBPF 做高性能流量识别与验证
当设备多了、流量大了,用户态抓包和 iptables 匹配会成为瓶颈。我后来把核心识别逻辑迁到了 eBPF,在 XDP 层做初步过滤,只把可疑流量送到用户态。这样千兆网线下 CPU 占用从 80% 降到 15% 左右。下面是一个 eBPF 程序的骨架,用bcc编写,功能是统计每个 IP 的 DNS 查询次数,超过阈值就告警。
# dns_monitor.py # 依赖: pip install bcc from bcc import BPF program = r""" #include <uapi/linux/ptrace.h> #include <net/sock.h> #include <bcc/proto.h> BPF_HASH(dns_count, u32, u64); int kprobe__udp_sendmsg(struct pt_regs *ctx, struct sock *sk, struct msghdr *msg, size_t len) { u32 pid = bpf_get_current_pid_tgid(); u64 *val, zero = 0; val = dns_count.lookup_or_init(&pid, &zero); (*val)++; if (*val > 1000) { bpf_trace_printk("high dns query: pid=%d count=%llu\n", pid, *val); } return 0; } """ b = BPF(text=program) print("monitoring udp_sendmsg, Ctrl+C to exit") b.trace_print()这段代码的kprobe__udp_sendmsg挂在内核的 UDP 发送函数上,dns_count哈希表按 PID 统计查询次数。参数上,1000是阈值,超过就打印告警,实际可以改成动态阈值。bpf_trace_printk性能较差,生产环境应该用perf_event或者 ring buffer。验证方法:跑一段dig循环,看告警是否触发;然后用bpftool map dump查看哈希表内容,确认计数准确。
另一个验证技巧是“影子模式”:新策略先不阻断,只记录“如果阻断会命中哪些流量”,跑一周看误拦率。我一般会在 DNS 过滤器里加一个dry_run开关,命中黑名单时只写日志不返回阻断地址。这样能提前发现白名单遗漏,避免直接上线后孩子打不开作业网站。这个习惯帮我省了很多后悔药,毕竟家长控制的第一原则是“别耽误正事”。
希望帮到你。
本文还有配套的精品资源,点击获取