进程凭据是指unix domain socket(AF_UNIX)发送方的pid,uid,gid信息。
只能是AF_UNIX,不能是AF_INET的原因很简单,AF_INET可能都不在同一台机器上,pid,uid,gid没有意义。
在以下的内容中,socket server作为接收方,socket client作为发送方,当然反过来也没有问题,不过本文以这个为例。
有两种方法传递进程凭据:
1、SO_PEERCRED
man pages中的解释:
SO_PEERCRED
Return the credentials of the foreign process connected to
this socket. This is possible only for connected AF_UNIX
stream sockets and AF_UNIX stream and datagram socket pairs
created using socketpair(2); see unix(7). The returned
credentials are those that were in effect at the time of the
call to connect(2) or socketpair(2). The argument is a ucred
structure; define the _GNU_SOURCE feature test macro to obtain
the definition of that structure from <sys/socket.h>. This
socket option is read-only.
在socket server端调用如下代码:
struct ucred cred;
socklen_t len;
len = sizeof(struct ucred);
// ......, after accept
getsockopt(client_fd, SOL_SOCKET, SO_PEERCRED, &cred, &len);
printf("Credentials from SO_PEERCRED: pid=%d, uid=%d, gid=%d\n", cred.pid, cred.uid, cred.gid);
注意编译时先#define _GNU_SOURCE,再#include <sys/socket.h>,否则struct ucred的定义找不到的;
需要对client_fd调用getsockopt,如果对listen_fd调用的话,每次都是socket server自己的pid,uid,gid,没啥用处;
得到的pid,uid,gid是socket client在connect或者socketpair时的值;
在socket client端无需特殊的操作,也无需发送消息数据。
2、SO_PASSCRED + SCM_CREDENTIALS
man pages上的解释:
SO_PASSCRED
Enables the receiving of the credentials of the sending
process in an ancillary message. When this option is set and
the socket is not yet connected a unique name in the abstract
namespace will be generated automatically. Expects an integer
boolean flag.
SCM_CREDENTIALS
Send or receive UNIX credentials. This can be used for
authentication. The credentials are passed as a struct ucred
ancillary message. Thus structure is defined in
<sys/socket.h> as follows:
struct ucred {
pid_t pid; /* process ID of the sending process */
uid_t uid; /* user ID of the sending process */
gid_t gid; /* group ID of the sending process */
};
Since glibc 2.8, the _GNU_SOURCE feature test macro must be
defined (before including any header files) in order to obtain
the definition of this structure.
The credentials which the sender specifies are checked by the
kernel. A process with effective user ID 0 is allowed to
specify values that do not match its own. The sender must
specify its own process ID (unless it has the capability
CAP_SYS_ADMIN), its user ID, effective user ID, or saved set-
user-ID (unless it has CAP_SETUID), and its group ID,
effective group ID, or saved set-group-ID (unless it has
CAP_SETGID). To receive a struct ucred message the
SO_PASSCRED option must be enabled on the socket.
socket client同样无需特殊的操作,不过需要sendmsg之后,接收端才能够得到凭据,凭据数据由内核填充到消息结构体的控制数据中,如果发送方想自己填充也可以,伪造的数据会导致sendmsg失败。除非有root权限,才可能伪造pid,uid,gid信息。
socket client构建消息结构体的代码为:
// 权限数据由内核填充还是程序填充
#define AUTO_FILL_DATA
struct msghdr msgh;
struct iovec iov;
int data = 0xbeef;
#ifndef AUTO_FILL_DATA
union {
struct cmsghdr cmh;
char control[CMSG_SPACE(sizeof(struct ucred))];
/* Space large enough to hold a ucred structure */
} control_un;
struct cmsghdr *cmhp;
struct ucred *ucp;
#endif
/* On Linux, we must transmit at least 1 byte of real data in
order to send ancillary data */
msgh.msg_iov = &iov;
msgh.msg_iovlen = 1;
iov.iov_base = &data;
iov.iov_len = sizeof(int);
msgh.msg_name = NULL;
msgh.msg_namelen = 0;
#ifdef AUTO_FILL_DATA
msgh.msg_control = NULL;
msgh.msg_controllen = 0;
#else
msgh.msg_control = control_un.control;
msgh.msg_controllen = sizeof(control_un.control);
cmhp = CMSG_FIRSTHDR(&msgh);
cmhp->cmsg_len = CMSG_LEN(sizeof(struct ucred));
cmhp->cmsg_level = SOL_SOCKET;
cmhp->cmsg_type = SCM_CREDENTIALS;
ucp = (struct ucred *) CMSG_DATA(cmhp);
ucp->pid = getpid();
ucp->uid = getuid();
ucp->gid = getgid();
#endif
socket server端,需要设置期望接收的消息的结构体:
struct msghdr msgh;
struct iovec iov;
int data;
struct ucred *ucredp;
struct cmsghdr *cmhp;
union {
struct cmsghdr cmh;
char control[CMSG_SPACE(sizeof(struct ucred))];
/* Space large enough to hold a ucred structure */
} control_un;
control_un.cmh.cmsg_len = CMSG_LEN(sizeof(struct ucred));
control_un.cmh.cmsg_level = SOL_SOCKET;
control_un.cmh.cmsg_type = SCM_CREDENTIALS;
msgh.msg_control = control_un.control;
msgh.msg_controllen = sizeof(control_un.control);
msgh.msg_iov = &iov;
msgh.msg_iovlen = 1;
iov.iov_base = &data;
iov.iov_len = sizeof(int);
msgh.msg_name = NULL;
msgh.msg_namelen = 0;
在accept之后,对client_fd调用:
setsockopt(client_fd, SOL_SOCKET, SO_PASSCRED, &optval, sizeof(optval))
其中int optval = 1;
然后使用如下代码接收消息,并获取控制信息,即凭据:
if (recvmsg(client_fd, &msgh, 0) <= 0)
{
printf("ERROR recvmsg\n");
goto out;
}
cmhp = CMSG_FIRSTHDR(&msgh);
if (cmhp == NULL || cmhp->cmsg_len != CMSG_LEN(sizeof(struct ucred)))
{
printf("bad cmsg header / message length");
goto out;
}
if (cmhp->cmsg_level != SOL_SOCKET)
{
printf("cmsg_level != SOL_SOCKET");
goto out;
}
if (cmhp->cmsg_type != SCM_CREDENTIALS)
{
printf("cmsg_type != SCM_CREDENTIALS");
goto out;
}
ucredp = (struct ucred *) CMSG_DATA(cmhp);
printf("Received credentials pid=%d, uid=%d, gid=%d\n",
ucredp->pid, ucredp->uid, ucredp->gid);
setsockopt(listen_fd, SOL_SOCKET, SO_PASSCRED, &optval, sizeof(optval))
来获取client_fd上的凭据。但是,这种方法不保险,有些内核是不支持的,比如android goldfish 3.4。
因为在accept的时候,client_fd没有从listen_fd继承相关的标志位,所以会不支持。
在内核中添加这个patch即可:http://patchwork.ozlabs.org/patch/289624/
参考:
https://sourceware.org/bugzilla/show_bug.cgi?id=6545
http://man7.org/tlpi/code/online/dist/sockets/scm_cred_recv.c.html
http://man7.org/tlpi/code/online/dist/sockets/scm_cred_send.c.html