关于java:网络协议之haproxy的Proxy-Protocol代理协议

23次阅读

共计 5610 个字符,预计需要花费 15 分钟才能阅读完成。

简介

代理大家应该都很相熟了,比拟闻名的像是 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/

最艰深的解读,最粗浅的干货,最简洁的教程,泛滥你不晓得的小技巧等你来发现!

欢送关注我的公众号:「程序那些事」, 懂技术,更懂你!

正文完
 0