当前位置: 首页 > 后端技术 > Java

网络协议:ProxyProtocol

时间:2023-04-02 01:50:50 Java

haproxy介绍大家对proxy应该不陌生,比较有名的有nginx、apacheHTTPD、stunnel等。我们知道proxy就是代替client向server,我们希望在代理的过程中保留初始的TCP连接信息,比如源目的IP和端口等,以提供一些个性化的操作。一般来说,为了实现这个目标,有一些现成的解决方案。例如,在HTTP协议中,可以使用“X-Forwarded-For”报头来包含原始源地址的信息,而“X-Original-To”信息则用来携带目的地址。又如在SMTP协议中,XCLIENT协议可以专门用于邮件交换。或者您可以通过编译内核将代理用作服务器的默认网关。这些方法虽然都有,但都或多或少存在局限性,要么与协议有关,要么修改系统架构,可扩展性不强。尤其是在多个代理服务器链调用的情况下,上面的方法几乎是不可能完成的。这就需要一个统一的代理协议,通过它所有节点都兼容代理协议,可以无缝实现代理链调用。这个代理协议是haproxy在2010年提出的代理协议。这个代理协议的优点是:它是协议无关的(可以与任何第7层协议一起使用,甚至加密)它不需要任何基础设施更改可以穿透NAT防火墙具有可扩展性,haproxy本身是一款非常优秀的开源负载均衡和代理软件,提供了高负载能力和出色的性能,所以被很多公司广泛使用,比如:GoDaddy、GitHub、Bitbucket、StackOverflow、Reddit、Slack,Speedtest.net,Tumblr,Twitter等。今天要介绍的是haproxy的ProxyProtocol代理协议的底层细节。ProxyProtocol的实现细节上面我们提到了ProxyProtocol的目的是携带一些可以标记初始TCP连接信息的字段,比如IP地址和端口。如果客户端直接连接到服务器,服务器可以通过getsockname和getpeername获取以下信息:地址族:AF_INETforIPv4,AF_INET6forIPv6,AF_UNIXsocketprotocol:SOCK_STREAMforTCP,SOCK_DGRAMforUDP网络层地址传输层的源和目的的端口号,所以ProxyProtocol的目的就是封装上面的信息,然后把上面的信息放到请求头中,这样服务器就可以正确的读取到客户端的信息。在代理协议中,定义了两个版本。在版本1中,头文件信息是文本形式的,即人类可读的。这种方式主要是为了保证在协议应用的前期有更好的可调试性,以便快速修改场景。在版本2中,提供了头文件的二进制编码功能。在版本1的功能已经基本完善的前提下,提供二进制编码可以有效提高应用程序的传输和处理性能。因为有两个版本,服务端的接收端也需要实现对相应版本的支持。为了更好的应用ProxyProtocol,ProxyProtocol实际上只定义了一个header信息,当连接发起者发起连接时,它会被放在每个连接的开头。该协议是无状态的,因为它不希望发送方在发送标头之前等待接收方,也不希望接收方发回任何内容。接下来,让我们仔细看看协议的两个版本的实现。版本1在版本1中,代理标头由一系列US-ASCII编码字符串组成。此代理标头将在客户端和服务器建立连接并发送任何实际数据之前发送。我们先来看一个使用代理头的http请求的例子:PROXYTCP4192.168.0.1192.168.0.10212345443\r\nGET/HTTP/1.1\r\nHost:192.168.0.102\r\n\r\n上面的例子中,\r\n表示回车换行,是行尾的标志。代码向host:192.168.0.102发送一个HTTP请求,第一行的内容是使用的proxyheader。这到底是什么意思?第一个是字符串“PROXY”,表示这是一个代理协议头,而且是v1版本。后跟一个空格分隔符。然后是代理使用的INET协议和系列。对于v1版本,支持“TCP4”和“TCP6”。在上面的例子中,我们使用TCP4。如果要使用其他协议,可以将其设置为“UNKNOWN”。如果设置为“UNKNOWN”,直到CRLF的数据将被忽略。后跟一个空格分隔符。然后是网络层源的IP地址。根据选择的是TCP4还是TCP6,对应的源IP地址也有不同的表示。后跟一个空格分隔符。然后就是网络层目的地址的IP地址。根据选择的是TCP4还是TCP6,对应的源IP地址也有不同的表示。后跟一个空格分隔符。然后是TCP源的端口号,取值范围是0-65535。后跟一个空格分隔符。然后是TCP目的地址的端口号,取值范围是0-65535。紧随其后的是CRLF终止符。定义了这样一个v1版本的代理协议,是不是很简单?根据这个定义,我们可以很容易的计算出整个代理协议的最大长度。对于TC4,最大长度表示为:-TCP/IPv4:"PROXYTCP4255.255.255.255255.255.255.2556553565535\r\n"=>5+1+4+1+15+1+15+1+5+1+5+2=56个字符对于TCP6,最大长度表示为:-TCP/IPv6:"PROXYTCP6ffff:f...f:ffffffff:f...f:ffff6553565535\r\n"=>5+1+4+1+39+1+39+1+5+1+5+2=104个字符对于UNKNOWN,可能有以下最小和最大长度表示为:-未知连接(缩写形式):“PROXYUNKNOWN\r\n”=>5+1+7+2=15个字符-最坏情况(可选字段设置为0xff):“PROXYUNKNOWNffff:f...f:ffffffff:f...f:ffff6553565535\r\n"=>5+1+7+1+39+1+39+1+5+1+5+2=107个字符所以,一般来说,108个字符就足够了对于v1版本。Version2Version2主要采用二进制编码实现,对人的可读性不友好,但可以提高传输和解析效率。版本2的标头是以以下12个字节开头的块:\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A下一个字节(13个字节)是协议版本和命令。因为一个字节就是8位,用一个字节来保存就有点太奢侈了。所以把它分成两部分。高4位存储版本,其中版本号必须为“\x2”。低4位存储命令,有以下值:LOCAL(\x0):表示连接由代理自己发起,一般在代理向服务器发送健康检查时使用。PROXY(\x1):表示连接是由另一个节点发起的,是proxy代理请求。然后收件人必须使用协议块中提供的信息来获取原始地址。其他:其他命令需要丢弃,因为无法识别。下一个字节(14个字节)保存传输协议和地址族。其中,高4位保存地址族,低4位保存传输协议。地址族可能有以下值:AF_UNSPEC(0x0):表示不支持或未定义的协议。当发送方发送LOCAL命令或处理协议族时,可以使用该值。AF_INET(0x1):表示IPv4地址,占4bytes。AF_INET6(0x2):表示IPv6地址,占16bytes。AF_UNIX(0x3):表示unix地址,占用108字节。传输协议可能具有以下值:UNSPEC(0x0):未知协议类型。STREAM(0x1):使用SOCK_STREAM协议,如TCP或UNIX_STREAM。DGRAM(0x2):使用SOCK_DGRAM协议,如UDP或UNIX_DGRAM。结合低4位和高4位,可以得到如下值:UNSPEC(\x00)TCPoverIPv4(\x11)UDPoverIPv4(\x12)TCPoverIPv6(\x21)UDPoverIPv6(\x22)UNIX流(\x31)UNIX数据报(\x32)第15和第16字节表示的剩余字段的长度。综上所述,16字节的v2可以用如下结构表示:structproxy_hdr_v2{uint8_tsig[12];/*十六进制0D0A0D0A000D0A515549540A*/uint8_tver_cmd;/*协议版本和命令*/uint8_tfam;/*协议族和地址*/uint16_tlen;标题*/};从第17字节开始,是地址长度和端口号信息,可以用如下结构表示:unionproxy_addr{struct{/*forTCP/UDPoverIPv4,len=12*/uint32_tsrc_addr;uint32_tdst_addr;uint16_t源端口;uint16_tdst_port;}ipv4_地址;struct{/*对于基于IPv6的TCP/UDP,len=36*/uint8_tsrc_addr[16];uint8_tdst_addr[16];uint16_t源端口;uint16_tdst_port;}IPv6_地址;struct{/*对于AF_UNIX套接字,len=216*/uint8_tsrc_addr[108];uint8_tdst_addr[108];}unix_addr;};在V2版本中,头部除了地址信息外,还可以包含一些额外的扩展信息,称为Type-Length-Value(TLVvectors),格式如下:structpp2_tlv{uint8_ttype;uint8_tlength_hi;uint8_tlength_lo;uint8_t值[0];};和价值下面是目前支持的类型:#definePP2_TYPE_ALPN0x01#definePP2_TYPE_AUTHORITY0x02#definePP2_TYPE_CRC32C0x03#definePP2_TYPE_NOOP0x04#definePP2_TYPE_UNIQUE_ID0x05#definePP2_TYPE_SSL0x20#definePP2_SUBTYPE_SSL_VERSION0x21#definePP2_SUBTYPE_SSL_CN??0x22#definePP2_SUBTYPE_SSL_CIPHER0x23#definePP2_SUBTYPE_SSL_SIG_ALG0x24#definePP2_SUBTYPE_SSL_KEY_ALG0x25#definePP2_TYPE_NETNS0x30ProxyProtocol的使用上面也有提到,一个协议的好坏不仅与这个协议的定义有关,还与使用这个协议的软件数量有关。如果主流的代理软件没有使用你的代理协议,那么再好的协议也没有用。相反,如果每个人都在使用你的协议,那么无论协议定义多么糟糕,它都是主流协议。幸运的是,ProxyProtocol已经广泛应用于代理服务器行业。使用该协议的具体软件如下:ElasticLoadBalancing,AWS负载均衡器,自2013年7月起兼容Dovecot,POP/IMAP邮件服务器,自2.2.19版本起兼容exaproxy,正向和反向代理服务器,兼容gunicorn从1.0.0版本开始,pythonHTTP服务器,从0.15.0开始兼容haproxy,反向代理负载均衡器,从1.5-dev3开始兼容nginx,正向代理服务器,http服务器,从1.5.12开始兼容PerconaDB,数据库服务器,从5.6.25-73.0兼容stud,SSLoffloader,从第一个版本兼容stunnel,SSLoffloader,从4.45开始兼容apacheHTTPD,web服务器,在扩展模块myfixip中使用varnish,HTTP反向代理缓存,从版本开始4.1,基本上所有的主流服务器都兼容ProxyProtocol,所以我们可以把ProxyProtocol看成一个事实标准。总结在本文中,我们介绍了ProxyProtocol的底层定义,那么具体如何使用ProxyProtocol,能否实现自己的ProxyProtocol服务器呢?敬请关注。本文已收录于http://www.flydean.com/20-haproxy-protocol/最流行的解读,最深刻的干货,最简洁的教程,很多你不知道的小技巧等着你去探索!欢迎关注我的公众号:《程序那些事儿》,懂技术,更懂你!