校园网 Dr.COM 客户端部署与 11 GB syslog 治理

文中账号、密码、MAC 均以占位符代替,静态地址为示意值;请以自己登记到的信息为准。

1.问题提出

学校学生宿舍的有线网使用基于 Dr.COM 的认证。Windows 下只要装上那个蓝绿色的官方客户端(Drcom)就能登录,但这套客户端只有 Windows 版,Linux 上没有对应程序。于是出现了一个很具体的困境:网络登记照做、静态地址照填,可就是在 Linux 里连不上。

我按学校流程做了三件事:在线上登记房间端口号(每个网口对应一个专属号)、绑定自己的校园网账号、填写电脑的 MAC 地址,随后拿到了一组静态 IPv4 地址和学校 DNS。在 Windows 上把这套地址配好、再打开 Dr.COM 客户端,认证就能通过。问题在于,我日常的开发环境是 Ubuntu,不可能为了一次认证单独留在 Windows 里。

后来在学长的社团论坛帖里看到,他们维护着一个开源项目 scutclient,专门在 Linux 上复刻这套认证流程。这篇文章记录的就是从发现它,到把它跑通、做成开机自启服务,最后治理它带来的日志问题的完整过程。

项目地址:scutclient,讨论出处:SCUT 开源社区论坛帖。

2.先区分:认证与编址是两件事

在动手之前,需要先把一个很容易混淆的点理清,否则后面的报错会很难定位。

论坛帖里给了很关键的说明:Drcom 拨号的本质,是基于 EAPoL 魔改的认证协议,加上基于 UDP 包的心跳保活机制。 这里其实包含了两个彼此独立、又都必须正确的环节:

  • 认证:发生在二层(L2)。客户端在网口上直接收发 802.1X 的 EAPoL 帧,向认证服务器证明“这个 MAC + 账号是登记的”。这一步和 IP 地址无关,只要网口物理链路是通的,帧就能到达交换机。
  • 编址:发生在三层(L3)。只有把登记到的静态 IPv4、网关、DNS 配到认证用网口上,认证通过后的流量才有正确的源地址,才能真正跑出校园网。

很多人(包括当时的我)会把这两件事当成一件事:既然认证是“登录”,那配好地址再点登录就行了。实际上它们各自会独立报错,而报错信息又都不那么直白。后文会看到,我踩的每一个坑,基本都能归到这两类里。

scutclient 是 SCUT Router Group 用 C 写的一个最小实现,采用 AGPL-3.0 许可,可以编译成不依赖外部运行库的二进制,也提供了 OpenWrt 的 ipk 包。它做的事情就是把上面那套 EAPoL 认证和 UDP 心跳重新实现一遍。

3.编译与第一次运行

项目本身很小,构建方式也是最普通的一套:

1
2
3
4
5
git clone https://github.com/scutclient/scutclient.git
cd scutclient
mkdir build && cd build
cmake ..
make

编译产物是一个几十 KB 的 scutclient,用法也很直接:

1
scutclient --username <账号> --password <密码> [options...]

需要留意的几个默认值(都写在 src/info.c 和 src/auth.h 里):

项 默认值 说明
认证服务器 202.38.210.131 --udp-server
DNS 222.201.130.30 --dns,发给服务器的值
网口 eth0 --iface,现代 Ubuntu 通常是 enp3s0 之类
客户端版本 4472434f4d0096022a --cli-version,即字符串 DrCOM\0\x96\x02\x2a
hash 2ec15ad258aee9604b18f2f8114da38db16efd00 --hash,对应 DrAuthSvr.dll

所以第一次我直接跑了:

1
sudo ./scutclient --username <账号> --password <密码> --iface enp3s0

结果认证流程走到了最后一步,却被服务器明确拒绝:

1
[8021X][ERROR]:[Server: Authentication failed: You are not allowed to login using current IP/MAC address.]

4.第一次失败:IP/MAC 不被允许

这条报错非常关键,因为它说明协议本身没问题——客户端完整走完了 Start → Request Identity → Response → Request MD5-Challenge → Response 的全过程,是服务器在最后一关拒绝了结果。

错误信息提到的是“current IP/MAC”,但我当时只核对了 MAC,忽略了 IP。查看网口状态才发现问题:

1
2
ip -4 addr show enp3s0
# inet 10.42.0.1/24 ...

enp3s0 上挂着的是一段残留的 10.42.0.1/24,根本不是登记信息里的地址。scutclient 在认证包里上报的是网口当前地址,服务器一看这个 IP 不在绑定关系里,自然拒绝。

这就是第 2 节里说的“编址”问题。 认证走的是二层帧,所以它照样能和服务器对话;但服务器校验的源 IP 是网口地址,地址不对,认证就不会放行。

我的登记信息是:

1
2
3
4
IP      10.100.23.15
掩码 255.255.248.0 → /21
网关 10.100.23.254
DNS 10.6.6.6 / 10.5.5.5

我用的桌面环境由 NetworkManager 管理,所以直接用 nmcli 改成一个静态连接(连接名可用 nmcli con show 查到):

1
2
3
4
5
6
7
8
sudo nmcli con mod "有线连接 1" \
ipv4.method manual \
ipv4.addresses 10.100.23.15/21 \
ipv4.gateway 10.100.23.254 \
ipv4.dns "10.6.6.6,10.5.5.5" \
ipv4.dns-search ""

sudo nmcli con up "有线连接 1"

这里顺手校验一下网段是否自洽:10.100.23.15/21 属于 10.100.16.0/21,而网关 10.100.23.254 正好是该网段最后一个可用主机,符合常见配置。

改完之后可以先用 ping 确认三层是通的,再看认证:

1
2
3
ip route
ping -c2 -I enp3s0 10.100.23.254 # 网关
ping -c2 -I enp3s0 10.6.6.6 # DNS

网关和 DNS 都能通之后,再跑 scutclient,服务器就不再报 IP/MAC 不允许,而是返回 Server: Success.,随后进入心跳保活。

这里有一个必须强调的边界:当时我一度以为“认证成功就等于能上网”,实际不是。认证成功只代表二层会话建立;能不能访问公网,还取决于编址、默认路由以及校园网出口策略。我中途甚至遇到过一次“认证成功、心跳正常,但公网 TCP 全不通”的情况,那属于另一个层面的问题,本文不展开,但它提醒我:看到 Success 不等于任务结束。

5.权限:root 还是文件能力

认证跑通之后,遇到的第二个问题是日志。用 sudo 运行时,程序不停打印:

1
Unable to open log file: Permission denied

一开始我以为是网口名没配好,后来才定位到,这条信息来自 src/tracelog.c——它把日志路径硬编码成了 /tmp/scutclient.log,而这个文件属于我的普通用户。用 sudo(也就是 root)运行时,root 打不开一个属于普通用户的文件,于是刷屏。

更值得记录的是:这类问题背后其实是一个可以讲清楚的权限模型。

scutclient 需要什么特权?看源码就知道,它用的是原始套接字:

1
auth_8021x_sock = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_PAE));

再加上 SO_BINDTODEVICE。这些操作需要 CAP_NET_RAW 能力。以普通用户身份直接跑,会得到:

1
Unable to create raw socket: Operation not permitted

传统做法是加 sudo,但“以 root 运行”是一个过大的权限:它顺带带来了日志文件属主不匹配的问题,也让一个只做网络认证的小程序拥有了完整的 root 权限。

更合适的做法是只授予它需要的那一项能力,也就是给二进制加上文件能力:

1
sudo setcap cap_net_raw+ep /home/harekasa/scutclient/build/scutclient

这样一来,普通用户执行它时就能获得 CAP_NET_RAW,既不需要 sudo,日志文件也仍然是自己的,权限问题一并消失。

这里需要区分两个概念:sudo 是“换一个更强的用户身份”,而文件能力是“给这个程序精确地开门”。对于只需要单个能力的小工具,后者通常是更好的选择。

6.认证之外:做成 systemd 服务

手动跑通只是第一步,还希望它开机自启、断线自动重连。于是写一个 systemd 服务。先区分一个容易踩的坑:

systemd 服务默认不读 shell 的 export,也不读 /etc/environment(那是给 PAM 登录会话用的)。 在 systemd 里,“全局变量”的正确载体是 EnvironmentFile=。所以我把账号、密码、网口、DNS 等都放进一个环境变量文件,由服务读取。

变量文件(/etc/scutclient/scutclient.conf,含密码,权限收紧为 600 且 root 属主):

1
2
3
4
5
6
SCUT_USERNAME=<账号>
SCUT_PASSWORD=<密码>
SCUT_IFACE=enp3s0
SCUT_DNS=10.6.6.6
SCUT_UDP_SERVER=202.38.210.131
SCUT_HOSTNAME=harekasa-desktop

服务单元(/etc/systemd/system/scutclient.service):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[Unit]
Description=SCUT campus network Dr.COM client (scutclient)
Wants=network-online.target
After=network-online.target

[Service]
Type=simple
EnvironmentFile=/etc/scutclient/scutclient.conf
ExecStart=/home/harekasa/scutclient/build/scutclient \
--iface ${SCUT_IFACE} \
--dns ${SCUT_DNS} \
--udp-server ${SCUT_UDP_SERVER} \
--hostname ${SCUT_HOSTNAME} \
-D0
Restart=always
RestartSec=10
StandardOutput=null
StandardError=null
User=harekasa
Group=harekasa

[Install]
WantedBy=multi-user.target

几点说明:

  • User=harekasa 配合第 5 节的 setcap:不以 root 运行,但仍有 CAP_NET_RAW。
  • Restart=always + RestartSec=10:退出后 10 秒自动拉起,等价于断线重连。
  • -D0 和 StandardOutput=null 是第 8、9 节的主角,先埋个伏笔。

注意:若你的用户名不是 harekasa,或者重编译过二进制,需要相应修改 User= 和重新执行 setcap。

7.账号密码不该出现在命令行上

服务第一次跑起来后,systemctl status 显示的命令行让我警觉:

1
... scutclient --username <账号> --password <密码> --iface enp3s0 ...

密码是明文出现在进程命令行(argv)里的。原因在于:服务单元里写的是 --password ${SCUT_PASSWORD},systemd 会把变量展开成真实值再拼进命令行,于是 ps、systemctl status、journalctl 全都能看到。

这里要区分两个东西:命令行的 argv,和进程的环境变量。 scutclient 原本只支持从 --password 参数读密码,所以密码必然落到 argv。要避免它,就得让程序改从环境变量读——而 systemd 的 EnvironmentFile 恰好就是设进程环境变量的。

所以我改了一小段 src/main.c:参数解析结束后,如果命令行没给账号密码,就从环境变量取:

1
2
3
4
if (UserName == NULL)
UserName = getenv("SCUT_USERNAME");
if (Password == NULL)
Password = getenv("SCUT_PASSWORD");

再把服务单元里的 --username / --password 两行删掉。这样密码只存在于进程环境(/proc/<pid>/environ,仅 root 和属主可读),不再出现在任何命令行里。重新编译后记得补一次 setcap(重编译会清掉文件能力)。

必须诚实地说清楚边界:这只是把密码从“人人可见的 argv”挪到了“权限受限的环境变量”,并不等于密码变得绝对安全。密码本身仍以明文存在配置文件中——这是 scutclient 只能命令行传参的设计决定的。此外,调试过程中密码已经在我的终端历史与日志里出现过,所以事后我把密码改掉了。

8.十一 GB 的 syslog 是怎么来的

真正让我下决心治理的,是磁盘。某天检查系统时发现 /var/log/syslog 异常膨胀,一度达到 11 GB;同时 systemd journal 也占到了 500 MB 左右。

排查后原因很直接:scutclient 的日志级别默认是 INF,而它每约 13 秒就会打印一次心跳日志。 登录成功后,日志会稳定地产生这样的内容:

1
2
3
4
[DRCOM][INF]:[Client: Send alive heartbeat.]
[DRCOM][INF]:[Server: MISC_RESPONSE_HEART_BEAT. Send MISC_HEART_BEAT_01.]
[DRCOM][INF]:[Server: MISC_HEART_BEAT_02. Send MISC_HEART_BEAT_03.]
[DRCOM][INF]:[Server: MISC_HEART_BEAT_04. Waiting next heart beat cycle.]

这里需要区分两个容易混为一谈的东西:

  • journal(journald):systemd 的日志系统,服务进程的 stdout/stderr 默认都被它捕获。
  • syslog(rsyslog):传统的日志系统,/var/log/syslog 就是它写出来的。

scutclient 本身并不调用 syslog()。但它同时把日志写进了两处:/tmp/scutclient.log(文件)和 stdout(printf)。作为 systemd 服务运行时,stdout 进入 journal;而在我的系统上,journal 又被转发给 rsyslog,最终落进 /var/log/syslog。一条心跳日志,经过这么一条链路,就同时污染了三个地方。

换句话说,磁盘被塞满不是因为“认证出了问题”,而是因为一个需要 7×24 常驻的程序,默认开启了逐次心跳的 INFO 日志。这是长期常驻服务很容易忽略的一类成本。

9.让心跳静默:一个可选参数的坑

治理思路有两层:让程序自己别输出,以及让服务把输出丢掉。

第一层:降低程序自身的日志级别。 scutclient 的日志级别枚举是 NONE, ERROR, INF, DEBUG, TRACE,通过 -D 选项设置。于是很自然地写成:

1
-D 0

结果毫无效果,心跳照打。查了一下才发现坑在哪:-D 在 getopt_long 里是可选参数(D::),而可选参数必须紧贴在选项后面,写成 -D0 才会被解析;写成 -D 0(中间带空格)会被当成“-D 没有参数”,从而落到 -D 的默认分支——也就是 DEBUG,反而比原来更啰嗦。

正确的写法是:

1
-D0

-D0 把级别设为 NONE,登录成功后所有心跳日志都不再产生,/tmp/scutclient.log 也不再增长。功能(认证与心跳保活)完全不受影响。

第二层:让服务把残留输出丢弃。 main() 顶部还有几行启动横幅,它在解析 -D 参数之前就打出来了,压不掉。所以在服务单元里加上:

1
2
StandardOutput=null
StandardError=null

这样即便有残留输出,也会被丢进 /dev/null,不再进入 journal。

最后清理已经积累的日志:

1
2
sudo journalctl --vacuum-size=50M
truncate -s 0 /tmp/scutclient.log

治理之后,/var/log/syslog 从 11 GB 回落到几 MB 的正常量级。这里也要说明边界:这套做法是“静默”,不是“关闭日志”。如果以后需要排查认证问题,把 -D0 和那两行 StandardOutput 去掉、重启服务,就能用 journalctl -u scutclient -f 看到完整过程。

10.最终结构与清单

路径 作用
/home/harekasa/scutclient/ 源码与构建目录(build/scutclient 为可执行文件)
/etc/scutclient/scutclient.conf 环境变量文件(账号、密码、网口、DNS),600 + root
/etc/systemd/system/scutclient.service 开机自启服务单元
/tmp/scutclient.log 程序自身的日志(静默后不再写入)

最终达到的状态:

  • 以普通用户身份运行,仅依赖二进制的 CAP_NET_RAW 能力;
  • 开机自动启动,退出后自动重启;
  • 账号密码不出现在命令行(ps 不可见);
  • 心跳日志静默,syslog/journal 不再膨胀;
  • 随时可回滚:停用服务、去掉 setcap、清空配置文件即可。

11.总结

这次折腾的门槛其实不高,真正花时间的是把几个被默认混在一起的概念拆开:

  1. 认证(L2)与编址(L3)是两件事。 前者是 EAPoL 帧,后者是静态 IP;not allowed to login using current IP/MAC address 这种报错,往往问题出在后者。
  2. root 与文件能力是两件事。 一个只需要 CAP_NET_RAW 的小工具,用 setcap 比用 sudo 更精确,也顺带避免了日志属主错乱。
  3. argv 与环境变量是两件事。 变量在命令行里会被展开成明文,这是“全局变量”最容易踩的泄露点。
  4. journal 与 syslog 是两件事。 服务的 stdout 先进 journal,再可能被转发到 rsyslog;一个常驻程序的心跳日志,会沿着这条链路同时污染多个位置。

还有一个更朴素的教训:需要长期常驻的服务,日志级别要从一开始就设计好。 scutclient 的默认 INF 级别对人交互使用完全合理,但对 7×24 运行来说,每 13 秒一条心跳日志就是持续不断的磁盘消耗。让它“跑起来”只是第一步,让它“安静地、可控地长期跑”才是真正的部署。

最后仍然要说明:本文的方案依赖我的具体环境(Ubuntu 24.04、NetworkManager、systemd 255),不同学校、不同版本的 Dr.COM 服务器协议细节可能不同,--cli-version、--hash 等参数也可能需要按实际情况调整。真正操作前,请以自己的登记信息和现场日志为准。


署名

  • 实际作者:DeepSeekV41Flash(本文由 AI 生成)
  • 参与编辑:Harekasa(人工编辑与校对)
  • 日期:2026-09-18