跳转至

HTTP隧道

介绍

HTTP隧道指的是,“利用HTTP的CONNECT方法在两台网络受限的计算机间建立网络链接,通常一方是在受限网络的内部,一方在外部,借外部方来代理内部方的流量”。其中,网络受限包括“防火墙”、“NAT”和“访问控制”等。该隧道由中间的“代理服务器”创建,通常部署于“DMZ”区域。

在隧道中可以传输一些被限制的协议,最终借由“代理服务器”跳出受限网络。


HTTP CONNECT方法

HTTP隧道中最常用的方法是CONNECT,过程如下:

  客户端                            代理 (proxy_connect)                    服务器
    |                                     |                                  |
(1) |-- CONNECT server.example.com:443 -->|                                  |
    |                                     |                                  |
    |                                     |----[ TCP connection ]----------->|
    |                                     |                                  |
(2) |<- HTTP/1.1 200                   ---|                                  |
    |   Connection Established            |                                  |
    |                                     |                                  |
    |                                                                        |
    ================== CONNECT tunnel has been establesied. ==================
    |                                                                        |
    |                                     |                                  |
    |                                     |                                  |
    |   [ SSL stream       ]              |                                  |
(3) |---[ GET / HTTP/1.1   ]------------->|   [ SSL stream       ]           |
    |   [ Host: server.example.com ]      |---[ GET / HTTP/1.1   ]---------->|
    |                                     |   [ Host: server.example.com ]   |
    |                                     |                                  |
    |                                     |                                  |
    |                                     |                                  |
    |                                     |   [ SSL stream       ]           |
    |   [ SSL stream       ]              |<--[ HTTP/1.1 200 OK  ]-----------|
(4) |<--[ HTTP/1.1 200 OK  ]--------------|   [ < html page >    ]           |
    |   [ < html page >    ]              |                                  |
    |                                     |                                  |
  1. “客户端”告诉代理自己想要连接的目标“服务器”,“代理”跟“服务器”建立TCP连接
  2. “代理”返回HTTP/1.1 200告诉“客户端”隧道已经建立
  3. “客户端”发送TCP之上(包括SSL)的数据给“代理”,再经由“代理”转发给“服务器”。
  4. “服务器“响应数据给”代理“,再经由”代理“转发给”客户端“

从上面流程可以看出,仅在初始化连接的时候是HTTP,即“1”和“2”两个步骤,后续“代理”都只是简单的转发“客户端”和“服务器”的数据而已。

“代理“也可以去支持一些限制功能,比如仅代理某些特定端口和某些主机等。


协商示例

“客户端”发送连接请求,告知“代理”想要连接的地址和端口,这里是developer.mozilla.org:443

CONNECT developer.mozilla.org:443 HTTP/1.1
Host: developer.mozilla.org:443
Proxy-Connection: keep-alive
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36

如果连接请求被“代理”允许且成功与服务器建立TCP连接,则响应2XX状态码告诉“客户端”隧道建立成功:

HTTP/1.1 200 Connection Established
Proxy-agent: nginx

接着“客户端”可以发送任何数据,所有数据都将经由“代理”发往“服务器”,同时“服务器”的响应数据也都将转发给“客户端”:

# 以下来自客户端的数据
GET / HTTP/1.1
...

# 以下来自服务器的数据
HTTP/1.1 200 OK
...

非CONNECT方法建立HTTP隧道

建立HTTP隧道可以是任何方法,它只是一种思想,而CONNECT是最为常见的方式而已,因为它的命名就是显而易见的。

建立HTTP隧道的场景中,“客户端”部署在保护(受限)网络的内部,而“代理”则部署在外部。在任何时刻,“客户端”都可以将要发送的数据封装成HTTP请求发往“代理”,“代理”再将其还原成原始的“客户端”请求并中继到目标“服务器”;反之,“服务器”响应请求的数据则通过“代理”封装成HTTP响应并中继回“客户端”,最终“客户端”和“服务器”这些被封装在HTTP协议之内的数据将穿过防火墙等设备。


代理认证

这里的“代理认证“指的是”代理“对”客户端“进行身份认证,认证通过后才允许建立HTTP隧道。

“代理认证”的交互过程有两种形式,一种是“客户端”主动在CONNECT请求中携带身份信息给“代理”;另一种是“客户端”在收到“代理”响应407后,再重新发送一个携带身份信息的CONNECT请求给“代理”。

主动携带

  客户端                            代理 (proxy_connect)
    |                                     |
(1) |-- CONNECT server.example.com:443 -->|
    |   Proxy-Authorization: credentials  |
    |                                     |
    |                                     |
    |                                     |
(2) |<- HTTP/1.1 200                   ---|
    |   Connection Established            |
    |                                     |
  1. “客户端”发送携带用户凭据的CONNECT请求给“代理”。
    • credentials:是用户凭据,其格式为<type> <token>token的值取决于type。比如:“Proxy-Authorization: Basic dXNlcjE6MTIzNDU2Cg==”。
    • type:常见的就是Basic(其余可参阅引用[5])。
  2. “代理“在身份验证通过并且与服务器建立TCP连接后返回200给“客户端”。

被动携带

  客户端                                    代理 (proxy_connect)
    |                                               |
(1) |-- CONNECT server.example.com:443           -->|
    |                                               |
    |                                               |
    |                                               |
(2) |<- HTTP/1.1 407 Proxy Authenticate Required ---|
    |   Proxy-Authenticate: 1#challenge             |
    |                                               |
    |                                               |
(3) |-- CONNECT server.example.com:443           -->|
    |   Proxy-Authorization: credentials            |
    |                                               |
    |                                               |
(4) |<- HTTP1.1 200 Connection Established        --|
  1. “客户端”先发送CONNECT请求给代理,但并没有携带用户凭据。
  2. “代理“返回407通知“客户端”需要进行用户身份认证,这里1#challenge指的是可以返回至少1个Proxy-Authenticate头部,而challenge的格式为<type> realm=<realm>,比如“Proxy-Authenticate: Basic realm=dev”:
    • type通常为Basic,(其余可参阅引用[5])
    • realm是领域的意思,与URI结合可以实现不同的用户访问不同的URI资源,比如用户的请求可能会被改写为:https://host:port/$realm/$uri
  3. “客户端”在收到407后,重新发起携带用户凭据的CONNECT请求给“代理”。
  4. “代理”在身份验证通过并且与服务器建立TCP连接后返回200给“客户端”。

实施

基于Tengine二次的开发项目,支持CONNECT代理及“Basic代理认证”:

nginx.conf配置如下:

worker_processes 2;

user root;

error_log logs/info.log info;

events {
    use epoll;
    worker_connections 1024;
}

http {
    #
    # HTTP代理服务器(基于CONNECT方法的HTTP隧道)
    #
    server {
        listen 10080;                             # 代理端口为10080
        server_name your.domain.com;

        # 认证配置
        auth_basic web_proxy;                     # 领域
        auth_basic_user_file user_list.htpasswd;  # 用户信息
        proxy_set_header Authorization "";        # 去掉认证信息

        # 代理 CONNECT 请求
        proxy_connect;
        proxy_connect_auth              on;       # 进行代理认证
        proxy_connect_allow             443 80;   # 允许代理443和80端口
        proxy_connect_connect_timeout   120s;
        proxy_connect_read_timeout      120s;
        proxy_connect_send_timeout      120s;

        proxy_set_header Host           $host;

        # 转发所有非HTTP CONNECT的请求
        location / {
            auth_basic off;
            proxy_set_header Host $host;
            proxy_pass http://$host;
        }
    }
}

淘宝基于Nginx二次开发的项目,支持CONNECT代理,但不支持“代理认证”。参阅官方手册proxy_connect

但是可以通过Lua脚本,附上淘宝给的Lua脚本实现示例:https://github.com/chobits/ngx_http_proxy_connect_module/issues/42#issuecomment-502985437


引用

  1. Wiki - HTTP Tunnel
  2. RFC - 7235 -Hypertext Transfer Protocol (HTTP/1.1): Authentication
  3. developer.mozilla.org - Proxy-Authorization》
  4. developer.mozilla.org - Proxy-Authenticate》
  5. Hypertext Transfer Protocol (HTTP) Authentication Scheme Registry

评论