1. 程式人生 > >Miniftp專案實戰剖析(含原始碼連結)

Miniftp專案實戰剖析(含原始碼連結)

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 */
           };

在這裡就不在贅述其他東西,主要關心uidpw_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命令
}

在這裡將usernamepasswd分開驗證可以優化一下體驗,至少知道是什麼輸錯了,由於是分開設計的,所以我們需要儲存一個使用者資訊,因為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複用的情況下,當前目錄是共享的,無法根據每一個連線來擁有自己的當前目錄,也就是說當前使用者目錄的切換會影響到其他的使用者。

3ftp伺服器的架構

程序間通訊的協議制定

首先採用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_cmdsend_cmd的實現,可以看到只是簡單包裝下sendread函式

這裡就不再贅述其他函式,對於這些函式,我們主要關注一組特殊函式

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指向資料包緩衝區,即引數buffiov_lenbuff的長度。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伺服器是如何工作的吧

[[email protected]_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_effectiveCAP_SYS_TIME(25)是否有效.

  • cap_permitted:表示程序能夠使用的能力,cap_permitted中可以包含cap_effective中沒有的能力,這些能力是被程序自己臨時放棄的,也可以說cap_effectivecap_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_processtrue,安裝訊號的時候,檢測到data_process就不再安裝限時訊號。

第三個問題,如何實現呢?

首先,我們需要現在session中,安裝data_processsession是每個程序維護一個包含配置檔案和程序資訊的一個全域性節點。

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最大連線數

現在我們知道,我們需要一個數據結構來儲存ipip對應的連結數,所以我們首先想到的就是用鍵值對模型,即每個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)    //子程序
        {
            //防止子程序的子程序退出的干擾