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

本篇介绍的不是本地应用,而是体现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:~# 
上一篇:windows下dbeaver客户端连接hive--kerberos认证方式


下一篇:Java WebService记