網(wǎng)絡(luò)協(xié)議之:haproxy的Proxy Protocol代理協(xié)議

簡介

代理大家應(yīng)該都很熟悉了撑螺,比較出名的像是nginx,apache HTTPD纺念,stunnel等苫耸。

我們知道代理就是代替客戶端向服務(wù)器端進行消息請求疫鹊,并且希望在代理的過程中保留初始的TCP連接信息,例如源和目標(biāo)IP和端口等伐蒂,以提供一些個性化的操作煞躬。

一般情況下,為了實現(xiàn)這個目標(biāo)逸邦,有一些現(xiàn)成的解決辦法恩沛,比如在HTTP協(xié)議中,可以使用“X-Forwarded-For”標(biāo)頭缕减,來包含有關(guān)原始源地址复唤,還有"X-Original-To"用來攜帶目的地址的信息。

又比如在SMTP協(xié)議中烛卧,可以特別使用XCLIENT協(xié)議來進行郵件交換佛纫。

或者可以通過編譯內(nèi)核,把你的代理作為你服務(wù)器的默認網(wǎng)關(guān)总放。

這些方式雖然可用呈宇,但是或多或少有一些限制,要么與協(xié)議相關(guān)局雄,要么修改修改系統(tǒng)架構(gòu)甥啄,從而可擴展性不強。

尤其是在多個代理服務(wù)器鏈?zhǔn)秸{(diào)用的情況下炬搭,上述方法幾乎是不可能完成的蜈漓。

這就需要一個統(tǒng)一的代理協(xié)議,通過所有的節(jié)點都兼容這個代理協(xié)議就可以無縫實現(xiàn)代理的鏈?zhǔn)秸{(diào)用宫盔。這個代理協(xié)議就是haproxy在2010年提出的proxy Protocol融虽。

這個代理協(xié)議的優(yōu)點是:

  • 它與協(xié)議無關(guān)(可以與任何7層協(xié)議一起使用,即使在加密的情況也可用)
  • 它不需要任何基礎(chǔ)架構(gòu)更改
  • 可以穿透NAT防火墻
  • 它是可擴展的

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

今天要介紹的就是haproxy的Proxy Protocol代理協(xié)議的底層細節(jié)巍佑。

Proxy Protocol的實現(xiàn)細節(jié)

上面我們提到了Proxy Protocol的目的就是可以攜帶一些可以標(biāo)記初始的TCP連接信息的字段茴迁,比如IP地址和端口等。

如果是客戶端和服務(wù)器端直連萤衰,那么服務(wù)器端可以通過getsockname和getpeername獲得如下的信息:

  • address family: AF_INET for IPv4, AF_INET6 for IPv6, AF_UNIX
  • socket protocol: SOCK_STREAM for TCP, SOCK_DGRAM for UDP
  • 網(wǎng)絡(luò)層的源和目標(biāo)地址
  • 傳輸層的源和目標(biāo)的端口號

所以Proxy Protocol的目的就是封裝上面的這些信息堕义,然后將上述信息放到請求頭中去,這樣服務(wù)器端就可以正確讀取客戶端的信息脆栋。

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

在版本1中筹吐,頭文件信息是文本形式的糖耸,也就是人類可讀的秘遏,采用這種方式丘薛,主要是為了在協(xié)議應(yīng)用的早期保證更好的可調(diào)試性,從而快速景修正邦危。

在版本2中洋侨,提供了對頭文件的二進制編碼功能,在版本1的功能已經(jīng)基本完善的前提下倦蚪,提供二進制編碼希坚,可以有效的提高應(yīng)用的傳輸和處理性能。

因為有兩個版本陵且,所以在服務(wù)器的接收端也需要實現(xiàn)對相應(yīng)版本的支持裁僧。

為了更好的應(yīng)用Proxy Protocol,Proxy Protocol實際只定義了一個header信息慕购,這個請求頭會在連接發(fā)起者發(fā)起連接的時候放在每個連接的開頭聊疲。并且該協(xié)議是無狀態(tài)的,因為它不期望發(fā)送者在發(fā)送標(biāo)頭之前等待接收者沪悲,也不期望接收者發(fā)送回任何內(nèi)容获洲。

接下來,我們具體觀察一下兩個版本協(xié)議的實現(xiàn)殿如。

版本1

在版本1中贡珊,proxy header是由一串US-ASCII編碼的字符串組成的。這個proxy header將會在客戶端和服務(wù)器端建立連接涉馁,并且發(fā)送任何真實數(shù)據(jù)之前發(fā)送门岔。

先來看一個使用了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表示的是回車換行烤送,也就是行結(jié)束的標(biāo)記固歪。該代碼向host:192.168.0.102發(fā)送了一個HTTP請求,第一行的內(nèi)容就是使用的proxy header。

具體什么含義呢牢裳?

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

接著是一個空格分隔符。

然后是proxy使用的INET protocol 和 family蒲讯。對于v1版本來說忘朝,支持"TCP4"和"TCP6"這兩種方式。上面的例子中判帮,我們使用的是TCP4.

如果要使用其他的協(xié)議局嘁,那么可以設(shè)置為"UNKNOWN"。如果設(shè)置為"UNKNOWN"晦墙,那么后面到CRLF之前的數(shù)據(jù)將會被忽略悦昵。

接著是一個空格分隔符。

然后是網(wǎng)絡(luò)層源的IP地址晌畅,根據(jù)選的是TCP4還是TCP6但指,對應(yīng)的源IP地址也有不同的表示形式。

接著是一個空格分隔符抗楔。

然后是網(wǎng)絡(luò)層目標(biāo)地址的IP地址棋凳,根據(jù)選的是TCP4還是TCP6,對應(yīng)的源IP地址也有不同的表示形式连躏。

接著是一個空格分隔符剩岳。

然后是TCP源的端口號,取值范圍是0-65535入热。

接著是一個空格分隔符拍棕。

然后是TCP目標(biāo)地址的端口號,取值范圍是0-65535勺良。

接著是CRLF結(jié)束符绰播。

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

根據(jù)這樣的定義幅垮,我們很好來計算整個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個字符已經(jīng)足夠v1版本使用了呵萨。

版本2

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

版本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): 表示連接是由代理自己發(fā)起的,一般用在代理向服務(wù)器發(fā)送健康檢查時础米。

  • PROXY(\x1): 代表連接是由另外一個節(jié)點發(fā)起的分苇,這是一個proxy代理請求。 然后接收者必須使用協(xié)議塊中提供的信息來獲取原始地址屁桑。

  • 其他:其他command都需要被丟棄医寿,因為不可識別。

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

其中高4位保存的是address family,低4位保存的是transport protocol靖秩。

address family可能有下面的值:

  • AF_UNSPEC(0x0): 表示的是不支持的,或者未定義的protocol乌叶。當(dāng)sender發(fā)送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): 未知協(xié)議類型。
  • 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可以用下面的結(jié)構(gòu)體表示:

    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開始蒲凶,就是地址的長度和端口號信息,可以用下面的結(jié)構(gòu)體表示:

    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的使用情況

上面也提到了,一個協(xié)議的好壞不僅僅在與這個協(xié)議定義的好不好,也在于使用這個協(xié)議的軟件多不多刻肄。

如果主流的代理軟件都沒有使用你這個代理協(xié)議瓤球,那么協(xié)議定義的再好也沒有用。相反敏弃,如果大家都在使用你這個協(xié)議冰垄,協(xié)議定義的再差也是主流協(xié)議。

好在Proxy Protocol已經(jīng)在代理服務(wù)器界被廣泛的使用了权她。

具體使用該協(xié)議的軟件如下:

  • Elastic Load Balancing虹茶,AWS的負載均衡器,從2013年7月起兼容
  • Dovecot隅要,一個POP/IMAP郵件服務(wù)器從2.2.19版本開始兼容
  • exaproxy蝴罪,一個正向和反向代理服務(wù)器,從1.0.0版本開始兼容
  • gunicorn 步清,python HTTP 服務(wù)器要门,從0.15.0開始兼容
  • haproxy,反向代理負載均衡器廓啊,從1.5-dev3開始兼容
  • nginx欢搜,正方向代理服務(wù)器,http服務(wù)器谴轮,從1.5.12開始兼容
  • Percona DB炒瘟,數(shù)據(jù)庫服務(wù)器,從5.6.25-73.0開始兼容
  • stud第步,SSL offloader,從第一個版本開始兼容
  • stunnel疮装,SSL offloader,從4.45開始兼容
  • apache HTTPD粘都,web 服務(wù)器廓推,在擴展模塊myfixip中使用
  • varnish,HTTP 反向代理緩存翩隧,從4.1版開始兼容

基本上所有的主流服務(wù)器都兼容Proxy Protocol樊展,所以我們可以把Proxy Protocol當(dāng)做是事實上的標(biāo)準(zhǔn)。

總結(jié)

在本文中堆生,我們介紹了Proxy Protocol的底層定義专缠,那么Proxy Protocol具體怎么使用,能不能實現(xiàn)自己的Proxy Protocol服務(wù)器呢顽频?敬請期待藤肢。

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

最通俗的解讀,最深刻的干貨糯景,最簡潔的教程嘁圈,眾多你不知道的小技巧等你來發(fā)現(xiàn)省骂!

歡迎關(guān)注我的公眾號:「程序那些事」,懂技術(shù),更懂你最住!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末钞澳,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子涨缚,更是在濱河造成了極大的恐慌轧粟,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,252評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件脓魏,死亡現(xiàn)場離奇詭異兰吟,居然都是意外死亡,警方通過查閱死者的電腦和手機茂翔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評論 3 399
  • 文/潘曉璐 我一進店門混蔼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人珊燎,你說我怎么就攤上這事惭嚣。” “怎么了悔政?”我有些...
    開封第一講書人閱讀 168,814評論 0 361
  • 文/不壞的土叔 我叫張陵晚吞,是天一觀的道長。 經(jīng)常有香客問我谋国,道長槽地,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,869評論 1 299
  • 正文 為了忘掉前任烹卒,我火速辦了婚禮闷盔,結(jié)果婚禮上弯洗,老公的妹妹穿的比我還像新娘旅急。我一直安慰自己,他們只是感情好牡整,可當(dāng)我...
    茶點故事閱讀 68,888評論 6 398
  • 文/花漫 我一把揭開白布藐吮。 她就那樣靜靜地躺著,像睡著了一般逃贝。 火紅的嫁衣襯著肌膚如雪谣辞。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,475評論 1 312
  • 那天沐扳,我揣著相機與錄音泥从,去河邊找鬼。 笑死沪摄,一個胖子當(dāng)著我的面吹牛躯嫉,可吹牛的內(nèi)容都是我干的纱烘。 我是一名探鬼主播,決...
    沈念sama閱讀 41,010評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼祈餐,長吁一口氣:“原來是場噩夢啊……” “哼擂啥!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起帆阳,我...
    開封第一講書人閱讀 39,924評論 0 277
  • 序言:老撾萬榮一對情侶失蹤哺壶,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后蜒谤,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體山宾,經(jīng)...
    沈念sama閱讀 46,469評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,552評論 3 342
  • 正文 我和宋清朗相戀三年鳍徽,在試婚紗的時候發(fā)現(xiàn)自己被綠了塌碌。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,680評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡旬盯,死狀恐怖台妆,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情胖翰,我是刑警寧澤接剩,帶...
    沈念sama閱讀 36,362評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站萨咳,受9級特大地震影響懊缺,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜培他,卻給世界環(huán)境...
    茶點故事閱讀 42,037評論 3 335
  • 文/蒙蒙 一鹃两、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧舀凛,春花似錦俊扳、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,519評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至懊烤,卻和暖如春梯醒,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背腌紧。 一陣腳步聲響...
    開封第一講書人閱讀 33,621評論 1 274
  • 我被黑心中介騙來泰國打工茸习, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人壁肋。 一個月前我還...
    沈念sama閱讀 49,099評論 3 378
  • 正文 我出身青樓号胚,卻偏偏與公主長得像代箭,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子涕刚,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,691評論 2 361

推薦閱讀更多精彩內(nèi)容