1. 程式人生 > >ss-libev 原始碼解析local篇(2):ss_local和socks5客戶端握手

ss-libev 原始碼解析local篇(2):ss_local和socks5客戶端握手

上一篇說到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中還有一些處理,為避免本篇太長,留下篇再說。