路由器和群辉把日志发到 SER8,ScanWatch 在地图上实时标出谁在扫你、从哪来、扫了什么,按危险程度着色,可查今天 / 近 7 天 / 近 30 天 / 全部,高危事件推送到飞书。
公网扫描 ─► RouterOS 防火墙日志 ┐
群辉 DSM 登录失败日志 ─────────┴─► syslog UDP 5140 ─► ScanWatch(SER8 docker)
├ 解析 + 定位(GeoIP/ip2region) + 打分
├ SQLite 存档(默认 90 天)
├ 看板 http://192.168.0.188:9110(地图 + WebSocket 实时)
└ 飞书告警卡片 + 每日摘要
界面
态势页:世界地图 + 中国省份地图,来源点按危险等级着色,红 / 橙弧线飞向"你在这里";新事件实时飞一条弧线。下面是各地区来源 IP 卡片。
数据看板:独立 IP / 总访问次数 / 被拦截三张卡片;"攻击流程"(打过来多少 → 摸到服务端口 → 被拦截 / 放行);访问量趋势(可缩放);被探测最多的端口;中国境内 / 境外对比;可搜索、筛选、排序的 IP 明细表,点"展开明细"看每一条记录。
浅色 / 深色模式;"遮"按钮把 IP 后两段打码(截图分享用);可选密码登录(只读)。
地图是离线的(ECharts 自带世界和中国省份地图),不依赖外网底图服务器。
它能看到什么
看不到的:内网设备之间的流量,以及 DSM 登录成功的记录。
危险等级
表格里的分类:高 / 严重 = 恶意扫描,中 = 可疑行为,低 = 低风险探测。规则在 app/scoring.py,想调直接改。
部署步骤
第 1 步:上传到 SER8
scp scanwatch.zip 用户名@192.168.0.188:~/
ssh 用户名@192.168.0.188
unzip scanwatch.zip && cd scanwatch
第 2 步:下载 IP 库
./scripts/get-geoip.sh
一次下载两个库:DB-IP Lite(全球坐标,用来在地图上定位)和 ip2region(中国境内的省 / 市 / 运营商)。下载失败的话,脚本会提示手动下载的文件名和存放位置。
第 3 步:改配置
cp .env.example .env
nano .env
必改的几项:
ROUTER_IP:路由器的内网 IP(Winbox 里/ip address print看 bridge1 上的地址)。填了以后只接受路由器和 NAS 发来的日志。NAS_IP:群辉的内网 IP,已填192.168.0.88。DASHBOARD_PASSWORD:看板访问密码。留空则不需要登录。IGNORE_IPS:你自己的 VPS 公网 IP,否则你用 nmap 自测会被记成攻击。HOME_LAT/HOME_LON:可以留空,会按路由器日志里的公网 IP 自动定位到市。
第 4 步:启动
docker compose up -d --build
docker compose logs -f
日志里看到 syslog 监听 udp/5140、GeoIP 库已加载、ip2region 库已加载 就是成功。Ctrl+C 退出日志。
如果 SER8 本机开了防火墙,放行 TCP 9110(Web)和 UDP 5140(syslog),来源限制为 192.168.0.0/24。
第 5 步:配置路由器
先确认 routeros/setup.rsc 里的 SER8 地址 192.168.0.188 和外网接口 pppoe-out1 和你的环境一致,然后在 Winbox 终端里整段粘贴执行。它会:把 firewall 和 critical 日志发到 SER8;加两条带限速的日志规则(记录公网打到路由器的新连接、命中端口转发的连接),并移到各自链的最前面。
第 6 步:配置群辉 DSM(NAS 登录失败上地图)
DSM → 日志中心 → 日志发送(不同版本菜单名略有出入)。
新增 syslog 服务器:服务器
192.168.0.188,端口5140,协议 UDP,日志格式 BSD (RFC 3164)。日志类型勾选连接(登录成功 / 失败都在这一类),其他可不选。
保存后,故意在 DSM 登录页输错一次密码,几秒内看板应出现一条 "NAS 登录失败"。
DSM 实际发出的日志长这样,ScanWatch 就是按它解析的:
Connection: User [admin] from [1.2.3.4] failed to sign in to [DSM] via [password] due to authorization failure.
DSM 如果是中文界面,日志文字不同,程序会用关键词(失败 / 登录 + 方括号 + IP)做宽松匹配。万一没识别,打开 http://192.168.0.188:9110/api/debug/unparsed 能看到最近 50 条没识别的原始日志,把它们发给我就能调整规则。
第 7 步:配置飞书告警(可选)
飞书群 → 设置 → 群机器人 → 添加机器人 → 自定义机器人,复制 Webhook 地址。
安全设置里如果勾了"签名校验",复制密钥;没勾就不用。
填进
.env:FEISHU_WEBHOOK=...、FEISHU_SECRET=...,可顺手改DASHBOARD_URL。让配置生效,然后发一条测试:
docker compose up -d
docker compose exec scanwatch python -m app.tools.test_feishu
群里收到红色测试卡片就成功了。注意改了 .env 要用 up -d 而不是 restart,后者不会重新读取配置。
推送规则(防止刷屏):
扫描类事件达到
ALERT_MIN_SEV(默认 4=严重)才推;命中端口转发、NAS 登录失败达到 3(高)就推。同一个来源 IP 30 分钟内只推一次,等级升高时例外。
每小时最多 20 条,超过后只推一条"已限流"提示,之后只记录不推送。
每天 8 点推送"过去 24 小时摘要"(
DAILY_DIGEST_HOUR=-1关闭)。
第 8 步:验证
用模拟数据看界面:
docker compose exec scanwatch python -m app.tools.send_test 127.0.0.1 5140
浏览器打开 http://192.168.0.188:9110。看完清掉模拟数据:
docker compose down && rm -f data/scanwatch.db* && docker compose up -d
用真实流量验证:在你的 VPS 上扫一下家里公网 IP(先把这个 VPS 的 IP 从 IGNORE_IPS 去掉),几秒内页面出现一条来自 VPS 所在地的记录。
常用命令
docker compose logs -f # 运行日志
docker compose up -d # 改了 .env 后让配置生效
docker compose up -d --build # 改了代码后重新构建
docker compose restart # 换了 data/geoip 里的库后重启
curl localhost:9110/healthz # received 在涨说明在收日志,parsed 是识别并入库的条数
排错
查一个 IP 的底细(判断攻击动机)
点任意 IP 打开详情,再点最上面的 IP 地址,会展开归属信息:位置、运营商、机构、ASN、网段、注册机构、反向解析域名、是否数据中心 / 代理 / 移动网络、滥用举报邮箱,并根据它扫的端口、尝试的账号、归属类型给出"可能的动机"提示(例如:专扫 RouterOS、SMB 勒索入口、已知的测绘机构可忽略、云主机批量扫描等)。提示是推断,仅供参考。
联网部分用 ip-api.com(HTTP)和 rdap.org,每个 IP 缓存 6 小时。SER8 访问不了这些站点时,只会少几项,本地库信息照常显示。
不想联网:
.env里设IPINFO_ONLINE=0。
把扫描数据变成防御:封禁命令生成器
在"数据看板"的 IP 明细表里:
勾选想封的 IP(或不勾,直接按当前筛选条件批量生成);也可以在某个 IP 的详情里点"生成封禁命令"。
点 生成封禁命令,选最低等级、封禁时长(1 天 / 7 天 / 30 天 / 永久),需要的话勾"按 /24 网段封禁"。
点复制命令,到 Winbox 新建终端粘贴执行;命令很多时点下载 .rsc,上传到路由器后
/import file-name=文件名.rsc。命令末尾自带"检查"和"撤销这一批"两条命令,每一批有独立标签,可以单独撤销。
ScanWatch 只生成文本,不连接、不修改路由器。 前提是路由器上已有丢弃这个名单的规则:
/ip firewall raw add chain=prerouting in-interface=pppoe-out1 src-address-list=blocklist action=drop comment="drop blocklist"
防误封保护(避免批量封禁封到自己):
内网 / 保留地址、你的公网 IP、NAS、路由器、
IGNORE_IPS里的地址一律不会出现在命令里。按条件批量生成时,默认排除中国境内来源,以及命中过端口转发的来源(可能是你自己或家人在外访问摄像头)。这两项可以手动取消勾选。
手选的 IP 不受上面两项限制(你明确选了),但受保护地址仍会被跳过。
按 /24 封网段时,只要网段里含受保护地址,就自动退回成只封单个 IP。
重复添加不会报错,已在名单里的地址自动跳过。
第一次使用,建议先只勾一个 IP、时长选 1 天,执行后用文末的检查命令确认。
升级定位算法后:重算历史数据
新版以 ip2region 的省份为准定位中国境内来源(两个 IP 库对同一个 IP 判断不一致时,不再出现"点在北京、弹窗写上海")。已存的历史事件要跑一次:
docker compose exec scanwatch python -m app.tools.regeo
数据量与合并
互联网上的扫描器经常对同一个端口每秒打好几次。为了不撑爆数据库,同一来源 + 同一端口 + 同一类型的事件,60 秒内只存一行,并累计次数(明细里会显示"合并了 N 次",所有统计数字仍然是真实次数)。COALESCE_SECONDS 可调,设 0 关闭合并。等级升高、登录失败、飞书告警不受合并影响,照常即时处理。
注意
syslog 是明文 UDP,没有身份验证,
ROUTER_IP/NAS_IP只是简单的来源过滤。看板只放在内网或 WireGuard 里,不要做端口转发到公网,即使设了密码也一样。
GeoIP 精度是城市级,运营商出口、VPN、云服务器的位置不一定是攻击者真实位置。
数据在
data/scanwatch.db,备份直接复制。保留天数由RETENTION_DAYS控制。
评论区