網路協議之:haproxy的Proxy Protocol代理協議

flydean發表於2022-06-01

簡介

代理大家應該都很熟悉了,比較出名的像是nginx,apache HTTPD,stunnel等。

我們知道代理就是代替客戶端向伺服器端進行訊息請求,並且希望在代理的過程中保留初始的TCP連線資訊,例如源和目標IP和埠等,以提供一些個性化的操作。

一般情況下,為了實現這個目標,有一些現成的解決辦法,比如在HTTP協議中,可以使用“X-Forwarded-For”標頭,來包含有關原始源地址,還有"X-Original-To"用來攜帶目的地址的資訊。

又比如在SMTP協議中,可以特別使用XCLIENT協議來進行郵件交換。

或者可以通過編譯核心,把你的代理作為你伺服器的預設閘道器。

這些方式雖然可用,但是或多或少有一些限制,要麼與協議相關,要麼修改修改系統架構,從而可擴充套件性不強。

尤其是在多個代理伺服器鏈式呼叫的情況下,上述方法幾乎是不可能完成的。

這就需要一個統一的代理協議,通過所有的節點都相容這個代理協議就可以無縫實現代理的鏈式呼叫。這個代理協議就是haproxy在2010年提出的proxy Protocol。

這個代理協議的優點是:

  • 它與協議無關(可以與任何7層協議一起使用,即使在加密的情況也可用)
  • 它不需要任何基礎架構更改
  • 可以穿透NAT防火牆
  • 它是可擴充套件的

而haproxy本身就是一個非常優秀的開源負載均衡和代理軟體,提供了高負載能力和優秀的效能,所以在很多公司得以廣泛的使用,比如:GoDaddy, GitHub, Bitbucket,Stack Overflow,Reddit, Slack,Speedtest.net, Tumblr, Twitter等。

今天要介紹的就是haproxy的Proxy Protocol代理協議的底層細節。

Proxy Protocol的實現細節

上面我們提到了Proxy Protocol的目的就是可以攜帶一些可以標記初始的TCP連線資訊的欄位,比如IP地址和埠等。

如果是客戶端和伺服器端直連,那麼伺服器端可以通過getsockname和getpeername獲得如下的資訊:

  • address family: AF_INET for IPv4, AF_INET6 for IPv6, AF_UNIX
  • socket protocol: SOCK_STREAM for TCP, SOCK_DGRAM for UDP
  • 網路層的源和目標地址
  • 傳輸層的源和目標的埠號

所以Proxy Protocol的目的就是封裝上面的這些資訊,然後將上述資訊放到請求頭中去,這樣伺服器端就可以正確讀取客戶端的資訊。

在Proxy Protocol中,定義了兩個版本。

在版本1中,標頭檔案資訊是文字形式的,也就是人類可讀的,採用這種方式,主要是為了在協議應用的早期保證更好的可除錯性,從而快速景修正。

在版本2中,提供了對標頭檔案的二進位制編碼功能,在版本1的功能已經基本完善的前提下,提供二進位制編碼,可以有效的提高應用的傳輸和處理效能。

因為有兩個版本,所以在伺服器的接收端也需要實現對相應版本的支援。

為了更好的應用Proxy Protocol,Proxy Protocol實際只定義了一個header資訊,這個請求頭會在連線發起者發起連線的時候放在每個連線的開頭。並且該協議是無狀態的,因為它不期望傳送者在傳送標頭之前等待接收者,也不期望接收者傳送回任何內容。

接下來,我們具體觀察一下兩個版本協議的實現。

版本1

在版本1中,proxy header是由一串US-ASCII編碼的字串組成的。這個proxy header將會在客戶端和伺服器端建立連線,並且傳送任何真實資料之前傳送。

先來看一個使用了proxy header的http請求的例子:

    PROXY TCP4 192.168.0.1 192.168.0.102 12345 443\r\n
    GET / HTTP/1.1\r\n
    Host: 192.168.0.102\r\n
    \r\n

上面的例子中,\r\n表示的是回車換行,也就是行結束的標記。該程式碼向host:192.168.0.102傳送了一個HTTP請求,第一行的內容就是使用的proxy header。

具體什麼含義呢?

首先是字串"PROXY",表示這是一個proxy protocol的header,並且是v1版本的。

接著是一個空格分隔符。

然後是proxy使用的INET protocol 和 family。對於v1版本來說,支援"TCP4"和"TCP6"這兩種方式。上面的例子中,我們使用的是TCP4.

如果要使用其他的協議,那麼可以設定為"UNKNOWN"。如果設定為"UNKNOWN",那麼後面到CRLF之前的資料將會被忽略。

接著是一個空格分隔符。

然後是網路層源的IP地址,根據選的是TCP4還是TCP6,對應的源IP地址也有不同的表示形式。

接著是一個空格分隔符。

然後是網路層目標地址的IP地址,根據選的是TCP4還是TCP6,對應的源IP地址也有不同的表示形式。

接著是一個空格分隔符。

然後是TCP源的埠號,取值範圍是0-65535。

接著是一個空格分隔符。

然後是TCP目標地址的埠號,取值範圍是0-65535。

接著是CRLF結束符。

這樣一個v1版本的proxy protocol就定義完了,是不是很簡單。

根據這樣的定義,我們很好來計算整個proxy protocol的最大長度,對於TC4來說,最大的長度表示為:

  - TCP/IPv4 :
      "PROXY TCP4 255.255.255.255 255.255.255.255 65535 65535\r\n"
    => 5 + 1 + 4 + 1 + 15 + 1 + 15 + 1 + 5 + 1 + 5 + 2 = 56 chars

對於TCP6來說,最大的長度表示為:

  - TCP/IPv6 :
      "PROXY TCP6 ffff:f...f:ffff ffff:f...f:ffff 65535 65535\r\n"
    => 5 + 1 + 4 + 1 + 39 + 1 + 39 + 1 + 5 + 1 + 5 + 2 = 104 chars

對於UNKNOWN來說,可能有下面的最小長度和最大長度表示為:

  - unknown connection (short form) :
      "PROXY UNKNOWN\r\n"
    => 5 + 1 + 7 + 2 = 15 chars

  - worst case (optional fields set to 0xff) :
      "PROXY UNKNOWN ffff:f...f:ffff ffff:f...f:ffff 65535 65535\r\n"
    => 5 + 1 + 7 + 1 + 39 + 1 + 39 + 1 + 5 + 1 + 5 + 2 = 107 chars

所以,總體來說108個字元已經足夠v1版本使用了。

版本2

版本2主要是實現的二進位制編碼,雖然對人類可讀不友好,但是可以提高傳輸和解析效率。

版本2的header是以下面12 bytes開頭的block:

\x0D \x0A \x0D \x0A \x00 \x0D \x0A \x51 \x55 \x49 \x54 \x0A

接下來的一個byte(13 bytes)是protocol version 和 command。因為一個byte是8個bits,使用一個byte來儲存有點太奢侈了。所以將其拆分成兩部分。

高位的4個bits儲存的是版本,這裡版本號必須是"\x2"。

低位的4個bits儲存的是command,有下面幾個值:

  • LOCAL(\x0): 表示連線是由代理自己發起的,一般用在代理向伺服器傳送健康檢查時。
  • PROXY(\x1): 代表連線是由另外一個節點發起的,這是一個proxy代理請求。 然後接收者必須使用協議塊中提供的資訊來獲取原始地址。
  • 其他:其他command都需要被丟棄,因為不可識別。

接下來的一個byte(14 bytes)儲存的是transport protocol 和 address family。

其中高4位儲存的是address family,低4位儲存的是transport protocol。

address family可能有下面的值:

  • AF_UNSPEC(0x0): 表示的是不支援的,或者未定義的protocol。當sender傳送LOCAL command或者處理為止protocol families的時候就可以使用這個值。
  • AF_INET(0x1):表示的是IPv4地址,佔用4bytes。
  • AF_INET6(0x2):表示的是IPv6地址,佔用16bytes。
  • AF_UNIX(0x3):表示的是unix address地址,佔用108 bytes。

transport protocol可能有下面的值:

  • UNSPEC(0x0): 未知協議型別。
  • STREAM(0x1):使用的是SOCK_STREAM protocol,比如TCP 或者UNIX_STREAM。
  • DGRAM(0x2):使用的是SOCK_DGRAM protocol,比如UDP 或者UNIX_DGRAM。

低4位和高4位進行組合,可以得到下面幾種值:

  • UNSPEC(\x00)
  • TCP over IPv4(\x11)
  • UDP over IPv4(\x12)
  • TCP over IPv6(\x21)
  • UDP over IPv6(\x22)
  • UNIX stream(\x31)
  • UNIX datagram(\x32)

第15和16 bytes表示的剩下的欄位的長度,綜上,16-byte的v2可以用下面的結構體表示:

    struct proxy_hdr_v2 {
        uint8_t sig[12];  /* hex 0D 0A 0D 0A 00 0D 0A 51 55 49 54 0A */
        uint8_t ver_cmd;  /* protocol version and command */
        uint8_t fam;      /* protocol family and address */
        uint16_t len;     /* number of following bytes part of the header */
    };

從第17個byte開始,就是地址的長度和埠號資訊,可以用下面的結構體表示:

    union proxy_addr {
        struct {        /* for TCP/UDP over IPv4, len = 12 */
            uint32_t src_addr;
            uint32_t dst_addr;
            uint16_t src_port;
            uint16_t dst_port;
        } ipv4_addr;
        struct {        /* for TCP/UDP over IPv6, len = 36 */
             uint8_t  src_addr[16];
             uint8_t  dst_addr[16];
             uint16_t src_port;
             uint16_t dst_port;
        } ipv6_addr;
        struct {        /* for AF_UNIX sockets, len = 216 */
             uint8_t src_addr[108];
             uint8_t dst_addr[108];
        } unix_addr;
    };

在V2版本中,除了address資訊之外,header中還可以包含一些額外的擴充套件資訊,這些資訊被稱為Type-Length-Value (TLV vectors),格式如下:

        struct pp2_tlv {
            uint8_t type;
            uint8_t length_hi;
            uint8_t length_lo;
            uint8_t value[0];
        };

欄位的含義分別是型別,長度和值。

下面是目前支援的型別:

        #define PP2_TYPE_ALPN           0x01
        #define PP2_TYPE_AUTHORITY      0x02
        #define PP2_TYPE_CRC32C         0x03
        #define PP2_TYPE_NOOP           0x04
        #define PP2_TYPE_UNIQUE_ID      0x05
        #define PP2_TYPE_SSL            0x20
        #define PP2_SUBTYPE_SSL_VERSION 0x21
        #define PP2_SUBTYPE_SSL_CN      0x22
        #define PP2_SUBTYPE_SSL_CIPHER  0x23
        #define PP2_SUBTYPE_SSL_SIG_ALG 0x24
        #define PP2_SUBTYPE_SSL_KEY_ALG 0x25
        #define PP2_TYPE_NETNS          0x30

Proxy Protocol的使用情況

上面也提到了,一個協議的好壞不僅僅在與這個協議定義的好不好,也在於使用這個協議的軟體多不多。

如果主流的代理軟體都沒有使用你這個代理協議,那麼協議定義的再好也沒有用。相反,如果大家都在使用你這個協議,協議定義的再差也是主流協議。

好在Proxy Protocol已經在代理伺服器界被廣泛的使用了。

具體使用該協議的軟體如下:

  • Elastic Load Balancing,AWS的負載均衡器,從2013年7月起相容
  • Dovecot,一個POP/IMAP郵件伺服器從2.2.19版本開始相容
  • exaproxy,一個正向和反向代理伺服器,從1.0.0版本開始相容
  • gunicorn ,python HTTP 伺服器,從0.15.0開始相容
  • haproxy,反向代理負載均衡器,從1.5-dev3開始相容
  • nginx,正方向代理伺服器,http伺服器,從1.5.12開始相容
  • Percona DB,資料庫伺服器,從5.6.25-73.0開始相容
  • stud,SSL offloader,從第一個版本開始相容
  • stunnel,SSL offloader,從4.45開始相容
  • apache HTTPD,web 伺服器,在擴充套件模組myfixip中使用
  • varnish,HTTP 反向代理快取,從4.1版開始相容

基本上所有的主流伺服器都相容Proxy Protocol,所以我們可以把Proxy Protocol當做是事實上的標準。

總結

在本文中,我們介紹了Proxy Protocol的底層定義,那麼Proxy Protocol具體怎麼使用,能不能實現自己的Proxy Protocol伺服器呢?敬請期待。

本文已收錄於 http://www.flydean.com/20-haproxy-protocol/

最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!

歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!

相關文章