本篇介绍的不是本地应用,而是体现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:~#