上一篇說到ss-libev創建listen_ctx_t對象用於監聽客戶端連接,調用accept_cb處理來自客戶端的新連接,創建server_t對象用於處理和客戶端之間的交互。本篇分析來自客戶端的SOCK5連接的建立以及傳輸數據的過程。
首先,回憶一下使用new_server()函數創建server_t對象時,註冊了客戶端連接的讀寫事件的回調:

ev_io_init(&server->recv_ctx->io, server_recv_cb, fd, EV_READ);
ev_io_init(&server->send_ctx->io, server_send_cb, fd, EV_WRITE);

即,對於客戶端和ss-local server之間的TCP連接,當local server的accepted fd上有可讀事件時,即客戶端發送數據過來了,server_recv_cb被調用;當local server的accepted fd上有可寫事件時,即可以向客戶端發送數據時(一般是準備發送數據時start,當寫緩衝可用時觸發),server_send_cb被調用。這是libev異步io的用法,具體可以查詢libev。注意這兩句只是註冊了事件回調,還沒有啟用監聽。在accept_cb裏面,new_server之後,隨即調用了 ev_io_start(EV_A_ & server->recv_ctx->io); 啟用了server->recv_ctx->io上面註冊的事件即讀事件的監聽,當客戶端有數據發過來時,server_recv_cb即被調用。而server->send_ctx->io的監聽暫時還沒啟用,因為此刻還不可能有數據可寫,要等到遠程服務器返迴轉發的數據之後才會啟用監聽。好了,先分析server_recv_cb。

static void server_recv_cb(EV_P_ ev_io *w, int revents) 分析

幾個數據結構

  • server_ctx_t
    typedef struct server_ctx {
      ev_io io;
      int connected;
      struct server *server;
    } server_ctx_t;
  • server_t

    typedef struct server {
      int fd;
      int stage;
    
      cipher_ctx_t *e_ctx;
      cipher_ctx_t *d_ctx;
      struct server_ctx *recv_ctx;
      struct server_ctx *send_ctx;
      struct listen_ctx *listener;
      struct remote *remote;
    
      buffer_t *buf;
      buffer_t *abuf;
    
      ev_timer delayed_connect_watcher;
    
      struct cork_dllist_item entries;
    } server_t;
  • remote_t

    typedef struct remote {
      int fd;
      int direct;
      int addr_len;
      uint32_t counter;
    
      buffer_t *buf;
    
      struct remote_ctx *recv_ctx;
      struct remote_ctx *send_ctx;
      struct server *server;
      struct sockaddr_storage addr;
    } remote_t;
  • buffer_t
    typedef struct buffer {
      size_t idx;
      size_t len;
      size_t capacity;
      char   *data;
    } buffer_t;

    其中 server_t 上篇已經說過,客戶端每來一個新的TCP請求,都會生成一個server_t,可以認為server_t描述了客戶端和ss-local server的交互狀態。server_t包含了server_ctx和buffer_t對象,並引用了remote對象。server_ctx_t用來具體處理客戶端和local server之間的數據讀寫,因此server中包含了兩個server_ctx:recv_ctx和send_ctx。server_ctx_t的結構上篇也說了,主要包含ev_io對象,以及connected標誌,並且還指向了他所屬於的server。remote是用於描述ss-local server和遠程ss-server之間的交互,暫且不說。buffer_t是一個緩衝區結構,server包含兩個緩衝區,分別是buf和abuf。另外server還有一個stage標誌,用於描述自身所處的狀態,在new_server裏面被初始化為STAGE_INIT。所有的狀態描述如下:

    #define STAGE_ERROR     -1  /* Error detected                   */
    #define STAGE_INIT       0  /* Initial stage                    */
    #define STAGE_HANDSHAKE  1  /* Handshake with client            */
    #define STAGE_PARSE      2  /* Parse the header                 */
    #define STAGE_RESOLVE    4  /* Resolve the hostname             */
    #define STAGE_WAIT       5  /* Wait for more data               */
    #define STAGE_STREAM     6  /* Stream between client and server */

參數解析

libev的io回調函數,會把io對象傳入。而server_recv_cb實際需要處理server等對象。在函數開頭,利用結構體裏面的指針位置,解出需要的指針:

server_ctx_t *server_recv_ctx = (server_ctx_t *)w;
server_t *server              = server_recv_ctx->server;
remote_t *remote              = server->remote;

隨後,根據是否已經創建remote,判斷使用哪個buf

if (remote == NULL) {
    buf = server->buf;
} else {
    buf = remote->buf;
}

如果已經創建了remote,則使用remote的buf,將數據讀入其中,後面會看到這是因為此時已經是要傳送具體的數據了,所以直接把數據讀取到remote的buf中。

讀取數據

既然server_recv_cb是可讀事件的回調,所以被調用時就可以讀取數據了。但這兒要注意的是server的stage,如果是STAGE_WAIT則不讀取,這個wait後面再說。

r = recv(server->fd, buf->data + buf->len, BUF_SIZE - buf->len, 0);

r是成功讀取到的數據的字節數,如果為0,表示讀取到了一個EOF,即連接已經close,沒有數據可讀了。因為這兒是讀取從客戶端發送過來的數據,因此是客戶端主動斷開了連接。此時ss-local要做的是關閉和釋放server和remote,本次代理交互結束。

if (r == 0) {
            // connection closed
            close_and_free_remote(EV_A_ remote);
            close_and_free_server(EV_A_ server);
            return;
 }

如果r==-1,則有可能是出錯了,也需要結束本次代理,但因為是非阻塞io,如果errno == EAGAIN 或 errno == EWOULDBLOCK,則表示現在沒有數據需要再試。如果r>0,則表示讀取到了r字節數據,記錄在buf的len中,並繼續執行。

buf->len += r;

SOCKS5 方法選擇 (in STAGE_INIT)

上文提到,一個新的server_t,stage初始化為STAGE_INIT,因此在server_recv_cb中,會先處理這個狀態。在STAGE_INIT中,處理SOCK5握手的第一個請求,即method select請求,按照SOCK5規範,客戶端先向SOCK5服務端查詢支持的認證方式,最常用的是匿名和用戶名密碼認證,客戶端在請求頭裡面將他希望使用的認證方式都列出來,服務端選擇他要用的方式並返迴響應。具體可參閱SOKC5 RFC1928。因為ss-local是在本地運行的,其實認證方式並沒有太大的意義,所以無論客戶端請求什麼樣的方式,ss-local都只返回匿名認證,也就是不認證直接通過,這樣就沒有後續的驗證環節了。

struct method_select_response response;
response.ver    = SVERSION;
response.method = 0;
char *send_buf = (char *)&response;
send(server->fd, send_buf, sizeof(response), 0);
server->stage = STAGE_HANDSHAKE;

如這段代碼所示,ss-local直接給客戶端發送了一個0X0500,表示我這是匿名認證你來握手吧,然後將server的stage設置為STAGE_HANDSHAKE。
發送method select reponse之後,有個tcp粘包處理,因為tcp是流協議,沒有消息邊界,一次recv出來的數據可能超過了本條消息的長度,因此ss-local在這兒做了個處理:

if (method->ver == SVERSION && method_len < (int)(buf->len)) {
      memmove(buf->data, buf->data + method_len , buf->len - method_len);
      buf->len -= method_len;
      continue;
}

此處method_len為SOCKS5方法選擇請求的消息長度,根據方法數而變化,如果方法數為1,則長度為3,方法數為2,則長度為4,即長度為方法數+2。如果讀取到的數據超過消息長度,則把超出的部分數據移動到buf的前端,並把buf的len設置為剩餘數據的長度。這樣相當於清除掉了已經處理過的方法選擇消息,保留了多讀取到的內容。不過我認為這種情況應該是不會發生的,因為SOCKS5握手階段的消息都是一應一答,如果服務端不返回method select response,客戶端應該不會進一步發送其他消息。TCP粘包一般發生在連續發送數據時。可以認為ss-local的處理比較嚴謹,可以看成是一種防禦性編程。上面沒提到的是,在處理方法選擇請求時,ss-local同樣處理了斷包的情況,即如果收到的數據長度不滿足消息的長度則直接返回。因為上面列出的數據讀取代碼,總是將數據添加到buf->data + buf->len處,所以斷包的情況自然能處理好。不過ss-local只是對請求的長度進行檢查,實際並不檢測其內容,所以客戶端只要發一個長度為3的任意包,也能通過這一階段,進入下面的握手。

SOCKS5握手(in STAGE_HANDSHAKE)

SOCK5匿名認證成功之後,客戶端就可以發送具體的請求細節了,ss-local稱之為握手階段。具體就是客戶端發送一個SOCKS5請求,粘一段RFC的內容:

        +----+-----+-------+------+----------+----------+
        |VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |
        +----+-----+-------+------+----------+----------+
        | 1  |  1  | X'00' |  1   | Variable |    2     |
        +----+-----+-------+------+----------+----------+

     Where:

          o  VER    protocol version: X'05'
          o  CMD
             o  CONNECT X'01'
             o  BIND X'02'
             o  UDP ASSOCIATE X'03'
          o  RSV    RESERVED
          o  ATYP   address type of following address
             o  IP V4 address: X'01'
             o  DOMAINNAME: X'03'
             o  IP V6 address: X'04'
          o  DST.ADDR       desired destination address
          o  DST.PORT desired destination port in network octet
             order

其中cmd為1是tcp轉發請求,3是udp聯合轉發請求,ss-local只支持這兩個cmd。
先來看代碼:

else if (server->stage == STAGE_HANDSHAKE || server->stage == STAGE_PARSE) {
            struct socks5_request *request = (struct socks5_request *)buf->data;
            size_t request_len = sizeof(struct socks5_request);
            struct sockaddr_in sock_addr;
            memset(&sock_addr, 0, sizeof(sock_addr));

            if (buf->len < request_len) {
                return;
            }

在server的stage為STAGE_HANDSHAKE或STAGE_PARSE時,將讀入的數據視為socks5 request處理,STAGE_PARSE後面再說,此處會判斷讀取的buf是否滿足request的長度,如果不滿足就返回等待數據繼續讀入。注意這兒定義了一個sockaddr_in對象sock_addr並設置為0,這個sock_addr會在socks5_response中返回給客戶端。繼續往後看,如果request->cmd為3,則會在sock_addr中填充udp轉發的監聽地址和端口:

if (request->cmd == 3) {
                udp_assc = 1;
                socklen_t addr_len = sizeof(sock_addr);
                getsockname(udp_fd, (struct sockaddr *)&sock_addr,
                            &addr_len);
                if (verbose) {
                    LOGI("udp assc request accepted");
                }
            }

這個udp_fd,就是上一篇中說的init_udprelay返回的fd。如果cmd是1,則sock_addr則不會有任何處理,保持0。如果cmd不是3也不是1,則ss-local會返回一個表示cmd不支持的response,其rep字段填0x07表示不支持cmd,並關閉這個代理連接。如果不是不支持,則代碼繼續往下,走到Fake reply。

// Fake reply
            if (server->stage == STAGE_HANDSHAKE) {
                struct socks5_response response;
                response.ver  = SVERSION;
                response.rep  = 0;
                response.rsv  = 0;
                response.atyp = 1;

                buffer_t resp_to_send;
                buffer_t *resp_buf = &resp_to_send;
                balloc(resp_buf, BUF_SIZE);

                memcpy(resp_buf->data, &response, sizeof(struct socks5_response));
                memcpy(resp_buf->data + sizeof(struct socks5_response),
                       &sock_addr.sin_addr, sizeof(sock_addr.sin_addr));
                memcpy(resp_buf->data + sizeof(struct socks5_response) +
                       sizeof(sock_addr.sin_addr),
                       &sock_addr.sin_port, sizeof(sock_addr.sin_port));

                int reply_size = sizeof(struct socks5_response) +
                                 sizeof(sock_addr.sin_addr) + sizeof(sock_addr.sin_port);

                int s = send(server->fd, resp_buf->data, reply_size, 0);

                bfree(resp_buf);

                if (s < reply_size) {
                    LOGE("failed to send fake reply");
                    close_and_free_remote(EV_A_ remote);
                    close_and_free_server(EV_A_ server);
                    return;
                }
                if (udp_assc) {
                    close_and_free_remote(EV_A_ remote);
                    close_and_free_server(EV_A_ server);
                    return;
                }
            }

為啥叫fake reply呢,因為按照正常流程,客戶端發過來的請求包含了域名或者ip地址,socks5服務端需要去實際連接目的服務器才知道是否能代理,如果不能代理,比如根本訪問不了,則會在socks5 response中的rep中填入相應的錯誤碼,如0x04 Host unreachable。而ss-local的處理是直接認為可以訪問,返回可成功代理的response,也就是0x0500 0001加後上sock_addr中的地址和端口號,sock_addr是socks5服務器訪問目的地址使用的ip地址和端口。對於tcp的情況,這個地址和端口號對於客戶端沒什麼用,可以全為0,並且這兒是fake replay,此時連接沒有建立,所以只能填0。且通過代碼看,sock_addr確實初始化為0了,所以這兒填的全是0。如果是udp,則不全為0了,最後兩位端口號是真實的端口號,即udp_fd對應的端口號。ss-local處理udp轉發的方式是建立一個全局的udp監聽端口,在fake replay的時候直接把這個端口發給客戶端,客戶端就可以往此端口發送數據了。注意在fake reply最後,如果是udp_assc的情況,直接把tcp連接斷開了,這點不符合SOCKS5規範,不過在最新版中已經改掉了。在SOCKS5規範中,如果udp assc的tcp斷開,客戶端會認為udp端口不再可用,需要重新請求。
正常來說,發送fake reply之後,握手就結束了,不過STAGE_HANDSHAKE中還有一些處理,為避免本篇太長,留下篇再說。

分享