nginx 的透明代理实现
概述
作为代理服务器,把客户端真实ip传递到后端有时候是需要的。 Haproxy作者设计了代理协议、在7层http协议又可以通过header头传递。详细请看《nginx的proxy protocol实现》。而在Linux 2.6.24以后,socket增加了一个选项IP_TRANSPARENT可以接受目的地址没有配置的数据包,也可以发送原地址不是本地地址的数据包。可以通过该特性实现4层以上的透明代理。注意:需要设置路由。
代码实现
nginx通过指令 proxy_bind $remote_addr transparent; 让代理服务器绑定真实的客户端ip向上游发起请求连接。
指令解析
nginx 在http模块和stream均支持该指令,逻辑基本相似。来看下stream模块的指令解析。
static char *
ngx_stream_proxy_bind(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_stream_proxy_srv_conf_t *pscf = conf;
ngx_int_t rc;
ngx_str_t *value;
ngx_stream_complex_value_t cv;
ngx_stream_upstream_local_t *local;
ngx_stream_compile_complex_value_t ccv;
if (pscf->local != NGX_CONF_UNSET_PTR) {
return "is duplicate";
}
value = cf->args->elts;
if (cf->args->nelts == 2 && ngx_strcmp(value[1].data, "off") == 0) {
pscf->local = NULL;
return NGX_CONF_OK;
}
ngx_memzero(&ccv, sizeof(ngx_stream_compile_complex_value_t));
ccv.cf = cf;
ccv.value = &value[1];
ccv.complex_value = &cv;
if (ngx_stream_compile_complex_value(&ccv) != NGX_OK) {
return NGX_CONF_ERROR;
}
local = ngx_pcalloc(cf->pool, sizeof(ngx_stream_upstream_local_t));
if (local == NULL) {
return NGX_CONF_ERROR;
}
pscf->local = local;
if (cv.lengths) {
local->value = ngx_palloc(cf->pool, sizeof(ngx_stream_complex_value_t));
if (local->value == NULL) {
return NGX_CONF_ERROR;
}
*local->value = cv;
} else {
local->addr = ngx_palloc(cf->pool, sizeof(ngx_addr_t));
if (local->addr == NULL) {
return NGX_CONF_ERROR;
}
rc = ngx_parse_addr_port(cf->pool, local->addr, value[1].data,
value[1].len);
switch (rc) {
case NGX_OK:
local->addr->name = value[1];
break;
case NGX_DECLINED:
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"invalid address \"%V\"", &value[1]);
/* fall through */
default:
return NGX_CONF_ERROR;
}
}
if (cf->args->nelts > 2) {
if (ngx_strcmp(value[2].data, "transparent") == 0) {
#if (NGX_HAVE_TRANSPARENT_PROXY)
ngx_core_conf_t *ccf;
ccf = (ngx_core_conf_t *) ngx_get_conf(cf->cycle->conf_ctx,
ngx_core_module);
ccf->transparent = 1;
local->transparent = 1;
#else
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"transparent proxying is not supported "
"on this platform, ignored");
#endif
} else {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"invalid parameter \"%V\"", &value[2]);
return NGX_CONF_ERROR;
}
}
return NGX_CONF_OK;
}
请求处理
// ngx_stream_proxy_handler函数调用
// local 是proxy_bind指令配置内容。
// 设置上游需要绑定的本地地址信息
static ngx_int_t
ngx_stream_proxy_set_local(ngx_stream_session_t *s, ngx_stream_upstream_t *u,
ngx_stream_upstream_local_t *local)
{
ngx_int_t rc;
ngx_str_t val;
ngx_addr_t *addr;
if (local == NULL) {
u->peer.local = NULL;
return NGX_OK;
}
#if (NGX_HAVE_TRANSPARENT_PROXY)
// 支持透明代理
u->peer.transparent = local->transparent;
#endif
if (local->value == NULL) {
u->peer.local = local->addr;
return NGX_OK;
}
// 解析变量的值
// $remote_addr 表示真实的客户端的IP地址
if (ngx_stream_complex_value(s, local->value, &val) != NGX_OK) {
return NGX_ERROR;
}
if (val.len == 0) {
return NGX_OK;
}
addr = ngx_palloc(s->connection->pool, sizeof(ngx_addr_t));
if (addr == NULL) {
return NGX_ERROR;
}
rc = ngx_parse_addr_port(s->connection->pool, addr, val.data, val.len);
if (rc == NGX_ERROR) {
return NGX_ERROR;
}
if (rc != NGX_OK) {
ngx_log_error(NGX_LOG_ERR, s->connection->log, 0,
"invalid local address \"%V\"", &val);
return NGX_OK;
}
addr->name = val;
u->peer.local = addr;
return NGX_OK;
}
// ngx_event_connect.c
// 向上游发起连接
//
ngx_int_t
ngx_event_connect_peer(ngx_peer_connection_t *pc)
{
int rc, type;
#if (NGX_HAVE_IP_BIND_ADDRESS_NO_PORT || NGX_LINUX)
in_port_t port;
#endif
ngx_int_t event;
ngx_err_t err;
ngx_uint_t level;
ngx_socket_t s;
ngx_event_t *rev, *wev;
ngx_connection_t *c;
// 获取一个上游服务器IP
rc = pc->get(pc, pc->data);
if (rc != NGX_OK) {
return rc;
}
type = (pc->type ? pc->type : SOCK_STREAM);
s = ngx_socket(pc->sockaddr->sa_family, type, 0);
ngx_log_debug2(NGX_LOG_DEBUG_EVENT, pc->log, 0, "%s socket %d",
(type == SOCK_STREAM) ? "stream" : "dgram", s);
if (s == (ngx_socket_t) -1) {
ngx_log_error(NGX_LOG_ALERT, pc->log, ngx_socket_errno,
ngx_socket_n " failed");
return NGX_ERROR;
}
c = ngx_get_connection(s, pc->log);
if (c == NULL) {
if (ngx_close_socket(s) == -1) {
ngx_log_error(NGX_LOG_ALERT, pc->log, ngx_socket_errno,
ngx_close_socket_n "failed");
}
return NGX_ERROR;
}
c->type = type;
if (pc->rcvbuf) {
if (setsockopt(s, SOL_SOCKET, SO_RCVBUF,
(const void *) &pc->rcvbuf, sizeof(int)) == -1)
{
ngx_log_error(NGX_LOG_ALERT, pc->log, ngx_socket_errno,
"setsockopt(SO_RCVBUF) failed");
goto failed;
}
}
if (ngx_nonblocking(s) == -1) {
ngx_log_error(NGX_LOG_ALERT, pc->log, ngx_socket_errno,
ngx_nonblocking_n " failed");
goto failed;
}
if (pc->local) {
#if (NGX_HAVE_TRANSPARENT_PROXY)
if (pc->transparent) {
// 设置IP_TRANSPARENT,允许bind非本地IP地址
if (ngx_event_connect_set_transparent(pc, s) != NGX_OK) {
goto failed;
}
}
#endif
#if (NGX_HAVE_IP_BIND_ADDRESS_NO_PORT || NGX_LINUX)
port = ngx_inet_get_port(pc->local->sockaddr);
#endif
#if (NGX_HAVE_IP_BIND_ADDRESS_NO_PORT)
if (pc->sockaddr->sa_family != AF_UNIX && port == 0) {
static int bind_address_no_port = 1;
if (bind_address_no_port) {
if (setsockopt(s, IPPROTO_IP, IP_BIND_ADDRESS_NO_PORT,
(const void *) &bind_address_no_port,
sizeof(int)) == -1)
{
err = ngx_socket_errno;
if (err != NGX_EOPNOTSUPP && err != NGX_ENOPROTOOPT) {
ngx_log_error(NGX_LOG_ALERT, pc->log, err,
"setsockopt(IP_BIND_ADDRESS_NO_PORT) "
"failed, ignored");
} else {
bind_address_no_port = 0;
}
}
}
}
#endif
#if (NGX_LINUX)
if (pc->type == SOCK_DGRAM && port != 0) {
int reuse_addr = 1;
if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR,
(const void *) &reuse_addr, sizeof(int))
== -1)
{
ngx_log_error(NGX_LOG_ALERT, pc->log, ngx_socket_errno,
"setsockopt(SO_REUSEADDR) failed");
goto failed;
}
}
#endif
// 绑定向上游发起请求的ip地址。可能是非本地地址。
if (bind(s, pc->local->sockaddr, pc->local->socklen) == -1) {
ngx_log_error(NGX_LOG_CRIT, pc->log, ngx_socket_errno,
"bind(%V) failed", &pc->local->name);
goto failed;
}
}
。。。。。。
return NGX_OK;
failed:
ngx_close_connection(c);
pc->connection = NULL;
return NGX_ERROR;
}
#if (NGX_HAVE_TRANSPARENT_PROXY)
static ngx_int_t
ngx_event_connect_set_transparent(ngx_peer_connection_t *pc, ngx_socket_t s)
{
int value;
value = 1;
#if defined(SO_BINDANY)
if (setsockopt(s, SOL_SOCKET, SO_BINDANY,
(const void *) &value, sizeof(int)) == -1)
{
ngx_log_error(NGX_LOG_ALERT, pc->log, ngx_socket_errno,
"setsockopt(SO_BINDANY) failed");
return NGX_ERROR;
}
#else
switch (pc->local->sockaddr->sa_family) {
case AF_INET:
#if defined(IP_TRANSPARENT)
if (setsockopt(s, IPPROTO_IP, IP_TRANSPARENT,
(const void *) &value, sizeof(int)) == -1)
{
ngx_log_error(NGX_LOG_ALERT, pc->log, ngx_socket_errno,
"setsockopt(IP_TRANSPARENT) failed");
return NGX_ERROR;
}
#elif defined(IP_BINDANY)
if (setsockopt(s, IPPROTO_IP, IP_BINDANY,
(const void *) &value, sizeof(int)) == -1)
{
ngx_log_error(NGX_LOG_ALERT, pc->log, ngx_socket_errno,
"setsockopt(IP_BINDANY) failed");
return NGX_ERROR;
}
#endif
break;
#if (NGX_HAVE_INET6)
case AF_INET6:
#if defined(IPV6_TRANSPARENT)
if (setsockopt(s, IPPROTO_IPV6, IPV6_TRANSPARENT,
(const void *) &value, sizeof(int)) == -1)
{
ngx_log_error(NGX_LOG_ALERT, pc->log, ngx_socket_errno,
"setsockopt(IPV6_TRANSPARENT) failed");
return NGX_ERROR;
}
#elif defined(IPV6_BINDANY)
if (setsockopt(s, IPPROTO_IPV6, IPV6_BINDANY,
(const void *) &value, sizeof(int)) == -1)
{
ngx_log_error(NGX_LOG_ALERT, pc->log, ngx_socket_errno,
"setsockopt(IPV6_BINDANY) failed");
return NGX_ERROR;
}
#else
ngx_log_error(NGX_LOG_ALERT, pc->log, 0,
"could not enable transparent proxying for IPv6 "
"on this platform");
return NGX_ERROR;
#endif
break;
#endif /* NGX_HAVE_INET6 */
}
#endif /* SO_BINDANY */
return NGX_OK;
}
#endif
配置
- 在代理服务器的配置:
user root;
...
http{
...
server {
listen 8080;
...
location /tproxy/ {
...
proxy_bind $remote_addr transparent;
proxy_pass http://172.17.0.2:8081/;
}
location /proxy/ {
proxy_pass http://172.17.0.2:8081/;
}
}
}
ip rule add fwmark 1 lookup 100 # 对标记为1的数据使用路由表100的路由。
ip route add local 0.0.0.0/0 dev lo table 100 #在100路由表添加规则:所有流量引导到本地lo网口
iptables -t mangle -A PREROUTING -p tcp -s 172.17.0.0/16 --sport 8081 -j MARK --set-xmark 0x1/0xffffffff # 对上游回复的流量打上标记
- 在上游服务器的配置:
# route del default gw 10.0.2.2
route add default gw 172.16.0.1
总结
nginx在1.11 以后的版本中,在proxy_bind 指令支持了transparent参数。支持上游发起连接的ip地址为非本地地址。 通过配置$remote_addr 取得客户端的真实ip并配置transparent参数,nginx向上游发起连接的源地址就成为真实客户端IP了。 因该IP并不在本机配置,上游服务器需要配置路由,把代理服务器作为默认网关。并需要把上游回来的包引导到nginx进程上,需要配置iptables规则。
参考:https://www.nginx.com/blog/ip-transparency-direct-server-return-nginx-plus-transparent-proxy/
http://www.fairysoftware.com/tproxy_tou_ming_dai_li.html
up
不知有没有试过让 listen 的 socket 也开启 IP_TRANSPARENT,用来获取请求的原始目标 IP?
例如 DNS 负载均衡的场合,一个域名对应多个 IP。直接 proxy_pass $host 相当于要再次做域名解析了,没法直接用客户端请求的真实目标 IP。
您好! 我看许多都是下面这样做法,
1-启用tproxy启动。
2-规则如下。
iptables –t mangle –N DIVERT
iptables –t mangle –A PREROUTING –p tcp –m socket –j DIVERT
iptables –t mangle –A DIVERT –j MARK --set-mark 1
iptables –t mangle –A DIVERT –j ACCEPT
ip rule add fwmark 1 lookup 100
ip route add local 0.0.0.0/0 dev lo table 100
我看你说:并需要把上游回来的包引导到nginx进程上,需要配置iptables规则。这个需要咋配置呢?nginx之前访问后端的socket不能直接获取数据包? 如何配置iptables规则? 谢谢
作者您好,我的代理服务器和上游服务器是直连的,上游没有配置默认网关的情况下,在代理服务器上抓包也能抓到上游响应的包,按照上述配置后响应流量似乎并没有正常引入到nginx,最终nginx向客户端响应504 Gateway Time-out,这是怎么一回事呢