一、FTP两种工作模式
主要是针对数据连接而言的,控制连接的建立总是由客户端向服务器端发起。而数据连接通道的建立则不同,既可以是服务器端向客户端发起连接建立数据连接通道,这种模式称为主动模式。也可以是客户端向服务器端发起连接建立数据连接通道,这种模式称为被动模式。
详细介绍请看FTP简介
二、nobody进程
为什么要用两个进程为一个客户端服务?
当一个客户端连接过来的时候,如果是wangkai用户登录,那么就将当前进程更改为wangkai,如果接下来涉及数据传输,假设是PORT模式,这是服务器端主动连接客户端,服务器需要绑定一个20的端口连接客户端,如何绑定20端口号呢?一种方法是提升ftp服务进程的权限,但是ftp服务进程是与外界进行交互的进程,如果提升了ftp服务进程权限,也就意味着外界能够更多的对ftp服务器的控制权,从而使得FTP服务器处于不安全的状态。所以要创建一个内部进程,内部进程不与外界进行通信,它仅仅只是协助ftp服务进程来完成数据连接通道的创建,将这个进程称为是Nobody进程。并且赋予这个nobody进程一些特殊的权限。
Linux中的nobody用户
nobody在linux中是一个不能登录的账号,它是一个普通用户,非特权用户。 使用nobody用户名的'目的'是,使任何人都可以登录系统,但是其 UID 和 GID 不提供任何特权,即该uid和gid只能访问人人皆可读写的文件,所以我们这里的nobody进程还需要提升权限。
实现
1.当一个客户端连接成功之后,服务端就由两个进程进行服务,一个是前台进程,另一个是辅助进程。这两个进程是通过sockpair套接字对进行套接字传送以及命令发送接收。
//初始化内部进程间通信通道
void priv_sock_init(session_t *sess)
{
int sockfds[2];
//创建一个套接字对
//Linux环境下使用socketpair函数创造一对未命名的、相互连接的UNIX域套接字
//Unix域套接字往往比通信两端位于同一主机的TCP套接字快出一倍
if (socketpair(PF_UNIX, SOCK_STREAM, 0, sockfds) < 0)
ERR_EXIT("socketpair");
//对父子进程的套接字进行设置
sess->parent_fd = sockfds[0];
sess->child_fd = sockfds[1];
}
2.接下来,子进程就属于前台进程,负责与客户端进行通信。将父进程变为nobody进程,并且提升权限使得能够绑定20端口。
capset函数用于设置进程的特殊能力。
//给nobody必要的特权
void minimize_privilege(void)
{
//将父进程变为Nobody进程,原来是root
//getpwnam获取用户登录相关信息
struct passwd *pw = getpwnam("nobody");
if (pw == NULL)
return;
//没有改之前用户ID和组ID都为0,是root用户启动的
//将当前进程的有效组ID改为pw_gid
if (setegid(pw->pw_gid) < 0)
ERR_EXIT("setegid");
//将当前进程的有效用户ID改为pw_uid
if (seteuid(pw->pw_uid) < 0)
ERR_EXIT("seteuid");
struct __user_cap_header_struct cap_header;
struct __user_cap_data_struct cap_data;
memset(&cap_header, 0, sizeof(cap_header));
memset(&cap_data, 0, sizeof(cap_data));
//64位的系统选择_2
cap_header.version = _LINUX_CAPABILITY_VERSION_2;
//不需要设置
cap_header.pid = 0;
__u32 cap_mask = 0;
//获得绑定特权端口的权限
//把1左移了10位
cap_mask |= (1 << CAP_NET_BIND_SERVICE);
//要赋予的特权
cap_data.effective = cap_data.permitted = cap_mask;
//不允许继承
cap_data.inheritable = 0;
capset(&cap_header, &cap_data);
}
3.父进程nobody的工作
循环接收子进程发送过来的命令,并且执行相应的操作。
//接收的命令是从子进程发送过来的,协助完成任务
void handle_parent(session_t *sess)
{
//先给nobody特权
minimize_privilege();
char cmd;
//因为是死循环,所以一直处于接收子进程命令的状态,子进程的退出能够使得父进程也
//收到通知,进而退出
while (1) {
//子进程(ftp服务进程)发送来的命令
cmd = priv_sock_get_cmd(sess->parent_fd);
// 解析内部命令
// 处理内部命令
switch (cmd) {
//4个处理函数
case PRIV_SOCK_GET_DATA_SOCK:
privop_pasv_get_data_sock(sess);
break;
case PRIV_SOCK_PASV_ACTIVE:
privop_pasv_active(sess);
break;
case PRIV_SOCK_PASV_LISTEN:
privop_pasv_listen(sess);
break;
case PRIV_SOCK_PASV_ACCEPT:
privop_pasv_accept(sess);
break;
}
}
}
如果服务进程退出,那么nobody进程也会随之退出,因为read返回0。
//接收命令(父->子)
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);
}
//只有1个字节
if (ret != sizeof(res)) {
fprintf(stderr, "priv_sock_get_cmd error\n");
exit(EXIT_FAILURE);
}
return res;
}
三、主动模式的实现
1.客户端向服务器端发送PORT命令
PORT命令后面跟的是客户端的IP与端口,服务器端收到PORT命令之后,将执行do_port函数,在这个函数内部,首先要将IP与端口解析出来,之后保存到port_addr变量中,紧接着服务端给客户端一个200的应答。一旦客户端收到200的应答之后,将开始发起实际的数据传输命令。
//主动模式的实现
//FTP服务进程接收到PORT h1,h2,h3,h4,p1,p2
static void do_port(session_t *sess)
{
unsigned int v[6];
//arg中保存的是IP和端口,解析出来
//sscanf从字符串获取输入按照一定的格式 格式化到相应的变量中
sscanf(sess->arg, "%u,%u,%u,%u,%u,%u", &v[2], &v[3], &v[4], &v[5], &v[0], &v[1]);
sess->port_addr = (struct sockaddr_in *)malloc(sizeof(struct sockaddr_in));
memset(sess->port_addr, 0, sizeof(struct sockaddr_in));
sess->port_addr->sin_family = AF_INET;
unsigned char *p = (unsigned char *)&sess->port_addr->sin_port;
p[0] = v[0];
p[1] = v[1];
p = (unsigned char *)&sess->port_addr->sin_addr;
p[0] = v[2];
p[1] = v[3];
p[2] = v[4];
p[3] = v[5];
ftp_reply(sess, FTP_PORTOK, "PORT command successful. Consider using PASV.");
}
2.数据传输需要数据套接字
获取主动模式下的数据套接字,因为主动模式需要绑定端口20,所以当前用户没有办法做到,所以向nobody发出命令,同时将客户端的ip和端口发送给父进程nobody,父进程接收到客户端的ip和端口之后,绑定本机地址以及20端口,然后向客户端发起连接。将已连接的数据套接字发送给子进程中。
//获取PORT模式下的数据套接字
int get_port_fd(session_t *sess)
{
/*
向nobody发送PRIV_SOCK_GET_DATA_SOCK命令
向nobbody发送一个整数port
向nobody发送一个字符串ip 不定长
*/
//获得数据连接套接字
priv_sock_send_cmd(sess->child_fd, PRIV_SOCK_GET_DATA_SOCK);
unsigned short port = ntohs(sess->port_addr->sin_port);
char *ip = inet_ntoa(sess->port_addr->sin_addr);
//发送端口号和IP地址
priv_sock_send_int(sess->child_fd, (int)port);
priv_sock_send_buf(sess->child_fd, ip, strlen(ip));
//接受应答
char res = priv_sock_get_result(sess->child_fd);
//失败的应答
if (res == PRIV_SOCK_RESULT_BAD) {
return 0;
}
//成功的应答
else if (res == PRIV_SOCK_RESULT_OK) {
//获取到主动模式数据套接字
sess->data_fd = priv_sock_recv_fd(sess->child_fd);
}
return 1;
}
//数据连接字的建立
static void privop_port_get_data_sock(session_t *sess)
{
//接收端口号
unsigned short port = (unsigned short)priv_sock_get_int(sess->parent_fd);
char ip[16] = {0};//255.255.255.255这不就是16个字节吗
//接收IP
priv_sock_recv_buf(sess->parent_fd, ip, sizeof(ip));
//nobody进程负责连接客户端
//注意nobody进程的sess->addr和ftp服务进程的sess->addr不是一回事,因为是两个不同的进程
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip);
//绑定20的端口号
int fd = tcp_client(20);
//创建套接字失败的话,给FTP服务进程应答
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);
//给FTP服务进程传输文件描述符,从而实现了FTP服务进程与客户端之间连接通道的创建
priv_sock_send_fd(sess->parent_fd, fd);
close(fd);
}
3.保存由父进程发送来的数据连接套接字
sess->data_fd = priv_sock_recv_fd(sess->child_fd);
4.接下来就可以利用此数据连接字进行数据传输了。
四、被动模式的实现
1.客户端向服务器端发送PASV命令
被动模式的过程,客户端向服务器端发送PASV命令,服务端收到之后,给227响应。子进程向父进程nobody发送创建监听套接字的命令,由nobody进程负责监听,同时nobody进程将监听端口发送给前台进程,前台进程将监听的端口以及IP地址信息回馈给客户端,以便客户端发起数据连接。
//被动模式的实现
//首先是客户端向ftp发送pasv的命令,FTP服务进程收到pasv命令之后,
//执行do_pasv函数
static void do_pasv(session_t *sess)
{
//Entering Passive Mode (192,168,244,100,101,46).
char ip[16] = {0};
//获取本地的IP地址
getlocalip(ip);
//监听的操作由nobody来完成
priv_sock_send_cmd(sess->child_fd, PRIV_SOCK_PASV_LISTEN);
//nobody监听完成之后,将实际绑定的端口号发送过来
unsigned short port = (int)priv_sock_get_int(sess->child_fd);
//将端口号格式化,然后发送给客户端
unsigned int v[4];
sscanf(ip, "%u.%u.%u.%u", &v[0], &v[1], &v[2], &v[3]);
char text[1024] = {0};
sprintf(text, "Entering Passive Mode (%u,%u,%u,%u,%u,%u).",
v[0], v[1], v[2], v[3], port>>8, port&0xFF);//高8位低8位的获取
//给客户端响应,包括IP地址和端口号
ftp_reply(sess, FTP_PASVOK, text);
}
nobody进程负责监听
监听的端口号是动态获取的,所以需要发送给前台进程。
//创建套接字,绑定、监听
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);
}
2.数据传输需要数据套接字
进行数据传输的时候,获取被动模式的数据套接字,此套接字是由nobody进程建立的,然后发送给前台进程,然后前台进程保存被动模式的数据套接字。
//获取被动模式的数据套接字
int get_pasv_fd(session_t *sess)
{
//发送一个PRIV_SOCK_PASV_ACCEPT给nobody进程
priv_sock_send_cmd(sess->child_fd, PRIV_SOCK_PASV_ACCEPT);
char res = priv_sock_get_result(sess->child_fd);
if (res == PRIV_SOCK_RESULT_BAD) {
return 0;
}
else if (res == PRIV_SOCK_RESULT_OK) {
//接收回传回来的数据套接字
sess->data_fd = priv_sock_recv_fd(sess->child_fd);
}
return 1;
}
nobody进程负责接受连接并且返回已连接套接字
//被动模式的数据连接字交给wangkai
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);
}
3.接下来就可以利用此数据连接字进行数据传输了。