暗无天日

=============>DarkSun的个人博客

在Linux上限制儿童使用电脑

作为家长,你可能希望控制孩子使用电脑的时间——比如只能在晚上 6 点到 8 点登录,每天最多用 2 小时,超时后自动踢出。Windows 有专门的家长控制面板,而 Linux 虽然没有这样的集成工具,但它提供的原生机制完全可以实现同样的效果,而且更加灵活。

本文将介绍如何用纯 Linux 原生方案(PAM、cron、iptables 等)实现以下功能:

本文假设系统中已创建一个名为 geo 的受限用户。所有配置需要 root 权限。

限制允许登录的时间段

Linux 中控制用户登录时间的机制是 PAM(Pluggable Authentication Modules,可插拔认证模块)中的 pam_time 模块。

快速配置

首先,在 PAM 配置文件中启用 pam_time.so 。需要修改的文件取决于你使用的登录方式:

  • 控制台登录: /etc/pam.d/login
  • 图形界面登录: /etc/pam.d/lightdm/etc/pam.d/gdm
  • SSH 登录: /etc/pam.d/sshd

在上述文件的 account 部分添加一行:

account required pam_time.so

然后在 /etc/security/time.conf 中添加规则。该文件的格式为:

services;ttys;users;times

各字段含义:

services
服务名, * 表示所有服务
ttys
终端类型, * 表示所有终端
users
用户名
times
时间规则

时间规则的语法比较特别:

  • Wk 表示工作日(周一到周五), Wd 表示周末(周六、周日), Al 表示每天
  • 时间格式为 HHMM-HHMM
  • 前面加 ! 表示取反("除此时段外禁止")

例如,限制 geo 用户只能在每天 18:00 到 20:00 登录:

*;*;geo;Al1800-2000

这行规则的意思是:对所有服务、所有终端、geo 用户,允许每天 18:00-20:00 登录( Al 表示每天都适用)。注意, pam_time 的逻辑是:匹配到的允许登录,未匹配到则拒绝。

验证配置:在非允许时段尝试用 geo 账户登录,应该会看到类似 "Permission denied" 的提示。

原理说明

PAM 是 Linux 的可插拔认证框架,它将认证逻辑从应用程序中解耦出来。PAM 定义了四种模块类型:

auth
验证用户身份(如检查密码)
account
检查账户是否允许访问(如检查账户是否过期、是否在允许的时间段内)
password
管理密码更新
session
管理会话的创建和销毁

pam_time 属于 account 类型。它的工作时机是在用户通过了 auth 阶段的密码验证之后、系统创建会话之前。此时 pam_time 会检查当前时间是否匹配 time.conf 中的规则,如果不匹配则拒绝登录。

限制每日累计使用时长

pam_time 只能控制"什么时候能登录",但无法控制"登录后能用多久"。要实现每日使用时长的累计限制,需要自己动手:记录每次登录和登出的时间,然后定期检查是否超限。

快速配置

整个方案由两部分组成:一个记录登录/登出时间的脚本,和一个定期检查累计时长的脚本。

第一步:记录登录/登出时间

创建 /usr/local/bin/session-logger.sh

#!/bin/bash
# /usr/local/bin/session-logger.sh
# 由 pam_exec 调用,记录用户登录/登出时间

LOGFILE="/var/log/user-sessions.log"
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
USERNAME="$PAM_USER"
TYPE="$PAM_TYPE"

if [ -z "$USERNAME" ]; then
    exit 0
fi

case "$TYPE" in
    open_session)
        echo "LOGIN $TIMESTAMP $USERNAME" >> "$LOGFILE"
        ;;
    close_session)
        echo "LOGOUT $TIMESTAMP $USERNAME" >> "$LOGFILE"
        ;;
esac

exit 0

然后赋予执行权限:

sudo chmod +x /usr/local/bin/session-logger.sh
sudo touch /var/log/user-sessions.log
sudo chmod 600 /var/log/user-sessions.log

在 PAM 配置文件(如 /etc/pam.d/login/etc/pam.d/lightdm )的 session 部分添加:

session optional pam_exec.so /usr/local/bin/session-logger.sh

第二步:检查累计时长

创建 /usr/local/bin/check-usage.sh

#!/bin/bash
# /usr/local/bin/check-usage.sh
# 检查用户当日累计使用时长

LOGFILE="/var/log/user-sessions.log"
TODAY=$(date '+%Y-%m-%d')
MAX_MINUTES=120  # 每天最多使用 120 分钟(2 小时)
TARGET_USER="geo"
LOCKFILE="/var/run/check-usage.lock"

# 使用锁文件防止 cron 重叠执行
exec 9>"$LOCKFILE"
flock -n 9 || exit 0

# 计算今日累计在线分钟数
# 注意:此算法假设 LOGIN/LOGOUT 记录严格按序配对。如果用户同时有多个会话
#(如 SSH + 图形界面),日志可能出现交错序列,导致错误配对。
# 家庭环境通常单会话,可以安全使用。
calculate_usage() {
    local user=$1
    local total_seconds=0
    local login_time=""

    while IFS= read -r line; do
        local action=$(echo "$line" | awk '{print $1}')
        local timestamp=$(echo "$line" | awk '{print $2, $3}')
        local username=$(echo "$line" | awk '{print $4}')

        [ "$username" != "$user" ] && continue

        case "$action" in
            LOGIN)
                login_time="$timestamp"
                ;;
            LOGOUT)
                if [ -n "$login_time" ]; then
                    local login_ts=$(date -d "$login_time" '+%s')
                    local logout_ts=$(date -d "$timestamp" '+%s')
                    total_seconds=$((total_seconds + logout_ts - login_ts))
                    login_time=""
                fi
                ;;
        esac
    done < <(grep "$TODAY" "$LOGFILE" | grep -E "^(LOGIN|LOGOUT)")

    # 如果用户当前在线(有未配对的 LOGIN),累加到现在
    if [ -n "$login_time" ]; then
        local login_ts=$(date -d "$login_time" '+%s')
        local now_ts=$(date '+%s')
        total_seconds=$((total_seconds + now_ts - login_ts))
    fi

    echo $((total_seconds / 60))
}

usage=$(calculate_usage "$TARGET_USER")
remaining=$((MAX_MINUTES - usage))

if [ "$remaining" -le 0 ]; then
    echo "使用时间已到,系统将在 2 分钟后将 $TARGET_USER 踢出!" | wall 2>/dev/null
    sleep 120
    pkill -u "$TARGET_USER"
    sleep 10
    pkill -9 -u "$TARGET_USER"
elif [ "$remaining" -le 5 ]; then
    echo "$TARGET_USER 还有 $remaining 分钟的使用时间" | wall 2>/dev/null
fi

赋予执行权限,并创建 systemd timer 每分钟运行一次:

sudo chmod +x /usr/local/bin/check-usage.sh

创建 systemd service 文件:

[Unit]
Description=Check user daily usage time

[Service]
Type=oneshot
ExecStart=/usr/local/bin/check-usage.sh

创建对应的 timer 文件:

[Unit]
Description=Run check-usage every minute

[Timer]
OnCalendar=*-*-* *:*:00
Persistent=true

[Install]
WantedBy=timers.target

启用 timer:

sudo systemctl daemon-reload
sudo systemctl enable --now check-usage.timer

原理说明

这里用到了两个机制:

PAM session 管理
当用户登录成功后,PAM 会依次调用 session 类型的模块的 open 钩子;当用户登出时,调用 close 钩子。 pam_exec.so 会将钩子类型通过环境变量 PAM_TYPE (值为 openclose )传递给外部脚本,同时通过 PAM_USER 传递用户名。
systemd timer 定时任务
OnCalendar=*-*-* *:*:00 表示每分钟整点执行一次。systemd timer 是 cron 的现代替代方案,几乎所有使用 systemd 的 Linux 发行版都内置支持。它的优势包括:自动记录日志、支持依赖关系、失败后可自动重试。这个频率足够及时地检测到超时,同时不会给系统带来明显负担。

超时警告并踢出

上一节的脚本已经包含了基本的警告和踢出逻辑。这里再详细说明一下各个踢出手段的区别。

多级警告策略

实际使用中,建议采用多级警告,给孩子一个心理准备的过程:

以下是一个示例,展示如何替换脚本中的警告逻辑:

# 在 check-usage.sh 中替换警告逻辑
usage=$(calculate_usage "$TARGET_USER")
remaining=$((MAX_MINUTES - usage))

if [ "$remaining" -le 0 ]; then
    # 已超时,通知并踢出
    echo "$TARGET_USER,今日使用时间已到!" | wall 2>/dev/null
    sleep 120
    pkill -u "$TARGET_USER"
elif [ "$remaining" -le 2 ]; then
    # 最后 2 分钟警告
    echo "$TARGET_USER,还有 $remaining 分钟,请保存工作!" | wall 2>/dev/null
elif [ "$remaining" -le 5 ]; then
    # 5 分钟提醒
    echo "$TARGET_USER,还有 $remaining 分钟的使用时间" | wall 2>/dev/null
fi

踢出用户的不同方式

pkill -u geo
向 geo 用户的所有进程发送 SIGTERM 信号。这是最简单的方式,但如果有进程忽略了 SIGTERM,可能无法完全清理。可以追加 pkill -9 -u geo 来强制结束。
loginctl terminate-session SESSION_ID
通过 systemd-logind 来终止指定的登录会话。这比 pkill 更精确,因为它能正确地清理会话资源。查看当前会话列表可以用 loginctl list-sessions
shutdown -h +2
2 分钟后关闭整个系统。这是最彻底的方式,但会影响所有用户。如果电脑是孩子独占使用的,这也是个不错的选择。

原理说明

wall 命令的工作原理是向系统中所有终端设备( /dev/pts/*/dev/tty* )写入消息。只有拥有终端的登录用户才能看到消息。

pkill 通过匹配进程的 UID 来选择目标进程,先发送 SIGTERM(信号 15),允许进程优雅退出。如果进程不响应,可以发送 SIGKILL(信号 9)强制终止。

loginctl 是 systemd-logind 的命令行接口。logind 作为 systemd 的用户登录管理器,跟踪系统中所有的登录会话(包括图形和终端会话)。 terminate-session 会向会话中的所有进程发送终止信号,并清理会话资源。

限制可访问网站

在 HTTPS 普及的今天,我们无法在不安装额外证书的情况下检查 HTTP 请求的内容。因此 Linux 原生方案只能在 IP 和域名级别进行过滤。下面介绍两种方法。

方法一:DNS 级过滤

通过配置本地 DNS 服务器 dnsmasq ,将不希望访问的域名解析到无效地址。

首先安装 dnsmasq

sudo pacman -S dnsmasq  # Arch Linux

创建黑名单文件 /etc/blocked-hosts

# /etc/blocked-hosts
# 每行一个要屏蔽的域名(标准 hosts 格式)
127.0.0.1 example-bad-site.com
127.0.0.1 another-bad-site.com

/etc/dnsmasq.conf 中添加配置:

# 将黑名单中的域名解析到 127.0.0.1
addn-hosts=/etc/blocked-hosts

然后启动 dnsmasq 并将系统 DNS 指向本地:

sudo systemctl enable --now dnsmasq
# 修改 /etc/resolv.conf
echo "nameserver 127.0.0.1" | sudo tee /etc/resolv.conf

这种方法的缺点是:只要孩子知道如何修改 DNS 设置,就能轻松绕过。

方法二:iptables 按用户限制

iptablesowner 模块可以根据进程的 UID 来匹配网络流量, time 模块可以按时间段匹配。两者结合,可以限制特定用户在特定时间段内的网络访问。

# 允许 geo 用户在 18:00-20:00 访问网络
sudo iptables -A OUTPUT -m owner --uid-owner geo -m time --timestart 18:00 --timestop 20:00 -j ACCEPT

# 禁止 geo 用户在其他时间访问网络
sudo iptables -A OUTPUT -m owner --uid-owner geo -j DROP

注意: --timestop 20:00 实际上不包含 20:00 整点,规则有效时间为 18:00:00 到 19:59:59。如果需要包含 20:00 整点,可以设置为 =--timestop 20:01=。

要持久化这些规则,可以将它们写入一个规则文件,然后在启动时自动恢复:

# /etc/iptables/parental-control.rules
*filter
:OUTPUT ACCEPT [0:0]
-A OUTPUT -m owner --uid-owner geo -m time --timestart 18:00 --timestop 20:00 -j ACCEPT
-A OUTPUT -m owner --uid-owner geo -j DROP
COMMIT

恢复规则:

sudo iptables-restore < /etc/iptables/parental-control.rules

原理说明

Linux 的网络过滤由 Netfilter 框架实现。Netfilter 在网络协议栈中设置了五个钩子点:

PREROUTING
数据包进入路由之前
INPUT
数据包发往本机进程之前
FORWARD
数据包转发到其他接口之前
OUTPUT
本机进程发出的数据包
POSTROUTING
数据包发出之前

我们使用的是 OUTPUT 链,它处理本机进程发出的所有数据包。 owner 模块通过检查数据包对应的 socket 的 UID 来匹配, time 模块通过系统时间来匹配。规则从上到下依次匹配,匹配到后就执行指定的动作( ACCEPTDROP )。

限制可运行程序

不想让孩子玩某些游戏或使用某些软件?最直接的方法是通过 Linux 的权限系统来限制。

方法一:文件权限控制

Linux 的每个文件都有三组权限:所有者(owner)、所属组(group)、其他人(others)。我们可以利用这个机制来限制特定用户执行特定程序。

假设要禁止 geo 用户运行 steam

# 创建受限组
sudo groupadd restricted

# 将 steam 的组改为 restricted,权限设为 750
sudo chgrp restricted /usr/bin/steam
sudo chmod 750 /usr/bin/steam

# 确保 geo 不在 restricted 组中
sudo gpasswd -d geo restricted 2>/dev/null || true

这样,只有 root 用户和 restricted 组的成员可以运行 steam ,而 geo 用户会得到 "Permission denied" 的错误。

对于需要限制的每个程序,重复上述步骤即可。也可以写一个脚本来批量处理:

#!/bin/bash
# /usr/local/bin/restrict-programs.sh
# 限制指定程序只能由 restricted 组执行

PROGRAMS="/usr/bin/steam /usr/bin/wine /usr/bin/discord"

for prog in $PROGRAMS; do
    if [ -f "$prog" ]; then
        sudo chgrp restricted "$prog"
        sudo chmod 750 "$prog"
        echo "Restricted: $prog"
    fi
done

需要注意:当软件包更新时,权限可能会被重置。可以将这个脚本放在 systemd 的 timer 或 pacman 的 hook 中定期执行。

方法二:AppArmor

AppArmor 提供了更强大的强制访问控制(MAC)。与文件权限不同,AppArmor 的策略无法被用户(包括文件所有者)修改。

在 Arch Linux 上安装并启用 AppArmor:

sudo pacman -S apparmor
sudo systemctl enable --now apparmor

AppArmor 通过 profile 来定义程序的访问权限。需要注意的是,AppArmor 的 profile 是绑定到程序的,不能直接按用户限制。要实现按用户限制,需要配合 pam_apparmor 模块,在用户登录时为不同用户加载不同的 profile。

首先,创建一个限制性的 profile:

# /etc/apparmor.d/restrict-geo-steam
abi <abi/4.0>,
include <tunables/global>

/usr/bin/steam {
    include <abstractions/base>

    # 只允许特定用户组执行
    # 注意:AppArmor 本身不直接支持按用户过滤,
    # owner 关键字匹配进程所有者(即运行 steam 的用户)
    owner /usr/bin/steam rx,
}

如果需要更精确的按用户控制,建议结合文件权限方法使用——将程序的组设为受限组,然后在 AppArmor profile 中限制该组的访问。或者使用 pam_apparmor 在用户登录时动态加载不同的 profile。

加载 profile:

sudo apparmor_parser -r /etc/apparmor.d/restrict-geo-steam

原理说明

DAC(自主访问控制)
Linux 默认的权限模型。每个文件有三组权限位(读 r、写 w、执行 x),分别对应所有者、所属组、其他人。文件的所有者可以自主修改文件的权限。"自主"的含义就在于此——权限由所有者决定。
MAC(强制访问控制)
AppArmor 实现的权限模型。策略由系统管理员定义,普通用户无法绕过或修改。AppArmor 使用基于路径的匹配(而非 SELinux 的基于标签的匹配),配置相对简单。

总结

本文介绍了五种用纯 Linux 原生机制限制儿童使用电脑的方法:

限制登录时段
使用 PAM 的 pam_time 模块,通过 /etc/security/time.conf 配置
限制使用时长
使用 pam_exec 记录登录/登出时间,配合 cron 定时检查
超时踢出
使用 wall 发送警告, pkillloginctl 踢出用户
限制网站访问
使用 dnsmasq 做 DNS 过滤,或 iptables 按用户和时间限制网络访问
限制可运行程序
使用文件权限或 AppArmor 控制程序执行

这些方案组合使用,可以构建一个相当完整的家长控制系统。但需要注意以下几点:

  • 这些方案需要 root 权限 来配置。如果你的孩子拥有 sudo 权限,ta 理论上可以绕过所有限制。
  • pam_time 只能控制新登录,无法踢出已经在线的用户。需要配合时长检查脚本来处理。
  • iptables 的 owner 模块只能匹配本机进程发出的流量,对于通过代理或 VPN 的流量可能无效。

最后,技术手段只是辅助。与孩子建立良好的沟通和信任,比任何软件限制都更有效。

linux和它的小伙伴