leevis.com icon indicating copy to clipboard operation
leevis.com copied to clipboard

nginx的proxy protocol实现

Open vislee opened this issue 7 years ago • 0 comments

概述

代理协议(proxy protocol)是haproxy的作者设计的一个协议,通过在tcp流的开始添加一小段内容传递客户端和代理的连接信息。格式如下:

PROXY_STRING + single space + INET_PROTOCOL + single space + CLIENT_IP + single space + PROXY_IP + single space + CLIENT_PORT + single space + PROXY_PORT + "\r\n"

例如 198.168.0.1:34343(客户端) -> 192.16.1.1:80(4层代理)->nginx 那么4层代理就会把这个连接的信息转给后端的nginx或者Apache等服务器。具体协议是: PROXY TCP4 198.168.0.1 192.16.1.1 34343 80\r\n 后端的nginx拿到这段协议解析了以后,就可以知道真实的客户端IP和端口了。

更新V2版本

大概在2014年,proxy protocol更新了V2版本,是非ASCII码协议,该协议报文包含报文头和报文体。

  • 其中报文头格式:

    • 12字节的固定signature。 \x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A

    在nginx中定义为:static const u_char signature[] = "\r\n\r\n\0\r\nQUIT\n";(实际上就是\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A)

    • 4bits 协议版本号: \x2 : v2

    • 4bits cmd: \x0 : LOCAL \x1 : PROXY

    • 4bits 地址族 \x0 : AF_UNSPEC \x1 : AF_INET \x2 : AF_INET6 \x3 : AF_UNIX

    • 4bits transport protocol \x0 : UNSPEC \x1 : STREAM \x2 : DGRAM

    • 2字节地址长度字段(网络字节序),指接下来剩余的报文长度

整个报文头在nginx中定义的结构体

typedef struct {
    u_char                                  signature[12];
    u_char                                  version_command;
    u_char                                  family_transport;
    u_char                                  len[2];
} ngx_proxy_protocol_header_t;

  • ipv4报文体格式:

    • 4字节源地址
    • 4字节目标地址
    • 2字节源端口
    • 2字节目标端口

在nginx中定义的结构体:

typedef struct {
    u_char                                  src_addr[4];
    u_char                                  dst_addr[4];
    u_char                                  src_port[2];
    u_char                                  dst_port[2];
} ngx_proxy_protocol_inet_addrs_t;

  • ipv6报文体格式:

    • 16字节源地址
    • 16字节目标地址
    • 2字节源端口
    • 2字节目标端口

在nginx中定义的结构体

typedef struct {
    u_char                                  src_addr[16];
    u_char                                  dst_addr[16];
    u_char                                  src_port[2];
    u_char                                  dst_port[2];
} ngx_proxy_protocol_inet6_addrs_t;

支持V2版tlvs解析

2022年10月支持了V2版tlvs的解析。 https://github.com/nginx/nginx/commit/50e3ff8a006100feaa0666cf5e4f9fd5fdcfb721

源码

nginx实现在ngx_proxy_protocol.h|c文件中。提供了两个函数,ngx_proxy_protocol_read解析代理协议,ngx_proxy_protocol_write组装代理协议。

// 解析v2协议


static u_char *
ngx_proxy_protocol_v2_read(ngx_connection_t *c, u_char *buf, u_char *last)
{
    u_char                             *end;
    size_t                              len;
    socklen_t                           socklen;
    ngx_uint_t                          version, command, family, transport;
    ngx_sockaddr_t                      sockaddr;
    ngx_proxy_protocol_header_t        *header;
    ngx_proxy_protocol_inet_addrs_t    *in;
#if (NGX_HAVE_INET6)
    ngx_proxy_protocol_inet6_addrs_t   *in6;
#endif

    header = (ngx_proxy_protocol_header_t *) buf;

    // buf指向报文体
    buf += sizeof(ngx_proxy_protocol_header_t);

    // 4bit版本, 目前只能等于2
    version = header->version_command >> 4;

    if (version != 2) {
        ngx_log_error(NGX_LOG_ERR, c->log, 0,
                      "unknown PROXY protocol version: %ui", version);
        return NULL;
    }

    // 报文体长度
    len = ngx_proxy_protocol_parse_uint16(header->len);

    if ((size_t) (last - buf) < len) {
        // 读到的内容太少
        ngx_log_error(NGX_LOG_ERR, c->log, 0, "header is too large");
        return NULL;
    }

    // 报文体结尾
    end = buf + len;

    // 4bit command,仅支持PROXY,所以只能等于1
    command = header->version_command & 0x0f;

    /* only PROXY is supported */
    if (command != 1) {
        ngx_log_debug1(NGX_LOG_DEBUG_CORE, c->log, 0,
                       "PROXY protocol v2 unsupported command %ui", command);
        return end;
    }

    // 4bit 传输协议,仅支持STREAM,所以只能等于1
    transport = header->family_transport & 0x0f;

    /* only STREAM is supported */
    if (transport != 1) {
        ngx_log_debug1(NGX_LOG_DEBUG_CORE, c->log, 0,
                       "PROXY protocol v2 unsupported transport %ui",
                       transport);
        return end;
    }

    // 4bit family
    family = header->family_transport >> 4;

    switch (family) {

    // ipv4
    case NGX_PROXY_PROTOCOL_AF_INET:
        // 判断报文体长度
        if ((size_t) (end - buf) < sizeof(ngx_proxy_protocol_inet_addrs_t)) {
            return NULL;
        }

        // 报文体映射成ngx的结构体
        in = (ngx_proxy_protocol_inet_addrs_t *) buf;

        sockaddr.sockaddr_in.sin_family = AF_INET;
        sockaddr.sockaddr_in.sin_port = 0;
        memcpy(&sockaddr.sockaddr_in.sin_addr, in->src_addr, 4);

        c->proxy_protocol_port = ngx_proxy_protocol_parse_uint16(in->src_port);

        socklen = sizeof(struct sockaddr_in);

        buf += sizeof(ngx_proxy_protocol_inet_addrs_t);

        break;

#if (NGX_HAVE_INET6)

    // ipv6
    case NGX_PROXY_PROTOCOL_AF_INET6:

        if ((size_t) (end - buf) < sizeof(ngx_proxy_protocol_inet6_addrs_t)) {
            return NULL;
        }

        in6 = (ngx_proxy_protocol_inet6_addrs_t *) buf;

        sockaddr.sockaddr_in6.sin6_family = AF_INET6;
        sockaddr.sockaddr_in6.sin6_port = 0;
        memcpy(&sockaddr.sockaddr_in6.sin6_addr, in6->src_addr, 16);

        c->proxy_protocol_port = ngx_proxy_protocol_parse_uint16(in6->src_port);

        socklen = sizeof(struct sockaddr_in6);

        buf += sizeof(ngx_proxy_protocol_inet6_addrs_t);

        break;

#endif

    default:
        ngx_log_debug1(NGX_LOG_DEBUG_CORE, c->log, 0,
                       "PROXY protocol v2 unsupported address family %ui",
                       family);
        return end;
    }

    c->proxy_protocol_addr.data = ngx_pnalloc(c->pool, NGX_SOCKADDR_STRLEN);
    if (c->proxy_protocol_addr.data == NULL) {
        return NULL;
    }

    c->proxy_protocol_addr.len = ngx_sock_ntop(&sockaddr.sockaddr, socklen,
                                               c->proxy_protocol_addr.data,
                                               NGX_SOCKADDR_STRLEN, 0);

    ngx_log_debug2(NGX_LOG_DEBUG_CORE, c->log, 0,
                   "PROXY protocol v2 address: %V %d", &c->proxy_protocol_addr,
                   c->proxy_protocol_port);

    /*
    if (buf < end) {
        ngx_log_debug1(NGX_LOG_DEBUG_CORE, c->log, 0,
                       "PROXY protocol v2 %z bytes of tlv ignored", end - buf);
    }
    */

    // 2022年10月
    if (buf < end) {
        pp->tlvs.data = ngx_pnalloc(c->pool, end - buf);
        if (pp->tlvs.data == NULL) {
            return NULL;
        }

        ngx_memcpy(pp->tlvs.data, buf, end - buf);
        pp->tlvs.len = end - buf;
    }

    return end;
}


// 解析代理协议, 只解析客户端IP和端口
u_char *
ngx_proxy_protocol_read(ngx_connection_t *c, u_char *buf, u_char *last)
{
    size_t     len;
    u_char     ch, *p, *addr, *port;
    ngx_int_t  n;

    // v2 固定的signature
    static const u_char signature[] = "\r\n\r\n\0\r\nQUIT\n";

    p = buf;
    len = last - buf;

    //  通过signature判断是否是v2协议。
    if (len >= sizeof(ngx_proxy_protocol_header_t)
        && memcmp(p, signature, sizeof(signature) - 1) == 0)
    {
        return ngx_proxy_protocol_v2_read(c, buf, last);
    }

    // 代理协议必须以PROXY开始
    if (len < 8 || ngx_strncmp(p, "PROXY ", 6) != 0) {
        goto invalid;
    }

    p += 6;
    len -= 6;

    // 未知的协议,可能客户端和代理通过socket域连接
    if (len >= 7 && ngx_strncmp(p, "UNKNOWN", 7) == 0) {
        ngx_log_debug0(NGX_LOG_DEBUG_CORE, c->log, 0,
                       "PROXY protocol unknown protocol");
        p += 7;
        goto skip;
    }

    // 代理协议只支持tcp4和tcp6
    if (len < 5 || ngx_strncmp(p, "TCP", 3) != 0
        || (p[3] != '4' && p[3] != '6') || p[4] != ' ')
    {
        goto invalid;
    }

    p += 5;
    addr = p;

    for ( ;; ) {
        if (p == last) {
            goto invalid;
        }

        ch = *p++;

        if (ch == ' ') {
            break;
        }

        if (ch != ':' && ch != '.'
            && (ch < 'a' || ch > 'f')
            && (ch < 'A' || ch > 'F')
            && (ch < '0' || ch > '9'))
        {
            goto invalid;
        }
    }

    len = p - addr - 1;
    // 客户端ip
    c->proxy_protocol_addr.data = ngx_pnalloc(c->pool, len);

    if (c->proxy_protocol_addr.data == NULL) {
        return NULL;
    }

    ngx_memcpy(c->proxy_protocol_addr.data, addr, len);
    c->proxy_protocol_addr.len = len;

    for ( ;; ) {
        if (p == last) {
            goto invalid;
        }

        if (*p++ == ' ') {
            break;
        }
    }

    port = p;

    for ( ;; ) {
        if (p == last) {
            goto invalid;
        }

        if (*p++ == ' ') {
            break;
        }
    }

    len = p - port - 1;

    n = ngx_atoi(port, len);

    if (n < 0 || n > 65535) {
        goto invalid;
    }
    // 客户端端口
    c->proxy_protocol_port = (in_port_t) n;

    ngx_log_debug2(NGX_LOG_DEBUG_CORE, c->log, 0,
                   "PROXY protocol address: %V %i", &c->proxy_protocol_addr, n);

skip:

    for ( /* void */ ; p < last - 1; p++) {
        if (p[0] == CR && p[1] == LF) {
            return p + 2;
        }
    }

invalid:

    ngx_log_error(NGX_LOG_ERR, c->log, 0,
                  "broken header: \"%*s\"", (size_t) (last - buf), buf);

    return NULL;
}


// 拼接代理协议字符串
u_char *
ngx_proxy_protocol_write(ngx_connection_t *c, u_char *buf, u_char *last)
{
    ngx_uint_t  port, lport;

    if (last - buf < NGX_PROXY_PROTOCOL_MAX_HEADER) {
        return NULL;
    }

    if (ngx_connection_local_sockaddr(c, NULL, 0) != NGX_OK) {
        return NULL;
    }

    switch (c->sockaddr->sa_family) {

    case AF_INET:
        buf = ngx_cpymem(buf, "PROXY TCP4 ", sizeof("PROXY TCP4 ") - 1);
        break;

#if (NGX_HAVE_INET6)
    case AF_INET6:
        buf = ngx_cpymem(buf, "PROXY TCP6 ", sizeof("PROXY TCP6 ") - 1);
        break;
#endif

    default:
        return ngx_cpymem(buf, "PROXY UNKNOWN" CRLF,
                          sizeof("PROXY UNKNOWN" CRLF) - 1);
    }

    buf += ngx_sock_ntop(c->sockaddr, c->socklen, buf, last - buf, 0);

    *buf++ = ' ';

    buf += ngx_sock_ntop(c->local_sockaddr, c->local_socklen, buf, last - buf,
                         0);

    port = ngx_inet_get_port(c->sockaddr);
    lport = ngx_inet_get_port(c->local_sockaddr);

    return ngx_slprintf(buf, last, " %ui %ui" CRLF, port, lport);
}

总结

后端服务器有时候需要根据客户端ip做一些限制操作,而四层的代理协议可以把真实的客户端ip传递到后端。七层http是通过在header添加 X-Forwarded-For或X-Real-IP的字段实现传递的,当然七层http协议也可以在tcp流的开始用代理协议传递客户端地址信息,这在nginx里也是支持的。

代理协议的客户端地址信息,除了提供变量可以访问,realip模块也使用该信息替换了connection上的客户端IP结构。

在nginx中,四层代理提供了解析和拼接代理协议。而七层协议仅提供了解析代理协议,没有提供拼接代理协议,如果要向上游传递真实客户端信息只能通过header头。

vislee avatar Feb 08 '18 09:02 vislee