前言

在前面一篇文章中 vsftpd 的调试环境的搭建, 我们搭建了一个 vsftpd 的一个调试环境 

既然搭建了一个调试环境, 目的就是为了能够调试 vsftpd, 如果 不去调试, 那岂非毫无意义 

这里 就拿相对比较简单的登录流程 来进行开刀 

呵呵 虽然只是简单的登录流程, 但是 代码的设计还是颇为复杂, 初看的话 也很容易 迷糊 

里面对于 多进程的使用, 多进程来组合完成 ftp服务, 多进程之间的数据交互, 着实还是有一些 令人头痛(头秃)的地方的  

并且 由于处理业务使用的是 多进程的模式, 调试也非常不方便  

 

以下调试基于 vsftpd 3.0.2, 配置均基于 源码仓库的默认配置 

 

 

ftp 的登录

master:~ jerry$ ftp 
ftp> open 192.168.202.133
Connected to 192.168.202.133.
220 this is greeting
Name (192.168.202.133:jerry): root
331 Please specify the password.
Password: 
230 Login successful.

 

 

ftp 客户端 & 服务端 tcp 交互

一下为 以上命令 的过程中, 客户端 和 服务端的 协议交互概览信息 

其中 192.168.202.1 为 ftp 客户端, 也就是我本机, 192.168.202.133 为服务端, 虚拟机里面的一个 ftp 服务器 

10	0.005591	192.168.202.133	192.168.202.1	FTP	88	Response: 220 this is greeting
12	1.224853	192.168.202.1	192.168.202.133	FTP	77	Request: USER root
14	1.225629	192.168.202.133	192.168.202.1	FTP	100	Response: 331 Please specify the password.
16	1.989461	192.168.202.1	192.168.202.133	FTP	77	Request: PASS root
20	2.034438	192.168.202.133	192.168.202.1	FTP	89	Response: 230 Login successful.

 

 

vsftpd 服务器的启动

首先我们来看一下 它绑定的端口, 从代码中可以看到 取到的是 21, 可以通过配置 listen_port 来修改这个端口 

但是 从下面的 p_sockaddr.u.u_sockaddr_in.sin_port 看到的是 5376, 这两者是什么对应关系 ? 

21 = 0x0015
htons(0x0015) = 0x1500 = 5376

03 vsftpd 登录过程的调试_客户端

 

服务创建了之后 可以看到 开始在这里不断的接收连接了 

03 vsftpd 登录过程的调试_客户端_02

 

 

open 192.168.202.133 

当我们输入了 open 192.168.202.133 之后, 两台机器之间会有如下 协议数据交互 

1	0.000000	192.168.202.1	192.168.202.133	TCP	78	54946 → 21 [SYN, ECN, CWR] Seq=0 Win=65535 Len=0 MSS=1460 WS=64 TSval=1002536282 TSecr=0 SACK_PERM=1
2	0.000358	192.168.202.133	192.168.202.1	TCP	74	21 → 54946 [SYN, ACK, ECN] Seq=0 Ack=1 Win=28960 Len=0 MSS=1460 SACK_PERM=1 TSval=11985962 TSecr=1002536282 WS=128
3	0.000428	192.168.202.1	192.168.202.133	TCP	66	54946 → 21 [ACK] Seq=1 Ack=1 Win=131712 Len=0 TSval=1002536282 TSecr=11985962
4	0.004275	192.168.202.133	192.168.202.1	FTP	88	Response: 220-this is greeting
5	0.004342	192.168.202.1	192.168.202.133	TCP	66	54946 → 21 [ACK] Seq=1 Ack=23 Win=131712 Len=0 TSval=1002536286 TSecr=11985963
6	0.004366	192.168.202.133	192.168.202.1	FTP	72	Response: 220 
7	0.004380	192.168.202.1	192.168.202.133	TCP	66	54946 → 21 [ACK] Seq=1 Ack=29 Win=131712 Len=0 TSval=1002536286 TSecr=11985963

前三行对应的是 tcp 客户端, 服务端建立请求的三次握手 

第 4, 6 行对应的是 ftp 服务器响应个客户端的一个 友好问候提示, 客户端收到 数据之后 提示用户输入密码 

第 5, 7 行分别对应的是, 第 4, 6 行的 tcp 确认请求 

可以看到我们 open 之后是没有录入端口到信息, 这里使用的是 默认的 21 端口作为 控制端口 

 

当客户端的请求来到服务端, 我们先看一下 请求的相关信息, p_accept_addr 为客户端的相关信息 

# 服务器拿到的客户端的端口
14551 = 0x38D7
htons(port) = 0x38D7
port = 0xD738 = 55096

# 服务器拿到的 客户端的 ip
30058688 = 0x1CAA8C0
htons(ip) = 0x1CAA8C0
ip = 0xc0a8ca01 = 192.168.202.1

当然也有现成的工具方法来解析 ip 和 端口, 比如如下 156 行的 println3 输出的结果如下 

40697 - 192.168.202.1 <- host & port -> 55096

 

03 vsftpd 登录过程的调试_子进程_03

 

然后再来看看 处理这个 tcp 请求的逻辑 

03 vsftpd 登录过程的调试_用户名_04

可以看到的是 fork 了一个子进程出来 

父进程处理的事情就是 记录了一下客户端的信息, 然后继续 去阻塞接受请求了 

子进程的处理是 return 了, 继续走 standalone_main 之后的处理 

到此为止 可以再 ide 里面调试的东西 就这些了, 呵呵 因为处理业务的进程是 fork 了一个进程来进行处理 

 

 

vsf_standalone_main fork 的子线程

调试tips

所以 只能通过一些"输出" 之类的方式来进行调试了 

并且 普通的 printf 是不行的, 因为 printf 的数据会被返回给客户端, 导致客户端 数据异常, 我这里采用的方式是 输出到 日志文件  

 

 

子进程在默认的配合下面 会走 main 后面的 vsf_two_process_start  

以下截图省略掉了一部分我们这里不太关心的代码, priv_sock_init 里面初始化了两个 fd, parent_id, child_fd, 用于 之后的父子进程进行通讯 

然后 之后父进程进入等待子进程和客户端进行输入用户名, 密码的交互 

子进程发送 友好问候信息[提示客户端输入用户名], 也就是 上面的 "220 this is greeting"  

03 vsftpd 登录过程的调试_vsftpd_05

 

 

vsf_two_process_start fork 的子线程

子进程需要执行的 init_connection 如下, 如果启动 ftp 服务, 需要给客户端 greeting 

然后之后 进入与客户端进行交互的 repl 

03 vsftpd 登录过程的调试_ftp_06

 

greeting 消息的处理, banner_str 表示的是 banner_file 解析出来的配置信息, 在 main 方法里面配置的 

另外可以手动配置 ftpd_banner 作为 banner 返回  

默认的 banner 为 (vsFTPd $version) 

03 vsftpd 登录过程的调试_vsftpd_07

 

和客户端交互的 认证相关操作, 这里省略了一些其他的 认证方式, 只留下了我们这里的 用户名 密码 的认证

03 vsftpd 登录过程的调试_vsftpd_08

 

ftp 客户端拿到了 服务器传递的 greeting 的信息之后, 会提示输入 用户, 也就是接下来的 "Name (192.168.202.133:jerry): root" 

这个 greeting 信息对应于上面的 ftp 响应 "Response: 220 this is greeting" 

 

 

Name (192.168.202.133:jerry): root

客户端提示用户输入用户名, 录入用户名 回车之后, 客户端发送 请求给服务端, 也就是上面的 ftp 请求 "Request: USER root" 

处理请求的业务代码大致如下[省略了一部分ssl相关的校验代码], 对于上下文比较重要的就是设置了 user_str 的值, 其他的大部分为数据校验 

最后, 如果匿名用户不需要密码 直接走登录流程, 如果需要密码, 返回 ftp 响应 "331 Please specify the password." 

03 vsftpd 登录过程的调试_客户端_09

 

 

Password: ****

客户端拿到了 "331 Please specify the password.", 之后提示用户输入密码  

331 Please specify the password.
Password:

 

用户录入了密码之后, 客户端发送请求给服务端, 也就是上面的 ftp 请求 "Request: PASS root" 

处理请求的业务代码大致如下, 主要是将业务委托给了 vsf_two_process_login, 带上了 session 和 ftp_arg_str[用户输入的密码]

03 vsftpd 登录过程的调试_客户端_10

 

vsf_two_process_login 的处理流程大致如下, 封装了一个命令, 并带上了需要的几个参数, 用户名 密码, use_ssl 等相关参数 

和它的父进程进行交互, 它的父进程 也就是 vsf_standalone_main fork 出来的子进程, 如果是登录成功之后, 当前这个进程就已经完成了它的事情了, exit 

如果登录失败 继续进入 parse_username_password 的循环流程, 客户端可以再传入密码, 或者其他的方式

对于登录失败的场景, 我们这里的 ftp 客户端的处理方式很简单 你需要重新建立连接 

03 vsftpd 登录过程的调试_ftp_11

 

 

登录的业务处理

那么 登录的逻辑是在哪里处理的呢? 

上面 的子进程吧用户录入的用户名密码 等信息交给了父进程, 也就是 vsf_standalone_main fork 的子进程, vsf_two_process_start 中的父进程 

复制上面的这张图下来, 可以看到的是 父进程是进入了一个 while(true), 处理的函数是 process_login_req, 呵呵 名字还是 很符合实际意义的 

03 vsftpd 登录过程的调试_vsftpd_05

 

这里面等待的是 子进程发送消息过来, 校验了一下消息, 然后获取参数 等等, 校验用户名密码, 是否有效 

如果登录失败 返回标记给 子进程, 如果登录成功 最终调用核心处理函数 common_do_login 走登录之后的流程 

具体的用户名密码的校验是基于 PAM认证机制, 有兴趣可以了解了解 

03 vsftpd 登录过程的调试_子进程_13

 

 

common_do_login fork 子进程

然后我们来看看 common_do_login 的相关处理 

这里我们可以看到 上面的 和 vsf_two_process_start 类似的处理方式 

priv_sock_init 创建了一个 socket pair 来供父子进程交互 

子进程走的是 process_post_login, 接受客户端的请求并处理 

父进程走的是 vsf_priv_partent_postlogin, 主要是 子进程这边有什么需要进行业务的支撑 

03 vsftpd 登录过程的调试_ftp_14

 

process_post_login 的处理就是 和 ftp 客户端登录之后的相关交互了 

另外就是 登录成功之后的 ftp 响应 "230 Login successful." 来自于这里  

03 vsftpd 登录过程的调试_用户名_15

 

 

以上 ftp 请求/响应

ftp 请求 "USER root", 格式为 请求命令 + 空格 + 参数.. + \r\n

03 vsftpd 登录过程的调试_vsftpd_16

 

ftp 响应 "331 Please specify the password.“, 格式为 响应码 + 空格 + 响应内容 + \r\n

03 vsftpd 登录过程的调试_子进程_17

 

其他的请求响应 不多加赘述, 类似的  

 

 

FileZilla 的一次登录 

可以看出的是, 大致的流程是和 命令行的 ftp 客户端的登录流程一致 

使用 用户名, 密码 验证之前尝试使用 auth 来进行验证, 但是我服务器不支持 ssl, 因此这段处理 走的是 ”vsf_cmdio_write(p_sess, FTP_LOGINERR,                         "Please login with USER and PASS.");“ 

最后尝试使用  明文的 用户名, 密码进行的登录 

登录之后 客户端自动发起了一些请求用于支撑客户端的展示, 以及业务使用等等, 比如这里的 SYST, PWD, LIST 等等 

4	0.004657	192.168.202.133	192.168.202.1	FTP	88	Response: 220-this is greeting
6	0.004733	192.168.202.133	192.168.202.1	FTP	72	Response: 220 
8	0.005402	192.168.202.1	192.168.202.133	FTP	76	Request: AUTH TLS
10	0.005663	192.168.202.133	192.168.202.1	FTP	104	Response: 530 Please login with USER and PASS.
12	0.005765	192.168.202.1	192.168.202.133	FTP	76	Request: AUTH SSL
13	0.005838	192.168.202.133	192.168.202.1	FTP	104	Response: 530 Please login with USER and PASS.
15	2.200732	192.168.202.1	192.168.202.133	FTP	77	Request: USER root
16	2.201208	192.168.202.133	192.168.202.1	FTP	100	Response: 331 Please specify the password.
18	2.201349	192.168.202.1	192.168.202.133	FTP	77	Request: PASS root
20	2.257612	192.168.202.133	192.168.202.1	FTP	89	Response: 230 Login successful.
22	2.257870	192.168.202.1	192.168.202.133	FTP	72	Request: SYST
24	2.259606	192.168.202.133	192.168.202.1	FTP	85	Response: 215 UNIX Type: L8
26	2.259859	192.168.202.1	192.168.202.133	FTP	72	Request: FEAT
27	2.260888	192.168.202.133	192.168.202.1	FTP	81	Response: 211-Features:
29	2.261840	192.168.202.133	192.168.202.1	FTP	73	Response:  EPRT
31	2.262123	192.168.202.133	192.168.202.1	FTP	73	Response:  EPSV
33	2.262237	192.168.202.133	192.168.202.1	FTP	73	Response:  MDTM
35	2.262311	192.168.202.133	192.168.202.1	FTP	73	Response:  PASV
37	2.263128	192.168.202.133	192.168.202.1	FTP	80	Response:  REST STREAM
39	2.263194	192.168.202.133	192.168.202.1	FTP	73	Response:  SIZE
41	2.263916	192.168.202.133	192.168.202.1	FTP	73	Response:  TVFS
43	2.264924	192.168.202.133	192.168.202.1	FTP	73	Response:  UTF8
45	2.265039	192.168.202.133	192.168.202.1	FTP	75	Response: 211 End
47	2.265168	192.168.202.1	192.168.202.133	FTP	80	Request: OPTS UTF8 ON
48	2.267011	192.168.202.133	192.168.202.1	FTP	92	Response: 200 Always in UTF8 mode.
50	2.269004	192.168.202.1	192.168.202.133	FTP	71	Request: PWD
51	2.270520	192.168.202.133	192.168.202.1	FTP	79	Response: 257 "/root"
53	2.277446	192.168.202.1	192.168.202.133	FTP	74	Request: TYPE I
54	2.278030	192.168.202.133	192.168.202.1	FTP	97	Response: 200 Switching to Binary mode.
56	2.278217	192.168.202.1	192.168.202.133	FTP	72	Request: PASV
57	2.278825	192.168.202.133	192.168.202.1	FTP	117	Response: 227 Entering Passive Mode (192,168,202,133,70,9).
60	2.281531	192.168.202.1	192.168.202.133	FTP	72	Request: LIST
63	2.283049	192.168.202.133	192.168.202.1	FTP	105	Response: 150 Here comes the directory listing.
69	2.283372	192.168.202.133	192.168.202.1	FTP	90	Response: 226 Directory send OK.
73	2.284575	192.168.202.1	192.168.202.133	FTP	83	Request: MDTM vsftpd.log
74	2.285112	192.168.202.133	192.168.202.1	FTP	86	Response: 213 20201214034847

 

 

commons.net-3.6 的客户端的登录 

呵呵 和命令行的 ftp 客户端的步骤一致, 丝毫没有多余的交互 

513	570.291665	192.168.202.133	192.168.202.1	FTP	88	Response: 220-this is greeting
515	570.291781	192.168.202.133	192.168.202.1	FTP	72	Response: 220 
517	570.294873	192.168.202.1	192.168.202.133	FTP	77	Request: USER root
519	570.295090	192.168.202.133	192.168.202.1	FTP	100	Response: 331 Please specify the password.
521	570.295219	192.168.202.1	192.168.202.133	FTP	77	Request: PASS root
525	570.339024	192.168.202.133	192.168.202.1	FTP	89	Response: 230 Login successful.

 

 

一次登录经历过的进程

从日志可以看到 fork 了三次, 但是 从程序里面获取到的 进程号 似乎是存在问题的, newPid 不为 0 的是父进程, newPid 为 0 的是 fork 出来的子进程 

40697 - 192.168.202.1 <- host & port -> 60628
40697 - 40697 - vsf_standalone_main fork with newPid - 42738
40697 - 40697 - vsf_standalone_main fork with newPid - 0
40697 -  vsf_two_process_start, forked with newPid = 2
40697 -  vsf_two_process_start, forked with newPid = 0
40697 - common_do_login, forked with newPid = 3
40697 - common_do_login, forked with newPid = 0

 

查看一下相关的进程, 可以看到 40697 是监听 21 端口的主进程, 在 vsf_standalone_main 里面 fork 出来的是 42738

42738 在 vsf_two_process_start 中 fork 出了 xxx[程序里面拿到的是 2], 然后登陆成功之后 exit 掉了, 所以 ps -ef 里面是看不到这个进程的  

42738 在 common_do_login 中 fork 出了 42740, 用于和登陆之后的客户端的输入进行交互 

root@ubuntu:~# ps -ef | grep ftp
root      40697  40695  0 14:43 pts/21   00:00:00 /root/ClionWorkStations/vsftpd/vsftpd ./vsftpd.conf
nobody    42738  40697  0 20:03 ?        00:00:00 /root/ClionWorkStations/vsftpd/vsftpd ./vsftpd.conf
root      42740  42738  0 20:03 ?        00:00:00 /root/ClionWorkStations/vsftpd/vsftpd ./vsftpd.conf

 

 

inspect 一下 vsf_two_process_start fork 出来的 xxx[程序里面拿到的是 2] 

从代码中我们可以知道, 这个 2 号进程向客户端返回了 提示信息, 并在等待客户端的输入, 那么我们可以在这里 看到这个进程 

ftp 客户端操作为, 等待用户输入用户名, 密码 

ftp> open 192.168.202.133
Connected to 192.168.202.133.
220-this is greeting
220 
Name (192.168.202.133:jerry):

此时后台日志信息如下 

40697 - 192.168.202.1 <- host & port -> 60713
40697 - 40697 - vsf_standalone_main fork with newPid - 42793
40697 - 40697 - vsf_standalone_main fork with newPid - 0
40697 -  vsf_two_process_start, forked with newPid = 2
40697 -  vsf_two_process_start, forked with newPid = 0

进程信息如下 

root@ubuntu:~# ps -ef | grep ftp
root      40697  40695  0 14:43 pts/21   00:00:00 /root/ClionWorkStations/vsftpd/vsftpd ./vsftpd.conf
root      42793  40697  0 20:12 ?        00:00:00 /root/ClionWorkStations/vsftpd/vsftpd ./vsftpd.conf
nobody    42794  42793  0 20:12 ?        00:00:00 /root/ClionWorkStations/vsftpd/vsftpd ./vsftpd.conf

 

 

完