Osheep

时光不回头,当下最重要。

ss-libev源码阅读(5):ss-local之remote_send_cb

remote_send_cb这个回调函数的工作是将从客户端收取来的数据转发给ss-server。在之前阅读server_recv_cb代码时可以看到,在STAGE_STREAM阶段有几种可能都会开启remote->fd的写事件的监听,从而当有写事件触发时调用remote_send_cb。从代码结构看,外层的分支是remote->send_ctx->connected是否为0,内部的分支是是否fast_open或直连。而根据实际代码执行流向,可看成是否有fast_open或直接。而最普通的情况就是没开启fast_open或者remote为直连目标服务器的情况,先讨论这种情况下的connected分支:

  1. remote->send_ctx->connected为0时,即第一次进入STAGE_STREAM,尚未连接remote server时。
    在server_recv_cb中直接调用非阻塞connect后,开启事件监听:

    ev_io_start(EV_A_ & remote->send_ctx->io);
    ev_timer_start(EV_A_ & remote->send_ctx->watcher);

    因为非阻塞fd调用connect后,当connect成功后,fd可写。所以这儿监听了send_ctx->io即写事件(回忆一下:ev_io_init(&remote->send_ctx->io, remote_send_cb, fd, EV_WRITE);
    现在看一下remote_send_cb函数中,当connect成功回调cb时,由于此时 remote->send_ctx->connected==0,所以会进入:

    if (!remote_send_ctx->connected) {
         struct sockaddr_storage addr;
         socklen_t len = sizeof addr;
         int r         = getpeername(remote->fd, (struct sockaddr *)&addr, &len);
         if (r == 0) {
             remote_send_ctx->connected = 1;
             ev_timer_stop(EV_A_ & remote_send_ctx->watcher);
             ev_timer_start(EV_A_ & remote->recv_ctx->watcher);
             ev_io_start(EV_A_ & remote->recv_ctx->io);
    
             // no need to send any data
             if (remote->buf->len == 0) {
                 ev_io_stop(EV_A_ & remote_send_ctx->io);
                 ev_io_start(EV_A_ & server->recv_ctx->io);
                 return;
             }
         } else {
             // not connected
             ERROR("getpeername");
             close_and_free_remote(EV_A_ remote);
             close_and_free_server(EV_A_ server);
             return;
         }
     }

    这儿通过getpeername获取对端地址,即服务器地址,如果返回值为0表示获取成功,因此判断为连接成功(其实我不明白为什么要判断,但是这么判断肯定没错)。连接成功后设置remote_send_ctx->connected=1,并且stop remote send timer。说明这种情况下先前设置的remote send timer只是用来做connect超时用。然后start remote recv的timer,且start remote fd的读事件监听(回忆一下:ev_io_init(&remote->recv_ctx->io, remote_recv_cb, fd, EV_READ);),即准备从remote接受返回的数据
    这之后判断remote->buf是否为空,如果为空则停止发送并开始监听读取客户端的数据。当前这种情况,buf不可能为空,因为至少还有socks5相关的头,即之前的abuf。如果getpeername返回非0则表示连接失败,关闭remote和server。
    继续看remote_send_cb的后半段:

    if (remote->buf->len == 0) {
         // close and free
         close_and_free_remote(EV_A_ remote);
         close_and_free_server(EV_A_ server);
         return;
     } else {
         // has data to send
         ssize_t s = send(remote->fd, remote->buf->data + remote->buf->idx,
                          remote->buf->len, 0);
         if (s == -1) {
             if (errno != EAGAIN && errno != EWOULDBLOCK) {
                 ERROR("remote_send_cb_send");
                 // close and free
                 close_and_free_remote(EV_A_ remote);
                 close_and_free_server(EV_A_ server);
             }
             return;
         } else if (s < (ssize_t)(remote->buf->len)) {
             // partly sent, move memory, wait for the next time to send
             remote->buf->len -= s;
             remote->buf->idx += s;
             return;
         } else {
             // all sent out, wait for reading
             remote->buf->len = 0;
             remote->buf->idx = 0;
             ev_io_stop(EV_A_ & remote_send_ctx->io);
             ev_io_start(EV_A_ & server->recv_ctx->io);
         }
     }

    如果buf为空则关闭连接,目前应该不会为空,所以走到else里面。使用send发送数据,发送的数据的指针是remote->buf->data+remote->buf->idx,回忆一下server_recv_cb里面,如果未connected的情况下,这个idx是设置为0的,所以这儿发送数据就是整个buf的数据。因为remote->fd是非阻塞的,send调用后就立即返回了,如果返回值s是-1,要检查一下errno是否为EAGAIN或EWOULDBLOCK,如果不是他俩就真出错了,断开连接;如果是他俩说明要等一下再发送,直接return出去等下次remote_send_cb被回调。下次再回调进来时,remote_send_ctx->connected已经是1了,所以直接进下半段代码继续send。
    如果s小于buf->len,说明发送了部分数据,需要调整idx的位置并从len减去s。等下次写事件触发可以继续发送时,就从idx的位置继续发送。
    如果s等于buf->len说明全部发送完毕,len和idx清0,stop remote fd的写事件监听并sart server fd的读事件监听,即继续从客户端读取数据。
    小结一下上面的讨论:是不使用fast_open或是直接目标服务器的情况下,之前没有connect时进行connet调用,connect成功后调用了remote_send_cb,随即发送remote->buf中的全部数据,可能一次send只发送了一部分,那么回调就会多次触发; 如果发送完成了则需要再次从客户端读取数据,此时客户端还是处于STAGE_STREAM状态。好了,我们现在回过头继续看server_recv_cb,此时已经是connect过了。

  2. remote->send_ctx->connected==1的情况。请进入server_recv_cb继续分析:
    在进入if (!remote->send_ctx->connected)对应的else代码块之前。先看一下此时读取的数据,if (!remote->direct) 里面之前是第一次进入,会将abuf插入到前面,此时已经没有abuf了,所以读取出来的数据直接加密后发送。从这儿可以看到ss只是TCP一次连接要发送的所有数据的开头加上一个头数据,之后就都是原始数据了,当然所有这些数据都是加密的。好了,继续看server_recv_cb里面的发送代码:
    else {
                 int s = send(remote->fd, remote->buf->data, remote->buf->len, 0);
                 if (s == -1) {
                     if (errno == EAGAIN || errno == EWOULDBLOCK) {
                         // no data, wait for send
                         remote->buf->idx = 0;
                         ev_io_stop(EV_A_ & server_recv_ctx->io);
                         ev_io_start(EV_A_ & remote->send_ctx->io);
                         return;
                     } else {
                         ERROR("server_recv_cb_send");
                         close_and_free_remote(EV_A_ remote);
                         close_and_free_server(EV_A_ server);
                         return;
                     }
                 } else if (s < (int)(remote->buf->len)) {
                     remote->buf->len -= s;
                     remote->buf->idx  = s;
                     ev_io_stop(EV_A_ & server_recv_ctx->io);
                     ev_io_start(EV_A_ & remote->send_ctx->io);
                     return;
                 } else {
                     remote->buf->idx = 0;
                     remote->buf->len = 0;
                 }
             }

    此处,即server_recv_cb的STAGE_STREAM阶段,已经连接上ss-server后,读取到新的客户端数据后随即发送到ss-server。即调用了send,注意这儿的send是从buf->data的开头发送的,结合之前的代码有个结论,server_recv_cb读取到数据后即试图发送buf,这些数据是新读取到的,所以从buf开头发送,如果发送了部分数据,则设置idx,然后在remote_send_cb里面从idx处继续发送,全部发送完毕后再次开启server_recv_cb对应的监听,继续读取客户端数据然后转发到ss-server。
    因此这儿send之后的代码很好理解,出错处理就不说了都一样;部分发送就是设置idx和len并停启相应事件;全部发送就是请空idx,len就行并不需要再设置开启send监听了,因为已经发送完了,就等着从客户端再次读取数据过来。在server_recv_cb前面,如果读取到EOF,即返回值为0,则说明客户端已经没有数据要发送了,且断开了连接,此次转发发送成功,回忆一下之前的代码:

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

    好了,那么remote_send_cb其实处理的就是server_recv_cb没有发送出去的数据,他要继续发送,因为已经connect过,所以直接进入上面贴出的remote_send_cb的后半段代码,和上面connect成功进入一样,调用send发送数据,处理部分发送和全部发送的情况。
    至此,不使用fast open或者直连目标服务器的情况已经分析完了。再总结一下整个流程:

    • remote_send_ctx->connected==0时,先在server_recv_cb里面执行connect,connect成功remote_send_cb被回调
    • remote_send_cb第一次被调用是connect成功时,通过getpeername判断是否成功,如果成功会开启remote fd的读事件监听和timer,即准备从ss-server接受数据。然后就是向ss-server发送数据了。connect时附带的数据可能分几次才发送完。总之发送完后就再启用server_recv_cb读取数据。
    • server_recv_cb再次被调用后就继续读取客户端的数据然后发送,如果是部分发送就再次调用remote_send_cb。最终没有数据可读取则断开连接。

以上,是不开启fast open或直连的情况。下面分析开启fast open的情况。

点赞