隧道用 CONNECT 建一条只管搬字节的管子。建好之后,中介既看不懂也不该看懂里面是什么。
代理转发、网关翻译,两者都要理解自己搬的东西。 隧道是第三种态度:我什么都不知道,我只管把两头接起来。
一、隧道的目的就是让中介闭嘴
有些通信必须端到端保密,中间任何一环都不能看见内容。 可它又需要穿过一层层的代理和防火墙——那些设备只放行 HTTP。 隧道的办法是:先用 HTTP 打个招呼把管子建起来,然后在这根管子里跑别的协议。 对路上那些设备来说,它看起来就是一条已经建立的 HTTP 连接。 书里点了两个用途:SSL 隧道和SSH 隧道。 今天最常见的用法是前者——浏览器访问 HTTPS 时经过代理,走的就是隧道。
二、CONNECT 只做一件事
建立隧道的请求长这样:
CONNECT example.com:443 HTTP/1.1
Host: example.com:443
目标不是路径,是 主机:端口。
服务端如果同意,回一个 2XX(惯例写 200 Connection Established),这条连接从此变成字节管道,
里面再发什么都不再是 HTTP。
这里有个容易忽略的点:CONNECT 之后,那条连接上的字节不再遵循 HTTP 的报文格式。
所以写代理程序时,CONNECT 必须特殊处理,不能套用「读请求—读响应」的循环。
Demo 里就是单独写了一个双向搬运的函数。
三、隧道里的东西,中介到底能看见什么
分两种情况。 一种是真加密(HTTPS):隧道里跑的是 TLS, 中介只能看到「连了哪个域名」(如果开了 SNI)和「传了多少字节」,路径、首部、正文全看不见。 一种是不加密(比如明文跑 SSH 之外的私有协议):隧道本身不提供任何保密, 只是把字节从一头搬到另一头。这时候中介如果愿意,完全可以拆开看。 这是本节最常见的误解:隧道不等于加密。 也正因为看不清,企业做审计时不能靠代理拆 HTTPS,只能靠 SNI、DNS 日志, 或者给员工装一张根证书做中间人解密。最后这种方案在技术上可行,在合规和信任上代价很大。
四、动手 Demo:把非 HTTP 的字节塞进隧道
目标服务只会把收到的字节原样弹回去,完全不认 HTTP。 先直接给它发一个 HTTP 请求,再走隧道发同样的字节,对比两次的结果。
# 隧道端点:读到空行,回 200,然后把这条连接变成双向管道
def serve_tunnel(sock, st):
buf = b''
while b'\r\n\r\n' not in buf:
buf += sock.recv(4096)
head, rest = buf.split(b'\r\n\r\n', 1)
first = head.split(b'\r\n')[0].decode(u'latin-1')
hostport = first.split(u' ')[1]
h_, _, p_ = hostport.rpartition(u':')
up = socket.create_connection((h_, int(p_)))
sock.sendall(b'HTTP/1.1 200 Connection Established\r\n\r\n')
if rest:
up.sendall(rest)
# 两个方向各开一个线程,无脑搬字节
threading.Thread(target=pump, args=(sock, up, 0)).start()
threading.Thread(target=pump, args=(up, sock, 1)).start()
实跑输出:
--------------------------------------------------------------------
隧道:CONNECT 之后,中介就再也看不懂里面是什么了
--------------------------------------------------------------------
目标服务 127.0.0.1:57338 只会回显,完全不认 HTTP
隧道端点 127.0.0.1:57339 负责接 CONNECT
先直接给目标发一个 HTTP 请求,看它什么反应:
发出去的 38 字节
收到的 38 字节(原样弹回来,没有状态行)
是 HTTP 响应吗 不是,第一行是「GET / HTTP/1.1」
再走隧道:先 CONNECT,再在管子里面发同样的字节
隧道端点回 HTTP/1.1 200 Connection Established
管子里发出去 24 字节
管子里收回来 24 字节
内容一致 是
隧道上行累计 24 字节
隧道下行累计 24 字节
隧道端点偷看到的字节(它只能看到一串没有意义的字符):
文本 THIS IS NOT HTTP AT ALL\n
十六 54 48 49 53 20 49 53 20 4e 4f 54 20 48 54 54 50 20 41 54 20 41 4c 4c 0a
隧道只负责保持连接、双向搬字节,它不解析、不缓存、不改写。
HTTPS 走代理时就是先 CONNECT 建隧道,TLS 握手全在管子里完成,
所以代理看不见你请求了哪个路径,只看得到你连了哪个域名。
直接发 HTTP 请求:发出去 38 字节,收回 38 字节,第一行是 GET / HTTP/1.1 原样弹回来。
目标连状态行都不给,因为它压根不是 HTTP 服务。
走隧道:CONNECT 之后收到 HTTP/1.1 200 Connection Established,
然后在管子里发 24 字节非 HTTP 的字节串,原样收回 24 字节,内容一致。
上行累计 24、下行累计 24,一字节不多一字节不少。
最值得看的是最后那段:隧道端点顺手记下了它转发的字节,
打出来是 THIS IS NOT HTTP AT ALL——一串它根本没有解析的字符。这就是隧道的全部职责:搬,不读。
30 秒实验
- 把隧道里发的字节换成一个假的 TLS ClientHello,看隧道端点的日志还是不是原样。
- 给
CONNECT一个不存在的端口,看create_connection抛什么,想想该回哪个状态码。 - 想一想:如果公司要求审计所有出网流量,而员工用隧道跑加密协议,你能看到什么?
常见误区
- 「隧道就是加密」——隧道只负责建管子搬字节,保密是 TLS 的功劳。
- 「
CONNECT之后还是 HTTP」——那句 200 之后,连接上跑的就不再是 HTTP 了。 - 「隧道可以随便开」——它是很常见的绕过内网策略的手段,所以出口侧通常会限制目标端口。
自测三问(点开对答案)
1. 建立隧道用哪个方法,目标写成什么形式? —— `CONNECT`,目标是 `主机:端口`,不是路径。 2. 服务端回 2XX 之后,连接变成了什么? —— 一条字节管道,里面跑的不再是 HTTP 报文,中介不解析。 3. HTTPS 走代理时,代理能看见什么、看不见什么? —— 能看见连了哪个域名(SNI)和流量大小;看不见路径、首部、正文。代理、网关、隧道都属于「转发」。接下来换个角度:服务器返回的东西怎么被存起来重复使用。
- ☐ (本节完,下一节 5.3.1 缓存的有效期限)