RPC中的UNIX认证

来源:互联网 发布:如何在C语言中用开根 编辑:程序博客网 时间:2024/06/10 09:18

        上一篇文章中我们讲解了一个RPC请求的处理流程,其中涉及到了RPC请求报文中的认证信息。Linux支持多种认证方式,不同的认证方式中认证信息的内容是不同的,需要按照认证方式进行处理。这篇文件中我们以UNIX认证为例,详细讲解UNIX认证方式中认证信息的处理过程。

        Linux中,每种认证方式用数据结构auth_ops表示,每种认证方式都需要实现这个数据结构中的函数。

struct auth_ops {        char *  name;           // 这是认证方式的名称        struct module *owner;        int     flavour;        // 这是认证方式的编号        // 这是认证信息处理函数,负责解析RPC请求报文中的Credential字段和Verifier字段         // 然后填充RPC应答报文中的Verifier字段        int     (*accept)(struct svc_rqst *rq, __be32 *authp);        // 这个函数负责一些清理工作,当RPC请求处理结束后会调用这个函数        int     (*release)(struct svc_rqst *rq);        // 这是释放认证域的函数,auth_domain是一个与认证信息相关的数据结构,        // 存放了认证相关的数据        void    (*domain_release)(struct auth_domain *);                // 这个函数负责对用户进行认证,负责填充auth_domain结构,        // 这个函数被svc_program结构中的pg_authenticate函数调用        int     (*set_client)(struct svc_rqst *rq);};

UNIX认证中,这个数据结构的定义如下

struct auth_ops svcauth_unix = {        .name           = "unix",        .owner          = THIS_MODULE,        .flavour        = RPC_AUTH_UNIX,        .accept         = svcauth_unix_accept,        .release        = svcauth_unix_release,        .domain_release = svcauth_unix_domain_release,        .set_client     = svcauth_unix_set_client,};

1.svcauth_unix_accept

这个函数负责解析RPC请求报文中的认证信息,并组装RPC应答报文中的认证信息。这个函数只解析数据,不进行用户身份验证。这个函数的完整流程如下:

static intsvcauth_unix_accept(struct svc_rqst *rqstp, __be32 *authp){        // 调用这个函数前已经解析出了Credential中的Flavor字段,确定了RPC请求报文中包含的是UNIX认证数据,        // 接着解析报文中下一个字段Length,这个字段表示认证信息的长度        struct kvec     *argv = &rqstp->rq_arg.head[0];        // resv是RPC应答消息的缓存,调用这个函数前已经封装了XID、Message Type、Reply State三个字段,        // 这个函数中需要继续封装Verifier字段.        struct kvec     *resv = &rqstp->rq_res.head[0];        // 这是保存认证信息的数据结构,从RPC请求报文中解析出的UID、GID将保存在这个数据结构中.        struct svc_cred *cred = &rqstp->rq_cred;        u32             slen, i;        // 由于argv指向的缓存中保存的是RPC请求报文中的数据,因此argv->iov_len是RPC报文剩余数据的长度.        int             len   = argv->iov_len;      // 这是缓冲区长度  96 剩余的字节数        // 先将用户信息设置为空.        cred->cr_group_info = NULL;        cred->cr_principal = NULL;        rqstp->rq_client = NULL;        // length    stamp    machine name length        // 这里在检查RPC报文中是否有足够的数据,RPC报文中的数据经过了XDR编码,以4字节为一个单位.        // 通过对比RPC报文,3*4表示LENGTH、STAMP、machine name length三个字段的长度.        // 如果缓存中的数据小于这个长度,就说明RPC请求报文中的认证信息有问题,直接返回.        if ((len -= 3*4) < 0)                return SVC_GARBAGE;        // 取出RPC请求报文中标明的认证信息的长度        svc_getu32(argv);                       /* length */        // 这是认证信息中封装的时间戳        svc_getu32(argv);                       /* time stamp */        // 解析认证信息中machine name的长度        slen = XDR_QUADLEN(svc_getnl(argv));    /* machname length */        // 这又是一个检查函数,(slen + 3)*4表示machine name、UID、GID三个字段的长度        if (slen > 64 || (len -= (slen + 3)*4) < 0)                goto badcred;        // 这里直接跳过了主机名,没有使用主机名的信息.        argv->iov_base = (void*)((__be32*)argv->iov_base + slen);       /* skip machname */         argv->iov_len -= slen*4;                // 因此,现在argv指向了UID字段

感觉这个函数中虽然在一些地方检查了RPC请求报文中剩余信息长度,但是好像不严谨。


2.svcauth_unix_set_client

        这是UNIX认证方式中的用户身份认证函数,这个函数有点复杂,因为这个函数涉及到了RPC中的缓存机制,我们将在下篇文章中介绍这种机制。这里先以NFS服务为例简单讲讲这种缓存是干什么用的。我们在前面的文章中讲过,当服务器端开启NFS服务以后,会将允许挂载的客户端范围和文件系统的信息解析出来,保存在内存中。但是需要注意,这保存在了线程rpc.mountd的内存地址中。当内存接收到NFS请求后内核需要查询这些信息,确认客户端是否有访问权限,内核会将查询的结果作为缓存保存在内核空间中,这就是RPC缓存。当下个请求到来时,如果缓存中的数据仍然有效,就不需要向rpc.mountd查询了。在实际操作过程中,内核和rpc.mountd线程是通过proc文件系统进行通信的,当rpc.mountd解析完/var/lib/nfs/etab中的数据后,会通过select()函数监听proc文件系统中的一个文件。内核需要向rpc.mountd查询数据时就将相应的信息写入这个文件中,rpc.mountd读取这个文件中的数据,进行查询,将查询结果写回这个文件中,内核就得到结果了。先说这么多,下篇文章中我们进一步解释。

    下面开始讲解svcauth_unix_set_client函数流程。

(1)跳过NULL例程

rqstp->rq_client = NULL;if (rqstp->rq_proc == 0)    // RPC例程编号0,不需要认证        return SVC_OK;

      RPC中,编号为0的例程是一个标准例程,这个例程的作用是测试客户端和服务器端的连接是否正常,这个例程没有处理函数,也不需要认证。因此,如果例程编号是0,则svcauth_unix_set_client直接退出。

      这里还需要说说rqstp->rq_client,这个字段的类型是struct auth_domain,这个数据结构的定义如下:

struct auth_domain {        struct kref             ref;    // 这个结构的引用计数        struct hlist_node       hash;   // 有的auth_domain结构保存在一个全局hash表中        char                    *name;          // 名称,如 192.168.6.0/24        struct auth_ops         *flavour;       // 认证方式};

这是一个通用的数据结构,被各种认证方式使用。UNIX认证方式中对应的数据结构是

struct unix_domain {        struct auth_domain      h;        /* other stuff later */};

auth_domain和unix_domain之间的关系类似于文件系统中索引节点inode和ext2_inode_info的关系。auth_domain中包含了各种认证方式中都需要使用的信息,unix_domain中保存了UNIX认证方式中特定的信息,只不过UNIX认证方式中不需要其他信息,因此unix_domain中只有一个字段h。函数svcauth_unix_set_client对用户身份进行认证,认证后的信息就保存在数据结构unix_domain中,也就是保存在rqstp->rq_client中。

(2)用户身份验证

switch (rqstp->rq_addr.ss_family) {     // 这是RPC客户端的地址类型case AF_INET:   // IPv4网络        sin = svc_addr_in(rqstp);        sin6 = &sin6_storage;        ipv6_addr_set_v4mapped(sin->sin_addr.s_addr, &sin6->sin6_addr);        break;  case AF_INET6:        sin6 = svc_addr_in6(rqstp);        break;  default:        BUG();  }ipm = __ip_map_lookup(sn->ip_map_cache, rqstp->rq_server->sv_program->pg_class,                            &sin6->sin6_addr);// cache_check()就是RPC缓存了,如果缓存中的数据无效,就向用户态进程请求数据. switch (cache_check(sn->ip_map_cache, &ipm->h, &rqstp->rq_chandle)) {        default:                BUG();          case -ETIMEDOUT:                return SVC_CLOSE;        case -EAGAIN:                return SVC_DROP;        case -ENOENT:                return SVC_DENIED;      // 如果不允许这个客户端访问,这里就拒绝了.         case 0:                 // 这里设置了RPC请求的认证域    cache_check中设置了ipm->m_client                rqstp->rq_client = &ipm->m_client->h;   // 这是一个auth_domain结构,就采用这个认证域了                kref_get(&rqstp->rq_client->ref);                ip_map_cached_put(xprt, ipm);                break;  }

这是UNIX认证方式中用户身份认证的主要代码段,rqstp->rq_addr中保存的是客户端的地址,包括IP地址以及地址类型,这是从RPC请求报文的IP头中解析出来的,客户端无法伪造这个地址。__ip_map_lookup()是和RPC缓存相关的一个函数,这个函数主要在操作一个数据结构ip_map

struct ip_map {        struct cache_head       h;      // 这是RPC缓存需要使用的数据结构        char                    m_class[8]; /* e.g. "nfsd" */   // 这是一类RPC服务        struct in6_addr         m_addr;         // 这是客户端的地址        struct unix_domain      *m_client;      // 认证域};

struct ip_map结构的定义如上所示,__ip_map_lookup()在系统中查找是否存在符合条件的ip_map结构,如果不存在就创建一个新的ip_map结构。查找条件包含两个:(1)m_class 这是一个字符串,相当于一个类型,比如nfsd,通过__ip_map_lookup()第二个参数设置;(2)m_addr 这是客户端的地址,通过__ip_map_lookup()第三个参数设置。

cache_check()也是和RPC缓存相关的一个函数,这个函数的作用是检查ip_map中的数据是否有效,如果无效就向rpc.mountd进程发送请求更新这个数据结构中的数据。更新结束后,m_client就包含有效的数据了。这个更新过程是通过文件/proc/net/rpc/auth.unix.ip/channel实现的。内核将m_class和m_addr写入channel中,rpc.mountd中的处理函数是auth_unix_ip(),这个函数从channel文件中读取客户端的IP地址,查找这个客户端所属于的认证区域,将结果写回channel文件中,内核解析channel文件中的数据。auth_unix_ip()的代码可以在nfs-utils中找到。这里以一个例子说说处理后的结果。假设服务器端导出了下列两个文件系统

/tmp/nfs/root1  192.168.6.0/24(rw,sync)
/tmp/nfs/root2  192.168.0.0/16(rw,sync)

如果客户端192.168.6.14向服务器发起了RPC请求,请求的是文件系统/tmp/nfs/root1。通过上面文件系统的配置信息我们可以看到,192.168.6.14属于两个认证域192.168.6.0/24和192.168.0.0/16,因此cache_check()执行结束后,m_client中各个字段的信息如下

struct auth_domain {
        struct kref             ref; 
        struct hlist_node       hash;
        char                    *name;          // 名称,这是客户端所属于的所有的认证域,这里的值是 192.168.6.0/24,192.168.0.0/16
        struct auth_ops         *flavour;       // 认证方式,这里就是UNIX认证方式操作函数集合
};
因此,这里只是一个基本的认证过程,只要客户端对服务器有访问权限,认证就会通过。假设客户端的地址是192.168.5.10,请求的是/tmp/nfs/root1中的数据,虽然192.168.5.10对root1没有访问权限,但是对root2有访问权限,认证也会通过。NFS在处理具体请求中会再次检查192.168.5.10是否有root1的访问权限。

(3)设置用户身份

gi = unix_gid_find(cred->cr_uid, rqstp); switch (PTR_ERR(gi)) {case -EAGAIN:        return SVC_DROP;case -ESHUTDOWN:        return SVC_CLOSE;case -ENOENT:        break;     // 服务器端没有查找数据,使用RPC请求报文中的GIDSdefault:        put_group_info(cred->cr_group_info);        cred->cr_group_info = gi;     // 使用服务器端的GIDS}

Linux系统中,每个用户都有自己的UID,同时用户会属于一个或多个用户组,每个用户组用一个GID表示。RPC请求报文中包含了用户的UID和GIDs,但是这是客户端中的信息,不是服务器端的信息。比如用户在客户端的UID是500,而服务器端可能不存在UID是500的用户,或者虽然存在UID是500的用户,但是这个用户的GIDs和客户端的GIDs不同,因此UNIX认证需要对这种情况进行处理,这个处理过程是通过函数unix_gid_find()完成的。这个处理过程也使用了RPC缓存,内核将从RPC请求报文中解析出的UID写到文件/proc/net/rpc/auth.unix.gid/channel中,rpc.mountd读取channel文件中的数据,查找这个用户所属于的用户组,将查找结果写回channel文件中,内核再解析channel文件中的数据。

3.svcauth_unix_domain_release

释放一个auth_domain结构

static void svcauth_unix_domain_release(struct auth_domain *dom){        // 找到unix_domain结构        struct unix_domain *ud = container_of(dom, struct unix_domain, h);        // 释放内存        kfree(dom->name);        kfree(ud);}

这个函数很简单,直接释放内存就可以了。

4.svcauth_unix_release

这个函数也很简单

static intsvcauth_unix_release(struct svc_rqst *rqstp){        /* Verifier (such as it is) is already in place.         */        if (rqstp->rq_client)           // rqstp->rq_client是一个认证域结构(auth_domain)                auth_domain_put(rqstp->rq_client);      // 释放这个认证域        rqstp->rq_client = NULL;        // 撤销关联的认证域        if (rqstp->rq_cred.cr_group_info)       // 释放group info结构                put_group_info(rqstp->rq_cred.cr_group_info);        rqstp->rq_cred.cr_group_info = NULL;        return 0;}

当RPC请求处理完毕后需要执行这个函数,这个函数的主要任务也是释放内存。auth_domain_put()减少auth_domain的引用计数,当计数减到0时执行svcauth_unix_domain_release()。