前言

为了叙述方便,本文假设我们控制了一台主机,其80端口之上运行着apache服务,而我们需要复用80端口

利用IPtables

首先第一种方法我是参考了网上的一篇文章,主要是利用IPtables规则来实现端口复用。

实际上这个方法属于伪端口复用,因为实际上我们的程序并不会复用同样的端口,而是利用IPtables会在路由决策之前生效的特性,将我们的程序监听在本地的另一个端口上(比方说8888),将外部流量全部导向我们自己的程序,再由我们自己的程序进行流量的分发(通过识别流量特征区分工具流量和正常http流量)

那么这个方法实际上确实效果还不错,不会影响到正常服务器的运行,而且在严格受限的环境当中(即只允许80端口的出入站连接),确实可以完成既定目标

但是,这个方法的局限性也比较明显,那就是需要完成这个操作需要有root权限(废话。。。),并且仅支持linux服务器,而且会监听额外的端口。root权限虽然说不是每次都搞不到,但是也算是一个受限的点,毕竟我们无法保证能否在一些受限严格的边缘服务器上搞到root权限。

总的来说,在有root权限的情况下,是个不错的方式。

利用SO_REUSEADDR和SO_REUSEPORT

其实这个方法也是由来已久,也是大家在设置socket的时候常常忽略的黑魔法。

Windows

在windows上复用端口时,我们主要是利用了SO_REUSEADDR这个选项,这个选项一方面将允许两个进程监听相同端口,只需要ip地址不完全相同即可,打个比方,绝大多数web服务器默认监听在0.0.0.0上,所以我们的程序可以监听在192.168.x.x的ip上,并且监听同一个端口(因为通配符0.0.0.0和具体的ip地址192.168.x.x不是“同一个”ip地址),这样就可以劫持端口流量(因为在路由决策时,是最大匹配原则,当用户在外网访问服务器上的web页面时,会输入具体的ip地址192.168.x.x,这样的话在路由决策时,所有应当流向web服务器的流量将会被我们劫持,而我们只需要分发流量就可以成功完成复用)

另一方面,在windows上SO_REUSEADD也同样具有SO_REUSEPORT(下面会讲到)的作用,也就是说只要在windows上设置了SO_REUSEADDR,同样也支持完全相同的ip:port对的监听。

在windows上还有一个特性,也就是如果已经监听的进程是由管理员启动的,并且监听在了0.0.0.0:80上,那么此时设置了SO_REUSEADDR选项的,同样是由管理员启动的第二个进程,将可以实现127.0.0.1:80,0.0.0.0:80,192.168.x.x:80的复用,但如果第二个进程(设置了SO_REUSEADDR)是由普通用户设置的,那么将无法监听0.0.0.0:80,只能监听192.168.x.x:80 以及127.0.0.1:80.而如果反过来,第一个进程由普通用户启动,那么,不管第二个进程(设置了SO_REUSEADDR)是由管理员或者是普通用户启动,都可以实现0.0.0.0:80,127.0.0.1:80,192.168.x.x:80的监听

值得一提的是,在windows上启动一个新的设置了SO_REUSEADDR选项的进程时是不会校验之前已经监听在相同端口上的程序是否同样启用了该选项的,只要新进程设置即可,但这也导致端口劫持的风险,所以微软在windows中又加入了一个特殊的选项SO_EXCLUCIVEADDRUSE,只要之前监听的程序设置此选项,那么之后再有进程想要监听同样的端口,将会被直接拒绝,防止端口劫持的发生

Linux

在linux上,包含了SO_REUSEADDR以及SO_REUSEPORT两个选项。

SO_REUSEADDR本身这个选项将允许两个进程监听相同端口,只需要ip地址不完全相同即可;但是在linux上有着特殊情况,此特殊情况导致了linux对于SO_REUSEADDR这个选项的实现和BSD及mac有很大区别,比如在linux上一个程序如果监听了通配符地址,那么就算后面的进程设置了SO_REUSEADDR,它也不能监听192.168.x.x或者127.0.0.1(在相同端口的情况下),反过来也一样(监听了127.0.0.1:80,就不能监听0.0.0.0:80,即通配符地址与所有地址互斥),而这在BSD及mac中这种情况是被允许的。

讲一下SO_REUSEPORT,这个选项在在linux是可用的,在windows上并没有这个设置,它的出现是旨在进一步实现负
载均衡,可以让两个不同的进程同时监听在完全一致的ip:port组合上,从而实现端口复用

一旦监听端口的前一个进程启用了SO_REUSEPORT,那么只要第二个启动的进程也启用SO_REUSEPORT选项,就可以实现完全相同的ip:port复用,此时发送到此端口的syn请求将被内核尝试平均分配,即两个进程将都会有机会获得外部的请求。

但是,linux上的许多web程序或者其他功能程序默认情况下不会设置SO_REUSEPORT选项,故而很难用SO_REUSEADDR和SO_REUSEPORT来复用linux端口,需要另辟蹊径。

PS:如果在启用了SO_REUSEPORT的情况下同时启用了SO_REUSEADDR(不确定SO_REUSEADDR是否需要启用,但是我们一般如果需要端口复用,都会将两者同时启用,在这边呢根据SO_REUSEADDR的功能描述,我更倾向与linux上将原本SO_REUSEADDR应该有的功能(使得通配符与特定地址监听不冲突)“锁住”了,只有当两个选项同时启用的时候,SO_REUSEADDR的原本功能才会“解锁”,重新恢复其原来应该具有的功能。所以我偏向于SO_REUSEADDR需要同时启用),那么SO_REUSEADDR将恢复原来应该有的功能,即哪怕前一个进程(同时启用了SO_REUSEADDR和SO_REUSEPORT)监听在0.0.0.0:80上,第二个进程(同样启用两个选项)也可以监听在192.168.x.x:80上,通配符地址不再与特定地址冲突。

此外,在linux上还有一个限制,就是当前一个进程启用了SO_REUSEPORT时,第二个启动的进程必须与第一个启动的进程具有相同的用户id,不然哪怕第二个进程也启用了SO_REUSEPORT选项,同样无法复用端口。

BSD及MAC

由于MAC及BSD在此功能上的实现是一样的,故只拿mac举例了

在mac上,与linux一样,包含了SO_REUSEADDR以及SO_REUSEPORT两个选项

与linux的不同之处就在于,在单独启用SO_REUSEADDR选项时,通配符地址不与特定地址冲突,也就是说与linux上监听了0.0.0.0就不能监听192.168.x.x不同,只要两个进程都启用SO_REUSEADDR,这种监听就是可以成立的。

而如果两个进程同时启用了SO_REUSEADDR以及SO_REUSEPORT,那么除了可以实现SO_REUSEADDR自身功能(允许两个进程监听相同端口,只需要ip地址不完全相同)以外,完全监听相同的ip:port选项也是被允许的

数据包流向

刚才说了那么多情况,有一个问题还没有解决,那就是如果两个功能完全不同的进程监听在完全相同的ip:port上,那么流量应当给谁呢?

实际上这也要分情况讨论:

  • 在windows上,如果两个进程同时监听在完全相同的ip:port上,那么遵守先来先得的规则,外部请求只会被第一个监听在此ip:port的socket所接收,第二个socket将完全无法收到外部的请求

  • 在linux上,同样情况下,由于linux内核对此的实现是尝试将外部请求平均分配,实现负载均衡,所以两个进程均有近似相同的几率可以获得来自外部的请求

  • 在BSD/MAC上,需要分两种情况,当监听的ip地址为通配符地址时,与windows相同,遵守先来先得的规则,外部请求只会被第一个监听在此ip:port的socket所接收,第二个socket将无法接收外部的请求,而当监听的地址为特定地址时,遵守后来先得的规则,外部请求只会被第二个监听在此ip:port的socket所接收,第一个socket将无法接收外部的请求

结语

感觉在应用层上做端口复用效果不是很理想,要在内核层面上做才会有更好的效果