|
前言
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 的定义位于 文件中:- /* 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 mayun@192.168.1.118
- mayun@192.168.1.118'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 mayun@192.168.1.118
- /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
- mayun@192.168.1.118's password:
- Number of key(s) added: 1
- Now try logging into the machine, with: "ssh 'mayun@192.168.1.118'"
- 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 mayun@192.168.1.118
- 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 的定义位于 文件中:- 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
复制代码 看起来像是因为没有包含 从而不识别 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 的定义位于 文件中:- /* 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 结构体的定义位于 文件中:- 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 结构体位于 文件中:- 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 结构体位于 文件中:- 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 <A.Ballardie@cs.ucl.ac.uk>
- 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 结构体位于 文件中:- 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 结构的定义位于 文件中:- #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 字段
下面是进程生命周期过程中各个字段的变更逻辑:
- init 进程根据 inittab 新建进程时如果没有匹配的空记录 (通过 ut_id) 则插入一条新的 utmp 记录,ut_id 设置为 inittab 中对应的字段,并且设置 ut_pid 和 ut_time 字段,最后设置 ut_type 为 INIT_PROCESS (5)。
- mingettty 或 agetty 进程根据 pid 查找入口设置 ut_line,修改 ut_type 为 LOGIN_PROCESS (6),更新 ut_time 字段
- login 进程校验用户登录信息后,设置 ut_type 为 USER_PROCESS (7),设置 ut_host 和 ut_addr 字段,更新 ut_time 字段
- 上面是通过 sshd 伪终端登录的情况,直接通过 xterm 登录 (本地登录) 的情况则简单很多,xterm 直接设置 ut_type 为 USER_PROCESS,ut_id 设置为终端名称的末 4 位。
- 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 用户登录历史
来源:https://www.cnblogs.com/goodcitizen/p/things_about_unix_system_data_file_and_information.html
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作! |
|