使用 Envoy 实现 HTTP/2 CONNECT 隧道:原理与实践

深入讲解如何利用 HTTP/2 CONNECT 方法创建隧道,涵盖技术原理、实践流程与示例代码。

版权声明
本文为 Jimmy Song 原创。转载请注明来源: https://jimmysong.io/blog/http2-envoy-tunnel-demo/
查看本文大纲

在最近对 Istio Ambient 模式的研究中,我发现 HTTP2 Connect 方法被用作创建隧道的核心技术,以实现透明流量的拦截和转发。HTTP/2 CONNECT 隧道是一种强大的工具,可以在已有的 HTTP/2 连接中创建高效的隧道,用于传输原始的 TCP 数据。这篇文章通过一个简单的 Demo,展示了如何使用 Envoy 来实现 HTTP/2 CONNECT 隧道的基本功能。

什么是 HTTP2 Connect 方法以及 HBONE 隧道?

HTTP2 Connect 方法是一种标准化的方式来创建隧道,用于透明地传输数据。特别是在 Istio 的 Ambient 模式中,它为代理数据平面之间的通信提供了一种高效的手段。HBONE(HTTP-Based Overlay Network Environment)隧道则是基于这种 HTTP2 Connect 技术的实现,用于 Istio 中的透明流量拦截和转发。通过使用 HBONE,数据可以有效地通过 HTTP2 隧道安全传输,替代了传统的 Sidecar 模式。这一创新设计极大地简化了服务网格的管理和部署。

HBONE 是 Istio 特有的术语,它是一种安全隧道协议,用于在 Istio 组件之间进行通信。在当前的 Istio 实现中,HBONE 协议包含了三个开放标准:

  • HTTP/2
  • HTTP CONNECT
  • Mutual TLS (mTLS)

HTTP CONNECT 用于建立隧道连接,mTLS 用于安全地加密连接,而 HTTP/2 用于在单一安全隧道中多路复用应用连接流并传输附加的流级元数据。更多关于 HBONE 隧道的细节可以参考官方文档:HBONE 详细介绍

使用 HTTP2 Connect 建立隧道的基本原理

HTTP2 Connect 方法允许我们创建一个类似于 VPN 的隧道,通过这个隧道可以安全地传递数据。建立隧道的基本步骤如下:

  1. 首先,客户端向代理发送一个普通的 TCP 或 HTTP 链接请求。
  2. 代理接收到请求后,代表客户端向目标服务器发送一个带有 CONNECT 方法的 HTTP2 请求。
  3. 如果服务器允许建立隧道,那么它会返回一个 HTTP2 200 OK 的响应给代理。
  4. 随后,客户端、代理和服务器之间的双向流数据就可以通过这个隧道进行传输。

这种方法能够使得数据的传输过程更加透明且安全,特别适用于需要高效通信和端到端加密的场景。

下图展示了 HTTP2 Connect 方法建立隧道的基本过程。

image
HTTP2 Connect 方法建立隧道的基本过程

Demo:使用 Envoy 与上游 Server 建立 HTTP/2 Connect 隧道

本示例展示了一个基础场景:

  1. 客户端:向 Envoy 代理发送文本消息。
  2. Envoy:接收客户端的 TCP 数据,将其封装为 HTTP/2 CONNECT 请求,并与上游服务器建立加密隧道。
  3. 服务器:接收来自 Envoy 的 HTTP/2 CONNECT 流量,解封装并返回响应给客户端。

架构图如下:

image
架构图

我们将使用 Node.js 来编写客户端和服务端,并将服务端和 Envoy 代理运行在容器中,在本地通过客户端访问 Envoy 代理从而达到访问客户端的目的。

完整的目录结构如下:

envoy-http2-tunnel/
├── certs/
│   ├── openssl.cnf
│   ├── server.crt
│   ├── server.key
├── client/
│   └── client.js
├── docker-compose.yml
├── envoy.yaml
└── server/
    ├── Dockerfile
    └── server.js

核心功能展示

1. HTTP/2 CONNECT 隧道的基本实现

  • 客户端通过普通的 TCP 连接与 Envoy 通信。
  • Envoy 将 TCP 数据封装为 HTTP/2 CONNECT 请求,发送到上游服务器。
  • 服务器接收并解封装隧道中的数据,进行处理后返回响应。
  • 隧道通信对客户端完全透明。

2. Envoy 的透明代理能力

  • Envoy 作为中间代理,将客户端与服务器之间的通信逻辑完全封装。
  • 客户端无需支持复杂的协议(如 HTTP/2 或 TLS),Envoy 代理完成所有协议转换。

3. 加密通信的实现

  • Envoy 与服务器之间的通信通过 TLS 加密,确保隧道内的数据安全。
  • 服务器终止 TLS,处理解密后的数据。

4. 隧道的简化使用场景

  • 通过该 Demo,可以快速理解 HTTP/2 CONNECT 隧道的建立和基本数据传输流程。

环境准备

1. 安装 Node.js

确保您的系统已安装 Node.js(版本 >= 10.10.0),因为 http2 模块在该版本后稳定。

2. 安装 Docker 和 Docker Compose

3. 创建项目目录

在您的工作空间中创建一个新目录并进入:

mkdir envoy-http2-tunnel
cd envoy-http2-tunnel

生成自签名证书

由于 Envoy 和服务器之间需要加密通信,我们需要生成包含正确配置的自签名证书。

1. 创建证书目录和 OpenSSL 配置文件

创建 certs 目录:

mkdir certs
cd certs

创建 openssl.cnf,内容如下:

[ req ]
default_bits       = 2048
default_md         = sha256
prompt             = no
distinguished_name = dn
req_extensions     = req_ext

[ dn ]
C            = US
ST           = California
L            = San Francisco
O            = My Company
OU           = My Division
CN           = server

[ req_ext ]
subjectAltName = @alt_names

[ alt_names ]
DNS.1   = server
DNS.2   = localhost

2. 生成密钥和证书

运行以下命令生成密钥和证书:

openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
  -keyout server.key -out server.crt -config openssl.cnf

这将在 certs 目录中生成 server.keyserver.crt 文件。

配置 Envoy 代理

我们需要配置 Envoy,使其能够接受客户端的普通 TCP 连接,将数据通过 HTTP/2 CONNECT 隧道传递给服务器。

1. 创建 Envoy 配置文件

在项目根目录创建 envoy.yaml,内容如下:

static_resources:
  listeners:
  - name: listener_0
    address:
      socket_address:
        protocol: TCP
        address: 0.0.0.0
        port_value: 10000
    filter_chains:
    - filters:
      - name: envoy.filters.network.tcp_proxy
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy
          stat_prefix: tcp_stats
          cluster: tunnel_cluster
          tunneling_config:
            hostname: server:8080
          access_log:
          - name: envoy.access_loggers.stdout
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
              log_format:
                json_format:
                  start_time: "%START_TIME%"
                  method: "%REQ(:METHOD)%"
                  path: "%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%"
                  protocol: "%PROTOCOL%"
                  response_code: "%RESPONSE_CODE%"
                  response_flags: "%RESPONSE_FLAGS%"
                  bytes_received: "%BYTES_RECEIVED%"
                  bytes_sent: "%BYTES_SENT%"
                  duration: "%DURATION%"
                  upstream_service_time: "%RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)%"
                  x_forwarded_for: "%REQ(X-FORWARDED-FOR)%"
                  user_agent: "%REQ(USER-AGENT)%"
                  request_id: "%REQ(X-REQUEST-ID)%"
                  upstream_host: "%UPSTREAM_HOST%"
                  upstream_cluster: "%UPSTREAM_CLUSTER%"
                  downstream_local_address: "%DOWNSTREAM_LOCAL_ADDRESS%"
                  downstream_remote_address: "%DOWNSTREAM_REMOTE_ADDRESS%"
  clusters:
  - name: tunnel_cluster
    connect_timeout: 5s
    type: LOGICAL_DNS
    lb_policy: ROUND_ROBIN
    transport_socket:
      name: envoy.transport_sockets.tls
      typed_config:
        "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
        sni: server
        common_tls_context:
          validation_context:
            trusted_ca:
              filename: "/certs/server.crt"
          alpn_protocols: [ "h2" ]
    http2_protocol_options: {}
    load_assignment:
      cluster_name: tunnel_cluster
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: server
                port_value: 8080

2. 关键点

  • Envoy 监听 TCP 连接,将流量通过 HTTP/2 CONNECT 隧道转发到服务器。
  • 客户端只需与 Envoy 建立 TCP 连接,发送文本消息。
  • Envoy负责隧道的建立和加密,客户端无需感知。

实现服务器

1. 创建服务器目录和文件

在项目根目录创建 server 目录:

mkdir server

server 目录中创建 server.jsDockerfile

2. 编写 server.js

server/server.js 中添加以下代码:

const http2 = require('http2');
const fs = require('fs');

const server = http2.createSecureServer({
  key: fs.readFileSync('/certs/server.key'),
  cert: fs.readFileSync('/certs/server.crt'),
});

server.on('stream', (stream, headers) => {
  const method = headers[':method'];
  const path = headers[':path'];

  if (method === 'CONNECT') {
    console.log(`Received CONNECT request for ${path}`);

    // 响应 200,建立隧道
    stream.respond({
      ':status': 200,
    });

    // 在隧道内处理数据
    stream.on('data', (chunk) => {
      const message = chunk.toString();
      console.log(`Received from client: ${message}`);

      // 回应客户端
      const response = `Echo from server: ${message}`;
      stream.write(response);
    });

    stream.on('end', () => {
      console.log('Stream ended by client.');
      stream.end();
    });
  } else {
    // 对于非 CONNECT 请求,返回 404
    stream.respond({
      ':status': 404,
    });
    stream.end();
  }

注意:

  • 监听 secureConnection 事件,直接处理 TLS 连接后的 socket。
  • 在 socket 上接收数据,处理来自客户端的文本消息,并回复。

3. 创建 Dockerfile

server/Dockerfile 中添加以下内容:

FROM node:14

WORKDIR /app

COPY server.js .

COPY certs /certs

EXPOSE 8080

CMD ["node", "server.js"]

实现客户端

1. 创建客户端目录和文件

在项目根目录创建 client 目录:

mkdir client

client 目录中创建 client.js

2. 编写 client.js

client/client.js 中添加以下代码:

const net = require('net');

// 创建与 Envoy 的 TCP 连接
const client = net.createConnection({ port: 10000 }, () => {
  console.log('Connected to Envoy.');

  // 发送消息给服务器
  let counter = 1;
  const interval = setInterval(() => {
    const message = `Message ${counter} from client!`;
    client.write(message);
    counter += 1;
  }, 1000);

  // 关闭连接
  setTimeout(() => {
    clearInterval(interval);
    client.end();
  }, 5000);
});

client.on('data', (data) => {
  console.log(`Received from server: ${data.toString()}`);
});

client.on('end', () => {
  console.log('Disconnected from server.');
});

client.on('error', (err) => {
  console.error('Client error:', err);
});

说明:

  • 客户端Envoy 建立普通的 TCP 连接,发送文本消息。
  • 该客户端的存在只是为了触发 Envoy 与服务器建立隧道。

创建 Docker Compose 文件

在项目根目录创建 docker-compose.yml

version: '3.8'

services:
  envoy:
    image: envoyproxy/envoy:v1.32.1
    volumes:
      - ./envoy.yaml:/etc/envoy/envoy.yaml
      - ./certs:/certs  # 挂载证书目录
    ports:
      - "10000:10000"
    networks:
      - envoy_network
    depends_on:
      - server
    command: /usr/local/bin/envoy -c /etc/envoy/envoy.yaml --service-cluster envoy --log-level debug

  server:
    build:
      context: ./server
    networks:
      - envoy_network
    expose:
      - "8080"
    volumes:
      - ./certs:/certs  # 挂载证书目录

networks:
  envoy_network:

运行示例

1. 启动 Docker Compose

在项目根目录下,运行:

docker-compose up --build

预期输出:

  • Envoy 容器: 显示启动信息和调试日志。
  • 服务器容器: 显示 Secure HTTP/2 server is listening on port 8080

2. 运行客户端

打开新的终端窗口,进入 client 目录:

cd client

运行客户端:

node client.js

预期输出:

Connected to Envoy.
Received from server: Echo from server: Message 1 from client!
Received from server: Echo from server: Message 2 from client!
Received from server: Echo from server: Message 3 from client!
Received from server: Echo from server: Message 4 from client!
Received from server: Echo from server: Message 5 from client!
Disconnected from server.

3. 检查服务器日志

在 Docker Compose 的输出中,您应该能看到服务器的日志:

envoy_1   | {"downstream_remote_address":"192.168.65.1:46306","path":null,"request_id":null,"bytes_sent":160,"protocol":null,"upstream_service_time":null,"bytes_received":88,"response_code":0,"user_agent":null,"downstream_local_address":"172.21.0.3:10000","upstream_host":"172.21.0.2:8080","start_time":"2024-12-03T11:37:59.542Z","upstream_cluster":"tunnel_cluster","duration":5012,"response_flags":"-","method":null,"x_forwarded_for":null}
server_1  | Secure HTTP/2 server is listening on port 8080
server_1  | New secure connection established.
server_1  | Received from client: Message 1 from client!
server_1  | Received from client: Message 2 from client!
server_1  | Received from client: Message 3 from client!
server_1  | Received from client: Message 4 from client!
server_1  | Received from client: Message 5 from client!
server_1  | Connection ended by client.

4. 检查 Envoy 日志

在 Envoy 的日志中,您可以看到它使用 HTTP/2 CONNECT 隧道与服务器建立连接的记录。

测试通信

  • 客户端通过 TCP 连接向 Envoy 代理发送文本消息。
  • Envoy将客户端的 TCP 流量通过 HTTP/2 CONNECT 隧道转发给 服务器
  • 服务器接收到来自客户端的消息,处理并回复。
  • Envoy将服务器的回复通过隧道传回给 客户端
  • 客户端收到服务器的回复。

注意事项

  • 证书管理: 确保证书正确配置,并在 Envoy 和服务器中正确使用。
  • Docker 网络: 使用 Docker Compose 定义的网络,容器可以通过服务名称互相通信。
  • 端口冲突: 确保端口 10000(Envoy)和 8080(服务器)未被占用。
  • TLS 配置: 在示例中,Envoy 与服务器之间的通信使用 TLS 和 HTTP/2,确保了安全性。

隧道建立过程

下图展示了客户端、Envoy 代理和服务器之间的交互,反映了数据的传递和隧道连接的建立的流程。

image
隧道建立流程

说明:

  1. 客户端与 Envoy 建立 TCP 连接

    • 客户端向 Envoy 发起 TCP 连接请求。
    • Envoy 接受连接,建立新的 TCP 代理会话(ConnectionId: 0)。
  2. Envoy 创建到服务器的连接

    • Envoy 尝试连接上游集群 tunnel_cluster,创建新的连接(ConnectionId: 1)。
  3. 建立 HTTP/2 CONNECT 隧道

    • Envoy 与服务器建立 HTTP/2 连接。
    • Envoy 发送 HTTP/2 CONNECT 请求,目标主机名为 server:8080
    • 服务器响应 200 OK,隧道建立成功。
  4. 数据传输

    • 消息传递循环
      • 客户端发送数据(Message N)到 Envoy。
      • Envoy 将数据通过隧道转发给服务器。
      • 服务器处理数据并返回响应(Echo Message N)给 Envoy。
      • Envoy 将响应转发给客户端。
    • 日志记录
      • 服务器记录收到的消息,例如 Received from client: Message N from client!
  5. 连接关闭

    • 客户端发送 FIN 请求,通知关闭连接。
    • Envoy 将 FIN 转发给服务器,关闭隧道。
    • 服务器响应 ACK 确认关闭。
    • Envoy 向客户端发送 ACK,完成连接关闭。
  6. 日志记录

    • Envoy 记录连接关闭日志,包括 ConnectionId 和统计信息。
    • 服务器记录日志,显示流已由客户端结束,例如 Stream ended by client.

结语

虽然这是一个入门示例,但它为理解和进一步探索 HTTP/2 CONNECT 隧道功能提供了坚实的基础。在下一篇博客中讲解通过两个 Envoy 代理实现的隧道,带您进一步了解 Istio ambient 模式中的 HBONE 透明隧道。

参考

最后更新于 2025/01/17