chromium开发备忘——socks5密码认证支持
丑角的晨歌 发布于 2019-05-23

chromium虽然支持socks5bt365体育在线网址,但是不支持用户名密码认证。根据官方的反馈,大致是认为socks5认证协议中用户名密码都是通过明文传输,所以没有太大意义(https://bugs.chromium.org/p/chromium/issues/detail?id=256785)。但是项目中碰到了相关需求,所以只能自己打个补丁支持一下了。

协议分析

只分析用户名密码认证流程协议,这里的抓包来自QQ的bt365体育在线网址测试功能;

第一步

TCP建立连接之后,客户端发送请求协商协议版本和认证方法,具体字段如下:
客户端连到服务器后,然后就发送请求来协商版本和认证方法,具体字段如下:

字段名 VER NMETHODS METHODS
字节数 1 1 1-255

VER:版本号,sock5 为X'05'
NMETHODS:可选的认证方法数量,与METHODS的长度对应
METHOD:有以下几种:
X'00' 无需认证
X'01' 通用安全服务应用程序(GSSAPI)
X'02' 用户名/密码 auth (USERNAME/PASSWORD)
X'03'- X'7F' IANA 分配(IANA ASSIGNED)
X'80'- X'FE' 私人方法保留(RESERVED FOR PRIVATE METHODS)
X'FF' 无可接受方法(NO ACCEPTABLE METHODS)


图中客户端发送的为05 01 02,即只支持用户名密码认证方式。

服务端响应第一步

服务器从METHODS给出的方法中选出一种,发送一个METHOD(方法)选择报文。

字段名 VER METHOD
字节数 1 1

socks5协议的VER为X'05',METHOD为所选择的认证方式。由于这里是要进行用户名密码认证,所以自然返回02:


第二步

协商确定使用用户名密码认证之后,客户端发送用户名密码。请求格式如下:

字段名 VER ULEN USERNAME PLEN PASSWORD
字节数 1 1 1-255 1 1-255

VER为鉴权协议版本,这里固定为0x01;
ULEN与PLEN为用户名或密码长度,决定了服务端会读多少个字节作为用户名或密码。注意到这里只有一个字节用来表述长度,所以用户名与密码的长度不允许超过255;对于这条请求更详细的介绍可以参考https://www.ietf.org/rfc/rfc1929.txt
唔……这个就不放抓包图了,里面账号密码。

服务端响应第二步

服务端响应格式:

字段名 VER STATUS
字节数 1 1

VER为鉴权协议版本,这里固定为0x01;
STATUS为0x00即成功,为0x01则失败:



到这里鉴权就完成了,客户端就可以发起CONNECT、BIND、UDP ASSOCIATE等命令了,后面的协议就不浪费篇幅了多讲了。实际上后面的过程chromium已经完成了,我们这里所需要做的只是补充一下前两步。

代码编写

SOCKS5的实现代码位于net/socket/socks5_client_socket.h,这里先简单分析一下原来的代码结构,可以看成一个状态机。核心在于DoLoop函数:

int SOCKSClientSocket::DoLoop(int last_io_result) {
  DCHECK_NE(next_state_, STATE_NONE);
  int rv = last_io_result;
  do {
    State state = next_state_;
    next_state_ = STATE_NONE;
    switch (state) {
      case STATE_RESOLVE_HOST:
        DCHECK_EQ(OK, rv);
        rv = DoResolveHost();
        break;
      case STATE_RESOLVE_HOST_COMPLETE:
        rv = DoResolveHostComplete(rv);
        break;
      ......
      default:
        NOTREACHED() << "bad state";
        rv = ERR_UNEXPECTED;
        break;
    }
  } while (rv != ERR_IO_PENDING && next_state_ != STATE_NONE);
  return rv;
}

DoLoop中根据state决定走哪一步流程,而流程实际的处理函数中可以通过修改state来决定下一步流程,chromium源码中有很多类似这样的写法,可以学习。(HttpCacheTransaction中维护了一个几十个状态的状态机 0.0)
那么回到SOCKS5的问题,Socks5ClientSocket中其实只有两个步骤:GREET、HANDSHAKE,每个步骤又会分为READ、READ_COMPLETE、WRITE、WRITE_COMPLETE四个流程。通过调试抓包等可以确定,GREET对应的是上文所述的协议第一步(确定认证方法),而HANDSHAKE则为CONNECT请求。由于不支持用户认证,所以代码中没有上面第二步的流程。所以我们需要先给enum State加上认证的状态:

  case STATE_AUTHORIZATION_WRITE:
    rv = DoAuthorizationWrite();
    break;
  case STATE_AUTHORIZATION_WRITE_COMPLETE:
    rv = DoAuthorizationWriteComplete(rv);
    break;
  case STATE_AUTHORIZATION_READ:
    rv = DoAuthorizationRead();
    break;
  case STATE_AUTHORIZATION_READ_COMPLETE:
    rv = DoAuthorizationReadComplete(rv);
    break;

DoAuthorizationWrite即为发送认证数据,DoAuthorizationWriteComplete用来确认客户端数据发送完毕,DoAuthorizationRead(Complete)函数不多赘述:

int SOCKS5ClientSocket::DoAuthorizationWrite() {
  next_state_ = STATE_AUTHORIZATION_WRITE_COMPLETE;

  if (buffer_.empty()) {
    /// build auth buffer
    // Since we only have 1 byte to send the usr/pwd length in, if the
    // usr/pwd length is longer than 255 characters we can't send it.
    buffer_.push_back(0x01);
    DCHECK(auth_username_.size() <= 255);
    DCHECK(auth_password_.size() <= 255);

    size_t auth_username_len = auth_username_.size();
    size_t auth_password_len = auth_password_.size();
    if (auth_username_len > 255 || auth_password_len > 255)
      return ERR_SOCKS_CONNECTION_FAILED;
  
    buffer_.push_back(auth_username_len);
    buffer_.append(auth_username_);
    buffer_.push_back(auth_password_len);
    buffer_.append(auth_password_);

    bytes_sent_ = 0;
  }

  int handshake_buf_len = buffer_.size() - bytes_sent_;
  DCHECK_LT(0, handshake_buf_len);
  handshake_buf_ = new IOBuffer(handshake_buf_len);
  memcpy(handshake_buf_->data(), &buffer_[bytes_sent_],
         handshake_buf_len);
  return transport_->socket()->Write(handshake_buf_.get(), handshake_buf_len,
                                     io_callback_, traffic_annotation_);  
}
int SOCKS5ClientSocket::DoAuthorizationWriteComplete(int result) {
  if (result < 0)
    return result;

  bytes_sent_ += result;
  if (bytes_sent_ == buffer_.size()) {
    buffer_.clear();
    bytes_received_ = 0;
    next_state_ = STATE_AUTHORIZATION_READ;
  } else {
    next_state_ = STATE_AUTHORIZATION_WRITE;
  }
  return OK;
}

其实啊对照着原来的代码依样画葫芦就完事了。别忘了还需要修改一下第一步(协商协议),由于chromium原来不打算支持身份认证,所以发送的是05 01 00,我们既然加上了认证支持,那这里就需要改成发送05 01 00 02了;在GreetReadComplete中也需要对服务端返回的方法进行判断,相应跳转到STATE_HANDSHAKE_WRITE或者STATE_AUTHORIZATION_WRITE;
这里还需要考虑一个问题,bt365体育在线网址的用户名密码如何传过来。这里我的实现是放在HttpNetworkSession的实例中,在初始化SocksParam时顺便传过来,已经可以满足业务实现。也可以参考chromium弹HTTPbt365体育在线网址的用户名密码输入框的方式弹框让用户自己输入,这个就相对复杂一些,需要的可以自行研究。

推荐阅读