图1 UDP报文的协议分层
在TCP/IP或者 OSI网络七层模型中,每层的任务都是如此明确:
图2 IP报文头部

IP协议头(本文只谈IPv4)里最关键的是Source IP Address发送方的源地址、Destination IP Address目标方的目的地址。这两个地址担保一个报文可以由一台windows主机到达一台linux主机,但并不能决定一个chrome浏览的GET要求可以到达linux上的nginx。
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协议的会话保持机制有了上面的知识储备,我们可以来搞清楚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方法。
须要C/C++ Linux做事器架构师学习资料后台私信“资料”(资料包括C/C++,Linux,golang技能,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享
接下来我们谈谈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反向代理做事以上是nginx在udp反向代理上的事情事理。实际配置则很大略:
# Load balance UDP-based DNS traffic across two serversstream { 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向后端做事通报客户真实IP末了我们来谈一谈经由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协议的。怎么办呢?
方案1:IP地址透传可以用IP地址透传的办理方案。如下图所示:
图5 nginx作为四层反向代理向upstream展示客户端ip时的ip透传方案
这里在nginx与upstream做事间做了一些hack的行为:
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;}
upstream上的路由表须要修正,由于upstream是在内网,它的网关是内网网关,并不知道把目的ip是client ip的包向哪里发。而且,它的源地址端口是upstream的,client也不会认的。以是,须要修正默认网关为nginx所在的机器。
# route del default gw 原网关ip# route add default gw nginx的ip
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也是适用的。
方案2:DSR(上游做事无公网)除了上述方案外,还有个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所在机器(任何一台拥有公网的机器都行)。
3、在nginx的主机上修正iptables,使得nginx可以转发upstream发回的相应,同时把源ip和端口由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
方案3:DSR(上游做事有公网)
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向后端做事通报客户端真实IP地址,但都须要nginx的worker进程跑在root权限下,这对运维并不友好。从协议层面,可以期待后续版本支持proxy protocol通报客户端ip以办理此问题。在当下的诸多运用处景下,除非业务场景明确无误的谢绝超时重传机制,否则还是应该利用TCP协议,其完善的流量、拥塞掌握都是我们必须拥有的能力,如果在UDP层上重新实现这套机制就得不偿失落了。