Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions reverse_tunnel/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
To learn about this sandbox and for instructions on how to run it please head over
to the [Envoy docs](https://www.envoyproxy.io/docs/envoy/latest/start/sandboxes/reverse_tunnel.html).
46 changes: 46 additions & 0 deletions reverse_tunnel/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
services:

initiator-envoy:
build:
context: .
dockerfile: ../shared/envoy/Dockerfile
args:
ENVOY_CONFIG: initiator-envoy.yaml
command: ["envoy", "-c", "/etc/envoy.yaml", "--concurrency", "1", "-l", "trace", "--drain-time-s", "3"]
ports:
# Admin interface
- "${PORT_ADMIN_DOWNSTREAM:-8888}:8888"
# Reverse connection API listener
- "${PORT_REVERSE_API_DOWNSTREAM:-9000}:9000"
extra_hosts:
- "host.docker.internal:host-gateway"
networks:
- envoy-network
depends_on:
- downstream-service

downstream-service:
image: nginxdemos/hello:plain-text
networks:
- envoy-network

responder-envoy:
build:
context: .
dockerfile: ../shared/envoy/Dockerfile
args:
ENVOY_CONFIG: responder-envoy.yaml
command: ["envoy", "-c", "/etc/envoy.yaml", "--concurrency", "1", "-l", "trace", "--drain-time-s", "3"]
ports:
# Admin interface
- "${PORT_ADMIN_UPSTREAM:-8889}:8888"
# Reverse connection API listener
- "${PORT_REVERSE_API_UPSTREAM:-9001}:9000"
# Egress listener
- "${PORT_PROXY:-8085}:8085"
networks:
- envoy-network

networks:
envoy-network:
driver: bridge
177 changes: 177 additions & 0 deletions reverse_tunnel/example.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
.. _install_sandboxes_reverse_tunnel:

Reverse Tunnels
===============

.. sidebar:: Requirements

.. include:: _include/docker-env-setup-link.rst

:ref:`curl <start_sandboxes_setup_curl>`
Used to make HTTP requests.

This sandbox demonstrates Envoy's :ref:`reverse tunnels <config_reverse_connection>` feature, which allows establishing
long-lived connections from downstream to upstream in scenarios where direct connectivity from upstream to downstream
is not possible, and using these cached connection sockets to send data traffic from upstream to downstream Envoy.

In this example, a downstream Envoy proxy initiates reverse tunnels to an upstream Envoy using a custom address resolver. The configuration includes bootstrap extensions and reverse connection listeners with specialized address formats.

Step 1: Build the sandbox
*************************

Change to the ``reverse_tunnel`` directory.

To build this sandbox example, and start the example services run the following commands:

.. code-block:: console

$ pwd
examples/reverse_tunnel
$ docker compose pull
$ docker compose up --build -d
$ docker compose ps

Name Command State Ports
-------------------------------------------------------------------------------------------------------------------------------
reverse_tunnel_initiator-envoy_1 /docker-entrypoint.sh /usr ... Up 0.0.0.0:8888->8888/tcp, 0.0.0.0:9000->9000/tcp
reverse_tunnel_responder-envoy_1 /docker-entrypoint.sh /usr ... Up 0.0.0.0:8085->8085/tcp,
0.0.0.0:8889->8888/tcp,
0.0.0.0:9001->9000/tcp
reverse_tunnel_downstream-service_1 /docker-entrypoint.sh /usr ... Up 80/tcp

Step 2: Understanding the configuration
**************************************

The reverse tunnel configuration is explained in the :ref:`Reverse Tunnels <config_reverse_connection>` section.

Step 3: Validate reverse tunnel establishment
*********************************************

Verify that reverse tunnels have been successfully established by checking the stats counters on both downstream and upstream Envoy proxies.

Check downstream Envoy stats (port 8888):

.. code-block:: console

$ curl "http://localhost:8888/stats?hidden=include" | grep downstream_reverse_connection

Expected downstream stats showing connected reverse tunnels:

.. code-block:: console

downstream_reverse_connection.cluster.upstream-cluster.connected: 1
downstream_reverse_connection.cluster.upstream-cluster.connecting: 0
downstream_reverse_connection.host.172.27.0.2:9000.connected: 1
downstream_reverse_connection.host.172.27.0.2:9000.connecting: 0
downstream_reverse_connection.worker_0.cluster.upstream-cluster.connected: 1
downstream_reverse_connection.worker_0.cluster.upstream-cluster.connecting: 0
downstream_reverse_connection.worker_0.host.172.27.0.2:9000.connected: 1
downstream_reverse_connection.worker_0.host.172.27.0.2:9000.connecting: 0

Check upstream Envoy stats (port 8889):

.. code-block:: console

$ curl "http://localhost:8889/stats?hidden=include" | grep upstream_reverse_connection

Expected upstream stats showing received reverse connections:

.. code-block:: console

upstream_reverse_connection.clusters.downstream-cluster: 1
upstream_reverse_connection.nodes.downstream-node: 1
upstream_reverse_connection.worker_0.cluster.downstream-cluster: 1
upstream_reverse_connection.worker_0.node.downstream-node: 1

The stats confirm that:

- **Downstream Envoy**: Has successfully connected (``connected: 1``) to the upstream cluster
with no pending connections (``connecting: 0``)
- **Upstream Envoy**: Has received reverse connections from the downstream node and cluster,
as indicated by the reverse connection counters

Step 4: Test reverse tunnel
***************************

Perform HTTP requests for the service behind downstream Envoy, to upstream Envoy.
These requests will be sent over a reverse tunnel. You can route requests using either
cluster ID or node ID headers.

**Option 1: Route by cluster ID**

.. code-block:: console

$ curl -H "x-cluster-id: downstream-cluster" http://localhost:8085/downstream_service -v

Expected response:

.. code-block:: console

* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8085 (#0)
>GET /downstream_service HTTP/1.1
>Host: localhost:8085
>User-Agent: curl/7.61.1
>Accept: */*
>x-cluster-id: downstream-cluster
>
<HTTP/1.1 200 OK
<server: envoy
<date: Thu, 02 Oct 2025 23:47:18 GMT
<content-type: text/plain
<content-length: 159
<expires: Thu, 02 Oct 2025 23:47:17 GMT
<cache-control: no-cache
<x-envoy-upstream-service-time: 13
<
Server address: 172.31.0.3:80
Server name: 90dde65fa099
Date: 02/Oct/2025:23:47:18 +0000
URI: /downstream_service
Request ID: db89e5aa9fb485d5f1749f537240ad9d
* Connection #0 to host localhost left intact

**Option 2: Route by node ID**

.. code-block:: console

$ curl -H "x-node-id: downstream-node" http://localhost:8085/downstream_service -v

Expected response:

.. code-block:: console

* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8085 (#0)
>GET /downstream_service HTTP/1.1
>Host: localhost:8085
>User-Agent: curl/7.61.1
>Accept: */*
>x-node-id: downstream-node
>
<HTTP/1.1 200 OK
<server: envoy
<date: Thu, 02 Oct 2025 23:48:17 GMT
<content-type: text/plain
<content-length: 159
<expires: Thu, 02 Oct 2025 23:48:16 GMT
<cache-control: no-cache
<x-envoy-upstream-service-time: 4
<
Server address: 172.31.0.3:80
Server name: 90dde65fa099
Date: 02/Oct/2025:23:48:17 +0000
URI: /downstream_service
Request ID: 31657da3c832fb66dbc1990a8c18b828
* Connection #0 to host localhost left intact

Both routing methods demonstrate that the reverse tunnel is working correctly,
with requests being successfully routed from upstream Envoy to the downstream service
over the established reverse connection.

.. seealso::

:ref:`Reverse Tunnels architecture overview <config_reverse_connection>`
Learn more about Envoy's reverse tunnel functionality.
99 changes: 99 additions & 0 deletions reverse_tunnel/initiator-envoy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
---
node:
id: downstream-node
cluster: downstream-cluster

# Enable reverse connection bootstrap extension which registers the custom resolver
bootstrap_extensions:
- name: envoy.bootstrap.reverse_tunnel.downstream_socket_interface
typed_config:
"@type": >-
type.googleapis.com/envoy.extensions.bootstrap.reverse_tunnel.downstream_socket_interface.v3.DownstreamReverseConnectionSocketInterface
stat_prefix: "downstream_reverse_connection"
enable_detailed_stats: true

static_resources:
listeners:
# Initiates rev connere hierse connections to upstream using custom resolver
- name: reverse_conn_listener
listener_filters_timeout: 0s
listener_filters: []
# Use custom address with reverse connection metadata encoded in URL format
address:
socket_address:
# This encodes: src_node_id, src_cluster_id, src_tenant_id
# and remote clusters: upstream-cluster with 1 connection
address: "rc://downstream-node:downstream-cluster:downstream-tenant@upstream-cluster:1"
port_value: 0
# Use custom resolver that can parse reverse connection metadata
resolver_name: "envoy.resolvers.reverse_connection"
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": >-
type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: reverse_conn_listener
route_config:
virtual_hosts:
- name: backend
domains:
- "*"
routes:
- match:
prefix: '/downstream_service'
route:
cluster: downstream-service
http_filters:
- name: envoy.filters.http.router
typed_config:
"@type": >-
type.googleapis.com/envoy.extensions.filters.http.router.v3.Router

# Cluster designating upstream-envoy
clusters:
- name: upstream-cluster
type: STRICT_DNS
connect_timeout: 30s
load_assignment:
cluster_name: upstream-cluster
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: responder-envoy # Address of responder-envoy service
port_value: 9000 # Port for rev_conn_api_listener

# Backend HTTP service behind downstream which
# we will access via reverse connections
- name: downstream-service
type: STRICT_DNS
connect_timeout: 30s
load_assignment:
cluster_name: downstream-service
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: downstream-service
port_value: 80

admin:
access_log:
- name: envoy.access_loggers.file
typed_config:
"@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
path: "/dev/stdout"
address:
socket_address:
protocol: TCP
address: 0.0.0.0
port_value: 8888

layered_runtime:
layers:
- name: layer
static_layer:
re2.max_program_size.error_level: 1000
Loading