Unix 系統資料檔案那些事兒

2023-01-09 12:00:54

前言

Unix like 系統和 windows 的最大區別就是有一套標準的系統資訊資料檔案,一般存放在 /etc/ 目錄下,並且提供了一組近似的介面存取和查詢資訊,這些基礎設施讓系統管理看起來井井有條,下面就來盤點一下。

總覽

下面這個表列出了 unix 系統常用的幾種資料檔案:

資訊類別 檔案路徑 結構 查詢 遍歷
口令檔案 /etc/passwd passwd getpwnam / getpwuid setpwent / getpwent / endpwent
陰影口令 /etc/shadow spwd getspnam setspent / getspent / endspent
組檔案 /etc/group group getgrname / getgrgid setgrent / getgrent / endgrent
主機 /etc/hosts hostent gethostbyname / gethostbyaddr sethostnet / gethostent / endhostent
網路 /etc/networks netent getnetbyname / getnetbyaddr setnetent / getnetent / endnetent
協定 /etc/protocols protoent getprotobyname / getprotobynumber setprotoent / getprotoent / endprotoent
服務 /etc/services servent getservbyname / getservbyport setservent / getservent / endservent
使用者登入 /var/run/utmp /var/log/wtmp utmp getutid / getutline setutent / getutent / endutent

從表中可以看到不論是查詢還是遍歷,介面具有某種一致性:

  • 查詢介面遵循:getxxname / getxxbyname / getxxbyxx,name、xid 與 by 後面的關鍵字為 key,查詢成功返回結構體指標,失敗返回 NULL;
  • 遍歷介面遵循:setxxent / getxxent / endxxent,其中:
    • set 用於 rewind 到檔案開始,避免之前的呼叫移動遍歷指標
    • get 第一次呼叫時開啟檔案,之後從上次遍歷的位置向下遍歷,直到結尾返回 NULL
    • end 用於明確關閉檔案

有了上面的鋪墊,下面分類來說明一下。

口令檔案

在 CentOS 上 struct passwd 的定義位於 <pwd.h> 檔案中:

/* The passwd structure.  */
struct passwd
{
  char *pw_name;                /* Username.  */
  char *pw_passwd;              /* Password.  */
  __uid_t pw_uid;               /* User ID.  */
  __gid_t pw_gid;               /* Group ID.  */
  char *pw_gecos;               /* Real name.  */
  char *pw_dir;                 /* Home directory.  */
  char *pw_shell;               /* Shell program.  */
};

其中 POSIX.1 標準只定義了其中 5 個:pw_name / pw_uid / pw_gid / pw_dir / pw_shell,大多數平臺至少和 linux 一樣包含了 7 個欄位,有的甚至包含 10 個,例如 MacOS:

struct passwd {
	char	*pw_name;		/* user name */
	char	*pw_passwd;		/* encrypted password */
	uid_t	pw_uid;			/* user uid */
	gid_t	pw_gid;			/* user gid */
	__darwin_time_t pw_change;		/* password change time */
	char	*pw_class;		/* user access class */
	char	*pw_gecos;		/* Honeywell login info */
	char	*pw_dir;		/* home directory */
	char	*pw_shell;		/* default shell */
	__darwin_time_t pw_expire;		/* account expiration */
};

多了 pw_class / pw_change / pw_expire。而 linux 中這些資訊是儲存在陰影口令檔案中的,下一節再對它們進行說明。

注意 MacOS 中的 pwd.h 不位於 /usr/include 目錄,可以使用以下命令定位系統標頭檔案路徑:

> gcc -v -E -x c++ -                                                           
Apple clang version 12.0.5 (clang-1205.0.22.11)
Target: x86_64-apple-darwin20.6.0
Thread model: posix
InstalledDir: /Library/Developer/CommandLineTools/usr/bin
 "/Library/Developer/CommandLineTools/usr/bin/clang" -cc1 -triple x86_64-apple-macosx11.0.0 -Wdeprecated-objc-isa-usage -Werror=deprecated-objc-isa-usage -Werror=implicit-function-declaration -E -disable-free -disable-llvm-verifier -discard-value-names -main-file-name - -mrelocation-model pic -pic-level 2 -mframe-pointer=all -fno-strict-return -fno-rounding-math -munwind-tables -target-sdk-version=12.1 -fvisibility-inlines-hidden-static-local-var -target-cpu penryn -debugger-tuning=lldb -target-linker-version 650.9 -v -resource-dir /Library/Developer/CommandLineTools/usr/lib/clang/12.0.5 -isysroot /Library/Developer/CommandLineTools/SDKs/MacOSX12.1.sdk -I/usr/local/include -stdlib=libc++ -internal-isystem /Library/Developer/CommandLineTools/SDKs/MacOSX12.1.sdk/usr/include/c++/v1 -internal-isystem /Library/Developer/CommandLineTools/SDKs/MacOSX12.1.sdk/usr/local/include -internal-isystem /Library/Developer/CommandLineTools/usr/lib/clang/12.0.5/include -internal-externc-isystem /Library/Developer/CommandLineTools/SDKs/MacOSX12.1.sdk/usr/include -internal-externc-isystem /Library/Developer/CommandLineTools/usr/include -Wno-reorder-init-list -Wno-implicit-int-float-conversion -Wno-c99-designator -Wno-final-dtor-non-final-class -Wno-extra-semi-stmt -Wno-misleading-indentation -Wno-quoted-include-in-framework-header -Wno-implicit-fallthrough -Wno-enum-enum-conversion -Wno-enum-float-conversion -Wno-elaborated-enum-base -fdeprecated-macro -fdebug-compilation-dir /Users/yunhai01/code/cnblogs -ferror-limit 19 -stack-protector 1 -fstack-check -mdarwin-stkchk-strong-link -fblocks -fencode-extended-block-signature -fregister-global-dtors-with-atexit -fgnuc-version=4.2.1 -fcxx-exceptions -fexceptions -fmax-type-align=16 -fcommon -fcolor-diagnostics -clang-vendor-feature=+disableNonDependentMemberExprInCurrentInstantiation -fno-odr-hash-protocols -mllvm -disable-aligned-alloc-awareness=1 -o - -x c++ -
clang -cc1 version 12.0.5 (clang-1205.0.22.11) default target x86_64-apple-darwin20.6.0
ignoring nonexistent directory "/Library/Developer/CommandLineTools/SDKs/MacOSX12.1.sdk/usr/local/include"
ignoring nonexistent directory "/Library/Developer/CommandLineTools/SDKs/MacOSX12.1.sdk/Library/Frameworks"
#include "..." search starts here:
#include <...> search starts here:
 /usr/local/include
 /Library/Developer/CommandLineTools/SDKs/MacOSX12.1.sdk/usr/include/c++/v1
 /Library/Developer/CommandLineTools/usr/lib/clang/12.0.5/include
 /Library/Developer/CommandLineTools/SDKs/MacOSX12.1.sdk/usr/include
 /Library/Developer/CommandLineTools/usr/include
 /Library/Developer/CommandLineTools/SDKs/MacOSX12.1.sdk/System/Library/Frameworks (framework directory)
End of search list.
^C

在 #include <...> search starts here 後的第一個包含 MacOS 版本號的 usr/include 的目錄就是,這裡是第三行:/Library/Developer/CommandLineTools/SDKs/MacOSX12.1.sdk/usr/include。

passwd 結構體的各個欄位和資料檔案中的欄位是一一對應的,在 CentOS 上有以下的檔案內容:

> cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/spool/mail:/sbin/nologin
operator:x:11:0:operator:/root:/sbin/nologin
games:x:12:100:games:/usr/games:/sbin/nologin
ftp:x:14:50:FTP User:/:/sbin/nologin
nobody:x:99:99:Nobody:/:/sbin/nologin
systemd-network:x:192:192:systemd Network Management:/:/sbin/nologin
dbus:x:81:81:System message bus:/:/sbin/nologin
polkitd:x:999:998:User for polkitd:/:/sbin/nologin
libstoragemgmt:x:998:997:daemon account for libstoragemgmt:/var/run/lsm:/sbin/nologin
abrt:x:173:173::/etc/abrt:/sbin/nologin
rpc:x:32:32:Rpcbind Daemon:/var/lib/rpcbind:/sbin/nologin
sshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologin
postfix:x:89:89::/var/spool/postfix:/sbin/nologin
ntp:x:38:38::/etc/ntp:/sbin/nologin
chrony:x:997:995::/var/lib/chrony:/sbin/nologin
tcpdump:x:72:72::/:/sbin/nologin
work:x:1000:1000::/home/work:/bin/bash
centos:x:1001:1002:Cloud User:/home/centos:/bin/bash

欄位以冒號分隔,分別對應著 pw_name / pw_passwd / pw_uid / pw_gid / pw_gecos / pw_dir / pw_shell 欄位,其中:

  • pw_name 是使用者名稱。nobody 表示任何人都可以存取的賬戶,但只能存取 other 組設定許可權的檔案
  • pw_passwd 是加密後的口令,因安全問題已轉移到陰影口令檔案中,後面再說
  • pw_getcos 是 real name,放一些解釋性的文字,可以為空
  • pw_dir 是初始目錄,login 後所在的目錄
  • pw_shell 是啟動 shell,可以指定一些特殊的 shell 來禁止使用者登入

nobody

在 CentOS 上,這個賬戶的使用者 ID 和組 ID 都是 99,不提供任何特權;

在 Ubuntu 上這個值變為 65534:

nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin

在 MacOS 上這個值變為 -2:

nobody:*:-2:-2:Unprivileged User:/var/empty:/usr/bin/false

nologin

pw_shell 如果指定以下程式,則表示禁止使用該賬戶登入系統:

  • /sbin/nologin
  • /usr/bin/false
  • /usr/bin/true
  • /dev/null
  • ……

上面的例子使用的是 nologin,mac 上使用 false 比較多一些。對比一下,使用 /sbin/nologin 可讀性較優,登入時會列印一行提示資訊:

This account is currently not available.

其次是 /dev/null:

su: failed to execute /dev/null: Permission denied

true / false 不返回任何資訊,賬戶也不會切換。

空密碼

pw_passwd 域在 CentOS 上永遠保持 x,即使賬戶的密碼為空也是如此,先來看看如何在 linux 建立空密碼的賬戶:

> sudo useradd mayun -d /home/mayun -m
> sudo passwd -d mayun
> sudo passwd -S mayun
mayun NP 2022-10-30 1 99999 7 -1 (Empty password.)
> su mayun

並不像一些人想象的,useradd 不給 -p 引數就是空密碼,此時新建立的賬號無法登入,需要使用 passwd 設定密碼後才可以。這裡使用 passwd -d 選項刪除賬戶密碼,並通過 -S 選項驗證 (Empty password.)。另外 useradd 中的 -d 和 -m 引數也是必需的 (-d 指定 pw_dir,-m 表示立即建立),不然在 Ubuntu 圖形介面無法登入。檢視 passwd 檔案內容,增加了一行:

mayun:x:1002:1003::/home/mayun:/bin/bash

可見 pw_passwd 域仍為 'x',那空密碼在哪裡體現呢?請參考陰影口令一節。

ssh 免密登入

空密碼的賬號無法通過 ssh 登入:

> ssh [email protected]
[email protected]'s password: 
Permission denied, please try again.

因為這裡 ssh 要求必需輸入密碼。可通過設定 ssh key 來實現免密登入,主要分以下幾步。

1. 建立專門用於 ssh 免密登入的金鑰對

> ssh-keygen -b 4096 -t rsa 
Generating public/private rsa key pair.
Enter file in which to save the key (/Users/yunhai01/.ssh/id_rsa): /Users/yunhai01/.ssh/id_rsa_ssh
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /Users/yunhai01/.ssh/id_rsa_ssh.
Your public key has been saved in /Users/yunhai01/.ssh/id_rsa_ssh.pub.
The key fingerprint is:
SHA256:2M+iLH6QvLqETuJ+E88Jr5DrMKMUObZ/Y/f3ze1o9h0 yunhai01@bogon
The key's randomart image is:
+---[RSA 4096]----+
|                 |
|                 |
|                 |
|  .    o         |
| = . .. S        |
|..=o+    o       |
|**. *o. . o    E |
|O++ooX.o . .  =.+|
|+*+**o= ... .+.==|
+----[SHA256]-----+

注意這裡沒有使用預設檔名 id_rsa,因為已經有存取 github 程式碼倉庫的其它金鑰存在,這裡命名為 id_rsa_ssh 以做區分。

2. 將金鑰同步到要登入的遠端機器

> ssh-copy-id -i .ssh/id_ras_ssh [email protected]
/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/Users/yunhai01/.ssh/id_rsa_ssh.pub"
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
[email protected]'s password: 

Number of key(s) added:        1

Now try logging into the machine, with:   "ssh '[email protected]'"
and check to make sure that only the key(s) you wanted were added.

注意這一步需要使用者密碼,所以必需暫時為 mayun 賬戶建立密碼,稍後 ssh 連線成功後可以再刪除。同步後的公鑰將記錄在遠端賬戶 $HOME/.ssh/authorized_keys 檔案中,用於稍後 sshd 的連線校驗。

3. 指定金鑰登入遠端賬戶

> ssh -i .ssh/id_rsa_ssh  [email protected]
Welcome to Ubuntu 20.04.5 LTS (GNU/Linux 5.15.0-53-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

642 updates can be applied immediately.
To see these additional updates run: apt list --upgradable

New release '22.04.1 LTS' available.
Run 'do-release-upgrade' to upgrade to it.

Your Hardware Enablement Stack (HWE) is supported until April 2025.
Last login: Sat Nov 26 12:41:04 2022 from 192.168.1.18
>

注意這一步需要通過 -i 明確指定使用的金鑰檔案,否則還是需要輸入密碼。也可以通過 ssh config 組態檔來避免指定金鑰:

> cat ~/.ssh/config
……
# ssh
Host 192.168.1.118
HostName 192.168.1.118
User mayun
IdentityFile ~/.ssh/id_rsa_ssh

注意 Host 欄位必需指定 ip,除非在 hosts 檔案中進行了對映。

4. 總結

ssh 免密設定是使用者到使用者的,假設有兩臺機器 M 和 N,M 上分別有 U 和 P 兩個賬戶,N 上分別有 S 和 T 兩個賬戶,U 遠端登入 S 需要設定一遍金鑰,同機器的 P 想免密存取 S 也需要設定一遍,不能複用 U 的設定;同理,U 想要登入 T 也需要重新設定一遍,不能複用 S 的設定。U->S / U->T / P->S / P->T 這四對關係中,可以使用不同金鑰,也可以使用相同金鑰,即使使用相同金鑰,S 和 T的 ~/.ssh/authorized_keys 檔案中都會有兩條記錄,分別記錄 U 和 T 的公鑰。你學會了嗎?

賬號註釋

pw_getcos 說是 real name,其實是一串可被解釋的註釋資訊,例如使用 sudo vipw 編譯 /etc/passwd 檔案中的第 5 列:

mayun:x:1002:1003:Jack Ma,Alibaba HangZhou China,12345678,18810245201:/home/mayun:/bin/bash

為新增使用者新增一些額外資訊,再通過以下命令就可以展示這些資訊:

> finger -s mayun
Login     Name       Tty      Idle  Login Time   Office     Office Phone   Host
mayun     Jack Ma    pts/4       *  Oct 30 19:24 Alibaba Ha 12345678  

可以看到顯示了 Name / Office Address / Office Phone 三項,如果使用 -p 選項:

> finger -p mayun
Login: mayun          			Name: Jack Ma
Directory: /home/mayun              	Shell: /bin/bash
Office: Alibaba HangZhou China, 12345678	Home Phone: +1-881-024-5201
Last login Sun Oct 30 19:24 (CST) on pts/4
No mail.

可以展示額外的 Home Phone 資訊,並且各個欄位也能顯示全了。不過 finger 已經是老古董命令了,即使在 CentOS 6.3 上也需要安裝一下才能使用。

另外需要說明的是 vipw 命令,相比直接 vi /etc/passwd,它可以序列化對口令檔案的更改,並且確保所做的更改與其它相關檔案保持一致。

遍歷順序

使用 setpwent / getpwent / endpwent 介面遍歷 passwd 資料檔案時,得到的順序是否和檔案中記錄的順序一致?借用書上一個例子做個演示:

#include <pwd.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include "../apue.h"

struct passwd* my_getpwnam (char const* name)
{
  struct passwd *ptr = 0; 
  setpwent (); 
  while ((ptr = getpwent ()) != NULL)
  {
    printf ("%s\n", ptr->pw_name); 
    if (strcmp (name, ptr->pw_name) == 0)
      break; 
  }

  endpwent (); 
  return (ptr); 
}

int main(int argc, char *argv[])
{
     struct passwd pwd;
     struct passwd *result;

     if (argc != 2) {
          fprintf(stderr, "Usage: %s username\n", argv[0]);
          exit(EXIT_FAILURE);
      }

      result = my_getpwnam(argv[1]);
      if (result == NULL) {
          perror("getpwnam");
          exit(EXIT_FAILURE);
      }

      pwd = *result; 
      printf("Name: [%p] %s; UID: %ld\n", pwd.pw_gecos, pwd.pw_gecos, (long) pwd.pw_uid);
      exit(EXIT_SUCCESS);
}

這個例子演示瞭如何使用遍歷介面模擬 getpwnam 的,這裡主要的修改是在 my_getpwnam 中增加了對遍歷使用者名稱的輸出,這樣當給一個不存在的使用者名稱後,就可以把整個檔案過一遍啦:

> ./getpwnam_ent abc > users.txt

再對比 users.txt 與 /etc/passwd  的區別,在一臺 Ubuntu 筆電上,得到下面的結果:

檢視程式碼
 > paste -d':' users.txt /etc/passwd
root:root:x:0:0:root:/root:/bin/bash
daemon:daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:sync:x:4:65534:sync:/bin:/bin/sync
games:games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-network:systemd-network:x:100:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
systemd-timesync:systemd-timesync:x:102:104:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
messagebus:messagebus:x:103:106::/nonexistent:/usr/sbin/nologin
syslog:syslog:x:104:110::/home/syslog:/usr/sbin/nologin
_apt:_apt:x:105:65534::/nonexistent:/usr/sbin/nologin
tss:tss:x:106:111:TPM software stack,,,:/var/lib/tpm:/bin/false
uuidd:uuidd:x:107:114::/run/uuidd:/usr/sbin/nologin
tcpdump:tcpdump:x:108:115::/nonexistent:/usr/sbin/nologin
avahi-autoipd:avahi-autoipd:x:109:116:Avahi autoip daemon,,,:/var/lib/avahi-autoipd:/usr/sbin/nologin
usbmux:usbmux:x:110:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
rtkit:rtkit:x:111:117:RealtimeKit,,,:/proc:/usr/sbin/nologin
dnsmasq:dnsmasq:x:112:65534:dnsmasq,,,:/var/lib/misc:/usr/sbin/nologin
cups-pk-helper:cups-pk-helper:x:113:120:user for cups-pk-helper service,,,:/home/cups-pk-helper:/usr/sbin/nologin
speech-dispatcher:speech-dispatcher:x:114:29:Speech Dispatcher,,,:/run/speech-dispatcher:/bin/false
avahi:avahi:x:115:121:Avahi mDNS daemon,,,:/var/run/avahi-daemon:/usr/sbin/nologin
kernoops:kernoops:x:116:65534:Kernel Oops Tracking Daemon,,,:/:/usr/sbin/nologin
saned:saned:x:117:123::/var/lib/saned:/usr/sbin/nologin
nm-openvpn:nm-openvpn:x:118:124:NetworkManager OpenVPN,,,:/var/lib/openvpn/chroot:/usr/sbin/nologin
hplip:hplip:x:119:7:HPLIP system user,,,:/run/hplip:/bin/false
whoopsie:whoopsie:x:120:125::/nonexistent:/bin/false
colord:colord:x:121:126:colord colour management daemon,,,:/var/lib/colord:/usr/sbin/nologin
geoclue:geoclue:x:122:127::/var/lib/geoclue:/usr/sbin/nologin
pulse:pulse:x:123:128:PulseAudio daemon,,,:/var/run/pulse:/usr/sbin/nologin
gnome-initial-setup:gnome-initial-setup:x:124:65534::/run/gnome-initial-setup/:/bin/false
gdm:gdm:x:125:130:Gnome Display Manager:/var/lib/gdm3:/bin/false
yunh:yunh:x:1000:1000:yunh,Baidu Beijing China,010-82335469,13552560213:/home/yunh:/bin/bash
systemd-coredump:systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin

結果是完全相同的,但在另外兩臺工作筆電上,出現了不一致的結果,主要表現在兩個方面:

  • 工作的 CentOS 虛擬機器器上遍歷介面返回了更多的內容
  • 工作的 MacOS 筆電上順序與原檔案不一致

下面是 CentOS 對比結果:

檢視程式碼
 > paste -d':' users.txt /etc/passwd
root:root:x:0:0:root:/root:/bin/bash
bin:bin:x:1:1:bin:/bin:/sbin/nologin
daemon:daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:sync:x:5:0:sync:/sbin:/bin/sync
shutdown:shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:halt:x:7:0:halt:/sbin:/sbin/halt
mail:mail:x:8:12:mail:/var/spool/mail:/sbin/nologin
operator:operator:x:11:0:operator:/root:/sbin/nologin
games:games:x:12:100:games:/usr/games:/sbin/nologin
ftp:ftp:x:14:50:FTP User:/:/sbin/nologin
nobody:nobody:x:99:99:Nobody:/:/sbin/nologin
systemd-network:systemd-network:x:192:192:systemd Network Management:/:/sbin/nologin
dbus:dbus:x:81:81:System message bus:/:/sbin/nologin
polkitd:polkitd:x:999:998:User for polkitd:/:/sbin/nologin
libstoragemgmt:libstoragemgmt:x:998:997:daemon account for libstoragemgmt:/var/run/lsm:/sbin/nologin
abrt:abrt:x:173:173::/etc/abrt:/sbin/nologin
rpc:rpc:x:32:32:Rpcbind Daemon:/var/lib/rpcbind:/sbin/nologin
sshd:sshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologin
postfix:postfix:x:89:89::/var/spool/postfix:/sbin/nologin
ntp:ntp:x:38:38::/etc/ntp:/sbin/nologin
chrony:chrony:x:997:995::/var/lib/chrony:/sbin/nologin
tcpdump:tcpdump:x:72:72::/:/sbin/nologin
work:work:x:1000:1000::/home/work:/bin/bash
centos:centos:x:1001:1002:Cloud User:/home/centos:/bin/bash
mayun:mayun:x:1002:1003:Jack Ma,Alibaba HangZhou China,12345678,18810245201:/home/mayun:/bin/bash
zhaomingfu:
jiangze:
shifanjie:
yangmoda:
zhuxiaoxi:
xulei26:
wangzishuo:
yuehongda:
yueguangbin:
lifengjie:
yugeyang:
wangming04:
houhuikun:
liuxinran01:
hanzecheng:
yanghongjun:
lizheyuan:
zhanyongdong:
huxiaoran01:
liuchenghui01:
yunhai01:
liyanan14:
suoning:
panchenglong:
shenhuiyang:
donghan:
chenyun05:
xianghao01:
zhouqi03:
mengzhe:
zhaokexin04:
liuchao15:
niukanglong:
zhengyongpan:
wangjunhan:
shiyiyu:
liuguangming:
piaoxiaoyu:
guochuanlei:
hulingxuan:
ranyunchao:
liushuai06:
songpeipei:
guanzhicheng02:
yuanxueran:
liqilin01:
lirui04:
gaocongcong:
jiahongpeng:
wangyuanyuan14:
chezhuo:
huangfengzhi:
yanxin08:
tanrenzong:
pankai01:
wuyinping:

可以看到通過介面得到的結果前半部分順序是一致的,後半部分是多出來的。何時會出現介面返回比資料檔案多的情況?摘錄一段書中的原文作為解答:

使用者和組資料是用網路資訊服務 (Network Information Service, NIS) 實現的。這使管理員可以編輯資料庫的主副本,然後將它自動分發到組織中的所有伺服器上。使用者端系統可以聯絡伺服器檢視使用者和組的有關資訊。NIS+ 和輕量級目錄存取協定 (Lightweight Directory Access Protocol, LDAP) 提供了類似功能。很多系統通過組態檔 /etc/nsswitch.conf 來控制管理每一類資訊的方法。

看上面例子中多出來的資訊,確實和網路中真實的使用者資訊相吻合,這是第一種不一致的場景。

MacOS 上的情況更復雜一些,/etc/passwd 的內容比較多就不全貼出來了:

> cat /etc/passwd | wc -l
     120

一共有 120 行,除去開頭的註釋是 110 條記錄。再來看通過介面遍歷的結果:

> ./getpwnam_ent abc | wc -l
getpwnam: Undefined error: 0
     221

居然有 221 行, 發現其中有大量重複記錄,排序去重後變為 111 條記錄:

> ./getpwnam_ent abc | sort | uniq | wc -l
getpwnam: Undefined error: 0
     111

將它和 /etc/passwd 去掉頭部註釋後的排序內容做個比較:

> paste -d':' users.txt passwd.txt
_amavisd:_amavisd:*:83:83:AMaViS Daemon:/var/virusmails:/usr/bin/false
......
daemon:daemon:*:1:1:System Services:/var/root:/usr/bin/false
nobody:nobody:*:-2:-2:Unprivileged User:/var/empty:/usr/bin/false
root:root:*:0:0:System Administrator:/var/root:/bin/sh
yunhai01:

遍歷結果只比資料檔案多了一條記錄:yunhai01,這正是我在這臺 MacOS 上的賬戶名稱。不過即使除去這條記錄,原始的遍歷順序和資料檔案也是不一致的,摘錄書中一段話強行解釋一下:

在 FreeBSD 中,……,還會產生該檔案的雜湊版本。/etc/pwd.db 是 /etc/passwd 的雜湊版本,……。這些為大型系統提供了更好的效能。

雜湊版本應該是根據使用者名稱或 uid 對內容進行排序以提高查詢效能的副本,但是並沒有在我的機器上找到 /etc/pwd.db 這個檔案。出現重複記錄確實是個問題,這樣會導致對部分使用者 (本例中除 yunhai01 外) 進行兩次操作,屬於系統級 bug。幸好對於 MacOS 來說,只在單使用者模式下 (維護模式) 才會使用這些資訊,平時都是通過 netinfo 儲存的,問題不大。。

典型案例

補充一下介面使用案例,ls -l 選項因為需要根據 uid 展示使用者名稱,用到了 getpwuid;login 程式因為需要根據使用者名稱查詢使用者資訊,用到了 getpwnam。

前者使用 strace 沒有看到 getpwuid 呼叫:

> strace ls -lh |& less
......
open("/etc/passwd", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=1276, ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f1f48875000
read(3, "root:x:0:0:root:/root:/bin/bash\n"..., 4096) = 1276
read(3, "", 4096)                       = 0
close(3)                                = 0
......

只看到了 open /etc/passwd 的內容。不過這不能說明問題,畢竟 strace 只能跟蹤系統呼叫,而 getpwuid 屬於庫函數,它底層也是通過開啟 passwd 檔案來查詢資訊的,因此不能說明什麼。網上有一個通過 stat 模擬 ls -l 的例子,確實用到了 getpwuid 來顯示使用者資訊,具體可參考附錄。

login 是在使用者登入時被呼叫的,strace 無從下口,只能改天拿來 linux 原始碼分析下了。。

陰影口令

先來探討一下這個檔案存在的必要性,我們都知道檔案中儲存的都是經過加密的口令,使用的是非可逆的加密演演算法,從密文無法倒推回明文,那為何還怕密文洩露呢?參照書上的一段話做個說明:

但是可以對口令進行猜測,將猜測的口令經過單向演演算法變換成加密形式,然後將其與使用者的加密口令相比較……使用者往往以非隨機方式選擇口令……一個經常重複的試驗是先得到一份口令檔案,然後用試探方法猜測口令

對這段話深有同感,有太多伺服器或測試機使用了 123qwe!@#、1qaz@WSX、111qqq!!!… 這類符合作業系統要求卻又簡單好記的密碼。如果將加密口令欄位移入另外一個需要更高許可權的單獨檔案中 (如 /etc/shadow),普通使用者就無法獲取用於猜測口令的原始資訊從而避免了很多風險。存取陰影口令檔案的程式會非常有限 (如 login / passwd),況且這些程式通常是設定使用者 ID 為 root 的,也能正常執行 (關於 set-user-id,可以參考之前寫的:《[apue] linux 檔案存取許可權那些事兒》)。

在 CentOS 上 struct spwd 的定義位於 <shadow.h> 檔案中:

struct spwd
{
    char *sp_namp;		/* Login name.  */
    char *sp_pwdp;		/* Encrypted password.  */
    long int sp_lstchg;		/* Date of last change.  */
    long int sp_min;		/* Minimum number of days between changes.  */
    long int sp_max;		/* Maximum number of days between changes.  */
    long int sp_warn;		/* Number of days to warn user to change the password.  */
    long int sp_inact;		/* Number of days the account may be inactive.  */
    long int sp_expire;		/* Number of days since 1970-01-01 until account expires.  */
    unsigned long int sp_flag;	/* Reserved.  */
};

陰影口令不是 POSIX.1 標準的一部分,大多數實現至少要求包含其中 2 個:sp_namp / sp_pwdp,其它欄位用於控制口令改動頻率 (sp_lstchg / sp_min / sp_max / sp_warn) 及賬戶保持活動狀態的時間 (sp_inact / sp_expire),freebsd 和 MacOS 甚至沒有陰影口令,賬戶的額外資訊是放在 passwd 檔案中的 (pw_change / pw_expire),而 linux 和 Solaris 在這一點上非常接近但是也有細微差別:

  • Solaris 中整數位段均定義均為 int;linux 上為 long int
  • sp_inact 在 Solaris 上表示使用者上次登入以來所經過的天數;linux 上為口令過期的尚餘天數

spwd 結構體的各個欄位和資料檔案中的欄位是一一對應的,在 CentOS 上有以下的檔案內容:

> sudo cat /etc/shadow
root:$6$hT9cNMJc$Ej4tEC3hSHv4jepws0wDgXbIO6lK6GOJ4Yzm1iECfKiq9Bl.zeoNCzr.bI7I3NhPnBezZTK51clj5LuzyXDXc1:18717:0:99999:7:::
bin:*:17632:0:99999:7:::
daemon:*:17632:0:99999:7:::
adm:*:17632:0:99999:7:::
lp:*:17632:0:99999:7:::
sync:*:17632:0:99999:7:::
shutdown:*:17632:0:99999:7:::
halt:*:17632:0:99999:7:::
mail:*:17632:0:99999:7:::
operator:*:17632:0:99999:7:::
games:*:17632:0:99999:7:::
ftp:*:17632:0:99999:7:::
nobody:*:17632:0:99999:7:::
systemd-network:!!:17850::::::
dbus:!!:17850::::::
polkitd:!!:17850::::::
libstoragemgmt:!!:17850::::::
abrt:!!:17850::::::
rpc:!!:17850:0:99999:7:::
sshd:!!:17850::::::
postfix:!!:17850::::::
ntp:!!:17850::::::
chrony:!!:17850::::::
tcpdump:!!:17850::::::
work:$6$NHiZrcs5$igsfZKouoJNEYJMezMfG.sDQYA4Xt6Nu1jEkfz/7/C1qs96aXiAsgRJoeYBo7fAf4oeUkV8T3424ZQ4RIrOix0:18058:1:99999:7:::
centos:!!:18108:1:99999:7:::
mayun::19295:1:99999:7:::

欄位仍以冒號分隔,做個簡單說明:

  • sp_pwdp 除了密文口令外,還可以有以下選擇:*、!!、空,其中除了空表示沒有口令外,其它含義目前不清楚
  • sp_lstchg 是上次更新口令時間,單位是 1970.1.1 開始計算的天數,例如上例中 work 使用者的值 18058 表示:1970+18058/365=2019.61,大概是 2019 年中,以上僅是粗略演演算法,精細一點的可以使用日期計算器
  • sp_min 是最小口令更改間隔,小於這個天數會被系統拒絕,0 表示隨時改
  • sp_max 是最大口令更改間隔,超出這個天數系統會讓使用者強制更新密碼,99999 大概是 274 年,終其一生應該不用改了
  • sp_warn 是過期前提醒天數,一般是一週內 (7),設定為 -1 表示不提醒
  • sp_inact 是過期後多少天內賬號變為 inactive 狀態,此時可登陸但不能操作,必需更新密碼
  • sp_expire 是多少天后賬號會過期,此時無法登陸

使用 chage 命令可以修改與賬戶改動頻率控制相關的欄位,感興趣的可自行 man 查閱用法。

遍歷結果

使用 setspent / getspent / endspent 對 shadow 檔案進行遍歷時,順序和檔案順序一致,這一點和 passwd 檔案結論一樣,同樣的,使用一個書上的一個例子稍加改進進行試驗:

#include <shadow.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include "../apue.h"

struct spwd* my_getspnam (char const* name)
{
  struct spwd *ptr = 0; 
  setspent (); 
  while ((ptr = getspent ()) != NULL)
  {
    printf ("%s\t%s\t%ld\t%ld\t%ld\t%ld\t%ld\t%ld\n", 
            ptr->sp_namp, ptr->sp_pwdp, ptr->sp_lstchg, 
            ptr->sp_min, ptr->sp_max, ptr->sp_warn, 
            ptr->sp_inact, ptr->sp_expire); 
      
    if (strcmp (name, ptr->sp_namp) == 0)
      break; 
  }

  endpwent (); 
  return (ptr); 
}

int main(int argc, char *argv[])
{
     struct spwd pwd;
     struct spwd *result;

     if (argc != 2) {
          fprintf(stderr, "Usage: %s username\n", argv[0]);
          exit(EXIT_FAILURE);
      }

      result = my_getspnam(argv[1]);
      if (result == NULL) {
          perror("my_getspnam");
          exit(EXIT_FAILURE);
      }

      pwd = *result; 
      printf("Name: %s; Pwd: %s\n", pwd.sp_namp, pwd.sp_pwdp);
      exit(EXIT_SUCCESS);
}

這個例子演示瞭如何使用遍歷介面模擬 getspnam 的,這裡主要的修改是在 my_getspnam 中增加了對遍歷資訊的輸出,這樣當給一個不存在的使用者名稱後,就可以把整個檔案過一遍啦: 

檢視程式碼
 > sudo ./getspnam_ent abc
root	$6$hT9cNMJc$Ej4tEC3hSHv4jepws0wDgXbIO6lK6GOJ4Yzm1iECfKiq9Bl.zeoNCzr.bI7I3NhPnBezZTK51clj5LuzyXDXc1	18717	0	99999	7	-1	-1
bin	*	17632	0	99999	7	-1	-1
daemon	*	17632	0	99999	7	-1	-1
adm	*	17632	0	99999	7	-1	-1
lp	*	17632	0	99999	7	-1	-1
sync	*	17632	0	99999	7	-1	-1
shutdown	*	17632	0	99999	7	-1	-1
halt	*	17632	0	99999	7	-1	-1
mail	*	17632	0	99999	7	-1	-1
operator	*	17632	0	99999	7	-1	-1
games	*	17632	0	99999	7	-1	-1
ftp	*	17632	0	99999	7	-1	-1
nobody	*	17632	0	99999	7	-1	-1
systemd-network	!!	17850	-1	-1	-1	-1	-1
dbus	!!	17850	-1	-1	-1	-1	-1
polkitd	!!	17850	-1	-1	-1	-1	-1
libstoragemgmt	!!	17850	-1	-1	-1	-1	-1
abrt	!!	17850	-1	-1	-1	-1	-1
rpc	!!	17850	0	99999	7	-1	-1
sshd	!!	17850	-1	-1	-1	-1	-1
postfix	!!	17850	-1	-1	-1	-1	-1
ntp	!!	17850	-1	-1	-1	-1	-1
chrony	!!	17850	-1	-1	-1	-1	-1
tcpdump	!!	17850	-1	-1	-1	-1	-1
work	$6$NHiZrcs5$igsfZKouoJNEYJMezMfG.sDQYA4Xt6Nu1jEkfz/7/C1qs96aXiAsgRJoeYBo7fAf4oeUkV8T3424ZQ4RIrOix0	18058	1	99999	7	-1	-1
centos	!!	18108	1	99999	7	-1	-1
mayun		19295	1	99999	7	-1	-1
zhaomingfu	!!	12000	0	999999	7	-1	-1
jiangze	!!	12000	0	999999	7	-1	-1
shifanjie	!!	12000	0	999999	7	-1	-1
yangmoda	!!	12000	0	999999	7	-1	-1
zhuxiaoxi	!!	12000	0	999999	7	-1	-1
xulei26	!!	12000	0	999999	7	-1	-1
wangzishuo	!!	12000	0	999999	7	-1	-1
yuehongda	!!	12000	0	999999	7	-1	-1
yueguangbin	!!	12000	0	999999	7	-1	-1
lifengjie	!!	12000	0	999999	7	-1	-1
yugeyang	!!	12000	0	999999	7	-1	-1
wangming04	!!	12000	0	999999	7	-1	-1
houhuikun	!!	12000	0	999999	7	-1	-1
liuxinran01	!!	12000	0	999999	7	-1	-1
hanzecheng	!!	12000	0	999999	7	-1	-1
yanghongjun	!!	12000	0	999999	7	-1	-1
lizheyuan	!!	12000	0	999999	7	-1	-1
zhanyongdong	!!	12000	0	999999	7	-1	-1
huxiaoran01	!!	12000	0	999999	7	-1	-1
liuchenghui01	!!	12000	0	999999	7	-1	-1
yunhai01	!!	12000	0	999999	7	-1	-1
liyanan14	!!	12000	0	999999	7	-1	-1
suoning	!!	12000	0	999999	7	-1	-1
panchenglong	!!	12000	0	999999	7	-1	-1
shenhuiyang	!!	12000	0	999999	7	-1	-1
donghan	!!	12000	0	999999	7	-1	-1
chenyun05	!!	12000	0	999999	7	-1	-1
xianghao01	!!	12000	0	999999	7	-1	-1
zhouqi03	!!	12000	0	999999	7	-1	-1
mengzhe	!!	12000	0	999999	7	-1	-1
zhaokexin04	!!	12000	0	999999	7	-1	-1
liuchao15	!!	12000	0	999999	7	-1	-1
niukanglong	!!	12000	0	999999	7	-1	-1
zhengyongpan	!!	12000	0	999999	7	-1	-1
wangjunhan	!!	12000	0	999999	7	-1	-1
shiyiyu	!!	12000	0	999999	7	-1	-1
liuguangming	!!	12000	0	999999	7	-1	-1
piaoxiaoyu	!!	12000	0	999999	7	-1	-1
guochuanlei	!!	12000	0	999999	7	-1	-1
hulingxuan	!!	12000	0	999999	7	-1	-1
ranyunchao	!!	12000	0	999999	7	-1	-1
liushuai06	!!	12000	0	999999	7	-1	-1
lulintong	!!	12000	0	999999	7	-1	-1
songpeipei	!!	12000	0	999999	7	-1	-1
guanzhicheng02	!!	12000	0	999999	7	-1	-1
yuanxueran	!!	12000	0	999999	7	-1	-1
liqilin01	!!	12000	0	999999	7	-1	-1
lirui04	!!	12000	0	999999	7	-1	-1
gaocongcong	!!	12000	0	999999	7	-1	-1
jiahongpeng	!!	12000	0	999999	7	-1	-1
wangyuanyuan14	!!	12000	0	999999	7	-1	-1
chezhuo	!!	12000	0	999999	7	-1	-1
huangfengzhi	!!	12000	0	999999	7	-1	-1
yanxin08	!!	12000	0	999999	7	-1	-1
tanrenzong	!!	12000	0	999999	7	-1	-1
pankai01	!!	12000	0	999999	7	-1	-1
wuyinping	!!	12000	0	999999	7	-1	-1
my_getspnam: No such file or directory

觀察到幾點現象:

  • sp_pwdp 中的 * / !! 保留原樣輸出
  • 檔案中空的 sp_inact / sp_expire 欄位變為了 -1
  • 輸出比 shadow 檔案中的要多,考慮是 NIS 服務提供的網路使用者資訊

特別是最後一點,當不使用 sudo 提權時,不同機器表現不一致,有的無法從 shadow 檔案中獲取資訊,只能獲取 NIS 服務提供的這部分;有的直接失敗返回 EACCESS。

一個崩潰

這個程式碼是複製上一個例子的,複製後無意間少改了一個地方,導致程式一啟動就崩潰:

> git diff
diff --git a/06.chapter/getspnam_ent.c b/06.chapter/getspnam_ent.c
index c7021ff..903f96d 100644
--- a/06.chapter/getspnam_ent.c
+++ b/06.chapter/getspnam_ent.c
@@ -11,8 +11,9 @@ my_getspnam (char const* name)
 {
   struct spwd *ptr = 0; 
   setspent (); 
-  while ((ptr = getpwent ()) != NULL)
+  while ((ptr = getspent ()) != NULL)
   {
     if (strcmp (name, ptr->sp_namp) == 0)
       break; 
   }

原來是將 getpwent 返回的 struct passwd* 強轉成了 struct spwd*,之後存取成員導致崩潰,可是這裡並沒有 (struct spwd*) 強轉操作,C 語言不應該報個編譯錯?

> make
gcc -Wall -g -c getspnam_ent.c -o getspnam_ent.o
getspnam_ent.c: In function ‘my_getspnam’:
getspnam_ent.c:14:3: warning: implicit declaration of function ‘getpwent’ [-Wimplicit-function-declaration]
   while ((ptr = getpwent ()) != NULL)
   ^
getspnam_ent.c:14:15: warning: assignment makes pointer from integer without a cast [enabled by default]
   while ((ptr = getpwent ()) != NULL)
               ^
gcc -Wall -g getspnam_ent.o apue.o -o getspnam_ent

看起來像是因為沒有包含 <pwd.h> 從而不識別 getpwent,將它的返回值推斷為 int 了,但那也轉不到 struct spwd*,而且即使包含了這個標頭檔案也仍然是個 warning,謎之 C  語言……

最終破案了,原來是沒有把 apue.h 放在最前面,裡有一句定義至關重要:

#define _XOPEN_SOURCE 600 /* Single Unix Specification, Version 3 */

在 XSI 擴充套件中定義的介面必需定義上面的版本號才可以使用:

#if defined __USE_SVID || defined __USE_MISC || defined __USE_XOPEN_EXTENDED
/* Rewind the password-file stream.

   This function is a possible cancellation point and therefore not
   marked with __THROW.  */
extern void setpwent (void);

/* Close the password-file stream.

   This function is a possible cancellation point and therefore not
   marked with __THROW.  */
extern void endpwent (void);

/* Read an entry from the password-file stream, opening it if necessary.

   This function is a possible cancellation point and therefore not
   marked with __THROW.  */
extern struct passwd *getpwent (void);
#endif

組檔案

在 CentOS 上 struct group 的定義位於 <grp.h> 檔案中:

/* The group structure.	 */
struct group
{
    char *gr_name;		/* Group name.	*/
    char *gr_passwd;		/* Password.	*/
    __gid_t gr_gid;		/* Group ID.	*/
    char **gr_mem;		/* Member list.	*/
};

POSIX.1 標準定義了上面全部 4 個欄位,下面做個簡單說明:

  • gr_name 是組名,可通過  getgrname 查詢組資訊
  • gr_passwd 是組密碼,可通過 gpasswd 修改刪除組的密碼;和 struct passwd 一樣,密碼不直接儲存在這個檔案,而是存放於 shadow 檔案:/etc/gshadow;當然這是非標準的部分,並不是所有平臺都支援
  • gr_gid 是組的唯一 id,可通過 getgrgid 查詢組資訊
  • gr_mem 是一個指標陣列,可以儲存多個屬於該組的使用者名稱,以 NULL結尾。

這些欄位和資料檔案中的欄位是一一對應的,在 CentOS 上有以下的檔案內容:

> cat /etc/group
root:x:0:
bin:x:1:
daemon:x:2:
sys:x:3:
adm:x:4:
tty:x:5:
disk:x:6:
lp:x:7:
mem:x:8:
kmem:x:9:
wheel:x:10:
cdrom:x:11:
mail:x:12:postfix
man:x:15:
dialout:x:18:
floppy:x:19:
games:x:20:
tape:x:33:
video:x:39:
ftp:x:50:
lock:x:54:
audio:x:63:
nobody:x:99:
users:x:100:
utmp:x:22:
utempter:x:35:
input:x:999:
systemd-journal:x:190:
systemd-network:x:192:
dbus:x:81:
polkitd:x:998:
libstoragemgmt:x:997:
ssh_keys:x:996:
abrt:x:173:
rpc:x:32:
sshd:x:74:
slocate:x:21:
postdrop:x:90:
postfix:x:89:
ntp:x:38:
chrony:x:995:
tcpdump:x:72:
stapusr:x:156:
stapsys:x:157:
stapdev:x:158:
yunhai01:x:1000:
cgred:x:994:
mayun:x:1001:

欄位以冒號分隔,分別對應著 gr_name / gr_passwd / gr_gid / gr_mem 欄位,其中:

  • 組密碼一直保持 'x'
  • 組成員為空表示只包含和組名同名的使用者,當一個使用者屬於多個組時,這裡就會有非空資訊了,例如上面的 postfix 使用者,下面講到附加組時還會舉更多的例子

遍歷順序

使用 setgrent / getgrent / endgrend 遍歷組檔案時,順序和檔案順序一致,這一點和 passwd 檔案結論一樣,同樣的,使用一個書上的一個例子稍加改進進行驗證:

#include "../apue.h"
#include <grp.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>

struct group* my_getgrnam (char const* name)
{
  struct group *ptr = 0; 
  setgrent (); 
  while ((ptr = getgrent ()) != NULL)
  {
    if (strcmp (name, ptr->gr_name) == 0)
      break; 

    printf ("%s\n", ptr->gr_name); 
  }

  endgrent (); 
  return (ptr); 
}

int main(int argc, char *argv[])
{
     struct group grp;
     struct group *result;

     if (argc != 2) {
          fprintf(stderr, "Usage: %s group\n", argv[0]);
          exit(EXIT_FAILURE);
      }

      result = my_getgrnam(argv[1]);
      if (result == NULL) {
          perror("getgrnam");
          exit(EXIT_FAILURE);
      }

      grp = *result; 
      printf("Name: %s; GID: %d\n", grp.gr_name, grp.gr_gid);
      for (int i=0; grp.gr_mem[i] != 0; ++i)
        printf ("  %s\n", grp.gr_mem[i]); 
      exit(EXIT_SUCCESS);
}

這個例子演示瞭如何使用遍歷介面模擬 getgrnam 的,這裡主要的修改是在 my_getgrnam 中增加了對遍歷資訊的輸出,這樣當給一個不存在的使用者名稱後,就可以把整個檔案過一遍啦: 

> ./getgrnam_ent abc
root
bin
daemon
sys
adm
tty
disk
lp
mem
kmem
wheel
cdrom
mail
man
dialout
floppy
games
tape
video
ftp
lock
audio
nobody
users
utmp
utempter
input
systemd-journal
systemd-network
dbus
polkitd
libstoragemgmt
ssh_keys
abrt
rpc
sshd
slocate
postdrop
postfix
ntp
chrony
tcpdump
stapusr
stapsys
stapdev
work
nogroup
cgred
centos
mayun
DOORGOD
getgrnam: Success

相比 /etc/group 檔案,多了 NIS 返回的部分資料:

> paste /etc/group group.txt
root:x:0:	root
bin:x:1:	bin
daemon:x:2:	daemon
sys:x:3:	sys
adm:x:4:centos	adm
tty:x:5:	tty
disk:x:6:	disk
lp:x:7:	lp
mem:x:8:	mem
kmem:x:9:	kmem
wheel:x:10:centos	wheel
cdrom:x:11:	cdrom
mail:x:12:postfix	mail
man:x:15:	man
dialout:x:18:	dialout
floppy:x:19:	floppy
games:x:20:	games
tape:x:33:	tape
video:x:39:	video
ftp:x:50:	ftp
lock:x:54:	lock
audio:x:63:	audio
nobody:x:99:	nobody
users:x:100:	users
utmp:x:22:	utmp
utempter:x:35:	utempter
input:x:999:	input
systemd-journal:x:190:centos	systemd-journal
systemd-network:x:192:	systemd-network
dbus:x:81:	dbus
polkitd:x:998:	polkitd
libstoragemgmt:x:997:	libstoragemgmt
ssh_keys:x:996:	ssh_keys
abrt:x:173:	abrt
rpc:x:32:	rpc
sshd:x:74:	sshd
slocate:x:21:	slocate
postdrop:x:90:	postdrop
postfix:x:89:	postfix
ntp:x:38:	ntp
chrony:x:995:	chrony
tcpdump:x:72:	tcpdump
stapusr:x:156:	stapusr
stapsys:x:157:	stapsys
stapdev:x:158:	stapdev
work:x:1000:	work
nogroup:x:1001:	nogroup
cgred:x:994:	cgred
centos:x:1002:	centos
mayun:x:1003:	mayun
	DOORGOD

其中 DOORGOD 即是 NIS 提供的,由 NIS 提供的使用者都在這個組中:

> ls -lhrt
total 132K
-rw-rw-r-- 1 yunhai01 DOORGOD 1.4K May 15  2021 getgrnam.c
-rw-rw-r-- 1 yunhai01 DOORGOD  11K May 15  2021 wtmp2.txt
-rw-rw-r-- 1 yunhai01 DOORGOD  174 May 15  2021 utmp.c
-rw-rw-r-- 1 yunhai01 DOORGOD  566 May 15  2021 uname.c
-rw-rw-r-- 1 yunhai01 DOORGOD 1.6K May 15  2021 timeshift.c
-rw-rw-r-- 1 yunhai01 DOORGOD 1.8K May 15  2021 timeprintf.c
-rw-rw-r-- 1 yunhai01 DOORGOD  958 May 15  2021 time.c.org
-rw-rw-r-- 1 yunhai01 DOORGOD  958 May 15  2021 time.c
-rw-rw-r-- 1 yunhai01 DOORGOD 1.3K May 15  2021 setgrps.c
-rw-rw-r-- 1 yunhai01 DOORGOD  15K May 15  2021 ls.out
-rw-rw-r-- 1 yunhai01 DOORGOD  339 May 15  2021 hostname.c
-rw-rw-r-- 1 yunhai01 DOORGOD 1.3K May 15  2021 getspnam.c
-rw-rw-r-- 1 yunhai01 DOORGOD 1.2K May 15  2021 getservnam_ent.c
-rw-rw-r-- 1 yunhai01 DOORGOD  841 May 15  2021 getservnam.c
-rw-rw-r-- 1 yunhai01 DOORGOD 1.2K May 15  2021 getpwnam.c
-rw-rw-r-- 1 yunhai01 DOORGOD 1.1K May 15  2021 getprotonam_ent.c
-rw-rw-r-- 1 yunhai01 DOORGOD  800 May 15  2021 getprotonam.c
-rw-rw-r-- 1 yunhai01 DOORGOD  988 May 15  2021 getnetnam_ent.c
-rw-rw-r-- 1 yunhai01 DOORGOD  906 May 15  2021 getnetnam.c
-rw-rw-r-- 1 yunhai01 DOORGOD 1.1K May 15  2021 gethostnam_ent.c
-rw-rw-r-- 1 yunhai01 DOORGOD  992 May 15  2021 gethostnam.c
-rw-rw-r-- 1 yunhai01 DOORGOD 1.1K May 15  2021 getgrps.c
-rw-r--r-- 1 yunhai01 DOORGOD  342 Nov 13 00:08 shadow.sh
-rw-rw-r-- 1 yunhai01 DOORGOD  974 Nov 13 00:49 getspnam_ent.c
-rw-r--r-- 1 yunhai01 DOORGOD  868 Nov 27 16:34 getpwnam_ent.c
-rw-rw-r-- 1 yunhai01 DOORGOD  945 Nov 27 16:34 getgrnam_ent.c
-rw-rw-r-- 1 yunhai01 DOORGOD 3.3K Nov 27 16:35 Makefile
-rw-r--r-- 1 yunhai01 DOORGOD  337 Nov 27 16:48 group.txt

附加組

早期 unix 系統中,一個使用者只能屬於一個組,當臨時需要借用另一組許可權時,使用 newgrp {group} 命令切換,完成後再使用無引數的 newgrp 返回。如果新的組有密碼,需要輸入匹配的密碼才可以加入。後面隨著系統的發展,引入了附加組的概念,一個使用者除了屬於一個主組 (initial group) 外,還可以屬於最多不超過 NGROUPS_MAX (65536 CentOS) 個附加組,相應的檔案許可權檢查時,除了將程序有效組 ID 與主組 ID 進行比較外,還與所有附加組 ID 進行比較,只有有一個能匹配上,就可以通過許可權檢查。這樣一來就避免了頻繁的切換組。關於檔案許可權的內容,可以參考我之前寫的這篇:《[apue] linux 檔案系統那些事兒 》。

在開始用例子說明新增使用者到組之前,先熟悉下與使用者和使用者組相關的幾個命令:

  • useradd / userdel / usermod 是使用者的增刪改;
  • groupadd / groupdel / groupmod 是使用者組的增刪改;
  • passwd / gpaaswd 分別是使用者和組密碼的增刪改。

其中:

  • useradd / usermod 都可以通過 -g 引數指定主組、-G 引數指定附加組,多個組名之前以逗號分隔
  • usermod -G 指定的附加組列表會直接替換使用者的附加組,如果僅新增,需要指定 -a 選項。對於刪除,usermod 比較無力,需要得到使用者之前的所有附加組,去掉想刪除的組後直接使用 -G 設定
  • 除了 usermod 從使用者角度出發,gpasswd 從使用者組的角度出發也可以修改組包含的使用者列表,主要是通過  -a 選項新增使用者,-d 選項刪除使用者,-M 選項直接設定組的所有使用者。對比下來,想刪除某個使用者的附加組,使用 gpasswd -d 更方便一些

下面演示為 mayun 賬戶新增多個附加組:

> sudo usermod -a -G centos,sshd,work,ntp,dbus,games,ftp,man mayun
> sudo gpasswd -M centos,sshd,work,ntp,dbus,games,ftp,daemon mayun
> id mayun
uid=1002(mayun) gid=1003(mayun) groups=1003(mayun),15(man),20(games),50(ftp),81(dbus),74(sshd),38(ntp),1000(work),1002(centos)
> cat /etc/group
root:x:0:
bin:x:1:
daemon:x:2:
sys:x:3:
adm:x:4:centos
tty:x:5:
disk:x:6:
lp:x:7:
mem:x:8:
kmem:x:9:
wheel:x:10:centos
cdrom:x:11:
mail:x:12:postfix
man:x:15:mayun
dialout:x:18:
floppy:x:19:
games:x:20:mayun
tape:x:33:
video:x:39:
ftp:x:50:mayun
lock:x:54:
audio:x:63:
nobody:x:99:
users:x:100:
utmp:x:22:
utempter:x:35:
input:x:999:
systemd-journal:x:190:centos
systemd-network:x:192:
dbus:x:81:mayun
polkitd:x:998:
libstoragemgmt:x:997:
ssh_keys:x:996:
abrt:x:173:
rpc:x:32:
sshd:x:74:mayun
slocate:x:21:
postdrop:x:90:
postfix:x:89:
ntp:x:38:mayun
chrony:x:995:
tcpdump:x:72:
stapusr:x:156:
stapsys:x:157:
stapdev:x:158:
work:x:1000:mayun
nogroup:x:1001:
cgred:x:994:
centos:x:1002:mayun
mayun:x:1003:centos,sshd,work,ntp,dbus,games,ftp,daemon
> sudo usermod -G mayun mayun
> id mayun
uid=1002(mayun) gid=1003(mayun) groups=1003(mayun)
> sudo gpasswd -M mayun mayun

指令碼使用 usermod 為使用者 mayun 新增附加組,使用 gpasswd 為使用者組 mayun 新增使用者,通過 id 展示使用者所屬組資訊,也通過檢視 /etc/group 驗證了這一點,最後恢復原狀。

典型用例

關於附加組有如下幾個 api:

int getgroups(int gidsetsize, gid_t grouplist[]);
int setgroups(int ngroups, const gid_t gidlist[]);
int initgroups(const char *name, gid_t basegid);

下面結合使用場景對他們做個簡單說明:

  • getgropus 隨時可以呼叫,gidsetsize 應與 grouplist 維度匹配,如果 gidsetsize = 0,則返回 grouplist 的維度,以便使用者分配儲存空間接收它們
  • 只有超級使用者可以呼叫 setgroups 來為呼叫程序設定附加組 ID 列表,ngroups 不能大於 NGROUPS_MAX
  • 只有超級使用者可以呼叫 initgroups 來初始化賬戶的附加組列表,它通過 setgrent/getgrent/endgrent 讀取組檔案,遍歷其中包含成員為 name 的組,然後呼叫 setgroups 設定這它們,此外還會設定 basegid 作為初始組,它是 name 在口令檔案中的對應的組 ID,用以區分組 ID 和附加組 ID
  • login 程序會在使用者登入時呼叫 initgroups

主機

在 CentOS 上 hostent 結構體的定義位於 <netdb.h> 檔案中:

struct hostent
{
  char *h_name;			/* Official name of host.  */
  char **h_aliases;		/* Alias list.  */
  int h_addrtype;		/* Host address type.  */
  int h_length;			/* Length of address.  */
  char **h_addr_list;		/* List of addresses from name server.  */
};

其中:

  • h_name 表示主機名,這通常是用域名錶示的,如 baidu.com
  • h_addrtype 一般為 AF_INET 或 AF_INET6
  • h_addr_list 用來存放多個地址指標,以 NULL 結尾。為了向後相容,通常將 h_addr 定義為連結串列中第一個元素

/etc/hosts 檔案一般只有很少的內容,除非明確指定域名到 IP 的對映,一般不更改這個檔案,我的 CentOS 上它有以下內容:

127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1         localhost localhost.localdomain localhost6 localhost6.localdomain6
10.9.225.242 goodcitizen.bcc-gzhxy.baidu.com goodcitizen.bcc-gzhxy.baidu.com
140.82.114.3 github.com
140.82.114.10 nodeload.github.com
140.82.114.6 api.github.com
140.82.114.10 codeload.github.com
203.208.39.193 dl.google.com

第一列是 IP 地址,第二列是域名。可以看到為了增加國內 github 的解析我增加了一些內容,這樣 ping github.com 時將直接使用指定的 IP 進行連線。

通過 sethostent/gethostent/endhostent 遍歷的資訊將僅限檔案內容,而 gethostbyname/gethostbyaddr 則可以返回任意合法域名的地址,它的取值範圍遠遠大於 /etc/hosts 的範圍,這是和其它 api 最大的不同點。下面的這個程式演示了這一點,首先驗證檔案遍歷的方式:

#include <netdb.h>
#include <sys/socket.h> 
#include <netinet/in.h>
#include <arpa/inet.h> 
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include "../apue.h"

struct hostent* my_gethostnam (char const* name)
{
  struct hostent *ptr = 0; 
  sethostent (1); 
  while ((ptr = gethostent ()) != NULL)
  {
    printf ("searching %s\n", ptr->h_name); 
    if (strcmp (name, ptr->h_name) == 0)
      break; 
  }

  endhostent (); 
  return (ptr); 
}

int
main(int argc, char *argv[])
{
     struct hostent *result;

     if (argc != 2) {
          fprintf(stderr, "Usage: %s hostname\n", argv[0]);
          exit(EXIT_FAILURE);
      }

      result = my_gethostnam(argv[1]);
      if (result == NULL) {
          perror("gethostnam");
          exit(EXIT_FAILURE);
      }

      printf("Name: %s; type: %d\n", result->h_name, result->h_addrtype);

      int i = 0; 
      char **p = result->h_addr_list; 
      while (p && p[i])
      {
        printf("  %s\n", inet_ntoa(*(struct in_addr*)p[i])); 
        i++; 
      }
      exit(EXIT_SUCCESS);
}

這個例子演示瞭如何使用遍歷介面模擬 gethostbynam 的,這裡主要的修改是在 my_gethostnam 中增加了對遍歷資訊的輸出,這樣當給一個不存在的域名後,就可以把整個檔案過一遍啦: 

> ./gethostnam_ent baidu.com
searching localhost
searching localhost
searching goodcitizen.bcc-gzhxy.baidu.com
searching github.com
searching nodeload.github.com
searching api.github.com
searching codeload.github.com
searching dl.google.com
gethostnam: Success

可以看到因為給定的域名不在 hosts 檔案中,所以即使是合法的域名最後也沒有找到。如果將這裡的 my_gethostbynam 替換為標準的 gethostbynam,結果就大不相同了:

> ./gethostnam baidu.com
Name: baidu.com; type: 2
  220.181.38.251
  220.181.38.148

如果給定的域名是 hosts 中已經存在的,則不管是否有網路都可以得到結果:

> ./gethostnam github.com
Name: github.com; type: 2
  140.82.114.3

因此可以這樣理解,hosts 僅僅是在系統自動解析域名的基礎上增加了自定義域名對映的功能,而且具有更高優先順序。另外遍歷的時候只返回檔案中的內容也好理解,如果將網路上的 DNS 資訊遍歷一遍,那絕對是一件不可能完成的任務,也沒有必要。gethostbynam 對於沒有 DNS 快取的域名,也是通過在網路上傳送 DNS 請求來實現的,所以當網路不通時,這個介面也無法正常工作了。

uname

上面的內容主要是獲取網路主機名地址的,那如何獲取本地主機名呢?POSIX 提供了兩個介面,首先來看 uname:

/* Structure describing the system and machine.  */
struct utsname
{
    /* Name of the implementation of the operating system.  */
    char sysname[_UTSNAME_SYSNAME_LENGTH];
    /* Name of this node on the network.  */
    char nodename[_UTSNAME_NODENAME_LENGTH];
    /* Current release level of this implementation.  */
    char release[_UTSNAME_RELEASE_LENGTH];
    /* Current version level of this release.  */
    char version[_UTSNAME_VERSION_LENGTH];
    /* Name of the hardware type the system is running on.  */
    char machine[_UTSNAME_MACHINE_LENGTH];
};

int uname(struct utsname *name);

uname 返回 utsname 結構體,分別包含了系統名稱、主機名稱、釋出名稱、版本、機器型別,下面是在 CentOS 上呼叫的輸出:

> ./uname
sizeof (struct utsname) = 390
sysname: Linux
nodename: goodcitizen.bcc-gzhxy.baidu.com
release: 3.10.0-1160.80.1.el7.x86_64
version: #1 SMP Tue Nov 8 15:48:59 UTC 2022
machine: x86_64

系統命令 uname 可以直接輸出這些資訊:

> uname -s
Linux
> uname -n
goodcitizen.bcc-gzhxy.baidu.com
> uname -r
3.10.0-1160.80.1.el7.x86_64
> uname -v
#1 SMP Tue Nov 8 15:48:59 UTC 2022
> uname -m
x86_64
> uname -a
Linux goodcitizen.bcc-gzhxy.baidu.com 3.10.0-1160.80.1.el7.x86_64 #1 SMP Tue Nov 8 15:48:59 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux

可以看到範例中各選項與欄位的對應關係。

gethostname

int gethostname(char *name, size_t namelen);
int sethostname(const char *name, int namelen);

gethostname 只輸出主機名稱,看原始碼它直接呼叫 uname 並返回 nodename 欄位,名稱長度限制為 HOST_NAME_MAX (CentOS 64)。 sethostname 則只有超級使用者可以呼叫,通常在系統自舉時設定,由 /etc/rc 或 init 取自一個啟動檔案。

網路

在 CentOS 上 netent 結構體位於 <netdb.h> 檔案中:

struct netent {
    char      *n_name;     /* official network name */
    char     **n_aliases;  /* alias list */
    int        n_addrtype; /* net address type */
    uint32_t   n_net;      /* network number */
}

對應的檔案是 /etc/networks,在 CentOS 上只找到寥寥幾條記錄:

default 0.0.0.0
loopback 127.0.0.0
link-local 169.254.0.0

關於 getnetbyname 及 getnetbyaddr,一直沒明白有什麼用處,所以這節就簡單帶過了,用法和上一節別無二致。

協定

在 CentOS 上 protoent 結構體位於 <netdb.h> 檔案中:

struct protoent {
    char  *p_name;       /* official protocol name */
    char **p_aliases;    /* alias list */
    int    p_proto;      /* protocol number */
}

其中:

  • p_name 是協定名,如 icmp、tcp、ip
  • p_proto 是協定號,對應著 IPPROTO_XXX 的定義,例如 IPPROTO_ICMP = 1,IPPROTO_TCP = 6, IPPROTO_IP = 0

/etc/protocols 包含了所有的協定,內容比較多,這裡就不貼整個檔案了,取一些典型的資料列出來:

> cat /etc/protocols
...
ip	0	IP		# internet protocol, pseudo protocol number
hopopt	0	HOPOPT		# hop-by-hop options for ipv6
icmp	1	ICMP		# internet control message protocol
igmp	2	IGMP		# internet group management protocol
ggp	3	GGP		# gateway-gateway protocol
ipv4	4	IPv4		# IPv4 encapsulation
st	5	ST		# ST datagram mode
tcp	6	TCP		# transmission control protocol
cbt	7	CBT		# CBT, Tony Ballardie <[email protected]>
egp	8	EGP		# exterior gateway protocol
igp	9	IGP		# any private interior gateway (Cisco: for IGRP)
bbn-rcc	10	BBN-RCC-MON		# BBN RCC Monitoring
nvp	11	NVP-II		# Network Voice Protocol
pup	12	PUP		# PARC universal packet protocol
argus	13	ARGUS		# ARGUS
emcon	14	EMCON		# EMCON
xnet	15	XNET		# Cross Net Debugger
chaos	16	CHAOS		# Chaos
udp	17	UDP		# user datagram protocol
mux	18	MUX		# Multiplexing protocol
dcn	19	DCN-MEAS		# DCN Measurement Subsystems
hmp	20	HMP		# host monitoring protocol
prm	21	PRM		# packet radio measurement protocol
xns-idp	22	XNS-IDP		# Xerox NS IDP
trunk-1	23	TRUNK-1		# Trunk-1
trunk-2	24	TRUNK-2		# Trunk-2
leaf-1	25	LEAF-1		# Leaf-1
leaf-2	26	LEAF-2		# Leaf-2
rdp	27	RDP		# "reliable datagram" protocol
irtp	28	IRTP		# Internet Reliable Transaction Protocol
iso-tp4	29	ISO-TP4		# ISO Transport Protocol Class 4
netblt	30	NETBLT		# Bulk Data Transfer Protocol
...
>  cat /etc/protocols | wc -l
162

第一列是協定名,第二列是協定號,第三列是別名。# 號開頭的為註釋不是有效記錄。

通過 setprotoent/getprotoent/endprotoent 遍歷的內容與檔案內容完全一致,且順序一致。這裡就不再演示了。

與 /etc/networks 一樣,我沒找到這些介面的使用場景,一般在程式設計階段就要確定使用的協定型別,直接指定標頭檔案中的 IPPROTO_XX 即可,有什麼必要通過 getprotobynam 來查詢一遍呢?除非是為了某種可拓展性,當不同協定經過抽象後除了協定部分的程式碼完全一致時,可以通過在組態檔中指定協定名的方式來快速切換底層的實現,那麼這時就可以使用 getprotobynam 來查詢對應的協定號,這樣一看還是有點用的哈~

服務

在 CentOS 上 servent 結構體位於 <netdb.h> 檔案中:

struct servent {
    char  *s_name;       /* official service name */
    char **s_aliases;    /* alias list */
    int    s_port;       /* port number */
    char  *s_proto;      /* protocol to use */
}

其中:

  • s_name 表示服務名,如 ssh、http、https、ftp 等
  • s_port 表示連線的埠號,注意位元組順序是網路序,展示前需要轉換為主機序
  • s_proto 表示底層傳輸層協定,如 tcp、udp 等

/etc/services 包含了所有的服務,在 CentOS 上有以下內容 (內容有縮減):

> cat /etc/services | wc -l
11176
> cat /etc/services
...
# 21 is registered to ftp, but also used by fsp
ftp             21/tcp
ftp             21/udp          fsp fspd
ssh             22/tcp                          # The Secure Shell (SSH) Protocol
ssh             22/udp                          # The Secure Shell (SSH) Protocol
telnet          23/tcp
telnet          23/udp
# 24 - private mail system
lmtp            24/tcp                          # LMTP Mail Delivery
lmtp            24/udp                          # LMTP Mail Delivery
smtp            25/tcp          mail
smtp            25/udp          mail
time            37/tcp          timserver
time            37/udp          timserver
rlp             39/tcp          resource        # resource location
rlp             39/udp          resource        # resource location
nameserver      42/tcp          name            # IEN 116
nameserver      42/udp          name            # IEN 116
nicname         43/tcp          whois
nicname         43/udp          whois
...

第一列是服務名,第二列是埠號與協定名,通過斜槓分隔。# 號開頭的為註釋不是有效記錄。

通過 setservent/getservent/endservent 遍歷的內容與檔案內容完全一致,且順序一致,這裡就不再演示了。

getaddrinfo

為了簡化 gethostbynam/gethostbyaddr 與 getservbynam/getservbyport 呼叫,Linux 上推出了一組新的介面:

int getnameinfo(const struct sockaddr *sa, socklen_t salen,
                char *host, size_t hostlen,
                char *serv, size_t servlen, int flags);

int getaddrinfo(const char *node, const char *service,
                const struct addrinfo *hints,
                struct addrinfo **res);

void freeaddrinfo(struct addrinfo *res);

const char *gai_strerror(int errcode);

其中:

  • getnameinfo = gethostbyaddr + getservbyport,根據地址查詢主機名與服務名
  • getaddrinfo = gethostbyname + getservbyname,根據主機名與服務名查詢地址資訊
  • freeaddrinfo 用來釋放與地址相關的記憶體,這塊記憶體由 getaddrinfo 返回
  • gai_strerror 用來解釋 getaddrinfo 的返回值

addrinfo 結構體定義如下:

struct addrinfo {
    int              ai_flags;
    int              ai_family;
    int              ai_socktype;
    int              ai_protocol;
    socklen_t        ai_addrlen;
    struct sockaddr *ai_addr;
    char            *ai_canonname;
    struct addrinfo *ai_next;
};

不多做解釋了,感興趣的讀者可以檢視 man 手冊頁,這裡主要關注一下 ai_next 欄位,返回的多個地址可以通過這個欄位串連成連結串列,比之前直觀了不少。

除了簡化使用者呼叫,這組介面最大的好處是可重入性,無需擔心靜態儲存區覆蓋的問題,同時也能助力消除 ipv4 與 ipv6 的依賴問題 (allows programs to eliminate IPv4-versus-IPv6 dependencies)。

使用者登入

使用者登入相關的資訊主要儲存於 utmp/wtmp/btmp 三個檔案中,下面一一說明。

utmp

一般位於 /var/run/utmp,記錄當前登入進系統的各個使用者。login 程式在使用者登入時會填寫一條 utmp 記錄到該檔案,登出時, init 程序將 utmp 檔案中相應的記錄擦除 (每個位元組都填 0),utmp 結構的定義位於 <utmp.h> 檔案中:

#define UT_LINESIZE      32
#define UT_NAMESIZE      32
#define UT_HOSTSIZE     256

struct exit_status {              /* Type for ut_exit, below */
    short int e_termination;      /* Process termination status */
    short int e_exit;             /* Process exit status */
};

struct utmp {
    short   ut_type;              /* Type of record */
    pid_t   ut_pid;               /* PID of login process */
    char    ut_line[UT_LINESIZE]; /* Device name of tty - "/dev/" */
    char    ut_id[4];             /* Terminal name suffix, or inittab(5) ID */
    char    ut_user[UT_NAMESIZE]; /* Username */
    char    ut_host[UT_HOSTSIZE]; /* Hostname for remote login, or kernel version for run-level messages */
    struct  exit_status ut_exit;  /* Exit status of a process marked as DEAD_PROCESS; not used by Linux init(8) */

     long   ut_session;           /* Session ID */
     struct timeval ut_tv;        /* Time entry was made */

    int32_t ut_addr_v6[4];        /* Internet address of remote host; IPv4 address uses just ut_addr_v6[0] */
    char __unused[20];            /* Reserved for future use */
};

/* Backward compatibility hacks */
#define ut_name ut_user
#define ut_time ut_tv.tv_sec
#define ut_addr ut_addr_v6[0]

註釋基本可以解釋各個欄位的含義,書上老版本的結構體只介紹了 ut_line / ut_name / ut_time 三個欄位,後兩個通過 define 定義到了 ut_user 和 ut_tv.tv_sec 欄位。另外 64 位的 ut_tv / ut_session 型別會不一樣,這裡為了簡化沒有列出完整的定義,感興趣的可以 man utmp 自行檢視。

utmpdump

由於 /var/run/utmp 是二進位制的,無法直接檢視,想要看這個檔案的內容,只能通過 utmpdump 命令轉換後檢視:

> utmpdump /var/run/utmp 
Utmp dump of /var/run/utmp
[2] [00000] [~~  ] [reboot  ] [~           ] [3.10.0-1160.80.1.el7.x86_64] [0.0.0.0        ] [Wed Dec 07 16:22:20 2022 CST]
[1] [00051] [~~  ] [runlevel] [~           ] [3.10.0-1160.80.1.el7.x86_64] [0.0.0.0        ] [Wed Dec 07 16:22:29 2022 CST]
[6] [01321] [tyS0] [LOGIN   ] [ttyS0       ] [                    ] [0.0.0.0        ] [Wed Dec 07 16:22:29 2022 CST]
[6] [01320] [tty1] [LOGIN   ] [tty1        ] [                    ] [0.0.0.0        ] [Wed Dec 07 16:22:29 2022 CST]
[7] [03628] [ts/0] [yunhai01] [pts/0       ] [172.31.43.62        ] [172.31.43.62   ] [Sun Jan 01 11:48:53 2023 CST]
[8] [24901] [ts/1] [        ] [pts/1       ] [                    ] [172.31.23.41   ] [Wed Dec 14 15:18:50 2022 CST]
[8] [04965] [ts/2] [        ] [pts/2       ] [                    ] [172.31.22.20   ] [Wed Dec 21 18:49:00 2022 CST]
[8] [27816] [ts/3] [        ] [pts/3       ] [                    ] [172.31.23.41   ] [Fri Dec 30 18:28:00 2022 CST]

其中各個列和欄位並不是一一對應的關係,不過關鍵的程序 ID、使用者名稱、主機名、ip 地址、登入時間還是很好分辨的:

  • 第一列為 ut_type,取值如下 (其中 7 表示當前正在登入):
#define EMPTY         0 /* Record does not contain valid info (formerly known as UT_UNKNOWN on Linux) */
#define RUN_LVL       1 /* Change in system run-level (see init(8)) */
#define BOOT_TIME     2 /* Time of system boot (in ut_tv) */
#define NEW_TIME      3 /* Time after system clock change (in ut_tv) */
#define OLD_TIME      4 /* Time before system clock change (in ut_tv) */
#define INIT_PROCESS  5 /* Process spawned by init(8) */
#define LOGIN_PROCESS 6 /* Session leader process for user login */
#define USER_PROCESS  7 /* Normal process */
#define DEAD_PROCESS  8 /* Terminated process */
#define ACCOUNTING    9 /* Not implemented */
  •  第二列是 ut_pid,通過 pstree 可以驗證 (其中狀態 7 對應的 PID 3628 為 sshd 程序):
> pstree -nph
...
           ├─sshd(1282)───sshd(3628)───sshd(3751)───bash(3777)───bash(3807)─┬─man(29705)───less(29719)
           │                                                                └─pstree(30238)
...
  • 第三列為 ut_id,有一些例外:
    • 當狀態為 RUN_LVL (1:執行級別改變) 或 BOOT_TIME (2:系統重啟) 時,為 ~~
    • ttyX 表示終端名。注意因為長度限制,ttyS0 會被截斷為 tyS0
    • pts/X 表示偽終端名。同樣因為長度限制,pts/0 會被截斷為 ts/0
  • 第四列為 ut_user,也有例外:
    • 當狀態為 RUN_LVL 時為 runlevel
    • 當狀態為 BOOT_TIME 時為 reboot
    • 空表示非活躍使用者 (DEAD_PROCESS)
  • 第五列為 ut_line,是 ut_id 的完整版
  • 第六列為 ut_host,有例外:
    • 當狀態為 RUN_LVL 或 BOOT_TIME 時為核心版本號
    • 本地登入為空
  • 第七列為 ut_addr,v4 地址僅使用 ut_addr_v6[0] 表示,全零表示本地登入
  • 第八列為 ut_time

各個列具體列含義可參考文末連結。

欄位變更

系統啟動後首先啟動 init 程序,該程序首先會清理 utmp 檔案,這主要是通過:

  • 將 ut_type 設定為 DEAD_PROCESS (8)
  • 對於查詢不到 ut_pid 資訊且並狀態不為 DEAD_PROCESS / RUN_LVL 記錄,清空其 ut_user / ut_host / ut_time 欄位

下面是程序生命週期過程中各個欄位的變更邏輯:

  1. init 程序根據 inittab 新建程序時如果沒有匹配的空記錄 (通過 ut_id) 則插入一條新的 utmp 記錄,ut_id 設定為 inittab 中對應的欄位,並且設定 ut_pid 和 ut_time 欄位,最後設定 ut_type 為 INIT_PROCESS (5)。
  2. mingettty 或 agetty 程序根據  pid 查詢入口設定 ut_line,修改 ut_type 為 LOGIN_PROCESS (6),更新 ut_time 欄位
  3. login 程序校驗使用者登入資訊後,設定 ut_type 為 USER_PROCESS (7),設定 ut_host 和 ut_addr 欄位,更新 ut_time 欄位
  4. 上面是通過 sshd 偽終端登入的情況,直接通過 xterm 登入 (本地登入) 的情況則簡單很多,xterm 直接設定 ut_type 為 USER_PROCESS,ut_id 設定為終端名稱的末 4 位。
  5. init 或 xterm 程序檢測到程序退出後,設定對應記錄的 ut_type 為 DEAD_PROCESS,清理 ut_user / ut_host / ut_time 欄位 (ut_time 被清理存疑)

遍歷內容

通過 setutent/getutent/endutent  可以遍歷的 utmp 檔案內容,像之前一樣,寫一個 demo 演示一下:

#include "../apue.h"
#include <utmp.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>

struct utmp* my_getutnam (char const* name)
{
  struct utmp *ptr = 0; 
  setutent (); 
  while ((ptr = getutent ()) != NULL)
  {
    struct in_addr addr = { 0 }; 
    addr.s_addr = ptr->ut_addr_v6[0]; 
    printf("type: %d, pid: %u, line: %s, utid: %.4s, user: %s, host: %s, exit: %d, sess: %d, time: %d, addr: %s\n", 
            ptr->ut_type, ptr->ut_pid, ptr->ut_line, ptr->ut_id, ptr->ut_user, ptr->ut_host, ptr->ut_exit.e_exit, 
            ptr->ut_session, ptr->ut_tv.tv_sec, inet_ntoa(addr));
    if (strcmp (name, ptr->ut_user) == 0)
      break; 
  }

  endutent (); 
  return (ptr); 
}

int main(int argc, char *argv[])
{
     struct utmp tmp;
     struct utmp *result;

     if (argc != 2) {
          fprintf(stderr, "Usage: %s username\n", argv[0]);
          exit(EXIT_FAILURE);
      }

      result = my_getutnam(argv[1]);
      if (result == NULL) {
          perror("getutnam");
          exit(EXIT_FAILURE);
      }

      tmp = *result; 
      printf ("find record!\n"); 
      exit(EXIT_SUCCESS);
}

執行 demo 得到如下輸出:

$ ./getutnam_ent abc
type: 2, pid: 0, line: ~, utid: ~~, user: reboot, host: 3.10.0-1160.80.1.el7.x86_64, exit: 0, sess: 0, time: 1670401340, addr: 0.0.0.0
type: 1, pid: 51, line: ~, utid: ~~, user: runlevel, host: 3.10.0-1160.80.1.el7.x86_64, exit: 0, sess: 0, time: 1670401349, addr: 0.0.0.0
type: 6, pid: 1321, line: ttyS0, utid: tyS0, user: LOGIN, host: , exit: 0, sess: 1321, time: 1670401349, addr: 0.0.0.0
type: 6, pid: 1320, line: tty1, utid: tty1, user: LOGIN, host: , exit: 0, sess: 1320, time: 1670401349, addr: 0.0.0.0
type: 7, pid: 28957, line: pts/0, utid: ts/0, user: yunhai01, host: 172.31.23.41, exit: 0, sess: 0, time: 1672645341, addr: 172.31.23.41
type: 8, pid: 24901, line: pts/1, utid: ts/1, user: , host: , exit: 0, sess: 0, time: 1671002330, addr: 172.31.23.41
type: 8, pid: 4965, line: pts/2, utid: ts/2, user: , host: , exit: 0, sess: 0, time: 1671619740, addr: 172.31.22.20
type: 8, pid: 27816, line: pts/3, utid: ts/3, user: , host: , exit: 0, sess: 0, time: 1672396080, addr: 172.31.23.41
getutnam: No such file or directory

輸出與 utmpdump 基本相同,並且驗證了 ut_line 欄位是完整的 (偽) 終端名稱,而 ut_id 只是其最後四位。

典型案例

who 命令的實現依賴 utmp 的資訊:

> who
yunhai01 pts/0        2023-01-01 11:48 (172.31.43.62)

通過 strace 的資訊可以觀察到這一點:

> strace who |& grep -E 'open|access'
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
open("/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) = 3
access("/var/run/utmpx", F_OK)          = -1 ENOENT (No such file or directory)
open("/var/run/utmp", O_RDONLY|O_CLOEXEC) = 3
open("/etc/nsswitch.conf", O_RDONLY|O_CLOEXEC) = 3
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libnss_files.so.2", O_RDONLY|O_CLOEXEC) = 3
open("/etc/group", O_RDONLY|O_CLOEXEC)  = 3
open("/etc/localtime", O_RDONLY|O_CLOEXEC) = 3

重點看 open 呼叫,有開啟 /var/run/utmp 的記錄,通過 starce 紀錄檔還發現在此之前嘗試過 /var/run/utmpx 檔案,這個可能是 linux 上衍生出的新的 utmp 檔案,不過在我這臺 CentOS 上並沒有這個檔案,所以走的還是老 utmp 檔案。

除 who 之外,w 命令也是通過 utmp 命令獲取正在登入使用者的資訊:

> w
 12:42:54 up 24 days, 20:20,  1 user,  load average: 0.04, 0.15, 0.18
USER     TTY      FROM             LOGIN@   IDLE   JCPU   PCPU WHAT
yunhai01 pts/0    172.31.43.62     11:48    6.00s  4.12s  0.00s w

並且列印了使用者當前正在執行的命令及負載資訊。通過 starce 也能看到同樣的結論:

> strace w |& grep utmp
read(4, "grep\0--color=auto\0utmp\0", 2047) = 23
access("/var/run/utmpx", F_OK)          = -1 ENOENT (No such file or directory)
open("/var/run/utmp", O_RDONLY|O_CLOEXEC) = 4
access("/var/run/utmpx", F_OK)          = -1 ENOENT (No such file or directory)
open("/var/run/utmp", O_RDONLY|O_CLOEXEC) = 5

通過紀錄檔發現 w 命令一次開啟兩個 utmp 檔案控制程式碼。

wtmp

一般位於 /var/log/wtmp,用於跟蹤各個登入和登出事件。login 程式在使用者登入時會填寫一條記錄到該檔案,登出時,init 程序將一個新記錄添寫到 wtmp 檔案,其中 ut_name 欄位清空。在系統重新啟動、更改系統時間和日期的前後,都會在 wtmp 檔案中添寫特殊的記錄項。wtmp 檔案中記錄的也是 utmp 結構體,因此可以通過 utmpdump 來檢視:

> utmpdump /var/log/wtmp
[2] [00000] [~~  ] [reboot  ] [~           ] [3.10.0-1160.80.1.el7.x86_64] [0.0.0.0        ] [Wed Dec 07 15:58:12 2022 CST]
[1] [00051] [~~  ] [runlevel] [~           ] [3.10.0-1160.80.1.el7.x86_64] [0.0.0.0        ] [Wed Dec 07 15:58:21 2022 CST]
[5] [01320] [tyS0] [        ] [ttyS0       ] [                    ] [0.0.0.0        ] [Wed Dec 07 15:58:24 2022 CST]
[5] [01319] [tty1] [        ] [tty1        ] [                    ] [0.0.0.0        ] [Wed Dec 07 15:58:24 2022 CST]
[6] [01319] [tty1] [LOGIN   ] [tty1        ] [                    ] [0.0.0.0        ] [Wed Dec 07 15:58:24 2022 CST]
[6] [01320] [tyS0] [LOGIN   ] [ttyS0       ] [                    ] [0.0.0.0        ] [Wed Dec 07 15:58:24 2022 CST]
[7] [01319] [tty1] [root    ] [tty1        ] [                    ] [0.0.0.0        ] [Wed Dec 07 15:59:52 2022 CST]
[7] [01749] [ts/0] [yunhai01] [pts/0       ] [172.31.43.62        ] [172.31.43.62   ] [Wed Dec 07 16:02:22 2022 CST]
[8] [01319] [tty1] [        ] [tty1        ] [                    ] [0.0.0.0        ] [Wed Dec 07 16:22:05 2022 CST]
[8] [01320] [tyS0] [        ] [ttyS0       ] [                    ] [0.0.0.0        ] [Wed Dec 07 16:22:05 2022 CST]
[8] [01749] [    ] [        ] [pts/0       ] [                    ] [0.0.0.0        ] [Wed Dec 07 16:22:05 2022 CST]
[1] [00000] [~~  ] [shutdown] [~           ] [3.10.0-1160.80.1.el7.x86_64] [0.0.0.0        ] [Wed Dec 07 16:22:11 2022 CST]
...

各個列與 utmp 檔案一致。last 命令讀取 wtmp 內容並展示給使用者:

> last
yunhai01 pts/0        172.31.22.20     Sat Jan  7 18:15   still logged in    
yunhai01 pts/0        172.31.43.62     Sun Jan  1 11:47 - 11:48  (00:00)    
yunhai01 pts/3        172.31.43.61     Wed Dec 21 20:54 - 14:13  (17:18)    
yunhai01 pts/2        172.31.22.20     Wed Dec 21 14:21 - 18:49  (04:27)   
yunhai01 pts/0        172.31.23.41     Tue Dec 13 11:52 - 11:26 (12+23:34)  
...
reboot   system boot  3.10.0-1160.80.1 Wed Dec  7 16:22 - 18:44 (31+02:22)  
yunhai01 pts/0        172.31.43.62     Wed Dec  7 16:02 - 16:22  (00:19)    
root     tty1                          Wed Dec  7 15:59 - 16:22  (00:22)    
reboot   system boot  3.10.0-1160.80.1 Wed Dec  7 15:58 - 16:22  (00:23)    
yunhai01 pts/0        172.31.43.62     Wed Dec  7 15:28 - 15:53  (00:25)     
yunhai01 tty1                          Wed Dec  7 15:18 - 15:18  (00:00)    
yunhai01 pts/0        172.31.43.62     Wed Dec  7 14:49 - 15:18  (00:29)    
reboot   system boot  3.10.0-1160.80.1 Wed Dec  7 14:48 - 15:22  (00:33)    
yunhai01 pts/0        172.31.43.62     Wed Dec  7 14:45 - 14:48  (00:02)    
reboot   system boot  3.10.0-1160.80.1 Wed Dec  7 14:45 - 14:48  (00:03)    
yunhai01 pts/0        172.31.43.62     Wed Dec  7 14:44 - 14:45  (00:00)    
reboot   system boot  3.10.0-1160.80.1 Wed Dec  7 14:43 - 14:48  (00:04)   
...
yunhai01 pts/0        172.31.22.20     Fri Nov 18 11:39 - 14:26  (02:46)    
root     pts/0        172.31.22.20     Fri Nov 18 11:12 - 11:15  (00:02)    
reboot   system boot  3.10.0-1160.76.1 Fri Nov 18 11:00 - 14:48 (19+03:48)  

wtmp begins Fri Nov 18 11:00:28 2022

last 讀取 wtmp 檔案並整理其中的記錄,將同一使用者的登入登出記為一條記錄,分別列印使用者名稱、終端、主機、登入登出時間,通過選項可以控制列印的內容,也可以篩選特定使用者、終端的記錄,例如只看 pts/2 的記錄:

> last pts/2
yunhai01 pts/2        172.31.22.20     Wed Dec 21 14:21 - 18:49  (04:27)    
yunhai01 pts/2        172.31.22.20     Tue Dec 20 15:34 - 17:39  (02:04)    
yunhai01 pts/2        172.31.23.41     Tue Dec 20 12:15 - 13:52  (01:36)    
yunhai01 pts/2        172.31.22.20     Mon Dec 19 16:56 - 07:22  (14:25)    
yunhai01 pts/2        172.31.22.20     Mon Dec 19 10:28 - 15:47  (05:19)    
yunhai01 pts/2        172.31.22.20     Fri Dec  2 14:31 - 16:42  (02:10)    

wtmp begins Fri Nov 18 11:00:28 2022

也可以只看使用者名稱:

> last root
root     tty1                          Wed Dec  7 15:59 - 16:22  (00:22)    
root     pts/0        172.31.22.20     Fri Nov 18 11:17 - 11:39  (00:21)    
root     pts/0        172.31.22.20     Fri Nov 18 11:12 - 11:15  (00:02)    

wtmp begins Fri Nov 18 11:00:28 2022

預設的展示順序是新的記錄在上面、舊的記錄在下面。

btmp

wtmp 記錄是登入成功的使用者,對於失敗的因為沒有走到 login 這一步,所以並不會記錄下來,btmp 檔案是專門用來記錄登入失敗資訊的,一般位於  /var/log/btmp,其中記錄的也是 utmp 結構體,可以通過 utmpdump 檢視:

> sudo utmpdump /var/log/btmp
Utmp dump of /var/log/btmp
[6] [32150] [    ] [yunhai01] [ssh:notty   ] [172.31.43.61        ] [172.31.43.61   ] [Fri Jan 06 10:57:52 2023 CST]
[6] [32344] [    ] [yunhai01] [ssh:notty   ] [172.31.22.20        ] [172.31.22.20   ] [Sat Jan 07 18:15:18 2023 CST]

各個列與 wtmp 無異,只有終端名因未分配而留空。lastb 命令負責讀取 btmp 檔案:

> sudo lastb
[sudo] password for yunhai01: 
yunhai01 ssh:notty    172.31.22.20     Sat Jan  7 18:15 - 18:15  (00:00)    
yunhai01 ssh:notty    172.31.43.61     Fri Jan  6 10:57 - 10:57  (00:00)    

btmp begins Fri Jan  6 10:57:52 2023

當懷疑有人嘗試破解密碼,可以通過 lastb 來定位攻擊來源。

最後補充一點,btmp 並不屬於 POSIX 標準的一部分,在 mac 上就沒有 lastb 命令。

結語

本文介紹了 unix 系統資料檔案相關的內容,其中介紹的很多介面都是不可重入的,因此只能在單執行緒非訊號處理器中使用,其實現代 unix 都提供了可重入版本,在現有介面上增加 _r 字尾即可,例如這樣就可以在更多的場景中使用它們了。感興趣的可以檢視 man 手冊頁。

參考

[1]. mac vscode c/c++ 解決include路徑問題

[2]. linux使用者實現root使用者空密碼登入

[3]. 【Ubuntu 20.04】useradd 建立使用者無法登入圖形介面解決方案

[4]. Linux多個檔案按列合併的多種場景操作方式

[5]. mac下的strace命令

[6]. Linux筆記:使用stat函數實現ls -l的功能(getpwuid函數 getgrgid函數使用)

[7]. linux /etc/shadow檔案詳解

[8]. linux使用者認證機制

[9]. 模擬密碼登陸過程

[10]. ssh免密碼登入

[11]. ssh設定指定金鑰檔案登入linux

[12]. SSH 免密登入(設定後仍需輸入密碼的原因及解決方法)

[13]. linux使用者剔除輔助組,用usermod、gpasswd、Shell Script、Manual Method將使用者新增到組

[14]. 使用 utmpdump 監控 CentOS 使用者登入歷史