wangbin
  • wangbin
  • 2019-08-04
  • IT

proxy protocol - 让haproxy反向代理的后端nginx拿到用户真实ip

一. 简介

之前使用awstats统计博客的访问数据统计得好好的。

后来用了haproxy在nginx之前做了反向代理,结果nginx的访问日志显示的ip都是127.0.0.1,一直想着怎么解决这个问题,最近终于解决了,这儿记录下。

如何解决的呢?答案就是:proxy protocol.

二. proxy protocol

proxy protocol是haproxy的作者Willy Tarreau于2010年开发和设计的一个Internet协议,通过为tcp添加一个很小的头信息,来方便的传递客户端信息(协议栈、源IP、目的IP、源端口、目的端口等),在网络情况复杂又需要获取用户真实IP时非常有用。

代理协议分为v1和v2两个版本,v1人类易读,v2是二进制格式,方便程序处理。proxy protocol是比较新的协议,但目前已经有很多软件支持,如haproxy、nginx、apache、squid、mysql等等,要使用proxy protocol需要两个角色sender和receiver,sender在与receiver之间建立连接后,会先发送一个带有客户信息的tcp header,因为更改了tcp协议,需receiver也支持proxy protocol,否则不能识别tcp包头,导致无法成功建立连接。

三. haproxy配置

我们这次的场景,haproxy的角色是sender,nginx的角色是receiver.

haproxy.cfg

global
    log /dev/log    local0
    log /dev/log    local1 notice
    chroot          /var/lib/haproxy
    pidfile         /var/run/haproxy.pid 
    # daemon方式运行,取消该注释
    # daemon

defaults
    log global
    mode    tcp
    option  tcplog  
    option  dontlognull
    maxconn 2000
    timeout connect  5000
    timeout client 500000
    timeout server 500000

frontend ssl
    mode tcp
    bind 0.0.0.0:443

    tcp-request inspect-delay 3s
    tcp-request content accept if { req.ssl_hello_type 1 }

    acl tls req.ssl_hello_type 1

    acl has_sni req.ssl_sni -m found

    use_backend nginx if tls has_sni

backend nginx
    mode tcp
    server webserver 127.0.0.1:50443 send-proxy

只需要在backend nginx中的server webserver 127.0.0.1:50443后面加上send-proxy就可以了,haproxy代理访问的时候,就会先把真实ip带上.

四. nginx配置

nginx的角色是receiver.

nginx配置的修改有两点

1.server配置修改

/usr/local/nginx/conf/vhosts/wangbin.io/wangbin.io.conf

# wangbin.io
# www.wangbin.io
server {

    listen       50443 ssl http2 proxy_protocol;
    listen       [::]:50443 ssl http2 proxy_protocol;

    server_name  wangbin.io;
    server_name  www.wangbin.io;

    # ssl
    ssl_certificate             /vps/save/certificate/acme/*.wangbin.io_ecc/fullchain.cer;
    ssl_certificate_key         /vps/save/certificate/acme/*.wangbin.io_ecc/*.wangbin.io.key;
    ssl_trusted_certificate     /vps/save/certificate/acme/*.wangbin.io/fullchain.cer;

    # ecc
    ssl_certificate             /vps/save/certificate/acme/*.wangbin.io_ecc/fullchain.cer;
    ssl_certificate_key         /vps/save/certificate/acme/*.wangbin.io_ecc/*.wangbin.io.key;

    # log
    access_log  logs/wangbin.io/wangbin.io/access-wangbin.io.log siyou325;
    error_log   logs/wangbin.io/wangbin.io/error.log;

    # header
    add_header  X-Robots-Tag    "";

    root        /vps/hosts/www;
    index       index.html index.htm index.php;

    location ~ \.(jpg|png|gif|js|css|swf|flv|ico)$ {
        expires 12h;
    }

    location / {
        if (!-e $request_filename) {
            rewrite ^(.*)$ /index.php?$1 last ;
            break;
        }
    }

    location ~* ^/(doc|logs|app|sys)/ {
        deny all;
    }

    location ~ .*\.(php|php5)?$ {
        fastcgi_connect_timeout         300;
        fastcgi_send_timeout            300;
        fastcgi_read_timeout            300;
        fastcgi_pass                    127.0.0.1:9000;
        fastcgi_index                   index.php;
        fastcgi_param                   SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include                         fastcgi_params;
        fastcgi_param                   QUERY_STRING     $query_string;
        fastcgi_param                   REQUEST_METHOD   $request_method;
        fastcgi_param                   CONTENT_TYPE     $content_type;
        fastcgi_param                   CONTENT_LENGTH   $content_length;
    }

    # 禁止访问.svn目录,防止svn信息泄漏,必加项
    location ~ ^(.*)\/\.svn\/ {
        deny all;
    }

}   

修改的地方

listen       50443 ssl http2 proxy_protocol;
listen       [::]:50443 ssl http2 proxy_protocol;

listen后面加上了proxy_protocol.

需要注意的是, proxy_protocol是针对端口生效的,如果一个端口支持proxy_protocol,则该端口所有server的域名都会支持.

2. log_format的修改

之前是

log_format                  siyou325    '$remote_addr - $remote_user [$time_local] "$request" '
                                        '$status $body_bytes_sent "$http_referer" '
                                        '"$http_user_agent" $http_x_forwarded_for';

修改为

log_format                  siyou325    '$proxy_protocol_addr - $remote_user [$time_local] "$request" '
                                        '$status $body_bytes_sent "$http_referer" '
                                        '"$http_user_agent" $http_x_forwarded_for';

将$remote_addr改为了$proxy_protocol_addr,之前$remote_addr一直是127.0.0.1,现在$proxy_protocol_addr是用户的真实地址了.

五. 另外两种解决方法

其实解决这个问题有3种方法

  1. option forwardfor
  2. send-proxy
  3. source 0.0.0.0 usesrc clientip

option forwardfor

最简单,但是只支持7层http协议。

send-proxy

就是本文介绍的proxy protocol了,既可以工作在4层tcp协议上,也可以工作在7层http协议上。

source 0.0.0.0 usesrc clientip

这个实现最复杂,需要iptables、tproxy、ip rule、 ip route命令的知识,试了好久也没有成功,可能和我只在一台机器上部署有关,后面有时间再折腾。

六. 结尾

proxy protocol解决问题的方式真是很优雅,给力!

参考:

  1. https://www.cnblogs.com/smail-bao/p/6693630.html
  2. https://www.52os.net/articles/PROXY_protocol_pass_client_ip.html