ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

LDAP/SASL/GSSAPI/Kerberos编程API(5)--krb5应用服务

2021-05-16 08:51:19  阅读:194  来源: 互联网

标签:krb5 00 vmsrv -- Kerberos sock context printf


本篇介绍的不是本地应用,而是体现krb5真正价值的应用服务.分服务器端和客户端,即C/S,简单的说,server不架设自己的鉴权功能,client访问server的用户认证交由Kerberos处理
本篇要用到开发库libkrb5-dev,参考了MIT krb5源码中的演示例子appl/sample/sserver/sserver.c(Sample Kerberos v5 server)、appl/sample/sclient/sclient.c(Sample Kerberos v5 client)
本实验的目的:以最少设置实现最基本krb5应用服务
本实验的功能:服务器是一个面向连接TCP循环服务器,没使用并发功能,即多个客户连接到服务器,先到先服务,只有上一个客户完成服务才轮到下个客户;
客户和服务器成功建立一条连接后可开始传输数据,客户需先传输用户鉴权,认证失败服务器关闭这条连接退出服务,认证成功便在这条连接上进入服务;
客户端发送一串字符到服务器,然后服务器echo(回显)这些字符串给客户端;

说明:1)因为是面向连接,所以认证成功服务器继续维系这条连接的客/服传输,因此客户是可信的;客户不可信在鉴权失败后被服务器关闭掉连接了;
2)MIT krb5源码中有面向无连接UDP的演示例子,就不象TCP那样认证成功后直接传输数据,UDP那样的客户有可能不可信的;UDP例子好像每次的传输数据都要利用krb5进行加解密以达到客户可信效果,本人没深究不表
3)按TCP/UDP协议,面向连接是可靠的,面向无连接是不可靠的;协议的'可靠'术语是指数据包能够完整无误的送达;
本文面向连接/面向无连接的'连接'是基于本实验的语境下,按本人方便自己理解的'可信/不可信'类似于一个登录会话,这样理解可能不妥,请读者判别;

一.准备工作

Kerberos服务器(KDC)   vmkdc
应用服务器            vmsrv.ctp.net   192.168.1.20
客户机                vmcln.ctp.net

以上主机的安装配置请参考<Kerberos+LDAP+NFSv4 实现单点登录>系列文章

领域为CTP.NET,并创建了krblinlin@CTP.NET用户主体和mysv/vmsrv.ctp.net应用服务主体

mysv为本篇实验的应用服务名,常见的应用服务有ldap、nfs

KDC上添加应用服务器
root@vmkdc:~# kadmin -l
kadmin> add -r mysv/vmsrv.ctp.net
新增

Max ticket life [unlimited]:
Max renewable life [unlimited]:
Principal expiration time [never]:
Password expiration time [never]:
Attributes [disallow-svr, disallow-proxiable, disallow-renewable, disallow-forwardable, disallow-postdated]:
Policy [default]:
上面一路回车缺省

kadmin> modify -a -disallow-svr mysv/vmsrv.ctp.net
删除disallow-svr,使mysv/vmsrv.ctp.net成为应用服务器

kadmin> ext -k /home/linlin/srv/krb5.keytab mysv/vmsrv.ctp.net
导出keytab

将krb5.keytab复制到vmsrv.ctp.net服务器的/etc目录下,确保krb5.keytab权限为root拥有,仅root可读

二.应用服务器
1.源代码

//源文件名:krbsrv.c
#include <krb5.h>
#include <stdio.h>
#include <netdb.h>
int main(int argc, char *argv[])
{
    krb5_context context;
    krb5_auth_context auth_context = NULL;
    krb5_ticket * ticket;
    struct sockaddr_in peername;    
    int  namelen = sizeof(peername);       

    krb5_error_code retval;
    krb5_principal server;    

    retval = krb5_init_context(&context);

    if (retval) {
        exit(1);
    }

    retval = krb5_sname_to_principal(context, NULL, 
                                     "mysv",//应用服务名
                                     KRB5_NT_SRV_HST, &server);
    if (retval) {
        exit(1);
    }

    int acc=0;// #1

//--v-- #2
    int on = 1;
    int sock = -1;                      /* incoming connection fd */ 
    struct sockaddr_in sockin;
    if ((sock = socket(PF_INET, SOCK_STREAM, 0)) < 0) {
            exit(3);
    }
    // Let the socket be reused right away  
    (void) setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (char *)&on,sizeof(on));
    sockin.sin_family = AF_INET;
    sockin.sin_addr.s_addr = 0;
    sockin.sin_port = htons(12345);//端口号
    if (bind(sock, (struct sockaddr *) &sockin, sizeof(sockin))) {
            exit(3);
    }
    if (listen(sock, 5) == -1) {    
            exit(3);
    }

    for(;;)

//--^--
    { 

//--v-- #3
        if ((acc = accept(sock, (struct sockaddr *)&peername, &namelen)) == -1){
            exit(3);
        }
//--^--

//--v-- #4

//--^--

        char str[100];
        int done ,n;
        // #5
        retval = krb5_recvauth(context, &auth_context, (krb5_pointer)&acc,
                           NULL, //应用服务版本,如"KRB5_sample_protocol_v1.0",这里设为NULL不验证版本
                           server,
                           0,    // no flags 
                           NULL, //缺省NULL会读取/etc/krb5.keytab
                           &ticket);
        if (retval) 
        {   printf(" auth failed\n");// 鉴权失败要关闭连接,见#6处      
        }
        else
        {   printf(" auth ok\n");
            done=0;
            do{ 
        n=recv(acc,str,100,0);
                if (n<=0) //因为客户端一次性发过来,服务端有可能要分多次recv,所以要while循环,直到n=0
                { if (n<0)
            printf("error-recv\n");
                  done=1; //退出while循环
                  printf("recv done:%i\n",done);
                }
                printf("recv from client:%s\n",str);
                sleep(10);//观察测试目的

                if (!done)
                {
                  if (send(acc,str,n,0)<0)
                  { printf("error-send\n");
                    done=1;
                    printf("send done:%i\n",done);
                  }
                  printf("send to client ok:%s",str);
                }
            }while(!done);    
        }
        close(acc);// #6
        printf("close\n");

//--v-- #7

//--^--         
        krb5_auth_con_free(context, auth_context);
        auth_context = NULL;  //一定要此句,否则进入下个for循环出现段错误  
    };//for无限循环,进入下一个accept(),因是循环服务器,所以多个客户的连接是要排队,要等到上一个close()

    krb5_free_ticket(context, ticket);  // #8 
    krb5_free_principal(context, server);
    krb5_free_context(context);
    exit(0);
}

2.解析
1)见代码注释

2)
krb5_recvauth是阻塞的,不能在#5处设置非阻塞,否则客户端在#12处出错
见MIT krb5源码../src/krb5/1.18.3-4/src/lib/krb5/os/net_read.c中注释

/*
 * krb5_net_read() reads from the file descriptor "fd" to the buffer
 * "buf", until either 1) "len" bytes have been read or 2) cannot
 * read anymore from "fd".  It returns the number of bytes read
 * or a read() error.  (The calling interface is identical to
 * read(2).)
 *
 * XXX must not use non-blocking I/O
 */

3)
#8处语句不能提前放在for循环里#7处;测试放在#7处票据过期auth failed,进入下个for循环会出现段错误

3.编译
linlin@debian:~$ gcc -o krbsrv krbsrv.c -lkrb5

三.客户机
1.源代码

//源文件名:krbcln.c
#include <krb5.h>
#include <stdio.h>
#include <string.h>
#include <netdb.h>
#include <signal.h>

int main(int argc, char *argv[])
{
    int sock;
    krb5_context context;

    krb5_error_code retval;
    krb5_ccache ccdef;
    krb5_principal client, server;
    krb5_error *err_ret;
    krb5_ap_rep_enc_part *rep_ret;
    krb5_auth_context auth_context = 0;

    retval = krb5_init_context(&context);
    if (retval) {
        printf("err:while initializing krb5\n");
        exit(1);
    }

    (void) signal(SIGPIPE, SIG_IGN);

    retval = krb5_sname_to_principal(context, 
                                     "vmsrv.ctp.net", 
                                     "mysv",//应用服务名,同服务端,这两个应构成 mysv/vmsrv.ctp.net
                                     KRB5_NT_SRV_HST, &server);
    if (retval) {
        printf("err:creating server name for host\n");
        exit(1);
    }

    sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0) {
            printf(  " socket: error\n" );
            exit(1);
    }

    struct sockaddr_in remote;

    remote.sin_addr.s_addr=inet_addr("192.168.1.20");// #10   vmsrv.ctp.net地址

    remote.sin_family = AF_INET;  
    remote.sin_port = htons(12345); //端口号
    bzero( &(remote.sin_zero)  ,8);

    if (connect(sock, (struct sockaddr *)&remote, sizeof(struct sockaddr)) < 0) {
            printf( " connect: error\n");
            close(sock);
            sock = -1;
            exit(1);
    }

    if (sock == -1)
        /* Already printed error message above.  */
        exit(1);
    printf("connected\n");

    retval = krb5_cc_default(context, &ccdef);
    if (retval) {
        printf("err:while getting default ccache\n");
        exit(1);
    }

    retval = krb5_cc_get_principal(context, ccdef, &client);
    if (retval) {
        printf("err:while getting client principal name\n");
        exit(1);
    }

    retval = krb5_sendauth(context, &auth_context, (krb5_pointer) &sock,
                           "",//应用服务版本,同服务端,如"KRB5_sample_protocol_v1.0";服务端用NULL没问题,经测试客户端用NULL运行段错误,所以这里用""
                           client, server,
                           AP_OPTS_MUTUAL_REQUIRED,
                           NULL,        // #11
                           0,           // no creds, use ccache instead  
                           ccdef, &err_ret, &rep_ret, NULL);
    printf("  sendauth\n");

    krb5_free_principal(context, server);       // finished using it 
    krb5_free_principal(context, client);
    krb5_cc_close(context, ccdef);
    if (auth_context) krb5_auth_con_free(context, auth_context);

    if (retval && retval != KRB5_SENDAUTH_REJECTED) {
        printf("err:while using sendauth\n");   // #12
        exit(1);
    }
    if (retval == KRB5_SENDAUTH_REJECTED) {
        printf("sendauth rejected, error reply is:\n\t\"%*s\"\n", err_ret->text.length, err_ret->text.data);
    } 
    else 
      if (rep_ret)
      {
        krb5_free_ap_rep_enc_part(context, rep_ret);

        printf("sendauth succeeded, reply is:\n");
        char str[100];
        int t;
        while( printf(">"),fgets(str,100,stdin),!feof(stdin))
        {   if (send(sock,str,strlen(str),0)==-1)
            { printf("error-send\n");
              exit(1);
            }
            printf("send to srv ok:%s",str);
            if ((t=recv(sock,str,100,0))>0)
            { str[t]='\0';
              printf("echo> %s",str);
            }
            else
            { if (t<0)printf("error-recv\n");
              else  printf("close\n");
              exit(1);
            }            
        }
      }
    close(sock);
    krb5_free_context(context);
    exit(0);
}

2.解析
1)见代码注释

2)
#10处可改为域名解析

    struct hostent *he;
    if ((he=gethostbyname("vmzhsvr.ctp.net"))==NULL)
    { printf("error vmsrvs\n");
      exit(1);
    }
    remote.sin_addr= ( *((struct in_addr *)he->h_addr)  );

3)
#11处为校验数据,本文不校验,用NULL参数

3.编译
linlin@debian:~$ gcc -o krbcln krbcln.c -lkrb5

四.运行测试
1.服务端
应用服务krbsrv需读取/etc/krb5.keytab,为方便在普通用户linlin下测试,设置该文件用户linlin可访问

运行应用服务

linlin@vmsrv:~$ ./krbsrv
 auth failed      认证失败
close             关闭连接
             --^--未有票据(对应章节2.客户端)

             --v--获取票据(对应章节2.客户端)
 auth ok          认证成功
recv from client:abcd   接收了客户
send to client ok:abcd  发送到客户

recv from client:1234
send to client ok:1234

2.客户端
linlin@vmcln:~$ klist
klist: No ticket file: /tmp/krb5cc_1000

1)未有票据
linlin@vmcln:~$ ./krbcln
connected
err:while getting client principal name
认证失败

2)获取票据

linlin@vmcln:~$ kinit --no-forwardable krblinlin
krblinlin@CTP.NET's Password:

linlin@vmcln:~$ ./krbcln
connected
  sendauth
sendauth succeeded, reply is:   认证成功
>abcd                 输入字符发给服务器
send to srv ok:abcd
echo> abcd            从服务器返回字符

>1234
send to srv ok:1234
echo> 1234
>

3)票据过期
linlin@vmcln:~$ ./krbcln
connected
sendauth
err:while using sendauth 失败,即客户端代码#12处

服务端认证失败、关闭连接

五.客户机(无krb5)
1.源代码

//源文件名:sendcln.c
#include <stdio.h>
#include <string.h>
#include <netdb.h>

int main(int argc, char *argv[])
{
    int sock;

    sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0) {
            printf(  " socket: error\n" );
            exit(1);
    }

    struct sockaddr_in remote;

    remote.sin_addr.s_addr=inet_addr("192.168.1.20");//vmsrv.ctp.net地址

    remote.sin_family = AF_INET;  
    remote.sin_port = htons(12345); //端口号
    bzero( &(remote.sin_zero)  ,8);

    if (connect(sock, (struct sockaddr *)&remote, sizeof(struct sockaddr)) < 0) {
            printf( " connect: error\n");
            close(sock);
            sock = -1;
            exit(1);
    }

    if (sock == -1)
        /* Already printed error message above.  */
        exit(1);
    printf("connected\n");
    {
        char str[100];

        while( printf(">"),fgets(str,100,stdin),!feof(stdin))
        {   if (send(sock,str,strlen(str),0)==-1)
            { printf("error-send\n");
              exit(1);
            }
            printf("send to srv ok:%s",str);
        }
    }
    close(sock);

    exit(0);
}

2.解析
不发出krb5验证,只发送数据,不接收数据

3.编译
linlin@debian:~$ gcc -o sendcln sendcln.c

4.跟踪运行
1)服务端

linlin@vmsrv:~$ strace  ./krbsrv
...
read(4, "abcd", 4)                      = 4
mmap(NULL, 1633841152, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f9d7f6ca000
read(4, "efgh\n", 1633837924)           = 5
read(4, "1234567890\n", 1633837919)     = 11

...期间不断接收客户发来的数据

read(4, 直到客户机强制退出
read(4, "", 1633837807)                 = 0
munmap(0x7f9d7f6ca000, 1633841152)      = 0
write(1, " auth failed\n", 13 auth failed     认证失败
)          = 13
close(4)                                = 0   关闭连接
write(1, "close\n", 6close
)                  = 6
accept(3,                                     进入下一个for循环等待客户连接

2)客户端(无krb5)
linlin@vmcln:~$ ./sendcln
connected

abcdefgh
send to srv ok:abcdefgh
1234567890
send to srv ok:1234567890

...不断发送数据

^C 强制退出
linlin@vmcln:~$

3)小结
可见服务端的库函数krb5_recvauth()没有识别无效数据,客户端一直发垃圾数据,服务端krb5_recvauth就一直读取,客/服的连接一直维系下去,直到客户端退出

3.1)

//--v--  超时处理
         int rc;
         fd_set fds;
         struct timeval tv;    
         FD_ZERO(&fds);
         FD_SET(acc,&fds);
         tv.tv_sec = tv.tv_usec = 15;    //超时15秒
         rc = select(acc+1, &fds, NULL, NULL, &tv);
         if (rc < 0)
         { printf(" select failed\n");           
           close(acc);
           //exit(1);
         }    
         if (FD_ISSET(acc,&fds)) 
           printf(" select ok\n");
         else
         { printf(" time out\n");
           close(acc);
           //exit(1);
         }

//--^--

服务端在#4处,即accept和krb5_recvauth之间加上超时处理,这也仅仅能处理客户端只连接不发送数据的情况(即一定时间内未有数据到来就不要进行读操作krb5_recvauth,避免读操作阻塞);
一旦客户端发送垃圾数据,krb5_recvauth总跳不出,我也找不到好的超时处理方法

六.超级服务器inetd
inetd可以简单地提供并发功能

应用服务器krbsrv.c中将#2、#3注释掉,重新编译
在inetd已将连接套接字复制到描述符0、1、2,所以#1处该行连接套接字描述符acc为0
用inetd编程,规范读、写分别0、1,为方便,本文只用描述符0(在inetd里0、1、2为同一个套接字)

1.应用服务器配置
1)安装inetd
root@vmsrv:~# apt-get install openbsd-inetd

2)修改/etc/services文件
增加一行:
mykrbsrv 12345/tcp

#服务名 端口号/网络类型

3)修改/etc/inetd.conf文件
增加一行:
mykrbsrv stream tcp nowait root /home/linlin/krbsrv

#服务名 套接字类型 采用的协议 等待否 用户名 应用程序及路径

2.运行测试
服务器已启动inetd

root@vmsrv:~# ps -e
    PID TTY          TIME CMD
      1 ?        00:00:00 systemd
     18 ?        00:00:00 systemd-journal
     39 ?        00:00:00 dhclient
     51 ?        00:00:00 dbus-daemon
     81 ?        00:00:00 inetd
     84 pts/3    00:00:00 login
     85 ?        00:00:00 sshd
    106 pts/3    00:00:00 bash
    129 pts/3    00:00:00 ps

查看服务器监听端口,进程名inetd一行的端口服务名mykrbsrv(即可从/etc/services找出对应端口号)

root@vmsrv:~# netstat -ltp
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0 0.0.0.0:ssh             0.0.0.0:*               LISTEN      85/sshd: /usr/sbin/ 
tcp        0      0 0.0.0.0:mykrbsrv        0.0.0.0:*               LISTEN      81/inetd            
tcp6       0      0 [::]:ssh                [::]:*                  LISTEN      85/sshd: /usr/sbin/ 
root@vmsrv:~# 

客户机vmcln(192.168.1.28)运行./krbcln连上服务器后,服务器inetd启动了krbsrv进程

root@vmsrv:~# ps -e
    PID TTY          TIME CMD
...
     81 ?        00:00:00 inetd
...
    136 ?        00:00:00 krbsrv
    137 pts/3    00:00:00 ps
root@vmsrv:~# 

客户机vmcln再运行./krbcln,应用服务器可见两个krbsrv进程

root@vmsrv:~# ps -e
    PID TTY          TIME CMD
...
    136 ?        00:00:00 krbsrv
    138 ?        00:00:00 krbsrv
    139 pts/3    00:00:00 ps

查看服务器连接信息

root@vmsrv:~# netstat -tp
Active Internet connections (w/o servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0 192.168.1.20:mykrbsrv   192.168.1.28:35850      ESTABLISHED 136/krbsrv       
tcp        0      0 192.168.1.20:mykrbsrv   192.168.1.28:35860      ESTABLISHED 138/krbsrv       
root@vmsrv:~# 

客户机终结其中一个客户程序后

root@vmsrv:~# netstat -tp
Active Internet connections (w/o servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0 192.168.1.20:mykrbsrv   192.168.1.28:35850      ESTABLISHED 136/krbsrv       
tcp        0      0 192.168.1.20:mykrbsrv   192.168.1.28:35860      CLOSE_WAIT  138/krbsrv       
root@vmsrv:~# 

等会儿就剩下一个krbsrv进程

root@vmsrv:~# netstat -tp
Active Internet connections (w/o servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0 192.168.1.20:mykrbsrv   192.168.1.28:35850      ESTABLISHED 136/krbsrv       

root@vmsrv:~# ps -e
    PID TTY          TIME CMD
...
    136 ?        00:00:00 krbsrv
    155 pts/3    00:00:00 ps
root@vmsrv:~# 

标签:krb5,00,vmsrv,--,Kerberos,sock,context,printf
来源: https://blog.51cto.com/u_13752418/2778563

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有