Istio Ambient 模式 HBONE 隧道原理詳解 - 上

語言: CN / TW / HK

作者:趙化冰,原文地址:https://www.zhaohuabing.com/post/2022-09-11-ambient-hbone/

Istio ambient 模式採用了被稱為 HBONE [1] 的方式來連接 ztunnel 和 waypoint proxy。HBONE 是 HTTP-Based Overlay Network Environment 的縮寫。雖然該名稱是第一次看到,其實 HBONE 並不是 Istio 創建出來的一個新協議,而只是利用了 HTTP 協議標準提供的隧道能力。簡單地説,ambient 模式採用了  HTTP 的 CONNECT 方法  [2] 在 ztunnel 和 waypoint proxy 創建了一個隧道,通過該隧道來傳輸數據。本文將分析 HBONE 的實現機制和原理。

HTTP 隧道原理

建立 HTTP 隧道的常見形式是採用 HTTP 協議的 CONNECT 方法。在這種機制下,客户端首先向 HTTP 代理服務器發送一個 HTTP CONNECT 請求,請求中攜帶需要連接的目的服務器。代理服務器根據該請求代表客户端連接目的服務器。和目的服務器建立連接後,代理服務器將客户端 TCP 數據流直接透明地傳送給目的服務器。在這種方式中,只有初始連接請求是 HTTP,之後代理服務器處理的是 TCP 數據流。

HTTP CONNECT 隧道

通過這種方法,我們可以採用 HTTP CONNECT 創建一個隧道,該隧道中可以傳輸任何類型的 TCP 數據。

例如在一個內網環境中,我們只允許通過 HTTP 代理來訪問外部的 web 服務器。但我們可以通過 HTTP 隧道的方式來連接到一個外部的 SSH 服務器上。。

客户端連接到代理服務器,發送 HTTP CONNECT 請求通過和指定主機的 22 端口建立隧道。

CONNECT for.bar.com:22 HTTP/1.1

如果代理允許連接,並且代理已連接到指定的主機,則代理將返回 2XX 成功響應。

HTTP/1.1 200 OK

現在客户端將通過代理訪問遠程主機。發送到代理服務器的所有數據都將原封不動地轉發到遠程主機。

客户端和服務器開始 SSH 通信。

SSH-2.0-OpenSSH_4.3\r\n
... ggg

備註:除了 HTTP CONNECT 以外,採用 HTTP GET 和 POST 也可以創建 HTTP 隧道,這種方式創建的隧道的原理是將 TCP 數據封裝到 HTTP 數據包中發送到外部服務器,該外部服務器會提取並執行客户端的原始網絡請求。外部服務器收到此請求的響應後,將其重新打包為 HTTP 響應,併發送回客户端。在這種方式中,客户端所有流量都封裝在 HTTP GET 或者 POST 請求中。

Envoy 的 Internal Listener 機制

我們知道, socket [3] 在操作系統內核接收網絡數據,但 Envoy 還支持一種 “用户空間 socket”。 Internal Listener [4] 就用於從該 “用户空間 socket” 接收數據包。

Internal Listener 需要和一個 Cluster 一起使用,配置在 Cluster 中作為接收流量的 endpoint。如下所示:

定義一個 Internal Listener:

name: demo_internal_listener
internal_listener: {}
filter_chains:
- filters: [
  ......
]

採用一個 Cluster 來連接 Egress Listener 和 Internal Listener。該 Cluster 配置在 Egress Listener 的 HCM 中,其 endpoint 是 Internal Listener 的 name。

name: encap_cluster
load_assignment:
  cluster_name: encap_cluster
  endpoints:
  - lb_endpoints:
    - endpoint:
        address:
          envoy_internal_address:
            server_listener_name: demo_internal_listener

通過這種方式, 可以將兩個 Listener 串聯起來,第一個 Listener 從操作系統內核接收網絡數據,然後再經過 interal_listener_cluster 傳遞給 demo_internal_listener 處理,如下面的配置所示:

name: ingress
address:
  socket_address:
    protocol: TCP
    address: 127.0.0.1
    port_value: 9999
filter_chains:
- filters:
  - name: tcp
    typed_config:
      "@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy
      stat_prefix: ingress
      cluster: encap_cluster

Envoy 的 HTTP Tunnel

我們可以採用 Envoy 來作為客户端創建一個到 HTTP Proxy 的 HTTP Tunnel,也可以採用 Envoy 來作為 HTTP Proxy 服務器接收來自客户端的 HTTP CONNECT 請求。

Envoy 作為 HTTP 隧道客户端

通過串聯兩個 Listener,可以將外部 Listener 中收到的 HTTP 請求通過 Internal Listener 創建的 HTTP 隧道發送到後端的代理服務器,如下所示(該配置文件來自 Envoy Github 中的示例文件  [5] ):

Egress(入口) Listener,從端口 1000 接收來自客户端的 HTTP 請求

name: http
address:
  socket_address:
    protocol: TCP
    address: 127.0.0.1
    port_value: 10000
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: ingress_http
      route_config:
        name: local_route
        virtual_hosts:
        - name: local_service
          domains: ["*"]
          routes:
          - match:
              prefix: "/"
            route:
              cluster: encap_cluster
      http_filters:
      - name: envoy.filters.http.router
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router

Internal Listener,其 filter chain 中配置的是一個 TcpProxy。該 TcpProxy 中設置了 tunneling_config [6] 選項,表示該 TcpProxy 將同 upstream 建立一個 HTTP 隧道,將收到的 TCP 數據通過該 HTTP 隧道發送到 upstream。Envoy 支持採用 HTTP/1.1 和 HTTP/2 兩種方式創建隧道,具體採用哪種協議取決於 upstream cluster 配置中的 typed_extension_protocol_options 部分。

name: encap
internal_listener: {}
filter_chains:
- filters:
  - name: tcp
    typed_config:
      "@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy
      stat_prefix: tcp_stats
      cluster: cluster_0
      # 表示該 TcpProxy 將採用 HTTP 隧道的方式代理數據
      tunneling_config: 
        hostname: host.com:443

該 Cluster 配置在 Egress Cluster 的 HCM 中,用於關聯 Egress Listener 和 Internal Listener。

clusters:
- name: encap_cluster
  load_assignment:
    cluster_name: encap_cluster
    endpoints:
    - lb_endpoints:
      - endpoint:
          address:
            envoy_internal_address:
              server_listener_name: encap

該 Cluster 配置在 Internal Cluster 中,是 HTTP 隧道連接的 Upstream。

  - name: cluster_0
    # 該選項表示將採用 HTTP2 CONNECT 來創建隧道
    typed_extension_protocol_options:
      envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
        "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
        explicit_http_config:
          http2_protocol_options: {}
    load_assignment:
    # 隧道連接的 upstream server 地址
      cluster_name: cluster_0
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: 127.0.0.1
                port_value: 10001
採用 Internal Listener 創建 HTTP 隧道,代理 downstream 的 HTTP 請求

上面的示例中 Egress Listener 的 filter chain 中配置的是 HCM。由於 HTTP 隧道是透明傳輸 TCP 數據流的,因此其中可以是任意七層協議的數據,Egress Listener 中的 filter chain 中也可以配置為 Tcp Proxy。

Envoy 作為 HTTP 隧道服務器

當然,我們可以採用 Envoy 來作為 HTTP Proxy 來接收 HTTP CONNECT 請求,建立和客户端的 HTTP 隧道。Envoy 不能在同一個 Listener 裏面建立隧道並將從 HTTP 數據從隧道中解封出來。要實現這一點,我們需要兩層 listener,第一層 listener 中的 HCM 負責創建 HTTP CONNECT 隧道並從隧道中拿到 TCP 數據流,然後將該 TCP 數據流交給個 listener 中的 HCM 進行 HTTP 處理。

下面的配置將 Envoy 作為一個 HTTP CONNECT 隧道服務器端,並採用一個 Internal Listen 對隧道中的數據進行 HTTP 處理。(該配置文件來自 Envoy Github 中的示例文件  [7]

Egress Listener,從 10001 端口接收來自隧道客户端的 HTTP CONNECT 請求,並將隧道中的數據遞交給 Internal Listener 進行下一步處理。注意其中 HCM 的 upgrade_type: CONNECT 選項表示支持 HTTP CONNECT 隧道, http2_protocol_options 表示採用 HTTP/2。

listeners:
- name: listener_0
  address:
    socket_address:
      protocol: TCP
      address: 127.0.0.1
      port_value: 10001
  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: ingress_http
        route_config:
          name: local_route
          virtual_hosts:
          - name: local_service
            domains:
            - "*"
            routes:
            - match:
                connect_matcher:
                  {}
              route:
                # 數據將被髮送給 decap_cluster
                cluster: decap_cluster
                upgrade_configs:
                - upgrade_type: CONNECT
                  connect_config:
                    {}
        http_filters:
        - name: envoy.filters.http.router
          typed_config:
            "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
        http2_protocol_options:
          allow_connect: true
        upgrade_configs:
        # 該選項標準支持採用 HTTP CONNECT 請求來創建隧道
        - upgrade_type: CONNECT

Internal Listener,從隧道中拿到的 TCP 流解析出 HTTP 請求,並返回一個 HTTP 200 響應。

  - name: decap
    internal_listener: {}
    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: ingress_http
          route_config:
            name: local_route
            virtual_hosts:
            - name: local_service
              domains: ["*"]
              routes:
              - match:
                  prefix: "/"
                direct_response:
                  status: 200
                  body:
                    inline_string: "Hello, world!\n"
          http_filters:
          - name: envoy.filters.http.router
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router

採用一個 Cluster 來連接 Egress Listener 和 Internal Listener。該 Cluster 配置在 Egress Listener 的 HCM 中,其 endpoint 是 Internal Listener 的 name。

  clusters:
  - name: decap_cluster
    load_assignment:
      cluster_name: decap_cluster
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              envoy_internal_address:
                server_listener_name: decap
採用 Internal Listener 對來自 HTTP CONNECT 隧道的數據進行 HTTP 處理

採用 Envoy 來創建一個端到端的 HTTP CONNECT 隧道

從上面的分析可以得知,Envoy 可以作為 Tunnel Client 發起一個 HTTP CONNECT 隧道創建請求,也可以作為 Tunnel Server 來創建一個 HTTP CONNECT 隧道。因此我們可以採用兩個 Envoy 來作為 HTTP CONNECT 隧道的兩端,如下圖所示:

採用 Envoy 來創建 HTTP CONNECT 隧道,並對隧道中的數據進行 HTTP 處理

Istio 的 HBONE 隧道

Istio HBONE 採用了上面介紹的方法來創建 HTTP CONNET 隧道,TCP 流量在進入隧道時會進行 mTLS 加密,在出隧道時進行 mTLS 卸載。一個採用 HBONE 創建的連接如下所示:

HBONE 連接

HBONE 由於採用了 HTTP CONNECT 創建隧道,還可以在 HTTP CONNECT 請求中加入一些 header 來很方便地在 downstream 和 upstream 之間傳遞上下文信息,包括:

  • • authority - 請求的原始目的地址,例如 1.2.3.4:80。

  • • X-Forwarded-For(可選) - 請求的原始源地址,用於在多跳訪問之間保留源地址。

  • • baggage (可選) - client/server 的一些元數據,在 telemetry 中使用。

在這篇文章中,我們介紹了 Istio ambient 模式用來連接 ztunnel 和 waypoint proxy 的 HBONE 隧道的基本原理。下一篇文章中,我們將以 bookinfo demo 程序為例來深入分析 ambient 模式中 HBONE 的流量路徑。

參考資料

  • • https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/CONNECT

  • https://zh.wikipedia.org/wiki/HTTP%E9%9A%A7%E9%81%93 [8]

  • • https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/http/upgrades

  • • https://www.envoyproxy.io/docs/envoy/latest/configuration/other_features/internal_listener

  • • https://docs.google.com/document/d/1Ofqtxqzk-c_wn0EgAXjaJXDHB9KhDuLe-W3YGG67Y8g

  • • https://docs.google.com/document/d/1ubUG78rNQbwwkqpvYcr7KgM14kEHwitSsuorCZjR6qY/edit#

引用鏈接

[1] HBONE:  https://www.zhaohuabing.com/post/2022-09-08-introducing-ambient-mesh/# 構建一個 - ambient-mesh

[2] HTTP 的 CONNECT 方法:  https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/CONNECT

[3] socket:  https://man7.org/linux/man-pages/man2/socket.2.html

[4] Internal Listener:  https://www.envoyproxy.io/docs/envoy/latest/configuration/other_features/internal_listener

[5] Envoy Github 中的示例文件:  https://github.com/envoyproxy/envoy/blob/8537d2a29265e61aaa0349311e6fc5d592659b08/configs/encapsulate_http_in_http2_connect.yaml

[6] tunneling_config:  https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/network/tcp_proxy/v3/tcp_proxy.proto#envoy-v3-api-msg-extensions-filters-network-tcp-proxy-v3-tcpproxy-tunnelingconfig

[7] Envoy Github 中的示例文件:  https://github.com/envoyproxy/envoy/blob/8537d2a29265e61aaa0349311e6fc5d592659b08/configs/terminate_http_in_http2_connect.yaml

[8] https://zh.wikipedia.org/wiki/HTTP%E9%9A%A7%E9%81%93:  https://zh.wikipedia.org/wiki/HTTP 隧道