在最近对 Istio Ambient 模式的研究中,我发现 HTTP2 Connect 方法被用作创建隧道的核心技术,以实现透明流量的拦截和转发。HTTP/2 CONNECT 隧道是一种强大的工具,可以在已有的 HTTP/2 连接中创建高效的隧道,用于传输原始的 TCP 数据。这篇文章通过一个简单的 Demo,展示了如何使用 Envoy 来实现 HTTP/2 CONNECT 隧道的基本功能。
HTTP2 Connect 方法是一种标准化的方式来创建隧道,用于透明地传输数据。特别是在 Istio 的 Ambient 模式中,它为代理数据平面之间的通信提供了一种高效的手段。HBONE(HTTP-Based Overlay Network Environment)隧道则是基于这种 HTTP2 Connect 技术的实现,用于 Istio 中的透明流量拦截和转发。通过使用 HBONE,数据可以有效地通过 HTTP2 隧道安全传输,替代了传统的 Sidecar 模式。这一创新设计极大地简化了服务网格的管理和部署。
HBONE 是 Istio 特有的术语,它是一种安全隧道协议,用于在 Istio 组件之间进行通信。在当前的 Istio 实现中,HBONE 协议包含了三个开放标准:
HTTP CONNECT 用于建立隧道连接,mTLS 用于安全地加密连接,而 HTTP/2 用于在单一安全隧道中多路复用应用连接流并传输附加的流级元数据。更多关于 HBONE 隧道的细节可以参考官方文档:HBONE 详细介绍。
HTTP2 Connect 方法允许我们创建一个类似于 VPN 的隧道,通过这个隧道可以安全地传递数据。建立隧道的基本步骤如下:
这种方法能够使得数据的传输过程更加透明且安全,特别适用于需要高效通信和端到端加密的场景。
下图展示了 HTTP2 Connect 方法建立隧道的基本过程。
本示例展示了一个基础场景:
架构图如下:
我们将使用 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
确保您的系统已安装 Node.js(版本 >= 10.10.0),因为 http2
模块在该版本后稳定。
在您的工作空间中创建一个新目录并进入:
mkdir envoy-http2-tunnel
cd envoy-http2-tunnel
由于 Envoy 和服务器之间需要加密通信,我们需要生成包含正确配置的自签名证书。
创建 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
运行以下命令生成密钥和证书:
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout server.key -out server.crt -config openssl.cnf
这将在 certs
目录中生成 server.key
和 server.crt
文件。
我们需要配置 Envoy,使其能够接受客户端的普通 TCP 连接,将数据通过 HTTP/2 CONNECT 隧道传递给服务器。
在项目根目录创建 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
在项目根目录创建 server
目录:
mkdir server
在 server
目录中创建 server.js
和 Dockerfile
。
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。Dockerfile
在 server/Dockerfile
中添加以下内容:
FROM node:14
WORKDIR /app
COPY server.js .
COPY certs /certs
EXPOSE 8080
CMD ["node", "server.js"]
在项目根目录创建 client
目录:
mkdir client
在 client
目录中创建 client.js
。
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);
});
说明:
在项目根目录创建 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:
在项目根目录下,运行:
docker-compose up --build
预期输出:
Secure HTTP/2 server is listening on port 8080
。打开新的终端窗口,进入 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.
在 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.
在 Envoy 的日志中,您可以看到它使用 HTTP/2 CONNECT 隧道与服务器建立连接的记录。
10000
(Envoy)和 8080
(服务器)未被占用。下图展示了客户端、Envoy 代理和服务器之间的交互,反映了数据的传递和隧道连接的建立的流程。
说明:
客户端与 Envoy 建立 TCP 连接:
Envoy 创建到服务器的连接:
tunnel_cluster
,创建新的连接(ConnectionId: 1)。建立 HTTP/2 CONNECT 隧道:
server:8080
。200 OK
,隧道建立成功。数据传输:
Message N
)到 Envoy。Echo Message N
)给 Envoy。Received from client: Message N from client!
。连接关闭:
日志记录:
Stream ended by client.
。虽然这是一个入门示例,但它为理解和进一步探索 HTTP/2 CONNECT 隧道功能提供了坚实的基础。在下一篇博客中讲解通过两个 Envoy 代理实现的隧道,带您进一步了解 Istio ambient 模式中的 HBONE 透明隧道。
最后更新于 2025/01/17