As part of my recent exploration into Istio Ambient mode, I discovered that the HTTP/2 CONNECT method is a core technology for creating tunnels that enable transparent traffic interception and forwarding. HTTP/2 CONNECT tunnel is a powerful tool that can create an efficient tunnel in an existing HTTP/2 connection for transmitting raw TCP data. This article shows how to implement the basic functions of the HTTP/2 CONNECT tunnel using Envoy through a simple Demo.
The HTTP/2 CONNECT method provides a standardized approach for creating tunnels to transparently transmit data. In Istio Ambient mode, it offers an efficient way for proxy data planes to communicate. HBONE (HTTP-Based Overlay Network Environment) tunnel, built on HTTP/2 CONNECT, is Istio’s implementation for transparent traffic interception and forwarding. Using HBONE, data can securely traverse the tunnel via HTTP/2, offering a sidecar-less alternative. This innovative design simplifies the management and deployment of service meshes.
HBONE, specific to Istio, is a secure tunneling protocol for communication between Istio components. Its current implementation incorporates three open standards:
HTTP CONNECT establishes the tunnel, mTLS encrypts the connection, and HTTP/2 multiplexes streams within the secure tunnel while transmitting stream-level metadata. For more details, see the HBONE documentation.
The HTTP/2 CONNECT method creates a VPN-like tunnel for secure data transmission. The basic steps are as follows:
This method ensures secure and transparent data transmission, particularly suited for scenarios requiring efficient communication and end-to-end encryption.
Below is a diagram showing the basic process of creating a tunnel using the HTTP/2 CONNECT method:
In this demo:
Below is the architecture for this demo. To simplify the setup, we will configure TLS only for the server:
The complete directory structure of this example is as follows:
envoy-http2-tunnel/
├── certs/
│ ├── openssl.cnf
│ ├── server.crt
│ ├── server.key
├── client/
│ └── client.js
├── docker-compose.yml
├── envoy.yaml
└── server/
├── Dockerfile
└── server.js
By following this guide, you can establish efficient, transparent, and secure traffic tunnels using HTTP/2 CONNECT, a cornerstone of Istio’s Ambient mode and HBONE technology.
Ensure that Node.js (version >= 10.10.0) is installed on your system, as the http2
module became stable starting with this version.
Create a new directory in your workspace and navigate to it:
mkdir envoy-http2-tunnel
cd envoy-http2-tunnel
To enable encrypted communication between Envoy and the server, we need to generate a self-signed certificate with the correct configuration.
Create the certs
directory:
mkdir certs
cd certs
Create an openssl.cnf
file with the following content:
[ 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
Run the following command to generate the key and certificate:
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout server.key -out server.crt -config openssl.cnf
This will generate the server.key
and server.crt
files in the certs
directory.
We need to configure Envoy to accept plain TCP connections from the client and forward the data to the server through an HTTP/2 CONNECT tunnel.
In the project root directory, create a file named envoy.yaml
with the following content:
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
10000
and forwards traffic to the upstream server.server.crt
and server.key
.Next, proceed to set up the server and client components to test the tunnel functionality.
Create the server
directory in the project root:
mkdir server
Inside the server
directory, create server.js
and Dockerfile
.
server.js
Add the following code to 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}`);
// Respond with 200 to establish the tunnel
stream.respond({
':status': 200,
});
// Handle data within the tunnel
stream.on('data', (chunk) => {
const message = chunk.toString();
console.log(`Received from client: ${message}`);
// Respond to the client
const response = `Echo from server: ${message}`;
stream.write(response);
});
stream.on('end', () => {
console.log('Stream ended by client.');
stream.end();
});
} else {
// Return 404 for non-CONNECT requests
stream.respond({
':status': 404,
});
stream.end();
}
});
// Start the server and listen on port 8080
server.listen(8080, () => {
console.log('Secure HTTP/2 server is listening on port 8080');
});
Notes:
secureConnection
event to process TLS-secured sockets.Dockerfile
Add the following content to server/Dockerfile
:
FROM node:14
WORKDIR /app
COPY server.js .
EXPOSE 8080
CMD ["node", "server.js"]
Create the client
directory in the project root:
mkdir client
Inside the client
directory, create client.js
.
client.js
Add the following code to client/client.js
:
const net = require('net');
// Create a TCP connection to Envoy
const client = net.createConnection({ port: 10000 }, () => {
console.log('Connected to Envoy.');
// Send messages to the server
let counter = 1;
const interval = setInterval(() => {
const message = `Message ${counter} from client!`;
client.write(message);
counter += 1;
}, 1000);
// Close the connection
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);
});
Explanation:
Create docker-compose.yml
in the project root:
version: '3.8'
services:
envoy:
image: envoyproxy/envoy:v1.32.1
volumes:
- ./envoy.yaml:/etc/envoy/envoy.yaml
- ./certs:/certs # Mount the certificates directory
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 # Mount the certificates directory
networks:
envoy_network:
This configuration sets up both the Envoy proxy and the server to operate within the same Docker network, facilitating seamless communication.
In the project root directory, run:
docker-compose up --build
Expected Output:
Secure HTTP/2 server is listening on port 8080
.Open a new terminal, navigate to the client
directory:
cd client
Run the client:
node client.js
Expected Output:
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.
In the Docker Compose output, you should see logs from the server:
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.
In Envoy’s logs, you can see records of the connection established with the server using the HTTP/2 CONNECT tunnel.
10000
(Envoy) and 8080
(server) are not occupied.The following diagram illustrates the interaction between the client, Envoy proxy, and server, showing how data is transmitted and the tunnel connection is established:
Explanation:
Client connects to Envoy via TCP:
Envoy establishes a connection with the server:
tunnel_cluster
, creating a new connection (ConnectionId: 1).Establishing the HTTP/2 CONNECT tunnel:
server:8080
.200 OK
, successfully establishing the tunnel.Data transmission:
Message N
) to Envoy.Echo Message N
).Received from client: Message N from client!
.Closing the connection:
Logging:
Stream ended by client.
.Although this is an introductory example, it provides a solid foundation for understanding and further exploring the HTTP/2 CONNECT tunnel function. In the next blog, I will explain the tunnel implemented by two Envoy proxies and take you to further understand the HBONE transparent tunnel in the Istio ambient mode.
Last updated on Feb 13, 2025