簡介
代理大家應(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ù),更懂你最住!