如何免密 ssh 登錄空密碼賬戶?getpwent 為何會返回比 /etc/passwd 文件更多的內容?同樣是修改附加組 ID,usermod -G 和 gpasswd -a/-d 有何區別?/etc/networks 有什麼用處?gethostbyname / getservbyname 和 g... ...
前言
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 一樣,我沒找到這些介面的使用場景,一般在編程階段就要確