ftp关键技术一:账户验证
对于Linux端的ftp服务的而言,账户即为Linux端的用户。
一般步骤是:
- 以root用户权限启动ftp服务
- 获取客户端的验证信息
- 从系统获取用户名对应的加密后的密码
- 对客户端发过来的密码进行对应的加密,并对比
如何验证是否以root用户启动服务?
我们可以通过getuid()函数获取当前程序运行的uid。
一般root用户的uid =0。
所以可以通过以下方式验证是否以root用户启动:
if (getuid() != 0)
{
fprintf(stderr, "miniftp must start be as root\n");
exit(EXIT_FAILURE);
}
系统获取用户名对应的加密后的密码
UNIX系统口令文件定义包含在<pwd.h>中定义的passwd的结构中。
struct passwd {
char *pw_name; /* username */
char *pw_passwd; /* user password */
uid_t pw_uid; /* user ID */
gid_t pw_gid; /* group ID */
char *pw_gecos; /* user information */
char *pw_dir; /* home directory */
char *pw_shell; /* shell program */
};
在这里就不在赘述其他东西,主要关心uid和pw_passwd,分别是用户id和用户所对应的密码。
于此同时在其中定义了两个函数需要我们去关注
struct passwd *getpwnam(const char *name);
//通过用户名获取passwd
struct passwd *getpwuid(uid_t uid);
//通过uid获取passwd
初次之外需要我们注意的是这些函数需要运行在root权限下,这就是为什么要验证是否root启动服务原因之一。
另外如果输入的用户名或者uid是错误的话,返回的passwd是一个NULL
于是我们便可以这样设计ftp的验证
static void do_user(session_t *sess)
{
//略去了接受和分割命令细节,这里sess->arg是用户名
struct passwd *pw = getpwnam(sess->arg);
if (pw == NULL)
{
ftp_reply(sess, FTP_LOGINERR, "Login incorrect");
//发送FTP_LOGINERR命令
return;
}
sess->uid = pw->pw_uid;
ftp_reply(sess, FTP_GIVEPWORD, "Please specify the password.");
//发送FTP_GIVEPWORD命令
}
在这里将username和passwd分开验证可以优化一下体验,至少知道是什么输错了,由于是分开设计的,所以我们需要保存一个用户信息,因为uid是最好操作的,所以我们在sess(用户信息)中存储了一个uid
对客户端发过来的密码进行对应的加密,并对比
这里可能有读者会问,为什么要将客户端的密码进行加密对比,而不是将系统的密码解密对比呢?
为了安全考虑,Linux的加密口令是经单向加密算法处理过的用户口令副本。因此此算法是单向的,所以不能从加密猜测到原来的口令。
基于此linux设计了一个叫阴影口令的文件。该文件至少包含用户名和加密口令,与该口令相关的其他信息也存放其中。
在<shadow.h>文件中定义了
struct spwd {
char *sp_namp; /* Login name */
char *sp_pwdp; /* Encrypted password */
long sp_lstchg; /* Date of last change
(measured in days since
1970-01-01 00:00:00 +0000 (UTC)) */
long sp_min; /* Min # of days between changes */
long sp_max; /* Max # of days between changes */
long sp_warn; /* # of days before password expires
to warn user to change it */
long sp_inact; /* # of days after password expires
until account is disabled */
long sp_expire; /* Date when account expires
(measured in days since
1970-01-01 00:00:00 +0000 (UTC)) */
unsigned long sp_flag; /* Reserved */
};
在这个文件中,我们主要用到sp_pwdp这个字段,sp_pwdp是指加密口令,通过这个加密口令我们可以通过crypt函数对获取客户端发送的密码进行加密。
同样定义了函数
struct spwd *getspnam(const char *name);
当我们获得了spwd的后,我们还需要对客户端发送的pwsswd进行加密
在<unistd.h>中定义了一个函数
char *crypt(const char *key, const char *salt);
key:要加密的明文。
salt:密钥。
salt 默认使用DES加密方法。DES加密时,salt只能取两个字符,多出的字符会被丢弃。
需要注意的是这个函数在编译的时候需要链接lcrypt。
ps:这个函数多用于md5 SHA-256等加密,感兴趣的可以学学,我在这里就不赘述了。
于是我们便可以这样写do_pass函数来验证客户端的密码
static void do_pass(session_t *sess)
{
struct passwd *pw = getpwuid(sess->uid); //获取pw_name
if (pw == NULL)
{
ftp_reply(sess, FTP_LOGINERR, "Login incorrect");
return;
}
struct spwd *sp = getspnam(pw->pw_name); //获取sp_pwdp
if (sp == NULL)
{
ftp_reply(sess, FTP_LOGINERR, "Login incorrect");
return;
}
char *encrypted_pw = crypt(sess->arg, sp->sp_pwdp); //加密
if (strcmp(encrypted_pw, sp->sp_pwdp) != 0) //比对加密后的密码和sp_pwdp
{
ftp_reply(sess, FTP_LOGINERR, "Login incorrect.");
return;
}
setegid(pw->pw_gid); //更改gid
seteuid(pw->pw_uid); //更改uid
chdir(pw->pw_dir); //更改Dir
ftp_reply(sess, FTP_LOGINOK, "Login success.");
}
ftp关键技术二:nobody进程创建和使用(一)
本文将从以下几个方面具体阐述nobody进程的前世今生
- 为什么需要nobody进程?
- 进程间通信的协议制定
为什么需要nobody进程
(1)为什么要使用nobody进程和服务进程两个进程?
1.在PORT模式下,服务器会主动建立数据通道连接客户端,服务器可能就没有权限做这种事情,就需要nobody进程来帮忙。 Nobody进程会通过unix域协议(本机通信效率高) 将套接字传递给服务进程。普通用户没有权限绑定20端口,需要nobody进程的协助,所以需要nobody进程作为控制进程。
2.事实上无论是PORT模式还是PASV模式,创建套接字还是后面对套接字的监听这些操作涉及到于内核的相关操作放在服务进程都是不安全。其实最近看到一个文章,文中指出以root启动在验证后转到用户进程也会不安全的。
(2)为什么使用多进程而不是多线程?
原因是在多线程或IO复用的情况下,当前目录是共享的,无法根据每一个连接来拥有自己的当前目录,也就是说当前用户目录的切换会影响到其他的用户。
(3)ftp服务器的架构
进程间通信的协议制定
首先采用Unix域的内部通信协议需要创建一个Unix的套接字进行通信
void priv_sock_init(session_t *sess)
{
int sockfds[2];
if (socketpair(PF_UNIX, SOCK_STREAM, 0, sockfds) < 0)
ERR_EXIT("socketpair");
sess->parent_fd = sockfds[0];
sess->child_fd = sockfds[1];
}
void priv_sock_set_parent_context(session_t *sess)
{
if (sess->child_fd != -1)
{
close(sess->child_fd);
sess->child_fd = -1;
}
}
void priv_sock_set_child_context(session_t *sess)
{
if (sess->parent_fd != -1)
{
close(sess->parent_fd);
sess->parent_fd = -1;
}
}
sess作为两个进程共有的用户信息,在两个进程创建初期sess内部便被写入了Unix的套接字通信
void begin_session(session_t *sess)
{
activate_oobinline(sess->ctrl_fd);
priv_sock_init(sess); //写入套接字
pid_t pid;
pid = fork();
if (pid < 0)
ERR_EXIT("fork");
if (pid == 0)
{
priv_sock_set_child_context(sess);
handle_child(sess);
}
else
{
priv_sock_set_parent_context(sess);
handle_parent(sess);
}
}
然后让我们看看内部协议制定包装了一系列函数
void priv_sock_send_cmd(int fd, char cmd);
char priv_sock_get_cmd(int fd);
void priv_sock_send_result(int fd, char res);
char priv_sock_get_result(int fd);
void priv_sock_send_int(int fd, int the_int);
int priv_sock_get_int(int fd);
void priv_sock_send_buf(int fd, const char *buf, unsigned int len);
void priv_sock_recv_buf(int fd, char *buf, unsigned int len);
void priv_sock_send_fd(int sock_fd, int fd);
int priv_sock_recv_fd(int sock_fd);
我们可以看到主要有两个功能的函数,一是负责内部的命令的接受、实现和返回结果,二是负责传输数据。
首先看看第一部分是怎么实现的吧
// FTP服务进程向nobody进程请求的命令
#define PRIV_SOCK_GET_DATA_SOCK 1
#define PRIV_SOCK_PASV_ACTIVE 2
#define PRIV_SOCK_PASV_LISTEN 3
#define PRIV_SOCK_PASV_ACCEPT 4
// nobody进程对FTP服务进程的应答
#define PRIV_SOCK_RESULT_OK 1
#define PRIV_SOCK_RESULT_BAD 2
//这里提供部分实现
void priv_sock_send_cmd(int fd, char cmd)
{
int ret;
ret = writen(fd, &cmd, sizeof(cmd));
if (ret != sizeof(cmd))
{
fprintf(stderr, "priv_sock_send_cmd error\n");
exit(EXIT_FAILURE);
}
}
char priv_sock_get_cmd(int fd)
{
char res;
int ret;
ret = readn(fd, &res, sizeof(res));
if (ret == 0)
{
printf("ftp process exit\n");
exit(EXIT_SUCCESS);
}
if (ret != sizeof(res))
{
fprintf(stderr, "priv_sock_get_cmd error\n");
exit(EXIT_FAILURE);
}
return res;
}
这里提供了get_cmd和send_cmd的实现,可以看到只是简单包装下send和read函数
这里就不再赘述其他函数,对于这些函数,我们主要关注一组特殊函数
void priv_sock_send_fd(int sock_fd, int fd)
{
send_fd(sock_fd, fd);
}
int priv_sock_recv_fd(int sock_fd)
{
return recv_fd(sock_fd);
}
为什么这个比较特殊呢?因为这不是传输一个四个字节的整形,而是传输一个打开的文件描述符,我们想让发送进程和接受进程共享同一文件表项。在技术上,我们是将一个打开文件表项的指针从一个进程发送到另一个进程,该指针被分配到接受进程第一个可用的描述符中。发送结束后,发送进程通常会关闭该描述符。
为了在UNIX域套接字交换文件描述符,我们需要关注以下系统函数
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
struct msghdr
{
void *msg_name; /* optional address */
socklen_t msg_namelen; /* size of address */
struct iovec *msg_iov; /* scatter/gather array */
size_t msg_iovlen; /* # elements in msg_iov */
void *msg_control; /* ancillary data, see below */
size_t msg_controllen; /* ancillary data buffer len */
int msg_flags; /* flags on received message */
};
前两个元素主要用于网络通信,msg_name存数据包的目的地址,网络包指向struct sockaddr_in,msg_namelen值地址长度,一般为16。一般在UNIX域设置为NULL, 0。
接下来的两个元素我们可以指定一个或多个内存缓存区,第一个元素指向一个数据包缓存区的buff 。其中 iov_base指向数据包缓冲区,即参数buff,iov_len是buff的长度。msghdr中允许一次传递多buff,以数组的形式组织在 msg_iov中,msg_iovlen就记录数组的长度(即有多少个buff)。
struct iovec { /* Scatter/gather array items */
void *iov_base; /* Starting address */
size_t iov_len; /* Number of bytes to transfer */
};
最后两个元素,msg_flags字段包含了描述接收到的消息的标志,如带外数据MSG_OOB等。mgs_controllen字段指向cmsghdr结构,用于控制信息字节数
struct cmsghdr {
socklen_t cmsg_len; /* data byte count, including header */
int cmsg_level; /* originating protocol */
int cmsg_type; /* protocol-specific type */
/* followed by unsigned char cmsg_data[]; */
};
为了发送文件描述符,需要将cmsg_len设置为cmsghdr结构的长度加一个文件描述符的长度,将cmg_level设计为SOL_SOCKET, cmsg_type字段设置为SCM_RIGHTS,用以表明传送访问权,描述符紧随cmsg_type字段之后储存,用CMSG_DATA宏获得该整型量的指针。
struct cmsghdr *CMSG_FIRSTHDR(struct msghdr *msgh);
//获得指向与msghadr结构关联的第一个cmsghdr结构
size_t CMSG_SPACE(size_t length);
//计算 cmsghdr 头结构加上附属数据大小,并包括对其字段和可能的结尾填充字符
size_t CMSG_LEN(size_t length);
//计算 cmsghdr 头结构加上附属数据大小
unsigned char *CMSG_DATA(struct cmsghdr *cmsg);
//返回一个指针和cmsghdr结构关联的数据
我们可以通过控制这些宏对这些进行cmsghdr初始化,具体实现看下面。
/**
* send_fd -向sock_fd 发送 fd
* @sock_fd: 发送目标套接字
* @fd: 发送套接字
*/
void send_fd(int sock_fd, int fd)
{
int ret;
struct msghdr msg;
struct cmsghdr *p_cmsg;
struct iovec vec;
char cmsgbuf[CMSG_SPACE(sizeof(fd))]; //配置cmsgbuf的大小
int *p_fds;
char sendchar = 0;
msg.msg_control = cmsgbuf;
msg.msg_controllen = sizeof(cmsgbuf);
p_cmsg = CMSG_FIRSTHDR(&msg); //通过宏获得struct cmsghdr指针
p_cmsg->cmsg_level = SOL_SOCKET; //指定是socket协议
p_cmsg->cmsg_type = SCM_RIGHTS; //套接字控制信息,仅UNIX域可以传递该信息
p_cmsg->cmsg_len = CMSG_LEN(sizeof(fd)); //用宏储存fd所需的对象长度,一般是整型+ cmsghdr长度
p_fds = (int*)CMSG_DATA(p_cmsg); //获得关联数据 即fd的指针
*p_fds = fd;
msg.msg_name = NULL; //UNIX域 初始化为NULL
msg.msg_namelen = 0;
msg.msg_iov = &vec; //初始化缓冲区buff
msg.msg_iovlen = 1;
msg.msg_flags = 0;
vec.iov_base = &sendchar;
vec.iov_len = sizeof(sendchar);
ret = sendmsg(sock_fd, &msg, 0);
if (ret != 1)
ERR_EXIT("sendmsg");
}
/**
* send_fd -向sock_fd 发送 fd
* @sock_fd: 接受目标套接字
* 返回目标套接字
*/
int recv_fd(const int sock_fd)
{
int ret;
struct msghdr msg;
char recvchar;
struct iovec vec;
int recv_fd;
char cmsgbuf[CMSG_SPACE(sizeof(recv_fd))];
struct cmsghdr *p_cmsg;
int *p_fd;
vec.iov_base = &recvchar;
vec.iov_len = sizeof(recvchar);
msg.msg_name = NULL;
msg.msg_namelen = 0;
msg.msg_iov = &vec;
msg.msg_iovlen = 1;
msg.msg_control = cmsgbuf;
msg.msg_controllen = sizeof(cmsgbuf);
msg.msg_flags = 0;
p_fd = (int*)CMSG_DATA(CMSG_FIRSTHDR(&msg));
*p_fd = -1;
ret = recvmsg(sock_fd, &msg, 0);
if (ret != 1)
ERR_EXIT("recvmsg");
p_cmsg = CMSG_FIRSTHDR(&msg); //通过宏获得信息头
if (p_cmsg == NULL)
ERR_EXIT("no passed fd");
p_fd = (int*)CMSG_DATA(p_cmsg); //通过宏获得传输数据
recv_fd = *p_fd;
if (recv_fd == -1)
ERR_EXIT("no passed fd");
return recv_fd;
}
ftp关键技术二:nobody进程创建和使用(二)
本文将从以下几个方面具体阐述nobody进程的前世今生
- 如何给予nobody权限
- nobody进程负责的任务以及实现
如何给予nobody权限
先看看在系统中ftp服务器是如何工作的吧
[root@VM_0_11_redhat ~]# ps -ef | grep miniftp
root 6362 1 0 May13 ? 00:00:00 ./miniftpd
nobody 32305 6362 0 18:23 ? 00:00:00 ./miniftpd
root 32306 32305 0 18:23 ? 00:00:00 ./miniftpd
在这里,miniftpd的客户端是以root用户链接的,这显然不太合适,但是这不是重点,我们可以看到负责链接unix内核和用户环境的nobody进程,居然是以noobody权限启动的,这个权限显然不足以绑定固定端口20,所以我们需要对其提升权限。
首先我们将进程的用户更改成nobody
if (setegid(pw->pw_gid) < 0)
ERR_EXIT("setegid");
if (seteuid(pw->pw_uid) < 0)
ERR_EXIT("seteuid");
在capablity.h文件中定义了以下的结构体
typedef struct __user_cap_header_struct {
__u32 version;
int pid;
} *cap_user_header_t;
typedef struct __user_cap_data_struct {
__u32 effective;
__u32 permitted;
__u32 inheritable;
} *cap_user_data_t;
对于version我们可以看到如下描述
Kernels prior to 2.6.25 prefer 32-bit capabilities with version
_LINUX_CAPABILITY_VERSION_1. Linux 2.6.25 added 64-bit capability
sets, with version _LINUX_CAPABILITY_VERSION_2.
所以对于version,由于我们电脑是64位的操作系统,所以用_LINUX_CAPABILITY_VERSION_2
在man capabilities中我们找到我们需要绑定的权限
CAP_NET_BIND_SERVICE
Bind a socket to Internet domain privileged ports (port
numbers less than 1024).
- cap_effective:当一个进程要进行某个特权操作时,操作系统会检查cap_effective的对应位是否有效,而不再是检查进程的有效UID是否为0.
例如,如果一个进程要设置系统的时钟,Linux的内核就会检查cap_effective的CAP_SYS_TIME位(第25位)是否有效.
- cap_permitted:表示进程能够使用的能力,在cap_permitted中可以包含cap_effective中没有的能力,这些能力是被进程自己临时放弃的,也可以说cap_effective是cap_permitted的一个子集.
- cap_inheritable:表示能够被当前进程执行的程序继承的能力.
所以我们就可以这样初始化权限
struct __user_cap_header_struct head;
struct __user_cap_data_struct data;
memset(&head, 0, sizeof(head));
memset(&data, 0, sizeof(data));
head.version = _LINUX_CAPABILITY_VERSION_2;
head.pid = 0;
__u32 mask = 0;
mask |= (1 << CAP_NET_BIND_SERVICE);
data.effective = data.permitted = mask;
data.inheritable = 0;
然后由于capset输入系统调用操作,所以我们需要用Syscall让内核来进行间接的函数调用。
long syscall(long number, ...);
在asm/unistd.h文件中定义了一系列的宏,定义调用的具体内容
#define __NR_capget 125
#define __NR_capset 126
然后我们就可以自己写一个capset函数来实现我们想要的功能
int capset(cap_user_header_t hdrp, const cap_user_data_t datap)
{
return syscall(__NR_capset, hdrp, datap);
}
nobody进程负责的任务以及实现
在被动模式的sockfd获取函数中我们有以下步骤
- nobody进程接收PRIV_SOCK_GET_DATA_SOCK命令
- 进一步接收一个整数,也就是port = 20;
- 接收一个字符串,也就是ip;
- 调用系统函数绑定20端口;
- 回复用户进程ok
- 发送fd
static void privop_pasv_get_data_sock(session_t *sess)
{
unsigned int port = (unsigned int)priv_sock_get_int(sess->parent_fd);
char ip[16] = {0};
priv_sock_recv_buf(sess->parent_fd, ip, sizeof(ip));
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_addr.s_addr = inet_addr(ip);
addr.sin_port = htons(port);
addr.sin_family = AF_INET;
int fd = tcp_client(20);
if (fd == -1)
{
priv_sock_send_result(sess->parent_fd, PRIV_SOCK_RESULT_BAD);
return;
}
if (connect_timeout(fd, &addr, tunable_connect_timeout) < 0)
{
close(fd);
priv_sock_send_result(sess->parent_fd, PRIV_SOCK_RESULT_BAD);
return;
}
priv_sock_send_result(sess->parent_fd, PRIV_SOCK_RESULT_OK);
priv_sock_send_fd(sess->parent_fd, fd);
close(fd);
}
获得主动模式的监听套接字
- nobody进程接收PRIV_SOCK_PASV_LISTEN命令
- 创建任意一个端口的套接字
- 将端口发送给客户端
//创建一个监听套接字
static void privop_pasv_listen(session_t *sess)
{
char ip[16] = {0};
getlocalip(ip);
sess->pasv_listen_fd = tcp_server(ip, 0);
struct sockaddr_in addr;
socklen_t addrlen = sizeof(addr);
if (getsockname(sess->pasv_listen_fd, (struct sockaddr*)&addr,&addrlen) < 0)
{
ERR_EXIT("getsockname");
}
unsigned short port = ntohs(addr.sin_port);
priv_sock_send_int(sess->parent_fd, (int)port);
}
获得主动模式的客户端链接的fd
- nobody进程接收PRIV_SOCK_PASV_ACCEPT命令
- 关闭nobody进程的监听套接字
- 发送用户进程ok
- 发送用户进程客户端链接的fd
//获取链接
static void privop_pasv_accept(session_t *sess)
{
int fd = accept_timeout(sess->pasv_listen_fd, NULL, tunable_accept_timeout);
//得到一个已连接套接字
close(sess->pasv_listen_fd);
sess->pasv_listen_fd = -1;
if (fd == -1)
{
priv_sock_send_result(sess->parent_fd, PRIV_SOCK_RESULT_BAD);
return;
}
priv_sock_send_result(sess->parent_fd, PRIV_SOCK_RESULT_OK);
priv_sock_send_fd(sess->parent_fd, fd);
close(fd);
}
ftp关键技术四:空闲断开
首先提出一个问题,我们为什么需要空闲断开?
对于服务端而言,由于连接数和内存的限制,我们不可能对一个长时间处于不活跃的客户端,单独维护一个fd,一个进程/线程始终为其服务,fd单个进程上限默认值为1024,由于内存的限制,也不能无限制的分配出进程或者线程为其服务,这个时候我们就需要断开在规定时间内没有任何动作的客户端,腾出内存为其他客户端服务。
第二个问题,我们要基于什么实现空闲断开呢?
我们可以先设想需要一个可以在一定时间后能唤醒一个断开的服务的东西,而不是我们去维护一个进程,负责更新时间和断开(这样代价太大)。
然后就想到了信号,设置一个定时的信号。
等等,在设置一个定时信号的时候,我们是不是在该考虑一个问题,假设我们服务一个终端的ftp客户端,恰巧它开始下载一个非常大的文件,恰巧下载时间超过了服务端设置的空闲断开时间,这个时候就尴尬了,按照之前的想法,我们会空闲断开(注意:终端的ftp客户端下载时候,不可以发送其他命令,当然ctrl +c 的 abort除外)。
所以我们就需要先定义一个data_process,如果在进行数据传输的时候,就设置data_process为true,安装信号的时候,检测到data_process就不再安装限时信号。
第三个问题,如何实现呢?
首先,我们需要现在session中,安装data_process,session是每个进程维护一个包含配置文件和进程信息的一个全局节点。
typedef struct session_t
{
...
// 是否数据连接
int data_process;
...
}session_t ;
然后我们就可以开始设置信号了,注意为我们需要为cmd模式和transfer data模式各自设置一个信号。
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
首先看到这个函数,可能很多人都不明白这个函数是在做什么,大致可以理解它传入一个signum(待会见)和一个函数指针。所以我从以下三个方面说明这个函数:
- 参数是一个int
- 无返回值
- 作用是用来处理产生的信号(signum)
于是我们在关注这个signal函数,它是一个典型的回调函数,在我们设置一个信号的时候,我们会传入一个new handler,它会返回一个old handler,在出错的时候会返回一个SIG_ERR。
我们可以man 7 signal,获得详细的signum所对应的宏定义和相关说明。
SIGALRM 14 Term Timer signal from alarm(2)
这个用于设置一个alrm信号,用来捕捉alarm产生的信号。
If seconds is zero, any pending alarm is canceled.
In any event any previously set alarm() is canceled.
这个是man 文档中的原文,我觉得这个写的非常干练,就不在累赘翻译了。而且这个也是可以重复刷新空闲时间的原因!
所以下面我们就开始写函数的实现把!
void start_cmdio_alarm()
{
if (tunable_idle_session_timeout > 0)
{
//安装信号
signal(SIGALRM, handle_alarm_timeout);
//启动闹钟
alarm(tunable_idle_session_timeout);
}
}
void start_data_alarm()
{
if (tunable_data_connection_timeout > 0)
{
//安装信号
signal(SIGALRM, handle_sigalrm);
//启动闹钟
alarm(tunable_data_connection_timeout);
}
else if(tunable_idle_session_timeout > 0)
{
alarm(0);
}
}
在这里我们实现了两种信号,分别用于设置两种模式,固然也需要两种handler函数用于处理这个信号!
注意我们这里增加了一个tunable_data_connection_timeout,这个是用来控制数据传输链接超时的情况,我们不永远等待一个始终不进行的数据链接传输却发送数据链接请求的进程吧!!!
请再等等,这里我们就产生了第四个问题。
第四个问题,我们要怎么样关闭链接才合适?
先假设一种情况,我们在处理函数先发送一个进程链接被终止信号,然后直接close 掉链接的fd,这会发生一种什么情况,说一种很无聊的情况,假设一个客户端在收到被终止信号,立即发送一个命令,无论什么命令,这个时候会产生什么情况,信号又被重新设置了???到底会发生???可以看来这样并不安全。
这个时候就有一种较为安全的情况,我们可以先关闭读,然后给客户端发送终止信号,然后关闭写。
void handle_alarm_timeout(int sig)
{
shutdown(p_sess->ctrl_fd, SHUT_RD);
ftp_reply(p_sess, FTP_IDLE_TIMEOUT, "Timeout");
shutdown(p_sess->ctrl_fd, SHUT_WR);
exit(EXIT_FAILURE);
}
//然后我们要怎么处理data模式呢?
void handle_sigalrm(int sig)
{
if (!p_sess->data_process)
{
ftp_reply(p_sess, FTP_DATA_TIMEOUT, "Data timeout, Reconnect sorry");
exit(EXIT_FAILURE);
}
//否则 当前处于数据传输的状态收到了超时信号
p_sess->data_process = 1;
start_data_alarm();
}
ftp关键技术五:限制链接数
通常在一些网站中,为了防止恶意大量的访问和超大量访问导致内存占满,会对单个链接的连接数和总链接数做出一个限制。
以本FTP服务端为例,假设每个客户链接,我们都需要两个进程来处理它,假设了一个客户需要分配总共1M的栈内存出来,1000个链接,接近1G的内存就没有了。另一方面,如果单个ip大量链接服务端,会占用大量的带宽、内存和文件句柄,实际上每个用户(ip)只需要两三个链接就可以解决问题,所以对单个ip连接数进行限制,有助于维持服务端的性能稳定和防止恶意访问。
在系统自带vsftpd中有如下的配置文件
max_clients=300 最大客户端连接数为300
max_per_ip=10 每个IP最大连接数
现在我们知道,我们需要一个数据结构来存储ip和ip对应的链接数,所以我们首先想到的就是用键值对模型,即每个ip对应一个链接数,并存储起来,并且我们需要能够快速插入、删除和查找,适合的就是树和hash表。但是连接数真的能解决问题吗?
首先,何时链接,我们何时增加一个连接数这个毋容置疑,但是,我们要如何知道这个链接结束了呢?我们可以在进程结束的时候,获得进程结束的信号,从而感知到一个进程的结束,所以这个时候,我们就不能单单依靠连接数,而是依靠进程的pid来对应ip。
所以我们需要两个hash表,一个hash表是pid to ip,另一个表是ip to conn。
至于hash_table我们可以自己定制写一个,也可以用stl库中的,但是还是自己写一个吧,我们只需要用到hash_table的部分功能。
下面的函数只是对主要的成员函数进行注释,方便理解(这里并没用泛型,而是借鉴了redis的实现方式用void *实现的hashtable)。
void* hash_lookup_entry(void* key, unsigned int key_size);
//寻找并返回key所对应的value, 如value为空,则返回空。
void hash_add_entry(void *key, unsigned int key_size,
void *value, unsigned int value_size);
//新增一个key-value
void hash_free_entry(void *key, unsigned int key_size);
//删除一个key-value
下面我们需要两个表
static hash* s_ip_conn_hash;
static hash* s_pid_ip_hash;
在接受链接成功后,我们可以获得一个unsigned int类型的ip,我们可以根据这个ip,找到ip所对应的p_count,再对p_count进行操作就完成了ip-conn的创建和增长。
unsigned int ip = addr.sin_addr.s_addr;
sess.num_this_ip = handle_ip_count(&ip);
......
unsigned int handle_ip_count(void *ip)
{
unsigned int count;
unsigned int *p_count = (unsigned int*)s_ip_conn_hash->hash_lookup_entry(ip, sizeof(unsigned int));
if (p_count == NULL)
{
count = 1;
//不存在即创建
s_ip_conn_hash->hash_add_entry(ip, sizeof(unsigned int), &count, sizeof(unsigned int));
}
else
{
//存在便增1,不过没有考虑到原子操作,失误失误
count = *p_count;
++count;
*p_count = count;
}
return count;
}
为了减少主进程的工作,我们将检测链接过限制放到子进程中。
if (pid == 0) //子进程
{
...
check_limits(&sess);
...
}
void check_limits(session_t *sess)
{
if (tunable_max_clients > 0 && sess->num_clients > tunable_max_clients)
{
ftp_reply(sess, FTP_TOO_MANY_USERS, "There are too many connection, please try later");
exit(EXIT_FAILURE);
}
if (tunable_max_per_ip > 0 && sess->num_this_ip > tunable_max_per_ip)
{
ftp_reply(sess, FTP_IP_LIMIT, "There are too many connection,from internet address");
exit(EXIT_FAILURE);
}
}
在链接数的删除上,我们需要明白一个流程。
1.创建/增加 ip-value
2.在父进程中创建pid-ip键值对
3.在父进程中检测到子进程的退出
4.查找pid对应的ip,删除ip对于的两个键值对。
那如何实现检测呢?我们可以设置一个信号,当检测到SIGCHLD时候,执行操作四。
signal(SIGCHLD, handle_sighid);
//接受链接后
if (pid == 0) //子进程
{
//防止子进程的子进程退出的干扰
signal(SIGCHLD, SIG_IGN);
}
//检测到子进程退出后执行
void handle_sighid(int sig)
{
pid_t pid;
while ((pid = waitpid(-1, NULL, WNOHANG)) > 0)
{
--s_children;
unsigned int*ip = (unsigned int *)s_pid_ip_hash->hash_lookup_entry(&pid, sizeof(pid)); //查找pid对于的ip
if (ip == NULL)
{
continue;
}
drop_ip_count(ip); //对ip-value减一或者删除操作
s_pid_ip_hash->hash_free_entry(&pid, sizeof(pid));
}
}
void drop_ip_count(void *ip)
{
unsigned int count;
unsigned int *p_count = (unsigned int *)s_ip_conn_hash->hash_lookup_entry(ip, sizeof(unsigned int));
if (p_count == NULL)
{
return;
}
count = *p_count;
if (count <= 0)
{
s_ip_conn_hash->hash_free_entry(ip, sizeof(unsigned int));
return;
}
--count;
*p_count = count;
if (count == 0)
{
s_ip_conn_hash->hash_free_entry(ip, sizeof(unsigned int));
}
}
百度云源代码分享链接: https://pan.baidu.com/s/1GpeduK3PcTXiyZxkwctPDA 密码:m1dg