multimap 线程安全_简单安全http中继服务

简单http安全proxy服务

http代理概念

HTTP 代理存在两种形式,分别简单介绍如下:

  1. 第一种是 RFC 7230 - HTTP/1.1: Message Syntax and Routing(即修订后的 RFC 2616,HTTP/1.1 协议的第一部分)描述的普通代理。这种代理扮演的是「中间人」角色,对于连接到它的客户端来说,它是服务端;对于要连接的服务端来说,它是客户端。它就负责在两端之间来回传送 HTTP 报文, 负责http相关报文的解析。
  2. 第二种是 Tunneling TCP based protocols through Web proxy servers(通过 Web 代理服务器用隧道方式传输基于 TCP 的协议)描述的隧道代理。它通过 HTTP 协议正文部分(Body)完成通讯,以 HTTP 的方式实现任意基于 TCP 的应用层协议代理。这种代理使用 HTTP 的 CONNECT 方法建立连接,当原始服务器返回连接成功之后,浏览器与最终服务器之间的消息通信不再采用http协议来封装数据,而是直接传递字节流,代理服务器的作用就是进行数据转发。

通过代理访问 A 网站,对于 A 来说,它会把代理当做客户端,完全察觉不到真正客户端的存在,这实现了隐藏客户端 IP 的目的。代理也可以修改 HTTP 请求头部,通过 X-Forwarded-IP 这样的自定义头部告诉服务端真正的客户端 IP。但服务器无法验证这个自定义头部真的是由代理添加,还是客户端修改了请求头。

给浏览器显式的指定代理,需要手动修改浏览器或操作系统相关设置,或者指定 PAC(Proxy Auto-Configuration,自动配置代理)文件自动设置,还有些浏览器支持 WPAD(Web Proxy Autodiscovery Protocol,Web 代理自动发现协议)。显式指定浏览器代理这种方式一般称之为正向代理,浏览器启用正向代理后,会对 HTTP 请求报文做一些修改,来规避老旧代理服务器的一些问题。

还有一种情况是访问 A 网站时,实际上访问的是代理,代理收到请求报文后,再向真正提供服务的服务器发起请求,并将响应转发给浏览器。这种情况一般被称之为反向代理,它可以用来隐藏服务器 IP 及端口。一般使用反向代理后,需要通过修改 DNS 让域名解析到代理服务器 IP,这时浏览器无法察觉到真正服务器的存在,当然也就不需要修改配置了。反向代理是 Web 系统最为常见的一种部署方式,一般使用nginx来当反向代理以处理负载均衡问题。

HTTP 协议解析

http 报文可以分为两种:请求报文和应答报文,对于报文结构不再赘述,网上有很多资料,这里就简单贴一下图。

http 请求的格式如下:

34466a97bf3fbc81291238e61626f01d.png

http 响应的格式如下:

8be74804a95dac81fababc415c556af8.png

这里提一下自己解析协议时候的一些关键点:

  1. 同一个packet内,可能存在重复的首部字段名,所有字段值是可以用逗号分割的都可以转换成多行描述的形式, 所以存储的时候不能用简单的unordered_map, 而要用unordered_multimap
  2. 报文内的Content-Length字段给出了entity body的长度, 但是有些报文可能压根没有这个字段,采取的可能是不定的,例如Transfer-Encoding:chunked,此时采取的是游程编码,客户端需要不停的读取数据流,直到读取到rnrn,此时才代表一个packet的结束。
  3. connect方法返回ok之后,后续流量不再是http协议,代理服务器也不要试图去解析。

连接管理

最早的http协议里面,一个请求就要开一个tcp链接,等待请求结果回来之后,关闭这个连接。由于tcp链接的三次握手和四次握手的问题,导致延迟和消耗都非常大,所以http1.1版本中,加入了keep-alive选项。如果开启了keep-alive,则客户端收到请求之后,不再关闭链接,后续对于同站点的相关请求可以复用此链接。

对于显示的代理服务器来说,客户端发过来的请求头并没有Connection: keep-alive, 取而代之的是'Proxy-Connection: keep-alive',代理服务器在收到这个字段之后,才能对最终服务器的链接开启keep-alive,否则收到返回之后直接断开与原始服务器的链接。至于为什么不复用Connection而采用Proxy-Connection,参考这个链接。

流量加密

上面所说的代理服务器从原理上来说比较简单,配合一些脚本语言可能一两行就可以跑起来一个localhost的代理服务器,但是一旦代理服务器跨越了我国公网,则几乎100%的概率是连接超时。因为代理所发出来的http包是很容易识别的,所以基于这种方式通过vps搭建简单代理来走向世界是不行的,为此我们需要对http请求的tcp流量进行加密,即模拟一个轻量版(漏洞百出版)的https

加密又分为两种:对称加密和非对称加密。对称加密里的加密和解密key是一样的,而非对称加密则分为公钥和私钥之分,两个密钥的内容不同,公钥公开给其他使用当前加密服务的人员使用,私钥则自己存储,一份数据经过公钥加密后可以通过私钥解密,同样的经过私钥加密之后可以通过公钥解密。这种公钥分享系统也叫做PKI。

由于非对称加密的复杂度一般远远大于对称加密的复杂度, 所以实际使用时一般是首先通过公钥加密系统来握手,同时商定对称加密的key, 之后的处理都走对称加密流程。这样既确保了对称密钥的私密性,又加大了数据处理的速度。

这里我们使用openssl来进行流量的加密传输,非对称加密系统采取RSA, 对称加密系统则采用AES,同时对称加密的分组模式提供ECB、CBC、CFB、OFB四种选项,具体选项的含义就不要去追究了,直接调用相关api就好。

工作流程

由于我们现在加入了加密这个流程,简单的一个代理服务器已经无法满足我们当前的需求,所以当前程序设计成了两个部分:本地代理客户端和远程代理服务端。本地代理客户端充当原来的简单代理服务端,但是他接收到浏览器请求之后并不直接代理访问,而是转发到远程代理服务端,中继代理请求。其实本地的代理就是一个中继器,负责请求转发和流量加密。

现在的整个流程可以分为四个部分:

  1. 代理客户端与代理服务端握手
  2. 代理客户端中继浏览器请求到代理服务端
  3. 代理服务端收到请求之后执行http访问,将结果返回代理客户端
  4. 代理客户端将代理服务端返回结果通知到浏览器

代理客户端与代理服务端握手

这里的握手过程就是协商密钥的过程, 流程可以分为如下几步:

  1. 代理客户端 读取服务端RSA公钥,选择一个合适的对称加密算法和生成对应的加密密钥,通过RSA公钥加密之后,发送到加密服务端,此时客户端记录的链接状态是negotiating,
  2. 服务端验证数据有效性、密钥合法性、账户合法性,如果验证结果错误,直接断开连接。否则当前客户端准备同时接收浏览器和代理服务端的数据, 代理服务端开始等待代理客户端转发过来的请求。

代理客户端与代理服务端数据交换

在握手成功之后,代理客户端线程处于双工的状态,同时发送和接受服务端和浏览器的数据。由于http pipeline基本不怎么受支持,所以其实也不算双工。这里使用了四个函数接口:

  1. async_read_from_ua从浏览器这里读取数据,每次读取到数据之后都直接进行加密,然后调用async_wirte_to_server将这些数据发送出去
  2. async_write_to_proxy_server则不停的读取当前的发送缓冲,并发送到server端, 当所有都发送完成之后,调用async_read_from_ua等待浏览器数据
  3. async_read_from_proxy_server 不断的接受proxy_server断端发送过来的数据并解密,然后立即调用async_write_to_ua发送回浏览器
  4. async_write_to_ua 则是不断的调用自身将发送缓冲清空,然后调用async_read_from_proxy_server 继续等待proxy_server的数据

这里的代码实现时将这四个操作都用一个strand串联起来了,主要是因为openssl内的decryptencrypt函数对于多线程的支持不好,官方网站也说对于openssl的多线程不要抱太大希望。所以对于一个线程内的所有操作都用同一个strand。同时asio那边对于connection的处理也是推荐使用strand去管理,否则会出现各种稀奇古怪的bug。

握手成功后,代理服务端线程则只能处于单工状态,接受代理客户端请求,解析之后向原始服务器请求,原始服务器请求回来之后,解析返回的数据,然后发送回代理客户端。这里的逻辑就比客户端的纯连接转发功能复杂多了:

  1. async_read_from_proxy_client 从client读取数据,
  2. on_proxy_client_data_read 解密从代理客户端获取的数据:
  3. 如果当前状态是tunnel(就是connect 请求成功之后的状态) 则直接调用async_write_to_origin_server发送数据;
  4. 否则解析请求头,获取目标服务器的地址:
    1. 如果当前已经在与目标服务器的keep-alive状态,则调用on_origin_server_connected处理后续数据的发送,
    2. 否则调用async_connect_to_origin_server去连接目标服务器;
  5. async_connect_to_origin_server,遍历所有目标服务器的endpoint去链接,成功之后调用on_origin_server_connected处理之前的数据;
  6. on_origin_server_connected
  7. 如果之前的请求方法是connect,则向代理客户端发送链接成功的响应,同时将当前链接的状态设置为tunnel,
  8. 否则,处理连接之前收取的数据,当获取到一个完整的请求头或者任意数量的内容之后,调用async_wtite_to_origin_server
  9. async_write_to_origin_server 不断的调用底层的发送接口去发送数据,当发送完成之后,调用on_origin_server_data_send
  10. on_origin_server_data_send
  11. 如果当前在tunnel状态,则直接调用 async_read_from_proxy_client 等待数据
  12. 如果当前刚好发送完一个完整的packet,则调用async_read_from_origin_server等待目标服务器的应答
  13. 当前正在读取packet之中,继续调用async_read_from_proxy_client等待数据
  14. async_read_from_origin_server 从目标服务器等待响应数据
  15. on_origin_server_data_read 处理目标服务器的数据返回
  16. 如果当前状态是tunnel,则加密数据之后直接调用async_write_to_proxy_client发送加密后数据
  17. 如果当前已读数据不足以构造packet头部,则继续调用async_read_from_origin_server等待数据
  18. 把已读数据加密,调用async_write_to_proxy_client发送加密后数据。
  19. async_write_to_proxy_client调用底层的发送接口把数据发送到客户端,发送完成之后调用on_proxy_client_data_send
  20. on_proxy_client_data_send
    1. 如果当前状态在tunnel,直接调用async_read_from_origin_server
    2. 如果当前发送的数据并不能组成一个packet,则调用async_read_from_origin_server等待剩余数据
    3. 如果完整发送了一个packet,则根据是否接受keep-alive选项来决定是否关闭链接,如果不关闭,则调用async_read_from_proxy_client等待下一个packet请求。

更多改进

实现这些接口之后,一个极简且可用的加密代理服务搭建完成。使用者可以将本机启动client,远程vps启动server,同时将浏览器设置为以client作为代理服务器,就可以解决部分网络不好的问题了。但是,当前服务也只是能用而已,目前还有很多问题,有很多改进的点:

  1. 每次浏览器发起一个请求的时候,client都会新建一个connection去连接服务器,以处理这个请求。虽然我们目前支持了keep-alive,但是正常使用过程中,多开几个页面,同时连接数经常在30多个以上。可以考虑client和server维持一个连接池,每个请求的session绑定到其中一个连接上,这样就进一步减少了ping-pong延迟。最佳情况是可以达到ftp的链接速率。
  2. 国内网络环境还是比较恶劣的,在外租用的vps经常时不时网络抽风,可以尝试把多个server组成一个集群,动态调整各个server的连接优先级,甚至可以做一个serverlist的服务发现
  3. client和server之间的连接流量模式还是过于明显,可以考虑在连接池的基础上,client-server之间加入稳定的随机流量,同时把每个packet做一下padding到1024的整数倍。

相关链接

  1. 本文最早参考的项目azure http proxy 一个基于boost.asio和c++11的加密代理
  2. 我自己对于此项目的修改版azure_http_proxy 基于asio和c++17
  3. 关于http代理的更详细的说明https://imququ.com/post/web-proxy.html
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值