本文完整阅读约需 29 分钟,如时间较长请考虑收藏后慢慢阅读~

motd,全称Message Of The Day,是Linux中发送问候消息的功能。其消息内容通常存储在/etc/motd中,在用户登录时显示于终端提示符的上方。静态的motd可以用来显示的内容通常有限,但如果我们想要显示一些更加灵活的消息(例如系统负载、内存使用情况、磁盘使用量等),静态的motd文件无法满足我们的需求。本文将为读者介绍motd的实现原理,并使用多种方法实现动态的motd消息显示。

0x01 motd的历史

motd,全称Message Of The Day,其历史几乎与操作系统的历史一样长:1965年,在第一款得到广泛应用的大型机System/360上,其OS/360操作系统就会在加载完成后向打印机输出一段自检消息。但这只能算是『问候消息』,却并不能定制,其功能与现在的motd相差甚远。

真正的motd脱胎于多用户操作系统,用于管理员向所有登录终端的用户批量发送消息,如:

通知:系统将于1970年1月1日开始维护,以庆祝Unix元年 —— Benjamin Chris

当然,上面的内容是我杜撰的,但这一概念其实远早于Unix:在1965年10月27日于纽约举行的第二届全国工程信息研讨会上,会议记录中写着这一段话:

“当用户坐在他的办公桌(终端)上时,他会看到『今日消息』。这是根据他的特定兴趣量身定制的,也是系统已知的。”

这是有记载对motd功能与作用最早的描述,随后在Maurice Wilkes于1968年撰写的《Time-sharing Computer Systems》中,也有类似的内容,但相对来说更为具体:

(登录之后)通常会显示『今日消息』,这是一个让用户尽快了解系统新特性或近期变更的功能。

而目前所知最早将这一概念用于实际的操作系统是1961年的 CTSS 操作系统,这是第一款为多用户使用而设计的操作系统,引入motd机制也就不足为奇。耐人寻味的是,CTSS中一段知名的轶事(现在经常被作为计算机系统安全问题得到重视的起源,如Net Insecurity: Then and Now (1969-1998))就与motd有关:

最经典的motd故事就来源于CTSS操作系统。CTSS操作系统会在用户目录下建立一个名字永远相同的文件,用于保存文件之前的临时存储,而motd文件是只有管理员才能编辑的。用户的密码则存储在密码文件中,通过修改文件的方式来维护用户信息。
悲剧的是,很多人都有管理员账户的权限,这就造成了某天一个管理员在修改账户信息,而另一个管理员在发布motd消息,这直接造成所有用户的账号密码(机密)被作为motd(公开)显示在所有登录用户的屏幕上。
好在第一个看到这个问题发生的用户马上写了一行汇编命令来让计算机停止工作:

HERE  TRA *HERE

这里的星号指的是相对寻址。

原文可以在这个邮件列表:[TUHS] Origin of the MOTD file?看到,这里TRA汇编指令的含义为跳转,类似x86下的JMP指令,可以从这段代码直观看出:32K 709/7090 FORTRAN ASSEMBLY PROGRAM – MACRO-FAP

上面的代码含义为从HERE所在内存地址跳转到HERE所在内存地址,不断循环直到系统卡死,以达到『让系统停下来』的目的,至于为什么不关掉系统电源?别忘了这是大型机,用户并不在电脑面前。具体可以看看我写的这篇文章:[源码级解析] 漫游源码、纵观Linux&Unix历史,探索shutdown、poweroff、halt与reboot的前世今生

作为CTSS的子嗣(但后者在商业上更成功),Multics操作系统继承了CTSS的绝大多数功能,也就顺理成章的包括了motd;而后来的Unix系统又继承了Multics操作系统、Linux系统又继承了Unix操作系统……就这样,motd以/etc/motd这个文件的形式,伴随着计算机与操作系统的发展,一路走到了现在。

0x02 motd的原理及其局限性

上面简单的介绍了一下motd悠久且有趣的历史,读者们应该能发现,这几十年来motd的形式几乎没有任何变化——从CTSS到最新的Linux,motd一直是以『文件』的形式存在,甚至连脚本都不是。

实际上,在历史演变过程中的确存在过一些变化:例如Multics曾使用print_motd的形式,希望将motd变成一种服务,能够通过这一命令来阅读多篇motd,并能区分已读与未读。这篇文章很具体的讲解了Multics曾计划将motd变成『新闻阅读软件』的尝试:On the MOTD。可惜这一创意随着更加简洁的Unix出现,被/etc/motd这样一个纯文本文件所替代,从而消失在历史的长河中。

在最新的Linux系统中,/etc/motd文件被pam_motd模块所调用,而pam_motd模块会在用户登录时被加载,它所做的事情就是读取motd文件,并将其显示在终端上。

而这样一个文本文件,很显然无法完美满足我们对于motd的需求。在笔者看来,一个完美的motd应该至少具有以下功能:

  1. 能显示系统相关信息,便于用户及时发现系统异常或根据当前负载安排工作
  2. 能显示管理员发布的提醒或问候消息
  3. 能异步运行(即在后台自动生成,在用户登录时显示)
  4. 能显示安全相关信息(如上次登录时间,最近登录失败次数,最近登录IP地址等)
  5. 建议进行的操作(如安全更新、系统维护)

0x03 Ubuntu下饱受争议的motd-news

在上一节,我们列举了五种motd应该支持的功能,Ubuntu的开发者们和笔者有一样的想法,于是他们在Canonical的Landscape 项目中(一个批量管理Ubuntu服务器的平台)引入了update-motd这样一个框架(或称之为服务),并在Ubuntu 10.04中首次预装。

最初版本中,这一框架的核心是update-motd服务,它会按数字顺序(使用run-parts实现)执行/etc/update-motd.d/中的脚本文件,并将脚本结果输出到/var/run/motd中,而/etc/motd则是它的软链接。这样一来,在用户登录时就能够通过pam_motd模块读取/etc/motd文件,并将其显示在终端里。

但在Ubuntu 18.04中,Caronical对其做出了一些修改,去掉了/var/run/motd文件,转而使用/run/motd.dynamic文件代替之,这一文件同样被软链接到/etc/motd。与此同时,这一版本的Ubuntu还引入了『臭名昭著』的广告,如图所示:

需要特别注意的是:广告内容并非是通过手动更新(如使用apt upgrade)的方式下发,而是定时请求Ubuntu的服务器。这不由得让人感到怀疑:Ubuntu是否借助这一方式变相窃取了Linux用户的隐私信息?

结果是肯定的。我们打开/etc/update-motd.d/50-motd-news,会在该脚本中找到这行命令:

wget --timeout "$WAIT" -U "$USER_AGENT" -O- --content-on-error "$u" >"$NEWS" 2>"$ERR" || result=$?

这里的$USER_AGENT同样也在脚本中被定义:

# Piece together the user agent
USER_AGENT="wget/$wget_ver $lsb $platform $cpu cloud_id/$cloud_id"

$USER_AGENT变量的定义,我们可以很显然看出,该脚本将wget版本号、操作系统版本号、当前系统架构、CPU型号与使用cloud-init对服务器进行集群管理后的cloud_id放在了User-Agent中,而这些数据最终会被发送到https://motd.ubuntu.com

如果我们使用tcpdumpupdate-motd的网络请求进行跟踪(需要先修改脚本,将域名修改为任意一个http地址,否则获取到的是加密后数据,无法阅读),我们会得到如下结果:

tcpdump -vv host motd.ubuntu.com

所抓取到的请求如下:

[...]
    ubuntu-s-1vcpu-1gb-fra1-01.32954 > ec2-54-171-230-55.eu-west-1.compute.amazonaws.com.http: Flags [P.], cksum 0xaf4b (incorrect -> 0x8276), seq 1:257, ack 1, win 502, options [nop,nop,TS val 2868376694 ecr 4019067513], length 256: HTTP, length: 256
    GET / HTTP/1.1
    User-Agent: wget/1.20.3-1ubuntu1 Ubuntu/20.04.1/LTS GNU/Linux/5.4.0-45-generic/x86_64 Intel(R)/Xeon(R)/CPU/E5-2650/v4/@/2.20GHz cloud_id/digitalocean
    Accept: */*
    Accept-Encoding: identity
    Host: motd.ubuntu.com
    Connection: Keep-Alive
[...]

可以看到,发送出去的HTTP请求包含了许多的隐私内容。

如果说上传操作系统版本号与CPU型号是为了在操作系统或CPU固件出现漏洞(如Intel此前爆出的幽灵漏洞)时及时提示用户,那么上传cloud_id的用意又是什么呢?

笔者做此演示的时候使用的是DigitalOcean的服务器,而这一信息居然在用户完全不知情的情况下被上传给Caronical!更别提伴随着请求的发出,用户的IP地址也随即暴露。

在自由软件中打广告这种事情已经有很多年历史,如Vim知名的『帮助可怜的乌干达儿童』或core-js安装时的求职广告。这些广告在某种程度上被认为是合理的,毕竟开发一款软件需要付出大量精力甚至财力。

的确,自由软件不等于免费软件,但当这一行为逾越可忍受的界限时,它就已从『自由软件』逐渐向『专有软件』偏移。在绝大多数用户不知情情况下上传隐私数据,这样的操作系统从行为上来,明显是在滥用广大用户对开源/自由软件的信任。Ubuntu是这样、Chromium是这样、Android也是这样……这些打着自由软件之名,却以提供方便作为借口剥夺用户自由的行为,或以开源为幌子向社区吸血,随意上传用户数据并美其名曰『遥测』的行径,某种程度上是否已违背其作为自由软件的初衷?

GPL在提供给用户自由的同时,也提供给开发者『自由使用GPL协议』的自由,但近些年来部分『自由软件』的现状标明:并不是使用GPL的软件都一定自由。一个软件是否真正自由,需要我们多擦亮眼睛,尤其许多大企业正使用自己在技术上的垄断地位去曲解自由软件的定义。

0x04 Linux下实现动态motd消息显示

前面铺垫了不少内容,读者们应该已经对motd有了较为深刻的认知。这一节我们将尝试使用多种方法,自己动手构建一个属于方便直观的、属于自己的、完全自由且可控制的motd

以下内容使用Shell编写,如果你不熟悉Shell语言,可以参考这里:Shell 教程 – 菜鸟教程。如果你偏好Python、PHP、Node.js、Perl等脚本语言,甚至C、Golang、Rust等编译型语言也没关系,可以在当前系统被执行即可,操作同理。

方法1:启动脚本

这一方法是最为直接的,即在用户登录时执行一段脚本,但其优点其实与缺点一样明显:如果所执行的脚本较为复杂,或在你知情的情况下涉及网络请求,那么用户在登录时需要像登录PowerShell一样,等待很久(有时候甚至要10秒以上)才能显示出提示符,因此这一方法适用于显示较为简单的信息。

这里我们来写一段简单脚本,内容是在用户登录时向用户问好:

#!/bin/bash

TIME=$(date "+%H")

if [ $TIME -lt 12 ]; then
    GREETING="早上好"
elif [ $TIME -lt 18 ]; then
    GREETING="下午好"
else
    GREETING="晚上好"
fi

echo "$GREETING,$(whoami)"

将其保存为任何文件名,这里我们将其保存为/home/scripts/greeting.sh,并通过chmod +x /home/scripts/greeting.sh将其赋予可执行权限。

接下来我们找到所使用Shell的启动文件,这里以zsh为例(bash在~/.bash_profile/etc/profile),我们编辑~/.zshrc,并在最后一行加入/home/scripts/greeting.sh。保存后执行source ~/.zshrc,就可以在登录时看到我们所设置的脚本:

方法2:后台服务

上面提到过,如果在用户登录时执行命令会导致登录速度缓慢,那么对于实时性不那么强的提示内容(比如上面的问候,一天只会变换三次),我们也可以将其改造为后台服务。

这一方案的思路是使用crontab定时执行脚本,并将其结果输出到/etc/motd,优点是无侵入性(不需要修改每个用户的profile了),缺点则是:所有用户将看到一模一样的内容。

由于/etc/motd适用于所有用户,这里我们换用其他的脚本:

#!/bin/bash

LOAD1=`cat /proc/loadavg | awk {'print $1'}`
LOAD5=`cat /proc/loadavg | awk {'print $2'}`
LOAD15=`cat /proc/loadavg | awk {'print $3'}`

echo "当前系统负载:$LOAD1 - $LOAD5 - $LOAD15 (1-5-15 min)"

这里我们将其保存到/home/scripts/load.sh,为其设置可执行权限,然后编辑/etc/crontab,输入如下内容:

  0  *  *  *  * root       /home/scripts/load.sh > /etc/motd

这样就能以root用户权限,每小时执行一次/home/scripts/load.sh,并将其结果输出到/etc/motd中。这时候当用户登录,pam_motd.so会从/etc/motd读取文本,并将其显示在提示符之前。

方法3:直接从Ubuntu移植

也许上面两种方法都无法满足你,你需要更加集成的方案(类似Ubuntu那样),但又因为各种原因无法直接使用Ubuntu,那么你也可以直接从Ubuntu移植update-motd服务到你所使用的发行版。

由于发行版之间差异较大,此处无法一一说明,感兴趣的读者可以从gdubicki/centos-pam-with-update-motd这个仓库寻找思路。这是CentOS 7下的update-motd实现,主要做了以下几项工作:

  1. run-parts移植到CentOS
  2. 修改pam_motd的配置(使用Patch的方式),使其行为与Ubuntu的update-motd一样
  3. 将其封装为rpm包,便于用户安装,且由于使用Patch的方式修改配置,卸载后不会有残留

分享一个实用的motd脚本

这里笔者分享一下个人觉得非常实用的motd脚本,适用于CentOS 7,其他发行版可以在此基础上自行修改使其兼容你所使用的发行版:

脚本地址为:https://gist.github.com/Equim-chan/37319f5d157de3131cb50eb5584536b1

效果如图所示:

附:如何设计出更好看的motd

一个设计精美、信息详尽的motd可以极大程度提升用户体验,这就势必涉及到『如何让motd更好看』的问题。篇幅所限,笔者不展开描述,对此感兴趣的读者可以自行尝试以下工具:

  • figlet: 将英文字母转换为ASCII字符画
  • jp2a: 将图片转换为ASCII字符画
  • asciitable 输出好看的ASCII表格

笔者也会在未来撰写一篇关于ASCII文本的文章,对其进行展开介绍,感兴趣的读者可以关注后续的分享。