提示：本人对于电话网络的了解停留在初步阶段，本博客仅仅是我初步的学习总结，不仅十分不完善，还可能出现部分知识性错误，如遇错误与意见可联系070127andy@gmail.com，感谢您的理解和指正！另：本文默认您已经加入了DN42并至少和一位peer进行了对等连接。本文部分基础概念和图示基于 Yukari Chiba 的《telephony42 完全指南》整理和改写，原文采用 CC BY-SA 4.0，本文相应部分亦按 CC BY-SA 4.0 发布。前言本人在DN42网络中扮演 AS4242422921 并拥有一个四节点的小集群，集群间建立了 FullMesh 的 IBGP 组网，通过 OSPF 作为 IGP 协议建立内部动态路由，并申请了 andy.dn42 作为我的域名，搭建了 1主+1从 的权威服务器（由knot驱动）。最近在DN42的群里看到了 Yukari 大佬提到的telephony42，即一套实验性质的去中心化电话网络。我觉得自建一个电话网络很酷，于是打算跟着Yukari的 这篇博客 搓一个 PBX(Private Branch Exchange - 私有交换机) ，接入telephony42， 打电话骚扰群友 。前期准备{x} 一个DN42域名 {x} 一台 已接入DN42 的，配置不低于1C1G的服务器 {x} 自建的DN42内部权威DNS（这里以knot演示）概念须知PS：本篇博客仅是“最小加入指南”，后面譬如SRTP的进阶操作本人也在学习，还请自行了解，本部分参考了0x7f的博客内容。组件简要解释PBX (Private Branch Exchange - 私有交换机)视作 VoIP 网络中的路由器，负责处理信令和路由，有时也处理语音数据。B2BUA (Back-to-Back User Agent - 背靠背用户代理)Asterisk 之类的 PBX 就是一个典型的 B2BUA。与仅转发信令的 SIP 代理不同，当 A 通过 PBX 呼叫 B 时，PBX 实际上是先以被叫的身份接听了 A 的电话，然后再以主叫的身份向 B 发起一通全新的电话，最后在内部把这两个独立的通道桥接起来。这使得 PBX 能随时介入通话过程，进行录音、转码、甚至强制掐线。Endpoint (终端设备)任何可以发起或接收呼叫的设备。它可以是电脑上的软件电话应用、桌面的实体 IP 电话机、ATA、网页上的 WebRTC 电话，甚至是另一个网络的 PBX。协议简要解释会话发起协议 (SIP - RFC 3261)作为控制平面。采用纯文本格式，长得有点像 HTTP，包含 URI、头字段和正文。主要负责信令传输，例如传递呼叫的来源和目标、接通、挂断或拒接等，不负责传输具体的语音流。SIP 默认监听于 UDP/TCP 5060 端口（在报文较大时通常优先使用 TCP），TLS 加密状态下监听 TCP 5061 端口。实时传输协议 (RTP - RFC 3550)作为数据平面。当 SIP 握手与 SDP 协商完成后，双方会在协商好的高位动态端口（通常在 UDP 10000 至 20000 之间）互相传递 RTP 数据包。这些包对延迟和抖动极其敏感。如果防火墙只放行了 SIP (5060) 但没放行 RTP，就会遇到 VoIP 史上最著名的故障： 单通（接通了但听不到声音）。路由简要解释ENUM (E.164 Number Mapping)是一种自动发现路由的机制。当拨打目标是未知目的地的电话时，PBX 会根据号码查询 DNS 的 NAPTR 记录，查找到该号码对应的 SIP 目标地址，在DN42通过一个特殊的DNS查询实现Context (上下文)是 Asterisk 拨号计划的关键组件。可以把它理解为 VoIP 系统中的 VRF。它可以把不信任的外部呼叫丢进一个没有外拨权限的隔离上下文中，并且能够通过 Goto 等语句实现在不同上下文间链式跳转，在后面的拦截伪造来电等功能上很有用对于跨网的SIP呼叫，可以分为 三个阶段 ：第一部分：SIP 呼叫建立PBX将充当一个类似“ 中转站 ”的角色，先接听主叫设备的 INVITE ，使用 ENUM DNS 查询（ 后面会展开解释 ）与信令路由，查询到对方的PBX之后向其转发INVITE，由对方的PBX再呼叫被叫设备，接听后层层转发回200 OKsequenceDiagram participant Caller as 主叫设备 (A) participant PBXA as 本地 PBX participant PBXB as 对端 PBX participant Callee as 被叫设备 (B) Note over Callee, Caller: 控制平面交互 (SIP) Caller->>PBXA: INVITE sip:B@PBXA_IP (含 SDP 提议) PBXA->>Caller: 100 Trying Note over PBXA, PBXB: ENUM DNS 查询与信令路由 PBXA->>PBXB: INVITE sip:B@PBXB_IP (含 SDP 提议) PBXB->>PBXA: 100 Trying PBXB->>Callee: INVITE (含 SDP 提议) Callee->>PBXB: 180 Ringing (振铃) PBXB->>PBXA: 180 Ringing PBXA->>Caller: 180 Ringing Note over Callee: 用户接听电话 Callee->>PBXB: 200 OK (含 SDP 应答) PBXB->>PBXA: 200 OK (含 SDP 应答) PBXA->>Caller: 200 OK (含 SDP 应答) Caller->>PBXA: ACK PBXA->>PBXB: ACK PBXB->>Callee: ACK第二部分：RTP 语音传输在接通之后，通过两台PBX作为桥梁，两台终端即可在10000端口附近（可手动指定范围）互传udp数据包，即RTP语音流， 在本文关闭 direct_media 、由 PBX 锚定媒体的配置下 ，所有数据在逻辑上仍通过PBX转发sequenceDiagram participant Caller as 主叫设备 (A) participant PBXA as 本地 PBX participant PBXB as 对端 PBX participant Callee as 被叫设备 (B) Note over Caller, Callee: 数据平面交互 (RTP) Caller-->>PBXA: RTP 语音流 PBXA-->>PBXB: RTP 语音流 PBXB-->>Callee: RTP 语音流 Callee-->>PBXB: RTP 语音流 PBXB-->>PBXA: RTP 语音流 PBXA-->>Caller: RTP 语音流第三部分：SIP 挂断在挂断阶段，仍是通过PBX转发BYE，回传200 OK进行挂断操作sequenceDiagram participant Caller as 主叫设备 (A) participant PBXA as 本地 PBX participant PBXB as 对端 PBX participant Callee as 被叫设备 (B) Note over Caller, Callee: 控制平面交互 (SIP) Caller->>PBXA: BYE PBXA->>PBXB: BYE PBXB->>Callee: BYE Callee->>PBXB: 200 OK PBXB->>PBXA: 200 OK PBXA->>Caller: 200 OK通过以上内容，我们可以发现，主要的过程都离不开PBX交换机，故只要我们搭建好PBX即可初步建起自己的电话网络。而对于PBX的 软交换平台 ，对于新手玩家Asterisk是比较好的选择。PS：Yukari把PBX比作 电话网络的自治系统 ，把Asterisk比作 电话网络的 BIRD开始部署1.SIP 域名创建在 yourdomain.dn42 Zone 中添加：sip.andy.dn42. 300 IN A 172.21.118.162 #ip对应你的PBX服务器 sip.andy.dn42. 300 IN AAAA fdd2:e3e2:c922::2 #ip对应你的PBX服务器 pbx.andy.dn42. 300 IN CNAME sip.andy.dn42.2.ENUM (E.164 Number Mapping)建立由上面的内容我们可以知道，要想呼叫其他PBX下的设备，必须 知道对方的地址 ，故我们需要一种机制将电话号码翻译成 PBX 的 IP 地址，这就是 ENUM ENUM基于 DNS ，它很像 rDNS 的解析方式，即先将电话号码翻转，加入点号分隔，再加上一个特定的域名后缀最后，利用 DNS 的 NAPTR (Naming Authority Pointer) 记录，一个包含正则表达式的记录，就可以可以将这串倒写的号码解析成 SIP URI。下面是demo，此正则可以匹配该前缀下的所有号码$ORIGIN 4.3.2.1.0.4.2.4.tel.dn42. * IN NAPTR 10 100 "u" "E2U+sip" "!^(.*)$!sip:\\1@pbx.yourdomain.dn42!" .我的权威DNS使用的是kont，下面是我创建ENUM的步骤：2.1按号码计算出自己的ENUM Zone我的号码是 +042429211001去掉 +： 042429211001逐位反转并加点： 1.0.0.1.1.2.9.2.4.2.4.0.tel.dn42.号码前缀： +04242921对应的ENUM Zone： 1.2.9.2.4.2.4.0.tel.dn42.2.2创建对应 ENUM Zone创建Zone文件：/var/lib/knot/zones/1.2.9.2.4.2.4.0.tel.dn42.zone内容：$ORIGIN 1.2.9.2.4.2.4.0.tel.dn42. $TTL 300 @ IN SOA ns1.andy.dn42. admin.andy.dn42. ( 2026071001 3600 900 604800 300 ) @ IN NS ns1.andy.dn42. @ IN NS ns2.andy.dn42. ; +042429211001 #我准备创建两个分机 1.0.0.1 300 IN NAPTR 100 10 "u" "E2U+sip" \ "!^.*$!sip:1001@sip.andy.dn42!" . ; +042429211002 2.0.0.1 300 IN NAPTR 100 10 "u" "E2U+sip" \ "!^.*$!sip:1002@sip.andy.dn42!" .2.3使用 kzonecheck 检查 ENUM Zone 文件的合法性。kzonecheck -o "$ZONE" "$ZONEFILE"退出码 0 即表示PASS。2.4创建主NS对应的Knot DNS服务配置文件示例：key: - id: telephony42-xfr algorithm: hmac-sha256 secret: "[数据删除]" remote: - id: telephony42-secondary address: 172.21.118.164 key: telephony42-xfr acl: - id: telephony42-transfer key: telephony42-xfr action: transfer zone: - domain: 1.2.9.2.4.2.4.0.tel.dn42. file: /var/lib/knot/zones/1.2.9.2.4.2.4.0.tel.dn42.zone notify: telephony42-secondary acl: telephony42-transfer在主配置中加入include：include: "/etc/knot/conf.d/*.conf"检查合法性：knotc conf-check设置文件权限：chown root:knot /etc/knot/conf.d chmod 750 /etc/knot/conf.d chown root:knot /etc/knot/conf.d/telephony42.conf chmod 640 /etc/knot/conf.d/telephony42.conf chown knot:knot /var/lib/knot/zones/1.2.9.2.4.2.4.0.tel.dn42.zone chmod 640 /var/lib/knot/zones/1.2.9.2.4.2.4.0.tel.dn42.zone最后重载 kont2.5同步到从服务器首先生成密钥：umask 077 openssl rand -base64 32 > /root/telephony42-xfr.secret chmod 600 /root/telephony42-xfr.secret chown root:root /root/telephony42-xfr.secret stat -c '%A %U:%G %s bytes %n' /root/telephony42-xfr.secret把密钥文件存放到ns2上再到 ns2 执行：chmod 600 /root/telephony42-xfr.secret chown root:root /root/telephony42-xfr.secret stat -c '%A %U:%G %s bytes %n' /root/telephony42-xfr.secret #查看权限是否正确ns2通过AXFR从ns1同步。示例：key: - id: telephony42-key algorithm: hmac-sha256 secret: "[刚刚生成的密钥]" remote: - id: ns1 address: 172.21.118.161 key: telephony42-key zone: - domain: 1.2.9.2.4.2.4.0.tel.dn42 master: ns1重载kont。3.安装并配置Asterisk我的系统是Debian13，其默认 trixie 仓库中没有完整的 Asterisk 主程序包，故打算自己编译安装。在询问ai之后，得知：教程中说可以 APT 安装，是因为它隐含使用了 Debian sid 仓库。对于已经运行 BIRD 和其他 DN42 服务的核心节点，不建议为了 Asterisk 混入 sid，可能导致基础库被大规模升级。3.1源码编译 Asterisk安装编译依赖：apt install -y \ build-essential pkg-config autoconf-archive bison flex \ wget ca-certificates patch bzip2 python3-dev \ libedit-dev libjansson-dev libsqlite3-dev uuid-dev libxml2-dev \ libssl-dev libcurl4-openssl-dev liburiparser-dev libxslt1-dev \ libcap-dev libnewt-dev libncurses-dev \ libsrtp2-dev libgsm1-dev libspeexdsp-dev \ libogg-dev libvorbis-dev \ bind9-dnsutils下载并解压 Asterisk 22.10.1：cd /usr/src wget https://downloads.asterisk.org/pub/telephony/asterisk/releases/asterisk-22.10.1.tar.gz tar -xzf asterisk-22.10.1.tar.gz cd asterisk-22.10.1配置时使用 Asterisk 自带的 pjproject：./configure --with-pjproject-bundled make menuselect.makeopts启用所需模块：for MODULE in \ res_pjsip \ chan_pjsip \ res_pjsip_authenticator_digest \ res_pjsip_endpoint_identifier_ip \ res_pjsip_registrar \ res_pjsip_session \ res_pjsip_sdp_rtp \ res_rtp_asterisk \ func_enum \ app_dial \ app_echo \ app_playback \ pbx_config \ codec_alaw \ codec_ulaw \ codec_g722 do menuselect/menuselect \ --enable "$MODULE" \ menuselect.makeopts done由于我的vps只有1c1g，故采用单线程编译：make -j1完成后安装：make install make samples make config make install-logrotate ldconfig3.2创建Asterisk用户官方建议Asterisk以低权限运行，故创建专用用户：groupadd --system asterisk useradd \ --system \ --gid asterisk \ --home-dir /var/lib/asterisk \ --shell /usr/sbin/nologin \ asterisk准备目录：install -d -o asterisk -g asterisk /run/asterisk install -d -o asterisk -g asterisk /var/log/asterisk install -d -o asterisk -g asterisk /var/spool/asterisk install -d -o asterisk -g asterisk /var/lib/asterisk最后注册系统服务并保持自启3.3配置Asterisk首先是最重要的： Asterisk只应监听DN42内部网段!防止公网 SIP 扫描和暴力注册!172.21.118.162:5060/UDP [fdd2:e3e2:c922::2]:5060/UDP3.4创建PJSIP 分机我打算创建两个分机：1001：Windows Blink 1002：iOS SIP 客户端 1001 → +042429211001 1002 → +042429211002核心配置：[transport-dn42-ipv4] type=transport protocol=udp bind=172.21.118.162:5060 local_net=172.20.0.0/14 [transport-dn42-ipv6] type=transport protocol=udp bind=[fdd2:e3e2:c922::2]:5060 local_net=fd00::/8分机模板：[local-endpoint](!) type=endpoint context=from-local disallow=all allow=g722 allow=alaw allow=ulaw direct_media=no rtp_symmetric=yes force_rport=yes rewrite_contact=yes dtmf_mode=rfc4733注：DNS NAPTR 定义的是外部号码如何找到本地分机；分机外呼时使用的 Telephony42 主叫号码则需要通过 endpoint 的 callerid 或拨号计划另行设置。3.5缩小RTP范围我的内部网络目前只有两个分机，所以决定将 RTP 端口缩小到： UDP 10000–10100[general] rtpstart=10000 rtpend=10100 strictrtp=yes3.6拨号计划我的内部号码： 1001 1002 我打算使用一个号码进行本地ECHO测试： 5000 我的核心拨号计划：[from-local] exten => _XXXX,1,NoOp(Local call) same => n,Goto(local-extensions,${EXTEN},1) [local-extensions] exten => 1001,1,Dial(PJSIP/1001,30) same => n,Hangup() exten => 1002,1,Dial(PJSIP/1002,30) same => n,Hangup() exten => 5000,1,Answer() same => n,Wait(1) same => n,Echo() same => n,Hangup()最后验证模块是否正常：asterisk -rx 'module show like chan_pjsip' asterisk -rx 'module show like res_pjsip' asterisk -rx 'module show like res_rtp_asterisk' asterisk -rx 'module show like func_enum' asterisk -rx 'core show application Echo' asterisk -rx 'core show function ENUMLOOKUP'重载Asterisk：asterisk -rx "pjsip reload"3.7在PBX上部署分流 DNS由于PBX的默认DNS并不能解析tel.dn42，故应该部署dnsmasq对以下域名进行分流：<yourdomain>.dn42 tel.dn42安装dnsmasq：apt install -y dnsmasq我的配置文件示例： /etc/dnsmasq.d/dn42.confserver=/andy.dn42/172.21.118.161 server=/andy.dn42/172.21.118.164 server=/tel.dn42/172.21.118.161 server=/tel.dn42/172.21.118.164 #注：Knot DNS 只实现权威 DNS 服务，不是递归解析器，上面两行请设置成自己的权威DNS！ listen-address=127.0.0.1 bind-interfaces重启dnsmasq（由于 监听地址 变化，无法进行热重载）:systemctl restart dnsmasq在Asterisk 中测试：asterisk -rx \ 'dialplan eval function ENUMLOOKUP(+042429211001,sip,u,1,tel.dn42)'如果正常，应该返回：sip:1001@sip.andy.dn423.8配置 Telephony42 外呼拨号计划由于ENUM只负责告诉Asterisk“打给谁”，但后续Asterisk需要决定用哪个PJSIP endpoint 发起呼叫等等，故我们需要一个通用出站模板peer-enum-outbound 于是在 pjsip.conf 中配置一个无需注册的出站 endpoint。示例：[peer-enum-outbound] type=endpoint transport=transport-udp context=from-enum disallow=all allow=ulaw allow=alaw aors=peer-enum-outbound direct_media=no [peer-enum-outbound] type=aor实际目标由 ENUM 返回的 SIP URI 决定3.9配置 Telephony42 外呼拨号计划在 extensions.conf 中添加：[internal] exten => _+X.,1,NoOp(Telephony42 outbound: ${EXTEN}) same => n,Set(ENUMURI=${ENUMLOOKUP(${EXTEN},sip,u,1,tel.dn42)}) same => n,Set(ENUMURI=${ENUMLOOKUP(${ENUMNUM},sip,,1,tel.dn42)}) same => n,NoOp(ENUM result: ${ENUMURI}) same => n,GotoIf($["${ENUMURI}"=""]?enum-failed) same => n,Dial(PJSIP/${ENUMURI}@peer-enum-outbound,60) same => n,Hangup() same => n(enum-failed),Playback(invalid) same => n,Hangup()重载Asterisk:asterisk -rx "pjsip reload" asterisk -rx "dialplan reload"此时通过 1001 拨打 +042429211002应该已经能通了。 推荐使用 sngrep 进行通话流程分析，其是一个专用于 SIP 的 TUI 抓包与可视化工具（附有颜色标识）。3.10配置 Telephony42 入站 SIP为了接收其他 Telephony42 节点的呼叫，需要设置入站 endpoint。示例：[peer-enum-inbound] type=endpoint transport=transport-udp context=context-enum disallow=all allow=ulaw allow=alaw direct_media=no然后添加identify，让来自DN42的SIP包匹配到该endpoint。[peer-enum-inbound-identify-v4] type=identify endpoint=peer-enum-inbound match=10.0.0.0/8 match=172.20.0.0/14 match=172.31.0.0/16 [peer-enum-inbound-identify-v6] type=identify endpoint=peer-enum-inbound match=fd00::/8再次强调： 千万不要把公网 0.0.0.0/0 放进去！！3.11配置独立入站 Context在入站时，通话会先被引入到一个独立的Context中。[context-enum] exten => 1001,1,NoOp(Telephony42 inbound to 1001) same => n,Dial(PJSIP/1001,30) same => n,Hangup() exten => 1002,1,NoOp(Telephony42 inbound to 1002) same => n,Dial(PJSIP/1002,30) same => n,Hangup()重新加载Asterisk 需要注意，独立的入站 Context 只能限制外部来电的拨号权限，不能证明 CallerID 真实有效。4.提交 Telephony42 Registrytelephony: +04242921 nserver: ns1.andy.dn42 nserver: ns2.andy.dn42 admin-c: ANDY-DN42 tech-c: ANDY-DN42 mnt-by: ANDY-MNT source: DN425.PR合并后验证父区委派生效后，你可以使用 DN42 递归 DNS 查询。ZONE="1.2.9.2.4.2.4.0.tel.dn42." #检查 NS： dig @172.20.0.53 "$ZONE" NS \ +noall +answer +authority #正常应显示： #ns1.andy.dn42. #ns2.andy.dn42.至此，大公告成！快访问yp.dn42拨打 奇奇怪怪 的电话吧！参考文章：telephony42 完全指南：从零开始在 dn42 构建电话网络在 Telephony42 中配置 E.164 ENUM
一次 Looking Glass 参数注入事件的复盘：从 /etc/shadow 泄露到全集群加固
本文隐去了真实主机名、域名与 IP，但保留了完整的攻击 payload 与处置命令，方便复现与自查。TL;DR我的 DN42 集群跑着一个公网可访问的 bird-lg-go Looking Glass，四台节点共用一个前端。其中 bird-lgproxy ≤ v1.4.6 存在参数注入漏洞（CVE-2026-26514，GHSA-3qm5-22pm-wqg9）：攻击者通过 traceroute 功能的 q 参数注入 mtr 的 -F 选项，把任意文件当作“报告文件”读出来，最终读到了每台机器的 /etc/shadow——也就是 root 密码哈希。注意 v1.4.6 的修复不完整、可被绕过，真正修好是在 v1.4.7。处置结论：四台节点全部升级到 v1.4.7、轮换全部 root 密码、审计 SSH 密钥与计划任务、确认无后门与异常登录。 漏洞在公网边缘已验证彻底关闭。漏洞原理Looking Glass 的 traceroute 功能，本质上是把用户输入的目标丢给系统的 mtr / traceroute 去跑。问题出在旧版 bird-lgproxy 直接把用户输入拼进 exec.Command，既没有把它当成单一目标、也没有拒绝以 - 开头的输入。于是用户输入里可以夹带选项。攻击者只要构造：-F /etc/shadow test.invalid它就被当成了 mtr -F /etc/shadow test.invalid。而 mtr 的 -F 选项恰好是“从文件读取主机列表（report file）”——mtr 会尝试打开 /etc/shadow，并在报错信息里带出文件内容；而 proxy 用 CombinedOutput() 把 stdout + stderr 一起回显给前端，于是文件内容就顺着错误输出泄露了出去。test.invalid 只是凑一个“看起来像目标”的占位。于是一条 HTTP GET 就能读任意文件：# 攻击者视角：通过公网 LG 前端读取 /etc/shadow curl 'https://<lg-frontend>/traceroute/<node>/-F%20%2Fetc%2Fshadow%20test.invalid'这类问题的根源是老生常谈的一句话：永远不要把用户输入拆成命令行参数。目标就该当作一个不可拆分的整体字符串，任何以 - 开头的东西都不该被解释成选项。下面这张图把整条注入链路画出来了——注意红色路径是漏洞如何从一个 HTTP 参数一路变成任意文件读取的：flowchart LR A[公网攻击者] -->|"GET /traceroute/node/ -F /etc/shadow test.invalid"| B[LG 前端 bird-lg] B -->|转发请求 源 IP = 前端| C[节点 bird-lgproxy :8000] C -->|"输入直接进 exec.Command 未拒绝 - 开头参数"| D{"mtr -F /etc/shadow test.invalid"} D -->|-F 被解释为 read report file| E"读取 /etc/shadow" E -->|回显文件内容| A style A fill:#ffd5d5,stroke:#c0392b style D fill:#ffe6cc,stroke:#e67e22 style E fill:#ffd5d5,stroke:#c0392b,stroke-width:2px攻击时间线从被攻击节点的 bird-lgproxy 日志里，能看到一段非常“教科书”的探测→提权信息收集过程（时间为东八区，2026 年 7 月 1 日晚）：时间（UTC+8）谁干了什么07-01 21:47攻击者（经前端转发）bird-lgproxy 读 /etc/passwd（枚举用户、确认可读文件）07-01 21:53攻击者读 /root/.bash_history（翻历史命令，找线索/凭据）07-01 21:57攻击者读 /etc/shadow（拿到 root 密码哈希）用时序图看这十分钟的动作更直观，也能看清“攻击者 → 前端 → proxy → 文件系统”这条链路上，日志分别记在了哪一层：sequenceDiagram autonumber actor Atk as 攻击者（公网） participant FE as LG 前端 bird-lg participant PX as 节点 bird-lgproxy participant FS as 节点文件系统 Note over Atk,FS: 2026-07-01 晚（UTC+8） Atk->>FE: 21:47 GET -F /etc/passwd FE->>PX: 转发（源 IP = 前端） PX->>FS: mtr -F /etc/passwd FS-->>Atk: 用户列表回显 Note right of PX: proxy 日志只记到 前端 IP，非攻击者 Atk->>FE: 21:53 GET -F /root/.bash_history FE->>PX: 转发 PX->>FS: 读取历史命令 FS-->>Atk: 命令历史回显 Atk->>FE: 21:57 GET -F /etc/shadow FE->>PX: 转发 PX->>FS: mtr -F /etc/shadow FS-->>Atk: root 密码哈希泄露日志里的 payload 长这样（URL 编码前）：GET /traceroute?q=-F+/etc/passwd+<占位主机> GET /traceroute?q=-F+/etc/shadow+<占位主机>从 passwd → bash_history → shadow 的顺序很典型：先确认漏洞可用、再翻可能藏凭据的地方、最后直取密码哈希准备离线爆破。整个过程只花了十分钟。取证的一个坑：日志里的“攻击者 IP”是我自己的前端机这里有个值得单独拎出来讲的教训。bird-lg 是前端 + 后端 proxy 分离的架构：公网用户访问前端，前端再把 traceroute 请求转发给各节点的 bird-lgproxy。所以当我去被攻击节点的 proxy 日志里查“攻击者 IP”时，看到的源地址全是前端机器自己的 IP——因为对 proxy 来说，请求确实是前端发来的。真正的攻击者公网 IP，只存在于前端（反向代理 / 前端容器）的访问日志里。而等我开始做取证时：proxy 容器在升级时被重建，日志只剩当天；前端所在机器的反代访问日志已经轮转，最早的记录已经晚于攻击发生的时间；各节点 journald 的留存窗口也已经滚过了原始攻击时刻。结果就是：现存日志里所有 -F /etc/shadow 的请求，溯源后都指向我自己后来做的 PoC 验证和加固测试（payload 里还带着我自己打的 poc.invalid、codex-audit 之类的标记）。原始攻击者的真实 IP，随日志轮转丢失了。溯源时对每一条 -F /etc/shadow 请求的判定逻辑大致是这样，最后能落到“真实攻击者 IP”的路径全被日志留存挡死了：flowchart TD Q["现存日志里的 -F /etc/shadow 请求"] --> M{"payload 带 poc.invalid / codex-audit ？"} M -->|是| SELF["自己的 PoC / 加固验证"] M -->|否| T{"时间早于 已知攻击时刻 ？"} T -->|否| SELF2["事后测试 （仍是自己）"] T -->|是| L{"能查到 前端访问日志 ？"} L -->|否：已轮转/重建| LOST["真实攻击者 IP 已丢失 ✗"] L -->|是| REAL["定位攻击者 IP ✓"] style SELF fill:#d5f5e3,stroke:#27ae60 style SELF2 fill:#d5f5e3,stroke:#27ae60 style LOST fill:#ffd5d5,stroke:#c0392b,stroke-width:2px style REAL fill:#d6eaf8,stroke:#2980b9也做了兜底核查——确认没有异常后果：登录审计：所有成功的 root 登录源 IP 要么是我自己的出口，要么是我常用宽带 IP 段（带密钥登录用的是我自己的部署密钥），没有陌生境外 IP 成功登录，无横向移动迹象。authorized_keys：逐个核对指纹，只有我自己的密钥，无陌生 key。计划任务 / 进程：无后门 cron，无可疑常驻进程。教训很直接：面向公网的服务，一定要把访问日志的留存时间调长、并集中收走。等出事了再回头翻，日志往往已经不在了——尤其是这种“真实来源只记录在前端一层”的架构，取证窗口比你想的更短。漏洞报告与致谢发现异常后，我第一时间意识到这是个真实的安全问题，但我本身对安全研究并不在行。于是我把情况同步给了朋友 @KaguraiYoRoy，最终由他完成了漏洞分析、复现与规范的安全报告，一起负责任地披露给了上游作者。上游据此发布了官方安全公告并修复：GHSA-3qm5-22pm-wqg9 — Argument Injection / Exposure of Sensitive InformationCVE-2026-26514 · CWE-88（Argument Injection）+ CWE-200（Sensitive Info Exposure）CVSS 3.1: 7.5 High（AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N）影响版本：bird-lgproxy ≤ 1.4.6 · 修复版本：1.4.7 · 发布：2026-07-02致谢：KaguraiYoRoy（Analyst）、AndyYJF（Reporter）公告原文：https://github.com/xddxdd/bird-lg-go/security/advisories/GHSA-3qm5-22pm-wqg9借这次事，也想说一句：不懂安全不丢人，但发现问题别憋着。找个懂行的朋友一起走负责任披露的流程，比自己瞎折腾或者装作没看见都强得多。修复：升级到 v1.4.7需要强调的是：v1.4.6 的第一次修复并不彻底——它可以被无空格变体绕过，比如 -F/etc/shadow（选项和路径连在一起，绕过了当时基于空格的判断）。真正修好是在 v1.4.7：它对 traceroute 目标增加了 strings.HasPrefix(query, "-") 检查，任何以 - 开头的输入直接拒绝，从根上断掉了“把输入当选项”的可能。这样 -F /etc/shadow 会被直接拦下返回 Invalid target.，CVE 到这里彻底堵死。我把四台节点全部拉到了 v1.4.7。集群里有两种部署方式，处理方式不同：systemd 直接跑二进制的节点——换二进制 + 重启：cd /tmp curl -sLO https://github.com/xddxdd/bird-lg-go/releases/download/v1.4.7/bird-lgproxy-go-v1.4.7-linux-amd64.tar.gz tar xzf bird-lgproxy-go-v1.4.7-linux-amd64.tar.gz cp -a /usr/local/bin/bird-lgproxy-go /usr/local/bin/bird-lgproxy-go.pre-v1.4.7.bak # 留回滚点 cp -f bird-lgproxy-go /usr/local/bin/bird-lgproxy-go chmod +x /usr/local/bin/bird-lgproxy-go systemctl restart bird-lgproxyDocker 部署的节点——用 FROM scratch 的本地镜像，替换其中的 proxy 二进制后重建：# 构建上下文里就一个 Dockerfile + proxy 二进制 + 自带的 traceroute # Dockerfile: # FROM scratch # COPY proxy /proxy # COPY traceroute /traceroute # ENTRYPOINT ["/proxy"] cp -f proxy.v147 ./bird-lgproxy-v1.4.7/proxy docker compose up -d --build bird-lgproxy验证：用攻击者的原始 payload 打自己修复不能只看版本号，得用攻击者的真实路径去验证。我把原始 payload 通过公网前端重放到每一台节点：for node in <node1> <node2> <node3> <node4>; do curl -s "https://<lg-frontend>/traceroute/$node/-F%20%2Fetc%2Fshadow%20test.invalid" \ | grep -c 'root:' # 期望：0 done四台全部返回 Invalid target.，/etc/shadow 内容零泄露。直接对本地 :8000 打也是同样结果：curl -s 'http://127.0.0.1:8000/traceroute?q=-F+/etc/shadow+8.8.8.8' # 旧版：吐出 /etc/shadow 内容 # v1.4.7：HTTP 400 / Invalid target.收尾清单{x} 四台节点升级到 v1.4.7，保留回滚二进制/compose{x} 用原始 payload 经公网前端重放验证，四台 /etc/shadow 零泄露{x} 轮换全部 root 密码（≥16 位混合）{x} 审计 authorized_keys 指纹、cron、常驻进程——无异常{x} 核查登录历史——无陌生 IP 成功登录几点反思别把用户输入拆成命令行参数。 这不是 LG 独有的坑，任何把外部输入拼进 exec 的地方都要警惕以 - 开头的注入。目标当整体字符串处理是底线。公网服务的 proxy 别裸奔在 0.0.0.0。 我这次四台的 bird-lgproxy :8000 都是公网直接可达的——下一步准备把它收敛到只允许前端访问（iptables / 只监听内网）。少一个公网入口，就少一层攻击面。日志留存要提前配好、集中收走。 出事后才发现真正的攻击者 IP 只记录在前端一层、而且已经轮转丢了。取证的黄金窗口比想象中短得多。本文用于记录与分享，已隐去全部真实主机名、域名与 IP。如果你也在跑公网 Looking Glass，建议立刻确认 bird-lgproxy 版本 ≥ v1.4.7。
写在前面本人在dn42网络中有4个节点，本来搭建了LookingGlass来监测节点运行状态，但考虑到不够直观 其实是想折腾，所以打算搭建一个可以图形化监控每台节点Bird运行状态的仪表盘，在社区里面发现可以用Grafana搭配Prometheus作为数据源实现拓扑图如下：graph TD subgraph DN42 Network N1[Node 1] --> NE1(Node Exporter) N1 --> BE1(Bird/FRR Exporter) N2[Node 2] --> NE2(Node Exporter) N2 --> BE2(Bird/FRR Exporter) N3[Node 3] --> NE3(Node Exporter) N3 --> BE3(Bird/FRR Exporter) N4[Node 4] --> NE4(Node Exporter) N4 --> BE4(Bird/FRR Exporter) end NE1 --> P(Prometheus Server) NE2 --> P NE3 --> P NE4 --> P BE1 --> P BE2 --> P BE3 --> P BE4 --> P P --> G(Grafana Server) User[用户] --> G搭建部署 Prometheus ServerPrometheus Server 将运行在独立的监控服务器上。创建工作目录mkdir -p /opt/prometheus/config /opt/prometheus/data创建 Prometheus 配置文件 (/opt/prometheus/config/prometheus.yml)global: scrape_interval: 15s # 默认抓取间隔 scrape_configs: - job_name: 'prometheus' static_configs: - targets: ['localhost:9090'] # 监控 Prometheus 自身 - job_name: 'node_exporter' static_configs: - targets: ['<node1_ip>:9100', '<node2_ip>:9100', '<node3_ip>:9100', '<node4_ip>:9100'] # 替换为您的 DN42 节点 IP - job_name: 'bird_exporter' static_configs: - targets: ['<node1_ip>:9324', '<node2_ip>:9324', '<node3_ip>:9324', '<node4_ip>:9324'] # 替换为您的 DN42 节点 IP注意: 请将 <nodeX_ip> 替换为您的 DN42 节点的实际 IP 地址。使用 Docker Compose 部署 Prometheus创建 docker-compose.yml 文件：services: prometheus: image: prom/prometheus container_name: prometheus network_mode: host ports: - "9090:9090" volumes: - /opt/prometheus/config/prometheus.yml:/etc/prometheus/prometheus.yml - /opt/prometheus/data:/prometheus command: - '--config.file=/etc/prometheus/prometheus.yml' - '--storage.tsdb.path=/prometheus' - '--web.enable-lifecycle' restart: unless-stopped将数据目录的权限赋予 Prometheus 用户：chown -R 65534:65534 /opt/prometheus/data启动 Prometheus：docker-compose up -d通过访问 http://<监控服务器IP>:9090 来验证 Prometheus 是否正常运行。部署 Node Exporter (每个 DN42 节点上)创建工作目录mkdir -p /opt/node_exporter方式1：使用 Docker 部署 Node Exporterdocker run -d \ --name node_exporter \ --net="host" \ --pid="host" \ -v "/:/host:ro,rslave" \ quay.io/prometheus/node-exporter:latest \ --path.rootfs=/host方式2：使用二进制可执行文件部署 Node Exporter下载并解压 Node Exporter访问 Prometheus 下载页面 获取最新版本的 Node Exporter。以下示例使用 1.7.0 版本wget https://github.com/prometheus/node_exporter/releases/download/v1.7.0/node_exporter-1.7.0.linux-amd64.tar.gz tar xvfz node_exporter-1.7.0.linux-amd64.tar.gz sudo cp node_exporter-1.7.0.linux-amd64/node_exporter /usr/local/bin sudo chown prometheus:prometheus /usr/local/bin/node_exporter rm -rf node_exporter-1.7.0.linux-amd64.tar.gz node_exporter-1.7.0.linux-amd64创建 Systemd 服务文件 (/etc/systemd/system/node_exporter.service)[Unit] Description=Node Exporter Wants=network-online.target After=network-online.target [Service] Type=simple ExecStart=/usr/local/bin/node_exporter \ --web.listen-address=":9100" \ --collector.textfile.directory="/var/lib/node_exporter/textfile_collector" [Install] WantedBy=multi-user.target重新加载 Systemd 并启动 Node Exportersudo systemctl daemon-reload sudo systemctl start node_exporter sudo systemctl enable node_exporterBird Exporter首先，确保 BIRD 配置允许 bird_exporter 访问其控制套接字。通常需要在 /etc/bird/bird.conf 或 /etc/bird2/bird.conf 中添加类似以下内容：control socket "/var/run/bird/bird.ctl" mode 0777;然后，使用 Docker 部署 Bird Exporter (推荐用于内存充足的节点)：docker run -d \ --name bird_exporter \ --network host \ -v /var/run/bird/bird.ctl:/var/run/bird/bird.ctl \ czerwonk/bird_exporter:latest使用二进制方式部署 Bird Exporter下载并解压 Bird Exporter访问 Bird Exporter GitHub Releases 获取最新版本的 Bird Exporter。以下示例使用 1.1.0 版本，请根据实际情况替换。wget https://github.com/czerwonk/bird_exporter/releases/download/v1.1.0/bird_exporter-1.1.0.linux-amd64.tar.gz tar xvfz bird_exporter-1.1.0.linux-amd64.tar.gz sudo cp bird_exporter-1.1.0.linux-amd64/bird_exporter /usr/local/bin sudo chown prometheus:prometheus /usr/local/bin/bird_exporter rm -rf bird_exporter-1.1.0.linux-amd64.tar.gz bird_exporter-1.1.0.linux-amd64创建 Systemd 服务文件 (/etc/systemd/system/bird_exporter.service)[Unit] Description=Bird Exporter Wants=network-online.target After=network-online.target [Service] Type=simple ExecStart=/usr/local/bin/bird_exporter \ --bird.socket="/var/run/bird/bird.ctl" \ --web.listen-address=":9324" [Install] WantedBy=multi-user.target重新加载 Systemd 并启动 Bird Exportersudo systemctl daemon-reload sudo systemctl start bird_exporter sudo systemctl enable bird_exporter部署 Grafana ServerGrafana Server 建议与 Prometheus Server 部署在同一台监控服务器上。创建工作目录mkdir -p /opt/grafana/data4.4.2 使用 Docker Compose 部署 Grafana更新之前创建的 docker-compose.yml 文件，添加 Grafana 服务：services: prometheus: image: prom/prometheus container_name: prometheus ports: - "9090:9090" volumes: - /opt/prometheus/config/prometheus.yml:/etc/prometheus/prometheus.yml - /opt/prometheus/data:/prometheus command: - '--config.file=/etc/prometheus/prometheus.yml' - '--storage.tsdb.path=/prometheus' - '--web.enable-lifecycle' restart: unless-stopped grafana: image: grafana/grafana container_name: grafana ports: - "3000:3000" volumes: - /opt/grafana/data:/var/lib/grafana environment: - GF_SECURITY_ADMIN_USER=admin - GF_SECURITY_ADMIN_PASSWORD=your_strong_password # 请替换为强密码 depends_on: - prometheus restart: unless-stopped重新启动 Docker Compose 服务以启动 Grafana：docker-compose up -d配置 Grafana 数据源登录 Grafana。在左侧导航栏中，点击齿轮图标 (Configuration) -> Data Sources。点击 Add data source。选择 Prometheus。在 HTTP 部分的 URL 字段中输入 http://prometheus:9090 (如果 Prometheus 和 Grafana 在同一个 Docker Compose 网络中) 或 http://localhost:9090 (如果 Prometheus 运行在宿主机上，且 Grafana 可以直接访问)。点击 Save & Test。如果一切正常，您将看到 Data source is working 的消息。导入 Grafana 仪表盘Grafana 社区提供了许多现成的仪表盘，可以大大简化监控配置。以下是一些推荐的仪表盘 ID：Node Exporter Full: ID 1860 (或搜索 Node Exporter Full)，用于监控主机操作系统指标。BIRD RS: ID 5259 (或搜索 BIRD RS)，用于监控 BIRD 路由协议状态。FRR Exporter - BGP: ID 22943 (或搜索 FRR Exporter - BGP)，用于监控 FRR BGP 状态。导入步骤在 Grafana 左侧导航栏中，点击 + 图标 (Create) -> Import。在 Import via grafana.com 字段中输入上述仪表盘 ID，然后点击 Load。选择您的 Prometheus 数据源。点击 Import。重复此过程，导入所有您需要的仪表盘。告警配置 (可选)Grafana 允许您基于 Prometheus 收集的指标配置告警规则。当指标达到预设阈值时，Grafana 可以通过邮件、Slack、Webhook 等方式发送通知。配置告警通道在 Grafana 左侧导航栏中，点击齿轮图标 (Configuration) -> Alerting -> Notification channels。点击 New channel。选择偏好的通知类型 (例如 Email, Slack) 并填写相关配置信息。点击 Save。创建告警规则打开想要添加告警的仪表盘。选择一个面板，点击面板标题，然后选择 Edit。在面板编辑视图中，切换到 Alert 选项卡。点击 Create Alert。定义告警规则的条件、评估周期和通知通道。点击 Save。分享我的JSON{collapse}{collapse-item label="Sample" close}{ "annotations": { "list": [ { "builtIn": 1, "datasource": { "uid": "-- Grafana --" }, "enable": true, "hide": true, "iconColor": "rgba(0, 211, 255, 1)", "name": "Annotations \u0026 Alerts", "type": "dashboard" } ] }, "description": "Live BIRD routing health, prefix visibility, session status and routing activity across all monitored routers.", "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 1, "id": 6, "links": [ ], "panels": [ { "datasource": { "type": "prometheus", "uid": "dfdlzb2hrvlz4b" }, "description": "Healthy Prometheus scrape targets for the selected BIRD exporters.", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [ ], "thresholds": { "mode": "absolute", "steps": [ { "color": "red", "value": 0 }, { "color": "green", "value": 1 } ] }, "unit": "short" }, "overrides": [ ] }, "gridPos": { "h": 4, "w": 6, "x": 0, "y": 0 }, "id": 10, "options": { "colorMode": "background_solid", "graphMode": "none", "justifyMode": "center", "orientation": "horizontal", "percentChangeColorMode": "standard", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showPercentChange": false, "textMode": "auto", "wideLayout": true }, "pluginVersion": "12.3.3", "targets": [ { "datasource": { "type": "prometheus", "uid": "dfdlzb2hrvlz4b" }, "editorMode": "code", "expr": "count(up{job=\"bird_exporter\",instance=~\"$instance\"} == 1) or vector(0)", "format": "time_series", "instant": true, "legendFormat": "__auto", "range": false, "refId": "A" } ], "title": "BIRD Exporters Online", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "dfdlzb2hrvlz4b" }, "description": "Established BGP sessions across the selected router scope.", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [ ], "thresholds": { "mode": "absolute", "steps": [ { "color": "red", "value": 0 }, { "color": "green", "value": 1 } ] }, "unit": "short" }, "overrides": [ ] }, "gridPos": { "h": 4, "w": 6, "x": 6, "y": 0 }, "id": 11, "options": { "colorMode": "background_solid", "graphMode": "none", "justifyMode": "center", "orientation": "horizontal", "percentChangeColorMode": "standard", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showPercentChange": false, "textMode": "auto", "wideLayout": true }, "pluginVersion": "12.3.3", "targets": [ { "datasource": { "type": "prometheus", "uid": "dfdlzb2hrvlz4b" }, "editorMode": "code", "expr": "count(bird_protocol_up{proto=\"BGP\",instance=~\"$instance\"} == 1) or vector(0)", "format": "time_series", "instant": true, "legendFormat": "__auto", "range": false, "refId": "A" } ], "title": "BGP Sessions Up", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "dfdlzb2hrvlz4b" }, "description": "BGP sessions currently not established.", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [ ], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": 0 }, { "color": "red", "value": 1 } ] }, "unit": "short" }, "overrides": [ ] }, "gridPos": { "h": 4, "w": 6, "x": 12, "y": 0 }, "id": 12, "options": { "colorMode": "background_solid", "graphMode": "none", "justifyMode": "center", "orientation": "horizontal", "percentChangeColorMode": "standard", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showPercentChange": false, "textMode": "auto", "wideLayout": true }, "pluginVersion": "12.3.3", "targets": [ { "datasource": { "type": "prometheus", "uid": "dfdlzb2hrvlz4b" }, "editorMode": "code", "expr": "count(bird_protocol_up{proto=\"BGP\",instance=~\"$instance\"} == 0) or vector(0)", "format": "time_series", "instant": true, "legendFormat": "__auto", "range": false, "refId": "A" } ], "title": "BGP Sessions Down", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "dfdlzb2hrvlz4b" }, "description": "Total adjacent OSPF and OSPFv3 neighbors.", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [ ], "thresholds": { "mode": "absolute", "steps": [ { "color": "red", "value": 0 }, { "color": "green", "value": 1 } ] }, "unit": "short" }, "overrides": [ ] }, "gridPos": { "h": 4, "w": 6, "x": 18, "y": 0 }, "id": 13, "options": { "colorMode": "background_solid", "graphMode": "none", "justifyMode": "center", "orientation": "horizontal", "percentChangeColorMode": "standard", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showPercentChange": false, "textMode": "auto", "wideLayout": true }, "pluginVersion": "12.3.3", "targets": [ { "datasource": { "type": "prometheus", "uid": "dfdlzb2hrvlz4b" }, "editorMode": "code", "expr": "(sum(bird_ospf_neighbor_adjacent_count{instance=~\"$instance\"}) or vector(0)) + (sum(bird_ospfv3_neighbor_adjacent_count{instance=~\"$instance\"}) or vector(0))", "format": "time_series", "instant": true, "legendFormat": "__auto", "range": false, "refId": "A" } ], "title": "OSPF Adjacent Neighbors", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "dfdlzb2hrvlz4b" }, "description": "Prefixes advertised to PITER-IX peers across the selected time range.", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisBorderShow": false, "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "left", "barAlignment": 0, "barWidthFactor": 0.6, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "insertNulls": false, "lineInterpolation": "stepAfter", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "showValues": false, "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [ ], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": 0 }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byValue", "options": { "op": "gte", "reducer": "allIsZero", "value": 0 } }, "properties": [ { "id": "custom.hideFrom", "value": { "legend": true, "tooltip": true, "viz": false } } ] }, { "matcher": { "id": "byValue", "options": { "op": "gte", "reducer": "allIsNull", "value": 0 } }, "properties": [ { "id": "custom.hideFrom", "value": { "legend": true, "tooltip": true, "viz": false } } ] }, { "matcher": { "id": "byValue", "options": { "op": "gte", "reducer": "allIsZero", "value": 0 } }, "properties": [ { "id": "custom.hideFrom", "value": { "legend": true, "tooltip": true, "viz": false } } ] }, { "matcher": { "id": "byValue", "options": { "op": "gte", "reducer": "allIsNull", "value": 0 } }, "properties": [ { "id": "custom.hideFrom", "value": { "legend": true, "tooltip": true, "viz": false } } ] }, { "matcher": { "id": "byValue", "options": { "op": "gte", "reducer": "allIsZero", "value": 0 } }, "properties": [ { "id": "custom.hideFrom", "value": { "legend": true, "tooltip": true, "viz": false } } ] }, { "matcher": { "id": "byValue", "options": { "op": "gte", "reducer": "allIsNull", "value": 0 } }, "properties": [ { "id": "custom.hideFrom", "value": { "legend": true, "tooltip": true, "viz": false } } ] } ] }, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 4 }, "id": 2, "options": { "legend": { "calcs": [ "lastNotNull" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "hideZeros": true, "mode": "multi", "sort": "desc" } }, "pluginVersion": "12.3.3", "targets": [ { "datasource": { "type": "prometheus", "uid": "dfdlzb2hrvlz4b" }, "dateTimeType": "DATETIME", "editorMode": "code", "expr": "bird_protocol_prefix_export_count{\n instance=~\"$instance\",\r\n name=~\"(dn42_peer_|dn42_)[0-9a-zA-Z]+\", \r\n name!~\"dn42_ospf\",\r\n name!~\"dn42_ospf6\", \r\n ip_version=\"4\"\r\n} \u003e 0\r\n", "format": "time_series", "formattedQuery": "SELECT $timeSeries as t, count() FROM $table WHERE $timeFilter GROUP BY t ORDER BY t", "intervalFactor": 1, "legendFormat": "{{name}}", "query": "SELECT\n $timeSeries as t,\n count()\nFROM $table\nWHERE $timeFilter\nGROUP BY t\nORDER BY t", "range": true, "refId": "A", "round": "0s" } ], "title": "PITER-IX · Exported Prefixes", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "dfdlzb2hrvlz4b" }, "description": "Prefixes received from PITER-IX peers across the selected time range.", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisBorderShow": false, "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "prefixes", "axisPlacement": "left", "barAlignment": 0, "barWidthFactor": 0.6, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "insertNulls": false, "lineInterpolation": "stepAfter", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "showValues": false, "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [ ], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": 0 }, { "color": "red", "value": 80 } ] }, "unit": "none" }, "overrides": [ { "matcher": { "id": "byValue", "options": { "op": "gte", "reducer": "allIsZero", "value": 0 } }, "properties": [ { "id": "custom.hideFrom", "value": { "legend": true, "tooltip": true, "viz": false } } ] }, { "matcher": { "id": "byValue", "options": { "op": "gte", "reducer": "allIsNull", "value": 0 } }, "properties": [ { "id": "custom.hideFrom", "value": { "legend": true, "tooltip": true, "viz": false } } ] }, { "matcher": { "id": "byValue", "options": { "op": "gte", "reducer": "allIsZero", "value": 0 } }, "properties": [ { "id": "custom.hideFrom", "value": { "legend": true, "tooltip": true, "viz": false } } ] }, { "matcher": { "id": "byValue", "options": { "op": "gte", "reducer": "allIsNull", "value": 0 } }, "properties": [ { "id": "custom.hideFrom", "value": { "legend": true, "tooltip": true, "viz": false } } ] }, { "matcher": { "id": "byValue", "options": { "op": "gte", "reducer": "allIsZero", "value": 0 } }, "properties": [ { "id": "custom.hideFrom", "value": { "legend": true, "tooltip": true, "viz": false } } ] }, { "matcher": { "id": "byValue", "options": { "op": "gte", "reducer": "allIsNull", "value": 0 } }, "properties": [ { "id": "custom.hideFrom", "value": { "legend": true, "tooltip": true, "viz": false } } ] } ] }, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 4 }, "id": 3, "options": { "legend": { "calcs": [ "lastNotNull" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "hideZeros": true, "mode": "multi", "sort": "desc" } }, "pluginVersion": "12.3.3", "targets": [ { "datasource": { "type": "prometheus", "uid": "dfdlzb2hrvlz4b" }, "dateTimeType": "DATETIME", "editorMode": "code", "expr": "bird_protocol_prefix_import_count{\n instance=~\"$instance\",\r\n name=~\"(dn42_peer_|dn42_)[0-9a-zA-Z]+\", \r\n name!~\"dn42_ospf\",\r\n name!~\"dn42_ospf6\", \r\n ip_version=\"4\"\r\n} \u003e 0", "format": "time_series", "formattedQuery": "SELECT $timeSeries as t, count() FROM $table WHERE $timeFilter GROUP BY t ORDER BY t", "intervalFactor": 1, "legendFormat": "{{name}}", "query": "SELECT\n $timeSeries as t,\n count()\nFROM $table\nWHERE $timeFilter\nGROUP BY t\nORDER BY t", "range": true, "refId": "A", "round": "0s" } ], "title": "PITER-IX · Imported Prefixes", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "dfdlzb2hrvlz4b" }, "description": "Accepted route updates and withdrawals per second. Spikes make routing churn immediately visible.", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisBorderShow": false, "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "events / second", "axisPlacement": "left", "barAlignment": 0, "barWidthFactor": 0.6, "drawStyle": "line", "fillOpacity": 8, "gradientMode": "opacity", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "insertNulls": false, "lineInterpolation": "smooth", "lineWidth": 2, "pointSize": 4, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "showValues": false, "spanNulls": true, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [ ], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": 0 } ] }, "unit": "ops" }, "overrides": [ ] }, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 12 }, "id": 14, "options": { "legend": { "calcs": [ "lastNotNull" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "hideZeros": true, "mode": "multi", "sort": "desc" } }, "pluginVersion": "12.3.3", "targets": [ { "datasource": { "type": "prometheus", "uid": "dfdlzb2hrvlz4b" }, "editorMode": "code", "expr": "sum by (instance) (rate(bird_protocol_changes_update_import_accept_count{instance=~\"$instance\"}[$__rate_interval]))", "format": "time_series", "legendFormat": "{{instance}} · imported updates", "range": true, "refId": "A" }, { "datasource": { "type": "prometheus", "uid": "dfdlzb2hrvlz4b" }, "editorMode": "code", "expr": "sum by (instance) (rate(bird_protocol_changes_withdraw_import_accept_count{instance=~\"$instance\"}[$__rate_interval]))", "format": "time_series", "legendFormat": "{{instance}} · imported withdrawals", "range": true, "refId": "B" }, { "datasource": { "type": "prometheus", "uid": "dfdlzb2hrvlz4b" }, "editorMode": "code", "expr": "sum by (instance) (rate(bird_protocol_changes_update_export_accept_count{instance=~\"$instance\"}[$__rate_interval]))", "format": "time_series", "legendFormat": "{{instance}} · exported updates", "range": true, "refId": "C" }, { "datasource": { "type": "prometheus", "uid": "dfdlzb2hrvlz4b" }, "editorMode": "code", "expr": "sum by (instance) (rate(bird_protocol_changes_withdraw_export_accept_count{instance=~\"$instance\"}[$__rate_interval]))", "format": "time_series", "legendFormat": "{{instance}} · exported withdrawals", "range": true, "refId": "D" } ], "title": "Routing Activity · Updates \u0026 Withdrawals", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "dfdlzb2hrvlz4b" }, "description": "Only non-established BGP sessions are listed. An empty table means all matched sessions are healthy.", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "custom": { "align": "auto", "cellOptions": { "type": "auto" }, "filterable": true, "footer": { "reducers": [ ] }, "inspect": false }, "mappings": [ { "options": { "0": { "color": "red", "index": 0, "text": "DOWN" } }, "type": "value" } ], "thresholds": { "mode": "absolute", "steps": [ { "color": "red", "value": 0 } ] } }, "overrides": [ ] }, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 12 }, "id": 15, "options": { "cellHeight": "sm", "footer": { "countRows": false, "enablePagination": true, "fields": "", "reducer": [ "sum" ], "show": false }, "showHeader": true, "sortBy": [ { "desc": false, "displayName": "instance" } ] }, "pluginVersion": "12.3.3", "targets": [ { "datasource": { "type": "prometheus", "uid": "dfdlzb2hrvlz4b" }, "editorMode": "code", "expr": "bird_protocol_up{proto=\"BGP\",instance=~\"$instance\"} == 0", "format": "table", "instant": true, "legendFormat": "{{instance}} · {{name}} · IPv{{ip_version}}", "range": false, "refId": "A" } ], "title": "BGP Sessions Requiring Attention", "transformations": [ { "id": "organize", "options": { "excludeByName": { "Time": true, "__name__": true, "export_filter": true, "import_filter": true, "job": true, "proto": true }, "indexByName": { "Value": 4, "instance": 0, "ip_version": 2, "name": 1, "state": 3 }, "renameByName": { "Value": "Status", "instance": "Router", "ip_version": "IP", "name": "Session", "state": "State" } } } ], "type": "table" }, { "datasource": { "type": "prometheus", "uid": "dfdlzb2hrvlz4b" }, "description": "IPv4 and IPv6 prefixes exported by the BIRD protocol named flapalerted. Use the Router selector to isolate a node.", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisBorderShow": false, "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "events / second", "axisPlacement": "left", "barAlignment": 0, "barWidthFactor": 0.6, "drawStyle": "line", "fillOpacity": 6, "gradientMode": "opacity", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "insertNulls": false, "lineInterpolation": "smooth", "lineWidth": 2, "pointSize": 4, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "showValues": false, "spanNulls": true, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [ ], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": 0 } ] }, "unit": "short" }, "overrides": [ ] }, "gridPos": { "h": 8, "w": 24, "x": 0, "y": 20 }, "id": 16, "options": { "legend": { "calcs": [ "lastNotNull", "max" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "hideZeros": true, "mode": "multi", "sort": "desc" } }, "pluginVersion": "12.3.3", "targets": [ { "datasource": { "type": "prometheus", "uid": "dfdlzb2hrvlz4b" }, "editorMode": "code", "expr": "bird_protocol_prefix_export_count{name=\"flapalerted\",instance=~\"$instance\"}", "format": "time_series", "legendFormat": "{{instance}} · IPv{{ip_version}}", "range": true, "refId": "A" } ], "title": "FlapAlerted · Exported Prefixes", "type": "timeseries" } ], "preload": false, "refresh": "30s", "schemaVersion": 42, "tags": [ ], "templating": { "list": [ { "current": { "text": "All", "value": "$__all" }, "datasource": { "type": "prometheus", "uid": "dfdlzb2hrvlz4b" }, "definition": "label_values(up{job=\"bird_exporter\"}, instance)", "includeAll": true, "label": "Router", "multi": true, "name": "instance", "options": [ ], "query": { "query": "label_values(up{job=\"bird_exporter\"}, instance)", "refId": "PrometheusVariableQueryEditor-VariableQuery" }, "refresh": 1, "regex": "", "sort": 1, "type": "query" } ] }, "time": { "from": "now-5m", "to": "now" }, "timepicker": { }, "timezone": "", "title": "BIRD · Routing Overview", "uid": "f6bde99c-ee14-44cf-9b57-17052329ab55", "version": 15 }{/collapse-item}{/collapse}参考文献Grafana Labs - FRR Exporter - BGP DashboardGrafana Labs - BIRD RS DashboardGrafana Labs - Node Exporter Full DashboardGitHub - czerwonk/bird_exporterGitHub - tynany/frr_exporterPrometheus DocumentationGrafana Documentation