Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

nginx的proxy protocol实现 #141

Open
vislee opened this issue Feb 8, 2018 · 0 comments
Open

nginx的proxy protocol实现 #141

vislee opened this issue Feb 8, 2018 · 0 comments
Labels

Comments

@vislee
Copy link
Owner

vislee commented Feb 8, 2018

概述

代理协议(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的解析。
nginx/nginx@50e3ff8

源码

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 vislee changed the title nginx的proxy protocol nginx的proxy protocol实现 Feb 8, 2018
@vislee vislee added the nginx label Feb 12, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant