主动模式和被动模式

一、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.接下来就可以利用此数据连接字进行数据传输了。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值