nginx 解决首页跳转问题

nginx和tomcat负载均衡


比如 www.csdn.net 网站后面有 2个tomcat。
配置负载均衡:

upstream csdn-tomcat{
    server 192.168.100.101:8080;
    server 192.168.100.102:8080;
}
server {
  listen 80;
  server_name  www.csdn.net csdn.net;
  index  index.html;
  location  / {
    if ( $request_uri = "/" ) {
        rewrite "/" http://www.csdn.net/index.html break;
    }
    proxy_pass http://csdn-tomcat$request_uri;
  }
  # 301 redirect:
  location /blog/index.html {
    return 301 http://www.iteye.com$request_uri;
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

研究好半天,终于解决了。
首先假设首页上面是一个静态的html。
当用户没有直接输入 www.csdn.net的时候进行 301 跳转。
引导用户到 www.csdn.net/index.html 首页。
其他动态请求打到tomcat上面。
这样的在nginx上面直接做了301 跳转。

这样解决的是问题是由于tomcat 是用spring做的。
后缀成.html了,没有办法区分tomcat 和 普通html了。
要是tomcat 的后缀成.do就好办了。

主要是为了减轻 tomcat的压力。将html css image 都交给nginx去处理。
但是上线的时候比较麻烦,分开上线。

——————— 本文来自 freewebsys 的CSDN 博客 ,全文地址请点击:https://blog.csdn.net/freewebsys/article/details/51277923?utm_source=copy

Nginx通过二级目录(路径)映射不同的反向代理,规避IP+端口访问

这是我上一家公司的案例总结,发现躺在草稿箱好几个月了,今天得空就整理发布一下。

先说一下开发那边提来的 2 个 case:

①、同一个域名需要反向代理到前台和后台(不同机器和端口);

②、需要采用 IP+端口的模式,嵌入到 APP 作为 DNS 污染后的备选方案。

Nginx通过二级目录(路径)映射不同的反向代理,规避IP+端口访问

对于第①个问题,很好解决:通过区分二级目录来反代不同的节点即可,所以代码类似如下:

如上配置即可实现通过一个域名来反代不同的后端节点,用到的思路就是匹配二级目录来反代。

对于第②个问题,可能粗略一看,还没理解是个啥意思吧!

其实就是现在业界流行的一种防 DNS 污染的解决方案之一:手机 APP 里面除了通过域名来获取数据,还会额外嵌入一些备用的 IP。APP 在获取数据时,会先通过域名向服务器发起一个简单的校验请求,如果得到的不是预期数据,说明该网络环境下的 DNS 已被污染,比如被运营商劫持,请求 A 内容却给你展示 B 内容!这时候,APP 将会启动备用预案,通过 IP 的方式来请求数据!很明显,这个做法可以有效避免恶心的 DNS 劫持了(看完这段是不是有所收获呢?)。

做法很简单,就是在 APP 中集成多个 IP 和端口作为备用的访问途径。

当开发 GG 找到我,提出的需求是:

需要实现公网 IP+端口来访问,比如邮件 API 使用 http://192.168.1.10:125

Ps:公网服务器是多线的,那么就有多个 IP,本文假设电信是 192.168.1.10,联通是 192.168.2.10,移动是 192.168.3.10 等

说白了就是要用端口来区分不同的 API,此时如果我不深究,顺手可能会写出如下配置:

粗略一看,确实是可以实现开发 GG 的要求啊!再仔细一想,你会发现如此做法会开放越来越多的端口!运维成本以及辨识度低还只是其次,咱说好的安全第一呢?

经过思考和测试,我写出的最终配置如下:

最终实现的效果就是:你要通过 IP 请求邮件 API,只要请求 http://192.168.1.1/mail_api/ 即可,而不需要开放多余端口。而且,后续要新增更多 API,只需要定义不同的二级路径即可,这些二级路径的辨识度可比端口要好得多!

Ps:正如代码中的注释,示例代码只用了一个 DemoBackend 节点配置,为的是分享另一个小技巧:当后端节点承载了多个站点而且都是监听 80 端口时(比如某些小公司同一个 IIS 服务器部署了 N 个站点),反向代理中的 proxy_set_header 参数,可以自定义传递一个 host 域名给后端节点,从而正确响应预期内容!

这段解释有点无力,还是拿实际例子举例吧!

我之前供职的公司节点用的是 IIS 服务器,前端用 Nginx 反向代理,IIS 服务器上有多个站点,站点之间部分会通过 rewrite 规则联系起来。

打个比方:比如 A 网站有个专题内容(www.a.com/zt/)是通过 IIS 伪静态映射到了 B 网站(content.b.com)。也就是访问到 http://www.a.com/zt/,其实最后是通过 A 网站映射到了 B 网站上面。

后来发现 IIS 有个伪静态 BUG,会经常奔溃,就要我用前端的 Nginx 来实现直接映射,而不再走 IIS 的 A 网站中转。

那么这个需求就正好用到了 proxy_set_header 技巧,一看就懂:

很明显,通过传递自定义域名,就可以实现通过 A 网站访问 Nginx,返回 B 网站内容,和反向代理谷歌的原理是一致的。

当然,上文为了实现 IP 和域名都可以访问,这个 proxy_set_header 设置也是必须的。说白了就是在反代过程中,对后端服务器伪装(传递)了一个自定域名,让后端响应该域名预期内容。

当然,在之前张戈博客分享的《分享几个 WordPress 本地缓存 gravatar 评论头像的方案》一文中也用到了这个技巧,感兴趣的朋友可以前往查看。

本文分享的经验,其实比较简单,主要就是通过不同路径来反代不同的目标。估计很多大拿早就用烂了吧!不过值得注意的是,通过自定义路径反代,需要注意 proxy_pass 参数后面是否需要斜杠,避免将自定义的路径传递到后端节点,导致访问 404!

nginx 跳转方法

Nginx搭建了一个https访问的虚拟主机,监听的域名是itlnmp.com,但是很多用户不清楚https和http的区别,会很容易敲成http://itlnmp.com,这时会报出404错误,所以我需要做基于itlnmp.com域名的http向https的强制跳转

我总结了两种方式,跟大家共享一下

一、nginx的rewrite方法

思路这应该是大家最容易想到的方法,将所有的http请求通过rewrite重写到https上即可,配置如下

server { 
    listen  80;  
    server_name www.itlnmp.com; 
    rewrite ^(.*)$  http://$host$1 permanent;  
}
server {
     listen 443;
     server_name www.itlnmp.com;
     root /www;
     ssl on;
     ssl_certificate /etc/nginx/certs/server.crt;
     ssl_certificate_key /etc/nginx/certs/server.key;
 }

搭建此虚拟主机完成后,就可以将http://www.itlnmp.com的请求全部重写到http://www.itlnmp.com上了

二、index.html刷新网页

上述两种方法均会耗费服务器的资源,我们用curl访问baidu.com试一下,看百度的公司是如何实现baidu.com向www.baidu.com的跳转
可以看到百度很巧妙的利用meta的刷新作用,将baidu.com跳转到www.baidu.com.因此我们可以基于http://www.itlnmp.com的虚拟主机路径下也写一个index.html,内容就是http向https的跳转
1、index.html
<html>
<meta http-equiv="refresh" content="0;url=http://www.itlnmp.com/">
</html>

2、Nginx虚拟主机配置

server {
    listen 80;
    server_name www.itlnmp.com;
    location / {
                #index.html放在虚拟主机监听的根目录下
        root /www;
    }
        #将404的页面重定向到https的首页
    error_page  404 http://www.itlnmp.com/;
}

server {
     listen 443;
     server_name www.itlnmp.com;
     root /www;
     ssl on;
     ssl_certificate /etc/nginx/certs/server.crt;
     ssl_certificate_key /etc/nginx/certs/server.key;
 }

nginx之proxy_pass指令完全拆解

一、proxy_pass的nginx官方指南

nginx中有两个模块都有proxy_pass指令。

  • ngx_http_proxy_moduleproxy_pass
语法: proxy_pass URL;
场景: location, if in location, limit_except
说明: 设置后端代理服务器的协议(protocol)和地址(address),以及location中可以匹配的一个可选的URI。协议可以是"http""https"。地址可以是一个域名或ip地址和端口,或者一个 unix-domain socket 路径。  
详见官方文档: http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_pass
URI的匹配,本文第四部分重点讨论。
  • ngx_stream_proxy_moduleproxy_pass
语法: proxy_pass address;
场景: server
说明: 设置后端代理服务器的地址。这个地址(address)可以是一个域名或ip地址和端口,或者一个 unix-domain socket路径。  
详见官方文档: http://nginx.org/en/docs/stream/ngx_stream_proxy_module.html#proxy_pass

二、两个proxy_pass的关系和区别

在两个模块中,两个proxy_pass都是用来做后端代理的指令。
ngx_stream_proxy_module模块的proxy_pass指令只能在server段使用使用, 只需要提供域名或ip地址和端口。可以理解为端口转发,可以是tcp端口,也可以是udp端口。
ngx_http_proxy_module模块的proxy_pass指令需要在location段,location中的if段,limit_except段中使用,处理需要提供域名或ip地址和端口外,还需要提供协议,如”http”或”https”,还有一个可选的uri可以配置。

三、proxy_pass的具体用法

ngx_stream_proxy_module模块的proxy_pass指令

server {
    listen 127.0.0.1:12345;
    proxy_pass 127.0.0.1:8080;
}

server {
    listen 12345;
    proxy_connect_timeout 1s;
    proxy_timeout 1m;
    proxy_pass example.com:12345;
}

server {
    listen 53 udp;
    proxy_responses 1;
    proxy_timeout 20s;
    proxy_pass dns.example.com:53;
}

server {
    listen [::1]:12345;
    proxy_pass unix:/tmp/stream.socket;
}

ngx_http_proxy_module模块的proxy_pass指令

server {
    listen      80;
    server_name www.test.com;

    # 正常代理,不修改后端url的
    location /some/path/ {
        proxy_pass http://127.0.0.1;
    }

    # 修改后端url地址的代理(本例后端地址中,最后带了一个斜线)
    location /testb {
        proxy_pass http://www.other.com:8801/;
    }

    # 使用 if in location
    location /google {
        if ( $geoip_country_code ~ (RU|CN) ) {
            proxy_pass http://www.google.hk;
        }
    }

    location /yongfu/ {
        # 没有匹配 limit_except 的,代理到 unix:/tmp/backend.socket:/uri/
        proxy_pass http://unix:/tmp/backend.socket:/uri/;;

        # 匹配到请求方法为: PUT or DELETE, 代理到9080
        limit_except PUT DELETE {
            proxy_pass http://127.0.0.1:9080;
        }
    }

}

四、proxy_pass后,后端服务器的url(request_uri)情况分析

server {
    listen      80;
    server_name www.test.com;

    # 情形A
    # 访问 http://www.test.com/testa/aaaa
    # 后端的request_uri为: /testa/aaaa
    location ^~ /testa/ {
        proxy_pass http://127.0.0.1:8801;
    }
    
    # 情形B
    # 访问 http://www.test.com/testb/bbbb
    # 后端的request_uri为: /bbbb
    location ^~ /testb/ {
        proxy_pass http://127.0.0.1:8801/;
    }

    # 情形C
    # 下面这段location是正确的
    location ~ /testc {
        proxy_pass http://127.0.0.1:8801;
    }

    # 情形D
    # 下面这段location是错误的
    #
    # nginx -t 时,会报如下错误: 
    #
    # nginx: [emerg] "proxy_pass" cannot have URI part in location given by regular 
    # expression, or inside named location, or inside "if" statement, or inside 
    # "limit_except" block in /opt/app/nginx/conf/vhost/test.conf:17
    # 
    # 当location为正则表达式时,proxy_pass 不能包含URI部分。本例中包含了"/"
    location ~ /testd {
        proxy_pass http://127.0.0.1:8801/;   # 记住,location为正则表达式时,不能这样写!!!
    }

    # 情形E
    # 访问 http://www.test.com/ccc/bbbb
    # 后端的request_uri为: /aaa/ccc/bbbb
    location /ccc/ {
        proxy_pass http://127.0.0.1:8801/aaa$request_uri;
    }

    # 情形F
    # 访问 http://www.test.com/namea/ddd
    # 后端的request_uri为: /yongfu?namea=ddd
    location /namea/ {
        rewrite    /namea/([^/]+) /yongfu?namea=$1 break;
        proxy_pass http://127.0.0.1:8801;
    }

    # 情形G
    # 访问 http://www.test.com/nameb/eee
    # 后端的request_uri为: /yongfu?nameb=eee
    location /nameb/ {
        rewrite    /nameb/([^/]+) /yongfu?nameb=$1 break;
        proxy_pass http://127.0.0.1:8801/;
    }

    access_log /data/logs/www/www.test.com.log;
}

server {
    listen      8801;
    server_name www.test.com;
    
    root        /data/www/test;
    index       index.php index.html;

    rewrite ^(.*)$ /test.php?u=$1 last;

    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_pass unix:/tmp/php-cgi.sock;
        fastcgi_index index.php;
        include fastcgi.conf;
    }

    access_log /data/logs/www/www.test.com.8801.log;
}

文件: /data/www/test/test.php

<?php
echo '$_SERVER[REQUEST_URI]:' . $_SERVER['REQUEST_URI'];

通过查看 $_SERVER[‘REQUEST_URI’] 的值,我们可以看到每次请求的后端的request_uri的值,进行验证。

小结

情形A和情形B进行对比,可以知道proxy_pass后带一个URI,可以是斜杠(/)也可以是其他uri,对后端request_uri变量的影响。
情形D说明,当location为正则表达式时,proxy_pass不能包含URI部分。
情形E通过变量($request_uri, 也可以是其他变量),对后端的request_uri进行改写。
情形F和情形G通过rewrite配合break标志,对url进行改写,并改写后端的request_uri。需要注意,proxy_pass地址的URI部分在情形G中无效,不管如何设置,都会被忽略。

UDP反向代理nginx

许多人眼中的 udp 协议是没有反向代理、负载均衡这个概念的。毕竟,udp 只是在 IP 包上加了个仅仅 8 个字节的包头,这区区 8 个字节又如何能把 session 会话这个特性描述出来呢?

图 1 UDP 报文的协议分层

在 TCP/IP 或者 OSI 网络七层模型中,每层的任务都是如此明确:

  1. 物理层专注于提供物理的、机械的、电子的数据传输,但这是有可能出现差错的;
  2. 数据链路层在物理层的基础上通过差错的检测、控制来提升传输质量,并可在局域网内使数据报文跨主机可达。这些功能是通过在报文的前后添加 Frame 头尾部实现的,如上图所示。每个局域网由于技术特性,都会设置报文的最大长度 MTU(Maximum Transmission Unit),用 netstat -i(linux) 命令可以查看 MTU 的大小:
  3. 而 IP 网络层的目标是确保报文可以跨广域网到达目的主机。由于广域网由许多不同的局域网,而每个局域网的 MTU 不同,当网络设备的 IP 层发现待发送的数据字节数超过 MTU 时,将会把数据拆成多个小于 MTU 的数据块各自组成新的 IP 报文发送出去,而接收主机则根据 IP 报头中的 Flags 和 Fragment Offset 这两个字段将接收到的无序的多个 IP 报文,组合成一段有序的初始发送数据。IP 报头的格式如下图所示:
    图 2 IP 报文头部

  4. 传输层主要包括 TCP 协议和 UDP 协议。这一层最主要的任务是保证端口可达,因为端口可以归属到某个进程,当 chrome 的 GET 请求根据 IP 层的 destination IP 到达 linux 主机时,linux 操作系统根据传输层头部的 destination port 找到了正在 listen 或者 recvfrom 的 nginx 进程。所以传输层无论什么协议其头部都必须有源端口和目的端口。例如下图的 UDP 头部:
    图 3 UDP 的头部

TCP 的报文头比 UDP 复杂许多,因为 TCP 除了实现端口可达外,它还提供了可靠的数据链路,包括流控、有序重组、多路复用等高级功能。由于上文提到的 IP 层报文拆分与重组是在 IP 层实现的,而 IP 层是不可靠的所有数组效率低下,所以 TCP 层还定义了 MSS(Maximum Segment Size)最大报文长度,这个 MSS 肯定小于链路中所有网络的 MTU,因此 TCP 优先在自己这一层拆成小报文避免的 IP 层的分包。而 UDP 协议报文头部太简单了,无法提供这样的功能,所以基于 UDP 协议开发的程序需要开发人员自行把握不要把过大的数据一次发送。

对报文有所了解后,我们再来看看 UDP 协议的应用场景。相比 TCP 而言 UDP 报文头不过 8 个字节,所以 UDP 协议的最大好处是传输成本低(包括协议栈的处理),也没有 TCP 的拥塞、滑动窗口等导致数据延迟发送、接收的机制。但 UDP 报文不能保证一定送达到目的主机的目的端口,它没有重传机制。所以,应用 UDP 协议的程序一定是可以容忍报文丢失、不接受报文重传的。如果某个程序在 UDP 之上包装的应用层协议支持了重传、乱序重组、多路复用等特性,那么他肯定是选错传输层协议了,这些功能 TCP 都有,而且 TCP 还有更多的功能以保证网络通讯质量。因此,通常实时声音、视频的传输使用 UDP 协议是非常合适的,我可以容忍正在看的视频少了几帧图像,但不能容忍突然几分钟前的几帧图像突然插进来:-)

有了上面的知识储备,我们可以来搞清楚 UDP 是如何维持会话连接的。对话就是会话,A 可以对 B 说话,而 B 可以针对这句话的内容再回一句,这句可以到达 A。如果能够维持这种机制自然就有会话了。UDP 可以吗?当然可以。例如客户端(请求发起者)首先监听一个端口 Lc,就像他的耳朵,而服务提供者也在主机上监听一个端口 Ls,用于接收客户端的请求。客户端任选一个源端口向服务器的 Ls 端口发送 UDP 报文,而服务提供者则通过任选一个源端口向客户端的端口 Lc 发送响应端口,这样会话是可以建立起来的。但是这种机制有哪些问题呢?

问题一定要结合场景来看。比如:1、如果客户端是 windows 上的 chrome 浏览器,怎么能让它监听一个端口呢?端口是会冲突的,如果有其他进程占了这个端口,还能不工作了?2、如果开了多个 chrome 窗口,那个第 1 个窗口发的请求对应的响应被第 2 个窗口收到怎么办?3、如果刚发完一个请求,进程挂了,新启的窗口收到老的响应怎么办?等等。可见这套方案并不适合消费者用户的服务与服务器通讯,所以视频会议等看来是不行。

有其他办法么?有!如果客户端使用的源端口,同样用于接收服务器发送的响应,那么以上的问题就不存在了。像 TCP 协议就是如此,其 connect 方的随机源端口将一直用于连接上的数据传送,直到连接关闭。这个方案对客户端有以下要求:不要使用 sendto 这样的方法,几乎任何语言对 UDP 协议都提供有这样的方法封装。应当先用 connect 方法获取到 socket,再调用 send 方法把请求发出去。这样做的原因是既可以在内核中保存有 5 元组(源 ip、源 port、目的 ip、目的端口、UDP 协议),以使得该源端口仅接收目的 ip 和端口发来的 UDP 报文,又可以反复使用 send 方法时比 sendto 每次都上传递目的 ip 和目的 port 两个参数。

对服务器端有以下要求:不要使用 recvfrom 这样的方法,因为该方法无法获取到客户端的发送源 ip 和源 port,这样就无法向客户端发送响应了。应当使用 recvmsg 方法(有些编程语言例如 python2 就没有该方法,但 python3 有)去接收请求,把获取到的对端 ip 和 port 保存下来,而发送响应时可以仍然使用 sendto 方法。

接下来我们谈谈 nginx 如何做 udp 协议的反向代理。Nginx 的 stream 系列模块核心就是在传输层上做反向代理,虽然 TCP 协议的应用场景更多,但 UDP 协议在 Nginx 的角度看来也与 TCP 协议大同小异,比如:nginx 向 upstream 转发请求时仍然是通过 connect 方法得到的 fd 句柄,接收 upstream 的响应时也是通过 fd 调用 recv 方法获取消息;nginx 接收客户端的消息时则是通过上文提到过的 recvmsg 方法,同时把获取到的客户端源 ip 和源 port 保存下来。我们先看下 recvmsg 方法的定义:

ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

相对于 recvfrom 方法,多了一个 msghdr 结构体,如下所示:

struct msghdr {
    void         *msg_name;       /* optional address */
    socklen_t     msg_namelen;    /* size of address */
    struct iovec *msg_iov;        /* scatter/gather array */
    size_t        msg_iovlen;     /* # elements in msg_iov */
    void         *msg_control;    /* ancillary data, see below */
    size_t        msg_controllen; /* ancillary data buffer len */
    int           msg_flags;      /* flags on received message */
};

其中 msg_name 就是对端的源 IP 和源端口(指向 sockaddr 结构体)。以上是 C 库的定义,其他高级语言类似方法会更简单,例如 python 里的同名方法是这么定义的:

(data, ancdata, msg_flags, address) = socket.recvmsg(bufsize[, ancbufsize[, flags]])

其中返回元组的第 4 个元素就是对端的 ip 和 port。

以上是 nginx 在 udp 反向代理上的工作原理。实际配置则很简单:

# Load balance UDP-based DNS traffic across two servers
stream {
    upstream dns_upstreams {
        server 192.168.136.130:53;
        server 192.168.136.131:53;
    }

    server {
        listen 53 udp;
        proxy_pass dns_upstreams;
        proxy_timeout 1s;
        proxy_responses 1;
        error_log logs/dns.log;
    }
}

在 listen 配置中的 udp 选项告诉 nginx 这是 udp 反向代理。而 proxy_timeout 和 proxy_responses 则是维持住 udp 会话机制的主要参数。

UDP 协议自身并没有会话保持机制,nginx 于是定义了一个非常简单的维持机制:客户端每发出一个 UDP 报文,通常期待接收回一个报文响应,当然也有可能不响应或者需要多个报文响应一个请求,此时 proxy_responses 可配为其他值。而 proxy_timeout 则规定了在最长的等待时间内没有响应则断开会话。

最后我们来谈一谈经过 nginx 反向代理后,upstream 服务如何才能获取到客户端的地址?如下图所示,nginx 不同于 IP 转发,它事实上建立了新的连接,所以正常情况下 upstream 无法获取到客户端的地址:

图 4 nginx 反向代理掩盖了客户端的 IP

上图虽然是以 TCP/HTTP 举例,但对 UDP 而言也一样。而且,在 HTTP 协议中还可以通过 X-Forwarded-For 头部传递客户端 IP,而 TCP 与 UDP 则不行。Proxy protocol 本是一个好的解决方案,它通过在传输层 header 之上添加一层描述对端的 ip 和 port 来解决问题,例如:

但是,它要求 upstream 上的服务要支持解析 proxy protocol,而这个协议还是有些小众。最关键的是,目前 nginx 对 proxy protocol 的支持则仅止于 tcp 协议,并不支持 udp 协议,我们可以看下其代码:

可见 nginx 目前并不支持 udp 协议的 proxy protocol(笔者下的 nginx 版本为 1.13.6)。

虽然 proxy protocol 是支持 udp 协议的。怎么办呢?可以用 IP 地址透传的解决方案。如下图所示:

图 5 nginx 作为四层反向代理向 upstream 展示客户端 ip 时的 ip 透传方案

这里在 nginx 与 upstream 服务间做了一些 hack 的行为:

  1. nginx 向 upstream 发送包时,必须开启 root 权限以修改 ip 包的源地址为 client ip,以让 upstream 上的进程可以直接看到客户端的 IP。
    server {
     listen 53 udp;
    
     proxy_responses 1;
    proxy_timeout 1s;
    proxy_bind $remote_addr transparent;
    
     proxy_pass dns_upstreams;
    }
  2. upstream 上的路由表需要修改,因为 upstream 是在内网,它的网关是内网网关,并不知道把目的 ip 是 client ip 的包向哪里发。而且,它的源地址端口是 upstream 的,client 也不会认的。所以,需要修改默认网关为 nginx 所在的机器。

# route del default gw 原网关 ip

# route add default gw nginx 的 ip

3. nginx 的机器上必须修改 iptable 以使得 nginx 进程处理目的 ip 是 client 的        报文。

# ip rule add fwmark 1 lookup 100

# ip route add local 0.0.0.0/0 dev lo table 100

# iptables -t mangle -A PREROUTING -p tcp -s 172.16.0.0/28 --sport 80 -j MARK --set-xmark 0x1/0xffffffff

这套方案其实对 TCP 也是适用的。除了上述方案外,还有个 Direct Server Return 方案,即 upstream 回包时 nginx 进程不再介入处理。这种 DSR 方案又分为两种,第 1 种假定 upstream 的机器上没有公网网卡,其解决方案图示如下:

图 6 nginx 做 udp 反向代理时的 DSR 方案(upstream 无公网)

这套方案做了以下 hack 行为:

  1. 在 nginx 上同时绑定 client 的源 ip 和端口,因为 upstream 回包后将不再经过 nginx 进程了。同时,proxy_responses 也需要设为 0。
server {
    listen 53 udp;

proxy_responses 0;
    proxy_bind $remote_addr:$remote_port transparent;

    proxy_pass dns_upstreams;
}

2. 与第一种方案相同,修改 upstream 的默认网关为 nginx 所在机器(任何  一台拥有公网的机器都行)。

# tc qdisc add dev eth0 root handle 10: htb

# tc filter add dev eth0 parent 10: protocol ip prio 10 u32 match ip src 172.16.0.11 match ip sport 53 action nat egress 172.16.0.11 192.168.99.10

# tc filter add dev eth0 parent 10: protocol ip prio 10 u32 match ip src 172.16.0.12 match ip sport 53 action nat egress 172.16.0.12 192.168.99.10

# tc filter add dev eth0 parent 10: protocol ip prio 10 u32 match ip src 172.16.0.13 match ip sport 53 action nat egress 172.16.0.13 192.168.99.10

# tc filter add dev eth0 parent 10: protocol ip prio 10 u32 match ip src 172.16.0.14 match ip sport 53 action nat egress 172.16.0.14 192.168.99.10

DSR 的另一套方案是假定 upstream 上有公网线路,这样 upstream 的回包可以直接向 client 发送,如下图所示:

图 6 nginx 做 udp 反向代理时的 DSR 方案(upstream 有公网)

这套 DSR 方案与上一套 DSR 方案的区别在于:由 upstream 服务所在主机上修改发送报文的源地址与源端口为 nginx 的 ip 和监听端口,以使得 client 可以接收到报文。例如:

# tc qdisc add dev eth0 root handle 10: htb

# tc filter add dev eth0 parent 10: protocol ip prio 10 u32 match ip src 172.16.0.11 match ip sport 53 action nat egress 172.16.0.11 192.168.99.10

以上三套方案都需要 nginx 的 worker 跑在 root 权限下,这并不友好。从协议层面,可以期待后续版本支持 proxy protocol 传递 client 的 ip。

Nginx 四层代理功能主要部分文档

在 Nginx 中,四层的数据被称为 stream,和四层代理有关的模块主要有:

ngx_stream_core_module:四层代理的基本功能模块
ngx_stream_upstream_module:四层代理转发到上游的模块
ngx_stream_proxy_module:四层代理相关配置
其他 stream 相关模块用于如 SSL 支持、geoip 支持、简单访问控制支持等,本次测试并没有使用到。

使用的注意事项

Nginx 的四层反代功能较为简单,其访问控制模块因为源站 IP 可以进行伪造,基本不可用于 UDP Flood 的防护。

使用健康检测功能的前提是他们在一个共享内存的 zone 里,注意各个区块的层次关系即可,zone 是配置上游服务器组共享内存的功能,因此 zone 要放在 upstream 区块。status 命令即监控dashboard 是 ngx_http_status_module 的内容,严格来说不是四层代理的部分。

我使用的 UDP 反代配置:

stream {

upstream dns_server {
zone stream_dns 10m;
server 127.0.0.1:53;
}

match dns {
send \x00\x2a\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x06\x73\x65\x72\x76\x65\x72\x0a\x73\x74\x61\x72\x64\x75\x73\x74\x65\x72\x02\x6d\x65\x00\x00\x01\x00\x01;
expect ~* \x6a;
}

server {
listen 10086 udp;
proxy_pass dns_server;
error_log  /home/nginx/dns-error.log debug;
health_check udp match=dns interval=1s;
status_zone stream_dns;
}
}

健康检测页面配置:

server {
listen 8080;

root   /usr/share/nginx/html;

location /status {
status;
}
location = /status.html {
}
}
ngx_stream_core_module

ngx_stream_core_module 模块从1.9.0版本开始提供,默认编译不包含此模块,需要在编译参数中加上–with-stream。

语法:listen address:port [ssl] [udp] [backlog=number] [bind] [ipv6only=on|off] [reuseport] [so_keepalive=on|off|[keepidle]:[keepintvl]:[keepcnt]];
环境:server

设置接受连接的端口和地址,可以只有端口号,也可以包含IP或主机名。

listen 127.0.0.1:12345;
listen *:12345;
listen 12345;     # same as *:12345
listen localhost:12345;
IPV6 地址需要使用中括号标注:

Shell

listen [::1]:12345;
listen [::]:12345;
1
2
listen [::1]:12345;
listen [::]:12345;
Unix socket 需要使用前缀“Unix:”

listen unix:/var/run/nginx.sock;

ssl 参数所有连接都需要使用 SSL 加密;

udp 参数指定监听 UDP 端口(从1.9.13版本开始支持);

其他相关参数:

backlog=number 参数设置 listen() 调用的最大等待连接队列数,默认情况下,backlog 在 BSD 上被设置为-1,在其他平台被设置为511;

bind 参数使用所给的 address:port 产生一个独立的 bind() 调用,用于使用多个 listen 命令监听不同地址上的相同端口号;

ipv6only=on|off 参数,配置 [::] 监听地址是否接受 IPV4 请求,此选项默认为 on;

reuseport 参数(从1.9.1开始支持)为每个 worker 进程独立地创建一个监听的 socket(使用 SO_REUSEPORT),允许内核将入站连接分发到不同 worker 进程,仅支持 Linux3.9+ 和 DragonFly BSD;

so_keepalive=on|off|[keepidle]:[keepintvl]:[keepcnt] 参数配置 TCP keepalive 特性;

不同的 Server 区块必须 listen 不同的地址+端口组合。

语法:resolver address … [valid=time] [ipv6=on|off];
环境:server

配置解析 upstream 域名所使用的 DNS 服务器:

resolver 127.0.0.1 [::1]:5353;

地址可以以 IP 或者域名的形式给出,也可以附带端口号。默认情况下,nginx 会解析 V4 和 V6 地址,并会在 TTL 时间内缓存解析结果。

语法:resolver_timeout time;
默认:30秒
环境:stream,server
(从1.11.3版本开始提供)

设置 DNS 解析超时时间。

语法:server {…}
环境:stream

设置一个独立的 Server。

语法:stream {…}
环境:main

提供四层代理的 Server 的配置环境,和 http {…} 类似。

语法:tcp_nodelay on | off;
默认:on
环境:stream,server
(从1.9.4版本开始提供)

开启或关闭 TCP_NODELAY 选项,这个选项将会同时应用于到客户端和到上游服务器的连接。

语法:variables_hash_bucket_size size;
默认:64
环境:stream
(从1.11.2版本开始提供)

设置变量 hash 表的桶大小,详细设置参见独立文档

语法:variables_hash_max_size size;
默认:1024
环境:stream
(从1.11.2版本开始提供)

设置 hash 表的最大大小,详细设置参见独立文档

内置变量,(ngx_stream_core_module 模块从1.11.2开始支持变量):

$binary_remote_addr:二进制形式的客户端地址
$bytes_sent:向客户端发送的字节数量
$connection:连接序号
$hostname:主机名
$msec:以毫秒为分辨率的当前时间
$nginx_version:nginx 的版本
$pid:worker 的 PID
$remote_addr:客户端地址
$remote_port:客户端端口
$server_addr:接受连接的服务端地址,获取此变量需要一次 system call,为避免产生 system call 在 listen 命令后指定监听地址并增加 bind 参数
$server_port:接受连接的服务端端口
$time_iso8601:ISO 8601 格式的本地时间
$time_local:通常日志格式中的本地时间
ngx_stream_proxy_module

ngx_stream_proxy_module允许被代理的数据流通过 TCP、UDP、Unix socket,从1.9.0版本开始提供。

语法:proxy_bind address [transparent] | off;
环境:stream, server
(从1.9.2版本开始提供)

指定向外的连接通过指定的 IP,参数的值可以包含变量(1.11.2)。参数 off 可以取消从上一级继承的 proxy_bind 命令的效果,让系统自动选择出口 IP。

transparent(透明代理)参数可以发往源站(proxied server)的连接的源 IP 为非本地的 IP,如:客户端的真实 IP。proxy_bind $remote_addr transparent; 为达到这个目的,nginx worker 进程需要以超级用户权限运行并配置内核路由表以拦截被源站发回的网络流量。

语法:proxy_buffer_size size;
默认:16k
环境:stream, server
(从1.9.14版本开始提供)

设置从源站读取数据时候缓冲区的大小,也将同时设置从客户端读取数据的缓冲区大小。

语法:proxy_connect_timeout time;
默认:60s;
环境:stream, server

定义和源站已经建立的连接超时时间。

语法:proxy_download_rate rate;
默认:0;
环境:stream, server

从源站接受数据的速率限制,rate 的单位是 byte,默认数值为0即没有限制。这个限制是针对每个连接的,因此在打开两个到源站的连接时,总速率将会是 rate 值的两倍。

语法:proxy_next_upstream on | off;
默认:on;
环境:stream, server

当往源站的连接无法建立时,是否尝试将客户端的连接传往下一个源站。

尝试连接下一个源站的次数和时间都可被限制。

语法:proxy_next_upstream_timeout time;
默认:0;
环境:stream, server

限制连接到下一个源站时最大的尝试时间,默认数值为0 ,即没有限制。

语法:proxy_next_upstream_tires number;
默认:0;
环境:stream, server

限制连接到下一个源站时最大的尝试次数,默认数值为0 ,即没有限制。

语法:proxy_pass address;
环境:server

设置源站地址,地址可以以域名或 IP 和端口的形式出现,如proxy_pass localhost:12345;
,也可以是 Unix socket 如proxy_pass unix:/tmp/stream.socket;。

当域名被解析为多个 IP 时,将使用 round-robin 方式进行轮询,也可以使用服务器组(ngx_stream_upstream_module 提供),如proxy_pass $upstream;

在使用服务器组的情况下,server name 将首先在服务器组中搜索,如无结果则使用 resolver 进行查询。

语法:proxy_protocol;
默认:off;
环境:stream, server
(从1.9.2版本开始提供)

启用PROXY proxy_protocol
连接到源站。

语法:proxy_response number;
环境:stream, server
(从1.9.3版本开始提供)

在 UDP 代理启用的情况下,设置从源站期望接收的数据报数量,默认情况下接收报文的数量没有限制,将持续接收响应直到 proxy_timeout 超时。

语法:proxu_ssl on | off;
默认:off;
环境:stream, server

对和源站之间的连接启用 SSL。

语法:proxy_ssl_certificate file;
环境:stream, server

指定一个 PEM 格式的证书用于进行和源站的 SSL 连接认证。

语法:proxy_ssl_certificate_key file;
环境:stream, server

指定一个 PEM 格式的私钥用于进行和源站的 SSL 连接认证。

语法:proxy_ssl_ciphers ciphers;
默认:DEAFAULT;
环境:stream, server

指定和源站进行握手所使用的加密套件,以 OpenSSL 库可读的格式书写,使用命令openssl ciphers可以查看完整的列表

语法:proxy_ssl_crl file;
环境:stream, server

指定吊销证书列表。

语法:proxy_ssl_name name;
默认:host from proxy_pass;
环境:stream, server

用于覆盖发往源站的 SNI 请求中的 server name,该 server name 可以包含变量(自1.11.3版本开始),默认情况下,proxy_pass 参数中的主机名将被使用。

语法:proxy_ssl_password_file file;
环境:stream, server

指定私钥的加密口令,每行一个,口令将在加载私钥文件的时候被依次尝试。

语法:proxy_ssl_session_reuse on | off;
Default:on;
Context:stream, server

指定和源站连接时,SSL session 是否被复用,如果出现“SSL3_GET_FINISHED”错误,请尝试关闭 SSL 会话复用。

语法:proxy_ssl_protocols [SSLv2] [SSLv3] [TLSv1] [TLSv1.1] [TLSv1.2];
默认:TLSv1 TLSv1.1 TLSv1.2;
环境:stream, server

和源站连接时使用指定的 SSL 协议版本。

语法:proxy_ssl_trusted_certificate file;
环境:stream, server

指定 PEM 格式的可信的 CA 列表用于验证源站的认证有效性。

语法:proxy_ssl_verify on | off;
默认:off;
环境:stream, server

是否启用和源站之间的认证。

语法:proxy_ssl_verify_depth number;
默认:1;
环境:stream, server

指定验证源站认证的证书链层数。

语法:proxy_timeout time;
默认:10m;
环境:stream, server

指定两次读和写操作的超时间隔,此设置将同时应用在与客户端的连接和与源站的连接。若在这个时间内没有数据传输,连接将被关闭。

语法:proxy_upload_rate rate;
默认:0;
环境:stream, server
(从1.9.3开始提供)

限制从客户端读取数据的速率,数字单位是 byte/s,默认值0表示没有限制。这个数值是针对每个连接的,因此打开两个连接时,数据传输速率将是这个值的两倍。

ngx_stream_upstream_module

ngx_stream_upstream_module模块出现于1.9.0版本,用于定义可以被proxy_pass所调用的源站服务器组和相关的配置,收费订阅版本还支持动态配置组和主动健康检测。

语法:upstream name { … }
环境:stream

定义一组服务器,服务器可以监听不同的端口,也可以监听 TCP 端口和 Unix Socket 的混合搭配:

upstream backend {
server backend1.example.com:12345 weight=5;
server 127.0.0.1:12345            max_fails=3 fail_timeout=30s;
server unix:/tmp/backend2;
server backend3.example.com:12345 resolve;

server backup1.example.com:12345  backup;
}
默认情况下,入站连接将被 round-robin 算法均分到每个后端,在上面这个配置了权重的示例中,平均每7个入站连接有5个将被转到 backend1,第二和第三个 backend 各接到一个连接;连接如果失败,将会尝试下一个 server,全部失败时连接将被关闭。

语法:server address [parameters];
环境:upstream

定义 server 的地址和参数,地址可以以 IP 或者域名的形式给出,此时端口号必须指明;也可以使用unix:前缀指明地址是个 Unix socket,如果域名被解析为多个 IP,则被认为是定义了多个服务器。

参数表:

weight=number 参数指定了加权 round-robin 算法的权重值。
max_fails=number 参数指定了最大失败重试次数。
fail_timeout=number 参数同时指定了一次到后端 server 的连接的超时时间,和一个 server 被认为不可用的时间。
backup 参数将该 server 标注为备用,在主要服务器全部不可用的时候连接将被传到 backup 服务器。
down 参数将手动将一台 server 标注为不可用。
max_conns=number 限制向源站连接的最大连接数,默认为0,即没有限制。
resolve 参数监视 server 域名对应的 IP 变化,而不需要在 upstream 中修改服务器配置也不需要重启 nginx,本功能要求服务器组在一个共享内存的 zone 中。使用这个参数必须在 stream 区块里有至少一个 resolver 命令:
Shell

stream {
resolver 10.0.0.1;

upstream u {
zone …;

server example.com:12345 resolve;
}
}

service=name 参数将解析 DNS SRV 记录,要使用这个命令,需要 resolve 参数且 hostname 后不能含有端口号。
slow_start=time 参数设定了一个被认为是不可用的 Server 恢复的时间,默认为0,即 slow_start 被关闭
注意,当一个服务器组里只有一个服务器的时候,max_fails fail_timeout slow_start 参数将会被忽略。

语法:zone name [size];
环境:upstream

一个 zone 使用 name 和 size 定义了一个共享的内存区域,workers 之间可以共享配置文件和实时状态,多个服务器组可以共享一个 zone,因此只需要声明一个 zone。

另外,作为收费订阅的一部分,动态配置组允许组内服务器在不完整重读配置文件的前提下更换或者修改组内成员,相关配置方式参见upstream_conf in ngx_http_stream_module。

语法:state file;
环境:upstream
(从1.9.7版本开始提供)

指定一个文件路径,保存动态配置组当前配置状态,当前版本中这个状态包括配置组中的服务器列表和各个服务器的参数。这个文件在每次配置文件解析时被载入、upstream 配置变动的时候被更新,注意不应直接对这个文件进行编辑。

语法:hash key [consistent];
环境:upstream

负载均衡所使用的客户端-服务端映射的哈希算法是基于 key 的,hash key 可以包含文字和变量,如hash $remote_addr。

语法:least_conn;
环境:upstream

使用“最少连接”负载均衡算法,nginx 将自动选择连接最少的后端,如果存在多个后端连接数相等,将使用加权的 round-robin 进行选择。

语法:least_time connect | first_byte | last_byte;
环境:upstream

使用“最快连接”负载均衡算法,nginx 将选择速度最快的后端,可以通过参数选择“最快”的定义是首包时间、传输完成时间还是连接建立时间。

语法:health_check [parameters];
环境:server

开启服务器组的周期性主动健康检测功能。

参数表:

interval=time 参数设置了两次健康检查的间隔,单位是秒,默认值为5。
fails=number 参数设置了健康检查连续失败达到指定次数后,将 Server 标记为不健康。
pass=number 参数设置了健康检查连续通过达到指定次数后,将 Server 标记为健康。
match=name 参数通过名称指定某个 match 区块为健康检测通过的条件,默认情况下,只会检测是否可以成功建立 TCP 连接。
port=number 参数定义了进行健康检测时连接的端口号,默认情况等于 server 区块的端口号。
udp 参数指定了进行 UDP 健康检测,此时必须指定 match 参数,并提供 send 和 expect 内容。
Shell

server {
proxy_pass backend;
health_check;
}

这个配置将会每5秒检测配置组中的每台服务器是否可以成功建立 TCP 连接,当连接建立失败,健康检测将不会通过并将服务器标记为不健康的。此时客户端将不会连接到不健康的服务器。

健康检测也可以指定发送的数据内容和期待接收的内容,这部分配置独立地在 match 命令中设置,并在 health_check 命令的 match 参数中进行引用。

服务器组必须在同一个共享内存区域里。

如果一个服务器组设置了多项健康检查,一项未能通过的检查就会将整个服务器组标记为不健康的。

语法:health_check_timeout timeout;
默认:health_check_timeout 5s;
环境:stream, server

对健康检测操作,以 health_check_timeout 的值覆盖 proxy_timeout 的值。

语法:match name { … }
环境:stream

以名称定义一组验证健康检测返回值的测试集。

参数表:

send string 参数定义发送给 server 的字符串。
expect string | ~ regex 参数定义了服务器返回的字符串,可以使用正则进行匹配,如“~*”进行大小写不敏感匹配,使用“~”进行大小写敏感匹配.
send 和 expect 参数都可以使用“\x”前缀表示16进制字符,如“\x80\x1a”。
当满足下面条件时,健康检测通过:
TCP 连接建立
send 里的字符串成功发送
服务器响应符合 expect
未超出 health_check_timeout 指定的值
样例:

Shell

upstream backend {
zone     upstream_backend 10m;
server   127.0.0.1:12345;
}

match http {
send     “GET / HTTP/1.0\r\nHost: localhost\r\n\r\n”;
expect ~ “200 OK”;
}

server {
listen       12346;
proxy_pass   backend;
health_check match=http;
}

 

以上是云栖社区小编为您精心准备的的内容,在云栖社区的博客、问答、公众号、人物、课程等栏目也有的相关内容,欢迎继续使用右上角搜索按钮进行搜索服务器 , 变量 , 参数 , 配置 , 端口 时间 nginx反向代理功能、nginx 反向代理、nginx 代理、nginx反向代理配置、nginx 代理转发,以便于您获取更多的相关知识。

经过nginx做数据转发源地址发生改变

我在做某项目的时候,需将18网段的各种网络设备syslog日志经过一个18网段的中转机发送给17网段的三台日志服务器(不要问我为什么需要转发而不直接从设备上发过去,因为跨网段啊大哥,只能给你一台中转机让你转发数据),那台中转机我是利用nginx负载均衡的功能实现数据转发的,但是到达日志服务器的时候数据来源地址均变成了转发机的地址了,直接在nginx配置文件中加载一行配置就行了,做个透明代理,具体配置如下:

 

  1. upstream syslog_1514 {
  2. server 17.1.1.1:1514 weight=1;
  3. server 17.1.1.2:1514 weight=1;
  4. server 17.1.1.3:1514 weight=1;
  5. }
  6. server {
  7. listen 1514 udp;
  8. proxy_pass syslog_1514;
  9. proxy_bind $remote_addr transparent;
  10. proxy_responses 0;
  11. error_log /usr/log/nginx/logs/syslog_1514_err.log;
  12. }

 

我这边是UDP的数据源地址改变,TCP的数据不变。

第一篇小文章到此结束,over。