nginx的proxy protocol实现
概述
代理协议(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字节地址长度字段(网络字节序),指接下来剩余的报文长度
- 12字节的固定signature。
整个报文头在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头。